New toolchain installer. Serious number of bugs fixed. New Program Manager and File Manager. TCP/IP and printing added. SQLite upgraded. Safe video mode startup added. File associations. Terminal program. First run welcome. New docs. Whew.
This commit is contained in:
parent
2c170d263f
commit
28092f2ef2
1387 changed files with 472437 additions and 247833 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -11,3 +11,5 @@ lib/
|
||||||
capture/
|
capture/
|
||||||
just-stuff/
|
just-stuff/
|
||||||
dvx.log
|
dvx.log
|
||||||
|
claudeDocs/
|
||||||
|
toolchains/
|
||||||
|
|
|
||||||
207
Makefile
207
Makefile
|
|
@ -32,47 +32,40 @@
|
||||||
# src/loader/ -> bin/DVX.EXE
|
# src/loader/ -> bin/DVX.EXE
|
||||||
# src/tools/ -> bin/host/<tool>
|
# src/tools/ -> bin/host/<tool>
|
||||||
|
|
||||||
.PHONY: all clean libdvx libtasks loader texthelp listhelp widgets dvxshell taskmgr serial sql apps tools deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-samples coverage
|
# Libraries are discovered, never listed: every src/libs/kpunch/<name>/
|
||||||
|
# with a Makefile is one. Run-time load order comes from each library's
|
||||||
|
# .dep file; the build only needs the core libraries first (and the shell
|
||||||
|
# before the task manager, which links against it).
|
||||||
|
LIBS = $(sort $(notdir $(patsubst %/Makefile,%,$(wildcard src/libs/kpunch/*/Makefile))))
|
||||||
|
CORE_LIBS = libdvx libtasks
|
||||||
|
|
||||||
all: libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps deploy-helpsrc compile-help deploy-sdk
|
.PHONY: all clean $(LIBS) loader widgets apps tools deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-samples coverage
|
||||||
|
|
||||||
libdvx:
|
all: $(LIBS) loader tools widgets apps deploy-helpsrc compile-help deploy-sdk
|
||||||
$(MAKE) -C src/libs/kpunch/libdvx
|
|
||||||
|
|
||||||
libtasks:
|
$(LIBS):
|
||||||
$(MAKE) -C src/libs/kpunch/libtasks
|
$(MAKE) -C src/libs/kpunch/$@
|
||||||
|
|
||||||
|
$(filter-out $(CORE_LIBS),$(LIBS)): $(CORE_LIBS)
|
||||||
|
|
||||||
|
taskmgr: dvxshell
|
||||||
|
|
||||||
loader: libdvx libtasks
|
loader: libdvx libtasks
|
||||||
$(MAKE) -C src/loader
|
$(MAKE) -C src/loader
|
||||||
|
|
||||||
texthelp: libdvx libtasks
|
# Every .wgt recipe runs bin/host/dvxres, so widgets need the tools.
|
||||||
$(MAKE) -C src/libs/kpunch/texthelp
|
widgets: libdvx libtasks texthelp listhelp tools
|
||||||
|
|
||||||
listhelp: libdvx libtasks
|
|
||||||
$(MAKE) -C src/libs/kpunch/listhelp
|
|
||||||
|
|
||||||
widgets: libdvx libtasks texthelp listhelp
|
|
||||||
$(MAKE) -C src/widgets/kpunch
|
$(MAKE) -C src/widgets/kpunch
|
||||||
|
|
||||||
dvxshell: libdvx libtasks
|
|
||||||
$(MAKE) -C src/libs/kpunch/dvxshell
|
|
||||||
|
|
||||||
taskmgr: dvxshell
|
|
||||||
$(MAKE) -C src/libs/kpunch/taskmgr
|
|
||||||
|
|
||||||
serial: libdvx libtasks
|
|
||||||
$(MAKE) -C src/libs/kpunch/serial
|
|
||||||
|
|
||||||
sql: libdvx libtasks
|
|
||||||
$(MAKE) -C src/libs/kpunch/sql
|
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
$(MAKE) -C src/tools
|
$(MAKE) -C src/tools
|
||||||
|
|
||||||
apps: libdvx libtasks dvxshell tools
|
apps: libdvx libtasks dvxshell tools
|
||||||
$(MAKE) -C src/apps/kpunch
|
$(MAKE) -C src/apps/kpunch
|
||||||
|
|
||||||
deploy-helpsrc:
|
# Copies help sources into the module directories under bin/, so every
|
||||||
|
# module that owns one must be built first.
|
||||||
|
deploy-helpsrc: $(LIBS) widgets apps tools
|
||||||
$(MAKE) -C src/tools deploy-helpsrc
|
$(MAKE) -C src/tools deploy-helpsrc
|
||||||
|
|
||||||
# Host-side tests. Every suite is native gcc; the GUI suites build
|
# Host-side tests. Every suite is native gcc; the GUI suites build
|
||||||
|
|
@ -84,7 +77,9 @@ deploy-helpsrc:
|
||||||
# make test-asan -- BASIC harnesses rebuilt under ASan/UBSan plus the
|
# make test-asan -- BASIC harnesses rebuilt under ASan/UBSan plus the
|
||||||
# host suites (which are already sanitized)
|
# host suites (which are already sanitized)
|
||||||
DVXBASIC_DIR = src/apps/kpunch/dvxbasic
|
DVXBASIC_DIR = src/apps/kpunch/dvxbasic
|
||||||
HOST_SUITES = src/test/dvxtest src/test/sql src/test/unit src/test/wm src/test/widgets src/test/dialogs src/test/formrt src/test/ide src/test/shell src/test/serial
|
|
||||||
|
# HOST_SUITES: the one list of host suites (shared with runCoverage.sh).
|
||||||
|
include src/test/suites.mk
|
||||||
|
|
||||||
test: apps
|
test: apps
|
||||||
$(MAKE) -C $(DVXBASIC_DIR) tests
|
$(MAKE) -C $(DVXBASIC_DIR) tests
|
||||||
|
|
@ -104,12 +99,19 @@ test-asan: apps
|
||||||
$(MAKE) -C $(DVXBASIC_DIR) tests TESTSAN=1
|
$(MAKE) -C $(DVXBASIC_DIR) tests TESTSAN=1
|
||||||
$(MAKE) test-host
|
$(MAKE) test-host
|
||||||
|
|
||||||
|
# Runs every suite even after a failure, then fails naming the failed ones.
|
||||||
test-host:
|
test-host:
|
||||||
@for d in $(HOST_SUITES); do $(MAKE) -C $$d || exit 1; done
|
@failed=""; for d in $(HOST_SUITES); do $(MAKE) -C src/test/$$d || failed="$$failed $$d"; done; \
|
||||||
|
if [ -n "$$failed" ]; then echo "test-host: FAILED suites:$$failed"; exit 1; fi
|
||||||
|
|
||||||
test-samples:
|
test-samples:
|
||||||
src/test/samples/runSamples.sh
|
src/test/samples/runSamples.sh
|
||||||
|
|
||||||
|
# The real DOS build in an emulator (src/test/dos/runDos.sh; not part of
|
||||||
|
# make test). EMULATOR=86box (default) or dosbox; SCRIPTS= a subset.
|
||||||
|
test-dos: all
|
||||||
|
src/test/dos/runDos.sh $(or $(EMULATOR),86box) --no-build $(SCRIPTS)
|
||||||
|
|
||||||
# Line coverage of the host suites: rebuilds everything with gcc
|
# Line coverage of the host suites: rebuilds everything with gcc
|
||||||
# --coverage (no sanitizers) under obj/hostcov, runs the BASIC
|
# --coverage (no sanitizers) under obj/hostcov, runs the BASIC
|
||||||
# harnesses, sample goldens and every host suite, then writes the
|
# harnesses, sample goldens and every host suite, then writes the
|
||||||
|
|
@ -119,121 +121,110 @@ coverage: tools
|
||||||
|
|
||||||
HLPC = bin/host/dvxhlpc
|
HLPC = bin/host/dvxhlpc
|
||||||
|
|
||||||
SYSTEM_DHS = src/libs/kpunch/libdvx/sysdoc.dhs \
|
HELP_FILES = src/tools/helpFiles.sh
|
||||||
src/libs/kpunch/libdvx/arch.dhs src/libs/kpunch/libdvx/apiref.dhs \
|
|
||||||
src/libs/kpunch/libtasks/libtasks.dhs src/libs/kpunch/dvxshell/dvxshell.dhs src/libs/kpunch/sql/dvxsql.dhs \
|
# The system help: the user guide (userHelp.dhs, kept beside Program
|
||||||
src/libs/kpunch/texthelp/texthelp.dhs src/libs/kpunch/listhelp/listhelp.dhs \
|
# Manager; Program Manager's own progman.dhs is its separate progman.hlp),
|
||||||
src/libs/kpunch/taskmgr/taskmgr.dhs src/libs/kpunch/serial/serial.dhs \
|
# then the library reference found by $(HELP_FILES), then the widget
|
||||||
src/apps/kpunch/dvxbasic/basrt.dhs \
|
# reference. Libraries, apps and widgets are discovered, so adding one
|
||||||
src/widgets/kpunch/wgtsys.dhs
|
# needs no edit here.
|
||||||
|
USER_GUIDE_DHS = src/apps/kpunch/progman/userHelp.dhs
|
||||||
|
|
||||||
BASIC_DHS = src/apps/kpunch/dvxbasic/ideguide.dhs src/apps/kpunch/dvxbasic/langref.dhs \
|
BASIC_DHS = src/apps/kpunch/dvxbasic/ideguide.dhs src/apps/kpunch/dvxbasic/langref.dhs \
|
||||||
src/apps/kpunch/dvxbasic/ctrlover.dhs src/apps/kpunch/dvxbasic/form.dhs
|
src/apps/kpunch/dvxbasic/ctrlover.dhs src/apps/kpunch/dvxbasic/form.dhs
|
||||||
|
|
||||||
compile-help:
|
# Needs bin/host/dvxhlpc, and every module built and its help deployed
|
||||||
|
# (library and app help are classified by the modules found in bin/).
|
||||||
|
# Each app's own help (<app>.dhs beside its source) becomes <app>.hlp
|
||||||
|
# next to <app>.app.
|
||||||
|
compile-help: deploy-helpsrc
|
||||||
@mkdir -p docs
|
@mkdir -p docs
|
||||||
$(HLPC) -o bin/apps/kpunch/dvxhelp/dvxhelp.hlp \
|
$(HLPC) -o bin/apps/kpunch/dvxhelp/dvxhelp.hlp \
|
||||||
--html docs/dvx_help_viewer.html \
|
--html docs/dvx_help_viewer.html \
|
||||||
src/apps/kpunch/dvxhelp/help.dhs
|
src/apps/kpunch/dvxhelp/help.dhs
|
||||||
|
libHelp=$$($(HELP_FILES) libs) && \
|
||||||
$(HLPC) -o bin/apps/kpunch/progman/dvxhelp.hlp \
|
$(HLPC) -o bin/apps/kpunch/progman/dvxhelp.hlp \
|
||||||
--html docs/dvx_system_reference.html \
|
--html docs/dvx_system_reference.html \
|
||||||
-i src/libs/kpunch/libdvx \
|
-i src/libs/kpunch/libdvx \
|
||||||
$(SYSTEM_DHS) \
|
$(USER_GUIDE_DHS) \
|
||||||
|
$$libHelp \
|
||||||
|
src/widgets/kpunch/wgtsys.dhs \
|
||||||
$$(find src/widgets/kpunch -name "*.dhs" ! -path "*/wgtsys.dhs" | sort)
|
$$(find src/widgets/kpunch -name "*.dhs" ! -path "*/wgtsys.dhs" | sort)
|
||||||
$(HLPC) -o bin/apps/kpunch/dvxbasic/dvxbasic.hlp \
|
$(HLPC) -o bin/apps/kpunch/dvxbasic/dvxbasic.hlp \
|
||||||
--html docs/dvx_basic_reference.html \
|
--html docs/dvx_basic_reference.html \
|
||||||
$(BASIC_DHS) \
|
$(BASIC_DHS) \
|
||||||
$$(find src/widgets/kpunch -name "*.bhs" | sort)
|
$$(find src/widgets/kpunch -name "*.bhs" | sort)
|
||||||
|
$(HELP_FILES) compile $(HLPC)
|
||||||
|
|
||||||
SDKDIR = bin/sdk
|
SDKDIR = bin/sdk
|
||||||
|
|
||||||
deploy-sdk:
|
# Public header lists (SDK_*_H), shared with the ABI size test.
|
||||||
|
SDK_ROOT =
|
||||||
|
include src/sdkHeaders.mk
|
||||||
|
|
||||||
|
# The SDK's own files (README, sdk.mk, newProject.sh, project templates)
|
||||||
|
# live in src/sdk; its bin/ holds the host tools a project build runs.
|
||||||
|
SDK_SRC = src/sdk
|
||||||
|
SDK_TOOLS = bin/host/dvxres bin/host/dvxhlpc bin/host/bascomp src/tools/dxeGen.sh
|
||||||
|
|
||||||
|
# The host tools come from tools (dvxres, dvxhlpc) and apps (bascomp).
|
||||||
|
deploy-sdk: tools apps
|
||||||
@echo "Building SDK..."
|
@echo "Building SDK..."
|
||||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
|
@# Start from an empty tree so a header dropped from the lists goes away.
|
||||||
@# Core headers (libdvx public API)
|
rm -rf $(SDKDIR)
|
||||||
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
|
@mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/fileops $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
|
||||||
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
|
cp $(SDK_CORE_H) $(SDKDIR)/include/core/
|
||||||
src/libs/kpunch/libdvx/dvxWm.h src/libs/kpunch/libdvx/dvxDraw.h \
|
cp $(SDK_SHELL_H) $(SDKDIR)/include/shell/
|
||||||
src/libs/kpunch/libdvx/dvxVideo.h src/libs/kpunch/libdvx/dvxComp.h \
|
cp $(SDK_TASKS_H) $(SDKDIR)/include/tasks/
|
||||||
src/libs/kpunch/libdvx/dvxPrefs.h src/libs/kpunch/libdvx/dvxDlg.h \
|
cp $(SDK_SQL_H) $(SDKDIR)/include/sql/
|
||||||
src/libs/kpunch/libdvx/dvxRes.h src/libs/kpunch/libdvx/dvxFont.h \
|
cp $(SDK_TEXTHELP_H) $(SDKDIR)/include/texthelp/
|
||||||
src/libs/kpunch/libdvx/dvxCur.h src/libs/kpunch/libdvx/dvxPal.h \
|
cp $(SDK_LISTHELP_H) $(SDKDIR)/include/listhelp/
|
||||||
src/libs/kpunch/libdvx/dvxMem.h src/libs/kpunch/libdvx/platform/dvxPlat.h; do \
|
cp $(SDK_FILEOPS_H) $(SDKDIR)/include/fileops/
|
||||||
[ -f "$$f" ] && cp "$$f" $(SDKDIR)/include/core/; \
|
cp $(SDK_XFER_H) $(SDKDIR)/include/xfer/
|
||||||
|
cp $(SDK_CONN_H) $(SDKDIR)/include/conn/
|
||||||
|
cp $(SDK_PRINT_H) $(SDKDIR)/include/print/
|
||||||
|
@# Networking: dvxNet.h and the standard socket headers, which keep their
|
||||||
|
@# subdirectories (sys/, netinet/, arpa/); sdk.mk adds include/net only
|
||||||
|
@# for programs that set NET = 1.
|
||||||
|
cp $(SDK_NET_H) $(SDKDIR)/include/net/
|
||||||
|
@for h in $(SDK_NET_STD_H); do \
|
||||||
|
mkdir -p $(SDKDIR)/include/net/$$(dirname $$h) && cp $(SDK_NET_DIR)/$$h $(SDKDIR)/include/net/$$h || exit 1; \
|
||||||
|
done
|
||||||
|
@for h in $(SDK_SERIAL_H); do \
|
||||||
|
mkdir -p $(SDKDIR)/include/serial/$$(dirname $$h) && cp $(SDK_SERIAL_DIR)/$$h $(SDKDIR)/include/serial/$$h || exit 1; \
|
||||||
done
|
done
|
||||||
@# Shell header
|
|
||||||
@cp src/libs/kpunch/dvxshell/shellApp.h $(SDKDIR)/include/shell/
|
|
||||||
@cp src/libs/kpunch/dvxshell/shellInf.h $(SDKDIR)/include/shell/
|
|
||||||
@# Tasks header
|
|
||||||
@cp src/libs/kpunch/libtasks/taskSwch.h $(SDKDIR)/include/tasks/
|
|
||||||
@# SQL header
|
|
||||||
@cp src/libs/kpunch/sql/dvxSql.h $(SDKDIR)/include/sql/
|
|
||||||
@# Widget headers -- one subdir per widget
|
@# Widget headers -- one subdir per widget
|
||||||
@for d in src/widgets/kpunch/*/; do \
|
@for h in $(SDK_WIDGET_H); do \
|
||||||
for h in "$$d"*.h; do \
|
wgt=$$(basename $$(dirname $$h)); \
|
||||||
[ -f "$$h" ] || continue; \
|
mkdir -p $(SDKDIR)/include/widget/$$wgt && cp $$h $(SDKDIR)/include/widget/$$wgt/ || exit 1; \
|
||||||
wgt=$$(basename "$$d"); \
|
|
||||||
mkdir -p $(SDKDIR)/include/widget/"$$wgt"; \
|
|
||||||
cp "$$h" $(SDKDIR)/include/widget/"$$wgt"/; \
|
|
||||||
done; \
|
|
||||||
done
|
done
|
||||||
@# BASIC include files
|
@# BASIC include files
|
||||||
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/
|
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/
|
||||||
@# BASIC sample: basdemo (project file + form + icon)
|
@# BASIC sample: basdemo (project file + form + icon + help source)
|
||||||
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
|
@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/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
|
||||||
|
@cp src/apps/kpunch/basdemo/basdemo.dhs $(SDKDIR)/samples/basic/basdemo/
|
||||||
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
|
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
|
||||||
@# BASIC sample: widshow (widget showcase)
|
@# BASIC sample: widshow (widget showcase)
|
||||||
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
|
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
|
||||||
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
|
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
|
||||||
|
@cp src/apps/kpunch/widshow/widshow.dhs $(SDKDIR)/samples/basic/widshow/
|
||||||
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
|
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
|
||||||
@# README
|
@# SDK files, project templates and the host tools
|
||||||
@printf '%s\n' \
|
cp $(SDK_SRC)/README.TXT $(SDK_SRC)/sdk.mk $(SDK_SRC)/newProject.sh $(SDKDIR)/
|
||||||
'DVX SDK' \
|
cp -R $(SDK_SRC)/templates $(SDKDIR)/
|
||||||
'=======' \
|
cp $(SDK_TOOLS) $(SDKDIR)/bin/
|
||||||
'' \
|
|
||||||
'Headers for developing applications and widgets against the DVX GUI.' \
|
|
||||||
'' \
|
|
||||||
'Directory Structure' \
|
|
||||||
'-------------------' \
|
|
||||||
'' \
|
|
||||||
' include/' \
|
|
||||||
' core/ DVX core headers (types, drawing, windows, widgets)' \
|
|
||||||
' shell/ Shell/app loading API' \
|
|
||||||
' tasks/ Cooperative task switching API' \
|
|
||||||
' 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' \
|
|
||||||
'------------' \
|
|
||||||
'' \
|
|
||||||
' - DJGPP cross-compiler (i586-pc-msdosdjgpp-gcc)' \
|
|
||||||
' - dxe3gen (included with DJGPP)' \
|
|
||||||
' - DVX resource compiler (SYSTEM/DVXRES.EXE on the target,' \
|
|
||||||
' or bin/host/dvxres on the host)' \
|
|
||||||
' - DVX help compiler (SYSTEM/DVXHLPC.EXE on the target,' \
|
|
||||||
' or bin/host/dvxhlpc on the host)' \
|
|
||||||
'' \
|
|
||||||
'For complete working examples, see the app sources under' \
|
|
||||||
'src/apps/kpunch/ in the DVX repository.' \
|
|
||||||
> $(SDKDIR)/README.TXT
|
|
||||||
|
|
||||||
|
# Every build output: the DOS targets through each directory's clean,
|
||||||
|
# then the whole object tree (host libraries, test binaries, coverage)
|
||||||
|
# and the host-only bin/ directories.
|
||||||
clean:
|
clean:
|
||||||
$(MAKE) -C src/libs/kpunch/libdvx clean
|
for d in $(LIBS); do $(MAKE) -C src/libs/kpunch/$$d clean || exit 1; done
|
||||||
$(MAKE) -C src/libs/kpunch/libtasks clean
|
|
||||||
$(MAKE) -C src/loader clean
|
$(MAKE) -C src/loader clean
|
||||||
$(MAKE) -C src/libs/kpunch/texthelp clean
|
|
||||||
$(MAKE) -C src/libs/kpunch/listhelp clean
|
|
||||||
$(MAKE) -C src/widgets/kpunch clean
|
$(MAKE) -C src/widgets/kpunch clean
|
||||||
$(MAKE) -C src/libs/kpunch/dvxshell clean
|
|
||||||
$(MAKE) -C src/libs/kpunch/taskmgr clean
|
|
||||||
$(MAKE) -C src/libs/kpunch/serial clean
|
|
||||||
$(MAKE) -C src/libs/kpunch/sql clean
|
|
||||||
$(MAKE) -C src/apps/kpunch clean
|
$(MAKE) -C src/apps/kpunch clean
|
||||||
$(MAKE) -C src/tools clean
|
$(MAKE) -C src/tools clean
|
||||||
-rmdir obj 2>/dev/null
|
$(MAKE) -C src/tools/proxy clean
|
||||||
-rm -rf bin/config bin/widgets bin/libs bin/sdk
|
rm -rf obj bin/host bin/test bin/system
|
||||||
-rm -f docs/*.html
|
rm -rf bin/config bin/widgets bin/libs bin/sdk
|
||||||
|
rm -f docs/*.html
|
||||||
|
|
|
||||||
46
README.md
46
README.md
|
|
@ -54,17 +54,47 @@ to what actually changes on screen, so the desktop is responsive even at
|
||||||
## Building and Deploying
|
## Building and Deploying
|
||||||
|
|
||||||
```
|
```
|
||||||
make # build everything
|
./installToolchain.sh # once, after a fresh clone: fetch the toolchain
|
||||||
make clean # remove all build artifacts
|
make # build everything
|
||||||
./mkcd.sh # build and produce a CD-ROM ISO image
|
make test # host test suites, goldens and the lint gate
|
||||||
|
make test-dos # run the real DOS build in 86Box (see below)
|
||||||
|
make clean # remove all build artifacts
|
||||||
|
./mkcd.sh # build and produce a CD-ROM ISO image
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Everything the build and the tests need that is not a standard Linux
|
||||||
|
package lives in `toolchains/`, which is not kept in git.
|
||||||
|
`./installToolchain.sh` downloads each piece, checks it against a
|
||||||
|
pinned SHA-256, and unpacks it there (no sudo, nothing outside the
|
||||||
|
project); run it again with `--force` to reinstall:
|
||||||
|
|
||||||
|
* DJGPP (GCC 12.2.0) and the CWSDPMI stub -- the DOS cross compiler
|
||||||
|
* 86Box 6.0 and its ROM set, FreeDOS 1.3 boot files and CuteMouse,
|
||||||
|
and DOSBox Staging 0.83.0 -- the emulator test harness
|
||||||
|
* cppcheck 2.21, built from source -- used by the static gate
|
||||||
|
* lrzsz 0.12.20 -- the Terminal tests' ZMODEM peer
|
||||||
|
|
||||||
|
It needs curl, tar, bzip2, xz, unzip, make, g++, mtools and xvfb-run.
|
||||||
|
To build with a different DOS toolchain, pass `DJGPP_PREFIX=...` and
|
||||||
|
`CWSDSTUB=...` to `make`.
|
||||||
|
|
||||||
`make` builds in dependency order: core, tasks, loader, helpers,
|
`make` builds in dependency order: core, tasks, loader, helpers,
|
||||||
host tools, widgets, shell, task manager, apps. Build output goes to
|
host tools, widgets, shell, task manager, apps. Build output goes to
|
||||||
`bin/` and intermediate objects to `obj/`.
|
`bin/` and intermediate objects to `obj/`.
|
||||||
|
|
||||||
|
`make test-dos` boots FreeDOS in 86Box (headless, under Xvfb), runs
|
||||||
|
`DVX /SCRIPT=...` for every script in `src/test/dos/scripts`, and
|
||||||
|
compares the screenshots with the goldens. `EMULATOR=dosbox` uses
|
||||||
|
DOSBox Staging instead (much faster); `SCRIPTS="boot apps"` runs a
|
||||||
|
subset. The emulators never touch your home directory. See
|
||||||
|
`src/test/dos/runDos.sh --help`.
|
||||||
|
|
||||||
`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
|
`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
|
||||||
image for use with an emulator or for burning to physical media.
|
image for use with an emulator or for burning to physical media. On
|
||||||
|
the target machine, change to the CD drive and type `INSTALL`: DVX
|
||||||
|
starts from the CD with its Setup program, which copies DVX to a
|
||||||
|
hard-disk folder (and can add it to AUTOEXEC.BAT). Holding Shift while
|
||||||
|
DVX starts (or `DVX /SAFE`) starts it in 640x480 safe video mode.
|
||||||
|
|
||||||
|
|
||||||
## Directory Layout
|
## Directory Layout
|
||||||
|
|
@ -76,6 +106,7 @@ dvxgui/
|
||||||
config/ shipped INI, themes, wallpapers
|
config/ shipped INI, themes, wallpapers
|
||||||
docs/ HTML exports of the built-in help content
|
docs/ HTML exports of the built-in help content
|
||||||
LICENSE.txt MIT license
|
LICENSE.txt MIT license
|
||||||
|
installToolchain.sh fetches the toolchain and test tools into toolchains/
|
||||||
Makefile top-level build
|
Makefile top-level build
|
||||||
mkcd.sh build-and-package script
|
mkcd.sh build-and-package script
|
||||||
src/
|
src/
|
||||||
|
|
@ -83,14 +114,19 @@ dvxgui/
|
||||||
include/ shared include files (BASIC library declarations)
|
include/ shared include files (BASIC library declarations)
|
||||||
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
|
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
|
||||||
loader/ bootstrap executable
|
loader/ bootstrap executable
|
||||||
|
sdk/ SDK project templates, build rules and newProject.sh
|
||||||
tools/ host-native development tools and the BASIC compiler
|
tools/ host-native development tools and the BASIC compiler
|
||||||
widgets/ widget plug-in modules
|
widgets/ widget plug-in modules
|
||||||
|
toolchains/ DJGPP, CWSDPMI, emulators, cppcheck (not in git; see
|
||||||
|
installToolchain.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
See `src/tools/README.md` for the host-side development tools
|
See `src/tools/README.md` for the host-side development tools
|
||||||
(resource tool, icon generators, help compiler, proxy), and
|
(resource tool, icon generators, help compiler, proxy), and
|
||||||
`src/apps/kpunch/README.md` for the SDK contract applications must
|
`src/apps/kpunch/README.md` for the SDK contract applications must
|
||||||
follow.
|
follow. `make` also writes the SDK to `bin/sdk`: headers, host tools
|
||||||
|
and a template for each kind of module (`bin/sdk/newProject.sh app
|
||||||
|
NAME`); the SDK Quick Start in the system help walks through it.
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ set -u
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
REAL_CC="$HOME/djgpp/djgpp/bin/i586-pc-msdosdjgpp-gcc"
|
# The DJGPP compiler the build uses (src/common.mk finds the toolchain;
|
||||||
|
# DJGPP_PREFIX in the environment overrides it).
|
||||||
|
REAL_CC="$(make -s --no-print-directory -f src/common.mk --eval 'printCc: ; @echo $(CC)' printCc)"
|
||||||
|
|
||||||
if [ ! -x "$REAL_CC" ]; then
|
if [ ! -x "$REAL_CC" ]; then
|
||||||
echo "analyze.sh: $REAL_CC not executable" >&2
|
echo "analyze.sh: $REAL_CC not executable" >&2
|
||||||
|
|
|
||||||
23
config/ASSOC.INI
Normal file
23
config/ASSOC.INI
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
; DVX File Associations
|
||||||
|
;
|
||||||
|
; Which program opens each kind of document. Program Manager, File
|
||||||
|
; Manager and the Control Panel (File Types page) all use this list.
|
||||||
|
;
|
||||||
|
; Each line is: EXTENSION = program[,arguments]
|
||||||
|
; EXTENSION the file extension without the dot
|
||||||
|
; program the program to run, relative to the DVX folder
|
||||||
|
; arguments optional; %1 stands for the document. Without arguments
|
||||||
|
; the document is passed on its own.
|
||||||
|
|
||||||
|
[associations]
|
||||||
|
TXT = apps/kpunch/notepad/notepad.app
|
||||||
|
INI = apps/kpunch/notepad/notepad.app
|
||||||
|
BMP = apps/kpunch/imgview/imgview.app
|
||||||
|
JPG = apps/kpunch/imgview/imgview.app
|
||||||
|
PNG = apps/kpunch/imgview/imgview.app
|
||||||
|
GIF = apps/kpunch/imgview/imgview.app
|
||||||
|
BAS = apps/kpunch/dvxbasic/dvxbasic.app
|
||||||
|
DBP = apps/kpunch/dvxbasic/dvxbasic.app
|
||||||
|
FRM = apps/kpunch/dvxbasic/dvxbasic.app
|
||||||
|
HLP = apps/kpunch/dvxhelp/dvxhelp.app
|
||||||
|
DHS = apps/kpunch/dvxhelp/helpedit.app
|
||||||
|
|
@ -11,8 +11,10 @@ build. Text files are converted to DOS line endings (CR+LF) via sed.
|
||||||
| Source File | Build Output | Description |
|
| Source File | Build Output | Description |
|
||||||
|-------------|-------------|-------------|
|
|-------------|-------------|-------------|
|
||||||
| `dvx.ini` | `bin/config/dvx.ini` | System configuration |
|
| `dvx.ini` | `bin/config/dvx.ini` | System configuration |
|
||||||
|
| `ASSOC.INI` | `bin/config/ASSOC.INI` | File associations: which program opens each document type (shellAssoc.h; edited on the Control Panel's File Types page) |
|
||||||
| `themes/cde.thm` | `bin/config/themes/cde.thm` | CDE color theme |
|
| `themes/cde.thm` | `bin/config/themes/cde.thm` | CDE color theme |
|
||||||
| `themes/geos.thm` | `bin/config/themes/geos.thm` | GEOS Ensemble theme |
|
| `themes/geos.thm` | `bin/config/themes/geos.thm` | GEOS Ensemble theme |
|
||||||
|
| `themes/hotdog.thm` | `bin/config/themes/hotdog.thm` | Hot Dog Stand theme |
|
||||||
| `themes/win31.thm` | `bin/config/themes/win31.thm` | Windows 3.1 theme |
|
| `themes/win31.thm` | `bin/config/themes/win31.thm` | Windows 3.1 theme |
|
||||||
| `wpaper/blueglow.jpg` | `bin/config/wpaper/blueglow.jpg` | Wallpaper image |
|
| `wpaper/blueglow.jpg` | `bin/config/wpaper/blueglow.jpg` | Wallpaper image |
|
||||||
| `wpaper/swoop.jpg` | `bin/config/wpaper/swoop.jpg` | Wallpaper image |
|
| `wpaper/swoop.jpg` | `bin/config/wpaper/swoop.jpg` | Wallpaper image |
|
||||||
|
|
@ -47,21 +49,27 @@ Comments start with `;`. The shell loads this at startup via
|
||||||
| `height` | 480 | Requested vertical resolution |
|
| `height` | 480 | Requested vertical resolution |
|
||||||
| `bpp` | 16 | Preferred color depth (8, 15, 16, 24, 32) |
|
| `bpp` | 16 | Preferred color depth (8, 15, 16, 24, 32) |
|
||||||
|
|
||||||
The system picks the closest available VESA mode.
|
The system picks the closest available VESA mode. If that mode cannot
|
||||||
|
be set, or a Shift key is held while DVX starts (or `DVX /SAFE`), the
|
||||||
|
shell starts in safe video mode (640x480, 16 bpp or the closest the card
|
||||||
|
has) and asks whether to write that mode here.
|
||||||
|
|
||||||
### [mouse] Section
|
### [mouse] Section
|
||||||
|
|
||||||
| Key | Default | Values | Description |
|
| Key | Default | Values | Description |
|
||||||
|-----|---------|--------|-------------|
|
|-----|---------|--------|-------------|
|
||||||
| `wheel` | normal | normal, reversed | Scroll wheel direction |
|
| `wheel` | normal | normal, reversed | Scroll wheel direction |
|
||||||
|
| `wheelspeed` | 3 | 1-10 | Lines scrolled per wheel notch |
|
||||||
| `doubleclick` | 500 | 200-900 | Double-click speed (ms) |
|
| `doubleclick` | 500 | 200-900 | Double-click speed (ms) |
|
||||||
| `acceleration` | medium | off, low, medium, high | Mouse acceleration |
|
| `acceleration` | medium | off, low, medium, high | Mouse acceleration |
|
||||||
|
| `speed` | 8 | 2-32 | Mickeys per 8 pixels of pointer travel (lower = faster pointer) |
|
||||||
|
|
||||||
### [shell] Section
|
### [shell] Section
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|-----|---------|-------------|
|
|-----|---------|-------------|
|
||||||
| `desktop` | apps/progman/progman.app | Path to the desktop app loaded at startup |
|
| `desktop` | apps/kpunch/progman/progman.app | Path to the desktop app loaded at startup |
|
||||||
|
| `welcome` | (none) | Program started after the desktop; the shipped file names the Welcome window, which deletes the key when the user ticks "Don't show this again" |
|
||||||
|
|
||||||
### [colors] Section
|
### [colors] Section
|
||||||
|
|
||||||
|
|
@ -98,6 +106,42 @@ compiled-in defaults.
|
||||||
| `wallpaper` | (none) | file path | Path to wallpaper image |
|
| `wallpaper` | (none) | file path | Path to wallpaper image |
|
||||||
| `mode` | stretch | stretch, tile, center | Wallpaper display mode |
|
| `mode` | stretch | stretch, tile, center | Wallpaper display mode |
|
||||||
|
|
||||||
|
### [network] Section
|
||||||
|
|
||||||
|
Read by the networking library (dvxnet) when the stack starts; the
|
||||||
|
Control Panel's Network page edits it.
|
||||||
|
|
||||||
|
| Key | Default | Values | Description |
|
||||||
|
|-----|---------|--------|-------------|
|
||||||
|
| `dhcp` | true | true, false | Get the address from a DHCP server |
|
||||||
|
| `address` | (none) | dotted IPv4 | Static address (dhcp = false) |
|
||||||
|
| `netmask` | (none) | dotted IPv4 | Static subnet mask (dhcp = false) |
|
||||||
|
| `gateway` | (none) | dotted IPv4 | Static default gateway (optional) |
|
||||||
|
| `dns1`, `dns2` | (none) | dotted IPv4 | Name servers; they win over the DHCP server's |
|
||||||
|
| `hostname` | DVX | text | Name sent to the DHCP server and returned by gethostname() |
|
||||||
|
| `vector` | auto | auto, 0x60-0x80 | Packet driver interrupt |
|
||||||
|
|
||||||
|
### [printer] Section
|
||||||
|
|
||||||
|
Read by the printing library (print.lib) for every print; the Control
|
||||||
|
Panel's Printer page edits the port and model, File > Page Setup (and
|
||||||
|
the Printer page's Page Setup button) the rest. A page setup that
|
||||||
|
cannot work (margins leaving no room, values out of range) is ignored
|
||||||
|
as a whole in favour of the defaults.
|
||||||
|
|
||||||
|
| Key | Default | Values | Description |
|
||||||
|
|-----|---------|--------|-------------|
|
||||||
|
| `port` | LPT1 | LPT1, LPT2, LPT3, FILE | Where printed output goes |
|
||||||
|
| `file` | PRINTER.PRN | file path | The file used with port = FILE |
|
||||||
|
| `model` | text | text, epson9, epson24, laserjet | Printer model; all but text print pictures |
|
||||||
|
| `linesPerPage` | 0 | 0, 10-200 | Lines per page; 0 = the model's usual length (66, laser 60) |
|
||||||
|
| `columns` | 80 | 20-255 | Characters across the page |
|
||||||
|
| `marginTop`, `marginBottom` | 3 | lines | Top and bottom margins (header / footer in their middle line) |
|
||||||
|
| `marginLeft`, `marginRight` | 5 | characters | Left and right margins |
|
||||||
|
| `tabWidth` | 8 | 1-16 | Tab stops for printed text |
|
||||||
|
| `wrap` | true | true, false | Wrap long lines at a word, or cut them off |
|
||||||
|
| `header`, `footer` | &f, Page &p | text | &f file name, &p page, &n pages, &d date, &t time, &l &c &r left / centre / right, && an ampersand |
|
||||||
|
|
||||||
|
|
||||||
## Theme Files (.thm)
|
## Theme Files (.thm)
|
||||||
|
|
||||||
|
|
@ -111,6 +155,7 @@ Bundled themes:
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `cde.thm` | CDE (Common Desktop Environment) -- warm tan/blue palette |
|
| `cde.thm` | CDE (Common Desktop Environment) -- warm tan/blue palette |
|
||||||
| `geos.thm` | GEOS Ensemble -- cyan/grey palette |
|
| `geos.thm` | GEOS Ensemble -- cyan/grey palette |
|
||||||
|
| `hotdog.thm` | Hot Dog Stand -- the infamous yellow/red/black palette |
|
||||||
| `win31.thm` | Windows 3.1 -- grey/navy palette |
|
| `win31.thm` | Windows 3.1 -- grey/navy palette |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ bpp = 16
|
||||||
; wheelspeed: lines per wheel notch (1-10, default 3)
|
; wheelspeed: lines per wheel notch (1-10, default 3)
|
||||||
; doubleclick: double-click speed in milliseconds (200-900, default 500)
|
; doubleclick: double-click speed in milliseconds (200-900, default 500)
|
||||||
; acceleration: off, low, medium, high (default medium)
|
; acceleration: off, low, medium, high (default medium)
|
||||||
; speed: cursor speed (2-32, default 8; higher = faster)
|
; speed: mouse travel needed per step of pointer movement (2-32, default 8;
|
||||||
|
; lower = faster pointer)
|
||||||
|
|
||||||
[mouse]
|
[mouse]
|
||||||
wheel = normal
|
wheel = normal
|
||||||
|
|
@ -25,6 +26,52 @@ speed = 8
|
||||||
|
|
||||||
; Shell settings.
|
; Shell settings.
|
||||||
; desktop: path to the desktop app loaded at startup
|
; desktop: path to the desktop app loaded at startup
|
||||||
|
; welcome: program started after the desktop (the Welcome window shown at
|
||||||
|
; the first start); it removes this line itself when you tick
|
||||||
|
; "Don't show this again"
|
||||||
|
|
||||||
[shell]
|
[shell]
|
||||||
desktop = apps/kpunch/progman/progman.app
|
desktop = apps/kpunch/progman/progman.app
|
||||||
|
welcome = apps/kpunch/welcome/welcome.app
|
||||||
|
|
||||||
|
; Network settings (TCP/IP through a packet driver loaded before DVX).
|
||||||
|
; dhcp: true = get the address from a DHCP server; false = use address,
|
||||||
|
; netmask and gateway below
|
||||||
|
; dns1, dns2: name servers (empty = the DHCP server's)
|
||||||
|
; hostname: this computer's name on the network
|
||||||
|
; vector: packet driver interrupt, e.g. 0x60 (auto = search 0x60-0x80)
|
||||||
|
|
||||||
|
[network]
|
||||||
|
dhcp = true
|
||||||
|
address =
|
||||||
|
netmask =
|
||||||
|
gateway =
|
||||||
|
dns1 =
|
||||||
|
dns2 =
|
||||||
|
hostname = DVX
|
||||||
|
vector = auto
|
||||||
|
|
||||||
|
; Printer settings (Control Panel's Printer page and File > Page Setup).
|
||||||
|
; port: LPT1, LPT2, LPT3 or FILE (print into the file named by file)
|
||||||
|
; model: text (any printer, text only), epson9, epson24 (Epson ESC/P
|
||||||
|
; dot matrix) or laserjet (HP PCL); all but text print pictures
|
||||||
|
; linesPerPage: 0 = the printer's usual page length (66, laser 60)
|
||||||
|
; columns: characters across the page; margins in lines (top, bottom)
|
||||||
|
; and characters (left, right)
|
||||||
|
; header, footer: &f file name, &p page, &n pages, &d date, &t time,
|
||||||
|
; &l &c &r left / centre / right, && an ampersand
|
||||||
|
|
||||||
|
[printer]
|
||||||
|
port = LPT1
|
||||||
|
file = PRINTER.PRN
|
||||||
|
model = text
|
||||||
|
linesPerPage = 0
|
||||||
|
columns = 80
|
||||||
|
marginTop = 3
|
||||||
|
marginBottom = 3
|
||||||
|
marginLeft = 5
|
||||||
|
marginRight = 5
|
||||||
|
tabWidth = 8
|
||||||
|
wrap = true
|
||||||
|
header = &f
|
||||||
|
footer = Page &p
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ INCLUDES=(
|
||||||
# of eliding whole functions behind unknown macro calls.
|
# of eliding whole functions behind unknown macro calls.
|
||||||
DEFINES=(
|
DEFINES=(
|
||||||
-DDXE_EXPORT=
|
-DDXE_EXPORT=
|
||||||
-DDVX_WIDGET_IMPL
|
|
||||||
-D__DJGPP__=2
|
-D__DJGPP__=2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -30,10 +30,14 @@ img { max-width: 100%; }
|
||||||
<h3>Contents</h3>
|
<h3>Contents</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#help.overview">DVX Help Viewer</a></li>
|
<li><a href="#help.overview">DVX Help Viewer</a></li>
|
||||||
|
<li><strong>Writing Help Files (for Developers)</strong>
|
||||||
|
<ul>
|
||||||
<li><a href="#help.format">Help Source Format</a></li>
|
<li><a href="#help.format">Help Source Format</a></li>
|
||||||
<li><a href="#help.compiler">Help Compiler</a></li>
|
<li><a href="#help.compiler">Help Compiler</a></li>
|
||||||
<li><a href="#help.integration">Application Integration</a></li>
|
<li><a href="#help.integration">Application Integration</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<h3>Index</h3>
|
<h3>Index</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#help.format">.dhs</a></li>
|
<li><a href="#help.format">.dhs</a></li>
|
||||||
|
|
@ -46,16 +50,19 @@ img { max-width: 100%; }
|
||||||
<li><a href="#help.overview">Help Viewer</a></li>
|
<li><a href="#help.overview">Help Viewer</a></li>
|
||||||
<li><a href="#help.integration">helpFile</a></li>
|
<li><a href="#help.integration">helpFile</a></li>
|
||||||
<li><a href="#help.integration">helpTopic</a></li>
|
<li><a href="#help.integration">helpTopic</a></li>
|
||||||
<li><a href="#help.integration">shellLoadAppWithArgs</a></li>
|
<li><a href="#help.integration">HelpView</a></li>
|
||||||
|
<li><a href="#help.integration">shellOpenHelp</a></li>
|
||||||
<li><a href="#help.format">Source Format</a></li>
|
<li><a href="#help.format">Source Format</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
<div class="topic" id="help.overview">
|
<div class="topic" id="help.overview">
|
||||||
<h1>DVX Help Viewer</h1>
|
<h1>DVX Help Viewer</h1>
|
||||||
<p>The DVX Help Viewer displays .hlp help files compiled from .dhs source documents. It provides a tree-based table of contents, scrollable content with word-wrapped text, clickable hyperlinks, full-text search, and a keyword index.</p>
|
<p>DVX Help shows help pages. The list on the left is the table of contents, the page you are reading is on the right, and underlined words are links to other pages. You can also look things up in the Index or with Search.</p>
|
||||||
|
<blockquote><strong>Tip:</strong> New to DVX? Click the Program Manager window and press F1, or choose DVX Help from the Program Manager's Help menu, to open the DVX user guide. It explains how to start programs, work with windows, menus and dialog boxes, and everything else that works the same in every program. Each program's own commands are explained in that program's help: press F1 while you work in it.</blockquote>
|
||||||
<h2>Opening Help</h2>
|
<h2>Opening Help</h2>
|
||||||
<p>Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.</p>
|
<p>Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.</p>
|
||||||
|
<p>If the help viewer is already running, F1 reuses its window: the requested page is shown there (loading the other application's help file if needed, which starts a fresh Back/Forward history) and the window is brought to the front. The Help menu of the Program Manager does the same. F1 pressed in the help viewer itself shows this help about the viewer. While the viewer's Index or Search dialog is open, the page asked for with F1 is shown once the dialog closes.</p>
|
||||||
<p>You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.</p>
|
<p>You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.</p>
|
||||||
<h2>Navigation</h2>
|
<h2>Navigation</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -63,8 +70,11 @@ img { max-width: 100%; }
|
||||||
<li>Click underlined links in the content to jump to other topics</li>
|
<li>Click underlined links in the content to jump to other topics</li>
|
||||||
<li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li>
|
<li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li>
|
||||||
<li>Use Navigate > Index to browse an alphabetical keyword list</li>
|
<li>Use Navigate > Index to browse an alphabetical keyword list</li>
|
||||||
<li>Use Navigate > Search to find topics by keyword</li>
|
<li>Use Navigate > Search to find topics by the words they contain</li>
|
||||||
|
<li>Press Tab to move between links and Enter or Space to follow one</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<h2>Searching</h2>
|
||||||
|
<p>Search looks through the title and text of every topic. Type one or more words separated by spaces; a topic matches when it contains all of them, in any order and in any mix of upper and lower case. Words may be partial: "scroll" also finds "ScrollPane". Index keywords that contain the whole search text match as well. Words need at least three letters or digits in a row to be looked up in topic text; a search made only of shorter words (such as "if" or "a b") matches topic titles and index keywords only. When one topic matches it is shown at once; otherwise pick one from the list of matching topics.</p>
|
||||||
<h2>Keyboard Shortcuts</h2>
|
<h2>Keyboard Shortcuts</h2>
|
||||||
<pre> Alt+Left Back
|
<pre> Alt+Left Back
|
||||||
Alt+Right Forward
|
Alt+Right Forward
|
||||||
|
|
@ -84,8 +94,11 @@ img { max-width: 100%; }
|
||||||
.h2 <text> Level 2 heading (underlined)
|
.h2 <text> Level 2 heading (underlined)
|
||||||
.h3 <text> Level 3 heading (plain)
|
.h3 <text> Level 3 heading (plain)
|
||||||
.hr Horizontal rule
|
.hr Horizontal rule
|
||||||
.link <id> <text> Hyperlink to another topic
|
.link <id> <text> Hyperlink to another topic in the same help
|
||||||
.image <file.bmp> Inline image (BMP format)</pre>
|
file (see Links below)
|
||||||
|
.image <file> [left|center|right]
|
||||||
|
Inline image (BMP, PNG, JPEG or GIF), path
|
||||||
|
relative to the -i directory</pre>
|
||||||
<h2>Block Directives</h2>
|
<h2>Block Directives</h2>
|
||||||
<pre> .list Start a bulleted list
|
<pre> .list Start a bulleted list
|
||||||
.item <text> List item (must be inside .list)
|
.item <text> List item (must be inside .list)
|
||||||
|
|
@ -98,6 +111,13 @@ img { max-width: 100%; }
|
||||||
.endnote End callout box</pre>
|
.endnote End callout box</pre>
|
||||||
<h2>Index Directives</h2>
|
<h2>Index Directives</h2>
|
||||||
<pre> .index <keyword> Add a keyword to the index pointing to this topic</pre>
|
<pre> .index <keyword> Add a keyword to the index pointing to this topic</pre>
|
||||||
|
<h2>File Directives</h2>
|
||||||
|
<pre> .include <file> Insert another source file here (path absolute or
|
||||||
|
relative to the including file)
|
||||||
|
.section <name> Group the following table of contents entries of
|
||||||
|
this file under a <name> heading
|
||||||
|
.section <a>|<b> Nest the group: heading <b> sits under heading <a>
|
||||||
|
(any depth; entries filed under <a> come first)</pre>
|
||||||
<h2>Example</h2>
|
<h2>Example</h2>
|
||||||
<pre><code>.topic intro
|
<pre><code>.topic intro
|
||||||
.title Welcome
|
.title Welcome
|
||||||
|
|
@ -138,12 +158,22 @@ This is a warning message.
|
||||||
<h1>Help Compiler (dvxhlpc)</h1>
|
<h1>Help Compiler (dvxhlpc)</h1>
|
||||||
<p>The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.</p>
|
<p>The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.</p>
|
||||||
<h2>Usage</h2>
|
<h2>Usage</h2>
|
||||||
<pre><code>dvxhlpc -o output.hlp [-i imagedir] [--html out.html] input.dhs [...]</code></pre>
|
<pre><code>dvxhlpc -o output.hlp [-i imagedir] [--html out.html] [--quiet] input.dhs [@filelist] [...]</code></pre>
|
||||||
<h2>Options</h2>
|
<h2>Options</h2>
|
||||||
<pre> -o output.hlp Output binary help file (required)
|
<pre> -o output.hlp Output binary help file (required)
|
||||||
-i imagedir Directory to find .image files (default: current dir)
|
-i imagedir Directory to find .image files (default: current dir)
|
||||||
--html out.html Also emit a self-contained HTML file</pre>
|
--html out.html Also emit a self-contained HTML file
|
||||||
|
--quiet Suppress progress output
|
||||||
|
@filelist Read more input file names from filelist, one per
|
||||||
|
line (blank lines and lines starting with # are
|
||||||
|
skipped)</pre>
|
||||||
<p>Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.</p>
|
<p>Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.</p>
|
||||||
|
<h2>Input Files</h2>
|
||||||
|
<p>Each input file starts fresh: no topic is current and no .list, .note, .code or .table block is left open from the previous file. Text before a file's first .topic is ignored, and directives there are errors. A .code or .table block still open at the end of a file is closed with a warning. Files named with .include are different: they are read in place, as if their lines were part of the including file.</p>
|
||||||
|
<p>Source lines longer than 4094 characters are an error. The .image directive may name a path relative to the -i directory, including a subdirectory (for example sub/pic.png).</p>
|
||||||
|
<p>If any error is reported the compile stops and the previous output file is kept.</p>
|
||||||
|
<h2>Links</h2>
|
||||||
|
<p>Every .link must name a topic that is compiled into the same help file: a topic in any of the input files or the files they include, before or after the link. A link to any other ID is an error that names the file, the line and the missing ID. The one exception is the help DVX rebuilds by itself when it starts after programs, widgets or libraries were added or removed: there a link to a topic that is no longer present shows as plain text instead, so removing a piece of DVX never breaks the rest of the help. Links cannot reach into another help file; name the other file's page in the text instead. A .link line inside a .code block is example text and is never checked.</p>
|
||||||
<h2>Build Integration</h2>
|
<h2>Build Integration</h2>
|
||||||
<p>The standard build pattern globs all fragments:</p>
|
<p>The standard build pattern globs all fragments:</p>
|
||||||
<pre><code>dvxhlpc -o dvxhelp.hlp docs/src/overview.dhs widgets/*/*.dhs</code></pre>
|
<pre><code>dvxhlpc -o dvxhelp.hlp docs/src/overview.dhs widgets/*/*.dhs</code></pre>
|
||||||
|
|
@ -153,18 +183,24 @@ This is a warning message.
|
||||||
</div>
|
</div>
|
||||||
<div class="topic" id="help.integration">
|
<div class="topic" id="help.integration">
|
||||||
<h1>Application Integration</h1>
|
<h1>Application Integration</h1>
|
||||||
<p>Any DVX application can provide context-sensitive help via the F1 key.</p>
|
<p>Every DVX application should have its own help file. F1 in the application, and the Contents command of its Help menu, open it. An application with no help file of its own gets the DVX user guide on F1.</p>
|
||||||
<h2>Setting Up Help</h2>
|
<h2>Applications in the DVX Source Tree</h2>
|
||||||
<p>In your appMain, set the help file path on the app context:</p>
|
<p>For an application built with DVX itself, write the help source as <app>.dhs in the application's source directory, next to <app>.c or <app>.dbp, where <app> is the name of the application's .app file. The build finds it by that name: it compiles it to <app>.hlp next to <app>.app, deploys the source, and writes an <APP>.HCF so the help is rebuilt on the target too. Nothing else needs to be listed anywhere.</p>
|
||||||
<pre><code>snprintf(ctx->helpFile, sizeof(ctx->helpFile),
|
<h2>C Applications</h2>
|
||||||
"%s%cMYAPP.HLP", ctx->appDir, DVX_PATH_SEP);</code></pre>
|
<p>Name the help file after the application (myapp.hlp beside myapp.app) and F1 works with no code: before appMain runs, DVX sets ctx->helpFile to that file when it exists. A help file with any other name or location is set in appMain instead:</p>
|
||||||
|
<pre><code>snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "other.hlp");</code></pre>
|
||||||
|
<p>To open it from a Help menu item, call shellOpenHelp. It reuses a running help viewer (and brings it to the front) instead of starting another one:</p>
|
||||||
|
<pre><code>shellOpenHelp(ctx->shellCtx, ctx->helpFile);</code></pre>
|
||||||
|
<p>The argument is "helpfile [topicId]", so a menu item can also open a particular topic. An empty help file name opens the DVX user guide, as F1 does for an application without a help file.</p>
|
||||||
<h2>Context-Sensitive Topics</h2>
|
<h2>Context-Sensitive Topics</h2>
|
||||||
<p>Update helpTopic as the user navigates your application:</p>
|
<p>Update helpTopic as the user moves around your application, or set onHelpQuery to a function that fills it in when F1 is pressed:</p>
|
||||||
<pre><code>snprintf(ctx->helpTopic, sizeof(ctx->helpTopic), "settings.video");</code></pre>
|
<pre><code>snprintf(ctx->helpTopic, sizeof(ctx->helpTopic), "settings.video");</code></pre>
|
||||||
<p>When the user presses F1, the shell launches the help viewer with your help file opened to the specified topic.</p>
|
<p>When the user presses F1, the help viewer opens your help file at that topic.</p>
|
||||||
<h2>Launching Help from Menus</h2>
|
<h2>DVX BASIC Applications</h2>
|
||||||
<p>To add a Help menu item that opens your help file:</p>
|
<p>A DVX BASIC program gets its help file from its project: set Help File in Project Properties. The file is copied next to the compiled program, and F1 opens it at the HelpTopic of the focused control, or else of the form. As for C applications, a myapp.hlp next to myapp.app is used when the project names no help file. To open it from a Help menu item, add help.bas to the project and call HelpView with the file name; a name without a folder is looked for next to the program:</p>
|
||||||
<pre><code>shellLoadAppWithArgs(ctx, viewerPath, helpFilePath);</code></pre>
|
<pre><code>SUB mnuHelpContents_Click
|
||||||
|
HelpView "myapp.hlp"
|
||||||
|
END SUB</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
347
installToolchain.sh
Executable file
347
installToolchain.sh
Executable file
|
|
@ -0,0 +1,347 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# installToolchain.sh -- Install the DOS cross toolchain and test tools into toolchains/
|
||||||
|
#
|
||||||
|
# Usage: ./installToolchain.sh [--force]
|
||||||
|
#
|
||||||
|
# Downloads and unpacks, into toolchains/ next to this script:
|
||||||
|
# toolchains/djgpp DJGPP cross compiler, GCC 12.2.0 (prebuilt
|
||||||
|
# linux64 release of andrewwutw/build-djgpp v3.4)
|
||||||
|
# toolchains/cwsdpmi CWSDPMI r7 (csdpmi7b.zip); src/common.mk uses
|
||||||
|
# toolchains/cwsdpmi/bin/bin/CWSDSTUB.EXE
|
||||||
|
# toolchains/86box 86Box v6.0 (build 9001) x86_64 Linux AppImage,
|
||||||
|
# extracted once into app/ (no FUSE needed), and
|
||||||
|
# the matching 86Box ROM set (tag v6.0) in roms/
|
||||||
|
# toolchains/freedos FreeDOS 1.3 master boot record code, FAT16 boot
|
||||||
|
# sector, KERNEL.SYS and COMMAND.COM (from the
|
||||||
|
# LiteUSB disk image) and CuteMouse 2.1b4
|
||||||
|
# CTMOUSE.EXE: the DOS the emulator test disk boots
|
||||||
|
# (src/test/dos)
|
||||||
|
# toolchains/dosbox DOSBox Staging v0.83.0 (the quick emulator)
|
||||||
|
# toolchains/cppcheck cppcheck 2.21, built from its source release
|
||||||
|
# (src/test/lint/staticGate.sh)
|
||||||
|
# toolchains/lrzsz lrzsz 0.12.20 (src/test/terminal/installLrzsz.sh,
|
||||||
|
# which pins it), the Terminal tests' ZMODEM peer
|
||||||
|
# toolchains/pktdrv NE2000.COM packet driver (src/test/net/
|
||||||
|
# installPktDrv.sh, which pins it) for the DOS
|
||||||
|
# network tests
|
||||||
|
#
|
||||||
|
# Every download is checked against a pinned SHA-256 before it is unpacked,
|
||||||
|
# so a changed or corrupted archive is refused instead of installed.
|
||||||
|
# Archives are cached in toolchains/cache and reused on later runs.
|
||||||
|
# An existing, working install is left alone unless --force is given.
|
||||||
|
# After installing 86Box the script runs it once with its isolated
|
||||||
|
# environment (src/test/dos/emuEnv.sh) and fails if anything appeared in
|
||||||
|
# the per-user folders of the home directory.
|
||||||
|
# toolchains/ is gitignored; run this after a fresh clone.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DJGPP_URL="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-linux64-gcc1220.tar.bz2"
|
||||||
|
DJGPP_SHA256="8464f17017d6ab1b2bb2df4ed82357b5bf692e6e2b7fee37e315638f3d505f00"
|
||||||
|
DJGPP_VERSION="12.2.0"
|
||||||
|
CWSDPMI_URL="https://sandmann.dotster.com/cwsdpmi/csdpmi7b.zip"
|
||||||
|
CWSDPMI_SHA256="deacda0488e1cdd7c4a9f32fab45662b34c0ed6b2d7d4d13bc07041b62004a8c"
|
||||||
|
BOX86_URL="https://github.com/86Box/86Box/releases/download/v6.0/86Box-Linux-x86_64-b9001.AppImage"
|
||||||
|
BOX86_FILE="86Box-Linux-x86_64-b9001.AppImage"
|
||||||
|
BOX86_SHA256="535d3c329f8c32b601103970c86d80aa80298f345452c3524ea2f433624727a4"
|
||||||
|
ROMS_URL="https://github.com/86Box/roms/archive/refs/tags/v6.0.tar.gz"
|
||||||
|
ROMS_FILE="86box-roms-v6.0.tar.gz"
|
||||||
|
ROMS_SHA256="38fccaf4a7b48e581ccdfee2dc3cc0545e7d3a778bc25611e902a117eec571e6"
|
||||||
|
FREEDOS_URL="https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.3/official/FD13-LiteUSB.zip"
|
||||||
|
FREEDOS_FILE="FD13-LiteUSB.zip"
|
||||||
|
FREEDOS_SHA256="64a934585087ccd91a18c55e20ee01f5f6762be712eeaa5f456be543778f9f7e"
|
||||||
|
FREEDOS_DISK_IMG="FD13LITE.img"
|
||||||
|
FREEDOS_PART_SECTOR=63 # first sector of the image's FAT16 partition
|
||||||
|
MBR_CODE_BYTES=446 # boot code before the partition table
|
||||||
|
CTMOUSE_URL="https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/repositories/1.3/base/ctmouse.zip"
|
||||||
|
CTMOUSE_FILE="ctmouse.zip"
|
||||||
|
CTMOUSE_SHA256="a891124cd5b13e8141778fcae718f3b2667b0a49727ce92b782ab11a8c4bb63a"
|
||||||
|
DOSBOX_URL="https://github.com/dosbox-staging/dosbox-staging/releases/download/v0.83.0/dosbox-staging-linux-x86_64-v0.83.0.tar.xz"
|
||||||
|
DOSBOX_FILE="dosbox-staging-linux-x86_64-v0.83.0.tar.xz"
|
||||||
|
DOSBOX_SHA256="d3a94f7f1c3e68a47ec88d61145506c7904452adb0c9c5928cb8cfe2331d6c5c"
|
||||||
|
CPPCHECK_URL="https://github.com/danmar/cppcheck/archive/refs/tags/2.21.0.tar.gz"
|
||||||
|
CPPCHECK_FILE="cppcheck-2.21.0.tar.gz"
|
||||||
|
CPPCHECK_SHA256="f028ff75ca5372738f3737c8b3e8611426a6526b6aea2ef01301ab0f5902f044"
|
||||||
|
CPPCHECK_VERSION="2.21.0"
|
||||||
|
|
||||||
|
# Seconds the isolation check lets 86Box run
|
||||||
|
BOX86_CHECK_SECONDS=8
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TOOLCHAINS="${ROOT}/toolchains"
|
||||||
|
CACHE="${TOOLCHAINS}/cache"
|
||||||
|
DJGPP_DIR="${TOOLCHAINS}/djgpp"
|
||||||
|
CWSDPMI_DIR="${TOOLCHAINS}/cwsdpmi"
|
||||||
|
DJGPP_GCC="${DJGPP_DIR}/bin/i586-pc-msdosdjgpp-gcc"
|
||||||
|
CWSDSTUB="${CWSDPMI_DIR}/bin/bin/CWSDSTUB.EXE"
|
||||||
|
|
||||||
|
# Emulator, FreeDOS and cppcheck locations and the isolated run environment
|
||||||
|
DVX_ROOT="${ROOT}"
|
||||||
|
# shellcheck source=src/test/dos/emuEnv.sh
|
||||||
|
. "${ROOT}/src/test/dos/emuEnv.sh"
|
||||||
|
|
||||||
|
# Each install records the SHA-256 of the archive(s) it came from here, so
|
||||||
|
# a changed pin reinstalls.
|
||||||
|
PIN_FILE=".installed"
|
||||||
|
|
||||||
|
FORCE=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "${arg}" in
|
||||||
|
--force) FORCE=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '/^# Usage/,/^# toolchains\/ is gitignored/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "installToolchain: unknown option '${arg}' (try --help)" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$(uname -s)" != "Linux" ] || [ "$(uname -m)" != "x86_64" ]; then
|
||||||
|
echo "installToolchain: the pinned DJGPP build is for x86_64 Linux; this host is $(uname -s) $(uname -m)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for tool in curl sha256sum tar bzip2 xz unzip make g++ mcopy xvfb-run; do
|
||||||
|
if ! command -v "${tool}" >/dev/null 2>&1; then
|
||||||
|
echo "installToolchain: required tool '${tool}' is not installed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "${CACHE}"
|
||||||
|
|
||||||
|
|
||||||
|
# fetch URL FILE SHA256 -- download FILE into the cache (unless a cached copy
|
||||||
|
# already matches) and verify it.
|
||||||
|
fetch() {
|
||||||
|
local url="$1"
|
||||||
|
local file="${CACHE}/$2"
|
||||||
|
local sum="$3"
|
||||||
|
|
||||||
|
if [ -f "${file}" ] && echo "${sum} ${file}" | sha256sum -c --status; then
|
||||||
|
echo "installToolchain: using cached $2"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "installToolchain: downloading $2"
|
||||||
|
curl -fL --retry 3 -o "${file}.part" "${url}"
|
||||||
|
mv "${file}.part" "${file}"
|
||||||
|
|
||||||
|
if ! echo "${sum} ${file}" | sha256sum -c --status; then
|
||||||
|
echo "installToolchain: checksum mismatch for $2 (expected ${sum}); refusing to install" >&2
|
||||||
|
rm -f "${file}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# pinned DIR PIN -- true when DIR holds an install made from PIN.
|
||||||
|
pinned() {
|
||||||
|
[ -f "$1/${PIN_FILE}" ] && [ "$(cat "$1/${PIN_FILE}")" = "$2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# DJGPP
|
||||||
|
if [ "${FORCE}" -eq 0 ] && [ -x "${DJGPP_GCC}" ] && [ "$("${DJGPP_GCC}" -dumpfullversion 2>/dev/null)" = "${DJGPP_VERSION}" ]; then
|
||||||
|
echo "installToolchain: DJGPP ${DJGPP_VERSION} already installed"
|
||||||
|
else
|
||||||
|
fetch "${DJGPP_URL}" "djgpp-linux64-gcc1220.tar.bz2" "${DJGPP_SHA256}"
|
||||||
|
rm -rf "${DJGPP_DIR}.new"
|
||||||
|
mkdir -p "${DJGPP_DIR}.new"
|
||||||
|
tar -xjf "${CACHE}/djgpp-linux64-gcc1220.tar.bz2" -C "${DJGPP_DIR}.new" --strip-components=1
|
||||||
|
rm -rf "${DJGPP_DIR}"
|
||||||
|
mv "${DJGPP_DIR}.new" "${DJGPP_DIR}"
|
||||||
|
echo "installToolchain: DJGPP installed in ${DJGPP_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CWSDPMI
|
||||||
|
if [ "${FORCE}" -eq 0 ] && [ -f "${CWSDSTUB}" ]; then
|
||||||
|
echo "installToolchain: CWSDPMI already installed"
|
||||||
|
else
|
||||||
|
fetch "${CWSDPMI_URL}" "csdpmi7b.zip" "${CWSDPMI_SHA256}"
|
||||||
|
rm -rf "${CWSDPMI_DIR}.new"
|
||||||
|
mkdir -p "${CWSDPMI_DIR}.new/bin"
|
||||||
|
(cd "${CWSDPMI_DIR}.new/bin" && unzip -qo "${CACHE}/csdpmi7b.zip")
|
||||||
|
rm -rf "${CWSDPMI_DIR}"
|
||||||
|
mv "${CWSDPMI_DIR}.new" "${CWSDPMI_DIR}"
|
||||||
|
echo "installToolchain: CWSDPMI installed in ${CWSDPMI_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 86Box AppImage, extracted once so it runs without FUSE
|
||||||
|
if [ "${FORCE}" -eq 0 ] && pinned "${BOX86_APP}" "${BOX86_SHA256}" && [ -x "${BOX86_BIN}" ]; then
|
||||||
|
echo "installToolchain: 86Box already installed"
|
||||||
|
else
|
||||||
|
fetch "${BOX86_URL}" "${BOX86_FILE}" "${BOX86_SHA256}"
|
||||||
|
chmod +x "${CACHE}/${BOX86_FILE}"
|
||||||
|
rm -rf "${BOX86_APP}.new"
|
||||||
|
mkdir -p "${BOX86_APP}.new"
|
||||||
|
(cd "${BOX86_APP}.new" && "${CACHE}/${BOX86_FILE}" --appimage-extract >/dev/null)
|
||||||
|
mv "${BOX86_APP}.new/squashfs-root" "${BOX86_APP}.tmp"
|
||||||
|
rm -rf "${BOX86_APP}.new" "${BOX86_APP}"
|
||||||
|
mv "${BOX86_APP}.tmp" "${BOX86_APP}"
|
||||||
|
echo "${BOX86_SHA256}" > "${BOX86_APP}/${PIN_FILE}"
|
||||||
|
echo "installToolchain: 86Box installed in ${BOX86_APP}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 86Box ROM set
|
||||||
|
if [ "${FORCE}" -eq 0 ] && pinned "${BOX86_ROMS}" "${ROMS_SHA256}"; then
|
||||||
|
echo "installToolchain: 86Box ROM set already installed"
|
||||||
|
else
|
||||||
|
fetch "${ROMS_URL}" "${ROMS_FILE}" "${ROMS_SHA256}"
|
||||||
|
rm -rf "${BOX86_ROMS}.new"
|
||||||
|
mkdir -p "${BOX86_ROMS}.new"
|
||||||
|
tar -xzf "${CACHE}/${ROMS_FILE}" -C "${BOX86_ROMS}.new" --strip-components=1
|
||||||
|
rm -rf "${BOX86_ROMS}"
|
||||||
|
mv "${BOX86_ROMS}.new" "${BOX86_ROMS}"
|
||||||
|
echo "${ROMS_SHA256}" > "${BOX86_ROMS}/${PIN_FILE}"
|
||||||
|
echo "installToolchain: 86Box ROM set installed in ${BOX86_ROMS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# FreeDOS boot files and CuteMouse
|
||||||
|
FREEDOS_PIN="${FREEDOS_SHA256} ${CTMOUSE_SHA256}"
|
||||||
|
|
||||||
|
if [ "${FORCE}" -eq 0 ] && pinned "${FREEDOS_DIR}" "${FREEDOS_PIN}"; then
|
||||||
|
echo "installToolchain: FreeDOS boot files already installed"
|
||||||
|
else
|
||||||
|
fetch "${FREEDOS_URL}" "${FREEDOS_FILE}" "${FREEDOS_SHA256}"
|
||||||
|
fetch "${CTMOUSE_URL}" "${CTMOUSE_FILE}" "${CTMOUSE_SHA256}"
|
||||||
|
rm -rf "${FREEDOS_DIR}.new"
|
||||||
|
mkdir -p "${FREEDOS_DIR}.new"
|
||||||
|
unzip -p "${CACHE}/${FREEDOS_FILE}" "${FREEDOS_DISK_IMG}" > "${FREEDOS_DIR}.new/disk.img"
|
||||||
|
dd if="${FREEDOS_DIR}.new/disk.img" of="${FREEDOS_DIR}.new/MBR.BIN" bs="${MBR_CODE_BYTES}" count=1 status=none
|
||||||
|
dd if="${FREEDOS_DIR}.new/disk.img" of="${FREEDOS_DIR}.new/BOOTSECT.BIN" bs=512 skip="${FREEDOS_PART_SECTOR}" count=1 status=none
|
||||||
|
MTOOLS_SKIP_CHECK=1 mcopy -n -i "${FREEDOS_DIR}.new/disk.img@@$((FREEDOS_PART_SECTOR * 512))" ::KERNEL.SYS ::COMMAND.COM "${FREEDOS_DIR}.new/"
|
||||||
|
unzip -p "${CACHE}/${CTMOUSE_FILE}" BIN/CTMOUSE.EXE > "${FREEDOS_DIR}.new/CTMOUSE.EXE"
|
||||||
|
rm -f "${FREEDOS_DIR}.new/disk.img"
|
||||||
|
rm -rf "${FREEDOS_DIR}"
|
||||||
|
mv "${FREEDOS_DIR}.new" "${FREEDOS_DIR}"
|
||||||
|
echo "${FREEDOS_PIN}" > "${FREEDOS_DIR}/${PIN_FILE}"
|
||||||
|
echo "installToolchain: FreeDOS boot files installed in ${FREEDOS_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DOSBox Staging
|
||||||
|
if [ "${FORCE}" -eq 0 ] && pinned "${DOSBOX_DIR}" "${DOSBOX_SHA256}" && [ -x "${DOSBOX_BIN}" ]; then
|
||||||
|
echo "installToolchain: DOSBox Staging already installed"
|
||||||
|
else
|
||||||
|
fetch "${DOSBOX_URL}" "${DOSBOX_FILE}" "${DOSBOX_SHA256}"
|
||||||
|
rm -rf "${DOSBOX_DIR}.new"
|
||||||
|
mkdir -p "${DOSBOX_DIR}.new"
|
||||||
|
tar -xJf "${CACHE}/${DOSBOX_FILE}" -C "${DOSBOX_DIR}.new" --strip-components=1
|
||||||
|
rm -rf "${DOSBOX_DIR}"
|
||||||
|
mv "${DOSBOX_DIR}.new" "${DOSBOX_DIR}"
|
||||||
|
echo "${DOSBOX_SHA256}" > "${DOSBOX_DIR}/${PIN_FILE}"
|
||||||
|
echo "installToolchain: DOSBox Staging installed in ${DOSBOX_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# cppcheck, built from source (no system package, no sudo)
|
||||||
|
if [ "${FORCE}" -eq 0 ] && pinned "${CPPCHECK_DIR}" "${CPPCHECK_SHA256}" && [ -x "${CPPCHECK_BIN}" ]; then
|
||||||
|
echo "installToolchain: cppcheck ${CPPCHECK_VERSION} already installed"
|
||||||
|
else
|
||||||
|
fetch "${CPPCHECK_URL}" "${CPPCHECK_FILE}" "${CPPCHECK_SHA256}"
|
||||||
|
rm -rf "${CPPCHECK_DIR}.build"
|
||||||
|
mkdir -p "${CPPCHECK_DIR}.build"
|
||||||
|
tar -xzf "${CACHE}/${CPPCHECK_FILE}" -C "${CPPCHECK_DIR}.build" --strip-components=1
|
||||||
|
echo "installToolchain: building cppcheck ${CPPCHECK_VERSION} (a few minutes)"
|
||||||
|
make -C "${CPPCHECK_DIR}.build" -j"$(nproc)" CXXFLAGS="-O2" FILESDIR="${CPPCHECK_DIR}/share/cppcheck" >"${CPPCHECK_DIR}.build/build.log" 2>&1 || {
|
||||||
|
echo "installToolchain: cppcheck build failed; see ${CPPCHECK_DIR}.build/build.log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
# FILESDIR is compiled in, so install straight to its final place.
|
||||||
|
rm -rf "${CPPCHECK_DIR}"
|
||||||
|
make -C "${CPPCHECK_DIR}.build" CXXFLAGS="-O2" PREFIX="${CPPCHECK_DIR}" FILESDIR="${CPPCHECK_DIR}/share/cppcheck" install >>"${CPPCHECK_DIR}.build/build.log" 2>&1
|
||||||
|
rm -rf "${CPPCHECK_DIR}.build"
|
||||||
|
echo "${CPPCHECK_SHA256}" > "${CPPCHECK_DIR}/${PIN_FILE}"
|
||||||
|
echo "installToolchain: cppcheck installed in ${CPPCHECK_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# lrzsz (host ZMODEM peer for the Terminal tests), pinned in its own script
|
||||||
|
if [ "${FORCE}" -eq 1 ]; then
|
||||||
|
"${ROOT}/src/test/terminal/installLrzsz.sh" --force
|
||||||
|
else
|
||||||
|
"${ROOT}/src/test/terminal/installLrzsz.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NE2000 packet driver for the emulator network tests, pinned in its own script
|
||||||
|
if [ "${FORCE}" -eq 1 ]; then
|
||||||
|
"${ROOT}/src/test/net/installPktDrv.sh" --force
|
||||||
|
else
|
||||||
|
"${ROOT}/src/test/net/installPktDrv.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify what the build needs.
|
||||||
|
if [ "$("${DJGPP_GCC}" -dumpfullversion)" != "${DJGPP_VERSION}" ]; then
|
||||||
|
echo "installToolchain: ${DJGPP_GCC} is not GCC ${DJGPP_VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${BOX86_BIN}" "${BOX86_ROMS}/machines" "${FREEDOS_DIR}/MBR.BIN" "${FREEDOS_DIR}/BOOTSECT.BIN" "${FREEDOS_DIR}/KERNEL.SYS" "${FREEDOS_DIR}/COMMAND.COM" "${FREEDOS_DIR}/CTMOUSE.EXE" "${DOSBOX_BIN}" "${CPPCHECK_BIN}" "${TOOLCHAINS}/lrzsz/bin/lsz" "${TOOLCHAINS}/pktdrv/NE2000.COM"; do
|
||||||
|
if [ ! -e "${f}" ]; then
|
||||||
|
echo "installToolchain: expected file missing after install: ${f}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$("${CPPCHECK_BIN}" --version)" in
|
||||||
|
"Cppcheck ${CPPCHECK_VERSION}"|"Cppcheck ${CPPCHECK_VERSION%.0}") ;;
|
||||||
|
*)
|
||||||
|
echo "installToolchain: ${CPPCHECK_BIN} is not cppcheck ${CPPCHECK_VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Run 86Box once, isolated, and prove nothing reached the home directory.
|
||||||
|
checkDir="${BOX86_DIR}/check"
|
||||||
|
rm -rf "${checkDir}"
|
||||||
|
mkdir -p "${checkDir}/vm"
|
||||||
|
printf '[Machine]\nmachine = ibmpc\nmem_size = 256\n' > "${checkDir}/vm/86box.cfg"
|
||||||
|
touch "${checkDir}/stamp"
|
||||||
|
emuHomeProbe "${checkDir}/stamp" > "${checkDir}/before.txt"
|
||||||
|
emuIsolatedEnv "${BOX86_HOME}"
|
||||||
|
setsid env "${EMU_ENV[@]}" xvfb-run -a "${BOX86_BIN}" -P "${checkDir}/vm" -R "${BOX86_ROMS}" -O "${checkDir}/global.cfg" -L "${checkDir}/86box.log" -N >/dev/null 2>&1 &
|
||||||
|
checkPid=$!
|
||||||
|
sleep "${BOX86_CHECK_SECONDS}"
|
||||||
|
emuStopGroup "${checkPid}"
|
||||||
|
emuHomeProbe "${checkDir}/stamp" > "${checkDir}/after.txt"
|
||||||
|
|
||||||
|
if ! diff -u "${checkDir}/before.txt" "${checkDir}/after.txt"; then
|
||||||
|
echo "installToolchain: 86Box changed the home directory (above); refusing to use it" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 86Box rewrites the machine config when it starts.
|
||||||
|
if ! grep -q "^emu_build_num" "${checkDir}/vm/86box.cfg"; then
|
||||||
|
echo "installToolchain: the 86Box check run did not start" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "${checkDir}"
|
||||||
|
echo "installToolchain: 86Box ran isolated (nothing written outside ${DVX_ROOT})"
|
||||||
|
|
||||||
|
echo "installToolchain: toolchain ready (GCC ${DJGPP_VERSION}, CWSDPMI r7, 86Box 6.0, FreeDOS 1.3, DOSBox Staging 0.83.0, cppcheck ${CPPCHECK_VERSION})"
|
||||||
9
mkcd.sh
9
mkcd.sh
|
|
@ -65,6 +65,9 @@ find "$SCRIPT_DIR/bin/" -name ".DBLOCALFILE*" -delete 2>/dev/null
|
||||||
# -iso-level 1: strict 8.3 filenames (DOS compatibility)
|
# -iso-level 1: strict 8.3 filenames (DOS compatibility)
|
||||||
# -J: Joliet extensions (long names for Windows/Linux)
|
# -J: Joliet extensions (long names for Windows/Linux)
|
||||||
# -V: volume label
|
# -V: volume label
|
||||||
|
# -m: leave out what does not belong on the target: the Linux host tools
|
||||||
|
# (bin/host, and their copies in the SDK's bin/sdk/bin), stale host
|
||||||
|
# test binaries (bin/test), the linker map and run logs.
|
||||||
# Note: -R (Rock Ridge) is omitted because DOS surfaces its
|
# Note: -R (Rock Ridge) is omitted because DOS surfaces its
|
||||||
# attribute sidecar files (._ATR_) as visible junk files.
|
# attribute sidecar files (._ATR_) as visible junk files.
|
||||||
echo "Creating ISO image..."
|
echo "Creating ISO image..."
|
||||||
|
|
@ -72,6 +75,12 @@ mkisofs \
|
||||||
-iso-level 1 \
|
-iso-level 1 \
|
||||||
-J \
|
-J \
|
||||||
-V "DVX" \
|
-V "DVX" \
|
||||||
|
-m "$SCRIPT_DIR/bin/host" \
|
||||||
|
-m "$SCRIPT_DIR/bin/sdk/bin" \
|
||||||
|
-m "$SCRIPT_DIR/bin/test" \
|
||||||
|
-m "*.map" \
|
||||||
|
-m "*.LOG" \
|
||||||
|
-m "*.log" \
|
||||||
-o "$ISO_PATH" \
|
-o "$ISO_PATH" \
|
||||||
"$SCRIPT_DIR/bin/"
|
"$SCRIPT_DIR/bin/"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,42 @@
|
||||||
#
|
#
|
||||||
# Source tree is now one dir per app, mirroring bin/apps/kpunch/.
|
# Source tree is now one dir per app, mirroring bin/apps/kpunch/.
|
||||||
|
|
||||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
include ../../common.mk
|
||||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
|
||||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
|
||||||
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
|
|
||||||
|
|
||||||
OBJDIR = ../../../obj/apps
|
APP_INCLUDES = -I../../libs/kpunch -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
|
||||||
BINDIR = ../../../bin/apps
|
CFLAGS = $(DOS_CFLAGS) $(APP_INCLUDES)
|
||||||
|
|
||||||
|
OBJDIR = ../../../obj/apps
|
||||||
|
BINROOT = ../../../bin
|
||||||
|
BINDIR = $(BINROOT)/apps
|
||||||
DVXRES = ../../../bin/host/dvxres
|
DVXRES = ../../../bin/host/dvxres
|
||||||
|
|
||||||
# C apps: one directory, one .c file each
|
# C apps are discovered, never listed: every directory <app>/ holding
|
||||||
C_APPS = progman clock dvxdemo cpanel dvxhelp
|
# <app>/<app>.c is one app. All .c files in that directory are linked into
|
||||||
|
# bin/apps/kpunch/<app>/<app>.app, and <app>/<app>.res (when present) is
|
||||||
|
# appended by dvxres with every .bmp in the directory as a dependency.
|
||||||
|
C_APP_MAINS = $(wildcard */*.c)
|
||||||
|
C_APPS = $(sort $(foreach f,$(C_APP_MAINS),$(if $(filter $(notdir $(basename $(f))),$(patsubst %/,%,$(dir $(f)))),$(patsubst %/,%,$(dir $(f))))))
|
||||||
|
|
||||||
BASCOMP = ../../../bin/host/bascomp
|
BASCOMP = ../../../bin/host/bascomp
|
||||||
# BASIC apps: each is a .dbp project in its own directory.
|
# BASIC apps: each is a .dbp project in its own directory.
|
||||||
# BASIC-only notepad, imgview, etc. replace the old C versions.
|
# BASIC-only notepad, imgview, etc. replace the old C versions.
|
||||||
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
|
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
|
||||||
|
|
||||||
|
# DOS batch files an app puts in the DVX folder itself (<app>/dosroot/*.bat,
|
||||||
|
# e.g. setup's install.bat), discovered like the apps, with DOS line endings.
|
||||||
|
ROOT_BATS = $(wildcard */dosroot/*.bat)
|
||||||
|
ROOT_BAT_OUT = $(addprefix $(BINROOT)/,$(notdir $(ROOT_BATS)))
|
||||||
|
|
||||||
|
vpath %.bat $(sort $(dir $(ROOT_BATS)))
|
||||||
|
|
||||||
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
|
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||||
|
|
||||||
all: $(C_APPS) dvxbasic $(BASIC_APPS)
|
all: $(C_APPS) dvxbasic $(BASIC_APPS) $(ROOT_BAT_OUT)
|
||||||
|
|
||||||
|
$(BINROOT)/%.bat: %.bat $(DVX_MAKEFILES)
|
||||||
|
@mkdir -p $(BINROOT)
|
||||||
|
sed 's/$$/\r/' $< > $@
|
||||||
|
|
||||||
dvxbasic:
|
dvxbasic:
|
||||||
$(MAKE) -C dvxbasic
|
$(MAKE) -C dvxbasic
|
||||||
|
|
@ -53,47 +69,41 @@ dvxbasic:
|
||||||
$(BASCOMP): | dvxbasic
|
$(BASCOMP): | dvxbasic
|
||||||
@:
|
@:
|
||||||
|
|
||||||
cpanel: $(BINDIR)/kpunch/cpanel/cpanel.app
|
# Per-app sources, objects and resource inputs.
|
||||||
progman: $(BINDIR)/kpunch/progman/progman.app
|
appSrcs = $(wildcard $(1)/*.c)
|
||||||
clock: $(BINDIR)/kpunch/clock/clock.app
|
appObjs = $(patsubst $(1)/%.c,$(OBJDIR)/$(1)/%.o,$(call appSrcs,$(1)))
|
||||||
dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app
|
appRes = $(wildcard $(1)/$(1).res)
|
||||||
dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
appDeps = $(call appRes,$(1)) $(if $(call appRes,$(1)),$(wildcard $(1)/*.bmp))
|
||||||
|
|
||||||
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
|
# $(1) = app directory name. $(2) = extra shell commands run after linking
|
||||||
$(DXE3GEN) -o $@ -U $<
|
# (empty for most apps).
|
||||||
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
|
define C_APP_RULES
|
||||||
|
$(1): $(BINDIR)/kpunch/$(1)/$(1).app
|
||||||
|
|
||||||
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
|
$(BINDIR)/kpunch/$(1)/$(1).app: $(call appObjs,$(1)) $(call appDeps,$(1))
|
||||||
$(DXE3GEN) -o $@ -U $<
|
@mkdir -p $$(dir $$@)
|
||||||
|
$$(DXE3GEN) -o $$@ -U $(call appObjs,$(1))
|
||||||
|
$(if $(call appRes,$(1)),cd $(1) && ../$$(DVXRES) build ../$$@ $(1).res)
|
||||||
|
$(2)
|
||||||
|
|
||||||
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
|
$(OBJDIR)/$(1)/%.o: $(1)/%.c $$(DVX_MAKEFILES)
|
||||||
$(DXE3GEN) -o $@ -U $<
|
@mkdir -p $$(dir $$@)
|
||||||
cd clock && ../$(DVXRES) build ../$@ clock.res
|
$$(CC) $$(CFLAGS) -c -o $$@ $$<
|
||||||
|
endef
|
||||||
|
|
||||||
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
||||||
|
DVXDEMO_POST = cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
|
||||||
|
|
||||||
$(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo
|
$(foreach a,$(C_APPS),$(eval $(call C_APP_RULES,$(a),$(if $(filter dvxdemo,$(a)),$(DVXDEMO_POST)))))
|
||||||
$(DXE3GEN) -o $@ -U $<
|
|
||||||
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
|
|
||||||
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
|
|
||||||
|
|
||||||
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
|
C_APP_OBJS = $(foreach a,$(C_APPS),$(call appObjs,$(a)))
|
||||||
$(DXE3GEN) -o $@ -U $<
|
|
||||||
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
|
|
||||||
|
|
||||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
|
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
|
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
|
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
|
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
|
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
|
||||||
|
|
||||||
# BASIC apps (compiled from .dbp projects via bascomp). Source for each app
|
# BASIC apps (compiled from .dbp projects via bascomp). Source for each app
|
||||||
# is under the app's own directory.
|
# is under the app's own directory.
|
||||||
|
#
|
||||||
|
# F1 needs no resource: the shell presets an app's help file to <name>.hlp
|
||||||
|
# beside the .app (compiled there from <dir>/<name>.dhs by compile-help).
|
||||||
|
|
||||||
iconed: $(BINDIR)/kpunch/iconed/iconed.app
|
iconed: $(BINDIR)/kpunch/iconed/iconed.app
|
||||||
|
|
||||||
$(BINDIR)/kpunch/iconed/iconed.app: iconed/iconed.dbp iconed/iconed.frm iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
|
$(BINDIR)/kpunch/iconed/iconed.app: iconed/iconed.dbp iconed/iconed.frm iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
|
||||||
|
|
@ -113,7 +123,6 @@ helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||||
|
|
||||||
$(BINDIR)/kpunch/dvxhelp/helpedit.app: dvxhelp/helpedit/helpedit.dbp dvxhelp/helpedit/helpedit.frm dvxhelp/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
|
$(BINDIR)/kpunch/dvxhelp/helpedit.app: dvxhelp/helpedit/helpedit.dbp dvxhelp/helpedit/helpedit.frm dvxhelp/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
|
||||||
$(BASCOMP) dvxhelp/helpedit/helpedit.dbp -o $@ -release
|
$(BASCOMP) dvxhelp/helpedit/helpedit.dbp -o $@ -release
|
||||||
$(DVXRES) add $@ helpfile text "dvxhelp.hlp"
|
|
||||||
|
|
||||||
resedit: $(BINDIR)/kpunch/resedit/resedit.app
|
resedit: $(BINDIR)/kpunch/resedit/resedit.app
|
||||||
|
|
||||||
|
|
@ -133,88 +142,51 @@ $(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm wi
|
||||||
# Native host build (test harness): each C app becomes a plain ELF shared
|
# Native host build (test harness): each C app becomes a plain ELF shared
|
||||||
# object under obj/host/apps/<name>.so with its resources appended by
|
# object under obj/host/apps/<name>.so with its resources appended by
|
||||||
# dvxres exactly as on DOS (the resource footer trails the ELF image, which
|
# dvxres exactly as on DOS (the resource footer trails the ELF image, which
|
||||||
# the dynamic loader ignores). SAN=1 adds ASan/UBSan.
|
# the dynamic loader ignores). SAN/COV per common.mk.
|
||||||
HOSTCC = gcc
|
HOSTOBJDIR = $(HOST_OBJROOT)/apps
|
||||||
HOSTOBJDIR = ../../../obj/host/apps
|
HOSTCFLAGS = $(HOST_CFLAGS) $(APP_INCLUDES)
|
||||||
HOSTCFLAGS = -O1 -g -fPIC -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -D_GNU_SOURCE -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
|
HOSTLDFLAGS = -shared $(HOST_LDFLAGS)
|
||||||
HOSTLDFLAGS = -shared
|
|
||||||
ifeq ($(SAN),1)
|
|
||||||
HOSTCFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
|
|
||||||
HOSTLDFLAGS += -fsanitize=address,undefined
|
|
||||||
endif
|
|
||||||
# COV=1 (make coverage): gcov instrumentation under obj/hostcov.
|
|
||||||
ifeq ($(COV),1)
|
|
||||||
HOSTOBJDIR = ../../../obj/hostcov/apps
|
|
||||||
HOSTCFLAGS += --coverage
|
|
||||||
HOSTLDFLAGS += --coverage
|
|
||||||
endif
|
|
||||||
HOST_SOS = $(foreach n,$(C_APPS),$(HOSTOBJDIR)/$(n).so)
|
HOST_SOS = $(foreach n,$(C_APPS),$(HOSTOBJDIR)/$(n).so)
|
||||||
|
|
||||||
|
appHostObjs = $(patsubst $(1)/%.c,$(HOSTOBJDIR)/$(1)/%.o,$(call appSrcs,$(1)))
|
||||||
|
|
||||||
|
# $(1) = app directory name.
|
||||||
|
define C_APP_HOST_RULES
|
||||||
|
$(HOSTOBJDIR)/$(1).so: $(call appHostObjs,$(1)) $(call appDeps,$(1))
|
||||||
|
$$(HOSTCC) $$(HOSTLDFLAGS) -o $$@ $(call appHostObjs,$(1))
|
||||||
|
$(if $(call appRes,$(1)),cd $(1) && ../$$(DVXRES) build ../$$@ $(1).res)
|
||||||
|
|
||||||
|
$(HOSTOBJDIR)/$(1)/%.o: $(1)/%.c $$(DVX_MAKEFILES)
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
$$(HOSTCC) $$(HOSTCFLAGS) -c -o $$@ $$<
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(foreach a,$(C_APPS),$(eval $(call C_APP_HOST_RULES,$(a))))
|
||||||
|
|
||||||
|
HOST_APP_OBJS = $(foreach a,$(C_APPS),$(call appHostObjs,$(a)))
|
||||||
|
|
||||||
.PHONY: host host-clean
|
.PHONY: host host-clean
|
||||||
|
|
||||||
host: $(HOST_SOS)
|
host: $(HOST_SOS)
|
||||||
|
|
||||||
$(HOSTOBJDIR)/progman.so: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
|
|
||||||
@mkdir -p $(HOSTOBJDIR)
|
|
||||||
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
|
|
||||||
|
|
||||||
$(HOSTOBJDIR)/clock.so: clock/clock.c clock/clock.res clock/icon32.bmp $(COMMON_H)
|
|
||||||
@mkdir -p $(HOSTOBJDIR)
|
|
||||||
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
|
|
||||||
cd clock && ../$(DVXRES) build ../$@ clock.res
|
|
||||||
|
|
||||||
$(HOSTOBJDIR)/cpanel.so: cpanel/cpanel.c cpanel/cpanel.res cpanel/icon32.bmp $(COMMON_H)
|
|
||||||
@mkdir -p $(HOSTOBJDIR)
|
|
||||||
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
|
|
||||||
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
|
|
||||||
|
|
||||||
$(HOSTOBJDIR)/dvxdemo.so: dvxdemo/dvxdemo.c dvxdemo/dvxdemo.res dvxdemo/icon32.bmp $(COMMON_H) $(WIDGET_H)
|
|
||||||
@mkdir -p $(HOSTOBJDIR)
|
|
||||||
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
|
|
||||||
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
|
|
||||||
|
|
||||||
$(HOSTOBJDIR)/dvxhelp.so: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h dvxhelp/dvxhelp.res dvxhelp/icon32.bmp $(COMMON_H)
|
|
||||||
@mkdir -p $(HOSTOBJDIR)
|
|
||||||
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
|
|
||||||
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
|
|
||||||
|
|
||||||
host-clean:
|
host-clean:
|
||||||
rm -rf $(HOSTOBJDIR)
|
rm -rf $(HOSTOBJDIR)
|
||||||
|
|
||||||
$(OBJDIR):
|
|
||||||
mkdir -p $(OBJDIR)
|
|
||||||
|
|
||||||
$(BINDIR)/kpunch/cpanel: ; mkdir -p $@
|
|
||||||
$(BINDIR)/kpunch/progman: ; mkdir -p $@
|
|
||||||
$(BINDIR)/kpunch/clock: ; mkdir -p $@
|
|
||||||
$(BINDIR)/kpunch/dvxdemo: ; mkdir -p $@
|
|
||||||
$(BINDIR)/kpunch/dvxhelp: ; mkdir -p $@
|
$(BINDIR)/kpunch/dvxhelp: ; mkdir -p $@
|
||||||
$(BINDIR)/kpunch/iconed: ; mkdir -p $@
|
$(BINDIR)/kpunch/iconed: ; mkdir -p $@
|
||||||
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
|
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
|
||||||
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
|
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
|
||||||
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
|
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
|
||||||
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
|
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
|
||||||
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
|
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
|
||||||
|
|
||||||
# Header dependencies
|
-include $(C_APP_OBJS:.o=.d) $(HOST_APP_OBJS:.o=.d)
|
||||||
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
|
|
||||||
# Every widget public header. Apps that pull in widget APIs (e.g. dvxdemo)
|
|
||||||
# must rebuild when these change, otherwise the struct-layout contract
|
|
||||||
# between the widget DXE's sApi and the app's cast to RadioApiT/etc.
|
|
||||||
# silently drifts and produces late-binding crashes.
|
|
||||||
WIDGET_H = $(wildcard ../../widgets/kpunch/*/*.h)
|
|
||||||
$(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h
|
|
||||||
$(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
|
|
||||||
$(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h
|
|
||||||
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(WIDGET_H)
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(OBJDIR)/*.o
|
rm -f $(C_APP_OBJS) $(C_APP_OBJS:.o=.d)
|
||||||
rm -f $(BINDIR)/kpunch/cpanel/cpanel.app
|
rm -f $(foreach a,$(C_APPS),$(BINDIR)/kpunch/$(a)/$(a).app)
|
||||||
rm -f $(BINDIR)/kpunch/progman/progman.app
|
rm -f $(ROOT_BAT_OUT)
|
||||||
rm -f $(BINDIR)/kpunch/clock/clock.app
|
rm -f $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS))
|
||||||
rm -f $(BINDIR)/kpunch/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS))
|
|
||||||
rm -f $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
|
||||||
rm -f $(BINDIR)/kpunch/iconed/iconed.app
|
rm -f $(BINDIR)/kpunch/iconed/iconed.app
|
||||||
rm -f $(BINDIR)/kpunch/notepad/notepad.app
|
rm -f $(BINDIR)/kpunch/notepad/notepad.app
|
||||||
rm -f $(BINDIR)/kpunch/imgview/imgview.app
|
rm -f $(BINDIR)/kpunch/imgview/imgview.app
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@ the BASIC-based sample apps. Two flavours of app live side by side:
|
||||||
host-side `bascomp`. The compiler links the BASIC bytecode into a
|
host-side `bascomp`. The compiler links the BASIC bytecode into a
|
||||||
copy of `basstub.app`, which acts as the runtime host. See
|
copy of `basstub.app`, which acts as the runtime host. See
|
||||||
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
|
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
|
||||||
`dvxhelp/helpedit/`.
|
`widshow/`, `dvxhelp/helpedit/`.
|
||||||
|
|
||||||
The rest of this document covers writing a C app against the SDK.
|
The rest of this document covers writing a C app against the SDK.
|
||||||
For BASIC apps, see `dvxbasic/README.md`.
|
For BASIC apps, see `dvxbasic/README.md`. A new app outside this tree
|
||||||
|
starts from the SDK templates (`bin/sdk/newProject.sh app|basic NAME`,
|
||||||
|
sources in `src/sdk/`).
|
||||||
|
|
||||||
|
|
||||||
## DXE App Contract
|
## DXE App Contract
|
||||||
|
|
@ -27,7 +29,7 @@ Every `.app` binary exports two required symbols and one optional
|
||||||
symbol:
|
symbol:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
AppDescriptorT appDescriptor; // required
|
AppDescriptorT appDescriptor; // required (SHELL_APP_DESCRIPTOR)
|
||||||
int32_t appMain(DxeAppContextT *ctx); // required
|
int32_t appMain(DxeAppContextT *ctx); // required
|
||||||
void appShutdown(void); // optional
|
void appShutdown(void); // optional
|
||||||
```
|
```
|
||||||
|
|
@ -35,10 +37,14 @@ void appShutdown(void); // optional
|
||||||
### `appDescriptor`
|
### `appDescriptor`
|
||||||
|
|
||||||
A statically-initialised struct read by the shell at load time to
|
A statically-initialised struct read by the shell at load time to
|
||||||
decide how to launch the app:
|
decide how to launch the app. Declare it with `SHELL_APP_DESCRIPTOR`,
|
||||||
|
which names the global and records the `DVX_ABI_VERSION` the app was
|
||||||
|
built with; the shell refuses an app whose `abiVersion` differs ("This
|
||||||
|
program was built for a different version of DVX and cannot run.").
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
uint32_t abiVersion; // set by SHELL_APP_DESCRIPTOR; first
|
||||||
char name[SHELL_APP_NAME_MAX]; // display name (<= 64 chars)
|
char name[SHELL_APP_NAME_MAX]; // display name (<= 64 chars)
|
||||||
bool hasMainLoop; // see below
|
bool hasMainLoop; // see below
|
||||||
bool multiInstance; // may have multiple copies open
|
bool multiInstance; // may have multiple copies open
|
||||||
|
|
@ -50,13 +56,13 @@ typedef struct {
|
||||||
Example (from `clock/clock.c`):
|
Example (from `clock/clock.c`):
|
||||||
|
|
||||||
```c
|
```c
|
||||||
AppDescriptorT appDescriptor = {
|
SHELL_APP_DESCRIPTOR(
|
||||||
.name = "Clock",
|
.name = "Clock",
|
||||||
.hasMainLoop = true,
|
.hasMainLoop = true,
|
||||||
.multiInstance = true,
|
.multiInstance = true,
|
||||||
.stackSize = SHELL_STACK_DEFAULT,
|
.stackSize = SHELL_STACK_DEFAULT,
|
||||||
.priority = TS_PRIORITY_LOW
|
.priority = TS_PRIORITY_LOW
|
||||||
};
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### `appMain`
|
### `appMain`
|
||||||
|
|
@ -116,8 +122,8 @@ Apps attach resources to their `.app` binary using `dvxres build`
|
||||||
| `publisher` | text | Publisher or studio. |
|
| `publisher` | text | Publisher or studio. |
|
||||||
| `copyright` | text | Copyright notice. |
|
| `copyright` | text | Copyright notice. |
|
||||||
| `version` | text | Version string, free-form. |
|
| `version` | text | Version string, free-form. |
|
||||||
| `description` | text | One-sentence description shown in About dialogs. |
|
| `description` | text | One-sentence description, shown in Program Manager's status bar and List view. |
|
||||||
| `helpfile` | text | Optional relative path to a `.hlp` file for F1 context help. |
|
| `helpfile` | text | BASIC apps: name of the app's `.hlp` file, relative to the app's directory (the project's Help File); the BASIC stub sets F1 help from it. Not needed when the file is `<app>.hlp`, which the shell uses by default. |
|
||||||
|
|
||||||
Additional application-specific resources may be attached with any
|
Additional application-specific resources may be attached with any
|
||||||
unique name. The runtime reads them via
|
unique name. The runtime reads them via
|
||||||
|
|
@ -137,6 +143,32 @@ description text "Digital clock with date display"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
Every app has its own help file; the system help (the user guide
|
||||||
|
plus the developer reference, opened from Program Manager) holds only
|
||||||
|
material that applies to every app, and never lists or describes
|
||||||
|
individual apps.
|
||||||
|
|
||||||
|
* Write the help source as `<app>.dhs` next to `<app>.c` or
|
||||||
|
`<app>.dbp`, where `<app>` is the name of the `.app` file.
|
||||||
|
* The top-level `compile-help` target runs `src/tools/helpFiles.sh`,
|
||||||
|
which finds every such file by name, compiles it to `<app>.hlp`
|
||||||
|
next to `<app>.app`, deploys the source as `<APP>.DHS` and writes an
|
||||||
|
`<APP>.HCF` so the loader's boot-time help rebuild produces it too.
|
||||||
|
No list needs editing when an app is added.
|
||||||
|
* F1 needs no code: before `appMain` the shell sets `ctx->helpFile` to
|
||||||
|
`<app>.hlp` beside `<app>.app` when that file exists.
|
||||||
|
* C apps open it from Help > Contents with
|
||||||
|
`shellOpenHelp(ctx->shellCtx, ctx->helpFile)`.
|
||||||
|
* BASIC apps call `HelpView "<app>.hlp"` (from
|
||||||
|
`src/include/basic/help.bas`) from Help > Contents. F1 opens the
|
||||||
|
focused control's or form's `HelpTopic`.
|
||||||
|
* Every app's Help menu has Contents and About.
|
||||||
|
|
||||||
|
See `claudeDocs/perAppHelp.md` for the full rules.
|
||||||
|
|
||||||
|
|
||||||
## Bundled Applications
|
## Bundled Applications
|
||||||
|
|
||||||
### Program Manager (progman)
|
### Program Manager (progman)
|
||||||
|
|
@ -147,16 +179,28 @@ description text "Digital clock with date display"
|
||||||
| Type | Callback-only |
|
| Type | Callback-only |
|
||||||
| Multi-instance | No |
|
| Multi-instance | No |
|
||||||
|
|
||||||
The desktop. Recursively scans `apps/` for `.app` files and displays
|
The desktop: program groups in a list on the left, the selected group's
|
||||||
them as a launchable grid with icons, names, and publishers. Double
|
items as large icons, small icons or a list on the right, a search box
|
||||||
click or Enter to launch. Includes a Help menu with the system help
|
that filters every group, and a status bar. The automatic "Installed
|
||||||
viewer and the About / System Info dialog. Registers with
|
Programs" group is rebuilt from `shellProgramScan()` at start, on
|
||||||
`shellRegisterDesktopUpdate()` so the grid refreshes when apps are
|
View > Refresh and when the window comes to the front with programs
|
||||||
loaded, terminated, or crash.
|
added or removed; items in the "Startup" group run once DVX is up.
|
||||||
|
Groups, items (name, program, arguments, working folder, icon, shortcut
|
||||||
|
key, run minimized, grid cell), view, options and Run history are saved
|
||||||
|
in `CONFIG/APPS/KPUNCH/PROGMAN/PROGMAN.INI` after every change (crash-safe
|
||||||
|
rewrite). Running, the Run dialog, program discovery and file
|
||||||
|
associations are shell library services (`shellRun.h`, `shellProg.h`,
|
||||||
|
`shellAssoc.h`) shared with other programs. Closing it exits DVX
|
||||||
|
through `shellRequestExit()`; `shellRegisterDesktopUpdate()` keeps the
|
||||||
|
running count current. Design notes: `claudeDocs/programManager.md`.
|
||||||
|
|
||||||
Widget headers used: `box/box.h`, `button/button.h`, `label/label.h`,
|
Sources: `progman.c` (window, menus, commands), `pmModel.c` (groups,
|
||||||
`statusBar/statBar.h`, `textInput/textInpt.h`, `imageButton/imgBtn.h`,
|
items, PROGMAN.INI; no widgets, host-tested), `pmView.c` (the icon view,
|
||||||
`scrollPane/scrlPane.h`, `wrapBox/wrapBox.h`.
|
a private widget class), `pmProps.c` (item Properties dialog). Its own
|
||||||
|
help is `progman.dhs` (progman.hlp). The directory also owns the system
|
||||||
|
help: `dvxhelp.hcf` (on-target rebuild recipe) and `userHelp.dhs`, the
|
||||||
|
end-user guide that is compiled first so its `user.welcome` page is the
|
||||||
|
help file's default topic.
|
||||||
|
|
||||||
### Notepad (notepad, BASIC)
|
### Notepad (notepad, BASIC)
|
||||||
|
|
||||||
|
|
@ -213,7 +257,7 @@ Resources include `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`,
|
||||||
| Type | Callback-only |
|
| Type | Callback-only |
|
||||||
| Multi-instance | No |
|
| Multi-instance | No |
|
||||||
|
|
||||||
System configuration with four tabs:
|
System configuration with five tabs:
|
||||||
|
|
||||||
* **Mouse** -- scroll direction, wheel speed, double-click speed,
|
* **Mouse** -- scroll direction, wheel speed, double-click speed,
|
||||||
acceleration, cursor speed
|
acceleration, cursor speed
|
||||||
|
|
@ -221,9 +265,12 @@ System configuration with four tabs:
|
||||||
and save
|
and save
|
||||||
* **Desktop** -- wallpaper image and display mode
|
* **Desktop** -- wallpaper image and display mode
|
||||||
* **Video** -- resolution and colour depth switching
|
* **Video** -- resolution and colour depth switching
|
||||||
|
* **File Types** -- file associations (`cpTypes.c`): add, change and
|
||||||
|
remove which program opens each extension
|
||||||
|
|
||||||
Changes preview live. OK writes to `CONFIG/DVX.INI`; Cancel reverts
|
Changes preview live. OK writes to `CONFIG/DVX.INI` and the file types
|
||||||
to the snapshot taken when the panel opened.
|
to `CONFIG/ASSOC.INI` (through `shellAssocSet`/`shellAssocRemove`);
|
||||||
|
Cancel reverts to the snapshot taken when the panel opened.
|
||||||
|
|
||||||
### Image Viewer (imgview, BASIC)
|
### Image Viewer (imgview, BASIC)
|
||||||
|
|
||||||
|
|
@ -278,8 +325,9 @@ and inline images.
|
||||||
| Type | BASIC (main-loop via basstub) |
|
| Type | BASIC (main-loop via basstub) |
|
||||||
| Multi-instance | No |
|
| Multi-instance | No |
|
||||||
|
|
||||||
Paint-program-style editor for creating 32x32 BMP icons. Pen /
|
Dot-by-dot editor for 16x16, 24x24 and 32x32 icons. Pencil /
|
||||||
eraser / fill tools, 16-colour palette, BMP save / load.
|
eraser / colour picker tools, 32-colour palette, BMP or PNG open,
|
||||||
|
PNG (or BMP by extension) save.
|
||||||
|
|
||||||
### Help Editor (helpedit, BASIC)
|
### Help Editor (helpedit, BASIC)
|
||||||
|
|
||||||
|
|
@ -289,8 +337,11 @@ eraser / fill tools, 16-colour palette, BMP save / load.
|
||||||
| Type | BASIC (main-loop via basstub) |
|
| Type | BASIC (main-loop via basstub) |
|
||||||
| Multi-instance | No |
|
| Multi-instance | No |
|
||||||
|
|
||||||
Editor for `.dhs` help source files with syntax-aware controls. Runs
|
Editor for `.dhs` help source files with syntax highlighting. Build >
|
||||||
the compiled system help viewer by default for live preview.
|
Compile writes `<name>.hlp` next to the source; Build > Preview
|
||||||
|
compiles and opens the result in the help viewer. Its own help is
|
||||||
|
`helpedit.hlp`; Help > Help File Format opens the format reference in
|
||||||
|
the viewer's `dvxhelp.hlp`.
|
||||||
|
|
||||||
### Resource Editor (resedit, BASIC)
|
### Resource Editor (resedit, BASIC)
|
||||||
|
|
||||||
|
|
@ -314,6 +365,18 @@ browse its resources, add, replace, extract, or remove them.
|
||||||
Gallery of short BASIC programs demonstrating the language and the
|
Gallery of short BASIC programs demonstrating the language and the
|
||||||
form designer. Intended as sample source for reading.
|
form designer. Intended as sample source for reading.
|
||||||
|
|
||||||
|
### Widget Showcase (widshow, BASIC)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| File | `apps/kpunch/widshow/widshow.app` |
|
||||||
|
| Type | BASIC (main-loop via basstub) |
|
||||||
|
| Multi-instance | No |
|
||||||
|
|
||||||
|
One tabbed window exercising the standard BASIC controls, with
|
||||||
|
tooltips and a status bar reporting each event. Shipped in the SDK as
|
||||||
|
a sample project, with basdemo.
|
||||||
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|
@ -325,9 +388,14 @@ make clean # remove all app build artifacts
|
||||||
|
|
||||||
The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
|
The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
|
||||||
|
|
||||||
* **C apps** compile each `<app>/<app>.c` to an object file, run
|
* **C apps** are discovered: every directory `<app>/` that holds
|
||||||
`dxe3gen -U` on it to produce `<app>.app`, then attach resources
|
`<app>/<app>.c` is one. All `.c` files in the directory are
|
||||||
with `dvxres build` if a `.res` manifest exists.
|
compiled and linked with `dxe3gen -U` into `<app>.app`, then
|
||||||
|
resources are attached with `dvxres build` if `<app>/<app>.res`
|
||||||
|
exists. No list needs editing.
|
||||||
|
* **DOS batch files** an app ships in the DVX folder itself
|
||||||
|
(`<app>/dosroot/*.bat`, e.g. `setup/dosroot/install.bat`) are
|
||||||
|
discovered the same way and copied to `bin/` with DOS line endings.
|
||||||
* **BASIC apps** run `bascomp <app>.dbp -o <app>.app -release`,
|
* **BASIC apps** run `bascomp <app>.dbp -o <app>.app -release`,
|
||||||
which handles project-file parsing, compilation, resource
|
which handles project-file parsing, compilation, resource
|
||||||
attachment, and linking into `basstub.app`.
|
attachment, and linking into `basstub.app`.
|
||||||
|
|
@ -352,13 +420,13 @@ static int32_t appMainImpl(DxeAppContextT *ctx);
|
||||||
static void onClose(WindowT *win);
|
static void onClose(WindowT *win);
|
||||||
|
|
||||||
// Required descriptor
|
// Required descriptor
|
||||||
AppDescriptorT appDescriptor = {
|
SHELL_APP_DESCRIPTOR(
|
||||||
.name = "My App",
|
.name = "My App",
|
||||||
.hasMainLoop = false,
|
.hasMainLoop = false,
|
||||||
.multiInstance = false,
|
.multiInstance = false,
|
||||||
.stackSize = SHELL_STACK_DEFAULT,
|
.stackSize = SHELL_STACK_DEFAULT,
|
||||||
.priority = 0
|
.priority = 0
|
||||||
};
|
);
|
||||||
|
|
||||||
|
|
||||||
static void onClose(WindowT *win) {
|
static void onClose(WindowT *win) {
|
||||||
|
|
@ -387,10 +455,10 @@ static int32_t appMainImpl(DxeAppContextT *ctx) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Add a Makefile target that mirrors the existing apps (see
|
Put the source in `apps/kpunch/<app>/<app>.c` (more `.c` files in the
|
||||||
`apps/kpunch/Makefile` for the pattern), drop a `.res` manifest next
|
same directory are linked in too), drop a `.res` manifest next to the
|
||||||
to the source, and provide a 32x32 `icon32.bmp`. `make <app>` should
|
source, and provide a 32x32 `icon32.bmp`. The Makefile finds the app by
|
||||||
then produce `bin/apps/kpunch/<app>/<app>.app`.
|
itself; `make <app>` then produces `bin/apps/kpunch/<app>/<app>.app`.
|
||||||
|
|
||||||
|
|
||||||
## Testing Locally
|
## Testing Locally
|
||||||
|
|
@ -406,6 +474,12 @@ dependencies, and per-app crash diagnostics.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
|
Each app's own help source `<app>.dhs` sits beside its `.c` or `.dbp`
|
||||||
|
(see Help above). `progman/`, `dvxbasic/` and `dvxhelp/` also hold
|
||||||
|
help configured by hand in the top-level Makefile and their `.hcf`
|
||||||
|
files: the system help (`progman/userHelp.dhs`), the BASIC help and the
|
||||||
|
viewer's help.
|
||||||
|
|
||||||
```
|
```
|
||||||
apps/kpunch/
|
apps/kpunch/
|
||||||
Makefile top-level app build
|
Makefile top-level app build
|
||||||
|
|
@ -413,8 +487,13 @@ apps/kpunch/
|
||||||
|
|
||||||
# C apps
|
# C apps
|
||||||
progman/
|
progman/
|
||||||
progman.c Program Manager
|
progman.c Program Manager window, menus, commands
|
||||||
|
pmModel.c/.h groups, items, PROGMAN.INI
|
||||||
|
pmView.c/.h icon view widget class
|
||||||
|
pmProps.c/.h item Properties dialog
|
||||||
|
progman.dhs Program Manager's own help
|
||||||
dvxhelp.hcf help-compiler config (system reference)
|
dvxhelp.hcf help-compiler config (system reference)
|
||||||
|
userHelp.dhs end-user guide (first part of the system help)
|
||||||
clock/
|
clock/
|
||||||
clock.c digital clock (main-loop reference)
|
clock.c digital clock (main-loop reference)
|
||||||
clock.res
|
clock.res
|
||||||
|
|
@ -426,6 +505,7 @@ apps/kpunch/
|
||||||
logo.bmp / new.bmp / open.bmp / sample.bmp / save.bmp
|
logo.bmp / new.bmp / open.bmp / sample.bmp / save.bmp
|
||||||
cpanel/
|
cpanel/
|
||||||
cpanel.c Control Panel
|
cpanel.c Control Panel
|
||||||
|
cpTypes.c/.h File Types page
|
||||||
cpanel.res
|
cpanel.res
|
||||||
icon32.bmp
|
icon32.bmp
|
||||||
dvxhelp/
|
dvxhelp/
|
||||||
|
|
@ -461,6 +541,10 @@ apps/kpunch/
|
||||||
resedit.dbp
|
resedit.dbp
|
||||||
resedit.frm
|
resedit.frm
|
||||||
ICON32.BMP
|
ICON32.BMP
|
||||||
|
widshow/
|
||||||
|
widshow.dbp
|
||||||
|
widshow.frm
|
||||||
|
ICON32.BMP
|
||||||
|
|
||||||
dvxbasic/ see dvxbasic/README.md
|
dvxbasic/ see dvxbasic/README.md
|
||||||
compiler/ lexer, parser, codegen, strip, obfuscate
|
compiler/ lexer, parser, codegen, strip, obfuscate
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
Name = BASIC Demo
|
Name = BASIC Demo
|
||||||
Author = Scott Duensing
|
Author = Scott Duensing
|
||||||
Publisher = Kangaroo Punch Studios
|
Publisher = Kangaroo Punch Studios
|
||||||
|
Version = 1.00
|
||||||
Copyright = Copyright 2026 Scott Duensing
|
Copyright = Copyright 2026 Scott Duensing
|
||||||
Description = Comprehensive tour of DVX BASIC features
|
Description = Comprehensive tour of DVX BASIC features
|
||||||
Icon = ICON32.BMP
|
Icon = ICON32.BMP
|
||||||
|
|
||||||
[Modules]
|
[Modules]
|
||||||
File0 = ../../../include/basic/commdlg.bas
|
File0 = ../../../include/basic/commdlg.bas
|
||||||
Count = 1
|
File1 = ../../../include/basic/help.bas
|
||||||
|
Count = 2
|
||||||
|
|
||||||
[Forms]
|
[Forms]
|
||||||
File0 = basdemo.frm
|
File0 = basdemo.frm
|
||||||
|
|
|
||||||
79
src/apps/kpunch/basdemo/basdemo.dhs
Normal file
79
src/apps/kpunch/basdemo/basdemo.dhs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
# BASIC Demo help. Compiled to basdemo.hlp next to basdemo.app; F1 and
|
||||||
|
# Help > Contents in the BASIC Demo open it.
|
||||||
|
|
||||||
|
.topic basdemo.overview
|
||||||
|
.title BASIC Demo
|
||||||
|
.toc 0 BASIC Demo
|
||||||
|
.default
|
||||||
|
.index BASIC Demo
|
||||||
|
.index Feature Tour
|
||||||
|
.index DVX BASIC Feature Tour
|
||||||
|
|
||||||
|
.h1 BASIC Demo
|
||||||
|
|
||||||
|
BASIC Demo is a guided tour of what programs written in DVX BASIC can do. Its window is called DVX BASIC Feature Tour. It is a good place to start if you are curious about writing your own programs.
|
||||||
|
|
||||||
|
.h2 Running a Demonstration
|
||||||
|
|
||||||
|
The buttons in the Language Demonstrations box each run a short example. The results appear in the large text area below the buttons, and the line at the bottom of the window says when an example is done.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Button What it shows
|
||||||
|
------ -------------
|
||||||
|
Types The kinds of values BASIC works with: whole numbers, decimals, text and true/false.
|
||||||
|
Math Arithmetic and math functions such as square roots.
|
||||||
|
Strings Working with text: cutting, joining, searching and changing case.
|
||||||
|
Arrays Lists and tables of values.
|
||||||
|
DATA/READ Values stored inside the program and read back.
|
||||||
|
Control Flow Loops and decisions.
|
||||||
|
UDT Records that group several values together.
|
||||||
|
Optional Procedures with optional parts.
|
||||||
|
Errors Catching a mistake (dividing by zero) without stopping the program.
|
||||||
|
Format Formatting numbers as money, percentages and so on.
|
||||||
|
File I/O Writing a small file, reading it back and deleting it.
|
||||||
|
System Where the program is stored, the date, the time and similar details.
|
||||||
|
INI Writing a small settings file, reading it back and deleting it.
|
||||||
|
Dialogs Message boxes and boxes that ask you to type or pick something.
|
||||||
|
Clear Empties the text area.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
The File I/O and INI examples create their files in the program's own data folder and delete them again when they finish.
|
||||||
|
|
||||||
|
.h2 Menus
|
||||||
|
|
||||||
|
.table
|
||||||
|
Menu Command What it does
|
||||||
|
---- ------- ------------
|
||||||
|
File Clear OutArea Empty the text area.
|
||||||
|
File Save OutArea... Save the text area to a text file.
|
||||||
|
File Exit Close the BASIC Demo and any windows it opened.
|
||||||
|
Demos Run All Text Demos Run every example that writes to the text area, one after another (all except Dialogs).
|
||||||
|
Demos Graphics... Open a window with a drawing. Draw paints shapes and text; Clear wipes it.
|
||||||
|
Demos Dynamic Form... Open a window that the program builds while running, with a button that counts.
|
||||||
|
Demos Timer... Open a window with a counter that ticks twice a second.
|
||||||
|
Help Contents Open this help.
|
||||||
|
Help About... Show the program name and copyright.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
|
@ -46,6 +46,12 @@ Begin Form BasicDemo
|
||||||
|
|
||||||
Begin Menu mnuHelp
|
Begin Menu mnuHelp
|
||||||
Caption = "&Help"
|
Caption = "&Help"
|
||||||
|
Begin Menu mnuHelpContents
|
||||||
|
Caption = "&Contents"
|
||||||
|
End
|
||||||
|
Begin Menu mnuHelpSep
|
||||||
|
Caption = "-"
|
||||||
|
End
|
||||||
Begin Menu mnuAbout
|
Begin Menu mnuAbout
|
||||||
Caption = "&About..."
|
Caption = "&About..."
|
||||||
End
|
End
|
||||||
|
|
@ -172,16 +178,34 @@ END TYPE
|
||||||
' Module-level state. Grouped here up front (rather than sprinkled
|
' Module-level state. Grouped here up front (rather than sprinkled
|
||||||
' between SUB definitions) so the compiler sees a simple top-level
|
' between SUB definitions) so the compiler sees a simple top-level
|
||||||
' run-once block followed by a flat list of SUBs / FUNCTIONs.
|
' run-once block followed by a flat list of SUBs / FUNCTIONs.
|
||||||
DIM gfxWin AS LONG
|
CONST GFX_W = 340
|
||||||
DIM dynForm AS LONG
|
CONST GFX_H = 260
|
||||||
DIM dynCount AS INTEGER
|
CONST GFX_BAR_H = 4
|
||||||
DIM timerWin AS LONG
|
CONST GFX_STARS = 40
|
||||||
DIM tickCount AS LONG
|
CONST GFX_CIRCLE_X = 250
|
||||||
|
CONST GFX_CIRCLE_Y = 80
|
||||||
|
CONST GFX_CIRCLE_R = 40
|
||||||
|
CONST GFX_CIRCLE_SEG = 32
|
||||||
|
CONST GFX_BOX_X = 20
|
||||||
|
CONST GFX_BOX_Y = 20
|
||||||
|
CONST GFX_BOX_W = 80
|
||||||
|
CONST GFX_BOX_H = 50
|
||||||
|
CONST GFX_TEXT_X = 60
|
||||||
|
CONST GFX_TEXT_Y = 200
|
||||||
|
CONST GFX_TEXT_LINE = 20
|
||||||
|
CONST PATH_SHOW_LEN = 40
|
||||||
|
CONST HELP_FILE = "basdemo.hlp"
|
||||||
|
|
||||||
gfxWin = 0
|
DIM SHARED gfxOpen AS BOOLEAN
|
||||||
dynForm = 0
|
DIM SHARED dynOpen AS BOOLEAN
|
||||||
|
DIM SHARED dynCount AS INTEGER
|
||||||
|
DIM SHARED timerOpen AS BOOLEAN
|
||||||
|
DIM SHARED tickCount AS LONG
|
||||||
|
|
||||||
|
gfxOpen = False
|
||||||
|
dynOpen = False
|
||||||
dynCount = 0
|
dynCount = 0
|
||||||
timerWin = 0
|
timerOpen = False
|
||||||
tickCount = 0
|
tickCount = 0
|
||||||
|
|
||||||
Load BasicDemo
|
Load BasicDemo
|
||||||
|
|
@ -214,14 +238,27 @@ END SUB
|
||||||
|
|
||||||
SUB mnuSaveOut_Click
|
SUB mnuSaveOut_Click
|
||||||
DIM path AS STRING
|
DIM path AS STRING
|
||||||
path = basFileSave("Save OutArea", "Text Files (*.txt)|All Files (*.*)")
|
DIM f AS INTEGER
|
||||||
|
DIM opened AS BOOLEAN
|
||||||
|
path = DlgFileSave$("Save OutArea", "Text Files (*.txt)|All Files (*.*)")
|
||||||
IF path = "" THEN
|
IF path = "" THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
OPEN path FOR OUTPUT AS #1
|
opened = False
|
||||||
PRINT #1, OutArea.Text
|
ON ERROR GOTO saveOutFailed
|
||||||
CLOSE #1
|
f = FREEFILE
|
||||||
|
OPEN path FOR OUTPUT AS #f
|
||||||
|
opened = True
|
||||||
|
PRINT #f, OutArea.Text
|
||||||
|
CLOSE #f
|
||||||
LblStatus.Caption = "Saved: " + path
|
LblStatus.Caption = "Saved: " + path
|
||||||
|
EXIT SUB
|
||||||
|
|
||||||
|
saveOutFailed:
|
||||||
|
IF opened THEN
|
||||||
|
CLOSE #f
|
||||||
|
END IF
|
||||||
|
MsgBox "Could not save " + path + " (error" + STR$(ERR) + ")."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -243,10 +280,16 @@ SUB mnuRunAll_Click
|
||||||
btnFormat_Click
|
btnFormat_Click
|
||||||
btnFileIO_Click
|
btnFileIO_Click
|
||||||
btnSystem_Click
|
btnSystem_Click
|
||||||
|
btnIni_Click
|
||||||
LblStatus.Caption = "All text demos complete."
|
LblStatus.Caption = "All text demos complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
SUB mnuHelpContents_Click
|
||||||
|
HelpView HELP_FILE
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuAbout_Click
|
SUB mnuAbout_Click
|
||||||
DIM msg AS STRING
|
DIM msg AS STRING
|
||||||
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
|
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
|
||||||
|
|
@ -256,6 +299,7 @@ SUB mnuAbout_Click
|
||||||
MsgBox msg, vbOKOnly, "About"
|
MsgBox msg, vbOKOnly, "About"
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnTypes_Click
|
SUB btnTypes_Click
|
||||||
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
|
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
|
||||||
Header "Types"
|
Header "Types"
|
||||||
|
|
@ -288,6 +332,7 @@ SUB btnTypes_Click
|
||||||
LblStatus.Caption = "Types demo complete."
|
LblStatus.Caption = "Types demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnMath_Click
|
SUB btnMath_Click
|
||||||
' Math: integer + float operators, built-in functions
|
' Math: integer + float operators, built-in functions
|
||||||
Header "Math"
|
Header "Math"
|
||||||
|
|
@ -324,6 +369,7 @@ SUB btnMath_Click
|
||||||
LblStatus.Caption = "Math demo complete."
|
LblStatus.Caption = "Math demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnStrings_Click
|
SUB btnStrings_Click
|
||||||
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
|
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
|
||||||
Header "Strings"
|
Header "Strings"
|
||||||
|
|
@ -349,6 +395,7 @@ SUB btnStrings_Click
|
||||||
LblStatus.Caption = "Strings demo complete."
|
LblStatus.Caption = "Strings demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnArrays_Click
|
SUB btnArrays_Click
|
||||||
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
|
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
|
||||||
Header "Arrays"
|
Header "Arrays"
|
||||||
|
|
@ -404,6 +451,7 @@ SUB btnArrays_Click
|
||||||
LblStatus.Caption = "Arrays demo complete."
|
LblStatus.Caption = "Arrays demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnData_Click
|
SUB btnData_Click
|
||||||
' DATA / READ / RESTORE
|
' DATA / READ / RESTORE
|
||||||
Header "DATA / READ / RESTORE"
|
Header "DATA / READ / RESTORE"
|
||||||
|
|
@ -418,6 +466,8 @@ SUB btnData_Click
|
||||||
DIM b AS INTEGER
|
DIM b AS INTEGER
|
||||||
DIM i AS INTEGER
|
DIM i AS INTEGER
|
||||||
|
|
||||||
|
' Start from the first DATA item every time the demo runs.
|
||||||
|
RESTORE
|
||||||
FOR i = 1 TO 3
|
FOR i = 1 TO 3
|
||||||
READ colorName
|
READ colorName
|
||||||
READ r
|
READ r
|
||||||
|
|
@ -435,6 +485,7 @@ SUB btnData_Click
|
||||||
LblStatus.Caption = "DATA/READ demo complete."
|
LblStatus.Caption = "DATA/READ demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnFlow_Click
|
SUB btnFlow_Click
|
||||||
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
|
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
|
||||||
Header "Control Flow"
|
Header "Control Flow"
|
||||||
|
|
@ -504,6 +555,7 @@ SUB btnFlow_Click
|
||||||
RETURN
|
RETURN
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnUdt_Click
|
SUB btnUdt_Click
|
||||||
' User-Defined Type
|
' User-Defined Type
|
||||||
Header "User-Defined Type"
|
Header "User-Defined Type"
|
||||||
|
|
@ -528,6 +580,7 @@ SUB btnUdt_Click
|
||||||
LblStatus.Caption = "UDT demo complete."
|
LblStatus.Caption = "UDT demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
|
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
|
||||||
IF greeting = "" THEN
|
IF greeting = "" THEN
|
||||||
greeting = "Hello"
|
greeting = "Hello"
|
||||||
|
|
@ -546,6 +599,7 @@ SUB btnOpt_Click
|
||||||
LblStatus.Caption = "Optional params demo complete."
|
LblStatus.Caption = "Optional params demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnError_Click
|
SUB btnError_Click
|
||||||
' ON ERROR GOTO
|
' ON ERROR GOTO
|
||||||
Header "ON ERROR GOTO"
|
Header "ON ERROR GOTO"
|
||||||
|
|
@ -566,6 +620,7 @@ SUB btnError_Click
|
||||||
LblStatus.Caption = "Error handler ran successfully."
|
LblStatus.Caption = "Error handler ran successfully."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnFormat_Click
|
SUB btnFormat_Click
|
||||||
' PRINT USING / FORMAT$
|
' PRINT USING / FORMAT$
|
||||||
Header "Formatting"
|
Header "Formatting"
|
||||||
|
|
@ -578,30 +633,34 @@ SUB btnFormat_Click
|
||||||
LblStatus.Caption = "Format demo complete."
|
LblStatus.Caption = "Format demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnFileIO_Click
|
SUB btnFileIO_Click
|
||||||
' File I/O
|
' File I/O
|
||||||
Header "File I/O"
|
Header "File I/O"
|
||||||
|
|
||||||
DIM path AS STRING
|
DIM path AS STRING
|
||||||
|
DIM f AS INTEGER
|
||||||
path = App.Data + "/demo.txt"
|
path = App.Data + "/demo.txt"
|
||||||
Say "Writing: " + path
|
Say "Writing: " + path
|
||||||
|
|
||||||
OPEN path FOR OUTPUT AS #1
|
f = FREEFILE
|
||||||
PRINT #1, "Line one"
|
OPEN path FOR OUTPUT AS #f
|
||||||
PRINT #1, "Line two"
|
PRINT #f, "Line one"
|
||||||
PRINT #1, "The answer is "; 42
|
PRINT #f, "Line two"
|
||||||
CLOSE #1
|
PRINT #f, "The answer is "; 42
|
||||||
|
CLOSE #f
|
||||||
|
|
||||||
Say "LOF = " + STR$(FILELEN(path))
|
Say "FILELEN = " + STR$(FILELEN(path))
|
||||||
|
|
||||||
Say "Reading back:"
|
Say "Reading back:"
|
||||||
OPEN path FOR INPUT AS #1
|
f = FREEFILE
|
||||||
|
OPEN path FOR INPUT AS #f
|
||||||
DIM ln AS STRING
|
DIM ln AS STRING
|
||||||
DO WHILE NOT EOF(1)
|
DO WHILE NOT EOF(f)
|
||||||
LINE INPUT #1, ln
|
LINE INPUT #f, ln
|
||||||
Say " " + ln
|
Say " " + ln
|
||||||
LOOP
|
LOOP
|
||||||
CLOSE #1
|
CLOSE #f
|
||||||
|
|
||||||
KILL path
|
KILL path
|
||||||
Say "Deleted."
|
Say "Deleted."
|
||||||
|
|
@ -609,6 +668,7 @@ SUB btnFileIO_Click
|
||||||
LblStatus.Caption = "File I/O demo complete."
|
LblStatus.Caption = "File I/O demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnSystem_Click
|
SUB btnSystem_Click
|
||||||
' System: App object, environment, current directory
|
' System: App object, environment, current directory
|
||||||
Header "System / App"
|
Header "System / App"
|
||||||
|
|
@ -619,11 +679,17 @@ SUB btnSystem_Click
|
||||||
Say "CurDir = " + CurDir()
|
Say "CurDir = " + CurDir()
|
||||||
Say "Date = " + Date$
|
Say "Date = " + Date$
|
||||||
Say "Time = " + Time$
|
Say "Time = " + Time$
|
||||||
Say "PATH env = " + LEFT$(Environ$("PATH"), 40) + "..."
|
DIM envPath AS STRING
|
||||||
|
envPath = Environ$("PATH")
|
||||||
|
IF LEN(envPath) > PATH_SHOW_LEN THEN
|
||||||
|
envPath = LEFT$(envPath, PATH_SHOW_LEN) + "..."
|
||||||
|
END IF
|
||||||
|
Say "PATH env = " + envPath
|
||||||
|
|
||||||
LblStatus.Caption = "System demo complete."
|
LblStatus.Caption = "System demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnIni_Click
|
SUB btnIni_Click
|
||||||
' INI read/write
|
' INI read/write
|
||||||
Header "INI Read/Write"
|
Header "INI Read/Write"
|
||||||
|
|
@ -647,6 +713,7 @@ SUB btnIni_Click
|
||||||
LblStatus.Caption = "INI demo complete."
|
LblStatus.Caption = "INI demo complete."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB btnDialogs_Click
|
SUB btnDialogs_Click
|
||||||
Header "Dialogs"
|
Header "Dialogs"
|
||||||
|
|
||||||
|
|
@ -659,11 +726,11 @@ SUB btnDialogs_Click
|
||||||
END IF
|
END IF
|
||||||
|
|
||||||
DIM text AS STRING
|
DIM text AS STRING
|
||||||
text = basInputBox2("Input", "What is your name?", "Anonymous")
|
text = DlgInputBox$("Input", "What is your name?", "Anonymous")
|
||||||
Say "InputBox returned: " + text
|
Say "InputBox returned: " + text
|
||||||
|
|
||||||
DIM choice AS INTEGER
|
DIM choice AS INTEGER
|
||||||
choice = basChoiceDialog("Favorite", "Pick a color:", "Red|Green|Blue|Yellow", 1)
|
choice = DlgChoice("Favorite", "Pick a color:", "Red|Green|Blue|Yellow", 1)
|
||||||
IF choice >= 0 THEN
|
IF choice >= 0 THEN
|
||||||
Say "Choice index: " + STR$(choice)
|
Say "Choice index: " + STR$(choice)
|
||||||
ELSE
|
ELSE
|
||||||
|
|
@ -671,7 +738,7 @@ SUB btnDialogs_Click
|
||||||
END IF
|
END IF
|
||||||
|
|
||||||
DIM n AS INTEGER
|
DIM n AS INTEGER
|
||||||
n = basIntInput("Number", "Pick a number (1-100):", 42, 1, 100)
|
n = DlgIntInput("Number", "Pick a number (1-100):", 42, 1, 100)
|
||||||
Say "IntInput: " + STR$(n)
|
Say "IntInput: " + STR$(n)
|
||||||
|
|
||||||
LblStatus.Caption = "Dialog demo complete."
|
LblStatus.Caption = "Dialog demo complete."
|
||||||
|
|
@ -682,21 +749,22 @@ SUB btnClear_Click
|
||||||
mnuClear_Click
|
mnuClear_Click
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuGraphics_Click
|
SUB mnuGraphics_Click
|
||||||
IF gfxWin <> 0 THEN
|
IF gfxOpen THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
|
|
||||||
DIM frm AS LONG
|
DIM frm AS LONG
|
||||||
SET frm = CreateForm("GraphicsForm", 380, 360)
|
SET frm = CreateForm("GraphicsForm", 380, 360)
|
||||||
GraphicsForm.Caption = "Graphics Demo"
|
frm.Caption = "Graphics Demo"
|
||||||
gfxWin = frm
|
gfxOpen = True
|
||||||
|
|
||||||
DIM cv AS LONG
|
DIM cv AS LONG
|
||||||
SET cv = CreateControl(frm, "PictureBox", "GfxCanvas")
|
SET cv = CreateControl(frm, "PictureBox", "GfxCanvas")
|
||||||
GfxCanvas.Width = 340
|
cv.Width = GFX_W
|
||||||
GfxCanvas.Height = 260
|
cv.Height = GFX_H
|
||||||
GfxCanvas.Weight = 1
|
cv.Weight = 1
|
||||||
|
|
||||||
DIM btnRow AS LONG
|
DIM btnRow AS LONG
|
||||||
SET btnRow = CreateControl(frm, "HBox", "GfxRow")
|
SET btnRow = CreateControl(frm, "HBox", "GfxRow")
|
||||||
|
|
@ -718,17 +786,12 @@ END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB GfxDrawAll
|
SUB GfxDrawAll
|
||||||
DIM w AS LONG
|
|
||||||
DIM h AS LONG
|
|
||||||
w = 340
|
|
||||||
h = 260
|
|
||||||
|
|
||||||
' Gradient background bars
|
' Gradient background bars
|
||||||
DIM y AS INTEGER
|
DIM y AS INTEGER
|
||||||
DIM shade AS LONG
|
DIM shade AS LONG
|
||||||
FOR y = 0 TO h - 1 STEP 4
|
FOR y = 0 TO GFX_H - 1 STEP GFX_BAR_H
|
||||||
shade = (y * 255) \ h
|
shade = (y * 255) \ GFX_H
|
||||||
GfxCanvas.FillRect 0, y, w, 4, RGB(shade, shade \ 2, 128)
|
GfxCanvas.FillRect 0, y, GFX_W, GFX_BAR_H, RGB(shade, shade \ 2, 128)
|
||||||
NEXT y
|
NEXT y
|
||||||
|
|
||||||
' Star field
|
' Star field
|
||||||
|
|
@ -736,41 +799,40 @@ SUB GfxDrawAll
|
||||||
DIM s AS INTEGER
|
DIM s AS INTEGER
|
||||||
DIM sx AS INTEGER
|
DIM sx AS INTEGER
|
||||||
DIM sy AS INTEGER
|
DIM sy AS INTEGER
|
||||||
FOR s = 1 TO 40
|
FOR s = 1 TO GFX_STARS
|
||||||
sx = INT(RND * w)
|
sx = INT(RND * GFX_W)
|
||||||
sy = INT(RND * h)
|
sy = INT(RND * GFX_H)
|
||||||
GfxCanvas.SetPixel sx, sy, RGB(255, 255, 255)
|
GfxCanvas.SetPixel sx, sy, RGB(255, 255, 255)
|
||||||
NEXT s
|
NEXT s
|
||||||
|
|
||||||
' Rectangle + outline
|
' Rectangle + outline
|
||||||
GfxCanvas.FillRect 20, 20, 80, 50, RGB(240, 240, 0)
|
GfxCanvas.FillRect GFX_BOX_X, GFX_BOX_Y, GFX_BOX_W, GFX_BOX_H, RGB(240, 240, 0)
|
||||||
GfxCanvas.DrawRect 20, 20, 80, 50, RGB(0, 0, 0)
|
GfxCanvas.DrawRect GFX_BOX_X, GFX_BOX_Y, GFX_BOX_W, GFX_BOX_H, RGB(0, 0, 0)
|
||||||
|
|
||||||
' Circle approximated by line segments
|
' Circle approximated by line segments; the last segment ends back
|
||||||
DIM cx AS INTEGER
|
' at the start point, so the outline is closed.
|
||||||
DIM cy AS INTEGER
|
DIM seg AS INTEGER
|
||||||
DIM r AS INTEGER
|
|
||||||
cx = 250
|
|
||||||
cy = 80
|
|
||||||
r = 40
|
|
||||||
DIM a AS DOUBLE
|
DIM a AS DOUBLE
|
||||||
|
DIM twoPi AS DOUBLE
|
||||||
DIM px AS INTEGER
|
DIM px AS INTEGER
|
||||||
DIM py AS INTEGER
|
DIM py AS INTEGER
|
||||||
DIM qx AS INTEGER
|
DIM qx AS INTEGER
|
||||||
DIM qy AS INTEGER
|
DIM qy AS INTEGER
|
||||||
px = cx + r
|
twoPi = 8 * ATN(1)
|
||||||
py = cy
|
px = GFX_CIRCLE_X + GFX_CIRCLE_R
|
||||||
FOR a = 0 TO 6.3 STEP 0.2
|
py = GFX_CIRCLE_Y
|
||||||
qx = cx + INT(r * COS(a))
|
FOR seg = 1 TO GFX_CIRCLE_SEG
|
||||||
qy = cy + INT(r * SIN(a))
|
a = seg * twoPi / GFX_CIRCLE_SEG
|
||||||
|
qx = GFX_CIRCLE_X + CINT(GFX_CIRCLE_R * COS(a))
|
||||||
|
qy = GFX_CIRCLE_Y + CINT(GFX_CIRCLE_R * SIN(a))
|
||||||
GfxCanvas.DrawLine px, py, qx, qy, RGB(255, 128, 0)
|
GfxCanvas.DrawLine px, py, qx, qy, RGB(255, 128, 0)
|
||||||
px = qx
|
px = qx
|
||||||
py = qy
|
py = qy
|
||||||
NEXT a
|
NEXT seg
|
||||||
|
|
||||||
' Text
|
' Text
|
||||||
GfxCanvas.DrawText 60, 200, "Canvas + math + colors", RGB(255, 255, 255)
|
GfxCanvas.DrawText GFX_TEXT_X, GFX_TEXT_Y, "Canvas + math + colors", RGB(255, 255, 255)
|
||||||
GfxCanvas.DrawText 60, 220, "DVX BASIC graphics", RGB(255, 255, 0)
|
GfxCanvas.DrawText GFX_TEXT_X, GFX_TEXT_Y + GFX_TEXT_LINE, "DVX BASIC graphics", RGB(255, 255, 0)
|
||||||
|
|
||||||
GfxCanvas.Refresh
|
GfxCanvas.Refresh
|
||||||
END SUB
|
END SUB
|
||||||
|
|
@ -790,26 +852,27 @@ END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB GraphicsForm_Unload
|
SUB GraphicsForm_Unload
|
||||||
gfxWin = 0
|
gfxOpen = False
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuDynamic_Click
|
SUB mnuDynamic_Click
|
||||||
IF dynForm <> 0 THEN
|
IF dynOpen THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
|
|
||||||
DIM frm AS LONG
|
DIM frm AS LONG
|
||||||
SET frm = CreateForm("DynForm", 320, 200)
|
SET frm = CreateForm("DynForm", 320, 200)
|
||||||
DynForm.Caption = "Dynamic Form (built in code)"
|
frm.Caption = "Dynamic Form (built in code)"
|
||||||
dynForm = frm
|
dynOpen = True
|
||||||
|
|
||||||
DIM lbl AS LONG
|
DIM lbl AS LONG
|
||||||
SET lbl = CreateControl(frm, "Label", "DynLabel")
|
SET lbl = CreateControl(frm, "Label", "DynLabel")
|
||||||
DynLabel.Caption = "This form was created 100% in code."
|
lbl.Caption = "This form was created 100% in code."
|
||||||
|
|
||||||
DIM lbl2 AS LONG
|
DIM lbl2 AS LONG
|
||||||
SET lbl2 = CreateControl(frm, "Label", "CountLabel")
|
SET lbl2 = CreateControl(frm, "Label", "CountLabel")
|
||||||
CountLabel.Caption = "Counter: 0"
|
lbl2.Caption = "Counter: 0"
|
||||||
|
|
||||||
DIM btns AS LONG
|
DIM btns AS LONG
|
||||||
SET btns = CreateControl(frm, "HBox", "DynBtns")
|
SET btns = CreateControl(frm, "HBox", "DynBtns")
|
||||||
|
|
@ -840,23 +903,24 @@ END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB DynForm_Unload
|
SUB DynForm_Unload
|
||||||
dynForm = 0
|
dynOpen = False
|
||||||
dynCount = 0
|
dynCount = 0
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuTimer_Click
|
SUB mnuTimer_Click
|
||||||
IF timerWin <> 0 THEN
|
IF timerOpen THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
|
|
||||||
DIM frm AS LONG
|
DIM frm AS LONG
|
||||||
SET frm = CreateForm("TimerForm", 260, 140)
|
SET frm = CreateForm("TimerForm", 260, 140)
|
||||||
TimerForm.Caption = "Timer Demo"
|
frm.Caption = "Timer Demo"
|
||||||
timerWin = frm
|
timerOpen = True
|
||||||
|
|
||||||
DIM lbl AS LONG
|
DIM lbl AS LONG
|
||||||
SET lbl = CreateControl(frm, "Label", "TickLabel")
|
SET lbl = CreateControl(frm, "Label", "TickLabel")
|
||||||
TickLabel.Caption = "Ticks: 0"
|
lbl.Caption = "Ticks: 0"
|
||||||
|
|
||||||
DIM t AS LONG
|
DIM t AS LONG
|
||||||
SET t = CreateControl(frm, "Timer", "Ticker")
|
SET t = CreateControl(frm, "Timer", "Ticker")
|
||||||
|
|
@ -874,6 +938,6 @@ END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB TimerForm_Unload
|
SUB TimerForm_Unload
|
||||||
timerWin = 0
|
timerOpen = False
|
||||||
tickCount = 0
|
tickCount = 0
|
||||||
END SUB
|
END SUB
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,11 @@
|
||||||
// would starve the shell and all other apps.
|
// would starve the shell and all other apps.
|
||||||
|
|
||||||
#include "dvxApp.h"
|
#include "dvxApp.h"
|
||||||
|
#include "dvxDlg.h"
|
||||||
#include "dvxWgt.h"
|
#include "dvxWgt.h"
|
||||||
#include "dvxDraw.h"
|
#include "dvxDraw.h"
|
||||||
#include "dvxVideo.h"
|
#include "dvxVideo.h"
|
||||||
|
#include "dvxWm.h"
|
||||||
#include "shellApp.h"
|
#include "shellApp.h"
|
||||||
#include "taskSwch.h"
|
#include "taskSwch.h"
|
||||||
|
|
||||||
|
|
@ -59,9 +61,16 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define CLOCK_WIN_W 200
|
#define CLOCK_WIN_W 200
|
||||||
#define CLOCK_WIN_H 100
|
#define CLOCK_FACE_H 100 // window height without the menu bar
|
||||||
|
#define CLOCK_WIN_H (CLOCK_FACE_H + CHROME_MENU_HEIGHT)
|
||||||
#define CLOCK_MARGIN 40
|
#define CLOCK_MARGIN 40
|
||||||
#define CLOCK_DATE_GAP 8
|
#define CLOCK_DATE_GAP 8
|
||||||
|
#define CLOCK_CASCADE 16 // offset between successive clock windows
|
||||||
|
#define CLOCK_TITLE "Clock"
|
||||||
|
|
||||||
|
// Menu command IDs
|
||||||
|
#define CMD_HELP_CONTENTS 100
|
||||||
|
#define CMD_HELP_ABOUT 101
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool quit;
|
bool quit;
|
||||||
|
|
@ -78,11 +87,13 @@ static ClockStateT sState;
|
||||||
// Prototypes
|
// Prototypes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
int32_t appMain(DxeAppContextT *ctx);
|
void appShutdown(void);
|
||||||
void appShutdown(void);
|
static int32_t countClockWindows(const AppContextT *ac);
|
||||||
static void onClose(WindowT *win);
|
static void onClose(WindowT *win);
|
||||||
static void onPaint(WindowT *win, RectT *dirty);
|
static void onMenu(WindowT *win, int32_t menuId);
|
||||||
static void updateTime(void);
|
static void onPaint(WindowT *win, RectT *dirty);
|
||||||
|
static void updateTime(void);
|
||||||
|
int32_t appMain(DxeAppContextT *ctx);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// App descriptor
|
// App descriptor
|
||||||
|
|
@ -91,13 +102,13 @@ static void updateTime(void);
|
||||||
// hasMainLoop = true tells the shell to create a dedicated task for appMain.
|
// hasMainLoop = true tells the shell to create a dedicated task for appMain.
|
||||||
// TS_PRIORITY_LOW because clock updates are cosmetic and should never
|
// TS_PRIORITY_LOW because clock updates are cosmetic and should never
|
||||||
// preempt interactive apps or the shell's event processing.
|
// preempt interactive apps or the shell's event processing.
|
||||||
AppDescriptorT appDescriptor = {
|
SHELL_APP_DESCRIPTOR(
|
||||||
.name = "Clock",
|
.name = "Clock",
|
||||||
.hasMainLoop = true,
|
.hasMainLoop = true,
|
||||||
.multiInstance = true,
|
.multiInstance = true,
|
||||||
.stackSize = SHELL_STACK_DEFAULT,
|
.stackSize = SHELL_STACK_DEFAULT,
|
||||||
.priority = TS_PRIORITY_LOW
|
.priority = TS_PRIORITY_LOW
|
||||||
};
|
);
|
||||||
|
|
||||||
|
|
||||||
// The shell calls appShutdown (if exported) when force-killing an app via
|
// The shell calls appShutdown (if exported) when force-killing an app via
|
||||||
|
|
@ -109,6 +120,21 @@ void appShutdown(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Clock windows already open (from other instances), used to cascade a new
|
||||||
|
// instance instead of opening it exactly on top of the previous one.
|
||||||
|
static int32_t countClockWindows(const AppContextT *ac) {
|
||||||
|
int32_t n = 0;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < ac->stack.count; i++) {
|
||||||
|
if (strcmp(ac->stack.windows[i]->title, CLOCK_TITLE) == 0) {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Setting quit = true tells the main loop (running in a separate task) to
|
// Setting quit = true tells the main loop (running in a separate task) to
|
||||||
// exit. The main loop then destroys the window and returns from appMain,
|
// exit. The main loop then destroys the window and returns from appMain,
|
||||||
// which causes the shell to reap this task and unload the DXE.
|
// which causes the shell to reap this task and unload the DXE.
|
||||||
|
|
@ -118,14 +144,34 @@ static void onClose(WindowT *win) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Menu callbacks run in the shell's task (like onPaint), not in the
|
||||||
|
// clock's main-loop task.
|
||||||
|
static void onMenu(WindowT *win, int32_t menuId) {
|
||||||
|
(void)win;
|
||||||
|
|
||||||
|
switch (menuId) {
|
||||||
|
case CMD_HELP_CONTENTS:
|
||||||
|
shellOpenHelp(sCtx->shellCtx, sCtx->helpFile);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_HELP_ABOUT:
|
||||||
|
dvxInfoBox(sCtx->shellCtx, "About Clock", "Clock\n\nShows the current time and date.\n\nCopyright 2026 Scott Duensing");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// onPaint demonstrates the raw paint callback approach (no widget tree).
|
// onPaint demonstrates the raw paint callback approach (no widget tree).
|
||||||
// Instead of using wgtInitWindow + widgets, this app renders directly into
|
// Instead of using wgtInitWindow + widgets, this app renders directly into
|
||||||
// the window's content buffer. This is the lower-level alternative to the
|
// the window's content buffer. This is the lower-level alternative to the
|
||||||
// widget system -- useful for custom rendering like this centered clock display.
|
// widget system -- useful for custom rendering like this centered clock display.
|
||||||
//
|
//
|
||||||
// We create a temporary DisplayT struct pointing at the window's content buffer
|
// dvxContentDisplay gives a DisplayT aimed at the window's content buffer,
|
||||||
// so the drawing primitives (rectFill, drawText) can operate on the window
|
// so the standard drawing primitives (rectFill, drawText) draw into the
|
||||||
// content directly rather than the backbuffer.
|
// window content without touching the global display state.
|
||||||
static void onPaint(WindowT *win, RectT *dirty) {
|
static void onPaint(WindowT *win, RectT *dirty) {
|
||||||
(void)dirty;
|
(void)dirty;
|
||||||
|
|
||||||
|
|
@ -133,19 +179,9 @@ static void onPaint(WindowT *win, RectT *dirty) {
|
||||||
const BlitOpsT *ops = dvxGetBlitOps(ac);
|
const BlitOpsT *ops = dvxGetBlitOps(ac);
|
||||||
const BitmapFontT *font = dvxGetFont(ac);
|
const BitmapFontT *font = dvxGetFont(ac);
|
||||||
const ColorSchemeT *colors = dvxGetColors(ac);
|
const ColorSchemeT *colors = dvxGetColors(ac);
|
||||||
|
DisplayT cd;
|
||||||
|
|
||||||
// Shallow copy of the display, redirected to the window's content buffer.
|
dvxContentDisplay(ac, win, &cd);
|
||||||
// This lets us reuse the standard drawing API without modifying the
|
|
||||||
// global display state.
|
|
||||||
DisplayT cd = *dvxGetDisplay(ac);
|
|
||||||
cd.backBuf = win->contentBuf;
|
|
||||||
cd.width = win->contentW;
|
|
||||||
cd.height = win->contentH;
|
|
||||||
cd.pitch = win->contentPitch;
|
|
||||||
cd.clipX = 0;
|
|
||||||
cd.clipY = 0;
|
|
||||||
cd.clipW = win->contentW;
|
|
||||||
cd.clipH = win->contentH;
|
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg);
|
rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg);
|
||||||
|
|
@ -197,22 +233,44 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
memset(&sState, 0, sizeof(sState));
|
memset(&sState, 0, sizeof(sState));
|
||||||
updateTime();
|
updateTime();
|
||||||
|
|
||||||
// Position in the upper-right corner, out of the way of other windows
|
// Position in the upper-right corner, out of the way of other windows,
|
||||||
int32_t winW = CLOCK_WIN_W;
|
// cascading down and left for each clock already open (back to the
|
||||||
int32_t winH = CLOCK_WIN_H;
|
// corner once the cascade would leave the screen).
|
||||||
int32_t winX = ac->display.width - winW - CLOCK_MARGIN;
|
int32_t winW = CLOCK_WIN_W;
|
||||||
int32_t winY = CLOCK_MARGIN;
|
int32_t winH = CLOCK_WIN_H;
|
||||||
|
int32_t offset = countClockWindows(ac) * CLOCK_CASCADE;
|
||||||
|
int32_t winX = ac->display.width - winW - CLOCK_MARGIN - offset;
|
||||||
|
int32_t winY = CLOCK_MARGIN + offset;
|
||||||
|
|
||||||
|
if (winX < 0 || winY + winH > ac->display.height) {
|
||||||
|
winX = ac->display.width - winW - CLOCK_MARGIN;
|
||||||
|
winY = CLOCK_MARGIN;
|
||||||
|
}
|
||||||
|
|
||||||
// resizable=false: clock has a fixed size, no resize handle
|
// resizable=false: clock has a fixed size, no resize handle
|
||||||
sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false);
|
sWin = dvxCreateWindow(ac, CLOCK_TITLE, winX, winY, winW, winH, false);
|
||||||
|
|
||||||
if (!sWin) {
|
if (!sWin) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
sWin->onClose = onClose;
|
sWin->onClose = onClose;
|
||||||
|
sWin->onMenu = onMenu;
|
||||||
sWin->onPaint = onPaint;
|
sWin->onPaint = onPaint;
|
||||||
|
|
||||||
|
MenuBarT *menuBar = wmAddMenuBar(sWin);
|
||||||
|
MenuT *helpMenu = menuBar ? wmAddMenu(menuBar, "&Help") : NULL;
|
||||||
|
|
||||||
|
if (helpMenu) {
|
||||||
|
wmAddMenuItem(helpMenu, "&Contents\tF1", CMD_HELP_CONTENTS);
|
||||||
|
wmAddMenuSeparator(helpMenu);
|
||||||
|
wmAddMenuItem(helpMenu, "&About Clock...", CMD_HELP_ABOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A raw-paint window must resize its content buffer itself after the
|
||||||
|
// menu bar shrinks the content area.
|
||||||
|
wmReallocContentBuf(sWin, &ac->display);
|
||||||
|
|
||||||
// Main loop: check if the second has changed, repaint if so, then yield.
|
// Main loop: check if the second has changed, repaint if so, then yield.
|
||||||
// First paint happens automatically on the next dvxUpdate frame.
|
// First paint happens automatically on the next dvxUpdate frame.
|
||||||
// tsYield() transfers control back to the shell's task scheduler.
|
// tsYield() transfers control back to the shell's task scheduler.
|
||||||
|
|
@ -231,11 +289,7 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
tsYield();
|
tsYield();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
dvxDestroyWindow(ac, sWin);
|
||||||
if (sWin) {
|
sWin = NULL;
|
||||||
dvxDestroyWindow(ac, sWin);
|
|
||||||
sWin = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
src/apps/kpunch/clock/clock.dhs
Normal file
61
src/apps/kpunch/clock/clock.dhs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
# Clock help. Compiled to clock.hlp next to clock.app; F1 and
|
||||||
|
# Help > Contents in a Clock window open it.
|
||||||
|
|
||||||
|
.topic clock.overview
|
||||||
|
.title Clock
|
||||||
|
.toc 0 Clock
|
||||||
|
.default
|
||||||
|
.index Clock
|
||||||
|
.index Time
|
||||||
|
.index Date
|
||||||
|
|
||||||
|
.h1 Clock
|
||||||
|
|
||||||
|
The Clock shows the current time and today's date in a small window. It updates every second.
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item The time is shown with hours, minutes and seconds, in 12-hour style with AM or PM, for example 3:07:42 PM.
|
||||||
|
.item The date is shown as year, month and day, for example 2026-09-27.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
The Clock takes the time and date from your computer. If they are wrong, set them with the DOS TIME and DATE commands before you start DVX.
|
||||||
|
|
||||||
|
.h2 More Than One Clock
|
||||||
|
|
||||||
|
You can open the Clock more than once. The first Clock opens near the top right corner of the screen, and each new one opens a little lower and further left, so they do not cover each other exactly.
|
||||||
|
|
||||||
|
.h2 The Clock Window
|
||||||
|
|
||||||
|
The Clock window has a fixed size; it cannot be made bigger or smaller, but you can move it, minimize it or close it like any other window. Closing the window ends that Clock.
|
||||||
|
|
||||||
|
.h2 Menus
|
||||||
|
|
||||||
|
.table
|
||||||
|
Menu Command What it does
|
||||||
|
---- ------- ------------
|
||||||
|
Help Contents Open this help.
|
||||||
|
Help About Clock Show the program name and copyright.
|
||||||
|
.endtable
|
||||||
329
src/apps/kpunch/cpanel/cpNet.c
Normal file
329
src/apps/kpunch/cpanel/cpNet.c
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// cpNet.c -- Control Panel's Network page (see cpNet.h).
|
||||||
|
//
|
||||||
|
// Settings: DHCP or a fixed address, name servers, the computer's name
|
||||||
|
// and the packet driver's interrupt. Status... opens the Network Status
|
||||||
|
// window: the state in plain words, the addresses in use, the network
|
||||||
|
// card and the traffic counters, refreshed every second while it is open
|
||||||
|
// (it may stay open after the Control Panel closes).
|
||||||
|
//
|
||||||
|
// The networking library is looked up by name when the page is built,
|
||||||
|
// so the Control Panel still works where it is not installed (the page
|
||||||
|
// then says so). Like every other page, nothing is written before OK.
|
||||||
|
|
||||||
|
#include "cpNet.h"
|
||||||
|
#include "dvxnet/dvxNet.h"
|
||||||
|
#include "dvxDlg.h"
|
||||||
|
#include "dvxPlat.h"
|
||||||
|
#include "box/box.h"
|
||||||
|
#include "button/button.h"
|
||||||
|
#include "checkbox/checkbox.h"
|
||||||
|
#include "dropdown/dropdown.h"
|
||||||
|
#include "label/label.h"
|
||||||
|
#include "textInput/textInpt.h"
|
||||||
|
#include "timer/timer.h"
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define CN_LABEL_W 110
|
||||||
|
#define CN_FIELD_CHARS 18
|
||||||
|
#define CN_GAP 4
|
||||||
|
#define CN_BTN_W 90
|
||||||
|
#define CN_REFRESH_MS 1000
|
||||||
|
#define CN_STATUS_W 360
|
||||||
|
#define CN_STATUS_H 150
|
||||||
|
#define CN_VECTOR_COUNT (NET_VECTOR_LAST - NET_VECTOR_FIRST + 2) // "Find it" + each vector
|
||||||
|
#define CN_VECTOR_TEXT 24
|
||||||
|
#define CN_LINE_MAX 96
|
||||||
|
|
||||||
|
// The library's functions (NULL when it is not installed)
|
||||||
|
typedef struct {
|
||||||
|
__typeof__(netConfigFromPrefs) *fromPrefs;
|
||||||
|
__typeof__(netConfigToPrefs) *toPrefs;
|
||||||
|
__typeof__(netConfigProblem) *problem;
|
||||||
|
__typeof__(netGetStatus) *status;
|
||||||
|
__typeof__(netRestart) *restart;
|
||||||
|
} NetFnsT;
|
||||||
|
|
||||||
|
static NetFnsT sNet;
|
||||||
|
static NetConfigT sOriginal;
|
||||||
|
static AppContextT *sAc = NULL;
|
||||||
|
static WindowT *sStatusWin = NULL;
|
||||||
|
static WidgetT *sDhcp = NULL;
|
||||||
|
static WidgetT *sAddress = NULL;
|
||||||
|
static WidgetT *sNetmask = NULL;
|
||||||
|
static WidgetT *sGateway = NULL;
|
||||||
|
static WidgetT *sDns[NET_DNS_SERVERS];
|
||||||
|
static WidgetT *sHostName = NULL;
|
||||||
|
static WidgetT *sVector = NULL;
|
||||||
|
static WidgetT *sState = NULL;
|
||||||
|
static WidgetT *sAddrLine = NULL;
|
||||||
|
static WidgetT *sDnsLine = NULL;
|
||||||
|
static WidgetT *sCardLine = NULL;
|
||||||
|
static WidgetT *sTraffic = NULL;
|
||||||
|
static char sVectorText[CN_VECTOR_COUNT][CN_VECTOR_TEXT];
|
||||||
|
static const char *sVectorLabels[CN_VECTOR_COUNT];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Prototypes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static WidgetT *addField(WidgetT *parent, const char *label, int32_t maxLen, const char *text);
|
||||||
|
void cpNetApply(void);
|
||||||
|
void cpNetBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
|
||||||
|
const char *cpNetProblem(void);
|
||||||
|
void cpNetStore(PrefsHandleT *prefs);
|
||||||
|
static void onDhcp(WidgetT *w);
|
||||||
|
static void onRestart(WidgetT *w);
|
||||||
|
static void onStatus(WidgetT *w);
|
||||||
|
static void onStatusClose(WindowT *win);
|
||||||
|
static void onTick(WidgetT *w);
|
||||||
|
static void readPage(NetConfigT *cfg);
|
||||||
|
static void refreshStatus(void);
|
||||||
|
static void setDhcpEnabled(void);
|
||||||
|
|
||||||
|
|
||||||
|
// "Label: [text input]" on one row.
|
||||||
|
static WidgetT *addField(WidgetT *parent, const char *label, int32_t maxLen, const char *text) {
|
||||||
|
WidgetT *row = wgtHBox(parent);
|
||||||
|
WidgetT *lbl = wgtLabel(row, label);
|
||||||
|
WidgetT *in = wgtTextInput(row, maxLen);
|
||||||
|
|
||||||
|
row->spacing = wgtPixels(CN_GAP);
|
||||||
|
lbl->prefW = wgtPixels(CN_LABEL_W);
|
||||||
|
in->prefW = wgtChars(CN_FIELD_CHARS);
|
||||||
|
wgtSetText(in, text);
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cpNetApply(void) {
|
||||||
|
NetConfigT cfg;
|
||||||
|
|
||||||
|
if (!sNet.restart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readPage(&cfg);
|
||||||
|
|
||||||
|
if (memcmp(&cfg, &sOriginal, sizeof(cfg)) != 0) {
|
||||||
|
sNet.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cpNetBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
|
||||||
|
WidgetT *row;
|
||||||
|
WidgetT *btn;
|
||||||
|
char label[CN_LINE_MAX];
|
||||||
|
|
||||||
|
sAc = ac;
|
||||||
|
sNet.fromPrefs = (__typeof__(sNet.fromPrefs))dlsym(RTLD_DEFAULT, DVX_SYM("netConfigFromPrefs"));
|
||||||
|
sNet.toPrefs = (__typeof__(sNet.toPrefs))dlsym(RTLD_DEFAULT, DVX_SYM("netConfigToPrefs"));
|
||||||
|
sNet.problem = (__typeof__(sNet.problem))dlsym(RTLD_DEFAULT, DVX_SYM("netConfigProblem"));
|
||||||
|
sNet.status = (__typeof__(sNet.status))dlsym(RTLD_DEFAULT, DVX_SYM("netGetStatus"));
|
||||||
|
sNet.restart = (__typeof__(sNet.restart))dlsym(RTLD_DEFAULT, DVX_SYM("netRestart"));
|
||||||
|
|
||||||
|
if (!sNet.fromPrefs || !sNet.toPrefs || !sNet.problem || !sNet.status || !sNet.restart) {
|
||||||
|
memset(&sNet, 0, sizeof(sNet));
|
||||||
|
wgtLabel(page, "Networking is not installed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sNet.fromPrefs(prefs, &sOriginal);
|
||||||
|
sDhcp = wgtCheckbox(page, "Get the address automatically (DHCP)");
|
||||||
|
wgtCheckboxSetChecked(sDhcp, sOriginal.dhcp);
|
||||||
|
sDhcp->onChange = onDhcp;
|
||||||
|
sAddress = addField(page, "Address:", NET_ADDR_MAX - 1, sOriginal.address);
|
||||||
|
sNetmask = addField(page, "Subnet mask:", NET_ADDR_MAX - 1, sOriginal.netmask);
|
||||||
|
sGateway = addField(page, "Gateway:", NET_ADDR_MAX - 1, sOriginal.gateway);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < NET_DNS_SERVERS; i++) {
|
||||||
|
snprintf(label, sizeof(label), "Name server %d:", (int)(i + 1));
|
||||||
|
sDns[i] = addField(page, label, NET_ADDR_MAX - 1, sOriginal.dns[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sHostName = addField(page, "Computer name:", NET_HOSTNAME_MAX - 1, sOriginal.hostName);
|
||||||
|
|
||||||
|
// Packet driver interrupt: "Find it" or one of 60h..80h
|
||||||
|
row = wgtHBox(page);
|
||||||
|
row->spacing = wgtPixels(CN_GAP);
|
||||||
|
wgtLabel(row, "Packet driver:")->prefW = wgtPixels(CN_LABEL_W);
|
||||||
|
snprintf(sVectorText[0], CN_VECTOR_TEXT, "%s", "Find it");
|
||||||
|
sVectorLabels[0] = sVectorText[0];
|
||||||
|
|
||||||
|
for (int32_t v = NET_VECTOR_FIRST; v <= NET_VECTOR_LAST; v++) {
|
||||||
|
int32_t i = v - NET_VECTOR_FIRST + 1;
|
||||||
|
|
||||||
|
snprintf(sVectorText[i], CN_VECTOR_TEXT, "Interrupt %02Xh", (unsigned)v);
|
||||||
|
sVectorLabels[i] = sVectorText[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
sVector = wgtDropdown(row);
|
||||||
|
wgtDropdownSetItems(sVector, sVectorLabels, CN_VECTOR_COUNT);
|
||||||
|
wgtDropdownSetSelected(sVector, (sOriginal.vector == NET_VECTOR_AUTO) ? 0 : sOriginal.vector - NET_VECTOR_FIRST + 1);
|
||||||
|
setDhcpEnabled();
|
||||||
|
|
||||||
|
row = wgtHBox(page);
|
||||||
|
row->spacing = wgtPixels(CN_GAP);
|
||||||
|
btn = wgtButton(row, "Status...");
|
||||||
|
btn->onClick = onStatus;
|
||||||
|
btn->prefW = wgtPixels(CN_BTN_W);
|
||||||
|
btn = wgtButton(row, "Restart");
|
||||||
|
btn->onClick = onRestart;
|
||||||
|
btn->prefW = wgtPixels(CN_BTN_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const char *cpNetProblem(void) {
|
||||||
|
NetConfigT cfg;
|
||||||
|
|
||||||
|
if (!sNet.problem) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
readPage(&cfg);
|
||||||
|
return sNet.problem(&cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cpNetStore(PrefsHandleT *prefs) {
|
||||||
|
NetConfigT cfg;
|
||||||
|
|
||||||
|
if (!sNet.toPrefs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readPage(&cfg);
|
||||||
|
sNet.toPrefs(prefs, &cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onDhcp(WidgetT *w) {
|
||||||
|
(void)w;
|
||||||
|
setDhcpEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Restart the network with the saved settings.
|
||||||
|
static void onRestart(WidgetT *w) {
|
||||||
|
(void)w;
|
||||||
|
sNet.restart();
|
||||||
|
|
||||||
|
if (sStatusWin) {
|
||||||
|
refreshStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Open (or bring back) the Network Status window.
|
||||||
|
static void onStatus(WidgetT *w) {
|
||||||
|
WidgetT *root;
|
||||||
|
WidgetT *timer;
|
||||||
|
|
||||||
|
(void)w;
|
||||||
|
|
||||||
|
if (sStatusWin) {
|
||||||
|
dvxRaiseWindow(sAc, sStatusWin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sStatusWin = dvxCreateWindowCentered(sAc, "Network Status", CN_STATUS_W, CN_STATUS_H, false);
|
||||||
|
|
||||||
|
if (!sStatusWin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sStatusWin->onClose = onStatusClose;
|
||||||
|
root = wgtInitWindow(sAc, sStatusWin);
|
||||||
|
sState = wgtLabel(root, "");
|
||||||
|
sAddrLine = wgtLabel(root, "");
|
||||||
|
sDnsLine = wgtLabel(root, "");
|
||||||
|
sCardLine = wgtLabel(root, "");
|
||||||
|
sTraffic = wgtLabel(root, "");
|
||||||
|
timer = wgtTimer(root, CN_REFRESH_MS, true);
|
||||||
|
timer->onChange = onTick;
|
||||||
|
refreshStatus();
|
||||||
|
dvxFitWindow(sAc, sStatusWin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onStatusClose(WindowT *win) {
|
||||||
|
dvxDestroyWindow(sAc, win);
|
||||||
|
sStatusWin = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onTick(WidgetT *w) {
|
||||||
|
(void)w;
|
||||||
|
refreshStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The page's settings as a configuration.
|
||||||
|
static void readPage(NetConfigT *cfg) {
|
||||||
|
int32_t sel = wgtDropdownGetSelected(sVector);
|
||||||
|
|
||||||
|
*cfg = sOriginal;
|
||||||
|
cfg->dhcp = wgtCheckboxIsChecked(sDhcp);
|
||||||
|
snprintf(cfg->address, sizeof(cfg->address), "%s", wgtGetText(sAddress));
|
||||||
|
snprintf(cfg->netmask, sizeof(cfg->netmask), "%s", wgtGetText(sNetmask));
|
||||||
|
snprintf(cfg->gateway, sizeof(cfg->gateway), "%s", wgtGetText(sGateway));
|
||||||
|
snprintf(cfg->hostName, sizeof(cfg->hostName), "%s", wgtGetText(sHostName));
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < NET_DNS_SERVERS; i++) {
|
||||||
|
snprintf(cfg->dns[i], sizeof(cfg->dns[i]), "%s", wgtGetText(sDns[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg->vector = (sel <= 0) ? NET_VECTOR_AUTO : NET_VECTOR_FIRST + sel - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void refreshStatus(void) {
|
||||||
|
NetStatusT st;
|
||||||
|
char line[CN_LINE_MAX];
|
||||||
|
|
||||||
|
sNet.status(&st);
|
||||||
|
wgtSetText(sState, st.message);
|
||||||
|
snprintf(line, sizeof(line), "Address %s mask %s gateway %s", st.address[0] ? st.address : "-", st.netmask[0] ? st.netmask : "-", st.gateway[0] ? st.gateway : "-");
|
||||||
|
wgtSetText(sAddrLine, line);
|
||||||
|
snprintf(line, sizeof(line), "Name servers %s %s", st.dns[0][0] ? st.dns[0] : "-", st.dns[1]);
|
||||||
|
wgtSetText(sDnsLine, line);
|
||||||
|
snprintf(line, sizeof(line), "Card %s %s", st.driverName[0] ? st.driverName : "-", st.mac);
|
||||||
|
wgtSetText(sCardLine, line);
|
||||||
|
snprintf(line, sizeof(line), "Received %lu packets (%lu bytes, %lu lost) sent %lu (%lu bytes)", (unsigned long)st.rxPackets, (unsigned long)st.rxBytes, (unsigned long)st.rxDropped, (unsigned long)st.txPackets, (unsigned long)st.txBytes);
|
||||||
|
wgtSetText(sTraffic, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The fixed-address fields only matter without DHCP.
|
||||||
|
static void setDhcpEnabled(void) {
|
||||||
|
bool fixed = !wgtCheckboxIsChecked(sDhcp);
|
||||||
|
|
||||||
|
wgtSetEnabled(sAddress, fixed);
|
||||||
|
wgtSetEnabled(sNetmask, fixed);
|
||||||
|
wgtSetEnabled(sGateway, fixed);
|
||||||
|
}
|
||||||
48
src/apps/kpunch/cpanel/cpNet.h
Normal file
48
src/apps/kpunch/cpanel/cpNet.h
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// cpNet.h -- Control Panel's Network page: the TCP/IP settings
|
||||||
|
// (DVX.INI [network]), and the Network Status window it opens (the
|
||||||
|
// network's current state and traffic).
|
||||||
|
|
||||||
|
#ifndef CP_NET_H
|
||||||
|
#define CP_NET_H
|
||||||
|
|
||||||
|
#include "dvxApp.h"
|
||||||
|
#include "dvxPrefs.h"
|
||||||
|
#include "dvxWgt.h"
|
||||||
|
|
||||||
|
// Build the page from the settings in prefs.
|
||||||
|
void cpNetBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
|
||||||
|
|
||||||
|
// OK, step 1: NULL when the page's settings can be used, else why not
|
||||||
|
// (the caller shows it and stays open).
|
||||||
|
const char *cpNetProblem(void);
|
||||||
|
|
||||||
|
// OK, step 2: store the settings in prefs (the caller saves the file).
|
||||||
|
void cpNetStore(PrefsHandleT *prefs);
|
||||||
|
|
||||||
|
// OK, step 3 (after the save): restart the network if the settings
|
||||||
|
// changed, so they take effect now.
|
||||||
|
void cpNetApply(void);
|
||||||
|
|
||||||
|
#endif // CP_NET_H
|
||||||
174
src/apps/kpunch/cpanel/cpPrinter.c
Normal file
174
src/apps/kpunch/cpanel/cpPrinter.c
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// cpPrinter.c -- Control Panel's Printer page (see cpPrinter.h).
|
||||||
|
//
|
||||||
|
// The printing library is looked up by name when the page is built, so
|
||||||
|
// the Control Panel still works where it is not installed (the page then
|
||||||
|
// says so). Page Setup... opens the library's own dialog, which saves
|
||||||
|
// the page setup at once; OK stores the model and port and keeps the
|
||||||
|
// page setup as it is in DVX.INI then.
|
||||||
|
|
||||||
|
#include "cpPrinter.h"
|
||||||
|
#include "print/print.h"
|
||||||
|
#include "dvxPlat.h"
|
||||||
|
#include "box/box.h"
|
||||||
|
#include "button/button.h"
|
||||||
|
#include "dropdown/dropdown.h"
|
||||||
|
#include "label/label.h"
|
||||||
|
#include "textInput/textInpt.h"
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define CPR_LABEL_W 110
|
||||||
|
#define CPR_FIELD_CHARS 24
|
||||||
|
#define CPR_GAP 4
|
||||||
|
#define CPR_BTN_W 110
|
||||||
|
|
||||||
|
// The library's functions and tables (NULL when it is not installed)
|
||||||
|
typedef struct {
|
||||||
|
__typeof__(printSettingsLoad) *load;
|
||||||
|
__typeof__(printSettingsRead) *read;
|
||||||
|
__typeof__(printSettingsWrite) *write;
|
||||||
|
__typeof__(printPageSetupDialog) *pageSetup;
|
||||||
|
__typeof__(printDestLabel) *destLabel;
|
||||||
|
__typeof__(printModelLabel) *modelLabel;
|
||||||
|
} PrintFnsT;
|
||||||
|
|
||||||
|
static PrintFnsT sPrint;
|
||||||
|
static AppContextT *sAc = NULL;
|
||||||
|
static WidgetT *sModel = NULL;
|
||||||
|
static WidgetT *sDest = NULL;
|
||||||
|
static WidgetT *sFile = NULL;
|
||||||
|
static const char *sDestLabels[PrintDestCountE];
|
||||||
|
static const char *sModelLabels[PrintModelCountE];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Prototypes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static WidgetT *addRow(WidgetT *parent, const char *label);
|
||||||
|
void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
|
||||||
|
const char *cpPrinterProblem(void);
|
||||||
|
void cpPrinterStore(PrefsHandleT *prefs);
|
||||||
|
static void onDest(WidgetT *w);
|
||||||
|
static void onPageSetup(WidgetT *w);
|
||||||
|
|
||||||
|
|
||||||
|
// "Label:" and the row for its control.
|
||||||
|
static WidgetT *addRow(WidgetT *parent, const char *label) {
|
||||||
|
WidgetT *row = wgtHBox(parent);
|
||||||
|
WidgetT *lbl = wgtLabel(row, label);
|
||||||
|
|
||||||
|
row->spacing = wgtPixels(CPR_GAP);
|
||||||
|
lbl->prefW = wgtPixels(CPR_LABEL_W);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
|
||||||
|
PrintSettingsT s;
|
||||||
|
|
||||||
|
sAc = ac;
|
||||||
|
sPrint.load = (__typeof__(sPrint.load))dlsym(RTLD_DEFAULT, DVX_SYM("printSettingsLoad"));
|
||||||
|
sPrint.read = (__typeof__(sPrint.read))dlsym(RTLD_DEFAULT, DVX_SYM("printSettingsRead"));
|
||||||
|
sPrint.write = (__typeof__(sPrint.write))dlsym(RTLD_DEFAULT, DVX_SYM("printSettingsWrite"));
|
||||||
|
sPrint.pageSetup = (__typeof__(sPrint.pageSetup))dlsym(RTLD_DEFAULT, DVX_SYM("printPageSetupDialog"));
|
||||||
|
sPrint.destLabel = (__typeof__(sPrint.destLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printDestLabel"));
|
||||||
|
sPrint.modelLabel = (__typeof__(sPrint.modelLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printModelLabel"));
|
||||||
|
|
||||||
|
if (!sPrint.load || !sPrint.read || !sPrint.write || !sPrint.pageSetup || !sPrint.destLabel || !sPrint.modelLabel) {
|
||||||
|
memset(&sPrint, 0, sizeof(sPrint));
|
||||||
|
wgtLabel(page, "Printing is not installed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sPrint.read(prefs, &s);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < PrintDestCountE; i++) {
|
||||||
|
sDestLabels[i] = sPrint.destLabel((PrintDestE)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < PrintModelCountE; i++) {
|
||||||
|
sModelLabels[i] = sPrint.modelLabel((PrintModelE)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
sModel = wgtDropdown(addRow(page, "Printer model:"));
|
||||||
|
wgtDropdownSetItems(sModel, sModelLabels, PrintModelCountE);
|
||||||
|
wgtDropdownSetSelected(sModel, s.model);
|
||||||
|
sDest = wgtDropdown(addRow(page, "Connected to:"));
|
||||||
|
sDest->onChange = onDest;
|
||||||
|
wgtDropdownSetItems(sDest, sDestLabels, PrintDestCountE);
|
||||||
|
wgtDropdownSetSelected(sDest, s.dest);
|
||||||
|
sFile = wgtTextInput(addRow(page, "File name:"), PRINT_PATH_MAX - 1);
|
||||||
|
sFile->prefW = wgtChars(CPR_FIELD_CHARS);
|
||||||
|
wgtSetText(sFile, s.filePath);
|
||||||
|
onDest(sDest);
|
||||||
|
wgtLabel(page, "Text only works with any printer; choose your printer's");
|
||||||
|
wgtLabel(page, "model to print pictures too.");
|
||||||
|
|
||||||
|
WidgetT *btn = wgtButton(wgtHBox(page), "Page Setup...");
|
||||||
|
|
||||||
|
btn->onClick = onPageSetup;
|
||||||
|
btn->prefW = wgtPixels(CPR_BTN_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const char *cpPrinterProblem(void) {
|
||||||
|
if (sPrint.load && wgtDropdownGetSelected(sDest) == PrintDestFileE && !wgtGetText(sFile)[0]) {
|
||||||
|
return "Type the name of the file to print to, or connect the printer to a port.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The page setup comes from DVX.INI as it is now (Page Setup... saves it
|
||||||
|
// there at once), so prefs, read when the Control Panel opened, does not
|
||||||
|
// put back an older one.
|
||||||
|
void cpPrinterStore(PrefsHandleT *prefs) {
|
||||||
|
PrintSettingsT s;
|
||||||
|
|
||||||
|
if (!sPrint.load) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sPrint.load(&s);
|
||||||
|
s.model = (PrintModelE)wgtDropdownGetSelected(sModel);
|
||||||
|
s.dest = (PrintDestE)wgtDropdownGetSelected(sDest);
|
||||||
|
snprintf(s.filePath, sizeof(s.filePath), "%s", wgtGetText(sFile));
|
||||||
|
sPrint.write(prefs, &s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The file name only matters when printing to a file.
|
||||||
|
static void onDest(WidgetT *w) {
|
||||||
|
wgtSetEnabled(sFile, wgtDropdownGetSelected(w) == PrintDestFileE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onPageSetup(WidgetT *w) {
|
||||||
|
(void)w;
|
||||||
|
sPrint.pageSetup(sAc);
|
||||||
|
}
|
||||||
43
src/apps/kpunch/cpanel/cpPrinter.h
Normal file
43
src/apps/kpunch/cpanel/cpPrinter.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// cpPrinter.h -- Control Panel's Printer page: the printer model, the
|
||||||
|
// port it is connected to (or a file) and the Page Setup (DVX.INI
|
||||||
|
// [printer], through the printing library).
|
||||||
|
|
||||||
|
#ifndef CP_PRINTER_H
|
||||||
|
#define CP_PRINTER_H
|
||||||
|
|
||||||
|
#include "dvxApp.h"
|
||||||
|
#include "dvxPrefs.h"
|
||||||
|
#include "dvxWgt.h"
|
||||||
|
|
||||||
|
// Build the page from the settings in prefs.
|
||||||
|
void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
|
||||||
|
|
||||||
|
// OK, step 1: NULL when the page's settings can be used, else why not.
|
||||||
|
const char *cpPrinterProblem(void);
|
||||||
|
|
||||||
|
// OK, step 2: store the settings in prefs (the caller saves the file).
|
||||||
|
void cpPrinterStore(PrefsHandleT *prefs);
|
||||||
|
|
||||||
|
#endif // CP_PRINTER_H
|
||||||
349
src/apps/kpunch/cpanel/cpTypes.c
Normal file
349
src/apps/kpunch/cpanel/cpTypes.c
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// cpTypes.c -- Control Panel's File Types page (see cpTypes.h)
|
||||||
|
//
|
||||||
|
// Lists the file associations (shellAssoc.h): which program opens each
|
||||||
|
// kind of document. Add, Change and Remove edit a copy; like every other
|
||||||
|
// page, nothing is written until OK, and Cancel drops the edits.
|
||||||
|
|
||||||
|
#include "cpTypes.h"
|
||||||
|
#include "shellAssoc.h"
|
||||||
|
#include "shellProg.h"
|
||||||
|
#include "dvxDlg.h"
|
||||||
|
#include "dvxPlat.h"
|
||||||
|
#include "box/box.h"
|
||||||
|
#include "button/button.h"
|
||||||
|
#include "label/label.h"
|
||||||
|
#include "listView/listView.h"
|
||||||
|
#include "spacer/spacer.h"
|
||||||
|
#include "textInput/textInpt.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include "dvxMem.h"
|
||||||
|
#include "stb_ds_wrap.h"
|
||||||
|
|
||||||
|
#define CT_VALUE_MAX (DVX_MAX_PATH + SHELL_ARGS_MAX)
|
||||||
|
#define CT_COLS 3
|
||||||
|
#define CT_EXT_CHARS 6
|
||||||
|
#define CT_NAME_CHARS 18
|
||||||
|
#define CT_DLG_W 320
|
||||||
|
#define CT_LABEL_W 80
|
||||||
|
#define CT_GAP 4
|
||||||
|
#define CT_BTN_W 80
|
||||||
|
#define CT_LIST_NAME "fileTypes" // wgtFind name, for scripts and tests
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char ext[SHELL_ASSOC_EXT_MAX];
|
||||||
|
char name[SHELL_APP_NAME_MAX]; // the program's display name
|
||||||
|
char value[CT_VALUE_MAX]; // program[,arguments] as stored
|
||||||
|
} TypeEntryT;
|
||||||
|
|
||||||
|
// The Add/Change dialog's widgets and result.
|
||||||
|
typedef struct {
|
||||||
|
WidgetT *ext;
|
||||||
|
WidgetT *program;
|
||||||
|
WidgetT *args;
|
||||||
|
TypeEntryT *entry;
|
||||||
|
bool isNew;
|
||||||
|
} TypeDlgT;
|
||||||
|
|
||||||
|
static TypeEntryT *sEntries = NULL; // stb_ds: the page's working copy
|
||||||
|
static TypeEntryT *sOriginal = NULL; // stb_ds: the store when the page opened
|
||||||
|
static const char **sCells = NULL; // stb_ds: list view cells into sEntries
|
||||||
|
static WidgetT *sList = NULL;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Prototypes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void cpTypesApply(void);
|
||||||
|
void cpTypesBuild(WidgetT *page);
|
||||||
|
void cpTypesFree(void);
|
||||||
|
static int32_t findEntry(const TypeEntryT *list, const char *ext);
|
||||||
|
static void onAdd(WidgetT *w);
|
||||||
|
static void onBrowse(WidgetT *w);
|
||||||
|
static void onChange(WidgetT *w);
|
||||||
|
static void onRemove(WidgetT *w);
|
||||||
|
static void programPart(const char *value, char *out, int32_t outSize);
|
||||||
|
static void refreshList(void);
|
||||||
|
static void setEntryValue(TypeEntryT *entry, const char *value);
|
||||||
|
static bool typeDialog(TypeEntryT *entry, bool isNew);
|
||||||
|
static bool typeDlgOk(WindowT *dlg, void *userCtx);
|
||||||
|
|
||||||
|
|
||||||
|
// Removed types first, then new and changed ones.
|
||||||
|
void cpTypesApply(void) {
|
||||||
|
for (int32_t i = 0; i < arrlen(sOriginal); i++) {
|
||||||
|
if (findEntry(sEntries, sOriginal[i].ext) < 0) {
|
||||||
|
shellAssocRemove(sOriginal[i].ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
||||||
|
int32_t was = findEntry(sOriginal, sEntries[i].ext);
|
||||||
|
|
||||||
|
if (was < 0 || strcmp(sOriginal[was].value, sEntries[i].value) != 0) {
|
||||||
|
shellAssocSet(sEntries[i].ext, sEntries[i].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cpTypesBuild(WidgetT *page) {
|
||||||
|
static const ListViewColT cols[CT_COLS] = {
|
||||||
|
{ "Type", 0, ListViewAlignLeftE },
|
||||||
|
{ "Opens with", 0, ListViewAlignLeftE },
|
||||||
|
{ "Program", 0, ListViewAlignLeftE }
|
||||||
|
};
|
||||||
|
TypeEntryT entry;
|
||||||
|
char value[CT_VALUE_MAX];
|
||||||
|
|
||||||
|
cpTypesFree();
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < shellAssocCount(); i++) {
|
||||||
|
memset(&entry, 0, sizeof(entry));
|
||||||
|
|
||||||
|
if (shellAssocGet(i, entry.ext, sizeof(entry.ext), value, sizeof(value))) {
|
||||||
|
setEntryValue(&entry, value);
|
||||||
|
arrput(sEntries, entry);
|
||||||
|
arrput(sOriginal, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtLabel(page, "Documents of each type open in this program:");
|
||||||
|
sList = wgtListView(page);
|
||||||
|
sList->weight = WGT_WEIGHT_FILL;
|
||||||
|
sList->onDblClick = onChange;
|
||||||
|
wgtSetName(sList, CT_LIST_NAME);
|
||||||
|
wgtListViewSetColumns(sList, cols, CT_COLS);
|
||||||
|
refreshList();
|
||||||
|
|
||||||
|
WidgetT *btnRow = wgtHBox(page);
|
||||||
|
|
||||||
|
btnRow->spacing = wgtPixels(CT_GAP);
|
||||||
|
|
||||||
|
WidgetT *addBtn = wgtButton(btnRow, "&Add...");
|
||||||
|
|
||||||
|
addBtn->prefW = wgtPixels(CT_BTN_W);
|
||||||
|
addBtn->onClick = onAdd;
|
||||||
|
|
||||||
|
WidgetT *changeBtn = wgtButton(btnRow, "C&hange...");
|
||||||
|
|
||||||
|
changeBtn->prefW = wgtPixels(CT_BTN_W);
|
||||||
|
changeBtn->onClick = onChange;
|
||||||
|
|
||||||
|
WidgetT *removeBtn = wgtButton(btnRow, "&Remove");
|
||||||
|
|
||||||
|
removeBtn->prefW = wgtPixels(CT_BTN_W);
|
||||||
|
removeBtn->onClick = onRemove;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void cpTypesFree(void) {
|
||||||
|
arrfree(sEntries);
|
||||||
|
arrfree(sOriginal);
|
||||||
|
arrfree(sCells);
|
||||||
|
sEntries = NULL;
|
||||||
|
sOriginal = NULL;
|
||||||
|
sCells = NULL;
|
||||||
|
sList = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t findEntry(const TypeEntryT *list, const char *ext) {
|
||||||
|
for (int32_t i = 0; i < arrlen(list); i++) {
|
||||||
|
if (strcasecmp(list[i].ext, ext) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onAdd(WidgetT *w) {
|
||||||
|
TypeEntryT entry;
|
||||||
|
|
||||||
|
(void)w;
|
||||||
|
memset(&entry, 0, sizeof(entry));
|
||||||
|
|
||||||
|
if (!typeDialog(&entry, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t at = findEntry(sEntries, entry.ext);
|
||||||
|
|
||||||
|
if (at >= 0) {
|
||||||
|
sEntries[at] = entry;
|
||||||
|
} else {
|
||||||
|
arrput(sEntries, entry);
|
||||||
|
at = (int32_t)arrlen(sEntries) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshList();
|
||||||
|
wgtListViewSetSelected(sList, at);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onBrowse(WidgetT *w) {
|
||||||
|
TypeDlgT *td = (TypeDlgT *)w->userData;
|
||||||
|
const FileFilterT filters[] = {
|
||||||
|
{ "Programs (*.app)" },
|
||||||
|
{ "All Files (*.*)" }
|
||||||
|
};
|
||||||
|
char path[DVX_MAX_PATH];
|
||||||
|
|
||||||
|
if (dvxFileDialog(wgtGetContext(w), "Choose Program", FD_OPEN, SHELL_PROG_DIR, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
|
||||||
|
wgtSetText(td->program, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onChange(WidgetT *w) {
|
||||||
|
int32_t sel = wgtListViewGetSelected(sList);
|
||||||
|
|
||||||
|
(void)w;
|
||||||
|
|
||||||
|
if (sel >= 0 && sel < arrlen(sEntries) && typeDialog(&sEntries[sel], false)) {
|
||||||
|
refreshList();
|
||||||
|
wgtListViewSetSelected(sList, sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void onRemove(WidgetT *w) {
|
||||||
|
int32_t sel = wgtListViewGetSelected(sList);
|
||||||
|
|
||||||
|
(void)w;
|
||||||
|
|
||||||
|
if (sel >= 0 && sel < arrlen(sEntries)) {
|
||||||
|
arrdel(sEntries, sel);
|
||||||
|
refreshList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The program of a stored value (the text before the argument separator).
|
||||||
|
static void programPart(const char *value, char *out, int32_t outSize) {
|
||||||
|
const char *sep = strchr(value, SHELL_ASSOC_ARGS_SEP);
|
||||||
|
int32_t len = sep ? (int32_t)(sep - value) : (int32_t)strlen(value);
|
||||||
|
|
||||||
|
snprintf(out, outSize, "%.*s", (int)len, value);
|
||||||
|
dvxTrimRight(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The list view keeps pointers to the cells, so they are rebuilt after
|
||||||
|
// every change to the entries.
|
||||||
|
static void refreshList(void) {
|
||||||
|
arrsetlen(sCells, 0);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
||||||
|
arrput(sCells, sEntries[i].ext);
|
||||||
|
arrput(sCells, sEntries[i].name);
|
||||||
|
arrput(sCells, sEntries[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtListViewSetData(sList, sCells, (int32_t)arrlen(sEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Stored value plus the display name of its program.
|
||||||
|
static void setEntryValue(TypeEntryT *entry, const char *value) {
|
||||||
|
char program[DVX_MAX_PATH];
|
||||||
|
ShellProgramT info;
|
||||||
|
|
||||||
|
snprintf(entry->value, sizeof(entry->value), "%s", value);
|
||||||
|
programPart(value, program, sizeof(program));
|
||||||
|
shellProgramInfo(program, &info);
|
||||||
|
snprintf(entry->name, sizeof(entry->name), "%s", info.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool typeDialog(TypeEntryT *entry, bool isNew) {
|
||||||
|
TypeDlgT td;
|
||||||
|
char program[DVX_MAX_PATH];
|
||||||
|
const char *comma = strchr(entry->value, SHELL_ASSOC_ARGS_SEP);
|
||||||
|
WindowT *dlg = dvxDialogCreate(wgtGetContext(sList), isNew ? "Add File Type" : "Change File Type", CT_DLG_W);
|
||||||
|
|
||||||
|
if (!dlg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
programPart(entry->value, program, sizeof(program));
|
||||||
|
td.entry = entry;
|
||||||
|
td.isNew = isNew;
|
||||||
|
td.ext = dvxDialogAddField(dlg, "&Extension:", CT_LABEL_W, SHELL_ASSOC_EXT_MAX - 1, entry->ext);
|
||||||
|
td.program = dvxDialogAddField(dlg, "&Program:", CT_LABEL_W, DVX_MAX_PATH - 1, program);
|
||||||
|
td.args = dvxDialogAddField(dlg, "&Arguments:", CT_LABEL_W, SHELL_ARGS_MAX - 1, comma ? dvxSkipWs(comma + 1) : "");
|
||||||
|
wgtSetEnabled(td.ext, isNew);
|
||||||
|
|
||||||
|
WidgetT *browse = wgtButton(td.program->parent, "&Browse...");
|
||||||
|
|
||||||
|
browse->userData = &td;
|
||||||
|
browse->onClick = onBrowse;
|
||||||
|
wgtLabel(dlg->widgetRoot, "In the arguments, %1 stands for the document.");
|
||||||
|
dvxDialogAddOkCancel(dlg, "&OK", typeDlgOk, &td);
|
||||||
|
wgtSetFocused(isNew ? td.ext : td.program);
|
||||||
|
return dvxDialogRun(wgtGetContext(sList), dlg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool typeDlgOk(WindowT *dlg, void *userCtx) {
|
||||||
|
TypeDlgT *td = (TypeDlgT *)userCtx;
|
||||||
|
AppContextT *ctx = wgtGetContext(dlg->widgetRoot);
|
||||||
|
char ext[SHELL_ASSOC_EXT_MAX];
|
||||||
|
char typed[SHELL_ASSOC_EXT_MAX + 1];
|
||||||
|
char program[DVX_MAX_PATH];
|
||||||
|
char value[CT_VALUE_MAX];
|
||||||
|
|
||||||
|
dvxCopyTrimmed(typed, sizeof(typed), wgtGetText(td->ext));
|
||||||
|
|
||||||
|
if (!shellAssocNormalize(typed, ext)) {
|
||||||
|
dvxErrorBox(ctx, "File Types", "An extension is one to eight letters or digits, such as TXT.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dvxCopyTrimmed(program, sizeof(program), wgtGetText(td->program));
|
||||||
|
|
||||||
|
if (!program[0]) {
|
||||||
|
dvxErrorBox(ctx, "File Types", "Type the program that opens this type.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *args = dvxSkipWs(wgtGetText(td->args));
|
||||||
|
|
||||||
|
if (args[0]) {
|
||||||
|
snprintf(value, sizeof(value), "%s%c%s", program, SHELL_ASSOC_ARGS_SEP, args);
|
||||||
|
} else {
|
||||||
|
snprintf(value, sizeof(value), "%s", program);
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(td->entry->ext, sizeof(td->entry->ext), "%s", ext);
|
||||||
|
setEntryValue(td->entry, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,24 +20,19 @@
|
||||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
// IN THE SOFTWARE.
|
// IN THE SOFTWARE.
|
||||||
|
|
||||||
// mylib.h -- Sample DVX library public header
|
// cpTypes.h -- Control Panel's File Types page (file associations)
|
||||||
//
|
#ifndef CP_TYPES_H
|
||||||
// A library exports functions via DXE3 symbol sharing.
|
#define CP_TYPES_H
|
||||||
// Any module loaded after this library can call its functions
|
|
||||||
// directly — no import library or dlsym needed.
|
|
||||||
|
|
||||||
#ifndef MYLIB_H
|
#include "dvxWgt.h"
|
||||||
#define MYLIB_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
// Build the page into a tab page and take a copy of the association store.
|
||||||
|
void cpTypesBuild(WidgetT *page);
|
||||||
|
|
||||||
// Add two integers
|
// OK: write every change made on the page to the store.
|
||||||
int32_t myLibAdd(int32_t a, int32_t b);
|
void cpTypesApply(void);
|
||||||
|
|
||||||
// Multiply two integers
|
// Release the page's copy (after the window is gone).
|
||||||
int32_t myLibMul(int32_t a, int32_t b);
|
void cpTypesFree(void);
|
||||||
|
|
||||||
// Return the library version string
|
#endif // CP_TYPES_H
|
||||||
const char *myLibVersion(void);
|
|
||||||
|
|
||||||
#endif // MYLIB_H
|
|
||||||
File diff suppressed because it is too large
Load diff
282
src/apps/kpunch/cpanel/cpanel.dhs
Normal file
282
src/apps/kpunch/cpanel/cpanel.dhs
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
# Control Panel help. Compiled to cpanel.hlp next to cpanel.app; F1 and
|
||||||
|
# Help > Contents in the Control Panel open it.
|
||||||
|
|
||||||
|
.topic cpanel.overview
|
||||||
|
.title Control Panel
|
||||||
|
.toc 0 Control Panel
|
||||||
|
.default
|
||||||
|
.index Control Panel
|
||||||
|
.index Customizing
|
||||||
|
.index Settings
|
||||||
|
.index OK and Cancel
|
||||||
|
|
||||||
|
.h1 Control Panel
|
||||||
|
|
||||||
|
The Control Panel changes how DVX looks and works: the mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of file, the network settings and the printer. Its window has seven tabs. Click a tab to see its settings.
|
||||||
|
|
||||||
|
.link cpanel.mouse Mouse: pointer speed, double-click speed and the scroll wheel
|
||||||
|
.link cpanel.colors Colors: the colors of every part of the screen, and color themes
|
||||||
|
.link cpanel.desktop Desktop: the picture behind the windows
|
||||||
|
.link cpanel.video Video: the screen resolution and number of colors
|
||||||
|
.link cpanel.types File Types: which program opens each kind of document
|
||||||
|
.link cpanel.network Network: connecting DVX to a network, and its status
|
||||||
|
.link cpanel.printer Printer: your printer's model, where it is connected, and the page setup
|
||||||
|
|
||||||
|
.h2 Trying Out Changes
|
||||||
|
|
||||||
|
Every change shows up right away, so you can see what it does before you decide.
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item OK keeps your changes. They are saved and used again the next time you start DVX.
|
||||||
|
.item Cancel, or closing the window, puts back everything you changed since you opened the Control Panel.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
The one exception is a theme saved with Save As... on the Colors tab: the theme file stays saved even if you click Cancel.
|
||||||
|
|
||||||
|
.h2 Menus
|
||||||
|
|
||||||
|
.table
|
||||||
|
Menu Command What it does
|
||||||
|
---- ------- ------------
|
||||||
|
Help Contents Open this help.
|
||||||
|
Help About Control Panel Show the program name and copyright.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.topic cpanel.mouse
|
||||||
|
.title Mouse Settings
|
||||||
|
.toc 0 Mouse
|
||||||
|
.index Mouse Settings
|
||||||
|
.index Double-Click Speed
|
||||||
|
.index Mouse Speed
|
||||||
|
.index Cursor Speed
|
||||||
|
.index Acceleration
|
||||||
|
.index Scroll Wheel
|
||||||
|
|
||||||
|
.h1 Mouse Settings
|
||||||
|
|
||||||
|
.table
|
||||||
|
Setting What it does
|
||||||
|
------- ------------
|
||||||
|
Scroll Wheel Normal or Reversed. Reversed makes the wheel scroll the other way.
|
||||||
|
Scroll Speed How many lines each click of the wheel scrolls, from 1 to 10. The number is shown at the right.
|
||||||
|
Double-Click Speed How quickly you must click twice for a double-click. Toward Slow gives you more time. The time in milliseconds is shown at the right.
|
||||||
|
Acceleration Off, Low, Medium or High. With acceleration, moving the mouse quickly makes the pointer travel farther.
|
||||||
|
Cursor Speed How far the pointer moves when you move the mouse. Toward Fast moves it farther.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 Testing the Double-Click Speed
|
||||||
|
|
||||||
|
Double-click the Test Area button. Each time it sees a double-click, the text beside it says so and counts up. If your double-clicks are not counted, move Double-Click Speed toward Slow and try again.
|
||||||
|
|
||||||
|
.note tip
|
||||||
|
If the pointer is hard to control, set Acceleration to Off and adjust Cursor Speed until it feels right. Then turn acceleration back on if you like.
|
||||||
|
.endnote
|
||||||
|
|
||||||
|
.topic cpanel.colors
|
||||||
|
.title Colors and Themes
|
||||||
|
.toc 0 Colors
|
||||||
|
.index Colors
|
||||||
|
.index Themes
|
||||||
|
.index Color Theme
|
||||||
|
.index Reset Colors
|
||||||
|
|
||||||
|
.h1 Colors and Themes
|
||||||
|
|
||||||
|
.h2 Changing One Color
|
||||||
|
|
||||||
|
The System Colors list names every part of the screen that has a color, such as the desktop, title bars, menus and buttons. Click a name, then move the Red, Green and Blue sliders to mix a new color for it. Each slider goes from 0 (none of that color) to 255 (full). The Preview box and the screen itself change as you go.
|
||||||
|
|
||||||
|
.h2 Themes
|
||||||
|
|
||||||
|
A theme is a saved set of all the colors. The row under the list works with themes:
|
||||||
|
|
||||||
|
.table
|
||||||
|
Button What it does
|
||||||
|
------ ------------
|
||||||
|
Theme list Choose one of the themes that came with DVX, or one you saved.
|
||||||
|
Apply Use the colors of the theme chosen in the list.
|
||||||
|
Load... Use the colors of a theme file from any folder.
|
||||||
|
Save As... Save the current colors as a new theme. If you do not type an ending, .THM is added, and the theme appears in the list.
|
||||||
|
Reset Put back the standard DVX colors.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Remember to click OK if you want to keep the colors after you close the Control Panel.
|
||||||
|
|
||||||
|
.topic cpanel.desktop
|
||||||
|
.title Desktop Picture
|
||||||
|
.toc 0 Desktop
|
||||||
|
.index Wallpaper
|
||||||
|
.index Desktop Picture
|
||||||
|
.index Background
|
||||||
|
|
||||||
|
.h1 Desktop Picture
|
||||||
|
|
||||||
|
The desktop can show a picture, called wallpaper, behind all the windows.
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item The list shows the pictures that came with DVX. Click one and click Apply, or double-click it.
|
||||||
|
.item Browse... lets you pick any BMP, JPEG, PNG or GIF picture from any folder.
|
||||||
|
.item Clear removes the picture, leaving the plain desktop color.
|
||||||
|
.item The line under the list shows the picture in use, or (none).
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
The Mode list chooses how the picture fills the screen:
|
||||||
|
|
||||||
|
.table
|
||||||
|
Mode What it does
|
||||||
|
---- ------------
|
||||||
|
Stretch Stretch the picture to fill the whole screen.
|
||||||
|
Tile Repeat the picture across the screen like floor tiles.
|
||||||
|
Center Show the picture once in the middle of the screen.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
If a picture cannot be loaded, the Control Panel says so and the desktop stays as it was.
|
||||||
|
|
||||||
|
.topic cpanel.video
|
||||||
|
.title Screen Resolution and Colors
|
||||||
|
.toc 0 Video
|
||||||
|
.index Video Mode
|
||||||
|
.index Screen Resolution
|
||||||
|
.index Resolution
|
||||||
|
.index Color Depth
|
||||||
|
|
||||||
|
.h1 Screen Resolution and Colors
|
||||||
|
|
||||||
|
The list shows every screen mode your video card offers, such as 800x600 65 thousand colors. The first numbers are the width and height of the screen in dots; the rest tells how many colors can be shown. The mode you are using now is selected when the tab opens.
|
||||||
|
|
||||||
|
Pick a mode and click Apply Mode (or double-click it). The screen switches to the new mode so you can see how it looks, and a box asks "Keep this display mode?" while it counts down from 15 seconds:
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Click Keep (or press Enter) to keep the new mode.
|
||||||
|
.item Click Revert, close the box, or wait for the countdown to finish, to go back to the old mode.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
So if the screen becomes hard to read, or goes blank, just wait 15 seconds.
|
||||||
|
|
||||||
|
Click OK on the Control Panel to use the new mode again the next time you start DVX.
|
||||||
|
|
||||||
|
If DVX ever starts with a screen you cannot use, hold down a Shift key while DVX starts: it then starts in 640x480, the mode every video card can show, and offers to keep it. The DVX help (Troubleshooting) explains this "safe video mode".
|
||||||
|
|
||||||
|
.note tip
|
||||||
|
Higher resolutions fit more on the screen but make everything smaller. More colors make pictures look better but can make DVX slower on older computers, and use more memory.
|
||||||
|
.endnote
|
||||||
|
|
||||||
|
.topic cpanel.types
|
||||||
|
.title File Types
|
||||||
|
.toc 0 File Types
|
||||||
|
.index File Types
|
||||||
|
.index Associations
|
||||||
|
.index Documents
|
||||||
|
.index Extensions
|
||||||
|
|
||||||
|
.h1 File Types
|
||||||
|
|
||||||
|
When you open a document (from the Program Manager, for example), DVX starts the program set up for its kind of file. The kind of file is its extension: the letters after the dot, such as TXT in NOTES.TXT. This page lists every extension DVX knows, the program that opens it, and that program's file.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Button What it does
|
||||||
|
------ ------------
|
||||||
|
Add... Set up a new extension: type it (such as TXT), pick the program with Browse..., and click OK. An extension that is already listed gets the new program.
|
||||||
|
Change... Pick a different program for the selected extension. Double-clicking an extension does the same.
|
||||||
|
Remove Forget the selected extension. The program itself is not removed.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Arguments, in the Add and Change boxes, are optional: they are given to the program along with the document, and %1 in them stands for the document. Leave them empty to just give the program the document.
|
||||||
|
|
||||||
|
Like every other page, your changes here are saved when you click OK, and forgotten if you click Cancel.
|
||||||
|
|
||||||
|
.topic cpanel.network
|
||||||
|
.title Network
|
||||||
|
.toc 0 Network
|
||||||
|
.index Network
|
||||||
|
.index TCP/IP
|
||||||
|
.index DHCP
|
||||||
|
.index IP Address
|
||||||
|
.index Name Server
|
||||||
|
.index DNS
|
||||||
|
.index Packet Driver
|
||||||
|
.index Network Status
|
||||||
|
|
||||||
|
.h1 Network
|
||||||
|
|
||||||
|
DVX can use a network card to reach other computers and the Internet (Terminal's Telnet sessions, for example). This page holds the network settings.
|
||||||
|
|
||||||
|
The network card is used through its packet driver: a small program for your card, usually supplied with it, that is started before DVX (for an NE2000 card, for example, NE2000.COM 0x60 3 0x300 in AUTOEXEC.BAT). Without one DVX works as usual, but only this computer can be reached.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Setting Meaning
|
||||||
|
------- -------
|
||||||
|
Get the address automatically (DHCP) The network gives DVX its address, mask, gateway and name servers. Most networks, and most emulators, work this way.
|
||||||
|
Address, Subnet mask, Gateway Only when not automatic: the address for this computer (such as 192.168.1.10), the mask (usually 255.255.255.0) and the router to other networks (may be empty).
|
||||||
|
Name server 1, 2 Computers that turn names such as bbs.example.org into addresses. Leave them empty to use the ones the network gives out; filled in, they are used instead.
|
||||||
|
Computer name This computer's name on the network (letters, digits and hyphens).
|
||||||
|
Packet driver "Find it" looks for the packet driver by itself. Pick an interrupt only if it is not found.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Like every other page, the settings are saved when you click OK. If they changed, the network then restarts with them, and connections that were open are closed.
|
||||||
|
|
||||||
|
.h2 Buttons
|
||||||
|
|
||||||
|
.table
|
||||||
|
Button What it does
|
||||||
|
------ ------------
|
||||||
|
Status... Opens the Network Status window (see below). It can stay open after the Control Panel is closed.
|
||||||
|
Restart Restarts the network with the saved settings, for example after starting a packet driver. Open connections are closed.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 Network Status
|
||||||
|
|
||||||
|
The Network Status window shows, updated every second:
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item What the network is doing: "Connected", "Waiting for a DHCP server", or that no packet driver was found.
|
||||||
|
.item The address, subnet mask and gateway in use, and the name servers.
|
||||||
|
.item The network card (its packet driver's name and hardware address).
|
||||||
|
.item How much has been received and sent. "Lost" counts arrivals DVX was too busy to take (for a connection, the other computer sends them again).
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.topic cpanel.printer
|
||||||
|
.title Printer
|
||||||
|
.toc 0 Printer
|
||||||
|
.index Printer
|
||||||
|
.index LPT1
|
||||||
|
.index Printer Model
|
||||||
|
.index Printing to a File
|
||||||
|
|
||||||
|
.h1 Printer
|
||||||
|
|
||||||
|
The Printer tab tells DVX about your printer. Every program prints to the printer set here.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Setting What it does
|
||||||
|
------- ------------
|
||||||
|
Printer model Text only works with any printer, but prints only text. Choose Epson 9-pin, Epson 24-pin or HP LaserJet if your printer is one of those or works like one (most dot matrix printers work like an Epson, most laser printers like a LaserJet); then pictures can be printed too.
|
||||||
|
Connected to The printer port the printer is plugged into: LPT1, LPT2 or LPT3. Choose File to print into a file instead.
|
||||||
|
File name The file printouts go into when Connected to is File. Each printout replaces it.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Page Setup... opens the Page Setup dialog: the margins, lines per page, header and footer used when text is printed. Its changes are saved as soon as you choose OK in that dialog.
|
||||||
|
|
||||||
|
Like the other tabs, the model and port are saved when you click OK.
|
||||||
|
|
@ -20,29 +20,52 @@
|
||||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
# IN THE SOFTWARE.
|
# IN THE SOFTWARE.
|
||||||
|
|
||||||
# DVX BASIC Makefile for DJGPP cross-compilation
|
# DVX BASIC Makefile
|
||||||
#
|
#
|
||||||
# Builds:
|
# Builds:
|
||||||
# basrt.lib -- BASIC runtime library (VM + values + dlregsym init)
|
# basrt.lib -- BASIC runtime library (VM + values + form
|
||||||
# dvxbasic.app -- BASIC IDE (compiler + UI, links against basrt.lib)
|
# runtime + dlregsym init)
|
||||||
|
# dvxbasic.app -- BASIC IDE (compiler + UI, links against
|
||||||
|
# basrt.lib, standalone stub embedded)
|
||||||
|
# basstub.app -- standalone app stub (obj/dvxbasic, embedded
|
||||||
|
# into the IDE and bascomp)
|
||||||
|
# bin/system/BASCOMP.EXE-- DOS command-line compiler
|
||||||
|
# bin/host/bascomp -- host command-line compiler
|
||||||
|
# bin/host/basrun -- host headless runner
|
||||||
|
# test_compiler, test_compact, test_suite -- host test harnesses
|
||||||
|
# ("make tests" builds and runs them)
|
||||||
|
# obj/host/basrt.a -- host compiler + runtime + form runtime for the
|
||||||
|
# src/test/formrt suite ("make host-basrt")
|
||||||
#
|
#
|
||||||
# The runtime is a separate library so compiled BASIC apps can use
|
# The runtime is a separate library so compiled BASIC apps can use
|
||||||
# it without including the compiler. The library's constructor calls
|
# it without including the compiler. The library's constructor calls
|
||||||
# dlregsym() to register its exports, making them available to DXEs
|
# dlregsym() to register its exports, making them available to DXEs
|
||||||
# loaded later (apps, widgets, etc.).
|
# loaded later (apps, widgets, etc.).
|
||||||
|
|
||||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
include ../../../common.mk
|
||||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
|
||||||
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
|
LIBDVX = ../../../libs/kpunch/libdvx
|
||||||
WARNFLAGS = -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation
|
TOOLSDIR = ../../../tools
|
||||||
DVX_INCLUDES = -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../libs/kpunch/libdvx/thirdparty -I.
|
DVX_INCLUDES = -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I.
|
||||||
CFLAGS = -O2 $(WARNFLAGS) -march=i486 -mtune=i586 $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks -MMD -MP
|
CFLAGS = $(DOS_CFLAGS) $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks
|
||||||
|
|
||||||
OBJDIR = ../../../../obj/dvxbasic
|
OBJDIR = ../../../../obj/dvxbasic
|
||||||
LIBSDIR = ../../../../bin/libs
|
LIBSDIR = ../../../../bin/libs
|
||||||
APPDIR = ../../../../bin/apps/kpunch/dvxbasic
|
APPDIR = ../../../../bin/apps/kpunch/dvxbasic
|
||||||
HOSTDIR = ../../../../bin/host
|
HOSTDIR = ../../../../bin/host
|
||||||
DVXRES = $(HOSTDIR)/dvxres
|
DVXRES = $(HOSTDIR)/dvxres
|
||||||
|
SYSTEMDIR = ../../../../bin/system
|
||||||
|
|
||||||
|
# Sources every command-line build (bascomp, basrun, test_suite) links:
|
||||||
|
# the build pipeline, the full compiler and the runtime without forms.
|
||||||
|
COMP_SRCS = compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c
|
||||||
|
VM_SRCS = runtime/vm.c runtime/values.c runtime/serialize.c
|
||||||
|
# Standalone programs that link the VM also need what libdvx provides it
|
||||||
|
# at run time on the target (INI access through dvxPrefs).
|
||||||
|
VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c
|
||||||
|
SUPPORT_SRCS = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
|
||||||
|
RES_SRCS = $(LIBDVX)/dvxResource.c $(TOOLSDIR)/dvxResWrite.c
|
||||||
|
CLI_SRCS = basBuild.c $(COMP_SRCS) $(VM_LINK_SRCS) $(RES_SRCS) $(SUPPORT_SRCS)
|
||||||
|
|
||||||
# Runtime library objects (VM + values + form runtime + serialization)
|
# Runtime library objects (VM + values + form runtime + serialization)
|
||||||
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o
|
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o
|
||||||
|
|
@ -50,23 +73,32 @@ RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
|
||||||
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
|
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
|
||||||
|
|
||||||
# Compiler objects (only needed by the IDE)
|
# Compiler objects (only needed by the IDE)
|
||||||
COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o $(OBJDIR)/obfuscate.o $(OBJDIR)/compact.o $(OBJDIR)/basBuild.o
|
COMP_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(COMP_SRCS))) $(OBJDIR)/basBuild.o
|
||||||
|
|
||||||
# IDE app objects
|
# IDE app objects
|
||||||
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
|
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
|
||||||
APP_OBJS = $(IDE_OBJS)
|
|
||||||
APP_TARGET = $(APPDIR)/dvxbasic.app
|
APP_TARGET = $(APPDIR)/dvxbasic.app
|
||||||
|
|
||||||
# Standalone stub (embedded as IDE resource)
|
# Standalone stub (embedded as IDE resource)
|
||||||
STUB_OBJS = $(OBJDIR)/basstub.o
|
STUB_OBJS = $(OBJDIR)/basstub.o $(OBJDIR)/basModuleRes.o
|
||||||
STUB_TARGET = $(OBJDIR)/basstub.app
|
STUB_TARGET = $(OBJDIR)/basstub.app
|
||||||
|
|
||||||
# Native test programs (host gcc, not cross-compiled).
|
# DOS command-line compiler: the same DOS objects the IDE uses plus the
|
||||||
# TESTSAN=1 rebuilds the harnesses under ASan/UBSan (make tests TESTSAN=1).
|
# resource and support code the loader normally provides.
|
||||||
HOSTCC = gcc
|
DOS_BASCOMP_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir stub/bascomp.c $(CLI_SRCS)))
|
||||||
HOSTCFLAGS = -O2 $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES)
|
|
||||||
|
# ---- Native host tools and harnesses (host gcc, not cross-compiled) ----
|
||||||
|
#
|
||||||
|
# TESTSAN=1 rebuilds the harnesses, bascomp and basrun under ASan/UBSan
|
||||||
|
# (make tests TESTSAN=1). Objects live in one directory per mode; the
|
||||||
|
# binaries share bin/host and depend on a mode stamp so switching TESTSAN
|
||||||
|
# relinks them instead of running a stale binary.
|
||||||
|
HOSTCFLAGS = $(HOST_TOOL_CFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES)
|
||||||
|
HOSTLDFLAGS =
|
||||||
|
HOSTTOOL_OBJDIR = $(OBJDIR)/host-$(if $(filter 1,$(TESTSAN)),san,plain)
|
||||||
ifeq ($(TESTSAN),1)
|
ifeq ($(TESTSAN),1)
|
||||||
HOSTCFLAGS += -g -fno-omit-frame-pointer -fsanitize=address,undefined
|
HOSTCFLAGS += -g $(SAN_FLAGS)
|
||||||
|
HOSTLDFLAGS += $(SAN_FLAGS)
|
||||||
endif
|
endif
|
||||||
# COV=1 (make coverage): every host build (harnesses, bascomp, basrun,
|
# COV=1 (make coverage): every host build (harnesses, bascomp, basrun,
|
||||||
# basrt.a) gets gcov instrumentation and lands under obj/hostcov so it
|
# basrt.a) gets gcov instrumentation and lands under obj/hostcov so it
|
||||||
|
|
@ -74,44 +106,39 @@ endif
|
||||||
# still the release tool from bin/host. Must precede the rules below
|
# still the release tool from bin/host. Must precede the rules below
|
||||||
# because target names expand when they are read.
|
# because target names expand when they are read.
|
||||||
ifeq ($(COV),1)
|
ifeq ($(COV),1)
|
||||||
HOSTDIR = ../../../../obj/hostcov/basic
|
HOSTDIR = ../../../../obj/hostcov/basic
|
||||||
DVXRES = ../../../../bin/host/dvxres
|
DVXRES = ../../../../bin/host/dvxres
|
||||||
HOSTOBJDIR = ../../../../obj/hostcov
|
HOSTTOOL_OBJDIR = $(HOSTDIR)/obj
|
||||||
HOSTCFLAGS += -g --coverage
|
HOSTCFLAGS += -g --coverage
|
||||||
HOSTRT_COVFLAGS = --coverage
|
HOSTLDFLAGS += --coverage
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Every header a host harness can pull in; listed as a prerequisite so a
|
TESTSAN_STAMP = $(HOSTDIR)/.testsan-$(if $(filter 1,$(TESTSAN)),on,off)
|
||||||
# header edit rebuilds the harness instead of leaving a stale binary.
|
|
||||||
HEADERS = $(wildcard *.h compiler/*.h runtime/*.h formrt/*.h)
|
# hostObjs SRCS -> the host tool objects for SRCS
|
||||||
|
hostObjs = $(patsubst %.c,$(HOSTTOOL_OBJDIR)/%.o,$(notdir $1))
|
||||||
|
|
||||||
TEST_COMPILER = $(HOSTDIR)/test_compiler
|
TEST_COMPILER = $(HOSTDIR)/test_compiler
|
||||||
TEST_COMPACT = $(HOSTDIR)/test_compact
|
TEST_COMPACT = $(HOSTDIR)/test_compact
|
||||||
TEST_SUITE = $(HOSTDIR)/test_suite
|
TEST_SUITE = $(HOSTDIR)/test_suite
|
||||||
|
|
||||||
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
|
|
||||||
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
|
|
||||||
TEST_COMPILER_SRCS = test_compiler.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 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)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
BASCOMP_TARGET = $(HOSTDIR)/bascomp
|
BASCOMP_TARGET = $(HOSTDIR)/bascomp
|
||||||
|
BASRUN_TARGET = $(HOSTDIR)/basrun
|
||||||
|
|
||||||
# Headless host runner (host tool; runs .app/.bas/.dbp with PRINT on stdout)
|
TEST_COMPILER_OBJS = $(call hostObjs,test_compiler.c $(COMP_SRCS) $(VM_LINK_SRCS) $(SUPPORT_SRCS))
|
||||||
BASRUN_SRCS = stub/basrun.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)
|
TEST_COMPACT_OBJS = $(call hostObjs,test_compact.c $(COMP_SRCS) $(VM_LINK_SRCS) $(SUPPORT_SRCS))
|
||||||
BASRUN_TARGET = $(HOSTDIR)/basrun
|
TEST_SUITE_OBJS = $(call hostObjs,test_suite.c $(CLI_SRCS))
|
||||||
|
BASCOMP_OBJS = $(call hostObjs,stub/bascomp.c $(CLI_SRCS))
|
||||||
|
BASRUN_OBJS = $(call hostObjs,stub/basrun.c stub/basModuleRes.c $(CLI_SRCS))
|
||||||
|
HOSTTOOL_OBJS = $(sort $(TEST_COMPILER_OBJS) $(TEST_COMPACT_OBJS) $(TEST_SUITE_OBJS) $(BASCOMP_OBJS) $(BASRUN_OBJS))
|
||||||
|
|
||||||
# Host-side BASIC test directory (basrun fixtures, fuzzers, compaction diff)
|
# Extra per-object host flags
|
||||||
|
HOSTFLAGS_test_suite = -DBAS_TEST_SRC_DIR='"$(abspath .)"'
|
||||||
|
|
||||||
|
# Host-side BASIC test directory (basrun fixtures, fuzzers, compaction
|
||||||
|
# diff) and the scratch directory the harnesses run in (they write files
|
||||||
|
# into the working directory).
|
||||||
BASIC_TESTDIR = ../../../test/basic
|
BASIC_TESTDIR = ../../../test/basic
|
||||||
|
HARNESS_WORK = ../../../../obj/test/harness
|
||||||
# DOS command-line compiler
|
|
||||||
DOSCC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
|
||||||
DOSCFLAGS = -O2 $(WARNFLAGS) -march=i486 -mtune=i586 $(DVX_INCLUDES)
|
|
||||||
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
|
|
||||||
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
|
|
||||||
SYSTEMDIR = ../../../../bin/system
|
|
||||||
|
|
||||||
.PHONY: all clean tests
|
.PHONY: all clean tests
|
||||||
|
|
||||||
|
|
@ -120,56 +147,58 @@ all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCO
|
||||||
# Run every built harness and fail the build if any returns non-zero.
|
# Run every built harness and fail the build if any returns non-zero.
|
||||||
# test_compiler, test_compact and test_suite each report a failure count
|
# test_compiler, test_compact and test_suite each report a failure count
|
||||||
# and return non-zero on failure. make stops at the first one that fails.
|
# and return non-zero on failure. make stops at the first one that fails.
|
||||||
# The harness binaries depend on the sanitizer stamp so switching TESTSAN
|
|
||||||
# on or off forces a rebuild instead of running a stale binary.
|
|
||||||
TESTSAN_STAMP = $(HOSTDIR)/.testsan-$(if $(filter 1,$(TESTSAN)),on,off)
|
|
||||||
|
|
||||||
# The src/test/basic suite (sample runs, fixtures, fuzzers, compaction
|
# The src/test/basic suite (sample runs, fixtures, fuzzers, compaction
|
||||||
# differential) needs bascomp and basrun; it inherits TESTSAN.
|
# differential) needs bascomp and basrun; it inherits TESTSAN.
|
||||||
tests: $(TEST_COMPILER) $(TEST_COMPACT) $(TEST_SUITE) $(BASCOMP_TARGET) $(BASRUN_TARGET)
|
tests: $(TEST_COMPILER) $(TEST_COMPACT) $(TEST_SUITE) $(BASCOMP_TARGET) $(BASRUN_TARGET)
|
||||||
$(TEST_COMPILER)
|
mkdir -p $(HARNESS_WORK)
|
||||||
$(TEST_COMPACT)
|
cd $(HARNESS_WORK) && $(abspath $(TEST_COMPILER))
|
||||||
$(TEST_SUITE)
|
cd $(HARNESS_WORK) && $(abspath $(TEST_COMPACT))
|
||||||
|
cd $(HARNESS_WORK) && $(abspath $(TEST_SUITE))
|
||||||
$(MAKE) -C $(BASIC_TESTDIR) test TESTSAN=$(TESTSAN)
|
$(MAKE) -C $(BASIC_TESTDIR) test TESTSAN=$(TESTSAN)
|
||||||
|
|
||||||
$(BASRUN_TARGET): $(BASRUN_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
$(TEST_COMPILER): $(TEST_COMPILER_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
||||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(BASRUN_SRCS) -lm
|
$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(TEST_COMPILER_OBJS) -lm
|
||||||
|
|
||||||
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
$(TEST_COMPACT): $(TEST_COMPACT_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
||||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
|
$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(TEST_COMPACT_OBJS) -lm
|
||||||
|
|
||||||
$(TEST_SUITE): $(TEST_SUITE_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
$(TEST_SUITE): $(TEST_SUITE_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
||||||
$(HOSTCC) $(HOSTCFLAGS) -I../../../tools -DBAS_TEST_SRC_DIR='"$(abspath .)"' -o $@ $(TEST_SUITE_SRCS) -lm
|
$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(TEST_SUITE_OBJS) -lm
|
||||||
|
|
||||||
|
$(BASRUN_TARGET): $(BASRUN_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
||||||
|
$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(BASRUN_OBJS) -lm
|
||||||
|
|
||||||
|
# Host command-line compiler -- basstub.app is appended as a STUB
|
||||||
|
# resource so bascomp is self-contained (no BASSTUB.APP companion file),
|
||||||
|
# and noicon.bmp as the ICON32 fallback for projects without an icon.
|
||||||
|
$(BASCOMP_TARGET): $(BASCOMP_OBJS) $(TESTSAN_STAMP) $(STUB_TARGET) noicon.bmp $(DVXRES) | $(HOSTDIR)
|
||||||
|
$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(BASCOMP_OBJS) -lm
|
||||||
|
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
||||||
|
$(DVXRES) add $@ noicon icon @noicon.bmp
|
||||||
|
|
||||||
$(TESTSAN_STAMP): | $(HOSTDIR)
|
$(TESTSAN_STAMP): | $(HOSTDIR)
|
||||||
rm -f $(HOSTDIR)/.testsan-on $(HOSTDIR)/.testsan-off
|
rm -f $(HOSTDIR)/.testsan-on $(HOSTDIR)/.testsan-off
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
$(TEST_COMPACT): $(TEST_COMPACT_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
|
# One host object rule per source (the sources live in several dirs).
|
||||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm
|
define HOST_OBJ_RULE
|
||||||
|
$(call hostObjs,$1): $1 $$(DVX_MAKEFILES) | $$(HOSTTOOL_OBJDIR)
|
||||||
|
$$(HOSTCC) $$(HOSTCFLAGS) $$(HOSTFLAGS_$(basename $(notdir $1))) -c -o $$@ $$<
|
||||||
|
endef
|
||||||
|
|
||||||
# Host command-line compiler -- basstub.app is appended as a STUB
|
$(foreach src,$(sort test_compiler.c test_compact.c test_suite.c stub/bascomp.c stub/basrun.c stub/basModuleRes.c $(CLI_SRCS)),$(eval $(call HOST_OBJ_RULE,$(src))))
|
||||||
# resource so bascomp is self-contained (no BASSTUB.APP companion file),
|
|
||||||
# and noicon.bmp as the ICON32 fallback for projects without an icon.
|
|
||||||
$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(HEADERS) ../../../tools/dvxResWrite.h $(STUB_TARGET) noicon.bmp $(DVXRES) | $(HOSTDIR)
|
|
||||||
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm
|
|
||||||
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
|
||||||
$(DVXRES) add $@ noicon icon @noicon.bmp
|
|
||||||
|
|
||||||
# DOS command-line compiler (same STUB / noicon embed as the host build)
|
# DOS command-line compiler (same STUB / noicon embed as the host build)
|
||||||
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) $(HEADERS) ../../../tools/dvxResWrite.h $(STUB_TARGET) noicon.bmp $(DVXRES) | $(SYSTEMDIR)
|
$(SYSTEMDIR)/BASCOMP.EXE: $(DOS_BASCOMP_OBJS) $(STUB_TARGET) noicon.bmp $(DVXRES) | $(SYSTEMDIR)
|
||||||
$(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm
|
$(CC) -o $(SYSTEMDIR)/bascomp.exe $(DOS_BASCOMP_OBJS) -lm
|
||||||
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
|
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
|
||||||
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@
|
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@
|
||||||
rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe
|
rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe
|
||||||
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
||||||
$(DVXRES) add $@ noicon icon @noicon.bmp
|
$(DVXRES) add $@ noicon icon @noicon.bmp
|
||||||
|
|
||||||
$(HOSTDIR):
|
$(HOSTDIR) $(HOSTTOOL_OBJDIR) $(SYSTEMDIR):
|
||||||
mkdir -p $(HOSTDIR)
|
mkdir -p $@
|
||||||
|
|
||||||
$(SYSTEMDIR):
|
|
||||||
mkdir -p $(SYSTEMDIR)
|
|
||||||
|
|
||||||
# Runtime library DXE (exports symbols via dlregsym constructor)
|
# Runtime library DXE (exports symbols via dlregsym constructor)
|
||||||
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
|
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
|
||||||
|
|
@ -184,73 +213,77 @@ $(STUB_TARGET): $(STUB_OBJS) | $(OBJDIR)
|
||||||
$(DXE3GEN) -o $@ -U $(STUB_OBJS)
|
$(DXE3GEN) -o $@ -U $(STUB_OBJS)
|
||||||
|
|
||||||
# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded)
|
# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded)
|
||||||
$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
|
$(APP_TARGET): $(COMP_OBJS) $(IDE_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
|
||||||
$(DXE3GEN) -o $@ -U $(COMP_OBJS) $(APP_OBJS)
|
$(DXE3GEN) -o $@ -U $(COMP_OBJS) $(IDE_OBJS)
|
||||||
$(DVXRES) build $@ dvxbasic.res
|
$(DVXRES) build $@ dvxbasic.res
|
||||||
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
|
||||||
|
|
||||||
|
|
||||||
# Object files. One pattern rule per source directory; header
|
# DOS object files. One pattern rule per source directory; header
|
||||||
# dependencies come from the compiler (-MMD -MP writes a .d next to each
|
# dependencies come from the compiler (DEPFLAGS writes a .d next to each
|
||||||
# object) so no hand-written list can drift out of date.
|
# object) so no hand-written list can drift out of date.
|
||||||
$(OBJDIR)/%.o: compiler/%.c | $(OBJDIR)
|
$(OBJDIR)/%.o: compiler/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(OBJDIR)/%.o: runtime/%.c | $(OBJDIR)
|
$(OBJDIR)/%.o: runtime/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(OBJDIR)/%.o: formrt/%.c | $(OBJDIR)
|
$(OBJDIR)/%.o: formrt/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(OBJDIR)/%.o: ide/%.c | $(OBJDIR)
|
$(OBJDIR)/%.o: ide/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(OBJDIR)/%.o: stub/%.c | $(OBJDIR)
|
$(OBJDIR)/%.o: stub/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
$(OBJDIR)/%.o: $(LIBDVX)/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
-include $(wildcard $(OBJDIR)/*.d)
|
$(OBJDIR)/%.o: $(LIBDVX)/platform/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: $(LIBDVX)/thirdparty/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: $(TOOLSDIR)/%.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
$(OBJDIR):
|
$(OBJDIR):
|
||||||
mkdir -p $(OBJDIR)
|
mkdir -p $(OBJDIR)
|
||||||
|
|
||||||
$(LIBSDIR):
|
|
||||||
mkdir -p $(LIBSDIR)
|
|
||||||
|
|
||||||
$(RT_TARGETDIR):
|
$(RT_TARGETDIR):
|
||||||
mkdir -p $(RT_TARGETDIR)
|
mkdir -p $(RT_TARGETDIR)
|
||||||
|
|
||||||
$(APPDIR):
|
$(APPDIR):
|
||||||
mkdir -p $(APPDIR)
|
mkdir -p $(APPDIR)
|
||||||
|
|
||||||
clean:
|
clean: host-basrt-clean
|
||||||
rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(OBJDIR)/*.d $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR)
|
rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(DOS_BASCOMP_OBJS) $(OBJDIR)/*.d $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR)
|
||||||
|
rm -rf $(OBJDIR)/host-plain $(OBJDIR)/host-san $(HARNESS_WORK)
|
||||||
rm -f $(TEST_COMPILER) $(TEST_COMPACT) $(TEST_SUITE) $(BASRUN_TARGET) $(HOSTDIR)/.testsan-on $(HOSTDIR)/.testsan-off
|
rm -f $(TEST_COMPILER) $(TEST_COMPACT) $(TEST_SUITE) $(BASRUN_TARGET) $(HOSTDIR)/.testsan-on $(HOSTDIR)/.testsan-off
|
||||||
$(MAKE) -C $(BASIC_TESTDIR) clean
|
$(MAKE) -C $(BASIC_TESTDIR) clean
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Host basrt archive (src/test/formrt suite) -- T5 block, keep last
|
# Host basrt archive (src/test/formrt suite)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
#
|
#
|
||||||
# Compiler + runtime + form runtime built with the native toolchain into
|
# Compiler + runtime + form runtime built with the native toolchain into
|
||||||
# obj/host/basrt.a, linked by the form-runtime test suite against the
|
# obj/host/basrt.a, linked by the form-runtime test suite against the
|
||||||
# host libdvx.a and the widget .so files. basNativeHost.c stands in for
|
# host libdvx.a and the widget .so files. basNativeHost.c stands in for
|
||||||
# the i386 basNativeDos.c trampoline (Makefile object-list selection, no
|
# the i386 basNativeDos.c trampoline (Makefile object-list selection, no
|
||||||
# preprocessor conditionals). SAN must match the rest of the host build.
|
# preprocessor conditionals). SAN/COV per common.mk; must match the rest
|
||||||
SAN ?= 1
|
# of the host build.
|
||||||
HOSTOBJDIR ?= ../../../../obj/host
|
HOSTRT_DIR = $(HOST_OBJROOT)/basrt
|
||||||
HOSTRT_DIR = $(HOSTOBJDIR)/basrt
|
HOSTRT_TARGET = $(HOST_OBJROOT)/basrt.a
|
||||||
HOSTRT_TARGET = $(HOSTOBJDIR)/basrt.a
|
HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/frmParser.c formrt/basNativeHost.c
|
||||||
HOSTRT_SRCS = 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 formrt/formrt.c formrt/frmParser.c formrt/basNativeHost.c
|
|
||||||
HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS)))
|
HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS)))
|
||||||
HOSTRT_CFLAGS = -O1 -g $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks $(HOSTRT_COVFLAGS)
|
HOSTRT_CFLAGS = $(HOST_CFLAGS) -Wno-stringop-truncation $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks
|
||||||
ifeq ($(SAN),1)
|
|
||||||
HOSTRT_CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: host-basrt host-basrt-clean
|
.PHONY: host-basrt host-basrt-clean
|
||||||
|
|
||||||
|
|
@ -260,17 +293,19 @@ $(HOSTRT_TARGET): $(HOSTRT_OBJS)
|
||||||
rm -f $@
|
rm -f $@
|
||||||
ar rcs $@ $(HOSTRT_OBJS)
|
ar rcs $@ $(HOSTRT_OBJS)
|
||||||
|
|
||||||
$(HOSTRT_DIR)/%.o: compiler/%.c $(HEADERS)
|
$(HOSTRT_DIR)/%.o: compiler/%.c $(DVX_MAKEFILES)
|
||||||
@mkdir -p $(HOSTRT_DIR)
|
@mkdir -p $(HOSTRT_DIR)
|
||||||
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
|
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(HOSTRT_DIR)/%.o: runtime/%.c $(HEADERS)
|
$(HOSTRT_DIR)/%.o: runtime/%.c $(DVX_MAKEFILES)
|
||||||
@mkdir -p $(HOSTRT_DIR)
|
@mkdir -p $(HOSTRT_DIR)
|
||||||
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
|
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
$(HOSTRT_DIR)/%.o: formrt/%.c $(HEADERS)
|
$(HOSTRT_DIR)/%.o: formrt/%.c $(DVX_MAKEFILES)
|
||||||
@mkdir -p $(HOSTRT_DIR)
|
@mkdir -p $(HOSTRT_DIR)
|
||||||
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
|
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
host-basrt-clean:
|
host-basrt-clean:
|
||||||
rm -rf $(HOSTRT_DIR) $(HOSTRT_TARGET)
|
rm -rf $(HOSTRT_DIR) $(HOSTRT_TARGET)
|
||||||
|
|
||||||
|
-include $(wildcard $(OBJDIR)/*.d) $(HOSTTOOL_OBJS:.o=.d) $(HOSTRT_OBJS:.o=.d)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
// 3. Debug builds serialize the debug info.
|
// 3. Debug builds serialize the debug info.
|
||||||
// 4. Extract the STUB resource from selfPath and write it as outPath.
|
// 4. Extract the STUB resource from selfPath and write it as outPath.
|
||||||
// 5. Append the resource set in canonical order:
|
// 5. Append the resource set in canonical order:
|
||||||
// BAS_RES_NAME (always, falls back to "BASIC App")
|
// BAS_RES_NAME (always, falls back to BAS_DEFAULT_APP_NAME)
|
||||||
// BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
|
// BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
|
||||||
// BAS_RES_ICON32 (project icon, else the NOICON fallback)
|
// BAS_RES_ICON32 (project icon, else the NOICON fallback)
|
||||||
// BAS_RES_HELPFILE (basename only)
|
// BAS_RES_HELPFILE (basename only)
|
||||||
|
|
@ -65,6 +65,9 @@
|
||||||
// Slack added to a .frm buffer for basStripFrmComments output.
|
// Slack added to a .frm buffer for basStripFrmComments output.
|
||||||
#define BAS_BUILD_STRIP_MARGIN 16
|
#define BAS_BUILD_STRIP_MARGIN 16
|
||||||
|
|
||||||
|
// Generic resource-append failure (disk full, I/O error).
|
||||||
|
#define BAS_BUILD_ERR_WRITE_RES "Failed writing resources to output file."
|
||||||
|
|
||||||
// Longest progress line handed to the log callback.
|
// Longest progress line handed to the log callback.
|
||||||
#define BAS_BUILD_LOG_BUF 128
|
#define BAS_BUILD_LOG_BUF 128
|
||||||
|
|
||||||
|
|
@ -76,6 +79,19 @@
|
||||||
#define BAS_BUILD_SOURCE_CHUNK 8192
|
#define BAS_BUILD_SOURCE_CHUNK 8192
|
||||||
// Room for a BEGINFORM "name" / ENDFORM wrapper around a form's code.
|
// Room for a BEGINFORM "name" / ENDFORM wrapper around a form's code.
|
||||||
#define BAS_BUILD_WRAP_SLACK (BAS_MAX_IDENT + 32)
|
#define BAS_BUILD_WRAP_SLACK (BAS_MAX_IDENT + 32)
|
||||||
|
// Longest [Modules]/[Forms] key, "File" plus a decimal index.
|
||||||
|
#define BAS_BUILD_KEY_LEN 32
|
||||||
|
|
||||||
|
// emitIcon's error text when it names the unreadable icon path (the
|
||||||
|
// other build errors are string literals)
|
||||||
|
static char sIconError[BAS_BUILD_ERROR_LEN];
|
||||||
|
|
||||||
|
// basBuildConcatProject passes, in source order.
|
||||||
|
typedef enum {
|
||||||
|
BAS_BUILD_PASS_MODULES,
|
||||||
|
BAS_BUILD_PASS_FORMS,
|
||||||
|
BAS_BUILD_PASS_COUNT
|
||||||
|
} BasBuildPassE;
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len);
|
static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len);
|
||||||
|
|
@ -83,11 +99,16 @@ static int32_t appendText(const char *path, const char *name, const char *va
|
||||||
const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
|
const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
|
||||||
bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines);
|
bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines);
|
||||||
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
|
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
|
||||||
|
void basBuildDirOf(const char *path, char *out, int32_t outSize);
|
||||||
void basBuildFreeProject(BasProjectSourceT *out);
|
void basBuildFreeProject(BasProjectSourceT *out);
|
||||||
|
bool basBuildMapLine(const BasSourceMapT *map, int32_t line, const char **path, int32_t *fileLine);
|
||||||
|
bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projectDir, int32_t projectDirSize);
|
||||||
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...);
|
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...);
|
||||||
static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
|
static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
|
||||||
static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec);
|
static int32_t countLines(const char *text, int32_t len);
|
||||||
static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
|
static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec);
|
||||||
|
static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
|
||||||
|
static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms);
|
||||||
static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen);
|
static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen);
|
||||||
static const char *writeStub(const char *outPath, const BasBuildSpecT *spec);
|
static const char *writeStub(const char *outPath, const BasBuildSpecT *spec);
|
||||||
|
|
||||||
|
|
@ -165,8 +186,12 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
|
||||||
int32_t stripCap = srcLen + BAS_BUILD_STRIP_MARGIN;
|
int32_t stripCap = srcLen + BAS_BUILD_STRIP_MARGIN;
|
||||||
uint8_t *stripped = (uint8_t *)malloc(stripCap);
|
uint8_t *stripped = (uint8_t *)malloc(stripCap);
|
||||||
|
|
||||||
|
// A form silently missing from the app would only surface as a
|
||||||
|
// LoadForm failure at runtime, so a failed allocation fails the
|
||||||
|
// build.
|
||||||
if (!stripped) {
|
if (!stripped) {
|
||||||
continue;
|
freeFrmWork(frmData, frmLens, obfFrms);
|
||||||
|
return "Out of memory.";
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t strippedLen = basStripFrmComments(src, srcLen, stripped, stripCap);
|
int32_t strippedLen = basStripFrmComments(src, srcLen, stripped, stripCap);
|
||||||
|
|
@ -188,8 +213,8 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
|
||||||
failure = writeStub(outPath, spec);
|
failure = writeStub(outPath, spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!failure && emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount) != 0) {
|
if (!failure) {
|
||||||
failure = "Failed writing resources to output file.";
|
failure = emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!failure) {
|
if (!failure) {
|
||||||
|
|
@ -198,15 +223,7 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
|
||||||
|
|
||||||
free(modData);
|
free(modData);
|
||||||
free(dbgData);
|
free(dbgData);
|
||||||
|
freeFrmWork(frmData, frmLens, obfFrms);
|
||||||
for (int32_t i = 0; i < frmCount; i++) {
|
|
||||||
free(frmData[i]);
|
|
||||||
free(obfFrms[i].data);
|
|
||||||
}
|
|
||||||
|
|
||||||
arrfree(frmData);
|
|
||||||
arrfree(frmLens);
|
|
||||||
arrfree(obfFrms);
|
|
||||||
|
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +236,11 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, co
|
||||||
char wrap[BAS_BUILD_WRAP_SLACK];
|
char wrap[BAS_BUILD_WRAP_SLACK];
|
||||||
int32_t n = snprintf(wrap, sizeof(wrap), "BEGINFORM \"%s\"\n", formName);
|
int32_t n = snprintf(wrap, sizeof(wrap), "BEGINFORM \"%s\"\n", formName);
|
||||||
|
|
||||||
|
if (n < 0 || n >= (int32_t)sizeof(wrap)) {
|
||||||
|
snprintf(ps->error, sizeof(ps->error), "Form name too long: %s", formName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!appendSource(ps, wrap, n)) {
|
if (!appendSource(ps, wrap, n)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -247,17 +269,20 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, co
|
||||||
|
|
||||||
|
|
||||||
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out) {
|
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out) {
|
||||||
static const char *const sections[2] = { BAS_INI_SECTION_MODULES, BAS_INI_SECTION_FORMS };
|
static const char *const sections[BAS_BUILD_PASS_COUNT] = {
|
||||||
|
[BAS_BUILD_PASS_MODULES] = BAS_INI_SECTION_MODULES,
|
||||||
|
[BAS_BUILD_PASS_FORMS] = BAS_INI_SECTION_FORMS
|
||||||
|
};
|
||||||
|
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
out->optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
|
out->optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
|
||||||
|
|
||||||
// Pass 0: .bas modules, pass 1: .frm code sections.
|
// .bas modules first, then .frm code sections.
|
||||||
for (int32_t pass = 0; pass < 2; pass++) {
|
for (int32_t pass = 0; pass < BAS_BUILD_PASS_COUNT; pass++) {
|
||||||
bool isForm = (pass == 1);
|
bool isForm = (pass == BAS_BUILD_PASS_FORMS);
|
||||||
|
|
||||||
for (int32_t i = 0; ; i++) {
|
for (int32_t i = 0; ; i++) {
|
||||||
char key[BAS_BUILD_LOG_BUF];
|
char key[BAS_BUILD_KEY_LEN];
|
||||||
snprintf(key, sizeof(key), "File%d", (int)i);
|
snprintf(key, sizeof(key), "File%d", (int)i);
|
||||||
const char *val = prefsGetString(prefs, sections[pass], key, NULL);
|
const char *val = prefsGetString(prefs, sections[pass], key, NULL);
|
||||||
|
|
||||||
|
|
@ -265,44 +290,62 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
char filePath[DVX_MAX_PATH];
|
BasSourceMapT entry;
|
||||||
snprintf(filePath, sizeof(filePath), "%s%s%s", projectDir, BAS_BUILD_SEP, val);
|
memset(&entry, 0, sizeof(entry));
|
||||||
|
snprintf(entry.path, sizeof(entry.path), "%s%s%s", projectDir, BAS_BUILD_SEP, val);
|
||||||
|
|
||||||
int32_t srcLen = 0;
|
int32_t srcLen = 0;
|
||||||
char *src = platformReadFile(filePath, &srcLen);
|
char *src = platformReadFile(entry.path, &srcLen);
|
||||||
|
|
||||||
if (!src) {
|
if (!src) {
|
||||||
snprintf(out->error, sizeof(out->error), "Cannot read %s", filePath);
|
snprintf(out->error, sizeof(out->error), "Cannot read %s", entry.path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok;
|
const char *code = src;
|
||||||
|
char formName[BAS_MAX_IDENT] = "";
|
||||||
|
|
||||||
|
entry.firstFileLine = 1;
|
||||||
|
|
||||||
if (isForm) {
|
if (isForm) {
|
||||||
char formName[BAS_MAX_IDENT] = "";
|
// A .frm without a Begin Form line would otherwise compile
|
||||||
basExtractFormName(src, formName, BAS_MAX_IDENT);
|
// its code unwrapped, as if it were a module.
|
||||||
|
if (!basExtractFormName(src, formName, BAS_MAX_IDENT)) {
|
||||||
|
snprintf(out->error, sizeof(out->error), "No Begin Form line in %s", entry.path);
|
||||||
|
free(src);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The BASIC code section follows the outer Begin Form block.
|
// The BASIC code section follows the outer Begin Form block.
|
||||||
const char *code = src + basFindFormEndPos(src, srcLen);
|
int32_t codeOff = basFindFormEndPos(src, srcLen);
|
||||||
|
|
||||||
ok = basBuildAppendFile(out, code, (int32_t)strlen(code), formName, NULL, NULL);
|
code = src + codeOff;
|
||||||
|
entry.firstFileLine = countLines(src, codeOff) + 1;
|
||||||
char *frmPath = (char *)malloc(strlen(filePath) + 1);
|
|
||||||
|
|
||||||
if (frmPath) {
|
|
||||||
strcpy(frmPath, filePath);
|
|
||||||
arrput(out->frmPaths, frmPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ok = basBuildAppendFile(out, src, (int32_t)strlen(src), NULL, NULL, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
free(src);
|
if (!basBuildAppendFile(out, code, (int32_t)strlen(code), formName, &entry.startLine, &entry.lineCount)) {
|
||||||
|
free(src);
|
||||||
if (!ok) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
arrput(out->map, entry);
|
||||||
|
|
||||||
|
if (isForm) {
|
||||||
|
char *frmPath = (char *)malloc(strlen(entry.path) + 1);
|
||||||
|
|
||||||
|
if (!frmPath) {
|
||||||
|
snprintf(out->error, sizeof(out->error), "Out of memory.");
|
||||||
|
free(src);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
strcpy(frmPath, entry.path);
|
||||||
|
arrput(out->frmPaths, frmPath);
|
||||||
|
arrput(out->frmTexts, src);
|
||||||
|
} else {
|
||||||
|
free(src);
|
||||||
|
}
|
||||||
|
|
||||||
out->fileCount++;
|
out->fileCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,17 +359,73 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void basBuildDirOf(const char *path, char *out, int32_t outSize) {
|
||||||
|
snprintf(out, outSize, "%s", path);
|
||||||
|
|
||||||
|
char *sep = platformPathDirEnd(out);
|
||||||
|
|
||||||
|
if (sep) {
|
||||||
|
*sep = '\0';
|
||||||
|
} else {
|
||||||
|
snprintf(out, outSize, ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void basBuildFreeProject(BasProjectSourceT *out) {
|
void basBuildFreeProject(BasProjectSourceT *out) {
|
||||||
for (int32_t i = 0; i < (int32_t)arrlen(out->frmPaths); i++) {
|
for (int32_t i = 0; i < (int32_t)arrlen(out->frmPaths); i++) {
|
||||||
free(out->frmPaths[i]);
|
free(out->frmPaths[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < (int32_t)arrlen(out->frmTexts); i++) {
|
||||||
|
free(out->frmTexts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
arrfree(out->frmPaths);
|
arrfree(out->frmPaths);
|
||||||
|
arrfree(out->frmTexts);
|
||||||
|
arrfree(out->map);
|
||||||
free(out->source);
|
free(out->source);
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool basBuildMapLine(const BasSourceMapT *map, int32_t line, const char **path, int32_t *fileLine) {
|
||||||
|
for (int32_t i = 0; i < (int32_t)arrlen(map); i++) {
|
||||||
|
const BasSourceMapT *m = &map[i];
|
||||||
|
|
||||||
|
if (line >= m->startLine && line < m->startLine + m->lineCount) {
|
||||||
|
*path = m->path;
|
||||||
|
*fileLine = m->firstFileLine + (line - m->startLine);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projectDir, int32_t projectDirSize) {
|
||||||
|
// prefsLoad treats a missing file as an empty INI, so probe it first
|
||||||
|
// or a bad path would surface as a confusing "no source files" error.
|
||||||
|
FILE *probe = fopen(dbpPath, "rb");
|
||||||
|
|
||||||
|
if (!probe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(probe);
|
||||||
|
|
||||||
|
*prefs = prefsLoad(dbpPath);
|
||||||
|
|
||||||
|
if (!*prefs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
basBuildDirOf(dbpPath, projectDir, projectDirSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) {
|
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) {
|
||||||
if (!spec->log) {
|
if (!spec->log) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -354,15 +453,7 @@ static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec)
|
||||||
snprintf(helpSrc, sizeof(helpSrc), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->helpFile);
|
snprintf(helpSrc, sizeof(helpSrc), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->helpFile);
|
||||||
|
|
||||||
char outDir[DVX_MAX_PATH];
|
char outDir[DVX_MAX_PATH];
|
||||||
snprintf(outDir, sizeof(outDir), "%s", outPath);
|
basBuildDirOf(outPath, outDir, sizeof(outDir));
|
||||||
char *sep = platformPathDirEnd(outDir);
|
|
||||||
|
|
||||||
if (sep) {
|
|
||||||
*sep = '\0';
|
|
||||||
} else {
|
|
||||||
outDir[0] = '.';
|
|
||||||
outDir[1] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
char helpDst[DVX_MAX_PATH];
|
char helpDst[DVX_MAX_PATH];
|
||||||
snprintf(helpDst, sizeof(helpDst), "%s" BAS_BUILD_SEP "%s", outDir, platformPathBaseName(spec->helpFile));
|
snprintf(helpDst, sizeof(helpDst), "%s" BAS_BUILD_SEP "%s", outDir, platformPathBaseName(spec->helpFile));
|
||||||
|
|
@ -375,9 +466,24 @@ static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Number of newlines in text[0..len).
|
||||||
|
static int32_t countLines(const char *text, int32_t len) {
|
||||||
|
int32_t n = 0;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < len; i++) {
|
||||||
|
if (text[i] == '\n') {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Embed the project icon, or the NOICON fallback carried by the running
|
// Embed the project icon, or the NOICON fallback carried by the running
|
||||||
// executable when the project names none.
|
// executable when the project names none. Returns NULL on success, else
|
||||||
static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec) {
|
// an error message (a named icon that cannot be read names the path).
|
||||||
|
static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) {
|
||||||
char iconPath[DVX_MAX_PATH];
|
char iconPath[DVX_MAX_PATH];
|
||||||
void *iconData = NULL;
|
void *iconData = NULL;
|
||||||
int32_t iconLen = 0;
|
int32_t iconLen = 0;
|
||||||
|
|
@ -390,7 +496,8 @@ static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec) {
|
||||||
// A named icon that cannot be read is a build failure --
|
// A named icon that cannot be read is a build failure --
|
||||||
// shipping the app without its ICON32 resource would never
|
// shipping the app without its ICON32 resource would never
|
||||||
// tell anyone.
|
// tell anyone.
|
||||||
return -1;
|
snprintf(sIconError, sizeof(sIconError), "Cannot read icon file %s", iconPath);
|
||||||
|
return sIconError;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);
|
DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);
|
||||||
|
|
@ -404,24 +511,24 @@ static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iconData) {
|
if (!iconData) {
|
||||||
return 0;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
|
int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
|
||||||
free(iconData);
|
free(iconData);
|
||||||
return rc;
|
return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) {
|
static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) {
|
||||||
// Accumulate every append result so a disk-full or I/O failure on any
|
// Accumulate every append result so a disk-full or I/O failure on any
|
||||||
// resource is reported instead of being silently treated as success.
|
// resource is reported instead of being silently treated as success.
|
||||||
int32_t rc = 0;
|
int32_t rc = 0;
|
||||||
|
|
||||||
// Project metadata. Name is required -- fall back to a generic label
|
// Project metadata. Name is required -- fall back to a generic label
|
||||||
// so the compiled app always has something sensible to display.
|
// so the compiled app always has something sensible to display.
|
||||||
const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : "BASIC App";
|
const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : BAS_DEFAULT_APP_NAME;
|
||||||
rc |= appendText(outPath, BAS_RES_NAME, projName);
|
rc |= appendText(outPath, BAS_RES_NAME, projName);
|
||||||
|
|
||||||
rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author);
|
rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author);
|
||||||
|
|
@ -430,7 +537,11 @@ static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, con
|
||||||
rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
|
rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
|
||||||
rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
|
rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
|
||||||
|
|
||||||
rc |= emitIcon(outPath, spec);
|
const char *iconFailure = emitIcon(outPath, spec);
|
||||||
|
|
||||||
|
if (iconFailure) {
|
||||||
|
return iconFailure;
|
||||||
|
}
|
||||||
|
|
||||||
if (spec->helpFile && spec->helpFile[0]) {
|
if (spec->helpFile && spec->helpFile[0]) {
|
||||||
rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
|
rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
|
||||||
|
|
@ -468,8 +579,7 @@ static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, con
|
||||||
// unreachable at runtime. Fail the build loudly instead of
|
// unreachable at runtime. Fail the build loudly instead of
|
||||||
// shipping forms the app can never load.
|
// shipping forms the app can never load.
|
||||||
if (outIdx >= BAS_MAX_FORM_RESOURCES) {
|
if (outIdx >= BAS_MAX_FORM_RESOURCES) {
|
||||||
rc |= -1;
|
return "Too many forms for one application.";
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
char resName[BAS_RES_FORM_NAME_LEN];
|
char resName[BAS_RES_FORM_NAME_LEN];
|
||||||
|
|
@ -480,7 +590,20 @@ static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, con
|
||||||
outIdx++;
|
outIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rc;
|
return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Releases the stripped / obfuscated form work arrays of basBuildApp.
|
||||||
|
static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms) {
|
||||||
|
for (int32_t i = 0; i < (int32_t)arrlen(frmData); i++) {
|
||||||
|
free(frmData[i]);
|
||||||
|
free(obfFrms[i].data);
|
||||||
|
}
|
||||||
|
|
||||||
|
arrfree(frmData);
|
||||||
|
arrfree(frmLens);
|
||||||
|
arrfree(obfFrms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -497,7 +620,10 @@ static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, i
|
||||||
if (!spec->release) {
|
if (!spec->release) {
|
||||||
*outMod = modData;
|
*outMod = modData;
|
||||||
*outDbg = basDebugSerialize(spec->module, outDbgLen);
|
*outDbg = basDebugSerialize(spec->module, outDbgLen);
|
||||||
return NULL;
|
|
||||||
|
// basDebugSerialize fails only on allocation; a debug build that
|
||||||
|
// silently lacks its DEBUG resource is not a debug build.
|
||||||
|
return *outDbg ? NULL : "Failed to serialize debug info.";
|
||||||
}
|
}
|
||||||
|
|
||||||
BasModuleT *modCopy = basModuleDeserialize(modData, *outModLen);
|
BasModuleT *modCopy = basModuleDeserialize(modData, *outModLen);
|
||||||
|
|
@ -517,8 +643,16 @@ static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, i
|
||||||
arrput(frmTexts, (const char *)frmData[i]);
|
arrput(frmTexts, (const char *)frmData[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
basObfuscateNames(modCopy, frmTexts, frmLens, frmCount, obfFrms);
|
bool obfOk = basObfuscateNames(modCopy, frmTexts, frmLens, frmCount, obfFrms);
|
||||||
arrfree(frmTexts);
|
arrfree(frmTexts);
|
||||||
|
|
||||||
|
// A failed obfuscation leaves the module and forms with
|
||||||
|
// inconsistent names; shipping them would break at runtime.
|
||||||
|
if (!obfOk) {
|
||||||
|
basModuleFree(modCopy);
|
||||||
|
return "Out of memory while obfuscating names.";
|
||||||
|
}
|
||||||
|
|
||||||
buildLog(spec, " obfuscated %d form(s)", (int)frmCount);
|
buildLog(spec, " obfuscated %d form(s)", (int)frmCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,19 +88,32 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
|
||||||
// Longest error text basBuildConcatProject reports (a path plus a reason).
|
// Longest error text basBuildConcatProject reports (a path plus a reason).
|
||||||
#define BAS_BUILD_ERROR_LEN (DVX_MAX_PATH + 64)
|
#define BAS_BUILD_ERROR_LEN (DVX_MAX_PATH + 64)
|
||||||
|
|
||||||
|
// One source file's span in the concatenated project source: lines
|
||||||
|
// [startLine, startLine + lineCount) of the concatenation are lines
|
||||||
|
// [firstFileLine, firstFileLine + lineCount) of path.
|
||||||
|
typedef struct {
|
||||||
|
char path[DVX_MAX_PATH];
|
||||||
|
int32_t startLine;
|
||||||
|
int32_t lineCount;
|
||||||
|
int32_t firstFileLine; // 1 for a module; the first code line of a .frm
|
||||||
|
} BasSourceMapT;
|
||||||
|
|
||||||
// A project's BASIC source as the compiler consumes it: every [Modules]
|
// A project's BASIC source as the compiler consumes it: every [Modules]
|
||||||
// file in order, then the code section of every [Forms] file wrapped in
|
// file in order, then the code section of every [Forms] file wrapped in
|
||||||
// BEGINFORM "name" / ENDFORM, each file ending on a newline. frmPaths
|
// BEGINFORM "name" / ENDFORM, each file ending on a newline. frmPaths
|
||||||
// lists the .frm files in the same order for the resource step.
|
// and frmTexts list the .frm files (path and full text, as read) in the
|
||||||
|
// same order for the resource step; map locates each file's lines.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *source; // NUL-terminated, malloc'd
|
char *source; // NUL-terminated, malloc'd
|
||||||
int32_t sourceLen;
|
int32_t sourceLen;
|
||||||
int32_t sourceCap;
|
int32_t sourceCap;
|
||||||
int32_t lineCount; // newlines in source (next text starts on lineCount + 1)
|
int32_t lineCount; // newlines in source (next text starts on lineCount + 1)
|
||||||
char **frmPaths; // stb_ds array of malloc'd paths
|
char **frmPaths; // stb_ds array of malloc'd paths
|
||||||
int32_t fileCount; // modules + forms
|
char **frmTexts; // stb_ds array of malloc'd .frm texts
|
||||||
bool optionExplicit; // [Settings] OptionExplicit
|
BasSourceMapT *map; // stb_ds array, one entry per file
|
||||||
char error[BAS_BUILD_ERROR_LEN];
|
int32_t fileCount; // modules + forms
|
||||||
|
bool optionExplicit; // [Settings] OptionExplicit
|
||||||
|
char error[BAS_BUILD_ERROR_LEN];
|
||||||
} BasProjectSourceT;
|
} BasProjectSourceT;
|
||||||
|
|
||||||
// Appends one file's text to *ps (a zeroed struct is a valid empty
|
// Appends one file's text to *ps (a zeroed struct is a valid empty
|
||||||
|
|
@ -116,9 +129,24 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, co
|
||||||
// call basBuildFreeProject either way.
|
// call basBuildFreeProject either way.
|
||||||
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
|
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
|
||||||
|
|
||||||
|
// Writes the directory part of path into out, or "." when path has none.
|
||||||
|
void basBuildDirOf(const char *path, char *out, int32_t outSize);
|
||||||
|
|
||||||
// Releases everything basBuildConcatProject allocated.
|
// Releases everything basBuildConcatProject allocated.
|
||||||
void basBuildFreeProject(BasProjectSourceT *out);
|
void basBuildFreeProject(BasProjectSourceT *out);
|
||||||
|
|
||||||
|
// Maps a 1-based line of the concatenated source to the file and 1-based
|
||||||
|
// line it came from, using a source map (stb_ds array, e.g. ps->map).
|
||||||
|
// Returns false (outputs untouched) for a line outside every file, e.g.
|
||||||
|
// a BEGINFORM/ENDFORM wrapper line.
|
||||||
|
bool basBuildMapLine(const BasSourceMapT *map, int32_t line, const char **path, int32_t *fileLine);
|
||||||
|
|
||||||
|
// Opens a .dbp project: *prefs receives the loaded INI and projectDir
|
||||||
|
// (projectDirSize bytes) the directory holding it. Returns false when
|
||||||
|
// the file cannot be opened (prefsLoad alone would open a missing file
|
||||||
|
// as an empty project).
|
||||||
|
bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projectDir, int32_t projectDirSize);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <strings.h> // strncasecmp
|
#include <strings.h> // strncasecmp
|
||||||
|
|
||||||
|
#include "formrt/frmParser.h" // FRM_KW_* .frm keyword spellings
|
||||||
|
|
||||||
// dvxSkipWs is declared in platform/dvxPlat.h, which every translation
|
// dvxSkipWs is declared in platform/dvxPlat.h, which every translation
|
||||||
// unit that includes this header also includes. We forward-declare it
|
// unit that includes this header also includes. We forward-declare it
|
||||||
// here (a redundant, compatible declaration is legal C) so the inline
|
// here (a redundant, compatible declaration is legal C) so the inline
|
||||||
|
|
@ -56,6 +58,7 @@ const char *dvxSkipWs(const char *s);
|
||||||
#define BAS_INI_KEY_DESCRIPTION "Description"
|
#define BAS_INI_KEY_DESCRIPTION "Description"
|
||||||
#define BAS_INI_KEY_ICON "Icon"
|
#define BAS_INI_KEY_ICON "Icon"
|
||||||
#define BAS_INI_KEY_HELPFILE "HelpFile"
|
#define BAS_INI_KEY_HELPFILE "HelpFile"
|
||||||
|
#define BAS_INI_KEY_COMMANDARGS "CommandArgs" // IDE only: COMMAND$ when run from the IDE
|
||||||
|
|
||||||
// [Settings] / [Modules] / [Forms] sections of a .dbp file
|
// [Settings] / [Modules] / [Forms] sections of a .dbp file
|
||||||
#define BAS_INI_SECTION_SETTINGS "Settings"
|
#define BAS_INI_SECTION_SETTINGS "Settings"
|
||||||
|
|
@ -68,6 +71,10 @@ const char *dvxSkipWs(const char *s);
|
||||||
// Resource names inside a compiled .app DXE
|
// Resource names inside a compiled .app DXE
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
// Display name of an app whose project sets no Name (resource fallback
|
||||||
|
// and the stub's descriptor before the NAME resource is read).
|
||||||
|
#define BAS_DEFAULT_APP_NAME "BASIC App"
|
||||||
|
|
||||||
// Text metadata -- mirrors the [Project] keys above but lowercase
|
// Text metadata -- mirrors the [Project] keys above but lowercase
|
||||||
// (resource names are case-sensitive; apps query these at runtime).
|
// (resource names are case-sensitive; apps query these at runtime).
|
||||||
#define BAS_RES_NAME "name"
|
#define BAS_RES_NAME "name"
|
||||||
|
|
@ -104,14 +111,14 @@ const char *dvxSkipWs(const char *s);
|
||||||
#define BAS_MAX_FORM_RESOURCES 256
|
#define BAS_MAX_FORM_RESOURCES 256
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Begin-Form name extraction (shared by bascomp and basstub)
|
// .frm scanners (shared by bascomp, basstub, basBuild and the obfuscator)
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
// Lengths of the .frm keywords scanned for below.
|
static inline bool basExtractFormName(const char *frmText, char *nameBuf, int32_t nameBufSize);
|
||||||
#define BAS_BEGIN_PREFIX_LEN 6 // "Begin "
|
static inline int32_t basFindFormEndPos(const char *text, int32_t len);
|
||||||
#define BAS_FORM_PREFIX_LEN 5 // "Form "
|
static inline int32_t basMatchBeginForm(const char *s, int32_t len);
|
||||||
#define BAS_BEGIN_FORM_PREFIX_LEN 11 // "Begin Form "
|
static inline int32_t basMatchKeywordWs(const char *s, int32_t len, const char *kw, int32_t kwLen);
|
||||||
#define BAS_END_KEYWORD_LEN 3 // "End"
|
|
||||||
|
|
||||||
// Extract the form name from a "Begin Form <name>" line in .frm text.
|
// Extract the form name from a "Begin Form <name>" line in .frm text.
|
||||||
// Writes a NUL-terminated name into nameBuf (max nameBufSize bytes,
|
// Writes a NUL-terminated name into nameBuf (max nameBufSize bytes,
|
||||||
|
|
@ -135,8 +142,16 @@ static inline bool basExtractFormName(const char *frmText, char *nameBuf, int32_
|
||||||
while (*p) {
|
while (*p) {
|
||||||
p = dvxSkipWs(p);
|
p = dvxSkipWs(p);
|
||||||
|
|
||||||
if (strncasecmp(p, "Begin Form ", BAS_BEGIN_FORM_PREFIX_LEN) == 0) {
|
int32_t lineLen = 0;
|
||||||
p = dvxSkipWs(p + BAS_BEGIN_FORM_PREFIX_LEN);
|
|
||||||
|
while (p[lineLen] && p[lineLen] != '\r' && p[lineLen] != '\n') {
|
||||||
|
lineLen++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t nameOff = basMatchBeginForm(p, lineLen);
|
||||||
|
|
||||||
|
if (nameOff >= 0) {
|
||||||
|
p += nameOff;
|
||||||
|
|
||||||
int32_t ni = 0;
|
int32_t ni = 0;
|
||||||
|
|
||||||
|
|
@ -163,17 +178,12 @@ static inline bool basExtractFormName(const char *frmText, char *nameBuf, int32_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
// Outer "Begin Form ... End" extent (shared by bascomp, basBuild and the
|
|
||||||
// obfuscator)
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
|
|
||||||
// Returns the offset just past the line holding the End that closes the
|
// Returns the offset just past the line holding the End that closes the
|
||||||
// outermost Begin Form block in text[0..len), i.e. the length of the pure
|
// outermost Begin Form block in text[0..len), i.e. the length of the pure
|
||||||
// form-definition part. Everything after it is the .frm's BASIC code
|
// form-definition part. Everything after it is the .frm's BASIC code
|
||||||
// section. Returns len when no Begin Form block is found or closed.
|
// section. Returns len when no Begin Form block is found or closed.
|
||||||
// Lines are delimited by LF or CR LF; an End keyword counts only when it
|
// Lines are delimited by LF or CR LF; End lines follow frmIsEndLine, the
|
||||||
// stands alone or is followed by whitespace.
|
// parser's own rule.
|
||||||
static inline int32_t basFindFormEndPos(const char *text, int32_t len) {
|
static inline int32_t basFindFormEndPos(const char *text, int32_t len) {
|
||||||
int32_t nesting = 0;
|
int32_t nesting = 0;
|
||||||
bool inForm = false;
|
bool inForm = false;
|
||||||
|
|
@ -204,21 +214,13 @@ static inline int32_t basFindFormEndPos(const char *text, int32_t len) {
|
||||||
|
|
||||||
int32_t rest = lineEnd - l;
|
int32_t rest = lineEnd - l;
|
||||||
|
|
||||||
if (rest >= BAS_BEGIN_PREFIX_LEN && strncasecmp(text + l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
|
if (basMatchKeywordWs(text + l, rest, FRM_KW_BEGIN, FRM_KW_BEGIN_LEN) >= 0) {
|
||||||
if (!inForm) {
|
if (!inForm && basMatchBeginForm(text + l, rest) >= 0) {
|
||||||
int32_t r = l + BAS_BEGIN_PREFIX_LEN;
|
inForm = true;
|
||||||
|
|
||||||
while (r < lineEnd && (text[r] == ' ' || text[r] == '\t')) {
|
|
||||||
r++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lineEnd - r >= BAS_FORM_PREFIX_LEN && strncasecmp(text + r, "Form ", BAS_FORM_PREFIX_LEN) == 0) {
|
|
||||||
inForm = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nesting++;
|
nesting++;
|
||||||
} else if (rest >= BAS_END_KEYWORD_LEN && strncasecmp(text + l, "End", BAS_END_KEYWORD_LEN) == 0 && (rest == BAS_END_KEYWORD_LEN || text[l + BAS_END_KEYWORD_LEN] == ' ' || text[l + BAS_END_KEYWORD_LEN] == '\t')) {
|
} else if (frmIsEndLine(text + l, rest)) {
|
||||||
nesting--;
|
nesting--;
|
||||||
|
|
||||||
if (inForm && nesting == 0) {
|
if (inForm && nesting == 0) {
|
||||||
|
|
@ -230,4 +232,38 @@ static inline int32_t basFindFormEndPos(const char *text, int32_t len) {
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Match "Begin<ws>Form<ws>" at s[0..len) (s already left-trimmed). The
|
||||||
|
// single definition of the Begin Form syntax for basExtractFormName and
|
||||||
|
// basFindFormEndPos. Returns the offset of the form name, or -1.
|
||||||
|
static inline int32_t basMatchBeginForm(const char *s, int32_t len) {
|
||||||
|
int32_t b = basMatchKeywordWs(s, len, FRM_KW_BEGIN, FRM_KW_BEGIN_LEN);
|
||||||
|
|
||||||
|
if (b < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t f = basMatchKeywordWs(s + b, len - b, FRM_KW_FORM, FRM_KW_FORM_LEN);
|
||||||
|
|
||||||
|
return (f < 0) ? -1 : b + f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Match a keyword followed by at least one space or tab at s[0..len).
|
||||||
|
// Returns the offset of the first character after that whitespace, or
|
||||||
|
// -1 when the keyword does not match.
|
||||||
|
static inline int32_t basMatchKeywordWs(const char *s, int32_t len, const char *kw, int32_t kwLen) {
|
||||||
|
if (len <= kwLen || strncasecmp(s, kw, kwLen) != 0 || (s[kwLen] != ' ' && s[kwLen] != '\t')) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t i = kwLen;
|
||||||
|
|
||||||
|
while (i < len && (s[i] == ' ' || s[i] == '\t')) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // BAS_RES_H
|
#endif // BAS_RES_H
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
# IN THE SOFTWARE.
|
# IN THE SOFTWARE.
|
||||||
|
|
||||||
.section Libraries
|
.section Technical Reference (for Developers)|Libraries
|
||||||
.topic lib.basrt
|
.topic lib.basrt
|
||||||
.title BASIC Runtime Library
|
.title BASIC Runtime Library
|
||||||
.toc 0 BASIC Runtime Library
|
.toc 0 BASIC Runtime Library
|
||||||
|
|
@ -418,12 +418,14 @@ void basVmSetExternCallbacks(BasVmT *vm,
|
||||||
|
|
||||||
.h3 BasExternCallbacksT
|
.h3 BasExternCallbacksT
|
||||||
|
|
||||||
|
The compiler converts every argument to its declared parameter type, so callExtern receives values whose type is the declared type. The stock basExternCall pushes cdecl stack words by that type: STRING as a char pointer, SINGLE as one 4-byte float, DOUBLE as 8 bytes, everything else as one int32_t. DOUBLE and SINGLE returns are read from the x87 ST(0) register; INTEGER returns keep only the low 16 bits.
|
||||||
|
|
||||||
.table
|
.table
|
||||||
Field Signature Description
|
Field Signature Description
|
||||||
----- --------- -----------
|
----- --------- -----------
|
||||||
resolveExtern void *(*)(ctx, libName, funcName) Resolve a native function by library and symbol name. Cached after first call.
|
resolveExtern void *(*)(ctx, libName, funcName) Resolve a native function by library and symbol name. Cached after first call.
|
||||||
callExtern BasValueT (*)(ctx, funcPtr, funcName, args, argc, retType) Call a resolved native function, marshalling arguments and return value.
|
callExtern BasValueT (*)(ctx, funcPtr, libName, funcName, args, argc, retType) Call a resolved native function, marshalling arguments and return value.
|
||||||
ctx void * User pointer passed to both callbacks.
|
ctx void * User pointer passed to both callbacks.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.h2 Form Context
|
.h2 Form Context
|
||||||
|
|
|
||||||
|
|
@ -120,4 +120,10 @@ extern const char *basEventSuffixes[];
|
||||||
// Defined once in strip.c (the owner of basEventSuffixes).
|
// Defined once in strip.c (the owner of basEventSuffixes).
|
||||||
bool basEventSuffixMatch(const char *suffix);
|
bool basEventSuffixMatch(const char *suffix);
|
||||||
|
|
||||||
|
// Split an event handler name "<Prefix>_<Event>": true when name ends in
|
||||||
|
// '_' plus an event suffix and the prefix fits prefixSize (with its NUL).
|
||||||
|
// prefix receives the part before the last '_' and *outSuffix points at
|
||||||
|
// the event name inside name. Defined in strip.c.
|
||||||
|
bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix);
|
||||||
|
|
||||||
#endif // BAS_EVENTS_H
|
#endif // BAS_EVENTS_H
|
||||||
|
|
|
||||||
172
src/apps/kpunch/dvxbasic/compiler/basKeywords.h
Normal file
172
src/apps/kpunch/dvxbasic/compiler/basKeywords.h
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// basKeywords.h -- the BASIC reserved words: one list for the lexer and
|
||||||
|
// every syntax colorizer
|
||||||
|
//
|
||||||
|
// Data only, so code that cannot link the compiler (the TextArea widget's
|
||||||
|
// built-in BASIC colorizer) can include it too. Define
|
||||||
|
// BAS_KEYWORD(text, token, cls) before including this header to expand
|
||||||
|
// the list; text is the uppercase word, token its TOK_* value (ignore it
|
||||||
|
// outside the compiler) and cls its BasKeywordClassE. The entries are in
|
||||||
|
// strcmp order, so an expanded table can be searched with bsearch.
|
||||||
|
|
||||||
|
#ifndef DVXBASIC_BAS_KEYWORDS_H
|
||||||
|
#define DVXBASIC_BAS_KEYWORDS_H
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BAS_KW_CLASS_OTHER = 0, // control-flow / statement / operator keyword
|
||||||
|
BAS_KW_CLASS_TYPE = 1, // INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
|
||||||
|
BAS_KW_CLASS_LITERAL = 2 // TRUE, FALSE, NOTHING
|
||||||
|
} BasKeywordClassE;
|
||||||
|
|
||||||
|
#endif // DVXBASIC_BAS_KEYWORDS_H
|
||||||
|
|
||||||
|
#ifdef BAS_KEYWORD
|
||||||
|
BAS_KEYWORD("AND", TOK_AND, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("APP", TOK_APP, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("APPEND", TOK_APPEND, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("AS", TOK_AS, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("BASE", TOK_BASE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("BINARY", TOK_BINARY, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("BOOLEAN", TOK_BOOLEAN, BAS_KW_CLASS_TYPE)
|
||||||
|
BAS_KEYWORD("BYREF", TOK_BYREF, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("BYVAL", TOK_BYVAL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CALL", TOK_CALL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CASE", TOK_CASE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CHDIR", TOK_CHDIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CHDRIVE", TOK_CHDRIVE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CLOSE", TOK_CLOSE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CONST", TOK_CONST, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CREATECONTROL", TOK_CREATECONTROL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CREATEFORM", TOK_CREATEFORM, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CURDIR", TOK_CURDIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("CURDIR$", TOK_CURDIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DATA", TOK_DATA, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DECLARE", TOK_DECLARE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DEF", TOK_DEF, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DEFDBL", TOK_DEFDBL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DEFINT", TOK_DEFINT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DEFLNG", TOK_DEFLNG, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DEFSNG", TOK_DEFSNG, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DEFSTR", TOK_DEFSTR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DIM", TOK_DIM, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DIR", TOK_DIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DIR$", TOK_DIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DO", TOK_DO, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DOEVENTS", TOK_DOEVENTS, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("DOUBLE", TOK_DOUBLE, BAS_KW_CLASS_TYPE)
|
||||||
|
BAS_KEYWORD("ELSE", TOK_ELSE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("ELSEIF", TOK_ELSEIF, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("END", TOK_END, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("EOF", TOK_EOF_KW, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("EQV", TOK_EQV, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("ERASE", TOK_ERASE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("ERL", TOK_ERL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("ERR", TOK_ERR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("ERROR", TOK_ERROR_KW, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("EXIT", TOK_EXIT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("EXPLICIT", TOK_EXPLICIT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("FALSE", TOK_FALSE_KW, BAS_KW_CLASS_LITERAL)
|
||||||
|
BAS_KEYWORD("FILECOPY", TOK_FILECOPY, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("FILELEN", TOK_FILELEN, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("FOR", TOK_FOR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("FUNCTION", TOK_FUNCTION, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("GET", TOK_GET, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("GETATTR", TOK_GETATTR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("GOSUB", TOK_GOSUB, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("GOTO", TOK_GOTO, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("HIDE", TOK_HIDE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("IF", TOK_IF, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("IMP", TOK_IMP, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INIREAD", TOK_INIREAD, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INIREAD$", TOK_INIREAD, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INIWRITE", TOK_INIWRITE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INPUT", TOK_INPUT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INPUTBOX", TOK_INPUTBOX, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INPUTBOX$", TOK_INPUTBOX, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("INTEGER", TOK_INTEGER, BAS_KW_CLASS_TYPE)
|
||||||
|
BAS_KEYWORD("IS", TOK_IS, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("KILL", TOK_KILL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("LBOUND", TOK_LBOUND, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("LET", TOK_LET, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("LINE", TOK_LINE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("LOAD", TOK_LOAD, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("LONG", TOK_LONG, BAS_KW_CLASS_TYPE)
|
||||||
|
BAS_KEYWORD("LOOP", TOK_LOOP, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("LPRINT", TOK_LPRINT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("ME", TOK_ME, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("MKDIR", TOK_MKDIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("MOD", TOK_MOD, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("MSGBOX", TOK_MSGBOX, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("NAME", TOK_NAME, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("NEXT", TOK_NEXT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("NOT", TOK_NOT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("NOTHING", TOK_NOTHING, BAS_KW_CLASS_LITERAL)
|
||||||
|
BAS_KEYWORD("ON", TOK_ON, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("OPEN", TOK_OPEN, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("OPTION", TOK_OPTION, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("OPTIONAL", TOK_OPTIONAL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("OR", TOK_OR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("OUTPUT", TOK_OUTPUT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("PRESERVE", TOK_PRESERVE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("PRINT", TOK_PRINT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("PUT", TOK_PUT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("RANDOM", TOK_RANDOM, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("RANDOMIZE", TOK_RANDOMIZE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("READ", TOK_READ, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("REDIM", TOK_REDIM, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("REM", TOK_REM, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("REMOVECONTROL", TOK_REMOVECONTROL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("RESTORE", TOK_RESTORE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("RESUME", TOK_RESUME, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("RETURN", TOK_RETURN, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("RMDIR", TOK_RMDIR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SEEK", TOK_SEEK, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SELECT", TOK_SELECT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SET", TOK_SET, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SETATTR", TOK_SETATTR, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SETEVENT", TOK_SETEVENT, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SHARED", TOK_SHARED, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SHELL", TOK_SHELL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SHOW", TOK_SHOW, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SINGLE", TOK_SINGLE, BAS_KW_CLASS_TYPE)
|
||||||
|
BAS_KEYWORD("SLEEP", TOK_SLEEP, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("STATIC", TOK_STATIC, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("STEP", TOK_STEP, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("STRING", TOK_STRING_KW, BAS_KW_CLASS_TYPE)
|
||||||
|
BAS_KEYWORD("SUB", TOK_SUB, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("SWAP", TOK_SWAP, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("THEN", TOK_THEN, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("TIMER", TOK_TIMER, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("TO", TOK_TO, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("TRUE", TOK_TRUE_KW, BAS_KW_CLASS_LITERAL)
|
||||||
|
BAS_KEYWORD("TYPE", TOK_TYPE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("UBOUND", TOK_UBOUND, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("UNLOAD", TOK_UNLOAD, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("UNTIL", TOK_UNTIL, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("WEND", TOK_WEND, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("WHILE", TOK_WHILE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("WITH", TOK_WITH, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("WRITE", TOK_WRITE, BAS_KW_CLASS_OTHER)
|
||||||
|
BAS_KEYWORD("XOR", TOK_XOR, BAS_KW_CLASS_OTHER)
|
||||||
|
#endif // BAS_KEYWORD
|
||||||
|
|
@ -30,36 +30,42 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <strings.h>
|
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
|
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
|
||||||
void basAddData(BasCodeGenT *cg, BasValueT val);
|
void basAddData(BasCodeGenT *cg, BasValueT val);
|
||||||
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
|
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName, bool shared);
|
||||||
|
void basCodeGenAddFormVarInfo(BasCodeGenT *cg, const BasFormVarInfoT *info);
|
||||||
|
void basCodeGenAddUdtDef(BasCodeGenT *cg, const BasDebugUdtDefT *def);
|
||||||
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
||||||
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
|
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
|
||||||
void basCodeGenFree(BasCodeGenT *cg);
|
void basCodeGenFree(BasCodeGenT *cg);
|
||||||
void basCodeGenInit(BasCodeGenT *cg);
|
void basCodeGenInit(BasCodeGenT *cg);
|
||||||
|
void basCodeGenInsert(BasCodeGenT *cg, int32_t pos, const uint8_t *bytes, int32_t len);
|
||||||
int32_t basCodePos(const BasCodeGenT *cg);
|
int32_t basCodePos(const BasCodeGenT *cg);
|
||||||
void basEmit16(BasCodeGenT *cg, int16_t v);
|
void basEmit16(BasCodeGenT *cg, int16_t v);
|
||||||
|
void basEmit32(BasCodeGenT *cg, int32_t v);
|
||||||
void basEmit8(BasCodeGenT *cg, uint8_t b);
|
void basEmit8(BasCodeGenT *cg, uint8_t b);
|
||||||
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len);
|
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len);
|
||||||
void basEmitDouble(BasCodeGenT *cg, double v);
|
void basEmitDouble(BasCodeGenT *cg, double v);
|
||||||
void basEmitU16(BasCodeGenT *cg, uint16_t v);
|
void basEmitU16(BasCodeGenT *cg, uint16_t v);
|
||||||
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
|
|
||||||
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
|
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
|
||||||
static uint32_t constantHash(const char *text, int32_t len);
|
static uint32_t constantHash(const char *text, int32_t len);
|
||||||
|
static bool copyDebugInfo(BasModuleT *mod, const BasCodeGenT *cg);
|
||||||
static bool isEmittableProc(const BasSymbolT *s);
|
static bool isEmittableProc(const BasSymbolT *s);
|
||||||
static bool needsGlobalInit(const BasSymbolT *s);
|
static bool needsGlobalInit(const BasSymbolT *s);
|
||||||
|
|
||||||
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
|
|
||||||
// Intern: compare the precomputed hash and length before touching the
|
|
||||||
// string bytes so the scan over the pool is a few integer compares per
|
|
||||||
// entry instead of a memcmp.
|
|
||||||
uint32_t h = constantHash(text, len);
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < cg->constCount; i++) {
|
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
|
||||||
|
// Intern: walk only the hash bucket, comparing the precomputed hash
|
||||||
|
// and length before touching the string bytes.
|
||||||
|
uint32_t h = constantHash(text, len);
|
||||||
|
int32_t *bucket = &cg->constBuckets[h & (BAS_CONST_BUCKETS - 1)];
|
||||||
|
|
||||||
|
for (int32_t link = *bucket; link != 0; link = cg->constNext[link - 1]) {
|
||||||
|
int32_t i = link - 1;
|
||||||
|
|
||||||
if (cg->constHashes[i] == h && cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) {
|
if (cg->constHashes[i] == h && cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) {
|
||||||
return (uint16_t)i;
|
return (uint16_t)i;
|
||||||
}
|
}
|
||||||
|
|
@ -70,10 +76,13 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t idx = (uint16_t)cg->constCount;
|
uint16_t idx = (uint16_t)cg->constCount;
|
||||||
BasStringT *s = basStringNew(text, len);
|
BasStringT *s = basStringNew(text, len);
|
||||||
|
|
||||||
arrput(cg->constants, s);
|
arrput(cg->constants, s);
|
||||||
arrput(cg->constHashes, h);
|
arrput(cg->constHashes, h);
|
||||||
|
arrput(cg->constNext, *bucket);
|
||||||
|
*bucket = idx + 1;
|
||||||
cg->constCount = (int32_t)arrlen(cg->constants);
|
cg->constCount = (int32_t)arrlen(cg->constants);
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
@ -86,12 +95,13 @@ void basAddData(BasCodeGenT *cg, BasValueT val) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName) {
|
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName, bool shared) {
|
||||||
BasDebugVarT dv;
|
BasDebugVarT dv;
|
||||||
memset(&dv, 0, sizeof(dv));
|
memset(&dv, 0, sizeof(dv));
|
||||||
snprintf(dv.name, BAS_MAX_IDENT, "%s", name);
|
snprintf(dv.name, BAS_MAX_IDENT, "%s", name);
|
||||||
dv.scope = scope;
|
dv.scope = scope;
|
||||||
dv.dataType = dataType;
|
dv.dataType = dataType;
|
||||||
|
dv.shared = shared;
|
||||||
dv.index = index;
|
dv.index = index;
|
||||||
dv.procIndex = procIndex;
|
dv.procIndex = procIndex;
|
||||||
|
|
||||||
|
|
@ -103,6 +113,18 @@ void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void basCodeGenAddFormVarInfo(BasCodeGenT *cg, const BasFormVarInfoT *info) {
|
||||||
|
arrput(cg->formVarInfo, *info);
|
||||||
|
cg->formVarInfoCount = (int32_t)arrlen(cg->formVarInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void basCodeGenAddUdtDef(BasCodeGenT *cg, const BasDebugUdtDefT *def) {
|
||||||
|
arrput(cg->debugUdtDefs, *def);
|
||||||
|
cg->debugUdtDefCount = (int32_t)arrlen(cg->debugUdtDefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
|
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
|
||||||
// basVmLoadModule refuses a module with more globals than it has
|
// basVmLoadModule refuses a module with more globals than it has
|
||||||
// slots; never build one (the parser reports the error with a
|
// slots; never build one (the parser reports the error with a
|
||||||
|
|
@ -117,11 +139,17 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy code
|
mod->globalCount = cg->globalCount;
|
||||||
|
mod->entryPoint = 0;
|
||||||
|
|
||||||
|
// Every table is copied in full or the build fails: a module missing
|
||||||
|
// its constants, DATA, form variables or debug tables would load and
|
||||||
|
// then misbehave. Each count is set only once its array exists, so
|
||||||
|
// basModuleFree releases exactly what was copied.
|
||||||
mod->code = (uint8_t *)malloc(cg->codeLen);
|
mod->code = (uint8_t *)malloc(cg->codeLen);
|
||||||
|
|
||||||
if (!mod->code) {
|
if (!mod->code) {
|
||||||
free(mod);
|
basModuleFree(mod);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,94 +161,36 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
|
||||||
mod->constants = (BasStringT **)malloc(cg->constCount * sizeof(BasStringT *));
|
mod->constants = (BasStringT **)malloc(cg->constCount * sizeof(BasStringT *));
|
||||||
|
|
||||||
if (!mod->constants) {
|
if (!mod->constants) {
|
||||||
free(mod->code);
|
basModuleFree(mod);
|
||||||
free(mod);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < cg->constCount; i++) {
|
for (int32_t i = 0; i < cg->constCount; i++) {
|
||||||
mod->constants[i] = basStringRef(cg->constants[i]);
|
mod->constants[i] = basStringRef(cg->constants[i]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mod->constCount = cg->constCount;
|
mod->constCount = cg->constCount;
|
||||||
mod->globalCount = cg->globalCount;
|
}
|
||||||
mod->entryPoint = 0;
|
|
||||||
|
|
||||||
// Copy data pool
|
// Copy data pool
|
||||||
if (cg->dataCount > 0) {
|
if (cg->dataCount > 0) {
|
||||||
mod->dataPool = (BasValueT *)malloc(cg->dataCount * sizeof(BasValueT));
|
mod->dataPool = (BasValueT *)malloc(cg->dataCount * sizeof(BasValueT));
|
||||||
|
|
||||||
if (!mod->dataPool) {
|
if (!mod->dataPool) {
|
||||||
// Release the constant pool refs taken above before freeing.
|
basModuleFree(mod);
|
||||||
for (int32_t i = 0; i < cg->constCount; i++) {
|
|
||||||
basStringUnref(mod->constants[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(mod->constants);
|
|
||||||
free(mod->code);
|
|
||||||
free(mod);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < cg->dataCount; i++) {
|
for (int32_t i = 0; i < cg->dataCount; i++) {
|
||||||
mod->dataPool[i] = basValCopy(cg->dataPool[i]);
|
mod->dataPool[i] = basValCopy(cg->dataPool[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod->dataCount = cg->dataCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod->dataCount = cg->dataCount;
|
if (!copyDebugInfo(mod, cg)) {
|
||||||
|
basModuleFree(mod);
|
||||||
// Copy form variable info
|
return NULL;
|
||||||
if (cg->formVarInfoCount > 0) {
|
|
||||||
mod->formVarInfo = (BasFormVarInfoT *)malloc(cg->formVarInfoCount * sizeof(BasFormVarInfoT));
|
|
||||||
|
|
||||||
if (mod->formVarInfo) {
|
|
||||||
memcpy(mod->formVarInfo, cg->formVarInfo, cg->formVarInfoCount * sizeof(BasFormVarInfoT));
|
|
||||||
// Keep count in sync with the pointer: on OOM the array stays
|
|
||||||
// NULL, so leaving count nonzero would make consumers deref NULL.
|
|
||||||
mod->formVarInfoCount = cg->formVarInfoCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy debug variable info
|
|
||||||
if (cg->debugVarCount > 0) {
|
|
||||||
mod->debugVars = (BasDebugVarT *)malloc(cg->debugVarCount * sizeof(BasDebugVarT));
|
|
||||||
|
|
||||||
if (mod->debugVars) {
|
|
||||||
memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT));
|
|
||||||
mod->debugVarCount = cg->debugVarCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy UDT type definitions for debugger
|
|
||||||
if (cg->debugUdtDefCount > 0) {
|
|
||||||
mod->debugUdtDefs = (BasDebugUdtDefT *)malloc(cg->debugUdtDefCount * sizeof(BasDebugUdtDefT));
|
|
||||||
|
|
||||||
if (mod->debugUdtDefs) {
|
|
||||||
for (int32_t i = 0; i < cg->debugUdtDefCount; i++) {
|
|
||||||
mod->debugUdtDefs[i] = cg->debugUdtDefs[i];
|
|
||||||
mod->debugUdtDefs[i].fields = NULL;
|
|
||||||
|
|
||||||
if (cg->debugUdtDefs[i].fieldCount > 0 && cg->debugUdtDefs[i].fields) {
|
|
||||||
mod->debugUdtDefs[i].fields = (BasDebugFieldT *)malloc(
|
|
||||||
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
|
|
||||||
|
|
||||||
if (mod->debugUdtDefs[i].fields) {
|
|
||||||
memcpy(mod->debugUdtDefs[i].fields, cg->debugUdtDefs[i].fields,
|
|
||||||
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep count in sync with the pointer: on OOM the fields
|
|
||||||
// array stays NULL, so leaving the copied count nonzero
|
|
||||||
// would make consumers deref NULL.
|
|
||||||
if (mod->debugUdtDefs[i].fields == NULL) {
|
|
||||||
mod->debugUdtDefs[i].fieldCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod->debugUdtDefCount = cg->debugUdtDefCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
|
|
@ -255,24 +225,30 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Without either table the module is silently broken (STRING globals
|
||||||
|
// start as numeric 0, event handlers cannot be found), so an
|
||||||
|
// allocation failure fails the build.
|
||||||
if (globalInitCount > 0) {
|
if (globalInitCount > 0) {
|
||||||
mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT));
|
mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT));
|
||||||
|
|
||||||
if (mod->globalInits) {
|
if (!mod->globalInits) {
|
||||||
int32_t gi = 0;
|
basModuleFree(mod);
|
||||||
|
return NULL;
|
||||||
for (int32_t i = 0; i < tab->count; i++) {
|
|
||||||
BasSymbolT *s = tab->symbols[i];
|
|
||||||
|
|
||||||
if (needsGlobalInit(s)) {
|
|
||||||
mod->globalInits[gi].index = s->index;
|
|
||||||
mod->globalInits[gi].dataType = s->dataType;
|
|
||||||
gi++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod->globalInitCount = gi;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t gi = 0;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < tab->count; i++) {
|
||||||
|
BasSymbolT *s = tab->symbols[i];
|
||||||
|
|
||||||
|
if (needsGlobalInit(s)) {
|
||||||
|
mod->globalInits[gi].index = s->index;
|
||||||
|
mod->globalInits[gi].dataType = s->dataType;
|
||||||
|
gi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod->globalInitCount = gi;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (procCount == 0) {
|
if (procCount == 0) {
|
||||||
|
|
@ -282,7 +258,8 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
|
||||||
mod->procs = (BasProcEntryT *)malloc(procCount * sizeof(BasProcEntryT));
|
mod->procs = (BasProcEntryT *)malloc(procCount * sizeof(BasProcEntryT));
|
||||||
|
|
||||||
if (!mod->procs) {
|
if (!mod->procs) {
|
||||||
return mod;
|
basModuleFree(mod);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t idx = 0;
|
int32_t idx = 0;
|
||||||
|
|
@ -339,6 +316,7 @@ void basCodeGenFree(BasCodeGenT *cg) {
|
||||||
arrfree(cg->code);
|
arrfree(cg->code);
|
||||||
arrfree(cg->constants);
|
arrfree(cg->constants);
|
||||||
arrfree(cg->constHashes);
|
arrfree(cg->constHashes);
|
||||||
|
arrfree(cg->constNext);
|
||||||
arrfree(cg->dataPool);
|
arrfree(cg->dataPool);
|
||||||
arrfree(cg->formVarInfo);
|
arrfree(cg->formVarInfo);
|
||||||
arrfree(cg->debugVars);
|
arrfree(cg->debugVars);
|
||||||
|
|
@ -360,13 +338,31 @@ void basCodeGenInit(BasCodeGenT *cg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void basCodeGenInsert(BasCodeGenT *cg, int32_t pos, const uint8_t *bytes, int32_t len) {
|
||||||
|
arrinsn(cg->code, pos, len);
|
||||||
|
memcpy(&cg->code[pos], bytes, len);
|
||||||
|
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int32_t basCodePos(const BasCodeGenT *cg) {
|
int32_t basCodePos(const BasCodeGenT *cg) {
|
||||||
return cg->codeLen;
|
return cg->codeLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void basEmit16(BasCodeGenT *cg, int16_t v) {
|
void basEmit16(BasCodeGenT *cg, int16_t v) {
|
||||||
basEmitBytes(cg, &v, (int32_t)sizeof(v));
|
uint8_t buf[sizeof(v)];
|
||||||
|
|
||||||
|
basWriteI16LE(buf, v);
|
||||||
|
basEmitBytes(cg, buf, (int32_t)sizeof(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void basEmit32(BasCodeGenT *cg, int32_t v) {
|
||||||
|
uint8_t buf[sizeof(v)];
|
||||||
|
|
||||||
|
basWriteI32LE(buf, v);
|
||||||
|
basEmitBytes(cg, buf, (int32_t)sizeof(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -377,12 +373,7 @@ void basEmit8(BasCodeGenT *cg, uint8_t b) {
|
||||||
|
|
||||||
|
|
||||||
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len) {
|
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len) {
|
||||||
const uint8_t *p = (const uint8_t *)data;
|
memcpy(arraddnptr(cg->code, len), data, len);
|
||||||
|
|
||||||
for (int32_t i = 0; i < len; i++) {
|
|
||||||
arrput(cg->code, p[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
cg->codeLen = (int32_t)arrlen(cg->code);
|
cg->codeLen = (int32_t)arrlen(cg->code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,30 +383,17 @@ void basEmitDouble(BasCodeGenT *cg, double v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void basEmitU16(BasCodeGenT *cg, uint16_t v) {
|
void basEmitU16(BasCodeGenT *cg, uint16_t v) {
|
||||||
basEmitBytes(cg, &v, (int32_t)sizeof(v));
|
uint8_t buf[sizeof(v)];
|
||||||
}
|
|
||||||
|
|
||||||
|
basWriteU16LE(buf, v);
|
||||||
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) {
|
basEmitBytes(cg, buf, (int32_t)sizeof(buf));
|
||||||
if (!mod || !mod->procs || !name) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
|
||||||
if (strcasecmp(mod->procs[i].name, name) == 0) {
|
|
||||||
return &mod->procs[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
|
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
|
||||||
if (pos >= 0 && pos + 2 <= cg->codeLen) {
|
if (pos >= 0 && pos + (int32_t)sizeof(val) <= cg->codeLen) {
|
||||||
memcpy(&cg->code[pos], &val, 2);
|
basWriteI16LE(&cg->code[pos], val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,6 +411,66 @@ static uint32_t constantHash(const char *text, int32_t len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Copy the form-variable and debugger tables into mod. Returns false on
|
||||||
|
// an allocation failure; each count is set only once its array exists, so
|
||||||
|
// the caller's basModuleFree releases exactly what was copied.
|
||||||
|
static bool copyDebugInfo(BasModuleT *mod, const BasCodeGenT *cg) {
|
||||||
|
if (cg->formVarInfoCount > 0) {
|
||||||
|
mod->formVarInfo = (BasFormVarInfoT *)malloc(cg->formVarInfoCount * sizeof(BasFormVarInfoT));
|
||||||
|
|
||||||
|
if (!mod->formVarInfo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(mod->formVarInfo, cg->formVarInfo, cg->formVarInfoCount * sizeof(BasFormVarInfoT));
|
||||||
|
mod->formVarInfoCount = cg->formVarInfoCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cg->debugVarCount > 0) {
|
||||||
|
mod->debugVars = (BasDebugVarT *)malloc(cg->debugVarCount * sizeof(BasDebugVarT));
|
||||||
|
|
||||||
|
if (!mod->debugVars) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT));
|
||||||
|
mod->debugVarCount = cg->debugVarCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cg->debugUdtDefCount > 0) {
|
||||||
|
mod->debugUdtDefs = (BasDebugUdtDefT *)calloc(cg->debugUdtDefCount, sizeof(BasDebugUdtDefT));
|
||||||
|
|
||||||
|
if (!mod->debugUdtDefs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The calloc left every fields pointer NULL, so a failure part way
|
||||||
|
// through leaves nothing basModuleFree cannot release.
|
||||||
|
mod->debugUdtDefCount = cg->debugUdtDefCount;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < cg->debugUdtDefCount; i++) {
|
||||||
|
const BasDebugUdtDefT *src = &cg->debugUdtDefs[i];
|
||||||
|
|
||||||
|
mod->debugUdtDefs[i] = *src;
|
||||||
|
mod->debugUdtDefs[i].fields = NULL;
|
||||||
|
|
||||||
|
if (src->fieldCount > 0) {
|
||||||
|
mod->debugUdtDefs[i].fields = (BasDebugFieldT *)malloc(src->fieldCount * sizeof(BasDebugFieldT));
|
||||||
|
|
||||||
|
if (!mod->debugUdtDefs[i].fields) {
|
||||||
|
mod->debugUdtDefs[i].fieldCount = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(mod->debugUdtDefs[i].fields, src->fields, src->fieldCount * sizeof(BasDebugFieldT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// A SUB/FUNCTION that has a body in this module (not a forward stub or
|
// A SUB/FUNCTION that has a body in this module (not a forward stub or
|
||||||
// a DECLARE LIBRARY extern) and therefore gets a proc-table entry.
|
// a DECLARE LIBRARY extern) and therefore gets a proc-table entry.
|
||||||
static bool isEmittableProc(const BasSymbolT *s) {
|
static bool isEmittableProc(const BasSymbolT *s) {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
|
|
||||||
#include "../runtime/vm.h"
|
#include "../runtime/vm.h"
|
||||||
#include "../runtime/values.h"
|
#include "../runtime/values.h"
|
||||||
|
#include "../runtime/serialize.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
@ -40,15 +41,24 @@
|
||||||
// Constant pool index is emitted as uint16_t; pool cannot exceed this.
|
// Constant pool index is emitted as uint16_t; pool cannot exceed this.
|
||||||
#define BAS_MAX_CONSTANTS 0x10000
|
#define BAS_MAX_CONSTANTS 0x10000
|
||||||
|
|
||||||
|
// Hash buckets for constant-pool interning (power of two).
|
||||||
|
#define BAS_CONST_BUCKETS 1024
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Code generator state
|
// Code generator state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// The count fields mirror the stb_ds array lengths for the public
|
||||||
|
// BasModuleT copy; only codegen.c writes them (every append goes through
|
||||||
|
// a basCodeGen* / basEmit* call), so they never drift from the arrays.
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t *code; // stb_ds dynamic array
|
uint8_t *code; // stb_ds dynamic array
|
||||||
int32_t codeLen;
|
int32_t codeLen;
|
||||||
BasStringT **constants; // stb_ds dynamic array
|
BasStringT **constants; // stb_ds dynamic array
|
||||||
uint32_t *constHashes; // stb_ds dynamic array, parallel to constants (FNV-1a of the text)
|
uint32_t *constHashes; // stb_ds dynamic array, parallel to constants (FNV-1a of the text)
|
||||||
|
int32_t *constNext; // stb_ds dynamic array, parallel to constants: 1 + index of the next entry in the same bucket (0 = end)
|
||||||
|
int32_t constBuckets[BAS_CONST_BUCKETS]; // 1 + index of the newest entry per hash bucket (0 = empty)
|
||||||
int32_t constCount;
|
int32_t constCount;
|
||||||
int32_t globalCount;
|
int32_t globalCount;
|
||||||
BasValueT *dataPool; // stb_ds dynamic array
|
BasValueT *dataPool; // stb_ds dynamic array
|
||||||
|
|
@ -79,7 +89,8 @@ void basEmit16(BasCodeGenT *cg, int16_t v);
|
||||||
// Emit 16-bit unsigned value
|
// Emit 16-bit unsigned value
|
||||||
void basEmitU16(BasCodeGenT *cg, uint16_t v);
|
void basEmitU16(BasCodeGenT *cg, uint16_t v);
|
||||||
|
|
||||||
// Emit 32-bit float
|
// Emit 32-bit signed value
|
||||||
|
void basEmit32(BasCodeGenT *cg, int32_t v);
|
||||||
|
|
||||||
// Emit 64-bit double
|
// Emit 64-bit double
|
||||||
void basEmitDouble(BasCodeGenT *cg, double v);
|
void basEmitDouble(BasCodeGenT *cg, double v);
|
||||||
|
|
@ -90,12 +101,21 @@ int32_t basCodePos(const BasCodeGenT *cg);
|
||||||
// Patch a 16-bit value at a previous position (for backpatching jumps)
|
// Patch a 16-bit value at a previous position (for backpatching jumps)
|
||||||
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
|
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
|
||||||
|
|
||||||
|
// Insert len bytes at pos, moving the code after it up.
|
||||||
|
void basCodeGenInsert(BasCodeGenT *cg, int32_t pos, const uint8_t *bytes, int32_t len);
|
||||||
|
|
||||||
// Add a string to the constant pool. Returns the pool index.
|
// Add a string to the constant pool. Returns the pool index.
|
||||||
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
|
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
|
||||||
|
|
||||||
// Adds a value to the data pool (for DATA statements).
|
// Adds a value to the data pool (for DATA statements).
|
||||||
void basAddData(BasCodeGenT *cg, BasValueT val);
|
void basAddData(BasCodeGenT *cg, BasValueT val);
|
||||||
|
|
||||||
|
// Record the per-form variable block of a BEGINFORM ... ENDFORM section.
|
||||||
|
void basCodeGenAddFormVarInfo(BasCodeGenT *cg, const BasFormVarInfoT *info);
|
||||||
|
|
||||||
|
// Record a TYPE definition for the debugger (takes ownership of def->fields).
|
||||||
|
void basCodeGenAddUdtDef(BasCodeGenT *cg, const BasDebugUdtDefT *def);
|
||||||
|
|
||||||
// Build a BasModuleT from the generated code. The caller takes
|
// Build a BasModuleT from the generated code. The caller takes
|
||||||
// ownership of the module and must free it with basModuleFree().
|
// ownership of the module and must free it with basModuleFree().
|
||||||
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
||||||
|
|
@ -104,15 +124,8 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
|
||||||
// symtab is a BasSymTabT* (cast to void* to avoid circular include).
|
// symtab is a BasSymTabT* (cast to void* to avoid circular include).
|
||||||
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
|
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
|
||||||
|
|
||||||
// Free a module built by basCodeGenBuildModule.
|
|
||||||
void basModuleFree(BasModuleT *mod);
|
|
||||||
|
|
||||||
// Add a debug variable entry (for debugger variable display).
|
// Add a debug variable entry (for debugger variable display).
|
||||||
// formName is only used for SCOPE_FORM vars (NULL or "" for others).
|
// formName is only used for SCOPE_FORM vars (NULL or "" for others).
|
||||||
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
|
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName, bool shared);
|
||||||
|
|
||||||
// Find a procedure by name in a module's procedure table.
|
|
||||||
// Case-insensitive. Returns NULL if not found.
|
|
||||||
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
|
|
||||||
|
|
||||||
#endif // DVXBASIC_CODEGEN_H
|
#endif // DVXBASIC_CODEGEN_H
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,9 @@
|
||||||
// - BasProcEntryT::codeAddr (absolute)
|
// - BasProcEntryT::codeAddr (absolute)
|
||||||
// - BasFormVarInfoT::initCodeAddr (absolute, negative = no init)
|
// - BasFormVarInfoT::initCodeAddr (absolute, negative = no init)
|
||||||
// - OP_CALL operand (absolute uint16)
|
// - OP_CALL operand (absolute uint16)
|
||||||
// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE operand (relative int16)
|
// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE / OP_GOSUB operand (relative int16)
|
||||||
// - OP_FOR_NEXT loopTop operand (relative int16)
|
// - OP_FOR_INIT / OP_FOR_NEXT offset operand (relative int16)
|
||||||
// - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped)
|
// - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped)
|
||||||
// - GOSUB return address (emitted as OP_PUSH_INT32 followed by OP_JMP,
|
|
||||||
// where the pushed value equals the PC immediately after the JMP)
|
|
||||||
//
|
//
|
||||||
// Safety: if any opcode cannot be sized, any jump overflows int16, or
|
// Safety: if any opcode cannot be sized, any jump overflows int16, or
|
||||||
// the walk doesn't reach codeLen exactly, the module is left untouched.
|
// the walk doesn't reach codeLen exactly, the module is left untouched.
|
||||||
|
|
@ -57,17 +55,18 @@
|
||||||
#define BAS_LINE_INST_SIZE (1 + BAS_OPERAND_U16) // OP_LINE [uint16 lineNum]
|
#define BAS_LINE_INST_SIZE (1 + BAS_OPERAND_U16) // OP_LINE [uint16 lineNum]
|
||||||
#define BAS_STMT_INST_SIZE 1 // OP_STMT
|
#define BAS_STMT_INST_SIZE 1 // OP_STMT
|
||||||
|
|
||||||
|
// Operand position and size of the address-carrying instructions.
|
||||||
|
#define BAS_ADDR_OPERAND_OFFSET 1 // operand directly after the opcode
|
||||||
|
#define BAS_JUMP_INST_SIZE (1 + BAS_OPERAND_U16) // OP_JMP* / OP_GOSUB / OP_ON_ERROR [int16 offset]
|
||||||
|
#define BAS_FOR_INST_SIZE (1 + BAS_OPERAND_FOR) // OP_FOR_INIT / OP_FOR_NEXT
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
int32_t basCompactBytecode(BasModuleT *mod);
|
int32_t basCompactBytecode(BasModuleT *mod);
|
||||||
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt, int32_t *outNewLen);
|
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt, int32_t *outNewLen);
|
||||||
static int16_t readI16LE(const uint8_t *p);
|
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t codeLen);
|
||||||
static uint16_t readU16LE(const uint8_t *p);
|
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, bool allowZero);
|
||||||
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t codeLen, int32_t newCodeLen);
|
|
||||||
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero);
|
|
||||||
static void writeI16LE(uint8_t *p, int16_t v);
|
|
||||||
static void writeI32LE(uint8_t *p, int32_t v);
|
|
||||||
static void writeU16LE(uint8_t *p, uint16_t v);
|
|
||||||
|
|
||||||
int32_t basCompactBytecode(BasModuleT *mod) {
|
int32_t basCompactBytecode(BasModuleT *mod) {
|
||||||
if (!mod || !mod->code || mod->codeLen <= 0) {
|
if (!mod || !mod->code || mod->codeLen <= 0) {
|
||||||
|
|
@ -158,86 +157,31 @@ int32_t basCompactBytecode(BasModuleT *mod) {
|
||||||
|
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case OP_CALL: {
|
case OP_CALL: {
|
||||||
uint16_t oldAddr = readU16LE(oldCode + oldPc + 1);
|
uint16_t oldAddr = basReadU16LE(oldCode + oldPc + BAS_ADDR_OPERAND_OFFSET);
|
||||||
|
|
||||||
if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, oldCodeLen, newCodeLen)) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ok = remapAbsU16(newCode, newPc, BAS_ADDR_OPERAND_OFFSET, oldAddr, remap, oldCodeLen);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case OP_JMP:
|
case OP_JMP:
|
||||||
case OP_JMP_TRUE:
|
case OP_JMP_TRUE:
|
||||||
case OP_JMP_FALSE: {
|
case OP_JMP_FALSE:
|
||||||
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
|
case OP_GOSUB:
|
||||||
|
|
||||||
if (!remapRelI16(newCode, newPc, 1, oldOff,
|
|
||||||
oldPc + 3, newPc + 3,
|
|
||||||
remap, oldCodeLen, newCodeLen, false)) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OP_FOR_NEXT: {
|
|
||||||
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_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: {
|
case OP_ON_ERROR: {
|
||||||
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
|
// ON ERROR's offset 0 means "disable" and is kept as is.
|
||||||
|
int16_t oldOff = basReadI16LE(oldCode + oldPc + BAS_ADDR_OPERAND_OFFSET);
|
||||||
if (!remapRelI16(newCode, newPc, 1, oldOff,
|
|
||||||
oldPc + 3, newPc + 3,
|
|
||||||
remap, oldCodeLen, newCodeLen, true)) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ok = remapRelI16(newCode, newPc, BAS_ADDR_OPERAND_OFFSET, oldOff, oldPc + BAS_JUMP_INST_SIZE, newPc + BAS_JUMP_INST_SIZE, remap, oldCodeLen, op == OP_ON_ERROR);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case OP_PUSH_INT32: {
|
case OP_FOR_INIT:
|
||||||
// Detect GOSUB return-address push and remap the absolute address.
|
case OP_FOR_NEXT: {
|
||||||
if (basIsGosubPush(oldCode, oldCodeLen, oldPc)) {
|
// [uint16 varIdx] [uint8 scope] [int16 offset]: FOR_INIT's
|
||||||
int32_t oldAddr = basReadI32LE(oldCode + oldPc + 1);
|
// offset skips an empty loop, FOR_NEXT's jumps back to the body.
|
||||||
|
int16_t oldOff = basReadI16LE(oldCode + oldPc + BAS_FOR_OFFSET_OPERAND);
|
||||||
if (oldAddr < 0 || oldAddr > oldCodeLen) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t newAddr = remap[oldAddr];
|
|
||||||
|
|
||||||
if (newAddr < 0 || newAddr > newCodeLen) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeI32LE(newCode + newPc + 1, newAddr);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ok = remapRelI16(newCode, newPc, BAS_FOR_OFFSET_OPERAND, oldOff, oldPc + BAS_FOR_INST_SIZE, newPc + BAS_FOR_INST_SIZE, remap, oldCodeLen, false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,11 +225,12 @@ int32_t basCompactBytecode(BasModuleT *mod) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t oldLen = mod->formVarInfo[i].initCodeLen;
|
int32_t oldEnd = oldAddr + mod->formVarInfo[i].initCodeLen;
|
||||||
int32_t oldEnd = oldAddr + oldLen;
|
|
||||||
|
|
||||||
if (oldEnd > oldCodeLen) {
|
if (mod->formVarInfo[i].initCodeLen < 0 || oldEnd > oldCodeLen) {
|
||||||
oldEnd = oldCodeLen;
|
free(newCode);
|
||||||
|
free(remap);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t newAddr = remap[oldAddr];
|
int32_t newAddr = remap[oldAddr];
|
||||||
|
|
@ -296,10 +241,14 @@ int32_t basCompactBytecode(BasModuleT *mod) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite entry point
|
// Rewrite entry point
|
||||||
if (mod->entryPoint >= 0 && mod->entryPoint <= oldCodeLen) {
|
if (mod->entryPoint < 0 || mod->entryPoint > oldCodeLen) {
|
||||||
mod->entryPoint = remap[mod->entryPoint];
|
free(newCode);
|
||||||
|
free(remap);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod->entryPoint = remap[mod->entryPoint];
|
||||||
|
|
||||||
// Swap in the new code
|
// Swap in the new code
|
||||||
free(mod->code);
|
free(mod->code);
|
||||||
mod->code = newCode;
|
mod->code = newCode;
|
||||||
|
|
@ -371,26 +320,13 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Little-endian helpers (bytecode is always LE regardless of host)
|
|
||||||
// ============================================================
|
|
||||||
static int16_t readI16LE(const uint8_t *p) {
|
|
||||||
return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static uint16_t readU16LE(const uint8_t *p) {
|
|
||||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Apply remap to a single instruction's operand
|
// Apply remap to a single instruction's operand
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Returns true on success, false if an offset overflows int16 or a
|
// Returns true on success, false if an offset overflows int16 or a
|
||||||
// target doesn't land on a valid instruction in the old code.
|
// target doesn't land on a valid instruction in the old code.
|
||||||
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t codeLen, int32_t newCodeLen) {
|
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t codeLen) {
|
||||||
// oldAddr is an arbitrary uint16 operand; remap[] only has codeLen+1
|
// oldAddr is an arbitrary uint16 operand; remap[] only has codeLen+1
|
||||||
// entries, so a corrupt CALL address would index out of bounds.
|
// entries, so a corrupt CALL address would index out of bounds.
|
||||||
if (oldAddr > codeLen) {
|
if (oldAddr > codeLen) {
|
||||||
|
|
@ -399,15 +335,11 @@ static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffse
|
||||||
|
|
||||||
int32_t newAddr = remap[oldAddr];
|
int32_t newAddr = remap[oldAddr];
|
||||||
|
|
||||||
if (newAddr < 0 || newAddr > newCodeLen) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAddr > BAS_OPERAND_U16_MAX) {
|
if (newAddr > BAS_OPERAND_U16_MAX) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeU16LE(newCode + newOpPos + operandOffset, (uint16_t)newAddr);
|
basWriteU16LE(newCode + newOpPos + operandOffset, (uint16_t)newAddr);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,9 +352,9 @@ static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffse
|
||||||
//
|
//
|
||||||
// Handles ON_ERROR's special case (offset == 0 means "disable").
|
// Handles ON_ERROR's special case (offset == 0 means "disable").
|
||||||
// For ON_ERROR the caller passes allowZero=true; the zero is preserved as-is.
|
// For ON_ERROR the caller passes allowZero=true; the zero is preserved as-is.
|
||||||
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero) {
|
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, bool allowZero) {
|
||||||
if (allowZero && oldOffset == 0) {
|
if (allowZero && oldOffset == 0) {
|
||||||
writeI16LE(newCode + newOpPos + operandOffset, 0);
|
basWriteI16LE(newCode + newOpPos + operandOffset, 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,13 +364,7 @@ static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffse
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t newTarget = remap[oldTarget];
|
int32_t newOffset = remap[oldTarget] - newPcAfter;
|
||||||
|
|
||||||
if (newTarget < 0 || newTarget > newCodeLen) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t newOffset = newTarget - newPcAfter;
|
|
||||||
|
|
||||||
// A nonzero ON ERROR handler offset that collapses to 0 would be misread
|
// A nonzero ON ERROR handler offset that collapses to 0 would be misread
|
||||||
// by the VM as "disable handler" (0 is the disable sentinel). The
|
// by the VM as "disable handler" (0 is the disable sentinel). The
|
||||||
|
|
@ -453,28 +379,6 @@ static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffse
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset);
|
basWriteI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void writeI16LE(uint8_t *p, int16_t v) {
|
|
||||||
uint16_t u = (uint16_t)v;
|
|
||||||
p[0] = (uint8_t)(u & 0xFF);
|
|
||||||
p[1] = (uint8_t)((u >> 8) & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void writeI32LE(uint8_t *p, int32_t v) {
|
|
||||||
uint32_t u = (uint32_t)v;
|
|
||||||
p[0] = (uint8_t)(u & 0xFF);
|
|
||||||
p[1] = (uint8_t)((u >> 8) & 0xFF);
|
|
||||||
p[2] = (uint8_t)((u >> 16) & 0xFF);
|
|
||||||
p[3] = (uint8_t)((u >> 24) & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void writeU16LE(uint8_t *p, uint16_t v) {
|
|
||||||
p[0] = (uint8_t)(v & 0xFF);
|
|
||||||
p[1] = (uint8_t)((v >> 8) & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
#include "lexer.h"
|
#include "lexer.h"
|
||||||
#include "opcodes.h"
|
#include "opcodes.h"
|
||||||
|
#include "dvxTypes.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
@ -41,146 +42,33 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *text;
|
const char *text;
|
||||||
uint8_t textLen; // precomputed so lookupKeyword() can length-reject cheaply
|
uint8_t textLen; // precomputed so lookupKeyword() can length-reject cheaply
|
||||||
BasTokenTypeE type;
|
BasTokenTypeE type;
|
||||||
|
BasKeywordClassE cls;
|
||||||
} KeywordEntryT;
|
} KeywordEntryT;
|
||||||
|
|
||||||
#define KW(s, t) { s, (uint8_t)(sizeof(s) - 1), t }
|
|
||||||
|
|
||||||
static const KeywordEntryT sKeywords[] = {
|
static const KeywordEntryT sKeywords[] = {
|
||||||
KW("AND", TOK_AND),
|
#define BAS_KEYWORD(s, t, c) { s, (uint8_t)(sizeof(s) - 1), t, c },
|
||||||
KW("APP", TOK_APP),
|
#include "basKeywords.h"
|
||||||
KW("APPEND", TOK_APPEND),
|
#undef BAS_KEYWORD
|
||||||
KW("AS", TOK_AS),
|
{ NULL, 0, TOK_ERROR, BAS_KW_CLASS_OTHER }
|
||||||
KW("BASE", TOK_BASE),
|
|
||||||
KW("BINARY", TOK_BINARY),
|
|
||||||
KW("BOOLEAN", TOK_BOOLEAN),
|
|
||||||
KW("BYREF", TOK_BYREF),
|
|
||||||
KW("BYVAL", TOK_BYVAL),
|
|
||||||
KW("CALL", TOK_CALL),
|
|
||||||
KW("CASE", TOK_CASE),
|
|
||||||
KW("CHDIR", TOK_CHDIR),
|
|
||||||
KW("CHDRIVE", TOK_CHDRIVE),
|
|
||||||
KW("CLOSE", TOK_CLOSE),
|
|
||||||
KW("CONST", TOK_CONST),
|
|
||||||
KW("CREATECONTROL", TOK_CREATECONTROL),
|
|
||||||
KW("CREATEFORM", TOK_CREATEFORM),
|
|
||||||
KW("CURDIR", TOK_CURDIR),
|
|
||||||
KW("CURDIR$", TOK_CURDIR),
|
|
||||||
KW("DATA", TOK_DATA),
|
|
||||||
KW("DECLARE", TOK_DECLARE),
|
|
||||||
KW("DEF", TOK_DEF),
|
|
||||||
KW("DEFDBL", TOK_DEFDBL),
|
|
||||||
KW("DEFINT", TOK_DEFINT),
|
|
||||||
KW("DEFLNG", TOK_DEFLNG),
|
|
||||||
KW("DEFSNG", TOK_DEFSNG),
|
|
||||||
KW("DEFSTR", TOK_DEFSTR),
|
|
||||||
KW("DIM", TOK_DIM),
|
|
||||||
KW("DIR", TOK_DIR),
|
|
||||||
KW("DIR$", TOK_DIR),
|
|
||||||
KW("DO", TOK_DO),
|
|
||||||
KW("DOEVENTS", TOK_DOEVENTS),
|
|
||||||
KW("DOUBLE", TOK_DOUBLE),
|
|
||||||
KW("ELSE", TOK_ELSE),
|
|
||||||
KW("ELSEIF", TOK_ELSEIF),
|
|
||||||
KW("END", TOK_END),
|
|
||||||
KW("EOF", TOK_EOF_KW),
|
|
||||||
KW("EQV", TOK_EQV),
|
|
||||||
KW("ERASE", TOK_ERASE),
|
|
||||||
KW("ERL", TOK_ERL),
|
|
||||||
KW("ERR", TOK_ERR),
|
|
||||||
KW("ERROR", TOK_ERROR_KW),
|
|
||||||
KW("EXIT", TOK_EXIT),
|
|
||||||
KW("EXPLICIT", TOK_EXPLICIT),
|
|
||||||
KW("FALSE", TOK_FALSE_KW),
|
|
||||||
KW("FILECOPY", TOK_FILECOPY),
|
|
||||||
KW("FILELEN", TOK_FILELEN),
|
|
||||||
KW("FOR", TOK_FOR),
|
|
||||||
KW("FUNCTION", TOK_FUNCTION),
|
|
||||||
KW("GET", TOK_GET),
|
|
||||||
KW("GETATTR", TOK_GETATTR),
|
|
||||||
KW("GOSUB", TOK_GOSUB),
|
|
||||||
KW("GOTO", TOK_GOTO),
|
|
||||||
KW("HIDE", TOK_HIDE),
|
|
||||||
KW("IF", TOK_IF),
|
|
||||||
KW("IMP", TOK_IMP),
|
|
||||||
KW("INIREAD", TOK_INIREAD),
|
|
||||||
KW("INIREAD$", TOK_INIREAD),
|
|
||||||
KW("INIWRITE", TOK_INIWRITE),
|
|
||||||
KW("INPUT", TOK_INPUT),
|
|
||||||
KW("INPUTBOX", TOK_INPUTBOX),
|
|
||||||
KW("INPUTBOX$", TOK_INPUTBOX),
|
|
||||||
KW("INTEGER", TOK_INTEGER),
|
|
||||||
KW("IS", TOK_IS),
|
|
||||||
KW("KILL", TOK_KILL),
|
|
||||||
KW("LBOUND", TOK_LBOUND),
|
|
||||||
KW("LET", TOK_LET),
|
|
||||||
KW("LINE", TOK_LINE),
|
|
||||||
KW("LOAD", TOK_LOAD),
|
|
||||||
KW("LONG", TOK_LONG),
|
|
||||||
KW("LOOP", TOK_LOOP),
|
|
||||||
KW("ME", TOK_ME),
|
|
||||||
KW("MKDIR", TOK_MKDIR),
|
|
||||||
KW("MOD", TOK_MOD),
|
|
||||||
KW("MSGBOX", TOK_MSGBOX),
|
|
||||||
KW("NAME", TOK_NAME),
|
|
||||||
KW("NEXT", TOK_NEXT),
|
|
||||||
KW("NOT", TOK_NOT),
|
|
||||||
KW("NOTHING", TOK_NOTHING),
|
|
||||||
KW("ON", TOK_ON),
|
|
||||||
KW("OPEN", TOK_OPEN),
|
|
||||||
KW("OPTION", TOK_OPTION),
|
|
||||||
KW("OPTIONAL", TOK_OPTIONAL),
|
|
||||||
KW("OR", TOK_OR),
|
|
||||||
KW("OUTPUT", TOK_OUTPUT),
|
|
||||||
KW("PRESERVE", TOK_PRESERVE),
|
|
||||||
KW("PRINT", TOK_PRINT),
|
|
||||||
KW("PUT", TOK_PUT),
|
|
||||||
KW("RANDOM", TOK_RANDOM),
|
|
||||||
KW("RANDOMIZE", TOK_RANDOMIZE),
|
|
||||||
KW("READ", TOK_READ),
|
|
||||||
KW("REDIM", TOK_REDIM),
|
|
||||||
KW("REM", TOK_REM),
|
|
||||||
KW("REMOVECONTROL", TOK_REMOVECONTROL),
|
|
||||||
KW("RESTORE", TOK_RESTORE),
|
|
||||||
KW("RESUME", TOK_RESUME),
|
|
||||||
KW("RETURN", TOK_RETURN),
|
|
||||||
KW("RMDIR", TOK_RMDIR),
|
|
||||||
KW("SEEK", TOK_SEEK),
|
|
||||||
KW("SELECT", TOK_SELECT),
|
|
||||||
KW("SET", TOK_SET),
|
|
||||||
KW("SETATTR", TOK_SETATTR),
|
|
||||||
KW("SETEVENT", TOK_SETEVENT),
|
|
||||||
KW("SHARED", TOK_SHARED),
|
|
||||||
KW("SHELL", TOK_SHELL),
|
|
||||||
KW("SHOW", TOK_SHOW),
|
|
||||||
KW("SINGLE", TOK_SINGLE),
|
|
||||||
KW("SLEEP", TOK_SLEEP),
|
|
||||||
KW("STATIC", TOK_STATIC),
|
|
||||||
KW("STEP", TOK_STEP),
|
|
||||||
KW("STRING", TOK_STRING_KW),
|
|
||||||
KW("SUB", TOK_SUB),
|
|
||||||
KW("SWAP", TOK_SWAP),
|
|
||||||
KW("THEN", TOK_THEN),
|
|
||||||
KW("TIMER", TOK_TIMER),
|
|
||||||
KW("TO", TOK_TO),
|
|
||||||
KW("TRUE", TOK_TRUE_KW),
|
|
||||||
KW("TYPE", TOK_TYPE),
|
|
||||||
KW("UBOUND", TOK_UBOUND),
|
|
||||||
KW("UNLOAD", TOK_UNLOAD),
|
|
||||||
KW("UNTIL", TOK_UNTIL),
|
|
||||||
KW("WEND", TOK_WEND),
|
|
||||||
KW("WHILE", TOK_WHILE),
|
|
||||||
KW("WITH", TOK_WITH),
|
|
||||||
KW("WRITE", TOK_WRITE),
|
|
||||||
KW("XOR", TOK_XOR),
|
|
||||||
{ NULL, 0, TOK_ERROR }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#undef KW
|
#define KEYWORD_COUNT (DVX_ARRAY_LEN(sKeywords) - 1)
|
||||||
|
|
||||||
#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1)
|
// Bits per digit of the &O / &B / &H numeric-base literals.
|
||||||
|
#define LEX_OCT_BITS 3
|
||||||
|
#define LEX_BIN_BITS 1
|
||||||
|
#define LEX_HEX_BITS 4
|
||||||
|
|
||||||
|
// First value of the hex letter digits A-F.
|
||||||
|
#define LEX_HEX_LETTER_BASE 10
|
||||||
|
|
||||||
|
// basRoundHalfEven: the tie point, and a magnitude bound far past any
|
||||||
|
// 32-bit literal that keeps the int64 conversion defined.
|
||||||
|
#define LEX_HALF 0.5
|
||||||
|
#define LEX_ROUND_LIMIT 1.0e18
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Type-suffix table
|
// Type-suffix table
|
||||||
|
|
@ -203,12 +91,12 @@ static const TypeSuffixEntryT sTypeSuffixes[] = {
|
||||||
{ '$', BAS_TYPE_STRING },
|
{ '$', BAS_TYPE_STRING },
|
||||||
};
|
};
|
||||||
|
|
||||||
#define TYPE_SUFFIX_COUNT ((int32_t)(sizeof(sTypeSuffixes) / sizeof(sTypeSuffixes[0])))
|
#define TYPE_SUFFIX_COUNT DVX_ARRAY_LEN(sTypeSuffixes)
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
static char advance(BasLexerT *lex);
|
static char advance(BasLexerT *lex);
|
||||||
static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c);
|
static bool appendTokenChar(BasLexerT *lex, int32_t *idx, char c);
|
||||||
static bool atEnd(const BasLexerT *lex);
|
static bool atEnd(const BasLexerT *lex);
|
||||||
char basAsciiUpper(char c);
|
char basAsciiUpper(char c);
|
||||||
bool basIsIdentChar(char c);
|
bool basIsIdentChar(char c);
|
||||||
|
|
@ -219,9 +107,11 @@ const char *basLexerKeywordAt(int32_t i);
|
||||||
BasKeywordClassE basLexerKeywordClass(int32_t i);
|
BasKeywordClassE basLexerKeywordClass(int32_t i);
|
||||||
int32_t basLexerKeywordCount(void);
|
int32_t basLexerKeywordCount(void);
|
||||||
BasTokenTypeE basLexerNext(BasLexerT *lex);
|
BasTokenTypeE basLexerNext(BasLexerT *lex);
|
||||||
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
|
bool basRoundHalfEven(double v, int64_t *out);
|
||||||
|
bool basTokenIsWord(const BasTokenT *tok);
|
||||||
const char *basTokenName(BasTokenTypeE type);
|
const char *basTokenName(BasTokenTypeE type);
|
||||||
int32_t basTypeSuffixType(char c);
|
int32_t basTypeSuffixType(char c);
|
||||||
|
static int32_t compareKeyword(const char *text, int32_t len, const KeywordEntryT *kw);
|
||||||
static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal);
|
static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal);
|
||||||
static BasTokenTypeE lookupKeyword(const char *text, int32_t len);
|
static BasTokenTypeE lookupKeyword(const char *text, int32_t len);
|
||||||
static BasTokenTypeE makeNewlineToken(BasLexerT *lex);
|
static BasTokenTypeE makeNewlineToken(BasLexerT *lex);
|
||||||
|
|
@ -235,6 +125,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex);
|
||||||
static BasTokenTypeE tokenizeNumber(BasLexerT *lex);
|
static BasTokenTypeE tokenizeNumber(BasLexerT *lex);
|
||||||
static BasTokenTypeE tokenizeString(BasLexerT *lex);
|
static BasTokenTypeE tokenizeString(BasLexerT *lex);
|
||||||
|
|
||||||
|
|
||||||
static char advance(BasLexerT *lex) {
|
static char advance(BasLexerT *lex) {
|
||||||
if (atEnd(lex)) {
|
if (atEnd(lex)) {
|
||||||
return '\0';
|
return '\0';
|
||||||
|
|
@ -255,10 +146,15 @@ static char advance(BasLexerT *lex) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c) {
|
// Append c to the token text. Returns false (dropping c) once the
|
||||||
if (*idx < BAS_MAX_TOKEN_LEN - 1) {
|
// buffer is full, so callers that must not truncate can detect it.
|
||||||
lex->token.text[(*idx)++] = c;
|
static bool appendTokenChar(BasLexerT *lex, int32_t *idx, char c) {
|
||||||
|
if (*idx >= BAS_MAX_TOKEN_LEN - 1) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lex->token.text[(*idx)++] = c;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -315,7 +211,7 @@ void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) {
|
||||||
|
|
||||||
|
|
||||||
const char *basLexerKeywordAt(int32_t i) {
|
const char *basLexerKeywordAt(int32_t i) {
|
||||||
if (i < 0 || i >= (int32_t)KEYWORD_COUNT) {
|
if (i < 0 || i >= KEYWORD_COUNT) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,32 +220,16 @@ const char *basLexerKeywordAt(int32_t i) {
|
||||||
|
|
||||||
|
|
||||||
BasKeywordClassE basLexerKeywordClass(int32_t i) {
|
BasKeywordClassE basLexerKeywordClass(int32_t i) {
|
||||||
if (i < 0 || i >= (int32_t)KEYWORD_COUNT) {
|
if (i < 0 || i >= KEYWORD_COUNT) {
|
||||||
return BAS_KW_CLASS_OTHER;
|
return BAS_KW_CLASS_OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (sKeywords[i].type) {
|
return sKeywords[i].cls;
|
||||||
case TOK_BOOLEAN:
|
|
||||||
case TOK_DOUBLE:
|
|
||||||
case TOK_INTEGER:
|
|
||||||
case TOK_LONG:
|
|
||||||
case TOK_SINGLE:
|
|
||||||
case TOK_STRING_KW:
|
|
||||||
return BAS_KW_CLASS_TYPE;
|
|
||||||
|
|
||||||
case TOK_TRUE_KW:
|
|
||||||
case TOK_FALSE_KW:
|
|
||||||
case TOK_NOTHING:
|
|
||||||
return BAS_KW_CLASS_LITERAL;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return BAS_KW_CLASS_OTHER;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int32_t basLexerKeywordCount(void) {
|
int32_t basLexerKeywordCount(void) {
|
||||||
return (int32_t)KEYWORD_COUNT;
|
return KEYWORD_COUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -358,6 +238,7 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
|
||||||
|
|
||||||
lex->token.line = lex->line;
|
lex->token.line = lex->line;
|
||||||
lex->token.col = lex->col;
|
lex->token.col = lex->col;
|
||||||
|
lex->token.pos = lex->pos;
|
||||||
lex->token.textLen = 0;
|
lex->token.textLen = 0;
|
||||||
lex->token.text[0] = '\0';
|
lex->token.text[0] = '\0';
|
||||||
|
|
||||||
|
|
@ -533,46 +414,100 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BasTokenTypeE basLexerPeek(const BasLexerT *lex) {
|
bool basRoundHalfEven(double v, int64_t *out) {
|
||||||
return lex->token.type;
|
if (!(v > -LEX_ROUND_LIMIT && v < LEX_ROUND_LIMIT)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t t = (int64_t)v;
|
||||||
|
double frac = v - (double)t;
|
||||||
|
|
||||||
|
if (frac > LEX_HALF || (frac == LEX_HALF && (t & 1) != 0)) {
|
||||||
|
t++;
|
||||||
|
} else if (frac < -LEX_HALF || (frac == -LEX_HALF && (t & 1) != 0)) {
|
||||||
|
t--;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = t;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Identifier or keyword: anything spelled like a word, the shape a
|
||||||
|
// property or method name takes after a dot (keywords such as Load, Show
|
||||||
|
// or Name are valid member names).
|
||||||
|
bool basTokenIsWord(const BasTokenT *tok) {
|
||||||
|
bool isWordType = tok->type == TOK_IDENT || (tok->type >= TOK_FIRST_KEYWORD && tok->type <= TOK_LAST_KEYWORD);
|
||||||
|
char c = tok->text[0];
|
||||||
|
|
||||||
|
return isWordType && ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const char *basTokenName(BasTokenTypeE type) {
|
const char *basTokenName(BasTokenTypeE type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TOK_INT_LIT: return "integer";
|
case TOK_INT_LIT:
|
||||||
case TOK_LONG_LIT: return "long";
|
return "integer";
|
||||||
case TOK_FLOAT_LIT: return "float";
|
case TOK_LONG_LIT:
|
||||||
case TOK_STRING_LIT: return "string";
|
return "long";
|
||||||
case TOK_IDENT: return "identifier";
|
case TOK_FLOAT_LIT:
|
||||||
case TOK_DOT: return "'.'";
|
return "float";
|
||||||
case TOK_COMMA: return "','";
|
case TOK_STRING_LIT:
|
||||||
case TOK_SEMICOLON: return "';'";
|
return "string";
|
||||||
case TOK_COLON: return "':'";
|
case TOK_IDENT:
|
||||||
case TOK_LPAREN: return "'('";
|
return "identifier";
|
||||||
case TOK_RPAREN: return "')'";
|
case TOK_DOT:
|
||||||
case TOK_HASH: return "'#'";
|
return "'.'";
|
||||||
case TOK_PLUS: return "'+'";
|
case TOK_COMMA:
|
||||||
case TOK_MINUS: return "'-'";
|
return "','";
|
||||||
case TOK_STAR: return "'*'";
|
case TOK_SEMICOLON:
|
||||||
case TOK_SLASH: return "'/'";
|
return "';'";
|
||||||
case TOK_BACKSLASH: return "'\\'";
|
case TOK_COLON:
|
||||||
case TOK_CARET: return "'^'";
|
return "':'";
|
||||||
case TOK_AMPERSAND: return "'&'";
|
case TOK_LPAREN:
|
||||||
case TOK_EQ: return "'='";
|
return "'('";
|
||||||
case TOK_NE: return "'<>'";
|
case TOK_RPAREN:
|
||||||
case TOK_LT: return "'<'";
|
return "')'";
|
||||||
case TOK_GT: return "'>'";
|
case TOK_HASH:
|
||||||
case TOK_LE: return "'<='";
|
return "'#'";
|
||||||
case TOK_GE: return "'>='";
|
case TOK_PLUS:
|
||||||
case TOK_NEWLINE: return "newline";
|
return "'+'";
|
||||||
case TOK_EOF: return "end of file";
|
case TOK_MINUS:
|
||||||
case TOK_ERROR: return "error";
|
return "'-'";
|
||||||
default: break;
|
case TOK_STAR:
|
||||||
|
return "'*'";
|
||||||
|
case TOK_SLASH:
|
||||||
|
return "'/'";
|
||||||
|
case TOK_BACKSLASH:
|
||||||
|
return "'\\'";
|
||||||
|
case TOK_CARET:
|
||||||
|
return "'^'";
|
||||||
|
case TOK_AMPERSAND:
|
||||||
|
return "'&'";
|
||||||
|
case TOK_EQ:
|
||||||
|
return "'='";
|
||||||
|
case TOK_NE:
|
||||||
|
return "'<>'";
|
||||||
|
case TOK_LT:
|
||||||
|
return "'<'";
|
||||||
|
case TOK_GT:
|
||||||
|
return "'>'";
|
||||||
|
case TOK_LE:
|
||||||
|
return "'<='";
|
||||||
|
case TOK_GE:
|
||||||
|
return "'>='";
|
||||||
|
case TOK_NEWLINE:
|
||||||
|
return "newline";
|
||||||
|
case TOK_EOF:
|
||||||
|
return "end of file";
|
||||||
|
case TOK_ERROR:
|
||||||
|
return "error";
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keywords
|
// Keywords
|
||||||
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
|
for (int32_t i = 0; i < KEYWORD_COUNT; i++) {
|
||||||
if (sKeywords[i].type == type) {
|
if (sKeywords[i].type == type) {
|
||||||
return sKeywords[i].text;
|
return sKeywords[i].text;
|
||||||
}
|
}
|
||||||
|
|
@ -593,16 +528,44 @@ int32_t basTypeSuffixType(char c) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Order text[0..len) (case-insensitive) against a keyword entry the way
|
||||||
|
// the sorted keyword table is ordered (bytewise on the upper-case text).
|
||||||
|
static int32_t compareKeyword(const char *text, int32_t len, const KeywordEntryT *kw) {
|
||||||
|
int32_t n = (len < kw->textLen) ? len : kw->textLen;
|
||||||
|
|
||||||
|
for (int32_t j = 0; j < n; j++) {
|
||||||
|
int32_t diff = (int32_t)(uint8_t)basAsciiUpper(text[j]) - (int32_t)(uint8_t)kw->text[j];
|
||||||
|
|
||||||
|
if (diff != 0) {
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len - kw->textLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Convert the decimal digits in token.text to an integer and range-check
|
// Convert the decimal digits in token.text to an integer and range-check
|
||||||
// the result against [minVal, maxVal]. On overflow the token becomes
|
// the result against [minVal, maxVal]. On overflow the token becomes
|
||||||
// TOK_ERROR naming `what`, and false is returned. strtoll is used rather
|
// TOK_ERROR naming `what`, and false is returned. strtoll is used rather
|
||||||
// than atol so the conversion is identical on the 32-bit DOS build and
|
// than atol so the conversion is identical on the 32-bit DOS build and
|
||||||
// the 64-bit host build of the compiler.
|
// the 64-bit host build of the compiler.
|
||||||
static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal) {
|
static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal) {
|
||||||
errno = 0;
|
int64_t val;
|
||||||
long long val = strtoll(lex->token.text, NULL, 10);
|
bool inRange;
|
||||||
|
|
||||||
if (errno == ERANGE || val < minVal || val > maxVal) {
|
// A literal with a fraction or exponent and an integer suffix (1E3%,
|
||||||
|
// 2.5&) takes the rounded value, the same banker's rounding CINT/CLNG
|
||||||
|
// use; plain digits convert exactly.
|
||||||
|
if (strpbrk(lex->token.text, ".E") != NULL) {
|
||||||
|
inRange = basRoundHalfEven(strtod(lex->token.text, NULL), &val);
|
||||||
|
} else {
|
||||||
|
errno = 0;
|
||||||
|
val = (int64_t)strtoll(lex->token.text, NULL, 10);
|
||||||
|
inRange = (errno != ERANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inRange || val < minVal || val > maxVal) {
|
||||||
char buf[BAS_LEX_ERROR_LEN];
|
char buf[BAS_LEX_ERROR_LEN];
|
||||||
snprintf(buf, sizeof(buf), "%s literal out of range", what);
|
snprintf(buf, sizeof(buf), "%s literal out of range", what);
|
||||||
setError(lex, buf);
|
setError(lex, buf);
|
||||||
|
|
@ -610,35 +573,28 @@ static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, co
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*outVal = (int64_t)val;
|
*outVal = val;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
|
static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
|
||||||
// Case-insensitive keyword lookup. Short-circuits on length mismatch
|
// Case-insensitive binary search of the sorted keyword table.
|
||||||
// (via cached keyword length) and on the very first character, both of
|
int32_t lo = 0;
|
||||||
// which reject the vast majority of entries before doing a full scan.
|
int32_t hi = KEYWORD_COUNT - 1;
|
||||||
char firstUp = basAsciiUpper(text[0]);
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
|
while (lo <= hi) {
|
||||||
const KeywordEntryT *kw = &sKeywords[i];
|
int32_t mid = (lo + hi) / 2;
|
||||||
|
int32_t cmp = compareKeyword(text, len, &sKeywords[mid]);
|
||||||
|
|
||||||
if (kw->textLen != len || kw->text[0] != firstUp) {
|
if (cmp == 0) {
|
||||||
continue;
|
return sKeywords[mid].type;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool match = true;
|
if (cmp < 0) {
|
||||||
|
hi = mid - 1;
|
||||||
for (int32_t j = 1; j < len; j++) {
|
} else {
|
||||||
if (basAsciiUpper(text[j]) != kw->text[j]) {
|
lo = mid + 1;
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
return kw->type;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -675,8 +631,12 @@ static char peekNext(const BasLexerT *lex) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Record a lexer error at the start of the current token. The message
|
||||||
|
// carries no position: the parser formats line and column once.
|
||||||
static void setError(BasLexerT *lex, const char *msg) {
|
static void setError(BasLexerT *lex, const char *msg) {
|
||||||
snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", (int)lex->line, (int)lex->col, msg);
|
snprintf(lex->error, sizeof(lex->error), "%s", msg);
|
||||||
|
lex->errLine = lex->token.line;
|
||||||
|
lex->errCol = lex->token.col;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -744,16 +704,15 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
|
||||||
int32_t maxDigit;
|
int32_t maxDigit;
|
||||||
|
|
||||||
if (base == 'O') {
|
if (base == 'O') {
|
||||||
shift = 3;
|
shift = LEX_OCT_BITS;
|
||||||
maxDigit = 7;
|
|
||||||
} else if (base == 'B') {
|
} else if (base == 'B') {
|
||||||
shift = 1;
|
shift = LEX_BIN_BITS;
|
||||||
maxDigit = 1;
|
|
||||||
} else {
|
} else {
|
||||||
shift = 4;
|
shift = LEX_HEX_BITS;
|
||||||
maxDigit = 15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maxDigit = (1 << shift) - 1;
|
||||||
|
|
||||||
bool overflow = false;
|
bool overflow = false;
|
||||||
int32_t idx = 0;
|
int32_t idx = 0;
|
||||||
uint64_t value = 0;
|
uint64_t value = 0;
|
||||||
|
|
@ -768,10 +727,10 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
|
||||||
|
|
||||||
if (c >= '0' && c <= '9') {
|
if (c >= '0' && c <= '9') {
|
||||||
digit = c - '0';
|
digit = c - '0';
|
||||||
} else if (shift == 4 && c >= 'A' && c <= 'F') {
|
} else if (shift == LEX_HEX_BITS && c >= 'A' && c <= 'F') {
|
||||||
digit = c - 'A' + 10;
|
digit = c - 'A' + LEX_HEX_LETTER_BASE;
|
||||||
} else if (shift == 4 && c >= 'a' && c <= 'f') {
|
} else if (shift == LEX_HEX_BITS && c >= 'a' && c <= 'f') {
|
||||||
digit = c - 'a' + 10;
|
digit = c - 'a' + LEX_HEX_LETTER_BASE;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -835,58 +794,32 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
|
||||||
lex->token.text[idx] = '\0';
|
lex->token.text[idx] = '\0';
|
||||||
lex->token.textLen = idx;
|
lex->token.textLen = idx;
|
||||||
|
|
||||||
// Check for type suffix. A keyword only ever takes '$' (CURDIR$,
|
BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx);
|
||||||
// DIR$, INPUT$ ...); any other suffix after a keyword belongs to the
|
|
||||||
// next token, so PRINT#1 / CLOSE#1 / INPUT#1 lex as keyword + '#'.
|
// Type suffix. A keyword only ever takes '$' (CURDIR$, DIR$,
|
||||||
|
// INPUTBOX$ ...); any other suffix after a keyword belongs to the next
|
||||||
|
// token, so PRINT#1 / CLOSE#1 / INPUT#1 lex as keyword + '#'. A
|
||||||
|
// suffixed word is a keyword only when the table lists it WITH its
|
||||||
|
// suffix, so REM$ or LEN$ stay identifiers.
|
||||||
if (!atEnd(lex)) {
|
if (!atEnd(lex)) {
|
||||||
char c = peek(lex);
|
char c = peek(lex);
|
||||||
|
|
||||||
if (basIsTypeSuffixChar(c) && (c == '$' || lookupKeyword(lex->token.text, idx) == TOK_IDENT)) {
|
if (basIsTypeSuffixChar(c) && (c == '$' || kwType == TOK_IDENT)) {
|
||||||
advance(lex);
|
advance(lex);
|
||||||
appendTokenChar(lex, &idx, c);
|
appendTokenChar(lex, &idx, c);
|
||||||
lex->token.text[idx] = '\0';
|
lex->token.text[idx] = '\0';
|
||||||
lex->token.textLen = idx;
|
lex->token.textLen = idx;
|
||||||
|
kwType = lookupKeyword(lex->token.text, idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a keyword
|
// REM is a comment -- skip to end of line.
|
||||||
// For suffix-bearing identifiers, only check the base (without suffix)
|
if (kwType == TOK_REM) {
|
||||||
int32_t baseLen = idx;
|
|
||||||
|
|
||||||
if (baseLen > 0) {
|
|
||||||
char last = lex->token.text[baseLen - 1];
|
|
||||||
|
|
||||||
if (basIsTypeSuffixChar(last)) {
|
|
||||||
baseLen--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try the full text first (including any type suffix). Suffix-bearing
|
|
||||||
// keywords like CURDIR$, DIR$, INIREAD$, INPUTBOX$ are listed in the
|
|
||||||
// keyword table with their $ and will match here. If the full text
|
|
||||||
// isn't a keyword, fall back to the base name (without suffix).
|
|
||||||
BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx);
|
|
||||||
bool matchedWithSuffix = (kwType != TOK_IDENT && baseLen != idx);
|
|
||||||
|
|
||||||
if (kwType == TOK_IDENT && baseLen != idx) {
|
|
||||||
kwType = lookupKeyword(lex->token.text, baseLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
// REM is a comment -- skip to end of line. Apply the same acceptance
|
|
||||||
// gate as below so a suffixed identifier like REM$ stays an identifier
|
|
||||||
// instead of swallowing the rest of the line as a comment.
|
|
||||||
if (kwType == TOK_REM && (baseLen == idx || matchedWithSuffix)) {
|
|
||||||
skipLineComment(lex);
|
skipLineComment(lex);
|
||||||
return makeNewlineToken(lex);
|
return makeNewlineToken(lex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept the keyword if it's a plain keyword (no suffix on source) or
|
return kwType;
|
||||||
// if it explicitly matched a $-suffixed entry in the keyword table.
|
|
||||||
if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) {
|
|
||||||
return kwType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TOK_IDENT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -894,19 +827,21 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
|
||||||
int32_t idx = 0;
|
int32_t idx = 0;
|
||||||
bool hasDecimal = false;
|
bool hasDecimal = false;
|
||||||
bool hasExp = false;
|
bool hasExp = false;
|
||||||
|
bool tooLong = false;
|
||||||
|
|
||||||
// Integer part
|
// Integer part
|
||||||
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||||
appendTokenChar(lex, &idx, advance(lex));
|
tooLong |= !appendTokenChar(lex, &idx, advance(lex));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decimal part
|
// Decimal part. A trailing '.' with no digits ("1.") is a float too,
|
||||||
if (!atEnd(lex) && peek(lex) == '.' && isdigit((unsigned char)peekNext(lex))) {
|
// unless a word follows (then the dot belongs to member syntax).
|
||||||
|
if (!atEnd(lex) && peek(lex) == '.' && (isdigit((unsigned char)peekNext(lex)) || !basIsIdentChar(peekNext(lex)))) {
|
||||||
hasDecimal = true;
|
hasDecimal = true;
|
||||||
appendTokenChar(lex, &idx, advance(lex)); // .
|
tooLong |= !appendTokenChar(lex, &idx, advance(lex)); // .
|
||||||
|
|
||||||
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||||
appendTokenChar(lex, &idx, advance(lex));
|
tooLong |= !appendTokenChar(lex, &idx, advance(lex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -922,18 +857,18 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
|
||||||
int32_t savedLine = lex->line;
|
int32_t savedLine = lex->line;
|
||||||
int32_t savedCol = lex->col;
|
int32_t savedCol = lex->col;
|
||||||
|
|
||||||
advance(lex); // consume marker
|
advance(lex); // consume marker
|
||||||
appendTokenChar(lex, &idx, 'E'); // always store 'E', never 'D'
|
tooLong |= !appendTokenChar(lex, &idx, 'E'); // always store 'E', never 'D'
|
||||||
|
|
||||||
if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) {
|
if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) {
|
||||||
appendTokenChar(lex, &idx, advance(lex));
|
tooLong |= !appendTokenChar(lex, &idx, advance(lex));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
if (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||||
hasExp = true;
|
hasExp = true;
|
||||||
|
|
||||||
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
|
||||||
appendTokenChar(lex, &idx, advance(lex));
|
tooLong |= !appendTokenChar(lex, &idx, advance(lex));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not a valid exponent -- roll back the consumed marker/sign.
|
// Not a valid exponent -- roll back the consumed marker/sign.
|
||||||
|
|
@ -947,7 +882,14 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
|
||||||
lex->token.text[idx] = '\0';
|
lex->token.text[idx] = '\0';
|
||||||
lex->token.textLen = idx;
|
lex->token.textLen = idx;
|
||||||
|
|
||||||
// Check for type suffix
|
// A dropped digit would silently change the value.
|
||||||
|
if (tooLong) {
|
||||||
|
setError(lex, "Numeric literal too long");
|
||||||
|
return TOK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for type suffix. Directly after unary minus the magnitude of
|
||||||
|
// the type's most negative value is accepted (-32768%, -2147483648&).
|
||||||
if (!atEnd(lex)) {
|
if (!atEnd(lex)) {
|
||||||
char c = peek(lex);
|
char c = peek(lex);
|
||||||
int64_t val;
|
int64_t val;
|
||||||
|
|
@ -955,7 +897,9 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
|
||||||
if (c == '%') {
|
if (c == '%') {
|
||||||
advance(lex);
|
advance(lex);
|
||||||
|
|
||||||
if (!lexIntegerLiteral(lex, INT16_MIN, INT16_MAX, "INTEGER", &val)) {
|
int64_t maxVal = lex->allowNegMagnitude ? (int64_t)INT16_MAX + 1 : INT16_MAX;
|
||||||
|
|
||||||
|
if (!lexIntegerLiteral(lex, INT16_MIN, maxVal, "INTEGER", &val)) {
|
||||||
return TOK_ERROR;
|
return TOK_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -966,7 +910,9 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
|
||||||
if (c == '&') {
|
if (c == '&') {
|
||||||
advance(lex);
|
advance(lex);
|
||||||
|
|
||||||
if (!lexIntegerLiteral(lex, INT32_MIN, INT32_MAX, "LONG", &val)) {
|
int64_t maxVal = lex->allowNegMagnitude ? BAS_LONG_NEG_MAGNITUDE : INT32_MAX;
|
||||||
|
|
||||||
|
if (!lexIntegerLiteral(lex, INT32_MIN, maxVal, "LONG", &val)) {
|
||||||
return TOK_ERROR;
|
return TOK_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@
|
||||||
#ifndef DVXBASIC_LEXER_H
|
#ifndef DVXBASIC_LEXER_H
|
||||||
#define DVXBASIC_LEXER_H
|
#define DVXBASIC_LEXER_H
|
||||||
|
|
||||||
|
#include "basKeywords.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
|
@ -125,6 +127,7 @@ typedef enum {
|
||||||
TOK_LOAD,
|
TOK_LOAD,
|
||||||
TOK_LONG,
|
TOK_LONG,
|
||||||
TOK_LOOP,
|
TOK_LOOP,
|
||||||
|
TOK_LPRINT,
|
||||||
TOK_ME,
|
TOK_ME,
|
||||||
TOK_MOD,
|
TOK_MOD,
|
||||||
TOK_INPUTBOX,
|
TOK_INPUTBOX,
|
||||||
|
|
@ -204,6 +207,10 @@ typedef enum {
|
||||||
TOK_ERROR // lexer error
|
TOK_ERROR // lexer error
|
||||||
} BasTokenTypeE;
|
} BasTokenTypeE;
|
||||||
|
|
||||||
|
// Keyword tokens occupy one contiguous range of the enum.
|
||||||
|
#define TOK_FIRST_KEYWORD TOK_AND
|
||||||
|
#define TOK_LAST_KEYWORD TOK_RANDOM
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Token
|
// Token
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -222,6 +229,7 @@ typedef struct {
|
||||||
BasTokenTypeE type;
|
BasTokenTypeE type;
|
||||||
int32_t line; // 1-based source line number
|
int32_t line; // 1-based source line number
|
||||||
int32_t col; // 1-based column number
|
int32_t col; // 1-based column number
|
||||||
|
int32_t pos; // offset of the token's first character in the source
|
||||||
|
|
||||||
// Value (depends on type)
|
// Value (depends on type)
|
||||||
union {
|
union {
|
||||||
|
|
@ -245,7 +253,9 @@ typedef struct {
|
||||||
int32_t line; // current line (1-based)
|
int32_t line; // current line (1-based)
|
||||||
int32_t col; // current column (1-based)
|
int32_t col; // current column (1-based)
|
||||||
BasTokenT token; // current token
|
BasTokenT token; // current token
|
||||||
char error[BAS_LEX_ERROR_LEN];
|
char error[BAS_LEX_ERROR_LEN]; // message of the last TOK_ERROR (no position prefix)
|
||||||
|
int32_t errLine; // line of the token that raised error
|
||||||
|
int32_t errCol; // column of the token that raised error
|
||||||
bool allowNegMagnitude; // next integer literal may be BAS_LONG_NEG_MAGNITUDE (set by the parser after unary minus)
|
bool allowNegMagnitude; // next integer literal may be BAS_LONG_NEG_MAGNITUDE (set by the parser after unary minus)
|
||||||
} BasLexerT;
|
} BasLexerT;
|
||||||
|
|
||||||
|
|
@ -261,12 +271,13 @@ void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
|
||||||
// The token is available in lex->token.
|
// The token is available in lex->token.
|
||||||
BasTokenTypeE basLexerNext(BasLexerT *lex);
|
BasTokenTypeE basLexerNext(BasLexerT *lex);
|
||||||
|
|
||||||
// Peek at the current token type without advancing.
|
|
||||||
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
|
|
||||||
|
|
||||||
// Return human-readable name for a token type.
|
// Return human-readable name for a token type.
|
||||||
const char *basTokenName(BasTokenTypeE type);
|
const char *basTokenName(BasTokenTypeE type);
|
||||||
|
|
||||||
|
// True when the token is an identifier or keyword spelled as a word (the
|
||||||
|
// shape of a property or method name after a dot).
|
||||||
|
bool basTokenIsWord(const BasTokenT *tok);
|
||||||
|
|
||||||
// True when c may appear inside an identifier (ASCII letter, digit or
|
// True when c may appear inside an identifier (ASCII letter, digit or
|
||||||
// underscore; locale independent).
|
// underscore; locale independent).
|
||||||
bool basIsIdentChar(char c);
|
bool basIsIdentChar(char c);
|
||||||
|
|
@ -283,6 +294,10 @@ bool basIsTypeSuffixChar(char c);
|
||||||
// source of truth for both this and basIsTypeSuffixChar.
|
// source of truth for both this and basIsTypeSuffixChar.
|
||||||
int32_t basTypeSuffixType(char c);
|
int32_t basTypeSuffixType(char c);
|
||||||
|
|
||||||
|
// Round v to the nearest integer, ties to even (the rounding CINT/CLNG
|
||||||
|
// use). Returns false when v is not a number or too large to convert.
|
||||||
|
bool basRoundHalfEven(double v, int64_t *out);
|
||||||
|
|
||||||
// Fold a single ASCII byte to upper case. Char in, char out (not int
|
// Fold a single ASCII byte to upper case. Char in, char out (not int
|
||||||
// via toupper) so bytes >= 0x80 pass through unchanged.
|
// via toupper) so bytes >= 0x80 pass through unchanged.
|
||||||
char basAsciiUpper(char c);
|
char basAsciiUpper(char c);
|
||||||
|
|
@ -291,15 +306,9 @@ char basAsciiUpper(char c);
|
||||||
// Keyword iteration
|
// Keyword iteration
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// The lexer's internal keyword table is the one source of truth.
|
// The keyword list in basKeywords.h is the one source of truth. Other
|
||||||
// Other modules (e.g. the IDE syntax highlighter) iterate through
|
// modules (e.g. the IDE syntax highlighter) iterate through the lexer's
|
||||||
// it here instead of keeping their own parallel list.
|
// table here instead of keeping their own parallel list.
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
BAS_KW_CLASS_OTHER = 0, // control-flow / statement / operator keyword
|
|
||||||
BAS_KW_CLASS_TYPE = 1, // INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
|
|
||||||
BAS_KW_CLASS_LITERAL = 2 // TRUE, FALSE, NOTHING
|
|
||||||
} BasKeywordClassE;
|
|
||||||
|
|
||||||
// Number of keywords in the table (excluding only the trailing NULL
|
// Number of keywords in the table (excluding only the trailing NULL
|
||||||
// sentinel). Dollar-suffixed aliases (DIR$, CURDIR$, etc.) are counted
|
// sentinel). Dollar-suffixed aliases (DIR$, CURDIR$, etc.) are counted
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,17 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define BAS_OBF_TOKEN_LEN BAS_MAX_IDENT // longest form/control/type token kept from a .frm line
|
#define BAS_OBF_TOKEN_LEN BAS_MAX_IDENT // longest form/control/type token kept from a .frm line
|
||||||
#define BAS_OBF_MAPPED_LEN 16 // "C" + decimal index + NUL
|
#define BAS_OBF_MAPPED_LEN 16 // "C" + decimal index + NUL
|
||||||
#define BAS_OBF_MAP_INIT_CAP 16 // initial name-map capacity (doubles on growth)
|
#define BAS_OBF_MAP_INIT_CAP 16 // initial name-map capacity (doubles on growth)
|
||||||
#define BAS_REM_KEYWORD_LEN 3 // "REM"
|
#define BAS_REM_KEYWORD_LEN 3 // "REM"
|
||||||
|
|
||||||
|
// .frm properties whose value names another control on the form; only
|
||||||
|
// these values follow a control rename.
|
||||||
|
static const char *sNameRefKeys[] = {
|
||||||
|
"DataSource",
|
||||||
|
"MasterSource",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Name map
|
// Name map
|
||||||
|
|
@ -56,18 +64,20 @@ typedef struct {
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
|
bool basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
|
||||||
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
|
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
|
||||||
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved);
|
static bool collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved);
|
||||||
static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len);
|
static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len);
|
||||||
|
static bool isNameRefKey(const char *key);
|
||||||
|
static bool isWholeLineRem(const char *text, int32_t len);
|
||||||
static const char *nameMapAdd(NameMapT *m, const char *name);
|
static const char *nameMapAdd(NameMapT *m, const char *name);
|
||||||
static void nameMapFree(NameMapT *m);
|
static void nameMapFree(NameMapT *m);
|
||||||
static void nameMapInit(NameMapT *m);
|
static void nameMapInit(NameMapT *m);
|
||||||
static const char *nameMapLookup(const NameMapT *m, const char *name);
|
static const char *nameMapLookup(const NameMapT *m, const char *name);
|
||||||
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize);
|
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize, bool *outTruncated);
|
||||||
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText);
|
static bool replaceConstant(BasModuleT *mod, int32_t idx, const char *newText);
|
||||||
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap);
|
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap);
|
||||||
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map);
|
static bool rewriteModuleConstants(BasModuleT *mod, const NameMapT *map);
|
||||||
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map);
|
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map);
|
||||||
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map);
|
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map);
|
||||||
static const char *skipWhitespace(const char *p, const char *end);
|
static const char *skipWhitespace(const char *p, const char *end);
|
||||||
|
|
@ -75,9 +85,9 @@ static const char *skipWhitespace(const char *p, const char *end);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Top-level entry point
|
// Top-level entry point
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) {
|
bool basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) {
|
||||||
if (!mod || frmCount < 0) {
|
if (!mod || frmCount < 0) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1: collect all names from all .frm texts. A control whose name
|
// Pass 1: collect all names from all .frm texts. A control whose name
|
||||||
|
|
@ -88,20 +98,21 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
|
||||||
NameMapT names;
|
NameMapT names;
|
||||||
NameMapT reserved;
|
NameMapT reserved;
|
||||||
NameMapT map;
|
NameMapT map;
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
nameMapInit(&names);
|
nameMapInit(&names);
|
||||||
nameMapInit(&reserved);
|
nameMapInit(&reserved);
|
||||||
nameMapInit(&map);
|
nameMapInit(&map);
|
||||||
|
|
||||||
for (int32_t i = 0; i < frmCount; i++) {
|
for (int32_t i = 0; ok && i < frmCount; i++) {
|
||||||
if (frmTexts[i] && frmLens[i] > 0) {
|
if (frmTexts[i] && frmLens[i] > 0) {
|
||||||
collectNamesFromFrm(frmTexts[i], frmLens[i], &names, &reserved);
|
ok = collectNamesFromFrm(frmTexts[i], frmLens[i], &names, &reserved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < names.count; i++) {
|
for (int32_t i = 0; ok && i < names.count; i++) {
|
||||||
if (!nameMapLookup(&reserved, names.entries[i].orig)) {
|
if (!nameMapLookup(&reserved, names.entries[i].orig)) {
|
||||||
nameMapAdd(&map, names.entries[i].orig);
|
ok = nameMapAdd(&map, names.entries[i].orig) != NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +125,7 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
|
||||||
outFrms[i].data = NULL;
|
outFrms[i].data = NULL;
|
||||||
outFrms[i].len = 0;
|
outFrms[i].len = 0;
|
||||||
|
|
||||||
if (!frmTexts[i] || frmLens[i] <= 0) {
|
if (!ok || !frmTexts[i] || frmLens[i] <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +134,7 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
|
||||||
uint8_t *outBuf = malloc(outCap);
|
uint8_t *outBuf = malloc(outCap);
|
||||||
|
|
||||||
if (!outBuf) {
|
if (!outBuf) {
|
||||||
|
ok = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,11 +150,14 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 3: rewrite module
|
// Pass 3: rewrite module
|
||||||
rewriteModuleConstants(mod, &map);
|
if (ok) {
|
||||||
rewriteModuleProcs(mod, &map);
|
ok = rewriteModuleConstants(mod, &map);
|
||||||
rewriteModuleFormVars(mod, &map);
|
rewriteModuleProcs(mod, &map);
|
||||||
|
rewriteModuleFormVars(mod, &map);
|
||||||
|
}
|
||||||
|
|
||||||
nameMapFree(&map);
|
nameMapFree(&map);
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -151,8 +166,12 @@ int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, in
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t outLen = 0;
|
// Inside the form block only whole-line comments exist (the form
|
||||||
int32_t i = 0;
|
// parser takes the rest of a property line verbatim); trailing
|
||||||
|
// comments are stripped from the BASIC code after it.
|
||||||
|
int32_t formEnd = basFindFormEndPos(src, srcLen);
|
||||||
|
int32_t outLen = 0;
|
||||||
|
int32_t i = 0;
|
||||||
|
|
||||||
while (i < srcLen) {
|
while (i < srcLen) {
|
||||||
int32_t lineStart = i;
|
int32_t lineStart = i;
|
||||||
|
|
@ -171,35 +190,31 @@ int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, in
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan for first unquoted ' (comment start).
|
|
||||||
bool inStr = false;
|
|
||||||
int32_t commentStart = -1;
|
|
||||||
|
|
||||||
for (int32_t j = lineStart; j < lineEnd; j++) {
|
|
||||||
char c = src[j];
|
|
||||||
|
|
||||||
if (c == '"') {
|
|
||||||
inStr = !inStr;
|
|
||||||
} else if (c == '\'' && !inStr) {
|
|
||||||
commentStart = j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd;
|
|
||||||
|
|
||||||
// Check for whole-line REM. Find first non-whitespace position.
|
|
||||||
int32_t firstNonWs = lineStart;
|
int32_t firstNonWs = lineStart;
|
||||||
|
|
||||||
while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) {
|
while (firstNonWs < lineEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) {
|
||||||
firstNonWs++;
|
firstNonWs++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentEnd - firstNonWs >= BAS_REM_KEYWORD_LEN &&
|
int32_t contentEnd = lineEnd;
|
||||||
strncasecmp(src + firstNonWs, "REM", BAS_REM_KEYWORD_LEN) == 0 &&
|
|
||||||
(contentEnd - firstNonWs == BAS_REM_KEYWORD_LEN ||
|
if (firstNonWs < lineEnd && src[firstNonWs] == '\'') {
|
||||||
src[firstNonWs + BAS_REM_KEYWORD_LEN] == ' ' ||
|
contentEnd = firstNonWs;
|
||||||
src[firstNonWs + BAS_REM_KEYWORD_LEN] == '\t')) {
|
} else if (lineStart >= formEnd) {
|
||||||
|
// Code section: scan for the first unquoted ' (comment start).
|
||||||
|
bool inStr = false;
|
||||||
|
|
||||||
|
for (int32_t j = firstNonWs; j < lineEnd; j++) {
|
||||||
|
if (src[j] == '"') {
|
||||||
|
inStr = !inStr;
|
||||||
|
} else if (src[j] == '\'' && !inStr) {
|
||||||
|
contentEnd = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWholeLineRem(src + firstNonWs, contentEnd - firstNonWs)) {
|
||||||
contentEnd = firstNonWs;
|
contentEnd = firstNonWs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,7 +252,7 @@ int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, in
|
||||||
|
|
||||||
// Scan a .frm text: every "Begin <Type> <Name>" adds Name to names, and
|
// Scan a .frm text: every "Begin <Type> <Name>" adds Name to names, and
|
||||||
// Type plus every "<Key> = ..." property key goes into reserved.
|
// Type plus every "<Key> = ..." property key goes into reserved.
|
||||||
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved) {
|
static bool collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved) {
|
||||||
const char *p = text;
|
const char *p = text;
|
||||||
const char *end = text + len;
|
const char *end = text + len;
|
||||||
|
|
||||||
|
|
@ -260,37 +275,43 @@ static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim leading whitespace
|
// Trim leading whitespace
|
||||||
const char *l = skipWhitespace(lineStart, lineEnd);
|
const char *l = skipWhitespace(lineStart, lineEnd);
|
||||||
|
int32_t beginOff = basMatchKeywordWs(l, (int32_t)(lineEnd - l), FRM_KW_BEGIN, FRM_KW_BEGIN_LEN);
|
||||||
char token[BAS_OBF_TOKEN_LEN];
|
char token[BAS_OBF_TOKEN_LEN];
|
||||||
|
bool truncated;
|
||||||
|
|
||||||
if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
|
if (beginOff >= 0) {
|
||||||
l = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
|
l = readToken(l + beginOff, lineEnd, token, sizeof(token), &truncated);
|
||||||
l = readToken(l, lineEnd, token, sizeof(token));
|
|
||||||
|
|
||||||
if (token[0] == '\0') {
|
if (token[0] == '\0') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
nameMapAdd(reserved, token);
|
if (!truncated && !nameMapAdd(reserved, token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
l = skipWhitespace(l, lineEnd);
|
l = skipWhitespace(l, lineEnd);
|
||||||
readToken(l, lineEnd, token, sizeof(token));
|
readToken(l, lineEnd, token, sizeof(token), &truncated);
|
||||||
|
|
||||||
if (token[0] && basIsValidIdent(token)) {
|
// A name too long for the token buffer is never mapped.
|
||||||
nameMapAdd(names, token);
|
if (!truncated && token[0] && basIsValidIdent(token) && !nameMapAdd(names, token)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "<Key> = value": the key is a property name.
|
// "<Key> = value": the key is a property name.
|
||||||
l = readToken(l, lineEnd, token, sizeof(token));
|
l = readToken(l, lineEnd, token, sizeof(token), &truncated);
|
||||||
l = skipWhitespace(l, lineEnd);
|
l = skipWhitespace(l, lineEnd);
|
||||||
|
|
||||||
if (l < lineEnd && *l == '=' && token[0] && basIsValidIdent(token)) {
|
if (l < lineEnd && *l == '=' && !truncated && token[0] && basIsValidIdent(token) && !nameMapAdd(reserved, token)) {
|
||||||
nameMapAdd(reserved, token);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -309,7 +330,31 @@ static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Returns true if c is a valid identifier character.
|
// True when a property key holds the name of another control.
|
||||||
|
static bool isNameRefKey(const char *key) {
|
||||||
|
for (int32_t i = 0; sNameRefKeys[i]; i++) {
|
||||||
|
if (strcasecmp(key, sNameRefKeys[i]) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// True when a line's content (leading whitespace already skipped) is a
|
||||||
|
// REM comment: "REM" alone or followed by whitespace.
|
||||||
|
static bool isWholeLineRem(const char *text, int32_t len) {
|
||||||
|
if (len < BAS_REM_KEYWORD_LEN || strncasecmp(text, "REM", BAS_REM_KEYWORD_LEN) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return len == BAS_REM_KEYWORD_LEN || text[BAS_REM_KEYWORD_LEN] == ' ' || text[BAS_REM_KEYWORD_LEN] == '\t';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Map name to the next generated name ("C1", "C2" ...) unless it is
|
||||||
|
// already mapped. Returns the mapped name, or NULL when memory runs out.
|
||||||
static const char *nameMapAdd(NameMapT *m, const char *name) {
|
static const char *nameMapAdd(NameMapT *m, const char *name) {
|
||||||
const char *existing = nameMapLookup(m, name);
|
const char *existing = nameMapLookup(m, name);
|
||||||
|
|
||||||
|
|
@ -332,11 +377,20 @@ static const char *nameMapAdd(NameMapT *m, const char *name) {
|
||||||
char mapped[BAS_OBF_MAPPED_LEN];
|
char mapped[BAS_OBF_MAPPED_LEN];
|
||||||
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
|
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
|
||||||
|
|
||||||
m->entries[m->count].orig = strdup(name);
|
char *origCopy = strdup(name);
|
||||||
m->entries[m->count].mapped = strdup(mapped);
|
char *mappedCopy = strdup(mapped);
|
||||||
|
|
||||||
|
if (!origCopy || !mappedCopy) {
|
||||||
|
free(origCopy);
|
||||||
|
free(mappedCopy);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
m->entries[m->count].orig = origCopy;
|
||||||
|
m->entries[m->count].mapped = mappedCopy;
|
||||||
m->count++;
|
m->count++;
|
||||||
|
|
||||||
return m->entries[m->count - 1].mapped;
|
return mappedCopy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -372,12 +426,21 @@ static const char *nameMapLookup(const NameMapT *m, const char *name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Copy next whitespace-delimited token into buf. Returns pointer after token.
|
// Copy the next whitespace-delimited token into buf and return a pointer
|
||||||
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) {
|
// past the WHOLE token. *outTruncated reports a token longer than buf.
|
||||||
|
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize, bool *outTruncated) {
|
||||||
int32_t len = 0;
|
int32_t len = 0;
|
||||||
|
|
||||||
while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) {
|
*outTruncated = false;
|
||||||
buf[len++] = *p++;
|
|
||||||
|
while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') {
|
||||||
|
if (len < bufSize - 1) {
|
||||||
|
buf[len++] = *p;
|
||||||
|
} else {
|
||||||
|
*outTruncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
p++;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf[len] = '\0';
|
buf[len] = '\0';
|
||||||
|
|
@ -390,23 +453,25 @@ static const char *readToken(const char *p, const char *end, char *buf, int32_t
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Replace the contents of a constant pool entry with a new string.
|
// Replace the contents of a constant pool entry with a new string.
|
||||||
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
|
static bool replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
|
||||||
BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText));
|
BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText));
|
||||||
|
|
||||||
if (!newStr) {
|
if (!newStr) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
basStringUnref(mod->constants[idx]);
|
basStringUnref(mod->constants[idx]);
|
||||||
mod->constants[idx] = newStr;
|
mod->constants[idx] = newStr;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Rewrites the .frm text line by line. Only positions that hold a
|
// Rewrites the .frm text line by line. Only positions that hold a
|
||||||
// form/control NAME are remapped: the name token of a "Begin <Type> <Name>"
|
// form/control NAME are remapped: the name token of a "Begin <Type> <Name>"
|
||||||
// line and a property value that is exactly a mapped name (bare, or the
|
// line and the value of a control-reference property (sNameRefKeys) that
|
||||||
// whole content of a quoted string such as DataSource = "datCat"). Type
|
// is exactly a mapped name (bare, or the whole content of a quoted string
|
||||||
// tokens and property keys are copied verbatim. Identifiers of any length
|
// such as DataSource = "datCat"). Type tokens, property keys and every
|
||||||
|
// other value (a Caption equal to the form name ...) are copied verbatim. Identifiers of any length
|
||||||
// are copied from the source span, never through a fixed buffer. With out
|
// are copied from the source span, never through a fixed buffer. With out
|
||||||
// NULL nothing is written and the return value is the required size.
|
// NULL nothing is written and the return value is the required size.
|
||||||
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) {
|
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) {
|
||||||
|
|
@ -431,33 +496,34 @@ static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *m
|
||||||
p++;
|
p++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *l = skipWhitespace(lineStart, lineEnd);
|
const char *l = skipWhitespace(lineStart, lineEnd);
|
||||||
|
int32_t beginOff = basMatchKeywordWs(l, (int32_t)(lineEnd - l), FRM_KW_BEGIN, FRM_KW_BEGIN_LEN);
|
||||||
const char *replaceStart = NULL; // span of the original name to swap
|
const char *replaceStart = NULL; // span of the original name to swap
|
||||||
const char *replaceEnd = NULL;
|
const char *replaceEnd = NULL;
|
||||||
const char *mapped = NULL;
|
const char *mapped = NULL;
|
||||||
char token[BAS_OBF_TOKEN_LEN];
|
char token[BAS_OBF_TOKEN_LEN];
|
||||||
|
bool truncated;
|
||||||
|
|
||||||
if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
|
if (beginOff >= 0) {
|
||||||
// Begin <Type> <Name>: only the name token is a candidate.
|
// Begin <Type> <Name>: only the name token is a candidate.
|
||||||
const char *t = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
|
const char *t = readToken(l + beginOff, lineEnd, token, sizeof(token), &truncated);
|
||||||
t = readToken(t, lineEnd, token, sizeof(token));
|
|
||||||
t = skipWhitespace(t, lineEnd);
|
t = skipWhitespace(t, lineEnd);
|
||||||
|
|
||||||
const char *nameStart = t;
|
const char *nameStart = t;
|
||||||
t = readToken(t, lineEnd, token, sizeof(token));
|
t = readToken(t, lineEnd, token, sizeof(token), &truncated);
|
||||||
|
|
||||||
if (token[0] && t - nameStart == (int32_t)strlen(token)) {
|
if (token[0] && !truncated) {
|
||||||
mapped = nameMapLookup(map, token);
|
mapped = nameMapLookup(map, token);
|
||||||
replaceStart = nameStart;
|
replaceStart = nameStart;
|
||||||
replaceEnd = t;
|
replaceEnd = t;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// <Key> = value: a value that is exactly a mapped name (bare or
|
// <Key> = value: the value of a control-reference property that
|
||||||
// quoted) refers to a control and follows the rename.
|
// is exactly a mapped name (bare or quoted) follows the rename.
|
||||||
const char *t = readToken(l, lineEnd, token, sizeof(token));
|
const char *t = readToken(l, lineEnd, token, sizeof(token), &truncated);
|
||||||
t = skipWhitespace(t, lineEnd);
|
t = skipWhitespace(t, lineEnd);
|
||||||
|
|
||||||
if (t < lineEnd && *t == '=') {
|
if (t < lineEnd && *t == '=' && !truncated && isNameRefKey(token)) {
|
||||||
t = skipWhitespace(t + 1, lineEnd);
|
t = skipWhitespace(t + 1, lineEnd);
|
||||||
|
|
||||||
const char *valueStart = t;
|
const char *valueStart = t;
|
||||||
|
|
@ -502,7 +568,7 @@ static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
|
static bool rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
|
||||||
// KNOWN LIMITATION: the constant pool dedupes a plain string literal and a
|
// KNOWN LIMITATION: the constant pool dedupes a plain string literal and a
|
||||||
// same-text control/form-name reference into ONE entry, so a release build
|
// same-text control/form-name reference into ONE entry, so a release build
|
||||||
// that does e.g. MsgBox "Save" while also having a control named "Save"
|
// that does e.g. MsgBox "Save" while also having a control named "Save"
|
||||||
|
|
@ -522,35 +588,34 @@ static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
|
||||||
const char *mapped = nameMapLookup(map, s->data);
|
const char *mapped = nameMapLookup(map, s->data);
|
||||||
|
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
replaceConstant(mod, i, mapped);
|
if (!replaceConstant(mod, i, mapped)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "<Name>_<Event>" handler names used as SetEvent targets follow the
|
// "<Name>_<Event>" handler names used as SetEvent targets follow the
|
||||||
// procedure rename so basModuleFindProc still resolves them.
|
// procedure rename so basModuleFindProc still resolves them.
|
||||||
const char *underscore = strrchr(s->data, '_');
|
char prefix[BAS_MAX_IDENT];
|
||||||
|
const char *suffix;
|
||||||
|
|
||||||
if (!underscore || !basEventSuffixMatch(underscore + 1)) {
|
if (!basSplitEventName(s->data, prefix, (int32_t)sizeof(prefix), &suffix)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t prefixLen = (int32_t)(underscore - s->data);
|
mapped = nameMapLookup(map, prefix);
|
||||||
char prefix[BAS_MAX_IDENT];
|
|
||||||
|
|
||||||
if (prefixLen >= (int32_t)sizeof(prefix)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(prefix, s->data, prefixLen);
|
|
||||||
prefix[prefixLen] = '\0';
|
|
||||||
mapped = nameMapLookup(map, prefix);
|
|
||||||
|
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
char newName[BAS_MAX_IDENT];
|
char newName[BAS_MAX_IDENT];
|
||||||
snprintf(newName, sizeof(newName), "%s_%s", mapped, underscore + 1);
|
snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
|
||||||
replaceConstant(mod, i, newName);
|
|
||||||
|
if (!replaceConstant(mod, i, newName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -584,30 +649,14 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find last underscore
|
// <Control>_<Event> handlers follow the control rename.
|
||||||
char *underscore = strrchr(proc->name, '_');
|
char prefix[BAS_MAX_IDENT];
|
||||||
|
const char *suffix;
|
||||||
|
|
||||||
if (!underscore) {
|
if (!basSplitEventName(proc->name, prefix, (int32_t)sizeof(prefix), &suffix)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *suffix = underscore + 1;
|
|
||||||
|
|
||||||
if (!basEventSuffixMatch(suffix)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split on underscore
|
|
||||||
int32_t prefixLen = (int32_t)(underscore - proc->name);
|
|
||||||
char prefix[BAS_MAX_IDENT];
|
|
||||||
|
|
||||||
if (prefixLen >= (int32_t)sizeof(prefix)) {
|
|
||||||
prefixLen = (int32_t)sizeof(prefix) - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(prefix, proc->name, prefixLen);
|
|
||||||
prefix[prefixLen] = '\0';
|
|
||||||
|
|
||||||
const char *mapped = nameMapLookup(map, prefix);
|
const char *mapped = nameMapLookup(map, prefix);
|
||||||
|
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
|
|
|
||||||
|
|
@ -51,18 +51,29 @@ typedef struct {
|
||||||
// - Procedure names matching <OrigName>_<Event>
|
// - Procedure names matching <OrigName>_<Event>
|
||||||
// - formVarInfo entries keyed by form name
|
// - formVarInfo entries keyed by form name
|
||||||
//
|
//
|
||||||
|
// Only the values of properties that name another control (DataSource,
|
||||||
|
// MasterSource) follow a rename; a Caption or Text that happens to equal
|
||||||
|
// a control name is left alone.
|
||||||
|
//
|
||||||
// frmTexts / frmLens: input .frm buffers (one per form).
|
// frmTexts / frmLens: input .frm buffers (one per form).
|
||||||
// outFrms: caller-allocated array of frmCount entries; function fills
|
// outFrms: caller-allocated array of frmCount entries; function fills
|
||||||
// in .data (malloc'd, caller frees) and .len for each.
|
// in .data (malloc'd, caller frees) and .len for each.
|
||||||
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
|
//
|
||||||
|
// Returns false when memory runs out part way. The module and forms are
|
||||||
|
// then inconsistent (some names renamed, some not) and must not be
|
||||||
|
// shipped: the caller fails the build.
|
||||||
|
bool basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
|
||||||
|
|
||||||
|
|
||||||
// Strip comments from .frm text.
|
// Strip comments from .frm text.
|
||||||
//
|
//
|
||||||
// Removes both whole-line comments (lines whose first non-whitespace
|
// Removes whole-line comments (lines whose first non-whitespace token is
|
||||||
// token is `'` or `REM`) and trailing comments (everything from the
|
// `'` or `REM`) everywhere, and trailing comments (everything from the
|
||||||
// first unquoted `'` to end of line). Pure-whitespace lines are
|
// first unquoted `'` to end of line) in the BASIC code after the form
|
||||||
// dropped. Quoted string literals are preserved as-is.
|
// block. Inside the form block a `'` after a property value is part of
|
||||||
|
// the value, exactly as the form parser reads it (Caption = Don't).
|
||||||
|
// Pure-whitespace lines are dropped. Quoted string literals are
|
||||||
|
// preserved as-is.
|
||||||
//
|
//
|
||||||
// srcLen: input length; outBuf: caller-allocated buffer of outCap bytes.
|
// srcLen: input length; outBuf: caller-allocated buffer of outCap bytes.
|
||||||
// Returns the number of bytes written.
|
// Returns the number of bytes written.
|
||||||
|
|
|
||||||
|
|
@ -175,12 +175,13 @@ typedef enum {
|
||||||
#define OP_JMP_TRUE 0x51 // [int16 offset] jump if TOS is true
|
#define OP_JMP_TRUE 0x51 // [int16 offset] jump if TOS is true
|
||||||
#define OP_JMP_FALSE 0x52 // [int16 offset] jump if TOS is false
|
#define OP_JMP_FALSE 0x52 // [int16 offset] jump if TOS is false
|
||||||
#define OP_CALL 0x53 // [uint16 addr] [uint8 argc] [uint8 baseSlot]
|
#define OP_CALL 0x53 // [uint16 addr] [uint8 argc] [uint8 baseSlot]
|
||||||
#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return)
|
#define OP_GOSUB_RET 0x54 // pop the current frame's latest GOSUB return address and jump there (error 3 if none)
|
||||||
#define OP_RET 0x55 // return from subroutine
|
#define OP_RET 0x55 // return from subroutine
|
||||||
#define OP_RET_VAL 0x56 // return from function (value on stack)
|
#define OP_RET_VAL 0x56 // return from function (value on stack)
|
||||||
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty
|
#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 scope] [int16 loopTop]
|
#define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 scope] [int16 loopTop]
|
||||||
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
|
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
|
||||||
|
#define OP_GOSUB 0x5A // [int16 offset] push the next instruction's address on the GOSUB stack, jump
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Type conversion
|
// Type conversion
|
||||||
|
|
@ -200,6 +201,7 @@ typedef enum {
|
||||||
#define OP_PRINT 0x70 // print TOS to current output
|
#define OP_PRINT 0x70 // print TOS to current output
|
||||||
#define OP_PRINT_NL 0x71 // print newline
|
#define OP_PRINT_NL 0x71 // print newline
|
||||||
#define OP_PRINT_TAB 0x72 // print tab (14-column zones)
|
#define OP_PRINT_TAB 0x72 // print tab (14-column zones)
|
||||||
|
#define OP_LPRINT 0x73 // [uint8 BAS_LPRINT_BEGIN / BAS_LPRINT_END] route PRINT output to the printer for one LPRINT statement
|
||||||
#define OP_INPUT 0x74 // read line into string on stack
|
#define OP_INPUT 0x74 // read line into string on stack
|
||||||
#define OP_FILE_OPEN 0x75 // [uint8 mode] filename, channel#, record length on stack
|
#define OP_FILE_OPEN 0x75 // [uint8 mode] filename, channel#, record length on stack
|
||||||
#define OP_FILE_CLOSE 0x76 // channel# on stack
|
#define OP_FILE_CLOSE 0x76 // channel# on stack
|
||||||
|
|
@ -301,6 +303,7 @@ typedef enum {
|
||||||
#define OP_TIME_STR 0xB6 // push TIME$ string "HH:MM:SS"
|
#define OP_TIME_STR 0xB6 // push TIME$ string "HH:MM:SS"
|
||||||
#define OP_SLEEP 0xB7 // pop seconds, sleep
|
#define OP_SLEEP 0xB7 // pop seconds, sleep
|
||||||
#define OP_ENVIRON 0xB8 // pop env var name, push value string
|
#define OP_ENVIRON 0xB8 // pop env var name, push value string
|
||||||
|
#define OP_COMMAND 0xDC // pop argument index, push COMMAND$ (0 = every argument)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// DATA/READ/RESTORE
|
// DATA/READ/RESTORE
|
||||||
|
|
@ -336,6 +339,20 @@ typedef enum {
|
||||||
#define OP_STR_FIXLEN 0xC5 // [uint16 len] pop string, pad/truncate, push
|
#define OP_STR_FIXLEN 0xC5 // [uint16 len] pop string, pad/truncate, push
|
||||||
#define OP_STR_MID_ASGN 0xC6 // pop replacement, len, start, str; push modified
|
#define OP_STR_MID_ASGN 0xC6 // pop replacement, len, start, str; push modified
|
||||||
|
|
||||||
|
// Length pushed for a MID$ statement without one: replace as much of the
|
||||||
|
// replacement as fits (an explicit 0 replaces nothing).
|
||||||
|
#define BAS_MID_LEN_OMITTED (-1)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// String append: var = var + x / var = var & x
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// [uint8 flags] [uint8 scope] [uint16 varIdx] pop x, store var + x (or
|
||||||
|
// var & x with BAS_APPEND_AMP) into the variable, appending in place
|
||||||
|
// when the variable's string is not shared.
|
||||||
|
#define OP_STR_APPEND 0xCE
|
||||||
|
#define BAS_APPEND_AMP 0x01 // flags: '&' semantics (both sides as strings) instead of '+'
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PRINT USING
|
// PRINT USING
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -413,6 +430,11 @@ typedef enum {
|
||||||
#define BAS_USING_FIRST 0x01
|
#define BAS_USING_FIRST 0x01
|
||||||
#define BAS_USING_LAST 0x02
|
#define BAS_USING_LAST 0x02
|
||||||
|
|
||||||
|
// OP_LPRINT operand: BEGIN sends the PRINT opcodes that follow to the
|
||||||
|
// printer, END sends the statement's output and goes back to the screen.
|
||||||
|
#define BAS_LPRINT_END 0
|
||||||
|
#define BAS_LPRINT_BEGIN 1
|
||||||
|
|
||||||
// OP_ERASE modes.
|
// OP_ERASE modes.
|
||||||
#define BAS_ERASE_ZERO 0 // fixed array: reset every element, keep the bounds
|
#define BAS_ERASE_ZERO 0 // fixed array: reset every element, keep the bounds
|
||||||
#define BAS_ERASE_FREE 1 // dynamic array: release the storage (undimensioned until REDIM)
|
#define BAS_ERASE_FREE 1 // dynamic array: release the storage (undimensioned until REDIM)
|
||||||
|
|
@ -428,15 +450,6 @@ typedef enum {
|
||||||
#define BAS_LAYOUT_MAX_LEN 1024
|
#define BAS_LAYOUT_MAX_LEN 1024
|
||||||
#define BAS_LAYOUT_MAX_DEPTH 16 // nesting cap for TYPE fields inside a layout
|
#define BAS_LAYOUT_MAX_DEPTH 16 // nesting cap for TYPE fields inside a layout
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Operand-size table and bytecode pattern helpers
|
|
||||||
// ============================================================
|
|
||||||
//
|
|
||||||
// Single machine-readable source of truth for the operand byte count of
|
|
||||||
// every opcode (the trailing comments above are human hints). Shared by
|
|
||||||
// the VM (RESUME NEXT statement walk) and the release compactor.
|
|
||||||
|
|
||||||
|
|
||||||
// OP_COMPARE_MODE operand values (OPTION COMPARE BINARY / TEXT).
|
// OP_COMPARE_MODE operand values (OPTION COMPARE BINARY / TEXT).
|
||||||
#define BAS_COMPARE_MODE_BINARY 0
|
#define BAS_COMPARE_MODE_BINARY 0
|
||||||
#define BAS_COMPARE_MODE_TEXT 1
|
#define BAS_COMPARE_MODE_TEXT 1
|
||||||
|
|
@ -445,6 +458,20 @@ typedef enum {
|
||||||
// clock when it pops this value.
|
// clock when it pops this value.
|
||||||
#define BAS_RANDOMIZE_TIMER_SEED (-1)
|
#define BAS_RANDOMIZE_TIMER_SEED (-1)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Operand-size table and bytecode pattern helpers
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Single machine-readable source of truth for the operand byte count of
|
||||||
|
// every opcode (the trailing comments above are human hints). Shared by
|
||||||
|
// the VM (RESUME NEXT statement walk) and the release compactor.
|
||||||
|
//
|
||||||
|
// The BAS_OPERAND_* names give the operand SIZE in bytes, not its layout:
|
||||||
|
// OP_DIM_ARRAY's two uint8 operands are BAS_OPERAND_U16 (2 bytes), OP_REDIM's
|
||||||
|
// three uint8 operands BAS_OPERAND_U8_U16 (3 bytes), and OP_CALL's
|
||||||
|
// [uint16 addr][uint8 argc][uint8 baseSlot] BAS_OPERAND_I32 (4 bytes). The
|
||||||
|
// per-opcode comments above describe the real layout.
|
||||||
|
|
||||||
// Operand widths (bytes after the 1-byte opcode).
|
// Operand widths (bytes after the 1-byte opcode).
|
||||||
#define BAS_OPERAND_NONE 0
|
#define BAS_OPERAND_NONE 0
|
||||||
#define BAS_OPERAND_U8 1
|
#define BAS_OPERAND_U8 1
|
||||||
|
|
@ -456,28 +483,16 @@ typedef enum {
|
||||||
#define BAS_OPERAND_F64 8
|
#define BAS_OPERAND_F64 8
|
||||||
#define BAS_OPERAND_UNKNOWN (-1)
|
#define BAS_OPERAND_UNKNOWN (-1)
|
||||||
|
|
||||||
// GOSUB call sequence: OP_PUSH_INT32 <returnAddr> OP_JMP <offset>, where
|
// Byte offsets inside the FOR opcodes' BAS_OPERAND_FOR operand.
|
||||||
// returnAddr equals the address right after the OP_JMP.
|
#define BAS_FOR_OFFSET_OPERAND (1 + BAS_OPERAND_U16 + BAS_OPERAND_U8) // opcode-relative position of the int16 jump offset
|
||||||
#define BAS_GOSUB_PATTERN_LEN 8 // PUSH_INT32(1+4) + JMP(1+2)
|
|
||||||
#define BAS_GOSUB_JMP_OFFSET 5 // byte offset of the OP_JMP inside the pattern
|
|
||||||
|
|
||||||
static inline bool basIsGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos);
|
|
||||||
static inline int32_t basOpcodeOperandSize(uint8_t op);
|
static inline int32_t basOpcodeOperandSize(uint8_t op);
|
||||||
|
static inline int16_t basReadI16LE(const uint8_t *p);
|
||||||
static inline int32_t basReadI32LE(const uint8_t *p);
|
static inline int32_t basReadI32LE(const uint8_t *p);
|
||||||
|
static inline uint16_t basReadU16LE(const uint8_t *p);
|
||||||
|
static inline void basWriteI16LE(uint8_t *p, int16_t v);
|
||||||
// True when code[pos] starts a GOSUB call sequence (see BAS_GOSUB_PATTERN_LEN).
|
static inline void basWriteI32LE(uint8_t *p, int32_t v);
|
||||||
static inline bool basIsGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
|
static inline void basWriteU16LE(uint8_t *p, uint16_t v);
|
||||||
if (pos + BAS_GOSUB_PATTERN_LEN > codeLen) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code[pos] != OP_PUSH_INT32 || code[pos + BAS_GOSUB_JMP_OFFSET] != OP_JMP) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return basReadI32LE(code + pos + 1) == pos + BAS_GOSUB_PATTERN_LEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Returns operand byte count for op, or BAS_OPERAND_UNKNOWN.
|
// Returns operand byte count for op, or BAS_OPERAND_UNKNOWN.
|
||||||
|
|
@ -526,7 +541,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
|
||||||
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
|
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
|
||||||
case OP_STR_STRING: case OP_STR_OCT: case OP_CONV_BOOL:
|
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_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
|
||||||
case OP_SLEEP: case OP_ENVIRON:
|
case OP_SLEEP: case OP_ENVIRON: case OP_COMMAND:
|
||||||
case OP_READ_DATA: case OP_RESTORE:
|
case OP_READ_DATA: case OP_RESTORE:
|
||||||
case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL:
|
case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL:
|
||||||
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK:
|
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK:
|
||||||
|
|
@ -553,7 +568,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
|
||||||
case OP_CALL_METHOD: case OP_SHOW_FORM:
|
case OP_CALL_METHOD: case OP_SHOW_FORM:
|
||||||
case OP_LBOUND: case OP_UBOUND:
|
case OP_LBOUND: case OP_UBOUND:
|
||||||
case OP_COMPARE_MODE: case OP_PRINT_USING:
|
case OP_COMPARE_MODE: case OP_PRINT_USING:
|
||||||
case OP_ERASE:
|
case OP_ERASE: case OP_LPRINT:
|
||||||
return BAS_OPERAND_U8;
|
return BAS_OPERAND_U8;
|
||||||
|
|
||||||
case OP_PUSH_INT16: case OP_PUSH_STR:
|
case OP_PUSH_INT16: case OP_PUSH_STR:
|
||||||
|
|
@ -562,6 +577,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
|
||||||
case OP_LOAD_FIELD: case OP_STORE_FIELD:
|
case OP_LOAD_FIELD: case OP_STORE_FIELD:
|
||||||
case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR:
|
case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR:
|
||||||
case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE:
|
case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE:
|
||||||
|
case OP_GOSUB:
|
||||||
case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
|
case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
|
||||||
case OP_PUSH_FORM_ADDR:
|
case OP_PUSH_FORM_ADDR:
|
||||||
case OP_DIM_ARRAY:
|
case OP_DIM_ARRAY:
|
||||||
|
|
@ -576,6 +592,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
|
||||||
|
|
||||||
case OP_PUSH_INT32:
|
case OP_PUSH_INT32:
|
||||||
case OP_CALL:
|
case OP_CALL:
|
||||||
|
case OP_STR_APPEND:
|
||||||
return BAS_OPERAND_I32;
|
return BAS_OPERAND_I32;
|
||||||
|
|
||||||
case OP_FOR_INIT:
|
case OP_FOR_INIT:
|
||||||
|
|
@ -594,10 +611,47 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Little-endian int32 operand read (bytecode is always LE regardless of host).
|
// ============================================================
|
||||||
|
// Little-endian operand access
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Bytecode is always little-endian regardless of the host; the compiler,
|
||||||
|
// the compactor and the serializer all go through these.
|
||||||
|
|
||||||
|
static inline int16_t basReadI16LE(const uint8_t *p) {
|
||||||
|
return (int16_t)basReadU16LE(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline int32_t basReadI32LE(const uint8_t *p) {
|
static inline int32_t basReadI32LE(const uint8_t *p) {
|
||||||
return (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
|
return (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline uint16_t basReadU16LE(const uint8_t *p) {
|
||||||
|
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void basWriteI16LE(uint8_t *p, int16_t v) {
|
||||||
|
basWriteU16LE(p, (uint16_t)v);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void basWriteI32LE(uint8_t *p, int32_t v) {
|
||||||
|
uint32_t u = (uint32_t)v;
|
||||||
|
|
||||||
|
p[0] = (uint8_t)(u & 0xFF);
|
||||||
|
p[1] = (uint8_t)((u >> 8) & 0xFF);
|
||||||
|
p[2] = (uint8_t)((u >> 16) & 0xFF);
|
||||||
|
p[3] = (uint8_t)((u >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void basWriteU16LE(uint8_t *p, uint16_t v) {
|
||||||
|
p[0] = (uint8_t)(v & 0xFF);
|
||||||
|
p[1] = (uint8_t)((v >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif // DVXBASIC_OPCODES_H
|
#endif // DVXBASIC_OPCODES_H
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -97,7 +97,11 @@ typedef struct {
|
||||||
int32_t errorLine;
|
int32_t errorLine;
|
||||||
int32_t prevLine; // line of the previous token (for error reporting)
|
int32_t prevLine; // line of the previous token (for error reporting)
|
||||||
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
|
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
|
||||||
int32_t tempCount; // compiler temporaries allocated so far (SWAP of array elements)
|
int32_t tempCount; // compiler temporaries created so far (names them uniquely)
|
||||||
|
BasSymbolT **tempPool; // stb_ds: temporaries of the current scope, reused from the start of every statement
|
||||||
|
int32_t tempPoolUsed; // temporaries of tempPool taken by the current statement
|
||||||
|
BasSymbolT **typeDefs; // stb_ds: TYPE definitions, indexed by typeId
|
||||||
|
bool inDefFn; // parsing a DEF FN body: undeclared names are module variables
|
||||||
int32_t optionBase; // default array lower bound (0 or 1)
|
int32_t optionBase; // default array lower bound (0 or 1)
|
||||||
bool optionExplicit; // true = variables must be declared with DIM
|
bool optionExplicit; // true = variables must be declared with DIM
|
||||||
bool currentProcIsFunction; // true while parsing a FUNCTION body (EXIT FUNCTION / return-value assignment)
|
bool currentProcIsFunction; // true while parsing a FUNCTION body (EXIT FUNCTION / return-value assignment)
|
||||||
|
|
@ -123,9 +127,9 @@ typedef struct {
|
||||||
// compile error rather than a jump patched against an outer scope.
|
// compile error rather than a jump patched against an outer scope.
|
||||||
int32_t forDepth;
|
int32_t forDepth;
|
||||||
int32_t doDepth;
|
int32_t doDepth;
|
||||||
// SELECT depth at the most recently defined label. A module-level
|
// SELECT depth at the most recently defined label. RETURN discards
|
||||||
// RETURN discards the test values of every SELECT opened since that
|
// the test values of every SELECT opened since that label so the
|
||||||
// label so OP_GOSUB_RET pops the return address, not a test value.
|
// eval stack is balanced after the GOSUB returns.
|
||||||
int32_t lastLabelSelectDepth;
|
int32_t lastLabelSelectDepth;
|
||||||
// Recursion guards (see BAS_MAX_EXPR_DEPTH / BAS_MAX_BLOCK_DEPTH).
|
// Recursion guards (see BAS_MAX_EXPR_DEPTH / BAS_MAX_BLOCK_DEPTH).
|
||||||
int32_t exprDepth;
|
int32_t exprDepth;
|
||||||
|
|
@ -154,6 +158,11 @@ bool basParse(BasParserT *p);
|
||||||
// Caller owns the module and must free with basModuleFree().
|
// Caller owns the module and must free with basModuleFree().
|
||||||
BasModuleT *basParserBuildModule(BasParserT *p);
|
BasModuleT *basParserBuildModule(BasParserT *p);
|
||||||
|
|
||||||
|
// BEGINFORM name of the code being parsed right now ("" outside every
|
||||||
|
// form). A validator's lookupCtrlType can call it (keep the parser in
|
||||||
|
// the validator ctx) to resolve a control name on the form it belongs to.
|
||||||
|
const char *basParserFormScope(const BasParserT *p);
|
||||||
|
|
||||||
// Free parser resources.
|
// Free parser resources.
|
||||||
void basParserFree(BasParserT *p);
|
void basParserFree(BasParserT *p);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,10 @@ const char *basEventSuffixes[] = {
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
bool basEventSuffixMatch(const char *suffix);
|
bool basEventSuffixMatch(const char *suffix);
|
||||||
|
bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix);
|
||||||
void basStripModule(BasModuleT *mod);
|
void basStripModule(BasModuleT *mod);
|
||||||
static bool nameEndsWithEventSuffix(const char *name);
|
static int compareNamesCI(const void *a, const void *b);
|
||||||
static bool nameInConstantPool(const BasModuleT *mod, const char *name);
|
static bool sortedHasName(const char **names, int32_t count, const char *name);
|
||||||
static bool procNameTaken(const BasModuleT *mod, int32_t selfIdx, const char *name);
|
|
||||||
|
|
||||||
|
|
||||||
bool basEventSuffixMatch(const char *suffix) {
|
bool basEventSuffixMatch(const char *suffix) {
|
||||||
|
|
@ -70,6 +70,26 @@ bool basEventSuffixMatch(const char *suffix) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix) {
|
||||||
|
const char *underscore = strrchr(name, '_');
|
||||||
|
|
||||||
|
if (!underscore || !basEventSuffixMatch(underscore + 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t prefixLen = (int32_t)(underscore - name);
|
||||||
|
|
||||||
|
if (prefixLen >= prefixSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(prefix, name, prefixLen);
|
||||||
|
prefix[prefixLen] = '\0';
|
||||||
|
*outSuffix = underscore + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void basStripModule(BasModuleT *mod) {
|
void basStripModule(BasModuleT *mod) {
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -94,73 +114,68 @@ void basStripModule(BasModuleT *mod) {
|
||||||
// Mangle proc names. Keep names that are needed for runtime name
|
// Mangle proc names. Keep names that are needed for runtime name
|
||||||
// lookup: event handlers (Control_Event pattern) and any name
|
// lookup: event handlers (Control_Event pattern) and any name
|
||||||
// referenced as a string constant (e.g. SetEvent's target name).
|
// referenced as a string constant (e.g. SetEvent's target name).
|
||||||
int32_t nextMangled = 1;
|
// Every other proc becomes F1, F2 ...; a generated name must not equal
|
||||||
|
// a kept name or a constant, or basModuleFindProc could resolve the
|
||||||
|
// wrong procedure at runtime. Both sets are sorted once so each check
|
||||||
|
// is a binary search. Without memory for them, nothing is mangled.
|
||||||
|
const char **consts = (const char **)malloc((mod->constCount + 1) * sizeof(char *));
|
||||||
|
const char **kept = (const char **)malloc((mod->procCount + 1) * sizeof(char *));
|
||||||
|
bool *mangle = (bool *)malloc((mod->procCount + 1) * sizeof(bool));
|
||||||
|
int32_t nConst = 0;
|
||||||
|
int32_t nKept = 0;
|
||||||
|
|
||||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
if (consts && kept && mangle) {
|
||||||
BasProcEntryT *proc = &mod->procs[i];
|
for (int32_t i = 0; i < mod->constCount; i++) {
|
||||||
|
if (mod->constants[i]) {
|
||||||
if (proc->name[0] == '\0') {
|
consts[nConst++] = mod->constants[i]->data;
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameEndsWithEventSuffix(proc->name)) {
|
qsort(consts, nConst, sizeof(char *), compareNamesCI);
|
||||||
continue;
|
|
||||||
|
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||||
|
const char *name = mod->procs[i].name;
|
||||||
|
char prefix[BAS_MAX_IDENT];
|
||||||
|
const char *suffix;
|
||||||
|
|
||||||
|
mangle[i] = name[0] != '\0' && !basSplitEventName(name, prefix, (int32_t)sizeof(prefix), &suffix) && !sortedHasName(consts, nConst, name);
|
||||||
|
|
||||||
|
if (!mangle[i]) {
|
||||||
|
kept[nKept++] = name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameInConstantPool(mod, proc->name)) {
|
qsort(kept, nKept, sizeof(char *), compareNamesCI);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip any generated name that collides with a kept proc (event
|
int32_t nextMangled = 1;
|
||||||
// handler or constant-pool-referenced) or a constant-pool entry, so
|
|
||||||
// basModuleFindProc can't resolve the wrong procedure at runtime.
|
|
||||||
char cand[BAS_MAX_IDENT];
|
|
||||||
|
|
||||||
snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);
|
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||||
|
if (!mangle[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
while (procNameTaken(mod, i, cand) || nameInConstantPool(mod, cand)) {
|
char cand[BAS_MAX_IDENT];
|
||||||
snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);
|
|
||||||
}
|
|
||||||
|
|
||||||
snprintf(proc->name, sizeof(proc->name), "%s", cand);
|
do {
|
||||||
}
|
snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);
|
||||||
}
|
} while (sortedHasName(kept, nKept, cand) || sortedHasName(consts, nConst, cand));
|
||||||
|
|
||||||
|
snprintf(mod->procs[i].name, sizeof(mod->procs[i].name), "%s", cand);
|
||||||
static bool nameEndsWithEventSuffix(const char *name) {
|
|
||||||
const char *underscore = strrchr(name, '_');
|
|
||||||
|
|
||||||
if (!underscore) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return basEventSuffixMatch(underscore + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static bool nameInConstantPool(const BasModuleT *mod, const char *name) {
|
|
||||||
for (int32_t i = 0; i < mod->constCount; i++) {
|
|
||||||
const BasStringT *s = mod->constants[i];
|
|
||||||
|
|
||||||
if (s && strcasecmp(s->data, name) == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
free(consts);
|
||||||
|
free(kept);
|
||||||
|
free(mangle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool procNameTaken(const BasModuleT *mod, int32_t selfIdx, const char *name) {
|
// qsort / bsearch order for arrays of names (case-insensitive).
|
||||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
static int compareNamesCI(const void *a, const void *b) {
|
||||||
if (i == selfIdx) {
|
return strcasecmp(*(const char * const *)a, *(const char * const *)b);
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (strcasecmp(mod->procs[i].name, name) == 0) {
|
static bool sortedHasName(const char **names, int32_t count, const char *name) {
|
||||||
return true;
|
return count > 0 && bsearch(&name, names, count, sizeof(char *), compareNamesCI) != NULL;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
#include "symtab.h"
|
#include "symtab.h"
|
||||||
#include "thirdparty/stb_ds_wrap.h"
|
#include "thirdparty/stb_ds_wrap.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
|
@ -33,18 +32,28 @@
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
static void basSymbolFree(BasSymbolT *sym);
|
static void basSymbolFree(BasSymbolT *sym);
|
||||||
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
||||||
|
BasSymbolT *basSymTabAddGlobal(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
||||||
|
BasSymbolT *basSymTabAddModuleVar(BasSymTabT *tab, const char *name, uint8_t dataType);
|
||||||
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
|
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
|
||||||
int32_t basSymTabAllocSlot(BasSymTabT *tab);
|
int32_t basSymTabAllocSlot(BasSymTabT *tab);
|
||||||
|
BasScopeE basSymTabCurrentScope(const BasSymTabT *tab, BasSymKindE kind);
|
||||||
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
|
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
|
||||||
void basSymTabEnterLocal(BasSymTabT *tab);
|
void basSymTabEnterLocal(BasSymTabT *tab);
|
||||||
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
|
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
|
||||||
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
|
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
|
||||||
|
BasSymbolT *basSymTabFindLabel(BasSymTabT *tab, const char *name);
|
||||||
|
BasSymbolT *basSymTabFindModuleVar(BasSymTabT *tab, const char *name);
|
||||||
void basSymTabFree(BasSymTabT *tab);
|
void basSymTabFree(BasSymTabT *tab);
|
||||||
void basSymTabInit(BasSymTabT *tab);
|
void basSymTabInit(BasSymTabT *tab);
|
||||||
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
|
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
|
||||||
void basSymTabLeaveLocal(BasSymTabT *tab);
|
void basSymTabLeaveLocal(BasSymTabT *tab);
|
||||||
|
static BasSymbolT **bucketFor(BasSymTabT *tab, uint32_t hash);
|
||||||
|
static BasSymbolT *findInScope(BasSymTabT *tab, const char *name, BasScopeE scope, bool isLabel);
|
||||||
|
static BasSymbolT *findSymbol(BasSymTabT *tab, const char *name, bool includeHidden);
|
||||||
static uint32_t nameHashCI(const char *name);
|
static uint32_t nameHashCI(const char *name);
|
||||||
static bool namesEqual(const char *a, const char *b);
|
static bool namesEqual(const char *a, const char *b);
|
||||||
|
static BasSymbolT *symTabAddScoped(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType, BasScopeE scope);
|
||||||
|
static void unlinkSymbol(BasSymTabT *tab, BasSymbolT *sym);
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -58,63 +67,23 @@ static void basSymbolFree(BasSymbolT *sym) {
|
||||||
|
|
||||||
|
|
||||||
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
|
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
|
||||||
// Determine scope: local > form > global.
|
return symTabAddScoped(tab, name, kind, dataType, basSymTabCurrentScope(tab, kind));
|
||||||
// Only variables get SCOPE_FORM; SUBs/FUNCTIONs/CONSTs remain global.
|
}
|
||||||
BasScopeE scope;
|
|
||||||
|
|
||||||
if (tab->inLocalScope) {
|
|
||||||
scope = SCOPE_LOCAL;
|
BasSymbolT *basSymTabAddGlobal(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
|
||||||
} else if (tab->inFormScope && kind == SYM_VARIABLE) {
|
return symTabAddScoped(tab, name, kind, dataType, SCOPE_GLOBAL);
|
||||||
scope = SCOPE_FORM;
|
}
|
||||||
} else {
|
|
||||||
scope = SCOPE_GLOBAL;
|
|
||||||
|
BasSymbolT *basSymTabAddModuleVar(BasSymTabT *tab, const char *name, uint8_t dataType) {
|
||||||
|
BasScopeE scope = tab->inFormScope ? SCOPE_FORM : SCOPE_GLOBAL;
|
||||||
|
BasSymbolT *sym = symTabAddScoped(tab, name, SYM_VARIABLE, dataType, scope);
|
||||||
|
|
||||||
|
if (sym != NULL) {
|
||||||
|
sym->index = tab->inFormScope ? tab->nextFormVarIdx++ : tab->nextGlobalIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Never truncate: a clipped name would hash and compare differently
|
|
||||||
// from every later reference, silently splitting one variable in two.
|
|
||||||
if (strlen(name) >= BAS_MAX_IDENT) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t h = nameHashCI(name);
|
|
||||||
|
|
||||||
// Check for duplicate in current scope (skip ended form symbols)
|
|
||||||
for (int32_t i = 0; i < tab->count; i++) {
|
|
||||||
if (tab->symbols[i]->formScopeEnded) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tab->symbols[i]->nameHash == h && tab->symbols[i]->scope == scope && namesEqual(tab->symbols[i]->name, name)) {
|
|
||||||
return NULL; // duplicate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BasSymbolT *sym = (BasSymbolT *)calloc(1, sizeof(BasSymbolT));
|
|
||||||
|
|
||||||
if (!sym) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
strcpy(sym->name, name);
|
|
||||||
sym->nameHash = h;
|
|
||||||
sym->kind = kind;
|
|
||||||
sym->scope = scope;
|
|
||||||
sym->dataType = dataType;
|
|
||||||
sym->isDefined = true;
|
|
||||||
|
|
||||||
// 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_IDENT - 1);
|
|
||||||
sym->formName[BAS_MAX_IDENT - 1] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
arrput(tab->symbols, sym);
|
|
||||||
tab->count = (int32_t)arrlen(tab->symbols);
|
|
||||||
return sym;
|
return sym;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,66 +106,56 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Local > form > global. Only variables get SCOPE_FORM; SUBs, FUNCTIONs,
|
||||||
|
// CONSTs and labels declared in a form scope remain global.
|
||||||
|
BasScopeE basSymTabCurrentScope(const BasSymTabT *tab, BasSymKindE kind) {
|
||||||
|
if (tab->inLocalScope) {
|
||||||
|
return SCOPE_LOCAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab->inFormScope && kind == SYM_VARIABLE) {
|
||||||
|
return SCOPE_FORM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SCOPE_GLOBAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) {
|
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) {
|
||||||
tab->inFormScope = true;
|
tab->inFormScope = true;
|
||||||
strncpy(tab->formScopeName, formName, BAS_MAX_IDENT - 1);
|
strncpy(tab->formScopeName, formName, BAS_MAX_IDENT - 1);
|
||||||
tab->formScopeName[BAS_MAX_IDENT - 1] = '\0';
|
tab->formScopeName[BAS_MAX_IDENT - 1] = '\0';
|
||||||
tab->nextFormVarIdx = 0;
|
tab->nextFormVarIdx = 0;
|
||||||
tab->formScopeSymStart = tab->count;
|
tab->formScopeSymStart = tab->count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void basSymTabEnterLocal(BasSymTabT *tab) {
|
void basSymTabEnterLocal(BasSymTabT *tab) {
|
||||||
tab->inLocalScope = true;
|
tab->inLocalScope = true;
|
||||||
|
tab->hideUnshared = true;
|
||||||
tab->nextLocalIdx = 0;
|
tab->nextLocalIdx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
|
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
|
||||||
uint32_t h = nameHashCI(name);
|
return findSymbol(tab, name, false);
|
||||||
|
|
||||||
// Search local scope first
|
|
||||||
if (tab->inLocalScope) {
|
|
||||||
for (int32_t i = tab->count - 1; i >= 0; i--) {
|
|
||||||
BasSymbolT *s = tab->symbols[i];
|
|
||||||
if (s->nameHash == h && s->scope == SCOPE_LOCAL && namesEqual(s->name, name)) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search form scope and global scope
|
|
||||||
for (int32_t i = tab->count - 1; i >= 0; i--) {
|
|
||||||
BasSymbolT *s = tab->symbols[i];
|
|
||||||
if (s->formScopeEnded) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s->nameHash == h && (s->scope == SCOPE_FORM || s->scope == SCOPE_GLOBAL) &&
|
|
||||||
namesEqual(s->name, name)) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
|
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
|
||||||
uint32_t h = nameHashCI(name);
|
return findInScope(tab, name, SCOPE_GLOBAL, false);
|
||||||
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < tab->count; i++) {
|
|
||||||
BasSymbolT *s = tab->symbols[i];
|
|
||||||
if (s->formScopeEnded) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s->nameHash == h && s->scope == SCOPE_GLOBAL && namesEqual(s->name, name)) {
|
BasSymbolT *basSymTabFindLabel(BasSymTabT *tab, const char *name) {
|
||||||
return s;
|
return findInScope(tab, name, tab->inLocalScope ? SCOPE_LOCAL : SCOPE_GLOBAL, true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
|
BasSymbolT *basSymTabFindModuleVar(BasSymTabT *tab, const char *name) {
|
||||||
|
BasSymbolT *sym = findSymbol(tab, name, true);
|
||||||
|
|
||||||
|
return (sym != NULL && sym->kind == SYM_VARIABLE && sym->scope != SCOPE_LOCAL) ? sym : NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -210,6 +169,7 @@ void basSymTabFree(BasSymTabT *tab) {
|
||||||
arrfree(tab->symbols);
|
arrfree(tab->symbols);
|
||||||
tab->symbols = NULL;
|
tab->symbols = NULL;
|
||||||
tab->count = 0;
|
tab->count = 0;
|
||||||
|
memset(tab->buckets, 0, sizeof(tab->buckets));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -244,12 +204,10 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
|
||||||
|
|
||||||
for (int32_t i = 0; i < tab->count; i++) {
|
for (int32_t i = 0; i < tab->count; i++) {
|
||||||
if (tab->symbols[i]->scope == SCOPE_LOCAL) {
|
if (tab->symbols[i]->scope == SCOPE_LOCAL) {
|
||||||
|
unlinkSymbol(tab, tab->symbols[i]);
|
||||||
basSymbolFree(tab->symbols[i]);
|
basSymbolFree(tab->symbols[i]);
|
||||||
} else {
|
} else {
|
||||||
if (i != newCount) {
|
tab->symbols[newCount] = tab->symbols[i];
|
||||||
tab->symbols[newCount] = tab->symbols[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
newCount++;
|
newCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -257,10 +215,64 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
|
||||||
arrsetlen(tab->symbols, newCount);
|
arrsetlen(tab->symbols, newCount);
|
||||||
tab->count = newCount;
|
tab->count = newCount;
|
||||||
tab->inLocalScope = false;
|
tab->inLocalScope = false;
|
||||||
|
tab->hideUnshared = false;
|
||||||
tab->nextLocalIdx = 0;
|
tab->nextLocalIdx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BasSymbolT **bucketFor(BasSymTabT *tab, uint32_t hash) {
|
||||||
|
return &tab->buckets[hash & (BAS_SYMTAB_BUCKETS - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The live symbol named name in exactly one scope and namespace (labels
|
||||||
|
// or everything else), or NULL.
|
||||||
|
static BasSymbolT *findInScope(BasSymTabT *tab, const char *name, BasScopeE scope, bool isLabel) {
|
||||||
|
uint32_t h = nameHashCI(name);
|
||||||
|
|
||||||
|
for (BasSymbolT *s = *bucketFor(tab, h); s != NULL; s = s->hashNext) {
|
||||||
|
if (s->nameHash == h && !s->formScopeEnded && s->scope == scope && (s->kind == SYM_LABEL) == isLabel && namesEqual(s->name, name)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Non-label lookup: local scope first, then form and global. Chains run
|
||||||
|
// newest first, so the most recent declaration wins. A module variable
|
||||||
|
// that is not SHARED is skipped inside a procedure unless includeHidden.
|
||||||
|
static BasSymbolT *findSymbol(BasSymTabT *tab, const char *name, bool includeHidden) {
|
||||||
|
uint32_t h = nameHashCI(name);
|
||||||
|
BasSymbolT *head = *bucketFor(tab, h);
|
||||||
|
|
||||||
|
if (tab->inLocalScope) {
|
||||||
|
for (BasSymbolT *s = head; s != NULL; s = s->hashNext) {
|
||||||
|
if (s->nameHash == h && s->scope == SCOPE_LOCAL && s->kind != SYM_LABEL && namesEqual(s->name, name)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hide = tab->hideUnshared && !includeHidden;
|
||||||
|
|
||||||
|
for (BasSymbolT *s = head; s != NULL; s = s->hashNext) {
|
||||||
|
if (s->nameHash != h || s->formScopeEnded || s->kind == SYM_LABEL || s->scope == SCOPE_LOCAL || !namesEqual(s->name, name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hide && s->kind == SYM_VARIABLE && !s->isShared) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
|
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
|
||||||
// Caller computes hash of search-name once; each entry stores its own
|
// Caller computes hash of search-name once; each entry stores its own
|
||||||
|
|
@ -299,3 +311,62 @@ static bool namesEqual(const char *a, const char *b) {
|
||||||
|
|
||||||
return *a == *b;
|
return *a == *b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BasSymbolT *symTabAddScoped(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType, BasScopeE scope) {
|
||||||
|
// Never truncate: a clipped name would hash and compare differently
|
||||||
|
// from every later reference, silently splitting one variable in two.
|
||||||
|
if (strlen(name) >= BAS_MAX_IDENT) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findInScope(tab, name, scope, kind == SYM_LABEL) != NULL) {
|
||||||
|
return NULL; // duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
BasSymbolT *sym = (BasSymbolT *)calloc(1, sizeof(BasSymbolT));
|
||||||
|
|
||||||
|
if (!sym) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
strcpy(sym->name, name);
|
||||||
|
sym->nameHash = nameHashCI(name);
|
||||||
|
sym->kind = kind;
|
||||||
|
sym->scope = scope;
|
||||||
|
sym->dataType = dataType;
|
||||||
|
sym->isDefined = true;
|
||||||
|
|
||||||
|
// 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_IDENT - 1);
|
||||||
|
sym->formName[BAS_MAX_IDENT - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
BasSymbolT **bucket = bucketFor(tab, sym->nameHash);
|
||||||
|
|
||||||
|
sym->hashNext = *bucket;
|
||||||
|
*bucket = sym;
|
||||||
|
|
||||||
|
arrput(tab->symbols, sym);
|
||||||
|
tab->count = (int32_t)arrlen(tab->symbols);
|
||||||
|
return sym;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Remove sym from its hash chain (before it is freed).
|
||||||
|
static void unlinkSymbol(BasSymTabT *tab, BasSymbolT *sym) {
|
||||||
|
BasSymbolT **link = bucketFor(tab, sym->nameHash);
|
||||||
|
|
||||||
|
while (*link != NULL && *link != sym) {
|
||||||
|
link = &(*link)->hashNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*link == sym) {
|
||||||
|
*link = sym->hashNext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,10 @@ typedef enum {
|
||||||
#define BAS_FNV1A_OFFSET 0x811C9DC5u
|
#define BAS_FNV1A_OFFSET 0x811C9DC5u
|
||||||
#define BAS_FNV1A_PRIME 0x01000193u
|
#define BAS_FNV1A_PRIME 0x01000193u
|
||||||
|
|
||||||
|
// Hash buckets of the symbol table (power of two; the name hash is masked
|
||||||
|
// with BAS_SYMTAB_BUCKETS - 1).
|
||||||
|
#define BAS_SYMTAB_BUCKETS 256
|
||||||
|
|
||||||
// UDT field definition
|
// UDT field definition
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[BAS_MAX_IDENT];
|
char name[BAS_MAX_IDENT];
|
||||||
|
|
@ -76,25 +80,27 @@ typedef struct {
|
||||||
int32_t fixedLen; // for STRING * n fields: fixed length (0 = variable-length)
|
int32_t fixedLen; // for STRING * n fields: fixed length (0 = variable-length)
|
||||||
} BasFieldDefT;
|
} BasFieldDefT;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct BasSymbolS {
|
||||||
// Fields are ordered by alignment (4-byte, then 2-byte, then 1-byte) to
|
// Fields are ordered by alignment (4-byte, then 2-byte, then 1-byte) to
|
||||||
// minimize struct padding on the 32-bit target.
|
// minimize struct padding on the 32-bit target.
|
||||||
uint32_t nameHash; // FNV-1a over uppercase(name); fast-reject during lookup
|
uint32_t nameHash; // FNV-1a over uppercase(name); fast-reject during lookup
|
||||||
|
struct BasSymbolS *hashNext; // next (older) symbol in the same hash bucket
|
||||||
BasSymKindE kind;
|
BasSymKindE kind;
|
||||||
BasScopeE scope;
|
BasScopeE scope;
|
||||||
int32_t index; // slot index (local or global)
|
int32_t index; // slot index (local or global)
|
||||||
int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL
|
int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL
|
||||||
int32_t localCount; // number of local variables (for SUB/FUNCTION, set on leave)
|
int32_t localCount; // SUB/FUNCTION: local variable count (set on leave); LABEL: SELECT depth at the definition
|
||||||
int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol
|
int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol
|
||||||
int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length)
|
int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length)
|
||||||
|
int32_t refLine; // LABEL/SUB/FUNCTION: source line of the first reference made before the definition
|
||||||
|
|
||||||
// For SUB/FUNCTION: parameter info
|
// For SUB/FUNCTION: parameter info
|
||||||
int32_t paramCount;
|
int32_t paramCount;
|
||||||
int32_t requiredParams; // count of non-optional params
|
int32_t requiredParams; // count of non-optional params
|
||||||
|
|
||||||
// Forward-reference backpatch list (code addresses to patch when defined)
|
// Forward-reference backpatch list (code addresses to patch when
|
||||||
|
// defined); emptied once the patches are applied.
|
||||||
int32_t *patchAddrs; // stb_ds dynamic array
|
int32_t *patchAddrs; // stb_ds dynamic array
|
||||||
int32_t patchCount;
|
|
||||||
|
|
||||||
// For CONST: the constant value
|
// For CONST: the constant value
|
||||||
union {
|
union {
|
||||||
|
|
@ -119,6 +125,8 @@ typedef struct {
|
||||||
bool hasSignature; // true once paramCount/paramTypes describe the real declaration (false for a call-site stub)
|
bool hasSignature; // true once paramCount/paramTypes describe the real declaration (false for a call-site stub)
|
||||||
bool isImplicit; // variable created by first use (no DIM); STRING locals need a prologue init
|
bool isImplicit; // variable created by first use (no DIM); STRING locals need a prologue init
|
||||||
bool isTyped; // variable declared with an explicit numeric type (suffix, AS clause or DEFxxx); stores convert to it
|
bool isTyped; // variable declared with an explicit numeric type (suffix, AS clause or DEFxxx); stores convert to it
|
||||||
|
bool isAlias; // procedure-local name for a module slot (STATIC backing variable or SHARED statement): lives in aliasScope slot 'index'
|
||||||
|
uint8_t aliasScope; // BasScopeE of the slot an alias addresses (SCOPE_GLOBAL or SCOPE_FORM)
|
||||||
|
|
||||||
char name[BAS_MAX_IDENT];
|
char name[BAS_MAX_IDENT];
|
||||||
char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars
|
char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars
|
||||||
|
|
@ -140,10 +148,12 @@ typedef struct {
|
||||||
// would be reallocated on growth, invalidating any held pointer.
|
// would be reallocated on growth, invalidating any held pointer.
|
||||||
// Indirection via a stable heap pointer per symbol avoids that.
|
// Indirection via a stable heap pointer per symbol avoids that.
|
||||||
BasSymbolT **symbols;
|
BasSymbolT **symbols;
|
||||||
|
BasSymbolT *buckets[BAS_SYMTAB_BUCKETS]; // hash chains, newest symbol first
|
||||||
int32_t count;
|
int32_t count;
|
||||||
int32_t nextGlobalIdx; // next global variable slot
|
int32_t nextGlobalIdx; // next global variable slot
|
||||||
int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION)
|
int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION)
|
||||||
bool inLocalScope; // true when inside SUB/FUNCTION
|
bool inLocalScope; // true when inside SUB/FUNCTION
|
||||||
|
bool hideUnshared; // inside a SUB/FUNCTION: module variables are visible only when SHARED (off for a DEF FN body)
|
||||||
bool inFormScope; // true inside BEGINFORM...ENDFORM
|
bool inFormScope; // true inside BEGINFORM...ENDFORM
|
||||||
char formScopeName[BAS_MAX_IDENT]; // current form name
|
char formScopeName[BAS_MAX_IDENT]; // current form name
|
||||||
int32_t nextFormVarIdx; // next form-level variable slot
|
int32_t nextFormVarIdx; // next form-level variable slot
|
||||||
|
|
@ -159,20 +169,50 @@ void basSymTabInit(BasSymTabT *tab);
|
||||||
// Free all symbols and the symbol array (called at parser teardown).
|
// Free all symbols and the symbol array (called at parser teardown).
|
||||||
void basSymTabFree(BasSymTabT *tab);
|
void basSymTabFree(BasSymTabT *tab);
|
||||||
|
|
||||||
// Add a symbol. Returns the symbol pointer, or NULL if the name already
|
// Add a symbol in the current scope (local > form > global, see
|
||||||
// exists in the current scope, is too long for BasSymbolT.name, or memory
|
// basSymTabCurrentScope). Returns the symbol pointer, or NULL if the name
|
||||||
|
// already exists in that scope, is too long for BasSymbolT.name, or memory
|
||||||
// allocation fails. Names are never truncated: a truncated name would
|
// allocation fails. Names are never truncated: a truncated name would
|
||||||
// hash and compare differently from the source spelling.
|
// hash and compare differently from the source spelling. Labels live in a
|
||||||
|
// namespace of their own: a label never collides with a variable, CONST,
|
||||||
|
// SUB or TYPE of the same name.
|
||||||
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
||||||
|
|
||||||
// Look up a symbol by name. Searches local scope first, then global.
|
// Add a symbol in the global scope regardless of the current local/form
|
||||||
// Case-insensitive.
|
// scope (SUB/FUNCTION/TYPE names, STATIC backing variables). The
|
||||||
|
// duplicate check is made against the global scope.
|
||||||
|
BasSymbolT *basSymTabAddGlobal(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
|
||||||
|
|
||||||
|
// Add a variable at module level from any scope: a form variable
|
||||||
|
// (SCOPE_FORM) inside BEGINFORM...ENDFORM, otherwise a global, with its
|
||||||
|
// slot allocated. Used by SHARED to create a module variable from inside
|
||||||
|
// a SUB/FUNCTION. NULL as for basSymTabAdd.
|
||||||
|
BasSymbolT *basSymTabAddModuleVar(BasSymTabT *tab, const char *name, uint8_t dataType);
|
||||||
|
|
||||||
|
// Scope a symbol of the given kind would get from basSymTabAdd right now.
|
||||||
|
BasScopeE basSymTabCurrentScope(const BasSymTabT *tab, BasSymKindE kind);
|
||||||
|
|
||||||
|
// Look up a non-label symbol by name. Searches local scope first, then
|
||||||
|
// form and global. Case-insensitive. Inside a SUB/FUNCTION a module
|
||||||
|
// (global or form) variable is found only when it was declared SHARED;
|
||||||
|
// CONSTs, procedures and TYPEs are always visible.
|
||||||
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
|
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
|
||||||
|
|
||||||
// Look up a symbol in the global scope only.
|
// Like basSymTabFind, but also finds a module variable that the current
|
||||||
|
// procedure cannot see (not SHARED). For diagnostics and the SHARED
|
||||||
|
// statement.
|
||||||
|
BasSymbolT *basSymTabFindModuleVar(BasSymTabT *tab, const char *name);
|
||||||
|
|
||||||
|
// Look up a non-label symbol in the global scope only.
|
||||||
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
|
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
|
||||||
|
|
||||||
// Enter local scope (called at SUB/FUNCTION start).
|
// Look up a label of the current procedure: a local label inside a
|
||||||
|
// SUB/FUNCTION, a global one at module level. Labels are never visible
|
||||||
|
// across that boundary.
|
||||||
|
BasSymbolT *basSymTabFindLabel(BasSymTabT *tab, const char *name);
|
||||||
|
|
||||||
|
// Enter local scope (called at SUB/FUNCTION start). Module variables
|
||||||
|
// that are not SHARED become invisible until basSymTabLeaveLocal.
|
||||||
void basSymTabEnterLocal(BasSymTabT *tab);
|
void basSymTabEnterLocal(BasSymTabT *tab);
|
||||||
|
|
||||||
// Leave local scope (called at END SUB/FUNCTION). Removes local symbols.
|
// Leave local scope (called at END SUB/FUNCTION). Removes local symbols.
|
||||||
|
|
|
||||||
|
|
@ -45,19 +45,22 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
|
||||||
Top Integer R/W Y position in pixels relative to the parent container.
|
Top Integer R/W Y position in pixels relative to the parent container.
|
||||||
Width Integer R/W Current width in pixels. Setting this changes the minimum width constraint.
|
Width Integer R/W Current width in pixels. Setting this changes the minimum width constraint.
|
||||||
Height Integer R/W Current height in pixels. Setting this changes the minimum height constraint.
|
Height Integer R/W Current height in pixels. Setting this changes the minimum height constraint.
|
||||||
MinWidth Integer R/W Minimum width for layout. Alias for Width in the setter.
|
MinWidth Integer R/W Minimum width for layout. Setting it works like setting Width; reading it returns the minimum that was set.
|
||||||
MinHeight Integer R/W Minimum height for layout. Alias for Height in the setter.
|
MinHeight Integer R/W Minimum height for layout. Setting it works like setting Height; reading it returns the minimum that was set.
|
||||||
MaxWidth Integer R/W Maximum width cap (0 = no limit, stretch to fill).
|
MaxWidth Integer R/W Maximum width cap (0 = no limit, stretch to fill).
|
||||||
MaxHeight Integer R/W Maximum height cap (0 = no limit, stretch to fill).
|
MaxHeight Integer R/W Maximum height cap (0 = no limit, stretch to fill).
|
||||||
Weight Integer R/W Layout weight. 0 = fixed size, >0 = share extra space proportionally.
|
Weight Integer R/W Layout weight. 0 = fixed size, >0 = share extra space proportionally.
|
||||||
Visible Boolean R/W Whether the control is visible.
|
Visible Boolean R/W Whether the control is visible.
|
||||||
Enabled Boolean R/W Whether the control accepts user input.
|
Enabled Boolean R/W Whether the control accepts user input. Disabling a container (Frame, VBox, HBox...) also blocks mouse and keyboard input to every control inside it.
|
||||||
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||||
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||||
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
|
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
|
||||||
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
|
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
|
||||||
|
HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767.
|
||||||
|
|
||||||
.h2 Common Events
|
.h2 Common Events
|
||||||
|
|
||||||
These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.
|
These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.
|
||||||
|
|
@ -230,14 +233,30 @@ End
|
||||||
|
|
||||||
.h2 Menu Item Properties
|
.h2 Menu Item Properties
|
||||||
|
|
||||||
|
In the .frm file a menu item takes these keys:
|
||||||
|
|
||||||
.table
|
.table
|
||||||
Property Type Description
|
Key Type Description
|
||||||
-------- ------- -------------------------------------------
|
---------- ------- -------------------------------------------
|
||||||
Caption String The text displayed. Use & for accelerator key. Set to "-" for a separator.
|
Caption String The text displayed. Use & for accelerator key. Set to "-" for a separator.
|
||||||
Checked Boolean Whether the menu item shows a checkmark.
|
Checked Boolean Whether the menu item shows a checkmark.
|
||||||
Enabled Boolean Whether the menu item is enabled (default True).
|
RadioCheck Boolean Show the check as a radio dot instead of a checkmark.
|
||||||
|
Enabled Boolean Whether the menu item is enabled (default True).
|
||||||
|
Visible Boolean On a top-level menu only: False makes it a popup menu (for ContextMenu) instead of a menu bar entry.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
At run time code can use only these properties of a menu item:
|
||||||
|
|
||||||
|
.table
|
||||||
|
Property Type R/W Description
|
||||||
|
-------- ------- --- -------------------------------------------
|
||||||
|
Name String R The menu item's name.
|
||||||
|
Checked Boolean R/W Whether the menu item shows a checkmark.
|
||||||
|
Enabled Boolean R/W Whether the menu item is enabled.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Caption, RadioCheck and Visible are design-time only: using them on a menu item in code is a compile error in the IDE and a runtime error (Unknown menu property) otherwise.
|
||||||
|
|
||||||
.h2 Nesting
|
.h2 Nesting
|
||||||
|
|
||||||
Menu items are nested by placing Begin Menu blocks inside other Begin Menu blocks:
|
Menu items are nested by placing Begin Menu blocks inside other Begin Menu blocks:
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,17 @@ The Form is the top-level container representing a DVX window. It is declared in
|
||||||
---------- ------- -------------- -------------------------------------------
|
---------- ------- -------------- -------------------------------------------
|
||||||
Name String "Form1" The form's name, used for event dispatch and Load statement.
|
Name String "Form1" The form's name, used for event dispatch and Load statement.
|
||||||
Caption String (same as Name) Window title bar text.
|
Caption String (same as Name) Window title bar text.
|
||||||
Width Integer 400 Window width in pixels. Setting this disables AutoSize.
|
Width Integer 400 Window width in pixels. In the .frm file it is ignored when AutoSize is True.
|
||||||
Height Integer 300 Window height in pixels. Setting this disables AutoSize.
|
Height Integer 300 Window height in pixels. In the .frm file it is ignored when AutoSize is True.
|
||||||
Left Integer 0 Initial X position. Used when Centered is False.
|
Left Integer 0 Initial X position. Used when Centered is False.
|
||||||
Top Integer 0 Initial Y position. Used when Centered is False.
|
Top Integer 0 Initial Y position. Used when Centered is False.
|
||||||
Layout String "VBox" Content box layout: "VBox" (vertical) or "HBox" (horizontal).
|
Layout String "VBox" Content box layout: "VBox" (vertical) or "HBox" (horizontal).
|
||||||
AutoSize Boolean False When True, the window shrink-wraps to fit its content.
|
AutoSize Boolean False When True, the window shrink-wraps to fit its content.
|
||||||
Resizable Boolean True Whether the user can resize the window at runtime.
|
Resizable Boolean True Whether the user can resize the window at runtime.
|
||||||
Centered Boolean True When True, the window is centered on screen. When False, Left/Top are used.
|
Centered Boolean True When True, the window is centered on screen. When False, Left/Top are used.
|
||||||
|
Visible Boolean False True shows the form and False hides it, exactly like Show and Hide (the first show fires Activate).
|
||||||
|
ContextMenu String "" Name of a popup menu shown when the form is right-clicked.
|
||||||
|
HelpTopic String "" Help topic opened by F1 while the form is active and the focused control has no HelpTopic of its own. Empty opens the project's help file at its default page.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.h2 Form Events
|
.h2 Form Events
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,27 @@
|
||||||
#include "formrt.h"
|
#include "formrt.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Shared asm body: save ESP in EDI, push nativeArgs[ECX-1..0] (right to
|
||||||
|
// left, one pushl per pass of the 1: loop), call the target in EBX, then
|
||||||
|
// restore ESP from EDI. Both return paths use it.
|
||||||
|
#define NATIVE_CALL_ASM \
|
||||||
|
"movl %%esp, %%edi\n\t" \
|
||||||
|
"1:\n\t" \
|
||||||
|
"testl %%ecx, %%ecx\n\t" \
|
||||||
|
"jle 2f\n\t" \
|
||||||
|
"decl %%ecx\n\t" \
|
||||||
|
"pushl (%%esi, %%ecx, 4)\n\t" \
|
||||||
|
"jmp 1b\n\t" \
|
||||||
|
"2:\n\t" \
|
||||||
|
"call *%[fn]\n\t" \
|
||||||
|
"movl %%edi, %%esp\n\t"
|
||||||
|
|
||||||
|
|
||||||
// Push the already-marshalled cdecl argument words and call a native
|
// Push the already-marshalled cdecl argument words and call a native
|
||||||
// function via funcPtr. Returns the EAX result word; when fpReturn is
|
// function via funcPtr. Returns the EAX result word; when fpReturn is
|
||||||
// true the x87 ST(0) result is also stored through fpResult via fstpl.
|
// true the x87 ST(0) result is handed back through a "=t" output and
|
||||||
|
// stored to *fpResult in C, so no store in the asm ever goes through a
|
||||||
|
// register the call clobbered.
|
||||||
//
|
//
|
||||||
// The ESP save, the entire push loop, the call, and the ESP restore
|
// The ESP save, the entire push loop, the call, and the ESP restore
|
||||||
// live inside ONE __asm__ block per return path: a saved-ESP value held
|
// live inside ONE __asm__ block per return path: a saved-ESP value held
|
||||||
|
|
@ -45,46 +63,29 @@
|
||||||
// EDX, ESI and EDI all spoken for there is nothing left to allocate;
|
// EDX, ESI and EDI all spoken for there is nothing left to allocate;
|
||||||
// the rest are clobber-listed so GCC reloads anything it kept there.
|
// the rest are clobber-listed so GCC reloads anything it kept there.
|
||||||
// "memory" stops GCC caching the string args the callee may
|
// "memory" stops GCC caching the string args the callee may
|
||||||
// dereference. fstpl is gated strictly to fpReturn so
|
// dereference. The "=t" output exists only on the fpReturn path, so
|
||||||
// the integer path never pops the x87 stack.
|
// the integer path never pops the x87 stack.
|
||||||
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
|
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
|
||||||
uint32_t rawResult = 0;
|
uint32_t rawResult = 0;
|
||||||
int32_t cnt = nativeCount;
|
int32_t cnt = nativeCount;
|
||||||
|
double fpValue = 0.0;
|
||||||
|
|
||||||
if (fpReturn) {
|
if (fpReturn) {
|
||||||
__asm__ __volatile__(
|
__asm__ __volatile__(
|
||||||
"movl %%esp, %%edi\n\t" // save ESP
|
NATIVE_CALL_ASM
|
||||||
"1:\n\t"
|
: "=a"(rawResult), "=t"(fpValue), [cnt] "+c"(cnt)
|
||||||
"testl %%ecx, %%ecx\n\t"
|
|
||||||
"jle 2f\n\t"
|
|
||||||
"decl %%ecx\n\t"
|
|
||||||
"pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX]
|
|
||||||
"jmp 1b\n\t"
|
|
||||||
"2:\n\t"
|
|
||||||
"call *%[fn]\n\t"
|
|
||||||
"fstpl %[fpres]\n\t" // pop x87 ST(0) -> *fpResult
|
|
||||||
"movl %%edi, %%esp\n\t" // restore ESP
|
|
||||||
: "=a"(rawResult), [fpres] "=m"(*fpResult), [cnt] "+c"(cnt)
|
|
||||||
: [fn] "b"(funcPtr), "S"(nativeArgs)
|
: [fn] "b"(funcPtr), "S"(nativeArgs)
|
||||||
: "edi", "edx", "cc", "memory"
|
: "edi", "edx", "cc", "memory"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
__asm__ __volatile__(
|
__asm__ __volatile__(
|
||||||
"movl %%esp, %%edi\n\t" // save ESP
|
NATIVE_CALL_ASM
|
||||||
"1:\n\t"
|
|
||||||
"testl %%ecx, %%ecx\n\t"
|
|
||||||
"jle 2f\n\t"
|
|
||||||
"decl %%ecx\n\t"
|
|
||||||
"pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX]
|
|
||||||
"jmp 1b\n\t"
|
|
||||||
"2:\n\t"
|
|
||||||
"call *%[fn]\n\t"
|
|
||||||
"movl %%edi, %%esp\n\t" // restore ESP
|
|
||||||
: "=a"(rawResult), [cnt] "+c"(cnt)
|
: "=a"(rawResult), [cnt] "+c"(cnt)
|
||||||
: [fn] "b"(funcPtr), "S"(nativeArgs)
|
: [fn] "b"(funcPtr), "S"(nativeArgs)
|
||||||
: "edi", "edx", "cc", "memory"
|
: "edi", "edx", "cc", "memory"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*fpResult = fpValue;
|
||||||
return rawResult;
|
return rawResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,43 @@
|
||||||
//
|
//
|
||||||
// The i386 cdecl trampoline in basNativeDos.c cannot run on a 64-bit
|
// The i386 cdecl trampoline in basNativeDos.c cannot run on a 64-bit
|
||||||
// host and the test harness has no libffi, so extern calls are refused
|
// host and the test harness has no libffi, so extern calls are refused
|
||||||
// here: nothing is called and the result reads as zero. Selected by the
|
// here: nothing is called. The marshalled argument words are recorded
|
||||||
// Makefile object list of the host basrt archive.
|
// and the result is whatever basNativeHostSetResult last set (zero by
|
||||||
|
// default), so the test suite can check basExternCall's marshalling
|
||||||
|
// and return narrowing. Selected by the Makefile object list of the
|
||||||
|
// host basrt archive.
|
||||||
|
|
||||||
#include "formrt.h"
|
#include "formrt.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static uint32_t sLastArgs[BAS_EXTERN_MAX_NATIVE_WORDS];
|
||||||
|
static int32_t sLastCount = 0;
|
||||||
|
static uint32_t sRawResult = 0;
|
||||||
|
static double sFpResult = 0.0;
|
||||||
|
|
||||||
|
|
||||||
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
|
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
|
||||||
(void)funcPtr;
|
(void)funcPtr;
|
||||||
(void)nativeArgs;
|
|
||||||
(void)nativeCount;
|
|
||||||
(void)fpReturn;
|
(void)fpReturn;
|
||||||
|
|
||||||
*fpResult = 0.0;
|
sLastCount = DVX_MIN(nativeCount, BAS_EXTERN_MAX_NATIVE_WORDS);
|
||||||
return 0;
|
memcpy(sLastArgs, nativeArgs, (size_t)sLastCount * sizeof(sLastArgs[0]));
|
||||||
|
|
||||||
|
*fpResult = sFpResult;
|
||||||
|
return sRawResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int32_t basNativeHostLastArgs(uint32_t *out, int32_t maxCount) {
|
||||||
|
int32_t n = DVX_MIN(sLastCount, maxCount);
|
||||||
|
|
||||||
|
memcpy(out, sLastArgs, (size_t)n * sizeof(sLastArgs[0]));
|
||||||
|
return sLastCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void basNativeHostSetResult(uint32_t rawResult, double fpResult) {
|
||||||
|
sRawResult = rawResult;
|
||||||
|
sFpResult = fpResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -56,6 +56,59 @@ typedef struct BasControlT BasControlT;
|
||||||
#define BAS_DEFAULT_FORM_W 400
|
#define BAS_DEFAULT_FORM_W 400
|
||||||
#define BAS_DEFAULT_FORM_H 300
|
#define BAS_DEFAULT_FORM_H 300
|
||||||
|
|
||||||
|
// Layout of a form or container that names none ("VBox" / "HBox" /
|
||||||
|
// "WrapBox" are the box widgets' BASIC names).
|
||||||
|
#define BAS_LAYOUT_VBOX "VBox"
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Property names
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// The one spelling of every property name shared by the form runtime,
|
||||||
|
// the .frm loader and writer, the IDE designer, the property grid and
|
||||||
|
// the compile-time validator. The descriptor tables behind
|
||||||
|
// basFormRtFormProps / basFormRtCommonProps / basFormRtMenuProps are
|
||||||
|
// built from these; .frm-only keys (Index, TabIndex, RadioCheck, ...)
|
||||||
|
// and the data-binding properties the runtime reads from widget
|
||||||
|
// interfaces are listed here too.
|
||||||
|
|
||||||
|
#define BAS_PROP_AUTOSIZE "AutoSize"
|
||||||
|
#define BAS_PROP_BACKCOLOR "BackColor"
|
||||||
|
#define BAS_PROP_CAPTION "Caption"
|
||||||
|
#define BAS_PROP_CENTERED "Centered"
|
||||||
|
#define BAS_PROP_CHECKED "Checked"
|
||||||
|
#define BAS_PROP_CONTEXTMENU "ContextMenu"
|
||||||
|
#define BAS_PROP_DATAFIELD "DataField"
|
||||||
|
#define BAS_PROP_DATASOURCE "DataSource"
|
||||||
|
#define BAS_PROP_DETAILFIELD "DetailField"
|
||||||
|
#define BAS_PROP_ENABLED "Enabled"
|
||||||
|
#define BAS_PROP_FORECOLOR "ForeColor"
|
||||||
|
#define BAS_PROP_HEIGHT "Height"
|
||||||
|
#define BAS_PROP_HELPTOPIC "HelpTopic"
|
||||||
|
#define BAS_PROP_INDEX "Index"
|
||||||
|
#define BAS_PROP_KEYCOLUMN "KeyColumn"
|
||||||
|
#define BAS_PROP_LAYOUT "Layout"
|
||||||
|
#define BAS_PROP_LEFT "Left"
|
||||||
|
#define BAS_PROP_LISTCOUNT "ListCount"
|
||||||
|
#define BAS_PROP_MASTERFIELD "MasterField"
|
||||||
|
#define BAS_PROP_MASTERSOURCE "MasterSource"
|
||||||
|
#define BAS_PROP_MAXHEIGHT "MaxHeight"
|
||||||
|
#define BAS_PROP_MAXWIDTH "MaxWidth"
|
||||||
|
#define BAS_PROP_MINHEIGHT "MinHeight"
|
||||||
|
#define BAS_PROP_MINWIDTH "MinWidth"
|
||||||
|
#define BAS_PROP_NAME "Name"
|
||||||
|
#define BAS_PROP_RADIOCHECK "RadioCheck"
|
||||||
|
#define BAS_PROP_READONLY "ReadOnly"
|
||||||
|
#define BAS_PROP_RECORDSOURCE "RecordSource"
|
||||||
|
#define BAS_PROP_RESIZABLE "Resizable"
|
||||||
|
#define BAS_PROP_TABINDEX "TabIndex"
|
||||||
|
#define BAS_PROP_TEXT "Text"
|
||||||
|
#define BAS_PROP_TOOLTIPTEXT "ToolTipText"
|
||||||
|
#define BAS_PROP_TOP "Top"
|
||||||
|
#define BAS_PROP_VISIBLE "Visible"
|
||||||
|
#define BAS_PROP_WEIGHT "Weight"
|
||||||
|
#define BAS_PROP_WIDTH "Width"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Property descriptors
|
// Property descriptors
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -81,9 +134,13 @@ const BasPropDescT *basFormRtFormProps(int32_t *count);
|
||||||
// is consulted, in table order.
|
// is consulted, in table order.
|
||||||
const BasPropDescT *basFormRtCommonProps(int32_t *count);
|
const BasPropDescT *basFormRtCommonProps(int32_t *count);
|
||||||
|
|
||||||
|
// Properties of a menu item (MenuName.Prop), in table order.
|
||||||
|
const BasPropDescT *basFormRtMenuProps(int32_t *count);
|
||||||
|
|
||||||
// Case-insensitive lookups; NULL when the name is not in the table.
|
// Case-insensitive lookups; NULL when the name is not in the table.
|
||||||
const BasPropDescT *basFormRtFindCommonProp(const char *name);
|
const BasPropDescT *basFormRtFindCommonProp(const char *name);
|
||||||
const BasPropDescT *basFormRtFindFormProp(const char *name);
|
const BasPropDescT *basFormRtFindFormProp(const char *name);
|
||||||
|
const BasPropDescT *basFormRtFindMenuProp(const char *name);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Common methods
|
// Common methods
|
||||||
|
|
@ -227,6 +284,16 @@ typedef struct BasFormT {
|
||||||
// Form runtime context
|
// Form runtime context
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Event-handler lookup cache: upper-cased proc name -> module procs[]
|
||||||
|
// index as basModuleFindProc resolved it, or BAS_PROC_NONE for a name with
|
||||||
|
// no proc (stb_ds string hashmap).
|
||||||
|
#define BAS_PROC_NONE (-1)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *key;
|
||||||
|
int32_t value;
|
||||||
|
} BasProcIndexT;
|
||||||
|
|
||||||
// Cached .frm source for reload after unload
|
// Cached .frm source for reload after unload
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char formName[BAS_MAX_IDENT];
|
char formName[BAS_MAX_IDENT];
|
||||||
|
|
@ -274,6 +341,16 @@ typedef struct {
|
||||||
// was missing. This is the last requested name; if the lookup
|
// was missing. This is the last requested name; if the lookup
|
||||||
// succeeded this value is still the name of that control.
|
// succeeded this value is still the name of that control.
|
||||||
char lastLookupName[BAS_MAX_IDENT];
|
char lastLookupName[BAS_MAX_IDENT];
|
||||||
|
// Memo of basModuleFindProc answers for event dispatch, so each fired
|
||||||
|
// event after the first is one hash probe instead of a scan.
|
||||||
|
BasProcIndexT *procIndex;
|
||||||
|
bool procIndexBuilt;
|
||||||
|
// Last object reference resolveCtrlRef validated (one entry, so a
|
||||||
|
// loop touching one control skips the scan). Reset whenever a form,
|
||||||
|
// control or menu proxy leaves the lookup sets, so a stale pointer
|
||||||
|
// is never trusted.
|
||||||
|
void *refCacheKey;
|
||||||
|
BasControlT *refCacheHit;
|
||||||
} BasFormRtT;
|
} BasFormRtT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -302,13 +379,22 @@ void basFormRtDestroy(BasFormRtT *rt);
|
||||||
// Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook.
|
// Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook.
|
||||||
void basFormRtSerialShutdown(void);
|
void basFormRtSerialShutdown(void);
|
||||||
|
|
||||||
|
#ifdef DVX_HOST_TEST
|
||||||
// Test-only: clear every file-scope static so a host test process can run
|
// Test-only: clear every file-scope static so a host test process can run
|
||||||
// many create/destroy cycles. Not used by the DOS build.
|
// many create/destroy cycles. Host test builds only.
|
||||||
void basFormRtTestReset(void);
|
void basFormRtTestReset(void);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Wire up the VM's UI callbacks to this form runtime.
|
// Wire up the VM's UI callbacks to this form runtime.
|
||||||
void basFormRtBindVm(BasFormRtT *rt);
|
void basFormRtBindVm(BasFormRtT *rt);
|
||||||
|
|
||||||
|
// F1 help topic for the focused window: the HelpTopic of the focused
|
||||||
|
// control, else of its form, else "" (the help file's default topic).
|
||||||
|
// Returns false, leaving topic untouched, when the focused window is not
|
||||||
|
// one of rt's forms. The IDE (a program run from the IDE) and the
|
||||||
|
// compiled-program stub both use it, so F1 follows the same rules.
|
||||||
|
bool basFormRtHelpTopic(const BasFormRtT *rt, char *topic, int32_t topicSize);
|
||||||
|
|
||||||
// Report a non-recoverable runtime error: log, halt the VM, mark the
|
// Report a non-recoverable runtime error: log, halt the VM, mark the
|
||||||
// runtime terminated and (unless suppressErrorDialog) show the modal
|
// runtime terminated and (unless suppressErrorDialog) show the modal
|
||||||
// error box. Follow-on errors while terminated are swallowed.
|
// error box. Follow-on errors while terminated are swallowed.
|
||||||
|
|
@ -336,7 +422,19 @@ void *basExternResolve(void *ctx, const char *libName, const char *funcName);
|
||||||
// the i386 asm; basNativeHost.c is the 64-bit host stub that calls
|
// the i386 asm; basNativeHost.c is the 64-bit host stub that calls
|
||||||
// nothing. Selected by the Makefile object list.
|
// nothing. Selected by the Makefile object list.
|
||||||
BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType);
|
BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType);
|
||||||
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult);
|
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult);
|
||||||
|
|
||||||
|
// Widest cdecl argument (DOUBLE) in 32-bit stack words, and the most
|
||||||
|
// words one extern call can marshal.
|
||||||
|
#define BAS_EXTERN_MAX_WORDS_PER_ARG 2
|
||||||
|
#define BAS_EXTERN_MAX_NATIVE_WORDS (BAS_VM_MAX_CALL_ARGS * BAS_EXTERN_MAX_WORDS_PER_ARG)
|
||||||
|
|
||||||
|
// Host build only (basNativeHost.c): inspect the words the last
|
||||||
|
// basNativeCall was handed, and set the result the next calls return.
|
||||||
|
#ifdef DVX_HOST_TEST
|
||||||
|
int32_t basNativeHostLastArgs(uint32_t *out, int32_t maxCount);
|
||||||
|
void basNativeHostSetResult(uint32_t rawResult, double fpResult);
|
||||||
|
#endif
|
||||||
|
|
||||||
// ---- Form caching ----
|
// ---- Form caching ----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
BlkTypeE blockStack[FRM_MAX_NESTING];
|
BlkTypeE blockStack[FRM_MAX_NESTING];
|
||||||
int32_t blockDepth = 0;
|
int32_t blockDepth = 0;
|
||||||
int32_t menuNestDepth = 0;
|
int32_t menuNestDepth = 0;
|
||||||
|
int32_t lineNum = 0;
|
||||||
bool inForm = false;
|
bool inForm = false;
|
||||||
|
|
||||||
const char *pos = source;
|
const char *pos = source;
|
||||||
|
|
@ -72,6 +73,8 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
|
|
||||||
int32_t lineLen = (int32_t)(pos - lineStart);
|
int32_t lineLen = (int32_t)(pos - lineStart);
|
||||||
|
|
||||||
|
lineNum++;
|
||||||
|
|
||||||
if (pos < end && *pos == '\r') {
|
if (pos < end && *pos == '\r') {
|
||||||
pos++;
|
pos++;
|
||||||
}
|
}
|
||||||
|
|
@ -83,12 +86,16 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
char line[FRM_MAX_LINE_LEN];
|
char line[FRM_MAX_LINE_LEN];
|
||||||
|
|
||||||
if (lineLen >= FRM_MAX_LINE_LEN) {
|
if (lineLen >= FRM_MAX_LINE_LEN) {
|
||||||
|
dvxLog("frm: line %d is longer than %d characters and was truncated", (int)lineNum, (int)(FRM_MAX_LINE_LEN - 1));
|
||||||
lineLen = FRM_MAX_LINE_LEN - 1;
|
lineLen = FRM_MAX_LINE_LEN - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(line, lineStart, lineLen);
|
memcpy(line, lineStart, lineLen);
|
||||||
line[lineLen] = '\0';
|
line[lineLen] = '\0';
|
||||||
|
|
||||||
|
// Right-trim so keyword tests ("End") see the bare keyword.
|
||||||
|
dvxTrimRight(line);
|
||||||
|
|
||||||
const char *trimmed = dvxSkipWs(line);
|
const char *trimmed = dvxSkipWs(line);
|
||||||
|
|
||||||
if (*trimmed == '\0' || *trimmed == '\'') {
|
if (*trimmed == '\0' || *trimmed == '\'') {
|
||||||
|
|
@ -111,8 +118,8 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Begin TypeName CtrlName"
|
// "Begin TypeName CtrlName"
|
||||||
if (strncasecmp(trimmed, "Begin ", sizeof("Begin ") - 1) == 0) {
|
if (strncasecmp(trimmed, FRM_KW_BEGIN, FRM_KW_BEGIN_LEN) == 0 && (trimmed[FRM_KW_BEGIN_LEN] == ' ' || trimmed[FRM_KW_BEGIN_LEN] == '\t')) {
|
||||||
const char *rest = trimmed + sizeof("Begin ") - 1;
|
const char *rest = dvxSkipWs(trimmed + FRM_KW_BEGIN_LEN);
|
||||||
char typeName[FRM_MAX_TOKEN_LEN];
|
char typeName[FRM_MAX_TOKEN_LEN];
|
||||||
char ctrlName[FRM_MAX_TOKEN_LEN];
|
char ctrlName[FRM_MAX_TOKEN_LEN];
|
||||||
BlkTypeE blkType;
|
BlkTypeE blkType;
|
||||||
|
|
@ -127,7 +134,7 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
|
|
||||||
// Classify the block first so overflow is checked once and all
|
// Classify the block first so overflow is checked once and all
|
||||||
// begin side-effects stay in lockstep with the stack push.
|
// begin side-effects stay in lockstep with the stack push.
|
||||||
if (strcasecmp(typeName, "Form") == 0) {
|
if (strcasecmp(typeName, FRM_KW_FORM) == 0) {
|
||||||
// A second Begin Form inside an open form is malformed: its
|
// A second Begin Form inside an open form is malformed: its
|
||||||
// End would otherwise terminate the whole file early and
|
// End would otherwise terminate the whole file early and
|
||||||
// silently drop every control after it.
|
// silently drop every control after it.
|
||||||
|
|
@ -137,7 +144,7 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
|
|
||||||
blkType = BLK_FORM;
|
blkType = BLK_FORM;
|
||||||
doPush = true;
|
doPush = true;
|
||||||
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
|
} else if (strcasecmp(typeName, FRM_KW_MENU) == 0 && inForm) {
|
||||||
blkType = BLK_MENU;
|
blkType = BLK_MENU;
|
||||||
doPush = true;
|
doPush = true;
|
||||||
} else if (inForm) {
|
} else if (inForm) {
|
||||||
|
|
@ -185,8 +192,8 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "End"
|
// "End", optionally followed by a ' comment
|
||||||
if (strcasecmp(trimmed, "End") == 0) {
|
if (frmIsEndLine(trimmed, (int32_t)strlen(trimmed))) {
|
||||||
if (blockDepth == 0) {
|
if (blockDepth == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -282,8 +289,12 @@ void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value,
|
||||||
|
|
||||||
int32_t ki = 0;
|
int32_t ki = 0;
|
||||||
|
|
||||||
while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < keyMax - 1) {
|
while (*line && *line != '=' && *line != ' ' && *line != '\t') {
|
||||||
key[ki++] = *line++;
|
if (ki < keyMax - 1) {
|
||||||
|
key[ki++] = *line;
|
||||||
|
}
|
||||||
|
|
||||||
|
line++;
|
||||||
}
|
}
|
||||||
|
|
||||||
key[ki] = '\0';
|
key[ki] = '\0';
|
||||||
|
|
@ -303,10 +314,7 @@ void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value,
|
||||||
}
|
}
|
||||||
|
|
||||||
value[vi] = '\0';
|
value[vi] = '\0';
|
||||||
|
dvxTrimRight(value);
|
||||||
while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) {
|
|
||||||
value[--vi] = '\0';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -320,7 +328,7 @@ void frmStripQuotes(char *val) {
|
||||||
if (len >= 2 && val[len - 1] == '"') {
|
if (len >= 2 && val[len - 1] == '"') {
|
||||||
memmove(val, val + 1, len - 2);
|
memmove(val, val + 1, len - 2);
|
||||||
val[len - 2] = '\0';
|
val[len - 2] = '\0';
|
||||||
} else if (len >= 1) {
|
} else {
|
||||||
// Unterminated leading quote -- strip just the leading "
|
// Unterminated leading quote -- strip just the leading "
|
||||||
memmove(val, val + 1, len - 1);
|
memmove(val, val + 1, len - 1);
|
||||||
val[len - 1] = '\0';
|
val[len - 1] = '\0';
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
// Parser limits. Consumers size their own line/name buffers and
|
// Parser limits. Consumers size their own line/name buffers and
|
||||||
// nesting stacks from these so the two sides can never disagree.
|
// nesting stacks from these so the two sides can never disagree.
|
||||||
|
|
@ -41,6 +42,17 @@
|
||||||
#define FRM_MAX_TOKEN_LEN 64 // longest Begin-line token or property key
|
#define FRM_MAX_TOKEN_LEN 64 // longest Begin-line token or property key
|
||||||
#define FRM_MAX_NESTING 16 // deepest Begin/End block nesting
|
#define FRM_MAX_NESTING 16 // deepest Begin/End block nesting
|
||||||
|
|
||||||
|
// .frm block keywords. The parser, basRes.h's Begin Form scanners and
|
||||||
|
// the obfuscator all match these spellings; a keyword is followed by at
|
||||||
|
// least one space or tab. An End line is decided by frmIsEndLine alone.
|
||||||
|
#define FRM_KW_BEGIN "Begin"
|
||||||
|
#define FRM_KW_FORM "Form"
|
||||||
|
#define FRM_KW_END "End"
|
||||||
|
#define FRM_KW_MENU "Menu"
|
||||||
|
#define FRM_KW_BEGIN_LEN ((int32_t)sizeof(FRM_KW_BEGIN) - 1)
|
||||||
|
#define FRM_KW_FORM_LEN ((int32_t)sizeof(FRM_KW_FORM) - 1)
|
||||||
|
#define FRM_KW_END_LEN ((int32_t)sizeof(FRM_KW_END) - 1)
|
||||||
|
|
||||||
// Callbacks supplied by the consumer. Any field may be NULL.
|
// Callbacks supplied by the consumer. Any field may be NULL.
|
||||||
typedef struct FrmParserCbsT {
|
typedef struct FrmParserCbsT {
|
||||||
void *userData;
|
void *userData;
|
||||||
|
|
@ -96,4 +108,24 @@ bool frmParseBool(const char *val);
|
||||||
// Use for properties whose unset state is true (menu Enabled/Visible).
|
// Use for properties whose unset state is true (menu Enabled/Visible).
|
||||||
bool frmParseBoolDefault(const char *val, bool defaultValue);
|
bool frmParseBoolDefault(const char *val, bool defaultValue);
|
||||||
|
|
||||||
|
// True when s[0..len) (left-trimmed, no line terminator) is an End line:
|
||||||
|
// the End keyword alone, or followed by whitespace and/or a ' comment.
|
||||||
|
// The one End-line rule for the parser and every .frm scanner.
|
||||||
|
static inline bool frmIsEndLine(const char *s, int32_t len);
|
||||||
|
|
||||||
|
|
||||||
|
static inline bool frmIsEndLine(const char *s, int32_t len) {
|
||||||
|
if (len < FRM_KW_END_LEN || strncasecmp(s, FRM_KW_END, FRM_KW_END_LEN) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t i = FRM_KW_END_LEN;
|
||||||
|
|
||||||
|
while (i < len && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r')) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i == len || s[i] == '\'';
|
||||||
|
}
|
||||||
|
|
||||||
#endif // DVXBASIC_FRMPARSER_H
|
#endif // DVXBASIC_FRMPARSER_H
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.h1 Help
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -46,18 +46,31 @@
|
||||||
// Limits
|
// Limits
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Name, text, and nesting limits are the .frm limits from formrt.h and
|
// Name, text, and nesting limits are derived from the .frm limits in
|
||||||
// frmParser.h so the designer never truncates what the parser and runtime
|
// formrt.h and frmParser.h so the designer never produces a file the
|
||||||
// accept.
|
// parser and runtime reject.
|
||||||
|
//
|
||||||
|
// Block depth: the Form block is depth 1, so a control can nest at most
|
||||||
|
// FRM_MAX_NESTING - 1 blocks deep, and a level-L menu item sits L + 2
|
||||||
|
// blocks deep (Form + L + 1 menus).
|
||||||
|
//
|
||||||
|
// Text values hold anything the parser can deliver (a value is part of a
|
||||||
|
// line of at most FRM_MAX_LINE_LEN - 1 characters). Whether a value fits
|
||||||
|
// depends on the indent and key of the line it is written on, so the
|
||||||
|
// writer checks every property line and dsgnSaveFrm fails with
|
||||||
|
// DSGN_SAVE_LINE_TOO_LONG rather than write a line the parser would cut.
|
||||||
|
|
||||||
#define DSGN_MAX_NAME BAS_MAX_IDENT
|
#define DSGN_MAX_NAME BAS_MAX_IDENT
|
||||||
#define DSGN_MENU_STACK_DEPTH FRM_MAX_NESTING // max menu nesting (editor and preview bar)
|
#define DSGN_INDENT_SPACES 4 // .frm indent per block level
|
||||||
#define DSGN_MAX_TEXT FRM_MAX_LINE_LEN
|
#define DSGN_MAX_NEST_DEPTH (FRM_MAX_NESTING - 1) // deepest control block below the Form block
|
||||||
#define DSGN_MAX_PROPS 32
|
#define DSGN_MAX_MENU_LEVEL (FRM_MAX_NESTING - 2) // deepest menu level (0 = menu bar)
|
||||||
|
#define DSGN_MENU_STACK_DEPTH (DSGN_MAX_MENU_LEVEL + 1) // menu levels 0..DSGN_MAX_MENU_LEVEL
|
||||||
|
#define DSGN_MAX_PROP_INDENT (FRM_MAX_NESTING * DSGN_INDENT_SPACES) // indent of the deepest property line
|
||||||
|
#define DSGN_MAX_TEXT FRM_MAX_LINE_LEN
|
||||||
#define DSGN_HANDLE_SIZE 6
|
#define DSGN_HANDLE_SIZE 6
|
||||||
#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items
|
#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items
|
||||||
#define DSGN_MAX_NEST_DEPTH 32 // container recursion cap (save, cascade)
|
#define DSGN_NO_INDEX (-1) // control not in a control array
|
||||||
#define DSGN_VBOX_LAYOUT "VBox" // default container / form layout name
|
#define DSGN_NEW_MENU_ID 0 // DsgnMenuItemT.id of an item added in the menu editor
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Design-time property (stored as key=value strings)
|
// Design-time property (stored as key=value strings)
|
||||||
|
|
@ -72,10 +85,18 @@ typedef struct {
|
||||||
// Design-time menu item (flat array with level for tree structure)
|
// Design-time menu item (flat array with level for tree structure)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// id is a stable identity for the menu editor: on entry to
|
||||||
|
// mnuEditorDialog every item of form->menuItems gets id = its position + 1,
|
||||||
|
// items the user adds get DSGN_NEW_MENU_ID, and the id travels with the
|
||||||
|
// item through insert, delete, indent and move. After the dialog returns
|
||||||
|
// true, an item with id > 0 is the item that was at form->menuItems[id - 1]
|
||||||
|
// before the call, so callers can detect renames by id.
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char caption[DSGN_MAX_TEXT]; // "&File", "-" for separator
|
char caption[DSGN_MAX_TEXT]; // "&File", "-" for separator
|
||||||
char name[DSGN_MAX_NAME]; // "mnuFile"
|
char name[DSGN_MAX_NAME]; // "mnuFile"
|
||||||
int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu
|
int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu
|
||||||
|
int32_t id; // menu editor identity (see above)
|
||||||
bool checked;
|
bool checked;
|
||||||
bool radioCheck; // true = radio bullet instead of checkmark
|
bool radioCheck; // true = radio bullet instead of checkmark
|
||||||
bool enabled; // default true
|
bool enabled; // default true
|
||||||
|
|
@ -91,12 +112,17 @@ typedef struct {
|
||||||
// before its live widget exists and across widget rebuilds. Interface
|
// before its live widget exists and across widget rebuilds. Interface
|
||||||
// properties are written back in the canonical form for their type
|
// properties are written back in the canonical form for their type
|
||||||
// (see dsgnEmitControl); props[] is only ever the raw string store.
|
// (see dsgnEmitControl); props[] is only ever the raw string store.
|
||||||
|
//
|
||||||
|
// The parent is named by parentName plus parentIndex, so a child of one
|
||||||
|
// member of a container control array belongs to that member only (see
|
||||||
|
// dsgnFindParent).
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[DSGN_MAX_NAME];
|
char name[DSGN_MAX_NAME];
|
||||||
char typeName[DSGN_MAX_NAME];
|
char typeName[DSGN_MAX_NAME];
|
||||||
char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form)
|
char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form)
|
||||||
int32_t index; // control array index (-1 = not in array)
|
int32_t parentIndex; // parent's control array index (DSGN_NO_INDEX = not an array member)
|
||||||
|
int32_t index; // control array index (DSGN_NO_INDEX = not in array)
|
||||||
int32_t left; // design-time position hint (0 = layout-managed)
|
int32_t left; // design-time position hint (0 = layout-managed)
|
||||||
int32_t top;
|
int32_t top;
|
||||||
int32_t width;
|
int32_t width;
|
||||||
|
|
@ -107,8 +133,7 @@ typedef struct {
|
||||||
char helpTopic[DSGN_MAX_NAME]; // help topic ID for F1
|
char helpTopic[DSGN_MAX_NAME]; // help topic ID for F1
|
||||||
bool visible; // design-time visibility (default true)
|
bool visible; // design-time visibility (default true)
|
||||||
bool enabled; // design-time enabled (default true)
|
bool enabled; // design-time enabled (default true)
|
||||||
DsgnPropT props[DSGN_MAX_PROPS];
|
DsgnPropT *props; // stb_ds array; freed by dsgnControlFree
|
||||||
int32_t propCount;
|
|
||||||
WidgetT *widget; // live widget (created at design time for WYSIWYG)
|
WidgetT *widget; // live widget (created at design time for WYSIWYG)
|
||||||
} DsgnControlT;
|
} DsgnControlT;
|
||||||
|
|
||||||
|
|
@ -150,6 +175,7 @@ typedef struct {
|
||||||
char *code; // BASIC code section (malloc'd, after End block)
|
char *code; // BASIC code section (malloc'd, after End block)
|
||||||
char layout[DSGN_MAX_NAME]; // "VBox" or "HBox" (contentBox type)
|
char layout[DSGN_MAX_NAME]; // "VBox" or "HBox" (contentBox type)
|
||||||
char helpTopic[DSGN_MAX_NAME]; // form-level help topic
|
char helpTopic[DSGN_MAX_NAME]; // form-level help topic
|
||||||
|
char contextMenu[DSGN_MAX_NAME]; // form popup menu name ("" = none)
|
||||||
bool centered; // true = center on screen, false = use left/top
|
bool centered; // true = center on screen, false = use left/top
|
||||||
bool autoSize; // true = dvxFitWindow, false = use width/height
|
bool autoSize; // true = dvxFitWindow, false = use width/height
|
||||||
bool resizable; // true = user can resize at runtime
|
bool resizable; // true = user can resize at runtime
|
||||||
|
|
@ -166,13 +192,22 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Grab handle IDs
|
// Grab handle IDs
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// The eight selection handles drawn around the selected control, clockwise
|
||||||
|
// from the top-left corner. Only E, SE and S resize (the layout owns the
|
||||||
|
// position); the rest are drawn inactive.
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
HANDLE_NONE = -1,
|
HANDLE_NONE = -1,
|
||||||
HANDLE_E = 0, // resize width
|
HANDLE_NW = 0,
|
||||||
HANDLE_S, // resize height
|
HANDLE_N,
|
||||||
|
HANDLE_NE,
|
||||||
|
HANDLE_E, // resize width
|
||||||
HANDLE_SE, // resize both
|
HANDLE_SE, // resize both
|
||||||
HANDLE_COUNT = 3
|
HANDLE_S, // resize height
|
||||||
|
HANDLE_SW,
|
||||||
|
HANDLE_W,
|
||||||
|
HANDLE_COUNT
|
||||||
} DsgnHandleE;
|
} DsgnHandleE;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -202,6 +237,7 @@ typedef struct {
|
||||||
WindowT *formWin;
|
WindowT *formWin;
|
||||||
AppContextT *ctx;
|
AppContextT *ctx;
|
||||||
const char *projectDir; // project directory (for resolving relative paths)
|
const char *projectDir; // project directory (for resolving relative paths)
|
||||||
|
bool treeStale; // controls[] order/parents changed; the Properties tree must be rebuilt
|
||||||
} DsgnStateT;
|
} DsgnStateT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -215,6 +251,35 @@ void dsgnInit(DsgnStateT *ds, AppContextT *ctx);
|
||||||
// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
|
// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
|
||||||
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
|
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
|
||||||
|
|
||||||
|
// Create one control's live design widget under parent (name, text,
|
||||||
|
// geometry, enabled state and interface properties from the control's
|
||||||
|
// data; Visible is left to the runtime). Sets and returns ctrl->widget,
|
||||||
|
// NULL when the type cannot be created at design time. form resolves the
|
||||||
|
// control's ancestors for the effective enabled state.
|
||||||
|
WidgetT *dsgnCreateControlWidget(const DsgnFormT *form, DsgnControlT *ctrl, WidgetT *parent);
|
||||||
|
|
||||||
|
// Initialize a blank control record: not in an array, top-level, visible,
|
||||||
|
// enabled, default size.
|
||||||
|
void dsgnControlInit(DsgnControlT *ctrl);
|
||||||
|
|
||||||
|
// Free a heap-allocated control record and its property array. The live
|
||||||
|
// widget is not touched.
|
||||||
|
void dsgnControlFree(DsgnControlT *ctrl);
|
||||||
|
|
||||||
|
// The control that ctrl's parentName/parentIndex refer to: the exact
|
||||||
|
// name + index match, else the first control with that name. NULL for a
|
||||||
|
// top-level control or one whose parent no longer exists.
|
||||||
|
DsgnControlT *dsgnFindParent(const DsgnFormT *form, const DsgnControlT *ctrl);
|
||||||
|
|
||||||
|
// Block depth of a control below the Form block (1 = top-level). A new
|
||||||
|
// child may only be added to a control whose depth is below
|
||||||
|
// DSGN_MAX_NEST_DEPTH.
|
||||||
|
int32_t dsgnControlDepth(const DsgnFormT *form, const DsgnControlT *ctrl);
|
||||||
|
|
||||||
|
// Push every control's effective enabled state (its own and its
|
||||||
|
// ancestors') to the live widgets.
|
||||||
|
void dsgnApplyEnabled(const DsgnFormT *form);
|
||||||
|
|
||||||
// Destroy every live widget under the form's contentBox and recreate them in
|
// Destroy every live widget under the form's contentBox and recreate them in
|
||||||
// the current controls[] order (the widget child list mirrors that order).
|
// the current controls[] order (the widget child list mirrors that order).
|
||||||
void dsgnRebuildWidgets(DsgnStateT *ds);
|
void dsgnRebuildWidgets(DsgnStateT *ds);
|
||||||
|
|
@ -222,17 +287,33 @@ void dsgnRebuildWidgets(DsgnStateT *ds);
|
||||||
// Load a .frm file into the designer.
|
// Load a .frm file into the designer.
|
||||||
bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen);
|
bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen);
|
||||||
|
|
||||||
|
// The BASIC code section of a .frm given the text after the form's End
|
||||||
|
// (frmParse's onFormEnd arguments): leading blank lines are skipped, the
|
||||||
|
// first code line keeps its indent. Returns its start and length, or
|
||||||
|
// NULL when there is no code. The one rule for the designer and the IDE.
|
||||||
|
const char *dsgnCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen);
|
||||||
|
|
||||||
|
// dsgnSaveFrm / dsgnEmitControl failure codes (never a partial result)
|
||||||
|
#define DSGN_SAVE_TOO_LARGE (-1) // no form, or the text does not fit in bufSize
|
||||||
|
#define DSGN_SAVE_LINE_TOO_LONG (-2) // a property line would exceed FRM_MAX_LINE_LEN - 1 characters
|
||||||
|
|
||||||
// Save the designer form to .frm text format.
|
// Save the designer form to .frm text format.
|
||||||
// Returns the number of bytes written (excluding null), or -1 on error or
|
// Returns the number of bytes written (excluding null), or one of the
|
||||||
// when the form does not fit in bufSize (the caller must not overwrite the
|
// DSGN_SAVE_* codes above; the caller must not overwrite the original
|
||||||
// original file with the partial result).
|
// file with the partial buffer.
|
||||||
int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize);
|
int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize);
|
||||||
|
|
||||||
// Append one control's "Begin ... End" block (without its children) to buf
|
// Append one control's "Begin ... End" block (without its children) to buf
|
||||||
// at pos, indented by indent levels. Returns the new position; shared by the
|
// at pos, indented by indent levels. Returns the new position, or
|
||||||
// .frm writer and the clipboard copy so both emit identical text.
|
// DSGN_SAVE_LINE_TOO_LONG when a property line is too long for the parser;
|
||||||
|
// shared by the .frm writer and the clipboard copy so both emit identical
|
||||||
|
// text.
|
||||||
int32_t dsgnEmitControl(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent);
|
int32_t dsgnEmitControl(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent);
|
||||||
|
|
||||||
|
// A buffer size that holds dsgnEmitControl's output for ctrl in full
|
||||||
|
// (every line at most FRM_MAX_LINE_LEN, plus the NUL).
|
||||||
|
int32_t dsgnEmitControlSize(const DsgnControlT *ctrl);
|
||||||
|
|
||||||
// Create a new blank form.
|
// Create a new blank form.
|
||||||
void dsgnNewForm(DsgnStateT *ds, const char *name);
|
void dsgnNewForm(DsgnStateT *ds, const char *name);
|
||||||
|
|
||||||
|
|
@ -251,8 +332,8 @@ const char *dsgnSelectedName(const DsgnStateT *ds);
|
||||||
// Return a control's property value by name, or NULL if it has no such property.
|
// Return a control's property value by name, or NULL if it has no such property.
|
||||||
const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name);
|
const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name);
|
||||||
|
|
||||||
// Set (or add) a control's raw string property. Silently ignored when the
|
// Set (or add) a control's raw string property. Values longer than
|
||||||
// control already holds DSGN_MAX_PROPS distinct properties.
|
// DSGN_MAX_TEXT - 1 characters are truncated.
|
||||||
void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value);
|
void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value);
|
||||||
|
|
||||||
// Fetch the current value of a widget interface property as a string: the
|
// Fetch the current value of a widget interface property as a string: the
|
||||||
|
|
@ -287,6 +368,15 @@ bool dsgnIsContainer(const char *typeName);
|
||||||
// does not collide with itself.
|
// does not collide with itself.
|
||||||
bool dsgnNameInUse(const DsgnFormT *form, const char *name, const char *exceptName, bool checkMenus);
|
bool dsgnNameInUse(const DsgnFormT *form, const char *name, const char *exceptName, bool checkMenus);
|
||||||
|
|
||||||
|
// Give ctrl control array index index; its children follow it (their
|
||||||
|
// parentIndex is updated) so they stay with this member.
|
||||||
|
void dsgnMakeArrayMember(DsgnFormT *form, DsgnControlT *ctrl, int32_t index);
|
||||||
|
|
||||||
|
// OptionButtons in one container are one group: when ctrl is an
|
||||||
|
// OptionButton whose Value is True, clear Value on the others in its
|
||||||
|
// group so a saved form never selects two. No-op for anything else.
|
||||||
|
void dsgnOptionSelected(DsgnFormT *form, DsgnControlT *ctrl);
|
||||||
|
|
||||||
// Initialize a blank menu item (enabled, visible, level 0).
|
// Initialize a blank menu item (enabled, visible, level 0).
|
||||||
void dsgnMenuItemInit(DsgnMenuItemT *mi);
|
void dsgnMenuItemInit(DsgnMenuItemT *mi);
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -45,7 +45,6 @@
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define MAX_MENU_LEVEL (DSGN_MENU_STACK_DEPTH - 1) // deepest level the preview bar can show
|
|
||||||
#define ARROW_STR "-> "
|
#define ARROW_STR "-> "
|
||||||
#define MED_MSG_BUF 128 // error-message scratch buffer
|
#define MED_MSG_BUF 128 // error-message scratch buffer
|
||||||
#define MED_SEP_PREFIX "mnuSep" // auto-generated separator name prefix
|
#define MED_SEP_PREFIX "mnuSep" // auto-generated separator name prefix
|
||||||
|
|
@ -100,22 +99,28 @@ static const char **sLabels = NULL;
|
||||||
// Prototypes
|
// Prototypes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void applyFields(void);
|
static void applyFields(void);
|
||||||
static int32_t findSubtreeEnd(int32_t idx);
|
static int32_t findSubtreeEnd(int32_t idx);
|
||||||
static void loadFields(void);
|
static void followMenuRenames(void);
|
||||||
static void onCancel(WidgetT *w);
|
static void loadFields(void);
|
||||||
static void onCaptionChange(WidgetT *w);
|
static void moveSubtree(int32_t from, int32_t subEnd, int32_t insertAt);
|
||||||
static void onDelete(WidgetT *w);
|
static void onCancel(WidgetT *w);
|
||||||
static void onIndent(WidgetT *w);
|
static void onCaptionChange(WidgetT *w);
|
||||||
static void onInsert(WidgetT *w);
|
static void onClose(WindowT *win);
|
||||||
static void onListClick(WidgetT *w);
|
static void onDelete(WidgetT *w);
|
||||||
static void onMoveDown(WidgetT *w);
|
static void onIndent(WidgetT *w);
|
||||||
static void onMoveUp(WidgetT *w);
|
static void onInsert(WidgetT *w);
|
||||||
static void onNameChange(WidgetT *w);
|
static void onListClick(WidgetT *w);
|
||||||
static void onNext(WidgetT *w);
|
static void onMoveDown(WidgetT *w);
|
||||||
static void onOk(WidgetT *w);
|
static void onMoveUp(WidgetT *w);
|
||||||
static void onOutdent(WidgetT *w);
|
static void onNameChange(WidgetT *w);
|
||||||
static void rebuildList(void);
|
static void onNext(WidgetT *w);
|
||||||
|
static void onOk(WidgetT *w);
|
||||||
|
static void onOutdent(WidgetT *w);
|
||||||
|
static void rebuildList(void);
|
||||||
|
static const char *renamedMenu(const char *oldName);
|
||||||
|
static void stripBlankItems(void);
|
||||||
|
static int32_t validateLevels(void);
|
||||||
|
|
||||||
|
|
||||||
static void applyFields(void) {
|
static void applyFields(void) {
|
||||||
|
|
@ -205,6 +210,30 @@ static int32_t findSubtreeEnd(int32_t idx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Point the form's and the controls' ContextMenu bindings at the new names
|
||||||
|
// of renamed menu items. Each binding is looked up against the original
|
||||||
|
// names, so swapped names cannot chain. Must run before form->menuItems is
|
||||||
|
// replaced.
|
||||||
|
static void followMenuRenames(void) {
|
||||||
|
DsgnFormT *form = sMed.form;
|
||||||
|
const char *newName = renamedMenu(form->contextMenu);
|
||||||
|
|
||||||
|
if (newName) {
|
||||||
|
snprintf(form->contextMenu, DSGN_MAX_NAME, "%s", newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
|
||||||
|
const char *ref = dsgnControlGetPropValue(form->controls[i], BAS_PROP_CONTEXTMENU);
|
||||||
|
|
||||||
|
newName = ref ? renamedMenu(ref) : NULL;
|
||||||
|
|
||||||
|
if (newName) {
|
||||||
|
dsgnSetPropValue(form->controls[i], BAS_PROP_CONTEXTMENU, newName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void loadFields(void) {
|
static void loadFields(void) {
|
||||||
int32_t count = (int32_t)arrlen(sMed.items);
|
int32_t count = (int32_t)arrlen(sMed.items);
|
||||||
|
|
||||||
|
|
@ -244,11 +273,15 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
|
||||||
sMed.form = form;
|
sMed.form = form;
|
||||||
sMed.selectedIdx = 0;
|
sMed.selectedIdx = 0;
|
||||||
|
|
||||||
// Clone menu items as working copy
|
// Clone menu items as working copy, numbering them so the caller (and
|
||||||
|
// onOk) can match each result item to the item it started as.
|
||||||
int32_t count = (int32_t)arrlen(form->menuItems);
|
int32_t count = (int32_t)arrlen(form->menuItems);
|
||||||
|
|
||||||
for (int32_t i = 0; i < count; i++) {
|
for (int32_t i = 0; i < count; i++) {
|
||||||
arrput(sMed.items, form->menuItems[i]);
|
DsgnMenuItemT mi = form->menuItems[i];
|
||||||
|
|
||||||
|
mi.id = i + 1;
|
||||||
|
arrput(sMed.items, mi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If empty, start with one blank item so the user can type immediately
|
// If empty, start with one blank item so the user can type immediately
|
||||||
|
|
@ -266,8 +299,10 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
win->maxW = win->w;
|
win->modal = true;
|
||||||
win->maxH = win->h;
|
win->onClose = onClose;
|
||||||
|
win->maxW = win->w;
|
||||||
|
win->maxH = win->h;
|
||||||
|
|
||||||
WidgetT *root = wgtInitWindow(ctx, win);
|
WidgetT *root = wgtInitWindow(ctx, win);
|
||||||
root->spacing = wgtPixels(MED_ROW_SPACING);
|
root->spacing = wgtPixels(MED_ROW_SPACING);
|
||||||
|
|
@ -294,9 +329,9 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
|
||||||
// Check row
|
// Check row
|
||||||
WidgetT *chkRow = wgtHBox(root);
|
WidgetT *chkRow = wgtHBox(root);
|
||||||
chkRow->spacing = wgtPixels(MED_CHECK_SPACING);
|
chkRow->spacing = wgtPixels(MED_CHECK_SPACING);
|
||||||
sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
|
sMed.checkedCb = wgtCheckbox(chkRow, BAS_PROP_CHECKED);
|
||||||
sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
|
sMed.radioCheckCb = wgtCheckbox(chkRow, BAS_PROP_RADIOCHECK);
|
||||||
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
|
sMed.enabledCb = wgtCheckbox(chkRow, BAS_PROP_ENABLED);
|
||||||
sMed.popupCb = wgtCheckbox(chkRow, "Popup");
|
sMed.popupCb = wgtCheckbox(chkRow, "Popup");
|
||||||
wgtCheckboxSetChecked(sMed.enabledCb, true);
|
wgtCheckboxSetChecked(sMed.enabledCb, true);
|
||||||
|
|
||||||
|
|
@ -386,6 +421,31 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Move the subtree items[from..subEnd) so it starts where index insertAt
|
||||||
|
// pointed before the move (insertAt <= from or insertAt >= subEnd), and
|
||||||
|
// select it.
|
||||||
|
static void moveSubtree(int32_t from, int32_t subEnd, int32_t insertAt) {
|
||||||
|
int32_t subSize = subEnd - from;
|
||||||
|
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc((size_t)subSize * sizeof(DsgnMenuItemT));
|
||||||
|
|
||||||
|
if (!tmp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(tmp, &sMed.items[from], (size_t)subSize * sizeof(DsgnMenuItemT));
|
||||||
|
arrdeln(sMed.items, from, subSize);
|
||||||
|
|
||||||
|
if (insertAt > from) {
|
||||||
|
insertAt -= subSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
arrinsn(sMed.items, insertAt, subSize);
|
||||||
|
memcpy(&sMed.items[insertAt], tmp, (size_t)subSize * sizeof(DsgnMenuItemT));
|
||||||
|
free(tmp);
|
||||||
|
sMed.selectedIdx = insertAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void onCancel(WidgetT *w) {
|
static void onCancel(WidgetT *w) {
|
||||||
(void)w;
|
(void)w;
|
||||||
sMed.accepted = false;
|
sMed.accepted = false;
|
||||||
|
|
@ -400,6 +460,13 @@ static void onCaptionChange(WidgetT *w) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The window close box cancels, like the Cancel button.
|
||||||
|
static void onClose(WindowT *win) {
|
||||||
|
(void)win;
|
||||||
|
onCancel(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void onDelete(WidgetT *w) {
|
static void onDelete(WidgetT *w) {
|
||||||
(void)w;
|
(void)w;
|
||||||
int32_t count = (int32_t)arrlen(sMed.items);
|
int32_t count = (int32_t)arrlen(sMed.items);
|
||||||
|
|
@ -447,13 +514,16 @@ static void onIndent(WidgetT *w) {
|
||||||
return; // already deeper than prev
|
return; // already deeper than prev
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sMed.items[idx].level >= MAX_MENU_LEVEL) {
|
// The whole subtree moves one level down, so its deepest item must
|
||||||
return;
|
// still be within the .frm nesting limit afterwards.
|
||||||
}
|
|
||||||
|
|
||||||
// Indent this item and all its children
|
|
||||||
int32_t subEnd = findSubtreeEnd(idx);
|
int32_t subEnd = findSubtreeEnd(idx);
|
||||||
|
|
||||||
|
for (int32_t i = idx; i < subEnd; i++) {
|
||||||
|
if (sMed.items[i].level >= DSGN_MAX_MENU_LEVEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int32_t i = idx; i < subEnd; i++) {
|
for (int32_t i = idx; i < subEnd; i++) {
|
||||||
sMed.items[i].level++;
|
sMed.items[i].level++;
|
||||||
}
|
}
|
||||||
|
|
@ -518,38 +588,17 @@ static void onMoveDown(WidgetT *w) {
|
||||||
|
|
||||||
int32_t subEnd = findSubtreeEnd(idx);
|
int32_t subEnd = findSubtreeEnd(idx);
|
||||||
|
|
||||||
if (subEnd >= count) {
|
// The item after our subtree must be our next sibling; a shallower
|
||||||
return; // already at bottom
|
// item means we are the last child, and moving past it would leave us
|
||||||
}
|
// under a different parent at a level jump.
|
||||||
|
if (subEnd >= count || sMed.items[subEnd].level != sMed.items[idx].level) {
|
||||||
// The item/subtree after us
|
|
||||||
int32_t nextEnd = findSubtreeEnd(subEnd);
|
|
||||||
|
|
||||||
// Rotate: move the next subtree before our subtree
|
|
||||||
// Simple approach: extract our subtree, delete it, insert after next subtree
|
|
||||||
int32_t subSize = subEnd - idx;
|
|
||||||
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));
|
|
||||||
|
|
||||||
if (!tmp) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));
|
// Move our subtree after the next sibling's subtree
|
||||||
|
int32_t nextEnd = findSubtreeEnd(subEnd);
|
||||||
|
|
||||||
// Delete our subtree
|
moveSubtree(idx, subEnd, nextEnd);
|
||||||
for (int32_t i = subEnd - 1; i >= idx; i--) {
|
|
||||||
arrdel(sMed.items, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert after what was the next subtree (now shifted)
|
|
||||||
int32_t insertAt = nextEnd - subSize;
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < subSize; i++) {
|
|
||||||
arrins(sMed.items, insertAt + i, tmp[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(tmp);
|
|
||||||
sMed.selectedIdx = insertAt;
|
|
||||||
rebuildList();
|
rebuildList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,29 +627,8 @@ static void onMoveUp(WidgetT *w) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move our subtree before the previous item's subtree
|
// Move our subtree before the previous sibling's subtree
|
||||||
int32_t subEnd = findSubtreeEnd(idx);
|
moveSubtree(idx, findSubtreeEnd(idx), prevIdx);
|
||||||
int32_t subSize = subEnd - idx;
|
|
||||||
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));
|
|
||||||
|
|
||||||
if (!tmp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));
|
|
||||||
|
|
||||||
// Delete our subtree
|
|
||||||
for (int32_t i = subEnd - 1; i >= idx; i--) {
|
|
||||||
arrdel(sMed.items, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert at prevIdx
|
|
||||||
for (int32_t i = 0; i < subSize; i++) {
|
|
||||||
arrins(sMed.items, prevIdx + i, tmp[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(tmp);
|
|
||||||
sMed.selectedIdx = prevIdx;
|
|
||||||
rebuildList();
|
rebuildList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -643,22 +671,21 @@ static void onNext(WidgetT *w) {
|
||||||
static void onOk(WidgetT *w) {
|
static void onOk(WidgetT *w) {
|
||||||
(void)w;
|
(void)w;
|
||||||
applyFields();
|
applyFields();
|
||||||
|
stripBlankItems();
|
||||||
|
|
||||||
// Strip blank items (no caption and no name)
|
// Validate: the levels form a tree, and every name is an identifier,
|
||||||
for (int32_t i = (int32_t)arrlen(sMed.items) - 1; i >= 0; i--) {
|
// unique among the menu items, and not the name of the form or one of
|
||||||
if (sMed.items[i].caption[0] == '\0' && sMed.items[i].name[0] == '\0') {
|
// its controls. A name with spaces would be written as
|
||||||
arrdel(sMed.items, i);
|
// "Begin Menu File Menu" and mis-parsed.
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate: every name is an identifier, unique among the menu items,
|
|
||||||
// and not the name of the form or one of its controls. A name with
|
|
||||||
// spaces would be written as "Begin Menu File Menu" and mis-parsed.
|
|
||||||
int32_t count = (int32_t)arrlen(sMed.items);
|
int32_t count = (int32_t)arrlen(sMed.items);
|
||||||
int32_t badIdx = -1;
|
int32_t badIdx = validateLevels();
|
||||||
const char *reason = NULL;
|
const char *reason = NULL;
|
||||||
char msg[MED_MSG_BUF];
|
char msg[MED_MSG_BUF];
|
||||||
|
|
||||||
|
if (badIdx >= 0) {
|
||||||
|
reason = "A menu item is nested more than one level below the item above it.";
|
||||||
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < count && badIdx < 0; i++) {
|
for (int32_t i = 0; i < count && badIdx < 0; i++) {
|
||||||
const char *name = sMed.items[i].name;
|
const char *name = sMed.items[i].name;
|
||||||
|
|
||||||
|
|
@ -693,6 +720,8 @@ static void onOk(WidgetT *w) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
followMenuRenames();
|
||||||
|
|
||||||
// Copy working items back to form
|
// Copy working items back to form
|
||||||
arrfree(sMed.form->menuItems);
|
arrfree(sMed.form->menuItems);
|
||||||
sMed.form->menuItems = NULL;
|
sMed.form->menuItems = NULL;
|
||||||
|
|
@ -773,3 +802,59 @@ static void rebuildList(void) {
|
||||||
wgtListBoxSetItems(sMed.listBox, sLabels, count);
|
wgtListBoxSetItems(sMed.listBox, sLabels, count);
|
||||||
wgtListBoxSetSelected(sMed.listBox, sMed.selectedIdx);
|
wgtListBoxSetSelected(sMed.listBox, sMed.selectedIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The working-copy name of the menu item that was named oldName when the
|
||||||
|
// dialog opened (matched through the item ids), or NULL when that item was
|
||||||
|
// deleted or no original item had that name.
|
||||||
|
static const char *renamedMenu(const char *oldName) {
|
||||||
|
int32_t oldCount = (int32_t)arrlen(sMed.form->menuItems);
|
||||||
|
int32_t count = (int32_t)arrlen(sMed.items);
|
||||||
|
|
||||||
|
for (int32_t k = 0; k < oldCount; k++) {
|
||||||
|
if (strcasecmp(sMed.form->menuItems[k].name, oldName) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < count; i++) {
|
||||||
|
if (sMed.items[i].id == k + 1) {
|
||||||
|
return sMed.items[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Remove items with neither caption nor name. Only childless ones go
|
||||||
|
// (walking backwards, a blank parent whose children were all blank goes
|
||||||
|
// too): removing a blank parent would leave its children under the wrong
|
||||||
|
// parent, so it stays and fails the Name check instead.
|
||||||
|
static void stripBlankItems(void) {
|
||||||
|
for (int32_t i = (int32_t)arrlen(sMed.items) - 1; i >= 0; i--) {
|
||||||
|
if (sMed.items[i].caption[0] == '\0' && sMed.items[i].name[0] == '\0' && findSubtreeEnd(i) == i + 1) {
|
||||||
|
arrdel(sMed.items, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Index of the first item that breaks the menu tree (the first item is
|
||||||
|
// not top-level, or an item is more than one level below its predecessor),
|
||||||
|
// or -1 when the levels are valid.
|
||||||
|
static int32_t validateLevels(void) {
|
||||||
|
int32_t count = (int32_t)arrlen(sMed.items);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < count; i++) {
|
||||||
|
int32_t maxLevel = (i == 0) ? 0 : sMed.items[i - 1].level + 1;
|
||||||
|
|
||||||
|
if (sMed.items[i].level > maxLevel) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,13 @@
|
||||||
#include "ideDesigner.h"
|
#include "ideDesigner.h"
|
||||||
#include "dvxApp.h"
|
#include "dvxApp.h"
|
||||||
|
|
||||||
// Show the modal Menu Editor dialog.
|
// Show the modal Menu Editor dialog. On OK the form's menuItems are
|
||||||
|
// replaced (and ContextMenu bindings on the form and its controls follow
|
||||||
|
// renamed items) and true is returned. Every returned item carries an id:
|
||||||
|
// id > 0 means it is the item that was form->menuItems[id - 1] when the
|
||||||
|
// dialog opened (so a caller that saved the old names can detect renames
|
||||||
|
// by id, whatever was inserted, deleted or moved); DSGN_NEW_MENU_ID marks
|
||||||
|
// an item added in the dialog.
|
||||||
bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form);
|
bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form);
|
||||||
|
|
||||||
#endif // IDE_MENUEDITOR_H
|
#endif // IDE_MENUEDITOR_H
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,10 @@
|
||||||
// Name = MyProject
|
// Name = MyProject
|
||||||
//
|
//
|
||||||
// [Modules]
|
// [Modules]
|
||||||
// Count = 2
|
|
||||||
// File0 = MAIN.BAS
|
// File0 = MAIN.BAS
|
||||||
// File1 = UTILS.BAS
|
// File1 = UTILS.BAS
|
||||||
//
|
//
|
||||||
// [Forms]
|
// [Forms]
|
||||||
// Count = 1
|
|
||||||
// File0 = FORM1.FRM
|
// File0 = FORM1.FRM
|
||||||
//
|
//
|
||||||
// [Settings]
|
// [Settings]
|
||||||
|
|
@ -78,14 +76,12 @@
|
||||||
#define PRJ_PLACEHOLDER_BPP 4 // bytes per pixel of the 1x1 icon placeholder
|
#define PRJ_PLACEHOLDER_BPP 4 // bytes per pixel of the 1x1 icon placeholder
|
||||||
#define PRJ_INI_KEY_LEN 16 // "FileNNN" INI key buffer size
|
#define PRJ_INI_KEY_LEN 16 // "FileNNN" INI key buffer size
|
||||||
#define PRJ_FILE_KEY_FMT "File%d" // INI key for an indexed module/form path
|
#define PRJ_FILE_KEY_FMT "File%d" // INI key for an indexed module/form path
|
||||||
#define PRJ_COUNT_KEY "Count" // INI key for the file count in a section
|
|
||||||
|
|
||||||
#define PPD_WIDTH 380
|
#define PPD_WIDTH 380
|
||||||
#define PPD_HEIGHT 410
|
#define PPD_HEIGHT 410
|
||||||
#define PPD_LABEL_W 96
|
#define PPD_LABEL_W 96
|
||||||
#define PPD_BTN_W 70
|
#define PPD_BTN_W 70
|
||||||
#define PPD_BTN_H 24
|
#define PPD_BTN_H 24
|
||||||
#define PPD_DESC_H 60
|
|
||||||
#define PPD_ROOT_SPACING 2
|
#define PPD_ROOT_SPACING 2
|
||||||
#define PPD_ROW_SPACING 4
|
#define PPD_ROW_SPACING 4
|
||||||
|
|
||||||
|
|
@ -109,6 +105,7 @@ static struct {
|
||||||
WidgetT *version;
|
WidgetT *version;
|
||||||
WidgetT *copyright;
|
WidgetT *copyright;
|
||||||
WidgetT *description;
|
WidgetT *description;
|
||||||
|
WidgetT *commandArgs;
|
||||||
WidgetT *startupForm;
|
WidgetT *startupForm;
|
||||||
WidgetT *optionExplicit;
|
WidgetT *optionExplicit;
|
||||||
const char **formNames; // stb_ds array of form name strings for startup dropdown
|
const char **formNames; // stb_ds array of form name strings for startup dropdown
|
||||||
|
|
@ -134,10 +131,14 @@ static void ppdOnBrowseIcon(WidgetT *w);
|
||||||
static void ppdOnCancel(WidgetT *w);
|
static void ppdOnCancel(WidgetT *w);
|
||||||
static void ppdOnClose(WindowT *win);
|
static void ppdOnClose(WindowT *win);
|
||||||
static void ppdOnOk(WidgetT *w);
|
static void ppdOnOk(WidgetT *w);
|
||||||
|
static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize);
|
||||||
static void prjDeriveDir(PrjStateT *prj, const char *dbpPath);
|
static void prjDeriveDir(PrjStateT *prj, const char *dbpPath);
|
||||||
|
static int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path);
|
||||||
static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm);
|
static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm);
|
||||||
|
static void prjLoadString(PrefsHandleT *h, const char *section, const char *key, char *dst, int32_t dstSize);
|
||||||
static const char *prjMakeRelative(const PrjStateT *prj, const char *path);
|
static const char *prjMakeRelative(const PrjStateT *prj, const char *path);
|
||||||
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm);
|
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm);
|
||||||
|
static void prjSaveOptional(PrefsHandleT *h, const char *key, const char *value);
|
||||||
static bool validateIcon(const char *fullPath, bool showErrors);
|
static bool validateIcon(const char *fullPath, bool showErrors);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,7 +154,7 @@ static void onTreeItemDblClick(WidgetT *w) {
|
||||||
|
|
||||||
int32_t fileIdx = (int32_t)(intptr_t)w->userData;
|
int32_t fileIdx = (int32_t)(intptr_t)w->userData;
|
||||||
|
|
||||||
if (fileIdx >= 0 && fileIdx < sPrj->fileCount) {
|
if (fileIdx >= 0 && fileIdx < prjFileCount(sPrj)) {
|
||||||
sOnClick(fileIdx, sPrj->files[fileIdx].isForm);
|
sOnClick(fileIdx, sPrj->files[fileIdx].isForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +227,7 @@ static void ppdOnBrowseHelp(WidgetT *w) {
|
||||||
|
|
||||||
char path[DVX_MAX_PATH];
|
char path[DVX_MAX_PATH];
|
||||||
|
|
||||||
if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), path, sizeof(path))) {
|
if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,7 +248,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
|
||||||
|
|
||||||
char path[DVX_MAX_PATH];
|
char path[DVX_MAX_PATH];
|
||||||
|
|
||||||
if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), path, sizeof(path))) {
|
if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
|
||||||
if (!validateIcon(path, true)) {
|
if (!validateIcon(path, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -272,11 +273,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
|
||||||
char destPath[DVX_MAX_PATH * 2];
|
char destPath[DVX_MAX_PATH * 2];
|
||||||
snprintf(destPath, sizeof(destPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, fname);
|
snprintf(destPath, sizeof(destPath), "%s" DVX_PATH_SEP "%s", sPpd.prj->projectDir, fname);
|
||||||
|
|
||||||
FILE *existing = fopen(destPath, "rb");
|
if (prjPathExists(destPath)) {
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
fclose(existing);
|
|
||||||
|
|
||||||
char msg[DVX_MAX_PATH + PRJ_MSG_BUF];
|
char msg[DVX_MAX_PATH + PRJ_MSG_BUF];
|
||||||
snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
|
snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
|
||||||
int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
|
int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
|
||||||
|
|
@ -332,20 +329,30 @@ static void ppdOnOk(WidgetT *w) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Copy a dialog input's text into dst (dst keeps its value when the input
|
||||||
|
// has none).
|
||||||
|
static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize) {
|
||||||
|
const char *s = wgtGetText(input);
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
snprintf(dst, dstSize, "%s", s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
|
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
|
||||||
PrjFileT entry;
|
PrjFileT entry;
|
||||||
memset(&entry, 0, sizeof(entry));
|
memset(&entry, 0, sizeof(entry));
|
||||||
snprintf(entry.path, sizeof(entry.path), "%s", relativePath);
|
snprintf(entry.path, sizeof(entry.path), "%s", relativePath);
|
||||||
entry.isForm = isForm;
|
entry.isForm = isForm;
|
||||||
arrput(prj->files, entry);
|
arrput(prj->files, entry);
|
||||||
prj->fileCount = (int32_t)arrlen(prj->files);
|
|
||||||
prj->dirty = true;
|
prj->dirty = true;
|
||||||
return prj->fileCount - 1;
|
return prjFileCount(prj) - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void prjClose(PrjStateT *prj) {
|
void prjClose(PrjStateT *prj) {
|
||||||
for (int32_t i = 0; i < prj->fileCount; i++) {
|
for (int32_t i = 0; i < prjFileCount(prj); i++) {
|
||||||
free(prj->files[i].buffer);
|
free(prj->files[i].buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,8 +423,25 @@ void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int32_t prjFileCount(const PrjStateT *prj) {
|
||||||
|
return (int32_t)arrlen(prj->files);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Index of the project file whose relative path is path, or -1.
|
||||||
|
static int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path) {
|
||||||
|
for (int32_t i = 0; i < prjFileCount(prj); i++) {
|
||||||
|
if (strcmp(prj->files[i].path, path) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) {
|
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) {
|
||||||
if (fileIdx < 0 || fileIdx >= prj->fileCount) {
|
if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) {
|
||||||
outPath[0] = '\0';
|
outPath[0] = '\0';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -448,6 +472,12 @@ void prjInit(PrjStateT *prj) {
|
||||||
|
|
||||||
|
|
||||||
bool prjLoad(PrjStateT *prj, const char *dbpPath) {
|
bool prjLoad(PrjStateT *prj, const char *dbpPath) {
|
||||||
|
// prefsLoad treats a missing file as an empty INI, so probe first or a
|
||||||
|
// deleted .dbp would "open" as an empty project.
|
||||||
|
if (!prjPathExists(dbpPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PrefsHandleT *h = prefsLoad(dbpPath);
|
PrefsHandleT *h = prefsLoad(dbpPath);
|
||||||
|
|
||||||
if (!h) {
|
if (!h) {
|
||||||
|
|
@ -463,39 +493,22 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
|
||||||
prjDeriveDir(prj, dbpPath);
|
prjDeriveDir(prj, dbpPath);
|
||||||
|
|
||||||
// [Project] section
|
// [Project] section
|
||||||
const char *val;
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, prj->name, sizeof(prj->name));
|
||||||
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, prj->author, sizeof(prj->author));
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, NULL);
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, prj->publisher, sizeof(prj->publisher));
|
||||||
if (val) { snprintf(prj->name, sizeof(prj->name), "%s", val); }
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, prj->version, sizeof(prj->version));
|
||||||
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, prj->copyright, sizeof(prj->copyright));
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, NULL);
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, prj->description, sizeof(prj->description));
|
||||||
if (val) { snprintf(prj->author, sizeof(prj->author), "%s", val); }
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, prj->iconPath, sizeof(prj->iconPath));
|
||||||
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, prj->helpFile, sizeof(prj->helpFile));
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, NULL);
|
prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COMMANDARGS, prj->commandArgs, sizeof(prj->commandArgs));
|
||||||
if (val) { snprintf(prj->publisher, sizeof(prj->publisher), "%s", val); }
|
|
||||||
|
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, NULL);
|
|
||||||
if (val) { snprintf(prj->version, sizeof(prj->version), "%s", val); }
|
|
||||||
|
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, NULL);
|
|
||||||
if (val) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", val); }
|
|
||||||
|
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, NULL);
|
|
||||||
if (val) { snprintf(prj->description, sizeof(prj->description), "%s", val); }
|
|
||||||
|
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, NULL);
|
|
||||||
if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); }
|
|
||||||
|
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, NULL);
|
|
||||||
if (val) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", val); }
|
|
||||||
|
|
||||||
// [Modules] and [Forms] sections -- File0, File1, ... (no cap)
|
// [Modules] and [Forms] sections -- File0, File1, ... (no cap)
|
||||||
prjLoadFileSection(prj, h, BAS_INI_SECTION_MODULES, false);
|
prjLoadFileSection(prj, h, BAS_INI_SECTION_MODULES, false);
|
||||||
prjLoadFileSection(prj, h, BAS_INI_SECTION_FORMS, true);
|
prjLoadFileSection(prj, h, BAS_INI_SECTION_FORMS, true);
|
||||||
|
|
||||||
// [Settings] section
|
// [Settings] section
|
||||||
val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL);
|
prjLoadString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm, sizeof(prj->startupForm));
|
||||||
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }
|
|
||||||
|
|
||||||
prj->optionExplicit = prefsGetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
|
prj->optionExplicit = prefsGetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
|
||||||
|
|
||||||
|
|
@ -508,19 +521,16 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
|
||||||
// prjLoadAllFiles -- read all project files into memory buffers
|
// prjLoadAllFiles -- read all project files into memory buffers
|
||||||
// and extract form names from .frm files.
|
// and extract form names from .frm files.
|
||||||
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
|
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
|
||||||
for (int32_t i = 0; i < prj->fileCount; i++) {
|
for (int32_t i = 0; i < prjFileCount(prj); i++) {
|
||||||
if (prj->files[i].buffer) {
|
if (prj->files[i].buffer) {
|
||||||
continue; // already loaded
|
continue; // already loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
char fullPath[DVX_MAX_PATH];
|
// An unreadable or oversized file stays unloaded; the IDE reports
|
||||||
prjFullPath(prj, i, fullPath, sizeof(fullPath));
|
// it when the file is opened or compiled.
|
||||||
|
char *buf = prjReadFileText(prj, i, NULL);
|
||||||
|
|
||||||
int32_t bytesRead = 0;
|
if (!buf) {
|
||||||
char *buf = platformReadFile(fullPath, &bytesRead);
|
|
||||||
|
|
||||||
if (!buf || bytesRead <= 0) {
|
|
||||||
free(buf);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,6 +567,17 @@ static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *sect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Copy an INI string into dst when the key exists (dst keeps its value
|
||||||
|
// otherwise).
|
||||||
|
static void prjLoadString(PrefsHandleT *h, const char *section, const char *key, char *dst, int32_t dstSize) {
|
||||||
|
const char *val = prefsGetString(h, section, key, NULL);
|
||||||
|
|
||||||
|
if (val) {
|
||||||
|
snprintf(dst, dstSize, "%s", val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Return path relative to prj->projectDir when path lives under it, else NULL
|
// Return path relative to prj->projectDir when path lives under it, else NULL
|
||||||
// (callers apply their own fallback).
|
// (callers apply their own fallback).
|
||||||
static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
|
static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
|
||||||
|
|
@ -572,17 +593,37 @@ static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
|
||||||
|
|
||||||
|
|
||||||
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
|
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
|
||||||
for (int32_t i = 0; i < prj->sourceMapCount; i++) {
|
const char *path = NULL;
|
||||||
const PrjSourceMapT *m = &prj->sourceMap[i];
|
int32_t localLine = 0;
|
||||||
|
|
||||||
if (concatLine >= m->startLine && concatLine < m->startLine + m->lineCount) {
|
if (!basBuildMapLine(prj->sourceMap, concatLine, &path, &localLine)) {
|
||||||
*outFileIdx = m->fileIdx;
|
return false;
|
||||||
*outLocalLine = concatLine - m->startLine + 1;
|
}
|
||||||
return true;
|
|
||||||
|
int32_t fileIdx = prjFileIndexByPath(prj, path);
|
||||||
|
|
||||||
|
if (fileIdx < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outFileIdx = fileIdx;
|
||||||
|
*outLocalLine = localLine;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int32_t prjMapStartLine(const PrjStateT *prj, int32_t fileIdx) {
|
||||||
|
if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < (int32_t)arrlen(prj->sourceMap); i++) {
|
||||||
|
if (strcmp(prj->sourceMap[i].path, prj->files[fileIdx].path) == 0) {
|
||||||
|
return prj->sourceMap[i].startLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -605,6 +646,18 @@ void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool prjPathExists(const char *path) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
|
||||||
|
if (!f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) {
|
bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) {
|
||||||
if (!ctx || !prj) {
|
if (!ctx || !prj) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -657,7 +710,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
||||||
sPpd.formNames = NULL;
|
sPpd.formNames = NULL;
|
||||||
int32_t selectedIdx = 0;
|
int32_t selectedIdx = 0;
|
||||||
|
|
||||||
for (int32_t i = 0; i < prj->fileCount; i++) {
|
for (int32_t i = 0; i < prjFileCount(prj); i++) {
|
||||||
if (!prj->files[i].isForm) {
|
if (!prj->files[i].isForm) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -669,7 +722,11 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
||||||
if (!name[0]) {
|
if (!name[0]) {
|
||||||
snprintf(fallback, sizeof(fallback), "%s", prj->files[i].path);
|
snprintf(fallback, sizeof(fallback), "%s", prj->files[i].path);
|
||||||
char *dot = strrchr(fallback, '.');
|
char *dot = strrchr(fallback, '.');
|
||||||
if (dot) { *dot = '\0'; }
|
|
||||||
|
if (dot) {
|
||||||
|
*dot = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
name = fallback;
|
name = fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,12 +793,11 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
||||||
sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)");
|
sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)");
|
||||||
wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit);
|
wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit);
|
||||||
|
|
||||||
// Description: label above, textarea below (matches Preferences layout)
|
// Description: one line, since it is stored as a single INI value.
|
||||||
wgtLabel(root, "Description:");
|
sPpd.description = ppdAddRow(root, "Description:", prj->description, sizeof(prj->description));
|
||||||
sPpd.description = wgtTextArea(root, (int32_t)sizeof(prj->description) - 1);
|
|
||||||
sPpd.description->weight = WGT_WEIGHT_FILL;
|
// What COMMAND$ returns when the program is run from the IDE.
|
||||||
sPpd.description->minH = wgtPixels(PPD_DESC_H);
|
sPpd.commandArgs = ppdAddRow(root, "Arguments:", prj->commandArgs, sizeof(prj->commandArgs));
|
||||||
wgtSetText(sPpd.description, prj->description);
|
|
||||||
|
|
||||||
// OK / Cancel buttons
|
// OK / Cancel buttons
|
||||||
WidgetT *btnRow = wgtHBox(root);
|
WidgetT *btnRow = wgtHBox(root);
|
||||||
|
|
@ -767,31 +823,16 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sPpd.accepted) {
|
if (sPpd.accepted) {
|
||||||
const char *s;
|
ppdReadInput(sPpd.name, prj->name, sizeof(prj->name));
|
||||||
|
ppdReadInput(sPpd.author, prj->author, sizeof(prj->author));
|
||||||
s = wgtGetText(sPpd.name);
|
ppdReadInput(sPpd.publisher, prj->publisher, sizeof(prj->publisher));
|
||||||
if (s) { snprintf(prj->name, sizeof(prj->name), "%s", s); }
|
ppdReadInput(sPpd.version, prj->version, sizeof(prj->version));
|
||||||
|
ppdReadInput(sPpd.copyright, prj->copyright, sizeof(prj->copyright));
|
||||||
s = wgtGetText(sPpd.author);
|
ppdReadInput(sPpd.description, prj->description, sizeof(prj->description));
|
||||||
if (s) { snprintf(prj->author, sizeof(prj->author), "%s", s); }
|
ppdReadInput(sPpd.helpFileInput, prj->helpFile, sizeof(prj->helpFile));
|
||||||
|
ppdReadInput(sPpd.commandArgs, prj->commandArgs, sizeof(prj->commandArgs));
|
||||||
s = wgtGetText(sPpd.publisher);
|
|
||||||
if (s) { snprintf(prj->publisher, sizeof(prj->publisher), "%s", s); }
|
|
||||||
|
|
||||||
s = wgtGetText(sPpd.version);
|
|
||||||
if (s) { snprintf(prj->version, sizeof(prj->version), "%s", s); }
|
|
||||||
|
|
||||||
s = wgtGetText(sPpd.copyright);
|
|
||||||
if (s) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", s); }
|
|
||||||
|
|
||||||
s = wgtGetText(sPpd.description);
|
|
||||||
if (s) { snprintf(prj->description, sizeof(prj->description), "%s", s); }
|
|
||||||
|
|
||||||
snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);
|
snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);
|
||||||
|
|
||||||
s = wgtGetText(sPpd.helpFileInput);
|
|
||||||
if (s) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", s); }
|
|
||||||
|
|
||||||
// Read startup form from dropdown
|
// Read startup form from dropdown
|
||||||
if (sPpd.startupForm && sPpd.formNames) {
|
if (sPpd.startupForm && sPpd.formNames) {
|
||||||
int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);
|
int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);
|
||||||
|
|
@ -819,6 +860,52 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
char *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing) {
|
||||||
|
char fullPath[DVX_MAX_PATH];
|
||||||
|
|
||||||
|
prjFullPath(prj, fileIdx, fullPath, sizeof(fullPath));
|
||||||
|
|
||||||
|
if (outMissing) {
|
||||||
|
*outMissing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size first: a file that cannot fit even with every line ending
|
||||||
|
// stripped is refused without reading it into memory
|
||||||
|
FILE *f = fopen(fullPath, "rb");
|
||||||
|
|
||||||
|
if (!f) {
|
||||||
|
if (outMissing) {
|
||||||
|
*outMissing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
long rawSize = (fseek(f, 0, SEEK_END) == 0) ? ftell(f) : -1;
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
if (rawSize < 0 || rawSize >= PRJ_MAX_RAW_SOURCE) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t len = 0;
|
||||||
|
char *text = platformReadFile(fullPath, &len);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The limit is the editor's, which holds the text without CRs
|
||||||
|
if (platformStripLineEndings(text, len) >= PRJ_MAX_SOURCE) {
|
||||||
|
free(text);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void prjRebuildTree(PrjStateT *prj) {
|
void prjRebuildTree(PrjStateT *prj) {
|
||||||
if (!sTree) {
|
if (!sTree) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -838,7 +925,7 @@ void prjRebuildTree(PrjStateT *prj) {
|
||||||
sLabels = NULL;
|
sLabels = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prj || prj->fileCount == 0) {
|
if (!prj || prjFileCount(prj) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -862,7 +949,7 @@ void prjRebuildTree(PrjStateT *prj) {
|
||||||
modsNode->userData = (void *)(intptr_t)-1;
|
modsNode->userData = (void *)(intptr_t)-1;
|
||||||
wgtTreeItemSetExpanded(modsNode, true);
|
wgtTreeItemSetExpanded(modsNode, true);
|
||||||
|
|
||||||
for (int32_t i = 0; i < prj->fileCount; i++) {
|
for (int32_t i = 0; i < prjFileCount(prj); i++) {
|
||||||
char buf[DVX_MAX_PATH + sizeof(PRJ_MODIFIED_SUFFIX)];
|
char buf[DVX_MAX_PATH + sizeof(PRJ_MODIFIED_SUFFIX)];
|
||||||
snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? PRJ_MODIFIED_SUFFIX : "");
|
snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? PRJ_MODIFIED_SUFFIX : "");
|
||||||
char *label = strdup(buf);
|
char *label = strdup(buf);
|
||||||
|
|
@ -877,13 +964,25 @@ void prjRebuildTree(PrjStateT *prj) {
|
||||||
|
|
||||||
|
|
||||||
void prjRemoveFile(PrjStateT *prj, int32_t idx) {
|
void prjRemoveFile(PrjStateT *prj, int32_t idx) {
|
||||||
if (idx < 0 || idx >= prj->fileCount) {
|
if (idx < 0 || idx >= prjFileCount(prj)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A removed startup form is no longer a valid startup form.
|
||||||
|
if (prj->files[idx].isForm && prj->files[idx].formName[0] && strcasecmp(prj->files[idx].formName, prj->startupForm) == 0) {
|
||||||
|
prj->startupForm[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The source map is keyed by path: drop the removed file's lines so
|
||||||
|
// they no longer map to a file until the next compile rebuilds it.
|
||||||
|
for (int32_t i = (int32_t)arrlen(prj->sourceMap) - 1; i >= 0; i--) {
|
||||||
|
if (strcmp(prj->sourceMap[i].path, prj->files[idx].path) == 0) {
|
||||||
|
arrdel(prj->sourceMap, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
free(prj->files[idx].buffer);
|
free(prj->files[idx].buffer);
|
||||||
arrdel(prj->files, idx);
|
arrdel(prj->files, idx);
|
||||||
prj->fileCount = (int32_t)arrlen(prj->files);
|
|
||||||
|
|
||||||
// Adjust active file index
|
// Adjust active file index
|
||||||
if (prj->activeFileIdx == idx) {
|
if (prj->activeFileIdx == idx) {
|
||||||
|
|
@ -909,14 +1008,14 @@ bool prjSave(const PrjStateT *prj) {
|
||||||
|
|
||||||
// [Project] section
|
// [Project] section
|
||||||
prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, prj->name);
|
prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, prj->name);
|
||||||
|
prjSaveOptional(h, BAS_INI_KEY_AUTHOR, prj->author);
|
||||||
if (prj->author[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, prj->author); }
|
prjSaveOptional(h, BAS_INI_KEY_PUBLISHER, prj->publisher);
|
||||||
if (prj->publisher[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, prj->publisher); }
|
prjSaveOptional(h, BAS_INI_KEY_VERSION, prj->version);
|
||||||
if (prj->version[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, prj->version); }
|
prjSaveOptional(h, BAS_INI_KEY_COPYRIGHT, prj->copyright);
|
||||||
if (prj->copyright[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, prj->copyright); }
|
prjSaveOptional(h, BAS_INI_KEY_DESCRIPTION, prj->description);
|
||||||
if (prj->description[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, prj->description); }
|
prjSaveOptional(h, BAS_INI_KEY_ICON, prj->iconPath);
|
||||||
if (prj->iconPath[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, prj->iconPath); }
|
prjSaveOptional(h, BAS_INI_KEY_HELPFILE, prj->helpFile);
|
||||||
if (prj->helpFile[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, prj->helpFile); }
|
prjSaveOptional(h, BAS_INI_KEY_COMMANDARGS, prj->commandArgs);
|
||||||
|
|
||||||
// [Modules] and [Forms] sections
|
// [Modules] and [Forms] sections
|
||||||
prjSaveFileSection(h, prj, BAS_INI_SECTION_MODULES, false);
|
prjSaveFileSection(h, prj, BAS_INI_SECTION_MODULES, false);
|
||||||
|
|
@ -933,19 +1032,43 @@ bool prjSave(const PrjStateT *prj) {
|
||||||
|
|
||||||
|
|
||||||
// Write the project files whose isForm flag matches into an INI section as
|
// Write the project files whose isForm flag matches into an INI section as
|
||||||
// File0, File1, ... plus a Count entry.
|
// File0, File1, ... (readers stop at the first missing key).
|
||||||
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
|
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
|
||||||
int32_t idx = 0;
|
int32_t idx = 0;
|
||||||
|
|
||||||
for (int32_t i = 0; i < prj->fileCount; i++) {
|
for (int32_t i = 0; i < prjFileCount(prj); i++) {
|
||||||
if (prj->files[i].isForm == isForm) {
|
if (prj->files[i].isForm == isForm) {
|
||||||
char key[PRJ_INI_KEY_LEN];
|
char key[PRJ_INI_KEY_LEN];
|
||||||
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++);
|
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++);
|
||||||
prefsSetString(h, section, key, prj->files[i].path);
|
prefsSetString(h, section, key, prj->files[i].path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prefsSetInt(h, section, PRJ_COUNT_KEY, idx);
|
|
||||||
|
_Static_assert(DVX_MAX_PATH <= PRJ_MAX_FIELD && PRJ_MAX_STRING <= PRJ_MAX_FIELD, "every [Project] field must fit PRJ_MAX_FIELD");
|
||||||
|
|
||||||
|
|
||||||
|
// Write a non-empty [Project] value as one INI line. Every [Project]
|
||||||
|
// field is a PrjStateT buffer of at most PRJ_MAX_FIELD bytes.
|
||||||
|
static void prjSaveOptional(PrefsHandleT *h, const char *key, const char *value) {
|
||||||
|
char line[PRJ_MAX_FIELD];
|
||||||
|
|
||||||
|
if (value[0]) {
|
||||||
|
prjSingleLine(line, (int32_t)sizeof(line), value);
|
||||||
|
prefsSetString(h, BAS_INI_SECTION_PROJECT, key, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void prjSingleLine(char *dst, int32_t dstSize, const char *src) {
|
||||||
|
int32_t i = 0;
|
||||||
|
|
||||||
|
for (; src[i] && i < dstSize - 1; i++) {
|
||||||
|
dst[i] = (src[i] == '\r' || src[i] == '\n') ? ' ' : src[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[i] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
#include "dvxApp.h"
|
#include "dvxApp.h"
|
||||||
#include "dvxPrefs.h"
|
#include "dvxPrefs.h"
|
||||||
#include "dvxTypes.h"
|
#include "dvxTypes.h"
|
||||||
|
#include "../basBuild.h"
|
||||||
|
#include "../basRes.h"
|
||||||
#include "../formrt/formrt.h"
|
#include "../formrt/formrt.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
@ -37,9 +39,13 @@
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define PRJ_MAX_NAME 32
|
#define PRJ_MAX_NAME 32
|
||||||
#define PRJ_MAX_STRING 128
|
#define PRJ_MAX_SOURCE 65536 // a project file must be smaller than this to be loaded or edited
|
||||||
#define PRJ_MAX_DESC 512
|
#define PRJ_MAX_STRING 128
|
||||||
|
#define PRJ_MAX_DESC 512 // description buffer, including the NUL (saved as one INI line)
|
||||||
|
#define PRJ_MAX_FIELD PRJ_MAX_DESC // largest [Project] field buffer (description; paths are DVX_MAX_PATH)
|
||||||
|
#define PRJ_MAX_ARGS PRJ_MAX_FIELD // command line arguments for runs from the IDE (one INI line)
|
||||||
|
#define PRJ_MAX_RAW_SOURCE (PRJ_MAX_SOURCE * 2) // on-disk size limit: CR LF line ends at worst double the text
|
||||||
|
|
||||||
// IDE preference sections and keys shared by the project code and the
|
// IDE preference sections and keys shared by the project code and the
|
||||||
// IDE preferences dialog (ideMain.c).
|
// IDE preferences dialog (ideMain.c).
|
||||||
|
|
@ -66,26 +72,14 @@ typedef struct {
|
||||||
bool modified; // true = buffer has unsaved changes
|
bool modified; // true = buffer has unsaved changes
|
||||||
} PrjFileT;
|
} PrjFileT;
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Source map entry (for multi-file error reporting)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
int32_t startLine; // 1-based line in concatenated source
|
|
||||||
int32_t lineCount; // lines contributed by this file
|
|
||||||
int32_t fileIdx; // index in PrjStateT.files[]
|
|
||||||
} PrjSourceMapT;
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Project state
|
// Project state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
// Fields are ordered by alignment to minimize struct padding.
|
// Fields are ordered by alignment to minimize struct padding.
|
||||||
PrjFileT *files; // stb_ds dynamic array
|
PrjFileT *files; // stb_ds dynamic array (prjFileCount)
|
||||||
PrjSourceMapT *sourceMap; // stb_ds dynamic array
|
BasSourceMapT *sourceMap; // stb_ds array: path = files[].path, firstFileLine = 1
|
||||||
int32_t fileCount;
|
|
||||||
int32_t sourceMapCount;
|
|
||||||
int32_t activeFileIdx; // index of file open in editor (-1 = none)
|
int32_t activeFileIdx; // index of file open in editor (-1 = none)
|
||||||
char name[PRJ_MAX_NAME];
|
char name[PRJ_MAX_NAME];
|
||||||
char projectPath[DVX_MAX_PATH]; // full path to .dbp file
|
char projectPath[DVX_MAX_PATH]; // full path to .dbp file
|
||||||
|
|
@ -99,6 +93,7 @@ typedef struct {
|
||||||
char description[PRJ_MAX_DESC];
|
char description[PRJ_MAX_DESC];
|
||||||
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
|
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
|
||||||
char helpFile[DVX_MAX_PATH]; // relative path to .hlp file
|
char helpFile[DVX_MAX_PATH]; // relative path to .hlp file
|
||||||
|
char commandArgs[PRJ_MAX_ARGS]; // COMMAND$ for runs from the IDE (not built into the program)
|
||||||
bool optionExplicit; // require DIM before use
|
bool optionExplicit; // require DIM before use
|
||||||
bool dirty;
|
bool dirty;
|
||||||
} PrjStateT;
|
} PrjStateT;
|
||||||
|
|
@ -109,14 +104,30 @@ typedef struct {
|
||||||
|
|
||||||
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
|
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
|
||||||
void prjClose(PrjStateT *prj);
|
void prjClose(PrjStateT *prj);
|
||||||
|
int32_t prjFileCount(const PrjStateT *prj);
|
||||||
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);
|
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);
|
||||||
void prjInit(PrjStateT *prj);
|
void prjInit(PrjStateT *prj);
|
||||||
bool prjLoad(PrjStateT *prj, const char *dbpPath);
|
bool prjLoad(PrjStateT *prj, const char *dbpPath);
|
||||||
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx);
|
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx);
|
||||||
void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
|
void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
|
||||||
|
|
||||||
|
// True when path can be opened for reading.
|
||||||
|
bool prjPathExists(const char *path);
|
||||||
|
|
||||||
|
// Read project file fileIdx from disk as the IDE edits it: carriage
|
||||||
|
// returns removed, NUL-terminated. Returns a malloc'd buffer, or NULL
|
||||||
|
// when the file cannot be read or its text (without CRs) is
|
||||||
|
// PRJ_MAX_SOURCE bytes or larger; *outMissing (may be NULL) then tells
|
||||||
|
// whether it simply does not exist.
|
||||||
|
char *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing);
|
||||||
void prjRemoveFile(PrjStateT *prj, int32_t idx);
|
void prjRemoveFile(PrjStateT *prj, int32_t idx);
|
||||||
bool prjSave(const PrjStateT *prj);
|
bool prjSave(const PrjStateT *prj);
|
||||||
|
|
||||||
|
// Copy src into dst (dstSize bytes) with every CR and LF replaced by a
|
||||||
|
// space: an INI value is one line, so an embedded line break would split
|
||||||
|
// it and start bogus keys or sections.
|
||||||
|
void prjSingleLine(char *dst, int32_t dstSize, const char *src);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Source map -- translate concatenated line to file + local line
|
// Source map -- translate concatenated line to file + local line
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -125,6 +136,10 @@ bool prjSave(const PrjStateT *prj);
|
||||||
// outLocalLine to the originating file and line within that file.
|
// outLocalLine to the originating file and line within that file.
|
||||||
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine);
|
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine);
|
||||||
|
|
||||||
|
// The concatenated line holding line 1 of file fileIdx's code, or 0 when
|
||||||
|
// the file is not in the map (not compiled since it was added).
|
||||||
|
int32_t prjMapStartLine(const PrjStateT *prj, int32_t fileIdx);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Project window UI
|
// Project window UI
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -47,25 +47,35 @@
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define PRP_WIN_W 220
|
#define PRP_WIN_W 220
|
||||||
#define PRP_WIN_H 400
|
#define PRP_WIN_H 400
|
||||||
#define PRP_WIN_RIGHT_MARGIN 10 // gap between the window and the screen edge
|
#define PRP_WIN_RIGHT_MARGIN 10 // gap between the window and the screen edge
|
||||||
#define PRP_WIN_Y 30
|
#define PRP_WIN_Y 30
|
||||||
#define PRP_INT_BUF 32 // decimal int32_t text
|
#define PRP_INT_BUF 32 // decimal int32_t text
|
||||||
#define PRP_QUERY_BUF 512 // SQL probe scratch buffer
|
#define PRP_QUERY_BUF 512 // SQL probe scratch buffer
|
||||||
#define PRP_PROMPT_BUF 128 // input-dialog prompt buffer
|
#define PRP_PROMPT_BUF 128 // input-dialog prompt buffer
|
||||||
#define PRP_TITLE_SUFFIX_PAD 16 // pad over DSGN_MAX_TEXT for " [Design]" suffix
|
#define PRP_TITLE_SUFFIX_PAD 16 // pad over DSGN_MAX_TEXT for " [Design]" suffix
|
||||||
#define DSGN_NO_INDEX (-1) // tree label has no "(idx)" / control not in an array
|
#define PRP_TABLE_CAP_INIT 16 // first capacity of the table-name array
|
||||||
|
#define PRP_CELL_COLUMNS 2 // property grid: name column + value column
|
||||||
|
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types
|
||||||
|
#define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)")
|
||||||
|
#define PRP_SELECT_PREFIX "SELECT " // RecordSource that is already a query
|
||||||
|
#define PRP_SELECT_PREFIX_LEN ((int32_t)(sizeof(PRP_SELECT_PREFIX) - 1))
|
||||||
|
#define PRP_DIALOG_TITLE "Properties"
|
||||||
|
#define PRP_NONE_ITEM "(none)" // dropdown entry that clears a binding
|
||||||
|
#define PRP_DATA_TYPE "Data" // Data control type name
|
||||||
|
#define PRP_ROW_TYPE "Type" // read-only grid row naming the control's type
|
||||||
|
|
||||||
#define PROP_TYPE_STRING WGT_IFACE_STRING
|
// Property grid editor kinds. A property is edited by its WGT_IFACE_*
|
||||||
#define PROP_TYPE_INT WGT_IFACE_INT
|
// type, or by one of these designer-only editors, numbered past the
|
||||||
#define PROP_TYPE_BOOL WGT_IFACE_BOOL
|
// WGT_IFACE_* range so the two never collide.
|
||||||
#define PROP_TYPE_ENUM WGT_IFACE_ENUM
|
typedef enum {
|
||||||
#define PROP_TYPE_READONLY 255
|
PRP_EDIT_READONLY = WGT_IFACE_ENUM + 1,
|
||||||
#define PROP_TYPE_LAYOUT 251
|
PRP_EDIT_LAYOUT,
|
||||||
#define PROP_TYPE_DATASOURCE 254
|
PRP_EDIT_DATASOURCE,
|
||||||
#define PROP_TYPE_DATAFIELD 253
|
PRP_EDIT_DATAFIELD,
|
||||||
#define PROP_TYPE_RECORDSRC 252
|
PRP_EDIT_RECORDSRC
|
||||||
|
} PrpEditE;
|
||||||
|
|
||||||
// No fixed caps: field and table arrays are heap-allocated and sized to the
|
// No fixed caps: field and table arrays are heap-allocated and sized to the
|
||||||
// actual count reported by SQL.
|
// actual count reported by SQL.
|
||||||
|
|
@ -96,29 +106,21 @@ static WidgetT *sPropList = NULL;
|
||||||
static AppContextT *sPrpCtx = NULL;
|
static AppContextT *sPrpCtx = NULL;
|
||||||
static bool sUpdating = false;
|
static bool sUpdating = false;
|
||||||
static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings
|
static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings
|
||||||
|
|
||||||
#define PRP_CELL_COLUMNS 2 // property grid: name column + value column
|
|
||||||
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types
|
|
||||||
#define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)")
|
|
||||||
#define PRP_SELECT_PREFIX "SELECT " // RecordSource that is already a query
|
|
||||||
#define PRP_SELECT_PREFIX_LEN ((int32_t)(sizeof(PRP_SELECT_PREFIX) - 1))
|
|
||||||
#define PRP_DIALOG_TITLE "Properties"
|
|
||||||
|
|
||||||
// Tree parent recorded for a control by collectTreeOrder, parallel to the
|
|
||||||
// collected control array.
|
|
||||||
typedef struct {
|
|
||||||
char name[DSGN_MAX_NAME];
|
|
||||||
} PrpParentT;
|
|
||||||
|
|
||||||
static char **sCellData = NULL; // stb_ds array of strdup'd strings
|
static char **sCellData = NULL; // stb_ds array of strdup'd strings
|
||||||
|
|
||||||
|
// Tree parent (NULL = the form) and block depth recorded for a control by
|
||||||
|
// collectTreeOrder, parallel to the collected control array.
|
||||||
|
typedef struct {
|
||||||
|
DsgnControlT *parent;
|
||||||
|
int32_t depth;
|
||||||
|
} PrpParentT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Prototypes
|
// Prototypes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void addPropRow(const char *name, const char *value);
|
static void addPropRow(const char *name, const char *value);
|
||||||
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth);
|
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, DsgnControlT *treeParent, int32_t depth);
|
||||||
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, const char *parentName);
|
|
||||||
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
|
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
|
||||||
static void freeCellData(void);
|
static void freeCellData(void);
|
||||||
static void freeTreeLabels(void);
|
static void freeTreeLabels(void);
|
||||||
|
|
@ -142,39 +144,11 @@ static void addPropRow(const char *name, const char *value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Recursively apply Visible or Enabled to all descendants of a
|
|
||||||
// container control.
|
|
||||||
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth) {
|
|
||||||
int32_t count = (int32_t)arrlen(ds->form->controls);
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < count; i++) {
|
|
||||||
DsgnControlT *child = ds->form->controls[i];
|
|
||||||
|
|
||||||
if (strcasecmp(child->parentName, parentName) != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child->widget) {
|
|
||||||
wgtSetVisible(child->widget, visible);
|
|
||||||
wgtSetEnabled(child->widget, enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse into nested containers. Guard against a self-parenting
|
|
||||||
// container and runaway cycles (A->B->A) via a depth cap so the
|
|
||||||
// recursion always terminates.
|
|
||||||
if (dsgnIsContainer(child->typeName) &&
|
|
||||||
strcasecmp(child->name, parentName) != 0 &&
|
|
||||||
depth < DSGN_MAX_NEST_DEPTH) {
|
|
||||||
cascadeToChildren(ds, child->name, visible, enabled, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Walk tree items recursively, collecting controls in tree order together
|
// Walk tree items recursively, collecting controls in tree order together
|
||||||
// with the tree parent each one sits under. The controls themselves are
|
// with the tree parent each one sits under and its block depth (1 =
|
||||||
// not modified; the caller decides whether to apply the new parents.
|
// top-level). The controls themselves are not modified; the caller decides
|
||||||
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, const char *parentName) {
|
// whether to apply the new parents.
|
||||||
|
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, DsgnControlT *treeParent, int32_t depth) {
|
||||||
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
|
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
|
||||||
const char *label = (const char *)item->userData;
|
const char *label = (const char *)item->userData;
|
||||||
|
|
||||||
|
|
@ -191,13 +165,14 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src
|
||||||
if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) {
|
if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) {
|
||||||
PrpParentT tp;
|
PrpParentT tp;
|
||||||
|
|
||||||
snprintf(tp.name, DSGN_MAX_NAME, "%s", parentName);
|
tp.parent = treeParent;
|
||||||
|
tp.depth = depth;
|
||||||
arrput(*outArr, srcArr[i]);
|
arrput(*outArr, srcArr[i]);
|
||||||
arrput(*outParents, tp);
|
arrput(*outParents, tp);
|
||||||
|
|
||||||
// Recurse into children (for containers)
|
// Recurse into children (for containers)
|
||||||
if (item->firstChild) {
|
if (item->firstChild) {
|
||||||
collectTreeOrder(item, srcArr, srcCount, outArr, outParents, itemName);
|
collectTreeOrder(item, srcArr, srcCount, outArr, outParents, srcArr[i], depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -279,12 +254,12 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
|
||||||
for (int32_t i = 0; i < ctrlCount; i++) {
|
for (int32_t i = 0; i < ctrlCount; i++) {
|
||||||
DsgnControlT *ctrl = ds->form->controls[i];
|
DsgnControlT *ctrl = ds->form->controls[i];
|
||||||
|
|
||||||
if (strcasecmp(ctrl->typeName, "Data") != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) {
|
if (strcasecmp(ctrl->typeName, PRP_DATA_TYPE) != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbName = dsgnControlGetPropValue(ctrl, "DatabaseName");
|
dbName = dsgnControlGetPropValue(ctrl, "DatabaseName");
|
||||||
recSrc = dsgnControlGetPropValue(ctrl, "RecordSource");
|
recSrc = dsgnControlGetPropValue(ctrl, BAS_PROP_RECORDSOURCE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,11 +288,19 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query with LIMIT 0 to get column names without fetching rows
|
// Query with LIMIT 0 to get column names without fetching rows. A
|
||||||
|
// user query is wrapped as a subquery so its own LIMIT or a trailing
|
||||||
|
// ';' still gives valid SQL.
|
||||||
char query[PRP_QUERY_BUF];
|
char query[PRP_QUERY_BUF];
|
||||||
|
|
||||||
if (strncasecmp(recSrc, PRP_SELECT_PREFIX, PRP_SELECT_PREFIX_LEN) == 0) {
|
if (strncasecmp(recSrc, PRP_SELECT_PREFIX, PRP_SELECT_PREFIX_LEN) == 0) {
|
||||||
snprintf(query, sizeof(query), "%s LIMIT 0", recSrc);
|
int32_t srcLen = (int32_t)strlen(recSrc);
|
||||||
|
|
||||||
|
while (srcLen > 0 && (recSrc[srcLen - 1] == ';' || recSrc[srcLen - 1] == ' ' || recSrc[srcLen - 1] == '\t' || recSrc[srcLen - 1] == '\r' || recSrc[srcLen - 1] == '\n')) {
|
||||||
|
srcLen--;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(query, sizeof(query), "SELECT * FROM (%.*s) LIMIT 0", (int)srcLen, recSrc);
|
||||||
} else {
|
} else {
|
||||||
snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc);
|
snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc);
|
||||||
}
|
}
|
||||||
|
|
@ -367,28 +350,36 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
|
||||||
// (which wins over the runtime tables, matching setProp dispatch), then
|
// (which wins over the runtime tables, matching setProp dispatch), then
|
||||||
// the runtime form/control property tables for the rest.
|
// the runtime form/control property tables for the rest.
|
||||||
static uint8_t getPropType(const char *propName, const char *typeName) {
|
static uint8_t getPropType(const char *propName, const char *typeName) {
|
||||||
// Designer-only read-only rows
|
// Designer rows with a fixed editor: the read-only Type/Index rows,
|
||||||
if (strcasecmp(propName, "Type") == 0) { return PROP_TYPE_READONLY; }
|
// Name and Layout (editable at design time although the runtime
|
||||||
if (strcasecmp(propName, "Index") == 0) { return PROP_TYPE_READONLY; }
|
// rejects assignment), and the data-binding pickers.
|
||||||
|
static const struct {
|
||||||
|
const char *name;
|
||||||
|
uint8_t kind;
|
||||||
|
} fixedKinds[] = {
|
||||||
|
{ PRP_ROW_TYPE, PRP_EDIT_READONLY },
|
||||||
|
{ BAS_PROP_INDEX, PRP_EDIT_READONLY },
|
||||||
|
{ BAS_PROP_NAME, WGT_IFACE_STRING },
|
||||||
|
{ BAS_PROP_LAYOUT, PRP_EDIT_LAYOUT },
|
||||||
|
{ BAS_PROP_DATASOURCE, PRP_EDIT_DATASOURCE },
|
||||||
|
{ BAS_PROP_DATAFIELD, PRP_EDIT_DATAFIELD },
|
||||||
|
{ BAS_PROP_RECORDSOURCE, PRP_EDIT_RECORDSRC },
|
||||||
|
{ BAS_PROP_KEYCOLUMN, PRP_EDIT_DATAFIELD },
|
||||||
|
{ BAS_PROP_MASTERSOURCE, PRP_EDIT_DATASOURCE },
|
||||||
|
{ BAS_PROP_MASTERFIELD, PRP_EDIT_DATAFIELD },
|
||||||
|
{ BAS_PROP_DETAILFIELD, PRP_EDIT_DATAFIELD }
|
||||||
|
};
|
||||||
|
|
||||||
// Designer-editable at design time even though the runtime rejects
|
for (int32_t i = 0; i < DVX_ARRAY_LEN(fixedKinds); i++) {
|
||||||
// assignment (Name is the object identity, Layout the container type).
|
if (strcasecmp(propName, fixedKinds[i].name) == 0) {
|
||||||
if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; }
|
return fixedKinds[i].kind;
|
||||||
if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
|
}
|
||||||
|
}
|
||||||
// Special editors
|
|
||||||
if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
|
|
||||||
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
|
|
||||||
if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; }
|
|
||||||
if (strcasecmp(propName, "KeyColumn") == 0) { return PROP_TYPE_DATAFIELD; }
|
|
||||||
if (strcasecmp(propName, "MasterSource") == 0) { return PROP_TYPE_DATASOURCE; }
|
|
||||||
if (strcasecmp(propName, "MasterField") == 0) { return PROP_TYPE_DATAFIELD; }
|
|
||||||
if (strcasecmp(propName, "DetailField") == 0) { return PROP_TYPE_DATAFIELD; }
|
|
||||||
|
|
||||||
const WgtPropDescT *p = findIfaceProp(typeName, propName);
|
const WgtPropDescT *p = findIfaceProp(typeName, propName);
|
||||||
|
|
||||||
if (p) {
|
if (p) {
|
||||||
return p->setFn ? p->type : PROP_TYPE_READONLY;
|
return p->setFn ? p->type : PRP_EDIT_READONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runtime tables: the form object when no control is selected, else
|
// Runtime tables: the form object when no control is selected, else
|
||||||
|
|
@ -396,12 +387,12 @@ static uint8_t getPropType(const char *propName, const char *typeName) {
|
||||||
const BasPropDescT *pd = typeName[0] ? basFormRtFindCommonProp(propName) : basFormRtFindFormProp(propName);
|
const BasPropDescT *pd = typeName[0] ? basFormRtFindCommonProp(propName) : basFormRtFindFormProp(propName);
|
||||||
|
|
||||||
if (pd) {
|
if (pd) {
|
||||||
return pd->writable ? pd->type : PROP_TYPE_READONLY;
|
return pd->writable ? pd->type : PRP_EDIT_READONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Designer storage fields not in the runtime tables (MinWidth alias
|
// Designer storage fields not in the runtime tables (MinWidth alias
|
||||||
// Width etc. are; this covers any table drift) and unknown keys.
|
// Width etc. are; this covers any table drift) and unknown keys.
|
||||||
return dsgnFindIntProp(propName) ? PROP_TYPE_INT : PROP_TYPE_STRING;
|
return dsgnFindIntProp(propName) ? WGT_IFACE_INT : WGT_IFACE_STRING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -457,7 +448,7 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count >= cap) {
|
if (count >= cap) {
|
||||||
int32_t newCap = cap == 0 ? 16 : cap * 2;
|
int32_t newCap = cap == 0 ? PRP_TABLE_CAP_INIT : cap * 2;
|
||||||
char (*grown)[DSGN_MAX_NAME] = (char (*)[DSGN_MAX_NAME])realloc(names, (size_t)newCap * DSGN_MAX_NAME);
|
char (*grown)[DSGN_MAX_NAME] = (char (*)[DSGN_MAX_NAME])realloc(names, (size_t)newCap * DSGN_MAX_NAME);
|
||||||
|
|
||||||
if (!grown) {
|
if (!grown) {
|
||||||
|
|
@ -495,7 +486,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
const char *curValue = sCellData[row * PRP_CELL_COLUMNS + 1];
|
const char *curValue = sCellData[row * PRP_CELL_COLUMNS + 1];
|
||||||
|
|
||||||
// Layout -- select from discovered layout containers
|
// Layout -- select from discovered layout containers
|
||||||
if (strcasecmp(propName, "Layout") == 0) {
|
if (strcasecmp(propName, BAS_PROP_LAYOUT) == 0) {
|
||||||
// Discover available layout types from loaded widget interfaces.
|
// Discover available layout types from loaded widget interfaces.
|
||||||
// A layout container is isContainer with WGT_CREATE_PARENT (no extra args).
|
// A layout container is isContainer with WGT_CREATE_PARENT (no extra args).
|
||||||
const char *layoutNames[PRP_MAX_LAYOUT_NAMES];
|
const char *layoutNames[PRP_MAX_LAYOUT_NAMES];
|
||||||
|
|
@ -524,12 +515,12 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
// A freshly placed container has no Layout entry yet; create
|
// A freshly placed container has no Layout entry yet; create
|
||||||
// the default so the grid row is editable.
|
// the default so the grid row is editable.
|
||||||
if (!dsgnControlGetPropValue(ctrl, "Layout")) {
|
if (!dsgnControlGetPropValue(ctrl, BAS_PROP_LAYOUT)) {
|
||||||
dsgnSetPropValue(ctrl, "Layout", DSGN_VBOX_LAYOUT);
|
dsgnSetPropValue(ctrl, BAS_PROP_LAYOUT, BAS_LAYOUT_VBOX);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t pi = 0; pi < ctrl->propCount; pi++) {
|
for (int32_t pi = 0; pi < (int32_t)arrlen(ctrl->props); pi++) {
|
||||||
if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) {
|
if (strcasecmp(ctrl->props[pi].name, BAS_PROP_LAYOUT) == 0) {
|
||||||
layoutField = ctrl->props[pi].value;
|
layoutField = ctrl->props[pi].value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -552,7 +543,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
int32_t chosenIdx = 0;
|
int32_t chosenIdx = 0;
|
||||||
|
|
||||||
if (!dvxChoiceDialog(sPrpCtx, "Layout", "Select layout type:", layoutNames, layoutCount, defIdx, &chosenIdx)) {
|
if (!dvxChoiceDialog(sPrpCtx, BAS_PROP_LAYOUT, "Select layout type:", layoutNames, layoutCount, defIdx, &chosenIdx)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,17 +579,17 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
uint8_t propType = getPropType(propName, ctrlTypeName);
|
uint8_t propType = getPropType(propName, ctrlTypeName);
|
||||||
|
|
||||||
if (propType == PROP_TYPE_READONLY) {
|
if (propType == PRP_EDIT_READONLY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
char newValue[DSGN_MAX_TEXT];
|
char newValue[DSGN_MAX_TEXT];
|
||||||
|
|
||||||
if (propType == PROP_TYPE_BOOL) {
|
if (propType == WGT_IFACE_BOOL) {
|
||||||
// Toggle boolean on double-click -- no input box
|
// Toggle boolean on double-click -- no input box
|
||||||
bool cur = frmParseBool(curValue);
|
bool cur = frmParseBool(curValue);
|
||||||
snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
|
snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
|
||||||
} else if (propType == PROP_TYPE_ENUM) {
|
} else if (propType == WGT_IFACE_ENUM) {
|
||||||
// Enum: cycle to next value on double-click
|
// Enum: cycle to next value on double-click
|
||||||
const WgtPropDescT *pd = findIfaceProp(ctrlTypeName, propName);
|
const WgtPropDescT *pd = findIfaceProp(ctrlTypeName, propName);
|
||||||
|
|
||||||
|
|
@ -623,17 +614,17 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
int32_t nextIdx = (curIdx + 1) % enumCount;
|
int32_t nextIdx = (curIdx + 1) % enumCount;
|
||||||
snprintf(newValue, sizeof(newValue), "%s", pd->enumNames[nextIdx]);
|
snprintf(newValue, sizeof(newValue), "%s", pd->enumNames[nextIdx]);
|
||||||
} else if (propType == PROP_TYPE_DATASOURCE) {
|
} else if (propType == PRP_EDIT_DATASOURCE) {
|
||||||
// Show dropdown of Data control names on the form
|
// Show dropdown of Data control names on the form
|
||||||
int32_t formCtrlCount = (int32_t)arrlen(sDs->form->controls);
|
int32_t formCtrlCount = (int32_t)arrlen(sDs->form->controls);
|
||||||
const char *dataNames[PRP_MAX_DATA_NAMES + 1];
|
const char *dataNames[PRP_MAX_DATA_NAMES + 1];
|
||||||
int32_t dataCount = 0;
|
int32_t dataCount = 0;
|
||||||
|
|
||||||
// First entry is "(none)" to clear binding
|
// First entry is "(none)" to clear binding
|
||||||
dataNames[dataCount++] = "(none)";
|
dataNames[dataCount++] = PRP_NONE_ITEM;
|
||||||
|
|
||||||
for (int32_t i = 0; i < formCtrlCount && dataCount < PRP_MAX_DATA_NAMES; i++) {
|
for (int32_t i = 0; i < formCtrlCount && dataCount < PRP_MAX_DATA_NAMES; i++) {
|
||||||
if (strcasecmp(sDs->form->controls[i]->typeName, "Data") == 0) {
|
if (strcasecmp(sDs->form->controls[i]->typeName, PRP_DATA_TYPE) == 0) {
|
||||||
dataNames[dataCount++] = sDs->form->controls[i]->name;
|
dataNames[dataCount++] = sDs->form->controls[i]->name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +645,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
int32_t chosenIdx = 0;
|
int32_t chosenIdx = 0;
|
||||||
|
|
||||||
if (!dvxChoiceDialog(sPrpCtx, "DataSource", "Select Data control:", dataNames, dataCount, defIdx, &chosenIdx)) {
|
if (!dvxChoiceDialog(sPrpCtx, BAS_PROP_DATASOURCE, "Select Data control:", dataNames, dataCount, defIdx, &chosenIdx)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,7 +654,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
} else {
|
} else {
|
||||||
snprintf(newValue, sizeof(newValue), "%s", dataNames[chosenIdx]);
|
snprintf(newValue, sizeof(newValue), "%s", dataNames[chosenIdx]);
|
||||||
}
|
}
|
||||||
} else if (propType == PROP_TYPE_DATAFIELD) {
|
} else if (propType == PRP_EDIT_DATAFIELD) {
|
||||||
// Show dropdown of column names from the Data control's database.
|
// Show dropdown of column names from the Data control's database.
|
||||||
// If the selected control IS a Data control (e.g. editing KeyColumn),
|
// If the selected control IS a Data control (e.g. editing KeyColumn),
|
||||||
// use its own name. Otherwise look up its DataSource property.
|
// use its own name. Otherwise look up its DataSource property.
|
||||||
|
|
@ -673,10 +664,10 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
|
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
|
||||||
DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
|
DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
|
||||||
|
|
||||||
if (strcasecmp(selCtrl->typeName, "Data") == 0) {
|
if (strcasecmp(selCtrl->typeName, PRP_DATA_TYPE) == 0) {
|
||||||
dataSrc = selCtrl->name;
|
dataSrc = selCtrl->name;
|
||||||
} else {
|
} else {
|
||||||
const char *dataSrcProp = dsgnControlGetPropValue(selCtrl, "DataSource");
|
const char *dataSrcProp = dsgnControlGetPropValue(selCtrl, BAS_PROP_DATASOURCE);
|
||||||
|
|
||||||
if (dataSrcProp) {
|
if (dataSrcProp) {
|
||||||
dataSrc = dataSrcProp;
|
dataSrc = dataSrcProp;
|
||||||
|
|
@ -693,7 +684,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (propType == PROP_TYPE_RECORDSRC) {
|
} else if (propType == PRP_EDIT_RECORDSRC) {
|
||||||
// Show dropdown of table names from the Data control's database
|
// Show dropdown of table names from the Data control's database
|
||||||
// Find DatabaseName on this control (which is a Data control)
|
// Find DatabaseName on this control (which is a Data control)
|
||||||
int32_t selCount = (int32_t)arrlen(sDs->form->controls);
|
int32_t selCount = (int32_t)arrlen(sDs->form->controls);
|
||||||
|
|
@ -717,7 +708,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (propType == PROP_TYPE_INT) {
|
} else if (propType == WGT_IFACE_INT) {
|
||||||
// Spinner dialog for integers
|
// Spinner dialog for integers
|
||||||
char prompt[PRP_PROMPT_BUF];
|
char prompt[PRP_PROMPT_BUF];
|
||||||
snprintf(prompt, sizeof(prompt), "%s:", propName);
|
snprintf(prompt, sizeof(prompt), "%s:", propName);
|
||||||
|
|
@ -729,12 +720,20 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
snprintf(newValue, sizeof(newValue), "%d", (int)intVal);
|
snprintf(newValue, sizeof(newValue), "%d", (int)intVal);
|
||||||
} else {
|
} else {
|
||||||
// Text input for strings
|
// Text input for strings. HelpTopic and ContextMenu are stored in
|
||||||
char prompt[PRP_PROMPT_BUF];
|
// name-sized fields, so the input stops at that length instead of
|
||||||
snprintf(prompt, sizeof(prompt), "%s:", propName);
|
// silently truncating.
|
||||||
snprintf(newValue, sizeof(newValue), "%s", curValue);
|
char prompt[PRP_PROMPT_BUF];
|
||||||
|
int32_t maxSize = (int32_t)sizeof(newValue);
|
||||||
|
|
||||||
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) {
|
if (strcasecmp(propName, BAS_PROP_HELPTOPIC) == 0 || strcasecmp(propName, BAS_PROP_CONTEXTMENU) == 0) {
|
||||||
|
maxSize = DSGN_MAX_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(prompt, sizeof(prompt), "%s:", propName);
|
||||||
|
snprintf(newValue, maxSize, "%s", curValue);
|
||||||
|
|
||||||
|
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, maxSize)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -744,7 +743,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) {
|
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) {
|
||||||
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
|
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
|
||||||
|
|
||||||
if (strcasecmp(propName, "Name") == 0) {
|
if (strcasecmp(propName, BAS_PROP_NAME) == 0) {
|
||||||
char oldName[DSGN_MAX_NAME];
|
char oldName[DSGN_MAX_NAME];
|
||||||
snprintf(oldName, sizeof(oldName), "%s", ctrl->name);
|
snprintf(oldName, sizeof(oldName), "%s", ctrl->name);
|
||||||
|
|
||||||
|
|
@ -779,13 +778,13 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
// If this is a Data control, update DataSource and MasterSource
|
// If this is a Data control, update DataSource and MasterSource
|
||||||
// references on all other controls that pointed to the old name
|
// references on all other controls that pointed to the old name
|
||||||
if (strcasecmp(ctrl->typeName, "Data") == 0) {
|
if (strcasecmp(ctrl->typeName, PRP_DATA_TYPE) == 0) {
|
||||||
for (int32_t i = 0; i < count; i++) {
|
for (int32_t i = 0; i < count; i++) {
|
||||||
DsgnControlT *c = sDs->form->controls[i];
|
DsgnControlT *c = sDs->form->controls[i];
|
||||||
|
|
||||||
for (int32_t j = 0; j < c->propCount; j++) {
|
for (int32_t j = 0; j < (int32_t)arrlen(c->props); j++) {
|
||||||
if ((strcasecmp(c->props[j].name, "DataSource") == 0 ||
|
if ((strcasecmp(c->props[j].name, BAS_PROP_DATASOURCE) == 0 ||
|
||||||
strcasecmp(c->props[j].name, "MasterSource") == 0) &&
|
strcasecmp(c->props[j].name, BAS_PROP_MASTERSOURCE) == 0) &&
|
||||||
strcasecmp(c->props[j].value, oldName) == 0) {
|
strcasecmp(c->props[j].value, oldName) == 0) {
|
||||||
snprintf(c->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
|
snprintf(c->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
|
||||||
}
|
}
|
||||||
|
|
@ -800,29 +799,16 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
|
|
||||||
*(int32_t *)((char *)ctrl + ip->offset) = atoi(newValue);
|
*(int32_t *)((char *)ctrl + ip->offset) = atoi(newValue);
|
||||||
dsgnSyncWidgetGeom(ctrl);
|
dsgnSyncWidgetGeom(ctrl);
|
||||||
} else if (strcasecmp(propName, "Visible") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
|
} else if (strcasecmp(propName, BAS_PROP_VISIBLE) == 0 && !dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_VISIBLE)) {
|
||||||
bool val = frmParseBool(newValue);
|
// Stored only: the design widget stays shown (and clickable),
|
||||||
ctrl->visible = val;
|
// as dsgnCreateControlWidget leaves it; the runtime hides it.
|
||||||
|
ctrl->visible = frmParseBool(newValue);
|
||||||
if (ctrl->widget) {
|
} else if (strcasecmp(propName, BAS_PROP_ENABLED) == 0 && !dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_ENABLED)) {
|
||||||
wgtSetVisible(ctrl->widget, val);
|
// Descendants keep their own Enabled; they are drawn disabled
|
||||||
}
|
// only while an ancestor is.
|
||||||
|
ctrl->enabled = frmParseBool(newValue);
|
||||||
if (dsgnIsContainer(ctrl->typeName)) {
|
dsgnApplyEnabled(sDs->form);
|
||||||
cascadeToChildren(sDs, ctrl->name, val, ctrl->enabled, 0);
|
} else if (strcasecmp(propName, BAS_PROP_HELPTOPIC) == 0) {
|
||||||
}
|
|
||||||
} else if (strcasecmp(propName, "Enabled") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
|
|
||||||
bool val = frmParseBool(newValue);
|
|
||||||
ctrl->enabled = val;
|
|
||||||
|
|
||||||
if (ctrl->widget) {
|
|
||||||
wgtSetEnabled(ctrl->widget, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dsgnIsContainer(ctrl->typeName)) {
|
|
||||||
cascadeToChildren(sDs, ctrl->name, ctrl->visible, val, 0);
|
|
||||||
}
|
|
||||||
} else if (strcasecmp(propName, "HelpTopic") == 0) {
|
|
||||||
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
|
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
|
||||||
} else {
|
} else {
|
||||||
// Try widget iface setter first
|
// Try widget iface setter first
|
||||||
|
|
@ -840,8 +826,9 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
if (p && p->setFn) {
|
if (p && p->setFn) {
|
||||||
// props[] is the design-time store (it survives a
|
// props[] is the design-time store (it survives a
|
||||||
// widget rebuild and is what gets saved); the live
|
// widget rebuild and is what gets saved); the live
|
||||||
// widget mirrors it. Strings are passed from the
|
// widget mirrors it. A setter must copy the
|
||||||
// props[] copy so they outlive this function.
|
// string: props[] is an stb_ds array that any
|
||||||
|
// later dsgnSetPropValue may reallocate.
|
||||||
dsgnSetPropValue(ctrl, propName, newValue);
|
dsgnSetPropValue(ctrl, propName, newValue);
|
||||||
|
|
||||||
const char *stored = dsgnControlGetPropValue(ctrl, propName);
|
const char *stored = dsgnControlGetPropValue(ctrl, propName);
|
||||||
|
|
@ -861,7 +848,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
dsgnSetPropValue(ctrl, propName, newValue);
|
dsgnSetPropValue(ctrl, propName, newValue);
|
||||||
|
|
||||||
// Update widget text from the persistent props array
|
// Update widget text from the persistent props array
|
||||||
if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) {
|
if (ctrl->widget && (strcasecmp(propName, BAS_PROP_CAPTION) == 0 || strcasecmp(propName, BAS_PROP_TEXT) == 0)) {
|
||||||
const char *stored = dsgnControlGetPropValue(ctrl, propName);
|
const char *stored = dsgnControlGetPropValue(ctrl, propName);
|
||||||
|
|
||||||
if (stored) {
|
if (stored) {
|
||||||
|
|
@ -869,6 +856,8 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dsgnOptionSelected(sDs->form, ctrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
sDs->form->dirty = true;
|
sDs->form->dirty = true;
|
||||||
|
|
@ -877,7 +866,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
|
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (strcasecmp(propName, "Name") == 0) {
|
if (strcasecmp(propName, BAS_PROP_NAME) == 0) {
|
||||||
char oldName[BAS_MAX_IDENT];
|
char oldName[BAS_MAX_IDENT];
|
||||||
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
|
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
|
||||||
|
|
||||||
|
|
@ -888,7 +877,7 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
snprintf(sDs->form->name, sizeof(sDs->form->name), "%s", newValue);
|
snprintf(sDs->form->name, sizeof(sDs->form->name), "%s", newValue);
|
||||||
ideRenameInCode(oldName, sDs->form->name);
|
ideRenameInCode(oldName, sDs->form->name);
|
||||||
prpRebuildTree(sDs);
|
prpRebuildTree(sDs);
|
||||||
} else if (strcasecmp(propName, "Caption") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_CAPTION) == 0) {
|
||||||
snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue);
|
snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue);
|
||||||
|
|
||||||
if (sDs->formWin) {
|
if (sDs->formWin) {
|
||||||
|
|
@ -896,35 +885,31 @@ static void onPropDblClick(WidgetT *w) {
|
||||||
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption);
|
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption);
|
||||||
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
|
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
|
||||||
}
|
}
|
||||||
} else if (strcasecmp(propName, "AutoSize") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_AUTOSIZE) == 0) {
|
||||||
sDs->form->autoSize = frmParseBool(newValue);
|
sDs->form->autoSize = frmParseBool(newValue);
|
||||||
|
} else if (strcasecmp(propName, BAS_PROP_RESIZABLE) == 0) {
|
||||||
if (sDs->form->autoSize && sDs->formWin) {
|
|
||||||
dvxFitWindow(sPrpCtx, sDs->formWin);
|
|
||||||
sDs->form->width = sDs->formWin->w;
|
|
||||||
sDs->form->height = sDs->formWin->h;
|
|
||||||
}
|
|
||||||
} else if (strcasecmp(propName, "Resizable") == 0) {
|
|
||||||
sDs->form->resizable = frmParseBool(newValue);
|
sDs->form->resizable = frmParseBool(newValue);
|
||||||
|
|
||||||
if (sDs->formWin) {
|
if (sDs->formWin) {
|
||||||
sDs->formWin->resizable = sDs->form->resizable;
|
sDs->formWin->resizable = sDs->form->resizable;
|
||||||
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
|
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
|
||||||
}
|
}
|
||||||
} else if (strcasecmp(propName, "Centered") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_CENTERED) == 0) {
|
||||||
sDs->form->centered = frmParseBool(newValue);
|
sDs->form->centered = frmParseBool(newValue);
|
||||||
} else if (strcasecmp(propName, "Left") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_LEFT) == 0) {
|
||||||
sDs->form->left = atoi(newValue);
|
sDs->form->left = atoi(newValue);
|
||||||
} else if (strcasecmp(propName, "Top") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_TOP) == 0) {
|
||||||
sDs->form->top = atoi(newValue);
|
sDs->form->top = atoi(newValue);
|
||||||
} else if (strcasecmp(propName, "Width") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_WIDTH) == 0) {
|
||||||
sDs->form->width = atoi(newValue);
|
sDs->form->width = atoi(newValue);
|
||||||
sDs->form->autoSize = false;
|
sDs->form->autoSize = false;
|
||||||
} else if (strcasecmp(propName, "Height") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_HEIGHT) == 0) {
|
||||||
sDs->form->height = atoi(newValue);
|
sDs->form->height = atoi(newValue);
|
||||||
sDs->form->autoSize = false;
|
sDs->form->autoSize = false;
|
||||||
} else if (strcasecmp(propName, "HelpTopic") == 0) {
|
} else if (strcasecmp(propName, BAS_PROP_HELPTOPIC) == 0) {
|
||||||
snprintf(sDs->form->helpTopic, DSGN_MAX_NAME, "%s", newValue);
|
snprintf(sDs->form->helpTopic, DSGN_MAX_NAME, "%s", newValue);
|
||||||
|
} else if (strcasecmp(propName, BAS_PROP_CONTEXTMENU) == 0) {
|
||||||
|
snprintf(sDs->form->contextMenu, DSGN_MAX_NAME, "%s", newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
sDs->form->dirty = true;
|
sDs->form->dirty = true;
|
||||||
|
|
@ -973,29 +958,18 @@ static void onTreeChange(WidgetT *w) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
|
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, NULL, 1);
|
||||||
|
|
||||||
// Revert if items were lost (dragged above the form) or a control was
|
// Revert if items were lost (dragged above the form), a control was
|
||||||
// dropped into something that cannot hold children: only containers
|
// dropped into something that cannot hold children (the runtime would
|
||||||
// are written with nested blocks, so a non-container parent would drop
|
// place it elsewhere), or the drop nests deeper than the .frm parser
|
||||||
// the control from the saved .frm.
|
// accepts.
|
||||||
bool valid = ((int32_t)arrlen(newArr) == count);
|
bool valid = ((int32_t)arrlen(newArr) == count);
|
||||||
|
|
||||||
for (int32_t i = 0; valid && i < count; i++) {
|
for (int32_t i = 0; valid && i < count; i++) {
|
||||||
const char *pName = newParents[i].name;
|
const DsgnControlT *parent = newParents[i].parent;
|
||||||
|
|
||||||
if (pName[0] == '\0') {
|
valid = newParents[i].depth <= DSGN_MAX_NEST_DEPTH && (!parent || dsgnIsContainer(parent->typeName));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
valid = false;
|
|
||||||
|
|
||||||
for (int32_t j = 0; j < count; j++) {
|
|
||||||
if (strcasecmp(newArr[j]->name, pName) == 0) {
|
|
||||||
valid = dsgnIsContainer(newArr[j]->typeName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
|
@ -1006,7 +980,19 @@ static void onTreeChange(WidgetT *w) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < count; i++) {
|
for (int32_t i = 0; i < count; i++) {
|
||||||
snprintf(newArr[i]->parentName, DSGN_MAX_NAME, "%s", newParents[i].name);
|
const DsgnControlT *parent = newParents[i].parent;
|
||||||
|
|
||||||
|
snprintf(newArr[i]->parentName, DSGN_MAX_NAME, "%s", parent ? parent->name : "");
|
||||||
|
newArr[i]->parentIndex = parent ? parent->index : DSGN_NO_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the same control selected at its new position.
|
||||||
|
DsgnControlT *selCtrl = (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) ? sDs->form->controls[sDs->selectedIdx] : NULL;
|
||||||
|
|
||||||
|
for (int32_t i = 0; selCtrl && i < count; i++) {
|
||||||
|
if (newArr[i] == selCtrl) {
|
||||||
|
sDs->selectedIdx = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
arrfree(newParents);
|
arrfree(newParents);
|
||||||
|
|
@ -1117,7 +1103,7 @@ static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const ch
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ptrs[0] = "(none)";
|
ptrs[0] = PRP_NONE_ITEM;
|
||||||
|
|
||||||
for (int32_t i = 0; i < count; i++) {
|
for (int32_t i = 0; i < count; i++) {
|
||||||
ptrs[i + 1] = names[i];
|
ptrs[i + 1] = names[i];
|
||||||
|
|
@ -1219,7 +1205,8 @@ void prpRebuildTree(DsgnStateT *ds) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sUpdating = true;
|
sUpdating = true;
|
||||||
|
ds->treeStale = false;
|
||||||
|
|
||||||
freeTreeLabels();
|
freeTreeLabels();
|
||||||
wgtDestroyChildren(sTree);
|
wgtDestroyChildren(sTree);
|
||||||
|
|
@ -1256,15 +1243,15 @@ void prpRebuildTree(DsgnStateT *ds) {
|
||||||
char *label = strdup(buf);
|
char *label = strdup(buf);
|
||||||
arrput(sTreeLabels, label);
|
arrput(sTreeLabels, label);
|
||||||
|
|
||||||
// Find the tree parent: form item or a container's tree item
|
// Find the tree parent: form item or the parent control's tree item
|
||||||
WidgetT *treeParent = formItem;
|
// (controls[] lists a parent before its children).
|
||||||
|
WidgetT *treeParent = formItem;
|
||||||
|
const DsgnControlT *parent = dsgnFindParent(ds->form, ctrl);
|
||||||
|
|
||||||
if (ctrl->parentName[0]) {
|
for (int32_t j = 0; parent && j < i; j++) {
|
||||||
for (int32_t j = 0; j < i; j++) {
|
if (ds->form->controls[j] == parent) {
|
||||||
if (strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
|
treeParent = treeItems[j];
|
||||||
treeParent = treeItems[j];
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1293,6 +1280,13 @@ void prpRefresh(DsgnStateT *ds) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A canvas reorder or placement changed controls[]; rebuild the tree
|
||||||
|
// first, or its next selection click would read the stale order as a
|
||||||
|
// tree drag and undo the change.
|
||||||
|
if (ds->treeStale) {
|
||||||
|
prpRebuildTree(ds);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync tree selection to match selectedIdx
|
// Sync tree selection to match selectedIdx
|
||||||
if (sTree && !sUpdating) {
|
if (sTree && !sUpdating) {
|
||||||
WidgetT *formItem = sTree->firstChild;
|
WidgetT *formItem = sTree->firstChild;
|
||||||
|
|
@ -1327,14 +1321,14 @@ void prpRefresh(DsgnStateT *ds) {
|
||||||
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
||||||
char buf[PRP_INT_BUF];
|
char buf[PRP_INT_BUF];
|
||||||
|
|
||||||
addPropRow("Name", ctrl->name);
|
addPropRow(BAS_PROP_NAME, ctrl->name);
|
||||||
|
|
||||||
if (ctrl->index >= 0) {
|
if (ctrl->index >= 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d", (int)ctrl->index);
|
snprintf(buf, sizeof(buf), "%d", (int)ctrl->index);
|
||||||
addPropRow("Index", buf);
|
addPropRow(BAS_PROP_INDEX, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
addPropRow("Type", ctrl->typeName);
|
addPropRow(PRP_ROW_TYPE, ctrl->typeName);
|
||||||
|
|
||||||
for (int32_t i = 0; dsgnIntPropAt(i); i++) {
|
for (int32_t i = 0; dsgnIntPropAt(i); i++) {
|
||||||
const DsgnIntPropT *ip = dsgnIntPropAt(i);
|
const DsgnIntPropT *ip = dsgnIntPropAt(i);
|
||||||
|
|
@ -1343,24 +1337,24 @@ void prpRefresh(DsgnStateT *ds) {
|
||||||
addPropRow(ip->name, buf);
|
addPropRow(ip->name, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
|
if (!dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_VISIBLE)) {
|
||||||
addPropRow("Visible", ctrl->visible ? "True" : "False");
|
addPropRow(BAS_PROP_VISIBLE, ctrl->visible ? "True" : "False");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
|
if (!dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_ENABLED)) {
|
||||||
addPropRow("Enabled", ctrl->enabled ? "True" : "False");
|
addPropRow(BAS_PROP_ENABLED, ctrl->enabled ? "True" : "False");
|
||||||
}
|
}
|
||||||
|
|
||||||
addPropRow("HelpTopic", ctrl->helpTopic);
|
addPropRow(BAS_PROP_HELPTOPIC, ctrl->helpTopic);
|
||||||
|
|
||||||
for (int32_t i = 0; i < ctrl->propCount; i++) {
|
for (int32_t i = 0; i < (int32_t)arrlen(ctrl->props); i++) {
|
||||||
addPropRow(ctrl->props[i].name, ctrl->props[i].value);
|
addPropRow(ctrl->props[i].name, ctrl->props[i].value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A container placed on the canvas has no Layout entry until the
|
// A container placed on the canvas has no Layout entry until the
|
||||||
// user picks one; show the default so the row is there to edit.
|
// user picks one; show the default so the row is there to edit.
|
||||||
if (dsgnIsContainer(ctrl->typeName) && !dsgnControlGetPropValue(ctrl, "Layout")) {
|
if (dsgnIsContainer(ctrl->typeName) && !dsgnControlGetPropValue(ctrl, BAS_PROP_LAYOUT)) {
|
||||||
addPropRow("Layout", DSGN_VBOX_LAYOUT);
|
addPropRow(BAS_PROP_LAYOUT, BAS_LAYOUT_VBOX);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget interface properties (from the .wgt descriptor)
|
// Widget interface properties (from the .wgt descriptor)
|
||||||
|
|
@ -1381,7 +1375,7 @@ void prpRefresh(DsgnStateT *ds) {
|
||||||
// Skip if already shown as a custom prop
|
// Skip if already shown as a custom prop
|
||||||
bool already = false;
|
bool already = false;
|
||||||
|
|
||||||
for (int32_t j = 0; j < ctrl->propCount; j++) {
|
for (int32_t j = 0; j < (int32_t)arrlen(ctrl->props); j++) {
|
||||||
if (strcasecmp(ctrl->props[j].name, p->name) == 0) {
|
if (strcasecmp(ctrl->props[j].name, p->name) == 0) {
|
||||||
already = true;
|
already = true;
|
||||||
break;
|
break;
|
||||||
|
|
@ -1408,8 +1402,8 @@ void prpRefresh(DsgnStateT *ds) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// One row per runtime form property the designer stores, in
|
// One row per runtime form property the designer stores, in
|
||||||
// table order; runtime-only ones (Visible, ContextMenu) have no
|
// table order; the runtime-only Visible has no designer field and
|
||||||
// designer field and are skipped.
|
// is skipped.
|
||||||
int32_t formPropCount = 0;
|
int32_t formPropCount = 0;
|
||||||
const BasPropDescT *formProps = basFormRtFormProps(&formPropCount);
|
const BasPropDescT *formProps = basFormRtFormProps(&formPropCount);
|
||||||
|
|
||||||
|
|
@ -1457,15 +1451,15 @@ static bool treeOrderMatches(void) {
|
||||||
PrpParentT *newParents = NULL;
|
PrpParentT *newParents = NULL;
|
||||||
|
|
||||||
// Order is detected via pointer identity (tree DFS order vs model
|
// Order is detected via pointer identity (tree DFS order vs model
|
||||||
// order); a reparent shows up as a differing tree parent name.
|
// order); a reparent shows up as a differing tree parent.
|
||||||
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
|
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, NULL, 1);
|
||||||
|
|
||||||
bool match = ((int32_t)arrlen(newArr) == count);
|
bool match = ((int32_t)arrlen(newArr) == count);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
for (int32_t i = 0; i < count; i++) {
|
for (int32_t i = 0; i < count; i++) {
|
||||||
if (newArr[i] != sDs->form->controls[i] ||
|
if (newArr[i] != sDs->form->controls[i] ||
|
||||||
strcasecmp(sDs->form->controls[i]->parentName, newParents[i].name) != 0) {
|
dsgnFindParent(sDs->form, sDs->form->controls[i]) != newParents[i].parent) {
|
||||||
match = false;
|
match = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,9 @@ typedef struct {
|
||||||
// Module state
|
// Module state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static DsgnStateT *sDs = NULL;
|
static DsgnStateT *sDs = NULL;
|
||||||
static TbxToolEntryT *sTbxTools = NULL; // stb_ds dynamic array
|
static TbxToolEntryT *sTbxTools = NULL; // stb_ds dynamic array
|
||||||
|
static bool sBuilding = false; // tbxCreate is pumping events between buttons
|
||||||
|
|
||||||
// Function prototypes
|
// Function prototypes
|
||||||
static void onTbxClose(WindowT *win);
|
static void onTbxClose(WindowT *win);
|
||||||
|
|
@ -104,6 +105,12 @@ static void onToolClick(WidgetT *w) {
|
||||||
|
|
||||||
|
|
||||||
WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
|
WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
|
||||||
|
// The icon loop pumps events; a second toolbox request arriving then
|
||||||
|
// (Window menu) must not reset the tool table the first one is filling.
|
||||||
|
if (sBuilding) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
sDs = ds;
|
sDs = ds;
|
||||||
arrsetlen(sTbxTools, 0);
|
arrsetlen(sTbxTools, 0);
|
||||||
|
|
||||||
|
|
@ -124,11 +131,15 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
|
||||||
WidgetT *row = NULL;
|
WidgetT *row = NULL;
|
||||||
WidgetT **buttons = NULL; // stb_ds array, parallel to sTbxTools
|
WidgetT **buttons = NULL; // stb_ds array, parallel to sTbxTools
|
||||||
|
|
||||||
for (int32_t i = 0; i < ifaceCount; i++) {
|
sBuilding = true;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < ifaceCount && ctx->running; i++) {
|
||||||
const char *wgtName = NULL;
|
const char *wgtName = NULL;
|
||||||
const WgtIfaceT *iface = wgtIfaceAt(i, &wgtName);
|
const WgtIfaceT *iface = wgtIfaceAt(i, &wgtName);
|
||||||
|
|
||||||
if (!iface || !iface->basName || !iface->basName[0]) {
|
// Types that need pixel data at creation (Image, ImageButton)
|
||||||
|
// cannot be placed at design time.
|
||||||
|
if (!iface || !iface->basName || !iface->basName[0] || iface->createSig == WGT_CREATE_PARENT_DATA) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +195,12 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
|
||||||
btn = wgtButton(row, iface->basName);
|
btn = wgtButton(row, iface->basName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Out of memory (wgtImageButton frees iconData itself): drop the tool.
|
||||||
|
if (!btn) {
|
||||||
|
arrsetlen(sTbxTools, toolIdx);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
btn->onClick = onToolClick;
|
btn->onClick = onToolClick;
|
||||||
btn->userData = (void *)(intptr_t)toolIdx;
|
btn->userData = (void *)(intptr_t)toolIdx;
|
||||||
arrput(buttons, btn);
|
arrput(buttons, btn);
|
||||||
|
|
@ -194,6 +211,8 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
|
||||||
dvxUpdate(ctx);
|
dvxUpdate(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sBuilding = false;
|
||||||
|
|
||||||
// Set tooltips after array is finalized (arrput can reallocate)
|
// Set tooltips after array is finalized (arrput can reallocate)
|
||||||
int32_t btnCount = (int32_t)arrlen(buttons);
|
int32_t btnCount = (int32_t)arrlen(buttons);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,11 +101,11 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
|
||||||
New Project... Create a new DVX BASIC project.
|
New Project... Create a new DVX BASIC project.
|
||||||
Open Project... Open an existing .dbp project file.
|
Open Project... Open an existing .dbp project file.
|
||||||
Save Project Save the current project file to disk.
|
Save Project Save the current project file to disk.
|
||||||
Close Project Close the current project (prompts to save unsaved changes).
|
Close Project Close the current project (prompts to save unsaved changes; Don't Save also discards unsaved Project Properties changes).
|
||||||
---
|
---
|
||||||
Project Properties... Edit project metadata (name, author, version, startup form, icon, etc.).
|
Project Properties... Edit project metadata (name, author, version, startup form, icon, etc.) and the command line arguments used when the program is run from the IDE.
|
||||||
---
|
---
|
||||||
Add File... Ctrl+O Add a .bas or .frm file to the project. If no project is open, creates an implicit project from the file.
|
Add File... Ctrl+O Add a .bas or .frm file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message.
|
||||||
Save File Ctrl+S Save the active file to disk.
|
Save File Ctrl+S Save the active file to disk.
|
||||||
Save All Save all modified files in the project.
|
Save All Save all modified files in the project.
|
||||||
---
|
---
|
||||||
|
|
@ -116,6 +116,10 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
|
||||||
Exit Close the IDE (prompts to save unsaved changes).
|
Exit Close the IDE (prompts to save unsaved changes).
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
A project (.dbp) or source file (.bas, .frm) named on the IDE's command line opens when the IDE starts, so double-clicking one in the File Manager, or starting it from the Program Manager, opens it in the IDE.
|
||||||
|
|
||||||
|
Files are saved exactly as edited (LF line endings). Each save writes a temporary file first and replaces the original only when the write succeeded. Opening a project that fails to load keeps the current project open and removes the failed entry from the Recent list.
|
||||||
|
|
||||||
.link ide.menu.edit Edit Menu
|
.link ide.menu.edit Edit Menu
|
||||||
.link ide.overview Back to Overview
|
.link ide.overview Back to Overview
|
||||||
|
|
||||||
|
|
@ -137,19 +141,21 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
|
||||||
.table
|
.table
|
||||||
Menu Item Shortcut Description
|
Menu Item Shortcut Description
|
||||||
--------- -------- -----------
|
--------- -------- -----------
|
||||||
Cut Ctrl+X Cut selected text to the clipboard.
|
Cut Ctrl+X Cut the selection (text, or the selected control in the form designer) to the clipboard.
|
||||||
Copy Ctrl+C Copy selected text to the clipboard.
|
Copy Ctrl+C Copy the selection to the clipboard.
|
||||||
Paste Ctrl+V Paste text from the clipboard.
|
Paste Ctrl+V Paste from the clipboard. A pasted control keeps all of its properties.
|
||||||
---
|
---
|
||||||
Select All Ctrl+A Select all text in the active editor.
|
Select All Ctrl+A Select all text in the active editor.
|
||||||
---
|
---
|
||||||
Delete Del Delete the selected text or control (in the form designer).
|
Delete Del Delete the selected text, or the selected control and everything inside it (in the form designer).
|
||||||
---
|
---
|
||||||
Find... Ctrl+F Open the Find/Replace dialog.
|
Find... Ctrl+F Open the Find/Replace dialog.
|
||||||
Find Next F3 Find the next occurrence of the search text.
|
Find Next F3 Find the next occurrence of the search text (also after the dialog is closed).
|
||||||
Replace... Ctrl+H Open the Find/Replace dialog with replace enabled.
|
Replace... Ctrl+H Open the Find/Replace dialog with replace enabled.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
Cut, Copy, Paste, Select All and Delete act on the window you last worked in: the form designer's selected control, or the text of the code, output or immediate window.
|
||||||
|
|
||||||
.link ide.menu.run Run Menu
|
.link ide.menu.run Run Menu
|
||||||
.link ide.overview Back to Overview
|
.link ide.overview Back to Overview
|
||||||
|
|
||||||
|
|
@ -177,14 +183,14 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
|
||||||
Run F5 Compile and run the program. If paused at a breakpoint, resumes execution with debugging disabled (runs free).
|
Run F5 Compile and run the program. If paused at a breakpoint, resumes execution with debugging disabled (runs free).
|
||||||
Debug Shift+F5 Compile and run with the debugger active. Breakpoints are active but execution does not pause at the first statement. If paused, resumes to the next breakpoint.
|
Debug Shift+F5 Compile and run with the debugger active. Breakpoints are active but execution does not pause at the first statement. If paused, resumes to the next breakpoint.
|
||||||
Run Without Recompile Ctrl+F5 Re-run the last compiled module without recompiling.
|
Run Without Recompile Ctrl+F5 Re-run the last compiled module without recompiling.
|
||||||
Stop Esc Stop the running program immediately.
|
Stop Esc Stop the running program. Run, Debug and the project commands stay disabled until the program has finished shutting down.
|
||||||
---
|
---
|
||||||
Step Into F8 Execute one statement, stepping into SUB/FUNCTION calls. If idle, starts a debug session and breaks at the first statement.
|
Step Into F8 Execute one statement, stepping into SUB/FUNCTION calls. If idle, starts a debug session and breaks at the first statement.
|
||||||
Step Over Shift+F8 Execute one statement, stepping over SUB/FUNCTION calls.
|
Step Over Shift+F8 Execute one statement, stepping over SUB/FUNCTION calls.
|
||||||
Step Out Ctrl+Shift+F8 Run until the current SUB/FUNCTION returns.
|
Step Out Ctrl+Shift+F8 Run until the current SUB/FUNCTION returns.
|
||||||
Run to Cursor Ctrl+F8 Run until execution reaches the line where the cursor is positioned.
|
Run to Cursor Ctrl+F8 Run until execution reaches the line where the cursor is positioned.
|
||||||
---
|
---
|
||||||
Output Window to Log Checkbox: when enabled, PRINT output is also mirrored to the DVX log. Persisted in preferences.
|
Output Window to Log Checkbox: when enabled, PRINT output (each line once) and compile and runtime error reports are also written to the DVX log. Persisted in preferences.
|
||||||
---
|
---
|
||||||
Toggle Breakpoint F9 Toggle a breakpoint on the current editor line.
|
Toggle Breakpoint F9 Toggle a breakpoint on the current editor line.
|
||||||
---
|
---
|
||||||
|
|
@ -212,12 +218,12 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
|
||||||
Menu Item Shortcut Description
|
Menu Item Shortcut Description
|
||||||
--------- -------- -----------
|
--------- -------- -----------
|
||||||
Code F7 Switch to Code view for the active file (or the file selected in the Project Explorer).
|
Code F7 Switch to Code view for the active file (or the file selected in the Project Explorer).
|
||||||
Designer Shift+F7 Switch to Design view for the active form file.
|
Designer Shift+F7 Switch to Design view for the active form file. With a module (.bas) active it reports that the file is not a form. The designer keeps its unsaved state while you work in a module.
|
||||||
---
|
---
|
||||||
Toolbar Checkbox: show or hide the toolbar. Persisted in preferences.
|
Toolbar Checkbox: show or hide the toolbar. Persisted in preferences.
|
||||||
Status Bar Checkbox: show or hide the status bar. Persisted in preferences.
|
Status Bar Checkbox: show or hide the status bar. Persisted in preferences.
|
||||||
---
|
---
|
||||||
Menu Editor... Ctrl+E Open the Menu Editor dialog for the active form. Allows designing menu bars with captions, names, levels, checked state, and enabled state.
|
Menu Editor... Ctrl+E Open the Menu Editor dialog for the active form (a form must be active). Allows designing menu bars with captions, names, levels, checked state, and enabled state. See Menu Editor below.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.link ide.menu.window Window Menu
|
.link ide.menu.window Window Menu
|
||||||
|
|
@ -363,6 +369,8 @@ At the top of the Code Editor are two dropdown lists:
|
||||||
|
|
||||||
The editor shows one procedure at a time. Each procedure has its own buffer, and switching between them is instantaneous. The (General) section contains module-level declarations and code.
|
The editor shows one procedure at a time. Each procedure has its own buffer, and switching between them is instantaneous. The (General) section contains module-level declarations and code.
|
||||||
|
|
||||||
|
Text outside the procedure being shown is never lost: lines typed above or below the SUB move to (General), a second SUB typed into the buffer becomes a procedure of its own, and a procedure whose SUB line was deleted becomes (General) text. When you leave an event handler that was generated and never edited it is removed; empty SUBs and FUNCTIONs you wrote yourself are kept. After a save the editor shows exactly what was stored (blank lines around a procedure are normalized). A section too large for the editor opens read-only, with a message in the status bar.
|
||||||
|
|
||||||
.h2 Syntax Highlighting
|
.h2 Syntax Highlighting
|
||||||
|
|
||||||
The editor applies real-time syntax coloring as you type. Seven categories are highlighted in distinct colors; the colors themselves are editable in Tools > Preferences > Colors.
|
The editor applies real-time syntax coloring as you type. Seven categories are highlighted in distinct colors; the colors themselves are editable in Tools > Preferences > Colors.
|
||||||
|
|
@ -411,15 +419,21 @@ The Form Designer provides a visual design surface for editing .frm files. Switc
|
||||||
.item Grid snapping -- controls snap to an 8-pixel grid when placed or resized.
|
.item Grid snapping -- controls snap to an 8-pixel grid when placed or resized.
|
||||||
.item Selection -- click a control to select it. The selected control is highlighted with grab handles.
|
.item Selection -- click a control to select it. The selected control is highlighted with grab handles.
|
||||||
.item Grab handles -- 6x6 pixel handles appear on the right edge (E), bottom edge (S), and bottom-right corner (SE) of the selected control. Drag a handle to resize the control.
|
.item Grab handles -- 6x6 pixel handles appear on the right edge (E), bottom edge (S), and bottom-right corner (SE) of the selected control. Drag a handle to resize the control.
|
||||||
.item Reordering -- drag a control vertically to reorder it within the form's layout (VBox/HBox).
|
.item Reordering -- drag a control to reorder it among its siblings in the layout (VBox/HBox). A container moves together with its contents.
|
||||||
.item Placing controls -- select a control type in the Toolbox, then click on the form to place a new instance. The control is auto-named (e.g., Command1, Command2). Clicking the same tool again deselects it (toggles back to pointer mode).
|
.item Placing controls -- select a control type in the Toolbox, then click on the form to place a new instance. The control is auto-named (e.g., Command1, Command2), skipping names already used by a control, a menu item or the form. Clicking the same tool again deselects it (toggles back to pointer mode). Controls nest at most 15 levels deep; a drop into a container that is already that deep goes into its parent container.
|
||||||
.item Menu bar preview -- if the form has menu items (defined via the Menu Editor), a preview menu bar is rendered on the design window.
|
.item Menu bar preview -- if the form has menu items (defined via the Menu Editor), a preview menu bar is rendered on the design window.
|
||||||
.item Delete key -- removes the selected control from the form.
|
.item Delete key -- removes the selected control and everything inside it.
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
|
Opening a form in the designer does not mark it modified. A control whose Visible property is False is still shown on the design surface (the setting applies when the program runs). Disabling a container greys the controls inside it; a control's own Enabled = False is kept when the container is enabled again. A .frm file without an AutoSize line opens without AutoSize; AutoSize = True wins over Width and Height lines. Text values (Caption, Text and so on) are limited to 379 characters.
|
||||||
|
|
||||||
|
.h2 Menu Editor
|
||||||
|
|
||||||
|
The Menu Editor (View > Menu Editor, Ctrl+E) edits the active form's menu bar and popup menus. Menus nest at most 15 levels (0-14); indenting refuses when any item below would go deeper. OK rejects an item nested more than one level below the item above it, and a blank item that has sub-items (it must be named); blank items without sub-items are dropped. The window's close box cancels. Renaming a menu item renames its event handlers in the code and updates the ContextMenu setting of the form and its controls; inserting, deleting and moving items does not rename anything.
|
||||||
|
|
||||||
.h2 Form Properties
|
.h2 Form Properties
|
||||||
|
|
||||||
Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, and Resizable.
|
Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, Resizable, HelpTopic and ContextMenu.
|
||||||
|
|
||||||
.link ide.properties Properties Panel
|
.link ide.properties Properties Panel
|
||||||
.link ide.toolbox Toolbox
|
.link ide.toolbox Toolbox
|
||||||
|
|
@ -443,7 +457,9 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi
|
||||||
.list
|
.list
|
||||||
.item Name -- the project display name (up to 32 characters).
|
.item Name -- the project display name (up to 32 characters).
|
||||||
.item Startup Form -- which form to show automatically when the program starts.
|
.item Startup Form -- which form to show automatically when the program starts.
|
||||||
.item Metadata -- Author, Company, Version, Copyright, Description, and Icon Path (for compiled binaries).
|
.item Metadata -- Author, Company, Version, Copyright, Description (a single line), and Icon Path (for compiled binaries).
|
||||||
|
.item Arguments -- the command line the program gets (COMMAND$) when it is run from the IDE, for trying out a program that opens the file it is given. It is not built into the compiled program: there COMMAND$ returns whatever the program was started with.
|
||||||
|
.item Help File -- a compiled help file (.hlp) for the program. It is copied next to the compiled program, and F1 in the running program opens it, at the HelpTopic of the focused control or, failing that, of the form. Add help.bas to the project and call HelpView to open it from a Help menu.
|
||||||
.item File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.
|
.item File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
|
|
@ -457,8 +473,9 @@ Before the source is handed to the compiler, the IDE scans every .frm file and c
|
||||||
|
|
||||||
.list
|
.list
|
||||||
.item Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).
|
.item Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).
|
||||||
.item Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.
|
.item Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name. A control name that is used with different types on different forms is not checked.
|
||||||
.item Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.
|
.item Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.
|
||||||
|
.item Menu items -- only Name (read), Checked and Enabled can be used on a menu item in code; Caption, Visible and RadioCheck are design-time settings.
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.
|
These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.
|
||||||
|
|
@ -469,12 +486,12 @@ These checks use the same metadata the Properties panel and Toolbox read from ea
|
||||||
Operation Description
|
Operation Description
|
||||||
--------- -----------
|
--------- -----------
|
||||||
New Project Creates a blank project with a name and directory. A default .frm file is added automatically.
|
New Project Creates a blank project with a name and directory. A default .frm file is added automatically.
|
||||||
Open Project Opens a .dbp file and loads all referenced files into memory.
|
Open Project Opens a .dbp file and loads all referenced files into memory. A project file that is 64 KB or larger, or cannot be read, is not loaded; opening it later reports "Cannot load <file>: unreadable or larger than 65535 bytes." Carriage returns are removed when files are loaded.
|
||||||
Save Project Writes the .dbp file to disk.
|
Save Project Writes the .dbp file to disk.
|
||||||
Close Project Closes the project, prompting to save unsaved changes.
|
Close Project Closes the project, prompting to save unsaved changes.
|
||||||
Add File Adds a .bas or .frm file to the project. Opening a file without a project auto-creates an implicit project.
|
Add File Adds a .bas or .frm file to the project. Opening a file without a project auto-creates an implicit project.
|
||||||
Remove File Removes a file from the project (prompts to save if modified).
|
Remove File Removes a file from the project (prompts to save if modified). Removing the startup form clears the Startup Form setting.
|
||||||
Project Properties Opens a dialog for editing project metadata (name, author, company, version, copyright, description, icon, startup form).
|
Project Properties Opens a dialog for editing project metadata (name, author, company, version, copyright, description, icon, help file, startup form) and the Arguments for runs from the IDE.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.h2 Project Explorer
|
.h2 Project Explorer
|
||||||
|
|
@ -533,8 +550,6 @@ Click a tool to select it (the active tool name is stored in the designer state)
|
||||||
ComboBox Drop-down combo box.
|
ComboBox Drop-down combo box.
|
||||||
DropDown Simple drop-down list.
|
DropDown Simple drop-down list.
|
||||||
PictureBox Canvas for drawing and images.
|
PictureBox Canvas for drawing and images.
|
||||||
Image Static image display.
|
|
||||||
ImageButton Clickable image button.
|
|
||||||
Frame Grouping container with a labeled border.
|
Frame Grouping container with a labeled border.
|
||||||
VBox Vertical layout container.
|
VBox Vertical layout container.
|
||||||
HBox Horizontal layout container.
|
HBox Horizontal layout container.
|
||||||
|
|
@ -557,6 +572,8 @@ Click a tool to select it (the active tool name is stored in the designer state)
|
||||||
DBGrid Data-bound grid for displaying database query results.
|
DBGrid Data-bound grid for displaying database query results.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
Image and ImageButton controls are not in the Toolbox; they can still be written into a .frm file by hand and are then edited in the Properties panel like any other control.
|
||||||
|
|
||||||
.link ide.designer Form Designer
|
.link ide.designer Form Designer
|
||||||
.link ide.overview Back to Overview
|
.link ide.overview Back to Overview
|
||||||
|
|
||||||
|
|
@ -621,7 +638,7 @@ Each breakpoint records the file it belongs to, the line number within that file
|
||||||
|
|
||||||
.h3 Breakpoint Adjustment on Edit
|
.h3 Breakpoint Adjustment on Edit
|
||||||
|
|
||||||
When lines are added or removed in the editor, breakpoints below the edit point are automatically shifted to stay on the correct line.
|
Breakpoints follow their statements when lines are added, removed, joined, split or pasted. A breakpoint whose line is deleted is removed.
|
||||||
|
|
||||||
.h2 Stepping
|
.h2 Stepping
|
||||||
|
|
||||||
|
|
@ -641,7 +658,7 @@ When a program is running under the debugger:
|
||||||
.list
|
.list
|
||||||
.item The program runs in short cooperative slices so the IDE remains responsive.
|
.item The program runs in short cooperative slices so the IDE remains responsive.
|
||||||
.item When the program hits a breakpoint, execution pauses immediately. The IDE switches to Code View, navigates to the breakpoint line, highlights it in yellow, and opens the Locals and Call Stack windows if they are not already visible.
|
.item When the program hits a breakpoint, execution pauses immediately. The IDE switches to Code View, navigates to the breakpoint line, highlights it in yellow, and opens the Locals and Call Stack windows if they are not already visible.
|
||||||
.item While paused, you can inspect variables in Locals and Watch, evaluate expressions in the Immediate window, assign new values to variables, toggle breakpoints, step, continue, or stop.
|
.item While paused, you can inspect variables in Locals and Watch, evaluate expressions in the Immediate window, assign new values to variables, toggle breakpoints, step, continue, or stop. A pause in module-level code works the same way as a pause in an event handler.
|
||||||
.item Resuming (F5, Shift+F5, or any Step command) returns the program to Running state.
|
.item Resuming (F5, Shift+F5, or any Step command) returns the program to Running state.
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
|
|
@ -679,12 +696,12 @@ Shows variables for the current execution scope. Displayed as a three-column Lis
|
||||||
The Locals window displays:
|
The Locals window displays:
|
||||||
|
|
||||||
.list
|
.list
|
||||||
.item Local variables for the current procedure (matched by proc index).
|
.item Local variables of the current procedure, including its STATIC variables and the module variables it names in a SHARED statement.
|
||||||
.item Global (module-level) variables.
|
.item Module-level variables. While execution is paused inside a SUB or FUNCTION, only the variables declared with DIM SHARED (or REDIM SHARED) are listed, because the procedure cannot see the others. This also applies inside a DEF FN function, although a DEF FN can read every module variable.
|
||||||
.item Form-scoped variables for the current form (if the program is executing within a form context).
|
.item Form-scoped variables for the current form (if the program is executing within a form context), with the same SHARED rule.
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
Up to 64 variables are displayed. The window is resizable.
|
The Watch window and the Immediate window follow the same rules. The window is resizable.
|
||||||
|
|
||||||
.link ide.debug.callstack Call Stack Window
|
.link ide.debug.callstack Call Stack Window
|
||||||
.link ide.overview Back to Overview
|
.link ide.overview Back to Overview
|
||||||
|
|
@ -789,7 +806,7 @@ Lists all set breakpoints as a three-column ListView:
|
||||||
|
|
||||||
.h1 Immediate Window
|
.h1 Immediate Window
|
||||||
|
|
||||||
The Immediate window is an interactive REPL at the bottom-right of the screen. Type a line of BASIC and press Enter to evaluate it. Results appear inline below your input.
|
The Immediate window is an interactive REPL at the bottom-right of the screen. Type a line of BASIC and press Enter to evaluate it. Results appear inline below your input. Enter evaluates the line it is pressed on (a line split by Enter is joined back first); Enter on an empty line does nothing. A statement that does not finish (for example DO: LOOP) is stopped with "Error: Statement did not finish; stopped."
|
||||||
|
|
||||||
.h2 Expression Evaluation
|
.h2 Expression Evaluation
|
||||||
|
|
||||||
|
|
@ -826,7 +843,7 @@ LET variableName = newValue
|
||||||
Assignment works for:
|
Assignment works for:
|
||||||
|
|
||||||
.list
|
.list
|
||||||
.item Scalar variables -- x = 42, name$ = "test"
|
.item Scalar variables -- x = 42, name$ = "test", count = count + 1 (any expression is allowed on the right)
|
||||||
.item Array elements -- arr(5) = 100, matrix(2, 3) = 7.5
|
.item Array elements -- arr(5) = 100, matrix(2, 3) = 7.5
|
||||||
.item UDT fields -- player.score = 1000
|
.item UDT fields -- player.score = 1000
|
||||||
.item Combined -- items(0).price = 9.99
|
.item Combined -- items(0).price = 9.99
|
||||||
|
|
@ -856,7 +873,7 @@ The Output window is a read-only TextArea at the bottom-left of the screen. It d
|
||||||
.item Compile errors -- if compilation fails, the error message and location are shown.
|
.item Compile errors -- if compilation fails, the error message and location are shown.
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
The output buffer holds up to 32,768 characters. Use Run > Clear Output to clear it.
|
The output buffer holds up to 32,768 characters; when it is full the oldest lines are dropped. The window always shows the newest output. Use Run > Clear Output to clear it.
|
||||||
|
|
||||||
INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.
|
INPUT statements prompt the user via a modal InputBox dialog; the prompt text is also echoed to the Output window.
|
||||||
|
|
||||||
|
|
@ -891,7 +908,7 @@ Open with Ctrl+F (Find) or Ctrl+H (Replace). The Find/Replace dialog is modeless
|
||||||
.table
|
.table
|
||||||
Button Action
|
Button Action
|
||||||
------ ------
|
------ ------
|
||||||
Find Next Find the next occurrence. Wraps across procedures, files, and the entire project depending on the scope setting.
|
Find Next Find the next occurrence. Function scope searches the current procedure, Object scope the selected object's procedures, File scope the current file and Project scope every file. At the end of the current procedure the search wraps around to its start.
|
||||||
Replace Replace the current match and find the next one.
|
Replace Replace the current match and find the next one.
|
||||||
Replace All Replace all occurrences within the selected scope.
|
Replace All Replace all occurrences within the selected scope.
|
||||||
Close Close the dialog.
|
Close Close the dialog.
|
||||||
|
|
@ -899,7 +916,7 @@ Open with Ctrl+F (Find) or Ctrl+H (Replace). The Find/Replace dialog is modeless
|
||||||
|
|
||||||
.h2 Keyboard Shortcut
|
.h2 Keyboard Shortcut
|
||||||
|
|
||||||
F3 repeats the last search (Find Next) without opening the dialog.
|
F3 repeats the last search (Find Next) without opening the dialog, also after the dialog was closed (then searching the whole project, ignoring case). Scope and direction chosen with the keyboard take effect like mouse clicks.
|
||||||
|
|
||||||
.link ide.overview Back to Overview
|
.link ide.overview Back to Overview
|
||||||
|
|
||||||
|
|
@ -939,7 +956,7 @@ These fields set the default values for new project metadata:
|
||||||
Publisher Default publisher/company name. (empty)
|
Publisher Default publisher/company name. (empty)
|
||||||
Version Default version string. 1.0
|
Version Default version string. 1.0
|
||||||
Copyright Default copyright notice. (empty)
|
Copyright Default copyright notice. (empty)
|
||||||
Description Default project description (multi-line). (empty)
|
Description Default project description (one line). (empty)
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.h2 Colors Tab
|
.h2 Colors Tab
|
||||||
|
|
@ -985,6 +1002,140 @@ Customizes the syntax-highlighting colors used in the code editor. The left side
|
||||||
Del Delete
|
Del Delete
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
.link ide.changes Changes in This Release
|
||||||
|
.link ide.overview Back to Overview
|
||||||
|
|
||||||
|
.topic ide.changes
|
||||||
|
.title Changes in This Release
|
||||||
|
.toc 0 Changes in This Release
|
||||||
|
.index Changes
|
||||||
|
.index Release Notes
|
||||||
|
.index What's New
|
||||||
|
.index Migration
|
||||||
|
|
||||||
|
.h1 Changes in This Release
|
||||||
|
|
||||||
|
This release fixes a large number of problems in the language, the runtime, the controls and the IDE. Most programs keep working unchanged. The one change that needs attention in existing programs is variable scope: see Module Variables Need SHARED below.
|
||||||
|
|
||||||
|
.h2 Module Variables Need SHARED
|
||||||
|
|
||||||
|
DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable declared at module level (including the code section of a form) only when it is declared with DIM SHARED, or named in a SHARED statement inside the procedure. Before, every module variable was visible everywhere.
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item In an existing program, change DIM to DIM SHARED for every module-level variable that a SUB, FUNCTION or event handler uses.
|
||||||
|
.item Without that change such a name silently becomes a local variable of the procedure (or a compile error with OPTION EXPLICIT, or when the procedure indexes or dots it).
|
||||||
|
.item New: the SHARED statement inside a procedure, and REDIM SHARED at module level. DIM SHARED inside a procedure is a compile error.
|
||||||
|
.item CONSTs, TYPEs, procedures, forms and controls are visible everywhere as before; a DEF FN still sees every module variable.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.link lang.declarations See: Variable Scope and SHARED
|
||||||
|
|
||||||
|
.h2 Language
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item GOSUB and RETURN work inside SUB and FUNCTION: each procedure call has its own GOSUB stack. RETURN with no pending GOSUB in the procedure raises error 3 (RETURN without GOSUB); before, RETURN inside a procedure left the procedure (use EXIT SUB / EXIT FUNCTION for that).
|
||||||
|
.item Labels belong to their SUB or FUNCTION (or to the module level). GOTO, GOSUB, ON GOTO/GOSUB and ON ERROR GOTO reach only labels of the same procedure; the same label name can be used in several procedures, and a label may share a name with a variable. An undefined label, or a jump to a SUB or variable name, is a compile error. "SubName:" at the start of a line is an error instead of a silent label.
|
||||||
|
.item A CONST inside a SUB or FUNCTION is local and no longer overrides a module constant. CONST name AS type converts the literal (Integer and Long round and range-check); a string/number mismatch or an overflow is a compile error.
|
||||||
|
.item DEF FN must be defined before it is used; its free names are module variables.
|
||||||
|
.item DEFINT, DEFLNG and the other DEFtype statements also apply to parameters, STATIC variables and FUNCTION results. A typed FUNCTION converts the value assigned to it (FUNCTION F AS INTEGER with F = 2.5 returns 2). STATIC variables convert on store and their names no longer leak out of the procedure.
|
||||||
|
.item DIM a, b AS STRING, c(3) declares several variables in one statement.
|
||||||
|
.item name(...) in an expression only calls FUNCTIONs; a SUB there, or indexing a scalar or CONST, is a compile error.
|
||||||
|
.item A FUNCTION can be called as a statement (F 3), discarding its result.
|
||||||
|
.item DECLARE must match the definition's BYVAL and OPTIONAL flags too.
|
||||||
|
.item SWAP and GET #n, , arr(i) evaluate each subscript exactly once. A BYREF argument written as an expression such as arr(i) + 1 is passed by value.
|
||||||
|
.item Numeric literals: an Integer/Long suffix on a literal with a fraction or exponent rounds (1E3% is 1000); -32768% and -2147483648& are accepted; 1. is a valid literal; an over-long literal is an error.
|
||||||
|
.item DATA: unquoted items are raw text up to the next comma, colon, comment or end of line.
|
||||||
|
.item READ, INPUT, INPUT # and LINE INPUT # accept array elements and TYPE fields; PRINT #n, alone writes an empty line.
|
||||||
|
.item PRINT USING: ; and , both separate values, and a trailing ; or , suppresses the newline.
|
||||||
|
.item More than 8 array dimensions, or an LBOUND/UBOUND dimension outside 1..8, is a compile error.
|
||||||
|
.item A missing END SUB, END FUNCTION, END TYPE or END DECLARE is a compile error.
|
||||||
|
.item FOR: a constant fractional STEP with an Integer or Long counter is a compile error, and a computed STEP is rounded to the counter's type. A fractional STEP no longer turns a Single counter into a Double.
|
||||||
|
.item DECLARE LIBRARY arguments are always passed by value, converted to the declared parameter type (a Single parameter receives a Single).
|
||||||
|
.item Name(idx).Member works with any member name (including keywords such as Name and Text), with method calls and in chains; Me.Method (x) is a method call.
|
||||||
|
.item Compile errors read "Line N, Col C: message".
|
||||||
|
.item New: COMMAND$ returns the arguments the program was started with (for example the document it was opened with), and COMMAND$(n) returns the nth argument without its quotes. The DVX programs that open documents (Notepad, Image Viewer, Icon Editor, Resource Editor, Help Editor) open the file named there when they start. In the IDE, set the arguments under Project Properties. A program that uses COMMAND$ does not run on an older DVX.
|
||||||
|
.item A SUB or FUNCTION may use at most 64 local slots (parameters, local variables and hidden temporaries). More is a compile error "Too many local variables in X (n, limit is 64)" instead of a runtime error 9; READ and INPUT # into many array elements no longer use a slot per target.
|
||||||
|
.item SHARED naming a variable that does not exist at module level creates it there; in a form's code it becomes a variable of that form, not a program-wide one.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.h2 Runtime
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Nested BYREF parameters reach the original variable or array element.
|
||||||
|
.item ON ERROR is per procedure. A procedure called from a handler can trap its own errors, returning from a procedure that only installed a handler leaves the caller's ERR alone, and an error inside a running handler passes to the caller's handler.
|
||||||
|
.item RESUME NEXT after an error in an IF/ELSEIF/WHILE/DO/LOOP condition skips the block or leaves the loop; after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false and the next value or CASE is tried; after an error in an ON selector no branch is taken.
|
||||||
|
.item New error numbers: 3 RETURN without GOSUB, 14 Out of string space, 28 Out of stack space (was reported as 7), 63 Bad record number, 91 Object variable not set. ERR is a Long, and ERROR n reports the standard message for known numbers.
|
||||||
|
.item Using a property or method (or Show, Hide, Unload) on a value that is not a form or control raises error 91 instead of doing nothing.
|
||||||
|
.item EXP, ^ and other math that leaves the numeric range raise error 6 (Overflow); an undefined result raises error 5.
|
||||||
|
.item INT, FIX and SGN return an Integer (Long or Double when needed); RND returns a Double and follows QBasic (RND(0) repeats the last number, a negative argument reseeds). RANDOMIZE with a negative or very large seed, and RND with a very large negative argument, seed predictably (the seed is taken modulo 2^32), so the sequence for a negative RANDOMIZE seed may differ from before. -x of the most negative Integer or Long widens like 0 - x.
|
||||||
|
.item INSTR follows OPTION COMPARE TEXT. INSTR and the MID$ statement reject a start below 1 (error 5); MID$ with a length of 0 replaces nothing.
|
||||||
|
.item PRINT # writes a space after numbers, as PRINT does. PRINT USING and FORMAT$ show a value too wide for the field in scientific notation.
|
||||||
|
.item SEEK on a RANDOM file works in records (statement and function); a position below 1 is error 63. PUT of a value longer than the record length raises error 59 before writing anything.
|
||||||
|
.item SLEEP keeps the desktop and the program's forms running, SLEEP 0 just processes events, and closing the program ends the sleep.
|
||||||
|
.item DIR$ returns only files. GETATTR and SETATTR on a missing file raise error 53; SETATTR raises 75 when it cannot change the attributes.
|
||||||
|
.item IniRead and IniWrite use the same INI reader as the DVX system settings. IniWrite keeps the text of every other line (of any length) unchanged, ends every line of the saved file with CR LF, and raises error 75 when it cannot read the existing file or write the new one (a write failure used to be ignored); a failed write leaves the old file intact. IniRead no longer finds keys in the tail of a long line, and keys above the first section header are read with section "". Section names are matched with surrounding blanks ignored.
|
||||||
|
.item VAL keeps the magnitude of very long digit strings. Running out of memory while building a string raises error 14.
|
||||||
|
.item REDIM PRESERVE of a TYPE array keeps the existing elements.
|
||||||
|
.item Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl or Shell, s is still read before x runs (left to right).
|
||||||
|
.item Programs compiled with an older release must be rebuilt. A program whose procedure table records more than 64 locals for a procedure is refused when it is loaded.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.h2 Forms and Controls
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item A reference to a removed control or to a control of an unloaded form raises an "unknown control" runtime error. CreateControl with a parent that is not a container on the same form raises "CreateControl: invalid parent".
|
||||||
|
.item SetEvent works on forms (Me) and menu items; a handler without parameters is called without the event's arguments.
|
||||||
|
.item Form.Visible = True/False behaves exactly like Show/Hide, but only on a real change: Visible = True on a form that is already shown does nothing (a modal form stays modal), and on a form that has no window it is ignored. MinWidth and MinHeight read back the value that was set. Size properties are limited to 0..32767.
|
||||||
|
.item Disabling a container (for example Frame.Enabled = False) blocks mouse and keyboard input to every control inside it (also inside a disabled ScrollPane or Splitter), and SetFocus on such a control does nothing.
|
||||||
|
.item OptionButton.Value is a read/write Boolean (True = selected); the button's index is the new read-only OptionIndex. Setting Value raises no Click.
|
||||||
|
.item HScrollBar has Min and Max properties. SpinButton's RealMode uses the range set with SetRange. PictureBox has read-only ImageWidth and ImageHeight.
|
||||||
|
.item ListBox SetItems "" empties the list, and every | separates items. ListView SetColumns has no length limit. TreeView Clear resets scrolling. TextArea FindNext finds a match that starts at the cursor, and the new SetCursor method moves the caret without selecting.
|
||||||
|
.item TabStrip/TabControl: a negative tab index selects the first tab and an index past the end the last one.
|
||||||
|
.item The mouse wheel over a DBGrid scrolls the rows without moving the current record, and over a Terminal scrolls its history locally.
|
||||||
|
.item .frm files: End followed by blanks or a comment (also End'comment) closes its block while End followed by a word does not, in the compiler, the build and the release renamer exactly as in the loader, Begin followed by a tab is accepted, Enabled = False on popup-menu items is applied, properties after a nested child's End apply to the parent, and Layout on a control that is not a container is reported.
|
||||||
|
.item Include libraries: sql.bas, comm.bas and commdlg.bas take and return Long (SQLFieldInt and SQLAffected no longer wrap above 32767); resource.bas string functions are declared AS STRING. CommHandshake takes an optional passphrase, and the new CommSetPlain accepts unencrypted data on a channel.
|
||||||
|
.item commdlg.bas functions are renamed to match the other include libraries: basFileOpen is now DlgFileOpen$, basFileSave is DlgFileSave$, basInputBox2 is DlgInputBox$, basInputCancelled is DlgInputCancelled, basChoiceDialog is DlgChoice, basIntInput is DlgIntInput and basPromptSave is DlgPromptSave. The old names are gone; programs that use them must be updated and rebuilt.
|
||||||
|
.item New in comm.bas: CommRecvEncrypted(handle, channel) returns True when every byte the last CommRecv$ on that channel returned arrived encrypted and authenticated (also when it returned nothing), and False when any arrived unencrypted (before CommHandshake or on a CommSetPlain channel).
|
||||||
|
.item KeyDown: Shift, Ctrl or Alt with F1-F12, and Alt with Home, End, PgUp, PgDn, Ins, Del and the arrow keys, report the plain key's KeyCode with the modifier in Shift, even when the modifier is released before the program sees the key.
|
||||||
|
.item Data control: while its Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh and Update on the same Data control are ignored (they used to validate the same record again without end); Delete is allowed. Only unloading the Data control itself from Validate or Reposition stops the operation that fired it; removing other controls does not.
|
||||||
|
.item Data control: writing a field back with the value it already has is not an edit, so leaving a bound control no longer fires Validate. With a SELECT query as RecordSource, edits are dropped when the current record changes. Update and Delete still find a record whose key is a number stored without a declared column type, or is NULL, when the key names exactly one record; Delete keeps the record shown when the database row could not be deleted.
|
||||||
|
.item DBGrid re-sorts and re-measures its columns when the Data control's records change (Refresh, AddNew, Delete or an edit), keeping the chosen sort column, and not on a plain record move.
|
||||||
|
.item Image and ImageButton: Picture = "" clears the picture (Picture then reads back ""); a file that cannot be loaded leaves the picture and Picture unchanged. A disabled Image keeps its transparent color transparent.
|
||||||
|
.item Timer: a Timer whose handler shows a modal dialog no longer fires a second time from inside that dialog.
|
||||||
|
.item TextArea: the horizontal scroll bar appears as soon as the longest line fills the visible width, and the caret stays visible while typing at the right edge.
|
||||||
|
.item Terminal: pasting with Ctrl+V returns the view to the live screen and sends the text in small pieces.
|
||||||
|
.item ListBox and ListView: changing the items while an item is being dragged ends the drag.
|
||||||
|
.item TabControl: switching tabs closes an open drop-down list only when it belongs to a control on the page being hidden. A Label on a Toolbar is drawn on the toolbar's color.
|
||||||
|
.item Compile errors from the command-line compiler name the file and line. A .frm without a Begin Form line is an error. A release build that runs out of memory while renaming names fails instead of producing a broken program.
|
||||||
|
.item A form shown modal has no minimize gadget.
|
||||||
|
.item Menu items accept only Name (read-only), Checked and Enabled in code; Caption, Visible and RadioCheck are design-time settings, and using them in code is a compile error in the IDE.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.h2 IDE
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Immediate window: Enter evaluates the line it is pressed on, assignments accept any expression, and a statement that does not finish is stopped.
|
||||||
|
.item Output window: always shows the newest output and drops the oldest lines when full; Output Window to Log also logs errors.
|
||||||
|
.item Find/Replace: Object and File scopes search only that object or file; F3 works after the dialog is closed.
|
||||||
|
.item Edit commands act on the window last worked in (designer control or text window). A pasted control keeps all its properties.
|
||||||
|
.item The code editor never drops text outside the shown procedure; only untouched generated event handlers are removed.
|
||||||
|
.item Files are saved exactly as written, with LF line endings, through a temporary file (for FORM1.FRM, FORM1.$FR, with the old file kept as FORM1.~FR until the save is complete; your own name.BAK is never touched); carriage returns are removed when files are loaded. Don't Save discards Project Properties changes too. A project file may hold up to 64 KB of text not counting carriage returns; a file of twice that size or more is refused without being read.
|
||||||
|
.item Debugger: while paused inside a SUB or FUNCTION, Locals and Watch show only the variables the procedure can see (its locals, its STATIC and SHARED names, and DIM SHARED module variables), and STATIC variables are now listed.
|
||||||
|
.item Compile-time checks of control properties and methods use the control on the form whose code is being compiled, so a control name used for different control types on different forms is checked too.
|
||||||
|
.item Form designer: Image and ImageButton are no longer in the Toolbox, controls nest at most 15 levels, Delete removes a control with its contents, AutoSize matches the runtime, TabStrip TabIndex and the form's ContextMenu are kept on save, control arrays inside containers no longer duplicate controls, and a control keeps any number of design-time properties.
|
||||||
|
.item Error and debugger line numbers in a form's code match the editor exactly (the indentation of the first code line is kept).
|
||||||
|
.item Menu Editor: at most 15 levels, the close box cancels, and only real renames rewrite event handlers.
|
||||||
|
.item Project Properties: Description is a single line.
|
||||||
|
.item A Sub or Function whose name already exists (typed or pasted) stays its own procedure, and the compiler reports the duplicate; before, it was moved into (General).
|
||||||
|
.item Breakpoints and the caret follow lines that the editor moves when it tidies a procedure (blank lines after End Sub, text typed outside a Sub, a deleted Sub line); a breakpoint whose line disappears is removed.
|
||||||
|
.item Immediate window: output leaves room for typing, and Enter in a completely full window evaluates nothing and says so in the status bar.
|
||||||
|
.item Debugger: a breakpoint reached while a stopped program is unwinding no longer pauses it.
|
||||||
|
.item The title bar's * also shows for a form changed in the designer but not yet saved.
|
||||||
|
.item Form designer: a property value may be as long as a full .frm line (511 characters); a value too long for the line it is saved on (deeply nested controls) stops the save with "A property value is too long to save at its position in the form". Making a container part of a control array keeps its contents. Only one OptionButton per container keeps Value = True: setting it on one (in the property grid or by pasting a selected one) clears the others.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.link ide.overview Back to Overview
|
||||||
|
|
||||||
.hr
|
.hr
|
||||||
|
|
||||||
DVX BASIC 1.0 -- Copyright 2026 Scott Duensing
|
DVX BASIC 1.0 -- Copyright 2026 Scott Duensing
|
||||||
|
|
|
||||||
|
|
@ -94,13 +94,15 @@ name$ = "Hello" ' String
|
||||||
Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent forms of a scientific-notation double).
|
Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent forms of a scientific-notation double).
|
||||||
.endnote
|
.endnote
|
||||||
|
|
||||||
|
A literal with an Integer or Long suffix and a fraction or exponent is rounded (1E3% is 1000, 2.5% is 2). The most negative values -32768% and -2147483648& are valid. A trailing decimal point is allowed (1. is the Double 1). A numeric literal too long to read is a compile error.
|
||||||
|
|
||||||
.h2 Type Promotion
|
.h2 Type Promotion
|
||||||
|
|
||||||
When mixing types in expressions, values are automatically promoted to a common type: Integer -> Long -> Single -> Double. Strings are not automatically converted to numbers (use VAL and STR$).
|
When mixing types in expressions, values are automatically promoted to a common type: Integer -> Long -> Single -> Double. + joins two strings; when a string meets a number in an arithmetic operator (including +) the string is converted as VAL does ("1" + 2 is 3, "abc" + 2 is 2). Use & to join text and numbers, and VAL / STR$ to convert explicitly.
|
||||||
|
|
||||||
.h2 Assignment Conversion
|
.h2 Assignment Conversion
|
||||||
|
|
||||||
Storing a value into a variable declared as Integer, Long, Single or Boolean (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, TYPE field or array element) converts the value to that type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.
|
Storing a value into a variable declared as Integer, Long, Single or Boolean (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Dim n As Integer
|
Dim n As Integer
|
||||||
|
|
@ -283,14 +285,16 @@ DIM variable() AS type
|
||||||
DIM variable AS UdtName
|
DIM variable AS UdtName
|
||||||
DIM variable AS STRING * n
|
DIM variable AS STRING * n
|
||||||
DIM SHARED variable AS type
|
DIM SHARED variable AS type
|
||||||
|
DIM variable1 [AS type], variable2 [AS type], ...
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
An array declared with bounds is a fixed array. DIM variable() with empty parentheses declares a dynamic array that has no storage until a REDIM gives it bounds; using it before then raises error 13. An array that is first created by REDIM, and an array parameter, are dynamic too (see ERASE for the difference).
|
An array declared with bounds is a fixed array. DIM variable() with empty parentheses declares a dynamic array that has no storage until a REDIM gives it bounds; using it before then raises error 13. An array that is first created by REDIM, and an array parameter, are dynamic too (see ERASE for the difference).
|
||||||
|
|
||||||
Multiple variables can be declared at module level so their values persist between procedure calls. Arrays can have up to 8 dimensions. A variable declared with type suffix does not need the AS clause:
|
One DIM can declare several variables, separated by commas. Each one takes its own AS clause; a name without one gets its suffix or DEFtype type (Single by default), so DIM a, b AS STRING makes only b a String. Arrays can have up to 8 dimensions; more is a compile error. A variable declared with type suffix does not need the AS clause:
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Dim name$, count%, total& ' suffixes infer the types
|
Dim name$, count%, total& ' suffixes infer the types
|
||||||
|
Dim first, last As String, list(10) As Integer
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
@ -306,11 +310,47 @@ Dim fixedStr As String * 20
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
.note info
|
.note info
|
||||||
DIM SHARED at module level makes a variable accessible from every procedure without passing it as a parameter. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A variable that is first used inside a SUB or FUNCTION without a DIM is also local to that procedure; to share it with module-level code or other procedures, declare it with DIM SHARED at module level.
|
A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables".
|
||||||
.endnote
|
.endnote
|
||||||
|
|
||||||
Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
|
Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
|
||||||
|
|
||||||
|
.h2 Variable Scope and SHARED
|
||||||
|
|
||||||
|
DVX BASIC follows the QBasic scope rules:
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Variables declared (or first used) at module level -- outside every SUB and FUNCTION -- are module variables. The code section of a form (.frm) is module-level code of that form.
|
||||||
|
.item A SUB or FUNCTION sees a module variable only when it was declared with DIM SHARED at module level, or when the procedure names it in a SHARED statement. This applies to arrays as well.
|
||||||
|
.item Any other name used inside a SUB or FUNCTION is a local variable of that procedure, even when a module variable of the same name exists. With OPTION EXPLICIT, using such a name without a DIM is a compile error that says the module variable is not SHARED.
|
||||||
|
.item Indexing (name(...)) or dotting (name.member) a module variable the procedure cannot see is a compile error.
|
||||||
|
.item CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, forms and controls are visible everywhere; no SHARED is needed for them.
|
||||||
|
.item A DEF FN is module-level code: its expression sees every module variable.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
.code
|
||||||
|
DIM SHARED total AS LONG ' visible in every SUB and FUNCTION
|
||||||
|
DIM count AS INTEGER ' module code only
|
||||||
|
|
||||||
|
SUB AddUp(n AS INTEGER)
|
||||||
|
total = total + n ' the module variable
|
||||||
|
count = 1 ' a local variable, not the module's count
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
SUB ShowCount
|
||||||
|
SHARED count ' this procedure uses the module's count
|
||||||
|
PRINT count
|
||||||
|
END SUB
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
DIM SHARED and REDIM SHARED are only allowed at module level. Inside a procedure use the SHARED statement:
|
||||||
|
|
||||||
|
.code
|
||||||
|
SHARED variable [()] [AS type] [, variable [()] [AS type] ...]
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
SHARED gives the procedure access to the named module variables (write name() for an array). The AS type, when given, must match the module variable's declaration. Naming a variable that does not exist at module level creates it there (in a form's code, as a variable of that form).
|
||||||
|
|
||||||
.h2 REDIM
|
.h2 REDIM
|
||||||
|
|
||||||
Reallocates an array, optionally preserving existing data. The array may have been declared with DIM name() or with bounds, or not declared at all (REDIM then declares a dynamic array).
|
Reallocates an array, optionally preserving existing data. The array may have been declared with DIM name() or with bounds, or not declared at all (REDIM then declares a dynamic array).
|
||||||
|
|
@ -318,8 +358,11 @@ Reallocates an array, optionally preserving existing data. The array may have be
|
||||||
.code
|
.code
|
||||||
REDIM array(newBounds) AS type
|
REDIM array(newBounds) AS type
|
||||||
REDIM PRESERVE array(newBounds) AS type
|
REDIM PRESERVE array(newBounds) AS type
|
||||||
|
REDIM SHARED array(newBounds) AS type
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
REDIM SHARED (module level only) also makes the array visible in every SUB and FUNCTION, like DIM SHARED.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
ReDim items(newSize) As String
|
ReDim items(newSize) As String
|
||||||
ReDim Preserve scores(1 To newCount) As Integer
|
ReDim Preserve scores(1 To newCount) As Integer
|
||||||
|
|
@ -342,7 +385,9 @@ Const DEBUG_MODE = True
|
||||||
Const HEADER As String = "=== Report ==="
|
Const HEADER As String = "=== Report ==="
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Constants are scoped to the file or procedure in which they are declared. Constants declared at module level are visible to all procedures in the same module.
|
With an AS clause the literal is converted to that type: Integer and Long values are rounded and must fit the type, and a string literal for a numeric type (or a number for String) is a compile error. Without an AS clause an integer literal makes a Long constant and a literal with a fraction a Double constant.
|
||||||
|
|
||||||
|
Constants declared at module level are visible to all procedures. A CONST declared inside a SUB or FUNCTION is local to that procedure; it may reuse the name of a module constant without changing it.
|
||||||
|
|
||||||
.h2 TYPE...END TYPE
|
.h2 TYPE...END TYPE
|
||||||
|
|
||||||
|
|
@ -379,6 +424,8 @@ DECLARE SUB name ([BYVAL] [OPTIONAL] param AS type, ...)
|
||||||
DECLARE FUNCTION name ([BYVAL] [OPTIONAL] param AS type, ...) AS returnType
|
DECLARE FUNCTION name ([BYVAL] [OPTIONAL] param AS type, ...) AS returnType
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
A DECLARE must match the SUB or FUNCTION it declares: the same parameter types, BYVAL and OPTIONAL flags, and return type.
|
||||||
|
|
||||||
.h2 DECLARE LIBRARY
|
.h2 DECLARE LIBRARY
|
||||||
|
|
||||||
Declares external functions from a native library bundled with DVX. This is how BASIC programs reach operating-system services, serial ports, databases, and other native facilities. Rather than writing DECLARE LIBRARY blocks yourself, add one of the shipped include files (such as commdlg.bas or sql.bas) to your project; the include file contains the proper declarations.
|
Declares external functions from a native library bundled with DVX. This is how BASIC programs reach operating-system services, serial ports, databases, and other native facilities. Rather than writing DECLARE LIBRARY blocks yourself, add one of the shipped include files (such as commdlg.bas or sql.bas) to your project; the include file contains the proper declarations.
|
||||||
|
|
@ -392,11 +439,13 @@ END DECLARE
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Declare Library "basrt"
|
Declare Library "basrt"
|
||||||
Declare Function SQLOpen(ByVal path As String) As Integer
|
Declare Function SQLOpen(ByVal path As String) As Long
|
||||||
Declare Sub SQLClose(ByVal db As Integer)
|
Declare Sub SQLClose(ByVal db As Long)
|
||||||
End Declare
|
End Declare
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
Arguments are always passed by value. Each argument is converted to the type its parameter is declared with before the call, so a Double passed to a Long parameter arrives as a whole number and a Single parameter receives a Single. Declare Integer only for native values that fit in 16 bits; use Long for 32-bit values, or large results are cut down to 16 bits.
|
||||||
|
|
||||||
.link lang.runtime See also: BASIC Runtime Libraries
|
.link lang.runtime See also: BASIC Runtime Libraries
|
||||||
|
|
||||||
.h2 STATIC
|
.h2 STATIC
|
||||||
|
|
@ -619,6 +668,8 @@ Next
|
||||||
|
|
||||||
The variable name after NEXT is optional. Use EXIT FOR to break out early.
|
The variable name after NEXT is optional. Use EXIT FOR to break out early.
|
||||||
|
|
||||||
|
With an Integer or Long counter a constant STEP must be a whole number (STEP 0.5 is a compile error); a STEP computed at run time is rounded to the counter's type. A Single or Double counter takes any step.
|
||||||
|
|
||||||
.h2 DO...LOOP
|
.h2 DO...LOOP
|
||||||
|
|
||||||
General-purpose loop with pre-test, post-test, or infinite forms.
|
General-purpose loop with pre-test, post-test, or infinite forms.
|
||||||
|
|
@ -695,7 +746,7 @@ Sub Greet(ByVal name As String)
|
||||||
End Sub
|
End Sub
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Parameters are passed by reference by default; BYREF may be written out to say so, and BYVAL selects value semantics. Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
|
Parameters are passed by reference by default (nested calls pass the same variable on, so a SUB called from a SUB can still change the original); BYREF may be written out to say so, and BYVAL selects value semantics. Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Greet "World"
|
Greet "World"
|
||||||
|
|
@ -749,11 +800,27 @@ Function Square(ByVal n As Double) As Double
|
||||||
End Function
|
End Function
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Assign to the function name to set the return value. Use EXIT FUNCTION to return early.
|
Assign to the function name to set the return value. The value is converted to the function's type (AS clause, type suffix or DEFtype), so FUNCTION F AS INTEGER with F = 2.5 returns 2. Use EXIT FUNCTION to return early.
|
||||||
|
|
||||||
|
A FUNCTION can also be called as a statement, with or without parentheses; its result is discarded:
|
||||||
|
|
||||||
|
.code
|
||||||
|
Function Beep2(ByVal n As Integer) As Integer
|
||||||
|
Beep2 = n * 2
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Beep2 3 ' result discarded
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
name(...) in an expression calls a FUNCTION; using a SUB there is a compile error.
|
||||||
|
|
||||||
|
.h3 Labels, GOSUB and Variables in Procedures
|
||||||
|
|
||||||
|
Labels belong to the SUB or FUNCTION they are written in (or to the module level). GOTO, GOSUB, ON ... GOTO/GOSUB and ON ERROR GOTO can only reach a label of the same procedure, and different procedures can reuse the same label name. GOSUB and RETURN work inside a procedure; see GOTO / GOSUB / RETURN. A procedure sees module variables only when they are SHARED; see Variable Scope and SHARED.
|
||||||
|
|
||||||
.h2 DEF FN
|
.h2 DEF FN
|
||||||
|
|
||||||
Defines a single-expression function. The function name begins with FN and is invoked just like a normal function. DEF FN functions cannot span multiple statements.
|
Defines a single-expression function. The function name begins with FN and is invoked just like a normal function. DEF FN functions cannot span multiple statements. A DEF FN must be defined before it is used, and only at module level (not inside a SUB or FUNCTION). Names in the expression other than the parameters are module variables, SHARED or not; the parameters are always passed by value.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
DEF FNname(params) = expression
|
DEF FNname(params) = expression
|
||||||
|
|
@ -810,7 +877,9 @@ GOSUB label
|
||||||
RETURN
|
RETURN
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
GOSUB pushes the return address, executes code at the label, and RETURN jumps back. At module level, RETURN returns from a GOSUB. Inside a SUB/FUNCTION, RETURN returns from the procedure.
|
A label is a name followed by a colon at the start of a line. Labels belong to the SUB or FUNCTION they are in (module-level labels to the module level): a jump can only reach a label of the same procedure, the same label name can be used in several procedures, and a label may have the same name as a variable. A jump to a label that is not defined in the procedure, or to a SUB or variable name, is a compile error.
|
||||||
|
|
||||||
|
GOSUB records where it was called from, runs the code at the label, and RETURN goes back to the statement after the GOSUB. This works at module level and inside a SUB or FUNCTION alike. Each procedure call has its own set of pending GOSUBs: RETURN only returns from a GOSUB made in the same procedure call, and leaving the procedure (END SUB, EXIT SUB, EXIT FUNCTION or an error handled in a caller) discards the ones still pending. RETURN with no pending GOSUB raises error 3 (RETURN without GOSUB). To leave a SUB or FUNCTION, use EXIT SUB / EXIT FUNCTION.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
GoSub Initialize
|
GoSub Initialize
|
||||||
|
|
@ -823,6 +892,17 @@ Initialize:
|
||||||
Return
|
Return
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
.code
|
||||||
|
Sub Report(n As Integer)
|
||||||
|
GoSub Heading
|
||||||
|
Print "value"; n
|
||||||
|
Exit Sub
|
||||||
|
Heading:
|
||||||
|
Print "--- report ---"
|
||||||
|
Return
|
||||||
|
End Sub
|
||||||
|
.endcode
|
||||||
|
|
||||||
.h2 ON...GOTO / ON...GOSUB
|
.h2 ON...GOTO / ON...GOSUB
|
||||||
|
|
||||||
Computed branch based on an integer expression.
|
Computed branch based on an integer expression.
|
||||||
|
|
@ -832,7 +912,7 @@ ON expression GOTO label1, label2, ...
|
||||||
ON expression GOSUB label1, label2, ...
|
ON expression GOSUB label1, label2, ...
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
If the expression evaluates to 1, control goes to the first label; 2, the second; and so on. If out of range, execution falls through.
|
If the expression evaluates to 1, control goes to the first label; 2, the second; and so on. If out of range, execution falls through. The labels follow the same rules as GOTO and GOSUB (same procedure only).
|
||||||
|
|
||||||
.topic lang.io
|
.topic lang.io
|
||||||
.title Input/Output Statements
|
.title Input/Output Statements
|
||||||
|
|
@ -845,6 +925,8 @@ If the expression evaluates to 1, control goes to the first label; 2, the second
|
||||||
.index SPC
|
.index SPC
|
||||||
.index TAB
|
.index TAB
|
||||||
.index PRINT USING
|
.index PRINT USING
|
||||||
|
.index LPRINT
|
||||||
|
.index LPRINT USING
|
||||||
|
|
||||||
.h1 Input/Output Statements
|
.h1 Input/Output Statements
|
||||||
|
|
||||||
|
|
@ -861,10 +943,12 @@ PRINT USING format$; expression [; expression] ...
|
||||||
.list
|
.list
|
||||||
.item ; between items -- no separator (items appear next to each other, or separated by a space for numbers)
|
.item ; between items -- no separator (items appear next to each other, or separated by a space for numbers)
|
||||||
.item , between items -- advance to the next 14-column tab zone
|
.item , between items -- advance to the next 14-column tab zone
|
||||||
.item A trailing ; or , suppresses the newline normally written at the end of the statement
|
.item A trailing ; or , suppresses the newline normally written at the end of the statement (PRINT USING and PRINT # too)
|
||||||
.item ? is an alias for PRINT
|
.item ? is an alias for PRINT
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
|
PRINT # writes numbers exactly as PRINT shows them, with the space after each number, so PRINT #1, 1; 2 writes "1 2 ". PRINT #n, with nothing after the comma writes an empty line.
|
||||||
|
|
||||||
Special functions inside PRINT:
|
Special functions inside PRINT:
|
||||||
|
|
||||||
.list
|
.list
|
||||||
|
|
@ -878,9 +962,28 @@ Print "X="; x, "Y="; y ' comma = next 14-column zone
|
||||||
Print #1, "Written to file"
|
Print #1, "Written to file"
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
.h2 LPRINT
|
||||||
|
|
||||||
|
Prints on the printer instead of the screen. Everything PRINT accepts works the same way: ; and , between items, SPC, TAB and LPRINT USING. The printer keeps its own column for TAB and the 14-column zones.
|
||||||
|
|
||||||
|
.code
|
||||||
|
LPRINT [expression [{; | ,} expression] ...]
|
||||||
|
LPRINT USING format$; expression [; expression] ...
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
The output goes to the printer chosen in the Control Panel (a printer port, or a file). Each LPRINT is sent at once, and lines end the way printers expect. Nothing starts a new page by itself: LPRINT CHR$(12) ejects the page. If the printer has a problem (out of paper, off line), the user is asked to fix it and choose Retry, or Cancel; after Cancel the LPRINT raises error 57. With no printer to print on, LPRINT raises error 68.
|
||||||
|
|
||||||
|
.code
|
||||||
|
LPrint "Invoice"; Tab(40); Date$
|
||||||
|
LPrint Using "Total: $$####.##"; total
|
||||||
|
LPrint Chr$(12) ' eject the page
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
For a whole document with margins, a header, a footer and page numbers, and for the Print dialog, use the Printing Library (print.bas, see Include Libraries).
|
||||||
|
|
||||||
.h3 PRINT USING
|
.h3 PRINT USING
|
||||||
|
|
||||||
PRINT USING formats each following expression according to a format string and prints the result. The format string is a mix of fields and literal text: each expression consumes the next field, the literal text before it is printed as it is, and the literal text after the last field is printed at the end. If more expressions are supplied than there are fields, the format string is reused from the start.
|
PRINT USING formats each following expression according to a format string and prints the result. The expressions after the format are separated by ; or , (both work the same here). The format string is a mix of fields and literal text: each expression consumes the next field, the literal text before it is printed as it is, and the literal text after the last field is printed at the end. If more expressions are supplied than there are fields, the format string is reused from the start.
|
||||||
|
|
||||||
.table
|
.table
|
||||||
Format character Meaning
|
Format character Meaning
|
||||||
|
|
@ -900,7 +1003,7 @@ PRINT USING formats each following expression according to a format string and p
|
||||||
_ The next character is printed literally
|
_ The next character is printed literally
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
A numeric field is as wide as its format characters; the value is right-aligned in it and a negative sign (or a leading +) floats directly in front of the first digit. A value that does not fit is printed in full with a leading %. A string expression must meet a string field and a numeric expression a numeric field, otherwise error 13 (Type mismatch) is raised.
|
A numeric field is as wide as its format characters; the value is right-aligned in it and a negative sign (or a leading +) floats directly in front of the first digit. A value that does not fit is printed in full with a leading %; a value too long to print in full is shown in scientific notation behind the %. A trailing + in a format with no digit positions is printed as literal text. A string expression must meet a string field and a numeric expression a numeric field, otherwise error 13 (Type mismatch) is raised.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Print Using "###.##"; -3.14159 ' " -3.14"
|
Print Using "###.##"; -3.14159 ' " -3.14"
|
||||||
|
|
@ -930,6 +1033,8 @@ Input "Enter your name: "; name$
|
||||||
Input #1, line$
|
Input #1, line$
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
The target may be a variable, an array element or a TYPE field (INPUT, INPUT #, LINE INPUT # and READ alike).
|
||||||
|
|
||||||
.h2 DATA / READ / RESTORE
|
.h2 DATA / READ / RESTORE
|
||||||
|
|
||||||
Inline data pool for constants.
|
Inline data pool for constants.
|
||||||
|
|
@ -940,7 +1045,7 @@ READ variable1, variable2, ...
|
||||||
RESTORE
|
RESTORE
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
DATA statements define a pool of values. READ reads the next value from the pool into a variable. RESTORE resets the read pointer to the beginning.
|
DATA statements define a pool of values. READ reads the next value from the pool into a variable, array element or TYPE field. RESTORE resets the read pointer to the beginning. An item in quotes is a string; an unquoted item is taken as raw text up to the next comma, colon, comment or end of line (so keywords, several words and text such as 12abc are read as they are written) and converted when READ into a numeric variable. The most negative Integer and Long values (-32768, -2147483648) are accepted.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Data 10, 20, 30, "Hello"
|
Data 10, 20, 30, "Hello"
|
||||||
|
|
@ -976,7 +1081,19 @@ RESUME NEXT ' Continue at the next statement after the error
|
||||||
ERROR n ' Raise a runtime error with error number n
|
ERROR n ' Raise a runtime error with error number n
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
The ERR keyword returns the current error number in expressions and ERL the source line the error was raised on (both are 0 when no error is active, and both reset after RESUME). RESUME or RESUME NEXT executed while no error is active raises error 20 (RESUME without error).
|
The ERR keyword returns the current error number (a Long) in expressions and ERL the source line the error was raised on (both are 0 when no error is active, and both reset after RESUME). RESUME or RESUME NEXT executed while no error is active raises error 20 (RESUME without error). ERROR n raises error n with its standard message (ERROR 11 reports "Division by zero"); numbers without a standard message report "User-defined error".
|
||||||
|
|
||||||
|
Each SUB and FUNCTION has its own ON ERROR handler:
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item An error is handled by the handler of the procedure it happens in; if that procedure has none, the calling procedures are searched outward.
|
||||||
|
.item A procedure called from a running handler can trap its own errors with its own handler.
|
||||||
|
.item Returning from a procedure that merely installed a handler leaves the caller's ERR and handler alone.
|
||||||
|
.item An error raised inside a running handler passes to the calling procedure's handler (untrapped if there is none).
|
||||||
|
.item The label named by ON ERROR GOTO must be in the same procedure.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
RESUME NEXT continues after the failing statement. When the error happened in the condition of an IF, ELSEIF, WHILE, DO or LOOP, the condition counts as false (the block is skipped, the loop is left); after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false (the next value or CASE is tried); after an error in an ON ... GOTO/GOSUB selector no branch is taken and execution continues with the next statement.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
On Error GoTo ErrorHandler
|
On Error GoTo ErrorHandler
|
||||||
|
|
@ -994,24 +1111,31 @@ ErrorHandler:
|
||||||
Number Meaning
|
Number Meaning
|
||||||
------ -------
|
------ -------
|
||||||
1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow)
|
1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow)
|
||||||
|
3 RETURN without GOSUB
|
||||||
4 Out of DATA
|
4 Out of DATA
|
||||||
5 Illegal function call (bad argument to a built-in function)
|
5 Illegal function call (bad argument to a built-in function; an undefined math result such as a negative number to a fractional power)
|
||||||
6 Overflow (value does not fit the target type)
|
6 Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range)
|
||||||
7 Out of memory
|
7 Out of memory
|
||||||
9 Subscript out of range / invalid variable or field index
|
9 Subscript out of range / invalid variable or field index
|
||||||
11 Division by zero
|
11 Division by zero
|
||||||
13 Type mismatch / not an array / not a TYPE instance
|
13 Type mismatch / not an array / not a TYPE instance
|
||||||
|
14 Out of string space (no memory to build a string)
|
||||||
20 RESUME without error
|
20 RESUME without error
|
||||||
26 FOR loop nesting too deep
|
26 FOR loop nesting too deep
|
||||||
|
28 Out of stack space (calls or expressions nested too deeply)
|
||||||
51 Internal error (bad opcode)
|
51 Internal error (bad opcode)
|
||||||
52 Bad file number or file not open
|
52 Bad file number or file not open
|
||||||
53 File not found (OPEN for INPUT, KILL, FILELEN, ...)
|
53 File not found (OPEN for INPUT, KILL, FILELEN, ...)
|
||||||
54 Bad file mode
|
54 Bad file mode (also: a printer opened for anything but OUTPUT or APPEND)
|
||||||
|
57 Device I/O error (printing failed or was cancelled)
|
||||||
58 File already exists or rename failed
|
58 File already exists or rename failed
|
||||||
59 Bad record length (OPEN ... LEN must be 1 to 32767; GET/PUT data longer than LEN)
|
59 Bad record length (OPEN ... LEN must be 1 to 32767; GET/PUT data longer than LEN)
|
||||||
|
63 Bad record number (SEEK, GET or PUT position below 1)
|
||||||
67 Too many files open
|
67 Too many files open
|
||||||
|
68 Device unavailable (no printer to print on)
|
||||||
75 Path/file access error
|
75 Path/file access error
|
||||||
76 Path not found
|
76 Path not found
|
||||||
|
91 Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control)
|
||||||
1000 No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)
|
1000 No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
|
@ -1032,7 +1156,7 @@ exitCode = Shell("COPY A.TXT B.TXT")
|
||||||
|
|
||||||
.h2 SLEEP
|
.h2 SLEEP
|
||||||
|
|
||||||
Pauses execution for the given number of seconds. With no argument, sleeps for 1 second.
|
Pauses execution for the given number of seconds. With no argument, sleeps for 1 second. The desktop and the program's own forms keep running while it sleeps, SLEEP 0 just lets pending events run, and closing the program ends the sleep.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
SLEEP [seconds]
|
SLEEP [seconds]
|
||||||
|
|
@ -1097,6 +1221,17 @@ OPEN filename$ FOR BINARY AS #channel
|
||||||
BINARY Open for raw binary I/O.
|
BINARY Open for raw binary I/O.
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
.h3 Printing Through OPEN
|
||||||
|
|
||||||
|
The names LPT1:, LPT2:, LPT3: (a printer port) and PRN (the printer chosen in the Control Panel) open a printer, in any case and with or without the colon. Only OUTPUT and APPEND work (anything else raises error 54). PRINT # and WRITE # collect the output, and CLOSE sends it to the printer; a printer problem the user cancels makes CLOSE raise error 57, and a printer that cannot be reached makes OPEN raise error 68.
|
||||||
|
|
||||||
|
.code
|
||||||
|
Open "LPT1:" For Output As #1
|
||||||
|
Print #1, "Line one"
|
||||||
|
Print #1, "Line two"
|
||||||
|
Close #1 ' now it prints
|
||||||
|
.endcode
|
||||||
|
|
||||||
.h2 CLOSE
|
.h2 CLOSE
|
||||||
|
|
||||||
Closes an open file channel.
|
Closes an open file channel.
|
||||||
|
|
@ -1107,7 +1242,7 @@ CLOSE #channel
|
||||||
|
|
||||||
.h2 PRINT #
|
.h2 PRINT #
|
||||||
|
|
||||||
Writes text to a file.
|
Writes text to a file, formatted exactly as PRINT shows it (numbers are followed by a space). See PRINT.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
PRINT #channel, expression
|
PRINT #channel, expression
|
||||||
|
|
@ -1157,13 +1292,13 @@ GET #channel, [recordNum], variable
|
||||||
PUT #channel, [recordNum], variable
|
PUT #channel, [recordNum], variable
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Integer and Boolean 2 bytes, Long 4, Single 4, Double 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59.
|
In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Integer and Boolean 2 bytes, Long 4, Single 4, Double 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59 before anything is written.
|
||||||
|
|
||||||
In BINARY mode recordNum is a 1-based byte position. Numbers are stored as in RANDOM mode. PUT writes the characters of a String with no length prefix, and GET reads as many bytes as the String variable currently holds, so set its length first (for example with SPACE$ or by declaring it as STRING * n).
|
In BINARY mode recordNum is a 1-based byte position. Numbers are stored as in RANDOM mode. PUT writes the characters of a String with no length prefix, and GET reads as many bytes as the String variable currently holds, so set its length first (for example with SPACE$ or by declaring it as STRING * n).
|
||||||
|
|
||||||
The variable may be a TYPE variable. Its fields are packed in declaration order with the sizes above; a STRING * n field takes exactly n bytes (padded with spaces), a variable-length String field is stored with its 2-byte length prefix in both modes, and nested TYPE fields are packed inline. GET fills the fields of the existing variable in place.
|
The variable may be a TYPE variable. Its fields are packed in declaration order with the sizes above; a STRING * n field takes exactly n bytes (padded with spaces), a variable-length String field is stored with its 2-byte length prefix in both modes, and nested TYPE fields are packed inline. GET fills the fields of the existing variable in place.
|
||||||
|
|
||||||
The variable may also be an array element, arr(i), of any type including a TYPE: PUT writes that element and GET reads into it. The index expression of a GET target is evaluated twice (once to fetch the element, once to store it back), so keep it free of side effects.
|
The variable may also be an array element, arr(i), of any type including a TYPE: PUT writes that element and GET reads into it. The index expression is evaluated once.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Type RecT
|
Type RecT
|
||||||
|
|
@ -1190,7 +1325,7 @@ Close #1
|
||||||
|
|
||||||
.h2 SEEK
|
.h2 SEEK
|
||||||
|
|
||||||
Sets the file position. As a function, returns the current position.
|
Sets the file position. As a function, returns the current position. On a RANDOM file the position is a record number (statement and function); on other files it is a 1-based byte position. A position below 1 raises error 63 (Bad record number).
|
||||||
|
|
||||||
.code
|
.code
|
||||||
SEEK #channel, position ' Statement: set position
|
SEEK #channel, position ' Statement: set position
|
||||||
|
|
@ -1281,7 +1416,7 @@ ChDrive "D"
|
||||||
|
|
||||||
.h2 GETATTR / SETATTR
|
.h2 GETATTR / SETATTR
|
||||||
|
|
||||||
GETATTR returns the attributes of a file (as a function). SETATTR sets them (as a statement). Attributes are a bitmask of the vb file-attribute constants.
|
GETATTR returns the attributes of a file (as a function). SETATTR sets them (as a statement). Attributes are a bitmask of the vb file-attribute constants. A missing file raises error 53 (File not found); SETATTR raises error 75 when the attributes cannot be changed.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
attrs = GETATTR(filename$)
|
attrs = GETATTR(filename$)
|
||||||
|
|
@ -1309,7 +1444,7 @@ SetAttr "secret.dat", vbHidden + vbReadOnly
|
||||||
|
|
||||||
.h2 CURDIR$ / DIR$
|
.h2 CURDIR$ / DIR$
|
||||||
|
|
||||||
CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done).
|
CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
cwd$ = CURDIR$
|
cwd$ = CURDIR$
|
||||||
|
|
@ -1380,7 +1515,7 @@ bytes = FILELEN(filename$)
|
||||||
VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character
|
VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call).
|
A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.
|
||||||
|
|
||||||
.h2 FORMAT$
|
.h2 FORMAT$
|
||||||
|
|
||||||
|
|
@ -1416,7 +1551,7 @@ Mid$(s$, 8, 5) = "DVX--"
|
||||||
Print s$ ' Hello, DVX--!
|
Print s$ ' Hello, DVX--!
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
If length is omitted, MID$ replaces up to LEN(replacement$) characters. The target string's total length never changes -- extra characters in replacement$ are truncated and shorter replacements leave trailing characters in place.
|
If length is omitted, MID$ replaces up to LEN(replacement$) characters; a length of 0 replaces nothing. The target string's total length never changes -- extra characters in replacement$ are truncated and shorter replacements leave trailing characters in place. A start position below 1 or a negative length raises error 5 (Illegal function call).
|
||||||
|
|
||||||
.topic lang.func.math
|
.topic lang.func.math
|
||||||
.title Math Functions
|
.title Math Functions
|
||||||
|
|
@ -1455,8 +1590,12 @@ If length is omitted, MID$ replaces up to LEN(replacement$) characters. The targ
|
||||||
TIMER Double Seconds since midnight
|
TIMER Double Seconds since midnight
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1.
|
||||||
|
|
||||||
|
INT, FIX and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^ and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).
|
||||||
|
|
||||||
.note info
|
.note info
|
||||||
RND with no argument returns the next number in the sequence. Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same sequence each run.
|
Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same sequence each run.
|
||||||
.endnote
|
.endnote
|
||||||
|
|
||||||
.h2 Color Functions
|
.h2 Color Functions
|
||||||
|
|
@ -1523,7 +1662,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
|
||||||
INPUT$(n, #channel) String Reads exactly n characters from the file
|
INPUT$(n, #channel) String Reads exactly n characters from the file
|
||||||
LOC(channel) Long BINARY: position of the last byte read or written (0 at the start); RANDOM: number of the last record read or written; sequential: number of 128-byte blocks read or written
|
LOC(channel) Long BINARY: position of the last byte read or written (0 at the start); RANDOM: number of the last record read or written; sequential: number of 128-byte blocks read or written
|
||||||
LOF(channel) Long Length of the file in bytes
|
LOF(channel) Long Length of the file in bytes
|
||||||
SEEK(channel) Long 1-based position of the next byte to be read or written
|
SEEK(channel) Long RANDOM: number of the next record; other modes: 1-based position of the next byte to be read or written
|
||||||
FILELEN(path$) Long Length of the named file in bytes (no OPEN needed; error 53 when missing)
|
FILELEN(path$) Long Length of the named file in bytes (no OPEN needed; error 53 when missing)
|
||||||
GETATTR(path$) Integer File attribute bits (see vbReadOnly, vbHidden, etc.)
|
GETATTR(path$) Integer File attribute bits (see vbReadOnly, vbHidden, etc.)
|
||||||
CURDIR$ String Current working directory
|
CURDIR$ String Current working directory
|
||||||
|
|
@ -1533,12 +1672,16 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
|
||||||
UBOUND(array [, dim]) Integer Upper bound of an array dimension (default dim = 1)
|
UBOUND(array [, dim]) Integer Upper bound of an array dimension (default dim = 1)
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
The dim argument of LBOUND and UBOUND must be a constant from 1 to 8.
|
||||||
|
|
||||||
.topic lang.func.misc
|
.topic lang.func.misc
|
||||||
.title Miscellaneous Functions
|
.title Miscellaneous Functions
|
||||||
.toc 2 Miscellaneous Functions
|
.toc 2 Miscellaneous Functions
|
||||||
.index DATE$
|
.index DATE$
|
||||||
.index TIME$
|
.index TIME$
|
||||||
.index ENVIRON$
|
.index ENVIRON$
|
||||||
|
.index COMMAND$
|
||||||
|
.index Command Line
|
||||||
|
|
||||||
.h1 Miscellaneous Functions
|
.h1 Miscellaneous Functions
|
||||||
|
|
||||||
|
|
@ -1549,10 +1692,24 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
|
||||||
TIME$ String Current time as "HH:MM:SS" (24-hour)
|
TIME$ String Current time as "HH:MM:SS" (24-hour)
|
||||||
TIMER Double Seconds since midnight
|
TIMER Double Seconds since midnight
|
||||||
ENVIRON$(name$) String Value of the environment variable; empty string if not set
|
ENVIRON$(name$) String Value of the environment variable; empty string if not set
|
||||||
ERR Integer Current runtime error number (0 if no error)
|
COMMAND$ String The arguments the program was started with; empty string if none
|
||||||
|
COMMAND$(n) String The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$
|
||||||
|
ERR Long Current runtime error number (0 if no error)
|
||||||
SHELL(cmd$) Integer Exit code of the command (also usable as a statement, discarding the code)
|
SHELL(cmd$) Integer Exit code of the command (also usable as a statement, discarding the code)
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
.h2 COMMAND$
|
||||||
|
|
||||||
|
COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs:
|
||||||
|
|
||||||
|
.code
|
||||||
|
IF COMMAND$(1) <> "" THEN
|
||||||
|
OpenFile COMMAND$(1)
|
||||||
|
END IF
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
A program gets arguments when a document of a type associated with it is opened (the document is the argument), from a Program Manager item's Arguments, or from a command typed in a Run box. When the program runs in the IDE, COMMAND$ returns the Arguments set in Project Properties. A negative n is error 5 (Illegal function call).
|
||||||
|
|
||||||
.topic lang.forms
|
.topic lang.forms
|
||||||
.title Form and Control Statements
|
.title Form and Control Statements
|
||||||
.toc 1 Form and Control Statements
|
.toc 1 Form and Control Statements
|
||||||
|
|
@ -1624,6 +1781,8 @@ x = Text1.Left
|
||||||
|
|
||||||
A control on another form is reached through that form's name, or through a variable holding a form reference: FormName.ControlName.Property works in both assignments and expressions, and so does FormName.ControlName.Method. Me.ControlName.Property does the same for the current form.
|
A control on another form is reached through that form's name, or through a variable holding a form reference: FormName.ControlName.Property works in both assignments and expressions, and so does FormName.ControlName.Method. Me.ControlName.Property does the same for the current form.
|
||||||
|
|
||||||
|
Using a property or method (or Show, Hide, Unload) on a variable that does not hold a form or control raises error 91 (Object variable not set). In the IDE, a property or method name that the control's type does not have is reported when the program is compiled.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Form2.Label1.Caption = "From Form1"
|
Form2.Label1.Caption = "From Form1"
|
||||||
total = Form2.List1.ListCount
|
total = Form2.List1.ListCount
|
||||||
|
|
@ -1707,7 +1866,7 @@ Creates a new form with the given name and initial size. Returns a form referenc
|
||||||
SET ctrlVar = CreateControl(formRef, typeName$, ctrlName$ [, parentCtrlRef])
|
SET ctrlVar = CreateControl(formRef, typeName$, ctrlName$ [, parentCtrlRef])
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Creates a control on the given form. typeName$ is the control type (e.g. "CommandButton", "Label", "TextBox"). ctrlName$ is the unique name for the new control. If a parentCtrlRef is supplied, the new control is nested inside it (useful for placing controls inside containers like HBox or Frame).
|
Creates a control on the given form. typeName$ is the control type (e.g. "CommandButton", "Label", "TextBox"). ctrlName$ is the unique name for the new control. If a parentCtrlRef is supplied, the new control is nested inside it (useful for placing controls inside containers like HBox or Frame); the parent must be a container on the same form, otherwise a runtime error (CreateControl: invalid parent) is raised.
|
||||||
|
|
||||||
.h3 SetEvent
|
.h3 SetEvent
|
||||||
|
|
||||||
|
|
@ -1715,7 +1874,7 @@ Creates a control on the given form. typeName$ is the control type (e.g. "Comman
|
||||||
SETEVENT ctrlRef, eventName$, handlerName$
|
SETEVENT ctrlRef, eventName$, handlerName$
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Wires an arbitrary SUB as the handler for a control's event. The default event-handler convention (ControlName_EventName) is used automatically for controls defined in a .frm file; SetEvent is for controls you create dynamically or for reusing a single handler across multiple controls.
|
Wires an arbitrary SUB as the handler for an event of a control, a form (Me or a form reference) or a menu item. The default event-handler convention (ControlName_EventName) is used automatically for controls defined in a .frm file; SetEvent is for controls you create dynamically or for reusing a single handler across multiple controls. A handler declared without parameters is called without the event's arguments.
|
||||||
|
|
||||||
.h3 RemoveControl
|
.h3 RemoveControl
|
||||||
|
|
||||||
|
|
@ -1723,7 +1882,7 @@ Wires an arbitrary SUB as the handler for a control's event. The default event-h
|
||||||
REMOVECONTROL formRef, ctrlName$
|
REMOVECONTROL formRef, ctrlName$
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
Removes a control from a form.
|
Removes a control from a form. A later reference to the removed control raises a runtime error (unknown control), as does a reference to a control of a form that has been unloaded.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Dim frm As Long
|
Dim frm As Long
|
||||||
|
|
@ -1860,13 +2019,13 @@ End Sub
|
||||||
|
|
||||||
.h1 SQL Functions
|
.h1 SQL Functions
|
||||||
|
|
||||||
DVX BASIC includes SQLite database support through the sql.bas include library. Add sql.bas to your project to gain access to these functions. All functions use database handles and result-set handles (integers) returned by SQLOpen and SQLQuery.
|
DVX BASIC includes SQLite database support through the sql.bas include library. Add sql.bas to your project to gain access to these functions. All functions use database handles and result-set handles (Long integers) returned by SQLOpen and SQLQuery. Numeric parameters and results are Long, so row counts and integer column values keep their full 32-bit range.
|
||||||
|
|
||||||
.h2 Opening and Closing Databases
|
.h2 Opening and Closing Databases
|
||||||
|
|
||||||
.h3 SQLOpen
|
.h3 SQLOpen
|
||||||
|
|
||||||
Opens a SQLite database file and returns a database handle (> 0) or 0 on failure. The file is created if it does not exist.
|
Opens a SQLite database file and returns a database handle (> 0) or 0 on failure. The file is created if it does not exist. A file that cannot be changed (for example one marked read-only) still opens, but only for reading. When SQLOpen returns 0, SQLError$(0) tells you why.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
db = SQLOpen(path$)
|
db = SQLOpen(path$)
|
||||||
|
|
@ -1900,6 +2059,8 @@ SQLExec db, "INSERT INTO users (name) VALUES ('Scott')"
|
||||||
ok = SQLExec(db, "DELETE FROM users WHERE id = 5")
|
ok = SQLExec(db, "DELETE FROM users WHERE id = 5")
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
Put text values in single quotes, as in the examples. Text in double quotes is read as a column name, so "Scott" in place of 'Scott' gives the error "no such column: Scott".
|
||||||
|
|
||||||
.h3 SQLAffected
|
.h3 SQLAffected
|
||||||
|
|
||||||
Returns the number of rows affected by the last INSERT, UPDATE, or DELETE.
|
Returns the number of rows affected by the last INSERT, UPDATE, or DELETE.
|
||||||
|
|
@ -1964,7 +2125,7 @@ colName$ = SQLFieldName$(rs, columnIndex)
|
||||||
|
|
||||||
.h3 SQLFieldInt
|
.h3 SQLFieldInt
|
||||||
|
|
||||||
Returns a field value as an integer.
|
Returns a field value as a Long integer.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
value = SQLFIELDINT(rs, columnIndex)
|
value = SQLFIELDINT(rs, columnIndex)
|
||||||
|
|
@ -2006,11 +2167,13 @@ Returns the last error message for the database.
|
||||||
msg$ = SQLError$(db)
|
msg$ = SQLError$(db)
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
When a database file could not be opened, read or written, the message ends with the reason in parentheses, for example "unable to open database file (No such file or directory)". If another program, or another handle in your own program, is changing the same database at that moment, the message is "database is locked": try again once the other change is finished, and free result sets with SQLFreeResult as soon as you are done with them, because an open result set stops other handles from saving changes.
|
||||||
|
|
||||||
.h2 Complete SQL Example
|
.h2 Complete SQL Example
|
||||||
|
|
||||||
.code
|
.code
|
||||||
Dim db As Integer
|
Dim db As Long
|
||||||
Dim rs As Integer
|
Dim rs As Long
|
||||||
|
|
||||||
db = SQLOpen(App.Data & "\contacts.db")
|
db = SQLOpen(App.Data & "\contacts.db")
|
||||||
SQLExec db, "CREATE TABLE IF NOT EXISTS contacts (name TEXT, phone TEXT)"
|
SQLExec db, "CREATE TABLE IF NOT EXISTS contacts (name TEXT, phone TEXT)"
|
||||||
|
|
@ -2063,7 +2226,7 @@ DVX BASIC provides built-in functions for reading and writing standard INI confi
|
||||||
|
|
||||||
.h2 IniRead
|
.h2 IniRead
|
||||||
|
|
||||||
Reads a value from an INI file. Returns the default value if the file, section, or key is not found. All four arguments are required.
|
Reads a value from an INI file. Returns the default value if the file, section, or key is not found. All four arguments are required. Section and key names match without regard to case, and blanks around them are ignored ([ User ] is section User). If a name appears twice, the first one counts. Keys above the first section header belong to section "" (an empty section name). Lines starting with ; or # are comments.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
value$ = IniRead(file$, section$, key$, default$)
|
value$ = IniRead(file$, section$, key$, default$)
|
||||||
|
|
@ -2076,7 +2239,7 @@ fontSize = Val(IniRead(App.Config & "\app.ini", "Display", "FontSize", "12"))
|
||||||
|
|
||||||
.h2 IniWrite
|
.h2 IniWrite
|
||||||
|
|
||||||
Writes a value to an INI file. Creates the file, section, or key if they do not exist.
|
Writes a value to an INI file. Creates the file, section, or key if they do not exist. A new key goes after the last key of its section and a new section at the end of the file. Every other line -- comments, blank lines, other keys, lines of any length -- is kept exactly as it is. The changed key is written as key = value. Every line of the saved file ends in a carriage return and line feed (CR LF). The file is saved through a temporary file, so a failed write leaves the old file intact. If the file exists but cannot be read, or the new file cannot be written, IniWrite raises error 75 (Path/file access error) and leaves the file alone.
|
||||||
|
|
||||||
.code
|
.code
|
||||||
INIWRITE file$, section$, key$, value$
|
INIWRITE file$, section$, key$, value$
|
||||||
|
|
@ -2197,6 +2360,18 @@ Bit values used with GETATTR and SETATTR.
|
||||||
.index resource.bas
|
.index resource.bas
|
||||||
.index help.bas
|
.index help.bas
|
||||||
.index DECLARE LIBRARY
|
.index DECLARE LIBRARY
|
||||||
|
.index CommHandshake
|
||||||
|
.index CommRecvEncrypted
|
||||||
|
.index CommSetPlain
|
||||||
|
.index print.bas
|
||||||
|
.index PrintTextDialog
|
||||||
|
.index PrintPictureDialog
|
||||||
|
.index PrintPageSetup
|
||||||
|
.index net.bas
|
||||||
|
.index TcpConnect
|
||||||
|
.index TcpRecv$
|
||||||
|
.index UdpSend
|
||||||
|
.index HostLookup$
|
||||||
|
|
||||||
.h1 Include Libraries
|
.h1 Include Libraries
|
||||||
|
|
||||||
|
|
@ -2210,6 +2385,8 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
|
||||||
comm.bas Serial-port I/O (raw and packet) and optional encrypted links
|
comm.bas Serial-port I/O (raw and packet) and optional encrypted links
|
||||||
resource.bas Read and write DVX resource blocks (icons, text, binary data) in bundled files
|
resource.bas Read and write DVX resource blocks (icons, text, binary data) in bundled files
|
||||||
help.bas Compile help source files and open the DVX help viewer
|
help.bas Compile help source files and open the DVX help viewer
|
||||||
|
net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, ...), UDP messages and name lookups
|
||||||
|
print.bas Printing: text with the page setup and pictures, with or without the Print dialog; the Page Setup dialog
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.h2 Using an Include Library
|
.h2 Using an Include Library
|
||||||
|
|
@ -2217,7 +2394,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
|
||||||
.code
|
.code
|
||||||
' With commdlg.bas added to the project
|
' With commdlg.bas added to the project
|
||||||
Dim path As String
|
Dim path As String
|
||||||
path = basFileOpen("Open a file", "Text Files (*.txt)|All Files (*.*)")
|
path = DlgFileOpen$("Open a file", "Text Files (*.txt)|All Files (*.*)")
|
||||||
|
|
||||||
If path <> "" Then
|
If path <> "" Then
|
||||||
Print "Selected: " & path
|
Print "Selected: " & path
|
||||||
|
|
@ -2228,4 +2405,92 @@ End If
|
||||||
DVX BASIC does not have an $INCLUDE directive. Libraries are added to the project so that the compiler sees their DECLARE LIBRARY blocks.
|
DVX BASIC does not have an $INCLUDE directive. Libraries are added to the project so that the compiler sees their DECLARE LIBRARY blocks.
|
||||||
.endnote
|
.endnote
|
||||||
|
|
||||||
.link lib.basrt See also: BASIC Runtime Library
|
.h2 Encrypted Serial Links
|
||||||
|
|
||||||
|
With comm.bas in the project, CommOpen opens a packet link and CommHandshake sets up encryption. Give both ends the same passphrase to authenticate the connection. Without a passphrase the link is still encrypted and tamper-checked, but it cannot tell who is on the other end. CommHandshake returns False if the passphrases differ, if only one side gives one, or if the link fails.
|
||||||
|
|
||||||
|
.code
|
||||||
|
Dim link As Long
|
||||||
|
link = CommOpen(1, 115200, 8, "N", 1, SER_HANDSHAKE_NONE)
|
||||||
|
|
||||||
|
If CommHandshake(link, "shared secret") Then
|
||||||
|
CommSend link, "hello", 0, True
|
||||||
|
Else
|
||||||
|
Print "Handshake failed"
|
||||||
|
End If
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.
|
||||||
|
|
||||||
|
.h2 Printing
|
||||||
|
|
||||||
|
With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header and a footer with the document name, date and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.
|
||||||
|
|
||||||
|
.code
|
||||||
|
Sub mnuPrint_Click ()
|
||||||
|
PrintTextDialog txtNotes.Text, "Notes"
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub mnuPageSetup_Click ()
|
||||||
|
PrintPageSetup
|
||||||
|
End Sub
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
For printer output a line at a time, see LPRINT and OPEN "LPT1:".
|
||||||
|
|
||||||
|
.h2 Network Connections
|
||||||
|
|
||||||
|
With net.bas in the project, a program talks to other computers over the network set up in the Control Panel. TcpConnect returns a handle (0 when it failed; NetError$ says why), TcpSend sends text, and TcpRecv$ returns whatever has arrived without waiting, so a Timer keeps the program responsive:
|
||||||
|
|
||||||
|
.code
|
||||||
|
Dim conn As Long
|
||||||
|
|
||||||
|
Sub Form_Load ()
|
||||||
|
conn = TcpConnect("bbs.example.org", 23)
|
||||||
|
|
||||||
|
If conn = 0 Then
|
||||||
|
Print "Could not connect: " & NetError$()
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Sub Timer1_Timer ()
|
||||||
|
If TcpPending(conn) > 0 Then Print TcpRecv$(conn)
|
||||||
|
If TcpConnected(conn) = 0 Then Timer1.Enabled = False
|
||||||
|
End Sub
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
.table
|
||||||
|
Function Description
|
||||||
|
-------- -----------
|
||||||
|
HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found.
|
||||||
|
NetAddress$() This computer's address ("" while it has none).
|
||||||
|
NetError$() Why the last call failed, in plain words.
|
||||||
|
TcpConnect(host, port) Connect; a handle, or 0. Waits up to 20 seconds; other programs keep running.
|
||||||
|
TcpListen(port) Wait for connections on port; a listening handle, or 0.
|
||||||
|
TcpAccept(handle) A connection that arrived on a listening handle, or 0 when none is waiting.
|
||||||
|
TcpSend(handle, text) Send text; the bytes sent, or -1.
|
||||||
|
TcpPending(handle) Bytes waiting to be read (-1 for a bad handle).
|
||||||
|
TcpRecv$(handle) What has arrived (up to 4096 bytes), or "".
|
||||||
|
TcpConnected(handle) 1 while the connection is open, 0 once the other side closed it.
|
||||||
|
TcpClose handle Close a connection or a listening handle.
|
||||||
|
UdpOpen(port) A UDP handle on port (0 = any free port), or 0.
|
||||||
|
UdpSend(handle, host, port, text) Send one message; the bytes sent, or -1. "255.255.255.255" reaches every computer nearby.
|
||||||
|
UdpRecv$(handle) The next message that arrived, or "".
|
||||||
|
UdpFrom$() "address:port" of the sender of the last UdpRecv$ message.
|
||||||
|
UdpClose handle Close a UDP handle.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
The functions work with text: a string stops at a zero byte. Every connection a program opens is closed for it when the program ends.
|
||||||
|
|
||||||
|
CommRecvEncrypted(handle, channel) tells whether the data the last CommRecv$ on that channel returned can be trusted. It returns True when every byte of it arrived encrypted and authenticated, and also when that CommRecv$ returned nothing or CommRecv$ has not been called on the channel yet. It returns False when any byte arrived unencrypted, which can only happen before CommHandshake or on a channel opened with CommSetPlain, and for an invalid handle or a channel outside 0-127.
|
||||||
|
|
||||||
|
.code
|
||||||
|
Dim msg As String
|
||||||
|
msg = CommRecv$(link, 1)
|
||||||
|
|
||||||
|
If msg <> "" And Not CommRecvEncrypted(link, 1) Then
|
||||||
|
Print "Unencrypted: " & msg
|
||||||
|
End If
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
.link lang.declarations See also: DECLARE LIBRARY
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
// conditions, preserving the historical numeric value at those sites.
|
// conditions, preserving the historical numeric value at those sites.
|
||||||
|
|
||||||
#define BAS_ERR_NEXT_WITHOUT_FOR 1
|
#define BAS_ERR_NEXT_WITHOUT_FOR 1
|
||||||
|
#define BAS_ERR_RETURN_NO_GOSUB 3
|
||||||
#define BAS_ERR_OUT_OF_DATA 4
|
#define BAS_ERR_OUT_OF_DATA 4
|
||||||
#define BAS_ERR_ILLEGAL_FUNC_CALL 5
|
#define BAS_ERR_ILLEGAL_FUNC_CALL 5
|
||||||
#define BAS_ERR_OVERFLOW 6
|
#define BAS_ERR_OVERFLOW 6
|
||||||
|
|
@ -43,17 +44,23 @@
|
||||||
#define BAS_ERR_SUBSCRIPT_RANGE 9
|
#define BAS_ERR_SUBSCRIPT_RANGE 9
|
||||||
#define BAS_ERR_DIV_BY_ZERO 11
|
#define BAS_ERR_DIV_BY_ZERO 11
|
||||||
#define BAS_ERR_TYPE_MISMATCH 13
|
#define BAS_ERR_TYPE_MISMATCH 13
|
||||||
|
#define BAS_ERR_OUT_OF_STRING 14
|
||||||
#define BAS_ERR_RESUME_WITHOUT_ERR 20
|
#define BAS_ERR_RESUME_WITHOUT_ERR 20
|
||||||
#define BAS_ERR_FOR_NESTING 26
|
#define BAS_ERR_FOR_NESTING 26
|
||||||
|
#define BAS_ERR_OUT_OF_STACK 28
|
||||||
#define BAS_ERR_BAD_OPCODE 51
|
#define BAS_ERR_BAD_OPCODE 51
|
||||||
#define BAS_ERR_BAD_FILE_NUM 52
|
#define BAS_ERR_BAD_FILE_NUM 52
|
||||||
#define BAS_ERR_FILE_NOT_FOUND 53
|
#define BAS_ERR_FILE_NOT_FOUND 53
|
||||||
#define BAS_ERR_BAD_FILE_MODE 54
|
#define BAS_ERR_BAD_FILE_MODE 54
|
||||||
|
#define BAS_ERR_DEVICE_IO 57
|
||||||
#define BAS_ERR_FILE_EXISTS 58
|
#define BAS_ERR_FILE_EXISTS 58
|
||||||
#define BAS_ERR_BAD_RECORD_LEN 59
|
#define BAS_ERR_BAD_RECORD_LEN 59
|
||||||
|
#define BAS_ERR_BAD_RECORD_NUM 63
|
||||||
#define BAS_ERR_TOO_MANY_FILES 67
|
#define BAS_ERR_TOO_MANY_FILES 67
|
||||||
|
#define BAS_ERR_DEVICE_UNAVAILABLE 68
|
||||||
#define BAS_ERR_PATH_FILE_ACCESS 75
|
#define BAS_ERR_PATH_FILE_ACCESS 75
|
||||||
#define BAS_ERR_PATH_NOT_FOUND 76
|
#define BAS_ERR_PATH_NOT_FOUND 76
|
||||||
|
#define BAS_ERR_OBJECT_NOT_SET 91
|
||||||
#define BAS_ERR_EXTERNAL_NOT_FOUND 453
|
#define BAS_ERR_EXTERNAL_NOT_FOUND 453
|
||||||
#define BAS_ERR_NO_FORM_RUNTIME 1000 // form/control statement with no form runtime attached (headless run)
|
#define BAS_ERR_NO_FORM_RUNTIME 1000 // form/control statement with no form runtime attached (headless run)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
// Module file header: 4-byte magic + uint16 version. A matched pair
|
// Module file header: 4-byte magic + uint16 version. A matched pair
|
||||||
|
|
@ -74,7 +75,7 @@
|
||||||
// so both sides pull from the same definition.
|
// so both sides pull from the same definition.
|
||||||
#define BAS_MOD_MAGIC "DBXM"
|
#define BAS_MOD_MAGIC "DBXM"
|
||||||
#define BAS_MOD_MAGIC_LEN 4
|
#define BAS_MOD_MAGIC_LEN 4
|
||||||
#define BAS_MOD_VERSION 1
|
#define BAS_MOD_VERSION 2 // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND
|
||||||
|
|
||||||
// Fixed header sizes on the wire. BAS_MOD_HEADER_SIZE is magic(4) +
|
// Fixed header sizes on the wire. BAS_MOD_HEADER_SIZE is magic(4) +
|
||||||
// version(2) + 6 int32 header fields(24) = 30. BAS_DBG_HEADER_SIZE is
|
// version(2) + 6 int32 header fields(24) = 30. BAS_DBG_HEADER_SIZE is
|
||||||
|
|
@ -83,14 +84,13 @@
|
||||||
#define BAS_DBG_HEADER_SIZE 4
|
#define BAS_DBG_HEADER_SIZE 4
|
||||||
|
|
||||||
// Minimum on-wire byte size of one entry in each variable-length table.
|
// Minimum on-wire byte size of one entry in each variable-length table.
|
||||||
// Used by readCount and the inline remaining-bytes count checks so that
|
// Used by countFits so that no bare integers appear at the call sites.
|
||||||
// no bare integers appear at the call sites.
|
|
||||||
#define BAS_CONST_MIN_ENTRY 2
|
#define BAS_CONST_MIN_ENTRY 2
|
||||||
#define BAS_DATA_MIN_ENTRY 2
|
#define BAS_DATA_MIN_ENTRY 2
|
||||||
#define BAS_PROC_MIN_ENTRY 18
|
#define BAS_PROC_MIN_ENTRY 18
|
||||||
#define BAS_FORMVAR_MIN_ENTRY 14
|
#define BAS_FORMVAR_MIN_ENTRY 14
|
||||||
#define BAS_GLOBALINIT_MIN_ENTRY 5
|
#define BAS_GLOBALINIT_MIN_ENTRY 5
|
||||||
#define BAS_DEBUGVAR_MIN_ENTRY 14
|
#define BAS_DEBUGVAR_MIN_ENTRY 15
|
||||||
#define BAS_BUF_INIT_CAP 4096
|
#define BAS_BUF_INIT_CAP 4096
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -116,20 +116,24 @@ typedef struct {
|
||||||
void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen);
|
void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen);
|
||||||
uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen);
|
uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen);
|
||||||
BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen);
|
BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen);
|
||||||
|
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
|
||||||
void basModuleFree(BasModuleT *mod);
|
void basModuleFree(BasModuleT *mod);
|
||||||
uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen);
|
uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen);
|
||||||
static void bufGrow(BufT *b, int32_t need);
|
static void bufGrow(BufT *b, int32_t need);
|
||||||
static void bufInit(BufT *b);
|
static void bufInit(BufT *b);
|
||||||
static void bufWrite(BufT *b, const void *data, int32_t len);
|
static void bufWrite(BufT *b, const void *data, int32_t len);
|
||||||
static void bufWriteI32(BufT *b, int32_t v);
|
static void bufWriteI32(BufT *b, int32_t v);
|
||||||
static void bufWriteStr(BufT *b, const char *s);
|
static void bufWriteStr(BufT *b, const char *s, int32_t len);
|
||||||
static void bufWriteU16(BufT *b, uint16_t v);
|
static void bufWriteU16(BufT *b, uint16_t v);
|
||||||
static void bufWriteU8(BufT *b, uint8_t v);
|
static void bufWriteU8(BufT *b, uint8_t v);
|
||||||
|
static bool countFits(ReaderT *r, int32_t count, int32_t minBytesPerEntry);
|
||||||
|
static bool moduleRangesValid(const BasModuleT *mod);
|
||||||
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
|
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
|
||||||
|
static float rF32(ReaderT *r);
|
||||||
static double rF64(ReaderT *r);
|
static double rF64(ReaderT *r);
|
||||||
static int32_t rI32(ReaderT *r);
|
static int32_t rI32(ReaderT *r);
|
||||||
static bool rOk(ReaderT *r, int32_t need);
|
static bool rOk(ReaderT *r, int32_t need);
|
||||||
static char *rStr(ReaderT *r);
|
static char *rStr(ReaderT *r, int32_t *outLen);
|
||||||
static void rStrInto(ReaderT *r, char *dst, int32_t cap);
|
static void rStrInto(ReaderT *r, char *dst, int32_t cap);
|
||||||
static uint16_t rU16(ReaderT *r);
|
static uint16_t rU16(ReaderT *r);
|
||||||
static uint8_t rU8(ReaderT *r);
|
static uint8_t rU8(ReaderT *r);
|
||||||
|
|
@ -158,20 +162,25 @@ void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen)
|
||||||
rStrInto(&r, v->formName, BAS_MAX_IDENT);
|
rStrInto(&r, v->formName, BAS_MAX_IDENT);
|
||||||
v->scope = rU8(&r);
|
v->scope = rU8(&r);
|
||||||
v->dataType = rU8(&r);
|
v->dataType = rU8(&r);
|
||||||
|
v->shared = rU8(&r) != 0;
|
||||||
v->index = rI32(&r);
|
v->index = rI32(&r);
|
||||||
v->procIndex = rI32(&r);
|
v->procIndex = rI32(&r);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A truncated debug resource would otherwise leave phantom entries
|
// A truncated debug resource would otherwise leave phantom entries
|
||||||
// (empty names, index 0 aliasing real global slot 0). Drop to
|
// (empty names, index 0 aliasing real global slot 0), and a
|
||||||
|
// procIndex outside the proc table would index past it. Drop to
|
||||||
// no-debug-info rather than display garbage.
|
// no-debug-info rather than display garbage.
|
||||||
|
for (int32_t i = 0; i < mod->debugVarCount && !r.err; i++) {
|
||||||
|
r.err = mod->debugVars[i].procIndex < -1 || mod->debugVars[i].procIndex >= mod->procCount;
|
||||||
|
}
|
||||||
|
|
||||||
if (r.err) {
|
if (r.err) {
|
||||||
free(mod->debugVars);
|
free(mod->debugVars);
|
||||||
mod->debugVars = NULL;
|
mod->debugVars = NULL;
|
||||||
mod->debugVarCount = 0;
|
mod->debugVarCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -188,10 +197,11 @@ uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen) {
|
||||||
|
|
||||||
for (int32_t i = 0; i < mod->debugVarCount; i++) {
|
for (int32_t i = 0; i < mod->debugVarCount; i++) {
|
||||||
const BasDebugVarT *v = &mod->debugVars[i];
|
const BasDebugVarT *v = &mod->debugVars[i];
|
||||||
bufWriteStr(&b, v->name);
|
bufWriteStr(&b, v->name, (int32_t)strlen(v->name));
|
||||||
bufWriteStr(&b, v->formName);
|
bufWriteStr(&b, v->formName, (int32_t)strlen(v->formName));
|
||||||
bufWriteU8(&b, v->scope);
|
bufWriteU8(&b, v->scope);
|
||||||
bufWriteU8(&b, v->dataType);
|
bufWriteU8(&b, v->dataType);
|
||||||
|
bufWriteU8(&b, v->shared ? 1 : 0);
|
||||||
bufWriteI32(&b, v->index);
|
bufWriteI32(&b, v->index);
|
||||||
bufWriteI32(&b, v->procIndex);
|
bufWriteI32(&b, v->procIndex);
|
||||||
}
|
}
|
||||||
|
|
@ -266,7 +276,7 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
r.pos += mod->codeLen;
|
r.pos += mod->codeLen;
|
||||||
|
|
||||||
// String constant pool
|
// String constant pool
|
||||||
if (mod->constCount > (r.len - r.pos) / BAS_CONST_MIN_ENTRY) {
|
if (!countFits(&r, mod->constCount, BAS_CONST_MIN_ENTRY)) {
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -282,14 +292,16 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < mod->constCount; i++) {
|
for (int32_t i = 0; i < mod->constCount; i++) {
|
||||||
char *s = rStr(&r);
|
int32_t len = 0;
|
||||||
mod->constants[i] = basStringNew(s ? s : "", s ? (int32_t)strlen(s) : 0);
|
char *s = rStr(&r, &len);
|
||||||
|
|
||||||
|
mod->constants[i] = basStringNew(s, len);
|
||||||
free(s);
|
free(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data pool
|
// Data pool
|
||||||
if (mod->dataCount > (r.len - r.pos) / BAS_DATA_MIN_ENTRY) {
|
if (!countFits(&r, mod->dataCount, BAS_DATA_MIN_ENTRY)) {
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -315,27 +327,19 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
mod->dataPool[i].longVal = rI32(&r);
|
mod->dataPool[i].longVal = rI32(&r);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BAS_TYPE_SINGLE: {
|
case BAS_TYPE_SINGLE:
|
||||||
float f;
|
mod->dataPool[i].sngVal = rF32(&r);
|
||||||
|
|
||||||
if (rOk(&r, sizeof(float))) {
|
|
||||||
memcpy(&f, r.data + r.pos, sizeof(float));
|
|
||||||
r.pos += sizeof(float);
|
|
||||||
} else {
|
|
||||||
f = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod->dataPool[i].sngVal = f;
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case BAS_TYPE_DOUBLE:
|
case BAS_TYPE_DOUBLE:
|
||||||
mod->dataPool[i].dblVal = rF64(&r);
|
mod->dataPool[i].dblVal = rF64(&r);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BAS_TYPE_STRING: {
|
case BAS_TYPE_STRING: {
|
||||||
char *s = rStr(&r);
|
int32_t len = 0;
|
||||||
mod->dataPool[i].strVal = basStringNew(s ? s : "", s ? (int32_t)strlen(s) : 0);
|
char *s = rStr(&r, &len);
|
||||||
|
|
||||||
|
mod->dataPool[i].strVal = basStringNew(s, len);
|
||||||
free(s);
|
free(s);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -346,10 +350,8 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// A non-scalar type byte means the stream is
|
// A non-scalar type byte means the stream is
|
||||||
// desynchronized; reject the whole module. Coerce
|
// desynchronized; reject the whole module (releasing
|
||||||
// this slot to a release-safe INTEGER first.
|
// this slot is a no-op: its payload pointer is NULL).
|
||||||
mod->dataPool[i].type = BAS_TYPE_INTEGER;
|
|
||||||
mod->dataPool[i].intVal = 0;
|
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -362,7 +364,7 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Procedure table
|
// Procedure table
|
||||||
if (mod->procCount > (r.len - r.pos) / BAS_PROC_MIN_ENTRY) {
|
if (!countFits(&r, mod->procCount, BAS_PROC_MIN_ENTRY)) {
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -428,8 +430,10 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
|
|
||||||
// A latched read error means the stream was truncated or corrupt; the
|
// A latched read error means the stream was truncated or corrupt; the
|
||||||
// reader returned benign defaults for everything past the fault, so the
|
// reader returned benign defaults for everything past the fault, so the
|
||||||
// module would load with zeroed proc/code addresses. Reject it.
|
// module would load with zeroed proc/code addresses. Reject it, and
|
||||||
if (r.err) {
|
// reject addresses and indices that point outside the module, so no
|
||||||
|
// consumer has to re-check them.
|
||||||
|
if (r.err || !moduleRangesValid(mod)) {
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
@ -438,6 +442,21 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) {
|
||||||
|
if (!mod || !mod->procs || !name) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||||
|
if (strcasecmp(mod->procs[i].name, name) == 0) {
|
||||||
|
return &mod->procs[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void basModuleFree(BasModuleT *mod) {
|
void basModuleFree(BasModuleT *mod) {
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -501,8 +520,8 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
|
||||||
|
|
||||||
// String constant pool
|
// String constant pool
|
||||||
for (int32_t i = 0; i < mod->constCount; i++) {
|
for (int32_t i = 0; i < mod->constCount; i++) {
|
||||||
const char *s = mod->constants[i] ? mod->constants[i]->data : "";
|
const BasStringT *s = mod->constants[i] ? mod->constants[i] : basEmptyString;
|
||||||
bufWriteStr(&b, s);
|
bufWriteStr(&b, s->data, s->len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data pool
|
// Data pool
|
||||||
|
|
@ -529,16 +548,20 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
|
||||||
bufWrite(&b, &v->dblVal, sizeof(double));
|
bufWrite(&b, &v->dblVal, sizeof(double));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BAS_TYPE_STRING:
|
case BAS_TYPE_STRING: {
|
||||||
bufWriteStr(&b, v->strVal ? v->strVal->data : "");
|
const BasStringT *str = v->strVal ? v->strVal : basEmptyString;
|
||||||
|
bufWriteStr(&b, str->data, str->len);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case BAS_TYPE_BOOLEAN:
|
case BAS_TYPE_BOOLEAN:
|
||||||
bufWriteU8(&b, v->boolVal ? 1 : 0);
|
bufWriteU8(&b, v->boolVal ? 1 : 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
bufWriteI32(&b, 0);
|
// The deserializer rejects any other type: fail the build
|
||||||
|
// here instead of emitting a module that can never load.
|
||||||
|
b.err = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -551,8 +574,8 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
|
||||||
bufWriteI32(&b, p->localCount);
|
bufWriteI32(&b, p->localCount);
|
||||||
bufWriteU8(&b, p->returnType);
|
bufWriteU8(&b, p->returnType);
|
||||||
bufWriteU8(&b, p->isFunction ? 1 : 0);
|
bufWriteU8(&b, p->isFunction ? 1 : 0);
|
||||||
bufWriteStr(&b, p->name);
|
bufWriteStr(&b, p->name, (int32_t)strlen(p->name));
|
||||||
bufWriteStr(&b, p->formName);
|
bufWriteStr(&b, p->formName, (int32_t)strlen(p->formName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form variable info (runtime-essential for per-form variable allocation)
|
// Form variable info (runtime-essential for per-form variable allocation)
|
||||||
|
|
@ -560,7 +583,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
|
||||||
|
|
||||||
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
|
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
|
||||||
const BasFormVarInfoT *fv = &mod->formVarInfo[i];
|
const BasFormVarInfoT *fv = &mod->formVarInfo[i];
|
||||||
bufWriteStr(&b, fv->formName);
|
bufWriteStr(&b, fv->formName, (int32_t)strlen(fv->formName));
|
||||||
bufWriteI32(&b, fv->varCount);
|
bufWriteI32(&b, fv->varCount);
|
||||||
bufWriteI32(&b, fv->initCodeAddr);
|
bufWriteI32(&b, fv->initCodeAddr);
|
||||||
bufWriteI32(&b, fv->initCodeLen);
|
bufWriteI32(&b, fv->initCodeLen);
|
||||||
|
|
@ -596,22 +619,25 @@ static void bufGrow(BufT *b, int32_t need) {
|
||||||
|
|
||||||
int32_t want = b->len + need;
|
int32_t want = b->len + need;
|
||||||
|
|
||||||
// cap can be 0 here, doubling 0 never reaches want, so seed it directly to
|
if (want <= b->cap) {
|
||||||
// escape the otherwise-infinite loop.
|
return;
|
||||||
if (b->cap < 1) {
|
|
||||||
b->cap = want;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (b->cap < want) {
|
// cap can be 0 here, doubling 0 never reaches want, so seed it directly to
|
||||||
if (b->cap > INT32_MAX / 2) {
|
// escape the otherwise-infinite loop. cap is only updated once the
|
||||||
b->cap = want;
|
// realloc succeeds, so it always describes the buffer actually held.
|
||||||
|
int32_t newCap = (b->cap < 1) ? want : b->cap;
|
||||||
|
|
||||||
|
while (newCap < want) {
|
||||||
|
if (newCap > INT32_MAX / 2) {
|
||||||
|
newCap = want;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
b->cap *= 2;
|
newCap *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t *nb = (uint8_t *)realloc(b->buf, b->cap);
|
uint8_t *nb = (uint8_t *)realloc(b->buf, newCap);
|
||||||
|
|
||||||
if (!nb) {
|
if (!nb) {
|
||||||
b->err = true;
|
b->err = true;
|
||||||
|
|
@ -619,6 +645,7 @@ static void bufGrow(BufT *b, int32_t need) {
|
||||||
}
|
}
|
||||||
|
|
||||||
b->buf = nb;
|
b->buf = nb;
|
||||||
|
b->cap = newCap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -652,9 +679,15 @@ static void bufWriteI32(BufT *b, int32_t v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void bufWriteStr(BufT *b, const char *s) {
|
// Writes a uint16 length prefix and len bytes. A string longer than the
|
||||||
uint16_t len = s ? (uint16_t)strlen(s) : 0;
|
// prefix can describe fails the build instead of wrapping silently.
|
||||||
bufWriteU16(b, len);
|
static void bufWriteStr(BufT *b, const char *s, int32_t len) {
|
||||||
|
if (len < 0 || len > UINT16_MAX) {
|
||||||
|
b->err = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bufWriteU16(b, (uint16_t)len);
|
||||||
|
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
bufWrite(b, s, len);
|
bufWrite(b, s, len);
|
||||||
|
|
@ -672,27 +705,74 @@ static void bufWriteU8(BufT *b, uint8_t v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Read an int32 count and validate it is non-negative and that the
|
// True when count is non-negative and count entries of at least
|
||||||
// declared minimum per-entry payload still fits in the remaining bytes.
|
// minBytesPerEntry bytes each still fit in the remaining input. Uses
|
||||||
// Uses division, not multiplication, to avoid overflow.
|
// division, not multiplication, to avoid overflow.
|
||||||
|
static bool countFits(ReaderT *r, int32_t count, int32_t minBytesPerEntry) {
|
||||||
|
return count >= 0 && count <= (r->len - r->pos) / minBytesPerEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Semantic checks on a module read from an untrusted file: every code
|
||||||
|
// address and slot index must point inside the module, and every
|
||||||
|
// procedure's local count must fit a call frame. entryPoint may equal
|
||||||
|
// codeLen (an empty program halts at once); a negative initCodeAddr means
|
||||||
|
// the form has no init code.
|
||||||
|
static bool moduleRangesValid(const BasModuleT *mod) {
|
||||||
|
if (mod->entryPoint < 0 || mod->entryPoint > mod->codeLen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||||
|
if (mod->procs[i].codeAddr < 0 || mod->procs[i].codeAddr >= mod->codeLen || mod->procs[i].localCount < 0 || mod->procs[i].localCount > BAS_VM_MAX_LOCALS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
|
||||||
|
const BasFormVarInfoT *fv = &mod->formVarInfo[i];
|
||||||
|
|
||||||
|
if (fv->varCount < 0 || fv->initCodeLen < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fv->initCodeAddr >= 0 && fv->initCodeAddr > mod->codeLen - fv->initCodeLen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < mod->globalInitCount; i++) {
|
||||||
|
if (mod->globalInits[i].index < 0 || mod->globalInits[i].index >= mod->globalCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Read an int32 count and validate it with countFits.
|
||||||
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) {
|
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) {
|
||||||
int32_t count = rI32(r);
|
int32_t count = rI32(r);
|
||||||
|
|
||||||
if (r->err || count < 0) {
|
if (r->err || !countFits(r, count, minBytesPerEntry)) {
|
||||||
r->err = true;
|
r->err = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minBytesPerEntry > 0) {
|
return count;
|
||||||
int32_t remaining = r->len - r->pos;
|
}
|
||||||
|
|
||||||
if (count > remaining / minBytesPerEntry) {
|
|
||||||
r->err = true;
|
static float rF32(ReaderT *r) {
|
||||||
return 0;
|
if (!rOk(r, sizeof(float))) {
|
||||||
}
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
float v;
|
||||||
|
memcpy(&v, r->data + r->pos, sizeof(float));
|
||||||
|
r->pos += sizeof(float);
|
||||||
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -722,9 +802,10 @@ static int32_t rI32(ReaderT *r) {
|
||||||
|
|
||||||
static bool rOk(ReaderT *r, int32_t need) {
|
static bool rOk(ReaderT *r, int32_t need) {
|
||||||
// r->len and r->pos are both non-negative and r->pos is within
|
// r->len and r->pos are both non-negative and r->pos is within
|
||||||
// [0, len], so r->len - r->pos cannot overflow; comparing against
|
// [0, len] (it only advances after this check), so r->len - r->pos
|
||||||
// need this way avoids the overflow of r->pos + need.
|
// cannot overflow; comparing against need this way avoids the
|
||||||
if (need < 0 || r->err || r->pos < 0 || need > r->len - r->pos) {
|
// overflow of r->pos + need.
|
||||||
|
if (need < 0 || r->err || need > r->len - r->pos) {
|
||||||
r->err = true;
|
r->err = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -733,20 +814,16 @@ static bool rOk(ReaderT *r, int32_t need) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Read a length-prefixed string into a malloc'd buffer.
|
// Read a length-prefixed string into a malloc'd, NUL-terminated buffer;
|
||||||
static char *rStr(ReaderT *r) {
|
// *outLen receives its length (the bytes may include NULs). NULL (length
|
||||||
|
// 0) when out of memory.
|
||||||
|
static char *rStr(ReaderT *r, int32_t *outLen) {
|
||||||
uint16_t len = rU16(r);
|
uint16_t len = rU16(r);
|
||||||
|
|
||||||
if (len == 0 || !rOk(r, len)) {
|
*outLen = 0;
|
||||||
char *s = (char *)malloc(1);
|
|
||||||
|
|
||||||
if (!s) {
|
if (!rOk(r, len)) {
|
||||||
r->err = true;
|
len = 0;
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
s[0] = '\0';
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
char *s = (char *)malloc(len + 1);
|
char *s = (char *)malloc(len + 1);
|
||||||
|
|
@ -757,8 +834,9 @@ static char *rStr(ReaderT *r) {
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(s, r->data + r->pos, len);
|
memcpy(s, r->data + r->pos, len);
|
||||||
s[len] = '\0';
|
s[len] = '\0';
|
||||||
r->pos += len;
|
r->pos += len;
|
||||||
|
*outLen = len;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -767,7 +845,9 @@ static char *rStr(ReaderT *r) {
|
||||||
// truncating to cap-1 chars. Wraps rStr so the reader position advances
|
// truncating to cap-1 chars. Wraps rStr so the reader position advances
|
||||||
// by the full on-wire string length regardless of truncation.
|
// by the full on-wire string length regardless of truncation.
|
||||||
static void rStrInto(ReaderT *r, char *dst, int32_t cap) {
|
static void rStrInto(ReaderT *r, char *dst, int32_t cap) {
|
||||||
char *s = rStr(r);
|
int32_t len = 0;
|
||||||
|
char *s = rStr(r, &len);
|
||||||
|
|
||||||
snprintf(dst, cap, "%s", s ? s : "");
|
snprintf(dst, cap, "%s", s ? s : "");
|
||||||
free(s);
|
free(s);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,11 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen);
|
||||||
// Returns NULL on failure.
|
// Returns NULL on failure.
|
||||||
BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen);
|
BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen);
|
||||||
|
|
||||||
// Free a deserialized module and all its allocated members.
|
// Find a procedure by name in a module's procedure table.
|
||||||
|
// Case-insensitive. Returns NULL if not found.
|
||||||
|
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
|
||||||
|
|
||||||
|
// Free a module (compiled or deserialized) and all its allocated members.
|
||||||
void basModuleFree(BasModuleT *mod);
|
void basModuleFree(BasModuleT *mod);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
#include "values.h"
|
#include "values.h"
|
||||||
#include "../compiler/opcodes.h"
|
#include "../compiler/opcodes.h"
|
||||||
|
#include "dvxTypes.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
@ -35,8 +36,13 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define BAS_STRING_IMMORTAL_REFCOUNT 999999 // sentinel refCount; empty string is never freed (see basStringUnref)
|
#define BAS_STRING_IMMORTAL_REFCOUNT 999999 // documents intent only: the empty string is never counted or freed (see basStringRef/Unref)
|
||||||
#define BAS_NUM_FORMAT_BUF_LEN 64 // scratch for the widest %g rendering of a double
|
#define BAS_NUM_FORMAT_BUF_LEN 64 // scratch for the widest %g rendering of a double
|
||||||
|
#define BAS_NUM_PARSE_BUF_LEN 64 // VAL: sign + mantissa digits + "e" + exponent
|
||||||
|
#define BAS_NUM_MANTISSA_DIGITS 40 // VAL: significant digits kept (well past a double's 17)
|
||||||
|
#define BAS_NUM_EXP_CLAMP 100000 // VAL: explicit exponents saturate here (far past a double's range)
|
||||||
|
#define BAS_BOOL_TRUE_TEXT "True"
|
||||||
|
#define BAS_BOOL_FALSE_TEXT "False"
|
||||||
#define BAS_SINGLE_SIG_DIGITS 7 // significant digits shown for SINGLE (VB3)
|
#define BAS_SINGLE_SIG_DIGITS 7 // significant digits shown for SINGLE (VB3)
|
||||||
#define BAS_DOUBLE_SIG_DIGITS 15 // significant digits shown for DOUBLE (VB3)
|
#define BAS_DOUBLE_SIG_DIGITS 15 // significant digits shown for DOUBLE (VB3)
|
||||||
#define BAS_RADIX_HEX 16
|
#define BAS_RADIX_HEX 16
|
||||||
|
|
@ -54,6 +60,8 @@ static struct {
|
||||||
} sEmptyStringStorage = { { .refCount = BAS_STRING_IMMORTAL_REFCOUNT, .len = 0, .cap = 1 }, '\0' };
|
} sEmptyStringStorage = { { .refCount = BAS_STRING_IMMORTAL_REFCOUNT, .len = 0, .cap = 1 }, '\0' };
|
||||||
BasStringT *basEmptyString = &sEmptyStringStorage.hdr;
|
BasStringT *basEmptyString = &sEmptyStringStorage.hdr;
|
||||||
|
|
||||||
|
bool basStringAllocFailed = false;
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical)
|
// Function prototypes (alphabetical)
|
||||||
void basArrayFree(BasArrayT *arr);
|
void basArrayFree(BasArrayT *arr);
|
||||||
|
|
@ -63,6 +71,7 @@ BasArrayT *basArrayRef(BasArrayT *arr);
|
||||||
void basArrayUnref(BasArrayT *arr);
|
void basArrayUnref(BasArrayT *arr);
|
||||||
double basParseNumber(const char *s);
|
double basParseNumber(const char *s);
|
||||||
BasStringT *basStringAlloc(int32_t cap);
|
BasStringT *basStringAlloc(int32_t cap);
|
||||||
|
bool basStringAppend(BasStringT **dst, const BasStringT *b);
|
||||||
int32_t basStringCompare(const BasStringT *a, const BasStringT *b);
|
int32_t basStringCompare(const BasStringT *a, const BasStringT *b);
|
||||||
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
|
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
|
||||||
BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b);
|
BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b);
|
||||||
|
|
@ -94,7 +103,10 @@ BasValueT basValStringFromC(const char *text);
|
||||||
BasValueT basValToBool(BasValueT v);
|
BasValueT basValToBool(BasValueT v);
|
||||||
BasValueT basValToDouble(BasValueT v);
|
BasValueT basValToDouble(BasValueT v);
|
||||||
int32_t basValToInt32(BasValueT v);
|
int32_t basValToInt32(BasValueT v);
|
||||||
|
BasValueT basValToInteger(BasValueT v);
|
||||||
|
BasValueT basValToLong(BasValueT v);
|
||||||
double basValToNumber(BasValueT v);
|
double basValToNumber(BasValueT v);
|
||||||
|
BasValueT basValToSingle(BasValueT v);
|
||||||
BasValueT basValToString(BasValueT v);
|
BasValueT basValToString(BasValueT v);
|
||||||
static int32_t formatDouble(double n, int32_t sigDigits, char *buf, int32_t bufSize);
|
static int32_t formatDouble(double n, int32_t sigDigits, char *buf, int32_t bufSize);
|
||||||
|
|
||||||
|
|
@ -123,19 +135,21 @@ int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t flatIdx = 0;
|
int32_t flatIdx = 0;
|
||||||
int32_t multiplier = 1;
|
int32_t multiplier = 1;
|
||||||
|
|
||||||
// Row-major order: last dimension varies fastest
|
// Row-major order: last dimension varies fastest. The subscript
|
||||||
|
// offset is computed in 64 bits: an extreme subscript minus the lower
|
||||||
|
// bound can overflow int32.
|
||||||
for (int32_t d = ndims - 1; d >= 0; d--) {
|
for (int32_t d = ndims - 1; d >= 0; d--) {
|
||||||
int32_t idx = indices[d] - arr->lbound[d];
|
int64_t idx = (int64_t)indices[d] - arr->lbound[d];
|
||||||
int32_t dimSize = arr->ubound[d] - arr->lbound[d] + 1;
|
int32_t dimSize = arr->ubound[d] - arr->lbound[d] + 1;
|
||||||
|
|
||||||
if (idx < 0 || idx >= dimSize) {
|
if (idx < 0 || idx >= dimSize) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
flatIdx += idx * multiplier;
|
flatIdx += (int32_t)idx * multiplier;
|
||||||
multiplier *= dimSize;
|
multiplier *= dimSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +243,7 @@ void basArrayUnref(BasArrayT *arr) {
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Value constructors / refcount helpers
|
// Number parsing (VAL)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
double basParseNumber(const char *s) {
|
double basParseNumber(const char *s) {
|
||||||
if (!s) {
|
if (!s) {
|
||||||
|
|
@ -246,63 +260,94 @@ double basParseNumber(const char *s) {
|
||||||
return (double)(int32_t)strtoul(s + 2, NULL, radix);
|
return (double)(int32_t)strtoul(s + 2, NULL, radix);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decimal: hand-scan the accepted span so atof's extra syntax (hex
|
// Decimal: hand-scan the accepted span so strtod's extra syntax (hex
|
||||||
// floats, inf, nan) is never reached, and 'D' exponents are accepted.
|
// floats, inf, nan) is never reached, and 'D' exponents are accepted.
|
||||||
char buf[BAS_NUM_FORMAT_BUF_LEN];
|
// The significant digits go to buf as one digit string scaled by a
|
||||||
int32_t len = 0;
|
// power of ten, so a digit run of any length keeps its magnitude:
|
||||||
const char *p = s;
|
// leading zeros carry no value and digits past the mantissa capacity
|
||||||
|
// only move the scale.
|
||||||
|
char buf[BAS_NUM_PARSE_BUF_LEN];
|
||||||
|
int32_t len = 0;
|
||||||
|
int32_t digits = 0;
|
||||||
|
int32_t expShift = 0;
|
||||||
|
bool sawDigit = false;
|
||||||
|
const char *p = s;
|
||||||
|
|
||||||
if (*p == '+' || *p == '-') {
|
if (*p == '+' || *p == '-') {
|
||||||
buf[len++] = *p++;
|
buf[len++] = *p++;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sawDigit = false;
|
while (isdigit((unsigned char)*p)) {
|
||||||
|
sawDigit = true;
|
||||||
|
|
||||||
while (isdigit((unsigned char)*p) && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
|
if (digits > 0 || *p != '0') {
|
||||||
buf[len++] = *p++;
|
if (digits < BAS_NUM_MANTISSA_DIGITS) {
|
||||||
sawDigit = true;
|
buf[len++] = *p;
|
||||||
|
digits++;
|
||||||
|
} else {
|
||||||
|
expShift++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*p == '.' && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
|
if (*p == '.') {
|
||||||
buf[len++] = *p++;
|
p++;
|
||||||
|
|
||||||
while (isdigit((unsigned char)*p) && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
|
// Every fraction digit up to the last one kept scales the result
|
||||||
buf[len++] = *p++;
|
// down one place; zeros before the first significant digit only
|
||||||
sawDigit = true;
|
// scale it.
|
||||||
|
while (isdigit((unsigned char)*p)) {
|
||||||
|
sawDigit = true;
|
||||||
|
|
||||||
|
if (digits < BAS_NUM_MANTISSA_DIGITS && (digits > 0 || *p != '0')) {
|
||||||
|
buf[len++] = *p;
|
||||||
|
digits++;
|
||||||
|
expShift--;
|
||||||
|
} else if (digits == 0) {
|
||||||
|
expShift--;
|
||||||
|
}
|
||||||
|
|
||||||
|
p++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sawDigit) {
|
if (!sawDigit || digits == 0) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((*p == 'E' || *p == 'e' || *p == 'D' || *p == 'd') && len < BAS_NUM_FORMAT_BUF_LEN - 3) {
|
int32_t expVal = 0;
|
||||||
const char *q = p + 1;
|
|
||||||
char expBuf[BAS_NUM_FORMAT_BUF_LEN];
|
if (*p == 'E' || *p == 'e' || *p == 'D' || *p == 'd') {
|
||||||
int32_t expLen = 0;
|
const char *q = p + 1;
|
||||||
|
bool expNeg = (*q == '-');
|
||||||
|
|
||||||
if (*q == '+' || *q == '-') {
|
if (*q == '+' || *q == '-') {
|
||||||
expBuf[expLen++] = *q++;
|
q++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isdigit((unsigned char)*q)) {
|
while (isdigit((unsigned char)*q)) {
|
||||||
while (isdigit((unsigned char)*q) && expLen < BAS_NUM_FORMAT_BUF_LEN - 1) {
|
if (expVal < BAS_NUM_EXP_CLAMP) {
|
||||||
expBuf[expLen++] = *q++;
|
expVal = expVal * 10 + (*q - '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len + 1 + expLen < BAS_NUM_FORMAT_BUF_LEN) {
|
q++;
|
||||||
buf[len++] = 'e';
|
}
|
||||||
memcpy(buf + len, expBuf, expLen);
|
|
||||||
len += expLen;
|
if (expNeg) {
|
||||||
}
|
expVal = -expVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buf[len] = '\0';
|
snprintf(buf + len, sizeof(buf) - len, "e%d", (int)(expVal + expShift));
|
||||||
return atof(buf);
|
return strtod(buf, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// String system
|
||||||
|
// ============================================================
|
||||||
BasStringT *basStringAlloc(int32_t cap) {
|
BasStringT *basStringAlloc(int32_t cap) {
|
||||||
if (cap < 1) {
|
if (cap < 1) {
|
||||||
cap = 1;
|
cap = 1;
|
||||||
|
|
@ -311,7 +356,8 @@ BasStringT *basStringAlloc(int32_t cap) {
|
||||||
BasStringT *s = (BasStringT *)malloc(sizeof(BasStringT) + cap);
|
BasStringT *s = (BasStringT *)malloc(sizeof(BasStringT) + cap);
|
||||||
|
|
||||||
if (!s) {
|
if (!s) {
|
||||||
return basStringRef(basEmptyString);
|
basStringAllocFailed = true;
|
||||||
|
return basEmptyString;
|
||||||
}
|
}
|
||||||
|
|
||||||
s->refCount = 1;
|
s->refCount = 1;
|
||||||
|
|
@ -322,8 +368,58 @@ BasStringT *basStringAlloc(int32_t cap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Appends b to *dst in place when *dst is not shared (refCount 1 and not
|
||||||
|
// the empty-string singleton). The buffer grows geometrically, so a loop
|
||||||
|
// of appends copies O(n) bytes overall instead of O(n^2). The result is
|
||||||
|
// truncated at BAS_STRING_MAX_LEN like basStringConcat. Returns false,
|
||||||
|
// changing nothing, when *dst is shared or cannot grow; the caller then
|
||||||
|
// concatenates the usual way.
|
||||||
|
bool basStringAppend(BasStringT **dst, const BasStringT *b) {
|
||||||
|
BasStringT *s = *dst;
|
||||||
|
|
||||||
|
if (s == basEmptyString || s->refCount != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t total = (int64_t)s->len + (int64_t)b->len;
|
||||||
|
|
||||||
|
if (total > BAS_STRING_MAX_LEN) {
|
||||||
|
total = BAS_STRING_MAX_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t newLen = (int32_t)total;
|
||||||
|
|
||||||
|
if (newLen + 1 > s->cap) {
|
||||||
|
int64_t newCap = (int64_t)s->cap * BAS_STRING_GROWTH;
|
||||||
|
|
||||||
|
if (newCap < newLen + 1) {
|
||||||
|
newCap = newLen + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newCap > (int64_t)BAS_STRING_MAX_LEN + 1) {
|
||||||
|
newCap = (int64_t)BAS_STRING_MAX_LEN + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BasStringT *grown = (BasStringT *)realloc(s, sizeof(BasStringT) + (size_t)newCap);
|
||||||
|
|
||||||
|
if (!grown) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
grown->cap = (int32_t)newCap;
|
||||||
|
s = grown;
|
||||||
|
*dst = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(s->data + s->len, b->data, (size_t)(newLen - s->len));
|
||||||
|
s->len = newLen;
|
||||||
|
s->data[newLen] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int32_t basStringCompare(const BasStringT *a, const BasStringT *b) {
|
int32_t basStringCompare(const BasStringT *a, const BasStringT *b) {
|
||||||
int32_t minLen = a->len < b->len ? a->len : b->len;
|
int32_t minLen = DVX_MIN(a->len, b->len);
|
||||||
int32_t cmp = memcmp(a->data, b->data, minLen);
|
int32_t cmp = memcmp(a->data, b->data, minLen);
|
||||||
|
|
||||||
if (cmp != 0) {
|
if (cmp != 0) {
|
||||||
|
|
@ -343,7 +439,7 @@ int32_t basStringCompare(const BasStringT *a, const BasStringT *b) {
|
||||||
|
|
||||||
|
|
||||||
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b) {
|
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b) {
|
||||||
int32_t minLen = a->len < b->len ? a->len : b->len;
|
int32_t minLen = DVX_MIN(a->len, b->len);
|
||||||
|
|
||||||
for (int32_t i = 0; i < minLen; i++) {
|
for (int32_t i = 0; i < minLen; i++) {
|
||||||
int32_t ca = toupper((unsigned char)a->data[i]);
|
int32_t ca = toupper((unsigned char)a->data[i]);
|
||||||
|
|
@ -373,10 +469,10 @@ BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b) {
|
||||||
total = BAS_STRING_MAX_LEN;
|
total = BAS_STRING_MAX_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t newLen = (int32_t)total;
|
int32_t newLen = (int32_t)total;
|
||||||
int32_t copyA = a->len < newLen ? a->len : newLen;
|
int32_t copyA = DVX_MIN(a->len, newLen);
|
||||||
int32_t copyB = newLen - copyA;
|
int32_t copyB = newLen - copyA;
|
||||||
BasStringT *s = basStringAlloc(newLen + 1);
|
BasStringT *s = basStringAlloc(newLen + 1);
|
||||||
|
|
||||||
if (s->cap >= newLen + 1) {
|
if (s->cap >= newLen + 1) {
|
||||||
memcpy(s->data, a->data, copyA);
|
memcpy(s->data, a->data, copyA);
|
||||||
|
|
@ -395,17 +491,22 @@ BasStringT *basStringNew(const char *text, int32_t len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
BasStringT *s = basStringAlloc(len + 1);
|
BasStringT *s = basStringAlloc(len + 1);
|
||||||
|
|
||||||
if (s->cap >= len + 1) {
|
if (s->cap >= len + 1) {
|
||||||
memcpy(s->data, text, len);
|
memcpy(s->data, text, len);
|
||||||
s->data[len] = '\0';
|
s->data[len] = '\0';
|
||||||
s->len = len;
|
s->len = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BasStringT *basStringRef(BasStringT *s) {
|
BasStringT *basStringRef(BasStringT *s) {
|
||||||
if (s) {
|
// The empty string is immortal and shared by every "" value; counting
|
||||||
|
// it would only overflow the counter eventually (basStringUnref skips
|
||||||
|
// it the same way).
|
||||||
|
if (s && s != basEmptyString) {
|
||||||
s->refCount++;
|
s->refCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -493,6 +594,10 @@ void basUdtFree(BasUdtT *udt) {
|
||||||
|
|
||||||
|
|
||||||
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount) {
|
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount) {
|
||||||
|
if (fieldCount < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
BasUdtT *udt = (BasUdtT *)calloc(1, sizeof(BasUdtT));
|
BasUdtT *udt = (BasUdtT *)calloc(1, sizeof(BasUdtT));
|
||||||
|
|
||||||
if (!udt) {
|
if (!udt) {
|
||||||
|
|
@ -502,11 +607,16 @@ BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount) {
|
||||||
udt->refCount = 1;
|
udt->refCount = 1;
|
||||||
udt->typeId = typeId;
|
udt->typeId = typeId;
|
||||||
udt->fieldCount = fieldCount;
|
udt->fieldCount = fieldCount;
|
||||||
udt->fields = (BasValueT *)calloc(fieldCount, sizeof(BasValueT));
|
|
||||||
|
|
||||||
if (!udt->fields) {
|
// calloc(0) may legitimately return NULL, so a TYPE with no fields
|
||||||
free(udt);
|
// simply has no field array.
|
||||||
return NULL;
|
if (fieldCount > 0) {
|
||||||
|
udt->fields = (BasValueT *)calloc(fieldCount, sizeof(BasValueT));
|
||||||
|
|
||||||
|
if (!udt->fields) {
|
||||||
|
free(udt);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return udt;
|
return udt;
|
||||||
|
|
@ -535,6 +645,9 @@ void basUdtUnref(BasUdtT *udt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Value constructors / refcount helpers
|
||||||
|
// ============================================================
|
||||||
BasValueT basValBool(bool v) {
|
BasValueT basValBool(bool v) {
|
||||||
BasValueT val;
|
BasValueT val;
|
||||||
val.type = BAS_TYPE_BOOLEAN;
|
val.type = BAS_TYPE_BOOLEAN;
|
||||||
|
|
@ -605,12 +718,10 @@ BasStringT *basValFormatString(BasValueT v) {
|
||||||
|
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case BAS_TYPE_INTEGER:
|
case BAS_TYPE_INTEGER:
|
||||||
snprintf(buf, sizeof(buf), "%d", (int)v.intVal);
|
return basStringNew(buf, snprintf(buf, sizeof(buf), "%d", (int)v.intVal));
|
||||||
return basStringNew(buf, (int32_t)strlen(buf));
|
|
||||||
|
|
||||||
case BAS_TYPE_LONG:
|
case BAS_TYPE_LONG:
|
||||||
snprintf(buf, sizeof(buf), "%ld", (long)v.longVal);
|
return basStringNew(buf, snprintf(buf, sizeof(buf), "%ld", (long)v.longVal));
|
||||||
return basStringNew(buf, (int32_t)strlen(buf));
|
|
||||||
|
|
||||||
case BAS_TYPE_SINGLE:
|
case BAS_TYPE_SINGLE:
|
||||||
return basStringNew(buf, formatDouble((double)v.sngVal, BAS_SINGLE_SIG_DIGITS, buf, sizeof(buf)));
|
return basStringNew(buf, formatDouble((double)v.sngVal, BAS_SINGLE_SIG_DIGITS, buf, sizeof(buf)));
|
||||||
|
|
@ -619,10 +730,14 @@ BasStringT *basValFormatString(BasValueT v) {
|
||||||
return basStringNew(buf, formatDouble(v.dblVal, BAS_DOUBLE_SIG_DIGITS, buf, sizeof(buf)));
|
return basStringNew(buf, formatDouble(v.dblVal, BAS_DOUBLE_SIG_DIGITS, buf, sizeof(buf)));
|
||||||
|
|
||||||
case BAS_TYPE_BOOLEAN:
|
case BAS_TYPE_BOOLEAN:
|
||||||
return basStringNew(v.boolVal ? "True" : "False", v.boolVal ? 4 : 5);
|
if (v.boolVal) {
|
||||||
|
return basStringNew(BAS_BOOL_TRUE_TEXT, (int32_t)sizeof(BAS_BOOL_TRUE_TEXT) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return basStringNew(BAS_BOOL_FALSE_TEXT, (int32_t)sizeof(BAS_BOOL_FALSE_TEXT) - 1);
|
||||||
|
|
||||||
case BAS_TYPE_STRING:
|
case BAS_TYPE_STRING:
|
||||||
return v.strVal ? basStringRef(v.strVal) : basStringRef(basEmptyString);
|
return basStringRef(v.strVal ? v.strVal : basEmptyString);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return basStringRef(basEmptyString);
|
return basStringRef(basEmptyString);
|
||||||
|
|
@ -728,7 +843,7 @@ BasValueT basValSingle(float v) {
|
||||||
BasValueT basValString(BasStringT *s) {
|
BasValueT basValString(BasStringT *s) {
|
||||||
BasValueT val;
|
BasValueT val;
|
||||||
val.type = BAS_TYPE_STRING;
|
val.type = BAS_TYPE_STRING;
|
||||||
val.strVal = s ? basStringRef(s) : basStringRef(basEmptyString);
|
val.strVal = basStringRef(s ? s : basEmptyString);
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -843,10 +958,9 @@ BasValueT basValToString(BasValueT v) {
|
||||||
return basValCopy(v);
|
return basValCopy(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
BasStringT *s = basValFormatString(v);
|
|
||||||
BasValueT result;
|
BasValueT result;
|
||||||
result.type = BAS_TYPE_STRING;
|
result.type = BAS_TYPE_STRING;
|
||||||
result.strVal = s;
|
result.strVal = basValFormatString(v);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,22 @@ typedef struct {
|
||||||
// Maximum string length in bytes (one byte reserved for the null terminator).
|
// Maximum string length in bytes (one byte reserved for the null terminator).
|
||||||
#define BAS_STRING_MAX_LEN (INT32_MAX - 1)
|
#define BAS_STRING_MAX_LEN (INT32_MAX - 1)
|
||||||
|
|
||||||
|
// Capacity multiplier when basStringAppend has to grow a string.
|
||||||
|
#define BAS_STRING_GROWTH 2
|
||||||
|
|
||||||
// Allocate a new string from a C string. refCount starts at 1.
|
// Allocate a new string from a C string. refCount starts at 1.
|
||||||
BasStringT *basStringNew(const char *text, int32_t len);
|
BasStringT *basStringNew(const char *text, int32_t len);
|
||||||
|
|
||||||
// Allocate an empty string with a given capacity.
|
// Allocate an empty string with a given capacity. When memory runs out
|
||||||
|
// the shared empty string (cap 1) comes back instead and
|
||||||
|
// basStringAllocFailed is set; the VM turns that into a runtime error.
|
||||||
BasStringT *basStringAlloc(int32_t cap);
|
BasStringT *basStringAlloc(int32_t cap);
|
||||||
|
|
||||||
// Increment reference count.
|
// Set when a string allocation failed and an empty string was substituted.
|
||||||
|
// The VM checks and clears it after every instruction.
|
||||||
|
extern bool basStringAllocFailed;
|
||||||
|
|
||||||
|
// Increment reference count (the immortal empty string is not counted).
|
||||||
BasStringT *basStringRef(BasStringT *s);
|
BasStringT *basStringRef(BasStringT *s);
|
||||||
|
|
||||||
// Decrement reference count. Frees if count reaches zero.
|
// Decrement reference count. Frees if count reaches zero.
|
||||||
|
|
@ -64,6 +73,10 @@ void basStringUnref(BasStringT *s);
|
||||||
// Concatenate two strings. Returns a new string (refCount 1).
|
// Concatenate two strings. Returns a new string (refCount 1).
|
||||||
BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b);
|
BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b);
|
||||||
|
|
||||||
|
// Append b to *dst in place (growing its buffer, which may move it) when
|
||||||
|
// *dst is not shared. Returns false and changes nothing otherwise.
|
||||||
|
bool basStringAppend(BasStringT **dst, const BasStringT *b);
|
||||||
|
|
||||||
// Substring. Returns a new string (refCount 1).
|
// Substring. Returns a new string (refCount 1).
|
||||||
BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len);
|
BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len);
|
||||||
|
|
||||||
|
|
@ -124,7 +137,8 @@ typedef struct {
|
||||||
BasValueT *fields; // array of field values
|
BasValueT *fields; // array of field values
|
||||||
} BasUdtT;
|
} BasUdtT;
|
||||||
|
|
||||||
// Allocate a new UDT instance. refCount starts at 1.
|
// Allocate a new UDT instance. refCount starts at 1. fieldCount may be
|
||||||
|
// zero; a negative count (or out of memory) returns NULL.
|
||||||
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
|
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
|
||||||
|
|
||||||
// Free all fields and release the UDT.
|
// Free all fields and release the UDT.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,9 @@
|
||||||
// vm.h -- DVX BASIC virtual machine
|
// vm.h -- DVX BASIC virtual machine
|
||||||
//
|
//
|
||||||
// Stack-based p-code interpreter. Executes compiled BASIC bytecode.
|
// Stack-based p-code interpreter. Executes compiled BASIC bytecode.
|
||||||
// Embeddable: the host provides I/O callbacks. No DVX dependencies.
|
// Embeddable: the host provides PRINT/INPUT/UI/extern callbacks. File,
|
||||||
|
// directory and path handling use the C library plus the DVX platform
|
||||||
|
// helpers (dvxPlat.h), so the VM links against the platform layer.
|
||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
// BasVmT *vm = basVmCreate();
|
// BasVmT *vm = basVmCreate();
|
||||||
|
|
@ -52,10 +54,11 @@
|
||||||
#define BAS_VM_MAX_GLOBALS 512 // global variable slots
|
#define BAS_VM_MAX_GLOBALS 512 // global variable slots
|
||||||
#define BAS_VM_MAX_LOCALS 64 // locals per stack frame
|
#define BAS_VM_MAX_LOCALS 64 // locals per stack frame
|
||||||
#define BAS_VM_MAX_FOR_DEPTH 32 // nested FOR loops
|
#define BAS_VM_MAX_FOR_DEPTH 32 // nested FOR loops
|
||||||
|
#define BAS_VM_GOSUB_STACK_SIZE 256 // pending GOSUB returns (all call frames together)
|
||||||
#define BAS_VM_MAX_FILES 16 // file channel array size; index 0 unused, so 15 usable channels (1..15)
|
#define BAS_VM_MAX_FILES 16 // file channel array size; index 0 unused, so 15 usable channels (1..15)
|
||||||
#define BAS_VM_RANDOM_RECORD_SIZE 128 // RANDOM-mode record length when OPEN has no LEN clause (bytes)
|
#define BAS_VM_RANDOM_RECORD_SIZE 128 // RANDOM-mode record length when OPEN has no LEN clause (bytes)
|
||||||
#define BAS_VM_MAX_RECORD_LEN 32767 // largest LEN= a RANDOM-mode OPEN accepts
|
#define BAS_VM_MAX_RECORD_LEN 32767 // largest LEN= a RANDOM-mode OPEN accepts
|
||||||
#define BAS_VM_DEFAULT_STEP_SLICE 10000 // bytecode steps per yield
|
#define BAS_VM_DEFAULT_STEP_SLICE 10000 // bytecode steps per yield (basVmRun slices and nested-call doEvents)
|
||||||
#define BAS_VM_MAX_CALL_ARGS 16 // max args to OP_CALL_METHOD / OP_CALL_EXTERN
|
#define BAS_VM_MAX_CALL_ARGS 16 // max args to OP_CALL_METHOD / OP_CALL_EXTERN
|
||||||
|
|
||||||
#define BAS_RGB_COMPONENT_MAX 255 // RGB(r,g,b): max per-channel value
|
#define BAS_RGB_COMPONENT_MAX 255 // RGB(r,g,b): max per-channel value
|
||||||
|
|
@ -213,6 +216,28 @@ typedef struct {
|
||||||
int32_t (*sqlAffectedRows)(int32_t db);
|
int32_t (*sqlAffectedRows)(int32_t db);
|
||||||
} BasSqlCallbacksT;
|
} BasSqlCallbacksT;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Printer callbacks (host-provided)
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// LPRINT and OPEN "LPT1:" / "LPT2:" / "LPT3:" / "PRN" reach the printer
|
||||||
|
// through these. open starts a job on LPT port 0..BAS_PRINTER_PORTS-1
|
||||||
|
// or on BAS_PRINTER_DEFAULT, the printer chosen in the settings (NULL =
|
||||||
|
// no printer there); write returns false once the output failed or was
|
||||||
|
// cancelled; close finishes the job and returns false when anything
|
||||||
|
// failed. Without open, LPRINT and a printer OPEN raise "Device
|
||||||
|
// unavailable".
|
||||||
|
|
||||||
|
#define BAS_PRINTER_DEFAULT (-1)
|
||||||
|
#define BAS_PRINTER_PORTS 3
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *(*open)(void *ctx, int32_t port);
|
||||||
|
bool (*write)(void *ctx, void *job, const char *data, int32_t len);
|
||||||
|
bool (*close)(void *ctx, void *job);
|
||||||
|
void *ctx;
|
||||||
|
} BasPrinterCallbacksT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// External library callbacks (host-provided)
|
// External library callbacks (host-provided)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -225,8 +250,8 @@ typedef struct {
|
||||||
|
|
||||||
// Resolve a function by library and symbol name.
|
// Resolve a function by library and symbol name.
|
||||||
// Returns an opaque function pointer, or NULL if not found.
|
// Returns an opaque function pointer, or NULL if not found.
|
||||||
// The VM caches the result so this is called only once per
|
// The VM caches the result per function-name constant, so this is
|
||||||
// unique (library, function) pair.
|
// called once per unique (library, function) pair.
|
||||||
typedef void *(*BasResolveExternFnT)(void *ctx, const char *libName, const char *funcName);
|
typedef void *(*BasResolveExternFnT)(void *ctx, const char *libName, const char *funcName);
|
||||||
|
|
||||||
// Call a resolved native function. funcPtr is the pointer returned
|
// Call a resolved native function. funcPtr is the pointer returned
|
||||||
|
|
@ -247,12 +272,10 @@ typedef struct {
|
||||||
// Extern function cache (resolved on first call)
|
// Extern function cache (resolved on first call)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define BAS_EXTERN_CACHE_SIZE 128
|
// One slot per constant-pool entry, indexed by the function-name constant.
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint16_t libNameIdx; // constant pool index for library name
|
|
||||||
uint16_t funcNameIdx; // constant pool index for function name
|
|
||||||
void *funcPtr; // resolved native function pointer (NULL = not yet resolved)
|
void *funcPtr; // resolved native function pointer (NULL = not yet resolved)
|
||||||
|
uint16_t libNameIdx; // constant pool index of the library the pointer came from
|
||||||
} BasExternCacheEntryT;
|
} BasExternCacheEntryT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -263,9 +286,13 @@ typedef struct {
|
||||||
int32_t returnPc; // instruction to return to
|
int32_t returnPc; // instruction to return to
|
||||||
int32_t localCount; // number of locals in this frame
|
int32_t localCount; // number of locals in this frame
|
||||||
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none)
|
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none)
|
||||||
|
int32_t resumePc; // RESUME target while this frame's handler runs
|
||||||
|
int32_t resumeNextPc; // RESUME NEXT target while this frame's handler runs (-1 = no safe continuation)
|
||||||
int32_t savedForDepth; // vm->forDepth when this frame was pushed (restored on return so EXIT SUB/FUNCTION inside FOR bodies cannot leak FOR frames)
|
int32_t savedForDepth; // vm->forDepth when this frame was pushed (restored on return so EXIT SUB/FUNCTION inside FOR bodies cannot leak FOR frames)
|
||||||
int32_t savedStmtPc; // caller's vm->stmtPc at call time (restored on return)
|
int32_t savedStmtPc; // caller's vm->stmtPc at call time (restored on return)
|
||||||
int32_t savedStmtSp; // caller's vm->stmtSp at call time (restored on return)
|
int32_t savedStmtSp; // caller's vm->stmtSp at call time (restored on return)
|
||||||
|
int32_t savedGosubDepth; // vm->gosubDepth when this frame was pushed: its own GOSUB returns sit above it (restored on return)
|
||||||
|
bool inHandler; // this frame's ON ERROR handler trapped an error and has not RESUMEd yet
|
||||||
BasValueT locals[BAS_VM_MAX_LOCALS];
|
BasValueT locals[BAS_VM_MAX_LOCALS];
|
||||||
} BasCallFrameT;
|
} BasCallFrameT;
|
||||||
|
|
||||||
|
|
@ -274,11 +301,13 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t varIdx; // loop variable slot index
|
int32_t varIdx; // loop variable slot index
|
||||||
uint8_t scopeTag; // BasScopeE value
|
uint8_t scopeTag; // BasScopeE value
|
||||||
BasValueT limit; // upper bound
|
bool integral; // limit and step are both INTEGER/LONG (integer NEXT fast path)
|
||||||
BasValueT step; // step value
|
bool stepFractional; // step has a fractional part (integer counters must widen to DOUBLE)
|
||||||
int32_t loopTop; // PC of the loop body start
|
BasValueT limit; // upper bound
|
||||||
|
BasValueT step; // step value
|
||||||
|
int32_t loopTop; // PC of the loop body start
|
||||||
} BasForStateT;
|
} BasForStateT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -287,6 +316,7 @@ typedef struct {
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void *handle; // FILE* or platform-specific
|
void *handle; // FILE* or platform-specific
|
||||||
|
void *printJob; // printer channel: the job the output (spooled in handle) goes to at CLOSE
|
||||||
int32_t mode; // BasFileModeE value
|
int32_t mode; // BasFileModeE value
|
||||||
int32_t recLen; // RANDOM-mode record length (OPEN ... LEN=)
|
int32_t recLen; // RANDOM-mode record length (OPEN ... LEN=)
|
||||||
} BasFileChannelT;
|
} BasFileChannelT;
|
||||||
|
|
@ -305,7 +335,7 @@ typedef struct {
|
||||||
char formName[BAS_MAX_IDENT]; // owning form (for form-scope vars), "" if global
|
char formName[BAS_MAX_IDENT]; // owning form (for form-scope vars), "" if global
|
||||||
int32_t codeAddr; // entry point in code[]
|
int32_t codeAddr; // entry point in code[]
|
||||||
int32_t paramCount; // number of parameters
|
int32_t paramCount; // number of parameters
|
||||||
int32_t localCount; // number of local variables (for debugger)
|
int32_t localCount; // local slots the procedure uses (call-frame size and debugger)
|
||||||
uint8_t returnType; // BAS_TYPE_* (0 for SUB)
|
uint8_t returnType; // BAS_TYPE_* (0 for SUB)
|
||||||
bool isFunction; // true = FUNCTION, false = SUB
|
bool isFunction; // true = FUNCTION, false = SUB
|
||||||
} BasProcEntryT;
|
} BasProcEntryT;
|
||||||
|
|
@ -330,8 +360,10 @@ typedef struct {
|
||||||
char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars (empty for others)
|
char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars (empty for others)
|
||||||
uint8_t scope; // SCOPE_GLOBAL, SCOPE_LOCAL, SCOPE_FORM
|
uint8_t scope; // SCOPE_GLOBAL, SCOPE_LOCAL, SCOPE_FORM
|
||||||
uint8_t dataType; // BAS_TYPE_*
|
uint8_t dataType; // BAS_TYPE_*
|
||||||
|
bool shared; // module variable (procIndex -1) that SUBs/FUNCTIONs see (DIM SHARED)
|
||||||
int32_t index; // variable slot index
|
int32_t index; // variable slot index
|
||||||
int32_t procIndex; // -1 for globals, else index into procs[]
|
int32_t procIndex; // -1 for module variables, else index into procs[]: a local, or a
|
||||||
|
// SHARED / STATIC name of that procedure for a module slot (scope GLOBAL/FORM)
|
||||||
} BasDebugVarT;
|
} BasDebugVarT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -429,6 +461,12 @@ typedef struct {
|
||||||
BasForStateT forStack[BAS_VM_MAX_FOR_DEPTH];
|
BasForStateT forStack[BAS_VM_MAX_FOR_DEPTH];
|
||||||
int32_t forDepth;
|
int32_t forDepth;
|
||||||
|
|
||||||
|
// GOSUB return addresses. Each call frame owns the entries above its
|
||||||
|
// savedGosubDepth, so RETURN only returns from a GOSUB made in the
|
||||||
|
// same SUB/FUNCTION (or at module level).
|
||||||
|
int32_t gosubStack[BAS_VM_GOSUB_STACK_SIZE];
|
||||||
|
int32_t gosubDepth;
|
||||||
|
|
||||||
// Global variables
|
// Global variables
|
||||||
BasValueT globals[BAS_VM_MAX_GLOBALS];
|
BasValueT globals[BAS_VM_MAX_GLOBALS];
|
||||||
|
|
||||||
|
|
@ -446,16 +484,26 @@ typedef struct {
|
||||||
int32_t printColumn;
|
int32_t printColumn;
|
||||||
int32_t usingPos;
|
int32_t usingPos;
|
||||||
|
|
||||||
// Error handling. The active ON ERROR handler lives on the call frame
|
// LPRINT state: the job opened by the first LPRINT (kept until the VM
|
||||||
// that installed it (BasCallFrameT.errorHandler); the dispatcher walks
|
// is destroyed), whether the running statement is an LPRINT, its own
|
||||||
// frames to find it.
|
// output column, and whether a write of this statement failed.
|
||||||
|
void *lprintJob;
|
||||||
|
bool lprinting;
|
||||||
|
bool lprintFailed;
|
||||||
|
int32_t lprintColumn;
|
||||||
|
|
||||||
|
// Error handling. ON ERROR state lives on the call frames: each frame
|
||||||
|
// records its own handler, whether that handler is running, and its
|
||||||
|
// RESUME targets (BasCallFrameT), so a SUB called from a handler has
|
||||||
|
// its own independent handler. The fields below are the Err state.
|
||||||
int32_t errorNumber; // current Err number
|
int32_t errorNumber; // current Err number
|
||||||
int32_t errorPc; // PC of the instruction that caused the error (for RESUME)
|
int32_t errorPc; // PC of the statement that raised the last error (host/debugger display; RESUME uses the frame's resumePc)
|
||||||
int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT)
|
|
||||||
int32_t errorLine; // source line where the error was raised (frozen; debuggers use this because currentLine keeps advancing while the error dialog pumps events)
|
int32_t errorLine; // source line where the error was raised (frozen; debuggers use this because currentLine keeps advancing while the error dialog pumps events)
|
||||||
bool inErrorHandler; // true when executing error handler code
|
|
||||||
char errorMsg[BAS_VM_ERROR_MSG_LEN]; // current error description
|
char errorMsg[BAS_VM_ERROR_MSG_LEN]; // current error description
|
||||||
|
|
||||||
|
// RND state: the last value returned (RND(0) repeats it).
|
||||||
|
double rndLast;
|
||||||
|
|
||||||
// I/O callbacks
|
// I/O callbacks
|
||||||
BasPrintFnT printFn;
|
BasPrintFnT printFn;
|
||||||
void *printCtx;
|
void *printCtx;
|
||||||
|
|
@ -467,15 +515,17 @@ typedef struct {
|
||||||
void *breakpointCtx;
|
void *breakpointCtx;
|
||||||
|
|
||||||
// UI callbacks (set by host for form/control support)
|
// UI callbacks (set by host for form/control support)
|
||||||
BasUiCallbacksT ui;
|
BasUiCallbacksT ui;
|
||||||
BasSqlCallbacksT sql;
|
BasSqlCallbacksT sql;
|
||||||
|
BasPrinterCallbacksT printer;
|
||||||
|
|
||||||
// External library callbacks (set by host)
|
// External library callbacks (set by host)
|
||||||
BasExternCallbacksT ext;
|
BasExternCallbacksT ext;
|
||||||
|
|
||||||
// Extern function cache (resolved on first call)
|
// Extern function cache (resolved on first call), allocated at load
|
||||||
BasExternCacheEntryT externCache[BAS_EXTERN_CACHE_SIZE];
|
// time with one entry per constant-pool slot.
|
||||||
int32_t externCacheCount;
|
BasExternCacheEntryT *externCache;
|
||||||
|
int32_t externCacheCount;
|
||||||
|
|
||||||
// Current form reference (set during event dispatch)
|
// Current form reference (set during event dispatch)
|
||||||
void *currentForm;
|
void *currentForm;
|
||||||
|
|
@ -486,6 +536,7 @@ typedef struct {
|
||||||
char appPath[DVX_MAX_PATH]; // App.Path -- project/app directory
|
char appPath[DVX_MAX_PATH]; // App.Path -- project/app directory
|
||||||
char appConfig[DVX_MAX_PATH]; // App.Config -- writable config directory
|
char appConfig[DVX_MAX_PATH]; // App.Config -- writable config directory
|
||||||
char appData[DVX_MAX_PATH]; // App.Data -- writable data directory
|
char appData[DVX_MAX_PATH]; // App.Data -- writable data directory
|
||||||
|
char *commandArgs; // COMMAND$ -- launch arguments (malloc'd copy, NULL = none; see basVmSetCommand)
|
||||||
|
|
||||||
// Dir$ iterator state -- per-VM so a second embedded VM (e.g. the IDE
|
// Dir$ iterator state -- per-VM so a second embedded VM (e.g. the IDE
|
||||||
// watch-expression evaluator) cannot clobber an in-progress enumeration.
|
// watch-expression evaluator) cannot clobber an in-progress enumeration.
|
||||||
|
|
@ -504,9 +555,10 @@ BasVmT *basVmCreate(void);
|
||||||
// Destroy a VM instance and free all resources.
|
// Destroy a VM instance and free all resources.
|
||||||
void basVmDestroy(BasVmT *vm);
|
void basVmDestroy(BasVmT *vm);
|
||||||
|
|
||||||
// Load a compiled module into the VM. A module that needs more global
|
// Load a compiled module into the VM. A NULL module, or one that needs
|
||||||
// slots than BAS_VM_MAX_GLOBALS is refused: vm->module stays NULL and
|
// more global slots than BAS_VM_MAX_GLOBALS, is refused: vm->module stays
|
||||||
// basVmRun reports BAS_VM_ERROR with the reason in basVmGetError().
|
// NULL and basVmRun reports BAS_VM_ERROR with the reason in basVmGetError().
|
||||||
|
// A successful load clears any error left by an earlier refused load.
|
||||||
void basVmLoadModule(BasVmT *vm, BasModuleT *module);
|
void basVmLoadModule(BasVmT *vm, BasModuleT *module);
|
||||||
|
|
||||||
// Execute the loaded module. Returns when the program ends,
|
// Execute the loaded module. Returns when the program ends,
|
||||||
|
|
@ -517,11 +569,18 @@ BasVmResultE basVmRun(BasVmT *vm);
|
||||||
// Useful for stepping/debugging.
|
// Useful for stepping/debugging.
|
||||||
BasVmResultE basVmStep(BasVmT *vm);
|
BasVmResultE basVmStep(BasVmT *vm);
|
||||||
|
|
||||||
|
// Set the launch arguments COMMAND$ returns (copied; NULL or "" = none).
|
||||||
|
// False when the copy cannot be allocated (COMMAND$ is then empty).
|
||||||
|
bool basVmSetCommand(BasVmT *vm, const char *args);
|
||||||
|
|
||||||
// Set I/O callbacks.
|
// Set I/O callbacks.
|
||||||
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
|
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
|
||||||
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
|
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
|
||||||
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
|
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
|
||||||
|
|
||||||
|
// Set the printer callbacks (LPRINT, OPEN "LPTn:").
|
||||||
|
void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer);
|
||||||
|
|
||||||
// Set UI callbacks (for form/control system).
|
// Set UI callbacks (for form/control system).
|
||||||
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
|
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
|
||||||
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
|
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
|
||||||
|
|
|
||||||
57
src/apps/kpunch/dvxbasic/stub/basModuleRes.c
Normal file
57
src/apps/kpunch/dvxbasic/stub/basModuleRes.c
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// basModuleRes.c -- load a compiled module (and its debug info) from an
|
||||||
|
// app's resources; shared by basstub and basrun.
|
||||||
|
|
||||||
|
#include "basModuleRes.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
|
||||||
|
BasModuleT *basModuleLoadRes(DvxResHandleT *res, const char **error) {
|
||||||
|
uint32_t modSize = 0;
|
||||||
|
uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize);
|
||||||
|
|
||||||
|
if (!modData) {
|
||||||
|
*error = "MODULE resource not found.";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize);
|
||||||
|
free(modData);
|
||||||
|
|
||||||
|
if (!mod) {
|
||||||
|
*error = "MODULE resource failed to deserialize.";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t dbgSize = 0;
|
||||||
|
uint8_t *dbgData = (uint8_t *)dvxResRead(res, BAS_RES_DEBUG, &dbgSize);
|
||||||
|
|
||||||
|
if (dbgData) {
|
||||||
|
basDebugDeserialize(mod, dbgData, (int32_t)dbgSize);
|
||||||
|
free(dbgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
43
src/apps/kpunch/dvxbasic/stub/basModuleRes.h
Normal file
43
src/apps/kpunch/dvxbasic/stub/basModuleRes.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// basModuleRes.h -- load a compiled module from an open .app resource set
|
||||||
|
//
|
||||||
|
// basstub (the shipped app stub) and basrun (the headless runner) both
|
||||||
|
// read the MODULE resource and the optional DEBUG resource the same
|
||||||
|
// way; this is the one copy of that sequence.
|
||||||
|
|
||||||
|
#ifndef BAS_MODULE_RES_H
|
||||||
|
#define BAS_MODULE_RES_H
|
||||||
|
|
||||||
|
#include "../basRes.h"
|
||||||
|
#include "../runtime/serialize.h"
|
||||||
|
#include "../runtime/vm.h"
|
||||||
|
#include "dvxRes.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Deserialize BAS_RES_MODULE from res and, when present, attach the
|
||||||
|
// BAS_RES_DEBUG info. Returns the module, or NULL with *error set to a
|
||||||
|
// static message.
|
||||||
|
BasModuleT *basModuleLoadRes(DvxResHandleT *res, const char **error);
|
||||||
|
|
||||||
|
#endif // BAS_MODULE_RES_H
|
||||||
|
|
@ -28,8 +28,8 @@
|
||||||
//
|
//
|
||||||
// The project file and all referenced source files (.bas, .frm)
|
// The project file and all referenced source files (.bas, .frm)
|
||||||
// are loaded relative to the directory containing the .dbp file.
|
// are loaded relative to the directory containing the .dbp file.
|
||||||
// The stub (basstub.app) is read from the same directory as the
|
// The app stub is the STUB resource embedded in this executable, and
|
||||||
// compiler executable.
|
// the fallback icon its NOICON resource.
|
||||||
|
|
||||||
#include "../compiler/parser.h"
|
#include "../compiler/parser.h"
|
||||||
#include "../compiler/symtab.h"
|
#include "../compiler/symtab.h"
|
||||||
|
|
@ -118,39 +118,18 @@ int main(int argc, char **argv) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the project file. prefsLoad treats a missing file as an
|
// Load the project file and derive its directory
|
||||||
// empty INI, so probe it first or a bad path would surface as a
|
PrefsHandleT *prefs = NULL;
|
||||||
// confusing "no modules" build error.
|
char projectDir[DVX_MAX_PATH];
|
||||||
FILE *dbpProbe = fopen(dbpPath, "rb");
|
|
||||||
|
|
||||||
if (!dbpProbe) {
|
if (!basBuildOpenProject(dbpPath, &prefs, projectDir, sizeof(projectDir))) {
|
||||||
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
|
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(dbpProbe);
|
// Read project metadata. prefsGetString returns "" for a present but
|
||||||
|
// empty Name=, which is treated like a missing one.
|
||||||
PrefsHandleT *prefs = prefsLoad(dbpPath);
|
const char *projName = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, "");
|
||||||
|
|
||||||
if (!prefs) {
|
|
||||||
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive project directory
|
|
||||||
char projectDir[DVX_MAX_PATH];
|
|
||||||
snprintf(projectDir, sizeof(projectDir), "%s", dbpPath);
|
|
||||||
char *sep = platformPathDirEnd(projectDir);
|
|
||||||
|
|
||||||
if (sep) {
|
|
||||||
*sep = '\0';
|
|
||||||
} else {
|
|
||||||
projectDir[0] = '.';
|
|
||||||
projectDir[1] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read project metadata
|
|
||||||
const char *projName = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, "App");
|
|
||||||
const char *author = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, "");
|
const char *author = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, "");
|
||||||
const char *publisher = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, "");
|
const char *publisher = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, "");
|
||||||
const char *version = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, "");
|
const char *version = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, "");
|
||||||
|
|
@ -159,14 +138,28 @@ int main(int argc, char **argv) {
|
||||||
const char *iconPath = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, "");
|
const char *iconPath = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, "");
|
||||||
const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
|
const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
|
||||||
|
|
||||||
// Derive output path
|
// Derive output path: the project Name, else the .dbp file's own
|
||||||
|
// base name (never a bare ".app").
|
||||||
char outBuf[DVX_MAX_PATH];
|
char outBuf[DVX_MAX_PATH];
|
||||||
|
|
||||||
if (!outputPath) {
|
if (!outputPath) {
|
||||||
snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName);
|
if (projName[0]) {
|
||||||
|
snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName);
|
||||||
|
} else {
|
||||||
|
const char *base = platformPathBaseName(dbpPath);
|
||||||
|
const char *dot = strrchr(base, '.');
|
||||||
|
int32_t baseLen = dot ? (int32_t)(dot - base) : (int32_t)strlen(base);
|
||||||
|
|
||||||
|
snprintf(outBuf, sizeof(outBuf), "%s/%.*s.app", projectDir, (int)baseLen, base);
|
||||||
|
}
|
||||||
|
|
||||||
outputPath = outBuf;
|
outputPath = outBuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!projName[0]) {
|
||||||
|
projName = BAS_DEFAULT_APP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
printf("Project: %s\n", projName);
|
printf("Project: %s\n", projName);
|
||||||
printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug");
|
printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug");
|
||||||
|
|
||||||
|
|
@ -196,7 +189,15 @@ int main(int argc, char **argv) {
|
||||||
parser->optionExplicit = project.optionExplicit;
|
parser->optionExplicit = project.optionExplicit;
|
||||||
|
|
||||||
if (!basParse(parser)) {
|
if (!basParse(parser)) {
|
||||||
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
|
const char *errPath = NULL;
|
||||||
|
int32_t errLine = 0;
|
||||||
|
|
||||||
|
if (basBuildMapLine(project.map, parser->errorLine, &errPath, &errLine)) {
|
||||||
|
fprintf(stderr, "Compile error at %s:%d: %s\n", errPath, (int)errLine, parser->error);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
|
||||||
|
}
|
||||||
|
|
||||||
basParserFree(parser);
|
basParserFree(parser);
|
||||||
free(parser);
|
free(parser);
|
||||||
goto failProject;
|
goto failProject;
|
||||||
|
|
@ -213,18 +214,6 @@ int main(int argc, char **argv) {
|
||||||
|
|
||||||
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
|
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
|
||||||
|
|
||||||
// Raw .frm texts; the shared build pipeline strips comments and, for
|
|
||||||
// release builds, obfuscates them.
|
|
||||||
char **frmSources = NULL; // stb_ds: owned form text
|
|
||||||
|
|
||||||
for (int32_t i = 0; i < (int32_t)arrlen(project.frmPaths); i++) {
|
|
||||||
char *fdata = platformReadFile(project.frmPaths[i], NULL);
|
|
||||||
|
|
||||||
if (fdata) {
|
|
||||||
arrput(frmSources, fdata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BasBuildSpecT spec;
|
BasBuildSpecT spec;
|
||||||
memset(&spec, 0, sizeof(spec));
|
memset(&spec, 0, sizeof(spec));
|
||||||
spec.projName = projName;
|
spec.projName = projName;
|
||||||
|
|
@ -238,18 +227,15 @@ int main(int argc, char **argv) {
|
||||||
spec.helpFile = helpFile;
|
spec.helpFile = helpFile;
|
||||||
spec.selfPath = selfPath(argv[0]);
|
spec.selfPath = selfPath(argv[0]);
|
||||||
spec.module = mod;
|
spec.module = mod;
|
||||||
spec.frmSources = (const char *const *)frmSources;
|
// Raw .frm texts as concatenation read them; the shared build
|
||||||
spec.frmCount = (int32_t)arrlen(frmSources);
|
// pipeline strips comments and, for release builds, obfuscates them.
|
||||||
|
spec.frmSources = (const char *const *)project.frmTexts;
|
||||||
|
spec.frmCount = (int32_t)arrlen(project.frmTexts);
|
||||||
spec.release = release;
|
spec.release = release;
|
||||||
spec.log = buildLog;
|
spec.log = buildLog;
|
||||||
|
|
||||||
const char *failure = basBuildApp(outputPath, &spec);
|
const char *failure = basBuildApp(outputPath, &spec);
|
||||||
|
|
||||||
for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) {
|
|
||||||
free(frmSources[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
arrfree(frmSources);
|
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
basBuildFreeProject(&project);
|
basBuildFreeProject(&project);
|
||||||
prefsClose(prefs);
|
prefsClose(prefs);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
// Options:
|
// Options:
|
||||||
// --steps N abort after N bytecode steps (0 = unlimited)
|
// --steps N abort after N bytecode steps (0 = unlimited)
|
||||||
// --data DIR directory for App.Config / App.Data (default: program dir)
|
// --data DIR directory for App.Config / App.Data (default: program dir)
|
||||||
|
// --args TEXT launch arguments returned by COMMAND$ (default: none)
|
||||||
// --compact .bas/.dbp only: strip + compact the module before running
|
// --compact .bas/.dbp only: strip + compact the module before running
|
||||||
// --quiet do not report runtime errors on stderr
|
// --quiet do not report runtime errors on stderr
|
||||||
//
|
//
|
||||||
|
|
@ -52,9 +53,9 @@
|
||||||
#include "../compiler/parser.h"
|
#include "../compiler/parser.h"
|
||||||
#include "../compiler/strip.h"
|
#include "../compiler/strip.h"
|
||||||
#include "../runtime/vm.h"
|
#include "../runtime/vm.h"
|
||||||
#include "../runtime/serialize.h"
|
|
||||||
#include "../basBuild.h"
|
#include "../basBuild.h"
|
||||||
#include "../basRes.h"
|
#include "../basRes.h"
|
||||||
|
#include "basModuleRes.h"
|
||||||
#include "dvxPrefs.h"
|
#include "dvxPrefs.h"
|
||||||
#include "dvxRes.h"
|
#include "dvxRes.h"
|
||||||
#include "dvxTypes.h"
|
#include "dvxTypes.h"
|
||||||
|
|
@ -72,17 +73,19 @@
|
||||||
|
|
||||||
#define BASRUN_STEP_SLICE BAS_VM_DEFAULT_STEP_SLICE
|
#define BASRUN_STEP_SLICE BAS_VM_DEFAULT_STEP_SLICE
|
||||||
#define BASRUN_INPUT_LINE_LEN 1024
|
#define BASRUN_INPUT_LINE_LEN 1024
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t steps;
|
int32_t steps;
|
||||||
bool compact;
|
bool compact;
|
||||||
bool quiet;
|
bool quiet;
|
||||||
const char *dataDir;
|
const char *dataDir;
|
||||||
|
const char *args;
|
||||||
const char *program;
|
const char *program;
|
||||||
} OptionsT;
|
} OptionsT;
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical; main last)
|
// Function prototypes (alphabetical; main last)
|
||||||
static BasModuleT *compileSource(const char *source, int32_t len, bool optionExplicit, const char *label);
|
static BasModuleT *compileSource(const char *source, int32_t len, bool optionExplicit, const char *label, const BasProjectSourceT *project);
|
||||||
static bool hostDoEvents(void *ctx);
|
static bool hostDoEvents(void *ctx);
|
||||||
static bool hostInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
|
static bool hostInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
|
||||||
static void hostPrint(void *ctx, const char *text, bool newline);
|
static void hostPrint(void *ctx, const char *text, bool newline);
|
||||||
|
|
@ -90,13 +93,14 @@ static BasModuleT *loadApp(const char *path);
|
||||||
static BasModuleT *loadBas(const char *path);
|
static BasModuleT *loadBas(const char *path);
|
||||||
static BasModuleT *loadDbp(const char *path);
|
static BasModuleT *loadDbp(const char *path);
|
||||||
static bool parseArgs(int argc, char **argv, OptionsT *opt);
|
static bool parseArgs(int argc, char **argv, OptionsT *opt);
|
||||||
static void programDir(const char *path, char *out, int32_t outSize);
|
|
||||||
static int32_t runModule(BasModuleT *mod, const OptionsT *opt);
|
static int32_t runModule(BasModuleT *mod, const OptionsT *opt);
|
||||||
static void usage(void);
|
static void usage(void);
|
||||||
int main(int argc, char **argv);
|
int main(int argc, char **argv);
|
||||||
|
|
||||||
|
|
||||||
static BasModuleT *compileSource(const char *source, int32_t len, bool optionExplicit, const char *label) {
|
// Compile source. project, when given, maps a compile-error line of the
|
||||||
|
// concatenated project source back to its file and line.
|
||||||
|
static BasModuleT *compileSource(const char *source, int32_t len, bool optionExplicit, const char *label, const BasProjectSourceT *project) {
|
||||||
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
||||||
|
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
|
|
@ -108,7 +112,14 @@ static BasModuleT *compileSource(const char *source, int32_t len, bool optionExp
|
||||||
parser->optionExplicit = optionExplicit;
|
parser->optionExplicit = optionExplicit;
|
||||||
|
|
||||||
if (!basParse(parser)) {
|
if (!basParse(parser)) {
|
||||||
fprintf(stderr, "basrun: %s: compile error at line %d: %s\n", label, (int)parser->errorLine, parser->error);
|
const char *errPath = label;
|
||||||
|
int32_t errLine = parser->errorLine;
|
||||||
|
|
||||||
|
if (project) {
|
||||||
|
basBuildMapLine(project->map, parser->errorLine, &errPath, &errLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "basrun: %s: compile error at line %d: %s\n", errPath, (int)errLine, parser->error);
|
||||||
basParserFree(parser);
|
basParserFree(parser);
|
||||||
free(parser);
|
free(parser);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -172,30 +183,11 @@ static BasModuleT *loadApp(const char *path) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t modSize = 0;
|
const char *loadError = NULL;
|
||||||
uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize);
|
BasModuleT *mod = basModuleLoadRes(res, &loadError);
|
||||||
|
|
||||||
if (!modData) {
|
|
||||||
fprintf(stderr, "basrun: %s: MODULE resource not found\n", path);
|
|
||||||
dvxResClose(res);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize);
|
|
||||||
free(modData);
|
|
||||||
|
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
fprintf(stderr, "basrun: %s: failed to deserialize module\n", path);
|
fprintf(stderr, "basrun: %s: %s\n", path, loadError);
|
||||||
dvxResClose(res);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t dbgSize = 0;
|
|
||||||
uint8_t *dbgData = (uint8_t *)dvxResRead(res, BAS_RES_DEBUG, &dbgSize);
|
|
||||||
|
|
||||||
if (dbgData) {
|
|
||||||
basDebugDeserialize(mod, dbgData, (int32_t)dbgSize);
|
|
||||||
free(dbgData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dvxResClose(res);
|
dvxResClose(res);
|
||||||
|
|
@ -212,39 +204,26 @@ static BasModuleT *loadBas(const char *path) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
BasModuleT *mod = compileSource(src, len, false, path);
|
BasModuleT *mod = compileSource(src, len, false, path, NULL);
|
||||||
free(src);
|
free(src);
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static BasModuleT *loadDbp(const char *path) {
|
static BasModuleT *loadDbp(const char *path) {
|
||||||
// prefsLoad opens a missing file as an empty INI; probe first so a
|
PrefsHandleT *prefs = NULL;
|
||||||
// bad path is reported as such.
|
char projectDir[DVX_MAX_PATH];
|
||||||
FILE *probe = fopen(path, "rb");
|
|
||||||
|
|
||||||
if (!probe) {
|
if (!basBuildOpenProject(path, &prefs, projectDir, sizeof(projectDir))) {
|
||||||
fprintf(stderr, "basrun: cannot open project %s\n", path);
|
fprintf(stderr, "basrun: cannot open project %s\n", path);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(probe);
|
|
||||||
|
|
||||||
PrefsHandleT *prefs = prefsLoad(path);
|
|
||||||
|
|
||||||
if (!prefs) {
|
|
||||||
fprintf(stderr, "basrun: cannot open project %s\n", path);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
char projectDir[DVX_MAX_PATH];
|
|
||||||
programDir(path, projectDir, sizeof(projectDir));
|
|
||||||
|
|
||||||
BasProjectSourceT project;
|
BasProjectSourceT project;
|
||||||
BasModuleT *mod = NULL;
|
BasModuleT *mod = NULL;
|
||||||
|
|
||||||
if (basBuildConcatProject(prefs, projectDir, &project)) {
|
if (basBuildConcatProject(prefs, projectDir, &project)) {
|
||||||
mod = compileSource(project.source, project.sourceLen, project.optionExplicit, path);
|
mod = compileSource(project.source, project.sourceLen, project.optionExplicit, path, &project);
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "basrun: %s: %s\n", path, project.error);
|
fprintf(stderr, "basrun: %s: %s\n", path, project.error);
|
||||||
}
|
}
|
||||||
|
|
@ -263,6 +242,8 @@ static bool parseArgs(int argc, char **argv, OptionsT *opt) {
|
||||||
opt->steps = (int32_t)strtol(argv[++i], NULL, 10);
|
opt->steps = (int32_t)strtol(argv[++i], NULL, 10);
|
||||||
} else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
||||||
opt->dataDir = argv[++i];
|
opt->dataDir = argv[++i];
|
||||||
|
} else if (strcmp(argv[i], "--args") == 0 && i + 1 < argc) {
|
||||||
|
opt->args = argv[++i];
|
||||||
} else if (strcmp(argv[i], "--compact") == 0) {
|
} else if (strcmp(argv[i], "--compact") == 0) {
|
||||||
opt->compact = true;
|
opt->compact = true;
|
||||||
} else if (strcmp(argv[i], "--quiet") == 0) {
|
} else if (strcmp(argv[i], "--quiet") == 0) {
|
||||||
|
|
@ -282,19 +263,6 @@ static bool parseArgs(int argc, char **argv, OptionsT *opt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void programDir(const char *path, char *out, int32_t outSize) {
|
|
||||||
snprintf(out, outSize, "%s", path);
|
|
||||||
char *sep = platformPathDirEnd(out);
|
|
||||||
|
|
||||||
if (sep) {
|
|
||||||
*sep = '\0';
|
|
||||||
} else {
|
|
||||||
out[0] = '.';
|
|
||||||
out[1] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static int32_t runModule(BasModuleT *mod, const OptionsT *opt) {
|
static int32_t runModule(BasModuleT *mod, const OptionsT *opt) {
|
||||||
BasVmT *vm = basVmCreate();
|
BasVmT *vm = basVmCreate();
|
||||||
|
|
||||||
|
|
@ -304,24 +272,33 @@ static int32_t runModule(BasModuleT *mod, const OptionsT *opt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
basVmLoadModule(vm, mod);
|
basVmLoadModule(vm, mod);
|
||||||
|
|
||||||
|
if (!vm->module) {
|
||||||
|
fprintf(stderr, "basrun: %s\n", basVmGetError(vm));
|
||||||
|
basVmDestroy(vm);
|
||||||
|
return BASRUN_EXIT_USAGE;
|
||||||
|
}
|
||||||
|
|
||||||
basVmSetPrintCallback(vm, hostPrint, NULL);
|
basVmSetPrintCallback(vm, hostPrint, NULL);
|
||||||
basVmSetInputCallback(vm, hostInput, NULL);
|
basVmSetInputCallback(vm, hostInput, NULL);
|
||||||
basVmSetDoEventsCallback(vm, hostDoEvents, NULL);
|
basVmSetDoEventsCallback(vm, hostDoEvents, NULL);
|
||||||
|
|
||||||
char dir[DVX_MAX_PATH];
|
char dir[DVX_MAX_PATH];
|
||||||
programDir(opt->program, dir, sizeof(dir));
|
basBuildDirOf(opt->program, dir, sizeof(dir));
|
||||||
|
|
||||||
const char *dataDir = opt->dataDir ? opt->dataDir : dir;
|
const char *dataDir = opt->dataDir ? opt->dataDir : dir;
|
||||||
snprintf(vm->appPath, DVX_MAX_PATH, "%s", dir);
|
snprintf(vm->appPath, sizeof(vm->appPath), "%s", dir);
|
||||||
snprintf(vm->appConfig, DVX_MAX_PATH, "%s", dataDir);
|
snprintf(vm->appConfig, sizeof(vm->appConfig), "%s", dataDir);
|
||||||
snprintf(vm->appData, DVX_MAX_PATH, "%s", dataDir);
|
snprintf(vm->appData, sizeof(vm->appData), "%s", dataDir);
|
||||||
|
basVmSetCommand(vm, opt->args);
|
||||||
|
|
||||||
// Run in slices so a --steps cap can be enforced across the whole run.
|
// Run in slices so a --steps cap can be enforced across the whole run.
|
||||||
basVmSetStepLimit(vm, BASRUN_STEP_SLICE);
|
basVmSetStepLimit(vm, BASRUN_STEP_SLICE);
|
||||||
|
|
||||||
int32_t total = 0;
|
// 64-bit: an uncapped host run passes 2^31 steps within seconds.
|
||||||
int32_t exit = BASRUN_EXIT_OK;
|
int64_t total = 0;
|
||||||
BasVmResultE result = basVmRun(vm);
|
int32_t exitCode = BASRUN_EXIT_OK;
|
||||||
|
BasVmResultE result = basVmRun(vm);
|
||||||
|
|
||||||
while (result == BAS_VM_STEP_LIMIT) {
|
while (result == BAS_VM_STEP_LIMIT) {
|
||||||
total += vm->stepCount;
|
total += vm->stepCount;
|
||||||
|
|
@ -331,7 +308,7 @@ static int32_t runModule(BasModuleT *mod, const OptionsT *opt) {
|
||||||
fprintf(stderr, "basrun: step cap of %d reached at line %d\n", (int)opt->steps, (int)vm->currentLine);
|
fprintf(stderr, "basrun: step cap of %d reached at line %d\n", (int)opt->steps, (int)vm->currentLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
exit = BASRUN_EXIT_STEPS;
|
exitCode = BASRUN_EXIT_STEPS;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,18 +326,18 @@ static int32_t runModule(BasModuleT *mod, const OptionsT *opt) {
|
||||||
errNum = 1;
|
errNum = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
exit = errNum > BASRUN_EXIT_ERR_MAX ? BASRUN_EXIT_ERR_MAX : errNum;
|
exitCode = DVX_MIN(errNum, BASRUN_EXIT_ERR_MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
basVmDestroy(vm);
|
basVmDestroy(vm);
|
||||||
return exit;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void usage(void) {
|
static void usage(void) {
|
||||||
fprintf(stderr, "DVX BASIC headless runner\n\n");
|
fprintf(stderr, "DVX BASIC headless runner\n\n");
|
||||||
fprintf(stderr, "Usage: basrun [--steps N] [--data DIR] [--compact] [--quiet] program.app|program.bas|project.dbp\n");
|
fprintf(stderr, "Usage: basrun [--steps N] [--data DIR] [--args TEXT] [--compact] [--quiet] program.app|program.bas|project.dbp\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -394,7 +371,7 @@ int main(int argc, char **argv) {
|
||||||
basCompactBytecode(mod);
|
basCompactBytecode(mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t exit = runModule(mod, &opt);
|
int32_t exitCode = runModule(mod, &opt);
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
return exit;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@
|
||||||
#include "shellApp.h"
|
#include "shellApp.h"
|
||||||
#include "../basRes.h"
|
#include "../basRes.h"
|
||||||
#include "../runtime/vm.h"
|
#include "../runtime/vm.h"
|
||||||
#include "../runtime/serialize.h"
|
|
||||||
#include "../formrt/formrt.h"
|
#include "../formrt/formrt.h"
|
||||||
|
#include "basModuleRes.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
@ -52,13 +52,13 @@
|
||||||
|
|
||||||
#define STUB_STACK_SIZE 65536
|
#define STUB_STACK_SIZE 65536
|
||||||
|
|
||||||
AppDescriptorT appDescriptor = {
|
SHELL_APP_DESCRIPTOR(
|
||||||
.name = "BASIC App",
|
.name = BAS_DEFAULT_APP_NAME,
|
||||||
.hasMainLoop = true,
|
.hasMainLoop = true,
|
||||||
.multiInstance = false,
|
.multiInstance = false,
|
||||||
.stackSize = STUB_STACK_SIZE,
|
.stackSize = STUB_STACK_SIZE,
|
||||||
.priority = TS_PRIORITY_NORMAL
|
.priority = TS_PRIORITY_NORMAL
|
||||||
};
|
);
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -66,11 +66,13 @@ AppDescriptorT appDescriptor = {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static AppContextT *sAc = NULL;
|
static AppContextT *sAc = NULL;
|
||||||
|
static BasFormRtT *sRt = NULL; // live while appMain runs the program (F1 queries it)
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes (alphabetical; appMain last)
|
// Function prototypes (alphabetical; appMain last)
|
||||||
void appShutdown(void);
|
void appShutdown(void);
|
||||||
static bool stubDoEvents(void *ctx);
|
static bool stubDoEvents(void *ctx);
|
||||||
|
static void stubHelpQuery(void *ctx);
|
||||||
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
|
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
|
||||||
static void stubPrint(void *ctx, const char *text, bool newline);
|
static void stubPrint(void *ctx, const char *text, bool newline);
|
||||||
int32_t appMain(DxeAppContextT *ctx);
|
int32_t appMain(DxeAppContextT *ctx);
|
||||||
|
|
@ -97,6 +99,16 @@ static bool stubDoEvents(void *ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// F1: refresh the app's help topic from the focused control or form
|
||||||
|
// (the shell then opens helpFile at it; "" opens its default topic).
|
||||||
|
static void stubHelpQuery(void *ctx) {
|
||||||
|
DxeAppContextT *dxe = (DxeAppContextT *)ctx;
|
||||||
|
|
||||||
|
dxe->helpTopic[0] = '\0';
|
||||||
|
basFormRtHelpTopic(sRt, dxe->helpTopic, sizeof(dxe->helpTopic));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
|
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
buf[0] = '\0';
|
buf[0] = '\0';
|
||||||
|
|
@ -152,34 +164,16 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
free(helpName);
|
free(helpName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load MODULE resource
|
// Load MODULE (and optional DEBUG) resource
|
||||||
uint32_t modSize = 0;
|
const char *loadError = NULL;
|
||||||
uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize);
|
BasModuleT *mod = basModuleLoadRes(res, &loadError);
|
||||||
|
|
||||||
if (!modData) {
|
|
||||||
dvxMessageBox(sAc, "Error", "MODULE resource not found.", 0);
|
|
||||||
dvxResClose(res);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize);
|
|
||||||
free(modData);
|
|
||||||
|
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
dvxMessageBox(sAc, "Error", "Failed to deserialize module.", 0);
|
dvxMessageBox(sAc, "Error", loadError, 0);
|
||||||
dvxResClose(res);
|
dvxResClose(res);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load optional DEBUG resource
|
|
||||||
uint32_t dbgSize = 0;
|
|
||||||
uint8_t *dbgData = (uint8_t *)dvxResRead(res, BAS_RES_DEBUG, &dbgSize);
|
|
||||||
|
|
||||||
if (dbgData) {
|
|
||||||
basDebugDeserialize(mod, dbgData, (int32_t)dbgSize);
|
|
||||||
free(dbgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create VM
|
// Create VM
|
||||||
BasVmT *vm = basVmCreate();
|
BasVmT *vm = basVmCreate();
|
||||||
|
|
||||||
|
|
@ -191,6 +185,17 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
basVmLoadModule(vm, mod);
|
basVmLoadModule(vm, mod);
|
||||||
|
|
||||||
|
// basVmLoadModule refuses a module the VM cannot hold (too many
|
||||||
|
// globals); stop before any form is loaded or shown.
|
||||||
|
if (!vm->module) {
|
||||||
|
dvxMessageBox(sAc, "Error", basVmGetError(vm), 0);
|
||||||
|
basVmDestroy(vm);
|
||||||
|
basModuleFree(mod);
|
||||||
|
dvxResClose(res);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
basVmSetPrintCallback(vm, stubPrint, NULL);
|
basVmSetPrintCallback(vm, stubPrint, NULL);
|
||||||
basVmSetInputCallback(vm, stubInput, NULL);
|
basVmSetInputCallback(vm, stubInput, NULL);
|
||||||
basVmSetDoEventsCallback(vm, stubDoEvents, NULL);
|
basVmSetDoEventsCallback(vm, stubDoEvents, NULL);
|
||||||
|
|
@ -199,12 +204,15 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
// App.Config and App.Data are writable subdirectories created on
|
// App.Config and App.Data are writable subdirectories created on
|
||||||
// demand. The IDE does the same split for project debugging, so
|
// demand. The IDE does the same split for project debugging, so
|
||||||
// behavior matches between compiled apps and in-IDE runs.
|
// behavior matches between compiled apps and in-IDE runs.
|
||||||
snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir);
|
snprintf(vm->appPath, sizeof(vm->appPath), "%s", ctx->appDir);
|
||||||
snprintf(vm->appConfig, DVX_MAX_PATH, "%s" DVX_PATH_SEP "CONFIG", ctx->appDir);
|
snprintf(vm->appConfig, sizeof(vm->appConfig), "%s" DVX_PATH_SEP "CONFIG", ctx->appDir);
|
||||||
snprintf(vm->appData, DVX_MAX_PATH, "%s" DVX_PATH_SEP "DATA", ctx->appDir);
|
snprintf(vm->appData, sizeof(vm->appData), "%s" DVX_PATH_SEP "DATA", ctx->appDir);
|
||||||
platformMkdirRecursive(vm->appConfig);
|
platformMkdirRecursive(vm->appConfig);
|
||||||
platformMkdirRecursive(vm->appData);
|
platformMkdirRecursive(vm->appData);
|
||||||
|
|
||||||
|
// COMMAND$: the arguments the program was started with
|
||||||
|
basVmSetCommand(vm, ctx->args);
|
||||||
|
|
||||||
// Set extern call callbacks (required for DECLARE LIBRARY functions)
|
// Set extern call callbacks (required for DECLARE LIBRARY functions)
|
||||||
BasExternCallbacksT extCb;
|
BasExternCallbacksT extCb;
|
||||||
memset(&extCb, 0, sizeof(extCb));
|
memset(&extCb, 0, sizeof(extCb));
|
||||||
|
|
@ -265,6 +273,11 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
|
|
||||||
dvxResClose(res);
|
dvxResClose(res);
|
||||||
|
|
||||||
|
// F1 follows the focused control's or form's HelpTopic
|
||||||
|
sRt = rt;
|
||||||
|
ctx->onHelpQuery = stubHelpQuery;
|
||||||
|
ctx->helpQueryCtx = ctx;
|
||||||
|
|
||||||
// Load all cached forms and show the startup form
|
// Load all cached forms and show the startup form
|
||||||
basFormRtLoadAllForms(rt, NULL);
|
basFormRtLoadAllForms(rt, NULL);
|
||||||
|
|
||||||
|
|
@ -272,6 +285,9 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
basFormRtRunSimple(rt);
|
basFormRtRunSimple(rt);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
|
ctx->onHelpQuery = NULL;
|
||||||
|
ctx->helpQueryCtx = NULL;
|
||||||
|
sRt = NULL;
|
||||||
basFormRtDestroy(rt);
|
basFormRtDestroy(rt);
|
||||||
basVmDestroy(vm);
|
basVmDestroy(vm);
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
|
|
|
||||||
83
src/apps/kpunch/dvxbasic/testCapture.h
Normal file
83
src/apps/kpunch/dvxbasic/testCapture.h
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
|
||||||
|
// testCapture.h -- PRINT capture shared by the host test harnesses
|
||||||
|
// (test_compiler, test_compact, test_suite)
|
||||||
|
//
|
||||||
|
// A fixed buffer that collects everything a VM PRINTs, for comparison
|
||||||
|
// against a test's expected output. Output past the buffer is dropped.
|
||||||
|
|
||||||
|
#ifndef DVXBASIC_TEST_CAPTURE_H
|
||||||
|
#define DVXBASIC_TEST_CAPTURE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define CAPTURE_MAX 8192
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char buf[CAPTURE_MAX];
|
||||||
|
int32_t len;
|
||||||
|
} CaptureT;
|
||||||
|
|
||||||
|
static inline void captureAppend(CaptureT *c, const char *s);
|
||||||
|
static inline void capturePrint(void *ctx, const char *text, bool newline);
|
||||||
|
static inline void captureReset(CaptureT *c);
|
||||||
|
|
||||||
|
|
||||||
|
static inline void captureAppend(CaptureT *c, const char *s) {
|
||||||
|
int32_t n = (int32_t)strlen(s);
|
||||||
|
|
||||||
|
if (c->len + n >= CAPTURE_MAX - 1) {
|
||||||
|
n = CAPTURE_MAX - 1 - c->len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n > 0) {
|
||||||
|
memcpy(c->buf + c->len, s, n);
|
||||||
|
c->len += n;
|
||||||
|
c->buf[c->len] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// BasPrintFnT for basVmSetPrintCallback; ctx is the CaptureT.
|
||||||
|
static inline void capturePrint(void *ctx, const char *text, bool newline) {
|
||||||
|
CaptureT *c = (CaptureT *)ctx;
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
captureAppend(c, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newline) {
|
||||||
|
captureAppend(c, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void captureReset(CaptureT *c) {
|
||||||
|
c->buf[0] = '\0';
|
||||||
|
c->len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DVXBASIC_TEST_CAPTURE_H
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
#include "runtime/vm.h"
|
#include "runtime/vm.h"
|
||||||
#include "compiler/opcodes.h"
|
#include "compiler/opcodes.h"
|
||||||
#include "runtime/values.h"
|
#include "runtime/values.h"
|
||||||
|
#include "testCapture.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
@ -39,8 +40,6 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
#define MAX_OUT 65536
|
|
||||||
|
|
||||||
// Every possible opcode byte, for the operand metadata sweep.
|
// Every possible opcode byte, for the operand metadata sweep.
|
||||||
#define TEST_OPCODE_SPACE 256
|
#define TEST_OPCODE_SPACE 256
|
||||||
|
|
||||||
|
|
@ -48,18 +47,12 @@
|
||||||
#define TEST_UNDEFINED_OPCODE 0xFD
|
#define TEST_UNDEFINED_OPCODE 0xFD
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char *buf;
|
|
||||||
int32_t len;
|
|
||||||
int32_t cap;
|
|
||||||
} OutBufT;
|
|
||||||
|
|
||||||
|
|
||||||
// Function prototypes
|
// Function prototypes
|
||||||
static void captureCallback(void *ctx, const char *text, bool newline);
|
|
||||||
static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeLen);
|
static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeLen);
|
||||||
static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen);
|
static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen);
|
||||||
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap);
|
static bool runAndCapture(const char *source, bool compact, CaptureT *cap);
|
||||||
static void testCompact(const char *name, const char *source);
|
static void testCompact(const char *name, const char *source);
|
||||||
static void testCraftedTables(void);
|
static void testCraftedTables(void);
|
||||||
static void testOperandKinds(void);
|
static void testOperandKinds(void);
|
||||||
|
|
@ -69,23 +62,7 @@ static int32_t sTotal = 0;
|
||||||
static int32_t sFailed = 0;
|
static int32_t sFailed = 0;
|
||||||
|
|
||||||
|
|
||||||
static void captureCallback(void *ctx, const char *text, bool newline) {
|
// Compaction must refuse the stream (return 0) and leave it untouched.
|
||||||
OutBufT *ob = (OutBufT *)ctx;
|
|
||||||
int32_t tlen = (int32_t)strlen(text);
|
|
||||||
|
|
||||||
if (ob->len + tlen + 2 >= ob->cap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(ob->buf + ob->len, text, tlen);
|
|
||||||
ob->len += tlen;
|
|
||||||
|
|
||||||
if (newline) {
|
|
||||||
ob->buf[ob->len++] = '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeLen) {
|
static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeLen) {
|
||||||
sTotal++;
|
sTotal++;
|
||||||
|
|
||||||
|
|
@ -103,6 +80,8 @@ static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A module built directly from raw bytes, exercising validation paths a
|
||||||
|
// compiled program can never produce.
|
||||||
static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen) {
|
static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen) {
|
||||||
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
|
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
|
||||||
|
|
||||||
|
|
@ -114,21 +93,24 @@ static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap) {
|
// Compile source (stripped and compacted when compact) and run it,
|
||||||
|
// capturing its PRINT output. False on a compile or runtime error.
|
||||||
|
static bool runAndCapture(const char *source, bool compact, CaptureT *cap) {
|
||||||
BasParserT parser;
|
BasParserT parser;
|
||||||
basParserInit(&parser, source, (int32_t)strlen(source));
|
basParserInit(&parser, source, (int32_t)strlen(source));
|
||||||
|
captureReset(cap);
|
||||||
|
|
||||||
if (!basParse(&parser)) {
|
if (!basParse(&parser)) {
|
||||||
fprintf(stderr, "compile error: %s\n", parser.error);
|
fprintf(stderr, "compile error: %s\n", parser.error);
|
||||||
basParserFree(&parser);
|
basParserFree(&parser);
|
||||||
return -1;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
BasModuleT *mod = basParserBuildModule(&parser);
|
BasModuleT *mod = basParserBuildModule(&parser);
|
||||||
basParserFree(&parser);
|
basParserFree(&parser);
|
||||||
|
|
||||||
if (!mod) {
|
if (!mod) {
|
||||||
return -1;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
|
|
@ -138,67 +120,40 @@ static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int
|
||||||
|
|
||||||
BasVmT *vm = basVmCreate();
|
BasVmT *vm = basVmCreate();
|
||||||
basVmLoadModule(vm, mod);
|
basVmLoadModule(vm, mod);
|
||||||
|
basVmSetPrintCallback(vm, capturePrint, cap);
|
||||||
OutBufT ob;
|
|
||||||
ob.buf = outBuf;
|
|
||||||
ob.len = 0;
|
|
||||||
ob.cap = outCap;
|
|
||||||
|
|
||||||
basVmSetPrintCallback(vm, captureCallback, &ob);
|
|
||||||
|
|
||||||
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
|
|
||||||
vm->callDepth = 1;
|
|
||||||
|
|
||||||
BasVmResultE result = basVmRun(vm);
|
BasVmResultE result = basVmRun(vm);
|
||||||
|
|
||||||
// Signal a real runtime error distinctly so testCompact's -1 branch flags
|
// Report a runtime error distinctly: two runs hitting the same VM error
|
||||||
// it as a failure; otherwise two runs hitting the same VM error produce
|
// produce identical truncated output and would falsely compare equal.
|
||||||
// identical truncated output and falsely compare equal (PASS).
|
|
||||||
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
|
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
|
||||||
fprintf(stderr, "vm error %d: %s\n", result, basVmGetError(vm));
|
fprintf(stderr, "vm error %d: %s\n", result, basVmGetError(vm));
|
||||||
basVmDestroy(vm);
|
|
||||||
basModuleFree(mod);
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
basVmDestroy(vm);
|
basVmDestroy(vm);
|
||||||
basModuleFree(mod);
|
basModuleFree(mod);
|
||||||
|
return result == BAS_VM_HALTED || result == BAS_VM_OK;
|
||||||
outBuf[ob.len] = '\0';
|
|
||||||
return ob.len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Runs name's program uncompacted and compacted; both runs must succeed
|
||||||
|
// and print the same output.
|
||||||
// A module built directly from raw bytes, exercising validation paths a
|
|
||||||
// compiled program can never produce.
|
|
||||||
|
|
||||||
|
|
||||||
// Compaction must refuse the stream (return 0) and leave it untouched.
|
|
||||||
|
|
||||||
|
|
||||||
// Crafted proc / form-var tables: out-of-range entries must abort
|
|
||||||
// compaction, a negative init address (no init code) must not.
|
|
||||||
static void testCompact(const char *name, const char *source) {
|
static void testCompact(const char *name, const char *source) {
|
||||||
|
static CaptureT orig;
|
||||||
|
static CaptureT comp;
|
||||||
|
|
||||||
sTotal++;
|
sTotal++;
|
||||||
|
|
||||||
static char orig[MAX_OUT];
|
if (!runAndCapture(source, false, &orig) || !runAndCapture(source, true, &comp)) {
|
||||||
static char comp[MAX_OUT];
|
printf("FAIL: %s (compile or runtime error)\n", name);
|
||||||
|
|
||||||
int32_t origLen = runAndCapture(source, false, orig, MAX_OUT);
|
|
||||||
int32_t compLen = runAndCapture(source, true, comp, MAX_OUT);
|
|
||||||
|
|
||||||
if (origLen < 0 || compLen < 0) {
|
|
||||||
printf("FAIL: %s (runtime error)\n", name);
|
|
||||||
sFailed++;
|
sFailed++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (origLen != compLen || memcmp(orig, comp, origLen) != 0) {
|
if (orig.len != comp.len || memcmp(orig.buf, comp.buf, orig.len) != 0) {
|
||||||
printf("FAIL: %s\n", name);
|
printf("FAIL: %s\n", name);
|
||||||
printf(" uncompacted (%d bytes):\n%s\n", (int)origLen, orig);
|
printf(" uncompacted (%d bytes):\n%s\n", (int)orig.len, orig.buf);
|
||||||
printf(" compacted (%d bytes):\n%s\n", (int)compLen, comp);
|
printf(" compacted (%d bytes):\n%s\n", (int)comp.len, comp.buf);
|
||||||
sFailed++;
|
sFailed++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +162,8 @@ static void testCompact(const char *name, const char *source) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Crafted proc / form-var tables: out-of-range entries must abort
|
||||||
|
// compaction, a negative init address (no init code) must not.
|
||||||
static void testCraftedTables(void) {
|
static void testCraftedTables(void) {
|
||||||
static const uint8_t okCode[] = { OP_LINE, 1, 0, OP_HALT };
|
static const uint8_t okCode[] = { OP_LINE, 1, 0, OP_HALT };
|
||||||
|
|
||||||
|
|
@ -625,7 +582,6 @@ int main(void) {
|
||||||
"PRINT\n"
|
"PRINT\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// ---- Crafted modules: compaction must refuse invalid streams ----
|
// ---- Crafted modules: compaction must refuse invalid streams ----
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -65,6 +65,7 @@
|
||||||
#include "toolbar/toolbar.h"
|
#include "toolbar/toolbar.h"
|
||||||
#include "treeView/treeView.h"
|
#include "treeView/treeView.h"
|
||||||
#include "dvxWm.h"
|
#include "dvxWm.h"
|
||||||
|
#include "dvxPlat.h"
|
||||||
#include "shellApp.h"
|
#include "shellApp.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
@ -100,7 +101,8 @@
|
||||||
#define CMD_WIN_TILE_H 351
|
#define CMD_WIN_TILE_H 351
|
||||||
#define CMD_WIN_TILE_V 352
|
#define CMD_WIN_TILE_V 352
|
||||||
#define CMD_WIN_TILE 353
|
#define CMD_WIN_TILE 353
|
||||||
#define CMD_HELP_ABOUT 400
|
#define CMD_HELP_ABOUT 400
|
||||||
|
#define CMD_HELP_CONTENTS 401
|
||||||
#define CMD_CTX_CUT 500
|
#define CMD_CTX_CUT 500
|
||||||
#define CMD_CTX_COPY 501
|
#define CMD_CTX_COPY 501
|
||||||
#define CMD_CTX_PASTE 502
|
#define CMD_CTX_PASTE 502
|
||||||
|
|
@ -108,6 +110,46 @@
|
||||||
#define CMD_CTX_SELALL 504
|
#define CMD_CTX_SELALL 504
|
||||||
#define CMD_CTX_PROPS 505
|
#define CMD_CTX_PROPS 505
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Layout
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#define DEMO_INFO_X 50
|
||||||
|
#define DEMO_INFO_Y 40
|
||||||
|
#define DEMO_INFO_W 340
|
||||||
|
#define DEMO_INFO_H 350
|
||||||
|
#define DEMO_GRAD_X 200
|
||||||
|
#define DEMO_GRAD_Y 100
|
||||||
|
#define DEMO_GRAD_W 280
|
||||||
|
#define DEMO_GRAD_H 250
|
||||||
|
#define DEMO_PATTERN_X 400
|
||||||
|
#define DEMO_PATTERN_Y 150
|
||||||
|
#define DEMO_PATTERN_W 250
|
||||||
|
#define DEMO_PATTERN_H 220
|
||||||
|
#define DEMO_PATTERN_SQ 16 // checkerboard square size
|
||||||
|
#define DEMO_PATTERN_RANGE 100 // pattern scroll range in pixels
|
||||||
|
#define DEMO_PATTERN_PAGE 25
|
||||||
|
#define DEMO_CTRL_X 380
|
||||||
|
#define DEMO_CTRL_Y 50
|
||||||
|
#define DEMO_CTRL_W 360
|
||||||
|
#define DEMO_CTRL_H 440
|
||||||
|
#define DEMO_TERM_X 60
|
||||||
|
#define DEMO_TERM_Y 60
|
||||||
|
#define DEMO_TERM_W 660
|
||||||
|
#define DEMO_TERM_H 420
|
||||||
|
#define DEMO_TERM_COLS 80
|
||||||
|
#define DEMO_TERM_ROWS 25
|
||||||
|
#define DEMO_TERM_BACKLOG 500
|
||||||
|
#define DEMO_FORM_X 80
|
||||||
|
#define DEMO_FORM_Y 200
|
||||||
|
#define DEMO_FORM_W 280
|
||||||
|
#define DEMO_FORM_H 360
|
||||||
|
#define DEMO_CANVAS_W 280
|
||||||
|
#define DEMO_CANVAS_H 80
|
||||||
|
#define DEMO_TEXT_MARGIN 4
|
||||||
|
#define DEMO_TEXT_LEADING 2
|
||||||
|
#define DEMO_MSG_LEN 300
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Module state
|
// Module state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -135,11 +177,39 @@ static const FileFilterT sFileFilters[] = {
|
||||||
static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"};
|
static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"};
|
||||||
static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"};
|
static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"};
|
||||||
|
|
||||||
|
// Menu commands whose only demo action is an information box.
|
||||||
|
typedef struct {
|
||||||
|
int32_t cmd;
|
||||||
|
const char *title;
|
||||||
|
const char *msg;
|
||||||
|
} DemoInfoCmdT;
|
||||||
|
|
||||||
|
static const DemoInfoCmdT sInfoCmds[] = {
|
||||||
|
{ CMD_FILE_NEW, "New", "A new document would be created here." },
|
||||||
|
{ CMD_EDIT_CUT, "Edit", "Cut selected." },
|
||||||
|
{ CMD_EDIT_COPY, "Edit", "Copied to clipboard." },
|
||||||
|
{ CMD_EDIT_PASTE, "Edit", "Pasted from clipboard." },
|
||||||
|
{ CMD_VIEW_ZOOM_IN, "Zoom", "Zoom in." },
|
||||||
|
{ CMD_VIEW_ZOOM_OUT, "Zoom", "Zoom out." },
|
||||||
|
{ CMD_VIEW_ZOOM_FIT, "Zoom", "Zoom to fit the window." },
|
||||||
|
{ CMD_CTX_CUT, "Context Menu", "Cut selected." },
|
||||||
|
{ CMD_CTX_COPY, "Context Menu", "Copied to clipboard." },
|
||||||
|
{ CMD_CTX_PASTE, "Context Menu", "Pasted from clipboard." },
|
||||||
|
{ CMD_CTX_DELETE, "Context Menu", "Deleted." },
|
||||||
|
{ CMD_CTX_SELALL, "Context Menu", "Selected all." },
|
||||||
|
{ CMD_CTX_PROPS, "Properties", "Window properties dialog would appear here." }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Last canvas point while drawing, so drags draw connected segments.
|
||||||
|
static int32_t sCanvasLastX = 0;
|
||||||
|
static int32_t sCanvasLastY = 0;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Prototypes
|
// Prototypes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
int32_t appMain(DxeAppContextT *ctx);
|
void appShutdown(void);
|
||||||
|
static void demoFilePath(char *buf, size_t bufSize, const char *name);
|
||||||
static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag);
|
static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag);
|
||||||
static void onCloseCb(WindowT *win);
|
static void onCloseCb(WindowT *win);
|
||||||
static void onCloseMainCb(WindowT *win);
|
static void onCloseMainCb(WindowT *win);
|
||||||
|
|
@ -148,12 +218,14 @@ static void onOkClick(WidgetT *w);
|
||||||
static void onPaintColor(WindowT *win, RectT *dirtyArea);
|
static void onPaintColor(WindowT *win, RectT *dirtyArea);
|
||||||
static void onPaintPattern(WindowT *win, RectT *dirtyArea);
|
static void onPaintPattern(WindowT *win, RectT *dirtyArea);
|
||||||
static void onPaintText(WindowT *win, RectT *dirtyArea);
|
static void onPaintText(WindowT *win, RectT *dirtyArea);
|
||||||
|
static void onScrollPattern(WindowT *win, ScrollbarOrientE orient, int32_t value);
|
||||||
static void onToolbarClick(WidgetT *w);
|
static void onToolbarClick(WidgetT *w);
|
||||||
static void setStatusByName(WidgetT *from, const char *name, const char *text);
|
static void setStatusByName(WidgetT *from, const char *name, const char *text);
|
||||||
static void setupControlsWindow(void);
|
static void setupControlsWindow(void);
|
||||||
static void setupMainWindow(void);
|
static void setupMainWindow(void);
|
||||||
static void setupTerminalWindow(void);
|
static void setupTerminalWindow(void);
|
||||||
static void setupWidgetDemo(void);
|
static void setupWidgetDemo(void);
|
||||||
|
int32_t appMain(DxeAppContextT *ctx);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// App descriptor
|
// App descriptor
|
||||||
|
|
@ -162,12 +234,12 @@ static void setupWidgetDemo(void);
|
||||||
// Callback-only: all windows use either widget trees (which handle their own
|
// Callback-only: all windows use either widget trees (which handle their own
|
||||||
// input) or onPaint callbacks (which are invoked by the compositor). No need
|
// input) or onPaint callbacks (which are invoked by the compositor). No need
|
||||||
// for a dedicated task since there's no ongoing computation.
|
// for a dedicated task since there's no ongoing computation.
|
||||||
AppDescriptorT appDescriptor = {
|
SHELL_APP_DESCRIPTOR(
|
||||||
.name = "DVX Demo",
|
.name = "DVX Demo",
|
||||||
.hasMainLoop = false,
|
.hasMainLoop = false,
|
||||||
.stackSize = SHELL_STACK_DEFAULT,
|
.stackSize = SHELL_STACK_DEFAULT,
|
||||||
.priority = TS_PRIORITY_NORMAL
|
.priority = TS_PRIORITY_NORMAL
|
||||||
};
|
);
|
||||||
|
|
||||||
|
|
||||||
// Cleanup hook the shell calls before dlclose. The accelerator table and the
|
// Cleanup hook the shell calls before dlclose. The accelerator table and the
|
||||||
|
|
@ -191,9 +263,23 @@ void appShutdown(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Builds the path of a file shipped in the demo's app directory.
|
||||||
|
static void demoFilePath(char *buf, size_t bufSize, const char *name) {
|
||||||
|
snprintf(buf, bufSize, "%s" DVX_PATH_SEP "%s", sDxeCtx->appDir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A press plots a point; each drag update draws a segment from the last
|
||||||
|
// point, so fast drags leave no gaps.
|
||||||
static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
|
static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
|
||||||
(void)drag;
|
if (!drag) {
|
||||||
wgtCanvasDrawLine(w, cx, cy, cx, cy);
|
sCanvasLastX = cx;
|
||||||
|
sCanvasLastY = cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtCanvasDrawLine(w, sCanvasLastX, sCanvasLastY, cx, cy);
|
||||||
|
sCanvasLastX = cx;
|
||||||
|
sCanvasLastY = cy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -218,8 +304,8 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
|
||||||
case CMD_FILE_OPEN: {
|
case CMD_FILE_OPEN: {
|
||||||
char path[DVX_MAX_PATH];
|
char path[DVX_MAX_PATH];
|
||||||
|
|
||||||
if (dvxFileDialog(sAc, "Open File", FD_OPEN, NULL, sFileFilters, 5, path, sizeof(path))) {
|
if (dvxFileDialog(sAc, "Open File", FD_OPEN, NULL, sFileFilters, DVX_ARRAY_LEN(sFileFilters), path, sizeof(path))) {
|
||||||
char msg[300];
|
char msg[DEMO_MSG_LEN];
|
||||||
snprintf(msg, sizeof(msg), "Selected: %s", path);
|
snprintf(msg, sizeof(msg), "Selected: %s", path);
|
||||||
dvxInfoBox(sAc, "Open", msg);
|
dvxInfoBox(sAc, "Open", msg);
|
||||||
}
|
}
|
||||||
|
|
@ -230,8 +316,8 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
|
||||||
case CMD_FILE_SAVE: {
|
case CMD_FILE_SAVE: {
|
||||||
char path[DVX_MAX_PATH];
|
char path[DVX_MAX_PATH];
|
||||||
|
|
||||||
if (dvxFileDialog(sAc, "Save As", FD_SAVE, NULL, sFileFilters, 5, path, sizeof(path))) {
|
if (dvxFileDialog(sAc, "Save As", FD_SAVE, NULL, sFileFilters, DVX_ARRAY_LEN(sFileFilters), path, sizeof(path))) {
|
||||||
char msg[300];
|
char msg[DEMO_MSG_LEN];
|
||||||
snprintf(msg, sizeof(msg), "Save to: %s", path);
|
snprintf(msg, sizeof(msg), "Save to: %s", path);
|
||||||
dvxInfoBox(sAc, "Save", msg);
|
dvxInfoBox(sAc, "Save", msg);
|
||||||
}
|
}
|
||||||
|
|
@ -251,12 +337,10 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
|
||||||
setupControlsWindow();
|
setupControlsWindow();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CMD_VIEW_DEBUG_LYT: {
|
case CMD_VIEW_DEBUG_LYT:
|
||||||
static bool sDebugLyt = false;
|
// The menu has already toggled the check mark.
|
||||||
sDebugLyt = !sDebugLyt;
|
wgtSetDebugLayout(sAc, wmMenuItemIsChecked(win->menuBar, CMD_VIEW_DEBUG_LYT));
|
||||||
wgtSetDebugLayout(sAc, sDebugLyt);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case CMD_WIN_CASCADE:
|
case CMD_WIN_CASCADE:
|
||||||
dvxCascadeWindows(sAc);
|
dvxCascadeWindows(sAc);
|
||||||
|
|
@ -274,34 +358,23 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
|
||||||
dvxTileWindows(sAc);
|
dvxTileWindows(sAc);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CMD_HELP_CONTENTS:
|
||||||
|
shellOpenHelp(sAc, sDxeCtx->helpFile);
|
||||||
|
break;
|
||||||
|
|
||||||
case CMD_HELP_ABOUT:
|
case CMD_HELP_ABOUT:
|
||||||
dvxInfoBox(sAc, "About DVX Demo",
|
dvxInfoBox(sAc, "About DVX Demo",
|
||||||
"DVX GUI Demonstration\n"
|
"DVX GUI Demonstration\n"
|
||||||
"A DOS Visual eXecutive windowing system for DOS.");
|
"A DOS Visual eXecutive windowing system for DOS.");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CMD_CTX_CUT:
|
default:
|
||||||
dvxInfoBox(sAc, "Context Menu", "Cut selected.");
|
for (int32_t i = 0; i < DVX_ARRAY_LEN(sInfoCmds); i++) {
|
||||||
break;
|
if (sInfoCmds[i].cmd == menuId) {
|
||||||
|
dvxInfoBox(sAc, sInfoCmds[i].title, sInfoCmds[i].msg);
|
||||||
case CMD_CTX_COPY:
|
break;
|
||||||
dvxInfoBox(sAc, "Context Menu", "Copied to clipboard.");
|
}
|
||||||
break;
|
}
|
||||||
|
|
||||||
case CMD_CTX_PASTE:
|
|
||||||
dvxInfoBox(sAc, "Context Menu", "Pasted from clipboard.");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CMD_CTX_DELETE:
|
|
||||||
dvxInfoBox(sAc, "Context Menu", "Deleted.");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CMD_CTX_SELALL:
|
|
||||||
dvxInfoBox(sAc, "Context Menu", "Selected all.");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CMD_CTX_PROPS:
|
|
||||||
dvxInfoBox(sAc, "Properties", "Window properties dialog would appear here.");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +419,7 @@ static void onPaintColor(WindowT *win, RectT *dirtyArea) {
|
||||||
// Renders a checkerboard pattern pixel-by-pixel. This is the slowest
|
// Renders a checkerboard pattern pixel-by-pixel. This is the slowest
|
||||||
// approach (per-pixel BPP branching) and exists to test that the content
|
// approach (per-pixel BPP branching) and exists to test that the content
|
||||||
// buffer renders correctly at all supported bit depths (8/16/32 bpp).
|
// buffer renders correctly at all supported bit depths (8/16/32 bpp).
|
||||||
|
// The scrollbars pan the pattern.
|
||||||
static void onPaintPattern(WindowT *win, RectT *dirtyArea) {
|
static void onPaintPattern(WindowT *win, RectT *dirtyArea) {
|
||||||
(void)dirtyArea;
|
(void)dirtyArea;
|
||||||
|
|
||||||
|
|
@ -353,16 +427,18 @@ static void onPaintPattern(WindowT *win, RectT *dirtyArea) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisplayT *d = dvxGetDisplay(sAc);
|
const DisplayT *d = dvxGetDisplay(sAc);
|
||||||
int32_t bpp = d->format.bytesPerPixel;
|
int32_t bpp = d->format.bytesPerPixel;
|
||||||
int32_t sq = 16;
|
int32_t sq = DEMO_PATTERN_SQ;
|
||||||
|
int32_t offX = win->hScroll ? win->hScroll->value : 0;
|
||||||
|
int32_t offY = win->vScroll ? win->vScroll->value : 0;
|
||||||
|
|
||||||
uint32_t c1 = packColor(d, 255, 255, 255);
|
uint32_t c1 = packColor(d, 255, 255, 255);
|
||||||
uint32_t c2 = packColor(d, 0, 0, 180);
|
uint32_t c2 = packColor(d, 0, 0, 180);
|
||||||
|
|
||||||
for (int32_t y = 0; y < win->contentH; y++) {
|
for (int32_t y = 0; y < win->contentH; y++) {
|
||||||
for (int32_t x = 0; x < win->contentW; x++) {
|
for (int32_t x = 0; x < win->contentW; x++) {
|
||||||
uint32_t color = ((x / sq) + (y / sq)) & 1 ? c2 : c1;
|
uint32_t color = (((x + offX) / sq) + ((y + offY) / sq)) & 1 ? c2 : c1;
|
||||||
uint8_t *px = win->contentBuf + y * win->contentPitch + x * bpp;
|
uint8_t *px = win->contentBuf + y * win->contentPitch + x * bpp;
|
||||||
|
|
||||||
if (bpp == 1) {
|
if (bpp == 1) {
|
||||||
|
|
@ -418,11 +494,11 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
int32_t textY = 4;
|
int32_t textY = DEMO_TEXT_MARGIN;
|
||||||
|
|
||||||
for (int32_t i = 0; lines[i] != NULL; i++) {
|
for (int32_t i = 0; lines[i] != NULL; i++) {
|
||||||
const char *line = lines[i];
|
const char *line = lines[i];
|
||||||
int32_t textX = 4;
|
int32_t textX = DEMO_TEXT_MARGIN;
|
||||||
|
|
||||||
for (int32_t j = 0; line[j] != '\0'; j++) {
|
for (int32_t j = 0; line[j] != '\0'; j++) {
|
||||||
int32_t idx = (uint8_t)line[j] - font->firstChar;
|
int32_t idx = (uint8_t)line[j] - font->firstChar;
|
||||||
|
|
@ -467,11 +543,19 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
|
||||||
textX += font->charWidth;
|
textX += font->charWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
textY += font->charHeight + 2;
|
textY += font->charHeight + DEMO_TEXT_LEADING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The pattern window's scrollbars pan the pattern; repaint at the new offset.
|
||||||
|
static void onScrollPattern(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
||||||
|
(void)orient;
|
||||||
|
(void)value;
|
||||||
|
dvxInvalidateWindow(sAc, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void onToolbarClick(WidgetT *w) {
|
static void onToolbarClick(WidgetT *w) {
|
||||||
setStatusByName(w, "advStatus", wgtGetText(w));
|
setStatusByName(w, "advStatus", wgtGetText(w));
|
||||||
}
|
}
|
||||||
|
|
@ -498,7 +582,7 @@ static void setStatusByName(WidgetT *from, const char *name, const char *text) {
|
||||||
// widget manages tab pages; each page is a container that shows/hides based
|
// widget manages tab pages; each page is a container that shows/hides based
|
||||||
// on the selected tab. This is the densest widget test in the project.
|
// on the selected tab. This is the densest widget test in the project.
|
||||||
static void setupControlsWindow(void) {
|
static void setupControlsWindow(void) {
|
||||||
WindowT *win = dvxCreateWindow(sAc, "Advanced Widgets", 380, 50, 360, 440, true);
|
WindowT *win = dvxCreateWindow(sAc, "Advanced Widgets", DEMO_CTRL_X, DEMO_CTRL_Y, DEMO_CTRL_W, DEMO_CTRL_H, true);
|
||||||
|
|
||||||
if (!win) {
|
if (!win) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -517,14 +601,14 @@ static void setupControlsWindow(void) {
|
||||||
WidgetT *ddRow = wgtHBox(page1);
|
WidgetT *ddRow = wgtHBox(page1);
|
||||||
wgtLabel(ddRow, "Co&lor:");
|
wgtLabel(ddRow, "Co&lor:");
|
||||||
WidgetT *dd = wgtDropdown(ddRow);
|
WidgetT *dd = wgtDropdown(ddRow);
|
||||||
wgtDropdownSetItems(dd, colorItems, 6);
|
wgtDropdownSetItems(dd, colorItems, DVX_ARRAY_LEN(colorItems));
|
||||||
wgtDropdownSetSelected(dd, 0);
|
wgtDropdownSetSelected(dd, 0);
|
||||||
wgtSetTooltip(dd, "Choose a color");
|
wgtSetTooltip(dd, "Choose a color");
|
||||||
|
|
||||||
WidgetT *cbRow = wgtHBox(page1);
|
WidgetT *cbRow = wgtHBox(page1);
|
||||||
wgtLabel(cbRow, "Si&ze:");
|
wgtLabel(cbRow, "Si&ze:");
|
||||||
WidgetT *cb = wgtComboBox(cbRow, 32);
|
WidgetT *cb = wgtComboBox(cbRow, 32);
|
||||||
wgtComboBoxSetItems(cb, sizeItems, 4);
|
wgtComboBoxSetItems(cb, sizeItems, DVX_ARRAY_LEN(sizeItems));
|
||||||
wgtComboBoxSetSelected(cb, 1);
|
wgtComboBoxSetSelected(cb, 1);
|
||||||
wgtSetTooltip(cb, "Type or select a size");
|
wgtSetTooltip(cb, "Type or select a size");
|
||||||
|
|
||||||
|
|
@ -597,8 +681,8 @@ static void setupControlsWindow(void) {
|
||||||
};
|
};
|
||||||
|
|
||||||
WidgetT *lv = wgtListView(page3);
|
WidgetT *lv = wgtListView(page3);
|
||||||
wgtListViewSetColumns(lv, lvCols, 4);
|
wgtListViewSetColumns(lv, lvCols, DVX_ARRAY_LEN(lvCols));
|
||||||
wgtListViewSetData(lv, lvData, 10);
|
wgtListViewSetData(lv, lvData, DVX_ARRAY_LEN(lvData) / DVX_ARRAY_LEN(lvCols));
|
||||||
wgtListViewSetSelected(lv, 0);
|
wgtListViewSetSelected(lv, 0);
|
||||||
wgtListViewSetMultiSelect(lv, true);
|
wgtListViewSetMultiSelect(lv, true);
|
||||||
wgtListViewSetReorderable(lv, true);
|
wgtListViewSetReorderable(lv, true);
|
||||||
|
|
@ -620,7 +704,7 @@ static void setupControlsWindow(void) {
|
||||||
"Item &5", "Item &6", "Item &7", "Item &8"
|
"Item &5", "Item &6", "Item &7", "Item &8"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int32_t i = 0; i < 8; i++) {
|
for (int32_t i = 0; i < DVX_ARRAY_LEN(spItems); i++) {
|
||||||
wgtCheckbox(sp, spItems[i]);
|
wgtCheckbox(sp, spItems[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -632,7 +716,7 @@ static void setupControlsWindow(void) {
|
||||||
"Radio 1", "Radio 2", "Radio 3", "Radio 4", "Radio 5"
|
"Radio 1", "Radio 2", "Radio 3", "Radio 4", "Radio 5"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int32_t i = 0; i < 5; i++) {
|
for (int32_t i = 0; i < DVX_ARRAY_LEN(spRadios); i++) {
|
||||||
wgtRadio(spRg, spRadios[i]);
|
wgtRadio(spRg, spRadios[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -646,7 +730,7 @@ static void setupControlsWindow(void) {
|
||||||
WidgetT *tb = wgtToolbar(page5tb);
|
WidgetT *tb = wgtToolbar(page5tb);
|
||||||
|
|
||||||
char iconPath[DVX_MAX_PATH];
|
char iconPath[DVX_MAX_PATH];
|
||||||
snprintf(iconPath, sizeof(iconPath), "%s/new.bmp", sDxeCtx->appDir);
|
demoFilePath(iconPath, sizeof(iconPath), "new.bmp");
|
||||||
WidgetT *btnNew = wgtImageButtonFromFile(tb, iconPath);
|
WidgetT *btnNew = wgtImageButtonFromFile(tb, iconPath);
|
||||||
|
|
||||||
if (!btnNew) {
|
if (!btnNew) {
|
||||||
|
|
@ -655,7 +739,7 @@ static void setupControlsWindow(void) {
|
||||||
|
|
||||||
btnNew->onClick = onToolbarClick;
|
btnNew->onClick = onToolbarClick;
|
||||||
|
|
||||||
snprintf(iconPath, sizeof(iconPath), "%s/open.bmp", sDxeCtx->appDir);
|
demoFilePath(iconPath, sizeof(iconPath), "open.bmp");
|
||||||
WidgetT *btnOpen = wgtImageButtonFromFile(tb, iconPath);
|
WidgetT *btnOpen = wgtImageButtonFromFile(tb, iconPath);
|
||||||
|
|
||||||
if (!btnOpen) {
|
if (!btnOpen) {
|
||||||
|
|
@ -664,7 +748,7 @@ static void setupControlsWindow(void) {
|
||||||
|
|
||||||
btnOpen->onClick = onToolbarClick;
|
btnOpen->onClick = onToolbarClick;
|
||||||
|
|
||||||
snprintf(iconPath, sizeof(iconPath), "%s/save.bmp", sDxeCtx->appDir);
|
demoFilePath(iconPath, sizeof(iconPath), "save.bmp");
|
||||||
WidgetT *btnSave = wgtImageButtonFromFile(tb, iconPath);
|
WidgetT *btnSave = wgtImageButtonFromFile(tb, iconPath);
|
||||||
|
|
||||||
if (!btnSave) {
|
if (!btnSave) {
|
||||||
|
|
@ -683,7 +767,7 @@ static void setupControlsWindow(void) {
|
||||||
WidgetT *page6m = wgtTabPage(tabs, "&Media");
|
WidgetT *page6m = wgtTabPage(tabs, "&Media");
|
||||||
|
|
||||||
char samplePath[DVX_MAX_PATH];
|
char samplePath[DVX_MAX_PATH];
|
||||||
snprintf(samplePath, sizeof(samplePath), "%s/sample.bmp", sDxeCtx->appDir);
|
demoFilePath(samplePath, sizeof(samplePath), "sample.bmp");
|
||||||
wgtLabel(page6m, "ImageFromFile (sample.bmp):");
|
wgtLabel(page6m, "ImageFromFile (sample.bmp):");
|
||||||
WidgetT *img = wgtImageFromFile(page6m, samplePath);
|
WidgetT *img = wgtImageFromFile(page6m, samplePath);
|
||||||
|
|
||||||
|
|
@ -703,7 +787,7 @@ static void setupControlsWindow(void) {
|
||||||
|
|
||||||
wgtLabel(page7e, "Canvas (draw with mouse):");
|
wgtLabel(page7e, "Canvas (draw with mouse):");
|
||||||
const DisplayT *d = dvxGetDisplay(sAc);
|
const DisplayT *d = dvxGetDisplay(sAc);
|
||||||
WidgetT *cv = wgtCanvas(page7e, 280, 80);
|
WidgetT *cv = wgtCanvas(page7e, DEMO_CANVAS_W, DEMO_CANVAS_H);
|
||||||
wgtCanvasSetPenColor(cv, packColor(d, 200, 0, 0));
|
wgtCanvasSetPenColor(cv, packColor(d, 200, 0, 0));
|
||||||
wgtCanvasDrawRect(cv, 5, 5, 50, 35);
|
wgtCanvasDrawRect(cv, 5, 5, 50, 35);
|
||||||
wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 200));
|
wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 200));
|
||||||
|
|
@ -749,7 +833,7 @@ static void setupControlsWindow(void) {
|
||||||
"EMM386.EXE", "MOUSE.COM"
|
"EMM386.EXE", "MOUSE.COM"
|
||||||
};
|
};
|
||||||
|
|
||||||
wgtListBoxSetItems(rightList, explorerItems, 8);
|
wgtListBoxSetItems(rightList, explorerItems, DVX_ARRAY_LEN(explorerItems));
|
||||||
|
|
||||||
// Bottom pane: detail/preview area
|
// Bottom pane: detail/preview area
|
||||||
WidgetT *detailFrame = wgtFrame(hSplit, "Preview");
|
WidgetT *detailFrame = wgtFrame(hSplit, "Preview");
|
||||||
|
|
@ -781,7 +865,7 @@ static void setupControlsWindow(void) {
|
||||||
static const char *disItems[] = {"Alpha", "Beta", "Gamma"};
|
static const char *disItems[] = {"Alpha", "Beta", "Gamma"};
|
||||||
|
|
||||||
WidgetT *enDd = wgtDropdown(enCol);
|
WidgetT *enDd = wgtDropdown(enCol);
|
||||||
wgtDropdownSetItems(enDd, disItems, 3);
|
wgtDropdownSetItems(enDd, disItems, DVX_ARRAY_LEN(disItems));
|
||||||
wgtDropdownSetSelected(enDd, 0);
|
wgtDropdownSetSelected(enDd, 0);
|
||||||
|
|
||||||
WidgetT *enSlider = wgtSlider(enCol, 0, 100);
|
WidgetT *enSlider = wgtSlider(enCol, 0, 100);
|
||||||
|
|
@ -820,7 +904,7 @@ static void setupControlsWindow(void) {
|
||||||
wgtSetEnabled(dR2, false);
|
wgtSetEnabled(dR2, false);
|
||||||
|
|
||||||
WidgetT *dDd = wgtDropdown(disCol);
|
WidgetT *dDd = wgtDropdown(disCol);
|
||||||
wgtDropdownSetItems(dDd, disItems, 3);
|
wgtDropdownSetItems(dDd, disItems, DVX_ARRAY_LEN(disItems));
|
||||||
wgtDropdownSetSelected(dDd, 0);
|
wgtDropdownSetSelected(dDd, 0);
|
||||||
wgtSetEnabled(dDd, false);
|
wgtSetEnabled(dDd, false);
|
||||||
|
|
||||||
|
|
@ -854,7 +938,7 @@ static void setupControlsWindow(void) {
|
||||||
// 2. Color gradient window
|
// 2. Color gradient window
|
||||||
// 3. Checkerboard pattern window with scrollbars
|
// 3. Checkerboard pattern window with scrollbars
|
||||||
static void setupMainWindow(void) {
|
static void setupMainWindow(void) {
|
||||||
WindowT *win1 = dvxCreateWindow(sAc, "DVX Information", 50, 40, 340, 350, true);
|
WindowT *win1 = dvxCreateWindow(sAc, "DVX Information", DEMO_INFO_X, DEMO_INFO_Y, DEMO_INFO_W, DEMO_INFO_H, true);
|
||||||
|
|
||||||
if (win1) {
|
if (win1) {
|
||||||
win1->onPaint = onPaintText;
|
win1->onPaint = onPaintText;
|
||||||
|
|
@ -921,6 +1005,8 @@ static void setupMainWindow(void) {
|
||||||
MenuT *helpMenu = wmAddMenu(bar, "&Help");
|
MenuT *helpMenu = wmAddMenu(bar, "&Help");
|
||||||
|
|
||||||
if (helpMenu) {
|
if (helpMenu) {
|
||||||
|
wmAddMenuItem(helpMenu, "&Contents\tF1", CMD_HELP_CONTENTS);
|
||||||
|
wmAddMenuSeparator(helpMenu);
|
||||||
wmAddMenuItem(helpMenu, "&About...", CMD_HELP_ABOUT);
|
wmAddMenuItem(helpMenu, "&About...", CMD_HELP_ABOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -933,7 +1019,6 @@ static void setupMainWindow(void) {
|
||||||
dvxAddAccel(sAccel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
|
dvxAddAccel(sAccel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
|
||||||
dvxAddAccel(sAccel, 'S', ACCEL_CTRL, CMD_FILE_SAVE);
|
dvxAddAccel(sAccel, 'S', ACCEL_CTRL, CMD_FILE_SAVE);
|
||||||
dvxAddAccel(sAccel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT);
|
dvxAddAccel(sAccel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT);
|
||||||
dvxAddAccel(sAccel, KEY_F1, 0, CMD_HELP_ABOUT);
|
|
||||||
win1->accelTable = sAccel;
|
win1->accelTable = sAccel;
|
||||||
|
|
||||||
// Right-click context menu, attached at the window level. If a widget
|
// Right-click context menu, attached at the window level. If a widget
|
||||||
|
|
@ -958,7 +1043,7 @@ static void setupMainWindow(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window 2: Color gradient
|
// Window 2: Color gradient
|
||||||
WindowT *win2 = dvxCreateWindow(sAc, "Color Gradient", 200, 100, 280, 250, true);
|
WindowT *win2 = dvxCreateWindow(sAc, "Color Gradient", DEMO_GRAD_X, DEMO_GRAD_Y, DEMO_GRAD_W, DEMO_GRAD_H, true);
|
||||||
|
|
||||||
if (win2) {
|
if (win2) {
|
||||||
win2->onPaint = onPaintColor;
|
win2->onPaint = onPaintColor;
|
||||||
|
|
@ -969,14 +1054,15 @@ static void setupMainWindow(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window 3: Checkerboard pattern with scrollbars
|
// Window 3: Checkerboard pattern with scrollbars
|
||||||
WindowT *win3 = dvxCreateWindow(sAc, "Pattern", 400, 150, 250, 220, true);
|
WindowT *win3 = dvxCreateWindow(sAc, "Pattern", DEMO_PATTERN_X, DEMO_PATTERN_Y, DEMO_PATTERN_W, DEMO_PATTERN_H, true);
|
||||||
|
|
||||||
if (win3) {
|
if (win3) {
|
||||||
win3->onPaint = onPaintPattern;
|
win3->onPaint = onPaintPattern;
|
||||||
win3->onClose = onCloseCb;
|
win3->onClose = onCloseCb;
|
||||||
|
win3->onScroll = onScrollPattern;
|
||||||
|
|
||||||
wmAddVScrollbar(win3, 0, 100, 25);
|
wmAddVScrollbar(win3, 0, DEMO_PATTERN_RANGE, DEMO_PATTERN_PAGE);
|
||||||
wmAddHScrollbar(win3, 0, 100, 25);
|
wmAddHScrollbar(win3, 0, DEMO_PATTERN_RANGE, DEMO_PATTERN_PAGE);
|
||||||
wmUpdateContentRect(win3);
|
wmUpdateContentRect(win3);
|
||||||
wmReallocContentBuf(win3, &sAc->display);
|
wmReallocContentBuf(win3, &sAc->display);
|
||||||
|
|
||||||
|
|
@ -991,7 +1077,7 @@ static void setupMainWindow(void) {
|
||||||
// colors, and CP437 box-drawing/graphic characters. The terminal is
|
// colors, and CP437 box-drawing/graphic characters. The terminal is
|
||||||
// minimized on creation to avoid cluttering the initial demo view.
|
// minimized on creation to avoid cluttering the initial demo view.
|
||||||
static void setupTerminalWindow(void) {
|
static void setupTerminalWindow(void) {
|
||||||
WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", 60, 60, 660, 420, true);
|
WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", DEMO_TERM_X, DEMO_TERM_Y, DEMO_TERM_W, DEMO_TERM_H, true);
|
||||||
|
|
||||||
if (!win) {
|
if (!win) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1000,10 +1086,10 @@ static void setupTerminalWindow(void) {
|
||||||
win->onClose = onCloseCb;
|
win->onClose = onCloseCb;
|
||||||
|
|
||||||
WidgetT *root = wgtInitWindow(sAc, win);
|
WidgetT *root = wgtInitWindow(sAc, win);
|
||||||
WidgetT *term = wgtAnsiTerm(root, 80, 25);
|
WidgetT *term = wgtAnsiTerm(root, DEMO_TERM_COLS, DEMO_TERM_ROWS);
|
||||||
|
|
||||||
term->weight = WGT_WEIGHT_FILL;
|
term->weight = WGT_WEIGHT_FILL;
|
||||||
wgtAnsiTermSetScrollback(term, 500);
|
wgtAnsiTermSetScrollback(term, DEMO_TERM_BACKLOG);
|
||||||
|
|
||||||
// Feed some ANSI content to demonstrate the terminal
|
// Feed some ANSI content to demonstrate the terminal
|
||||||
static const uint8_t ansiDemo[] =
|
static const uint8_t ansiDemo[] =
|
||||||
|
|
@ -1070,7 +1156,7 @@ static void setupTerminalWindow(void) {
|
||||||
// and a button row. The '&' in label text marks the Alt+key mnemonic that
|
// and a button row. The '&' in label text marks the Alt+key mnemonic that
|
||||||
// moves focus to the associated widget -- this is the accelerator mechanism.
|
// moves focus to the associated widget -- this is the accelerator mechanism.
|
||||||
static void setupWidgetDemo(void) {
|
static void setupWidgetDemo(void) {
|
||||||
WindowT *win = dvxCreateWindow(sAc, "Widget Demo", 80, 200, 280, 360, true);
|
WindowT *win = dvxCreateWindow(sAc, "Widget Demo", DEMO_FORM_X, DEMO_FORM_Y, DEMO_FORM_W, DEMO_FORM_H, true);
|
||||||
|
|
||||||
if (!win) {
|
if (!win) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1123,7 +1209,7 @@ static void setupWidgetDemo(void) {
|
||||||
WidgetT *listRow = wgtHBox(root);
|
WidgetT *listRow = wgtHBox(root);
|
||||||
wgtLabel(listRow, "&Items:");
|
wgtLabel(listRow, "&Items:");
|
||||||
WidgetT *lb = wgtListBox(listRow);
|
WidgetT *lb = wgtListBox(listRow);
|
||||||
wgtListBoxSetItems(lb, listItems, 5);
|
wgtListBoxSetItems(lb, listItems, DVX_ARRAY_LEN(listItems));
|
||||||
wgtListBoxSetReorderable(lb, true);
|
wgtListBoxSetReorderable(lb, true);
|
||||||
lb->weight = WGT_WEIGHT_FILL;
|
lb->weight = WGT_WEIGHT_FILL;
|
||||||
|
|
||||||
|
|
@ -1140,7 +1226,7 @@ static void setupWidgetDemo(void) {
|
||||||
wgtLabel(listRow, "&Multi:");
|
wgtLabel(listRow, "&Multi:");
|
||||||
WidgetT *mlb = wgtListBox(listRow);
|
WidgetT *mlb = wgtListBox(listRow);
|
||||||
wgtListBoxSetMultiSelect(mlb, true);
|
wgtListBoxSetMultiSelect(mlb, true);
|
||||||
wgtListBoxSetItems(mlb, multiItems, 7);
|
wgtListBoxSetItems(mlb, multiItems, DVX_ARRAY_LEN(multiItems));
|
||||||
mlb->weight = WGT_WEIGHT_FILL;
|
mlb->weight = WGT_WEIGHT_FILL;
|
||||||
|
|
||||||
wgtHSeparator(root);
|
wgtHSeparator(root);
|
||||||
|
|
|
||||||
135
src/apps/kpunch/dvxdemo/dvxdemo.dhs
Normal file
135
src/apps/kpunch/dvxdemo/dvxdemo.dhs
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
# DVX Demo help. Compiled to dvxdemo.hlp next to dvxdemo.app; F1 and
|
||||||
|
# Help > Contents in the DVX Information window open it.
|
||||||
|
|
||||||
|
.topic dvxdemo.overview
|
||||||
|
.title DVX Demo
|
||||||
|
.toc 0 DVX Demo
|
||||||
|
.default
|
||||||
|
.index DVX Demo
|
||||||
|
.index Demonstration
|
||||||
|
|
||||||
|
.h1 DVX Demo
|
||||||
|
|
||||||
|
DVX Demo shows off the kinds of windows and controls DVX offers. It opens several windows at once. Nothing in it changes your files, so feel free to click everything.
|
||||||
|
|
||||||
|
.h2 The Windows
|
||||||
|
|
||||||
|
.table
|
||||||
|
Window What it shows
|
||||||
|
------ -------------
|
||||||
|
DVX Information A window with a full menu bar, keyboard shortcuts and a right-click menu.
|
||||||
|
Color Gradient A window filled with a smooth color fade. Resize it to see the fade redrawn.
|
||||||
|
Pattern A checkerboard pattern with scroll bars. Use the scroll bars to move the pattern.
|
||||||
|
Widget Demo A small form with text boxes, check boxes, option buttons and lists.
|
||||||
|
Advanced Widgets Many more controls, arranged on tabs.
|
||||||
|
ANSI Terminal A text terminal showing colors and line-drawing characters. It starts minimized as an icon at the bottom of the screen; double-click the icon to open it.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Each window can be moved, resized, minimized and closed on its own. DVX Demo ends when you close its last window.
|
||||||
|
|
||||||
|
.link dvxdemo.info The DVX Information window
|
||||||
|
.link dvxdemo.widgets The Widget Demo and Advanced Widgets windows
|
||||||
|
|
||||||
|
.topic dvxdemo.info
|
||||||
|
.title The DVX Information Window
|
||||||
|
.toc 0 The DVX Information Window
|
||||||
|
.index DVX Information
|
||||||
|
.index Context Menu
|
||||||
|
.index Shortcut Keys
|
||||||
|
|
||||||
|
.h1 The DVX Information Window
|
||||||
|
|
||||||
|
This window shows a short description of DVX and a menu bar with many kinds of menu items. Most commands just show a message saying what was chosen.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Menu Commands
|
||||||
|
---- --------
|
||||||
|
File New, Open..., Save and Exit. Open... and Save show the real file dialog, then tell you which file you picked; no file is opened or saved.
|
||||||
|
Edit Cut, Copy and Paste show a message.
|
||||||
|
View Terminal opens another ANSI Terminal window and Controls opens another Advanced Widgets window. Toolbar and Status Bar show check marks, Small, Medium and Large show a round mark for the one chosen, and Zoom opens a smaller menu. Debug Layout draws outlines around every control in DVX so you can see how windows are laid out; choose it again to turn it off.
|
||||||
|
Window Cascade, Tile Grid, Tile Horizontal and Tile Vertical arrange all open windows.
|
||||||
|
Help Contents opens this help. About... shows a short message.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 Shortcut Keys
|
||||||
|
|
||||||
|
While this window is active:
|
||||||
|
|
||||||
|
.table
|
||||||
|
Key Command
|
||||||
|
--- -------
|
||||||
|
Ctrl+N File > New
|
||||||
|
Ctrl+O File > Open...
|
||||||
|
Ctrl+S File > Save
|
||||||
|
Ctrl+Q File > Exit
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 Right-Click Menu
|
||||||
|
|
||||||
|
Right-click inside the window to see a short menu with Cut, Copy, Paste and Properties....
|
||||||
|
|
||||||
|
.h2 Closing
|
||||||
|
|
||||||
|
Closing this window (or choosing File > Exit) asks "Close this window?" first. The other demo windows close without asking.
|
||||||
|
|
||||||
|
.topic dvxdemo.widgets
|
||||||
|
.title The Widget Demo and Advanced Widgets Windows
|
||||||
|
.toc 0 The Widget Windows
|
||||||
|
.index Widget Demo
|
||||||
|
.index Advanced Widgets
|
||||||
|
.index Controls
|
||||||
|
|
||||||
|
.h1 The Widget Demo and Advanced Widgets Windows
|
||||||
|
|
||||||
|
.h2 Widget Demo
|
||||||
|
|
||||||
|
A small form with the controls most programs use:
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Text boxes for a name, a password (the letters are hidden) and a phone number (only digits fit in the pattern).
|
||||||
|
.item Two check boxes and a group of three option buttons.
|
||||||
|
.item Two lists. In the Items list you can drag an item up or down to move it, and right-click for a short menu. The Multi list lets you select several items with Ctrl and Shift.
|
||||||
|
.item OK shows "Button clicked!" at the top of the window.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
Underlined letters work as shortcuts: hold Alt and press the letter to jump to that control.
|
||||||
|
|
||||||
|
.h2 Advanced Widgets
|
||||||
|
|
||||||
|
The tabs across the top each show a group of controls. The bar at the bottom shows messages from some of them.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Tab What it shows
|
||||||
|
--- -------------
|
||||||
|
Controls A drop-down list, a combo box you can type in, progress bars, a slider and a number box.
|
||||||
|
Tree A tree of folders and files. Items can be dragged to a new place.
|
||||||
|
List A list with columns. Click a column heading to sort by it, and drag items to reorder them.
|
||||||
|
Scroll A scrolling area holding more controls than fit.
|
||||||
|
Toolbar A toolbar with picture buttons.
|
||||||
|
Media A picture loaded from a file.
|
||||||
|
Editor A multi-line text box, and a drawing area you can draw on with the mouse.
|
||||||
|
Split Areas divided by bars you can drag to change their sizes.
|
||||||
|
Disabled The same controls shown usable and grayed out, side by side.
|
||||||
|
.endtable
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -29,12 +29,18 @@
|
||||||
|
|
||||||
.h1 DVX Help Viewer
|
.h1 DVX Help Viewer
|
||||||
|
|
||||||
The DVX Help Viewer displays .hlp help files compiled from .dhs source documents. It provides a tree-based table of contents, scrollable content with word-wrapped text, clickable hyperlinks, full-text search, and a keyword index.
|
DVX Help shows help pages. The list on the left is the table of contents, the page you are reading is on the right, and underlined words are links to other pages. You can also look things up in the Index or with Search.
|
||||||
|
|
||||||
|
.note tip
|
||||||
|
New to DVX? Click the Program Manager window and press F1, or choose DVX Help from the Program Manager's Help menu, to open the DVX user guide. It explains how to start programs, work with windows, menus and dialog boxes, and everything else that works the same in every program. Each program's own commands are explained in that program's help: press F1 while you work in it.
|
||||||
|
.endnote
|
||||||
|
|
||||||
.h2 Opening Help
|
.h2 Opening Help
|
||||||
|
|
||||||
Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.
|
Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.
|
||||||
|
|
||||||
|
If the help viewer is already running, F1 reuses its window: the requested page is shown there (loading the other application's help file if needed, which starts a fresh Back/Forward history) and the window is brought to the front. The Help menu of the Program Manager does the same. F1 pressed in the help viewer itself shows this help about the viewer. While the viewer's Index or Search dialog is open, the page asked for with F1 is shown once the dialog closes.
|
||||||
|
|
||||||
You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.
|
You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.
|
||||||
|
|
||||||
.h2 Navigation
|
.h2 Navigation
|
||||||
|
|
@ -44,9 +50,14 @@ You can also launch the help viewer from an application's Help menu, or by click
|
||||||
.item Click underlined links in the content to jump to other topics
|
.item Click underlined links in the content to jump to other topics
|
||||||
.item Use the Back and Forward buttons (or Navigate menu) to retrace your steps
|
.item Use the Back and Forward buttons (or Navigate menu) to retrace your steps
|
||||||
.item Use Navigate > Index to browse an alphabetical keyword list
|
.item Use Navigate > Index to browse an alphabetical keyword list
|
||||||
.item Use Navigate > Search to find topics by keyword
|
.item Use Navigate > Search to find topics by the words they contain
|
||||||
|
.item Press Tab to move between links and Enter or Space to follow one
|
||||||
.endlist
|
.endlist
|
||||||
|
|
||||||
|
.h2 Searching
|
||||||
|
|
||||||
|
Search looks through the title and text of every topic. Type one or more words separated by spaces; a topic matches when it contains all of them, in any order and in any mix of upper and lower case. Words may be partial: "scroll" also finds "ScrollPane". Index keywords that contain the whole search text match as well. Words need at least three letters or digits in a row to be looked up in topic text; a search made only of shorter words (such as "if" or "a b") matches topic titles and index keywords only. When one topic matches it is shown at once; otherwise pick one from the list of matching topics.
|
||||||
|
|
||||||
.h2 Keyboard Shortcuts
|
.h2 Keyboard Shortcuts
|
||||||
|
|
||||||
.table
|
.table
|
||||||
|
|
@ -56,6 +67,7 @@ You can also launch the help viewer from an application's Help menu, or by click
|
||||||
Escape Close viewer
|
Escape Close viewer
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
.section Writing Help Files (for Developers)
|
||||||
.topic help.format
|
.topic help.format
|
||||||
.title Help Source Format (.dhs)
|
.title Help Source Format (.dhs)
|
||||||
.toc 0 Help Source Format
|
.toc 0 Help Source Format
|
||||||
|
|
@ -83,8 +95,11 @@ Help files are authored as plain text .dhs source files using a simple line-orie
|
||||||
.h2 <text> Level 2 heading (underlined)
|
.h2 <text> Level 2 heading (underlined)
|
||||||
.h3 <text> Level 3 heading (plain)
|
.h3 <text> Level 3 heading (plain)
|
||||||
.hr Horizontal rule
|
.hr Horizontal rule
|
||||||
.link <id> <text> Hyperlink to another topic
|
.link <id> <text> Hyperlink to another topic in the same help
|
||||||
.image <file.bmp> Inline image (BMP format)
|
file (see Links below)
|
||||||
|
.image <file> [left|center|right]
|
||||||
|
Inline image (BMP, PNG, JPEG or GIF), path
|
||||||
|
relative to the -i directory
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
.h2 Block Directives
|
.h2 Block Directives
|
||||||
|
|
@ -107,6 +122,17 @@ Help files are authored as plain text .dhs source files using a simple line-orie
|
||||||
.index <keyword> Add a keyword to the index pointing to this topic
|
.index <keyword> Add a keyword to the index pointing to this topic
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
|
.h2 File Directives
|
||||||
|
|
||||||
|
.table
|
||||||
|
.include <file> Insert another source file here (path absolute or
|
||||||
|
relative to the including file)
|
||||||
|
.section <name> Group the following table of contents entries of
|
||||||
|
this file under a <name> heading
|
||||||
|
.section <a>|<b> Nest the group: heading <b> sits under heading <a>
|
||||||
|
(any depth; entries filed under <a> come first)
|
||||||
|
.endtable
|
||||||
|
|
||||||
.h2 Example
|
.h2 Example
|
||||||
|
|
||||||
.code
|
.code
|
||||||
|
|
@ -170,7 +196,7 @@ The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into bi
|
||||||
.h2 Usage
|
.h2 Usage
|
||||||
|
|
||||||
.code
|
.code
|
||||||
dvxhlpc -o output.hlp [-i imagedir] [--html out.html] input.dhs [...]
|
dvxhlpc -o output.hlp [-i imagedir] [--html out.html] [--quiet] input.dhs [@filelist] [...]
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
.h2 Options
|
.h2 Options
|
||||||
|
|
@ -179,10 +205,26 @@ dvxhlpc -o output.hlp [-i imagedir] [--html out.html] input.dhs [...]
|
||||||
-o output.hlp Output binary help file (required)
|
-o output.hlp Output binary help file (required)
|
||||||
-i imagedir Directory to find .image files (default: current dir)
|
-i imagedir Directory to find .image files (default: current dir)
|
||||||
--html out.html Also emit a self-contained HTML file
|
--html out.html Also emit a self-contained HTML file
|
||||||
|
--quiet Suppress progress output
|
||||||
|
@filelist Read more input file names from filelist, one per
|
||||||
|
line (blank lines and lines starting with # are
|
||||||
|
skipped)
|
||||||
.endtable
|
.endtable
|
||||||
|
|
||||||
Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.
|
Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.
|
||||||
|
|
||||||
|
.h2 Input Files
|
||||||
|
|
||||||
|
Each input file starts fresh: no topic is current and no .list, .note, .code or .table block is left open from the previous file. Text before a file's first .topic is ignored, and directives there are errors. A .code or .table block still open at the end of a file is closed with a warning. Files named with .include are different: they are read in place, as if their lines were part of the including file.
|
||||||
|
|
||||||
|
Source lines longer than 4094 characters are an error. The .image directive may name a path relative to the -i directory, including a subdirectory (for example sub/pic.png).
|
||||||
|
|
||||||
|
If any error is reported the compile stops and the previous output file is kept.
|
||||||
|
|
||||||
|
.h2 Links
|
||||||
|
|
||||||
|
Every .link must name a topic that is compiled into the same help file: a topic in any of the input files or the files they include, before or after the link. A link to any other ID is an error that names the file, the line and the missing ID. The one exception is the help DVX rebuilds by itself when it starts after programs, widgets or libraries were added or removed: there a link to a topic that is no longer present shows as plain text instead, so removing a piece of DVX never breaks the rest of the help. Links cannot reach into another help file; name the other file's page in the text instead. A .link line inside a .code block is example text and is never checked.
|
||||||
|
|
||||||
.h2 Build Integration
|
.h2 Build Integration
|
||||||
|
|
||||||
The standard build pattern globs all fragments:
|
The standard build pattern globs all fragments:
|
||||||
|
|
@ -204,35 +246,49 @@ The --html flag produces a single self-contained HTML file with a sidebar table
|
||||||
.index Context Help
|
.index Context Help
|
||||||
.index helpFile
|
.index helpFile
|
||||||
.index helpTopic
|
.index helpTopic
|
||||||
.index shellLoadAppWithArgs
|
.index shellOpenHelp
|
||||||
|
.index HelpView
|
||||||
|
|
||||||
.h1 Application Integration
|
.h1 Application Integration
|
||||||
|
|
||||||
Any DVX application can provide context-sensitive help via the F1 key.
|
Every DVX application should have its own help file. F1 in the application, and the Contents command of its Help menu, open it. An application with no help file of its own gets the DVX user guide on F1.
|
||||||
|
|
||||||
.h2 Setting Up Help
|
.h2 Applications in the DVX Source Tree
|
||||||
|
|
||||||
In your appMain, set the help file path on the app context:
|
For an application built with DVX itself, write the help source as <app>.dhs in the application's source directory, next to <app>.c or <app>.dbp, where <app> is the name of the application's .app file. The build finds it by that name: it compiles it to <app>.hlp next to <app>.app, deploys the source, and writes an <APP>.HCF so the help is rebuilt on the target too. Nothing else needs to be listed anywhere.
|
||||||
|
|
||||||
|
.h2 C Applications
|
||||||
|
|
||||||
|
Name the help file after the application (myapp.hlp beside myapp.app) and F1 works with no code: before appMain runs, DVX sets ctx->helpFile to that file when it exists. A help file with any other name or location is set in appMain instead:
|
||||||
|
|
||||||
.code
|
.code
|
||||||
snprintf(ctx->helpFile, sizeof(ctx->helpFile),
|
snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "other.hlp");
|
||||||
"%s%cMYAPP.HLP", ctx->appDir, DVX_PATH_SEP);
|
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
|
To open it from a Help menu item, call shellOpenHelp. It reuses a running help viewer (and brings it to the front) instead of starting another one:
|
||||||
|
|
||||||
|
.code
|
||||||
|
shellOpenHelp(ctx->shellCtx, ctx->helpFile);
|
||||||
|
.endcode
|
||||||
|
|
||||||
|
The argument is "helpfile [topicId]", so a menu item can also open a particular topic. An empty help file name opens the DVX user guide, as F1 does for an application without a help file.
|
||||||
|
|
||||||
.h2 Context-Sensitive Topics
|
.h2 Context-Sensitive Topics
|
||||||
|
|
||||||
Update helpTopic as the user navigates your application:
|
Update helpTopic as the user moves around your application, or set onHelpQuery to a function that fills it in when F1 is pressed:
|
||||||
|
|
||||||
.code
|
.code
|
||||||
snprintf(ctx->helpTopic, sizeof(ctx->helpTopic), "settings.video");
|
snprintf(ctx->helpTopic, sizeof(ctx->helpTopic), "settings.video");
|
||||||
.endcode
|
.endcode
|
||||||
|
|
||||||
When the user presses F1, the shell launches the help viewer with your help file opened to the specified topic.
|
When the user presses F1, the help viewer opens your help file at that topic.
|
||||||
|
|
||||||
.h2 Launching Help from Menus
|
.h2 DVX BASIC Applications
|
||||||
|
|
||||||
To add a Help menu item that opens your help file:
|
A DVX BASIC program gets its help file from its project: set Help File in Project Properties. The file is copied next to the compiled program, and F1 opens it at the HelpTopic of the focused control, or else of the form. As for C applications, a myapp.hlp next to myapp.app is used when the project names no help file. To open it from a Help menu item, add help.bas to the project and call HelpView with the file name; a name without a folder is looked for next to the program:
|
||||||
|
|
||||||
.code
|
.code
|
||||||
shellLoadAppWithArgs(ctx, viewerPath, helpFilePath);
|
SUB mnuHelpContents_Click
|
||||||
|
HelpView "myapp.hlp"
|
||||||
|
END SUB
|
||||||
.endcode
|
.endcode
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Name = Help Editor
|
Name = Help Editor
|
||||||
Author = Scott Duensing
|
Author = Scott Duensing
|
||||||
Publisher = Kangaroo Punch Studios
|
Publisher = Kangaroo Punch Studios
|
||||||
|
Version = 1.00
|
||||||
Copyright = Copyright 2026 Scott Duensing
|
Copyright = Copyright 2026 Scott Duensing
|
||||||
Description = DVX Help source editor with syntax highlighting and preview
|
Description = DVX Help source editor with syntax highlighting and preview
|
||||||
Icon = ICON32.BMP
|
Icon = ICON32.BMP
|
||||||
|
|
|
||||||
105
src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
Normal file
105
src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
# Help Editor help. Compiled to helpedit.hlp next to helpedit.app; F1
|
||||||
|
# and Help > Contents in the Help Editor open it. The help source
|
||||||
|
# format itself is documented in the help viewer's own help (help.dhs),
|
||||||
|
# which Help > Help File Format opens.
|
||||||
|
|
||||||
|
.topic helpedit.overview
|
||||||
|
.title Help Editor
|
||||||
|
.toc 0 Help Editor
|
||||||
|
.default
|
||||||
|
.index Help Editor
|
||||||
|
.index Help Source
|
||||||
|
.index .dhs
|
||||||
|
|
||||||
|
.h1 Help Editor
|
||||||
|
|
||||||
|
The Help Editor is for writing help files, like the one you are reading now. You type the help text with a few simple commands that mark titles, headings, lists and links, then turn it into a finished help file and look at it in DVX Help.
|
||||||
|
|
||||||
|
Help text is kept in a help source file, whose name ends in .dhs. The finished help file has the same name, ending in .hlp, and is saved in the same folder.
|
||||||
|
|
||||||
|
Double-clicking a help source in the File Manager (or starting one from the Program Manager) opens it in the Help Editor. If a help source cannot be read, the Help Editor says so and keeps what you had.
|
||||||
|
|
||||||
|
.link helpedit.editing Writing help text
|
||||||
|
.link helpedit.building Compiling and previewing
|
||||||
|
|
||||||
|
.h2 Menus
|
||||||
|
|
||||||
|
.table
|
||||||
|
Menu Command What it does
|
||||||
|
---- ------- ------------
|
||||||
|
File New Start a new help source, with a small example to build on.
|
||||||
|
File Open... Open a help source file.
|
||||||
|
File Save Save the help source (asks for a name the first time).
|
||||||
|
File Save As... Save the help source under a new name.
|
||||||
|
File Exit Close the Help Editor.
|
||||||
|
Build Compile Save the help source and turn it into a help file.
|
||||||
|
Build Preview Compile, then open the help file in DVX Help.
|
||||||
|
Help Contents Open this help.
|
||||||
|
Help Help File Format Open the description of every command you can use in a help source.
|
||||||
|
Help About Help Editor Show the program name and copyright.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.topic helpedit.editing
|
||||||
|
.title Writing Help Text
|
||||||
|
.toc 0 Writing Help Text
|
||||||
|
.index Editing Help
|
||||||
|
.index Topics
|
||||||
|
|
||||||
|
.h1 Writing Help Text
|
||||||
|
|
||||||
|
When the Help Editor starts, and after File > New, the page holds a small example: one topic with a title, a table of contents entry and a heading. Replace it with your own text.
|
||||||
|
|
||||||
|
A help file is made of topics, the pages you see in DVX Help. Lines that start with a period are commands, for example .topic starts a new topic, .title gives it a title and .h1 makes a heading. Every other line is ordinary text that DVX Help wraps to fit its window. Leave an empty line between paragraphs.
|
||||||
|
|
||||||
|
Choose Help > Help File Format to see every command, with an example.
|
||||||
|
|
||||||
|
.h2 The Editor
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Commands are shown in color, so mistakes are easy to spot.
|
||||||
|
.item Line numbers are shown down the left side. They help you find your place in a long file.
|
||||||
|
.item Pressing Enter starts the new line with the same indent as the line above.
|
||||||
|
.item The Tab key puts a tab in your text.
|
||||||
|
.item The usual editing keys work, including Ctrl+X, Ctrl+C and Ctrl+V to cut, copy and paste, and Ctrl+Z to undo.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
A star (*) at the start of the title bar means there are unsaved changes. The Help Editor asks "Save changes?" before it starts a new file, opens another one, or closes.
|
||||||
|
|
||||||
|
.topic helpedit.building
|
||||||
|
.title Compiling and Previewing
|
||||||
|
.toc 0 Compiling and Previewing
|
||||||
|
.index Compile
|
||||||
|
.index Preview
|
||||||
|
.index .hlp
|
||||||
|
|
||||||
|
.h1 Compiling and Previewing
|
||||||
|
|
||||||
|
Choose Build > Compile to turn the help source into a help file. The Help Editor saves the source first; if it has never been saved, it asks for a name. The finished help file is written next to the source, with the ending .hlp. The line at the bottom of the window says "Compiled:" and the file name when it worked, or "Compile failed!" when it did not.
|
||||||
|
|
||||||
|
Choose Build > Preview to compile and then open the result in DVX Help, so you can see exactly what readers will see. If DVX Help is already open, the preview replaces what it was showing.
|
||||||
|
|
||||||
|
.note tip
|
||||||
|
Keep DVX Help open beside the Help Editor. After each change, choose Build > Preview to see the result straight away.
|
||||||
|
.endnote
|
||||||
|
|
@ -49,8 +49,17 @@ Begin Form HelpEdit
|
||||||
End
|
End
|
||||||
Begin Menu mnuHelp
|
Begin Menu mnuHelp
|
||||||
Caption = "&Help"
|
Caption = "&Help"
|
||||||
Begin Menu mnuHelpTopic
|
Begin Menu mnuHelpContents
|
||||||
Caption = "Help &Topics"
|
Caption = "&Contents"
|
||||||
|
End
|
||||||
|
Begin Menu mnuHelpFormat
|
||||||
|
Caption = "Help File &Format"
|
||||||
|
End
|
||||||
|
Begin Menu mnuHelpSep
|
||||||
|
Caption = "-"
|
||||||
|
End
|
||||||
|
Begin Menu mnuAbout
|
||||||
|
Caption = "&About Help Editor..."
|
||||||
End
|
End
|
||||||
End
|
End
|
||||||
Begin TextArea Editor
|
Begin TextArea Editor
|
||||||
|
|
@ -84,10 +93,15 @@ End
|
||||||
' IN THE SOFTWARE.
|
' IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
DIM currentFile AS STRING
|
' Help Editor's own help, and the help source format reference in the
|
||||||
DIM hlpFile AS STRING
|
' help viewer's help ("file topic", opened at that topic).
|
||||||
DIM savedHash AS LONG
|
CONST HELP_FILE = "helpedit.hlp"
|
||||||
DIM dirty AS INTEGER
|
CONST FORMAT_HELP = "dvxhelp.hlp help.format"
|
||||||
|
|
||||||
|
DIM SHARED currentFile AS STRING
|
||||||
|
DIM SHARED hlpFile AS STRING
|
||||||
|
DIM SHARED savedHash AS LONG
|
||||||
|
DIM SHARED dirty AS INTEGER
|
||||||
currentFile = ""
|
currentFile = ""
|
||||||
hlpFile = ""
|
hlpFile = ""
|
||||||
savedHash = 0
|
savedHash = 0
|
||||||
|
|
@ -109,6 +123,12 @@ Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".t
|
||||||
MarkClean
|
MarkClean
|
||||||
LblStatus.Caption = "Ready."
|
LblStatus.Caption = "Ready."
|
||||||
|
|
||||||
|
' A help source named on the command line (a document opened from
|
||||||
|
' Program Manager or File Manager) is loaded at once.
|
||||||
|
IF COMMAND$(1) <> "" THEN
|
||||||
|
OpenSource COMMAND$(1)
|
||||||
|
END IF
|
||||||
|
|
||||||
|
|
||||||
FUNCTION TextHash() AS LONG
|
FUNCTION TextHash() AS LONG
|
||||||
DIM h AS LONG
|
DIM h AS LONG
|
||||||
|
|
@ -134,7 +154,7 @@ FUNCTION AskSave() AS INTEGER
|
||||||
EXIT FUNCTION
|
EXIT FUNCTION
|
||||||
END IF
|
END IF
|
||||||
DIM result AS INTEGER
|
DIM result AS INTEGER
|
||||||
result = basPromptSave("Help Editor")
|
result = DlgPromptSave("Help Editor")
|
||||||
IF result = DVX_SAVE_YES THEN
|
IF result = DVX_SAVE_YES THEN
|
||||||
FileSave
|
FileSave
|
||||||
AskSave = 0
|
AskSave = 0
|
||||||
|
|
@ -194,24 +214,36 @@ SUB mnuOpen_Click
|
||||||
END IF
|
END IF
|
||||||
|
|
||||||
DIM path AS STRING
|
DIM path AS STRING
|
||||||
path = basFileOpen("Open Help Source", "Help Source (*.dhs)|All Files (*.*)")
|
path = DlgFileOpen$("Open Help Source", "Help Source (*.dhs)|All Files (*.*)")
|
||||||
IF path = "" THEN
|
IF path = "" THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
|
OpenSource path
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
' Loads the help source in path; a file that cannot be read is reported
|
||||||
|
' and the current text stays.
|
||||||
|
SUB OpenSource(path AS STRING)
|
||||||
|
DIM f AS INTEGER
|
||||||
|
DIM opened AS BOOLEAN
|
||||||
DIM text AS STRING
|
DIM text AS STRING
|
||||||
DIM ln AS STRING
|
DIM ln AS STRING
|
||||||
text = ""
|
opened = False
|
||||||
|
text = ""
|
||||||
|
|
||||||
OPEN path FOR INPUT AS #1
|
ON ERROR GOTO openFailed
|
||||||
DO WHILE NOT EOF(1)
|
f = FREEFILE
|
||||||
LINE INPUT #1, ln
|
OPEN path FOR INPUT AS #f
|
||||||
|
opened = True
|
||||||
|
DO WHILE NOT EOF(f)
|
||||||
|
LINE INPUT #f, ln
|
||||||
IF text <> "" THEN
|
IF text <> "" THEN
|
||||||
text = text + CHR$(10)
|
text = text + CHR$(10)
|
||||||
END IF
|
END IF
|
||||||
text = text + ln
|
text = text + ln
|
||||||
LOOP
|
LOOP
|
||||||
CLOSE #1
|
CLOSE #f
|
||||||
|
|
||||||
Editor.Text = text
|
Editor.Text = text
|
||||||
currentFile = path
|
currentFile = path
|
||||||
|
|
@ -219,6 +251,13 @@ SUB mnuOpen_Click
|
||||||
MarkClean
|
MarkClean
|
||||||
LblStatus.Caption = path
|
LblStatus.Caption = path
|
||||||
UpdateTitle
|
UpdateTitle
|
||||||
|
EXIT SUB
|
||||||
|
|
||||||
|
openFailed:
|
||||||
|
IF opened THEN
|
||||||
|
CLOSE #f
|
||||||
|
END IF
|
||||||
|
MsgBox "Could not open " + path + " (error" + STR$(ERR) + ")."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -240,7 +279,7 @@ END SUB
|
||||||
|
|
||||||
SUB FileSaveAs
|
SUB FileSaveAs
|
||||||
DIM path AS STRING
|
DIM path AS STRING
|
||||||
path = basFileSave("Save Help Source", "Help Source (*.dhs)|All Files (*.*)")
|
path = DlgFileSave$("Save Help Source", "Help Source (*.dhs)|All Files (*.*)")
|
||||||
IF path = "" THEN
|
IF path = "" THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
|
|
@ -310,9 +349,8 @@ END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuPreview_Click
|
SUB mnuPreview_Click
|
||||||
IF hlpFile = "" THEN
|
' Always compile first so the preview shows the current text.
|
||||||
DoCompile
|
DoCompile
|
||||||
END IF
|
|
||||||
|
|
||||||
IF hlpFile = "" THEN
|
IF hlpFile = "" THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
|
|
@ -323,8 +361,22 @@ SUB mnuPreview_Click
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuHelpTopic_Click
|
SUB mnuHelpContents_Click
|
||||||
HelpView "dvxhelp.hlp"
|
HelpView HELP_FILE
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
SUB mnuHelpFormat_Click
|
||||||
|
HelpView FORMAT_HELP
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
SUB mnuAbout_Click
|
||||||
|
DIM msg AS STRING
|
||||||
|
msg = "Help Editor" + CHR$(10) + CHR$(10)
|
||||||
|
msg = msg + "Writes and previews DVX help files." + CHR$(10) + CHR$(10)
|
||||||
|
msg = msg + "Copyright 2026 Scott Duensing"
|
||||||
|
MsgBox msg, vbOKOnly + vbInformation, "About Help Editor"
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,9 +140,12 @@ typedef struct {
|
||||||
// TOC entry (8 bytes)
|
// TOC entry (8 bytes)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// topicIdx of a TOC entry that opens no topic (a .section group header).
|
||||||
|
#define HLP_NO_TOPIC 0xFFFF
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t titleStr; // string table offset
|
uint32_t titleStr; // string table offset
|
||||||
uint16_t topicIdx; // index into topic directory (0xFFFF = no topic)
|
uint16_t topicIdx; // index into topic directory (HLP_NO_TOPIC = no topic)
|
||||||
uint8_t depth; // nesting depth (0 = root)
|
uint8_t depth; // nesting depth (0 = root)
|
||||||
uint8_t flags; // HLP_TOC_EXPANDED
|
uint8_t flags; // HLP_TOC_EXPANDED
|
||||||
} HlpTocEntryT;
|
} HlpTocEntryT;
|
||||||
|
|
|
||||||
1740
src/apps/kpunch/fileMgr/fileMgr.c
Normal file
1740
src/apps/kpunch/fileMgr/fileMgr.c
Normal file
File diff suppressed because it is too large
Load diff
280
src/apps/kpunch/fileMgr/fileMgr.dhs
Normal file
280
src/apps/kpunch/fileMgr/fileMgr.dhs
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
# 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.
|
||||||
|
# File Manager help. Compiled to fileMgr.hlp next to fileMgr.app; F1 and
|
||||||
|
# Help > Contents in a File Manager window open it.
|
||||||
|
|
||||||
|
.topic filemgr.overview
|
||||||
|
.title File Manager
|
||||||
|
.toc 0 File Manager
|
||||||
|
.default
|
||||||
|
.index File Manager
|
||||||
|
.index Files
|
||||||
|
.index Folders
|
||||||
|
.index Drives
|
||||||
|
|
||||||
|
.h1 File Manager
|
||||||
|
|
||||||
|
The File Manager shows the files and folders on your disks and lets you open, copy, move, rename and delete them.
|
||||||
|
|
||||||
|
.link filemgr.window The parts of the window
|
||||||
|
.link filemgr.open Opening files and starting programs
|
||||||
|
.link filemgr.select Choosing files
|
||||||
|
.link filemgr.copy Copying and moving
|
||||||
|
.link filemgr.organize New folders, renaming and deleting
|
||||||
|
.link filemgr.props Properties
|
||||||
|
.link filemgr.keys Keys and menus
|
||||||
|
|
||||||
|
.h2 More Than One Window
|
||||||
|
|
||||||
|
You can open the File Manager more than once, for example one window on each of two folders. File > New Window opens another File Manager on the folder you are looking at. Copy, Cut and Paste move files between the windows (see Copying and Moving).
|
||||||
|
|
||||||
|
.topic filemgr.window
|
||||||
|
.title The File Manager Window
|
||||||
|
.toc 1 The Window
|
||||||
|
.index Drive Bar
|
||||||
|
.index Path Bar
|
||||||
|
.index Folder Tree
|
||||||
|
.index Status Bar
|
||||||
|
.index Sorting
|
||||||
|
|
||||||
|
.h1 The File Manager Window
|
||||||
|
|
||||||
|
.h2 Drive Bar
|
||||||
|
|
||||||
|
The row of buttons at the top has one button for each drive in your computer, such as A: and C:. Click one to go to that drive. Drives without a disk in them (an empty floppy or CD-ROM drive) are listed too; choosing one tells you the drive is not ready instead of stopping with a disk error. Put a disk in and try again.
|
||||||
|
|
||||||
|
.h2 Path Bar
|
||||||
|
|
||||||
|
The Path box shows the folder you are looking at. To go somewhere else, type a folder name there, such as C:\DVX\APPS, and press Enter.
|
||||||
|
|
||||||
|
.h2 Folder Tree
|
||||||
|
|
||||||
|
The left side shows the folders from the top of the drive down to the one you are in, with the folders next to each step and the folders inside the current one. Click a folder, or move to it with the arrow keys, to see what is in it; its own folders then appear beneath it.
|
||||||
|
|
||||||
|
.h2 File List
|
||||||
|
|
||||||
|
The right side lists the folder's contents: folders first, then files. The columns are:
|
||||||
|
|
||||||
|
.table
|
||||||
|
Column What it shows
|
||||||
|
------ -------------
|
||||||
|
Name The file or folder name.
|
||||||
|
Size The file size in bytes.
|
||||||
|
Type Folder, Program, or the kind of file (for example TXT File).
|
||||||
|
Modified The date and time the file was last changed.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
Click a column heading to sort by that column; click it again to reverse the order. The View menu has the same choices (Sort by Name, Size, Type or Date, and Descending). Folders always stay at the top.
|
||||||
|
|
||||||
|
To show only some files, choose View > Filter... and type a pattern such as *.TXT. Several patterns can be separated with semicolons, for example *.TXT;*.BAT. *.* shows everything again. While a filter is on, it is shown at the end of the window title.
|
||||||
|
|
||||||
|
.h2 Status Bar
|
||||||
|
|
||||||
|
The bottom line shows how many items are in the folder, how many you have selected and how big they are together, and how much free space is left on the drive.
|
||||||
|
|
||||||
|
.topic filemgr.open
|
||||||
|
.title Opening Files and Starting Programs
|
||||||
|
.toc 1 Opening Files
|
||||||
|
.index Opening Files
|
||||||
|
.index Starting Programs
|
||||||
|
.index File Types
|
||||||
|
|
||||||
|
.h1 Opening Files and Starting Programs
|
||||||
|
|
||||||
|
Double-click an item, or select it and press Enter (or choose File > Open):
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item A folder opens in the same window.
|
||||||
|
.item A program (Type: Program) starts.
|
||||||
|
.item A document opens in the program chosen for its type, for example a .TXT file in Notepad. Which program opens each kind of file is set on the File Types tab of the Control Panel. If no program is chosen for a file's type, the File Manager says so.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
To go back up to the folder that holds the current one, press Backspace or choose View > Up One Level.
|
||||||
|
|
||||||
|
.topic filemgr.select
|
||||||
|
.title Choosing Files
|
||||||
|
.toc 1 Choosing Files
|
||||||
|
.index Selecting Files
|
||||||
|
.index Select All
|
||||||
|
.index Invert Selection
|
||||||
|
|
||||||
|
.h1 Choosing Files
|
||||||
|
|
||||||
|
Most commands work on the selected files and folders in the list.
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Click an item to select it.
|
||||||
|
.item Ctrl+click adds an item to the selection or removes it.
|
||||||
|
.item Shift+click selects everything between the last item you clicked and this one.
|
||||||
|
.item With the keyboard, the arrow keys move the highlight, Shift+arrow keys select a range, and Space selects or unselects the highlighted item. Typing the first letters of a name jumps to it.
|
||||||
|
.item Edit > Select All (Ctrl+A) selects everything; Edit > Invert Selection selects everything that was not selected and unselects the rest.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
If nothing is selected, commands work on the highlighted item. When the folder tree has the focus (click in it, or press Tab), Rename, Delete, Copy, Cut and Properties work on the folder chosen in the tree instead.
|
||||||
|
|
||||||
|
.topic filemgr.copy
|
||||||
|
.title Copying and Moving
|
||||||
|
.toc 1 Copying and Moving
|
||||||
|
.index Copying Files
|
||||||
|
.index Moving Files
|
||||||
|
.index Cut and Paste
|
||||||
|
.index Replace Existing Files
|
||||||
|
|
||||||
|
.h1 Copying and Moving
|
||||||
|
|
||||||
|
There are two ways to copy or move files and folders:
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item Select them and choose File > Copy To... (F8) or File > Move To... (F7), then type the folder they should go to.
|
||||||
|
.item Select them and choose Edit > Copy (Ctrl+C) or Edit > Cut (Ctrl+X). Then, in this or another File Manager window, go to the folder they should go to and choose Edit > Paste (Ctrl+V). Copy leaves the originals where they are; Cut moves them. Files you copied can be pasted again in other folders; after pasting a cut, the clipboard is empty.
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
Folders are copied or moved with everything in them. Copies keep the date of the original.
|
||||||
|
|
||||||
|
.h2 While It Works
|
||||||
|
|
||||||
|
Copying, moving and deleting happen in the background, in a small window that shows what is being done and how far along it is. You can keep working in DVX meanwhile. Click Cancel (or close that window) to stop; a file that was only partly copied is removed, and everything finished before that stays done.
|
||||||
|
|
||||||
|
.h2 When a File Already Exists
|
||||||
|
|
||||||
|
If the place you copy or move to already has a file with the same name, you are asked what to do:
|
||||||
|
|
||||||
|
.table
|
||||||
|
Button What it does
|
||||||
|
------ ------------
|
||||||
|
Yes Replace that file.
|
||||||
|
No Keep the existing file and skip this one.
|
||||||
|
Yes to All Replace this file and every other one without asking again.
|
||||||
|
Cancel Stop copying.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 When Something Goes Wrong
|
||||||
|
|
||||||
|
If a file cannot be read or written (for example the disk is full, write-protected, or was taken out of the drive), you are told why and can choose Retry (try again, for example after putting the disk back), Skip (leave this item out and go on) or Cancel. A folder whose contents were skipped is not removed by a move.
|
||||||
|
|
||||||
|
A folder cannot be copied or moved into itself or into one of its own folders, and a file cannot be copied onto itself; you are told if you try.
|
||||||
|
|
||||||
|
.topic filemgr.organize
|
||||||
|
.title New Folders, Renaming and Deleting
|
||||||
|
.toc 1 Organizing
|
||||||
|
.index New Folder
|
||||||
|
.index Renaming Files
|
||||||
|
.index Deleting Files
|
||||||
|
.index Read-Only Files
|
||||||
|
.index File Names
|
||||||
|
|
||||||
|
.h1 New Folders, Renaming and Deleting
|
||||||
|
|
||||||
|
.h2 New Folder
|
||||||
|
|
||||||
|
File > New Folder... makes a folder in the current folder. Type its name and press Enter.
|
||||||
|
|
||||||
|
.h2 Rename
|
||||||
|
|
||||||
|
Select one file or folder and choose File > Rename... or press F2, then type the new name.
|
||||||
|
|
||||||
|
.h2 Delete
|
||||||
|
|
||||||
|
Select the files and folders and press Del or choose File > Delete. You are asked to confirm first. Folders are deleted with everything in them. If a file is read-only, you are asked again before it is deleted (Yes, No, Yes to All or Cancel).
|
||||||
|
|
||||||
|
.h2 File Names
|
||||||
|
|
||||||
|
Names follow the DOS rules: up to eight characters, optionally followed by a dot and up to three more (for example REPORT.TXT). Letters, digits and most punctuation are allowed; spaces, the characters " * + , / : ; < = > ? [ \ ] | and device names such as CON, PRN or LPT1 are not. If a name is not allowed, the File Manager tells you why.
|
||||||
|
|
||||||
|
.topic filemgr.props
|
||||||
|
.title Properties
|
||||||
|
.toc 1 Properties
|
||||||
|
.index Properties
|
||||||
|
.index Attributes
|
||||||
|
.index Read-Only
|
||||||
|
.index Hidden Files
|
||||||
|
.index Archive
|
||||||
|
|
||||||
|
.h1 Properties
|
||||||
|
|
||||||
|
Select one or more items and choose File > Properties or press Alt+Enter. The Properties window shows the name, the folder it is in, the total size (for a folder, everything in it) with the number of files and folders, and the date the item was last changed.
|
||||||
|
|
||||||
|
The Attributes boxes can be changed; click OK to apply them to every selected item, or Cancel to leave them as they were.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Attribute Meaning
|
||||||
|
--------- -------
|
||||||
|
Read-only The file cannot be changed or deleted by programs until this is cleared.
|
||||||
|
Hidden The file is left out of normal DOS directory listings.
|
||||||
|
System The file belongs to the operating system.
|
||||||
|
Archive The file has changed since it was last backed up.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.topic filemgr.keys
|
||||||
|
.title Keys and Menus
|
||||||
|
.toc 1 Keys and Menus
|
||||||
|
.index File Manager Keys
|
||||||
|
.index Shortcut Keys
|
||||||
|
|
||||||
|
.h1 Keys and Menus
|
||||||
|
|
||||||
|
.table
|
||||||
|
Key What it does
|
||||||
|
--- ------------
|
||||||
|
Tab Move between the drive bar, the path bar, the folder tree and the file list.
|
||||||
|
Enter Open the highlighted item.
|
||||||
|
Backspace Go up to the parent folder.
|
||||||
|
Del Delete the selection.
|
||||||
|
F2 Rename.
|
||||||
|
F5 Read the folder again (show changes made elsewhere).
|
||||||
|
F7 Move to a folder.
|
||||||
|
F8 Copy to a folder.
|
||||||
|
Alt+Enter Properties.
|
||||||
|
Ctrl+A Select all.
|
||||||
|
Ctrl+C Copy.
|
||||||
|
Ctrl+X Cut.
|
||||||
|
Ctrl+V Paste.
|
||||||
|
F1 Help.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 Menus
|
||||||
|
|
||||||
|
.table
|
||||||
|
Menu Command What it does
|
||||||
|
---- ------- ------------
|
||||||
|
File Open Open the highlighted item.
|
||||||
|
File New Folder Make a folder.
|
||||||
|
File Copy To Copy the selection to a folder you type.
|
||||||
|
File Move To Move the selection to a folder you type.
|
||||||
|
File Rename Rename the selected item.
|
||||||
|
File Delete Delete the selection.
|
||||||
|
File Properties Size, date and attributes.
|
||||||
|
File New Window Open another File Manager window.
|
||||||
|
File Exit Close this window.
|
||||||
|
Edit Cut, Copy, Paste Move or copy files between folders and windows.
|
||||||
|
Edit Select All Select everything.
|
||||||
|
Edit Invert Selection Swap selected and unselected items.
|
||||||
|
View Refresh Read the folder again.
|
||||||
|
View Filter Show only files matching a pattern.
|
||||||
|
View Up One Level Go to the parent folder.
|
||||||
|
View Sort by ... Sort by name, size, type or date.
|
||||||
|
View Descending Reverse the sort order.
|
||||||
|
Help Contents Open this help.
|
||||||
|
Help About Show the program name and copyright.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
If you close a File Manager window while it is still copying, moving or deleting, you are asked whether to stop.
|
||||||
7
src/apps/kpunch/fileMgr/fileMgr.res
Normal file
7
src/apps/kpunch/fileMgr/fileMgr.res
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# fileMgr.res -- Resource manifest for File Manager
|
||||||
|
icon32 icon icon32.bmp
|
||||||
|
name text "File Manager"
|
||||||
|
author text "Scott Duensing"
|
||||||
|
copyright text "Copyright 2026 Scott Duensing"
|
||||||
|
publisher text "Kangaroo Punch Studios"
|
||||||
|
description text "Browse, copy, move, rename and delete files and folders"
|
||||||
BIN
src/apps/kpunch/fileMgr/icon32.bmp
(Stored with Git LFS)
Normal file
BIN
src/apps/kpunch/fileMgr/icon32.bmp
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -9,7 +9,9 @@ Icon = ICON32.BMP
|
||||||
|
|
||||||
[Modules]
|
[Modules]
|
||||||
File0 = ../../../include/basic/commdlg.bas
|
File0 = ../../../include/basic/commdlg.bas
|
||||||
Count = 1
|
File1 = ../../../include/basic/help.bas
|
||||||
|
File2 = ../../../include/basic/print.bas
|
||||||
|
Count = 3
|
||||||
|
|
||||||
[Forms]
|
[Forms]
|
||||||
File0 = iconed.frm
|
File0 = iconed.frm
|
||||||
|
|
|
||||||
136
src/apps/kpunch/iconed/iconed.dhs
Normal file
136
src/apps/kpunch/iconed/iconed.dhs
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
# Icon Editor help. Compiled to iconed.hlp next to iconed.app; F1 and
|
||||||
|
# Help > Contents in the Icon Editor open it.
|
||||||
|
|
||||||
|
.topic iconed.overview
|
||||||
|
.title Icon Editor
|
||||||
|
.toc 0 Icon Editor
|
||||||
|
.default
|
||||||
|
.index Icon Editor
|
||||||
|
.index Icons
|
||||||
|
.index Drawing
|
||||||
|
|
||||||
|
.h1 Icon Editor
|
||||||
|
|
||||||
|
The Icon Editor lets you draw small square pictures, such as program icons, one dot at a time.
|
||||||
|
|
||||||
|
.h2 The Window
|
||||||
|
|
||||||
|
.list
|
||||||
|
.item The large grid on the left is your picture, enlarged so each dot is easy to hit.
|
||||||
|
.item Preview shows the picture at its real size.
|
||||||
|
.item Color shows the color you are drawing with.
|
||||||
|
.item Palette holds 32 colors to choose from.
|
||||||
|
.item The line at the bottom shows the tool you picked, or the position of the dot under the pointer (counting from 0 at the top left corner).
|
||||||
|
.endlist
|
||||||
|
|
||||||
|
The title bar shows the picture size, the file name once the picture has been opened or saved, and a star (*) when there are unsaved changes.
|
||||||
|
|
||||||
|
.link iconed.drawing Drawing a picture
|
||||||
|
.link iconed.files Opening and saving pictures
|
||||||
|
|
||||||
|
.topic iconed.drawing
|
||||||
|
.title Drawing a Picture
|
||||||
|
.toc 0 Drawing a Picture
|
||||||
|
.index Pencil
|
||||||
|
.index Eraser
|
||||||
|
.index Color Picker
|
||||||
|
.index Palette
|
||||||
|
.index Picture Size
|
||||||
|
|
||||||
|
.h1 Drawing a Picture
|
||||||
|
|
||||||
|
.h2 Choosing a Color
|
||||||
|
|
||||||
|
Click a color in the Palette. The Color box changes to show it.
|
||||||
|
|
||||||
|
.h2 Drawing
|
||||||
|
|
||||||
|
Click a square on the grid to paint that dot, or hold the mouse button down and drag to paint many dots.
|
||||||
|
|
||||||
|
.h2 Tools
|
||||||
|
|
||||||
|
Pick a tool from the Tools menu. The chosen tool has a mark beside it.
|
||||||
|
|
||||||
|
.table
|
||||||
|
Tool What it does
|
||||||
|
---- ------------
|
||||||
|
Pencil Paints dots with the current color. This is the tool you start with.
|
||||||
|
Eraser Paints dots with the light gray background color.
|
||||||
|
Color Picker Click a dot on the grid to make its color the current color. The Pencil is chosen again afterwards, ready to draw.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.note info
|
||||||
|
The Eraser's light gray is a real color, not a see-through area. It is saved as part of the picture.
|
||||||
|
.endnote
|
||||||
|
|
||||||
|
There is no Undo. To fix a mistake, paint over it.
|
||||||
|
|
||||||
|
.h2 Picture Size
|
||||||
|
|
||||||
|
The Size menu chooses how many dots the picture has: 16 x 16, 24 x 24 or 32 x 32. New pictures start at 32 x 32. Changing the size starts a fresh, empty picture; if you have unsaved changes, the Icon Editor asks first.
|
||||||
|
|
||||||
|
.topic iconed.files
|
||||||
|
.title Opening and Saving Pictures
|
||||||
|
.toc 0 Opening and Saving Pictures
|
||||||
|
.index New
|
||||||
|
.index Open
|
||||||
|
.index Save
|
||||||
|
.index PNG
|
||||||
|
.index BMP
|
||||||
|
|
||||||
|
.h1 Opening and Saving Pictures
|
||||||
|
|
||||||
|
.table
|
||||||
|
Command What it does
|
||||||
|
------- ------------
|
||||||
|
File > New Clears the picture, keeping the current size.
|
||||||
|
File > Open... Opens a BMP or PNG picture.
|
||||||
|
File > Save... Saves the picture as a PNG (or BMP) file.
|
||||||
|
File > Print... Prints the picture.
|
||||||
|
File > Exit Closes the Icon Editor.
|
||||||
|
.endtable
|
||||||
|
|
||||||
|
.h2 Opening
|
||||||
|
|
||||||
|
The Icon Editor can only open square pictures that are 16 x 16, 24 x 24 or 32 x 32 dots. Anything else is refused with a message, and your current picture stays as it was. Opening a picture switches the Size menu to match it.
|
||||||
|
|
||||||
|
A picture named when the Icon Editor starts, for example in the Arguments of a Program Manager item for it, opens right away.
|
||||||
|
|
||||||
|
.h2 Saving
|
||||||
|
|
||||||
|
File > Save... always asks for a file name, so you can save the picture more than once under different names. Pictures are saved in PNG format, unless you give the file name the ending .bmp, which saves a BMP picture instead.
|
||||||
|
|
||||||
|
.note info
|
||||||
|
When the screen is set to show fewer colors (256, or 32 or 65 thousand), colors can change slightly when a picture is opened or saved, because they are rounded to the nearest color the screen can show.
|
||||||
|
.endnote
|
||||||
|
|
||||||
|
.h2 Printing
|
||||||
|
|
||||||
|
File > Print... prints the picture as large as fits on the page, so every dot becomes a clearly visible square, in black and white with shades of gray made of dots. The Print dialog lets you choose where the printout goes and how many copies you want. Printing pictures needs the printer's model to be chosen on the Control Panel's Printer page (anything but Text only). You do not have to save the picture before printing it.
|
||||||
|
|
||||||
|
.h2 Unsaved Changes
|
||||||
|
|
||||||
|
File > New and File > Open... ask before throwing away unsaved changes. When you close the Icon Editor with unsaved changes it asks "Save changes?": Yes lets you save first, No closes without saving, and Cancel keeps the Icon Editor open.
|
||||||
|
|
@ -17,6 +17,9 @@ Begin Form IconEd
|
||||||
Begin Menu mnuSave
|
Begin Menu mnuSave
|
||||||
Caption = "&Save..."
|
Caption = "&Save..."
|
||||||
End
|
End
|
||||||
|
Begin Menu mnuPrint
|
||||||
|
Caption = "&Print..."
|
||||||
|
End
|
||||||
Begin Menu mnuSep1
|
Begin Menu mnuSep1
|
||||||
Caption = "-"
|
Caption = "-"
|
||||||
End
|
End
|
||||||
|
|
@ -58,6 +61,18 @@ Begin Form IconEd
|
||||||
RadioCheck = True
|
RadioCheck = True
|
||||||
End
|
End
|
||||||
End
|
End
|
||||||
|
Begin Menu mnuHelp
|
||||||
|
Caption = "&Help"
|
||||||
|
Begin Menu mnuHelpContents
|
||||||
|
Caption = "&Contents"
|
||||||
|
End
|
||||||
|
Begin Menu mnuHelpSep
|
||||||
|
Caption = "-"
|
||||||
|
End
|
||||||
|
Begin Menu mnuAbout
|
||||||
|
Caption = "&About Icon Editor..."
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
Begin Frame fraMain
|
Begin Frame fraMain
|
||||||
Caption = ""
|
Caption = ""
|
||||||
|
|
@ -140,6 +155,12 @@ End
|
||||||
|
|
||||||
OPTION EXPLICIT
|
OPTION EXPLICIT
|
||||||
|
|
||||||
|
|
||||||
|
' Pixels are held in pixels() at their exact RGB values. The preview
|
||||||
|
' canvas (used for Load and Save) stores the display's pixel format, so
|
||||||
|
' in 15/16-bit and 256-color display modes colors are rounded to what the
|
||||||
|
' display can show when an icon is opened or saved.
|
||||||
|
|
||||||
CONST CANVAS_SIZE = 256
|
CONST CANVAS_SIZE = 256
|
||||||
CONST BG_COLOR = &H00C0C0C0
|
CONST BG_COLOR = &H00C0C0C0
|
||||||
CONST GRID_COLOR = &H00808080
|
CONST GRID_COLOR = &H00808080
|
||||||
|
|
@ -149,18 +170,26 @@ CONST TOOL_ERASER = 1
|
||||||
CONST TOOL_PICKER = 2
|
CONST TOOL_PICKER = 2
|
||||||
CONST PAL_COLS = 8
|
CONST PAL_COLS = 8
|
||||||
CONST PAL_ROWS = 4
|
CONST PAL_ROWS = 4
|
||||||
|
CONST PAL_COUNT = 32 ' PAL_COLS * PAL_ROWS (CONST takes only literals)
|
||||||
CONST PAL_CELL = 10
|
CONST PAL_CELL = 10
|
||||||
|
CONST SIZE_SMALL = 16
|
||||||
|
CONST SIZE_MEDIUM = 24
|
||||||
|
CONST MAX_ICON = 32
|
||||||
|
CONST HELP_FILE = "iconed.hlp"
|
||||||
|
CONST OPEN_FILTER = "Icon Images (*.bmp;*.png)|All Files (*.*)"
|
||||||
|
CONST SAVE_FILTER = "PNG Images (*.png)|All Files (*.*)"
|
||||||
|
|
||||||
|
DIM SHARED pixels(MAX_ICON - 1, MAX_ICON - 1) AS LONG
|
||||||
|
DIM SHARED palette(PAL_COUNT - 1) AS LONG
|
||||||
|
DIM SHARED iconW AS INTEGER
|
||||||
|
DIM SHARED iconH AS INTEGER
|
||||||
|
DIM SHARED pixelSize AS INTEGER
|
||||||
|
DIM SHARED curTool AS INTEGER
|
||||||
|
DIM SHARED curColor AS LONG
|
||||||
|
DIM SHARED drawing AS BOOLEAN
|
||||||
|
DIM SHARED dirty AS BOOLEAN
|
||||||
|
DIM SHARED filePath AS STRING
|
||||||
|
|
||||||
DIM pixels(31, 31) AS LONG
|
|
||||||
DIM palette(31) AS LONG
|
|
||||||
DIM iconW AS INTEGER
|
|
||||||
DIM iconH AS INTEGER
|
|
||||||
DIM pixelSize AS INTEGER
|
|
||||||
DIM curTool AS INTEGER
|
|
||||||
DIM curColor AS LONG
|
|
||||||
DIM drawing AS BOOLEAN
|
|
||||||
DIM dirty AS BOOLEAN
|
|
||||||
DIM filePath AS STRING
|
|
||||||
|
|
||||||
SUB InitPalette
|
SUB InitPalette
|
||||||
palette(0) = &H00000000
|
palette(0) = &H00000000
|
||||||
|
|
@ -197,23 +226,46 @@ SUB InitPalette
|
||||||
palette(31) = &H00C0C0FF
|
palette(31) = &H00C0C0FF
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB ClearPixels
|
SUB ClearPixels
|
||||||
DIM x AS INTEGER
|
DIM x AS INTEGER
|
||||||
DIM y AS INTEGER
|
DIM y AS INTEGER
|
||||||
FOR y = 0 TO 31
|
FOR y = 0 TO MAX_ICON - 1
|
||||||
FOR x = 0 TO 31
|
FOR x = 0 TO MAX_ICON - 1
|
||||||
pixels(x, y) = BG_COLOR
|
pixels(x, y) = BG_COLOR
|
||||||
NEXT x
|
NEXT x
|
||||||
NEXT y
|
NEXT y
|
||||||
dirty = False
|
dirty = False
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
' The preview canvas always matches the icon size, so Save writes exactly
|
||||||
|
' iconW x iconH pixels.
|
||||||
SUB SetIconSize(w AS INTEGER)
|
SUB SetIconSize(w AS INTEGER)
|
||||||
iconW = w
|
iconW = w
|
||||||
iconH = w
|
iconH = w
|
||||||
pixelSize = CANVAS_SIZE \ w
|
pixelSize = CANVAS_SIZE \ w
|
||||||
|
cvPreview.Resize w, w
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
' Checks the Size menu item for the current icon size.
|
||||||
|
SUB CheckSizeMenu
|
||||||
|
IF iconW = SIZE_SMALL THEN
|
||||||
|
mnuSize16.Checked = True
|
||||||
|
ELSEIF iconW = SIZE_MEDIUM THEN
|
||||||
|
mnuSize24.Checked = True
|
||||||
|
ELSE
|
||||||
|
mnuSize32.Checked = True
|
||||||
|
END IF
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
FUNCTION IsIconSize(w AS INTEGER) AS BOOLEAN
|
||||||
|
IsIconSize = (w = SIZE_SMALL OR w = SIZE_MEDIUM OR w = MAX_ICON)
|
||||||
|
END FUNCTION
|
||||||
|
|
||||||
|
|
||||||
SUB DrawEditorGrid
|
SUB DrawEditorGrid
|
||||||
DIM x AS INTEGER
|
DIM x AS INTEGER
|
||||||
DIM y AS INTEGER
|
DIM y AS INTEGER
|
||||||
|
|
@ -235,10 +287,10 @@ SUB DrawEditorGrid
|
||||||
cvEditor.Refresh
|
cvEditor.Refresh
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB DrawPreview
|
SUB DrawPreview
|
||||||
DIM x AS INTEGER
|
DIM x AS INTEGER
|
||||||
DIM y AS INTEGER
|
DIM y AS INTEGER
|
||||||
cvPreview.Clear BG_COLOR
|
|
||||||
FOR y = 0 TO iconH - 1
|
FOR y = 0 TO iconH - 1
|
||||||
FOR x = 0 TO iconW - 1
|
FOR x = 0 TO iconW - 1
|
||||||
cvPreview.SetPixel x, y, pixels(x, y)
|
cvPreview.SetPixel x, y, pixels(x, y)
|
||||||
|
|
@ -247,12 +299,13 @@ SUB DrawPreview
|
||||||
cvPreview.Refresh
|
cvPreview.Refresh
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB DrawPalette
|
SUB DrawPalette
|
||||||
DIM i AS INTEGER
|
DIM i AS INTEGER
|
||||||
DIM px AS INTEGER
|
DIM px AS INTEGER
|
||||||
DIM py AS INTEGER
|
DIM py AS INTEGER
|
||||||
cvPalette.Clear BG_COLOR
|
cvPalette.Clear BG_COLOR
|
||||||
FOR i = 0 TO 31
|
FOR i = 0 TO PAL_COUNT - 1
|
||||||
px = (i MOD PAL_COLS) * PAL_CELL
|
px = (i MOD PAL_COLS) * PAL_CELL
|
||||||
py = (i \ PAL_COLS) * PAL_CELL
|
py = (i \ PAL_COLS) * PAL_CELL
|
||||||
cvPalette.FillRect px, py, PAL_CELL, PAL_CELL, palette(i)
|
cvPalette.FillRect px, py, PAL_CELL, PAL_CELL, palette(i)
|
||||||
|
|
@ -261,20 +314,23 @@ SUB DrawPalette
|
||||||
cvPalette.Refresh
|
cvPalette.Refresh
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB DrawSelectedColor
|
SUB DrawSelectedColor
|
||||||
cvSelColor.Clear curColor
|
cvSelColor.Clear curColor
|
||||||
cvSelColor.Refresh
|
cvSelColor.Refresh
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB RefreshAll
|
SUB RefreshAll
|
||||||
DrawEditorGrid
|
DrawEditorGrid
|
||||||
DrawPreview
|
DrawPreview
|
||||||
DrawSelectedColor
|
DrawSelectedColor
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB UpdateTitle
|
SUB UpdateTitle
|
||||||
DIM t AS STRING
|
DIM t AS STRING
|
||||||
t = "DVX Icon Editor - " + STR$(iconW) + "x" + STR$(iconH)
|
t = "DVX Icon Editor - " + LTRIM$(STR$(iconW)) + "x" + LTRIM$(STR$(iconH))
|
||||||
IF filePath <> "" THEN
|
IF filePath <> "" THEN
|
||||||
t = t + " - " + filePath
|
t = t + " - " + filePath
|
||||||
END IF
|
END IF
|
||||||
|
|
@ -284,6 +340,7 @@ SUB UpdateTitle
|
||||||
IconEd.Caption = t
|
IconEd.Caption = t
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB SetIconPixel(x AS INTEGER, y AS INTEGER)
|
SUB SetIconPixel(x AS INTEGER, y AS INTEGER)
|
||||||
DIM c AS LONG
|
DIM c AS LONG
|
||||||
IF x < 0 OR x >= iconW OR y < 0 OR y >= iconH THEN
|
IF x < 0 OR x >= iconW OR y < 0 OR y >= iconH THEN
|
||||||
|
|
@ -296,8 +353,11 @@ SUB SetIconPixel(x AS INTEGER, y AS INTEGER)
|
||||||
END IF
|
END IF
|
||||||
IF pixels(x, y) <> c THEN
|
IF pixels(x, y) <> c THEN
|
||||||
pixels(x, y) = c
|
pixels(x, y) = c
|
||||||
dirty = True
|
' The caption only changes on the clean-to-dirty transition.
|
||||||
UpdateTitle
|
IF NOT dirty THEN
|
||||||
|
dirty = True
|
||||||
|
UpdateTitle
|
||||||
|
END IF
|
||||||
cvEditor.FillRect x * pixelSize, y * pixelSize, pixelSize, pixelSize, c
|
cvEditor.FillRect x * pixelSize, y * pixelSize, pixelSize, pixelSize, c
|
||||||
cvEditor.DrawLine x * pixelSize, y * pixelSize, (x + 1) * pixelSize, y * pixelSize, GRID_COLOR
|
cvEditor.DrawLine x * pixelSize, y * pixelSize, (x + 1) * pixelSize, y * pixelSize, GRID_COLOR
|
||||||
cvEditor.DrawLine x * pixelSize, y * pixelSize, x * pixelSize, (y + 1) * pixelSize, GRID_COLOR
|
cvEditor.DrawLine x * pixelSize, y * pixelSize, x * pixelSize, (y + 1) * pixelSize, GRID_COLOR
|
||||||
|
|
@ -307,6 +367,7 @@ SUB SetIconPixel(x AS INTEGER, y AS INTEGER)
|
||||||
END IF
|
END IF
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB SelectTool(t AS INTEGER)
|
SUB SelectTool(t AS INTEGER)
|
||||||
curTool = t
|
curTool = t
|
||||||
IF t = TOOL_PENCIL THEN
|
IF t = TOOL_PENCIL THEN
|
||||||
|
|
@ -321,6 +382,7 @@ SUB SelectTool(t AS INTEGER)
|
||||||
END IF
|
END IF
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB ChangeSize(newSize AS INTEGER)
|
SUB ChangeSize(newSize AS INTEGER)
|
||||||
DIM ans AS INTEGER
|
DIM ans AS INTEGER
|
||||||
IF newSize = iconW THEN
|
IF newSize = iconW THEN
|
||||||
|
|
@ -329,27 +391,32 @@ SUB ChangeSize(newSize AS INTEGER)
|
||||||
IF dirty THEN
|
IF dirty THEN
|
||||||
ans = MsgBox("Unsaved changes will be lost. Change size?", vbYesNo)
|
ans = MsgBox("Unsaved changes will be lost. Change size?", vbYesNo)
|
||||||
IF ans = vbNo THEN
|
IF ans = vbNo THEN
|
||||||
|
' The menu already moved the radio check; put it back.
|
||||||
|
CheckSizeMenu
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
END IF
|
END IF
|
||||||
SetIconSize newSize
|
SetIconSize newSize
|
||||||
ClearPixels
|
ClearPixels
|
||||||
filePath = ""
|
filePath = ""
|
||||||
IF newSize = 16 THEN
|
CheckSizeMenu
|
||||||
mnuSize16.Checked = True
|
|
||||||
ELSEIF newSize = 24 THEN
|
|
||||||
mnuSize24.Checked = True
|
|
||||||
ELSE
|
|
||||||
mnuSize32.Checked = True
|
|
||||||
END IF
|
|
||||||
UpdateTitle
|
UpdateTitle
|
||||||
RefreshAll
|
RefreshAll
|
||||||
lblStatus.Caption = STR$(iconW) + "x" + STR$(iconH) + " - Ready."
|
lblStatus.Caption = STR$(iconW) + "x" + STR$(iconH) + " - Ready."
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
' Writes the preview canvas to path. Returns False when the file could
|
||||||
|
' not be written.
|
||||||
|
FUNCTION SaveIcon(path AS STRING) AS BOOLEAN
|
||||||
|
DrawPreview
|
||||||
|
SaveIcon = (cvPreview.Save(path) = 0)
|
||||||
|
END FUNCTION
|
||||||
|
|
||||||
|
|
||||||
SUB IconEd_Load
|
SUB IconEd_Load
|
||||||
InitPalette
|
InitPalette
|
||||||
SetIconSize 32
|
SetIconSize MAX_ICON
|
||||||
ClearPixels
|
ClearPixels
|
||||||
curTool = TOOL_PENCIL
|
curTool = TOOL_PENCIL
|
||||||
curColor = &H00000000
|
curColor = &H00000000
|
||||||
|
|
@ -358,8 +425,14 @@ SUB IconEd_Load
|
||||||
DrawPalette
|
DrawPalette
|
||||||
RefreshAll
|
RefreshAll
|
||||||
UpdateTitle
|
UpdateTitle
|
||||||
|
' An icon named on the command line (a document opened from Program
|
||||||
|
' Manager or File Manager) is loaded at once.
|
||||||
|
IF COMMAND$(1) <> "" THEN
|
||||||
|
OpenIcon COMMAND$(1)
|
||||||
|
END IF
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuNew_Click
|
SUB mnuNew_Click
|
||||||
DIM ans AS INTEGER
|
DIM ans AS INTEGER
|
||||||
IF dirty THEN
|
IF dirty THEN
|
||||||
|
|
@ -374,79 +447,130 @@ SUB mnuNew_Click
|
||||||
RefreshAll
|
RefreshAll
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuOpen_Click
|
SUB mnuOpen_Click
|
||||||
DIM ans AS INTEGER
|
DIM ans AS INTEGER
|
||||||
DIM path AS STRING
|
DIM path AS STRING
|
||||||
DIM x AS INTEGER
|
|
||||||
DIM y AS INTEGER
|
|
||||||
IF dirty THEN
|
IF dirty THEN
|
||||||
ans = MsgBox("Unsaved changes. Open another file?", vbYesNo)
|
ans = MsgBox("Unsaved changes. Open another file?", vbYesNo)
|
||||||
IF ans = vbNo THEN
|
IF ans = vbNo THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
END IF
|
END IF
|
||||||
path = InputBox$("Open BMP file:", "Open Icon", filePath)
|
path = DlgFileOpen$("Open Icon", OPEN_FILTER)
|
||||||
IF path = "" THEN
|
IF path = "" THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
cvPreview.Load path
|
OpenIcon path
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
' Loads the icon in path; a file that is not a 16, 24 or 32 pixel square
|
||||||
|
' image is refused (reported) and the current icon stays.
|
||||||
|
SUB OpenIcon(path AS STRING)
|
||||||
|
DIM w AS INTEGER
|
||||||
|
DIM x AS INTEGER
|
||||||
|
DIM y AS INTEGER
|
||||||
|
' A failed Load leaves the preview unchanged; a successful one gives
|
||||||
|
' it the image's size.
|
||||||
|
IF cvPreview.Load(path) <> 0 THEN
|
||||||
|
MsgBox "Could not load " + path + "."
|
||||||
|
EXIT SUB
|
||||||
|
END IF
|
||||||
|
w = cvPreview.ImageWidth
|
||||||
|
IF cvPreview.ImageHeight <> w OR NOT IsIconSize(w) THEN
|
||||||
|
' Put the current icon back into the preview.
|
||||||
|
cvPreview.Resize iconW, iconH
|
||||||
|
DrawPreview
|
||||||
|
MsgBox "Icons must be 16 x 16, 24 x 24 or 32 x 32 pixels."
|
||||||
|
EXIT SUB
|
||||||
|
END IF
|
||||||
|
SetIconSize w
|
||||||
|
ClearPixels
|
||||||
FOR y = 0 TO iconH - 1
|
FOR y = 0 TO iconH - 1
|
||||||
FOR x = 0 TO iconW - 1
|
FOR x = 0 TO iconW - 1
|
||||||
pixels(x, y) = cvPreview.GetPixel(x, y)
|
pixels(x, y) = cvPreview.GetPixel(x, y)
|
||||||
NEXT x
|
NEXT x
|
||||||
NEXT y
|
NEXT y
|
||||||
filePath = path
|
filePath = path
|
||||||
dirty = False
|
CheckSizeMenu
|
||||||
UpdateTitle
|
UpdateTitle
|
||||||
RefreshAll
|
RefreshAll
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuSave_Click
|
SUB mnuSave_Click
|
||||||
DIM path AS STRING
|
DIM path AS STRING
|
||||||
IF filePath = "" THEN
|
path = DlgFileSave$("Save Icon", SAVE_FILTER)
|
||||||
path = InputBox$("Save BMP file:", "Save Icon", "ICON.BMP")
|
|
||||||
ELSE
|
|
||||||
path = InputBox$("Save BMP file:", "Save Icon", filePath)
|
|
||||||
END IF
|
|
||||||
IF path = "" THEN
|
IF path = "" THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
DrawPreview
|
IF NOT SaveIcon(path) THEN
|
||||||
cvPreview.Save path
|
MsgBox "Could not save " + path + "."
|
||||||
|
EXIT SUB
|
||||||
|
END IF
|
||||||
filePath = path
|
filePath = path
|
||||||
dirty = False
|
dirty = False
|
||||||
UpdateTitle
|
UpdateTitle
|
||||||
lblStatus.Caption = "Saved: " + path
|
lblStatus.Caption = "Saved: " + path
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
' The printing library prints picture files, so the icon goes through a
|
||||||
|
' scratch copy in the program's data folder.
|
||||||
|
SUB mnuPrint_Click
|
||||||
|
DIM tmp AS STRING
|
||||||
|
DIM title AS STRING
|
||||||
|
tmp = App.Data + "/PRINTICO.BMP"
|
||||||
|
IF NOT SaveIcon(tmp) THEN
|
||||||
|
MsgBox "Could not prepare the icon for printing."
|
||||||
|
EXIT SUB
|
||||||
|
END IF
|
||||||
|
IF filePath <> "" THEN
|
||||||
|
title = filePath
|
||||||
|
ELSE
|
||||||
|
title = "Untitled icon"
|
||||||
|
END IF
|
||||||
|
PrintPictureDialog tmp, title
|
||||||
|
KILL tmp
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuExit_Click
|
SUB mnuExit_Click
|
||||||
Unload IconEd
|
Unload IconEd
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuPencil_Click
|
SUB mnuPencil_Click
|
||||||
SelectTool TOOL_PENCIL
|
SelectTool TOOL_PENCIL
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuEraser_Click
|
SUB mnuEraser_Click
|
||||||
SelectTool TOOL_ERASER
|
SelectTool TOOL_ERASER
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuPicker_Click
|
SUB mnuPicker_Click
|
||||||
SelectTool TOOL_PICKER
|
SelectTool TOOL_PICKER
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuSize16_Click
|
SUB mnuSize16_Click
|
||||||
ChangeSize 16
|
ChangeSize SIZE_SMALL
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuSize24_Click
|
SUB mnuSize24_Click
|
||||||
ChangeSize 24
|
ChangeSize SIZE_MEDIUM
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB mnuSize32_Click
|
SUB mnuSize32_Click
|
||||||
ChangeSize 32
|
ChangeSize MAX_ICON
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB cvEditor_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
SUB cvEditor_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
||||||
DIM px AS INTEGER
|
DIM px AS INTEGER
|
||||||
DIM py AS INTEGER
|
DIM py AS INTEGER
|
||||||
|
|
@ -465,6 +589,7 @@ SUB cvEditor_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
||||||
lblStatus.Caption = "(" + STR$(px) + "," + STR$(py) + ")"
|
lblStatus.Caption = "(" + STR$(px) + "," + STR$(py) + ")"
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB cvEditor_MouseMove(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
SUB cvEditor_MouseMove(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
||||||
DIM px AS INTEGER
|
DIM px AS INTEGER
|
||||||
DIM py AS INTEGER
|
DIM py AS INTEGER
|
||||||
|
|
@ -478,16 +603,18 @@ SUB cvEditor_MouseMove(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
||||||
END IF
|
END IF
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB cvEditor_MouseUp(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
SUB cvEditor_MouseUp(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
||||||
drawing = False
|
drawing = False
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
SUB IconEd_QueryUnload(Cancel AS INTEGER)
|
SUB IconEd_QueryUnload(Cancel AS INTEGER)
|
||||||
IF NOT dirty THEN
|
IF NOT dirty THEN
|
||||||
EXIT SUB
|
EXIT SUB
|
||||||
END IF
|
END IF
|
||||||
DIM result AS INTEGER
|
DIM result AS INTEGER
|
||||||
result = basPromptSave("Icon Editor")
|
result = DlgPromptSave("Icon Editor")
|
||||||
IF result = DVX_SAVE_YES THEN
|
IF result = DVX_SAVE_YES THEN
|
||||||
mnuSave_Click
|
mnuSave_Click
|
||||||
IF dirty THEN
|
IF dirty THEN
|
||||||
|
|
@ -514,3 +641,17 @@ SUB cvPalette_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
|
||||||
END IF
|
END IF
|
||||||
END IF
|
END IF
|
||||||
END SUB
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
SUB mnuHelpContents_Click
|
||||||
|
HelpView HELP_FILE
|
||||||
|
END SUB
|
||||||
|
|
||||||
|
|
||||||
|
SUB mnuAbout_Click
|
||||||
|
DIM msg AS STRING
|
||||||
|
msg = "Icon Editor" + CHR$(10) + CHR$(10)
|
||||||
|
msg = msg + "Draws small square pictures and icons." + CHR$(10) + CHR$(10)
|
||||||
|
msg = msg + "Copyright 2026 Scott Duensing"
|
||||||
|
MsgBox msg, vbOKOnly + vbInformation, "About Icon Editor"
|
||||||
|
END SUB
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue