From 824fe02f8d30bc07459639d7731c5983a576d8ae Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 12 Oct 2026 14:19:24 -0500 Subject: [PATCH] Huge amount of BASIC API work. --- Makefile | 20 +- analyze.sh | 8 +- config/ASSOC.INI | 2 + cppcheck.sh | 4 +- docs/dvx_basic_reference.html | 1944 +++++++- docs/dvx_help_viewer.html | 2 +- docs/dvx_system_reference.html | 1599 ++++++- screenshots/86box/apps-basdemo.png | 4 +- screenshots/86box/apps-helpedit.png | 4 +- screenshots/86box/cpanel-cp-types.png | 4 +- screenshots/86box/f1-h-basdem.png | 4 +- screenshots/86box/f1-h-helped.png | 4 +- screenshots/86box/hang-ended.png | 4 +- screenshots/86box/hang-taskmgr.png | 4 +- screenshots/86box/menus-m-design.png | 4 +- screenshots/86box/pardemo-pardemo.png | 3 + screenshots/86box/pardsgn-pardsgn.png | 3 + screenshots/86box/parhlp-parhlp-l.png | 3 + screenshots/86box/parhlp-parhlp.png | 3 + screenshots/86box/parpm-parpm.png | 3 + screenshots/86box/sdk-sdktbx.png | 4 +- screenshots/86box/ucdemo-ucdemo.png | 3 + screenshots/86box/ucide-ucide-ds.png | 3 + screenshots/86box/ucide-ucide-ru.png | 3 + screenshots/86box/ucide-ucide-tb.png | 3 + screenshots/dosbox/apps-basdemo.png | 4 +- screenshots/dosbox/apps-helpedit.png | 4 +- screenshots/dosbox/cpanel-cp-types.png | 4 +- screenshots/dosbox/f1-h-basdem.png | 4 +- screenshots/dosbox/f1-h-helped.png | 4 +- screenshots/dosbox/hang-ended.png | 4 +- screenshots/dosbox/hang-taskmgr.png | 4 +- screenshots/dosbox/install-su-disk.png | 4 +- screenshots/dosbox/menus-m-design.png | 4 +- screenshots/dosbox/pardemo-pardemo.png | 3 + screenshots/dosbox/pardsgn-pardsgn.png | 3 + screenshots/dosbox/parhlp-parhlp-l.png | 3 + screenshots/dosbox/parhlp-parhlp.png | 3 + screenshots/dosbox/parpm-parpm.png | 3 + screenshots/dosbox/sdk-sdktbx.png | 4 +- screenshots/dosbox/ucdemo-ucdemo.png | 3 + screenshots/dosbox/ucide-ucide-ds.png | 3 + screenshots/dosbox/ucide-ucide-ru.png | 3 + screenshots/dosbox/ucide-ucide-tb.png | 3 + screenshots/masks/hang-ended.mask | 2 + screenshots/masks/hang-taskmgr.mask | 2 + screenshots/masks/upgrade-su-upgr.mask | 2 + src/apps/kpunch/Makefile | 119 +- src/apps/kpunch/README.md | 147 +- .../kpunch/clock/{icon32.bmp => ICON32.BMP} | 0 src/apps/kpunch/clock/clock.c | 295 -- src/apps/kpunch/clock/clock.dbp | 21 + src/apps/kpunch/clock/clock.frm | 151 + src/apps/kpunch/clock/clock.res | 7 - src/apps/kpunch/cpanel/cpJoystick.c | 85 +- src/apps/kpunch/cpanel/cpNet.c | 6 +- src/apps/kpunch/cpanel/cpPrinter.c | 32 +- src/apps/kpunch/cpanel/cpSounds.c | 71 +- src/apps/kpunch/cpanel/cpanel.c | 163 +- src/apps/kpunch/dvxbasic/Makefile | 55 +- src/apps/kpunch/dvxbasic/basBuild.c | 546 ++- src/apps/kpunch/dvxbasic/basBuild.h | 112 +- src/apps/kpunch/dvxbasic/basRes.h | 177 +- src/apps/kpunch/dvxbasic/basdev/basDebug.c | 1039 ++++ src/apps/kpunch/dvxbasic/basdev/basDebug.h | 148 + src/apps/kpunch/dvxbasic/basdev/basDevBas.c | 2013 ++++++++ src/apps/kpunch/dvxbasic/basdev/basHelpMap.c | 337 ++ src/apps/kpunch/dvxbasic/basdev/basHelpMap.h | 48 + src/apps/kpunch/dvxbasic/basdev/basProcs.c | 116 + src/apps/kpunch/dvxbasic/basdev/basProcs.h | 55 + src/apps/kpunch/dvxbasic/basdev/basProject.c | 1048 ++++ src/apps/kpunch/dvxbasic/basdev/basProject.h | 287 ++ src/apps/kpunch/dvxbasic/basdev/basSource.c | 385 ++ src/apps/kpunch/dvxbasic/basdev/basSource.h | 92 + src/apps/kpunch/dvxbasic/basdev/basValidate.c | 308 ++ src/apps/kpunch/dvxbasic/basdev/basValidate.h | 116 + src/apps/kpunch/dvxbasic/basdev/basdev.dep | 2 + src/apps/kpunch/dvxbasic/basdev/basdev.dhs | 127 + src/apps/kpunch/dvxbasic/basrt.dhs | 262 +- src/apps/kpunch/dvxbasic/compiler/ast.h | 3 +- src/apps/kpunch/dvxbasic/compiler/basEvents.h | 181 +- src/apps/kpunch/dvxbasic/compiler/compiler.c | 290 +- src/apps/kpunch/dvxbasic/compiler/fold.c | 2 +- src/apps/kpunch/dvxbasic/compiler/front.h | 12 + src/apps/kpunch/dvxbasic/compiler/gen.c | 37 +- src/apps/kpunch/dvxbasic/compiler/genExpr.c | 3 +- src/apps/kpunch/dvxbasic/compiler/genStmt.c | 2 + src/apps/kpunch/dvxbasic/compiler/obfuscate.c | 58 +- src/apps/kpunch/dvxbasic/compiler/obfuscate.h | 9 +- src/apps/kpunch/dvxbasic/compiler/opcodes.h | 30 +- src/apps/kpunch/dvxbasic/compiler/parse.c | 2 +- src/apps/kpunch/dvxbasic/compiler/parseDecl.c | 43 + src/apps/kpunch/dvxbasic/compiler/parseExpr.c | 7 +- src/apps/kpunch/dvxbasic/compiler/parseStmt.c | 16 +- src/apps/kpunch/dvxbasic/compiler/resolve.c | 276 +- .../kpunch/dvxbasic/compiler/resolveExpr.c | 116 +- src/apps/kpunch/dvxbasic/compiler/scope.h | 10 +- src/apps/kpunch/dvxbasic/compiler/strip.c | 20 +- src/apps/kpunch/dvxbasic/compiler/strip.h | 7 + src/apps/kpunch/dvxbasic/compiler/typecheck.c | 7 +- src/apps/kpunch/dvxbasic/ctrlover.dhs | 163 +- src/apps/kpunch/dvxbasic/form.dhs | 119 +- .../kpunch/dvxbasic/formrt/basNativeDos.c | 25 +- .../kpunch/dvxbasic/formrt/basNativeHost.c | 132 +- src/apps/kpunch/dvxbasic/formrt/basRunHost.c | 213 + src/apps/kpunch/dvxbasic/formrt/basRunHost.h | 99 + src/apps/kpunch/dvxbasic/formrt/formCtl.c | 1877 ++++++++ src/apps/kpunch/dvxbasic/formrt/formDesign.c | 1351 ++++++ src/apps/kpunch/dvxbasic/formrt/formGfx.c | 452 +- src/apps/kpunch/dvxbasic/formrt/formrt.c | 3987 +++++++++++----- src/apps/kpunch/dvxbasic/formrt/formrt.h | 509 +- src/apps/kpunch/dvxbasic/formrt/frmModel.c | 2405 ++++++++++ src/apps/kpunch/dvxbasic/formrt/frmModel.h | 486 ++ src/apps/kpunch/dvxbasic/formrt/frmModelBas.c | 920 ++++ src/apps/kpunch/dvxbasic/formrt/frmParser.c | 49 +- src/apps/kpunch/dvxbasic/formrt/frmParser.h | 25 + src/apps/kpunch/dvxbasic/ide/ideDesigner.c | 1819 +------ src/apps/kpunch/dvxbasic/ide/ideDesigner.h | 335 +- src/apps/kpunch/dvxbasic/ide/ideMain.c | 3771 ++++----------- src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c | 412 +- src/apps/kpunch/dvxbasic/ide/ideMenuEditor.h | 6 +- src/apps/kpunch/dvxbasic/ide/ideProject.c | 656 +-- src/apps/kpunch/dvxbasic/ide/ideProject.h | 139 +- src/apps/kpunch/dvxbasic/ide/ideProperties.c | 636 +-- src/apps/kpunch/dvxbasic/ide/ideProperties.h | 11 + src/apps/kpunch/dvxbasic/ide/ideToolbox.c | 85 +- src/apps/kpunch/dvxbasic/ideguide.dhs | 197 +- src/apps/kpunch/dvxbasic/langref.dhs | 1358 +++++- src/apps/kpunch/dvxbasic/runtime/serialize.c | 290 +- src/apps/kpunch/dvxbasic/runtime/serialize.h | 4 + src/apps/kpunch/dvxbasic/runtime/values.c | 101 +- src/apps/kpunch/dvxbasic/runtime/values.h | 27 +- src/apps/kpunch/dvxbasic/runtime/vm.c | 503 +- src/apps/kpunch/dvxbasic/runtime/vm.h | 136 +- src/apps/kpunch/dvxbasic/stub/bascomp.c | 88 +- src/apps/kpunch/dvxbasic/stub/basrun.c | 4 +- src/apps/kpunch/dvxbasic/stub/basstub.c | 131 +- src/apps/kpunch/dvxbasic/test_compact.c | 43 +- src/apps/kpunch/dvxbasic/test_compiler.c | 359 +- src/apps/kpunch/dvxbasic/test_suite.c | 446 +- src/apps/kpunch/dvxbasic/userctl.dhs | 219 + src/apps/kpunch/dvxhelp/dvxhelp.c | 1607 +------ src/apps/kpunch/dvxhelp/help.dhs | 2 +- src/apps/kpunch/dvxhelp/helpedit/helpedit.frm | 8 +- .../kpunch/fileMgr/{icon32.bmp => ICON32.BMP} | 0 src/apps/kpunch/fileMgr/fileMgr.bas | 218 + src/apps/kpunch/fileMgr/fileMgr.c | 1740 ------- src/apps/kpunch/fileMgr/fileMgr.dbp | 29 + src/apps/kpunch/fileMgr/fileMgr.frm | 1507 ++++++ src/apps/kpunch/fileMgr/fileMgr.res | 7 - src/apps/kpunch/fileMgr/fmJob.cls | 39 + src/apps/kpunch/fileMgr/fmProps.frm | 116 + src/apps/kpunch/progman/pmProps.c | 10 +- src/apps/kpunch/progman/pmView.c | 532 +-- src/apps/kpunch/progman/progman.c | 135 +- .../terminal/{icon32.bmp => ICON32.BMP} | 0 src/apps/kpunch/terminal/termBook.frm | 181 + src/apps/kpunch/terminal/termMain.bas | 29 + src/apps/kpunch/terminal/termPass.frm | 92 + src/apps/kpunch/terminal/termProg.frm | 271 ++ src/apps/kpunch/terminal/termSess.frm | 402 ++ src/apps/kpunch/terminal/termSession.c | 455 -- src/apps/kpunch/terminal/termSession.h | 161 - src/apps/kpunch/terminal/termXfer.frm | 123 + src/apps/kpunch/terminal/terminal.bas | 716 +++ src/apps/kpunch/terminal/terminal.c | 1662 ------- src/apps/kpunch/terminal/terminal.dbp | 33 + src/apps/kpunch/terminal/terminal.frm | 888 ++++ src/apps/kpunch/terminal/terminal.res | 7 - .../kpunch/welcome/{icon32.bmp => ICON32.BMP} | 0 src/apps/kpunch/welcome/welcome.c | 213 - src/apps/kpunch/welcome/welcome.dbp | 20 + src/apps/kpunch/welcome/welcome.frm | 146 + src/apps/kpunch/welcome/welcome.res | 7 - src/include/basic/comm.bas | 107 +- src/include/basic/commdlg.bas | 27 +- src/include/basic/compiler.bas | 312 ++ src/include/basic/conn.bas | 224 + src/include/basic/debug.bas | 148 + src/include/basic/designer.bas | 239 + src/include/basic/fileops.bas | 259 +- src/include/basic/game.bas | 17 +- src/include/basic/help.bas | 20 +- src/include/basic/hlpfile.bas | 140 + src/include/basic/joystick.bas | 117 + src/include/basic/layout.bas | 43 + src/include/basic/listhelp.bas | 252 + src/include/basic/music.bas | 32 + src/include/basic/net.bas | 98 +- src/include/basic/prefs.bas | 156 + src/include/basic/print.bas | 129 +- src/include/basic/resource.bas | 13 + src/include/basic/security.bas | 131 + src/include/basic/shell.bas | 397 ++ src/include/basic/sound.bas | 226 + src/include/basic/system.bas | 217 + src/include/basic/xfer.bas | 89 +- src/installer/pkgBuild.c | 7 +- src/installer/setupCore.c | 6 +- src/libs/kpunch/audio/audio.c | 9 +- src/libs/kpunch/audio/audio.dhs | 18 +- src/libs/kpunch/audio/audio.h | 94 +- src/libs/kpunch/audio/audioBas.c | 829 +++- src/libs/kpunch/audio/audioDesk.c | 16 +- src/libs/kpunch/audio/audioInt.h | 6 +- src/libs/kpunch/audio/audioLoad.c | 6 + src/libs/kpunch/audio/audioSet.c | 87 + src/libs/kpunch/conn/Makefile | 8 +- src/libs/kpunch/conn/conn.dep | 1 + src/libs/kpunch/conn/conn.dhs | 45 +- src/libs/kpunch/conn/connBas.c | 818 ++++ src/libs/kpunch/dvxnet/dvxNet.c | 19 + src/libs/kpunch/dvxnet/dvxNet.h | 10 +- src/libs/kpunch/dvxnet/dvxnet.dhs | 35 +- src/libs/kpunch/dvxnet/netBas.c | 544 ++- src/libs/kpunch/dvxnet/netInt.h | 5 + src/libs/kpunch/dvxnet/netSock.c | 1 + src/libs/kpunch/dvxshell/Makefile | 2 +- src/libs/kpunch/dvxshell/dvxshell.dhs | 434 +- .../kpunch/dvxshell}/pmModel.c | 414 +- .../kpunch/dvxshell}/pmModel.h | 101 +- src/libs/kpunch/dvxshell/pmModelBas.c | 466 ++ src/libs/kpunch/dvxshell/shellApp.c | 131 +- src/libs/kpunch/dvxshell/shellApp.h | 268 +- src/libs/kpunch/dvxshell/shellBas.c | 882 ++++ src/libs/kpunch/dvxshell/shellMain.c | 234 +- src/libs/kpunch/dvxshell/shellProg.c | 70 +- src/libs/kpunch/dvxshell/shellProg.h | 15 + src/libs/kpunch/dvxshell/shellRun.c | 76 + src/libs/kpunch/dvxshell/shellRun.h | 14 + src/libs/kpunch/dvxshell/shellSysBas.c | 543 +++ src/libs/kpunch/fileops/fileOps.c | 321 +- src/libs/kpunch/fileops/fileOps.h | 123 +- src/libs/kpunch/fileops/fileOpsBas.c | 754 ++- src/libs/kpunch/fileops/fileOpsSys.c | 127 +- src/libs/kpunch/fileops/fileOpsSys.h | 55 + src/libs/kpunch/fileops/fileops.dhs | 7 +- src/libs/kpunch/game/game.dhs | 2 +- src/libs/kpunch/game/game.h | 7 +- src/libs/kpunch/game/gameBas.c | 42 +- src/libs/kpunch/hlpfile/Makefile | 63 + src/libs/kpunch/hlpfile/hlpFile.c | 692 +++ src/libs/kpunch/hlpfile/hlpFile.h | 115 + src/libs/kpunch/hlpfile/hlpFileBas.c | 450 ++ .../kpunch/hlpfile/hlpFormat.h} | 58 +- src/libs/kpunch/hlpfile/hlpfile.dep | 1 + src/libs/kpunch/hlpfile/hlpfile.dhs | 97 + .../kpunch/joystick/drivers/joykeys/joyKeys.c | 22 +- src/libs/kpunch/joystick/joyBas.c | 274 +- src/libs/kpunch/joystick/joystick.c | 124 +- src/libs/kpunch/joystick/joystick.dhs | 14 +- src/libs/kpunch/joystick/joystick.h | 75 +- src/libs/kpunch/libdvx/Makefile | 4 +- src/libs/kpunch/libdvx/apiref.dhs | 494 +- src/libs/kpunch/libdvx/dvxApp.c | 455 +- src/libs/kpunch/libdvx/dvxApp.h | 66 +- src/libs/kpunch/libdvx/dvxDraw.c | 109 +- src/libs/kpunch/libdvx/dvxDraw.h | 13 + src/libs/kpunch/libdvx/dvxPrefs.h | 9 + src/libs/kpunch/libdvx/dvxPrefsBas.c | 465 ++ src/libs/kpunch/libdvx/dvxRes.h | 12 + src/libs/kpunch/libdvx/dvxTypes.h | 3 +- src/libs/kpunch/libdvx/dvxVideo.c | 36 +- src/libs/kpunch/libdvx/dvxVideo.h | 4 +- src/libs/kpunch/libdvx/dvxWgt.h | 302 +- src/libs/kpunch/libdvx/dvxWm.c | 103 +- src/libs/kpunch/libdvx/dvxWm.h | 41 +- src/libs/kpunch/libdvx/games.dhs | 3 +- src/libs/kpunch/libdvx/platform/dvxPlat.h | 8 +- .../kpunch/libdvx/platform/dvxPlatformDos.c | 5 + src/libs/kpunch/libdvx/sdkGuide.dhs | 108 +- src/libs/kpunch/libdvx/widgetClass.c | 170 +- src/libs/kpunch/libdvx/widgetCore.c | 23 +- src/libs/kpunch/libdvx/widgetEvent.c | 167 +- src/libs/kpunch/libdvx/widgetIface.c | 911 ++++ src/libs/kpunch/libdvx/widgetLayout.c | 38 + src/libs/kpunch/libdvx/widgetOps.c | 5 + src/libs/kpunch/libtasks/taskswitch.c | 13 +- src/libs/kpunch/listhelp/Makefile | 2 +- src/libs/kpunch/listhelp/README.md | 12 +- src/libs/kpunch/listhelp/listHelp.c | 480 +- src/libs/kpunch/listhelp/listHelp.h | 160 + src/libs/kpunch/listhelp/listHelpBas.c | 600 +++ src/libs/kpunch/listhelp/listhelp.dhs | 149 +- src/libs/kpunch/music/music.c | 2 +- src/libs/kpunch/music/music.dhs | 2 +- src/libs/kpunch/music/music.h | 2 + src/libs/kpunch/music/musicBas.c | 143 +- src/libs/kpunch/print/print.dhs | 23 +- src/libs/kpunch/print/print.h | 14 + src/libs/kpunch/print/printBas.c | 377 +- src/libs/kpunch/print/printJob.c | 26 + src/libs/kpunch/print/printRaster.c | 7 +- src/libs/kpunch/print/printUi.c | 26 +- src/libs/kpunch/serial/Makefile | 2 +- src/libs/kpunch/serial/packet/packet.h | 8 +- src/libs/kpunch/serial/rs232/rs232.c | 10 +- src/libs/kpunch/serial/rs232/rs232.h | 41 +- src/libs/kpunch/serial/seclink/secLink.h | 16 +- src/libs/kpunch/serial/security/secBas.c | 538 +++ src/libs/kpunch/serial/security/security.h | 6 +- src/libs/kpunch/serial/serial.dhs | 29 +- src/libs/kpunch/sql/Makefile | 6 +- src/libs/kpunch/taskmgr/shellTaskMgr.c | 22 +- src/libs/kpunch/taskmgr/taskmgr.dhs | 4 +- src/libs/kpunch/texthelp/textHelp.c | 361 +- src/libs/kpunch/texthelp/textHelp.h | 67 +- src/libs/kpunch/texthelp/texthelp.dhs | 12 +- src/libs/kpunch/xfer/xfer.dhs | 20 +- src/libs/kpunch/xfer/xfer.h | 13 + src/libs/kpunch/xfer/xferBas.c | 342 +- src/loader/loaderMain.c | 72 + src/sdk/newProject.sh | 13 +- src/sdk/sdk.mk | 21 +- src/sdk/templates/control/Makefile | 14 + src/sdk/templates/control/tplName.bhs | 41 + src/sdk/templates/control/tplName.ctl | 98 + src/sdk/templates/control/tplName.dbp | 15 + src/sdk/templates/library/tplName.bas | 4 + src/sdk/templates/library/tplName.c | 28 +- src/sdk/templates/library/tplName.dhs | 4 +- src/sdk/templates/library/tplName.h | 12 +- src/sdk/templates/widget/tplName.c | 1 + src/sdkHeaders.mk | 3 +- src/test/abi/Makefile | 2 +- src/test/abi/abiSizes.txt | 39 +- src/test/audio/Makefile | 10 +- src/test/audio/audioTest.c | 575 ++- src/test/basdev/Makefile | 69 + src/test/basdev/testBasDev.c | 1378 ++++++ src/test/basic/Makefile | 2 +- src/test/basic/fuzzCommon.c | 2 +- src/test/basic/fuzzCommon.h | 2 + src/test/basic/fuzzCompile.c | 8 +- src/test/basic/fuzzModule.c | 90 +- src/test/basic/runBasic.sh | 26 +- src/test/coverage/runCoverage.sh | 4 +- src/test/dialogs/dialogsTest.c | 2 +- src/test/dos/emuEnv.sh | 5 +- src/test/dos/fixtures/bigdir/bigdir.bas | 61 + src/test/dos/fixtures/bigdir/bigdir.dbp | 12 + src/test/dos/fixtures/iconview/iconview.bmp | 3 + src/test/dos/fixtures/iconview/iconview.ctl | 885 ++++ src/test/dos/fixtures/iconview/iconview.dbp | 20 + src/test/dos/fixtures/parcp/parcp.dbp | 26 + src/test/dos/fixtures/parcp/parcp.frm | 627 +++ src/test/dos/fixtures/pardemo/ICON32.BMP | 3 + src/test/dos/fixtures/pardemo/logo.bmp | 3 + src/test/dos/fixtures/pardemo/new.bmp | 3 + src/test/dos/fixtures/pardemo/open.bmp | 3 + src/test/dos/fixtures/pardemo/parcopy.frm | 70 + src/test/dos/fixtures/pardemo/pardemo.bas | 29 + src/test/dos/fixtures/pardemo/pardemo.dbp | 22 + src/test/dos/fixtures/pardemo/pardemo.frm | 698 +++ src/test/dos/fixtures/pardemo/parterm.frm | 88 + src/test/dos/fixtures/pardemo/save.bmp | 3 + src/test/dos/fixtures/pardsgn/pardsgn.dbp | 19 + src/test/dos/fixtures/pardsgn/pardsgn.frm | 386 ++ src/test/dos/fixtures/parhlp/parhlp.dbp | 19 + src/test/dos/fixtures/parhlp/parhlp.dhs | 61 + src/test/dos/fixtures/parhlp/parhlp.frm | 345 ++ src/test/dos/fixtures/paride/kid/kid.bas | 46 + src/test/dos/fixtures/paride/kid/kid.dbp | 19 + src/test/dos/fixtures/paride/kid/kidform.frm | 44 + src/test/dos/fixtures/paride/paride.dbp | 20 + src/test/dos/fixtures/paride/paride.frm | 296 ++ src/test/dos/fixtures/parpm/parpm.dbp | 26 + src/test/dos/fixtures/parpm/parpm.frm | 454 ++ src/test/dos/fixtures/sndmore/sndmore.dbp | 18 + src/test/dos/fixtures/sndmore/sndmore.frm | 158 + src/test/dos/fixtures/terminal/xferperf.ini | 23 + src/test/dos/fixtures/ucdemo/ucdemo.dbp | 19 + src/test/dos/fixtures/ucdemo/ucdemo.frm | 135 + src/test/dos/fixtures/ucide/ucide.dbp | 23 + src/test/dos/fixtures/ucide/ucide.frm | 42 + src/test/dos/fixtures/ucide/uclog.bas | 31 + src/test/dos/fixtures/ucide/ucmeter.bmp | 3 + src/test/dos/fixtures/ucide/ucmeter.ctl | 69 + src/test/dos/runDos.sh | 34 +- src/test/dos/script/scriptLang.c | 1 + src/test/dos/script/scriptLang.h | 1 + src/test/dos/script/scriptRun.c | 6 + src/test/dos/scripts/fmperf.scr | 44 + src/test/dos/scripts/parcp.scr | 20 + src/test/dos/scripts/pardemo.scr | 30 + src/test/dos/scripts/pardsgn.scr | 30 + src/test/dos/scripts/parhlp.scr | 26 + src/test/dos/scripts/paride.scr | 19 + src/test/dos/scripts/parpm.scr | 26 + src/test/dos/scripts/sndmore.scr | 17 + src/test/dos/scripts/ucdemo.scr | 29 + src/test/dos/scripts/ucide.scr | 51 + src/test/dos/scripts/xferperf.scr | 66 + src/test/dos/secLinkHost.py | 182 +- src/test/dvxtest/README.md | 4 +- src/test/dvxtest/dvxtest.c | 46 +- src/test/dvxtest/dvxtest.h | 1 + src/test/dvxtest/dvxtest.mk | 50 +- src/test/fileops/Makefile | 9 +- src/test/fileops/fileMgrTest.c | 130 +- src/test/fileops/fileOpsTest.c | 163 + src/test/formrt/Makefile | 21 +- src/test/formrt/frtHarness.c | 90 +- src/test/formrt/frtHarness.h | 13 + src/test/formrt/frtStubs.c | 173 +- src/test/formrt/frtStubs.h | 8 + src/test/formrt/testFormRt.c | 4216 ++++++++++++++++- src/test/formrt/testFrmModel.c | 858 ++++ src/test/formrt/testLibBas.c | 635 +++ src/test/formrt/testUcDesign.c | 346 ++ src/test/formrt/testUserCtl.c | 1222 +++++ src/test/fuzz/Makefile | 116 + src/test/fuzz/basCompile/seeds/edges.bas | 44 + src/test/fuzz/frmModel/seeds/edges.frm | 38 + src/test/fuzz/fuzzBasCompile.c | 96 + src/test/fuzz/fuzzBasModule.c | 82 + src/test/fuzz/fuzzFrmModel.c | 114 + src/test/fuzz/fuzzHlpFile.c | 59 + src/test/fuzz/fuzzHlpWalk.c | 105 + src/test/fuzz/fuzzHlpWalk.h | 35 + src/test/fuzz/fuzzHlpc.c | 87 + src/test/fuzz/fuzzImage.c | 147 + src/test/fuzz/fuzzPacket.c | 280 ++ src/test/fuzz/fuzzPrefs.c | 157 + src/test/fuzz/fuzzRes.c | 163 + src/test/fuzz/fuzzUtil.c | 66 + src/test/fuzz/fuzzUtil.h | 45 + src/test/fuzz/hlpc/seeds/mini.dhs | 44 + src/test/fuzz/lsan.supp | 9 + src/test/fuzz/packet/mkSeeds.py | 83 + src/test/fuzz/packet/seeds/fullPayload.bin | Bin 0 -> 517 bytes src/test/fuzz/packet/seeds/inOrder.bin | Bin 0 -> 33 bytes src/test/fuzz/packet/seeds/nakRetransmit.bin | Bin 0 -> 28 bytes src/test/fuzz/packet/seeds/outOfOrder.bin | Bin 0 -> 33 bytes src/test/fuzz/packet/seeds/rawNoise.bin | Bin 0 -> 24 bytes src/test/fuzz/packet/seeds/reset.bin | Bin 0 -> 28 bytes src/test/fuzz/packet/seeds/sendAck.bin | Bin 0 -> 24 bytes src/test/fuzz/prefs/seeds/edges.ini | 20 + src/test/fuzz/res/seeds/resOnly.res | Bin 0 -> 398 bytes src/test/fuzz/res/seeds/textAndBinary.res | Bin 0 -> 229 bytes src/test/fuzz/runFuzz.sh | 267 ++ src/test/fuzz/ubsan.supp | 18 + src/test/game/gameTest.c | 21 +- src/test/hlpfile/Makefile | 49 + src/test/hlpfile/hlpFileTest.c | 673 +++ src/test/ide/Makefile | 8 +- src/test/ide/ideStubs.c | 1 + src/test/ide/ideStubs.h | 36 + src/test/ide/testIde.c | 26 +- src/test/ide/testIdeDebug.c | 10 +- src/test/ide/testIdeDesign.c | 513 +- src/test/ide/testIdeRegress.c | 166 +- src/test/ide/testIdeUserCtl.c | 1074 +++++ src/test/joystick/joystickTest.c | 176 + src/test/lint/Makefile | 79 +- .../lint/__pycache__/hostDb.cpython-312.pyc | Bin 0 -> 15448 bytes .../__pycache__/styleLint.cpython-312.pyc | Bin 29825 -> 29956 bytes src/test/lint/analyzerBaseline.txt | 34 +- src/test/lint/asciiCheck.sh | 4 +- src/test/lint/baseline.txt | 1408 ++++-- src/test/lint/clangHost.mk | 130 + src/test/lint/clangTidy.yaml | 99 + src/test/lint/clangTidyBaseline.txt | 146 + src/test/lint/codespellBaseline.txt | 4 + src/test/lint/codespellWords.txt | 46 + src/test/lint/cppcheckBaseline.txt | 75 +- src/test/lint/hostDb.py | 287 ++ src/test/lint/msan.mk | 106 + src/test/lint/msanStep.sh | 89 + src/test/lint/qaGate.sh | 257 + src/test/lint/shellcheckBaseline.txt | 48 + src/test/lint/staticGate.sh | 95 +- src/test/lint/styleLint.py | 42 +- src/test/lint/valgrind.mk | 65 + src/test/lint/valgrindRun.sh | 204 + src/test/music/musicTest.c | 22 +- src/test/net/Makefile | 8 +- src/test/net/netTest.c | 366 +- src/test/print/Makefile | 2 +- src/test/print/printTest.c | 313 +- src/test/samples/golden/basdemo-debug.app | Bin 25197 -> 24376 bytes src/test/samples/golden/basdemo-release.app | Bin 21914 -> 21093 bytes src/test/samples/golden/clock-debug.app | Bin 0 -> 9777 bytes src/test/samples/golden/clock-release.app | Bin 0 -> 9376 bytes src/test/samples/golden/fileMgr-debug.app | Bin 0 -> 34279 bytes src/test/samples/golden/fileMgr-release.app | Bin 0 -> 29110 bytes src/test/samples/golden/helpedit-debug.app | Bin 12743 -> 11918 bytes src/test/samples/golden/helpedit-release.app | Bin 11611 -> 10786 bytes src/test/samples/golden/iconed-debug.app | Bin 17425 -> 17066 bytes src/test/samples/golden/iconed-release.app | Bin 14839 -> 14480 bytes src/test/samples/golden/imgview-debug.app | Bin 9620 -> 9253 bytes src/test/samples/golden/imgview-release.app | Bin 9201 -> 8834 bytes src/test/samples/golden/inc-comm-debug.app | Bin 7480 -> 6721 bytes src/test/samples/golden/inc-comm-release.app | Bin 7428 -> 6669 bytes src/test/samples/golden/inc-commdlg-debug.app | Bin 7222 -> 6314 bytes .../samples/golden/inc-commdlg-release.app | Bin 7170 -> 6262 bytes .../samples/golden/inc-compiler-debug.app | Bin 0 -> 6908 bytes .../samples/golden/inc-compiler-release.app | Bin 0 -> 6856 bytes src/test/samples/golden/inc-conn-debug.app | Bin 0 -> 6536 bytes src/test/samples/golden/inc-conn-release.app | Bin 0 -> 6484 bytes src/test/samples/golden/inc-debug-debug.app | Bin 0 -> 6418 bytes src/test/samples/golden/inc-debug-release.app | Bin 0 -> 6366 bytes .../samples/golden/inc-designer-debug.app | Bin 0 -> 6861 bytes .../samples/golden/inc-designer-release.app | Bin 0 -> 6809 bytes src/test/samples/golden/inc-fileops-debug.app | Bin 7203 -> 7298 bytes .../samples/golden/inc-fileops-release.app | Bin 7151 -> 7246 bytes src/test/samples/golden/inc-game-debug.app | Bin 7124 -> 6201 bytes src/test/samples/golden/inc-game-release.app | Bin 7072 -> 6149 bytes src/test/samples/golden/inc-help-debug.app | Bin 7135 -> 6204 bytes src/test/samples/golden/inc-help-release.app | Bin 7083 -> 6152 bytes src/test/samples/golden/inc-hlpfile-debug.app | Bin 0 -> 6486 bytes .../samples/golden/inc-hlpfile-release.app | Bin 0 -> 6434 bytes .../samples/golden/inc-joystick-debug.app | Bin 7199 -> 6653 bytes .../samples/golden/inc-joystick-release.app | Bin 7147 -> 6601 bytes src/test/samples/golden/inc-layout-debug.app | Bin 0 -> 6171 bytes .../samples/golden/inc-layout-release.app | Bin 0 -> 6119 bytes .../samples/golden/inc-listhelp-debug.app | Bin 0 -> 6707 bytes .../samples/golden/inc-listhelp-release.app | Bin 0 -> 6655 bytes src/test/samples/golden/inc-music-debug.app | Bin 7346 -> 6462 bytes src/test/samples/golden/inc-music-release.app | Bin 7294 -> 6410 bytes src/test/samples/golden/inc-net-debug.app | Bin 7280 -> 6560 bytes src/test/samples/golden/inc-net-release.app | Bin 7228 -> 6508 bytes src/test/samples/golden/inc-prefs-debug.app | Bin 0 -> 6560 bytes src/test/samples/golden/inc-prefs-release.app | Bin 0 -> 6508 bytes src/test/samples/golden/inc-print-debug.app | Bin 7214 -> 6739 bytes src/test/samples/golden/inc-print-release.app | Bin 7162 -> 6687 bytes .../samples/golden/inc-resource-debug.app | Bin 7231 -> 6288 bytes .../samples/golden/inc-resource-release.app | Bin 7179 -> 6236 bytes .../samples/golden/inc-security-debug.app | Bin 0 -> 6437 bytes .../samples/golden/inc-security-release.app | Bin 0 -> 6385 bytes src/test/samples/golden/inc-shell-debug.app | Bin 0 -> 7579 bytes src/test/samples/golden/inc-shell-release.app | Bin 0 -> 7527 bytes src/test/samples/golden/inc-sound-debug.app | Bin 7233 -> 6961 bytes src/test/samples/golden/inc-sound-release.app | Bin 7181 -> 6909 bytes src/test/samples/golden/inc-sql-debug.app | Bin 7283 -> 6316 bytes src/test/samples/golden/inc-sql-release.app | Bin 7231 -> 6264 bytes src/test/samples/golden/inc-system-debug.app | Bin 0 -> 6852 bytes .../samples/golden/inc-system-release.app | Bin 0 -> 6800 bytes src/test/samples/golden/inc-xfer-debug.app | Bin 7215 -> 6470 bytes src/test/samples/golden/inc-xfer-release.app | Bin 7163 -> 6418 bytes src/test/samples/golden/notepad-debug.app | Bin 12394 -> 12039 bytes src/test/samples/golden/notepad-release.app | Bin 11153 -> 10798 bytes src/test/samples/golden/resedit-debug.app | Bin 14074 -> 13357 bytes src/test/samples/golden/resedit-release.app | Bin 12533 -> 11816 bytes src/test/samples/golden/terminal-debug.app | Bin 0 -> 46251 bytes src/test/samples/golden/terminal-release.app | Bin 0 -> 40999 bytes src/test/samples/golden/welcome-debug.app | Bin 0 -> 10033 bytes src/test/samples/golden/welcome-release.app | Bin 0 -> 9669 bytes src/test/samples/golden/widshow-debug.app | Bin 15299 -> 14373 bytes src/test/samples/golden/widshow-release.app | Bin 14373 -> 13447 bytes src/test/sdk/sdkTest.sh | 23 +- src/test/setup/Makefile | 9 +- src/test/setup/setupTest.c | 90 +- src/test/shell/Makefile | 39 +- src/test/shell/appsTest.c | 1114 ++++- src/test/shell/basic/newargs/newargs.dbp | 17 + src/test/shell/basic/newargs/newargs.frm | 57 + src/test/shell/basic/shbind/shbind.dbp | 21 + src/test/shell/basic/shbind/shbind.frm | 318 ++ src/test/shell/fixtures/fixClasses.c | 68 + src/test/shell/fixtures/fixClock.c | 165 + src/test/shell/fixtures/fixDesktop.c | 5 + src/test/shell/loaderTest.c | 85 +- src/test/shell/progmanTest.c | 543 ++- src/test/shell/shellFixture.c | 92 +- src/test/shell/shellFixture.h | 16 +- src/test/shell/shellMainTest.c | 13 +- src/test/shell/shellTest.c | 4 +- src/test/suites.mk | 2 +- src/test/terminal/Makefile | 55 +- src/test/terminal/bookTest/bookTest.bas | 335 ++ src/test/terminal/bookTest/bookTest.dbp | 18 + src/test/terminal/testConn.c | 296 +- src/test/terminal/testSession.c | 343 -- src/test/terminal/testTermApp.c | 759 ++- src/test/tools/checkModules.py | 18 +- src/test/tools/runTools.sh | 66 +- src/test/unit/Makefile | 2 +- src/test/unit/testDraw.c | 39 + src/test/unit/testIface.c | 811 ++++ src/test/unit/testPrefs.c | 209 + src/test/unit/testVideo.c | 38 +- src/test/unit/testWm.c | 72 + src/test/unit/unitMain.c | 1 + src/test/unit/unitTest.h | 1 + src/test/widgets/Makefile | 4 +- src/test/widgets/testCovListView.c | 7 + src/test/widgets/testCovMisc.c | 12 +- src/test/widgets/testCovTerm.c | 58 + src/test/widgets/testCovText.c | 486 +- src/test/widgets/testData.c | 63 +- src/test/widgets/testGeneric.c | 580 +++ src/test/widgets/testHelpView.c | 389 ++ src/test/widgets/testProps.c | 371 +- src/test/widgets/widgetsMain.c | 2 + src/test/widgets/wtCommon.c | 160 +- src/test/widgets/wtCommon.h | 33 +- src/test/wm/wmFixes.c | 64 + src/test/wm/wmSuite2.c | 96 + src/tools/README.md | 3 +- src/tools/dvxhlpc.c | 233 +- src/tools/hlpcCompile.h | 15 +- src/tools/mkwgticon.c | 18 + src/widgets/kpunch/Makefile | 1 + src/widgets/kpunch/ansiTerm/ansiTerm.dhs | 8 +- src/widgets/kpunch/ansiTerm/ansiterm.bhs | 12 +- src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c | 186 +- src/widgets/kpunch/box/box.bhs | 43 +- src/widgets/kpunch/box/widgetBox.c | 40 +- src/widgets/kpunch/button/button.bhs | 6 + src/widgets/kpunch/button/widgetButton.c | 46 +- src/widgets/kpunch/canvas/canvas.bhs | 6 +- src/widgets/kpunch/canvas/canvas.dhs | 19 +- src/widgets/kpunch/canvas/canvas.h | 13 + src/widgets/kpunch/canvas/widgetCanvas.c | 125 +- src/widgets/kpunch/checkbox/widgetCheckbox.c | 1 + src/widgets/kpunch/comboBox/comboBox.dhs | 7 + src/widgets/kpunch/comboBox/comboBox.h | 4 + src/widgets/kpunch/comboBox/combobox.bhs | 3 +- src/widgets/kpunch/comboBox/widgetComboBox.c | 29 +- src/widgets/kpunch/dataCtrl/datactrl.bhs | 9 +- src/widgets/kpunch/dataCtrl/widgetDataCtrl.c | 62 +- src/widgets/kpunch/dbGrid/dbgrid.bhs | 2 +- src/widgets/kpunch/dbGrid/widgetDbGrid.c | 7 +- src/widgets/kpunch/dropdown/dropdown.bhs | 2 +- src/widgets/kpunch/dropdown/widgetDropdown.c | 1 + src/widgets/kpunch/helpView/helpView.dhs | 83 + src/widgets/kpunch/helpView/helpView.h | 72 + src/widgets/kpunch/helpView/helpview.bhs | 90 + src/widgets/kpunch/helpView/helpview.bmp | 3 + src/widgets/kpunch/helpView/helpview.dep | 1 + src/widgets/kpunch/helpView/helpview.res | 7 + src/widgets/kpunch/helpView/widgetHelpView.c | 1019 ++++ src/widgets/kpunch/image/image.bhs | 6 +- src/widgets/kpunch/image/widgetImage.c | 65 +- src/widgets/kpunch/imageButton/imgbtn.bhs | 2 +- .../kpunch/imageButton/widgetImageButton.c | 1 + src/widgets/kpunch/label/widgetLabel.c | 1 + src/widgets/kpunch/listBox/listbox.bhs | 2 +- src/widgets/kpunch/listBox/widgetListBox.c | 1 + src/widgets/kpunch/listView/listView.dhs | 2 +- src/widgets/kpunch/listView/listview.bhs | 20 +- src/widgets/kpunch/listView/widgetListView.c | 133 +- src/widgets/kpunch/progressBar/progress.bhs | 3 +- src/widgets/kpunch/progressBar/progress.dhs | 9 +- src/widgets/kpunch/progressBar/progress.h | 4 + .../kpunch/progressBar/widgetProgressBar.c | 37 +- src/widgets/kpunch/radio/widgetRadio.c | 3 +- src/widgets/kpunch/scrollPane/scrlPane.h | 8 + src/widgets/kpunch/scrollPane/scrlpane.bhs | 42 +- src/widgets/kpunch/scrollPane/scrlpane.dhs | 29 +- .../kpunch/scrollPane/widgetScrollPane.c | 112 +- .../kpunch/separator/widgetSeparator.c | 1 + src/widgets/kpunch/slider/widgetSlider.c | 1 + src/widgets/kpunch/spacer/widgetSpacer.c | 1 + src/widgets/kpunch/spinner/spinner.bhs | 3 + src/widgets/kpunch/spinner/widgetSpinner.c | 44 +- src/widgets/kpunch/splitter/splitter.bhs | 5 +- src/widgets/kpunch/splitter/splitter.dhs | 9 +- src/widgets/kpunch/splitter/splitter.h | 4 + src/widgets/kpunch/splitter/widgetSplitter.c | 33 +- src/widgets/kpunch/statusBar/statbar.bhs | 18 +- .../kpunch/statusBar/widgetStatusBar.c | 1 + .../kpunch/tabControl/widgetTabControl.c | 2 + src/widgets/kpunch/textInput/textInpt.h | 80 +- src/widgets/kpunch/textInput/textinpt.bhs | 99 +- src/widgets/kpunch/textInput/textinpt.dhs | 58 +- .../kpunch/textInput/widgetTextInput.c | 945 +++- src/widgets/kpunch/timer/widgetTimer.c | 3 +- src/widgets/kpunch/toolbar/widgetToolbar.c | 1 + src/widgets/kpunch/treeView/treeview.bhs | 18 +- src/widgets/kpunch/treeView/widgetTreeView.c | 83 +- src/widgets/kpunch/wgtsys.dhs | 23 + src/widgets/kpunch/wrapBox/widgetWrapBox.c | 1 + 674 files changed, 87376 insertions(+), 18737 deletions(-) create mode 100644 screenshots/86box/pardemo-pardemo.png create mode 100644 screenshots/86box/pardsgn-pardsgn.png create mode 100644 screenshots/86box/parhlp-parhlp-l.png create mode 100644 screenshots/86box/parhlp-parhlp.png create mode 100644 screenshots/86box/parpm-parpm.png create mode 100644 screenshots/86box/ucdemo-ucdemo.png create mode 100644 screenshots/86box/ucide-ucide-ds.png create mode 100644 screenshots/86box/ucide-ucide-ru.png create mode 100644 screenshots/86box/ucide-ucide-tb.png create mode 100644 screenshots/dosbox/pardemo-pardemo.png create mode 100644 screenshots/dosbox/pardsgn-pardsgn.png create mode 100644 screenshots/dosbox/parhlp-parhlp-l.png create mode 100644 screenshots/dosbox/parhlp-parhlp.png create mode 100644 screenshots/dosbox/parpm-parpm.png create mode 100644 screenshots/dosbox/ucdemo-ucdemo.png create mode 100644 screenshots/dosbox/ucide-ucide-ds.png create mode 100644 screenshots/dosbox/ucide-ucide-ru.png create mode 100644 screenshots/dosbox/ucide-ucide-tb.png create mode 100644 screenshots/masks/hang-ended.mask create mode 100644 screenshots/masks/hang-taskmgr.mask create mode 100644 screenshots/masks/upgrade-su-upgr.mask rename src/apps/kpunch/clock/{icon32.bmp => ICON32.BMP} (100%) delete mode 100644 src/apps/kpunch/clock/clock.c create mode 100644 src/apps/kpunch/clock/clock.dbp create mode 100644 src/apps/kpunch/clock/clock.frm delete mode 100644 src/apps/kpunch/clock/clock.res create mode 100644 src/apps/kpunch/dvxbasic/basdev/basDebug.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basDebug.h create mode 100644 src/apps/kpunch/dvxbasic/basdev/basDevBas.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basHelpMap.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basHelpMap.h create mode 100644 src/apps/kpunch/dvxbasic/basdev/basProcs.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basProcs.h create mode 100644 src/apps/kpunch/dvxbasic/basdev/basProject.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basProject.h create mode 100644 src/apps/kpunch/dvxbasic/basdev/basSource.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basSource.h create mode 100644 src/apps/kpunch/dvxbasic/basdev/basValidate.c create mode 100644 src/apps/kpunch/dvxbasic/basdev/basValidate.h create mode 100644 src/apps/kpunch/dvxbasic/basdev/basdev.dep create mode 100644 src/apps/kpunch/dvxbasic/basdev/basdev.dhs create mode 100644 src/apps/kpunch/dvxbasic/formrt/basRunHost.c create mode 100644 src/apps/kpunch/dvxbasic/formrt/basRunHost.h create mode 100644 src/apps/kpunch/dvxbasic/formrt/formCtl.c create mode 100644 src/apps/kpunch/dvxbasic/formrt/formDesign.c create mode 100644 src/apps/kpunch/dvxbasic/formrt/frmModel.c create mode 100644 src/apps/kpunch/dvxbasic/formrt/frmModel.h create mode 100644 src/apps/kpunch/dvxbasic/formrt/frmModelBas.c create mode 100644 src/apps/kpunch/dvxbasic/userctl.dhs rename src/apps/kpunch/fileMgr/{icon32.bmp => ICON32.BMP} (100%) create mode 100644 src/apps/kpunch/fileMgr/fileMgr.bas delete mode 100644 src/apps/kpunch/fileMgr/fileMgr.c create mode 100644 src/apps/kpunch/fileMgr/fileMgr.dbp create mode 100644 src/apps/kpunch/fileMgr/fileMgr.frm delete mode 100644 src/apps/kpunch/fileMgr/fileMgr.res create mode 100644 src/apps/kpunch/fileMgr/fmJob.cls create mode 100644 src/apps/kpunch/fileMgr/fmProps.frm rename src/apps/kpunch/terminal/{icon32.bmp => ICON32.BMP} (100%) create mode 100644 src/apps/kpunch/terminal/termBook.frm create mode 100644 src/apps/kpunch/terminal/termMain.bas create mode 100644 src/apps/kpunch/terminal/termPass.frm create mode 100644 src/apps/kpunch/terminal/termProg.frm create mode 100644 src/apps/kpunch/terminal/termSess.frm delete mode 100644 src/apps/kpunch/terminal/termSession.c delete mode 100644 src/apps/kpunch/terminal/termSession.h create mode 100644 src/apps/kpunch/terminal/termXfer.frm create mode 100644 src/apps/kpunch/terminal/terminal.bas delete mode 100644 src/apps/kpunch/terminal/terminal.c create mode 100644 src/apps/kpunch/terminal/terminal.dbp create mode 100644 src/apps/kpunch/terminal/terminal.frm delete mode 100644 src/apps/kpunch/terminal/terminal.res rename src/apps/kpunch/welcome/{icon32.bmp => ICON32.BMP} (100%) delete mode 100644 src/apps/kpunch/welcome/welcome.c create mode 100644 src/apps/kpunch/welcome/welcome.dbp create mode 100644 src/apps/kpunch/welcome/welcome.frm delete mode 100644 src/apps/kpunch/welcome/welcome.res create mode 100644 src/include/basic/compiler.bas create mode 100644 src/include/basic/conn.bas create mode 100644 src/include/basic/debug.bas create mode 100644 src/include/basic/designer.bas create mode 100644 src/include/basic/hlpfile.bas create mode 100644 src/include/basic/layout.bas create mode 100644 src/include/basic/listhelp.bas create mode 100644 src/include/basic/prefs.bas create mode 100644 src/include/basic/security.bas create mode 100644 src/include/basic/shell.bas create mode 100644 src/include/basic/system.bas create mode 100644 src/libs/kpunch/audio/audioSet.c create mode 100644 src/libs/kpunch/conn/connBas.c rename src/{apps/kpunch/progman => libs/kpunch/dvxshell}/pmModel.c (61%) rename src/{apps/kpunch/progman => libs/kpunch/dvxshell}/pmModel.h (60%) create mode 100644 src/libs/kpunch/dvxshell/pmModelBas.c create mode 100644 src/libs/kpunch/dvxshell/shellBas.c create mode 100644 src/libs/kpunch/dvxshell/shellSysBas.c create mode 100644 src/libs/kpunch/fileops/fileOpsSys.h create mode 100644 src/libs/kpunch/hlpfile/Makefile create mode 100644 src/libs/kpunch/hlpfile/hlpFile.c create mode 100644 src/libs/kpunch/hlpfile/hlpFile.h create mode 100644 src/libs/kpunch/hlpfile/hlpFileBas.c rename src/{apps/kpunch/dvxhelp/hlpformat.h => libs/kpunch/hlpfile/hlpFormat.h} (77%) create mode 100644 src/libs/kpunch/hlpfile/hlpfile.dep create mode 100644 src/libs/kpunch/hlpfile/hlpfile.dhs create mode 100644 src/libs/kpunch/libdvx/dvxPrefsBas.c create mode 100644 src/libs/kpunch/libdvx/widgetIface.c create mode 100644 src/libs/kpunch/listhelp/listHelpBas.c create mode 100644 src/libs/kpunch/serial/security/secBas.c create mode 100644 src/sdk/templates/control/Makefile create mode 100644 src/sdk/templates/control/tplName.bhs create mode 100644 src/sdk/templates/control/tplName.ctl create mode 100644 src/sdk/templates/control/tplName.dbp create mode 100644 src/test/basdev/Makefile create mode 100644 src/test/basdev/testBasDev.c create mode 100644 src/test/dos/fixtures/bigdir/bigdir.bas create mode 100644 src/test/dos/fixtures/bigdir/bigdir.dbp create mode 100644 src/test/dos/fixtures/iconview/iconview.bmp create mode 100644 src/test/dos/fixtures/iconview/iconview.ctl create mode 100644 src/test/dos/fixtures/iconview/iconview.dbp create mode 100644 src/test/dos/fixtures/parcp/parcp.dbp create mode 100644 src/test/dos/fixtures/parcp/parcp.frm create mode 100644 src/test/dos/fixtures/pardemo/ICON32.BMP create mode 100644 src/test/dos/fixtures/pardemo/logo.bmp create mode 100644 src/test/dos/fixtures/pardemo/new.bmp create mode 100644 src/test/dos/fixtures/pardemo/open.bmp create mode 100644 src/test/dos/fixtures/pardemo/parcopy.frm create mode 100644 src/test/dos/fixtures/pardemo/pardemo.bas create mode 100644 src/test/dos/fixtures/pardemo/pardemo.dbp create mode 100644 src/test/dos/fixtures/pardemo/pardemo.frm create mode 100644 src/test/dos/fixtures/pardemo/parterm.frm create mode 100644 src/test/dos/fixtures/pardemo/save.bmp create mode 100644 src/test/dos/fixtures/pardsgn/pardsgn.dbp create mode 100644 src/test/dos/fixtures/pardsgn/pardsgn.frm create mode 100644 src/test/dos/fixtures/parhlp/parhlp.dbp create mode 100644 src/test/dos/fixtures/parhlp/parhlp.dhs create mode 100644 src/test/dos/fixtures/parhlp/parhlp.frm create mode 100644 src/test/dos/fixtures/paride/kid/kid.bas create mode 100644 src/test/dos/fixtures/paride/kid/kid.dbp create mode 100644 src/test/dos/fixtures/paride/kid/kidform.frm create mode 100644 src/test/dos/fixtures/paride/paride.dbp create mode 100644 src/test/dos/fixtures/paride/paride.frm create mode 100644 src/test/dos/fixtures/parpm/parpm.dbp create mode 100644 src/test/dos/fixtures/parpm/parpm.frm create mode 100644 src/test/dos/fixtures/sndmore/sndmore.dbp create mode 100644 src/test/dos/fixtures/sndmore/sndmore.frm create mode 100644 src/test/dos/fixtures/terminal/xferperf.ini create mode 100644 src/test/dos/fixtures/ucdemo/ucdemo.dbp create mode 100644 src/test/dos/fixtures/ucdemo/ucdemo.frm create mode 100644 src/test/dos/fixtures/ucide/ucide.dbp create mode 100644 src/test/dos/fixtures/ucide/ucide.frm create mode 100644 src/test/dos/fixtures/ucide/uclog.bas create mode 100644 src/test/dos/fixtures/ucide/ucmeter.bmp create mode 100644 src/test/dos/fixtures/ucide/ucmeter.ctl create mode 100644 src/test/dos/scripts/fmperf.scr create mode 100644 src/test/dos/scripts/parcp.scr create mode 100644 src/test/dos/scripts/pardemo.scr create mode 100644 src/test/dos/scripts/pardsgn.scr create mode 100644 src/test/dos/scripts/parhlp.scr create mode 100644 src/test/dos/scripts/paride.scr create mode 100644 src/test/dos/scripts/parpm.scr create mode 100644 src/test/dos/scripts/sndmore.scr create mode 100644 src/test/dos/scripts/ucdemo.scr create mode 100644 src/test/dos/scripts/ucide.scr create mode 100644 src/test/dos/scripts/xferperf.scr create mode 100644 src/test/formrt/testFrmModel.c create mode 100644 src/test/formrt/testLibBas.c create mode 100644 src/test/formrt/testUcDesign.c create mode 100644 src/test/formrt/testUserCtl.c create mode 100644 src/test/fuzz/Makefile create mode 100644 src/test/fuzz/basCompile/seeds/edges.bas create mode 100644 src/test/fuzz/frmModel/seeds/edges.frm create mode 100644 src/test/fuzz/fuzzBasCompile.c create mode 100644 src/test/fuzz/fuzzBasModule.c create mode 100644 src/test/fuzz/fuzzFrmModel.c create mode 100644 src/test/fuzz/fuzzHlpFile.c create mode 100644 src/test/fuzz/fuzzHlpWalk.c create mode 100644 src/test/fuzz/fuzzHlpWalk.h create mode 100644 src/test/fuzz/fuzzHlpc.c create mode 100644 src/test/fuzz/fuzzImage.c create mode 100644 src/test/fuzz/fuzzPacket.c create mode 100644 src/test/fuzz/fuzzPrefs.c create mode 100644 src/test/fuzz/fuzzRes.c create mode 100644 src/test/fuzz/fuzzUtil.c create mode 100644 src/test/fuzz/fuzzUtil.h create mode 100644 src/test/fuzz/hlpc/seeds/mini.dhs create mode 100644 src/test/fuzz/lsan.supp create mode 100755 src/test/fuzz/packet/mkSeeds.py create mode 100644 src/test/fuzz/packet/seeds/fullPayload.bin create mode 100644 src/test/fuzz/packet/seeds/inOrder.bin create mode 100644 src/test/fuzz/packet/seeds/nakRetransmit.bin create mode 100644 src/test/fuzz/packet/seeds/outOfOrder.bin create mode 100644 src/test/fuzz/packet/seeds/rawNoise.bin create mode 100644 src/test/fuzz/packet/seeds/reset.bin create mode 100644 src/test/fuzz/packet/seeds/sendAck.bin create mode 100644 src/test/fuzz/prefs/seeds/edges.ini create mode 100644 src/test/fuzz/res/seeds/resOnly.res create mode 100644 src/test/fuzz/res/seeds/textAndBinary.res create mode 100755 src/test/fuzz/runFuzz.sh create mode 100644 src/test/fuzz/ubsan.supp create mode 100644 src/test/hlpfile/Makefile create mode 100644 src/test/hlpfile/hlpFileTest.c create mode 100644 src/test/ide/ideStubs.h create mode 100644 src/test/ide/testIdeUserCtl.c create mode 100644 src/test/lint/__pycache__/hostDb.cpython-312.pyc create mode 100644 src/test/lint/clangHost.mk create mode 100644 src/test/lint/clangTidy.yaml create mode 100644 src/test/lint/clangTidyBaseline.txt create mode 100644 src/test/lint/codespellBaseline.txt create mode 100644 src/test/lint/codespellWords.txt create mode 100755 src/test/lint/hostDb.py create mode 100644 src/test/lint/msan.mk create mode 100755 src/test/lint/msanStep.sh create mode 100755 src/test/lint/qaGate.sh create mode 100644 src/test/lint/shellcheckBaseline.txt create mode 100644 src/test/lint/valgrind.mk create mode 100755 src/test/lint/valgrindRun.sh create mode 100644 src/test/samples/golden/clock-debug.app create mode 100644 src/test/samples/golden/clock-release.app create mode 100644 src/test/samples/golden/fileMgr-debug.app create mode 100644 src/test/samples/golden/fileMgr-release.app create mode 100644 src/test/samples/golden/inc-compiler-debug.app create mode 100644 src/test/samples/golden/inc-compiler-release.app create mode 100644 src/test/samples/golden/inc-conn-debug.app create mode 100644 src/test/samples/golden/inc-conn-release.app create mode 100644 src/test/samples/golden/inc-debug-debug.app create mode 100644 src/test/samples/golden/inc-debug-release.app create mode 100644 src/test/samples/golden/inc-designer-debug.app create mode 100644 src/test/samples/golden/inc-designer-release.app create mode 100644 src/test/samples/golden/inc-hlpfile-debug.app create mode 100644 src/test/samples/golden/inc-hlpfile-release.app create mode 100644 src/test/samples/golden/inc-layout-debug.app create mode 100644 src/test/samples/golden/inc-layout-release.app create mode 100644 src/test/samples/golden/inc-listhelp-debug.app create mode 100644 src/test/samples/golden/inc-listhelp-release.app create mode 100644 src/test/samples/golden/inc-prefs-debug.app create mode 100644 src/test/samples/golden/inc-prefs-release.app create mode 100644 src/test/samples/golden/inc-security-debug.app create mode 100644 src/test/samples/golden/inc-security-release.app create mode 100644 src/test/samples/golden/inc-shell-debug.app create mode 100644 src/test/samples/golden/inc-shell-release.app create mode 100644 src/test/samples/golden/inc-system-debug.app create mode 100644 src/test/samples/golden/inc-system-release.app create mode 100644 src/test/samples/golden/terminal-debug.app create mode 100644 src/test/samples/golden/terminal-release.app create mode 100644 src/test/samples/golden/welcome-debug.app create mode 100644 src/test/samples/golden/welcome-release.app create mode 100644 src/test/shell/basic/newargs/newargs.dbp create mode 100644 src/test/shell/basic/newargs/newargs.frm create mode 100644 src/test/shell/basic/shbind/shbind.dbp create mode 100644 src/test/shell/basic/shbind/shbind.frm create mode 100644 src/test/shell/fixtures/fixClasses.c create mode 100644 src/test/shell/fixtures/fixClock.c create mode 100644 src/test/terminal/bookTest/bookTest.bas create mode 100644 src/test/terminal/bookTest/bookTest.dbp delete mode 100644 src/test/terminal/testSession.c create mode 100644 src/test/unit/testIface.c create mode 100644 src/test/widgets/testGeneric.c create mode 100644 src/test/widgets/testHelpView.c create mode 100644 src/widgets/kpunch/helpView/helpView.dhs create mode 100644 src/widgets/kpunch/helpView/helpView.h create mode 100644 src/widgets/kpunch/helpView/helpview.bhs create mode 100644 src/widgets/kpunch/helpView/helpview.bmp create mode 100644 src/widgets/kpunch/helpView/helpview.dep create mode 100644 src/widgets/kpunch/helpView/helpview.res create mode 100644 src/widgets/kpunch/helpView/widgetHelpView.c diff --git a/Makefile b/Makefile index 16f9749f..64d7acc0 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ LIBS = $(sort $(notdir $(patsubst %/Makefile,%,$(wildcard src/libs/kpunch/*/Makefile)))) CORE_LIBS = libdvx libtasks -.PHONY: all clean $(LIBS) loader widgets apps tools installer dist deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-modules test-samples coverage bench bench-compare +.PHONY: all clean $(LIBS) loader widgets apps tools installer dist deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-modules test-samples coverage test-qa test-fuzz bench bench-compare # Every build ends by making the release media in dist/ (a second or # two), so dist/ always holds the latest DVX. @@ -159,6 +159,18 @@ test-dos: all coverage: tools $(MAKE) -C src/test/coverage +# Report-only quality tools beyond make test (claudeDocs/qaTools.md): +# test-qa runs the clang host build, the lizard complexity report, +# MemorySanitizer and valgrind (src/test/lint qa; clang-tidy is in the +# lint gate, so in make test); test-fuzz runs +# every libFuzzer harness for FUZZ_SECONDS (default 60) each +# (src/test/fuzz run). Neither is part of make test. +test-qa: + $(MAKE) -C src/test/lint qa + +test-fuzz: + $(MAKE) -C src/test/fuzz run + HLPC = bin/host/dvxhlpc HELP_FILES = src/tools/helpFiles.sh @@ -171,7 +183,8 @@ HELP_FILES = src/tools/helpFiles.sh USER_GUIDE_DHS = src/apps/kpunch/progman/userHelp.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 \ + src/apps/kpunch/dvxbasic/userctl.dhs # Needs bin/host/dvxhlpc, and every module built and its help deployed # (library and app help are classified by the modules found in bin/). @@ -212,7 +225,7 @@ deploy-sdk: tools apps @echo "Building SDK..." @# Start from an empty tree so a header dropped from the lists goes away. rm -rf $(SDKDIR) - @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/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow + @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/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow cp $(SDK_CORE_H) $(SDKDIR)/include/core/ cp $(SDK_SHELL_H) $(SDKDIR)/include/shell/ cp $(SDK_TASKS_H) $(SDKDIR)/include/tasks/ @@ -220,6 +233,7 @@ deploy-sdk: tools apps cp $(SDK_TEXTHELP_H) $(SDKDIR)/include/texthelp/ cp $(SDK_LISTHELP_H) $(SDKDIR)/include/listhelp/ cp $(SDK_FILEOPS_H) $(SDKDIR)/include/fileops/ + cp $(SDK_HLPFILE_H) $(SDKDIR)/include/hlpfile/ cp $(SDK_XFER_H) $(SDKDIR)/include/xfer/ cp $(SDK_CONN_H) $(SDKDIR)/include/conn/ cp $(SDK_PRINT_H) $(SDKDIR)/include/print/ diff --git a/analyze.sh b/analyze.sh index 9d844561..4ef750db 100755 --- a/analyze.sh +++ b/analyze.sh @@ -23,11 +23,13 @@ set -u -cd "$(dirname "$0")" +cd "$(dirname "$0")" || exit 1 # 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)" +# DJGPP_PREFIX in the environment overrides it). The query drops a +# calling make's flags: under make -j they bring its jobserver, and the +# query then prints directory lines into the path. +REAL_CC="$(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f src/common.mk --eval 'printCc: ; @echo $(CC)' printCc)" if [ ! -x "$REAL_CC" ]; then echo "analyze.sh: $REAL_CC not executable" >&2 diff --git a/config/ASSOC.INI b/config/ASSOC.INI index f10e92ce..cdb8bc90 100644 --- a/config/ASSOC.INI +++ b/config/ASSOC.INI @@ -19,5 +19,7 @@ GIF = apps/kpunch/imgview/imgview.app BAS = apps/kpunch/dvxbasic/dvxbasic.app DBP = apps/kpunch/dvxbasic/dvxbasic.app FRM = apps/kpunch/dvxbasic/dvxbasic.app +CLS = apps/kpunch/dvxbasic/dvxbasic.app +CTL = apps/kpunch/dvxbasic/dvxbasic.app HLP = apps/kpunch/dvxhelp/dvxhelp.app DHS = apps/kpunch/dvxhelp/helpedit.app diff --git a/cppcheck.sh b/cppcheck.sh index 2ce11418..8d83e3c7 100755 --- a/cppcheck.sh +++ b/cppcheck.sh @@ -15,7 +15,7 @@ set -e cd "$(dirname "$0")" -TARGETS="${@:-src}" +TARGETS=("${@:-src}") INCLUDES=( -Isrc/libs/kpunch/libdvx @@ -72,4 +72,4 @@ exec cppcheck \ "${INCLUDES[@]}" \ "${DEFINES[@]}" \ "${SUPPRESS[@]}" \ - $TARGETS + "${TARGETS[@]}" diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index f4728798..c400dab7 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -46,6 +46,7 @@ img { max-width: 100%; }
  • Code Editor
  • Form Designer
  • Project System
  • +
  • UserControls in the IDE
  • Properties Panel
  • Toolbox
  • Debugger
  • @@ -105,6 +106,7 @@ img { max-width: 100%; }
  • SQL Functions
  • App Object
  • Clipboard Object
  • +
  • Screen Object
  • INI Functions
  • Predefined Constants
  • Include Libraries
  • @@ -118,6 +120,7 @@ img { max-width: 100%; }
  • Control Arrays
  • FRM File Format
  • Form
  • +
  • UserControls: Your Own Controls
  • Special Controls
  • Terminal
  • Frame
  • @@ -131,6 +134,8 @@ img { max-width: 100%; }
  • Data
  • DBGrid
  • DropDown
  • +
  • Data Display Controls
  • +
  • HelpPane
  • ImageButton
  • Image
  • Label
  • @@ -163,17 +168,25 @@ img { max-width: 100%; }
    @@ -1035,18 +1420,20 @@ img { max-width: 100%; } --- 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, .frm or .cls 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. + Add File... Ctrl+O Add a .bas, .frm, .cls or .ctl 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. New Class Module Add a new, empty class module (Class1.cls, Class2.cls, ...) to the project and open it in the Code Editor. See Class Modules under Project System. + New UserControl Add a new UserControl, a control of your own (UserControl1.ctl, UserControl2.ctl, ...), to the project and open it in the Code Editor. See UserControls in the IDE. Save File Ctrl+S Save the active file to disk. Save All Save all modified files in the project. --- - Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build. + Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build. A release build leaves out debugging information and gives forms and controls short internal names, except in a program that uses names as text (the Name property, Controls, DesignSelection or a DesignMouse event procedure), which keeps them. + Make Control... In a control project (Project Properties, Type Control) this item replaces Make Executable: it compiles the project and saves its controls as a control file (.wgt, named after the project). See UserControls in the IDE. --- Rename File... Give the file selected in the Project Explorer a new name, on disk and in the project. See Renaming a File under Project System. Remove File Remove the selected file from the project (prompts to save if modified). --- Exit Close the IDE (prompts to save unsaved changes). -

    A project (.dbp) or source file (.bas, .frm, .cls) 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.

    +

    A project (.dbp) or source file (.bas, .frm, .cls, .ctl) 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.

    Edit Menu

    Back to Overview

    @@ -1104,7 +1491,7 @@ img { max-width: 100%; } Toolbar Checkbox: show or hide the toolbar. 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 (a form must be active). Allows designing menu bars with captions, names, levels, checked state, and enabled state. See Menu Editor below. + 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, shortcut keys, levels, checked state, and enabled state. See Menu Editor below.

    Window Menu

    Back to Overview

    @@ -1223,7 +1610,7 @@ img { max-width: 100%; }

    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.

    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.

    +

    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. Shortcut is a key that runs the item (F2, Ctrl+X, Alt+Enter ...), shown at its right; Text is key text shown without making a shortcut (see Menu Item Properties); OK rejects a Shortcut it does not know. 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.

    Form Properties

    Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, Resizable, HelpTopic and ContextMenu.

    Properties Panel

    @@ -1239,8 +1626,10 @@ img { max-width: 100%; }
  • Startup Form -- which form to show automatically when the program starts.
  • Metadata -- Author, Company, Version, Copyright, Description (a single line), and Icon Path (for compiled binaries).
  • 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.
  • +
  • More Than One Copy -- "Allow more than one copy to run at once" ([Settings] MultiInstance = True): the compiled program can be started again while it is running, and each copy runs on its own (like the Clock and the File Manager). Without it, starting the program again only says that it is already running.
  • 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.
  • -
  • File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section and forms (.frm) in the [Forms] section.
  • +
  • Type -- what the project makes: a program (an .app, the default) or, with Type = Control, a control file (a .wgt holding the project's UserControls for every program). See UserControls in the IDE.
  • +
  • File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section, UserControls (.ctl) in the [Controls] section and forms (.frm) in the [Forms] section.
  • The file sections all have the same shape: one FileN line per file, numbered from 0, and a Count line:

    [Modules]
    @@ -1256,14 +1645,14 @@ Count = 2
     File0 = form1.frm
     Count = 1

    How Projects are Compiled

    -

    When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module is likewise marked with its class name; a class module's header lines are not compiled.

    +

    When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the UserControls, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module and UserControl is likewise marked with its class name; the header lines of a class module or UserControl are not compiled.

    Compile-Time Validation

    Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:

    These checks use the same metadata the Properties panel and Toolbox read from each installed control, so property and method names always match the current widget set without any separate list to maintain.

    Project Operations

    @@ -1273,16 +1662,17 @@ Count = 1 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. Close Project Closes the project, prompting to save unsaved changes. - Add File Adds a .bas, .frm or .cls file to the project. Opening a file without a project auto-creates an implicit project. + Add File Adds a .bas, .frm, .cls or .ctl file to the project. Opening a file without a project auto-creates an implicit project. New Class Module Adds a new class module to the project and opens it (see Class Modules below). + New UserControl Adds a new UserControl to the project and opens it (see UserControls in the IDE). Rename File Gives a file a new name on disk and in the project file (see Renaming a File below). 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, help file, startup form) and the Arguments for runs from the IDE. + Project Properties Opens a dialog for editing project metadata (name, type, author, company, version, copyright, description, icon, help file, startup form), whether more than one copy may run at once, and the Arguments for runs from the IDE.

    Renaming a File

    -

    Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save.

    -

    A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form or class module uses. Code that uses the old class name is not changed. A form keeps its Name property; rename the form itself in the Properties window.

    +

    Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls, a UserControl .ctl and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save.

    +

    A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form, class module or UserControl uses. Code that uses the old class name is not changed. A UserControl named after its file (as File > New UserControl makes it) takes the new name too, on its Begin UserControl line; forms that use the old name are not changed. A form keeps its Name property; rename the form itself in the Properties window.

    Project Explorer

    -

    The Project Explorer is a tree view window listing all project files, in three groups: Forms, Modules and Classes. Double-click a file to open it: .bas and .cls files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.

    +

    The Project Explorer is a tree view window listing all project files, in four groups: Forms, Modules, Classes and Controls (UserControls). Double-click a file to open it: .bas, .cls and .ctl files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.

    Class Modules

    A class module (.cls file) defines a kind of object for your program: its member variables, properties and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.

    -

    Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls).

    +

    Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls). A UserControl placed on a form lists the properties its code defines.

    +

    While a UserControl (.ctl) is the active file, the panel shows the control's header instead: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description and DefaultEvent (see UserControls in the IDE).

    Form Designer

    +

    UserControls in the IDE

    Back to Overview

    Toolbox

    -

    The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox.

    +

    The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox: the controls DVX comes with, the controls written in BASIC that are installed for every program (control files in the WIDGETS folder), and the open project's own UserControls (see UserControls in the IDE). The buttons are in alphabetical order of the control's name. A button shows the control's picture, or its name when it has none; its tooltip is the control's description.

    Click a tool to select it (the active tool name is stored in the designer state), then click on the form to place a new instance. Click the same tool again to deselect it and return to pointer mode.

    Available Controls

      VB Name         Description
       -------         -----------
    -  CommandButton   Push button that triggers a Click event.
    -  Label           Static text label.
    -  TextBox         Single-line text input field.
    -  TextArea        Multi-line text editor.
       CheckBox        On/off checkbox.
    -  OptionButton    Radio button (mutually exclusive within a group).
    -  ListBox         Scrollable list of items.
       ComboBox        Drop-down combo box.
    +  CommandButton   Push button that triggers a Click event.
    +  Data            Data control for binding to a database.
    +  DBGrid          Data-bound grid for displaying database query results.
       DropDown        Simple drop-down list.
    -  PictureBox      Canvas for drawing and images.
       Frame           Grouping container with a labeled border.
    -  VBox            Vertical layout container.
       HBox            Horizontal layout container.
    -  WrapBox         Flow layout container that wraps items to the next row.
    -  Splitter        Resizable split between two child panes.
    -  ScrollPane      Scrollable container for large content.
    -  TabStrip        Tabbed container with multiple pages.
    -  ListView        Multi-column list with headers.
    -  TreeView        Hierarchical tree control.
    -  ProgressBar     Progress indicator bar.
    +  HelpPane        One topic of a help file, as the Help Viewer shows it.
       HScrollBar      Horizontal slider/scrollbar.
    -  SpinButton      Numeric up/down spinner.
    +  Label           Static text label.
       Line            Horizontal or vertical separator line.
    +  ListBox         Scrollable list of items.
    +  ListView        Multi-column list with headers.
    +  OptionButton    Radio button (mutually exclusive within a group).
    +  PictureBox      Canvas for drawing and images.
    +  ProgressBar     Progress indicator bar.
    +  ScrollPane      Scrollable container for large content.
       Spacer          Invisible spacing element for layout.
    +  SpinButton      Numeric up/down spinner.
    +  Splitter        Resizable split between two child panes.
    +  StatusBar       Status bar at the bottom of a form.
    +  TabPage         One page of a TabStrip, holding the controls shown on that tab.
    +  TabStrip        Tabbed container with multiple pages.
    +  Terminal        ANSI terminal emulator control.
    +  TextArea        Multi-line text editor.
    +  TextBox         Single-line text input field.
       Timer           Non-visual timer that fires periodic events.
       Toolbar         Toolbar container for buttons.
    -  StatusBar       Status bar at the bottom of a form.
    -  Terminal        ANSI terminal emulator control.
    -  Data            Data control for binding to a database.
    -  DBGrid          Data-bound grid for displaying database query results.
    + TreeView Hierarchical tree control. + VBox Vertical layout container. + WrapBox Flow layout container that wraps items to the next row.

    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.

    Form Designer

    +

    UserControls in the IDE

    Back to Overview

    @@ -1729,6 +2177,7 @@ Option Explicit
    • 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".
    • SetEvent works on forms (Me) and menu items; a handler without parameters is called without the event's arguments.
    • +
    • Frame: the Caption from the .frm file or set by code now shows on the frame's border, and reading Caption gives it back (it was ignored and read back "").
    • 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.
    • 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.
    • OptionButton.Value is a read/write Boolean (True = selected); the button's index is the new read-only OptionIndex. Setting Value raises no Click.
    • @@ -1752,7 +2201,7 @@ Option Explicit
    • 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.
    • 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.
    • A form shown modal has no minimize gadget.
    • -
    • 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.
    • +
    • Menu items accept only Name (read-only), Checked and Enabled in code; Caption, Visible, RadioCheck, Shortcut and ShortcutText are design-time settings, and using them in code is a compile error in the IDE.

    IDE

      @@ -1774,6 +2223,9 @@ Option Explicit
    • Immediate window: output leaves room for typing, and Enter in a completely full window evaluates nothing and says so in the status bar.
    • Debugger: a breakpoint reached while a stopped program is unwinding no longer pauses it.
    • The title bar's * also shows for a form changed in the designer but not yet saved.
    • +
    • Form designer: a .frm whose container has properties after the controls inside it (Index, Caption ...; DVX writes them before) opens as it runs: the properties are kept and the controls stay in that container. Before, they were dropped, and such a file with another control of the container's name opened as a blank form. Menu items' properties after their submenus are kept too. FrmOpen reads .frm files the same way.
    • +
    • The Toolbox lists the controls in alphabetical order, as CtlTypeName$ does.
    • +
    • UserControls: File > New UserControl, a Controls group in the Project Explorer, the UserControl object and its events in the Code Editor, the header in the Properties window, the project's UserControls in the Toolbox and on the design surface (their code runs, with UserMode False), and control projects with File > Make Control. A program built with Make Executable carries its UserControls. See UserControls in the IDE.
    • 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.

    Back to Overview

    @@ -2094,7 +2546,7 @@ Dim record As PersonType Dim fixedStr As String * 20
    Note: 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".

    Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.

    -

    DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules.

    +

    DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules. DIM variable AS NEW FormName makes a new copy of a form of the project (see Several Copies of a Form in the Form topic); DIM variable AS FormName declares a variable for that form or a copy of it.

    Variable Scope and SHARED

    DVX BASIC follows the QBasic scope rules:

      @@ -2230,6 +2682,14 @@ END DECLARE Declare Sub SQLClose(ByVal db As Long) End Declare

      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. A Byte parameter or result is 8 bits. A Currency parameter receives the amount as a 64-bit whole number of ten-thousandths; a DECLARE FUNCTION cannot return a Currency.

      +

      A String parameter or result declared AS STRING is text: it ends at the first CHR$(0). To pass or receive any bytes, CHR$(0) included (network data, sound samples, encrypted data, checksums of binary data), the declaration says BINARY STRING instead. To the program it is an ordinary String; LEN, MID$, ASC and the other string functions see every byte. BINARY STRING is allowed only in a DECLARE LIBRARY block, and the shipped include files already use it where a function works with bytes.

      +
      Declare Library "mylib"
      +    Declare Function MyChecksum(ByVal bytes As Binary String) As Long
      +    Declare Function MyRead$(ByVal handle As Long, ByVal maxLen As Long) As Binary String
      +End Declare
      +
      +packet$ = "A" + Chr$(0) + "B"
      +Print MyChecksum(packet$)

      See also: BASIC Runtime Libraries

      STATIC

      Declares a local variable that retains its value between calls.

      @@ -2310,6 +2770,7 @@ LSet h = r ' h.kind = "AB", h.size = 10

      Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field or property. After SET both names refer to the same object; nothing is copied.

      SET variable = objectExpression
       SET variable = NEW ClassName
      +SET variable = NEW FormName
       SET variable = Nothing
      Dim frm As Form
       Set frm = CreateForm("MyForm", 320, 240)
      @@ -2321,7 +2782,11 @@ c.Text = "Hello"
       
       Dim list As Collection
       Set list = New Collection      ' a new, empty Collection
      -Set list = Nothing             ' let it go
      +Set list = Nothing ' let it go + +Dim w As Form1 +Set w = New Form1 ' a second Form1 window +w.Show

      The value must be an object or Nothing; anything else raises error 424 (Object required). SET is not used for ordinary numeric or string assignment.

      ERASE

      Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False or a fresh TYPE instance.

      @@ -2459,6 +2924,7 @@ END SUB
      Sub Greet(ByVal name As String)
           Print "Hello, " & name
       End Sub
      +

      A SUB, FUNCTION or Property procedure stands on its own in the program: defining one inside another procedure, or inside an IF, FOR, DO, WHILE, SELECT CASE or WITH block, is a compile error.

      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. A by-reference parameter is the caller's variable itself, as in Visual Basic: a write to it changes the variable at once (the caller's code, or another parameter naming the same variable, sees the new value straight away, even if the procedure later stops with an error), and what is stored is converted to the variable's own type (a STRING * n keeps its length). This holds for a plain variable, an array element (`bump a(3)` changes `a(3)` in place), a TYPE field (`bump rec.count`, `bump recs(2).count`, `bump rec.inner.count`) and, inside a class, one of the class's own member variables. While an array element (or a field of one) is passed by reference, its array cannot be given a new size or erased: REDIM of that array, or ERASE of it when it is a dynamic array, raises error 10 (This array is fixed or temporarily locked), as in Visual Basic (ERASE of a fixed-size array only clears its elements and is allowed). The array is free again as soon as the call returns. Assigning a whole TYPE value to the variable (or element) whose field is being passed copies the value into the same record, so the parameter sees the new value (as in Visual Basic). 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:

      Greet "World"
       Greet("World")
      @@ -2552,7 +3018,7 @@ END FUNCTION
      Function Square(ByVal n As Double) As Double
           Square = n * n
       End Function
      -

      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.

      +

      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. Inside the function its name on its own reads the value set so far (Total = Total + n), as in Visual Basic; with an argument list -- F() or F(n - 1) -- it calls the function again. Use EXIT FUNCTION to return early.

      A FUNCTION can also be called as a statement, with or without parentheses; its result is discarded:

      Function Beep2(ByVal n As Integer) As Integer
           Beep2 = n * 2
      @@ -2719,6 +3185,7 @@ ERROR n                 ' Raise a runtime error with error number n
    • An error raised inside a running handler passes to the calling procedure's handler (untrapped if there is none).
    • The label named by ON ERROR GOTO must be in the same procedure.
    +

    An error that no handler traps ends the program, as in Visual Basic, wherever it happens: in module-level code or in an event handler (a Click, a Timer, a form's Load ...). DVX shows it in a BASIC Runtime Error box -- the error number, the line and the message -- and writes it to the DVX log; the program's windows close once the box is dismissed. In the IDE the Output window shows the error and the editor goes to its line.

    ON ERROR RESUME NEXT, as in Visual Basic, needs no handler: a statement that fails is abandoned and the program goes on at the next one, with ERR (Err.Number) and Err.Description telling what went wrong. An error in a procedure that has no handler of its own leaves that procedure, and the program goes on after the statement that called it. The statement clears ERR, and so does the end of the procedure that used it; check ERR right after the statements that may fail:

    On Error Resume Next
     Kill "old.tmp"
    @@ -2789,6 +3256,7 @@ ErrorHandler:
       449      Argument not optional (a method call through an object left out a parameter that is not OPTIONAL)
       450      Wrong number of arguments or invalid property assignment
       451      Object not a collection (FOR EACH over an object that is not a Collection)
    +  453      Library or function not installed (a DECLARE LIBRARY function whose library is not on this computer; see Include Libraries)
       457      This key is already associated with an element of this collection (Collection.Add with a key in use)
       481      Invalid picture (LOADPICTURE of a file that is not a picture DVX can show)
       1000     No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)
    @@ -2870,7 +3338,7 @@ PLAY "I74 O4 L8 GAB>C" ' on the flute y = STICK(1) If STRIG(0) Then FireShot

    FRAMEWAIT

    -

    Waits until the next frame of a steady clock of rate frames a second begins, keeping the desktop and the program's forms running meanwhile. A game loop that calls it once a frame runs at the same speed on every computer. Frames the program was too busy for are skipped, not caught up. rate must be at least 1; rates above 1000 count as 1000. Changing the rate starts the clock over.

    +

    Waits until the next frame of a steady clock of rate frames a second begins, keeping the desktop and the program's forms running meanwhile. A game loop that calls it once a frame runs at the same speed on every computer. Frames the program was too busy for are skipped, not caught up; FrameMissed() in game.bas counts them. rate must be at least 1; rates above 1000 count as 1000. Changing the rate starts the clock over.

    FRAMEWAIT rate
     
     Do
    @@ -3469,7 +3937,7 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)Print IIf(score >= 50, "Pass", "Fail")
     Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

    LOADPICTURE

    -

    LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG or GIF) and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working.

    +

    LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG or GIF) -- or the icon of a DVX program, widget or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).

    The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3) or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2) or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.

    A Picture object has three properties, which can only be read:

      Property   Type      Description
    @@ -3498,8 +3966,9 @@ END IF

    Loading and Unloading Forms

    LOAD FormName
     UNLOAD FormName
    -UNLOAD Me
    -

    LOAD creates the form and its controls in memory. It fires Form_Load when the form is first loaded. UNLOAD destroys the form, firing Form_QueryUnload (which can cancel the close) and then Form_Unload. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running.

    +UNLOAD Me +UNLOAD formVariable +

    LOAD creates the form and its controls in memory. It fires the form's Load event (Form1_Load for a form named Form1) when the form is first loaded. UNLOAD destroys the form, firing its QueryUnload event (which can cancel the close) and then its Unload event. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. UNLOAD also takes a Form variable, or anything else that gives a form (for example a form made with CreateForm and kept in an object's property), and unloads that form.

    Showing and Hiding Forms

    FormName.Show [mode]
     FormName.Hide
    @@ -3555,7 +4024,7 @@ End With

    In addition to loading forms from .frm files, you can build a form entirely from code with CreateForm, CreateControl, SetEvent, and RemoveControl. The returned reference should be stored with SET.

    CreateForm

    SET formVar = CreateForm(name$, width%, height%)
    -

    Creates a new form with the given name and initial size. Returns a form reference.

    +

    Creates a new form with the given name and initial size. Returns a form reference. If a form of that name is already loaded, CreateForm returns that form; to open another copy of a form designed in a .frm file, use New FormName (see Several Copies of a Form in the Form topic).

    CreateControl

    SET ctrlVar = CreateControl(formRef, typeName$, ctrlName$ [, parentCtrlRef])

    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.

    @@ -3565,6 +4034,21 @@ End With

    RemoveControl

    REMOVECONTROL formRef, ctrlName$

    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.

    +

    LoadFormText

    +
    SET formVar = LOADFORMTEXT(frmText$)
    +

    Loads a form from the text of a .frm file -- one a program made, read from a file or got from somewhere else -- and returns it, loaded but hidden like Load (its Load event runs; call Show). A form whose name is already loaded becomes another copy of its own, as with New. The form's controls fire events to procedures named after them in the running program (Gen1_Load, Go_Click), as for any form; a code section after the form's End is not compiled. Text that is not a form raises error 5 (Illegal function call). Form designers use it to show a .frm file being edited; Controls, CallByName and the designer.bas functions then read and change it.

    +
    Dim f As Object
    +Dim n As Integer
    +Dim t As String
    +
    +n = FreeFile
    +Open "PREVIEW.FRM" For Binary As #n
    +t = Input$(LOF(n), #n)
    +Close #n
    +
    +Set f = LoadFormText(t)
    +f.Show
    +Print f.Controls.Count; f.Controls(1).Name
    Dim frm As Form
     Dim lbl As Control
     
    @@ -3609,12 +4093,12 @@ If name$ <> "" Then
         Print "Hello, "; name$
     End If

    Event Handler Convention

    -

    Event handlers are named ControlName_EventName and defined as SUBs:

    +

    Event handlers are named ControlName_EventName and defined as SUBs. A form's own events are named after the form: the Load event of a form named Form1 is Form1_Load. (Visual Basic's Form_Load spelling is not used: every procedure name is shared by the whole program, so each form's handlers need names of their own.)

    Sub Command1_Click()
         MsgBox "Button clicked!"
     End Sub
     
    -Sub Form_Load()
    +Sub Form1_Load()
         Me.Caption = "My App"
     End Sub
     
    @@ -3636,12 +4120,13 @@ End Sub
    Scroll Control was scrolled (mouse wheel or scrollbar) GotFocus Control received input focus LostFocus Control lost input focus - Form_Load Form is being loaded (fires once, after controls are created) - Form_QueryUnload Form is about to close; set Cancel = 1 to abort - Form_Unload Form is being unloaded - Form_Resize Form was resized - Form_Activate Form gained focus - Form_Deactivate Form lost focus + Form1_Load Form is being loaded (fires once, after controls are created) + Form1_QueryUnload Form is about to close; set Cancel = 1 to abort + Form1_Unload Form is being unloaded + Form1_Resize Form was resized + Form1_Activate Form gained focus + Form1_Deactivate Form lost focus + Form1_NewArgs The running program was handed new arguments (COMMAND$ changed) Timer1_Timer Timer control interval elapsed (Timer control's default event)
    @@ -3751,6 +4236,16 @@ End Sub

    An object can be kept in a variable declared as its class name, As Object, or As Variant, in an array, in a TYPE field, in a Collection, or in another object's member variable. It can be passed to a SUB or FUNCTION and returned from a FUNCTION (assign the result with SET inside the function, Set MakeAccount = acct). TYPENAME gives an object's class name, and TypeOf obj Is ClassName tests it; Is tells whether two variables refer to the same object.

    Arguments of a class's procedures follow the rules of SUB and FUNCTION calls when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it, an omitted OPTIONAL argument gets its default, and a ParamArray collects the rest. When the object is reached through a variable declared As Object or As Variant, a variable or array element given as an argument is passed by reference too if the procedure's parameter is not BYVAL (Visual Basic's rule; other expressions go by value). What the procedure stores in such a parameter is converted to the type of the caller's variable (an Integer stays an Integer, as in Visual Basic). A TYPE field given as an argument (obj.Move rec.x, obj.Move pts(1).x) is passed by reference as well (a BYVAL parameter gets its value). The methods of forms and controls always take their arguments by value. An argument left out in the middle of a call (obj.Report a, , c) or at the end is an OPTIONAL parameter's default value, else its empty value: 0, "" and so on for a typed parameter, Missing for a Variant (ISMISSING is True). Leaving out a parameter that is not OPTIONAL raises error 449 (Argument not optional).

    An object parameter is itself passed like any other parameter: without BYVAL, SET on the parameter changes the caller's variable (a SUB can make a new object for the caller); with BYVAL, the procedure gets its own reference to the same object, and SET on it does not affect the caller.

    +

    CALLBYNAME

    +
    result = CALLBYNAME(object, memberName$, callType [, arguments...])
    +CALLBYNAME object, memberName$, callType [, arguments...]
    +

    Uses the member of an object whose name the program has in a string, as if it had been written after a dot: a form, a control, a class instance or a Collection. callType says what to do: vbGet reads a property (with arguments, a property that takes them), vbLet assigns it the last argument, vbSet assigns it an object (the last argument), vbMethod calls a method or function with the arguments. The function gives the property's value or the method's result (Empty for vbLet and vbSet); as a statement the result is dropped. Another callType raises error 5 (Illegal function call), as does vbLet or vbSet without a value; a member the object does not have raises the error the same member written after a dot would. A form designer's property window uses it to read and set the properties designer.bas lists for a control type.

    +
    Dim prop As String
    +prop = "Caption"
    +Print CallByName(Command1, prop, vbGet)       ' Command1.Caption
    +CallByName Command1, prop, vbLet, "OK"        ' Command1.Caption = "OK"
    +CallByName List1, "AddItem", vbMethod, "one"  ' List1.AddItem "one"
    +Print CallByName(acct, "Balance", vbGet)      ' a class instance's property

    Default Member

    A class can name one property or method as its default member, used when the object is written with parentheses directly after it. Put this line in the class, inside the procedure (as Visual Basic writes it) or on its own at the top of the class:

    Attribute Item.VB_UserMemId = 0
    @@ -3795,6 +4290,7 @@ NoMoney:

    See also: Events

    See also: Interfaces (Implements)

    +

    See also: UserControls: Your Own Controls

    See also: Collection Object

    See also: Variants

    See also: SET and PUBLIC / PRIVATE

    @@ -4042,6 +4538,39 @@ Print "Running from: " & App.Path If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()

    A control or form of your own named Clipboard takes the name's place.

    +
    +

    Screen Object

    +

    The Screen object is the DVX desktop, as in Visual Basic: its size, so a program can place its forms (for example in a corner of the screen), its colour depth, the mouse pointer, and the arrangement of the windows on it.

    +
      Property              Type      R/W   Description
    +  --------------------  -------   ---   -----------
    +  Screen.Width          Long      R     Width of the desktop in pixels
    +  Screen.Height         Long      R     Height of the desktop in pixels
    +  Screen.BitsPerPixel   Long      R     Colour depth of the display: 8, 15, 16, 24 or 32 bits per pixel
    +  Screen.MousePointer   Integer   R/W   vbHourglass (11) for the busy pointer while a long job runs; another pointer (vbArrow, vbCrosshair, vbSizeNS, vbSizeWE, vbSizeNWSE, vbSizeNESW, vbNoDrop: see Mouse Constants) over every form of the program, whatever their controls' MousePointer; vbDefault (0) back to each control's own
    +  Screen.ActiveControl  Control   R     The control of the program that has the keyboard focus, Nothing when none has it (another program's window has the focus)
    +  Screen.DebugLayout    Boolean   R/W   True draws a coloured border around every layout container (VBox, HBox, Frame ...) in every window, to see how a form is laid out; False removes them
    +

    The sizes are in pixels, the unit of a form's Left, Top, Width and Height in DVX (Visual Basic uses twips there).

    +
    ' Put the form at the right edge, 40 pixels from the top
    +Me.Left = Screen.Width - Me.Width - 40
    +Me.Top  = 40
    +

    Set the hourglass before a long job and put the arrow back after it. The pointer is the whole desktop's, so the program should not leave it busy; when the program ends, a busy pointer it set goes back to the arrow.

    +
    Screen.MousePointer = vbHourglass
    +CountWords
    +Screen.MousePointer = vbDefault
    +

    Arrange

    +

    Screen.Arrange arrangement puts every window on the desktop that is shown and not minimized (those of other programs too) in order, as the Window menu of a program does:

    +
      Constant           Value   Arrangement
    +  ----------------   -----   -----------
    +  vbCascade          0       Overlapping, each one a little down and to the right of the one before
    +  vbTileHorizontal   1       Stacked from top to bottom, each the full width of the screen
    +  vbTileVertical     2       Side by side from left to right, each the full height of the screen
    +  vbTileGrid         4       In rows and columns
    +

    The values are those of Visual Basic's MDIForm.Arrange (vbTileGrid is DVX's own; 3, Visual Basic's vbArrangeIcons, is not used).

    +
    Sub mnuCascade_Click ()
    +    Screen.Arrange vbCascade
    +End Sub
    +

    Any other property, method or value stops the program with an error. The QBasic statement SCREEN and function SCREEN(row, column) are unaffected: Screen followed by a dot is the object. A control or form of your own named Screen takes the name's place.

    +

    INI Functions

    DVX BASIC provides built-in functions for reading and writing standard INI configuration files (the same text format used by DVX system configuration).

    @@ -4087,6 +4616,17 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
      Constant   Value   Description
       --------   -----   -----------
       vbModal    1       Show form as modal dialog
    +

    Window State, Arrangement and Mouse Pointer

    +

    A form's WindowState and Screen.Arrange (see Form and Screen Object); the MousePointer values are under Mouse Constants.

    +
      Constant           Value   Description
    +  --------           -----   -----------
    +  vbNormal           0       WindowState: a normal window
    +  vbMinimized        1       WindowState: shown only as an icon
    +  vbMaximized        2       WindowState: filling the screen
    +  vbCascade          0       Arrange: overlapping windows
    +  vbTileHorizontal   1       Arrange: windows stacked top to bottom
    +  vbTileVertical     2       Arrange: windows side by side
    +  vbTileGrid         4       Arrange: windows in rows and columns

    File Attribute Flags

    Bit values used with GETATTR and SETATTR.

      Constant      Value   Description
    @@ -4135,6 +4675,67 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
     
      Constant   Value   Description
       --------   -----   -----------
       vbCFText   1       Text, the format the DVX clipboard holds
    +

    Mouse Constants

    +

    The Button argument of the MouseDown, MouseUp and MouseMove events (several bits when several buttons are down), and the values of MousePointer (controls, forms and Screen).

    +
      Constant         Value   Description
    +  --------         -----   -----------
    +  vbLeftButton     1       The left button
    +  vbRightButton    2       The right button
    +  vbMiddleButton   4       The middle button
    +  vbDefault        0       MousePointer: the control's own pointer
    +  vbArrow          1       MousePointer: the arrow
    +  vbCrosshair      2       MousePointer: a cross, for placing things
    +  vbSizeNESW       6       MousePointer: two-way arrow, top right to bottom left
    +  vbSizeNS         7       MousePointer: two-way arrow, up and down
    +  vbSizeNWSE       8       MousePointer: two-way arrow, top left to bottom right
    +  vbSizeWE         9       MousePointer: two-way arrow, left and right
    +  vbHourglass      11      MousePointer: busy
    +  vbNoDrop         12      MousePointer: a circle with a line through it
    +

    Visual Basic's other pointers (vbIbeam, vbUpArrow, custom pointers) have no DVX shape; DVX has no MouseIcon.

    +

    Syntax Colour Constants

    +

    The kinds of text a TextArea colours: the index of SetSyntaxColor and GetSyntaxColor, and the digits of the Colors argument of the Colorize event ("0" is vbSyntaxText ... "6" vbSyntaxType).

    +
      Constant           Value   Description
    +  --------           -----   -----------
    +  vbSyntaxText       0       Ordinary text (ForeColor)
    +  vbSyntaxKeyword    1       Keywords
    +  vbSyntaxString     2       Strings
    +  vbSyntaxComment    3       Comments
    +  vbSyntaxNumber     4       Numbers
    +  vbSyntaxOperator   5       Operators
    +  vbSyntaxType       6       Type names
    +

    CallByName Constants

    +
      Constant   Value   Description
    +  --------   -----   -----------
    +  vbMethod   1       Call a method (or a function of a class)
    +  vbGet      2       Read a property
    +  vbLet      4       Assign a property a value
    +  vbSet      8       Assign a property an object (Set)
    +

    Key Code Constants

    +

    The KeyCode a KeyDown event gets, and the bits of its Shift argument. These are DVX's codes, so compare KeyCode with the constants rather than with numbers from Visual Basic. A character key gives its character's code (Asc("a") with no Shift), and Ctrl with a letter gives 1 to 26 (Ctrl+A is 1, Ctrl+C 3, Ctrl+V 22, Ctrl+X 24).

    +
      Constant                    Value               Description
    +  --------                    -----               -----------
    +  vbKeyBack                   8                   Backspace
    +  vbKeyTab                    9                   Tab
    +  vbKeyReturn                 13                  Enter
    +  vbKeyEscape                 27                  Esc
    +  vbKeySpace                  32                  Space bar
    +  vbKeyPageUp, vbKeyPageDown  &H149, &H151        Page Up, Page Down
    +  vbKeyEnd, vbKeyHome         &H14F, &H147        End, Home
    +  vbKeyLeft, vbKeyUp          &H14B, &H148        Arrow keys
    +  vbKeyRight, vbKeyDown       &H14D, &H150        Arrow keys
    +  vbKeyInsert, vbKeyDelete    &H152, &H153        Ins, Del
    +  vbKeyF1 to vbKeyF10         &H13B to &H144      Function keys F1 to F10
    +  vbKeyF11, vbKeyF12          &H185, &H186        Function keys F11 and F12
    +  vbShiftMask                 3                   Shift argument: a Shift key is down
    +  vbCtrlMask                  4                   Shift argument: Ctrl is down
    +  vbAltMask                   8                   Shift argument: Alt is down
    +
    Sub List1_KeyDown(KeyCode As Integer, Shift As Integer)
    +    If KeyCode = vbKeyDelete Then
    +        DeleteSelected
    +    ElseIf KeyCode = 1 And (Shift And vbCtrlMask) <> 0 Then
    +        SelectEverything        ' Ctrl+A
    +    End If
    +End Sub

    Scale and Picture Constants

    The scale units of ScaleMode and of the ScaleX / ScaleY methods, and the values of a Picture object's Type (see Drawing on Forms and PictureBoxes, and LOADPICTURE).

      Constant          Value   Description
    @@ -4198,17 +4799,30 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
     

    DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory. Each file is a small library of DECLARE LIBRARY wrappers around native DVX runtime functions. Add an include file to your project (the IDE's Add File command, or editing the .dbp with another tool) and the declared functions become available to your code.

      File           Provides
       ----           --------
    -  commdlg.bas    File Open/Save dialogs, input dialogs, choice dialogs, save-prompt dialogs, the color dialog
    +  commdlg.bas    File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, message boxes with your own buttons (DlgButtons)
       sql.bas        SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
    -  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; port settings read back and changed, why a call failed, breaks, ring and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
    +  conn.bas       Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy or Telnet, shown on a Terminal control, with capture, answerback and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
    +  xfer.bas       File transfers with XMODEM, XMODEM-1K, YMODEM and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
    +  prefs.bas      Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...)
       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
    -  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
    -  game.bas       Games: whether a key is being held down right now (GameKeyDown)
    -  joystick.bas   Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...)
    -  sound.bas      Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...)
    -  music.bas      Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, ...)
    + help.bas Compile help source files and see the compiler's messages, and open the DVX help viewer, at a topic if you like (HelpCompile, HelpCompileMessages$, HelpView, HelpViewTopic; HelpView "" opens the DVX user guide) + hlpfile.bas Read compiled help files yourself, for a help viewer of your own: topics, table of contents, keyword index, full-text search, and each topic's headings, text, links and pictures; show a topic in a HelpPane control (HlpOpen, HlpTocTitle$, HlpIndexKeyword$, HlpSearch, HlpLoadTopic, ...) + net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...) + print.bas Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...) + listhelp.bas Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...) + security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...) + game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace) + joystick.bas Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...) + sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...) + music.bas Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$) + fileops.bas Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving and deleting whole folders at once or as a job with progress, Cancel and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...) + system.bas What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...) + layout.bas Control sizes in characters or in percent of the container instead of pixels (SizeChars, SizePercent) + compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...) + debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...) + designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...) + shell.bas DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, this program's settings folder, the DVX log; Program Manager's program groups with their items, shortcut keys and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellConfigFile$, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)

    Using an Include Library

    ' With commdlg.bas added to the project
     Dim path As String
    @@ -4217,6 +4831,27 @@ path = DlgFileOpen$("Open a file", "Text Files (*.txt)|All Files
     If path <> "" Then
         Print "Selected: " & path
     End If
    +

    When a Library Is Not Installed

    +

    Some libraries are optional parts of DVX (the network, printing, sound, joysticks ...). Calling a function of a library that is not on the computer raises run-time error 453, "Library or function not installed", which ON ERROR can trap -- for example to show a message instead of a page of settings, as Control Panel does:

    +
    Dim status As String
    +On Error Resume Next
    +status = NetStatus$("message")
    +If Err.Number = 453 Then
    +    lblNet.Caption = "The network is not installed."
    +Else
    +    lblNet.Caption = status
    +End If
    +On Error GoTo 0
    +

    Message Boxes with Your Own Buttons

    +

    MsgBox offers fixed sets of buttons (OK, Yes and No, ...). With commdlg.bas in the project, DlgButtons(title, message, buttons, icon) shows a message box with the buttons you list, separated by |, with & before each button's access key. It returns the index of the button chosen: 0 for the first, which Enter presses. Esc and closing the box choose the last button, so make the last one the choice that changes nothing. icon is vbQuestion, vbInformation, vbExclamation, vbCritical or 0. Up to six buttons; -1 is returned when the list is empty or longer.

    +
    Select Case DlgButtons("Copy", "notes.txt already exists. Replace it?", "&Yes|&No|Yes to &All|&Cancel", vbQuestion)
    +    Case 0
    +        ' replace this one
    +    Case 2
    +        ' replace this and every other one
    +    Case Else
    +        ' keep it (No), or stop (Cancel)
    +End Select

    Color Dialog

    With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.

    Dim c As Long
    @@ -4225,6 +4860,21 @@ c = DlgColor("Text Color", Label1.ForeColor)
     If c <> -1 Then
         Label1.ForeColor = c
     End If
    +

    Number Input Dialog

    +

    With commdlg.bas in the project, DlgIntInput(title, prompt, default, min, max) asks for a whole number with a spin box, and DlgIntInputStep(title, prompt, default, min, max, step) does the same with the arrows moving by step. Both return the number entered, or default when the user cancels; DlgInputCancelled() (True after a Cancel, False after OK) tells the two apart, as it does for DlgInputBox$.

    +
    n = DlgIntInputStep("Zoom", "Zoom in percent:", 100, 25, 400, 25)
    +
    +If Not DlgInputCancelled() Then
    +    SetZoom n
    +End If
    +

    Resources in Files

    +

    With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget and library files: icons, text and any bytes. ResOpen, ResCount, ResName$, ResType and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT or RES_TYPE_ICON). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.

    +
    Dim settings As String
    +settings = Chr$(1) + Chr$(0) + Chr$(255)
    +
    +If ResAddData("mygame.app", "state", RES_TYPE_BINARY, settings) Then
    +    Print Len(ResGetData$("mygame.app", "state"))   ' 3
    +End If
    Note: DVX BASIC does not have an $INCLUDE directive. Libraries are added to the project so that the compiler sees their DECLARE LIBRARY blocks.

    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.

    @@ -4237,6 +4887,32 @@ Else Print "Handshake failed" End If

    After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.

    +

    CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.

    +
    Sub Timer1_Timer ()
    +    Select Case CommHandshakeStep(link, txtPass.Text)
    +        Case COMM_PENDING
    +            lblState.Caption = "Connecting..."
    +        Case COMM_OK
    +            Timer1.Enabled = False
    +            lblState.Caption = "Connected"
    +        Case COMM_ERR_AUTH
    +            Timer1.Enabled = False
    +            lblState.Caption = "Wrong passphrase"
    +        Case Else
    +            Timer1.Enabled = False
    +            lblState.Caption = "The other side does not answer"
    +    End Select
    +End Sub
    +

    Serial Port Settings and Errors

    +

    With comm.bas, SerOpen returns False when a port cannot be opened; SerLastError(com) then says why, as an SER_ERR_ constant (SER_ERR_NO_UART: there is no port there, SER_ERR_IRQ_NOT_FOUND, SER_ERR_INVALID_BAUD, ...), or SER_OK when the last SerOpen or SerSet worked.

    +
      Function                                            Description
    +  --------                                            -----------
    +  SerSet(com, baud, dataBits, parity, stopBits, handshake)  Change the settings of an open port (SerOpen's arguments). True when done.
    +  SerGetBaud(com), SerGetDataBits(com), SerGetParity$(com), SerGetStopBits(com), SerGetHandshake(com)  The settings of an open port; parity is "N", "E", "O", "M" or "S". When the port is not open the numbers are an SER_ERR_ code and the parity "".
    +  SerTxPending(com), SerFlushTx com                   Bytes written but not sent yet; throw them away.
    +  SerSetBreak com, state                              Send a break: True holds the line in the break state, False releases it (hold it a quarter to half a second).
    +  SerGetRi(com)                                       True while the other end rings.
    +  SerLineErrors(com)                                  Errors seen on received data, added up: SER_LINE_OVERRUN (data came faster than it was read), SER_LINE_PARITY, SER_LINE_FRAMING, SER_LINE_BREAK; 0 for none.

    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.

    Sub mnuPrint_Click ()
    @@ -4247,12 +4923,210 @@ Sub mnuPageSetup_Click ()
         PrintPageSetup
     End Sub

    For printer output a line at a time, see LPRINT and OPEN "LPT1:".

    +

    A program that prints its own way has more:

    +
      Function                                            Description
    +  --------                                            -----------
    +  PrintPageCount(text)                                How many pages text takes with the saved page setup and printer.
    +  PrintTextPages(text, title, firstPage, lastPage, copies)  Print text without asking: those pages (0 and 0 for all), copies times.
    +  PrintChoose(title, pageCount)                       Show the Print dialog only (the page range when pageCount is above 0). True when the user chose Print; then PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst and PrintChoiceLast say what was chosen (pages 0 and 0: all).
    +  PrintForm(formName) / PrintFormDialog(formName)     Print what a loaded form shows, scaled to the page (a printer model that prints pictures); the second shows the Print dialog first.
    +  PrintPortPresent(port), PrintPortStatus(port)       Whether printer port LPTn (1 to 3) exists, and its state: PRINT_STATUS_ values added up (PRINT_STATUS_PAPER_OUT, PRINT_STATUS_SELECTED, PRINT_STATUS_NOT_BUSY, ...).
    +  PrintStatusProblem$(status, afterWrite, where)      What is wrong with a printer in that state, in words ("" when nothing is), for printer where such as "LPT1".
    +
    If PrintChoose("Report", 0) Then
    +    If PrintDestIsFile(PrintChoiceDest()) Then
    +        Open PrintChoiceFile$() For Output As #1
    +    Else
    +        Open "LPT" & (PrintChoiceDest() + 1) & ":" For Output As #1
    +    End If
    +    Print #1, "Report"
    +    Close #1
    +End If
    +

    The page setup can be read and changed without the Page Setup dialog. Like the printer settings these take a handle from prefs.bas:

    +
      Function                                            Description
    +  --------                                            -----------
    +  PrintPageSetting(prefs, field)                      A number of the page setup: "Lines" (lines per page, 0 = the printer's usual length), "Columns", "MarginTop", "MarginBottom", "MarginLeft", "MarginRight", "TabWidth", or "Wrap" (True: long lines wrap at a space); -1 for an unknown field.
    +  PrintPageText$(prefs, field)                        "Header" or "Footer". In them &f is the document name, &p the page, &n the number of pages, &d the date, &t the time, && an ampersand; &l, &c and &r put what follows left, centred or right.
    +  PrintPageSetupProblem$(lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer)  "" when this page setup can be used, else why not.
    +  PrintPageSettingsStore(prefs, lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer)  Put a page setup into the settings (then PrefsSave). False for one PrintPageSetupProblem$ objects to.
    +  PrintPageLines(prefs, model)                        Lines per page the setup gives on printer model.
    +

    The limits are CONSTs: PRINT_LINES_MIN to PRINT_LINES_MAX, PRINT_COLUMNS_MIN to PRINT_COLUMNS_MAX, PRINT_TAB_MAX, PRINT_COPIES_MAX, with the defaults PRINT_COLUMNS_DEFAULT, PRINT_MARGIN_TB_DEFAULT, PRINT_MARGIN_LR_DEFAULT and PRINT_TAB_DEFAULT.

    +

    A program can also change the printer settings the way the Control Panel's Printer page does. They are kept in DVX's settings file, so these calls take a handle from prefs.bas (add prefs.bas to the project too). Printer models and destinations are numbered from 0.

    +
      Function                                       Description
    +  --------                                       -----------
    +  PrintModelCount() / PrintModelLabel$(model)    How many printer models there are, and the name of each.
    +  PrintDestCount() / PrintDestLabel$(dest)       How many places the printer can be connected to (LPT1, LPT2, LPT3, a file), and the name of each.
    +  PrintDestIsFile(dest)                          True for the destination that prints into a file.
    +  PrintSettingModel(prefs)                       The printer model in the settings.
    +  PrintSettingDest(prefs)                        Where the printer is connected.
    +  PrintSettingFile$(prefs)                       The file printed into when the destination is a file.
    +  PrintSettingsProblem$(dest, filePath)          "" when this choice can be used, else why not (printing to a file needs its name).
    +  PrintSettingsStore(prefs, model, dest, filePath)  Put the choice into the settings, keeping the page setup as it is; then save with PrefsSave.
    +
    Dim prefs As Long
    +
    +Sub Form1_Load ()
    +    Dim i As Long
    +    prefs = PrefsLoad(PrefsDvxIni$())
    +    For i = 0 To PrintModelCount() - 1
    +        cboModel.AddItem PrintModelLabel$(i)
    +    Next i
    +    cboModel.ListIndex = PrintSettingModel(prefs)
    +End Sub
    +
    +Sub cmdOK_Click ()
    +    If PrintSettingsStore(prefs, cboModel.ListIndex, PrintSettingDest(prefs), PrintSettingFile$(prefs)) Then
    +        PrefsSave prefs
    +    End If
    +    PrefsClose prefs
    +End Sub
    +

    Files and Folders

    +

    With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name.

    +

    Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsBaseName$ and FileOpsExtension$. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.

    +

    Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName (whether a name can be used as a file name here; FileOpsError$ says why not), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.

    +

    Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    +

    FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.

    +
    Dim job As Long
    +
    +Sub cmdCopy_Click ()
    +    job = FileOpsJobCreate(FILEOPS_COPY, "C:\BACKUP")
    +    If FileOpsJobAddSource(job, "C:\DOCS") Then Timer1.Enabled = True
    +End Sub
    +
    +Sub Timer1_Timer ()
    +    Select Case FileOpsJobRunFor(job, 50)
    +        Case FILEOPS_STEP_ASK
    +            If MsgBox(FileOpsJobMessage$(job), vbYesNo + vbQuestion) = vbYes Then
    +                FileOpsJobAnswer job, FILEOPS_ANSWER_YES
    +            Else
    +                FileOpsJobAnswer job, FILEOPS_ANSWER_NO
    +            End If
    +        Case FILEOPS_STEP_ERROR
    +            FileOpsJobAnswer job, FILEOPS_ANSWER_SKIP
    +        Case FILEOPS_STEP_DONE, FILEOPS_STEP_CANCELLED
    +            Timer1.Enabled = False
    +            FileOpsJobFree job
    +        Case Else
    +            ProgressBar1.Value = FileOpsJobPercent(job)
    +    End Select
    +End Sub
    +

    Working with DVX

    +

    With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC.

    +

    Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), or opens a document in the program set for its type; it returns True when it started. ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (0 when it did not start; DVX has already said why). ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.

    +

    Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$.

    +

    File types. ShellAssocProgram$(extension) tells which program opens files with that extension (with ",arguments" after it when it is given any), ShellAssocCommand$(document) just the program that opens a document. ShellAssocCount() file types are set; ShellAssocExt$(index) and ShellAssocValue$(index) read them (index from 0). ShellAssocSet(extension, program) sets one and ShellAssocRemove(extension) removes one, saved at once; ShellAssocNormalize$(extension) is the extension as stored (".txt" gives "TXT"; "" when it cannot be one).

    +

    Running programs. ShellRunningCount() says how many programs are running, and ShellRunningId(index) is the id of each one (index from 0; the same id ShellRunProgram returns). ShellAppId() is this program's own id. ShellRunningName$(id) and ShellRunningPath$(id) are the name a running program shows and the path of its .app ("" when it is not running), and ShellRunningFind(program) is the id of a running copy of a program, 0 when it is not running. ShellRunningActivate id brings a program to the front the way Task Manager's Switch To does (a minimized window is restored). ShellRunningClose id asks a program to close as if the user had closed its windows: it may ask to save its work and the user may cancel, and it is asked once your code has returned to DVX, so check from a Timer whether it is still running. ShellRunningKill id ends a program at once without asking it, the way Task Manager's End Task does: whatever it had not saved is lost, so keep it for a program that does not respond. ShellAppIsHung(id) is True when a program has been busy for 5 seconds or longer ("Not responding" in Task Manager). A program cannot end itself with ShellRunningKill (END does that). ShellAppMemory(id) is how many bytes of memory a program is using. ShellChangeCount() changes whenever a program starts or ends: check it from a Timer and update what you show when it is not the value you saw last. ShellTaskManager opens the Task Manager, ShellSystemInfo$() describes the computer (several lines), and ShellExitDvx exits DVX: every program is asked to close, each asking about unsaved work, and a program that stays open stops the exit (ask the user first).

    +
    Sub cmdNotepad_Click ()
    +    Dim id As Long
    +    id = ShellRunningFind("APPS\KPUNCH\NOTEPAD\NOTEPAD.APP")
    +    If id = 0 Then
    +        ShellRun "APPS\KPUNCH\NOTEPAD\NOTEPAD.APP", ""
    +    Else
    +        ShellRunningActivate id
    +    End If
    +End Sub
    +
    +Sub FillTaskList ()
    +    Dim i As Long, id As Long
    +    lstTasks.Clear
    +    For i = 0 To ShellRunningCount() - 1
    +        id = ShellRunningId(i)
    +        If ShellAppIsHung(id) Then
    +            lstTasks.AddItem ShellRunningName$(id) & " (not responding)"
    +        Else
    +            lstTasks.AddItem ShellRunningName$(id)
    +        End If
    +    Next i
    +End Sub
    +

    Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). COMMAND$(n) gives this program's own arguments the same way.

    +

    Keys for every program. HotkeyAdd(keyText) makes a key such as "Ctrl+Alt+E" work whichever program is in front and returns its id (0 when it is not a key, needs Ctrl or Alt, or another program uses it). The program asks for the keys pressed from a Timer: HotkeyNext() returns the id of the oldest one not asked for yet, 0 when there is none. HotkeyRemove id stops one; the rest stop when the program ends. HotkeyText$(keyText) writes a key the way DVX does ("ctrl+alt+e" gives "Ctrl+Alt+E"; "" when it is not a key), handy for checking what the user typed. ShellItemKeyValid(keyText) is Program Manager's rule for an item's shortcut: Ctrl together with Alt or Shift, so it never takes a key a program uses itself ("" for no shortcut is valid too).

    +

    This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none).

    +

    Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE.

    +
    Dim groups As Long, main As Long, i As Long
    +groups = ShellGroupsLoad(ShellConfigFile$("groups.ini"))
    +main = ShellGroupFind(groups, "Main")
    +i = ShellItemAdd(groups, main, "Notepad", "APPS\KPUNCH\NOTEPAD\NOTEPAD.APP")
    +If Not ShellItemSet(groups, main, i, "key", "Ctrl+Alt+N") Then MsgBox "Not a shortcut."
    +picIcon.Picture = LOADPICTURE(ShellItemIcon$(ShellItemGet$(groups, main, i, "icon"), ShellItemGet$(groups, main, i, "program")))
    +If Not ShellGroupsSave(groups, ShellConfigFile$("groups.ini")) Then MsgBox "Not saved."
    +ShellGroupsFree groups
    +
    Sub Form1_Load ()
    +    Dim copies As Long
    +    copies = ShellInstanceCount()
    +    Me.Left = 40 + (copies - 1) * 20
    +    Me.Top = 30 + (copies - 1) * 20
    +End Sub
    +

    A desktop's Timer keeps its running count current and starts the programs whose keys were pressed:

    +
    Sub Timer1_Timer ()
    +    Dim id As Long
    +    If ShellChangeCount() <> lastChange Then
    +        lastChange = ShellChangeCount()
    +        lblRunning.Caption = ShellRunningCount() & " programs running"
    +    End If
    +    id = HotkeyNext()
    +    Do While id <> 0
    +        StartItemWithKey id
    +        id = HotkeyNext()
    +    Loop
    +End Sub
    +

    Changing DVX's Settings

    +

    With system.bas in the project, a program changes what Control Panel changes, at once for the whole desktop. Add prefs.bas too: the settings are saved through a prefs.bas handle.

    +

    Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written.

    +

    Colors: the system colors are numbered from 0 to SysColorCount() - 1. SysColorLabel$(id) is the text to show for one ("Desktop", "Window Face" ...), SysColorName$(id) the name it is stored as, and SysColorId(colorName) the id of a name (-1 for none) -- to draw in the user's colors, for example SysGetColor(SysColorId("desktop")). SysGetColor and SysSetColor read and set a color as an RGB() value; each SysSetColor redraws the whole screen. SysResetColors goes back to DVX's own colors. SysLoadTheme(path) and SysSaveTheme(path) read and write a theme file (.THM, kept in CONFIG\THEMES); both return True when they worked.

    +

    Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.

    +

    Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower) and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).

    +

    The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl and Alt keys held now (vbShiftMask, vbCtrlMask and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.

    +

    Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).

    +

    System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.

    +

    Saving: SysColorsStore, SysWallpaperStore and SysMouseStore write the colors, the wallpaper and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.

    +
    Dim snap As Long
    +Dim oldW As Long, oldH As Long, oldBpp As Long
    +
    +Sub Form1_Load ()
    +    snap = PrefsCreate()
    +    SysColorsStore snap
    +    SysWallpaperStore snap
    +    SysMouseStore snap
    +    oldW = Screen.Width
    +    oldH = Screen.Height
    +    oldBpp = Screen.BitsPerPixel
    +End Sub
    +
    +Sub cmdOK_Click ()
    +    Dim ini As Long
    +    ini = PrefsLoad(PrefsDvxIni$())
    +    SysColorsStore ini
    +    SysWallpaperStore ini
    +    SysMouseStore ini
    +    PrefsSetVideoMode ini, Screen.Width, Screen.Height, Screen.BitsPerPixel
    +    If Not PrefsSave(ini) Then MsgBox "The settings could not be saved."
    +    PrefsClose ini
    +    Unload Me
    +End Sub
    +
    +Sub cmdCancel_Click ()
    +    SysColorsLoad snap
    +    SysWallpaperLoad snap
    +    SysMouseLoad snap
    +    If Screen.Width <> oldW Or Screen.Height <> oldH Or Screen.BitsPerPixel <> oldBpp Then
    +        SysSetVideoMode oldW, oldH, oldBpp
    +    End If
    +    Unload Me
    +End Sub

    Held Keys for Games

    With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:

    Sub Timer1_Timer ()
         If GameKeyDown(GAMEKEY_LEFT) Then shipX = shipX - 2
         If GameKeyDown(GAMEKEY_RIGHT) Then shipX = shipX + 2
     End Sub
    +

    GameKeyClear forgets the keys held now, so each reads as up until it is pressed again (after a pause screen, say); like GameKeyDown it only works while one of the program's windows is active. FrameMissed() counts the frames FRAMEWAIT skipped because the program was too slow to draw them in time, since it began waiting at its present rate (0 before the first FRAMEWAIT), so a game can draw less when it falls behind. GameWaitRetrace waits until the screen starts drawing its next picture from the top; a full-screen game that draws right after it does not tear.

    +
    Do
    +    MoveEverything
    +    If FrameMissed() > lastMissed Then
    +        lastMissed = FrameMissed()
    +        detail = detail - 1
    +    End If
    +    DrawEverything
    +    FRAMEWAIT 60
    +Loop Until quitting

    Joysticks

    With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy) and JoyButtons(joy) describe the joystick.

    Sub Timer1_Timer ()
    @@ -4262,6 +5136,17 @@ End Sub
    If JoyButton(1, 1) Then FireShot End Sub

    When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.

    +

    joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.

    +
    Sub cmdCenter_Click ()
    +    ' The stick rests in the middle: remember the readings
    +    centerX = JoyRawAxis(1, 1)
    +End Sub
    +
    +Sub cmdFinish_Click ()
    +    If Not JoySetCalibration(1, 1, lowX, centerX, highX) Then
    +        MsgBox "Move the stick all the way to each side."
    +    End If
    +End Sub

    See: STICK and STRIG

    Sound

    With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on) or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.

    @@ -4276,13 +5161,43 @@ Sub cmdFire_Click () End If End Sub

    WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND and PLAY work too, without sound.bas.

    +

    PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator) and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits" and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.

    +

    A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.

    +
    Dim Shared laser As Long
    +
    +Sub Form1_Load ()
    +    laser = SoundLoad("LASER.WAV")
    +End Sub
    +
    +Sub Fire (x As Integer)
    +    Dim v As Long
    +    v = SoundPlay(laser, 80, (x - 160) * 100 \ 160, False)
    +    SoundSetPitch v, 90 + Int(Rnd * 20)
    +End Sub
    +

    A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:

    +
    Sub Timer1_Timer ()
    +    Dim taken As Long
    +    taken = SoundStreamWrite(stream, pending)
    +    pending = Mid$(pending, taken + 1)
    +    If Len(pending) = 0 Then pending = MakeMoreSound()
    +End Sub
    +

    On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo" and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).

    +

    sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle) and SoundSettingVolume(handle) read the sound driver, MIDI choice and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).

    +
    Dim ini As Long
    +Dim i As Long
    +
    +ini = PrefsLoad(PrefsDvxIni$())
    +
    +For i = 0 To SoundEventCount() - 1
    +    lstEvents.AddItem SoundEventLabel$(i) & ": " & SoundEventFile$(ini, i)
    +Next i

    See: BEEP, SOUND and PLAY

    Music

    With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.

    MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.

    Dim Shared tune As Long
     
    -Sub Form_Load ()
    +Sub Form1_Load ()
         tune = MusicLoad("THEME.MOD")
     
         If tune = MUSIC_FAILED Then
    @@ -4294,7 +5209,7 @@ End Sub

    A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off) and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.

    Dim Shared keys As Long
     
    -Sub Form_Load ()
    +Sub Form1_Load ()
         keys = MusicMidiOpen()
         MusicProgram keys, 1, 73
     End Sub
    @@ -4308,11 +5223,13 @@ Sub tmrNoteOff_Timer ()
         MusicNoteOff keys, 1, 60
         tmrNoteOff.Enabled = False
     End Sub
    +

    MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi" or "live"), "where" ("stream", "mpu401", "fm" or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).

    +

    A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present" and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again) and MidiRelease gives the port back.

    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:

    Dim conn As Long
     
    -Sub Form_Load ()
    +Sub Form1_Load ()
         conn = TcpConnect("bbs.example.org", 23)
     
         If conn = 0 Then
    @@ -4327,22 +5244,68 @@ End Sub
      Function                              Description
       --------                              -----------
       HostLookup$(hostName)                 The address of a name, such as "192.0.2.10", or "" when it is not found.
    +  HostName$(address)                    The name of an address; the address itself when no name is known for it.
       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.
    +  TcpSend(handle, bytes)                Send text or any other bytes; 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.
    +  TcpShutdown(handle)                   Tell the other side nothing more will be sent; what it sends can still be read.
    +  TcpPeer$(handle)                      "address:port" of the other side ("" when not connected).
    +  TcpLocalPort(handle)                  The port a handle uses on this computer (the one TcpListen(0) picked).
    +  TcpSetNoDelay(handle, enable)         True: send small pieces at once instead of gathering them (typing, games).
    +  TcpSetKeepAlive(handle, enable)       True: check now and then that the other side of an idle connection is still there.
       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.
    -

    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.

    +

    The strings sent and received may hold any bytes, CHR$(0) too, so binary protocols work as well as text. Every connection a program opens is closed for it when the program ends.

    +

    A program can also edit the network settings the way the Control Panel's Network page does, and show the network's state. The settings are kept in DVX's settings file, so NetConfigLoad and NetConfigStore take a handle from prefs.bas (add prefs.bas to the project too). NetConfigLoad gives a settings handle (0 when none is free); read and change its fields by name, check them, store them, and restart the network only when they changed:

    +
    Dim prefs As Long
    +Dim cfg As Long
    +
    +Sub Form1_Load ()
    +    prefs = PrefsLoad(PrefsDvxIni$())
    +    cfg = NetConfigLoad(prefs)
    +    txtName.Text = NetConfigGet$(cfg, "hostname")
    +    lblState.Caption = NetStatus$("message")
    +End Sub
    +
    +Sub cmdOK_Click ()
    +    NetConfigSet cfg, "hostname", txtName.Text
    +
    +    If NetConfigProblem$(cfg) <> "" Then
    +        MsgBox NetConfigProblem$(cfg)
    +        Exit Sub
    +    End If
    +
    +    NetConfigStore cfg, prefs
    +    PrefsSave prefs
    +
    +    If NetConfigChanged(cfg) Then NetRestart
    +    NetConfigFree cfg
    +    PrefsClose prefs
    +End Sub
    +
      Function                              Description
    +  --------                              -----------
    +  NetConfigLoad(prefs)                  A settings handle holding the network settings of prefs (0 for the usual settings), or 0 when none is free.
    +  NetConfigGet$(cfg, field)             One setting as text: "dhcp" ("-1" when the address comes from the network automatically, "0" when it is fixed), "address", "netmask", "gateway", "dns1", "dns2", "hostname", "vector" (the packet driver's interrupt, 0 = find it).
    +  NetConfigSet(cfg, field, value)       Change one setting (same names; "dhcp" and "vector" take numbers). True when the field exists.
    +  NetConfigProblem$(cfg)                "" when the settings can be used, else why not in plain words.
    +  NetConfigChanged(cfg)                 True when the settings differ from what NetConfigLoad read.
    +  NetConfigStore(cfg, prefs)            Put the settings into prefs (then save it with PrefsSave). True when done.
    +  NetConfigFree cfg                     Free the settings handle.
    +  NetVectorCount() / NetVector(index)   The packet driver interrupts to choose from: index 0 is 0 (find it), then each one the settings accept.
    +  NetRestart()                          Stop the network and start it with the saved settings. True when it is running.
    +  NetStop                               Stop the network (every program's connections are closed) until NetRestart.
    +  NetStatus$(field)                     The network's state as text: "message" (in plain words), "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", "gateway", "dns1", "dns2", "hostname", "driver" (the network card's driver), "mac" (its hardware address), "vector", "sockets", "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes". "" for an unknown field.
    +

    Settings handles a program leaves open are freed for it when it 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.

    Dim msg As String
     msg = CommRecv$(link, 1)
    @@ -4350,6 +5313,245 @@ msg = CommRecv$(link, 1)
     If msg <> "" And Not CommRecvEncrypted(link, 1) Then
         Print "Unencrypted: " & msg
     End If
    +

    Connections and Terminals

    +

    With conn.bas in the project, a program talks to another computer the way the Terminal program does, whatever is in between: a serial port, a modem (it sends its setup command, dials, dials again when the line is busy and notices when the carrier is lost), the secure link to the DVX host proxy (encrypted, and authenticated with a passphrase), or Telnet over the network. ConnCreate makes a connection (a number); ConnSetSerial, ConnSetModem, ConnSetHost and ConnSetPassphrase set it up, and ConnOpen(conn, type) starts it. Opening only starts: dialing and the key exchange go on while the program runs, so call ConnPoll often from a Timer. ConnState says where it is (CONN_CLOSED, CONN_OPENING, CONN_OPEN) and ConnStatus$ what is happening or why it ended; ConnStatusSerial changes whenever ConnStatus$ does.

    +

    ConnAttach conn, "Term1" puts what arrives on the Terminal control Term1 (ConnPoll moves it there) and sends what is typed in it. ConnPoll then also appends what arrives to a capture file (ConnCapture), answers the other side's ENQ with an answerback (ConnSetAnswerback), and notices a ZMODEM sender starting a download: ConnZmodemRequest is True once, and the program answers with ConnReceiveFiles in the same Timer event. ConnSetEcho shows what is typed on the Terminal too. Without a Terminal, read with ConnRead$ and send with ConnWrite (any bytes, CHR$(0) too). ConnOpen returns CONN_OK or a CONN_ERR_ code; ConnErrorText$ puts the code in words. ConnSetXferWindow conn, bytes limits how much a ZMODEM sender may send before it waits for an answer (0 = no limit, up to XFER_WINDOW_MAX; -1, the default, picks by the line). ConnSendBreak holds a serial line in the break state for a moment; ConnClose hangs up and ConnFree forgets the connection.

    +
    Dim Shared conn As Long
    +Dim Shared download As Long
    +
    +Sub Form1_Load ()
    +    conn = ConnCreate()
    +    ConnSetSerial conn, 2, 115200, 8, "N", 1, CONN_FLOW_RTSCTS
    +    ConnAttach conn, "Term1"
    +    ConnOpen conn, CONN_SERIAL
    +End Sub
    +
    +Sub Timer1_Timer ()
    +    ConnPoll conn
    +    If ConnZmodemRequest(conn) Then download = ConnReceiveFiles(conn, XFER_ZMODEM, "DOWNLOAD", XFER_EXIST_RENAME, False)
    +End Sub
    +

    File Transfers

    +

    With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say) and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.

    +

    XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name) gives the 8.3 name a receive stores a sender's file name as: no folder or device name, only characters every disk accepts.

    +

    Lists You Draw Yourself

    +

    With listhelp.bas in the project, a list or icon view a program draws itself -- in a UserControl or a PictureBox -- follows the same rules as ListBox and ListView. ListSelCreate(multi, count) makes a selection for count items (several at a time when multi is True) and returns a handle; it remembers which items are selected and which one is current (the one the keyboard moves). Items are numbered from 0. Give it the clicks and keys, and ask it what to draw:

    +
      Function                                            Description
    +  --------                                            -----------
    +  ListSelClick sel, index, shift                      A click on an item, with the MouseDown event's Shift: alone it selects only that item, with Ctrl it turns the item on or off, with Shift it selects the items from the last click up to it.
    +  ListSelKey(sel, keyCode, shift)                     Ctrl+A selects every item (returns LISTSEL_KEY_ALL), Space turns the current item on or off (LISTSEL_KEY_TOGGLE); LISTSEL_KEY_NONE for other keys.
    +  ListSelNavigate(sel, keyCode, shift, columns, pageRows)  The arrow keys, Home, End, PgUp and PgDn, in a list (columns 1) or a grid with columns items per row, pageRows rows a page; with Shift they select from the last click. The new current item, or -1 when nothing moved.
    +  ListSelMove sel, index, shift                       Make an item current as the keyboard does (after type-ahead).
    +  ListSelCursor(sel), ListSelSetCursor sel, index     The current item (-1: none); make one item current and the only one selected.
    +  ListSelIsSelected(sel, index), ListSelSelectedCount(sel)  Whether an item is selected; how many are.
    +  ListSelSetItem sel, index, selected; ListSelSetAll sel, selected  Select or clear one item, or all.
    +  ListSelInsert sel, index; ListSelRemove sel, index  An item was added or removed: the others keep their selection.
    +  ListSelReorder sel, fromIndex, dest                 An item was dragged to dest: its selection goes with it.
    +  ListReorderDest(fromIndex, toSlot, count)           Where an item dragged from fromIndex lands when dropped in front of slot toSlot (count: after the last item); -1 when the drop changes nothing.
    +  ListSelReset sel, count; ListSelCount(sel); ListSelFree sel  The list now holds count new items; how many it holds; free the handle.
    +  ListNavigateIndex(keyCode, current, count, pageSize), ListGridNavigate(keyCode, current, count, columns, pageRows)  The same keys without a selection handle.
    +  ListTypeAhead(ch, items, current)                   The next item after current whose text starts with the letter ch, any case (going round to the first); items holds the texts separated by CHR$(10). -1 when none does.
    +  ListTypeAheadMatch(ch, text)                        True when text starts with the letter ch.
    +
    Sub UserControl_KeyDown (KeyCode As Integer, Shift As Integer)
    +    If ListSelKey(sel, KeyCode, Shift) <> LISTSEL_KEY_NONE Then UserControl.Refresh
    +    If ListSelNavigate(sel, KeyCode, Shift, columns, rowsShown) >= 0 Then UserControl.Refresh
    +End Sub
    +
    +Sub UserControl_KeyPress (KeyAscii As Integer)
    +    Dim i As Long
    +    i = ListTypeAhead(Chr$(KeyAscii), itemTexts, ListSelCursor(sel))
    +    If i >= 0 Then
    +        ListSelMove sel, i, 0
    +        UserControl.Refresh
    +    End If
    +End Sub
    +

    Selections a program leaves open are freed when it ends.

    +

    An icon view drawn with listhelp.bas looks and behaves like Program Manager's: the same cells for large icons, small icons and the list, the same frame and scroll bar, a large icon's name on two lines, and the same rules for the keys, dragging and dropping. The grid functions take the view mode (LISTICON_VIEW_LARGE, LISTICON_VIEW_SMALL or LISTICON_VIEW_LIST), the size of a character (TextWidth("W") and TextHeight("W")), the size of the whole view, how many items it shows, and rowsTaken: LISTICON_BY_ORDER when the items fill the grid in order, else how many rows items placed in cells of their own take. Draw the view's frame with DrawBevel (LISTICON_FRAME wide; with the face -1 after the items, so a row cut off at the bottom stops at the frame) and its scroll bar with DrawScrollBar as a control's bar (its control argument True); the grid leaves room for it.

    +
      Function                                            Description
    +  --------                                            -----------
    +  ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken)  One value of the grid: a cell's size (LISTICON_CELL_W, LISTICON_CELL_H), where its picture goes and how big it is (LISTICON_PICTURE_X, LISTICON_PICTURE_Y, LISTICON_PICTURE_SIZE), where its name goes and how many characters it shows (LISTICON_TEXT_X, LISTICON_TEXT_Y, LISTICON_NAME_CHARS; a large icon's two lines are each centred in the cell), the list's detail column (LISTICON_DETAIL_X, 0 in the other views); the inside of the frame less the scroll bar (LISTICON_INNER_X, LISTICON_INNER_Y, LISTICON_INNER_W, LISTICON_INNER_H), the columns, the rows shown whole and in all (LISTICON_COLUMNS, LISTICON_ROWS_SHOWN, LISTICON_ROWS), the largest top row (LISTICON_TOP_ROW_MAX), True when the scroll bar is needed (LISTICON_SCROLLBAR), where the text of an empty view goes (LISTICON_EMPTY_X, LISTICON_EMPTY_Y), the frame's width (LISTICON_FRAME) and the rows a wheel notch scrolls (LISTICON_WHEEL_ROWS).
    +  ListIconCellAt(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  The cell under x, y with the view scrolled to topRow, numbered row * columns + column; -1 outside the grid.
    +  ListIconLabel$(name, maxChars, lineNo)              Line 1 or 2 of a large icon's name, maxChars characters a line (LISTICON_NAME_CHARS): the first line ends at the last blank that fits, else it is cut; the second is the rest, cut. "" for a line the name does not need.
    +  ListIconNavigate(keyCode, current, cells, columns, pageRows)  The item the arrow keys, Home, End, PgUp or PgDn move to when the items stand in cells of their own, gaps allowed: cells holds each item's column and row, all separated by commas ("0,0,2,0,1,1"). -1 for another key or when no item is that way.
    +  ListIconScrollPress(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  A left press on the scroll bar: the top row after it (an arrow moves a row, the trough a page, the thumb nothing); -1 when x, y is not on the scroll bar.
    +  ListIconScrollGrab(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  How far below the top of the thumb a press is; -1 when it is not on the thumb.
    +  ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y)  The top row with the thumb, held grab pixels below its top, dragged to y. While the button is down the control gets the mouse even outside it.
    +  ListIconDragStarted(dx, dy)                         True when a press has moved far enough to start a drag.
    +  ListIconDragScroll(mode, cw, ch, w, h, count, rowsTaken, topRow, y)  While an item is dragged: the top row after the view scrolls a row because the mouse is above or below the grid.
    +  ListIconDropCell(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  The cell a dragged item would be dropped on (above or below the grid: the nearest row); -1 for none.
    +  ListIconDropDest(fromIndex, cell, count)            Items in order: the place an item dragged from fromIndex takes when dropped on a cell -- that cell's, the last place for a cell past the end; -1 when the drop changes nothing.
    +
    Sub UserControl_Paint ()
    +    Dim cw As Long, ch As Long
    +    cw = UserControl.TextWidth("W")
    +    ch = UserControl.TextHeight("W")
    +    UserControl.DrawBevel 0, 0, UserControl.Width, UserControl.Height, True, vbWhite, ListIconMetric(LISTICON_VIEW_LARGE, LISTICON_FRAME, cw, ch, UserControl.Width, UserControl.Height, itemCount, LISTICON_BY_ORDER)
    +    ' ... each item in its cell, its name in two lines:
    +    UserControl.Print ListIconLabel$(itemName, ListIconMetric(LISTICON_VIEW_LARGE, LISTICON_NAME_CHARS, cw, ch, UserControl.Width, UserControl.Height, itemCount, LISTICON_BY_ORDER), 1);
    +End Sub
    +

    Security

    +

    With security.bas in the project, a program has the building blocks of DVX's encrypted serial link for its own protocols and files. All of them take and give strings that may hold any bytes, CHR$(0) too.

    +
      Function                                            Description
    +  --------                                            -----------
    +  SecRandom$(count)                                   count random bytes, good for keys and salts (RND is not). SecAddEntropy bytes stirs something unpredictable (key timings, mouse positions) into the generator.
    +  SecDhCreate()                                       A key exchange with fresh keys (a few seconds on a slow computer); a handle, or 0.
    +  SecDhPublicKey$(dh)                                 This side's public key (SEC_DH_KEY_SIZE bytes), to send to the other side.
    +  SecDhComputeSecret(dh, otherPublicKey)              Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM for a key that is not acceptable.
    +  SecDhSecret$(dh)                                    The secret both sides now share, which nobody listening can work out. Make keys from it with SecHkdf$; never use it as a key itself. SecDhFree dh frees the handle.
    +  SecCipherCreate(key)                                A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length.
    +  SecCipherCrypt$(cipher, bytes)                      Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it.
    +  SecSha256$(bytes)                                   The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle.
    +  SecHmac$(key, bytes)                                The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd and SecHmacEnd$ for pieces.
    +  SecHkdf$(salt, secret, info, length)                length bytes (up to SEC_HKDF_MAX_OUT) of keys made from secret; a different info gives different keys.
    +  SecEqual(a, b)                                      True when a and b hold the same bytes, compared in the same time wherever they differ (for signature checks).
    +
    ' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key
    +Dim dh As Long, cipher As Long, key As String
    +dh = SecDhCreate()
    +TcpSend conn, SecDhPublicKey$(dh)
    +' ... receive the other side's key into theirs ...
    +If SecDhComputeSecret(dh, theirs) = SEC_OK Then
    +    key = SecHkdf$("", SecDhSecret$(dh), "chat cipher", SEC_XTEA_KEY_SIZE)
    +    cipher = SecCipherCreate(key)
    +End If
    +SecDhFree dh
    +

    Handles a program leaves open are freed, and their keys wiped, when it ends.

    +

    Settings Files

    +

    With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.

    +
    Dim ini As Long
    +ini = PrefsLoad(ShellConfigFile$("MYAPP.INI"))
    +userName = PrefsGet$(ini, "User", "Name", "Unknown")
    +PrefsSetInt ini, "Window", "Left", Me.Left
    +If Not PrefsSave(ini) Then MsgBox "The settings could not be saved."
    +PrefsClose ini
    +

    PrefsDvxIni$() is the path of DVX's own settings file, DVX.INI, which Control Panel changes; other libraries (system.bas, joystick.bas, net.bas, print.bas and sound.bas) read and store their part of it through a prefs.bas handle. PrefsSetVideoMode handle, wide, high, bpp stores the screen mode DVX starts in, and PrefsVideoWidth(handle), PrefsVideoHeight(handle) and PrefsVideoBpp(handle) read it (DVX's own start-up mode, SYS_VIDEO_DEFAULT_* in system.bas, for a value that is not there). PrefsKeyName$(handle, section, index) is the name of key number index of a section (from 0, in the order of the file; "" past the last key), to go through every key of a section.

    +

    Some settings are stored by name, from a fixed list of choices: the mouse acceleration (PREFS_CHOICE_ACCEL), the wallpaper mode (PREFS_CHOICE_WALLPAPER) and the scroll wheel direction (PREFS_CHOICE_WHEEL). For each list, PrefsChoiceCount(table) says how many choices it has (numbered from 0), PrefsChoiceLabel$(table, i) is the text to show, PrefsChoiceName$(table, i) the name stored in the file and PrefsChoiceValue(table, i) the value it stands for (what SysSetMouse takes). PrefsChoiceIndex(table, choiceName) and PrefsChoiceIndexOfValue(table, value) find a choice (-1 when there is none), and PrefsGetChoice(handle, section, key, table) reads one from a file, giving DVX's default choice when the key is missing or unknown. PrefsChoiceDefault(table) is that default choice, what a Defaults button selects. For a list of your own, PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) does the same with the names separated by "|" ("large|small|list" stores choice 1 as "small"), giving defaultName's choice for a missing or unknown key.

    +
    For i = 0 To PrefsChoiceCount(PREFS_CHOICE_ACCEL) - 1
    +    cboAccel.AddItem PrefsChoiceLabel$(PREFS_CHOICE_ACCEL, i)
    +Next i
    +cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel())
    +

    Help Files

    +

    With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.

    +

    With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none) and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$ and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.

    +

    To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$ and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.

    +
    Dim hlp As Long
    +Dim history(63) As Long, histPos As Long
    +
    +Sub Form1_Load ()
    +    hlp = HlpOpen(App.Path + "\MYAPP.HLP")
    +    For i = 0 To HlpTocCount(hlp) - 1
    +        If HlpTocDepth(hlp, i) = 0 Then
    +            Toc.AddItem HlpTocTitle$(hlp, i)
    +        Else
    +            Toc.AddChildItem lastTop, HlpTocTitle$(hlp, i)
    +        End If
    +        If HlpTocDepth(hlp, i) = 0 Then lastTop = Toc.ItemCount() - 1
    +        Toc.SetItemTag Toc.ItemCount() - 1, Str$(HlpTocTopic(hlp, i))
    +    Next i
    +    GoTopic HlpStartTopic(hlp, "")
    +End Sub
    +
    +Sub GoTopic (ByVal topic As Long)
    +    histPos = histPos + 1
    +    history(histPos) = topic
    +    HelpPane1.ShowTopic hlp, topic
    +    Me.Caption = HlpTopicTitle$(hlp, topic) + " - My Help"
    +End Sub
    +
    +Sub HelpPane1_LinkClick (Topic As String)
    +    GoTopic HlpFindTopic(hlp, Topic)
    +End Sub
    +
    +Sub Toc_Change ()
    +    If Val(Toc.GetItemTag(Toc.ListIndex)) >= 0 Then GoTopic Val(Toc.GetItemTag(Toc.ListIndex))
    +End Sub
    +

    A help viewer written in BASIC can also take the place of DVX's own: installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP with a NewArgs event procedure, it is handed "helpfile topic" when F1 is pressed while it is open (see New Arguments in the Form topic).

    +

    Sizes in Characters

    +

    Control sizes (Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight) are pixels. With layout.bas in the project, SizeChars(n) is a size of n characters of the system font and SizePercent(n) one of n percent of the container (0 to 100); give them to a size property and the layout works out the pixels each time it arranges the form, so a box for an address keeps its width whatever the font. Reading the property back gives the pixels it stands for at that moment.

    +
    txtAddress.MinWidth = SizeChars(16)
    +lstFiles.MaxHeight = SizePercent(50)
    +

    Compiling and Debugging Programs

    +

    With compiler.bas in the project, a program compiles BASIC programs -- what a development tool written in BASIC needs. A source set holds the files of one program: CompSourceFromProject(src, dbpPath) reads a whole project (.dbp) from disk, or CompSourceAdd(src, path, text, kind) adds files one at a time, in the order a project compiles: modules, then class modules, then UserControls, then forms (COMP_KIND_AUTO tells them apart by the file name; a form's whole .frm text is added, and the code after its layout is compiled; a UserControl's whole .ctl text, its header and its code). CompCompile(src, optionExplicit, validate) gives a compiled module, or 0 with CompError$() saying what is wrong and CompErrorFile$() / CompErrorFileLine() where. With validate True, Label1.Captoin and other misspelled control members, and controls of an unknown type, are found while compiling instead of when the line runs (the set's own UserControls are known types).

    +

    Line numbers of compiled programs (errors, breakpoints, the line a program is paused on) count through all the files of the source set. CompMapFile$(src, line) and CompMapLine(src, line) give the file and the line in it; CompConcatLine(src, path, fileLine) goes the other way. CompSourceFiles$ lists the files in compile order. CompBuildApp(module, src, outPath, release) builds an .app the way the IDE's Make Executable does, with the project's name, icon and help file, its forms and its UserControls. For a control project (its Type setting is "Control") it builds a control file instead (outPath NAME.wgt), the way the IDE's Make Control does: the project's UserControls with their toolbox pictures and names, there for every program once the file is copied into DVX's WIDGETS folder (DVX loads it when it starts). CompProcList$(text) lists the SUB and FUNCTION blocks of source text with their first and last lines (also for code that does not compile yet), CompIsIdent checks a name and CompKeyword$ lists the language's keywords, CompKeywordClass the kind of each (COMP_KW_OTHER, COMP_KW_TYPE or COMP_KW_LITERAL) and CompKeywordReserved whether a program may also use it as a name of its own.

    +

    Project files: ProjOpen(dbpPath) opens a project file (.dbp) and ProjNew(name, folder) starts a new one; ProjSave writes it and ProjClose frees it. ProjFileCount, ProjFile$ (the name as the project lists it), ProjFilePath$ (the full path), ProjFileKind (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or COMP_KIND_CONTROL) and ProjFileFind list its files; ProjAddFile, ProjRemoveFile and ProjRenameFile (on disk and in the project together, saved at once) change the list. ProjGet$ and ProjSet read and set its settings by their names in the file: Name, Type ("Control" for a control project, empty for a program), Author, Publisher, Version, Copyright, Description, Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit and MultiInstance (the last two "true" or "false"). This is the project model the DVX BASIC IDE uses.

    +

    A UserControl's header (the Begin UserControl block at the top of its .ctl file) is read and changed the way the IDE's Properties window does it, the rest of the text left as it is: ProjCtlKeys$() lists the keys in the window's order with the kind of value each takes (Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent; the kinds Name, Integer, Boolean, Color and Text), ProjCtlHeader$(text, key) reads one (its default when the header leaves it out), ProjCtlSetHeader$(text, key, value) gives the text with one set (an empty value removes it; Name renames the control) and ProjCtlNew$(name) the text of a new UserControl, as File > New UserControl makes it.

    +

    With debug.bas too, a program runs a compiled module as a child program inside itself: RunCreate(module, src, appDir, args) prepares it, RunStart(run, startupForm, breakOnStart) loads its forms, and RunPoll(run, ms), called from a Timer, runs it for at most ms milliseconds at a time and returns what it is doing: RUN_RUNNING, RUN_EVENTS (waiting for events on its forms), RUN_PAUSED, RUN_ENDED or RUN_ERROR (RunError$ and RunErrorLine tell why and where). RunOutput$ collects what it PRINTs; INPUT asks with an input box. RunStop ends it and RunFree forgets it.

    +

    DbgSetBreakpoints run, "12, 40" sets breakpoints (line numbers of the source set). While the program is paused, DbgLine and DbgProc$ say where, DbgLocals$ and DbgCallStack$ give the variables and the calls as rows for a ListView's SetRows, DbgEval$(run, expr) shows a variable or expression as the IDE's Watch window does, DbgExec$(run, line) runs an Immediate-window line (x = 5 changes a variable, anything else is printed) and DbgSetVar changes a variable; DbgStepInto, DbgStepOver, DbgStepOut, DbgRunToLine and DbgContinue go on, DbgPause stops it at the next line. The program's form captions show [break] while it is paused.

    +

    A program paused in its module-level code waits until you step or continue it. One paused inside one of its event handlers -- or in its forms' Load code, which runs inside RunStart -- waits right there while your Timer and menus keep working; RunPoll called meanwhile returns at once, and RunStart returns only after such a pause.

    +
    ' compiler.bas and debug.bas in the project; tmrRun is a Timer
    +Dim Shared src As Long, mdl As Long, kid As Long
    +
    +Sub StartDebugging
    +    src = CompSourceCreate()
    +    If Not CompSourceFromProject(src, "C:\PROJ\HELLO.DBP") Then MsgBox CompError$(): Exit Sub
    +    mdl = CompCompile(src, True, True)
    +    If mdl = 0 Then MsgBox CompErrorFile$() & " line" & Str$(CompErrorFileLine()) & ": " & CompError$(): Exit Sub
    +    kid = RunCreate(mdl, src, "", "")
    +    DbgSetBreakpoints kid, Str$(CompConcatLine(src, "C:\PROJ\MAIN.BAS", 12))
    +    If RunStart(kid, "", False) Then tmrRun.Enabled = True
    +End Sub
    +
    +Sub tmrRun_Timer
    +    Select Case RunPoll(kid, 40)
    +        Case RUN_PAUSED
    +            lstLocals.SetRows DbgLocals$(kid)
    +            lblWhere.Caption = CompMapFile$(src, DbgLine(kid)) & ":" & Str$(CompMapLine(src, DbgLine(kid)))
    +        Case RUN_ENDED, RUN_ERROR
    +            tmrRun.Enabled = False
    +            txtOutput.Text = txtOutput.Text & RunOutput$(kid) & RunError$(kid)
    +    End Select
    +End Sub
    +

    Form Designers and Code Editors

    +

    A form designer or a code editor written in BASIC uses the same pieces as the DVX BASIC IDE. designer.bas describes the control types, which CtlTypeName$(0 to CtlTypeCount - 1) gives in alphabetical order: the toolbox lists the types CtlTypeFlags marks CTL_FLAG_DESIGNABLE (with CtlTypeTooltip$ and the icon CtlTypeIconFile writes), new controls are named CtlTypeNamePrefix$ plus a number, and the properties window lists CtlPropName$ for 0 to CtlPropCount - 1 with CtlPropType (CTL_PROP_STRING ... CTL_PROP_ENUM, plus CTL_PROP_READONLY) and CtlPropEnum$ for an enumerated property's choices. "Form" and "Menu" stand for the form itself and a menu item. The event list is CtlEventList$, and CtlEventParams$ writes an event procedure's parameter list; CtlTypeEventParams$(typeName, eventName) does it for one type, whose events may have parameters of their own (a UserControl's events), and "UserControl" stands for the object in a UserControl's own code (UserControl_MouseDown has Button, Shift, X and Y). For a project's files, FrmFormName$ and FrmControlNames$ read a .frm file's form and control names (the Object list), FrmCode$ its code, and ClsClassName$ a class module's name.

    +

    The form being designed is a real form: CreateControl and RemoveControl add and remove controls, CallByName reads and sets a property by its name, Me.Controls lists the controls, Container (Set ctl.Container = Frame1) and MoveBefore move them, and LoadFormText shows a .frm file. Its DesignMode (see the Form control) stops the controls from working and reports the mouse and keys in DesignMouse and DesignKey, with the selection handles drawn for DesignSelection.

    +

    A project's own UserControls go on the toolbox too, as in the IDE. Compile the project -- its forms may be left out -- and give the module and the source set to CompDesignControls(module, src, appDir) from compiler.bas: the UserControls are then control types of this program alone, listed by CtlTypeName$ with their Description as the tooltip and their ToolboxIcon as the icon, with their properties, methods and events, and known to CreateControl and LoadFormText. On the forms they run their code -- not the project's module-level code -- with UserControl.UserMode False on a form in DesignMode; App.Path is appDir, or the project's folder for "". When a control's code calls UserControl.PropertyChanged, the form raises DesignPropertyChanged (CtrlName, PropertyName), so the properties window can read them again. Call CompDesignControls again after a UserControl changed (the ones before are let go first) and CompDesignRelease when the project closes; the end of the program lets them go too. The module stays in use while they are there, even after CompModuleFree, and must be one this program compiled.

    +
    ' compiler.bas and designer.bas in the project; Canvas is the form
    +' being designed
    +Sub LoadProjectControls(dbpPath As String)
    +    Dim src As Long
    +    Dim mdl As Long
    +    Dim made As Long
    +    src = CompSourceCreate()
    +    If CompSourceFromProject(src, dbpPath) Then mdl = CompCompile(src, True, False)
    +    If mdl = 0 Then
    +        MsgBox CompError$()
    +    Else
    +        made = CompDesignControls(mdl, src, "")
    +        If CompError$() <> "" Then MsgBox CompError$()
    +        CompModuleFree mdl
    +    End If
    +    CompSourceFree src
    +End Sub
    +
    +Sub Canvas_DesignPropertyChanged(CtrlName As String, PropertyName As String)
    +    If CtrlName = Canvas.DesignSelection Then RefreshPropertyList
    +End Sub
    +

    A .frm file can also be worked on as data, the way the DVX BASIC IDE does it, without showing it. FrmOpen(text$), FrmOpenFile(path$) or FrmNew(name$) gives a handle to a form model (0 when the text is not a form); FrmText$ writes it back as .frm text in the form the IDE saves and FrmSave writes it to a file; FrmFree frees it (a program's open handles are freed when it ends). FrmGet$ and FrmSet read and change the form's properties (Caption, Width, AutoSize, Layout, Icon, Tag ...; Name renames the form), FrmGetCode$ and FrmSetCode its code. The controls are numbered from 0 in file order, each one followed by the controls inside it: FrmCtlCount, FrmCtlFind, FrmCtlName$, FrmCtlType$, FrmCtlIndex (its control array index, -1 when it is not in an array) and FrmCtlParent (the container, -1 for the form) describe them; FrmCtlGet$ and FrmCtlSet read and set any property as text (Name renames the control and its whole control array, the controls inside it follow; Index makes it an array member; an OptionButton set to True clears the others in its container), FrmCtlRemoveProp takes a set one out, and FrmCtlPropCount / FrmCtlPropName$ list the ones that were set. FrmCtlAdd adds a control (its name "" gives the next free one, Command1, Label2 ...) as the last control of a container (-1: the form), FrmCtlRemove removes one with what is inside it, FrmCtlMove puts one before another of the same container and FrmCtlReparent into another container. FrmCtlText$ gives a control with what is inside it as .frm text, what a designer puts on the Clipboard to copy it, and FrmCtlPaste puts such text into a container: a name that is already taken is replaced by a free one. The menu items are numbered from 0 in menu order with a Level (0 for a menu on the menu bar or a popup menu, 1 for its items, 2 and more for submenus): FrmMenuCount, FrmMenuGet$ / FrmMenuSet (Name, Caption, Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, Level), FrmMenuAdd, FrmMenuRemove (with the items under it), FrmMenuMove (past the previous or next item of its level) and FrmMenuIndent (one level in or out, with the items under it). Only what was set is written: a property a control has by default is left out of the file.

    +

    FormText$(formName$) writes a loaded form as it is now as .frm text -- one shown with LoadFormText, made with CreateForm and CreateControl, or one of the program's own: its properties, its controls in their containers with each property that differs from a new control of that type, and its menus (a form loaded this way has no code). A designer that keeps its form live saves it with FormText$, and FrmOpen(FormText$(...)) reads it as a model.

    +

    compiler.bas reads source text as the compiler does, for a code editor: CompTokens$(source) lists its tokens (line, column, length, kind and text, the kind one of Keyword, Identifier, String, Number, Date, Symbol, Comment and Error); CompInCode(source, position) tells whether a character is code rather than inside a string or a comment; CompRenameInCode$(source, oldName, newName, codeOnly) renames a form, control or menu item where code refers to it (Command1.Caption, Command1_Click), leaving strings and comments alone with codeOnly; CompWithEvents$ lists the WithEvents variables a file declares and CompClassEvents$ the Events of a class module, for the Object and Event lists; CompHelpTopic$(word) and CompHelpCtrlTopic$(typeName) are the help topics F1 opens (HelpViewTopic on dvxbasic.hlp beside the IDE).

    +

    A code editor is a TextArea: SetSyntaxMode "bas" colours BASIC as the IDE does (SetSyntaxColor changes the colours), SetLineMarker marks breakpoints and the current line, GutterClick reports clicks on the line numbers, SelStart / SelLength / SelText and Cut, Copy, Paste, SelectAll and Undo give the Edit menu, FindNext / ReplaceSelection / ReplaceAll find and replace, and LineCount / GetLine / CursorLine / CursorCol read the text (see TextArea).

    +
    ' designer.bas in the project: put a copy of the first control of a
    +' .frm file into a Frame of the same form and save it
    +Sub CopyIntoFrame(path As String)
    +    Dim h As Long
    +    Dim box As Long
    +    h = FrmOpenFile(path)
    +    If h = 0 Then Exit Sub
    +    box = FrmCtlAdd(h, "Frame", "", -1)
    +    If FrmCtlSet(h, box, "Caption", "Copies") And FrmCtlPaste(h, FrmCtlText$(h, 0), box) >= 0 Then
    +        If Not FrmSave(h, path) Then MsgBox "Could not save " & path
    +    End If
    +    FrmFree h
    +End Sub
    +
    ' designer.bas in the project: fill a properties list for a control
    +Sub ShowProperties(ctl As Object, typeName As String)
    +    Dim i As Integer
    +    Dim v As String
    +    lstProps.Clear
    +    For i = 0 To CtlPropCount(typeName) - 1
    +        If (CtlPropType(typeName, i) And CTL_PROP_TYPEMASK) <> CTL_PROP_OBJECT Then
    +            v = CallByName(ctl, CtlPropName$(typeName, i), vbGet)
    +            lstProps.AddItem CtlPropName$(typeName, i) & " = " & v
    +        End If
    +    Next
    +End Sub

    See also: DECLARE LIBRARY

    @@ -4376,7 +5578,11 @@ End If 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. DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below. - DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. + DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. + Parent Form R The form the control is on (Text1.Parent.Caption). + Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file. + MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11) or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error. + Container Object R/W The container control the control is in (a Frame, VBox, TabPage ...), or its form. Set Text1.Container = Frame1 moves the control into Frame1, after the controls there; Set Text1.Container = Me moves it onto the form itself. The new container must be a container control on the same form, not the control itself or one inside it.

    Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767.

    Default Properties

    As in Visual Basic, a control's name used where a value is needed means its default property: Text1 = "hi" sets Text1.Text, Print Label1 prints Label1.Caption, and If Check1 Then reads Check1.Value. Where an object is expected -- SET, Is, an object parameter, Collection.Add, TypeOf -- the name still means the control itself.

    @@ -4390,7 +5596,7 @@ End If Timer Enabled

    Other controls, forms and menu items have no default property: using one as a value raises error 13.

    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, whether it is loaded from a .frm file or created at runtime with CreateControl (whose handlers SetEvent names).

      Event       Parameters                                    Description
       ---------   -------------------------------------------   -------------------------------------------
       Click       (none)                                        Fires when the control is clicked.
    @@ -4399,7 +5605,7 @@ End If
    GotFocus (none) Fires when the control receives keyboard focus. LostFocus (none) Fires when the control loses keyboard focus. KeyPress KeyAscii As Integer Fires when a printable key is pressed. KeyAscii is the ASCII code. - KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down. KeyCode is the scan code; Shift indicates modifier keys. + KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down, after the control has handled it. KeyCode is the key (vbKeyDelete, vbKeyReturn, ... or a character's code; Ctrl+A to Ctrl+Z are 1 to 26); Shift holds vbShiftMask, vbCtrlMask and vbAltMask bits (see Key Code Constants in the language reference). KeyUp KeyCode As Integer, Shift As Integer Fires when a key is released. MouseDown Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control. MouseUp Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control. @@ -4412,7 +5618,9 @@ End If -------- ---------- ------------------------------------------- SetFocus (none) Gives keyboard focus to this control. Refresh (none) Forces the control to repaint. - Drag [Action] Starts or ends dragging this control: vbBeginDrag (1, the default) starts, vbEndDrag (2) drops it where the mouse is, 0 cancels the drag without a drop. + Drag [Action] Starts or ends dragging this control: vbBeginDrag (1, the default) starts, vbEndDrag (2) drops it where the mouse is, 0 cancels the drag without a drop. + MoveBefore [Control] Moves the control just before another control (into that control's container when it is in another one), or with none to the end of its own container. The layout places controls in this order, and Controls lists them in it. + ScrollToControl Control Of a ScrollPane: scrolls so that a control inside it (the control, or its name as a string) is in view, also one that cannot take the focus (a Label). On another control, or with a control that is not inside it, it is an error.

    Drag and Drop

    Any control can be dragged onto another control or onto a form, as in Visual Basic. While a control is dragged, a dotted outline of it (or its DragIcon picture) follows the mouse, and the controls and forms it passes over get DragOver events. Releasing the mouse button drops it: the control or form under the mouse gets a DragDrop event, with the dragged control as Source. Pressing Esc cancels the drag.

    The mouse pointer shows where a drop can land: over a control or form that has a DragDrop event procedure it is the usual arrow; over one without, and outside the program's forms, it is the "no drop" sign (a circle with a line through it). The outline or DragIcon picture still follows the mouse either way.

    @@ -4448,7 +5656,8 @@ End Sub PSet [Step](x, y) [, color] Draws a point (DrawWidth wide). Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF] Draws a line, a box (B) or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0. Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]] Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles). - Point(x, y) The colour of a pixel, -1 outside the picture. + DrawFocusRect x, y, width, height [, color] Draws the dotted rectangle DVX shows around the control that has the keyboard focus (every other pixel of its edge), for things a program draws itself, such as the focused link of a help topic. A negative width or height counts from the other edge. + Point(x, y) The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly. Print items Writes text at CurrentX, CurrentY in ForeColor, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,. PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]] Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations). Scale [(x1, y1)-(x2, y2)] Gives the drawing coordinates of its own: (x1, y1) is the top-left corner and (x2, y2) the bottom-right one (y2 below y1 makes y go up). Scale alone goes back to pixels. @@ -4456,6 +5665,35 @@ End Sub ScaleY(height [, fromScale [, toScale]]) A height in fromScale converted to toScale. TextWidth(text) Width of the text in the scale's units. TextHeight(text) Height of a line of text in the scale's units. +

    Drawing the DVX Look

    +

    These methods draw the parts DVX's own controls are made of, in the colours of the current theme, for controls a program draws itself (a PictureBox that acts as a list or a toolbar). Coordinates and lengths are in the scale's units; colours are RGB values.

    +
      Method                                                          Description
    +  ------                                                          -----------
    +  DrawBevel x, y, width, height [, sunken [, face [, border]]]    A raised box like a window or button edge, or with sunken True a sunken one like a text box's. face is the fill colour (the theme's face colour when left out; -1 leaves the inside as it is, to frame what is already drawn); border is the edge width in pixels (2).
    +  DrawButtonFace x, y, width, height [, pressed [, face]]         A button face, up or with pressed True pushed down.
    +  DrawArrow x, y, size, direction [, color]                       A solid triangle centred on x, y, size pixel rows tall, pointing dvxArrowUp (0), dvxArrowDown (1), dvxArrowLeft (2) or dvxArrowRight (3), in ForeColor unless a colour is given.
    +  DrawScrollBar x, y, length, position, visible, total [, horizontal [, control]]   A scroll bar with its arrows and its thumb, for a list of total rows of which visible are shown from row position, with its top-left corner at x, y. It stands upright (16 pixels wide, length tall, arrows at the top and bottom) unless horizontal is True: then it lies across (length wide, 16 pixels tall, arrows at the left and right ends). With control True it is the narrower bar of a control such as ListBox (14 pixels).
    +  DrawScrollArrow x, y, size, direction                           One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft or dvxArrowRight, in the theme's scroll bar colours.
    +  ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]])   The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing.
    +  PrintAccel text [, enabled]                                     Prints a caption at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text.
    +  TextWidthAccel(text)                                            Width of a caption without its & signs, in the scale's units.
    +  AccelKey(text)                                                  The access key of a caption in lower case ("f" for "&File"), "" when it has none.
    +
    Sub Picture1_Paint()
    +    Picture1.DrawBevel 0, 0, Picture1.ScaleWidth, Picture1.ScaleHeight, True, vbWhite
    +    Picture1.DrawButtonFace 4, 4, 60, 20, pressed
    +    Picture1.CurrentX = 10: Picture1.CurrentY = 8
    +    Picture1.PrintAccel "&Go", Picture1.Enabled
    +    Picture1.DrawScrollBar Picture1.ScaleWidth - 18, 2, Picture1.ScaleHeight - 22, topRow, 8, 40
    +    Picture1.DrawScrollBar 2, Picture1.ScaleHeight - 18, Picture1.ScaleWidth - 22, leftCol, 10, 60, True
    +End Sub
    +
    +Sub Picture1_MouseDown(Button As Integer, X As Integer, Y As Integer)
    +    ' A click on the bar along the bottom scrolls sideways
    +    If Y >= Picture1.ScaleHeight - 18 And X < Picture1.ScaleWidth - 20 Then
    +        leftCol = Picture1.ScrollBarClick(Picture1.ScaleWidth - 22, X - 2, leftCol, 10, 60, True)
    +        Picture1.Refresh
    +    End If
    +End Sub

    Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. Coordinates, radii, CurrentX and CurrentY and PaintPicture's rectangles are in the scale's units (a radius in its horizontal units); DrawWidth is always in pixels. A colour or angle out of range stops the program with an error.

    Raster Operations

    PaintPicture's opcode says how each copied pixel is combined with the pixel already there (the destination) and with the FillColor (the pattern), as in Visual Basic. Leaving it out copies the picture (vbSrcCopy).

    @@ -4473,7 +5711,10 @@ End Sub vbPatCopy &HF00021 The pattern vbPatPaint &HFB0A09 Pattern OR NOT picture OR destination vbPatInvert &H5A0049 Pattern XOR destination - vbDstInvert &H550009 NOT destination + vbDstInvert &H550009 NOT destination + vbGrayCopy &H1000000 The picture greyed, as DVX shows a disabled picture (DVX only) + vbTransparentCopy &H2000000 Every pixel of the picture but the key colour: a colour given after the opcode, else the colour of the picture's top-left pixel (DVX only) +

    vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite.

    Any of the 256 raster operations of Windows can be given by its number; the bits work on the pixel values, so with fewer than 32 bits a pixel the results follow the display's colour format.

    Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' XOR the picture in
     Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' and out again
    @@ -4628,17 +5869,61 @@ End
      Key          Type      Description
       ----------   -------   -------------------------------------------
       Caption      String    The text displayed. Use & for accelerator key. Set to "-" for a separator.
    -  Checked      Boolean   Whether the menu item shows a checkmark.
    -  RadioCheck   Boolean   Show the check as a radio dot instead of a checkmark.
    +  Checked      Boolean   Whether the menu item shows a checkmark. An item with a Checked line (True or False) is a check item: choosing it turns its checkmark on or off before its Click event runs, so the handler reads the new state.
    +  RadioCheck   Boolean   Show the check as a radio dot instead of a checkmark. Radio items next to each other form a group: choosing one checks it and clears the others.
    +  Shortcut     String    A key that runs the item from anywhere in the form, shown at the right of the item: "F2", "Ctrl+X", "Alt+Enter", "Shift+Del" (Ctrl, Alt and Shift, then a letter, digit, F1 to F12, or Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up).
    +  ShortcutText String    Key text shown at the right of the item without making a shortcut, for keys a control handles itself (for example "Del" on a Delete item whose list deletes on Del in its KeyDown handler, so a text box keeps its own Del). Shown instead of the Shortcut's text when both are set.
       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.
    + Visible Boolean On a top-level menu, False makes it a popup menu (for ContextMenu) instead of a menu bar entry. On an item or separator inside a menu, False hides it until code sets Visible = True. +

    A Shortcut key works while the form's window has the focus, before the focused control sees the key, so give one only to keys no control there needs (F-keys, Ctrl or Alt combinations). F1 always opens the help and never reaches a shortcut. An unknown Shortcut stops the program with an error when the form loads.

    +
    Begin Menu mnuRename
    +    Caption = "&Rename..."
    +    Shortcut = "F2"
    +End
    +Begin Menu mnuDelete
    +    Caption = "&Delete"
    +    ShortcutText = "Del"
    +End

    At run time code can use only these properties of a menu item:

      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.
    -

    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.

    + Caption String R/W The item's text, with & before its access key. Setting it keeps the item's key text (Shortcut or ShortcutText), and reading it gives the text without it. + Checked Boolean R/W Whether the menu item shows a checkmark. Setting it on an item that has neither Checked nor RadioCheck in the .frm makes it a check item (as in VB, any item can be checked). + Enabled Boolean R/W Whether the menu item is enabled. + Visible Boolean R/W Whether the item (or separator) is in its menu. A hidden item keeps its caption, checkmark and place: shown again, it returns where it was. A submenu cannot be hidden (hide or remove its items, or set Enabled = False). + Shortcut String R/W The key that runs the item ("Ctrl+K", written as in the .frm file, "" for none). Setting it changes the key and the key text shown in the item; "" removes both. An unknown key is an error. +

    RadioCheck and ShortcutText 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. The top-level menus of the menu bar (File, Edit) have no run-time properties.

    +

    Menus at Run Time

    +

    These methods work on any control of a form, or on the form itself (Me.AddMenuItem ...). They name menus by their .frm names: a top-level menu of the menu bar, a submenu, a popup menu from the .frm, or one made with CreateMenu.

    +
      Method             Parameters                          Description
    +  ----------------   ---------------------------------   -------------------------------------------
    +  PopupMenu          menu$ [, x, y]                      Shows the menu as a popup at the mouse, or at x, y in the control.
    +  CreateMenu         menu$                               Makes an empty popup menu of that name (nothing happens if one exists).
    +  AddMenu            menu$, caption$                     Adds a menu at the end of the form's menu bar, which appears with the first one (so a form made with CreateForm can have a menu bar). Add items to it by its name. The window keeps its size; the controls get the height the bar takes.
    +  RemoveMenu         menu$                               Removes a menu of the menu bar with all its items; with the last one the bar goes too. Not for popup menus (DestroyMenu) or items (RemoveMenuItem).
    +  AddMenuItem        menu$, item$, caption$ [, shortcut$]   Adds an item at the end of the menu. Choosing it runs the Sub item_Click, and item.Caption, .Checked, .Enabled, .Visible and .Shortcut work as for .frm items. shortcut$ gives it a key, as Shortcut in the .frm file.
    +  AddMenuRadioItem   menu$, item$, caption$ [, checked]  Adds a radio item: radio items next to each other form a group, and choosing one checks it and clears the others.
    +  AddMenuSeparator   menu$ [, separator$]                Adds a separator line at the end of the menu. With a name it can be hidden and removed like an item.
    +  AddSubMenu         menu$, submenu$, caption$           Adds a submenu at the end of the menu; add items to it by its name.
    +  RemoveMenuItem     item$                               Removes an item or named separator (shown or hidden) from its menu. An unknown name does nothing. A submenu cannot be removed.
    +  DestroyMenu        menu$                               Frees a popup menu made with CreateMenu (or from the .frm) and every item in it.
    +

    A recent-files list at the end of the File menu, rebuilt whenever it changes:

    +
    Dim recentCount As Integer
    +
    +Sub ShowRecent(files() As String, count As Integer)
    +    Dim i As Integer
    +    For i = 1 To recentCount
    +        Me.RemoveMenuItem "mnuRecent" & i
    +    Next
    +    Me.RemoveMenuItem "mnuRecentSep"
    +    If count > 0 Then Me.AddMenuSeparator "mnuFile", "mnuRecentSep"
    +    For i = 1 To count
    +        Me.AddMenuItem "mnuFile", "mnuRecent" & i, "&" & i & " " & files(i)
    +    Next
    +    recentCount = count
    +End Sub
    +

    Each item made this way needs its own Click procedure (Sub mnuRecent1_Click and so on), or fixed items in the .frm switched on and off with Visible serve the same purpose.

    Nesting

    Menu items are nested by placing Begin Menu blocks inside other Begin Menu blocks:

      @@ -4648,7 +5933,7 @@ End

    A level-0 menu that contains children becomes a top-level menu header. A non-level-0 menu that contains children becomes a submenu.

    Event Dispatch

    -

    Each clickable menu item (not headers, not separators) receives a unique numeric ID at load time. When clicked, the form's onMenu handler maps the ID to the menu item's name and fires MenuName_Click.

    +

    Choosing a menu item (not a header, separator or submenu) fires MenuName_Click, whether it is on the menu bar or in a popup menu.

    Sub mnuOpen_Click ()
         MsgBox "Open was clicked"
     End Sub
    @@ -4735,6 +6020,7 @@ End Sub
  • Controls are nested with Begin TypeName Name / End.
  • Container controls (Frame, VBox, HBox, Toolbar, TabStrip, ScrollPane, Splitter, WrapBox) can have child controls nested inside them.
  • Properties are assigned as Key = Value. String values are optionally quoted.
  • +
  • A Key = Value line belongs to the innermost block that is still open, also when it comes after the blocks nested inside it. DVX writes a block's properties before the blocks inside it.
  • Everything after the form's closing End is BASIC source code.
  • Comments in the form section use ' (single quote).
  • Blank lines are ignored in the form section.
  • @@ -4786,49 +6072,248 @@ End Sub 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. + 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. + Padding Integer 0 Space in pixels between the window's edges and the controls (the content box's Padding); 0 is the default of 4 pixels. + Spacing Integer 0 Space in pixels between one control and the next (the content box's Spacing); 0 is the default of 4 pixels. + WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen) or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. + Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form. + DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files. + Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. + Tag String "" Any text the program wants to keep with the form, as for controls. + MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties). + ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files. + DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files. +

    Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size.

    +
    frmTerminal.Show
    +frmTerminal.WindowState = vbMinimized   ' starts as an icon

    Form Events

      Event         Parameters              Description
       -----------   ---------------------   -------------------------------------------
       Load          (none)                  Fires after the form and all controls are created. This is the default event.
       Unload        (none)                  Fires when the form is being closed or unloaded.
       QueryUnload   Cancel As Integer       Fires before Unload. Set Cancel = 1 to abort the close.
    -  Resize        (none)                  Fires when the window is resized by the user.
    +  Resize        (none)                  Fires when the window changes size: resized by the user, maximized or restored (by the user or WindowState).
       Activate      (none)                  Fires when the window gains focus.
       Deactivate    (none)                  Fires when the window loses focus.
       Paint         (none)                  Fires when the form is shown or resized and on Refresh, unless AutoRedraw is True (see Drawing on Forms and PictureBoxes).
       DragDrop      Source As Control, X As Integer, Y As Integer                     Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form.
    -  DragOver      Source As Control, X As Integer, Y As Integer, State As Integer   Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver.
    + DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver. + NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below). + DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released or double-clicked in the form. See Designing Forms below. + DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown). + DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below.

    See Drag and Drop under Common Properties, Events, and Methods. While a control is dragged over a form with no DragDrop event procedure, the mouse pointer shows the "no drop" sign.

    +

    New Arguments

    +

    A program with a NewArgs event procedure on any of its forms can be handed new arguments while it runs, instead of DVX starting another copy of it. COMMAND$ (and COMMAND$(1), COMMAND$(2), ...) then give the new arguments, and NewArgs fires on every loaded form that has the procedure. When the arguments arrive while one of the program's event procedures is still running -- for example while it shows a message box or a dialog, or a form with vbModal -- they are held, and NewArgs fires once that procedure has finished; if several arrive meanwhile, only the last is given.

    +

    DVX hands new arguments to the help viewer: F1 and the Help menus of programs (and HelpView in BASIC) send "helpfile topic" to the program installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP when it is already running. A help viewer written in BASIC and installed there gets them in NewArgs. Starting a program that is already running -- from Program Manager, the Run dialog, a document opened in it or ShellRun -- gives it the new arguments when it allows only one copy and has a NewArgs procedure: its window comes to the front and NewArgs fires. Without a NewArgs procedure DVX says it is already running; a program that allows more than one copy (Project Properties) starts another copy instead.

    +
    Sub Form1_NewArgs ()
    +    ' F1 in another program while this help viewer is open
    +    If COMMAND$(1) <> openFile Then OpenHelpFile COMMAND$(1)
    +    GoTopic HlpStartTopic(hlp, COMMAND$(2))
    +End Sub

    Form Methods

      Statement           Description
       ------------------  -------------------------------------------
       Load FormName       Load the form (creates the window and controls, fires Load event).
       Unload FormName     Unload the form (fires Unload, destroys window).
       FormName.Show       Make the form visible.
    -  FormName.Show 1     Show as modal dialog (blocks until closed).
    -  FormName.Hide       Hide the form without unloading it.
    + FormName.Show 1 Show as a modal dialog (vbModal): the statement returns when the form is hidden or unloaded, so the code after it can read what the user chose. + FormName.Hide Hide the form without unloading it. + FormName.FitWidth Make the window just wide enough for its controls, keeping its height (AutoSize fits both). + FormName.FitHeight Make the window just tall enough for its controls, keeping its width. + FormName.SaveScreenshot file$ Save a picture of what the window shows inside its frame to a file: BMP when the name ends in .bmp, PNG otherwise. An error when the form has no window or the file cannot be written. +

    Several Copies of a Form

    +

    A form can be open more than once, the way a program opens a second document window. New FormName makes another copy of the form: its own window, controls, menus and form-level variables, built from the same .frm. Its Load event runs, and it starts hidden like any loaded form, so call Show. Keep the copy in a variable declared As the form's name (or As Form, As Object):

    +
    Dim f As Form1          ' a Form1 copy, or the form itself
    +Set f = New Form1       ' a new copy
    +f.Caption = "Second window"
    +f.Text1.Text = "Hello"  ' this copy's Text1
    +f.Show
    +
    +Dim g As New Form1      ' declares and makes a copy at once
    +

    The code of the form serves every copy. In it, Me, the controls named on their own (Text1) and the form-level variables are those of the copy whose event is running, so each window keeps its own state. Unload f closes that copy only.

    +

    The form's name (Form1.Caption, Form1.Show, Load Form1) always means the first copy loaded -- the one the program starts with -- as in Visual Basic. If that copy was unloaded, the name means the next one still open.

    +

    Designing Forms

    +

    A form designer written in BASIC shows the form being designed as a real form and turns on its design mode. While DesignMode is True, the controls on the form take no mouse clicks or keys and fire no events (Timers too are quiet); the form instead tells the designer what the user does:

    +
      +
    • DesignMouse (Action, Button, X, Y, CtrlName, Handle): Action is DSGN_MOUSE_DOWN (1, a button was pressed), DSGN_MOUSE_MOVE (2), DSGN_MOUSE_UP (3) or DSGN_MOUSE_DBLCLICK (4, a double click: open the code, say). Button is the button pressed or released (1 left, 2 right, 4 middle; for a move, the buttons held). X and Y are where, in the form. CtrlName is the control there -- the innermost one when containers hold others -- or "" for the form itself. Handle is the selection handle of the selected control under the mouse: DSGN_HANDLE_E (3, the right edge: drag to change the width), DSGN_HANDLE_SE (4, the corner) or DSGN_HANDLE_S (5, the bottom edge), or DSGN_HANDLE_NONE (-1).
    • +
    • DesignKey (KeyCode, Shift): a key, for Delete, the arrow keys and so on.
    • +
    • DesignPropertyChanged (CtrlName, PropertyName): the code of a UserControl on the form -- one of the project's own, put on the designer's toolbox with CompDesignControls (compiler.bas) -- called UserControl.PropertyChanged PropertyName: read its properties again. Such controls run their code on the form with UserControl.UserMode False.
    • +
    +

    DesignSelection names the selected control; the form draws its eight selection handles over the controls (the three that resize in black) and shows the resize mouse pointers over them. The constants are in designer.bas, which also lists the control types, their properties and events (see Include Libraries in the language reference). Together with CreateControl, RemoveControl, CallByName, Controls, Container, MoveBefore, LoadFormText and the .frm model of designer.bas (FrmOpen, FrmText$, FrmCtlText$ / FrmCtlPaste to copy and paste controls, FormText$ to write the live form as .frm text), a designer has what the DVX BASIC IDE's own designer uses.

    +
    Sub Canvas_DesignMouse(Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer)
    +    If Action = DSGN_MOUSE_DOWN Then
    +        If Handle <> DSGN_HANDLE_NONE Then
    +            resizing = True                  ' drag the selected control's size
    +        Else
    +            Canvas.DesignSelection = CtrlName
    +            ShowProperties CtrlName
    +        End If
    +    ElseIf Action = DSGN_MOUSE_UP Then
    +        resizing = False
    +    End If
    +End Sub

    Drawing

    -

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.

    +

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.

    Example

    -
    Sub Form_Load ()
    +
    Sub Form1_Load ()
         Form1.Caption = "Hello World"
         Print "Form loaded!"
     End Sub
     
    -Sub Form_QueryUnload (Cancel As Integer)
    +Sub Form1_QueryUnload (Cancel As Integer)
         If MsgBox("Really quit?", 4) <> 6 Then
             Cancel = 1
         End If
     End Sub
     
    -Sub Form_Resize ()
    +Sub Form1_Resize ()
         Print "Window resized"
     End Sub

    Common Properties, Events, and Methods

    Drawing on Forms and PictureBoxes

    FRM File Format

    +
    +

    UserControls: Your Own Controls

    +

    A UserControl is a control you write in DVX BASIC. Once written, it is used exactly like the controls DVX comes with: you put it on a form in the designer (or in a .frm file with Begin), set its properties, call its methods and write handlers for its events. Programs written in C can create and use it too.

    +

    A UserControl lives in a .ctl file. In the IDE, File > New UserControl makes one (see UserControls in the IDE). The file starts with a header that describes the control, then holds the control's code. The code is a class: every control on a form is one object of the class, with its own copies of the class's variables.

    +
    VERSION DVX 1.00
    +Begin UserControl Gauge
    +    Width        = 120          ' natural (smallest) size in pixels
    +    Height       = 30
    +    CanGetFocus  = True
    +    BackColor    = &H00FFFFFF&
    +    ToolboxIcon  = "gauge.bmp"   ' 24 x 24 picture for the toolbox
    +    Description  = "A gauge"     ' the toolbox tooltip
    +    DefaultEvent = "Changed"
    +End
    +Option Explicit
    +
    +Public Event Changed(ByVal Level As Integer)
    +
    +Private mLevel As Integer
    +
    +Public Property Get Level() As Integer
    +    Level = mLevel
    +End Property
    +
    +Public Property Let Level(ByVal Value As Integer)
    +    mLevel = Value
    +    RaiseEvent Changed(mLevel)
    +    UserControl.Refresh
    +End Property
    +
    +Private Sub UserControl_Paint()
    +    UserControl.Cls
    +    UserControl.Line (0, 0)-(mLevel * UserControl.Width \ 100, UserControl.Height - 1), RGB(0, 128, 0), BF
    +End Sub
    +

    A form uses it like any control:

    +
    Begin Form Form1
    +    Begin Gauge Gauge1
    +        Level = 40
    +    End
    +End
    +
    +Sub Gauge1_Changed (ByVal Level As Integer)
    +    lblLevel.Caption = Str$(Level)
    +End Sub
    +

    The Header

    +

    The header is a Begin UserControl block: the control's name (the type name forms use) and its settings. Every setting is optional. In the IDE, the Properties window shows and changes them while the .ctl file is the active file.

    +
      Setting        Meaning
    +  -------        -------
    +  Width, Height  The natural size in pixels: the smallest size the form's layout gives it (64 x 64 when not given). The code changes it with UserControl.MinWidth and MinHeight.
    +  CanGetFocus    True (the default): a click or Tab gives it the keyboard focus. False: it never has the focus and gets no keys.
    +  BackColor      The colour Cls clears it to (white when not given).
    +  ToolboxIcon    A 24 x 24 picture file in the project folder, shown in the designer's toolbox.
    +  Description    The toolbox tooltip (its name when not given).
    +  DefaultEvent   The event the designer opens when the control is double-clicked.
    +

    Properties, Methods and Events

    +

    The control's public members are what programs use:

    +
      +
    • Public Property Get / Let (or Set) pairs and Public variables are its properties. A property with only Property Get is read-only, with only Property Let write-only. A property whose type is Integer, Long or Byte is a whole number, Boolean is True / False, Single and Double (and Currency) are numbers, String, Date and Variant are text; a property of an object type, an array or a TYPE cannot be a property of a control.
    • +
    • A property declared As an ENUM is a choice of its members, as a built-in control's Alignment is: the designer's Properties window shows the member's name and a double-click steps to the next member, the form file stores the name (Look = Circle), and code sets it with a member, its number or its name as text (Shape1.Look = Circle, Shape1.Look = 1, Shape1.Look = "Circle"). This needs the members numbered 0, 1, 2 and so on, each number once, as an ENUM numbers them when no values are given; an ENUM with other values (Small = 10) makes a whole-number property.
    • +
    • Public Subs and Functions are its methods. They take any arguments; a Function's value is the method's result.
    • +
    • Public Events are its events. RaiseEvent sends one to the program that has the control: its handler CtrlName_EventName runs with the arguments, and a ByRef parameter the handler changes (a number, a Boolean or a String) comes back changed.
    • +
    +

    Names that every control already has (Name, Left, Top, Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Weight, Visible, Enabled, ReadOnly, BackColor, ForeColor, Caption, Text, ToolTipText, ContextMenu, HelpTopic, DataSource, DataField, Tag, MousePointer ...) belong to the form: a control's own property of such a name is not used. Give your properties other names (Title instead of Caption, Value or Level instead of Text).

    +

    A property's default is the value it has after UserControl_Initialize ran. The form's .frm sets only the properties the designer user changed, through your Property Let, after Initialize.

    +

    The UserControl Object

    +

    Inside the control's code, UserControl is the control itself: the surface it draws on and the side the program sees.

    +
      Member                       Description
    +  ------                       -----------
    +  Width, Height                Its current size in pixels (read-only; the form's layout decides it).
    +  MinWidth, MinHeight          Its natural size: the form lays it out again when you change them.
    +  Enabled                      False while the control, or a container it is in, is disabled (read-only).
    +  HasFocus                     True while it has the keyboard focus (read-only).
    +  CanGetFocus                  The header's CanGetFocus (read-only).
    +  UserMode                     False while it sits in a form designer (the IDE's, or a form with Form.DesignMode = True), True in a running program. In the designer the code draws it, but the user's clicks and keys go to the designer.
    +  Name                         The name the program gave the control ("" when a C program gave none).
    +  SetFocus                     Gives it the keyboard focus.
    +  Refresh                      Runs UserControl_Paint now and shows the result.
    +  Invalidate                   Asks for UserControl_Paint at the next chance, once however often it is called before then: change many things (add a hundred items) and the control is drawn once.
    +  PropertyChanged name         Tells a designer that a property changed, so its property sheet shows the new value.
    +  Cls, Line, PSet, Circle, Print, Point, PaintPicture, DrawFocusRect, TextWidth, TextHeight, Scale, ScaleX, ScaleY   Drawing, as on a PictureBox.
    +  DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, TextWidthAccel, AccelKey   Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox.
    +  CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight   Drawing properties, as on a PictureBox.
    +

    The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.

    +

    The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.

    +

    The Control's Events

    +

    The program runs these procedures of the control's code when they exist. Each is a Private Sub with exactly these parameters:

    +
      Procedure                                                              When
    +  ---------                                                              ----
    +  UserControl_Initialize ()                                              The control was made (before the form's property values are set).
    +  UserControl_Terminate ()                                               The control is going away (its form unloads). It cannot draw any more.
    +  UserControl_Resize ()                                                  Its size changed (and once when it first appears).
    +  UserControl_Paint ()                                                   It must be drawn (after Resize, after Refresh).
    +  UserControl_Click ()                                                   The left button was pressed and let go over it.
    +  UserControl_DblClick ()                                                A second quick left press.
    +  UserControl_MouseDown (Button As Integer, Shift As Integer, X As Single, Y As Single)   A button went down over it.
    +  UserControl_MouseMove (Button As Integer, Shift As Integer, X As Single, Y As Single)   The mouse moved over it (Button 0), or with the left button held after a press on it, anywhere.
    +  UserControl_MouseUp (Button As Integer, Shift As Integer, X As Single, Y As Single)     A button went up.
    +  UserControl_MouseWheel (Delta As Integer)                              The wheel turned over it (Delta is positive downwards).
    +  UserControl_KeyDown (KeyCode As Integer, Shift As Integer)             A key went down while it has the focus.
    +  UserControl_KeyPress (KeyAscii As Integer)                             A character key was typed.
    +  UserControl_KeyUp (KeyCode As Integer, Shift As Integer)               A key went up.
    +  UserControl_GotFocus ()                                                It got the keyboard focus.
    +  UserControl_LostFocus ()                                               It lost the keyboard focus.
    +

    Button is vbLeftButton, vbRightButton or vbMiddleButton (in MouseMove, the buttons held). Shift holds vbShiftMask, vbCtrlMask and vbAltMask. X and Y are pixels from the control's top-left corner. The compiler reports a handler with other parameters.

    +

    These events are the control's own: the program does not see them. The program sees only the events the control raises. To let a program react to clicks, raise your own Click (Public Event Click, then RaiseEvent Click in UserControl_Click); an event named Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll reaches the program's CtrlName_Click (and so on) handler as for any control. Give those events Visual Basic's parameters: Button, Shift, X, Y for MouseDown, MouseUp and MouseMove; KeyCode, Shift for KeyDown and KeyUp; KeyAscii for KeyPress; Delta for Scroll. GotFocus and LostFocus reach the program by themselves.

    +

    Using Controls in a Program

    +

    In the IDE, File > New UserControl (or File > Add File with a .ctl file) adds a control to the open project; while the project is open the control is in the Toolbox, ready to be placed on the project's forms. In the project file, the .ctl file is listed in the [Controls] section of the project's .dbp:

    +
    [Controls]
    +File0 = gauge.ctl
    +Count = 1
    +

    When the program starts, its controls are registered before any form is loaded, so its forms can use them; they go away when the program ends. They belong to the program: other programs (and other copies of the same program) can have controls of the same name, and each program uses its own. The control's code runs in the program, with the program's rules: an error the code does not handle ends the program, and DoEvents works as anywhere.

    +

    Installing a Control for Every Program

    +

    A control project holds only controls (no forms): its .dbp says

    +
    [Project]
    +Name = GAUGE
    +Type = Control
    +
    +[Controls]
    +File0 = gauge.ctl
    +Count = 1
    +

    Building it -- in the IDE, File > Make Control (set the project's Type to Control in File > Project Properties first); or with the BASIC compiler, bascomp gauge.dbp (on DVX, SYSTEM\BASCOMP.EXE) -- makes gauge.wgt, a control file. Copied into DVX's WIDGETS folder (in a folder of its own, like the controls DVX comes with), it is loaded when DVX starts: every program can use its controls, the designer's toolbox shows them with their ToolboxIcon and Description, and C programs create them by name. A control project may hold several controls and the modules they need; its help file goes beside the control file as for any widget.

    +

    All the programs using an installed control file share one copy of its code and its module variables: module-level code of the control project runs once, when the first of its controls is made. A control's own variables (its class members) are separate for each control. Because the code is shared:

    +
      +
    • DoEvents does nothing in an installed control's code, and its code never lets another program run in between.
    • +
    • While its code runs for one program, another program that needs it (to paint or for an event) is turned away: the control keeps its old picture and the event is dropped (DVX.LOG notes it once).
    • +
    • An error its code does not handle is shown, and the control carries on; it does not end the program that was using it.
    • +
    +

    Limits

    +
      +
    • A control cannot hold other controls; it draws what it shows on its UserControl surface.
    • +
    • Properties are numbers, Booleans, text and ENUM choices; a property of an ENUM whose members are not numbered 0, 1, 2 ... is a whole number in the Properties window.
    • +
    • CanGetFocus is fixed by the header (read-only in code).
    • +
    • UserControl_Terminate cannot draw: the surface is already gone.
    • +
    • A program cannot have a control of the same name as one installed in WIDGETS (the program stops with "Control cannot be registered").
    • +
    +

    See also: UserControls in the IDE

    +

    See also: Objects and Class Modules

    +

    See also: Events

    +

    See also: Drawing on Forms and PictureBoxes

    +

    Terminal

    DVX Extension -- DVX Widget: ansiterm

    @@ -4836,18 +6321,22 @@ End Sub

    Type-Specific Properties

      Property     Type      Description
       ----------   -------   -------------------------------------------
    -  Cols         Integer   Number of character columns (read-only).
    -  Rows         Integer   Number of character rows (read-only).
    +  Cols         Integer   Number of character columns (1-512). Setting Cols or Rows gives the terminal a new size, starting as a new terminal: empty screen and scrollback, modes the host set reset (its colors and connection stay); set the form's AutoSize to True afterwards to fit the window to it.
    +  Rows         Integer   Number of character rows (1-512).
       Scrollback   Integer   Maximum number of scrollback lines. Older lines are dropped (default 500). Setting it discards the existing history.

    Type-Specific Methods

      Method             Description
       ------             -----------
       Clear              Clear the terminal screen and reset the cursor to the home position.
    +  Copy()             Copy the selected text to the clipboard and clear the selection. Returns False when nothing is selected.
    +  Paste              Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does.
       Poll               Poll the attached communication channel for incoming bytes and feed them into the ANSI parser.
    +  ResetColors        Back to the standard 16 colors.
    +  SetColor index%, color&  The color (RGB(), as ForeColor takes it) of text attribute index (0-15, in CGA order: black, blue, green, cyan, red, magenta, brown, light gray, then their bright forms). An index outside 0-15 is ignored.
       Write text$        Write a string to the terminal with full ANSI escape processing.

    No default event. Uses common events only.

    Serial Communication

    -

    Use the comm.bas library to connect a Terminal to a serial link:

    +

    The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link or Telnet -- with capture, answerback and file transfers (ConnAttach; see Include Libraries). For a packet link of your own, use the comm.bas library:

    Dim link As Long
     link = CommOpen(1, 115200, 8, "N", 1, SER_HANDSHAKE_NONE)
     If link <> 0 Then
    @@ -4864,7 +6353,7 @@ End If
    MinHeight = 400 End -Sub Form_Load () +Sub Form1_Load () Terminal1.Write "Welcome!" & Chr$(13) & Chr$(10) End Sub

    Common Properties, Events, and Methods

    @@ -4874,9 +6363,14 @@ End Sub

    VB Equivalent: Frame -- DVX Widget: frame (titled container)

    A container with a titled border. Child controls are placed inside the frame using VBox layout. In the .frm file, nest Begin/End blocks inside the Frame block.

    Type-Specific Properties

    -
      Property   Type     Description
    -  --------   ------   -------------------------------------------
    -  Caption    String   The title displayed in the frame border.
    +
      Property    Type      Description
    +  --------    -------   -------------------------------------------
    +  Caption     String    The title displayed in the frame border.
    +
      Property    Type      Description
    +  --------    -------   -------------------------------------------
    +  Alignment   Enum      Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center or Right (the bottom).
    +  Padding     Integer   Space in pixels between the border and the children; 0 (the default) means 4 pixels.
    +  Spacing     Integer   Space in pixels between one child and the next; 0 (the default) means no space.

    No type-specific methods or events.

    Setting Enabled = False on a Frame disables every control inside it for mouse and keyboard input.

    Container: Yes

    @@ -4897,7 +6391,14 @@ End

    HBox

    DVX Extension -- DVX Widget: hbox (horizontal layout container)

    A container that arranges its children horizontally, left to right. No visible border. Use Weight on children to distribute extra space proportionally.

    -

    No type-specific properties, methods, or events.

    +

    Type-Specific Properties

    +
      Property    Type      Description
    +  --------    -------   -------------------------------------------
    +  Alignment   Enum      Where the children sit along the row when they do not fill it: Left (the left, the default), Center or Right (the right).
    +  Padding     Integer   Space in pixels between the edge of the box and the children; 0 (the default) means 4 pixels.
    +  Spacing     Integer   Space in pixels between one child and the next; 0 (the default) means 4 pixels.
    +

    Alignment = Right puts a row of buttons at the right, as dialog boxes have them.

    +

    No type-specific methods or events.

    Container: Yes

    Default Event: Click

    Example

    @@ -4917,7 +6418,13 @@ End

    VBox

    DVX Extension -- DVX Widget: vbox (vertical layout container)

    A container that arranges its children vertically, top to bottom. No title or border. Use Weight on children to distribute extra space proportionally.

    -

    No type-specific properties, methods, or events.

    +

    Type-Specific Properties

    +
      Property    Type      Description
    +  --------    -------   -------------------------------------------
    +  Alignment   Enum      Where the children sit along the column when they do not fill it: Left (the top, the default), Center or Right (the bottom).
    +  Padding     Integer   Space in pixels between the edge of the box and the children; 0 (the default) means 4 pixels.
    +  Spacing     Integer   Space in pixels between one child and the next; 0 (the default) means 4 pixels.
    +

    No type-specific methods or events.

    Container: Yes

    Default Event: Click

    Example

    @@ -4939,7 +6446,10 @@ End

    Type-Specific Properties

      Property   Type     Description
       --------   ------   -------------------------------------------
    -  Caption    String   The text displayed on the button. Use '&' for an accelerator key.
    + Caption String The text displayed on the button. Use '&' for an accelerator key. + Default Boolean True: Enter presses this button, wherever the focus is on its form (unless the focus is on another button, which Enter presses instead). + Cancel Boolean True: Escape presses this button, wherever the focus is on its form. +

    Default and Cancel make a form work like a dialog box: give the OK button Default = True and the Cancel button Cancel = True. If several buttons on a form have the property, the first one (in the order they were made) that is visible and enabled is pressed.

    No type-specific methods or events.

    Default Event: Click

    Example

    @@ -4963,6 +6473,7 @@ End Sub

    Type-Specific Properties

      Property      Type      Description
       --------      ----      -----------
    +  BorderStyle   Enum      0 - None: no frame, the control is exactly the size of the drawing. 1 - FixedSingle (the default): a sunken frame 2 pixels wide around the drawing. Mouse coordinates and drawing positions start inside the frame either way.
       ImageHeight   Integer   Height of the drawing in pixels (read-only). After Load it is the loaded image's height.
       ImageWidth    Integer   Width of the drawing in pixels (read-only). After Load it is the loaded image's width.

    Load and Save return 0 on success and -1 on failure (missing or unreadable file, unsupported format, path that cannot be written). Call them as functions to check the result, or as statements to ignore it. A failed Load leaves the drawing unchanged.

    @@ -4975,7 +6486,7 @@ End Sub DrawText x%, y%, text$ Draw text at the given position. FillCircle cx%, cy%, radius%, color% Draw a filled circle. FillRect x%, y%, w%, h%, color% Draw a filled rectangle. - GetPixel(x%, y%) Returns the color at a pixel. + GetPixel(x%, y%) Returns the color at a pixel. With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. Load(path$) Load a BMP, PNG, JPEG or GIF image; the canvas takes the image's size. Returns 0 on success, -1 on failure. Resize w%, h% Resize the canvas to new dimensions. The drawing is cleared to white. Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure. @@ -4993,7 +6504,7 @@ Picture1.DrawText 60, 130, "Hello!" If Picture1.Save("output.bmp") <> 0 Then MsgBox "Could not save output.bmp" End If -

    A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight) and a Paint event: see Drawing on Forms and PictureBoxes.

    +

    A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight) and a Paint event: see Drawing on Forms and PictureBoxes.

    Common Properties, Events, and Methods

    Drawing on Forms and PictureBoxes

    @@ -5030,7 +6541,8 @@ End Sub
      Property    Type      Description
       ---------   -------   -------------------------------------------
       Text        String    The text in the editable field.
    -  ListIndex   Integer   Index of the currently selected list item (-1 = user typed something not in the list).
    + ListIndex Integer Index of the currently selected list item (-1 = user typed something not in the list). + MaxLength Integer Most characters the edit field holds (0 = the default, 256). Longer text is cut when it is set.

    Type-Specific Methods

      Method              Description
       ------              -----------
    @@ -5044,7 +6556,7 @@ End Sub
    Begin ComboBox Combo1
     End
     
    -Sub Form_Load ()
    +Sub Form1_Load ()
         Combo1.AddItem "Small"
         Combo1.AddItem "Medium"
         Combo1.AddItem "Large"
    @@ -5072,7 +6584,10 @@ End Sub
    EOF Boolean True if the current position is past the last record or there are no records (read-only). MasterSource String Name of a master Data control (for master-detail binding). MasterField String Column in the master recordset to read for the filter value. - DetailField String Column in this recordset that matches the master field. + DetailField String Column in this recordset that matches the master field. + RecordCount Long The number of records (read-only). + FieldCount Integer The number of columns (read-only). + AbsolutePosition Long The current record, 0 for the first (-1 when there are none). Setting it moves there (saving pending changes first, as the Move methods do) and fires Reposition; a value out of range does nothing.

    Type-Specific Methods

      Method         Parameters   Description
       ------------   ----------   -------------------------------------------
    @@ -5083,7 +6598,11 @@ End Sub
    AddNew (none) Append a blank record and move to it. Delete (none) Delete the current record. Refresh (none) Re-query the database and reload records. - Update (none) Write pending changes to the database. + Update (none) Write pending changes to the database. + GetField(col$) The current record's value of a column, as text, without a bound control ("" for an unknown column). + SetField col$, value$ Change a column of the current record; written to the database by Update (or when the record changes). + FieldName(index%) The name of a column, 0 for the first ("" past the last). + GetCell(row&, col%) The text of any cell (both from 0) without moving the current record ("" outside).

    Type-Specific Events

      Event        Parameters          Description
       ----------   -----------------   -------------------------------------------
    @@ -5102,7 +6621,7 @@ Begin TextBox Text1
         DataField  = "title"
     End
     
    -Sub Form_Load ()
    +Sub Form1_Load ()
         Data1.Refresh
     End Sub

    Common Properties, Events, and Methods

    @@ -5143,7 +6662,7 @@ Begin DBGrid DBGrid1 GridLines = True End -Sub Form_Load () +Sub Form1_Load () Data1.Refresh End Sub @@ -5175,7 +6694,7 @@ End Sub
    Begin DropDown DropDown1
     End
     
    -Sub Form_Load ()
    +Sub Form1_Load ()
         DropDown1.AddItem "Small"
         DropDown1.AddItem "Medium"
         DropDown1.AddItem "Large"
    @@ -5188,6 +6707,45 @@ End Sub

    Common Properties, Events, and Methods

    See ListBox for details

    +
    +

    HelpPane

    +

    VB Equivalent: (none) -- DVX Widget: helpview

    +

    Shows one topic of a help file the way the DVX Help Viewer does: headings, paragraphs and lists wrapped to the control's width, notes, code, tables, pictures, rules and links, with a scroll bar when the topic is longer than the control. Open the help file with hlpfile.bas (HlpOpen) and give its handle to ShowTopic. With a TreeView for the table of contents and a few buttons, a HelpPane makes a complete help viewer (see Help Files under Include Libraries).

    +

    A link is followed with a click, or with Enter or Space after Tab has moved to it: the HelpPane fires LinkClick with the ID of the topic the link names and remembers it in LinkTopic. It does not change topic by itself -- the LinkClick event procedure usually shows the topic and adds it to its Back list. Everything a topic shows is copied when it is shown, so closing the help file afterwards does not change what the HelpPane shows.

    +

    Type-Specific Properties

    +
      Property    Type      R/W   Description
    +  ---------   -------   ---   -------------------------------------------
    +  Topic       Integer   R     The topic shown (numbered from 0, as hlpfile.bas numbers them); -1 when nothing is shown.
    +  LinkTopic   String    R     The topic ID of the last link followed ("" before any).
    +

    Type-Specific Methods

    +
      Method                         Description
    +  ------                         -----------
    +  ShowTopic hlpFile&, topic&     Show a topic of an open help file (an HlpOpen handle) from the top. A topic that is not in the file, or a closed handle, shows nothing.
    +  Clear                          Show nothing.
    +

    Type-Specific Events

    +
      Event       Parameters          Description
    +  ---------   -----------------   -------------------------------------------
    +  LinkClick   Topic As String     A link was followed (clicked, or Enter or Space on it). Topic is the ID of the topic it names; HlpFindTopic gives its number.
    +

    Default Event: LinkClick

    +

    Example

    +
    ' hlpfile.bas in the project
    +Dim hlp As Long
    +
    +Sub Form1_Load ()
    +    hlp = HlpOpen(App.Path + "\MYAPP.HLP")
    +    HelpPane1.ShowTopic hlp, HlpStartTopic(hlp, "")
    +End Sub
    +
    +Sub HelpPane1_LinkClick (Topic As String)
    +    HelpPane1.ShowTopic hlp, HlpFindTopic(hlp, Topic)
    +    Me.Caption = HlpTopicTitle$(hlp, HelpPane1.Topic)
    +End Sub
    +
    +Sub Form1_Unload ()
    +    HlpClose hlp
    +End Sub
    +

    Common Properties, Events, and Methods

    +

    ImageButton

    DVX Extension -- DVX Widget: imagebutton

    @@ -5195,7 +6753,7 @@ End Sub

    Type-Specific Properties

      Property      Type      Description
       -----------   -------   -------------------------------------------
    -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE). Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
    +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
       ImageWidth    Integer   Width of the loaded image in pixels (read-only).
       ImageHeight   Integer   Height of the loaded image in pixels (read-only).

    No type-specific methods or events.

    @@ -5222,10 +6780,12 @@ End Sub

    Type-Specific Properties

      Property      Type      Description
       -----------   -------   -------------------------------------------
    -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE). Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
    +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
       Stretch       Boolean   When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size.
       ImageWidth    Integer   Width of the loaded image in pixels (read-only).
    -  ImageHeight   Integer   Height of the loaded image in pixels (read-only).
    + ImageHeight Integer Height of the loaded image in pixels (read-only). + MaskColor Long The transparent colour (an RGB value): with UseMaskColor True, pixels of this colour are not drawn, so the form shows through. + UseMaskColor Boolean True leaves out the pixels of MaskColor (default False).

    No type-specific methods or events.

    Default Event: Click

    Example

    @@ -5282,7 +6842,7 @@ End SetReorderable Reorderable As Boolean Enable or disable drag-to-reorder.

    Default Event: Click

    Example

    -
    Sub Form_Load ()
    +
    Sub Form1_Load ()
         List1.AddItem "Apple"
         List1.AddItem "Banana"
         List1.AddItem "Cherry"
    @@ -5315,32 +6875,44 @@ End Sub
    ClearSelection Deselect all rows. GetCell$(row%, col%) Returns the text of a cell. IsItemSelected(index%) Returns True if the row is selected. + ListCount() Returns the number of rows (the same as RowCount; also read as the ListCount property). RemoveItem index% Remove a row by index. Rows after it keep their selection; removing the selected row leaves no selection. RowCount() Returns the number of rows. SelectAll Select all rows. + SelectedRows$() Returns the numbers of the selected rows (as IsItemSelected numbers them) separated by spaces, "" when none: one call instead of IsItemSelected for every row of a long list. SetCell row%, col%, text$ Set the text of a cell. - SetColumns spec$ Define columns. Pipe-delimited "Name,Width|Name,Width" (width in characters). + SetColumns spec$ Define columns. Pipe-delimited "Name,Width,Align|Name,Width": the width in characters (missing or 0 = as wide as the contents), then Left (the default), Center or Right for the column's text. SetItemSelected index%, selected Select or deselect a row. SetMultiSelect multi Enable or disable multi-select. SetReorderable reorderable Enable or disable row reordering. + SetRows text$ Replace all the rows at once: one row per line (separated by CHR$(10)), cells separated by CHR$(9) (Tab); missing cells are blank and extra ones are dropped. Much faster than AddItem and SetCell for long lists. The selection is cleared and the cursor goes to the first row. SetSort col%, dir% Sort by column. dir: 0=none, 1=ascending, 2=descending. An invalid column or direction clears the sort. Has no effect while reordering is enabled.
    +

    Type-Specific Events

    +
      Event         Parameters               Description
    +  -----------   ----------------------   -------------------------------------------
    +  ColumnClick   Column As Integer        A column header was clicked (Column 0 is the first). Without a handler the list sorts itself by that column's text (the same column again reverses); a program that orders the rows itself calls SetSort -1, 0 in the handler and fills the list in its own order.

    Default Event: Click

    Example

    ListView1.SetColumns "Name,20|Size,10|Type,15"
     ListView1.AddItem "readme.txt"
     ListView1.SetCell 0, 1, "1024"
     ListView1.SetCell 0, 2, "Text"
    -ListView1.SetSort 0, 1
    +ListView1.SetSort 0, 1 + +' Many rows at once; the size column right-aligned +ListView2.SetColumns "Name,14|Size,10,Right" +ListView2.SetRows "a.txt" & Chr$(9) & "120" & Chr$(10) & "b.txt" & Chr$(9) & "4,096"

    Common Properties, Events, and Methods

    ProgressBar

    VB Equivalent: ProgressBar -- DVX Widget: progressbar

    -

    A horizontal progress indicator bar.

    +

    A progress indicator bar, lying flat (the default) or standing upright.

    Type-Specific Properties

      Property   Type      Description
       --------   -------   -------------------------------------------
    -  Value      Integer   Current progress value (0-100).
    + Value Integer Current progress value (0-100). + Vertical Boolean True: the bar stands upright and fills from the bottom up. False (the default): it lies flat and fills from left to right. Can be set in the designer or at run time.

    No type-specific events or methods. No default event.

    Common Properties, Events, and Methods

    @@ -5366,9 +6938,32 @@ ListView1.SetSort 0, 1

    DVX Extension -- DVX Widget: scrollpane | Name Prefix: Scroll

    A scrollable container. Place child controls inside and the ScrollPane automatically provides scrollbars when the content exceeds the visible area. Clicking on content that does not respond to the mouse (a Label, an Image or empty space) gives the ScrollPane the focus, so the arrow and page keys scroll it.

    Type-Specific Properties

    -
      Property   Type      Description
    -  --------   -------   -------------------------------------------
    -  NoBorder   Boolean   When True, suppresses the border around the scroll pane.
    +
      Property     Type      Description
    +  --------     -------   -------------------------------------------
    +  NoBorder     Boolean   When True, suppresses the border around the scroll pane.
    +  Padding      Integer   Space in pixels between the edge and the controls inside (0, the default, is none).
    +  Spacing      Integer   Space in pixels between the controls inside, which are stacked top to bottom (0, the default, is none).
    +  ScrollTop    Integer   How far the content is scrolled down, in pixels (run time). Values past the end stop at the end.
    +  ScrollLeft   Integer   How far the content is scrolled to the right, in pixels (run time). Values past the end stop at the end.
    +

    Type-Specific Methods

    +
      Method        Description
    +  ------        -----------
    +  ScrollToTop   Scroll back to the top-left corner of the content.
    +  ScrollToControl ctrl   Scroll so that a control inside the pane (the control, or its name) is in view, also one that cannot take the focus.
    +

    Events

    +
      Event    Description
    +  -----    -----------
    +  Change   The user scrolled the content (the scrollbars, dragging a scroll box, the arrow, page, Home and End keys); read ScrollTop and ScrollLeft for the new position. Setting ScrollTop or ScrollLeft, or ScrollToTop, does not fire it.
    +

    Scrolling a Picture Bigger Than the Window

    +

    A ScrollPane with NoBorder = True filling a form, holding a PictureBox larger than the window, gives the form scrollbars of its own: the user scrolls the picture, Change says where it is, and ScrollTop / ScrollLeft move it from code. To draw only the part in view (for a picture too large to keep whole), keep the PictureBox the size of the view, and draw from Change with ScrollTop and ScrollLeft as the offset.

    +
    Sub Form1_Load ()
    +    picBig.Resize 1200, 900
    +    picBig.Line (0, 0)-(1199, 899), RGB(0, 0, 255), B
    +End Sub
    +
    +Sub spView_Change ()
    +    lblWhere.Caption = "Top " & spView.ScrollTop & ", left " & spView.ScrollLeft
    +End Sub

    Container: Yes

    No default event.

    Common Properties, Events, and Methods

    @@ -5417,23 +7012,27 @@ End Sub -------- ------- ------------------------------------------- Value Integer Current value (in real mode, the value truncated to an integer). RealMode Boolean True to use floating-point mode; False for integer mode. - Decimals Integer Number of decimal places shown in real mode. + Decimals Integer Number of decimal places shown in real mode. + RealValue Double The value in real mode, with its fraction (Value is it cut to a whole number). Setting it keeps it inside the range.

    Type-Specific Methods

      Method     Parameters                       Description
       --------   ------------------------------   -------------------------------------------
       SetRange   Min As Integer, Max As Integer   Set the allowed value range (used in both integer and real mode; default 0 to 100).
    -  SetStep    Step As Integer                  Set the increment per button click.
    + SetStep Step As Integer Set the increment per button click. + SetRealRange Min As Double, Max As Double The range in real mode, with fractions (SetRange takes whole numbers). + SetRealStep Step As Double The increment per click in real mode (0 or less gives 0.01).

    Default Event: Change

    Common Properties, Events, and Methods

    Splitter

    DVX Extension -- DVX Widget: splitter

    -

    A resizable split pane. Holds exactly two child widgets separated by a draggable divider. Default orientation is vertical (vertical divider, panes side-by-side).

    +

    A resizable split pane. Holds exactly two child widgets separated by a draggable divider. By default the divider is vertical and the panes sit side by side; set Vertical to False for a horizontal divider with one pane above the other.

    Type-Specific Properties

      Property   Type      Description
       --------   -------   -------------------------------------------
    -  Position   Integer   Position of the divider in pixels from the top (or left).
    + Position Integer Position of the divider in pixels from the left (or, when Vertical is False, from the top). + Vertical Boolean True (the default): panes side by side. False: one pane above the other. Can be set in the designer or at run time.

    Container: Yes

    No default event.

    Common Properties, Events, and Methods

    @@ -5441,8 +7040,19 @@ End Sub

    StatusBar

    VB Equivalent: StatusBar -- DVX Widget: statusbar

    -

    A horizontal container styled as a status bar, typically placed at the bottom of a form. At the C API level it holds child widgets (such as labels), each drawn with a sunken-panel border for the classic segmented status-bar look. It is not registered as a container in the form runtime, so child controls cannot be nested inside it in .frm files.

    +

    A horizontal container styled as a status bar, typically placed at the bottom of a form. Put Labels (or other controls) inside it as its panes: they sit side by side, each drawn with a sunken-panel border for the classic segmented status-bar look. Give the pane that should take the spare room Weight = 100. Panes can be nested in the designer or the .frm, or added at run time with CreateControl(form, "Label", name$, StatusBar1).

    No type-specific properties, events, or methods.

    +

    Container: Yes

    +

    Example

    +
    Begin StatusBar Status1
    +    Begin Label lblStatus
    +        Caption = "Ready"
    +        Weight  = 100
    +    End
    +    Begin Label lblPos
    +        Caption = "Line 1, Col 1"
    +    End
    +End

    Common Properties, Events, and Methods

    @@ -5466,14 +7076,29 @@ End Sub

    TextBox

    -

    VB Equivalent: TextBox -- DVX Widget: textbox (single-line text input, max 256 chars)

    +

    VB Equivalent: TextBox -- DVX Widget: textbox (single-line text input, 256 chars unless MaxLength says otherwise)

    A single-line text input field. Supports data binding via DataSource and DataField properties.

    Type-Specific Properties

    -
      Property     Type     Description
    -  ----------   ------   -------------------------------------------
    -  Text         String   The text content of the input field. Setting it clears the undo history.
    -  DataSource   String   Name of a Data control for data binding.
    -  DataField    String   Column name for data binding.
    +
      Property       Type     Description
    +  ------------   ------   -------------------------------------------
    +  Text           String   The text content of the input field. Setting it clears the undo history.
    +  MaxLength      Integer  The most characters the box holds (0 = the default, 256; at most 32767). A longer text is cut, and typing stops at the limit.
    +  PasswordChar   String   "" for a plain box; a character makes it a password box that shows that character for each one typed and does not copy or cut its text.
    +  Mask           String   "" for a plain box; an input mask makes it a masked box: # takes a digit, A a letter, * any character, and every other character of the mask is fixed text the cursor skips (Mask = "(###) ###-####" for a phone number).  The text keeps the mask's shape (empty places show _), and what the box held fills its places in order.  Mask = "" makes it a plain box again holding the formatted text, with its MaxLength from before.  A masked box is not a password box.
    +  DataSource     String   Name of a Data control for data binding.
    +  DataField      String   Column name for data binding.
    +  SelStart       Integer  Where the selection starts, or the cursor when nothing is selected (0 = before the first character).  Setting it moves the cursor there and selects nothing.
    +  SelLength      Integer  The number of characters selected (0 = none).  Setting it selects that many characters from SelStart.
    +  SelText        String   The selected text.  Setting it replaces the selection (or inserts at the cursor), leaves the cursor after the new text and fires Change; in a masked box it fills the places from the selection's start, as pasting does.
    +

    Type-Specific Methods

    +
      Method      Description
    +  ------      -----------
    +  Copy        Copy the selection to the clipboard (Ctrl+C); a password box copies nothing.
    +  Cut         Copy the selection to the clipboard and remove it (Ctrl+X); fires Change.  A password box does nothing.
    +  Paste       Put the clipboard's text in place of the selection (Ctrl+V; line breaks are left out); fires Change.
    +  SelectAll   Select the whole text (Ctrl+A).
    +  Undo        Take back the last change (Ctrl+Z); a second Undo puts it back.
    +

    The methods do what the keys do, so Cut, Paste and Undo do nothing in a read-only box. An Edit menu works on whichever box has the focus with Me.ActiveControl (see Form).

    Default Event: Change

    Example

    Begin TextBox Text1
    @@ -5494,22 +7119,54 @@ End Sub
      Property     Type      R/W   Description
       ----------   -------   ---   -------------------------------------------
       Text         String    R/W   The full text content. Setting it clears the undo history.
    -  CursorLine   Integer   R     Current cursor line number (1-based).
    + CursorLine Integer R Current cursor line number (1-based). + CursorCol Integer R Current cursor column (1 = the start of the line; a tab counts as one). + LineCount Integer R The number of lines (an empty text has one). + SelStart Integer R/W Where the selection starts, or the cursor when nothing is selected (0 = before the first character). Setting it moves the cursor there and selects nothing. + SelLength Integer R/W The number of characters selected (0 = none). Setting it selects that many characters from SelStart. + SelText String R/W The selected text. Setting it replaces the selection (or inserts at the cursor when nothing is selected), leaves the cursor after the new text and fires Change; Undo takes it back. + MaxLength Integer R/W The most characters the area holds (0 = the default, 65536; at most 262144). A smaller value cuts the text.

    Type-Specific Methods

      Method                                          Description
       ------                                          -----------
       AppendText text$                                Append text to the end of the buffer and invalidate the widget.
    +  ClearLineMarkers                                Remove every line marker.
    +  Copy                                            Copy the selection to the clipboard (Ctrl+C).
    +  CursorToEnd                                     Put the cursor after the last character, nothing selected.
    +  Cut                                             Copy the selection to the clipboard and remove it (Ctrl+X); fires Change.
       FindNext needle$, caseSensitive, forward        Search from the cursor and select the match. A forward search finds a match starting at the cursor; repeating it moves on to the next match. Returns True if found.
    +  GetLine(line%)                                  Returns the text of a line (1 = the first) without its line end; "" past the last line.
    +  GetSyntaxColor(index%)                          Returns the colour the syntax colouring draws that kind of text in (index as for SetSyntaxColor); -1 for the area's ForeColor.
       GetWordAtCursor()                               Returns the word under the cursor.
       GoToLine line%                                  Scroll to the given line and select it.
    +  Paste                                           Put the clipboard's text in place of the selection (Ctrl+V); fires Change.
       ReplaceAll needle$, replacement$, caseSensitive Replace all occurrences. Returns count of replacements.
    +  ReplaceSelection(needle$, replacement$, caseSensitive, forward)   When the selection is needle$, replace it (Change fires); then find the next needle$ in that direction, as FindNext does.  Returns True when it replaced or found something (a Replace button).
    +  SelectAll                                       Select the whole text (Ctrl+A).
       SetAutoIndent enabled                           Enable or disable automatic indentation.
       SetCaptureTabs enabled                          When True, Tab key inserts a tab/spaces (firing Change) instead of moving focus.
       SetCursor line%, col%                           Move the cursor to a line and column (both start at 1) without selecting anything. Values past the end clamp to the text.
    +  SetLineMarker line%, gutterColor&, backColor&   Mark a line (1 = the first): a dot of gutterColor in the line-number gutter (a breakpoint) and / or the line's background in backColor (the current line).  -1 leaves that part out; -1 for both removes the marker.  Markers stay on their line numbers when the text changes.
       SetShowLineNumbers show                         Show or hide the line number gutter.
    -  SetSyntaxMode mode$                             Activate built-in syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC). Pass "" to disable.
    +  SetSyntaxColor index%, color&                   The colour of one kind of text: 0 plain text, 1 keywords, 2 strings, 3 comments, 4 numbers, 5 operators, 6 types (and True, False, Nothing); -1 restores the default.
    +  SetSyntaxMode mode$                             Activate syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC, coloured as the DVX BASIC IDE colours it), "custom" (the program colours each line in the Colorize event). Pass "" to disable.
       SetTabWidth width%                              Set the tab stop width in characters.
    -  SetUseTabChar useChar                           When True, Tab inserts a tab character; when False, inserts spaces.
    + SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces. + Undo Take back the last change (Ctrl+Z); a second Undo puts it back. +

    Cut, Paste and Undo do nothing in a read-only area.

    +

    Type-Specific Events

    +
      Event         Parameters                Description
    +  -----------   -----------------------   -------------------------------------------
    +  GutterClick   LineNumber As Integer     The line-number gutter (shown by SetShowLineNumbers) was clicked beside a line: toggle a breakpoint there.  A click in the gutter does not move the cursor.
    +  Colorize      LineText As String, Colors As String   With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them).  Characters it leaves out stay plain.  It runs whenever lines are drawn, so it must be quick and must not change the area.
    +
    TextArea1.SetSyntaxMode "custom"
    +
    +Sub TextArea1_Colorize (LineText As String, Colors As String)
    +    Dim i As Integer
    +    For i = 1 To Len(LineText)
    +        If Mid$(LineText, i, 1) = "#" Then Mid$(Colors, i, 1) = CStr(vbSyntaxComment)
    +    Next
    +End Sub

    Default Event: Change

    Example

    TextArea1.SetShowLineNumbers True
    @@ -5522,10 +7179,17 @@ If TextArea1.FindNext("TODO", False, True) Then
         Print "Found at line"; TextArea1.CursorLine
     End If
     
    -' Replace all occurrences
    +'' Replace all occurrences
     Dim count As Integer
     count = TextArea1.ReplaceAll("old", "new", False)
    -Print "Replaced"; count; "occurrences"
    +Print "Replaced"; count; "occurrences" + +' A code editor: BASIC colours, a breakpoint dot on the clicked line +TextArea1.SetSyntaxMode "bas" + +Sub TextArea1_GutterClick (LineNumber As Integer) + TextArea1.SetLineMarker LineNumber, RGB(200, 0, 0), -1 +End Sub

    Common Properties, Events, and Methods

    @@ -5572,21 +7236,29 @@ End Sub

    TreeView

    VB Equivalent: TreeView -- DVX Widget: treeview

    -

    A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based).

    +

    A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based): an item's children follow it, before its next sibling. Adding children under an item that has none puts them right after it (index + 1, index + 2, ...), and moves the items after them along.

    +

    Type-Specific Properties

    +
      Property    Type      R/W   Description
    +  ---------   -------   ---   -------------------------------------------
    +  ListIndex   Integer   R/W   Index of the current (selected) node, -1 when none. Setting it selects that node as a click would: its parents are expanded, it is scrolled into view, and the Change event fires.

    Type-Specific Methods

      Method                                  Description
       ------                                  -----------
       AddChildItem parentIndex%, text$        Add a child node under the node at the given index.
       AddItem text$                           Add a root-level node.
       Clear                                   Remove all nodes.
    +  GetItemTag$(index%)                     Returns the text SetItemTag stored on the node ("" when none).
       GetItemText$(index%)                    Returns the text of the node at the given index.
    +  HasChildren(index%)                     Returns True if the node has nodes under it.
       IsExpanded(index%)                      Returns True if the node at the given index is expanded.
       IsItemSelected(index%)                  Returns True if the node at the given index is selected.
       ItemCount()                             Returns the total number of nodes (all levels).
       SetExpanded index%, expanded            Expand or collapse the node at the given index. Collapsing a node that hides the current item moves the current item to that node.
       SetItemSelected index%, selected        Select or deselect the node at the given index. Without multi-select this sets (or clears) the single selected node.
    +  SetItemTag index%, tag$                 Keep any text with the node (like VB's Node.Tag): a file path, a key. "" removes it.
       SetMultiSelect multi                    Enable or disable multi-select mode. Turning it off clears the multi-selection.
       SetReorderable reorderable              Enable or disable node reordering.
    +

    The Change event fires when the current node changes (a click, the arrow keys, or ListIndex set by the program).

    Default Event: Click

    Example

    TreeView1.AddItem "Animals"
    diff --git a/docs/dvx_help_viewer.html b/docs/dvx_help_viewer.html
    index 0a1539c2..1d524b56 100644
    --- a/docs/dvx_help_viewer.html
    +++ b/docs/dvx_help_viewer.html
    @@ -170,7 +170,7 @@ This is a warning message.
     

    Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.

    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).

    +

    Source lines longer than 4094 characters, and lines that contain a NUL byte, are errors. 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.

    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.

    diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index e42ef113..a899ce39 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -101,13 +101,19 @@ img { max-width: 100%; }
  • Libraries
  • Returns: Native pixel value suitable for direct framebuffer write.

    unpackColor

    void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b);
    -

    Reverse of packColor -- decode a native pixel value to 8-bit RGB components. For direct-color modes, reverses the shift/mask arithmetic. For 8-bit mode, looks up the palette entry at the given index.

    +

    Reverse of packColor -- decode a native pixel value to 8-bit RGB components. For direct-color modes, reverses the shift/mask arithmetic and widens a field narrower than 8 bits by repeating its bits, so black and full channels come back exact (white in a 16-bit mode reads as 255, 255, 255). For 8-bit mode, looks up the palette entry at the given index.

      Parameter   Description
       ---------   -----------
       d           Display context
    @@ -3280,9 +3574,34 @@ DVX_ABI_STAMP(mylib);
    rows Number of rows (tip to base) dir Direction the tip points color Fill color +

    drawScrollbar

    +
    void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize);
    +

    Paint a complete scrollbar from a ready-made thumb geometry: the sunken trough, an arrow button (drawScrollbarArrowButton) at each end, and a raised thumb thumbPos pixels past the first arrow button. The window scrollbars, the widget scrollbars and wmDrawScrollbarAt (which computes the thumb from a scroll state) all paint through it.

    +
      Parameter   Description
    +  ---------   -----------
    +  d           Display context
    +  ops         Blit operations vtable
    +  colors      Color scheme
    +  orient      ScrollbarVerticalE (arrows up and down) or ScrollbarHorizontalE (arrows left and right)
    +  x, y        Top-left corner of the bar
    +  length      Bar length in pixels along its axis, both arrow buttons included
    +  barW        Bar thickness, which is also the side of the square arrow buttons (SCROLLBAR_WIDTH for the standard look)
    +  thumbPos    Thumb offset in pixels from the end of the first arrow button
    +  thumbSize   Thumb length in pixels; 0 or less draws no thumb

    drawScrollbarArrow

    void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir);

    Draw the arrow glyph of a scrollbar button whose top-left is (x, y) and side is size, using drawArrowGlyph centered in the button with colors->scrollbarFg.

    +

    drawScrollbarArrowButton

    +
    void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir);
    +

    Draw one complete arrow button of a scrollbar, as the scrollbar painter does at each end of a bar: a size x size raised button (1px bevel on colors->scrollbarBg) whose top-left is (x, y), with the drawScrollbarArrow glyph pointing dir centered on it. The BASIC DrawScrollArrow method draws with it.

    +
      Parameter   Description
    +  ---------   -----------
    +  d           Display context
    +  ops         Blit operations vtable
    +  colors      Color scheme
    +  x, y        Top-left corner of the button
    +  size        Side of the square button in pixels
    +  dir         Direction the arrow points (ScrollArrowUpE/DownE/LeftE/RightE)

    drawFocusRect

    void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);

    Draw a 1px dotted rectangle (alternating pixels). Used for keyboard focus indicators, matching the Windows 3.x focus rectangle convention.

    @@ -3292,6 +3611,10 @@ DVX_ABI_STAMP(mylib); ops Blit operations vtable x, y, w, h Focus rectangle bounds color Dot color (packed) +

    drawFocusRectDots

    +
    typedef void (*DrawDotFnT)(void *ctx, int32_t x, int32_t y);
    +void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn, void *ctx);
    +

    The dots drawFocusRect draws, one fn call each and without clipping: the one definition of the pattern, for drawing it somewhere other than the back buffer. drawFocusRect plots them in the back buffer inside the clip rect; the BASIC DrawFocusRect method of forms and PictureBoxes sets them on the canvas.

    drawHLine

    void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);

    Draw a horizontal line (1px tall).

    @@ -3489,6 +3812,15 @@ DVX_ABI_STAMP(mylib);
      Parameter   Description
       ---------   -----------
       menu        Menu to append separator to
    +

    wmRemoveMenu

    +
    bool wmRemoveMenu(WindowT *win, MenuT *menu, const DisplayT *d);
    +

    Take one menu (a wmAddMenu result) off the window's menu bar and free it with its items and submenus. Removing the last menu removes the bar, and the content buffer is reallocated for d as by wmDestroyMenuBar. The content area changes size when the bar goes, so the caller lays the window out again (dvxResizeWindow with the same size does it and fires onResize). The BASIC RemoveMenu method uses it.

    +
      Parameter   Description
    +  ---------   -----------
    +  win         Window whose menu bar holds the menu
    +  menu        The menu to remove
    +  d           Display, for reallocating the content buffer when the bar goes
    +

    Returns: true if the menu was on the bar.

    wmRemoveMenuItem

    bool wmRemoveMenuItem(MenuT *menu, int32_t id);

    Remove a menu item by command ID. Searches the menu's items array and shifts remaining items down to close the gap.

    @@ -3527,6 +3859,9 @@ DVX_ABI_STAMP(mylib); bar Menu bar id Command ID enabled true = enabled, false = grayed out +

    wmMenuItemSetLabel

    +
    void wmMenuItemSetLabel(MenuItemT *item, const char *label);
    +

    Give an item a new label, written as wmAddMenuItem takes it: & before the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). The accelerator follows the new label; the item's ID, type and state stay. An open popup shows the new label the next time it is drawn. A NULL item or label does nothing.

    wmAddSubMenu

    MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label);

    Create a cascading submenu attached to a parent menu. The child MenuT is heap-allocated and freed when the parent window is destroyed.

    @@ -3605,22 +3940,36 @@ DVX_ABI_STAMP(mylib); colors Color scheme stack Window stack clipTo Dirty rectangle +

    wmDrawScrollbarAt

    +
    void wmDrawScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t scrollPos, int32_t visibleItems, int32_t totalItems);
    +

    Draw a standalone scrollbar of either orientation at the given screen coordinates, without a ScrollbarT struct. A vertical bar (ScrollbarVerticalE) is SCROLLBAR_WIDTH wide and length tall with up and down arrow buttons; a horizontal one (ScrollbarHorizontalE) is length wide and SCROLLBAR_WIDTH tall with left and right arrow buttons. Thumb size and position are computed from scrollPos / visibleItems / totalItems with the window scrollbars' rule; there is no thumb when everything is visible. wmVScrollbarAtClick maps a click on the bar to a new scrollPos. The BASIC DrawScrollBar method draws with it.

    +
      Parameter       Description
    +  ---------       -----------
    +  d               Display context
    +  ops             Blit operations vtable
    +  colors          Color scheme
    +  orient          ScrollbarVerticalE or ScrollbarHorizontalE
    +  x, y            Screen position (top-left corner of the bar)
    +  length          Bar length in pixels along its axis, both arrow buttons included
    +  scrollPos       Current first visible item index
    +  visibleItems    Visible item count (for thumb size)
    +  totalItems      Total item count

    wmDrawVScrollbarAt

    void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems);
    -

    Draw a standalone vertical scrollbar at the given screen coordinates, without a ScrollbarT struct. Used by popup lists (dropdown, combobox) that need a scrollbar inline within a popup overlay. Thumb size and position are computed from scrollPos / visibleItems / totalItems.

    +

    wmDrawScrollbarAt for a vertical bar h tall. Used by popup lists (dropdown, combobox) that need a scrollbar inline within a popup overlay.

      Parameter       Description
       ---------       -----------
       d               Display context
       ops             Blit operations vtable
       colors          Color scheme
       x, y            Screen position (top of scrollbar)
    -  h               Track height in pixels
    +  h               Bar height in pixels, both arrow buttons included
       scrollPos       Current top visible item index
       visibleItems    Visible item count (for thumb size)
       totalItems      Total item count

    wmVScrollbarAtClick

    -
    int32_t wmVScrollbarAtClick(int32_t h, int32_t relY, int32_t scrollPos, int32_t visibleItems, int32_t totalItems);
    -

    Apply a click on a scrollbar drawn by wmDrawVScrollbarAt with the same h and scroll state; relY is the click's offset from the top of the bar. Returns the new scrollPos: the arrow buttons step one item, the trough steps a page (visibleItems) toward the click, and the thumb leaves the position unchanged. The result is clamped to 0 .. totalItems - visibleItems. The thumb comes from the same geometry the paint uses, so a click always matches what is drawn.

    +
    int32_t wmVScrollbarAtClick(int32_t length, int32_t offset, int32_t scrollPos, int32_t visibleItems, int32_t totalItems);
    +

    Apply a click on a scrollbar drawn by wmDrawScrollbarAt (either orientation) or wmDrawVScrollbarAt with the same length and scroll state; offset is the click's offset along the bar from its start: down from the top of a vertical bar, right from the left end of a horizontal one. Only that offset matters, so the function serves both orientations. Returns the new scrollPos: the arrow buttons step one item, the trough steps a page (visibleItems) toward the click, and the thumb leaves the position unchanged. The result is clamped to 0 .. totalItems - visibleItems. The thumb comes from the same geometry the paint uses, so a click always matches what is drawn.

    Hit Testing

    wmHitTest

    int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hitPart);
    @@ -3809,6 +4158,9 @@ DVX_ABI_STAMP(mylib); int32_t wheelDirection 1 = normal, -1 = reversed int32_t wheelStep Lines per wheel notch (1-10, default 3) PlatformTicksT dblClickTicks Double-click interval in platformClock ticks + int32_t dblClickMs Double-click interval in milliseconds (as given to dvxSetMouseConfig) + int32_t mouseAccel Acceleration threshold last applied (MOUSE_ACCEL_*) + int32_t mouseMickeys Mickeys per 8 pixels last applied (MOUSE_SPEED_DEFAULT at start) uint32_t charHeightRecip 16.16 reciprocal of font.charHeight (popup row mapping) WindowT *modalWindow If non-NULL, only this window receives input int32_t dispatchDepth Callback nesting depth; destroys are deferred while > 0 @@ -3820,7 +4172,7 @@ DVX_ABI_STAMP(mylib); VideoModeInfoT *videoModes stb_ds array of settable modes char wallpaperPath[] Current wallpaper source path (empty = none) WallpaperModeE wallpaperMode Stretch, tile or center -

    The mouse speed and acceleration settings are not stored in AppContextT: dvxSetMouseConfig programs them into the platform layer, which re-applies them after every mouse driver reset.

    +

    dvxSetMouseConfig programs the mouse speed and acceleration into the platform layer, which re-applies them after every mouse driver reset; mouseAccel and mouseMickeys only record the last values applied (for dvxMouseToPrefs and settings pages).

    Initialization and Shutdown

    dvxInit

    int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
    @@ -4097,7 +4449,7 @@ DVX_ABI_STAMP(mylib); ctx Application context

    dvxLoadTheme

    bool dvxLoadTheme(AppContextT *ctx, const char *filename);
    -

    Load a theme file (INI format with [colors] section) and apply it. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names and malformed values are ignored, and channels are clamped to 0..255.

    +

    Load a theme file (INI format with [colors] section) and apply it through dvxColorsFromPrefs. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names and malformed values are ignored, and channels are clamped to 0..255. A missing file returns false.

      Parameter   Description
       ---------   -----------
       ctx         Application context
    @@ -4105,12 +4457,16 @@ DVX_ABI_STAMP(mylib);

    Returns: true on success.

    dvxSaveTheme

    bool dvxSaveTheme(const AppContextT *ctx, const char *filename);
    -

    Save the current color scheme to a theme file. When the file already exists its comments and other lines are kept and only the [colors] keys are updated. The new copy is written to a temporary file first and swapped in only when every write succeeded, so a failed save leaves the old theme intact.

    +

    Save the current color scheme to a theme file (the keys dvxColorsToPrefs writes). When the file already exists its comments and other lines are kept and only the [colors] keys are updated. The new copy is written to a temporary file first and swapped in only when every write succeeded, so a failed save leaves the old theme intact.

      Parameter   Description
       ---------   -----------
       ctx         Application context
       filename    Output file path

    Returns: true on success.

    +

    dvxColorsFromPrefs / dvxColorsToPrefs

    +
    bool dvxColorsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
    +void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    +

    The [colors] section of an INI handle, the one place its keys are read and written: dvxLoadTheme / dvxSaveTheme, the shell at start (DVX.INI) and Control Panel (OK, and its Cancel snapshot in a prefsCreate handle) all use these. FromPrefs reads every "name = r,g,b" key (malformed ones ignored, channels clamped to 0..255), repaints only when a color changed and returns true when any color key was read. ToPrefs writes every color.

    dvxColorName

    const char *dvxColorName(ColorIdE id);

    Return the INI key name for a color ID (e.g. "desktop", "windowFace").

    @@ -4136,11 +4492,15 @@ DVX_ABI_STAMP(mylib);

    Returns: true on success.

    dvxSetWallpaperMode

    void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode);
    -

    Change the wallpaper display mode and re-render. No effect if no wallpaper is loaded.

    +

    Change the wallpaper display mode; a loaded wallpaper is rendered again. With none loaded the mode is kept for the next one.

      Parameter   Description
       ---------   -----------
       ctx         Application context
       mode        WallpaperStretchE, WallpaperTileE, or WallpaperCenterE
    +

    dvxWallpaperFromPrefs / dvxWallpaperToPrefs

    +
    bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h);
    +void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    +

    The [desktop] wallpaper and mode keys of an INI handle (mode through prefsWallpaperChoices). FromPrefs applies them: a missing mode is WALLPAPER_MODE_DEFAULT and a missing wallpaper means none; the image is built again only when the path or mode differs from the current one. It returns false when the named image could not be loaded. ToPrefs writes the path (removes the key when there is none) and the mode.

    Mouse Configuration

    dvxSetMouseConfig

    void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep);
    @@ -4153,7 +4513,11 @@ DVX_ABI_STAMP(mylib); accelThreshold Double-speed threshold in mickeys/sec (0 = don't change) mickeyRatio Mickeys per 8 pixels (0 = don't change, 8 = default speed) wheelStep Lines to scroll per wheel notch (1-10, default 3) -

    The speed, acceleration and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change.

    +

    The speed, acceleration and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. The values are kept in AppContextT (wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep).

    +

    dvxMouseFromPrefs / dvxMouseToPrefs

    +
    void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h);
    +void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    +

    The [mouse] keys of an INI handle: wheel and acceleration (by name, through prefsWheelDirChoices and prefsAccelChoices), doubleclick (milliseconds), speed (mickeys per 8 pixels) and wheelspeed. FromPrefs applies them with dvxSetMouseConfig, defaults for missing keys (MOUSE_*_DEFAULT); ToPrefs writes the configuration in effect (an acceleration no choice has leaves that key alone). The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.

    Input Entropy

    dvxSetInputEntropyHook

    typedef void (*DvxEntropyHookT)(const uint8_t *data, int32_t len);
    @@ -4244,6 +4608,7 @@ const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
  • Letters are uppercased ('s' and 'S' are the same key).
  • Ctrl and Alt are always significant. Shift is significant for extended keys (F-keys, arrows, ...) and for any Ctrl/Alt combination, so Ctrl+S and Ctrl+Shift+S are different accelerators, as are F5 and Shift+F5.
  • A printable character typed without Ctrl/Alt matches regardless of Shift, because the character already encodes Shift ('+', 'A').
  • +
  • KEY_ENTER and KEY_BACKSPACE with ACCEL_CTRL or ACCEL_ALT match those keys however the BIOS reports them (with Alt it gives no ASCII byte; the keypad Enter has scancodes of its own), so (KEY_ENTER, ACCEL_ALT) is Alt with either Enter key.
  •   Parameter   Description
       ---------   -----------
    @@ -4251,6 +4616,20 @@ const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
    key ASCII character or KEY_Fxx / KEY_* constant modifiers Bitmask of ACCEL_CTRL / ACCEL_SHIFT / ACCEL_ALT cmdId Command ID passed to onMenu +

    dvxRemoveAccel

    +
    void dvxRemoveAccel(AccelTableT *table, int32_t cmdId);
    +

    Remove every shortcut of command cmdId from the table: a menu item that lost its key or went away. The BASIC menu Shortcut property and RemoveMenuItem use it.

    +
      Parameter   Description
    +  ---------   -----------
    +  table       Accelerator table (NULL is a no-op)
    +  cmdId       Command whose shortcuts go
    +

    dvxAccelFormat

    +
    void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize);
    +

    Write a key and its modifiers (given as for dvxAddAccel) as text, the way menus show it after a tab: the modifiers Ctrl, Alt and Shift in that order, each followed by +, then the key -- a letter or digit, F1 to F12, or one of the names Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up. Key 0 gives "".

    +
    dvxAccelFormat(KEY_ENTER, ACCEL_ALT, text, sizeof(text));   // "Alt+Enter"
    +

    dvxAccelParse

    +
    bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods);
    +

    Read key text as dvxAccelFormat writes it ("F2", "Ctrl+X", "Alt+Enter", "Shift+Del") into a key and ACCEL_* modifiers for dvxAddAccel or dvxRegisterHotkey. Case does not matter, blanks around the parts are ignored, and Delete and Insert are accepted for Del and Ins. Returns false, with *outKey 0, for empty text, an unknown modifier or an unknown key.

    System-Wide Hotkeys

    A hotkey is a keystroke that works whichever window has the focus (or when none has): its callback runs instead of anything the focused window would do with the keys, including its menu keys and accelerators. Program Manager's item shortcut keys are hotkeys. Hotkeys are not checked while a menu or a modal dialog is open, and keys the system handles first (Alt+Tab, Alt+F4, Ctrl+F12, Ctrl+Esc, F1, F10, Alt+Space) never reach them.

    A hotkey belongs to the app that was current when it was registered (ctx->currentAppId, set by the shell whenever app code runs). When an app exits, crashes or is ended from the Task Manager, the shell removes its hotkeys with dvxUnregisterAppHotkeys, so a callback never outlives the code it points into. The callback also runs with currentAppId set to that app.

    @@ -4310,14 +4689,14 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);

    Image I/O

    dvxLoadImage

    uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch);
    -

    Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. Caller must free with dvxFreeImage().

    +

    Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().

      Parameter     Description
       ---------     -----------
       ctx           Application context
       path          Image file path
       outW, outH    Output: image dimensions
       outPitch      Output: row pitch in bytes
    -

    Returns: Pixel buffer, or NULL on failure.

    +

    Returns: Pixel buffer, or NULL on failure. An image without pixels (a width or height of 0, which some BMP headers declare) is a failure for every image loader.

    dvxLoadImageFromMemory

    uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch);

    Load an image from a memory buffer (typically resource data). Same output format as dvxLoadImage(). Caller must free with dvxFreeImage().

    @@ -4355,7 +4734,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); --------- ----------- path Image file path outW, outH Output: image dimensions -

    Returns: true on success.

    +

    Returns: true on success; false for an unreadable image or one without pixels.

    dvxSaveImage

    int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);

    Save native-format pixel data to an image file: BMP when path ends in .bmp (case-insensitive), PNG otherwise.

    @@ -4429,6 +4808,14 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); outW, outH Output: image dimensions outPitch Output: row pitch

    Returns: Pixel buffer, or NULL if not found.

    +

    dvxResLoadIconData

    +
    void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize);
    +

    The icon of a DVX program, widget or library file as the bytes of its image file: its DVX_RES_NAME_ICON resource ("icon32", a program's icon), else DVX_RES_NAME_WIDGET_ICON ("icon24", a widget's toolbox icon). Decode it with dvxLoadImageFromMemory or dvxLoadImageAlpha. Caller must free() the buffer.

    +
      Parameter     Description
    +  ---------     -----------
    +  dxePath       Path to the program, widget or library file
    +  outSize       Output: size of the data in bytes
    +

    Returns: The data, or NULL when the file has neither resource (or is no DVX file).

    dvxResLoadText

    bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize);

    Load a text resource from a DXE file into a caller-provided buffer. Null-terminated and truncated to fit bufSize.

    @@ -4514,7 +4901,8 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); const char *tooltip Tooltip text (NULL = none) MenuT *contextMenu Right-click menu (NULL = none) uint32_t serial Unique per allocation, never reused while the process runs (tells "the same widget" from a new widget at a recycled address) - RectT paintClip Display clip when last painted (content-buffer coords, w = 0 until first paint); used by mouse dispatch inside clipping containers + RectT paintClip Display clip when last painted (content-buffer coords, w = 0 until first paint); used by mouse dispatch inside clipping containers + onEvent(w, name, args, argc) Custom (named) events: every event wgtFireEvent fires that has no universal callback below (see Custom Events)

    Universal Callbacks:

      Callback                                           Description
       --------                                           -----------
    @@ -4531,6 +4919,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
    onMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) Mouse moved onScroll(WidgetT *w, int32_t delta) Mouse wheel onValidate(WidgetT *w) Return false to cancel a write +

    The universal callbacks belong to the widget's host (the application or the form runtime). A widget class sees the same input earlier through its own class methods: WGT_METHOD_ON_MOUSE_EVENT, WGT_METHOD_ON_KEY, WGT_METHOD_ON_KEY_UP, WGT_METHOD_ON_FOCUS, WGT_METHOD_ON_BLUR and WGT_METHOD_ON_WHEEL run before the matching callback (see Method IDs).

    Size Specification Macros

      Macro            Description
       -----            -----------
    @@ -4552,7 +4941,9 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
    WCLASS_SWALLOWS_TAB Tab key goes to widget, not focus navigation WCLASS_RELAYOUT_ON_SCROLL Full relayout on scrollbar drag WCLASS_PRESS_RELEASE Click = press + release (Button, ImageButton) - WCLASS_ACCEL_WHEN_HIDDEN Accelerator matching works even when invisible + WCLASS_ACCEL_WHEN_HIDDEN Accelerator matching works even when invisible + WCLASS_HAS_TEXT The first field of w->data is the widget's own text (strdup / free) + WCLASS_RAISES_EVENTS The class fires the universal mouse, click, key and scroll callbacks itself (wgtFireEvent); the dispatcher does not call them for its widgets. Its class methods still get every event. A BASIC UserControl has it: its host sees only what the control's code raises. GotFocus / LostFocus still reach onFocus / onBlur.

    Window Integration

    wgtInitWindow

    WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win);
    @@ -4672,6 +5063,12 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); parentSize Parent dimension (for PERCENT mode) charWidth Font character width (for CHARS mode)

    Returns: Size in pixels.

    +

    wgtGetPaddingPx / wgtSetPaddingPx / wgtGetSpacingPx / wgtSetSpacingPx

    +
    int32_t wgtGetPaddingPx(const WidgetT *w);
    +void    wgtSetPaddingPx(WidgetT *w, int32_t pixels);
    +int32_t wgtGetSpacingPx(const WidgetT *w);
    +void    wgtSetSpacingPx(WidgetT *w, int32_t pixels);
    +

    A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return 0 when the size is unset (the container's default) or given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 restores the default) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.

    wgtLayout

    void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font);

    Execute the full two-pass layout algorithm on a tree at an explicit size. Pass 1 (bottom-up): compute minimum sizes. Pass 2 (top-down): allocate space with weighted distribution; a weighted child capped by maxW/maxH stops at its cap and the space it cannot take goes to the other weighted children. Window-managed trees are laid out by wgtInvalidate and the paint path; this is for laying out a tree on demand.

    @@ -4699,6 +5096,9 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); --------- ----------- ctx Application context enabled true = draw debug borders +

    wgtGetDebugLayout

    +
    bool wgtGetDebugLayout(void);
    +

    Returns true while the debug borders are drawn (the last value given to wgtSetDebugLayout; false at start).

    Dynamic Widget Registration

    wgtRegisterClass

    int32_t wgtRegisterClass(const WidgetClassT *wclass);
    @@ -4713,6 +5113,9 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
      Parameter   Description
       ---------   -----------
       classId     Type ID returned by wgtRegisterClass
    +

    wgtUnregisterIface

    +
    void wgtUnregisterIface(const char *name);
    +

    Remove the interface registered under name (wgtRegisterIface). A widget class made at run time -- a BASIC program's own UserControl -- leaves the registry this way when its owner is done with it, so the name can be registered again. Nothing happens when no interface has the name.

    wgtUnregisterAppClasses

    void wgtUnregisterAppClasses(int32_t appId);

    Release every class, API (wgtRegisterApi) and interface (wgtRegisterIface) registered while appId was the current app (AppContextT.currentAppId at registration time), since all of them point into the app's module. The shell calls it whenever it unloads an app (normal exit or End Task), once the app's windows and widgets are gone. appId 0 (boot-time widget modules) is ignored.

    @@ -4741,7 +5144,7 @@ static const ButtonApiT sApi = { WgtApiHeaderT { uint32_t abiVersion; uint32_t size; }: the DVX_ABI_VERSION the provider was built with and the sizeof its API struct. WGT_API_HEADER(apiType) Initializer for the header member: { DVX_ABI_VERSION, sizeof(apiType) }. WgtApiPrefixT { WgtApiHeaderT header; WgtApiFnT create; }: the part every API struct shares. Generic callers that do not know the widget's own API type use it. -
    Warning: The create function must be the first field after the header. BASIC's form runtime and the designer instantiate widgets through WgtApiPrefixT.create (see createWidgetByIface), cast to the signature WgtIfaceT.createSig names, so whichever field comes first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it.
    +
    Warning: The create function must be the first field after the header. wgtCreateByName (which the form runtime and the designer use) instantiates widgets through WgtApiPrefixT.create, cast to the signature WgtIfaceT.createSig names, so whichever field comes first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it.

    wgtGetApi

    const void *wgtGetApi(const char *name, uint32_t minSize);

    Retrieve a registered widget API struct by name, checked against the caller's view of it. A struct built for another DVX_ABI_VERSION, or smaller than minSize (an older, shorter table), is refused and the refusal logged.

    @@ -4839,7 +5242,11 @@ static const ButtonApiT sApi = { wclsQuickRepaint(w, outY, outH) WGT_METHOD_QUICK_REPAINT Fast partial repaint wclsScrollChildIntoView(parent, child) WGT_METHOD_SCROLL_CHILD_INTO_VIEW Scroll child visible wclsOnBlur(w) WGT_METHOD_ON_BLUR Widget lost keyboard focus - wclsOnWheel(w, delta) WGT_METHOD_ON_WHEEL Mouse wheel; true = consumed + wclsOnWheel(w, delta) WGT_METHOD_ON_WHEEL Mouse wheel; true = consumed + wclsOnMouseEvent(w, action, button, x, y, shift) WGT_METHOD_ON_MOUSE_EVENT Every press, release and move (hover too) + wclsOnKeyUp(w, keyCode, shift) WGT_METHOD_ON_KEY_UP Key released on the focused widget + wclsOnFocus(w) WGT_METHOD_ON_FOCUS Widget gained keyboard focus +

    The central dispatcher calls WGT_METHOD_ON_MOUSE_EVENT for every widget whose class has it, at exactly the points where it calls the universal mouse callbacks and on the same widget (the one under the cursor; inside a non-focusable WCLASS_NO_HIT_RECURSE container, a child with the method gets them itself, and a container with the method keeps them), and always before the callback. action is a WgtMouseActionE: WgtMouseDownE and WgtMouseUpE carry the 1-based button ordinal (1 left, 2 right, 3 middle), WgtMouseMoveE the MOUSE_* mask of the held buttons (0 is a hover move). x and y are content-relative (contentOffX / contentOffY subtracted), shift is the KEY_MOD_* Shift / Ctrl / Alt state. A class that captures the mouse on a press (sets sDragWidget in its WGT_METHOD_ON_MOUSE handler) also gets the moves while the button is held and the left release, wherever the pointer is. WGT_METHOD_ON_KEY_UP runs before onKeyUp with the same arguments, WGT_METHOD_ON_FOCUS before onFocus (the counterpart of ON_BLUR). If a class method destroys widgets, the callback after it is skipped. Classes without these methods see no change.

    The mouse wheel is offered first to an ON_WHEEL handler on the focused widget, then on each ancestor, with delta in notches (positive = down, the user's reverse-wheel setting already applied; the lines-per-notch preference is AppContextT.wheelStep). Returning true consumes the event. Otherwise a WCLASS_SCROLLABLE focused widget (or a scroll-container ancestor) receives Up/Down arrow keys, and failing that the window scrollbar moves. The widget's onScroll callback fires in every case.

    Method IDs

    Fixed constants used to index into WidgetClassT.handlers[]. Method IDs are stable ABI -- new methods are appended at the next sequential ID, never reordered or reused.

    @@ -4868,6 +5275,9 @@ static const ButtonApiT sApi = { 20 WGT_METHOD_SCROLL_CHILD_INTO_VIEW void (parent, child) 21 WGT_METHOD_ON_BLUR void (w) 22 WGT_METHOD_ON_WHEEL bool (w, delta) + 23 WGT_METHOD_ON_MOUSE_EVENT void (w, action, button, x, y, shift) + 24 WGT_METHOD_ON_KEY_UP void (w, keyCode, shift) + 25 WGT_METHOD_ON_FOCUS void (w) -- WGT_METHOD_MAX = 32 (room for future methods)

    WGT_CLASS_VERSION is DVX_ABI_VERSION (see Binary Interface Version). The framework rejects WidgetClassT structs whose version does not match.

    Widget Class Dispatch Table

    @@ -4901,6 +5311,7 @@ static const ButtonApiT sApi = { getFn Getter function pointer (NULL if write-only) setFn Setter function pointer (NULL if read-only) enumNames For WGT_IFACE_ENUM: NULL-terminated array of value names +

    A property is readable when getFn is not NULL and writable when setFn is not NULL -- the one rule for every interface (the form runtime, the designer and the property sheet all apply it). A dynamic interface (WgtIfaceT.dyn) puts WGT_DYN_ACCESS in getFn / setFn for "present"; they are never called. A FLOAT getter returns float and its setter takes float.

    WgtMethodDescT

    Describes an invokable method on a widget type.

      Field       Description
    @@ -4934,12 +5345,18 @@ static const ButtonApiT sApi = {
       WGT_SIG_INT_STR            20   void fn(WidgetT *, int32_t, const char *)
       WGT_SIG_STR_STR            21   void fn(WidgetT *, const char *, const char *)
       WGT_SIG_STR_BOOL           22   void fn(WidgetT *, const char *, bool)
    -  WGT_SIG_RET_INT_STR        23   int32_t fn(WidgetT *, const char *) -- status-returning string method (e.g. Load/Save)
    + WGT_SIG_RET_INT_STR 23 int32_t fn(WidgetT *, const char *) -- status-returning string method (e.g. Load/Save) + WGT_SIG_STR_STR_BOOL_BOOL 24 bool fn(WidgetT *, const char *, const char *, bool, bool) + WGT_SIG_RET_INT_INT 25 int32_t fn(const WidgetT *, int32_t) + WGT_SIG_DYNAMIC 26 bool fn(WidgetT *, const WgtValueT *args, int32_t argc, WgtValueT *ret) (WgtDynMethodFnT): any arguments, as they come +

    wgtSigInfo describes each signature (WgtSigInfoT: argc, retType and argTypes[], WGT_SIG_MAX_ARGS = 5; NULL for WGT_SIG_DYNAMIC). wgtIfaceCallMethod is the one place that calls through these signatures.

    WgtEventDescT

    -

    Describes an event beyond the standard common set (Click, DblClick, Change, GotFocus, LostFocus).

    +

    Describes an event beyond the standard common set (Click, DblClick, Change, GotFocus, LostFocus, the key and mouse events ...). The widget fires it with wgtFireEvent.

      Field       Description
       -----       -----------
    -  name        Event name (e.g. "Change", "Timer")
    + name Event name (e.g. "ColumnClick", "Timer") + params The handler's parameter list as BASIC writes it ("Topic As String", "" for none); NULL when the event is one of the BASIC master list's (Timer, Validate, Reposition) and takes its parameters from there +

    params is the only place a widget's own event parameters are written down: the IDE's handler stubs and designer.bas CtlEventParams$ read it. The shipped widgets' own events: ListView ColumnClick (Column As Integer, 0-based), TextArea GutterClick (LineNumber As Integer, 1-based), HelpPane LinkClick (Topic As String).

    WgtIfaceT

    Top-level interface descriptor registered by each widget DXE.

      Field         Description
    @@ -4955,15 +5372,92 @@ static const ButtonApiT sApi = {
       createArgs    Default numeric args for design-time instantiation
       isContainer   true = widget can hold child widgets
       defaultEvent  Default event name for form designer
    -  namePrefix    Auto-name prefix (NULL = derive from basName)
    + namePrefix Auto-name prefix (NULL = derive from basName) + dyn Dynamic interface hooks (WgtDynIfaceT), NULL for a widget written in C +

    WgtDynIfaceT

    +
    typedef struct {
    +    WidgetT *(*create)(WidgetT *parent, const WgtIfaceT *iface);
    +    bool     (*getProp)(WidgetT *w, int32_t propIdx, WgtValueT *out);
    +    bool     (*setProp)(WidgetT *w, int32_t propIdx, const WgtValueT *value);
    +    bool     (*callMethod)(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret);
    +    void      *ctx;
    +} WgtDynIfaceT;
    +

    A widget class whose properties and methods are not C functions with fixed signatures (a BASIC UserControl, a scripted widget) sets WgtIfaceT.dyn. The generic access functions then call these hooks instead of the descriptors' getFn / setFn / fn: create makes a widget for wgtCreateByName, propIdx and methodIdx index the interface's props[] and methods[]. Each hook returns false when it could not do the job. A value read is converted to the property's type; a method whose sig is a fixed WGT_SIG_* gets its arguments converted to that signature and its result converted to the signature's type, a WGT_SIG_DYNAMIC method gets them as they come. Strings returned follow the WgtValueT lifetime rule. ctx is the provider's own pointer.

    +

    Generic Values

    +

    WgtValueT

    +
    typedef struct {
    +    uint8_t type;   // WGT_IFACE_*
    +    union {
    +        int32_t     i;   // INT, BOOL (0 / 1), ENUM
    +        double      f;   // FLOAT, DOUBLE
    +        const char *s;   // STRING
    +    };
    +} WgtValueT;
    +

    One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.

    +

    wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText

    +
    int32_t     wgtValueToInt(const WgtValueT *v);
    +bool        wgtValueToBool(const WgtValueT *v);
    +double      wgtValueToFloat(const WgtValueT *v);
    +const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize);
    +

    Convert any value: numbers convert to each other (a FLOAT truncates toward zero and is clamped to the int32_t range), a BOOL is 0 / 1, a STRING is parsed (decimal, strtod, wgtParseBool), NONE is 0 / false / "". wgtValueToText returns the value's own text for a STRING, otherwise formats into buf (INT / ENUM decimal, BOOL "True" / "False", FLOAT %g); WGT_VALUE_TEXT_MAX (32) bytes hold any non-STRING value.

    +

    wgtParseBool

    +
    bool wgtParseBool(const char *text, bool defaultValue);
    +

    The .frm and property-sheet boolean rule: "True" or "-1" is true, "False" or "0" false (case-insensitive), anything else (and NULL) defaultValue.

    +

    Generic Widget Access

    +

    One implementation of "create, read, write and call by name" for every widget, static or dynamic. The form runtime, the IDE designer and C programs all use it; nothing else switches on WGT_SIG_* or WgtCreateSigE.

    +

    wgtIfaceSetType / wgtGetIfaceOf

    +
    void             wgtIfaceSetType(const char *name, int32_t typeId);
    +const WgtIfaceT *wgtGetIfaceOf(const WidgetT *w, const char **outName);
    +

    wgtIfaceSetType binds the interface registered under name to the widget class typeId (from wgtRegisterClass); every shipped widget calls it in wgtRegister after wgtRegisterIface. wgtGetIfaceOf then finds the interface of any widget of that class (NULL when none is bound); outName (may be NULL) receives the registered type name. wgtUnregisterClass drops the binding.

    +

    wgtResolveTypeName

    +
    const char *wgtResolveTypeName(const char *name);
    +

    The registered type name for a BASIC name ("CommandButton" -> "button", tried first) or for a type name that has an API or an interface; NULL when neither is registered.

    +

    wgtIfaceFindProp / wgtIfaceFindMethod / wgtIfaceFindEvent

    +
    const WgtPropDescT   *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propName);
    +const WgtMethodDescT *wgtIfaceFindMethod(const WgtIfaceT *iface, const char *methodName);
    +const WgtEventDescT  *wgtIfaceFindEvent(const WgtIfaceT *iface, const char *eventName);
    +

    Case-insensitive lookups of a descriptor by name (the first match); NULL when iface is NULL or nothing matches.

    +

    wgtCreateByName

    +
    WidgetT *wgtCreateByName(const char *name, WidgetT *parent);
    +

    Create a widget of type name (a BASIC or type name, wgtResolveTypeName) under parent: a dynamic interface's create hook, otherwise the API's create function called by the interface's createSig with its createArgs ("" for a text, an empty picture for WGT_CREATE_PARENT_DATA; a type without an interface is created as fn(parent)). Returns NULL when the type is unknown or creation failed.

    +

    wgtGetProp / wgtSetProp / wgtCallMethod

    +
    bool wgtGetProp(WidgetT *w, const char *propName, WgtValueT *out);
    +bool wgtSetProp(WidgetT *w, const char *propName, const WgtValueT *value);
    +bool wgtCallMethod(WidgetT *w, const char *methodName, const WgtValueT *args, int32_t argc, WgtValueT *ret);
    +

    By name on any widget, through its interface (wgtGetIfaceOf). False when the widget has no interface or no such property or method, otherwise as wgtIfaceGetProp / wgtIfaceSetProp / wgtIfaceCallMethod.

    +
    WgtValueT v = wgtValueStr("Right");
    +WgtValueT item = wgtValueStr("one");
    +WgtValueT count;
    +
    +wgtSetProp(label, "Alignment", &v);
    +wgtCallMethod(list, "AddItem", &item, 1, NULL);
    +wgtCallMethod(list, "ListCount", NULL, 0, &count);
    +

    wgtIfaceGetProp / wgtIfaceSetProp

    +
    bool wgtIfaceGetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, WgtValueT *out);
    +bool wgtIfaceSetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const WgtValueT *value);
    +

    Read or write property p (from iface's props[]) of w. A read is false when p is write-only or the dynamic hook failed (out is then the zero value of p's type); a STRING read is never NULL. A write converts value to p's type (wgtValueTo*; a STRING for an ENUM property is looked up in enumNames, case-insensitive) and is false when p is read-only, an enum name is unknown, or the dynamic hook refused.

    +

    wgtIfaceGetPropText / wgtIfaceSetPropText

    +
    bool wgtIfaceGetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize);
    +bool wgtIfaceSetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const char *text);
    +

    The same through the .frm / property-sheet text form: ENUM by value name, INT decimal, BOOL wgtParseBool (written "True" / "False"), FLOAT strtod / %g, STRING as it is. A read fails for an enum value that has no name.

    +

    wgtIfaceCallMethod

    +
    bool wgtIfaceCallMethod(const WgtIfaceT *iface, WidgetT *w, const WgtMethodDescT *m, const WgtValueT *args, int32_t argc, WgtValueT *ret);
    +

    Call method m with argc arguments, converted to the signature's types (wgtSigInfo); extra arguments are ignored. ret (may be NULL) receives the result, type WGT_IFACE_NONE for a void method. Returns false without calling when fewer arguments than the signature needs were given (ret is then the zero value of the result type), and when a dynamic method failed.

    +

    Custom Events

    +

    wgtFireEvent

    +
    bool wgtFireEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc);
    +

    Fire event name on w. The universal events go to their WidgetT callback with their arguments taken from args (a missing one is 0): Click -> onClick, DblClick -> onDblClick, Change -> onChange, GotFocus -> onFocus, LostFocus -> onBlur, KeyPress (KeyAscii) -> onKeyPress, KeyDown / KeyUp (KeyCode, Shift) -> onKeyDown / onKeyUp, MouseDown / MouseUp / MouseMove (Button, X, Y, or the VB form Button, Shift, X, Y) -> onMouseDown / onMouseUp / onMouseMove (which take no Shift), Scroll (Delta) -> onScroll (names case-insensitive). Every other name goes to w->onEvent; the universal names never do, so a host that sets both sees each event once. Validate is not mapped (onValidate answers a question; fire it directly). onEvent may write back into args (ByRef parameters; the form runtime writes every ByRef parameter back, a String as text that stays valid until the next event). Returns false when a handler destroyed widgets -- do not touch w then.

    +

    ListView (ColumnClick), TextArea (GutterClick) and HelpView (LinkClick) fire their own events this way after their C callbacks (wgtListViewSetHeaderClickCallback, wgtTextAreaSetGutterClick, wgtHelpViewSetLinkCallback), which stay for C programs; the event is skipped when the C callback destroyed widgets. A TextArea's gutter takes clicks when either is set.

    Property Data Type Constants

      Define              Value   Description
       ------              -----   -----------
       WGT_IFACE_STRING    0       const char *
       WGT_IFACE_INT       1       int32_t
       WGT_IFACE_BOOL      2       bool
    -  WGT_IFACE_FLOAT     3       float
    -  WGT_IFACE_ENUM      4       int32_t with named values
    + WGT_IFACE_FLOAT 3 float (WgtValueT: double) + WGT_IFACE_ENUM 4 int32_t with named values + WGT_IFACE_NONE 5 WgtValueT only: no value + WGT_IFACE_DOUBLE 6 double (BASIC Double)

    Create Function Signature Constants

      Define                              Value   Form
       ------                              -----   ----
    @@ -5187,7 +5681,9 @@ if (dvxDialogRun(ctx, dlg)) {
       DVX_RES_TEXT        2              Null-terminated string
       DVX_RES_BINARY      3              Arbitrary binary data (app-specific)
       DVX_RES_MAGIC       0x52585644     Footer signature ("DVXR" little-endian)
    -  DVX_RES_NAME_MAX    32             Max resource name length including null
    + DVX_RES_NAME_MAX 32 Max resource name length including null + DVX_RES_ONLY_MAGIC "DVXRESONLY" First bytes (NUL included, DVX_RES_ONLY_MAGIC_LEN) of a file that holds only resources +

    A file that starts with DVX_RES_ONLY_MAGIC has no code before its resources -- a control written in DVX BASIC, installed as a .wgt (see Using BASIC Controls from C). The loader reads such a widget file through the BASIC run-time library instead of loading it as a module.

    Structures

    DvxResDirEntryT

    Directory entry describing one resource (48 bytes on disk).

    @@ -5269,6 +5765,9 @@ if (dvxDialogRun(ctx, dlg)) {

    dvxPrefs.h -- INI Preferences System

    Handle-based API over classic INI files. Each prefsLoad/prefsCreate returns a PrefsHandleT that must be passed to all subsequent calls and freed with prefsClose when done. Multiple INI files can be open simultaneously.

    +

    BASIC programs reach the same functions through prefs.bas (dvxPrefsBas.c in libdvx): PrefsLoad, PrefsCreate, PrefsGet$, PrefsGetInt, PrefsGetBool, PrefsSet, PrefsSetInt, PrefsSetBool, PrefsRemove, PrefsKeyName$ (prefsKeyName, "" past the last key), PrefsSave, PrefsSaveAs and PrefsClose, on handles (1, 2, ...; 0 = none) that belong to the program that made them and are closed, unsaved, when it ends. True is -1, False 0. PrefsDvxIni$ is DVX_INI_PATH, PrefsSetVideoMode is prefsSetVideoMode, and PrefsVideoWidth / PrefsVideoHeight / PrefsVideoBpp are the three values of prefsGetVideoMode (DVX_DEFAULT_VIDEO_* for a missing key or a bad handle). The choice tables below are PREFS_CHOICE_ACCEL (0), PREFS_CHOICE_WALLPAPER (1) and PREFS_CHOICE_WHEEL (2) there: PrefsChoiceCount, PrefsChoiceLabel$, PrefsChoiceName$ (iniName), PrefsChoiceValue, PrefsChoiceIndex, PrefsChoiceIndexOfValue, PrefsChoiceDefault (the index of the choice MOUSE_ACCEL_DEFAULT / WALLPAPER_MODE_DEFAULT / MOUSE_WHEEL_DIR_DEFAULT names; -1 for an unknown table), and PrefsGetChoice(handle, section, key, table) with the table's default. PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) is prefsGetChoice on a table the program gives as "|"-separated names (each choice's iniName and label is its name, its value its index).

    +
    PrefsHandleT *prefsBasHandle(int32_t handle);
    +

    The settings behind a prefs.bas handle, NULL for 0 or a handle that is not open. Other libraries' BASIC entry points that take a prefs.bas handle (system.bas, joystick.bas, net.bas, print.bas, sound.bas) resolve it with this; libdvx is loaded first, so the symbol is always there.

    Constants

      Define          Value                                Description
       ------          -----                                -----------
    @@ -5389,7 +5888,7 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT];
     

    The entry counts are PREFS_ACCEL_CHOICE_COUNT (4), PREFS_WALLPAPER_CHOICE_COUNT (3) and PREFS_WHEEL_DIR_CHOICE_COUNT (2). MOUSE_WHEEL_DIR_NORMAL (1) and MOUSE_WHEEL_DIR_REVERSED (-1) in dvxTypes.h are also the wheelDir values for dvxSetMouseConfig.

    -

    Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. The shell reads [mouse] wheel, [mouse] acceleration and [desktop] mode through prefsGetChoice at startup, and the Control Panel builds its dropdowns and writes the INI from the same tables.

    +

    Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. dvxMouseFromPrefs and dvxWallpaperFromPrefs (dvxApp.h) read [mouse] wheel, [mouse] acceleration and [desktop] mode through prefsGetChoice, and the Control Panel builds its dropdowns from the same tables.

    prefsChoiceIndex

    int32_t prefsChoiceIndex(const PrefsChoiceT *choices, int32_t count, const char *name);

    Return the index of the choice whose iniName equals name (case-insensitive), or -1 when there is none (also for a NULL name).

    @@ -5662,8 +6161,9 @@ void widgetTextSet(WidgetT *w, const char *text);
    PlatformTicksT platformClock(void); PlatformTicksT platformClockHz(void); -#define PLATFORM_MS_TO_TICKS(ms) -

    Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, timers), advancing platformClockHz() ticks per second. PlatformTicksT is unsigned so "now - then" is wrap-safe: always compare intervals by subtraction, never raw tick values. PLATFORM_MS_TO_TICKS converts milliseconds to ticks at the platform rate.

    +#define PLATFORM_MS_TO_TICKS(ms) +#define PLATFORM_TICKS_TO_MS(ticks) +

    Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, timers), advancing platformClockHz() ticks per second. PlatformTicksT is unsigned so "now - then" is wrap-safe: always compare intervals by subtraction, never raw tick values. PLATFORM_MS_TO_TICKS converts milliseconds to ticks at the platform rate, PLATFORM_TICKS_TO_MS ticks to milliseconds (the millisecond clock the conn and xfer engines take is PLATFORM_TICKS_TO_MS(platformClock())).

    platformEntropyStamp

    uint64_t platformEntropyStamp(void);

    Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook.

    @@ -6009,8 +6509,9 @@ audioFmFree(audio, v); // key off, back to everyone +

    A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, volume) writes all three (an empty driver stores auto, a midi index out of range stores auto). audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.

    Desktop Sounds

    -

    The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file (a path from the DVX folder), empty or missing for none; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). The default set, made by DVX's own sound generator at build time, is in AUDIO_SOUNDS_DIR (config/sounds).

    +

    The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file (a path from the DVX folder), empty or missing for none; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in AUDIO_SOUNDS_DIR (config/sounds).

      Key           Event
       ---           -----
       default       the default beep (dvxBeep; a message box without an icon)
    @@ -6030,13 +6531,71 @@ audioFmFree(audio, v);                          // key off, back to everyone
    -

    audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen).

    +

    audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for).

    Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Sound Blaster, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: of each kind (FM chip, MIDI port) the first that finds its hardware is kept, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.

    Writing a Driver

    A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.

    A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM or AUDIO_SYNTH_MIDI, .open = ..., .close = ..., .write = ... or .put = ...). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready. Both run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver.

    BASIC

    src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.

    +

    For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile: a voice number, or 0 with SoundError$); SoundFolder$ (AUDIO_SOUNDS_DIR); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs) and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.

    +

    The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped) and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*) and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).

    +

    Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).

    +
    +
    +

    BASIC Development Library

    +

    basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h) and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub and debug.bas share.

    +

    Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h

    +

    Compiling (basBuild.h)

    +
      Function                Description
    +  --------                -----------
    +  basBuildAddSource       Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map.
    +  basBuildConcatProject   Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource.
    +  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY or BAS_COMPILE_NO_MODULE.
    +  basBuildErrorText       A compiler message without its "Line N, Col M: " prefix.
    +  basBuildMapLine         Line of the concatenated source to file and file line.
    +  basBuildMapFileLine     File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike).
    +  basBuildSpecFromProject Fill a BasBuildSpecT's project metadata from an open .dbp.
    +  basBuildApp             Build an .app from a module, the .frm texts and the .ctl headers (ctlSources). A NULL selfPath takes the stub from BAS_BUILD_STUB_CARRIER (this library).
    +  basBuildControl         Build a control file (.wgt, resources only) from a control project's module and .ctl headers: the module, each header as CTLn with its toolbox icon24 and name resources, the project's metadata and help file. The IDE's Make Control and CompBuildApp of a control project.
    +  basBuildIsControlProject  True when an open .dbp is a control project ([Project] Type = Control).
    +  basBuildWrapForPath     The kind of a project file by its extension.
    +

    Validating Control Members (basValidate.h)

    +

    A BasValidatorT (zeroed) collects the forms, controls and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.

    +

    Debugging (basDebug.h)

    +

    Every function takes the paused program's VM (or NULL when there is none).

    +
      Function                Description
    +  --------                -----------
    +  basDbgCurProcIdx        The compiled procedure the paused VM is in (-1: module level).
    +  basDbgProcNameForPc     Procedure name holding a code address (BAS_DBG_MODULE_FRAME before every procedure).
    +  basDbgVarVisible        Whether a debug variable is in scope where the VM is paused (QBasic rules: SHARED, STATIC, form and member variables).
    +  basDbgFindVar           The visible debug variable of a name.
    +  basDbgVarSlot           The live value slot of a debug variable.
    +  basDbgResolvePath       The live slot of var, var(i, j), var.field or var(i).field (a TYPE field or a class member).
    +  basDbgFormatValue       Display text of a value (strings quoted, #dates#, array bounds, class names, Collection item counts).
    +  basDbgLocals            The visible variables as rows (name, type, value) through a BasDbgRowFnT.
    +  basDbgCallStack         The call stack as rows (procedure, line).
    +  basDbgEval              A watch: a variable path read directly, any other expression compiled and run as a snippet.
    +  basDbgExec              An Immediate-window line: assignment to the paused program's variable, a statement, or an expression to print.
    +  basDbgSetVar            Set a variable path to the value of an expression (scalars).
    +  basDbgSnippetVm         Compile a snippet whose globals are the paused program's visible variables, with their values copied in.
    +  basDbgRunSnippet        Run a snippet in bounded slices (an endless loop is abandoned).
    +  basDbgAppendDropping    Append to a fixed output buffer, dropping the oldest lines.
    +

    Procedures (basProcs.h)

    +

    basProcDeclAt, basProcSkipToEnd and basProcNameFromDecl find the SUB / FUNCTION blocks of source text without compiling it (BAS_PROC_KW_END_SUB / BAS_PROC_KW_END_FUNCTION end them). The IDE's one-procedure-at-a-time editor and CompProcList$ use them.

    +

    Source Text (basSource.h)

    +

    Source text read with the compiler's lexer, line by line, on text that need not compile: basSourceTokens reports every token with its kind (BasSrcKindE: keyword, identifier, string, number, date, symbol, comment, error), line, column and source span; basSourceInCode tells whether an offset is code (not in a string literal or a comment); basSourceRename renames a form or control (oldName followed by '.' or '_' as a whole word), optionally only in code; basSourceWithEvents and basSourceClassEvents report the WithEvents variables and the Events of a class module. The IDE's rename (ideRenameInCode, also after the Menu Editor renames items) and its Object / Event lists use them; compiler.bas has CompTokens$, CompInCode, CompRenameInCode$, CompWithEvents$ and CompClassEvents$.

    +

    Help Topics (basHelpMap.h)

    +

    basHelpKeywordTopic gives the DVX BASIC help topic of a keyword or built-in function (NULL for none) and basHelpCtrlTopic the topic of a control type (BAS_HELP_CTRL_PREFIX and the type name in lower case): what F1 opens in the IDE, and CompHelpTopic$ / CompHelpCtrlTopic$.

    +

    Projects (basProject.h)

    +

    The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl) or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjGet$ and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.

    +

    A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$ and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).

    +

    Running a Program (basRunHost.h, in basrt.lib)

    +

    basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path and its CONFIG / DATA folders, COMMAND$, the print, input and DoEvents callbacks, the screen window's name, the help file and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.

    +

    VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.

    +

    From BASIC

    +

    compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.

    +

    CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.

    BASIC Runtime Library

    @@ -6044,6 +6603,7 @@ audioFmFree(audio, v); // key off, back to everyoneHeaders: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h

    Value System (values.h)

    Virtual Machine (vm.h)

    +

    The .frm Model (frmModel.h)

    Value System

    @@ -6262,6 +6822,7 @@ typedef bool (*BasDoEventsFnT)(void *ctx); hideForm void (*)(ctx, formRef) Hide a form (keep in memory). msgBox int32_t (*)(ctx, message, flags) Display a message box. inputBox BasStringT *(*)(ctx, prompt, title, defaultText) Display an input box. + loadFormText void *(*)(ctx, text) LoadFormText: a form from .frm text (its own copy when the name is loaded), NULL when it does not load. ctx void * User pointer passed to all callbacks.

    SQL Callbacks

    For database integration.

    @@ -6289,17 +6850,60 @@ typedef bool (*BasDoEventsFnT)(void *ctx);
    void basVmSetExternCallbacks(BasVmT *vm,
         const BasExternCallbacksT *ext);

    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.

    -
      Field           Signature                                                            Description
    -  -----           ---------                                                            -----------
    -  resolveExtern   void *(*)(ctx, libName, funcName)                                    Resolve a native function by library and symbol name. Cached after first call.
    -  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.
    +

    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, a BINARY STRING (binaryMask bit i set for argument i) as the char pointer followed by an int32_t length, SINGLE as one 4-byte float, DOUBLE as 8 bytes, everything else as one int32_t. When binaryMask has BAS_EXTERN_BINARY_RET the result is a BINARY STRING: an int32_t * is pushed last and the result is that many bytes at the returned pointer. DOUBLE and SINGLE returns are read from the x87 ST(0) register; INTEGER returns keep only the low 16 bits.

    +
      Field           Signature                                                                        Description
    +  -----           ---------                                                                        -----------
    +  resolveExtern   void *(*)(ctx, libName, funcName)                                                Resolve a native function by library and symbol name. Cached after first call.
    +  callExtern      BasValueT (*)(ctx, funcPtr, libName, funcName, args, argc, retType, binaryMask)  Call a resolved native function, marshalling arguments and return value.
    +  ctx             void *                                                                           User pointer passed to both callbacks.
    +

    A native SUB is called with OP_CALL_EXTERN like a FUNCTION and leaves a dummy result on the stack; a call written as a statement drops it, as it drops a FUNCTION's result.

    +

    Terminal Controls for Libraries

    +

    A library's BASIC functions can show a byte stream on a Terminal control the way SerAttach and CommAttach do (the connection library's ConnAttach is one). The form runtime (formrt.h, exported from basrt) keeps the binding safe:

    +
    typedef int32_t (*BasTermWriteFnT)(void *ctx, const uint8_t *data, int32_t len);
    +typedef void    (*BasTermUnbindFnT)(void *ctx);
    +
    +WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind);
    +void     basFormRtTermUnbind(void *ctx);
    +void     basFormRtTermForget(void *ctx);
    +

    basFormRtTermBind finds the Terminal control ctrlName on the running program's forms, ends whatever fed it before (a serial port, a packet link, another binding) and ctx's own earlier binding, sends what is typed there to writeFn, and returns the widget for the library to write what arrives to (NULL when there is no Terminal of that name). When the control goes away -- its form unloads, it is removed, another binding takes it -- the runtime ends the binding and calls onUnbind(ctx): the library must forget the widget. basFormRtTermUnbind ends a binding the library no longer wants; basFormRtTermForget drops it without touching the widget, for a library whose program was ended (its windows are already gone). A library outside basrt looks these up with dlsym when it first needs them.

    Form Context

    Set the active form context during event dispatch.

    void basVmSetCurrentForm(BasVmT *vm, void *formRef);
     void basVmSetCurrentFormVars(BasVmT *vm,
         BasValueT *vars, int32_t count);
    +

    Collections for the Host

    +

    A host hands BASIC a Collection of its own objects (the form runtime's Form.Controls) with these; basValInstance of the new Collection takes its one reference.

    +
    BasObjectT *basVmCollectionNew(BasVmT *vm);
    +int32_t     basVmCollectionAdd(BasObjectT *coll, BasValueT item, const char *key);   // key NULL: none; 0 or the BASIC error number
    +

    Control Types and the Design Surface

    +

    formrt/formDesign.c holds what a form designer needs, shared by the DVX BASIC IDE's designer and by designer.bas (DECLARE LIBRARY "basrt": CtlTypeCount, CtlTypeName, CtlTypeFlags, CtlTypeNamePrefix, CtlTypeDefaultEvent, CtlTypeTooltip, CtlTypeIconFile, CtlPropCount, CtlPropName, CtlPropType, CtlPropEnum, CtlMethodList, CtlEventList, CtlEventParams, CtlTypeEventParams, and FrmFormName, FrmControlNames, FrmCode, ClsClassName over the basRes.h scanners). CtlTypeTooltip and CtlTypeIconFile fall back to a UserControl's header (its Description; its ToolboxIcon file, basUcIconPath) when the type has no widget file:

    +
      Function                                                  Description
    +  --------                                                  -----------
    +  basCtlIface(typeName)                                     The widget interface of a BASIC type name, or NULL.
    +  basCtlPropAt(typeName, idx, out) / basCtlPropFind(...)    A type's properties as BasCtlPropT (name, type, readable, writable, enum names): the runtime table's ("Form", "Menu" or the common one), the widget's own winning on a name, then the widget's others. The IDE's properties window uses basCtlPropFind.
    +  basCtlMethodAt / basCtlEventAt(typeName, idx)             A type's methods (the widget's, then the common ones) and events (the master BAS_EVENT_LIST's for that kind of object, then the widget's); the IDE's event list uses basCtlEventAt.
    +  basCtlEventParams(typeName, eventName)                    The parameter list of a handler of a type's event: the widget's own (WgtEventDescT params), else the master list's; typeName NULL searches every widget after the master list, BAS_UC_OBJECT ("UserControl", the object in a UserControl's own code) has BAS_UC_EVENT_LIST's alone. The IDE's handler skeletons, CtlEventParams and CtlTypeEventParams.
    +  basCtlDefaultEvent, basCtlNamePrefix, basCtlIsContainer   As the toolbox and designer need them.
    +  basCtlResPath(typeName, resBase, resName, size)           The widget file and the (suffixed) resource name of its toolbox icon (BAS_CTL_RES_ICON) or tooltip (BAS_CTL_RES_NAME).
    +  basCtlTypeAt(rt, idx)                                     The idx-th control type rt sees (basUcIfaceVisible; rt NULL: a C host) in alphabetical order of BASIC name, NULL past the end. The IDE's toolbox and CtlTypeName list the types in this order; the registry's own (wgtIfaceAt) is the order the widget files loaded in.
    +  basDsgnHandleOrigin / basDsgnHandleAt / basDsgnCursorAt   Selection handle geometry: where handle h of a widget is, the resize handle under a point, the mouse pointer there.
    +  basDsgnPaintHandles(ctx, win, w)                          Paints a widget's eight selection handles over a window's painted content.
    +  basDsgnPropertyChanged(w, propName)                       The code of UserControl widget w called PropertyChanged: a form in DesignMode holding it raises DesignPropertyChanged (CtrlName, PropertyName). formCtl.c calls it beside the C hook (basUcSetPropChangedHook).
    +  basDsgnWidgetHit(w, x, y)                                 True when a point is on a visible widget.
    +

    Form.DesignMode replaces the form window's mouse, key, paint and cursor handlers while it is True (the controls get no input, and fireCtrlEvent drops their events), raises DesignMouse, DesignKey and DesignPropertyChanged, and paints DesignSelection's handles after the widgets. Form.Controls, Control.Parent / Container and MoveBefore (basFormRtMoveControl) are dispatched there too.

    +

    UserControls for a Host

    +

    formrt/formCtl.c registers UserControls (widget classes written in BASIC) with the widget system. A program's own controls (basFormRtRegisterCtl, the stub and the IDE's runs) and a designer's (basCtlLoadDesign) get private registry keys, the class name plus a serial, so the same project running and being designed, or two programs, never share one; a control file's (basCtlLoadFile) keeps its name. Resolve names through these instead of wgtGetIface:

    +
      Function                                                  Description
    +  --------                                                  -----------
    +  basCtlLoadDesign(rt, module, headers, count, path)        Designer rt's controls (rt: a BASIC program designing forms, compiler.bas CompDesignControls; NULL: a C host, the IDE): the .ctl headers' classes of module (the caller keeps it until basCtlUnloadDesign or rt's end), run without the module-level code, App.Path the folder of path. Returns how many were registered (a name a native or installed control has, or rt's own control has, is not). The set rt registered before is let go first.
    +  basCtlUnloadDesign(rt)                                    Lets designer rt's controls go (their widgets keep their last picture and run no more code). False while their code runs. basUcRuntimeEnd lets a program's go when it ends.
    +  basUcIconPath(iface, out, outSize)                        The ToolboxIcon picture file of a control file's or a designer's UserControl: the header's name in the folder of the path it was registered with (the project's, for a designer's). False when it names none or iface is no such control.
    +  basUcResolveType(rt, typeName)                            The registry key of a BASIC type name in rt's program: its own UserControl first (its program's or the ones it registered for design; rt NULL: the C host's design controls), else wgtResolveTypeName -- never another owner's private control (NULL then).
    +  basUcOwnIface(rt, typeName)                               The interface of rt's own UserControl (its program's or a design one; rt NULL: the C host's design controls), NULL for none. basCtlIface looks here first, for the current program (basFormRtCurrent).
    +  basUcIfaceVisible(rt, iface)                              False when iface is a private UserControl of another owner; a list of control types (basCtlTypeAt) leaves those out.
    +  basUcIfaceHeader(iface)                                   The parsed header (BasUcHeaderT: name, size, BackColor, ToolboxIcon, Description, DefaultEvent) of a UserControl's interface, NULL for any other.
    +  basUcSetPropChangedHook(fn)                               fn(widget, propName) runs when a control's code calls PropertyChanged: a C designer reads its property sheet again (a BASIC one gets DesignPropertyChanged, basDsgnPropertyChanged).
    +  basDsgnSetDesignWindow(win, on) / basDsgnIsDesignWindow   Marks a window as a designer's (unmark it when it is destroyed): UserControls on it see UserMode False and get no user input.

    Stack Access

    Push and pop values on the evaluation stack for host integration.

    bool basVmPush(BasVmT *vm, BasValueT val);
    @@ -6307,7 +6911,8 @@ bool basVmPop(BasVmT *vm, BasValueT *val);

    Both return true on success, false on stack overflow/underflow.

    Error Reporting

    const char *basVmGetError(const BasVmT *vm);
    -

    Returns the current error message string. Valid after basVmRun returns BAS_VM_ERROR.

    +

    Returns the current error message string. Valid after basVmRun returns BAS_VM_ERROR, and after a call of a SUB from the host (an event handler) that an untrapped error stopped.

    +

    The form runtime ends the program on every untrapped error, in module-level code and in event handlers alike (basFormRtRuntimeError for errors the runtime finds, the same path for the VM's own): the error is logged to DVX.LOG (summary line, details indented), the VM halts (ended), the runtime is marked terminated so its event loop ends, and the BASIC Runtime Error box shows it -- unless the host set suppressErrorDialog (the IDE), which reports it itself. The VM's message ("Runtime error 11 on line 8: Division by zero") stays readable with basVmGetError.

    Sub/Function Calls from Host

    Call a SUB or FUNCTION by its code address from host code.

    bool basVmCallSub(BasVmT *vm, int32_t codeAddr);
    @@ -6329,15 +6934,31 @@ void    basVmStepInto(BasVmT *vm);
     void    basVmStepOver(BasVmT *vm);
     void    basVmStepOut(BasVmT *vm);
     void    basVmRunToCursor(BasVmT *vm, int32_t line);
    -int32_t basVmGetCurrentLine(const BasVmT *vm);
    -
      Function              Description
    -  --------              -----------
    -  basVmSetBreakpoints   Set the breakpoint list (sorted array of source line numbers, host-owned memory).
    -  basVmStepInto         Break at the next OP_LINE instruction.
    -  basVmStepOver         Break when call depth returns to the current level.
    -  basVmStepOut          Break when call depth drops below the current level.
    -  basVmRunToCursor      Break when execution reaches the specified source line.
    -  basVmGetCurrentLine   Get the current source line number (from the last OP_LINE instruction).
    +void basVmClearStepState(BasVmT *vm); +void basVmSetBreakpointCallback(BasVmT *vm, + BasBreakpointFnT fn, void *ctx); +void basVmSetDebugPaused(BasVmT *vm, bool paused); +void basVmResume(BasVmT *vm); +void basVmStop(BasVmT *vm); +int32_t basVmGetCurrentLine(const BasVmT *vm); +int32_t basVmGetErrorLine(const BasVmT *vm); +void basVmSetAppPaths(BasVmT *vm, const char *path, + const char *config, const char *data); +
      Function                    Description
    +  --------                    -----------
    +  basVmSetBreakpoints         Set the breakpoint list (sorted array of source line numbers, host-owned memory).
    +  basVmStepInto               Break at the next OP_LINE instruction.
    +  basVmStepOver               Break when call depth returns to the current level.
    +  basVmStepOut                Break when call depth drops below the current level.
    +  basVmRunToCursor            Break when execution reaches the specified source line.
    +  basVmClearStepState         Cancel a pending step or run-to-cursor request.
    +  basVmSetBreakpointCallback  The pause notification for SUBs the host calls (see below).
    +  basVmSetDebugPaused         Break mode on or off: while on, basVmCallSub refuses to run event handlers.
    +  basVmResume                 Leave break mode; a handler paused at a breakpoint carries on.
    +  basVmStop                   Stop a debugged program: step state cleared, breakpoints detached, break mode ended, the running code unwinds.
    +  basVmGetCurrentLine         Get the current source line number (from the last OP_LINE instruction).
    +  basVmGetErrorLine           The source line where the current runtime error was raised (0: none).
    +  basVmSetAppPaths            Set App.Path, App.Config and App.Data.

    The breakpoint callback notifies the host when a breakpoint fires during nested sub calls:

    typedef void (*BasBreakpointFnT)(void *ctx, int32_t line);

    BasModuleT

    @@ -6355,6 +6976,18 @@ int32_t basVmGetCurrentLine(const BasVmT *vm); int32_t procCount; BasFormVarInfoT *formVarInfo; int32_t formVarInfoCount; + BasSlotInitT *globalInits; + int32_t globalInitCount; + BasSlotInitT *localInits; + int32_t localInitCount; + BasLineNumberT *lineNumbers; + int32_t lineNumberCount; + BasClassInfoT *classes; + int32_t classCount; + BasEnumInfoT *enums; + int32_t enumCount; + BasWithEventsT *withEvents; + int32_t withEventsCount; BasDebugVarT *debugVars; int32_t debugVarCount; BasDebugUdtDefT *debugUdtDefs; @@ -6374,15 +7007,112 @@ int32_t basVmGetCurrentLine(const BasVmT *vm); procCount Number of procedures. formVarInfo Per-form variable counts and init code addresses. formVarInfoCount Number of forms with form-scoped variables. + globalInits The starting type of each typed global slot (kept in a release build). + globalInitCount Number of global slot entries. + localInits The starting type of each typed local slot, per procedure (BasProcEntryT initFirst / initCount). + localInitCount Number of local slot entries. + lineNumbers The program's QBasic line numbers in source order (ERL reports them). + lineNumberCount Number of line numbers. + classes Class modules and UserControls (BasClassInfoT): members, events, Implements maps. + classCount Number of classes. + enums The Enums UserControl properties are declared As (BasEnumInfoT: member names and values in declaration order; a class member's enumIdx names one), so a host shows and stores such a property by member name. Kept in a release build. + enumCount Number of Enums. + withEvents WithEvents variables and their handlers. + withEventsCount Number of WithEvents variables. debugVars Variable names and metadata for the debugger. debugVarCount Number of debug variable entries. debugUdtDefs UDT type definitions for the debugger. debugUdtDefCount Number of debug UDT definitions.
    +
    +

    The .frm Model

    +

    A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields and every other "Key = Value" pair as text), its menu items and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).

    +

    Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)

    +

    Types

    +
      Type            Description
    +  ----            -----------
    +  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    +  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies and pastes start it at NULL).
    +  FrmMenuItemT    A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
    +  FrmPropT        One stored "Key = Value": name, value.
    +  FrmIntPropT     One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
    +

    Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); keys the widget does not know are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).

    +

    Loading and Writing

    +
      Function                                      Description
    +  --------                                      -----------
    +  frmModelNew(name)                             A blank form with the runtime form's defaults.
    +  frmModelLoad(text, len)                       The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
    +  frmModelFree(m)                               Frees the model, its controls and menus.
    +  frmModelWrite(m, buf, bufSize)                The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
    +  frmModelWriteSize(m)                          A buffer size that always holds the text.
    +  frmModelText(m, &len)                         The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
    +  frmModelSetCode(m, code)                      Replaces the code section.
    +  frmCodeSection(trailing, len, &outLen)        The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
    +  frmModelFromForm(rt, form)                    A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window), its menus; no code. What FormText$ writes.
    +

    Properties

    +
      Function                                              Description
    +  --------                                              -----------
    +  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0" or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
    +  frmModelSetFormProp(m, name, value)                   Sets one from text; Name renames the form; an empty value takes an optional one out.
    +  frmCtrlPropText(ctrl, name, out, size)                A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
    +  frmCtrlSetPropText(ctrl, key, value)                  Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
    +  frmModelSetCtrlProp(m, ctrl, name, value)             The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
    +  frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp   The stored properties.
    +  frmIntPropAt / frmIntPropFind / frmIntPropGet / Set   The built-in integer table.
    +  frmIfaceProp(typeName, propName)                      The widget interface property of a type, NULL when none.
    +

    Editing Controls

    +
      Function                                                  Description
    +  --------                                                  -----------
    +  frmModelAddCtrl(m, typeName, name, parent)                Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
    +  frmModelRemoveCtrl(m, ctrl)                               Removes ctrl and its subtree.
    +  frmModelRenameCtrl(m, oldName, newName)                   Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
    +  frmModelMakeArrayMember(m, ctrl, index)                   Gives a control array index; its children follow.
    +  frmModelMoveCtrl(m, ctrl, before)                         Moves ctrl with its subtree before a sibling (NULL: after the last).
    +  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers and too deep a nesting).
    +  frmModelSibling(m, idx, dir)                              The sibling next to a control (+1 after its subtree, -1 before).
    +  frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth       Lookups.
    +  frmModelIsDescendant / SubtreeEnd / NormalizeOrder        The tree order.
    +  frmModelNameInUse / AutoName / OptionSelected             Names and the OptionButton group rule.
    +  frmModelCtrlText(m, ctrl, &len)                           ctrl and its children as .frm control blocks (a copy for the Clipboard).
    +  frmModelCtrlsFromText(text, len)                          Copied blocks read into a scratch model.
    +  frmModelInsertTree(m, src, srcRoot, parent, name, index)  Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
    +

    Menus

    +

    The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.

    +
      Function                                                  Description
    +  --------                                                  -----------
    +  frmMenuItemInit(mi)                                       A blank item (enabled, visible, level 0).
    +  frmMenuInsert(&items, at, level)                          Inserts a blank item; the level is clamped so the menus stay a tree.
    +  frmMenuRemove(&items, idx)                                Removes an item with its subtree.
    +  frmMenuMove(&items, idx, dir)                             Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
    +  frmMenuIndent(items, idx, delta)                          Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
    +  frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName   Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
    +  frmMenuPropText / frmMenuSetPropText                      Menu properties as text (Checked makes a check item).
    +  frmModelMenuValidate(m, items, isIdent, reason, size)     The checks before items replace m's menus (levels; every name set, an identifier by isIdent, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
    +  frmModelMenuReplace(m, items)                             Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
    +  frmModelSetMenuProp(m, idx, key, value)                   Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
    +
    // Load a form, rename a control, add a label inside a frame, save.
    +FrmModelT *m = frmModelLoad(text, len);
    +
    +if (m) {
    +    int32_t   at     = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX);
    +    FrmCtrlT *frame  = (at >= 0) ? m->controls[at] : NULL;
    +    FrmCtrlT *lbl    = frmModelAddCtrl(m, "Label", "", frame);
    +    int32_t   outLen = 0;
    +
    +    frmModelRenameCtrl(m, "Command1", "cmdOk");
    +    frmModelSetCtrlProp(m, lbl, "Caption", "Name:");
    +
    +    char *out = frmModelText(m, &outLen);
    +
    +    // write out[0 .. outLen) to the file
    +    free(out);
    +    frmModelFree(m);
    +}
    +

    Connection Library

    One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.

    -

    Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial and networking libraries)

    +

    Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial, networking and transfer libraries; the transfer library for its BASIC functions)

    Header: conn/conn.h

    Connection Types

      Type         Behaviour
    @@ -6439,6 +7169,34 @@ n = connRead(c, buf, sizeof(buf));

    A Telnet session is the same with cfg.type = ConnTelnetE, cfg.host = "bbs.example.org" and cfg.tcpPort = 23.

    A file transfer runs over a connection by giving the transfer library connRead, connWrite and connPending as its XferIoT callbacks (ZMODEM over Telnet works: 0xFF bytes are escaped by the Telnet filter).

    +
    +

    Connections from BASIC

    +

    Add conn.bas to a BASIC project (and xfer.bas for transfers): connBas.c gives BASIC the connection library by handle, plus what a terminal program needs around it. The Terminal program is written this way.

    +
      Function                                  Description
    +  --------                                  -----------
    +  ConnCreate()                              A closed connection with connConfigDefaults; a handle, or 0.
    +  ConnSetSerial h, com, bps, data, parity$, stop, flow  COM port 1-4 and its framing (flow: CONN_FLOW_NONE, _XONXOFF, _RTSCTS).
    +  ConnSetModem h, init$, prefix$, phone$, redials, delay  The modem's commands and redialing.
    +  ConnSetHost h, host$, port                Telnet; the secure link over the network ("" = over the COM port).
    +  ConnSetPassphrase h, pass$                The secure link's passphrase, forgotten once ConnOpen used it.
    +  ConnSetWindowSize h, cols, rows           connSetWindowSize.
    +  ConnSetXferWindow h, bytes                XferOptionsT.rxWindow of the transfers started later: 0 = no limit, up to XFER_WINDOW_MAX; -1 (the default) = by the line, see below.
    +  ConnOpen(h, type)                         connOpen with type and the settings so far.
    +  ConnErrorText$(code)                      connErrorText of a ConnOpen result.
    +  ConnPoll h                                One step: connPoll, then the running transfer (cancelled when the line drops), else what arrived to the attached Terminal (see below). Call it from a Timer.
    +  ConnState, ConnStatus$, ConnStatusSerial  connState, connStatus, connStatusSerial.
    +  ConnWrite(h, bytes$), ConnRead$(h), ConnPending(h)  Bytes out and in without a Terminal: connWrite / connRead with BINARY STRING data, so every byte value passes.
    +  ConnCanBreak(type), ConnSendBreak(h)      connCanBreak, connSendBreak.
    +  ConnClose h, ConnFree h                   Close (a modem hangs up); close and free the handle.
    +  ConnAttach(h, ctrl$), ConnDetach h        Show the connection on a Terminal control, and send what is typed there.
    +  ConnSetEcho h, on; ConnSetAnswerback h, text$  Local echo (Enter as CR LF); the reply to ENQ.
    +  ConnCapture(h, path$)                     Append what arrives to path ("" stops). True while capturing.
    +  ConnZmodemRequest(h)                      True once after a ZMODEM sender started (xferDetectFeed).
    +  ConnSendFiles(h, proto, files$, escapeCtl, resume)  Send files (one path per line). An xfer.bas handle, with XFER_FAILED and the reason when it could not start; 0 when the line is not open or busy.
    +  ConnReceiveFiles(h, proto, target$, exists, escapeCtl)  Receive (exists: XFER_EXIST_).
    +

    ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.

    +

    The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.

    +

    Networking Library

    TCP/IP for DVX programs with the standard BSD socket API: a C programmer who knows sockets writes DVX network code the usual way. IPv4 today; the types and constants for IPv6 (AF_INET6, sockaddr_in6, sockaddr_storage) are defined so code can be written for both.

    @@ -6552,9 +7310,10 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. void netConfigLoad(NetConfigT *cfg) Read DVX.INI [network]. void netConfigFromPrefs(h, cfg) Read [network] from an open preferences handle. void netConfigToPrefs(h, cfg) Store cfg as [network] in an open handle (the caller saves). + bool netConfigEqual(a, b) True when two configurations are the same settings (the Control Panel restarts the network only when they differ). const char *netConfigProblem(cfg) NULL when usable, else why not in plain words (bad address or mask, name, interrupt). bool netParseAddress(text, &addr) Strict dotted IPv4 (four decimal parts 0-255) to network byte order. - void netCloseAppSockets(int32_t appId) Close every socket of that app (the shell calls this when an app ends). + void netCloseAppSockets(int32_t appId) Close every socket of that app and free the net.bas settings handles it left (the shell calls this when an app ends).

    Name servers set in the settings are used even with DHCP; left empty, the DHCP server's are used.

    @@ -6573,7 +7332,29 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    Networking from BASIC

    -

    net.bas declares simple functions over the socket API (netBas.c): HostLookup$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. Strings end at a zero byte, so the functions carry text. See the BASIC language reference, Include Libraries.

    +

    net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.

    +
      BASIC                                   C it calls
    +  -----                                   ----------
    +  HostName$(address)                      getnameinfo: the address itself, as this stack does not look names of addresses up; "" when it is not an address.
    +  TcpShutdown(handle)                     shutdown(SHUT_WR); True when done.
    +  TcpPeer$(handle)                        getpeername, as "address:port" ("" when not connected).
    +  TcpLocalPort(handle)                    getsockname: the local port (-1 for a bad handle).
    +  TcpSetNoDelay(handle, enable)           setsockopt(IPPROTO_TCP, TCP_NODELAY); True when done.
    +  TcpSetKeepAlive(handle, enable)         setsockopt(SOL_SOCKET, SO_KEEPALIVE); True when done.
    +  NetStop                                 netStop (every program's sockets are closed).
    +

    The network settings and status (what the Control Panel's Network page shows) are reachable too, over the functions of dvxNet.h:

    +
      BASIC                                   C it calls
    +  -----                                   ----------
    +  NetConfigLoad(prefs)                    netConfigFromPrefs on a prefs.bas handle (prefsBasHandle; 0 = the defaults) into a new settings handle, which also keeps the settings as read.
    +  NetConfigGet$ / NetConfigSet            One field by name: dhcp, vector, address, netmask, gateway, dns1, dns2, hostname (text; yes / no as "-1" / "0").
    +  NetConfigProblem$(cfg)                  netConfigProblem ("" when usable).
    +  NetConfigChanged(cfg)                   netConfigEqual against the settings as read.
    +  NetConfigStore(cfg, prefs)              netConfigToPrefs into a prefs.bas handle.
    +  NetConfigFree(cfg)                      Free the handle.
    +  NetVectorCount / NetVector(i)           The packet driver interrupt choices: 0 (find it), then NET_VECTOR_FIRST..NET_VECTOR_LAST.
    +  NetRestart()                            netRestart; True for NET_OK.
    +  NetStatus$(field)                       netGetStatus, one field as text: message, up, link, dhcp, address, netmask, gateway, dns1, dns2, hostname, driver, mac, vector, sockets, rxpackets, rxbytes, rxdropped, txpackets, txbytes.
    +

    Settings handles (1 to 16, 0 = none) belong to the program that made them; netCloseAppSockets frees what a program left.

    DVX Shell Library

    @@ -6624,6 +7405,8 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    Installed Programs

    Running Programs and Documents

    File Associations

    +

    BASIC Entry Points

    +

    BASIC System Settings

    Types and Constants

    @@ -6673,7 +7456,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. helpTopic[128] char[] Current help topic ID (updated by the app at runtime). Size is SHELL_HELP_TOPIC_MAX. onHelpQuery void (*)(void *ctx) Optional callback the shell fires on F1 so the app can refresh helpTopic from context. helpQueryCtx void * Opaque context pointer passed to onHelpQuery. - onNewArgs void (*)(void *ctx, const char *args) Optional. Set by an app that can take new launch arguments while running. F1 hands its "helpfile [topic]" arguments to a running help viewer that sets this, and raises it, instead of loading another copy. + onNewArgs void (*)(void *ctx, const char *args) Optional. Set by an app that can take new launch arguments while running. Loading a single-instance app that is already running and sets this hands it the new arguments and raises it (the load returns its app id) instead of saying it is already running; F1 hands its "helpfile [topic]" arguments to a running help viewer that sets this, and raises it, instead of loading another copy. newArgsCtx void * Opaque context pointer passed to onNewArgs.

    The appDir field is derived from the .app file path at load time so apps can find their own resources via relative paths. This is necessary because the working directory is shared by all apps in DOS. The config directory is mirrored from the app path: an app at APPS/KPUNCH/DVXBASIC/dvxbasic.app gets CONFIG/APPS/KPUNCH/DVXBASIC/. A full path inside the DVX directory (for example one picked with the Task Manager's Run... dialog) is mirrored the same way. An app outside the DVX directory, or loaded by a bare file name, gets CONFIG/APPS/<file name without extension>/.

    AppStateE

    @@ -6710,7 +7493,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    int32_t shellLoadApp(AppContextT *ctx, const char *path);

    Load and start an app from a DXE file. Returns the app ID (>= 1) on success, or -1 on error.

    For callback-only apps, appMain() runs synchronously and returns before shellLoadApp returns. For main-loop apps, a cooperative task is created and the app begins running on the next tsYield().

    -

    If multiInstance is false in the app's descriptor and the same DXE is already loaded, the call fails with an error dialog. If multiInstance is true, the DXE is copied to a temp file so dlopen gets an independent code and data image.

    +

    If multiInstance is false in the app's descriptor and the same DXE is already loaded, the running app is handed args through its onNewArgs and raised, and its app id is returned, when it sets onNewArgs; otherwise the call fails with an error dialog. If multiInstance is true, the DXE is copied to a temp file so dlopen gets an independent code and data image.

    shellLoadAppWithArgs

    int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args);

    Load and run an app with arguments. The args string is copied into DxeAppContextT.args before appMain() is called. Otherwise identical to shellLoadApp().

    @@ -6749,6 +7532,13 @@ bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId);
  • If the app showed nothing and still has the window after 5 seconds, it is not responding. The shell asks "'name' is not responding to the request to close. End it and continue exiting DVX?". Yes ends it with shellForceKillApp (no shutdownFn) and the exit continues; No returns false.
  • shellCloseAllApps also returns false, without asking any app, when a dialog box is already open (ctx->modalWindow): the dialog is raised so it can be answered first. It pumps the event loop, so call it only from the shell's top level, never from inside a callback. An app ended from the Task Manager while the sequence runs simply counts as closed.

    +

    Closing one app: shellRequestAppClose / shellRunCloseRequests / shellCloseApp

    +
    void shellRequestAppClose(int32_t appId);
    +void shellRunCloseRequests(AppContextT *ctx);
    +bool shellCloseApp(AppContextT *ctx, int32_t appId);
    +

    shellCloseApp asks one app to close exactly as shellCloseAllApps asks each (the same routine): its visible windows are closed the way the user would close them, so its save prompt or QueryUnload runs, and one that does not respond is offered to be ended ("'name' is not responding to the request to close. End it?", titled Close Program). True when the app is gone or was not running; false when it kept a window or a dialog box is open. It pumps the event loop, so it belongs to the shell's top level. shellRequestAppClose only records the request (a second request for the same app is not added; an app that ends, or is ended, before it runs has its requests dropped by shellPurgeAppCallbacks, since its slot may be reused); the main loop calls shellRunCloseRequests right after each frame, before shellRunExitRequest, and it asks the waiting apps oldest first. BASIC's ShellRunningClose is shellRequestAppClose.

    +

    The desktop ending

    +

    The shell itself exits DVX when the desktop app ends in any other way -- its last window closed, ended from the Task Manager, or crashed: an app-cleanup hook (shellRegisterAppCleanup) notices the desktop's id going away, logs "Shell: the desktop ended; exiting DVX" and calls shellRequestExit, so every other app is asked to close as above. If one of them refuses, DVX keeps running and ends as soon as the last app has ended. A desktop app (C or BASIC) therefore never calls dvxQuit itself.

    Query API

    @@ -6761,6 +7551,12 @@ bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId);

    shellRunningAppCount

    int32_t shellRunningAppCount(void);

    Count running apps, not counting the shell itself. Includes apps in both AppStateLoadedE and AppStateRunningE states.

    +

    shellRunningAppId

    +
    int32_t shellRunningAppId(int32_t index);
    +

    The id of running app index, 0 .. shellRunningAppCount() - 1 in slot order (the same apps shellRunningAppCount counts); 0 out of range. BASIC: ShellRunningId.

    +

    shellActivateApp

    +
    bool shellActivateApp(AppContextT *ctx, int32_t appId);
    +

    Bring an app to the front, as the Task Manager's Switch To does (it calls this): its topmost visible window that is not being destroyed is restored when minimized (dvxRestoreWindow; a maximized window stays maximized), else raised (dvxRaiseWindow), and gets the focus. False when the app has no visible window. BASIC: ShellRunningActivate.

    shellAppFindByPath

    ShellAppT *shellAppFindByPath(const char *path);

    Return an active app slot loaded from path, or NULL. Paths are compared with shellPathsMatch.

    @@ -6801,12 +7597,12 @@ shellConfigPath(ctx, "settings.ini", path, sizeof(path));

    Desktop Callbacks

    -

    The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation).

    +

    The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.

    Shell Event Hooks

      AppContextT field    Handler                 Purpose
       -----------------    -------                 -------
    -  idleCallback         shellIdleDispatch       Runs every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
    -  onCtrlEsc            ctrlEscHandler          Invokes shellCtrlEscFn if the Task Manager DXE is loaded.
    +  idleCallback         shellIdleDispatch       Runs one frame of Run Minimized (shellMinimizeIdle) and every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
    +  onCtrlEsc            ctrlEscHandler          shellOpenTaskManager.
       onF1                 f1HelpHandler           Opens the focused app's helpFile/helpTopic (or the system help) in the help viewer, reusing a running viewer that sets onNewArgs.
       onTitleChange        titleChangeHandler      Runs shellDesktopUpdate so Program Manager window lists refresh.

    shellRegisterDesktopUpdate

    @@ -6829,10 +7625,14 @@ void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);

    Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.

    shellDesktopUpdate

    void shellDesktopUpdate(void);
    -

    Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh.

    -

    shellCtrlEscFn

    -
    extern void (*shellCtrlEscFn)(AppContextT *ctx);
    -

    Function pointer set by the taskmgr DXE's constructor. The shell calls this when Ctrl+Esc is pressed. NULL if the task manager is not loaded.

    +

    Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. Each call also adds one to the change counter (shellDesktopChangeCount).

    +

    shellDesktopChangeCount

    +
    int32_t shellDesktopChangeCount(void);
    +

    How many times shellDesktopUpdate has run (it wraps around). For a desktop that cannot take a callback -- a BASIC program polls it from a Timer (ShellChangeCount) and refreshes its running-program count when the value differs from the one it saw last.

    +

    shellCtrlEscFn / shellOpenTaskManager

    +
    extern void (*shellCtrlEscFn)(AppContextT *ctx);
    +void shellOpenTaskManager(AppContextT *ctx);
    +

    shellCtrlEscFn is set by the taskmgr DXE's constructor; NULL if the task manager is not loaded. shellOpenTaskManager calls it when it is set and does nothing otherwise; Ctrl+Esc, Program Manager's Help > Task Manager and BASIC's ShellTaskManager all open Task Manager through it.

    System Information

    @@ -6870,6 +7670,12 @@ void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);

    shellProgramIcon

    uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch);

    A picture edge x edge pixels in display format, for drawing with rectCopy: a program's icon32 resource, or any image file dvxLoadImage reads (BMP, PNG, JPEG, GIF). Other sizes are scaled (nearest pixel). Returns NULL when there is no picture. Free with dvxFreeImage.

    +

    shellItemIcon

    +
    uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch);
    +

    The icon a launcher shows for path (a program or a document), edge x edge pixels as shellProgramIcon gives them: the picture file iconPath when it is given (NULL or "" for none) and can be read; else path's own picture -- a program's icon32, or path itself when it is a picture file; else, for a document, the icon of the program its file type opens in (shellAssocCommand). NULL when there is none; free with dvxFreeImage. Program Manager's items (both sizes) and its Properties preview use it, so an item and its preview always show the same icon. (A BASIC program's LOADPICTURE of a .app reads the program's icon itself, with dvxResLoadIconData.)

    +

    shellItemIconFile

    +
    bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize);
    +

    The same rule as shellItemIcon (one implementation), naming the file the icon comes from in out: iconPath, path, or the program path's file type opens in; false with "" when none gives a picture. dvxLoadImage of that file (BASIC LOADPICTURE) gives the icon. BASIC: ShellItemIcon$ in shell.bas.

    shellProgramListFree / shellIsProgram

    void shellProgramListFree(ShellProgramT *list);
     bool shellIsProgram(const char *path);
    @@ -6894,6 +7700,10 @@ bool shellIsProgram(const char *path);

    shellResolveArgs

    void shellResolveArgs(const char *folder, const char *args, char *out, int32_t outSize);

    Copy args to out with blanks normalized, replacing every relative argument that names an existing file inside folder by folder\name, so a program finds the files an item's arguments name no matter where it runs. Quoted arguments keep their quotes. With an empty or NULL folder the arguments are only normalized. DVX programs share one current folder, so this is how a program item's working folder takes effect.

    +

    shellMinimizeApp

    +
    void shellMinimizeApp(AppContextT *ctx, int32_t appId);
    +void shellMinimizeIdle(AppContextT *ctx);
    +

    Run Minimized. shellMinimizeApp minimizes every visible window of appId (an id from shellRunProgram) at once and again on each of the next SHELL_MINIMIZE_FRAMES (60) idle frames, because a program with a main loop opens its windows from its own task after it has started. Asking again for the same program starts its frames over; nothing happens for an id that is not running, and a program that ends is forgotten (an app-cleanup hook). shellMinimizeIdle is the per-frame step, called by the shell's idle dispatch. Program Manager's Run Minimized items and BASIC's ShellMinimizeApp use it.

    File Associations

    @@ -6918,6 +7728,184 @@ bool shellAssocRemove(const char *extension); bool shellAssocNormalize(const char *extension, char *out);

    shellAssocSet adds or replaces an association (programPath may carry ,arguments) and saves; false for a bad extension, an empty program, or a failed save. shellAssocRemove removes one and saves; false when it did not exist or the save failed. shellAssocNormalize writes the extension as the store keys it (SHELL_ASSOC_EXT_MAX bytes: no dot, upper case) and is false for an empty extension, one longer than eight characters, or anything but letters and digits.

    +
    +

    BASIC Entry Points

    +

    Source: dvxshell/shellBas.c; BASIC declarations: src/include/basic/shell.bas (DECLARE LIBRARY "dvxshell").

    +

    The shell's services for DVX BASIC programs -- everything Program Manager does, so a desktop can be written in BASIC. Only BASIC-marshalable types: strings and LONG in, LONG or STRING out, True (-1) / False (0) for success. "This program" is the app whose code is running (ctx->currentAppId, which the shell keeps for every app's task and window callback). A returned string stays valid until the next call of the same function. Program lists (1, 2, ...; 0 means none) and hotkeys belong to the program that made them and are released by an app-cleanup hook (shellRegisterAppCleanup) when it ends.

    +

    Starting programs and documents

    +
    int32_t     ShellRun(const char *program, const char *args);
    +int32_t     ShellRunProgram(const char *program, const char *args, const char *folder);
    +int32_t     ShellRunCommand(const char *command);
    +const char *ShellRunDialog(const char *history);
    +void        ShellMinimizeApp(int32_t appId);
    +int32_t     ShellOpenDocument(const char *path);
    +

    ShellRunProgram is shellRunProgram (a document opens through its association; file names in args, and a relative document, are looked for in folder; "" for none), returning the new app id or 0. ShellRun is ShellRunProgram without a folder, True when it started; the two are one launch path. ShellRunCommand is shellRunCommand (id or 0). ShellRunDialog splits history at line ends (CHR$(10), a CR before it is dropped, empty lines skipped) into the list shellRunDialog shows, and returns the command chosen or "" when cancelled. ShellMinimizeApp is shellMinimizeApp. ShellOpenDocument is shellAssocOpenDocument (it shows nothing when there is no association; ShellRunProgram does).

    +

    Installed programs

    +
    int32_t     ShellProgramScan(const char *folder, const char *skipPath);
    +int32_t     ShellProgramCount(int32_t list);
    +const char *ShellProgramPath(int32_t list, int32_t index);
    +void        ShellProgramFree(int32_t list);
    +const char *ShellProgramName(const char *path);
    +const char *ShellProgramDescription(const char *path);
    +int32_t     ShellIsProgram(const char *path);
    +

    ShellProgramScan is shellProgramScan of folder ("" for SHELL_PROG_DIR) without skipPath ("" for none), paths only (readInfo false), so it never opens a program; it returns a list handle, 0 when nothing was found. ShellProgramCount and ShellProgramPath read it (0 / "" for a bad list or index); ShellProgramFree frees it. ShellProgramName and ShellProgramDescription are shellProgramInfo's name and description, read one program at a time so a long list can be filled with DoEvents in between (the C progress callback). ShellIsProgram is shellIsProgram.

    +

    File types

    +
    const char *ShellAssocProgram(const char *extension);
    +const char *ShellAssocCommand(const char *document);
    +int32_t     ShellAssocCount(void);
    +const char *ShellAssocExt(int32_t index);
    +const char *ShellAssocValue(int32_t index);
    +int32_t     ShellAssocSet(const char *extension, const char *value);
    +int32_t     ShellAssocRemove(const char *extension);
    +const char *ShellAssocNormalize(const char *extension);
    +

    ShellAssocProgram is shellAssocLookup: the stored value, the program followed by ",args" when there is a template. ShellAssocCommand is the program part of shellAssocCommand for a document. ShellAssocCount, ShellAssocExt and ShellAssocValue enumerate shellAssocGet (0-based; "" past the end). ShellAssocSet, ShellAssocRemove and ShellAssocNormalize are shellAssocSet, shellAssocRemove and shellAssocNormalize ("" when the extension cannot be one). Every change is saved at once.

    +

    Running programs and DVX

    +
    int32_t     ShellRunningCount(void);
    +int32_t     ShellChangeCount(void);
    +void        ShellTaskManager(void);
    +const char *ShellSystemInfo(void);
    +void        ShellExitDvx(void);
    +

    ShellRunningCount is shellRunningAppCount. ShellChangeCount is shellDesktopChangeCount: a BASIC desktop polls it from a Timer in place of shellRegisterDesktopUpdate's callback. ShellTaskManager is shellOpenTaskManager, ShellSystemInfo is shellGetSystemInfo ("" before shellInfoInit), and ShellExitDvx is shellRequestExit (every app is asked to close, then DVX quits; a desktop asks the user first).

    +
    int32_t     ShellRunningId(int32_t index);
    +const char *ShellRunningName(int32_t appId);
    +const char *ShellRunningPath(int32_t appId);
    +int32_t     ShellRunningFind(const char *program);
    +int32_t     ShellRunningActivate(int32_t appId);
    +int32_t     ShellRunningClose(int32_t appId);
    +int32_t     ShellRunningKill(int32_t appId);
    +int32_t     ShellAppIsHung(int32_t appId);
    +int32_t     ShellAppMemory(int32_t appId);
    +

    The Task Manager's operations, one routine each with it. ShellRunningId is shellRunningAppId. ShellRunningName and ShellRunningPath are the ShellAppT name and path of an app in AppStateRunningE ("" otherwise; the same state every call below requires). ShellRunningFind is shellAppFindByPath (shellPathsMatch rules), 0 when that app is not running. ShellRunningActivate is shellActivateApp. ShellRunningClose is shellRequestAppClose: the app is asked at the main loop's top level, after the BASIC code has returned; True when the request was recorded. ShellRunningKill is shellForceKillApp followed by shellDesktopUpdate (End Task: no shutdownFn, callbacks purged, the unload deferred while the app's code is on a stack); the running program itself is refused (BASIC's END ends it). ShellAppIsHung is dvxAppIsHung and ShellAppMemory dvxMemGetAppUsage (clamped to INT32_MAX; 0 for an app that is not running).

    +

    Command lines and priority

    +
    const char *ShellCommandProgram(const char *command);
    +const char *ShellCommandArgs(const char *command);
    +int32_t     ShellArgCount(const char *command);
    +const char *ShellArg(const char *command, int32_t index);
    +int32_t     ShellAppId(void);
    +int32_t     ShellPriority(void);
    +int32_t     ShellSetPriority(int32_t priority);
    +

    ShellCommandProgram and ShellCommandArgs are the two parts of shellSplitCommand (what shellRunCommand runs). ShellArgCount and ShellArg walk a command line with dvxNextArg -- the tokenizer shellSplitCommand and COMMAND$(n) use -- counting the program as argument 0; ShellArg drops the quotes and gives "" past the last. ShellAppId is ctx->currentAppId. ShellPriority and ShellSetPriority are tsGetPriority / tsSetPriority on the running program's mainTaskId (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH, shell.bas SHELL_PRIORITY_*); -1 / False for a program without a task of its own (every BASIC program has one) or a priority out of range.

    +

    Hotkeys

    +
    int32_t     HotkeyAdd(const char *keyText);
    +int32_t     HotkeyNext(void);
    +void        HotkeyRemove(int32_t id);
    +const char *HotkeyText(const char *keyText);
    +

    HotkeyAdd parses keyText with dvxAccelParse and registers it with dvxRegisterHotkey for the running program; it returns an id of its own (> 0), or 0 when keyText is not a key or dvxRegisterHotkey refuses it (it needs Ctrl or Alt unless it is a function or navigation key, and a key already registered is refused). A BASIC program cannot be called back, so the hotkey's callback queues the id for its program (up to 16 presses not yet collected); HotkeyNext returns and removes the oldest one, 0 when there is none. HotkeyRemove drops one of the running program's hotkeys (other ids are ignored). HotkeyText is dvxAccelParse followed by dvxAccelFormat ("" when keyText is not a key): the spelling menus show. dvxUnregisterAppHotkeys removes a program's hotkeys when it ends; the app-cleanup hook drops their records and queued presses.

    +

    This program and DVX settings

    +
    const char *ShellAppPath(void);
    +const char *ShellConfigFile(const char *fileName);
    +int32_t     ShellInstanceCount(void);
    +void        ShellLog(const char *text);
    +int32_t     ShellSetDesktop(const char *program);
    +int32_t     ShellSetWelcome(const char *program);
    +

    ShellAppPath is the running program's .app path as started ("" when unknown). ShellConfigFile is shellConfigPath of fileName in the running program's config folder, which shellEnsureConfigDir makes first ("" when it cannot); every copy of a program gets the same folder, the one a C app's settings use. ShellInstanceCount counts the running apps with the same path, this one included (a second copy of a multiInstance program loads from a temporary copy but keeps the original path). ShellLog writes one line with dvxLog. ShellSetDesktop and ShellSetWelcome set, or with "" remove, DVX.INI [shell] desktop (the desktop app; SHELL_DESKTOP_APP when missing) and [shell] welcome (the program started after the desktop); both share one writer and return False (logged) when DVX.INI cannot be saved.

    +

    Programs folder, item icons, program groups

    +
    const char *ShellProgramsFolder(void);
    +const char *ShellItemIcon(const char *iconPath, const char *path);
    +

    ShellProgramsFolder is SHELL_PROG_DIR (where ShellProgramScan("") looks and a Browse for a program starts). ShellItemIcon is shellItemIconFile ("" for none). The program-group bindings (ShellGroups*, ShellGroup*, ShellItem*) are in pmModelBas.c; see Program Groups.

    +

    A desktop in BASIC

    +

    A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell exits DVX when it ends, so it needs no code for that; its Exit command confirms and calls ShellExitDvx. Its groups use the shell's program-group model (Program Groups: the same PROGMAN.INI format, rules and limits as Program Manager) through shell.bas, in ShellConfigFile$'s folder. An icon grid is drawn on a PictureBox with PaintPicture and pictures from LOADPICTURE of ShellItemIcon$ (a .app gives its icon), with MouseDown/MouseUp for selecting and dragging and KeyDown for the keyboard. The test fixture src/test/dos/fixtures/parpm does all of this.

    +
    +
    +

    Program Groups

    +

    Header: dvxshell/pmModel.h; source pmModel.c (model) and pmModelBas.c (BASIC entry points, shell.bas).

    +

    Program Manager's groups, items and PROGMAN.INI, as plain data with no windows, part of the shell so the C Program Manager and a desktop written in BASIC share one model (the pm prefix names it). groups[PM_INSTALLED_IDX] is the automatic Installed Programs group (pmInstalledScan: every program under SHELL_PROG_DIR, sorted by name, never saved; pmInstalledChanged tells cheaply whether a program was added or removed); the others are the user's, saved by pmModelSave with a crash-safe rewrite in this layout:

    +
    [options]  minimizeOnRun, restoreAlone, autoArrange, view, group
    +[groups]   count
    +[groupN]   name, count, itemM.name, itemM.program, itemM.args,
    +           itemM.folder, itemM.icon, itemM.key, itemM.minimized,
    +           itemM.col, itemM.row   (N and M count from 1)
    +[run]      historyN (newest first)
    +
      Function                                     Description
    +  --------                                     -----------
    +  pmModelInit / pmModelLoad / pmModelSave / pmModelFree   An empty model; load a file (one never saved gives Main and Startup; damaged entries are skipped); save; free.
    +  pmGroupAdd / pmGroupDelete / pmGroupFind / pmGroupRename / pmGroupNameValid   Groups (names trimmed, unique in any case, PM_GROUP_MAX of them; the automatic group cannot change).
    +  pmItemAdd / pmItemCopy / pmItemMove / pmItemDelete / pmItemReorder / pmItemPlace   Items (a name and a program each, PM_ITEM_MAX per group).
    +  pmItemGetField / pmItemSetField              An item's field by its PROGMAN.INI name (PM_FIELD_*); a key must pass pmHotkeyParse, a name or program cannot be emptied.
    +  pmOptionGet / pmOptionSet                    An [options] value by name (flags, view, group).
    +  pmArrangeGroup / pmPlaceUnplaced / pmCellItem   Grid cells for an icon view.
    +  pmItemMatches / pmContainsNoCase             Program Manager's search.
    +  pmHistoryAdd                                 The Run history (PM_HISTORY_MAX, newest first).
    +  pmHotkeyParse                                An item shortcut: dvxAccelParse, and Ctrl plus Alt or Shift so it never takes a key a program uses itself; "" is no shortcut.
    +  pmInstalledScan / pmInstalledChanged / pmInstalledReplace   The automatic group.
    +

    BASIC (shell.bas, DECLARE LIBRARY "dvxshell"): ShellGroupsLoad / Save / Free / Scan / Changed / Get / Set / History$ / HistoryAdd, ShellGroupCount / Name$ / Find / Add / Delete / Rename / NameValid / Arrange / PlaceUnplaced / CellItem, ShellItemCount / Add / Get$ / Set / Delete / Copy / Move / Reorder / Place / Matches / KeyValid. A groups file is a handle of the program that opened it, freed when it ends (shellRegisterAppCleanup).

    +
    +
    +

    BASIC System Settings

    +

    Source: dvxshell/shellSysBas.c; BASIC declarations: src/include/basic/system.bas (DECLARE LIBRARY "dvxshell"); prototypes in shellApp.h.

    +

    What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).

    +
    int32_t     SysVideoModeCount(void);
    +int32_t     SysVideoModeWidth(int32_t index);
    +int32_t     SysVideoModeHeight(int32_t index);
    +int32_t     SysVideoModeBpp(int32_t index);
    +int32_t     SysSetVideoMode(int32_t width, int32_t height, int32_t bpp);
    +int32_t     SysColorCount(void);
    +const char *SysColorLabel(int32_t id);
    +const char *SysColorName(int32_t id);
    +int32_t     SysColorId(const char *name);
    +int32_t     SysGetColor(int32_t id);
    +void        SysSetColor(int32_t id, int32_t rgb);
    +void        SysResetColors(void);
    +int32_t     SysLoadTheme(const char *path);
    +int32_t     SysSaveTheme(const char *path);
    +void        SysColorsStore(int32_t prefs);
    +int32_t     SysColorsLoad(int32_t prefs);
    +int32_t     SysSetWallpaper(const char *path);
    +const char *SysWallpaper(void);
    +void        SysSetWallpaperMode(int32_t mode);
    +int32_t     SysWallpaperMode(void);
    +void        SysWallpaperStore(int32_t prefs);
    +int32_t     SysWallpaperLoad(int32_t prefs);
    +void        SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
    +int32_t     SysMouseWheelDir(void);
    +int32_t     SysMouseDoubleClick(void);
    +int32_t     SysMouseAccel(void);
    +int32_t     SysMouseMickeys(void);
    +int32_t     SysMouseWheelStep(void);
    +void        SysMouseStore(int32_t prefs);
    +void        SysMouseLoad(int32_t prefs);
    +int32_t     SysFontWidth(void);
    +int32_t     SysFontHeight(void);
    +int32_t     SysFontFirstChar(void);
    +int32_t     SysFontCharCount(void);
    +int32_t     SysFontGlyphRow(int32_t code, int32_t row);
    +int32_t     SysMouseX(void);
    +int32_t     SysMouseY(void);
    +int32_t     SysMouseButtons(void);
    +void        SysSetMousePos(int32_t x, int32_t y);
    +int32_t     SysShiftState(void);
    +int32_t     SysSendKey(int32_t keyCode, int32_t shift);
    +int32_t     SysScreenshot(const char *path);
    +int32_t     SysMemoryTotalKb(void);
    +int32_t     SysMemoryFreeKb(void);
    +
      Function                     Calls
    +  --------                     -----
    +  SysVideoMode*                dvxGetVideoModes (0 out of range)
    +  SysSetVideoMode              dvxChangeVideoMode; False on -1 (also while a program is in exclusive full-screen mode)
    +  SysColorLabel / SysColorName dvxColorLabel / dvxColorName ("" for an id outside 0 .. ColorCountE - 1)
    +  SysColorId                   the id whose dvxColorName matches, any case; -1 for none
    +  SysGetColor / SysSetColor    dvxGetColor / dvxSetColor (an unknown id: 0 / nothing)
    +  SysResetColors               dvxResetColorScheme
    +  SysLoadTheme / SysSaveTheme  dvxLoadTheme / dvxSaveTheme
    +  SysColorsStore / Load        dvxColorsToPrefs / dvxColorsFromPrefs (Load: True when the handle had a [colors] key)
    +  SysSetWallpaper              dvxSetWallpaper ("" clears)
    +  SysSetWallpaperMode          dvxSetWallpaperMode with the value of prefsWallpaperChoices[mode]; other modes are ignored
    +  SysWallpaper / Mode          ctx->wallpaperPath; the prefsWallpaperChoices index of ctx->wallpaperMode
    +  SysWallpaperStore / Load     dvxWallpaperToPrefs / dvxWallpaperFromPrefs (Load: False when the image could not be loaded)
    +  SysSetMouse                  dvxSetMouseConfig
    +  SysMouse*                    ctx->wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep
    +  SysMouseStore / Load         dvxMouseToPrefs / dvxMouseFromPrefs
    +  SysFont*                     dvxGetFont: charWidth, charHeight, firstChar, numChars; SysFontGlyphRow is glyphData[(code - firstChar) * charHeight + row] (0 outside the font)
    +  SysMouseX / Y / Buttons      ctx->mouseX, mouseY, mouseButtons (MOUSE_LEFT / RIGHT / MIDDLE = vbLeftButton ...)
    +  SysSetMousePos               platformMouseWarp, clamped to the screen; the next poll reports the move like the user's
    +  SysShiftState                ctx->keyModifiers & KEY_MOD_ALL (KEY_MOD_SHIFT / CTRL / ALT = vbShiftMask / vbCtrlMask / vbAltMask)
    +  SysSendKey                   queues (up to 64) keyCode + shift as the BIOS stores the keystroke (platformBiosScanCode for combinations with codes of their own, KEY_EXT_FLAG keys by scan code, Ctrl+letter as its control character); a shell idle handler (shellRegisterIdle) injects one per idle frame with platformInjectKey, so each is read with its own modifiers, and releases them (platformInjectModifiers(0)) after the last
    +  SysScreenshot                dvxScreenshot (BMP for .bmp, else PNG)
    +  SysMemoryTotalKb / FreeKb    platformGetMemoryInfo; -1 when unknown
    +

    There is no binding for platformInjectMouse: on DOS it replaces the real mouse until the next call and has no way back to it, so a synthesized click would leave the pointer stuck. A program moves the pointer with SysSetMousePos and "clicks" a control by calling its event procedure or setting its value.

    +

    system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h.

    +

    The Store / Load routines are the ones the shell uses at start (dvxMouseFromPrefs, dvxColorsFromPrefs, dvxWallpaperFromPrefs on DVX.INI) and Control Panel uses for OK and for its Cancel snapshot (an in-memory prefsCreate handle), so the INI keys are written in one place (dvxApp.c).

    +

    dvxSql -- SQL Database Interface

    High-level wrapper around SQLite3 for DVX applications. Manages database connections and result set cursors via integer handles so BASIC code never touches raw pointers. All handles are 1-based; 0 indicates an error or invalid handle.

    @@ -7160,6 +8148,7 @@ dvxSqlExec(db, sql); fileOpsTypeName(entry, out, size) "Folder", "Program" (.APP), "TXT File", "File". fileOpsFormatSize(bytes, out, size) "1,234,567". fileOpsFormatDate(t, out, size) "YYYY-MM-DD HH:MM" in local time. + fileOpsParseDate(text, &t) The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", optionally ":SS"), local time; false for anything else, a date that does not exist included. fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it. fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure. fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use. @@ -7215,7 +8204,8 @@ fileOpsJobFree(job);

    fileOpsJobClearAttr(job, attr), called before the first step, clears those attribute bits on every file a copy or move writes: Setup passes FILEOPS_ATTR_READONLY, because every file on a CD-ROM is read-only and the installed copies must not be.

    fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) is called after every step (return false to cancel) and ask(job, kind, message, ctx) answers questions. With no callbacks, existing files are kept (No) and failing items are skipped.

    BASIC

    -

    src/include/basic/fileops.bas declares BASIC wrappers: FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree (a whole folder, without prompting), FileOpsDriveList$ and FileOpsFreeKb. Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.

    +

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    +

    Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.

    Game Helpers Library

    @@ -7248,7 +8238,46 @@ while (running) { gamePalCacheLookup(c, r, g, b) The entry nearest to the centre of the colour's 15-bit cell (5 bits per channel), searched once per cell and then remembered, so every colour in a cell gets the same answer. gamePalCacheSync(c, pal) Start over (and return true) when pal differs from the recorded palette; call it once a frame if the palette can change.

    BASIC

    -

    src/include/basic/game.bas declares GameKeyDown(keyCode), True while the key is held and one of the calling program's windows has the focus, with GAMEKEY_* constants for common keys. The BASIC runtime gives the library the GUI context (gameBasSetContext) when a program starts. Frame pacing from BASIC comes with the BASIC game statements later.

    +

    src/include/basic/game.bas declares GameKeyDown(keyCode), True while the key is held and one of the calling program's windows has the focus, with GAMEKEY_* constants for common keys; GameKeyClear (platformKeyStateClear, only while the calling program's window has the focus); GameWaitRetrace (platformVideoWaitRetrace); and FrameMissed(), the missed count of the BASIC runtime's own FRAMEWAIT pacer (GameFramePacerT.missed since FRAMEWAIT last started at a new rate; 0 before the first FRAMEWAIT). The BASIC runtime gives the library the GUI context (gameBasSetContext) when a program starts, and runs FRAMEWAIT on gameFrameInit / gameFrameNext.

    +
    +
    +

    Help File Library

    +

    Reads compiled DVX help files (.hlp, written by the help compiler dvxhlpc or HelpCompile): topics, the table of contents, the keyword index, each topic's content records in order, pictures and full-text search. The Help Viewer, the HelpView widget (BASIC HelpPane) and BASIC's hlpfile.bas all read help files through it, so they find, search and show topics the same way.

    +

    Header: hlpfile/hlpFile.h (the file format: hlpfile/hlpFormat.h, shared with the help compiler)

    +

    Loaded as: bin/libs/kpunch/hlpfile/hlpfile.lib

    +

    Opening and Topics

    +

    A file is an opaque HlpFileT. Strings returned point into the file's string table and stay valid until hlpFileClose.

    +
      Function                                      Description
    +  --------                                      -----------
    +  hlpFileOpen(path)                             Open a help file; NULL when it cannot be read or is not a help file. The header, topic directory, strings, table of contents, index and search index are read at once; topic content and pictures are read when asked for.
    +  hlpFileClose(f)                               Close it (NULL is ignored).
    +  hlpFileTopicCount(f)                          Number of topics.
    +  hlpFileTopicId(f, topic) / Title(f, topic)    A topic's ID (what links and HelpTopic name) and title; "" out of range.
    +  hlpFileFindTopic(f, topicId)                  The topic with that ID, or -1.
    +  hlpFileStartTopic(f, topicId)                 The topic to open for "file [topic]": topicId (NULL or "" for none), else the file's .default topic, else the first; -1 for a file with no topics.
    +  hlpFileString(f, offset)                      A string table entry ("" out of range).
    +  hlpFileTocCount(f) / hlpFileToc(f, i)         Table of contents entries (HlpTocEntryT: title string, topic or HLP_NO_TOPIC for a section heading, depth, HLP_TOC_EXPANDED flag).
    +  hlpFileIndexCount(f) / hlpFileIndex(f, i)     Keyword index entries (HlpIndexEntryT: keyword string, topic), sorted.
    +

    Records and Pictures

    +

    hlpFileForEachRecord(f, topic, fn, userData) calls fn(hdr, payload, userData) for each record of a topic in order (HLP_REC_TEXT, HEADING1 .. 3, LIST_ITEM, NOTE with its HLP_NOTE_ kind in hdr->flags, CODE, TABLE, IMAGE with its HLP_IMG_ alignment, LINK, HRULE); the payload is NUL-terminated. fn returns false to stop. hlpRecordLink gives a link record's target topic ID and display text (the target when the text is empty); hlpRecordImage its picture's place in the file, and hlpFileReadImage the picture itself: the image file as the compiler took it in (BMP, PNG, JPEG or GIF), malloc'd, for dvxLoadImageAlpha. Reading a picture inside the record walk is safe.

    +

    hlpWrapText(text, cols) wraps flowing text at cols columns (at least HLP_MIN_WRAP_COLS) as the viewer does, a malloc'd copy with '\n' at the wrap points.

    +

    Search

    +

    hlpFileSearch(f, query, matches) finds the topics whose title or text contains every word of query (any case, separated by blanks), plus those with an index keyword containing the whole query, setting matches[topic] (hlpFileTopicCount bytes) and returning the count (-1 on a failure). The query is not changed. The compiler's trigram index narrows the candidates first; the trigram characters (hlpTrigramSymbol) and the record types searched (hlpRecordSearchable) are defined once in hlpFormat.h for both. Words with no three letters or digits in a row cannot use the index, so then only titles and keywords are searched.

    +
    uint8_t *matches = malloc(hlpFileTopicCount(f));
    +int32_t  n       = hlpFileSearch(f, "serial port", matches);
    +
    +for (int32_t t = 0; n > 0 && t < hlpFileTopicCount(f); t++) {
    +    if (matches[t]) {
    +        printf("%s\n", hlpFileTopicTitle(f, t));
    +    }
    +}
    +
    +free(matches);
    +
    +
    +

    BASIC

    +

    src/include/basic/hlpfile.bas declares the BASIC functions (hlpFileBas.c): HlpOpen, HlpClose, HlpTopicCount, HlpTopicId$, HlpTopicTitle$, HlpFindTopic, HlpStartTopic, HlpTocCount, HlpTocTitle$, HlpTocTopic, HlpTocDepth, HlpTocExpanded, HlpIndexCount, HlpIndexKeyword$, HlpIndexTopic, HlpSearch, HlpSearchTopic, HlpLoadTopic, HlpRecordType, HlpRecordFlags, HlpRecordText$, HlpRecordLink$, HlpRecordImageSave and HlpWrap$, with the HLP_REC_, HLP_NOTE_ and HLP_IMG_ constants of hlpFormat.h.

    +

    A handle (a small number, 0 = none) is an open file plus what the program last asked of it -- HlpSearch keeps the topics found for HlpSearchTopic, HlpLoadTopic copies a topic's records for the HlpRecord functions -- since BASIC cannot take a callback or a byte array. Each handle records the program that opened it; the shell's app-cleanup hook closes what a program leaves open when it ends. hlpFileBasHandle(handle) gives the HlpFileT behind a handle to C code (the HelpPane control's ShowTopic uses it). A returned string stays valid until the next call returning one (the BASIC runtime copies it at once).

    Joystick Library

    @@ -7285,7 +8314,8 @@ while (running) { keyboard.up ... keyboard.right, keyboard.button1 ... keyboard.button4 Key names (joyKeyName: Up, Down, Left, Right, LCtrl, RCtrl, LShift, RShift, LAlt, RAlt, Space, Enter, Tab, Backspace, Insert, Delete, Home, End, PgUp, PgDn, Keypad0 to Keypad9, A to Z, 0 to 9). Defaults: the arrows, Left Ctrl, Space, Left Shift, Z. <id>.axis<n> min,center,max: axis n of the device with that id, e.g. gameport0.axis0 = 40,560,1150. <id>.deadZone The device's dead zone in percent. -

    joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise.

    +

    Two settings are offered as lists: joyChoices(which, &count) returns the choices of JoyChoiceGamePortE (gameport.mode: "Two joysticks", "One joystick with 4 axes") or JoyChoiceFallbackE (fallback: "When no joystick is found", "Always", "Never") in list order, each a PrefsChoiceT (dvxPrefs.h) with its label for people and the name stored; joyChoiceGet(which) is the index of the current one (the default for a missing or unknown value) and joyChoiceSet(which, index) stores one. The keyboard joystick's keys go by slot (0 to JOY_KBD_SLOT_COUNT - 1 in JOY_KBD_SLOT_NAMES order: up, down, left, right, button1 to button4): joyKbdSlotGet(slot) is the joyKeyName index of the slot's key (its default key when the setting names no known key), joyKbdSlotSet(slot, keyIndex) stores one; the keyboard driver reads its keys through joyKbdSlotGet. joyKeyIndex(name) finds a key name's index (joyKeyFind its raw code). The Control Panel's Game Controllers page uses these, so the stored names and the defaults live only in the library.

    +

    joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise. joyRescanApp(appId) closes every handle of that program first (joyAppCleanup) and then rescans, for a program that holds handles itself (the BASIC JoyRescan; the Control Panel closes its one handle and calls joyRescan).

    The Game Port Driver

    joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.

    In the default mode the port makes up to two joysticks (A with axes 0-1 and buttons 0-1, B with axes 2-3 and buttons 2-3; either may be missing); in the four-axis mode one joystick with four axes and four buttons.

    @@ -7306,6 +8336,7 @@ while (running) {

    Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor).

    BASIC

    src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle.

    +

    For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.

    The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.

    @@ -7489,7 +8520,7 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) { FRAMEWAIT 30 Loop Until quitting ' set in the form's Unload event End Sub -

    GameKeyDown (game.bas) tells whether a key is held; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference.

    +

    GameKeyDown (game.bas) tells whether a key is held, GameKeyClear forgets the held keys, FrameMissed() counts the frames FRAMEWAIT skipped and GameWaitRetrace waits for the screen's retrace; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, sounds loaded once and played with volume, pan and pitch, sound the game makes as it goes (streams) and FM instruments; music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference.

    Porting a Game Library to DVX

    @@ -7581,7 +8612,8 @@ End Sub library A C library of shared functions (NAME.lib) with BASIC declarations basic A DVX BASIC program (NAME.dbp, built to NAME.app) game A C game (NAME.app): a 320x200 screen in a window, frame pacing, keyboard and joystick, sound effects and music - basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY + basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY + control A control written in DVX BASIC (NAME.dbp with NAME.ctl, built to NAME.wgt) usable from C programs and DVX BASIC

    The two game templates are explained in Writing Games for DVX.

    NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1 and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.

    The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.

    @@ -7617,6 +8649,8 @@ End Sub

    Your First Widget

    Your First Library

    Your First BASIC Program

    +

    Your First BASIC Control

    +

    Using BASIC Controls from C

    Debugging: the Log and Crash Reports

    Binary Interface Rules

    Packaging and Installing

    @@ -7671,7 +8705,7 @@ End Sub
    • A widget class (WidgetClassT): the paint and minimum-size handlers, and the shared text handlers (the widget's data starts with its text, so widgetAllocWithText creates it and WCLASS_HAS_TEXT frees it).
    • An API table (BadgeApiT) under the name "badge". It starts with WgtApiHeaderT, filled by WGT_API_HEADER. Programs reach it through the macros in badge.h, which call wgtGetApi("badge", sizeof(BadgeApiT)).
    • -
    • A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself.
    • +
    • A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. wgtIfaceSetType binds the interface to the class, so wgtGetIfaceOf and the generic calls (wgtGetProp, wgtSetProp, wgtCallMethod) find it from any badge widget.

    DVX_ABI_STAMP(badge) at file scope records the binary interface version. Its argument must be the module's file name in lower case.

    Run

    @@ -7709,6 +8743,9 @@ End Sub

    How the Library Works

    DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.

    BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.

    +

    A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length or a length never set give an empty string). The template's tplNameXor shows both:

    +
      // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING
    +  const uint8_t *tplNameXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen);

    Run

    Install it (LIBS/LOCAL/MATHX/ gets mathx.lib, mathx.dep and MATHX.DHS) and restart DVX. Build programs that use it with CFLAGS=-I../mathx.

    Help

    @@ -7738,6 +8775,61 @@ End Sub

    Next

    Packaging and Installing

    +
    +

    Your First BASIC Control

    +

    Create and Build

    +
      bin/sdk/newProject.sh control dial
    +  cd dial
    +  make
    +

    The build compiles the control project with bascomp into dial.wgt, a control file, and the help into dial.hlp. The project holds:

    +
      File         Purpose
    +  ----         -------
    +  dial.dbp     The project: [Project] Type = Control, the [Controls] list (and any [Modules] the controls need)
    +  dial.ctl     The control: its Begin UserControl header (natural size, focus, toolbox picture and tooltip, default event) and its code, a class
    +  icon24.bmp   The toolbox picture (the header's ToolboxIcon)
    +  dial.bhs     The control's BASIC help, installed beside it and added to the DVX BASIC help
    +  Makefile     KIND = control
    +

    The control's Public properties, methods and events are what programs use; UserControl is its drawing surface. The DVX BASIC help (UserControls: Your Own Controls) describes the language side.

    +

    Install

    +

    make install puts dial.wgt and DIAL.BHS in WIDGETS/LOCAL/DIAL/. When DVX starts it reads the control file through the BASIC run-time library and registers its controls; BASIC programs put a Dial on a form (Begin Dial Dial1 in a .frm, or from the designer's toolbox) and C programs create one by name (see Using BASIC Controls from C).

    +

    Next

    +

    Using BASIC Controls from C

    +

    Packaging and Installing

    +
    +
    +

    Using BASIC Controls from C

    +

    A control written in DVX BASIC is an ordinary widget type once it is registered: it has a widget class, an interface (WgtIfaceT) with its properties, methods and events, and a type name equal to its BASIC name. A C program uses it through the generic widget access, without a header of its own:

    +
      WidgetT  *dial = wgtCreateByName("Dial", parent);
    +  WgtValueT v    = wgtValueInt(40);
    +  WgtValueT ret;
    +
    +  wgtSetProp(dial, "Level", &v);            // Public Property Let Level
    +  wgtGetProp(dial, "Level", &v);            // Property Get: v.i == 40
    +  wgtCallMethod(dial, "Reset", NULL, 0, &ret);  // Public Sub Reset (ret.type WGT_IFACE_NONE)
    +  dial->onEvent = onDialEvent;              // RaiseEvent in the control's code
    +
    +  static void onDialEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) {
    +      if (strcmp(name, "Changed") == 0 && argc > 0) {
    +          ...                               // args[0]: the event's first argument
    +      }
    +  }
    +

    Property values follow the BASIC types: Integer, Long and Byte are WGT_IFACE_INT, Boolean WGT_IFACE_BOOL, Single WGT_IFACE_FLOAT, Double and Currency WGT_IFACE_DOUBLE, String, Date and Variant WGT_IFACE_STRING. A property declared As an Enum whose members are numbered 0, 1, 2 ... (each number once) is WGT_IFACE_ENUM with the member names in value order (WgtPropDescT.enumNames; wgtSetProp takes the number or a member name, wgtIfaceGetPropText gives the name); an Enum with other values is WGT_IFACE_INT. A method takes its arguments as they come (WGT_SIG_DYNAMIC) and returns its Function's value, or no value (WGT_IFACE_NONE) for a Sub. An event's numeric and Boolean arguments that onEvent changes go back to the control (ByRef parameters). Events the control names Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll go to the matching universal callback (onClick ...) instead; the dispatcher calls those for a BASIC control only when its code raises them (WCLASS_RAISES_EVENTS). The interface's events list each event with its parameters as BASIC writes them (WgtEventDescT.params).

    +

    wgtCreateByName goes through the interface's dynamic create hook (WgtIfaceT.dyn); the class has no C API structure (wgtGetApi finds none), because one create function cannot tell which BASIC class to make.

    +

    The Control File

    +

    A control project builds NAME.wgt, a file that holds only resources:

    +
      Part          Contents
    +  ----          --------
    +  Marker        DVX_RES_ONLY_MAGIC ("DVXRESONLY" and its NUL) at offset 0: no code
    +  MODULE        The compiled BASIC module (with DEBUG unless built for release)
    +  CTL0, CTL1 ...  Each control's Begin UserControl header text, in [Controls] order
    +  icon24, name  The first control's toolbox picture and tooltip; the second and later have "-2", "-3" ... suffixes, as for any widget file holding several widgets
    +  author, version ...  The project's metadata; helpfile its help file's name
    +

    The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.

    +

    The Bridge

    +

    Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.

    +

    Your First BASIC Control

    +

    Your First Widget

    +

    Debugging: the Log and Crash Reports

    The Log

    @@ -7927,7 +9019,7 @@ End Sub

    List Helper Library

    -

    Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing and keyboard handling), dropdown arrow rendering, item measurement, keyboard navigation, type-ahead search, and popup scrollbar hit testing (with the same thumb geometry the window manager paints).

    +

    Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing and keyboard handling), dropdown arrow rendering, item measurement, keyboard navigation, type-ahead search, popup scrollbar hit testing (with the same thumb geometry the window manager paints), and the icon view rules of Program Manager's view, which BASIC icon views share through listhelp.bas.

    Header: listhelp/listHelp.h

    Loaded as: bin/libs/listhelp.lib

    Constants

    @@ -8044,6 +9136,10 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to count Total number of items in the list. pageSize Number of visible items (used for PgUp/PgDn step size).

    Returns the new index, clamped to [0, count-1]. Returns -1 for unrecognized keys so callers can detect whether the key was consumed.

    +

    widgetGridNavigateIndex

    +

    The same keys in a grid of count items laid out row by row, columns items per row (icon views): Left and Right step through the items in order, Up, Down, Page Up and Page Down move by rows (pageRows rows a page) the way widgetNavigateIndex moves through a list -- it is called on the row numbers -- keeping the column, or landing on the last item when the row below is shorter; Home and End go to the first and last item. With nothing selected (current below 0) every one of these keys goes to the first item. With one column it gives what widgetNavigateIndex gives.

    +
    int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows);
    +

    Returns the new index, or -1 for any other key.

    widgetDropdownPopupRect

    Calculate the screen rectangle for a dropdown popup list, positioning it below the owning widget and clamping to screen bounds.

    void widgetDropdownPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
    @@ -8103,6 +9199,117 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to visibleItems Number of visible rows in the popup. scrollPos [in/out] Current scroll offset; updated in place when the click hits the scrollbar.

    Returns true if the click landed on the scrollbar (even if scrollPos was not changed, for example clicking the thumb), false if the click was in the item list area. When itemCount is less than or equal to DROPDOWN_MAX_VISIBLE, always returns false (there is no scrollbar).

    +

    Icon Views

    +

    The rules of an icon view with large icons, small icons or a list: Program Manager's view (progman/pmView.c) is built on them, and so are icon views written in BASIC (listhelp.bas ListIcon...), so both look and behave alike. They cover each view's cell and where an item's picture and name go in it, the grid inside a sunken frame with a scroll bar only when the rows do not fit (which narrows the grid), the two label lines under a large icon, the arrow keys among items in order or in cells of their own (gaps allowed), the scroll bar (a control's, as ListBox's: arrows, trough, thumb drag), when a press becomes a drag, the scrolling and the target cell while dragging, and where a dropped item lands. Positions are pixels from the view's top left (ListIconGridT) or the cell's (ListIconCellT).

    +
      Constant                 Value  Description
    +  --------                 -----  -----------
    +  LIST_ICON_FRAME          2      Sunken frame around the view.
    +  LIST_ICON_PAD            2      Cell edge to its picture; inside of the frame to the empty text.
    +  LIST_ICON_LARGE          32     Large icon edge.
    +  LIST_ICON_SMALL          16     Small icon edge.
    +  LIST_ICON_LABEL_GAP      2      Large icon to its label.
    +  LIST_ICON_LABEL_LINES    2      Label lines under a large icon.
    +  LIST_ICON_TEXT_GAP       4      Small icon to its name.
    +  LIST_ICON_LARGE_CELL_W   80     Large icon cell width.
    +  LIST_ICON_SMALL_CHARS    14     Name beside a small icon, in characters.
    +  LIST_ICON_LIST_CHARS     24     The list's name column, in characters.
    +  LIST_ICON_COLUMN_GAP     1      The list's name column to its detail column, in characters.
    +  LIST_ICON_DRAG_SLOP      4      Pixels a press moves (either way) before it drags.
    +  LIST_ICON_WHEEL_ROWS     1      Rows a wheel notch scrolls.
    +  LIST_ICON_LINE_ROWS      1      Rows an arrow key or a scroll bar arrow moves.
    +  LIST_ICON_SB_W           WGT_SB_W  The scroll bar's width: a control's (ListBox's).
    +  LIST_ICON_NO_CELL        -1     No cell (outside the grid).
    +  LIST_ICON_BY_ORDER       -1     listIconGrid rowsTaken: the items fill the grid in order.
    +
    typedef enum { ListIconLargeE, ListIconSmallE, ListIconListE, ListIconViewCountE } ListIconViewE;
    +typedef enum { ListIconScrollNoneE, ListIconScrollMovedE, ListIconScrollThumbE } ListIconScrollE;
    +
    +typedef struct {
    +    int32_t cellW;          // a list row spans the grid
    +    int32_t cellH;
    +    int32_t pictureX;       // the picture, pictureSize square
    +    int32_t pictureY;
    +    int32_t pictureSize;
    +    int32_t textX;          // the name (large icons: each line centred, textX 0)
    +    int32_t textY;
    +    int32_t nameChars;      // its length (a large icon's: per line)
    +    int32_t detailX;        // the list's detail column, 0 in the other views
    +} ListIconCellT;
    +
    +typedef struct {
    +    ListIconCellT cell;
    +    int32_t       innerX;   // inside the frame, less the scroll bar
    +    int32_t       innerY;
    +    int32_t       innerW;
    +    int32_t       innerH;
    +    int32_t       cols;
    +    int32_t       rowsVisible;  // rows shown whole
    +    int32_t       totalRows;
    +    int32_t       maxScroll;    // the largest top row
    +    int32_t       emptyX;       // the empty view's text
    +    int32_t       emptyY;
    +    bool          needSb;       // the scroll bar stands right of innerW, innerH tall
    +} ListIconGridT;
    +
    +void    listIconCellLayout(ListIconViewE view, int32_t charW, int32_t charH, int32_t innerW, ListIconCellT *out);
    +void    listIconGrid(ListIconViewE view, int32_t charW, int32_t charH, int32_t viewW, int32_t viewH, int32_t count, int32_t rowsTaken, ListIconGridT *out);
    +int32_t listIconGridMetric(const ListIconGridT *g, ListIconMetricE metric);
    +int32_t listIconCellAt(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow);
    +void    listIconLabelSplit(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2);
    +int32_t listIconNavigate(int32_t key, int32_t current, const int32_t *cols, const int32_t *rows, int32_t count, int32_t gridCols, int32_t pageRows);
    +bool    listIconDragStarted(int32_t dx, int32_t dy);
    +int32_t listIconDragScroll(const ListIconGridT *g, int32_t y, int32_t scrollRow);
    +int32_t listIconDropCell(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow);
    +int32_t listIconDropDest(int32_t from, int32_t cell, int32_t count);
    +ListIconScrollE listIconScrollPress(const ListIconGridT *g, int32_t x, int32_t y, int32_t *pScrollRow, int32_t *outGrab);
    +int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab);
    +
      Function               Description
    +  --------               -----------
    +  listIconCellLayout     The cell of a view whose grid is innerW wide. Large icons: LIST_ICON_LARGE_CELL_W wide, the picture centred at the top, two label lines under it. Small icons and the list: a row as tall as the font or the small picture (plus padding), the picture then the name; the list's row spans the grid and adds a detail column.
    +  listIconGrid           The grid of a view viewW x viewH (frame included) holding count items: laid out without a scroll bar first, with one LIST_ICON_SB_W wide (narrowing the grid) only when the rows do not fit. rowsTaken is LIST_ICON_BY_ORDER for items in order (count / columns rows, rounded up) or the rows items placed in cells of their own take.
    +  listIconGridMetric     One value of a grid by ListIconMetricE (ListIconCellWE ... ListIconWheelRowsE; the scroll bar as 1 / 0; 0 for an unknown metric). The BASIC binding reads the grid through it.
    +  listIconCellAt         The cell number (row * cols + col) under x, y of a view scrolled to scrollRow, with its column and row; LIST_ICON_NO_CELL outside the grid or right of the last column.
    +  listIconLabelSplit     A large icon's two label lines maxChars wide: the first ends at the last blank that fits (else it is cut at maxChars), the second starts after the blanks there and is cut to maxChars. *line2 points into name; *len2 is 0 when the name fits one line.
    +  listIconNavigate       The item an arrow key, Home, End, PgUp or PgDn moves to when item i stands in cell (cols[i], rows[i]): Left / Right the nearest before / after in reading order, Up / Down the nearest row above / below with the closest column, PgUp / PgDn the same pageRows away, Home / End the first / last in reading order; with no current item the first. For items in order it gives what widgetGridNavigateIndex gives. -1 for another key or when no item is that way.
    +  listIconDragStarted    True when a press has moved LIST_ICON_DRAG_SLOP pixels either way.
    +  listIconDragScroll     The top row after a drag move to y: one row up above the grid, one down below it.
    +  listIconDropCell       The cell a dragged item would drop on with the pointer at x, y, held inside the grid's height (listIconCellAt).
    +  listIconScrollPress    A left press at x, y: ListIconScrollNoneE left of the scroll bar or without one; else widgetScrollbarApplyHit -- an arrow moves *pScrollRow LIST_ICON_LINE_ROWS, the trough a page (ListIconScrollMovedE), the thumb is grabbed (ListIconScrollThumbE, *outGrab how far below its top).
    +  listIconScrollDrag     The top row with the grabbed thumb dragged to y (widgetScrollbarThumbDragScroll).
    +  listIconDropDest       Items in order: the index an item dragged from `from` takes when dropped on cell number cell (that cell's, the last for a cell past the end); -1 when that changes nothing or from / cell is out of range.
    +

    Program Manager's pmView.c draws the scroll bar with widgetDrawScrollbarV; a BASIC icon view draws the same bar with DrawScrollBar's control argument True.

    +
    +
    +

    BASIC

    +

    src/include/basic/listhelp.bas gives BASIC programs -- above all UserControls that draw a list or an icon view themselves -- the rules of this library (listHelpBas.c), so their lists behave like ListBox and ListView. A selection handle (1, 2, ...; 0 = none) holds a ListSelT and the item count it covers; display order is data order (no order map). Each handle records the program that made it; the shell's app-cleanup hook frees what a program leaves open.

    +
      BASIC                                                  C it calls
    +  -----                                                  ----------
    +  ListSelCreate(multi, count), ListSelFree, ListSelReset(h, count), ListSelCount(h)  listSelInit + listSelReset; listSelFree; listSelReset then listSelSetCursor (a cursor past the end becomes -1).
    +  ListSelClick h, index, shift                           listSelClick; shift is the event's Shift (vbShiftMask = range, vbCtrlMask = toggle).
    +  ListSelKey(h, keyCode, shift)                          listSelKey: LISTSEL_KEY_NONE, LISTSEL_KEY_ALL (Ctrl+A), LISTSEL_KEY_TOGGLE (Space).
    +  ListSelNavigate(h, keyCode, shift, columns, pageRows)  widgetGridNavigateIndex from the cursor, then listSelMove; the new cursor, or -1 when the key does not move it.
    +  ListSelMove h, index, shift                            listSelMove.
    +  ListSelCursor(h), ListSelSetCursor h, index            ListSelT.cursor; listSelSetCursor.
    +  ListSelIsSelected(h, index), ListSelSelectedCount(h)   listSelIsSelected; how many are.
    +  ListSelSetItem h, index, selected; ListSelSetAll h, selected  listSelSetItem; listSelSetAll.
    +  ListSelInsert h, index; ListSelRemove h, index         listSelInsert / listSelRemove (the handle's count follows).
    +  ListSelReorder h, fromIndex, dest                      listSelReorder.
    +  ListReorderDest(fromIndex, toSlot, count)              listReorderDest.
    +  ListNavigateIndex(keyCode, current, count, pageSize)   widgetNavigateIndex.
    +  ListGridNavigate(keyCode, current, count, columns, pageRows)  widgetGridNavigateIndex.
    +  ListTypeAhead(ch, items, current)                      widgetTypeAheadSearch over items separated by CHR$(10).
    +  ListTypeAheadMatch(ch, text)                           widgetTypeAheadMatch.
    +  ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken)  listIconGrid, then listIconGridMetric; LISTICON_SCROLLBAR as True / False.
    +  ListIconCellAt(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  listIconGrid, then listIconCellAt.
    +  ListIconDragScroll(mode, cw, ch, w, h, count, rowsTaken, topRow, y)  listIconGrid, then listIconDragScroll.
    +  ListIconDropCell(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  listIconGrid, then listIconDropCell.
    +  ListIconDropDest(fromIndex, cell, count)               listIconDropDest.
    +  ListIconDragStarted(dx, dy)                            listIconDragStarted.
    +  ListIconLabel$(name, maxChars, lineNo)                 listIconLabelSplit: line 1 or 2 ("" for any other line number or a line the name does not need).
    +  ListIconNavigate(keyCode, current, cells, columns, pageRows)  listIconNavigate over cells given as text: each item's column and row, every number separated by a comma.
    +  ListIconScrollPress(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  listIconScrollPress: the new top row, -1 when not on the scroll bar.
    +  ListIconScrollGrab(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  listIconScrollPress: the grab offset when the thumb is hit, else -1.
    +  ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y)  listIconScrollDrag.
    +

    An icon view's grid is figured from the view's size on each call (no handle). The LISTSEL_KEY_ and LISTICON_ CONSTs of listhelp.bas are compared with ListSelKeyE, ListIconViewE, LIST_ICON_BY_ORDER and ListIconMetricE by a formrt test.

    Music Library

    @@ -8163,7 +9370,7 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");

    Several Programs at Once

    Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music. When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.

    BASIC

    -

    src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it.

    +

    src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease; see the sound library).

    The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount and musicBasBusy for the statements BEEP, SOUND, PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.

    Licences

    libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:

    @@ -8186,7 +9393,10 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4"); printSettingsDefaults(&s) LPT1, text only, PRINTER.PRN, 80 columns, margins 3 lines / 5 columns, tab 8, wrap, header "&f", footer "Page &p". printPageSetupProblem(&ps) NULL when the page setup can be used, else the reason (a static buffer). printPageLines(&ps, model) Lines per page used: the set value, or the model's usual length (66; 60 for a laser printer). - printDestChoices[] / printModelChoices[] The labels and INI spellings (PrefsChoiceT), indexed by PrintDestE / PrintModelE. + printDestChoices[] / printModelChoices[] The labels and INI spellings (PrefsChoiceT), indexed by PrintDestE / PrintModelE. + printDestLabel(d) / printModelLabel(m) The label of one choice (for programs that look the library up by name). + printDeviceProblem(dest, file) NULL when the destination can be used, else why not (printing to a file needs its name). + printDeviceStore(h, model, dest, file) Put model, destination and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it.

    Jobs and Printer Problems

    A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.

    PrintHooksT hooks = { myAsk, myIdle, NULL, myState };
    @@ -8223,12 +9433,26 @@ if (!job) {
       printDialog(ctx, title, pageCount, &choice)           The common Print dialog: print to a port or a file, copies and, when pageCount > 0, all pages or a range. Starts from the settings; true with the PrintChoiceT filled when the user chose Print.
       printPageSetupDialog(ctx)                             The Page Setup dialog; saves the page setup to DVX.INI.
       printUiText(ctx, text, title, askFirst)               Print text with the page setup on the printer in the settings (the Print dialog first when askFirst).
    +  printUiTextChoice(ctx, text, title, &choice)          Print text on the destination, copies and page range of a PrintChoiceT (a Print dialog answer, or one the program filled) without asking; printUiText ends here.
       printUiImageFile(ctx, path, title, askFirst)          Print a picture file (BMP, PNG, JPEG, GIF).
       printUiWindow(ctx, win, askFirst)                     Print a window's contents as a picture.
       printUiOpenPort(ctx, port)                            A job with the user interface hooks on LPTn (0-based) or PRINT_PORT_DEFAULT.

    Jobs opened by these ask about printer problems with a Retry / Cancel message box. While a write waits, a modal Printing window with a Cancel button appears (at once for a document, after half a second for other output) and the whole desktop keeps running under it. Problems are told to the user; the functions return true only when everything was printed. Pictures need a printer model that prints them; on a text-only printer the user is told to choose the model in the Control Panel.

    BASIC

    -

    src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup and PrintCanPrintPictures. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries).

    +

    src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem) and PrintSettingsStore (printDeviceStore).

    +
      BASIC                                             C it calls
    +  -----                                             ----------
    +  PrintPageSetting(prefs, field), PrintPageText$(prefs, field)  printSettingsRead on a prefs.bas handle: one PrintPageSetupT field by name (Lines, Columns, MarginTop, MarginBottom, MarginLeft, MarginRight, TabWidth, Wrap; Header, Footer).
    +  PrintPageSetupProblem$(lines, ..., footer)        printPageSetupProblem ("" when usable).
    +  PrintPageSettingsStore(prefs, lines, ..., footer) printSettingsWrite of the page part (the printer part as read); False when printPageSetupProblem objects.
    +  PrintPageLines(prefs, model)                      printPageLines.
    +  PrintPageCount(text)                              printTextPageCount with the settings in DVX.INI.
    +  PrintTextPages(text, title, first, last, copies)  printUiTextChoice on the destination in the settings.
    +  PrintChoose(title, pageCount), PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst, PrintChoiceLast  printDialog; the readers return the PrintChoiceT of the last PrintChoose.
    +  PrintForm(form), PrintFormDialog(form)            printUiWindow on a loaded form's window (these two live in the BASIC runtime, which knows the forms).
    +  PrintPortPresent(port), PrintPortStatus(port)     printPortPresent / printPortStatus; port 1-3 is LPT1-LPT3.
    +  PrintStatusProblem$(status, afterWrite, where)    printStatusProblem ("" when the status shows none).
    +

    print.bas also has CONSTs for the PRINT_STATUS_ bits and the page setup limits and defaults (PRINT_LINES_STANDARD ... PRINT_COPIES_MAX); a formrt test compares them with print.h. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries).

    Serial Stack

    @@ -8370,9 +9594,9 @@ if (!job) { rs232GetDsr(com) DSR line state (0 or 1). rs232GetDtr(com) DTR line state (0 or 1). rs232GetRts(com) RTS line state (0 or 1). - rs232GetLsr(com) Line status register value. + rs232GetLsr(com) Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR and RS232_LSR_BREAK are its error bits. rs232GetMcr(com) Modem control register value. - rs232GetMsr(com) Modem status register value. + rs232GetMsr(com) Modem status register value; RS232_MSR_RI is the ring indicator.

    Configuration Setters

      Function                          Description
       --------                          -----------
    @@ -8566,6 +9790,24 @@ void        secCipherDestroy(SecCipherT *c);
    secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number). secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.

    secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.

    +

    BASIC

    +

    src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.

    +
      BASIC                                          C it calls
    +  -----                                          ----------
    +  SecRandom$(count), SecAddEntropy bytes         secRngBytes; secRngAddEntropy.
    +  SecDhCreate()                                  secDhCreate + secDhGenerateKeys.
    +  SecDhPublicKey$(h), SecDhSecret$(h)            secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
    +  SecDhComputeSecret(h, remotePublicKey)         secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
    +  SecDhFree h                                    secDhDestroy.
    +  SecCipherCreate(key)                           secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
    +  SecCipherCrypt$(h, bytes)                      secCipherCrypt on a copy.
    +  SecCipherSetCounter h, counter                 secCipherSetCounter (a DOUBLE: exact up to 2^53).
    +  SecCipherFree h                                secCipherDestroy.
    +  SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$  secSha256; secSha256Init / Update / Final (End frees the handle).
    +  SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$  secHmacSha256; secHmacInit / Update / Final.
    +  SecHkdf$(salt, secret, info, length)           secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
    +  SecEqual(a, b)                                 secConstTimeEqual (False for different lengths).
    +

    The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a formrt test, which also calls every function for real.

    @@ -9106,17 +10355,29 @@ if (xferPoll(x, nowMs()) != XferRunningE) {

    File Transfers from BASIC

    Add xfer.bas (and comm.bas, for SerOpen) to a BASIC project to send and receive files over a serial port. Start a transfer, then call XferPoll often, for example from a Timer, until it no longer returns XFER_RUNNING. While it runs, do not read the port yourself and keep it detached from any Terminal control. Received files never replace existing ones: the new file gets a new name.

    +

    Transfers over a connection (conn.bas: ConnSendFiles, ConnReceiveFiles) are the same handles, with the options a terminal needs (what to do with an existing file -- XFER_EXIST_ASK stops in XFER_ASKING for XferAnswer --, escaping every control character, continuing interrupted files); ConnPoll moves them along. Every handle belongs to the program that made it: whatever a program leaves open is cancelled and freed when it ends.

      Function                             Description
       --------                             -----------
       XferSendFile(com, proto, path)       Send one file over COM port com (1-4). Returns a handle, or 0.
       XferReceive(com, proto, target)      Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0.
    -  XferPoll(handle)                     Move the transfer along. Returns XFER_RUNNING, XFER_DONE, XFER_FAILED or XFER_CANCELLED.
    +  XferPoll(handle)                     Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED or XFER_CANCELLED.
    +  XferStatus(handle)                   The same, without moving it along (XFER_IDLE for a bad handle).
    +  XferSending(handle), XferProtocol(handle)  True for a send; the protocol.
       XferBytes(handle), XferSize(handle)  Bytes of the current file done, and its size (-1 when unknown).
       XferFile$(handle)                    The file being transferred, as stored on this computer.
    +  XferFileIndex(handle), XferFileCount(handle)  Which file of the batch (from 0), and how many (-1 while unknown).
    +  XferBatchBytes(handle), XferBatchSize(handle)  Bytes of the whole batch done, and its size (-1 when unknown).
    +  XferResumedAt(handle), XferErrors(handle)  Where the current file continued from; retries so far.
    +  XferAskSize(handle), XferCanResume(handle)  XFER_ASKING: the size of the file already there; True when XFER_EXIST_RESUME can continue it.
    +  XferAskRemoteSize(handle)            XFER_ASKING: the size the sender offers (XferProgressT.askRemoteSize; -1 when unknown).
    +  XferAnswer handle, action            Answer XFER_ASKING with XFER_EXIST_RENAME, _RESUME, _OVERWRITE or _SKIP.
       XferMessage$(handle)                 How the transfer ended, or why it failed.
       XferCancel handle                    Stop a running transfer (the other side is told).
    -  XferClose handle                     Free the handle.
    -

    Protocols: XFER_XMODEM, XFER_XMODEM1K, XFER_YMODEM, XFER_ZMODEM. At most four transfers run at once.

    + XferClose handle Free the handle. + XferCrc16(bytes), XferCrc32(bytes) xferCrc16 / xferCrc32 of a BINARY STRING (every byte value counts); the CRC-32 as a LONG (negative when its top bit is set). + XferSafeName$(fileName) xferSafeName: the 8.3 name a receive stores a sender's file name as. +

    XFER_WINDOW_MAX (xfer.bas) is the largest ZMODEM receive window conn.bas's ConnSetXferWindow takes.

    +

    Protocols: XFER_XMODEM, XFER_XMODEM1K, XFER_YMODEM, XFER_ZMODEM. At most eight transfers run at once (XFER_MAX_FILES files in one batch).

    Dim h As Long
     If SerOpen(1, 115200, 8, "N", 1, SER_HANDSHAKE_RTSCTS) Then
         h = XferReceive(1, XFER_ZMODEM, "C:\DOWNLOAD")
    @@ -9182,7 +10443,18 @@ End Sub
    onMouseUp void (*)(WidgetT *w, int32_t button, int32_t x, int32_t y) Fires on mouse button release. onMouseMove void (*)(WidgetT *w, int32_t button, int32_t x, int32_t y) Fires on mouse movement over the widget. onScroll void (*)(WidgetT *w, int32_t delta) Fires on mouse wheel scroll. - onValidate bool (*)(WidgetT *w) Validation callback. Return false to cancel a pending write. + onValidate bool (*)(WidgetT *w) Validation callback. Return false to cancel a pending write. + onEvent void (*)(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) A named event the widget fires that has no callback above (ListView ColumnClick, TextArea GutterClick, HelpView LinkClick, an event a BASIC control raises). args may be written back. +

    wgtFireEvent(w, name, args, argc) fires an event by name: the names above (Click, DblClick, Change, GotFocus, LostFocus, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, Scroll) go to their callback, every other name to onEvent (see dvxWgt.h, Custom Events).

    +

    Generic Access

    +

    Every widget type can be created, read, written and called by name through its interface descriptor, with values as WgtValueT -- the way the BASIC form runtime and the designer drive widgets, and the way a C program uses a widget it has no header for (a BASIC control, for one):

    +
    WidgetT  *list = wgtCreateByName("ListBox", parent);
    +WgtValueT item = wgtValueStr("first");
    +WgtValueT count;
    +
    +wgtCallMethod(list, "AddItem", &item, 1, NULL);
    +wgtCallMethod(list, "ListCount", NULL, 0, &count);   // count.i == 1
    +

    wgtGetProp / wgtSetProp work the same way. See dvxWgt.h, Generic Widget Access.

    Common Operations

      Function                                                    Description
       --------                                                    -----------
    @@ -9241,14 +10513,18 @@ End Sub

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  Cols         Integer    Read-only    Number of columns.
    -  Rows         Integer    Read-only    Number of rows.
    +  Cols         Integer    Read/Write   Number of columns. Setting it (or Rows) resizes the terminal (1-512): screen and scrollback start empty and the modes the host set (wrap, origin, cursor visible, scroll region) are reset; palette, scrollback length and comm callbacks stay; the window is laid out again.
    +  Rows         Integer    Read/Write   Number of rows.
       Scrollback   Integer    Read/Write   Maximum number of scrollback lines. Setting it discards the existing history.

    Methods (BASIC Interface)

      Method       Description
       ------       -----------
       Clear        Clear the terminal screen.
    +  Copy         Copy the selection (copySelection); returns False when nothing is selected.
    +  Paste        Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks).
       Poll         Process any pending bytes on the attached communication channel.
    +  ResetColors  The standard CGA colors again (setPalette NULL).
    +  SetColor     index, color: one attribute color (0-15) from a BASIC color (&HBBGGRR, converted with wgtBasicColor).
       Write        Write a string into the terminal (with ANSI escape processing).

    Events

    AnsiTerm uses the common events only. No widget-specific events are defined.

    @@ -9335,7 +10611,7 @@ btn->onClick = onOkClicked;

    Canvas

    A freeform drawing surface with a fixed-size pixel buffer stored in the display's native pixel format. Provides drawing primitives (lines, rectangles, circles, text, individual pixels; coordinates may lie anywhere, drawing is clipped to the buffer) and supports loading BMP/PNG/JPEG/GIF files and saving BMP or PNG files (via dvxLoadImage/dvxSaveImage). Mouse interaction is available via a callback. The buffer is rendered to screen as a straight blit with no per-pixel conversion.

    -

    Canvas coordinates are independent of widget position: (0,0) is the top-left of the canvas content, not the widget. The widget frames the buffer with a 2-pixel sunken bevel.

    +

    Canvas coordinates are independent of widget position: (0,0) is the top-left of the canvas content, not the widget. By default the widget frames the buffer with a sunken bevel CANVAS_BORDER_WIDTH (2) pixels wide; wgtCanvasSetBorderStyle(w, CANVAS_BORDER_NONE) drops it, so the widget is exactly the buffer's size (a drawing surface for a UserControl or a form). The frame width is the widget's contentOffX / contentOffY, so mouse coordinates are relative to the buffer either way.

    Header: widgets/canvas.h

    Creation

    WidgetT *cv = wgtCanvas(parent, 320, 200);
    @@ -9356,7 +10632,10 @@ btn->onClick = onOkClicked; void wgtCanvasSetMouseCallback(w, cb) Set a mouse callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). cx/cy are canvas-relative, drag=true on mouse-move during press. int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file: BMP when path ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error. int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error. - void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white. + void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white. + void wgtCanvasSetBorderStyle(w, style) CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out. + int32_t wgtCanvasGetBorderStyle(w) CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE. + bool wgtCanvasGetDisplay(w, out) Fill a DisplayT aimed at the canvas pixels (the screen's pixel format, clipped to the canvas), so any libdvx drawing call -- drawText, drawBevel, drawArrowGlyph, wmDrawScrollbarAt, rectCopyTransparent -- draws on the canvas with the context's blit ops; call wgtInvalidatePaint afterwards. False (out untouched) for a widget that is not a canvas. The BASIC DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow and PrintAccel methods use it.

    API Struct (wgtRegisterApi "canvas")

      Slot                 Function
       ----                 --------
    @@ -9375,11 +10654,15 @@ btn->onClick = onOkClicked;
    setPixel wgtCanvasSetPixel getPixel wgtCanvasGetPixel drawText wgtCanvasDrawText - resize wgtCanvasResize + resize wgtCanvasResize + setBorderStyle wgtCanvasSetBorderStyle + getBorderStyle wgtCanvasGetBorderStyle + getDisplay wgtCanvasGetDisplay

    Properties

    -

    Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers two read-only properties:

    +

    Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers these properties:

      Property      Type      Access      Description
       --------      ----      ------      -----------
    +  BorderStyle   Enum      Read/Write  None (0) or FixedSingle (1, the default): wgtCanvasSetBorderStyle / wgtCanvasGetBorderStyle.
       ImageHeight   Integer   Read-only   Height of the canvas buffer in pixels (0 when there is no buffer).
       ImageWidth    Integer   Read-only   Width of the canvas buffer in pixels (0 when there is no buffer).

    Methods (BASIC Interface)

    @@ -9391,7 +10674,7 @@ btn->onClick = onOkClicked; DrawText x%, y%, text$ Draw text at the given position. FillCircle cx%, cy%, radius%, color% Fill a circle. FillRect x%, y%, w%, h%, color% Fill a rectangle. - GetPixel(x%, y%) Returns the color at a pixel (&HBBGGRR). + GetPixel(x%, y%) Returns the color at a pixel (&HBBGGRR). With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. Load(path$) Load a BMP, PNG, JPEG or GIF image (the canvas takes its size). Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). Resize w%, h% Resize the canvas (the drawing is cleared to white). Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). @@ -9463,7 +10746,9 @@ wgtComboBoxAddItem(cb, "Times"); void wgtComboBoxRemoveItem(w, idx) Remove the item at idx. The selection stays on the same item; removing the selected item leaves no selection (the edit text is kept). void wgtComboBoxClear(w) Remove all owned items and reset the selection. const char *wgtComboBoxGetItem(w, idx) Get the text of an item by index. - int32_t wgtComboBoxGetItemCount(w) Get the total number of items. + int32_t wgtComboBoxGetItemCount(w) Get the total number of items. + void wgtComboBoxSetMaxLength(w, maxLen) Change the maximum editable text length (0 => default 256); longer text is cut. + int32_t wgtComboBoxGetMaxLength(w) Get the maximum editable text length.

    API Struct (wgtRegisterApi "combobox")

      Slot           Function
       ----           --------
    @@ -9476,11 +10761,14 @@ wgtComboBoxAddItem(cb, "Times");
    removeItem wgtComboBoxRemoveItem clear wgtComboBoxClear getItem wgtComboBoxGetItem - getItemCount wgtComboBoxGetItemCount + getItemCount wgtComboBoxGetItemCount + setMaxLength wgtComboBoxSetMaxLength + getMaxLength wgtComboBoxGetMaxLength

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  ListIndex    Integer    Read/Write   Index of the currently selected item (-1 if none).
    + ListIndex Integer Read/Write Index of the currently selected item (-1 if none). + MaxLength Integer Read/Write Most characters the edit field holds (0 = the default, 256).

    Editable text is accessible via the generic "Text" property. "ListCount" is available through the ListCount method.

    Methods (BASIC Interface)

      Method              Description
    @@ -9683,6 +10971,39 @@ wgtDropdownAddItem(dd, "Blue");

    Default Event

    "Click" (VB basName: DropDown).

    +
    +

    HelpView

    +

    One topic of a compiled help file, shown the way the DVX Help Viewer shows it -- the Help Viewer itself uses this widget. Headings (three levels), word-wrapped paragraphs and list items, notes (info, tip and warning), code blocks and tables (preformatted), pictures (left, centered or right), horizontal rules and links, in a scrolling pane, in the theme's colours. Text re-wraps when the widget changes width.

    +

    Links take the keyboard focus (Tab) and are followed with a click, Enter or Space: the widget keeps the link's target topic ID (wgtHelpViewGetLinkTarget) and calls the link callback with it. The widget does not change topic by itself; the callback usually finds the topic (hlpFileFindTopic) and shows it, which may rebuild the pane from inside the link's own handler. Everything a topic shows is copied when it is shown, so the help file may be closed afterwards.

    +

    The help file is read with the help file library (hlpfile/hlpFile.h).

    +

    Header: widgets/helpView.h

    +

    BASIC name: HelpPane (see the HelpPane control in the BASIC reference)

    +

    Depends on: hlpfile.lib

    +

    Creation

    +
    HlpFileT *file = hlpFileOpen("MYAPP.HLP");
    +WidgetT  *view = wgtHelpView(parent);
    +
    +view->weight = WGT_WEIGHT_FILL;
    +wgtHelpViewSetLinkCallback(view, onLink);
    +wgtHelpViewShowTopic(view, file, hlpFileStartTopic(file, NULL));
    +
    +static void onLink(WidgetT *w, const char *target) {
    +    wgtHelpViewShowTopic(w, file, hlpFileFindTopic(file, target));
    +}
    +

    Macros

    +
      Macro                                      Description
    +  -----                                      -----------
    +  wgtHelpView(parent)                        Create an empty help view.
    +  wgtHelpViewShowTopic(w, file, topic)       Show topic (0 .. hlpFileTopicCount - 1) of an open help file, scrolled to the top. A NULL file or a topic out of range shows nothing.
    +  wgtHelpViewClear(w)                        Show nothing.
    +  wgtHelpViewGetTopic(w)                     The topic shown, -1 for none.
    +  wgtHelpViewGetLinkTarget(w)                The target topic ID of the last link followed ("" before any).
    +  wgtHelpViewSetLinkCallback(w, fn)          Call fn(w, target) when a link is followed. Signature: void (*fn)(WidgetT *w, const char *target); target is valid during the call. After it (unless it rebuilt the pane), the HelpView fires its own event LinkClick (Topic As String) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_LinkClick).
    +

    Events

    +
      Callback          Description
    +  --------          -----------
    +  link callback     A link was clicked, or Enter or Space was pressed on a focused link.
    +

    ImageButton

    A clickable button that displays an image instead of text. Has the same press/release visual feedback and focus handling as a regular Button. Can be created from raw pixel data or loaded from an image file. Typically used to populate a Toolbar.

    @@ -9901,7 +11222,7 @@ wgtListViewSetData(lv, cells, 2); wgtListViewGetSelected(w) Get the index of the selected row (-1 if none). wgtListViewSetSelected(w, idx) Set the selected row by index (outside the list = none) and scroll it into view. wgtListViewSetSort(w, col, dir) Set the sort column and direction. A column outside the defined columns, or ListViewSortNoneE, clears the sort. Ignored while reorder is enabled. - wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). + wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). After it, the ListView fires its own event ColumnClick (Column As Integer, 0-based) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_ColumnClick). wgtListViewSetMultiSelect(w, multi) Enable or disable multi-selection. wgtListViewIsItemSelected(w, idx) Check if a specific row is selected. wgtListViewSetItemSelected(w, idx, selected) Select or deselect a specific row. @@ -9953,7 +11274,9 @@ wgtListViewSetData(lv, cells, 2); WidgetT *wgtProgressBar(parent) Create a horizontal progress bar. WidgetT *wgtProgressBarV(parent) Create a vertical progress bar. void wgtProgressBarSetValue(w, val) Set the progress value (0-100). Clamped to 0..maxValue. - int32_t wgtProgressBarGetValue(w) Get the current progress value. + int32_t wgtProgressBarGetValue(w) Get the current progress value. + void wgtProgressBarSetVertical(w, v) Make the bar vertical (true) or horizontal (false); the layout is redone. + bool wgtProgressBarGetVertical(w) True when the bar is vertical.

    API Struct (wgtRegisterApi "progressbar")

      Slot       Function
       ----       --------
    @@ -9961,11 +11284,14 @@ wgtListViewSetData(lv, cells, 2);
    create wgtProgressBar createV wgtProgressBarV setValue wgtProgressBarSetValue - getValue wgtProgressBarGetValue + getValue wgtProgressBarGetValue + setVertical wgtProgressBarSetVertical + getVertical wgtProgressBarGetVertical

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  Value        Integer    Read/Write   Current progress value (0-100).
    + Value Integer Read/Write Current progress value (0-100). + Vertical Boolean Read/Write True: the bar stands upright and fills from the bottom.

    Methods

    No widget-specific methods.

    Events

    @@ -10034,8 +11360,14 @@ WidgetT *content = wgtVBox(sp); -------- ----------- WidgetT *wgtScrollPane(parent) Create a scroll pane container. void wgtScrollPaneScrollToChild(sp, child) Scroll so that the given child widget is visible. - void wgtScrollPaneScrollToTop(w) Scroll back to the top-left of the content. - void wgtScrollPaneSetNoBorder(w, noBorder) When true, removes the border around the scroll pane. + void wgtScrollPaneScrollToTop(w) Scroll back to the top-left of the content and re-lay out. + void wgtScrollPaneSetNoBorder(w, noBorder) When true, removes the border around the scroll pane. + int32_t wgtScrollPaneGetScrollTop(w) Vertical scroll position in pixels. + void wgtScrollPaneSetScrollTop(w, pos) Set the vertical scroll position; the layout clamps it to the content. + int32_t wgtScrollPaneGetScrollLeft(w) Horizontal scroll position in pixels. + void wgtScrollPaneSetScrollLeft(w, pos) Set the horizontal scroll position; the layout clamps it to the content. +

    Padding and spacing are the common WidgetT padding / spacing fields (unset = none); wgtSetPaddingPx / wgtSetSpacingPx set them in pixels.

    +

    When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event.

    API Struct (wgtRegisterApi "scrollpane")

      Slot             Function
       ----             --------
    @@ -10043,13 +11375,23 @@ WidgetT *content = wgtVBox(sp);
       create           wgtScrollPane
       scrollToChild    wgtScrollPaneScrollToChild
       setNoBorder      wgtScrollPaneSetNoBorder
    -  scrollToTop      wgtScrollPaneScrollToTop
    + scrollToTop wgtScrollPaneScrollToTop + getScrollLeft wgtScrollPaneGetScrollLeft + setScrollLeft wgtScrollPaneSetScrollLeft + getScrollTop wgtScrollPaneGetScrollTop + setScrollTop wgtScrollPaneSetScrollTop

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  NoBorder     Boolean    Read/Write   Whether the border around the scroll pane is hidden.
    + NoBorder Boolean Read/Write Whether the border around the scroll pane is hidden. + Padding Integer Read/Write Pixels between the edge and the children (wgtGetPaddingPx / wgtSetPaddingPx). + Spacing Integer Read/Write Pixels between the children (wgtGetSpacingPx / wgtSetSpacingPx). + ScrollTop Integer Read/Write Vertical scroll position in pixels. + ScrollLeft Integer Read/Write Horizontal scroll position in pixels.

    Methods

    -

    No widget-specific methods.

    +
      Method        Description
    +  ------        -----------
    +  ScrollToTop   wgtScrollPaneScrollToTop.

    Events

      Callback     Description
       --------     -----------
    @@ -10208,18 +11550,23 @@ WidgetT *right = wgtVBox(sp);
    -------- ----------- WidgetT *wgtSplitter(parent, vertical) Create a splitter. vertical=true for left/right panes (vertical divider), false for top/bottom. void wgtSplitterSetPos(w, pos) Set the divider position in pixels from the leading edge. - int32_t wgtSplitterGetPos(w) Get the current divider position in pixels. + int32_t wgtSplitterGetPos(w) Get the current divider position in pixels. + void wgtSplitterSetVertical(w, vertical) Change the orientation (true = left/right panes); the position is kept and clamped to the new axis at the next layout. + bool wgtSplitterGetVertical(w) True when the panes are side by side.

    API Struct (wgtRegisterApi "splitter")

      Slot       Function
       ----       --------
       header     WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
       create     wgtSplitter
       setPos     wgtSplitterSetPos
    -  getPos     wgtSplitterGetPos
    + getPos wgtSplitterGetPos + setVertical wgtSplitterSetVertical + getVertical wgtSplitterGetVertical

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  Position     Integer    Read/Write   Divider position in pixels.
    + Position Integer Read/Write Divider position in pixels. + Vertical Boolean Read/Write True (default): panes side by side. False: panes above and below.

    Methods

    No widget-specific methods.

    Events

    @@ -10291,7 +11638,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");

    TextInput / TextArea

    -

    Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization, line numbers, find/replace, and gutter decorators.

    +

    Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization (built-in BASIC and help-source colorizers, the BASIC one shared with the DVX BASIC IDE), line numbers, find/replace, line markers and gutter clicks.

    Header: widgets/textInpt.h

    Creation

      Macro                            Description
    @@ -10314,32 +11661,65 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
       bool wgtTextAreaFindNext(w, needle, caseSens, fwd)          Search from the cursor and select the match (a forward match at the cursor counts; repeating moves past it). Returns true if found.
       int32_t wgtTextAreaReplaceAll(w, needle, repl, caseSens)    Replace all occurrences. Returns the number of replacements made.
       bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd)  If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found.
    -  void wgtTextAreaSetLineDecorator(w, fn, ctx)                Set a gutter line decorator callback. Returns a color and receives the line number.
    +  void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb)  Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way.
    +  void wgtTextAreaClearLineMarkers(w)                         Remove every line marker.
    +  void wgtTextAreaSetSyntaxMode(w, mode)                      Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source) or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off.
       int32_t wgtTextAreaGetCursorLine(w)                         Get the current cursor line number.
       void wgtTextAreaSetGutterClickCallback(w, fn)               Set a callback for gutter clicks (e.g. for breakpoint toggling).
       int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize)         Copy the word under the cursor into buf. Returns its length.
    -  void wgtTextAreaSetSyntaxColors(w, colors, count)           Set the color palette used by the colorizer.
    +  void wgtTextAreaSetSyntaxColors(w, colors, count)           Set the color palette used by the colorizer (0xRRGGBB per TEXT_SYNTAX_ index; 0 = the default).
       void wgtTextAreaCursorToEnd(w)                              Move the cursor to the very end of the buffer (past the last character).
    -  void wgtTextAreaSetCursor(w, line, col)                     Move the cursor to 1-based line/column without selecting; scrolls it into view. Out-of-range values clamp to the text.
    + void wgtTextAreaSetCursor(w, line, col) Move the cursor to 1-based line/column without selecting; scrolls it into view. Out-of-range values clamp to the text. + int32_t wgtTextInputGetSelStart(w) A single-line box's selection start (0-based), or its cursor when nothing is selected. + int32_t wgtTextInputGetSelLength(w) Characters selected (0 = none). + const char *wgtTextInputGetSelText(w) The selected text, valid until the next call. + void wgtTextInputSetSelStart(w, start) Cursor to offset start, nothing selected. + void wgtTextInputSetSelLength(w, length) Select length characters from SelStart (clamped to the text). + void wgtTextInputSetSelText(w, text) Replace the selection (or insert at the cursor) with text (texthelp textEditSingleReplaceSel; a masked box fills its slots); fires onChange. + void wgtTextInputCut(w), wgtTextInputCopy(w), wgtTextInputPaste(w), wgtTextInputSelectAll(w), wgtTextInputUndo(w) The box's Ctrl+X / C / V / A / Z keys, run through its own key handler (password, mask and read-only rules included).

    API Struct (wgtRegisterApi "textinput")

    -

    The combined "textinput" API exposes all four constructors plus the TextArea functions. The designer uses per-type APIs registered under the names "textbox" (single-line) and "textarea" (multi-line), each exposing only the header and a create slot. Every API struct starts with a header (WGT_API_HEADER: ABI version and size, checked by wgtGetApi).

    +

    The combined "textinput" API exposes all four constructors plus the TextArea functions and, appended at its end (inputGetSelStart ... inputUndo), the TextBox selection and clipboard functions. The designer uses per-type APIs registered under the names "textbox" (single-line) and "textarea" (multi-line), each exposing only the header and a create slot. Every API struct starts with a header (WGT_API_HEADER: ABI version and size, checked by wgtGetApi).

    Properties (BASIC Interface)

    -

    TextBox has no widget-specific properties. Text is managed via the generic "Text" property. TextArea exposes:

    +

    Text is managed via the generic "Text" property. TextBox exposes:

    +
      Property       Type       Access       Description
    +  --------       ----       ------       -----------
    +  MaxLength      Integer    Read/Write   Characters the box holds (its buffer size less one). Setting it gives a new buffer of that size (0 = TEXT_EDIT_DEFAULT_MAX_LEN, at most TEXT_EDIT_SINGLE_MAX_LEN), the text cut to fit; a masked box keeps its mask's length.
    +  PasswordChar   String     Read/Write   "" for a plain box; one character makes it a password box (as wgtPasswordInput, which shows a small bullet) showing that character for each one typed. Ignored on a masked box.
    +  Mask           String     Read/Write   "" for a plain box; a mask makes the live box a masked input (as wgtMaskedInput) holding what fits of its text; "" makes it plain again with the formatted text and its earlier MaxLength. Replaces PasswordChar.
    +  SelStart       Integer    Read/Write   wgtTextInputGetSelStart / wgtTextInputSetSelStart.
    +  SelLength      Integer    Read/Write   wgtTextInputGetSelLength / wgtTextInputSetSelLength.
    +  SelText        String     Read/Write   wgtTextInputGetSelText / wgtTextInputSetSelText.
    +

    TextBox methods: Copy, Cut, Paste, SelectAll and Undo (wgtTextInputCopy ...).

    +

    TextArea exposes:

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  CursorLine   Integer    Read-only    Current cursor line number (1-based).
    + CursorLine Integer Read-only Current cursor line number (1-based). + CursorCol Integer Read-only Current cursor column (1-based, in characters). + LineCount Integer Read-only Number of lines. + SelStart Integer Read/Write Selection start offset (0-based), or the cursor's when none; setting it moves the cursor there. + SelLength Integer Read/Write Characters selected; setting it selects from SelStart. + SelText String Read/Write The selection; setting it replaces it (textEditMultiReplaceSel), firing Change. + MaxLength Integer Read/Write Buffer capacity; setting it makes a new buffer (0 = TEXTAREA_DEFAULT_BUF, at most TEXT_EDIT_MULTI_MAX_LEN) with the text cut to fit.

    Methods (BASIC Interface, TextArea)

      Method                                            Description
       ------                                            -----------
       AppendText text$                                  Append text to the end of the buffer.
    +  ClearLineMarkers                                  wgtTextAreaClearLineMarkers.
    +  Copy / Cut / Paste / SelectAll / Undo             The Ctrl+C / X / V / A / Z commands (the texthelp textEditMulti* functions the key handler runs); Cut, Paste and Undo fire Change and do nothing when read-only.
    +  CursorToEnd                                       wgtTextAreaCursorToEnd.
       FindNext needle$, caseSensitive, forward          Search from the cursor (a match at the cursor counts; repeating moves past it). Returns True if found.
    +  GetLine(line%)                                    Returns a line's text (1-based), "" past the end.
    +  GetSyntaxColor(index%)                            Returns an index's colour (BASIC &HBBGGRR), -1 for the foreground.
       GetWordAtCursor()                                 Returns the word under the cursor.
       GoToLine line%                                    Scroll to a line and select it.
       ReplaceAll needle$, replacement$, caseSensitive   Replace all occurrences. Returns the count.
    +  ReplaceSelection(needle$, repl$, caseSens, fwd)   wgtTextAreaReplaceSelection (WGT_SIG_STR_STR_BOOL_BOOL).
       SetAutoIndent enabled                             Enable or disable automatic indentation on newline.
       SetCaptureTabs enabled                            When True, Tab inserts whitespace instead of moving focus.
       SetCursor line%, col%                             Move the cursor to a 1-based line and column without selecting.
    +  SetLineMarker line%, gutter&, back&               wgtTextAreaSetLineMarker with BASIC colours (&HBBGGRR, -1 = TEXTAREA_MARKER_NONE).
       SetShowLineNumbers show                           Show or hide line numbers in the gutter.
    +  SetSyntaxColor index%, color&                     One palette entry (BASIC colour, -1 = the default).
       SetSyntaxMode mode$                               Activate built-in syntax highlighting (e.g. "dhs", "bas"; "" to disable).
       SetTabWidth width%                                Set the tab stop width in characters.
       SetUseTabChar useChar                             When True, Tab inserts a literal tab; when False, spaces.
    @@ -10349,6 +11729,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced"); onChange Fires when the text content changes. onKeyPress Fires on each key press (ASCII value). onValidate Called before committing a change. Return false to cancel. +

    The TextArea interface lists the event GutterClick (LineNumber As Integer, 1-based). The TextArea fires it with wgtFireEvent after its gutter-click callback, so it reaches the widget's onEvent (the form runtime's CtrlName_GutterClick); the gutter takes clicks when either the callback or onEvent is set.

    Default Event

    "Change" for both TextBox (basName: TextBox) and TextArea (basName: TextArea).

    diff --git a/screenshots/86box/apps-basdemo.png b/screenshots/86box/apps-basdemo.png index 42c6a707..1f073d2c 100644 --- a/screenshots/86box/apps-basdemo.png +++ b/screenshots/86box/apps-basdemo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bde86ad24acd488d9f87aafb4528ab272d5a99cda89a8a0d25a09aa3ce3af380 -size 16263 +oid sha256:887c02004051c2da25539cc3624e62ceda604bd27a8b5ed9a7fbaa3385d99c4a +size 16677 diff --git a/screenshots/86box/apps-helpedit.png b/screenshots/86box/apps-helpedit.png index 7bdbace7..e6f9a19f 100644 --- a/screenshots/86box/apps-helpedit.png +++ b/screenshots/86box/apps-helpedit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f416a623ee536b02de3d85d3725d04bfad9b4a84c78855dec17124455b058f0e -size 14159 +oid sha256:993ba360a941cf9a86d5c2acc34d623baa87d2ad406713511dac7a98f51e5d8d +size 14322 diff --git a/screenshots/86box/cpanel-cp-types.png b/screenshots/86box/cpanel-cp-types.png index c6fa0024..e8d0d9b3 100644 --- a/screenshots/86box/cpanel-cp-types.png +++ b/screenshots/86box/cpanel-cp-types.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3de4147235ff1e6471f76dba46886eebb711bb47971f919a5b3a46acf051e774 -size 21268 +oid sha256:539e278fe16fea322ab084e47d2acbcc8044e13cc63ee8fc473956788fe3b95d +size 22658 diff --git a/screenshots/86box/f1-h-basdem.png b/screenshots/86box/f1-h-basdem.png index 83acc722..f4a7268a 100644 --- a/screenshots/86box/f1-h-basdem.png +++ b/screenshots/86box/f1-h-basdem.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a853797b56c53f72336edbf96c838aed1d0806f7a3dcd77e6e3322a405dd5f75 -size 20582 +oid sha256:35fc158b43b5d885c56e667e5bb68ebc1c50d0f08889e0169da011d0cdb3c89e +size 20672 diff --git a/screenshots/86box/f1-h-helped.png b/screenshots/86box/f1-h-helped.png index 0a11172a..bdd51317 100644 --- a/screenshots/86box/f1-h-helped.png +++ b/screenshots/86box/f1-h-helped.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c4f1ae1c4be4016a416bcff9a3a066a367af1b22fbd026f9895cf39f096fc26 -size 22313 +oid sha256:bc316f8c448a6da952568e02e50c062bf7de0541912a50943f02de76dabaf83f +size 22492 diff --git a/screenshots/86box/hang-ended.png b/screenshots/86box/hang-ended.png index c60d4df3..961872bf 100644 --- a/screenshots/86box/hang-ended.png +++ b/screenshots/86box/hang-ended.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:643242f2675830fea0534491de621787206cba57b87b55b2a2a5311f2f618153 -size 14943 +oid sha256:cf4a5faa4fd6cc3686eda52e384972571582a170db641aad18d6c7bc87dad9ed +size 14938 diff --git a/screenshots/86box/hang-taskmgr.png b/screenshots/86box/hang-taskmgr.png index 0629d2be..b6caf9b6 100644 --- a/screenshots/86box/hang-taskmgr.png +++ b/screenshots/86box/hang-taskmgr.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97017a5b3ba488a78d44a87ad1afdd778dcf7adde5144486a3824e2d4d644da0 -size 15950 +oid sha256:196d00f8ddc2ae0e833eb0347de4b8426faa219b2fa952e9802452c1365ea60f +size 15935 diff --git a/screenshots/86box/menus-m-design.png b/screenshots/86box/menus-m-design.png index 8289b617..fa583cea 100644 --- a/screenshots/86box/menus-m-design.png +++ b/screenshots/86box/menus-m-design.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:360bd4460fd4938d5582d563aead1e9affab2586eecd0e4453a3ef753dc10e38 -size 23155 +oid sha256:a709d362c56c7f2bf5eeb5ff8bb94e7d4f97afa716f77b80689699dc1eea884a +size 22955 diff --git a/screenshots/86box/pardemo-pardemo.png b/screenshots/86box/pardemo-pardemo.png new file mode 100644 index 00000000..42980c2a --- /dev/null +++ b/screenshots/86box/pardemo-pardemo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92e955f717b37413086db8bc1962214226c6151dc4390cd41fc996bb7f9b3468 +size 16473 diff --git a/screenshots/86box/pardsgn-pardsgn.png b/screenshots/86box/pardsgn-pardsgn.png new file mode 100644 index 00000000..0fe9a583 --- /dev/null +++ b/screenshots/86box/pardsgn-pardsgn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:617df56ce40e2d0f508309ad32f96cbc9fc02cb5090dabd859a85a7e1ad31801 +size 14442 diff --git a/screenshots/86box/parhlp-parhlp-l.png b/screenshots/86box/parhlp-parhlp-l.png new file mode 100644 index 00000000..6ca03e6d --- /dev/null +++ b/screenshots/86box/parhlp-parhlp-l.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d4173cdb0913c648867acf611a1229eff3afa52d28dc1c80ea5fcb72e59f742 +size 15518 diff --git a/screenshots/86box/parhlp-parhlp.png b/screenshots/86box/parhlp-parhlp.png new file mode 100644 index 00000000..2f8244a4 --- /dev/null +++ b/screenshots/86box/parhlp-parhlp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acd4399dea1bb296a676325375ff32a630303af9580d0480b54802f5e63de14b +size 15525 diff --git a/screenshots/86box/parpm-parpm.png b/screenshots/86box/parpm-parpm.png new file mode 100644 index 00000000..77c5a7a4 --- /dev/null +++ b/screenshots/86box/parpm-parpm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d072e84afb4356c744d5fcc86b2e98887fcb4653f3b9df6119f3b6c5bfc9de1f +size 17113 diff --git a/screenshots/86box/sdk-sdktbx.png b/screenshots/86box/sdk-sdktbx.png index 1b20898f..f0b3c508 100644 --- a/screenshots/86box/sdk-sdktbx.png +++ b/screenshots/86box/sdk-sdktbx.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92e957fc212d63fde7e4a82bad1e94e01bd1940bae785a66962e412c02c1f56d -size 23051 +oid sha256:7ab577b67f8f4e5c1bd7aa6fb8c5d18ea03e6fdd59bb2be766840e5de3694f79 +size 22860 diff --git a/screenshots/86box/ucdemo-ucdemo.png b/screenshots/86box/ucdemo-ucdemo.png new file mode 100644 index 00000000..a76309de --- /dev/null +++ b/screenshots/86box/ucdemo-ucdemo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9b6867da3f53651d7806e4d9a145537ce7e4eb144a019146b264b4da893fad3 +size 15749 diff --git a/screenshots/86box/ucide-ucide-ds.png b/screenshots/86box/ucide-ucide-ds.png new file mode 100644 index 00000000..920b9f2b --- /dev/null +++ b/screenshots/86box/ucide-ucide-ds.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f58d53babe125be69f0ad2a26765586184956699f7805a67f76ad5322a7b8a2 +size 31057 diff --git a/screenshots/86box/ucide-ucide-ru.png b/screenshots/86box/ucide-ucide-ru.png new file mode 100644 index 00000000..d88ca3c8 --- /dev/null +++ b/screenshots/86box/ucide-ucide-ru.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1008fa88b4001f9d03f56b8cd15054a9740aaaaf5843b12796bcb8741c96d87e +size 25186 diff --git a/screenshots/86box/ucide-ucide-tb.png b/screenshots/86box/ucide-ucide-tb.png new file mode 100644 index 00000000..0520a246 --- /dev/null +++ b/screenshots/86box/ucide-ucide-tb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfd1bc33ff7d35f32a9a185d4e616d45eba84a484cba89d58784d918415e382 +size 31539 diff --git a/screenshots/dosbox/apps-basdemo.png b/screenshots/dosbox/apps-basdemo.png index 42c6a707..1f073d2c 100644 --- a/screenshots/dosbox/apps-basdemo.png +++ b/screenshots/dosbox/apps-basdemo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bde86ad24acd488d9f87aafb4528ab272d5a99cda89a8a0d25a09aa3ce3af380 -size 16263 +oid sha256:887c02004051c2da25539cc3624e62ceda604bd27a8b5ed9a7fbaa3385d99c4a +size 16677 diff --git a/screenshots/dosbox/apps-helpedit.png b/screenshots/dosbox/apps-helpedit.png index 7bdbace7..e6f9a19f 100644 --- a/screenshots/dosbox/apps-helpedit.png +++ b/screenshots/dosbox/apps-helpedit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f416a623ee536b02de3d85d3725d04bfad9b4a84c78855dec17124455b058f0e -size 14159 +oid sha256:993ba360a941cf9a86d5c2acc34d623baa87d2ad406713511dac7a98f51e5d8d +size 14322 diff --git a/screenshots/dosbox/cpanel-cp-types.png b/screenshots/dosbox/cpanel-cp-types.png index c6fa0024..e8d0d9b3 100644 --- a/screenshots/dosbox/cpanel-cp-types.png +++ b/screenshots/dosbox/cpanel-cp-types.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3de4147235ff1e6471f76dba46886eebb711bb47971f919a5b3a46acf051e774 -size 21268 +oid sha256:539e278fe16fea322ab084e47d2acbcc8044e13cc63ee8fc473956788fe3b95d +size 22658 diff --git a/screenshots/dosbox/f1-h-basdem.png b/screenshots/dosbox/f1-h-basdem.png index 83acc722..f4a7268a 100644 --- a/screenshots/dosbox/f1-h-basdem.png +++ b/screenshots/dosbox/f1-h-basdem.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a853797b56c53f72336edbf96c838aed1d0806f7a3dcd77e6e3322a405dd5f75 -size 20582 +oid sha256:35fc158b43b5d885c56e667e5bb68ebc1c50d0f08889e0169da011d0cdb3c89e +size 20672 diff --git a/screenshots/dosbox/f1-h-helped.png b/screenshots/dosbox/f1-h-helped.png index 0a11172a..bdd51317 100644 --- a/screenshots/dosbox/f1-h-helped.png +++ b/screenshots/dosbox/f1-h-helped.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c4f1ae1c4be4016a416bcff9a3a066a367af1b22fbd026f9895cf39f096fc26 -size 22313 +oid sha256:bc316f8c448a6da952568e02e50c062bf7de0541912a50943f02de76dabaf83f +size 22492 diff --git a/screenshots/dosbox/hang-ended.png b/screenshots/dosbox/hang-ended.png index f67d933d..e77dc8dd 100644 --- a/screenshots/dosbox/hang-ended.png +++ b/screenshots/dosbox/hang-ended.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2562044174c6bbea89cfc399511da6ff363bcf96b12b97b86f01b69ff4dedde9 -size 14940 +oid sha256:313f4a171758d088ece11f6092a4c679dcc7f29667c46d7a0d990442f3efd02b +size 14936 diff --git a/screenshots/dosbox/hang-taskmgr.png b/screenshots/dosbox/hang-taskmgr.png index 47330b2b..2143b040 100644 --- a/screenshots/dosbox/hang-taskmgr.png +++ b/screenshots/dosbox/hang-taskmgr.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:213863151003ceb8af9545b82011a3127289484e6252eed6b587786589c55403 -size 15939 +oid sha256:47243d3fb7da7f07a6a9c5afeb938baccb9e16a307a4740e40db57ea2bf0d850 +size 15931 diff --git a/screenshots/dosbox/install-su-disk.png b/screenshots/dosbox/install-su-disk.png index e8a4c386..906dfbf1 100644 --- a/screenshots/dosbox/install-su-disk.png +++ b/screenshots/dosbox/install-su-disk.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a4acd0f91618626865fcd83b05f2819e3af4883c7245c977df6fbcc8b47aa69 -size 13322 +oid sha256:5f65ea8b760721abb6dfab3c121216ada3236df01edd2e5814e49d8ae25a2028 +size 12917 diff --git a/screenshots/dosbox/menus-m-design.png b/screenshots/dosbox/menus-m-design.png index ec19a81e..fa583cea 100644 --- a/screenshots/dosbox/menus-m-design.png +++ b/screenshots/dosbox/menus-m-design.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be0bf991786d8272efe9b9f235be13b5058d0541d86569d99be59bcd472434f0 -size 23066 +oid sha256:a709d362c56c7f2bf5eeb5ff8bb94e7d4f97afa716f77b80689699dc1eea884a +size 22955 diff --git a/screenshots/dosbox/pardemo-pardemo.png b/screenshots/dosbox/pardemo-pardemo.png new file mode 100644 index 00000000..04fa7d29 --- /dev/null +++ b/screenshots/dosbox/pardemo-pardemo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08087f62083a444bfd0dd46a4f4778ea304666f6d42c61f8105c486ea1f0a000 +size 16464 diff --git a/screenshots/dosbox/pardsgn-pardsgn.png b/screenshots/dosbox/pardsgn-pardsgn.png new file mode 100644 index 00000000..0fe9a583 --- /dev/null +++ b/screenshots/dosbox/pardsgn-pardsgn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:617df56ce40e2d0f508309ad32f96cbc9fc02cb5090dabd859a85a7e1ad31801 +size 14442 diff --git a/screenshots/dosbox/parhlp-parhlp-l.png b/screenshots/dosbox/parhlp-parhlp-l.png new file mode 100644 index 00000000..6ca03e6d --- /dev/null +++ b/screenshots/dosbox/parhlp-parhlp-l.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d4173cdb0913c648867acf611a1229eff3afa52d28dc1c80ea5fcb72e59f742 +size 15518 diff --git a/screenshots/dosbox/parhlp-parhlp.png b/screenshots/dosbox/parhlp-parhlp.png new file mode 100644 index 00000000..2f8244a4 --- /dev/null +++ b/screenshots/dosbox/parhlp-parhlp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acd4399dea1bb296a676325375ff32a630303af9580d0480b54802f5e63de14b +size 15525 diff --git a/screenshots/dosbox/parpm-parpm.png b/screenshots/dosbox/parpm-parpm.png new file mode 100644 index 00000000..b109114d --- /dev/null +++ b/screenshots/dosbox/parpm-parpm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5facaffedcc9ddbf130a757c62c146443dcefb21a495f36bb60e3ab7774f657a +size 17185 diff --git a/screenshots/dosbox/sdk-sdktbx.png b/screenshots/dosbox/sdk-sdktbx.png index 6b0da42e..f0b3c508 100644 --- a/screenshots/dosbox/sdk-sdktbx.png +++ b/screenshots/dosbox/sdk-sdktbx.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7daa51bfb8398874f94cedb1acf2ef0064d6caf677607e941a548319e185f09c -size 22966 +oid sha256:7ab577b67f8f4e5c1bd7aa6fb8c5d18ea03e6fdd59bb2be766840e5de3694f79 +size 22860 diff --git a/screenshots/dosbox/ucdemo-ucdemo.png b/screenshots/dosbox/ucdemo-ucdemo.png new file mode 100644 index 00000000..a76309de --- /dev/null +++ b/screenshots/dosbox/ucdemo-ucdemo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9b6867da3f53651d7806e4d9a145537ce7e4eb144a019146b264b4da893fad3 +size 15749 diff --git a/screenshots/dosbox/ucide-ucide-ds.png b/screenshots/dosbox/ucide-ucide-ds.png new file mode 100644 index 00000000..920b9f2b --- /dev/null +++ b/screenshots/dosbox/ucide-ucide-ds.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f58d53babe125be69f0ad2a26765586184956699f7805a67f76ad5322a7b8a2 +size 31057 diff --git a/screenshots/dosbox/ucide-ucide-ru.png b/screenshots/dosbox/ucide-ucide-ru.png new file mode 100644 index 00000000..d88ca3c8 --- /dev/null +++ b/screenshots/dosbox/ucide-ucide-ru.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1008fa88b4001f9d03f56b8cd15054a9740aaaaf5843b12796bcb8741c96d87e +size 25186 diff --git a/screenshots/dosbox/ucide-ucide-tb.png b/screenshots/dosbox/ucide-ucide-tb.png new file mode 100644 index 00000000..0520a246 --- /dev/null +++ b/screenshots/dosbox/ucide-ucide-tb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfd1bc33ff7d35f32a9a185d4e616d45eba84a484cba89d58784d918415e382 +size 31539 diff --git a/screenshots/masks/hang-ended.mask b/screenshots/masks/hang-ended.mask new file mode 100644 index 00000000..e6cceb29 --- /dev/null +++ b/screenshots/masks/hang-ended.mask @@ -0,0 +1,2 @@ +# Task Manager's per-application memory figures (change with the code size) +372 117 58 49 diff --git a/screenshots/masks/hang-taskmgr.mask b/screenshots/masks/hang-taskmgr.mask new file mode 100644 index 00000000..e6cceb29 --- /dev/null +++ b/screenshots/masks/hang-taskmgr.mask @@ -0,0 +1,2 @@ +# Task Manager's per-application memory figures (change with the code size) +372 117 58 49 diff --git a/screenshots/masks/upgrade-su-upgr.mask b/screenshots/masks/upgrade-su-upgr.mask new file mode 100644 index 00000000..6978924d --- /dev/null +++ b/screenshots/masks/upgrade-su-upgr.mask @@ -0,0 +1,2 @@ +# Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line +202 83 176 16 diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile index da03f1b6..0b9a74ce 100644 --- a/src/apps/kpunch/Makefile +++ b/src/apps/kpunch/Makefile @@ -42,13 +42,30 @@ 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 -# BASIC apps: each is a .dbp project in its own directory. -# BASIC-only notepad, imgview, etc. replace the old C versions. -BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow -.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS) +# BASIC apps: each is a .dbp project. An entry is the project's directory, +# or dir:out when the program goes into another program's folder (the Help +# Editor, dvxhelp/helpedit, installs beside the Help Viewer in dvxhelp). +# The program is bin/apps/kpunch//.app, being the last +# part of the directory. +BASIC_APPS = iconed notepad imgview dvxhelp/helpedit:dvxhelp resedit basdemo widshow clock welcome fileMgr terminal -all: $(C_APPS) dvxbasic $(BASIC_APPS) +basicDir = $(firstword $(subst :, ,$(1))) +basicName = $(notdir $(call basicDir,$(1))) +basicOut = $(or $(word 2,$(subst :, ,$(1))),$(call basicName,$(1))) +basicApp = $(BINDIR)/kpunch/$(call basicOut,$(1))/$(call basicName,$(1)).app + +# Everything a project is built from: the files in its directory and the +# modules its .dbp names (include libraries elsewhere). +basicDeps = $(wildcard $(1)/*.dbp $(1)/*.frm $(1)/*.bas $(1)/*.cls $(1)/*.BMP) \ + $(addprefix $(1)/,$(shell sed -n 's/^File[0-9]* *= *\(.*\.bas\)\r*$$/\1/p' $(1)/$(notdir $(1)).dbp)) + +BASIC_TARGETS = $(foreach a,$(BASIC_APPS),$(call basicName,$(a))) +BASIC_OUTS = $(foreach a,$(BASIC_APPS),$(call basicApp,$(a))) + +.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_TARGETS) + +all: $(C_APPS) dvxbasic $(BASIC_TARGETS) dvxbasic: $(MAKE) -C dvxbasic @@ -64,8 +81,27 @@ appObjs = $(patsubst $(1)/%.c,$(OBJDIR)/$(1)/%.o,$(call appSrcs,$(1))) appRes = $(wildcard $(1)/$(1).res) appDeps = $(call appRes,$(1)) $(if $(call appRes,$(1)),$(wildcard $(1)/*.bmp)) -# $(1) = app directory name. $(2) = extra shell commands run after linking -# (empty for most apps). +# Data files a program installs beside itself, C or BASIC alike: +# APP_DATA_ lists them ( being the last part of the program's +# source directory, the files relative to it); they are copied into the +# program's folder under bin/apps/kpunch. +APP_DATA_dvxdemo = logo.bmp new.bmp open.bmp sample.bmp save.bmp + +appDataOut = $(addprefix $(BINDIR)/kpunch/$(2)/,$(APP_DATA_$(notdir $(1)))) + +# $(1) = source directory, $(2) = output folder under bin/apps/kpunch, +# $(3) = the program's make target. +define APP_DATA_RULES +$(3): $(call appDataOut,$(1),$(2)) + +$(call appDataOut,$(1),$(2)): $(BINDIR)/kpunch/$(2)/%: $(1)/% + @mkdir -p $$(dir $$@) + cp $$< $$@ +endef + +APP_DATA_OUTS = $(foreach a,$(C_APPS),$(call appDataOut,$(a),$(a))) $(foreach a,$(BASIC_APPS),$(call appDataOut,$(call basicDir,$(a)),$(call basicOut,$(a)))) + +# $(1) = app directory name. define C_APP_RULES $(1): $(BINDIR)/kpunch/$(1)/$(1).app @@ -73,60 +109,34 @@ $(BINDIR)/kpunch/$(1)/$(1).app: $(call appObjs,$(1)) $(call appDeps,$(1)) @mkdir -p $$(dir $$@) $$(DXE3GEN) -o $$@ -U $(call appObjs,$(1)) $(if $(call appRes,$(1)),cd $(1) && ../$$(DVXRES) build ../$$@ $(1).res) - $(2) $(OBJDIR)/$(1)/%.o: $(1)/%.c $$(DVX_MAKEFILES) @mkdir -p $$(dir $$@) $$(CC) $$(CFLAGS) -c -o $$@ $$< endef -DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp -DVXDEMO_POST = cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/ - -$(foreach a,$(C_APPS),$(eval $(call C_APP_RULES,$(a),$(if $(filter dvxdemo,$(a)),$(DVXDEMO_POST))))) +$(foreach a,$(C_APPS),$(eval $(call C_APP_RULES,$(a)))) +$(foreach a,$(C_APPS),$(if $(APP_DATA_$(a)),$(eval $(call APP_DATA_RULES,$(a),$(a),$(a))))) C_APP_OBJS = $(foreach a,$(C_APPS),$(call appObjs,$(a))) -# BASIC apps (compiled from .dbp projects via bascomp). Source for each app -# is under the app's own directory. +# BASIC apps (compiled from .dbp projects via bascomp), one rule set per +# BASIC_APPS entry. # # F1 needs no resource: the shell presets an app's help file to .hlp # beside the .app (compiled there from /.dhs by compile-help). -iconed: $(BINDIR)/kpunch/iconed/iconed.app +# $(1) = BASIC_APPS entry. +define BASIC_APP_RULES +$(call basicName,$(1)): $(call basicApp,$(1)) -$(BINDIR)/kpunch/iconed/iconed.app: iconed/iconed.dbp iconed/iconed.frm iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic - $(BASCOMP) iconed/iconed.dbp -o $@ -release +$(call basicApp,$(1)): $(call basicDeps,$(call basicDir,$(1))) $$(BASCOMP) | dvxbasic + @mkdir -p $$(dir $$@) + $$(BASCOMP) $(call basicDir,$(1))/$(call basicName,$(1)).dbp -o $$@ -release +endef -notepad: $(BINDIR)/kpunch/notepad/notepad.app - -$(BINDIR)/kpunch/notepad/notepad.app: notepad/notepad.dbp notepad/notepad.frm notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic - $(BASCOMP) notepad/notepad.dbp -o $@ -release - -imgview: $(BINDIR)/kpunch/imgview/imgview.app - -$(BINDIR)/kpunch/imgview/imgview.app: imgview/imgview.dbp imgview/imgview.frm imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic - $(BASCOMP) imgview/imgview.dbp -o $@ -release - -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 - $(BASCOMP) dvxhelp/helpedit/helpedit.dbp -o $@ -release - -resedit: $(BINDIR)/kpunch/resedit/resedit.app - -$(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic - $(BASCOMP) resedit/resedit.dbp -o $@ -release - -basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app - -$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/shape.cls basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic - $(BASCOMP) basdemo/basdemo.dbp -o $@ -release - -widshow: $(BINDIR)/kpunch/widshow/widshow.app - -$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic - $(BASCOMP) widshow/widshow.dbp -o $@ -release +$(foreach a,$(BASIC_APPS),$(eval $(call BASIC_APP_RULES,$(a)))) +$(foreach a,$(BASIC_APPS),$(if $(APP_DATA_$(call basicName,$(a))),$(eval $(call APP_DATA_RULES,$(call basicDir,$(a)),$(call basicOut,$(a)),$(call basicName,$(a)))))) # Native host build (test harness): each C app becomes a plain ELF shared # object under obj/host/apps/.so with its resources appended by @@ -161,25 +171,12 @@ host: $(HOST_SOS) host-clean: rm -rf $(HOSTOBJDIR) -$(BINDIR)/kpunch/dvxhelp: ; mkdir -p $@ -$(BINDIR)/kpunch/iconed: ; mkdir -p $@ -$(BINDIR)/kpunch/notepad: ; mkdir -p $@ -$(BINDIR)/kpunch/imgview: ; mkdir -p $@ -$(BINDIR)/kpunch/resedit: ; mkdir -p $@ -$(BINDIR)/kpunch/basdemo: ; mkdir -p $@ -$(BINDIR)/kpunch/widshow: ; mkdir -p $@ -include $(C_APP_OBJS:.o=.d) $(HOST_APP_OBJS:.o=.d) clean: rm -f $(C_APP_OBJS) $(C_APP_OBJS:.o=.d) rm -f $(foreach a,$(C_APPS),$(BINDIR)/kpunch/$(a)/$(a).app) - rm -f $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS)) - rm -f $(BINDIR)/kpunch/iconed/iconed.app - rm -f $(BINDIR)/kpunch/notepad/notepad.app - rm -f $(BINDIR)/kpunch/imgview/imgview.app - rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app - rm -f $(BINDIR)/kpunch/resedit/resedit.app - rm -f $(BINDIR)/kpunch/basdemo/basdemo.app - rm -f $(BINDIR)/kpunch/widshow/widshow.app + rm -f $(APP_DATA_OUTS) + rm -f $(BASIC_OUTS) $(MAKE) -C dvxbasic clean diff --git a/src/apps/kpunch/README.md b/src/apps/kpunch/README.md index d8033865..c95be71f 100644 --- a/src/apps/kpunch/README.md +++ b/src/apps/kpunch/README.md @@ -9,13 +9,14 @@ This directory holds the source for the bundled applications plus the BASIC-based sample apps. Two flavours of app live side by side: * **C apps** -- written directly against the DVX SDK headers, - compiled to a DXE3 by `dxe3gen`. See `progman/`, `clock/`, - `dvxdemo/`, `cpanel/`, `dvxhelp/`. + compiled to a DXE3 by `dxe3gen`. See `progman/`, `dvxdemo/`, + `cpanel/`, `dvxhelp/`. * **BASIC apps** -- `.dbp` projects compiled to `.app` files by the host-side `bascomp`. The compiler links the BASIC bytecode into a copy of `basstub.app`, which acts as the runtime host. See `iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`, - `widshow/`, `dvxhelp/helpedit/`. + `widshow/`, `dvxhelp/helpedit/`, `clock/`, `welcome/`, `fileMgr/`, + `terminal/`. The rest of this document covers writing a C app against the SDK. For BASIC apps, see `dvxbasic/README.md`. A new app outside this tree @@ -53,15 +54,15 @@ typedef struct { } AppDescriptorT; ``` -Example (from `clock/clock.c`): +Example (from the SDK's game template, `src/sdk/templates/game/tplName.c`): ```c SHELL_APP_DESCRIPTOR( - .name = "Clock", + .name = TPLNAME_TITLE, .hasMainLoop = true, - .multiInstance = true, + .multiInstance = false, .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_LOW + .priority = TS_PRIORITY_NORMAL ); ``` @@ -105,8 +106,9 @@ The shell creates a cooperative task for the app and calls the loop and returning. Use this pattern when the app needs continuous background work that -does not map cleanly onto event callbacks: Clock (polls the system -clock every second), terminal emulators, games. +does not map cleanly onto event callbacks: terminal emulators, games, +and every compiled BASIC program (the BASIC stub runs the program and +its event loop in the app's task). ## Resource Conventions @@ -133,13 +135,13 @@ unique name. The runtime reads them via Typical `.res` manifest: ``` -# clock.res +# dvxdemo.res icon32 icon icon32.bmp -name text "Clock" +name text "DVX Demo" author text "Scott Duensing" copyright text "Copyright 2026 Scott Duensing" publisher text "Kangaroo Punch Studios" -description text "Digital clock with date display" +description text "Widget toolkit demonstration" ``` @@ -215,18 +217,76 @@ menu (Cut, Copy, Paste, Select All). Built from a BASIC project (`notepad.dbp` + `notepad.frm`); demonstrates the form designer, TextInput widget, and common dialog integration. -### Clock (clock) +### Clock (clock, BASIC) | | | |---|---| | File | `apps/kpunch/clock/clock.app` | -| Type | Main-loop | +| Type | BASIC (main-loop via basstub) | | Multi-instance | Yes | -Digital clock showing time and date. Polls the system clock every -second and invalidates the window to trigger a repaint. Uses the raw -`onPaint` callback (no widgets) to draw centered text. Reference -implementation for main-loop apps. +Digital clock showing the time and date, updated by a Timer. Each new +copy opens a little lower and to the right of the previous one +(`ShellInstanceCount`, shell.bas). Built from `clock.dbp` + +`clock.frm`; its help is `clock.dhs`. + +### Welcome (welcome, BASIC) + +| | | +|---|---| +| File | `apps/kpunch/welcome/welcome.app` | +| Type | BASIC (main-loop via basstub) | +| Multi-instance | No | + +The window DVX shows after the desktop when it starts for the first +time (DVX.INI `[shell] welcome`, written by Setup). "Don't show this +again" removes the key when the window closes; unticked, it writes the +program's path back (`ShellSetWelcome`, shell.bas). "Open the User +Guide" opens the system help. Built from `welcome.dbp` + +`welcome.frm`; its help is `welcome.dhs`. + +### File Manager (fileMgr, BASIC) + +| | | +|---|---| +| File | `apps/kpunch/fileMgr/fileMgr.app` | +| Type | BASIC (main-loop via basstub) | +| Multi-instance | Yes | + +Drive buttons, a path box, a folder tree and a sortable file list +with open / run, copy, move, cut / copy / paste between windows, +rename, delete, new folder, properties (attributes), selection and +filter commands. Copy, move and delete run as jobs with their own +progress windows, driven by a Timer, so the program stays usable. +Built from `fileMgr.dbp`: `fileMgr.frm` (the window), `fmProps.frm` +(Properties), `fileMgr.bas` (the job engine), `fmJob.cls`, with the +fileops.bas, shell.bas, commdlg.bas and help.bas libraries; its help +is `fileMgr.dhs`. + +### Terminal (terminal, BASIC) + +| | | +|---|---| +| File | `apps/kpunch/terminal/terminal.app` | +| Type | BASIC (main-loop via basstub) | +| Multi-instance | Yes (one per COM port, for example) | + +A BBS / serial terminal: serial port, modem (dialing, redial, carrier +loss), the secure link to the DVX host proxy (with or without a +passphrase) and Telnet; a phone book of sessions (TERMINAL.INI in its +settings folder); the ANSI terminal with scrollback, colors, local echo, +answerback, capture to file and break; XMODEM, XMODEM-1K, YMODEM and +ZMODEM uploads and downloads (ZMODEM downloads start by themselves) +with a progress window that asks about existing files. The connection, +the terminal feed and the transfers run in the conn and xfer libraries +(conn.bas, xfer.bas); a 10 ms Timer moves them along. Started with +`telnet://host[:port]` it connects to that host. Built from +`terminal.dbp`: `terminal.frm` (the window), `termBook.frm` (Phone +Book), `termSess.frm` (Session Settings), `termXfer.frm` (Transfer +Options), `termPass.frm` (passphrase), `termProg.frm` (File Transfer), +`terminal.bas` (the phone book and shared state), `termMain.bas` (the +start), with the conn.bas, xfer.bas, prefs.bas, fileops.bas, shell.bas, +commdlg.bas and help.bas libraries; its help is `terminal.dhs`. ### DVX Demo (dvxdemo) @@ -315,7 +375,10 @@ editor, project manager, compiler, and runtime VM. See Renders compiled `.hlp` files produced by `dvxhlpc`. Supports table of contents, keyword index, full-text search (trigram), hyperlinks, -and inline images. +and inline images. The file is read with the `hlpfile` library +(`src/libs/kpunch/hlpfile`) and topics are drawn by the HelpView widget +(`src/widgets/kpunch/helpView`, BASIC type HelpPane), the same code a +BASIC program uses through `hlpfile.bas` and HelpPane. ### Icon Editor (iconed, BASIC) @@ -457,6 +520,14 @@ same directory are linked in too), drop a `.res` manifest next to the source, and provide a 32x32 `icon32.bmp`. The Makefile finds the app by itself; `make ` then produces `bin/apps/kpunch//.app`. +Files a program reads at run time (pictures, data) are installed beside +it by one Makefile rule, for C and BASIC programs alike: list them in +`APP_DATA_` (relative to the program's source directory, `` +being the last part of that directory), as `APP_DATA_dvxdemo` does. +`make ` copies them into the program's folder and `make clean` +removes them; the program finds them through its own folder +(`App.Path` in BASIC, the app context's `appDir` in C). + ## Testing Locally @@ -491,10 +562,6 @@ apps/kpunch/ progman.dhs Program Manager's own help dvxhelp.hcf help-compiler config (system reference) userHelp.dhs end-user guide (first part of the system help) - clock/ - clock.c digital clock (main-loop reference) - clock.res - icon32.bmp dvxdemo/ dvxdemo.c widget showcase dvxdemo.res @@ -506,10 +573,10 @@ apps/kpunch/ cpanel.res icon32.bmp dvxhelp/ - dvxhelp.c help viewer + dvxhelp.c help viewer (reads .hlp files with the hlpfile + library, shows topics in the HelpView widget) dvxhelp.res help.dhs source for the dvxhelp app's own help - hlpformat.h binary format shared with the compiler sample.dhs small sample source icon32.bmp helpedit/ BASIC project: help editor @@ -542,6 +609,36 @@ apps/kpunch/ widshow.dbp widshow.frm ICON32.BMP + clock/ + clock.dbp + clock.frm + clock.dhs + ICON32.BMP + welcome/ + welcome.dbp + welcome.frm + welcome.dhs + ICON32.BMP + fileMgr/ + fileMgr.dbp + fileMgr.frm the window + fmProps.frm Properties dialog + fileMgr.bas job engine (copy / move / delete windows) + fmJob.cls one job + fileMgr.dhs + ICON32.BMP + terminal/ + terminal.dbp + terminal.frm the window + termBook.frm Phone Book + termSess.frm Session Settings + termXfer.frm Transfer Options + termPass.frm secure link passphrase + termProg.frm File Transfer (progress) + terminal.bas phone book (TERMINAL.INI), shared state + termMain.bas start + terminal.dhs + ICON32.BMP dvxbasic/ see dvxbasic/README.md compiler/ lexer, parser, codegen, strip, obfuscate diff --git a/src/apps/kpunch/clock/icon32.bmp b/src/apps/kpunch/clock/ICON32.BMP similarity index 100% rename from src/apps/kpunch/clock/icon32.bmp rename to src/apps/kpunch/clock/ICON32.BMP diff --git a/src/apps/kpunch/clock/clock.c b/src/apps/kpunch/clock/clock.c deleted file mode 100644 index d9574dea..00000000 --- a/src/apps/kpunch/clock/clock.c +++ /dev/null @@ -1,295 +0,0 @@ -// 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.c -- Clock DXE application (main-loop with tsYield) -// -// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback- -// only apps like notepad or progman. The difference is fundamental: -// -// Callback-only: appMain creates UI, returns. Shell calls callbacks. -// Main-loop: appMain runs forever in its own task, calling tsYield() -// to cooperatively yield to the shell and other tasks. -// -// A main-loop app is needed when the app has ongoing work that can't be -// expressed purely as event callbacks -- here, polling the system clock -// every second and repainting. The shell allocates a dedicated task stack -// and schedules this task alongside the main event loop. -// -// The onPaint/onClose callbacks still execute in task 0 during dvxUpdate(), -// not in this task. The main loop only handles the clock-tick polling; the -// actual rendering and window management happen through the normal callback -// path. tsYield() is what makes this cooperative -- without it, this task -// would starve the shell and all other apps. - -#include "dvxApp.h" -#include "dvxDlg.h" -#include "dvxWgt.h" -#include "dvxDraw.h" -#include "dvxVideo.h" -#include "dvxWm.h" -#include "shellApp.h" -#include "taskSwch.h" - -#include -#include -#include -#include -#include -#include "dvxMem.h" - -// ============================================================ -// Module state -// ============================================================ - -#define CLOCK_WIN_W 200 -#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_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 { - bool quit; - char timeStr[32]; - char dateStr[32]; - time_t lastUpdate; -} ClockStateT; - -static DxeAppContextT *sCtx = NULL; -static WindowT *sWin = NULL; -static ClockStateT sState; - -// ============================================================ -// Prototypes -// ============================================================ - -void appShutdown(void); -static int32_t countClockWindows(const AppContextT *ac); -static void onClose(WindowT *win); -static void onMenu(WindowT *win, int32_t menuId); -static void onPaint(WindowT *win, RectT *dirty); -static void updateTime(void); -int32_t appMain(DxeAppContextT *ctx); - -// ============================================================ -// App descriptor -// ============================================================ - -// hasMainLoop = true tells the shell to create a dedicated task for appMain. -// TS_PRIORITY_LOW because clock updates are cosmetic and should never -// preempt interactive apps or the shell's event processing. -SHELL_APP_DESCRIPTOR( - .name = "Clock", - .hasMainLoop = true, - .multiInstance = true, - .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_LOW -); - - -// The shell calls appShutdown (if exported) when force-killing an app via -// Task Manager or during shell shutdown. For main-loop apps, this is the -// signal to break out of the main loop. Without this, shellForceKillApp -// would have to terminate the task forcibly, potentially leaking resources. -void appShutdown(void) { - sState.quit = true; -} - - -// 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 -// exit. The main loop then destroys the window and returns from appMain, -// which causes the shell to reap this task and unload the DXE. -static void onClose(WindowT *win) { - (void)win; - sState.quit = true; -} - - -// 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). -// Instead of using wgtInitWindow + widgets, this app renders directly into -// the window's content buffer. This is the lower-level alternative to the -// widget system -- useful for custom rendering like this centered clock display. -// -// dvxContentDisplay gives a DisplayT aimed at the window's content buffer, -// so the standard drawing primitives (rectFill, drawText) draw into the -// window content without touching the global display state. -static void onPaint(WindowT *win, RectT *dirty) { - (void)dirty; - - AppContextT *ac = sCtx->shellCtx; - const BlitOpsT *ops = dvxGetBlitOps(ac); - const BitmapFontT *font = dvxGetFont(ac); - const ColorSchemeT *colors = dvxGetColors(ac); - DisplayT cd; - - dvxContentDisplay(ac, win, &cd); - - // Background - rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg); - - // Time string (large, centered) - int32_t timeW = textWidth(font, sState.timeStr); - int32_t timeX = (win->contentW - timeW) / 2; - int32_t timeY = win->contentH / 3; - drawText(&cd, ops, font, timeX, timeY, sState.timeStr, colors->contentFg, colors->contentBg, true); - - // Date string (centered below) - int32_t dateW = textWidth(font, sState.dateStr); - int32_t dateX = (win->contentW - dateW) / 2; - int32_t dateY = timeY + font->charHeight + CLOCK_DATE_GAP; - drawText(&cd, ops, font, dateX, dateY, sState.dateStr, colors->contentFg, colors->contentBg, true); -} - - -static void updateTime(void) { - time_t now = time(NULL); - struct tm *tm = localtime(&now); - - if (!tm) { - snprintf(sState.timeStr, sizeof(sState.timeStr), "--:--:--"); - snprintf(sState.dateStr, sizeof(sState.dateStr), "----------"); - sState.lastUpdate = now; - return; - } - - int32_t hour12 = tm->tm_hour % 12; - const char *ampm = (tm->tm_hour < 12) ? "AM" : "PM"; - - if (hour12 == 0) { - hour12 = 12; - } - - snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", (int)hour12, tm->tm_min, tm->tm_sec, ampm); - snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); - sState.lastUpdate = now; -} - - -// This runs in its own task. The shell creates the task before calling -// appMain, and reaps it when appMain returns. -int32_t appMain(DxeAppContextT *ctx) { - sCtx = ctx; - AppContextT *ac = ctx->shellCtx; - - memset(&sState, 0, sizeof(sState)); - updateTime(); - - // Position in the upper-right corner, out of the way of other windows, - // cascading down and left for each clock already open (back to the - // corner once the cascade would leave the screen). - int32_t winW = CLOCK_WIN_W; - 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 - sWin = dvxCreateWindow(ac, CLOCK_TITLE, winX, winY, winW, winH, false); - - if (!sWin) { - return -1; - } - - sWin->onClose = onClose; - sWin->onMenu = onMenu; - 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. - // First paint happens automatically on the next dvxUpdate frame. - // tsYield() transfers control back to the shell's task scheduler. - // On a 486, time() resolution is 1 second, so we yield many times per - // second between actual updates -- this keeps CPU usage near zero. - // dvxInvalidateWindow marks the window dirty and calls onPaint to - // update the content buffer before the compositor flushes it. - while (!sState.quit) { - time_t now = time(NULL); - - if (now != sState.lastUpdate) { - updateTime(); - dvxInvalidateWindow(ac, sWin); - } - - tsYield(); - } - - dvxDestroyWindow(ac, sWin); - sWin = NULL; - return 0; -} diff --git a/src/apps/kpunch/clock/clock.dbp b/src/apps/kpunch/clock/clock.dbp new file mode 100644 index 00000000..b3094931 --- /dev/null +++ b/src/apps/kpunch/clock/clock.dbp @@ -0,0 +1,21 @@ +[Project] +Name = Clock +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Digital clock with date display +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/help.bas +File1 = ../../../include/basic/shell.bas +Count = 2 + +[Forms] +File0 = clock.frm +Count = 1 + +[Settings] +StartupForm = ClockForm +MultiInstance = True diff --git a/src/apps/kpunch/clock/clock.frm b/src/apps/kpunch/clock/clock.frm new file mode 100644 index 00000000..dffad937 --- /dev/null +++ b/src/apps/kpunch/clock/clock.frm @@ -0,0 +1,151 @@ +VERSION DVX 1.00 +Begin Form ClockForm + Caption = "Clock" + Layout = VBox + AutoSize = False + Resizable = False + Centered = False + Width = 200 + Height = 120 + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuHelpContents + Caption = "&Contents" + ShortcutText = "F1" + End + Begin Menu mnuHelpSep + Caption = "-" + End + Begin Menu mnuAbout + Caption = "&About Clock..." + End + End + Begin Spacer spcTop + Height = 12 + Weight = 0 + End + Begin Label lblTime + Caption = "" + Alignment = Center + Height = 20 + End + Begin Label lblDate + Caption = "" + Alignment = Center + Height = 20 + End + Begin Timer tmrTick + Interval = 100 + Enabled = True + End +End + + +' 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.frm -- the Clock: the time and today's date in a small window. +' +' The window has the C Clock's geometry: 200 x 120 with a menu bar, so the +' face below it is 188 x 68 pixels. The time is drawn a third of the way +' down (y 22) and the date 8 pixels below the time's line (y 46), both +' centred: a 12 pixel spacer and two labels 20 pixels high (the text sits +' 2 pixels into each) inside the form's 4 pixel padding and spacing fill +' the face exactly. A timer looks at the clock ten times a second and +' changes the labels when the second has changed. +' +' More than one Clock may run (MultiInstance in clock.dbp); each new one +' opens a little lower and further left than the one before. + +OPTION EXPLICIT + +CONST HELP_FILE = "clock.hlp" + +' Placement: the upper right corner, CLOCK_MARGIN pixels from the edges, +' and CLOCK_CASCADE pixels down and left for every Clock already open. +CONST CLOCK_MARGIN = 40 +CONST CLOCK_CASCADE = 16 + +CONST TIME_FORMAT = "h:nn:ss AM/PM" +CONST DATE_FORMAT = "yyyy-mm-dd" + + +SUB ClockForm_Load + DIM offset AS LONG + DIM winX AS LONG + DIM winY AS LONG + + ' Cascade from the corner; back to the corner once the cascade would + ' leave the screen. + offset = (ShellInstanceCount() - 1) * CLOCK_CASCADE + winX = Screen.Width - ClockForm.Width - CLOCK_MARGIN - offset + winY = CLOCK_MARGIN + offset + + IF winX < 0 OR winY + ClockForm.Height > Screen.Height THEN + winX = Screen.Width - ClockForm.Width - CLOCK_MARGIN + winY = CLOCK_MARGIN + END IF + + ClockForm.Left = winX + ClockForm.Top = winY + ShowTime +END SUB + + +' Puts the current time and date on the face; a label is only changed when +' its text has, so the window is redrawn once a second, not at every tick. +SUB ShowTime + DIM timeText AS STRING + DIM dateText AS STRING + + timeText = Format$(Now, TIME_FORMAT) + dateText = Format$(Now, DATE_FORMAT) + + IF lblTime.Caption <> timeText THEN + lblTime.Caption = timeText + END IF + + IF lblDate.Caption <> dateText THEN + lblDate.Caption = dateText + END IF +END SUB + + +SUB tmrTick_Timer + ShowTime +END SUB + + +SUB mnuHelpContents_Click + HelpView HELP_FILE +END SUB + + +SUB mnuAbout_Click + DIM msg AS STRING + + msg = "Clock" + CHR$(10) + CHR$(10) + msg = msg + "Shows the current time and date." + CHR$(10) + CHR$(10) + msg = msg + "Copyright 2026 Scott Duensing" + MsgBox msg, vbOKOnly + vbInformation, "About Clock" +END SUB diff --git a/src/apps/kpunch/clock/clock.res b/src/apps/kpunch/clock/clock.res deleted file mode 100644 index afd05f3b..00000000 --- a/src/apps/kpunch/clock/clock.res +++ /dev/null @@ -1,7 +0,0 @@ -# clock.res -- Resource manifest for Clock -icon32 icon icon32.bmp -name text "Clock" -author text "Scott Duensing" -copyright text "Copyright 2026 Scott Duensing" -publisher text "Kangaroo Punch Studios" -description text "Digital clock with date display" diff --git a/src/apps/kpunch/cpanel/cpJoystick.c b/src/apps/kpunch/cpanel/cpJoystick.c index 03954417..8c66d036 100644 --- a/src/apps/kpunch/cpanel/cpJoystick.c +++ b/src/apps/kpunch/cpanel/cpJoystick.c @@ -49,7 +49,6 @@ #include #include #include -#include #define CPJ_LABEL_W 90 #define CPJ_GAP 4 @@ -81,11 +80,16 @@ // The joystick library's functions (NULL when it is not installed) typedef struct { + __typeof__(joyChoiceGet) *choiceGet; + __typeof__(joyChoices) *choices; + __typeof__(joyChoiceSet) *choiceSet; __typeof__(joyClose) *close; __typeof__(joyCount) *count; __typeof__(joyGetDeadZone) *getDeadZone; __typeof__(joyGetInfo) *getInfo; __typeof__(joyGetRaw) *getRaw; + __typeof__(joyKbdSlotGet) *slotGet; + __typeof__(joyKbdSlotSet) *slotSet; __typeof__(joyKeyCount) *keyCount; __typeof__(joyKeyName) *keyName; __typeof__(joyOpen) *open; @@ -93,8 +97,6 @@ typedef struct { __typeof__(joyRescan) *rescan; __typeof__(joySetCalibration) *setCal; __typeof__(joySetDeadZone) *setDeadZone; - __typeof__(joySettingGet) *settingGet; - __typeof__(joySettingSet) *settingSet; __typeof__(joySettingsLoad) *load; __typeof__(joySettingsStore) *store; } JoyFnsT; @@ -128,11 +130,16 @@ typedef struct { #define JOY_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(JoyFnsT, field) } static const JoyFnSymT sSyms[] = { + JOY_FN_SYM(choiceGet, joyChoiceGet), + JOY_FN_SYM(choices, joyChoices), + JOY_FN_SYM(choiceSet, joyChoiceSet), JOY_FN_SYM(close, joyClose), JOY_FN_SYM(count, joyCount), JOY_FN_SYM(getDeadZone, joyGetDeadZone), JOY_FN_SYM(getInfo, joyGetInfo), JOY_FN_SYM(getRaw, joyGetRaw), + JOY_FN_SYM(slotGet, joyKbdSlotGet), + JOY_FN_SYM(slotSet, joyKbdSlotSet), JOY_FN_SYM(keyCount, joyKeyCount), JOY_FN_SYM(keyName, joyKeyName), JOY_FN_SYM(open, joyOpen), @@ -140,23 +147,12 @@ static const JoyFnSymT sSyms[] = { JOY_FN_SYM(rescan, joyRescan), JOY_FN_SYM(setCal, joySetCalibration), JOY_FN_SYM(setDeadZone, joySetDeadZone), - JOY_FN_SYM(settingGet, joySettingGet), - JOY_FN_SYM(settingSet, joySettingSet), JOY_FN_SYM(load, joySettingsLoad), JOY_FN_SYM(store, joySettingsStore) }; -// [joystick] gameport.mode and fallback: what the dropdowns show and store -static const char *sModeLabels[] = { "Two joysticks", "One joystick with 4 axes" }; -static const char *sModeValues[] = { JOY_GAMEPORT_TWO, JOY_GAMEPORT_FOUR }; - -static const char *sFallbackLabels[] = { "When no joystick is found", "Always", "Never" }; -static const char *sFallbackValues[] = { JOY_FALLBACK_AUTO, JOY_FALLBACK_ALWAYS, JOY_FALLBACK_NEVER }; - static const char *const sAxisNames[CPJ_SHOWN_AXES] = { "X", "Y", "3", "4" }; -static const char *const sSlotNames[JOY_KBD_SLOT_COUNT] = { JOY_KBD_SLOT_NAMES }; -static const char *const sSlotDefaults[JOY_KBD_SLOT_COUNT] = { JOY_KBD_SLOT_DEFAULTS }; static const char *const sSlotLabels[JOY_KBD_SLOT_COUNT] = { "Up:", "Down:", "Left:", "Right:", "Button 1:", "Button 2:", "Button 3:", "Button 4:" }; static const char *const sWizText[] = { @@ -182,6 +178,8 @@ static WidgetT *sModeDrop = NULL; static WidgetT *sKbdDrop = NULL; static WidgetT *sKeyDrops[JOY_KBD_SLOT_COUNT]; static const char **sKeyItems = NULL; // stb_ds array of the key names +static const char *sModeLabels[JOY_GAMEPORT_CHOICE_COUNT]; +static const char *sFallbackLabels[JOY_FALLBACK_CHOICE_COUNT]; static int32_t sDevice = -1; static int32_t sHandle = -1; static JoyStateT sShown; @@ -196,12 +194,13 @@ static CalWizT sWiz; static WidgetT *addRow(WidgetT *parent, const char *label); static int32_t axisPixel(int32_t value, int32_t size); +static void choiceDrop(WidgetT *drop, JoyChoiceE which, const char **labels); void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs); void cpJoystickFree(void); void cpJoystickRevert(void); void cpJoystickStore(PrefsHandleT *prefs); static void drawView(void); -static int32_t findValue(const char *const *values, int32_t count, const char *value); +static bool joyStateEqual(const JoyStateT *a, const JoyStateT *b); static void onCalibrate(WidgetT *w); static void onDetect(WidgetT *w); static void onDevice(WidgetT *w); @@ -219,7 +218,6 @@ static void refreshDevices(void); static bool resolveLibrary(void); static void selectDevice(int32_t device); static void showZone(void); -static void slotKey(int32_t slot, char *out, int32_t outSize); static bool wizAxis(int32_t axis); @@ -240,6 +238,17 @@ static int32_t axisPixel(int32_t value, int32_t size) { } +// A listed [joystick] setting's choices in drop, its current one chosen. +static void choiceDrop(WidgetT *drop, JoyChoiceE which, const char **labels) { + int32_t count = 0; + const PrefsChoiceT *choices = sJoy.choices(which, &count); + + prefsChoiceLabels(choices, count, labels); + wgtDropdownSetItems(drop, labels, count); + wgtDropdownSetSelected(drop, sJoy.choiceGet(which)); +} + + void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { sAc = ac; sPrefs = prefs; @@ -282,14 +291,12 @@ void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { row = addRow(page, "Game port:"); sModeDrop = wgtDropdown(row); sModeDrop->onChange = onMode; - wgtDropdownSetItems(sModeDrop, sModeLabels, DVX_ARRAY_LEN(sModeLabels)); - wgtDropdownSetSelected(sModeDrop, findValue(sModeValues, DVX_ARRAY_LEN(sModeValues), sJoy.settingGet(JOY_KEY_GAMEPORT_MODE, JOY_GAMEPORT_TWO))); + choiceDrop(sModeDrop, JoyChoiceGamePortE, sModeLabels); row = addRow(page, "Keyboard:"); sKbdDrop = wgtDropdown(row); sKbdDrop->onChange = onFallback; - wgtDropdownSetItems(sKbdDrop, sFallbackLabels, DVX_ARRAY_LEN(sFallbackLabels)); - wgtDropdownSetSelected(sKbdDrop, findValue(sFallbackValues, DVX_ARRAY_LEN(sFallbackValues), sJoy.settingGet(JOY_KEY_FALLBACK, JOY_FALLBACK_AUTO))); + choiceDrop(sKbdDrop, JoyChoiceFallbackE, sFallbackLabels); WidgetT *keys = wgtButton(row, "Keys..."); @@ -404,15 +411,10 @@ static void drawView(void) { } -// The index of value in values (case-insensitive), 0 when not there. -static int32_t findValue(const char *const *values, int32_t count, const char *value) { - for (int32_t i = 0; i < count; i++) { - if (strcasecmp(values[i], value) == 0) { - return i; - } - } - - return 0; +// Two states the view draws alike, compared field by field: the struct +// ends in a bool, and memcmp would compare its padding too. +static bool joyStateEqual(const JoyStateT *a, const JoyStateT *b) { + return memcmp(a->axes, b->axes, sizeof(a->axes)) == 0 && memcmp(a->hats, b->hats, sizeof(a->hats)) == 0 && a->buttons == b->buttons && a->connected == b->connected; } @@ -462,7 +464,8 @@ static void onCalibrate(WidgetT *w) { } -// Look for controllers again. The page's own handle is closed first; +// Look for controllers again. The page's own handle is closed first +// (with every handle of the Control Panel); // another program's handle stops the search. static void onDetect(WidgetT *w) { (void)w; @@ -483,15 +486,13 @@ static void onDevice(WidgetT *w) { static void onFallback(WidgetT *w) { - sJoy.settingSet(JOY_KEY_FALLBACK, sFallbackValues[wgtDropdownGetSelected(w)]); + sJoy.choiceSet(JoyChoiceFallbackE, wgtDropdownGetSelected(w)); onDetect(w); } // One dropdown of key names per keyboard joystick key. static void onKeys(WidgetT *w) { - char key[CPJ_KEY_BUF]; - (void)w; if (!sKeyItems) { @@ -507,10 +508,9 @@ static void onKeys(WidgetT *w) { } for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) { - slotKey(i, key, sizeof(key)); sKeyDrops[i] = wgtDropdown(addRow(dlg->widgetRoot, sSlotLabels[i])); wgtDropdownSetItems(sKeyDrops[i], sKeyItems, (int32_t)arrlen(sKeyItems)); - wgtDropdownSetSelected(sKeyDrops[i], findValue(sKeyItems, (int32_t)arrlen(sKeyItems), sJoy.settingGet(key, sSlotDefaults[i]))); + wgtDropdownSetSelected(sKeyDrops[i], sJoy.slotGet(i)); } dvxDialogAddOkCancel(dlg, "OK", onKeysOk, NULL); @@ -519,14 +519,11 @@ static void onKeys(WidgetT *w) { static bool onKeysOk(WindowT *dlg, void *userCtx) { - char key[CPJ_KEY_BUF]; - (void)dlg; (void)userCtx; for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) { - slotKey(i, key, sizeof(key)); - sJoy.settingSet(key, sKeyItems[wgtDropdownGetSelected(sKeyDrops[i])]); + sJoy.slotSet(i, wgtDropdownGetSelected(sKeyDrops[i])); } return true; @@ -535,7 +532,7 @@ static bool onKeysOk(WindowT *dlg, void *userCtx) { // The game port makes its joysticks anew in the other mode. static void onMode(WidgetT *w) { - sJoy.settingSet(JOY_KEY_GAMEPORT_MODE, sModeValues[wgtDropdownGetSelected(w)]); + sJoy.choiceSet(JoyChoiceGamePortE, wgtDropdownGetSelected(w)); onDetect(w); } @@ -556,7 +553,7 @@ static void onTick(WidgetT *w) { (void)w; - if (!sJoy.read(sHandle, &st) || (sShownValid && memcmp(&st, &sShown, sizeof(st)) == 0)) { + if (!sJoy.read(sHandle, &st) || (sShownValid && joyStateEqual(&st, &sShown))) { return; } @@ -734,12 +731,6 @@ static void showZone(void) { } -// The [joystick] key of a keyboard joystick slot. -static void slotKey(int32_t slot, char *out, int32_t outSize) { - snprintf(out, (size_t)outSize, "%s%s", JOY_KBD_KEY_PREFIX, sSlotNames[slot]); -} - - // Axis a of the controller being calibrated answers. static bool wizAxis(int32_t axis) { return (sWiz.info.axisPresent & (1U << axis)) != 0; diff --git a/src/apps/kpunch/cpanel/cpNet.c b/src/apps/kpunch/cpanel/cpNet.c index 1441c930..9494f12d 100644 --- a/src/apps/kpunch/cpanel/cpNet.c +++ b/src/apps/kpunch/cpanel/cpNet.c @@ -61,6 +61,7 @@ // The library's functions (NULL when it is not installed) typedef struct { + __typeof__(netConfigEqual) *equal; __typeof__(netConfigFromPrefs) *fromPrefs; __typeof__(netConfigToPrefs) *toPrefs; __typeof__(netConfigProblem) *problem; @@ -129,7 +130,7 @@ void cpNetApply(void) { readPage(&cfg); - if (memcmp(&cfg, &sOriginal, sizeof(cfg)) != 0) { + if (!sNet.equal(&cfg, &sOriginal)) { sNet.restart(); } } @@ -141,13 +142,14 @@ void cpNetBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { char label[CN_LINE_MAX]; sAc = ac; + sNet.equal = (__typeof__(sNet.equal))dlsym(RTLD_DEFAULT, DVX_SYM("netConfigEqual")); 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) { + if (!sNet.equal || !sNet.fromPrefs || !sNet.toPrefs || !sNet.problem || !sNet.status || !sNet.restart) { memset(&sNet, 0, sizeof(sNet)); wgtLabel(page, "Networking is not installed."); return; diff --git a/src/apps/kpunch/cpanel/cpPrinter.c b/src/apps/kpunch/cpanel/cpPrinter.c index 2b93f45e..82cd9694 100644 --- a/src/apps/kpunch/cpanel/cpPrinter.c +++ b/src/apps/kpunch/cpanel/cpPrinter.c @@ -38,7 +38,6 @@ #include "textInput/textInpt.h" #include -#include #include #define CPR_LABEL_W 110 @@ -48,9 +47,9 @@ // The library's functions and tables (NULL when it is not installed) typedef struct { - __typeof__(printSettingsLoad) *load; __typeof__(printSettingsRead) *read; - __typeof__(printSettingsWrite) *write; + __typeof__(printDeviceProblem) *problem; + __typeof__(printDeviceStore) *store; __typeof__(printPageSetupDialog) *pageSetup; __typeof__(printDestLabel) *destLabel; __typeof__(printModelLabel) *modelLabel; @@ -91,14 +90,14 @@ 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.problem = (__typeof__(sPrint.problem))dlsym(RTLD_DEFAULT, DVX_SYM("printDeviceProblem")); + sPrint.store = (__typeof__(sPrint.store))dlsym(RTLD_DEFAULT, DVX_SYM("printDeviceStore")); 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) { + if (!sPrint.read || !sPrint.problem || !sPrint.store || !sPrint.pageSetup || !sPrint.destLabel || !sPrint.modelLabel) { memset(&sPrint, 0, sizeof(sPrint)); wgtLabel(page, "Printing is not installed."); return; @@ -136,29 +135,22 @@ void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { 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."; + if (!sPrint.problem) { + return NULL; } - return NULL; + return sPrint.problem((PrintDestE)wgtDropdownGetSelected(sDest), wgtGetText(sFile)); } -// 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. +// The library keeps the page setup as it is in DVX.INI now (Page +// Setup... saves it there at once). void cpPrinterStore(PrefsHandleT *prefs) { - PrintSettingsT s; - - if (!sPrint.load) { + if (!sPrint.store) { 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); + sPrint.store(prefs, (PrintModelE)wgtDropdownGetSelected(sModel), (PrintDestE)wgtDropdownGetSelected(sDest), wgtGetText(sFile)); } diff --git a/src/apps/kpunch/cpanel/cpSounds.c b/src/apps/kpunch/cpanel/cpSounds.c index d440718e..87059dc2 100644 --- a/src/apps/kpunch/cpanel/cpSounds.c +++ b/src/apps/kpunch/cpanel/cpSounds.c @@ -56,20 +56,26 @@ #define CPS_LIST_H 148 // the event list: nine rows #define CPS_VOL_LBL_W 40 #define CPS_TEXT_BUF 128 -#define CPS_DRIVERS_MAX 16 -#define CPS_DRV_ITEMS (CPS_DRIVERS_MAX + 2) // Automatic, the drivers, DVX.INI's own choice +#define CPS_DRV_ITEMS (AUDIO_DRIVERS_MAX + 2) // Automatic, the drivers, DVX.INI's own choice #define PERCENT_MAX 100 // The sound library's functions (NULL when it is not installed) typedef struct { - __typeof__(audioDeskKey) *key; + __typeof__(audioDeskFile) *file; __typeof__(audioDeskLabel) *label; __typeof__(audioDeskPlayFile) *playFile; __typeof__(audioDeskReload) *reload; + __typeof__(audioDeskSetFile) *setFile; __typeof__(audioDriverNames) *driverNames; __typeof__(audioErrorText) *errorText; __typeof__(audioGetInfo) *getInfo; + const PrefsChoiceT *midiChoices; + __typeof__(audioPercentToVolume) *toVolume; __typeof__(audioSetMasterVolume) *setVolume; + __typeof__(audioSettingDriver) *driver; + __typeof__(audioSettingMidi) *midi; + __typeof__(audioSettingsStore) *store; + __typeof__(audioSettingVolume) *volume; } SndFnsT; typedef struct { @@ -80,23 +86,26 @@ typedef struct { #define SND_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(SndFnsT, field) } static const SndFnSymT sSyms[] = { - SND_FN_SYM(key, audioDeskKey), + SND_FN_SYM(file, audioDeskFile), SND_FN_SYM(label, audioDeskLabel), SND_FN_SYM(playFile, audioDeskPlayFile), SND_FN_SYM(reload, audioDeskReload), + SND_FN_SYM(setFile, audioDeskSetFile), SND_FN_SYM(driverNames, audioDriverNames), SND_FN_SYM(errorText, audioErrorText), SND_FN_SYM(getInfo, audioGetInfo), - SND_FN_SYM(setVolume, audioSetMasterVolume) + SND_FN_SYM(midiChoices, audioMidiChoices), + SND_FN_SYM(toVolume, audioPercentToVolume), + SND_FN_SYM(setVolume, audioSetMasterVolume), + SND_FN_SYM(driver, audioSettingDriver), + SND_FN_SYM(midi, audioSettingMidi), + SND_FN_SYM(store, audioSettingsStore), + SND_FN_SYM(volume, audioSettingVolume) }; // The sound files listed from the sounds folder static const char *sSoundExts[] = { ".wav", ".voc" }; -// [audio] midi: what the dropdown shows and stores -static const char *sMidiLabels[] = { "Automatic", "MIDI port (MPU-401)", "FM synthesizer chip", "Built-in synthesizer" }; -static const char *sMidiValues[] = { AUDIO_MIDI_AUTO, AUDIO_MIDI_MPU401, AUDIO_MIDI_FM, AUDIO_MIDI_SOFTWARE }; - // ============================================================ // Module state // ============================================================ @@ -116,6 +125,7 @@ static char sDrvNames[CPS_DRV_ITEMS][AUDIO_NAME_MAX]; // as stored static const char *sDrvLabels[CPS_DRV_ITEMS]; static int32_t sDrvCount = 0; static const char *sEventLabels[SoundCountE]; +static const char *sMidiLabels[AUDIO_MIDI_CHOICE_COUNT]; static int32_t sChoice[SoundCountE]; // ============================================================ @@ -132,7 +142,6 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *pref void cpSoundsFree(void); void cpSoundsRevert(void); void cpSoundsStore(PrefsHandleT *prefs); -static int32_t iniVolume(void); static void onBrowse(WidgetT *w); static void onEvent(WidgetT *w); static void onFile(WidgetT *w); @@ -242,7 +251,7 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { for (int32_t i = 0; i < SoundCountE; i++) { sEventLabels[i] = sSnd.label((DvxSoundE)i); - sChoice[i] = addFile(prefsGetString(prefs, AUDIO_SOUNDS_SECTION, sSnd.key((DvxSoundE)i), "")); + sChoice[i] = addFile(sSnd.file(prefs, (DvxSoundE)i)); } wgtLabel(page, "Events:"); @@ -272,14 +281,14 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { sVolSldr->onChange = onVolume; sVolLbl = wgtLabel(row, ""); sVolLbl->prefW = wgtPixels(CPS_VOL_LBL_W); - wgtSliderSetValue(sVolSldr, iniVolume()); + wgtSliderSetValue(sVolSldr, sSnd.volume(prefs)); showVolume(); // Driver: "Automatic", every installed driver, and DVX.INI's own // choice even when that driver is missing (so OK keeps it). - char names[CPS_DRIVERS_MAX][AUDIO_NAME_MAX]; - int32_t count = sSnd.driverNames(names, CPS_DRIVERS_MAX); - const char *want = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, AUDIO_DRIVER_AUTO); + char names[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX]; + int32_t count = sSnd.driverNames(names, AUDIO_DRIVERS_MAX); + const char *want = sSnd.driver(prefs); int32_t drvSel = -1; sDrvCount = 0; @@ -301,16 +310,10 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { wgtDropdownSetItems(sDrvDrop, sDrvLabels, sDrvCount); wgtDropdownSetSelected(sDrvDrop, drvSel); - const char *midi = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO); - int32_t midiSel = 0; - - for (int32_t i = 0; i < DVX_ARRAY_LEN(sMidiValues); i++) { - midiSel = strcasecmp(sMidiValues[i], midi) == 0 ? i : midiSel; - } - + prefsChoiceLabels(sSnd.midiChoices, AUDIO_MIDI_CHOICE_COUNT, sMidiLabels); sMidiDrop = wgtDropdown(addRow(page, "MIDI music:")); - wgtDropdownSetItems(sMidiDrop, sMidiLabels, DVX_ARRAY_LEN(sMidiLabels)); - wgtDropdownSetSelected(sMidiDrop, midiSel); + wgtDropdownSetItems(sMidiDrop, sMidiLabels, AUDIO_MIDI_CHOICE_COUNT); + wgtDropdownSetSelected(sMidiDrop, sSnd.midi(prefs)); AudioInfoT info; char text[CPS_TEXT_BUF]; @@ -339,31 +342,21 @@ void cpSoundsFree(void) { void cpSoundsRevert(void) { if (sSnd.setVolume) { - sSnd.setVolume(iniVolume() * AUDIO_VOLUME_MAX / PERCENT_MAX); + sSnd.setVolume(sSnd.toVolume(sSnd.volume(sPrefs))); } } void cpSoundsStore(PrefsHandleT *prefs) { - if (!sSnd.key) { + if (!sSnd.store) { return; } for (int32_t i = 0; i < SoundCountE; i++) { - prefsSetString(prefs, AUDIO_SOUNDS_SECTION, sSnd.key((DvxSoundE)i), sFileValues[sChoice[i]]); + sSnd.setFile(prefs, (DvxSoundE)i, sFileValues[sChoice[i]]); } - prefsSetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, wgtSliderGetValue(sVolSldr)); - prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, sDrvNames[wgtDropdownGetSelected(sDrvDrop)]); - prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, sMidiValues[wgtDropdownGetSelected(sMidiDrop)]); -} - - -// The master volume in DVX.INI as the Control Panel opened (percent). -static int32_t iniVolume(void) { - int32_t v = prefsGetInt(sPrefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, PERCENT_MAX); - - return v < 0 ? 0 : (v > PERCENT_MAX ? PERCENT_MAX : v); + sSnd.store(prefs, sDrvNames[wgtDropdownGetSelected(sDrvDrop)], wgtDropdownGetSelected(sMidiDrop), wgtSliderGetValue(sVolSldr)); } @@ -428,7 +421,7 @@ static void onTest(WidgetT *w) { static void onVolume(WidgetT *w) { - sSnd.setVolume(wgtSliderGetValue(w) * AUDIO_VOLUME_MAX / PERCENT_MAX); + sSnd.setVolume(sSnd.toVolume(wgtSliderGetValue(w))); showVolume(); } diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c index 8936a442..da451c68 100644 --- a/src/apps/kpunch/cpanel/cpanel.c +++ b/src/apps/kpunch/cpanel/cpanel.c @@ -24,7 +24,7 @@ // // System configuration app with these tabs: // Mouse -- scroll direction, wheel speed, double-click speed, acceleration, cursor speed -// Colors -- all 18 system colors, theme load/save +// Colors -- every system color, theme load/save // Desktop -- wallpaper image (stretch mode) // Video -- resolution and color depth // File Types -- which program opens each kind of document (cpTypes.c) @@ -34,7 +34,8 @@ // Sounds -- the sound of each desktop event, volume, sound driver, MIDI music (cpSounds.c) // // Changes preview live. OK saves to DVX.INI, Cancel reverts to the -// state captured when the control panel was opened. +// state captured when the control panel was opened (an in-memory INI +// written and read back with the same libdvx routines as DVX.INI). #include "dvxApp.h" #include "dvxDlg.h" @@ -143,18 +144,9 @@ static DxeAppContextT *sCtx = NULL; static WindowT *sWin = NULL; static PrefsHandleT *sPrefs = NULL; -// Saved state for Cancel -static uint8_t sSavedColorRgb[ColorCountE][3]; -static int32_t sSavedWheelDir; -static int32_t sSavedDblClick; // double-click ms (dvxSetMouseConfig input) -static int32_t sSavedAccelThresh; // accel threshold (dvxSetMouseConfig input) -static int32_t sSavedMickeys; // mickey ratio (dvxSetMouseConfig input) -static int32_t sSavedWheelStep; -static int32_t sSavedVideoW; -static int32_t sSavedVideoH; -static int32_t sSavedVideoBpp; -static WallpaperModeE sSavedWpMode; -static char sSavedWallpaperPath[DVX_MAX_PATH]; +// Saved state for Cancel: colors, mouse, desktop and video mode in the +// DVX.INI layout (prefsCreate; NULL when out of memory) +static PrefsHandleT *sSnapshot = NULL; // Mouse tab widgets static WidgetT *sWheelDrop = NULL; @@ -178,7 +170,6 @@ static WidgetT *sColorSwatch = NULL; static WidgetT *sWallpaperLbl = NULL; static WidgetT *sWpaperList = NULL; static WidgetT *sWpModeDrop = NULL; -static char sWallpaperPath[DVX_MAX_PATH]; static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array static const char **sWpaperLabels = NULL; // stb_ds dynamic array @@ -199,7 +190,6 @@ static WidgetT *sThemeList = NULL; // Prototypes // ============================================================ -static int32_t accelIndexFromPrefs(void); static void applyMouseConfig(void); void appShutdown(void); static void buildColorsTab(WidgetT *page); @@ -257,13 +247,6 @@ SHELL_APP_DESCRIPTOR( ); -// Index of the acceleration level stored in the INI (the default level -// when missing or unknown). -static int32_t accelIndexFromPrefs(void) { - return prefsGetChoice(sPrefs, "mouse", "acceleration", prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT); -} - - static void applyMouseConfig(void) { int32_t dirIdx = wgtDropdownGetSelected(sWheelDrop); int32_t dbl = wgtSliderGetValue(sDblClickSldr); @@ -307,6 +290,8 @@ void appShutdown(void) { sLabelBufs = NULL; prefsClose(sPrefs); sPrefs = NULL; + prefsClose(sSnapshot); + sSnapshot = NULL; cpTypesFree(); cpJoystickFree(); cpSoundsFree(); @@ -400,7 +385,7 @@ static void buildDesktopTab(WidgetT *page) { sWpaperList->onDblClick = onApplyWallpaper; wgtListBoxSetItems(sWpaperList, sWpaperLabels, arrlen(sWpaperEntries)); - sWallpaperLbl = wgtLabel(page, sWallpaperPath[0] ? sWallpaperPath : "(none)"); + sWallpaperLbl = wgtLabel(page, sAc->wallpaperPath[0] ? sAc->wallpaperPath : "(none)"); WidgetT *btnRow = wgtHBox(page); btnRow->spacing = wgtPixels(CP_SPACING); @@ -462,8 +447,7 @@ static void buildMouseTab(WidgetT *page) { sWheelStepSldr->weight = WGT_WEIGHT_FILL; sWheelStepSldr->onChange = onWheelStepSlider; - int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT); - wgtSliderSetValue(sWheelStepSldr, wheelStep); + wgtSliderSetValue(sWheelStepSldr, sAc->wheelStep); wgtLabel(wheelStepRow, "Fast "); sWheelStepLbl = wgtLabel(wheelStepRow, ""); sWheelStepLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W); @@ -482,8 +466,7 @@ static void buildMouseTab(WidgetT *page) { sDblClickSldr->weight = WGT_WEIGHT_FILL; sDblClickSldr->onChange = onDblClickSlider; - int32_t dblMs = prefsGetInt(sPrefs, "mouse", "doubleclick", MOUSE_DBLCLICK_DEFAULT_MS); - wgtSliderSetValue(sDblClickSldr, dblMs); + wgtSliderSetValue(sDblClickSldr, sAc->dblClickMs); wgtLabel(dblRow, "Slow "); sDblClickLbl = wgtLabel(dblRow, ""); sDblClickLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W); @@ -504,7 +487,7 @@ static void buildMouseTab(WidgetT *page) { sAccelDrop->weight = WGT_WEIGHT_FILL; sAccelDrop->onChange = onAccelChange; wgtDropdownSetItems(sAccelDrop, accelItems, PREFS_ACCEL_CHOICE_COUNT); - wgtDropdownSetSelected(sAccelDrop, accelIndexFromPrefs()); + wgtDropdownSetSelected(sAccelDrop, prefsChoiceIndexOfValue(prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, sAc->mouseAccel)); wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL); @@ -519,10 +502,9 @@ static void buildMouseTab(WidgetT *page) { sSpeedSldr->weight = WGT_WEIGHT_FILL; sSpeedSldr->onChange = onSpeedSlider; - // INI 'speed' holds the mickey ratio (the unit the platform and the - // shell boot path use); convert to the inverted slider position. - int32_t mickeys = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT); - wgtSliderSetValue(sSpeedSldr, CP_SPEED_INVERT - mickeys); + // The context holds the mickey ratio (the unit the platform and the + // INI 'speed' key use); convert to the inverted slider position. + wgtSliderSetValue(sSpeedSldr, CP_SPEED_INVERT - sAc->mouseMickeys); wgtLabel(speedRow, "Fast "); sSpeedLbl = wgtLabel(speedRow, ""); sSpeedLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W); @@ -685,7 +667,6 @@ static void onChooseWallpaper(WidgetT *w) { static void onClearWallpaper(WidgetT *w) { (void)w; dvxSetWallpaper(sAc, NULL); - sWallpaperPath[0] = '\0'; wgtSetText(sWallpaperLbl, "(none)"); } @@ -696,6 +677,8 @@ static void onClose(WindowT *win) { cpSoundsRevert(); prefsClose(sPrefs); sPrefs = NULL; + prefsClose(sSnapshot); + sSnapshot = NULL; dvxDestroyWindow(sAc, win); sWin = NULL; } @@ -784,46 +767,10 @@ static void onOk(WidgetT *w) { return; } - // Save mouse settings - int32_t wheelSel = wgtDropdownGetSelected(sWheelDrop); - int32_t accelIdx = wgtDropdownGetSelected(sAccelDrop); - - if (wheelSel >= 0 && wheelSel < PREFS_WHEEL_DIR_CHOICE_COUNT) { - prefsSetString(sPrefs, "mouse", "wheel", prefsWheelDirChoices[wheelSel].iniName); - } - - prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); - - if (accelIdx >= 0 && accelIdx < PREFS_ACCEL_CHOICE_COUNT) { - prefsSetString(sPrefs, "mouse", "acceleration", prefsAccelChoices[accelIdx].iniName); - } - - // Store the mickey ratio (not the raw slider position) so the boot - // path, which applies this value directly, matches the live setting. - prefsSetInt(sPrefs, "mouse", "speed", CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr)); - prefsSetInt(sPrefs, "mouse", "wheelspeed", wgtSliderGetValue(sWheelStepSldr)); - - // Save colors to INI - for (int32_t i = 0; i < ColorCountE; i++) { - uint8_t r; - uint8_t g; - uint8_t b; - dvxGetColor(sAc, (ColorIdE)i, &r, &g, &b); - - char val[16]; - snprintf(val, sizeof(val), "%d,%d,%d", r, g, b); - prefsSetString(sPrefs, "colors", dvxColorName((ColorIdE)i), val); - } - - // Save desktop settings - if (sWallpaperPath[0]) { - prefsSetString(sPrefs, "desktop", "wallpaper", sWallpaperPath); - } else { - prefsRemove(sPrefs, "desktop", "wallpaper"); - } - - prefsSetString(sPrefs, "desktop", "mode", prefsWallpaperChoices[sAc->wallpaperMode].iniName); - + // Every change was applied live, so the context holds what to save. + dvxMouseToPrefs(sAc, sPrefs); + dvxColorsToPrefs(sAc, sPrefs); + dvxWallpaperToPrefs(sAc, sPrefs); prefsSetVideoMode(sPrefs, sAc->display.width, sAc->display.height, sAc->display.format.bitsPerPixel); cpNetStore(sPrefs); @@ -833,6 +780,8 @@ static void onOk(WidgetT *w) { prefsSave(sPrefs); prefsClose(sPrefs); sPrefs = NULL; + prefsClose(sSnapshot); + sSnapshot = NULL; cpTypesApply(); cpNetApply(); cpSoundsApply(); @@ -971,7 +920,7 @@ static void onVideoApply(WidgetT *w) { PlatformTicksT now = platformClock(); - if ((PlatformTicksT)(now - lastSecond) >= hz) { + if (now - lastSecond >= hz) { lastSecond = now; secondsLeft--; setIntLabel(msgLbl, CP_REVERT_PREFIX, secondsLeft, CP_REVERT_SUFFIX); @@ -1030,54 +979,39 @@ static void onWheelStepSlider(WidgetT *w) { static void restoreSnapshot(void) { - // Restore colors (a full repaint, so only when something changed) - if (memcmp(sAc->colorRgb, sSavedColorRgb, sizeof(sSavedColorRgb)) != 0) { - memcpy(sAc->colorRgb, sSavedColorRgb, sizeof(sSavedColorRgb)); - dvxApplyColorScheme(sAc); + if (!sSnapshot) { + return; } - // Restore mouse to the pre-open state captured in saveSnapshot. - // The snapshot already holds the dvxSetMouseConfig inputs directly. - dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, sSavedAccelThresh, sSavedMickeys, sSavedWheelStep); + // Each of these changes (and repaints) only what differs. + dvxColorsFromPrefs(sAc, sSnapshot); + dvxMouseFromPrefs(sAc, sSnapshot); - // Restore video mode if changed - if (sAc->display.width != sSavedVideoW || - sAc->display.height != sSavedVideoH || - sAc->display.format.bitsPerPixel != sSavedVideoBpp) { - dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp); + int32_t w; + int32_t h; + int32_t bpp; + + prefsGetVideoMode(sSnapshot, &w, &h, &bpp); + + if (sAc->display.width != w || sAc->display.height != h || sAc->display.format.bitsPerPixel != bpp) { + dvxChangeVideoMode(sAc, w, h, bpp); } - // Restore wallpaper only if path or mode changed. A mode change - // alone rebuilds the scaled image from the current path. - if (strcmp(sAc->wallpaperPath, sSavedWallpaperPath) != 0) { - sAc->wallpaperMode = sSavedWpMode; - dvxSetWallpaper(sAc, sSavedWallpaperPath[0] ? sSavedWallpaperPath : NULL); - } else if (sAc->wallpaperMode != sSavedWpMode) { - dvxSetWallpaperMode(sAc, sSavedWpMode); - } + dvxWallpaperFromPrefs(sAc, sSnapshot); } +// The state Cancel goes back to, written like DVX.INI but kept in memory. static void saveSnapshot(void) { - memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb)); - sSavedWheelDir = sAc->wheelDirection; - // Double-click ms, accel and mickey ratio are captured from prefs, - // which hold the currently-applied values. The context only caches - // double-click as ticks (converting back loses precision each round - // trip), accel and mickeys are not cached at all, and the mouse-tab - // widgets do not exist yet (saveSnapshot runs before buildMouseTab). - sSavedDblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", MOUSE_DBLCLICK_DEFAULT_MS); - sSavedAccelThresh = prefsAccelChoices[accelIndexFromPrefs()].value; - sSavedMickeys = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT); - sSavedWheelStep = sAc->wheelStep; - sSavedVideoW = sAc->display.width; - sSavedVideoH = sAc->display.height; - sSavedVideoBpp = sAc->display.format.bitsPerPixel; + prefsClose(sSnapshot); + sSnapshot = prefsCreate(); - // Save the active wallpaper path and mode from the context - snprintf(sSavedWallpaperPath, sizeof(sSavedWallpaperPath), "%s", sAc->wallpaperPath); - snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sAc->wallpaperPath); - sSavedWpMode = sAc->wallpaperMode; + if (sSnapshot) { + dvxColorsToPrefs(sAc, sSnapshot); + dvxMouseToPrefs(sAc, sSnapshot); + dvxWallpaperToPrefs(sAc, sSnapshot); + prefsSetVideoMode(sSnapshot, sAc->display.width, sAc->display.height, sAc->display.format.bitsPerPixel); + } } @@ -1163,8 +1097,7 @@ static void setWallpaperFromPath(const char *path) { return; } - snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", path); - wgtSetText(sWallpaperLbl, sWallpaperPath); + wgtSetText(sWallpaperLbl, sAc->wallpaperPath); } diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile index aea6f1c2..531dbbfb 100644 --- a/src/apps/kpunch/dvxbasic/Makefile +++ b/src/apps/kpunch/dvxbasic/Makefile @@ -25,10 +25,14 @@ # Builds: # basrt.lib -- BASIC runtime library (VM + values + form # runtime + dlregsym init) -# dvxbasic.app -- BASIC IDE (compiler + UI, links against -# basrt.lib, standalone stub embedded) +# basdev.lib -- BASIC development library (compiler, build +# pipeline, validator, debugger; the IDE and +# BASIC programs through compiler.bas / +# debug.bas; standalone stub embedded) +# dvxbasic.app -- BASIC IDE (UI, links against basrt.lib and +# basdev.lib) # basstub.app -- standalone app stub (obj/dvxbasic, embedded -# into the IDE and bascomp) +# into basdev.lib and bascomp) # bin/system/BASCOMP.EXE-- DOS command-line compiler # bin/host/bascomp -- host command-line compiler # bin/host/basrun -- host headless runner @@ -46,7 +50,7 @@ include ../../../common.mk LIBDVX = ../../../libs/kpunch/libdvx TOOLSDIR = ../../../tools -DVX_INCLUDES = -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I. +DVX_INCLUDES = -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I../../../libs/kpunch/texthelp -I. CFLAGS = $(DOS_CFLAGS) $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks OBJDIR = ../../../../obj/dvxbasic @@ -68,13 +72,20 @@ 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) -RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o +RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/frmModel.o $(OBJDIR)/frmModelBas.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt RT_TARGET = $(RT_TARGETDIR)/basrt.lib -# Compiler objects (only needed by the IDE) +# Compiler objects (the development library) COMP_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(COMP_SRCS))) $(OBJDIR)/basBuild.o +# Development library: the compiler plus the shared validator, debugger +# and proc scanner and the BASIC entry points (basdev/) +DEV_SRCS = basdev/basValidate.c basdev/basDebug.c basdev/basProcs.c basdev/basSource.c basdev/basHelpMap.c basdev/basProject.c basdev/basDevBas.c +DEV_OBJS = $(COMP_OBJS) $(patsubst basdev/%.c,$(OBJDIR)/%.o,$(DEV_SRCS)) +DEV_TARGETDIR = $(LIBSDIR)/kpunch/basdev +DEV_TARGET = $(DEV_TARGETDIR)/basdev.lib + # IDE app objects IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o APP_TARGET = $(APPDIR)/dvxbasic.app @@ -142,7 +153,7 @@ HARNESS_WORK = ../../../../obj/test/harness .PHONY: all clean tests -all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(BASRUN_TARGET) $(SYSTEMDIR)/BASCOMP.EXE +all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(DEV_TARGET) $(DEV_TARGETDIR)/basdev.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(BASRUN_TARGET) $(SYSTEMDIR)/BASCOMP.EXE # 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 @@ -208,15 +219,25 @@ $(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR) $(RT_TARGETDIR)/basrt.dep: basrt.dep | $(RT_TARGETDIR) sed 's/$$/\r/' $< > $@ -# Standalone stub DXE (embedded as resource in IDE app) +# Development library DXE. It carries the STUB and noicon resources every +# build inside DVX uses (BAS_BUILD_STUB_CARRIER in basBuild.h). +$(DEV_TARGET): $(DEV_OBJS) $(STUB_TARGET) noicon.bmp $(DVXRES) | $(DEV_TARGETDIR) + $(DXE3GEN) -o $(DEV_TARGETDIR)/basdev.dxe -U $(DEV_OBJS) + mv $(DEV_TARGETDIR)/basdev.dxe $@ + $(DVXRES) add $@ STUB binary @$(STUB_TARGET) + $(DVXRES) add $@ noicon icon @noicon.bmp + +$(DEV_TARGETDIR)/basdev.dep: basdev/basdev.dep | $(DEV_TARGETDIR) + sed 's/$$/\r/' $< > $@ + +# Standalone stub DXE (embedded as resource in basdev.lib and bascomp) $(STUB_TARGET): $(STUB_OBJS) | $(OBJDIR) $(DXE3GEN) -o $@ -U $(STUB_OBJS) -# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded) -$(APP_TARGET): $(COMP_OBJS) $(IDE_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR) - $(DXE3GEN) -o $@ -U $(COMP_OBJS) $(IDE_OBJS) +# IDE app DXE (compiler from basdev.lib, runtime from basrt.lib) +$(APP_TARGET): $(IDE_OBJS) dvxbasic.res | $(APPDIR) + $(DXE3GEN) -o $@ -U $(IDE_OBJS) $(DVXRES) build $@ dvxbasic.res - $(DVXRES) add $@ STUB binary @$(STUB_TARGET) # DOS object files. One pattern rule per source directory; header @@ -237,6 +258,9 @@ $(OBJDIR)/%.o: ide/%.c $(DVX_MAKEFILES) | $(OBJDIR) $(OBJDIR)/%.o: stub/%.c $(DVX_MAKEFILES) | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< +$(OBJDIR)/%.o: basdev/%.c $(DVX_MAKEFILES) | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + $(OBJDIR)/%.o: $(LIBDVX)/%.c $(DVX_MAKEFILES) | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< @@ -259,11 +283,14 @@ $(OBJDIR): $(RT_TARGETDIR): mkdir -p $(RT_TARGETDIR) +$(DEV_TARGETDIR): + mkdir -p $(DEV_TARGETDIR) + $(APPDIR): mkdir -p $(APPDIR) clean: host-basrt-clean - 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 $(RT_OBJS) $(DEV_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) $(DEV_TARGET) $(DEV_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 $(MAKE) -C $(BASIC_TESTDIR) clean @@ -281,7 +308,7 @@ clean: host-basrt-clean # of the host build. HOSTRT_DIR = $(HOST_OBJROOT)/basrt HOSTRT_TARGET = $(HOST_OBJROOT)/basrt.a -HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/frmParser.c formrt/basNativeHost.c +HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c formrt/frmModel.c formrt/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS))) HOSTRT_CFLAGS = $(HOST_CFLAGS) -Wno-stringop-truncation $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c index 3a1eb77f..f938c494 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.c +++ b/src/apps/kpunch/dvxbasic/basBuild.c @@ -39,6 +39,7 @@ // BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION // BAS_RES_ICON32 (project icon, else the NOICON fallback) // BAS_RES_HELPFILE (basename only) +// BAS_RES_MULTIINSTANCE ("1", only when the project allows copies) // BAS_RES_MODULE, BAS_RES_DEBUG (debug builds) // FORM0, FORM1, ... cut at the end of the outer Begin Form block // so the .frm's BASIC code section (already compiled into @@ -58,6 +59,7 @@ #include "stb_ds_wrap.h" +#include #include #include #include @@ -83,6 +85,10 @@ #define BAS_BUILD_WRAP_SLACK 8 // Longest [Modules]/[Forms] key, "File" plus a decimal index. #define BAS_BUILD_KEY_LEN 32 +// How a compiler message starts when it names the line ("Line N, Col M: ") +#define BAS_BUILD_LINE_PREFIX "Line " +#define BAS_BUILD_ERR_MEMORY "Out of memory." +#define BAS_BUILD_ERR_MODULE "Failed to build module." // emitIcon's error text when it names the unreadable icon path (the // other build errors are string literals) @@ -92,39 +98,63 @@ static char sIconError[BAS_BUILD_ERROR_LEN]; typedef enum { BAS_BUILD_PASS_MODULES, BAS_BUILD_PASS_CLASSES, + BAS_BUILD_PASS_CONTROLS, BAS_BUILD_PASS_FORMS, BAS_BUILD_PASS_COUNT } BasBuildPassE; // The directives around a module's, a form's and a class module's code. static const char *const sWrapBegin[] = { - [BAS_BUILD_WRAP_MODULE] = BAS_WRAP_BEGINMODULE, - [BAS_BUILD_WRAP_FORM] = BAS_WRAP_BEGINFORM, - [BAS_BUILD_WRAP_CLASS] = BAS_WRAP_BEGINCLASS + [BAS_BUILD_WRAP_MODULE] = BAS_WRAP_BEGINMODULE, + [BAS_BUILD_WRAP_FORM] = BAS_WRAP_BEGINFORM, + [BAS_BUILD_WRAP_CLASS] = BAS_WRAP_BEGINCLASS, + [BAS_BUILD_WRAP_CONTROL] = BAS_WRAP_BEGINCLASS }; static const char *const sWrapEnd[] = { - [BAS_BUILD_WRAP_MODULE] = BAS_WRAP_ENDMODULE "\n", - [BAS_BUILD_WRAP_FORM] = BAS_WRAP_ENDFORM "\n", - [BAS_BUILD_WRAP_CLASS] = BAS_WRAP_ENDCLASS "\n" + [BAS_BUILD_WRAP_MODULE] = BAS_WRAP_ENDMODULE "\n", + [BAS_BUILD_WRAP_FORM] = BAS_WRAP_ENDFORM "\n", + [BAS_BUILD_WRAP_CLASS] = BAS_WRAP_ENDCLASS "\n", + [BAS_BUILD_WRAP_CONTROL] = BAS_WRAP_ENDCLASS "\n" }; +// What follows a UserControl's name on its BEGINCLASS line. +#define BAS_BUILD_CTL_MARK " " BAS_UC_CLASS_KW + +// A UserControl's toolbox name or path resource, the second and later +// "-n" suffixed (the numbers count from 1). +#define BAS_BUILD_CTL_FIRST 1 + // Function prototypes (alphabetical) static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len); static int32_t appendText(const char *path, const char *name, const char *value); +bool basBuildAddSource(BasProjectSourceT *ps, const char *path, const char *text, int32_t len, BasBuildWrapE wrap); const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec); bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, const char *ctrlList, int32_t *textLine, int32_t *textLines); +BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err); bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out); +const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec); void basBuildDirOf(const char *path, char *out, int32_t outSize); +const char *basBuildErrorText(const char *message); void basBuildFreeProject(BasProjectSourceT *out); +bool basBuildIsControlProject(PrefsHandleT *prefs); +bool basBuildIsControlType(const char *type); +int32_t basBuildMapFileLine(const BasSourceMapT *map, const char *path, int32_t fileLine); 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); +void basBuildSpecFromProject(PrefsHandleT *prefs, const char *projectDir, BasBuildSpecT *spec); +BasBuildWrapE basBuildWrapForPath(const char *path); static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...); static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec); static int32_t countLines(const char *text, int32_t len); +static const char *emitControls(const char *outPath, const BasBuildSpecT *spec); +static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec); 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 bool pathsEqual(const char *a, const char *b); 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 *stubCarrier(const BasBuildSpecT *spec); +static const char *writeMarker(const char *outPath); static const char *writeStub(const char *outPath, const BasBuildSpecT *spec); @@ -179,8 +209,109 @@ static int32_t appendText(const char *path, const char *name, const char *value) } +bool basBuildAddSource(BasProjectSourceT *ps, const char *path, const char *text, int32_t len, BasBuildWrapE wrap) { + BasSourceMapT entry; + const char *code = text; + char name[BAS_MAX_IDENT] = ""; + char *ctrlList = NULL; + + memset(&entry, 0, sizeof(entry)); + snprintf(entry.path, sizeof(entry.path), "%s", path); + entry.firstFileLine = 1; + + if (wrap == BAS_BUILD_WRAP_CLASS || wrap == BAS_BUILD_WRAP_CONTROL) { + // The class code follows the .cls / .ctl header; a UserControl is + // named by its Begin UserControl line + bool named = (wrap == BAS_BUILD_WRAP_CONTROL) ? basExtractCtlName(text, len, name, BAS_MAX_IDENT) : basExtractClassName(text, len, path, name, BAS_MAX_IDENT); + + if (!named) { + snprintf(ps->error, sizeof(ps->error), (wrap == BAS_BUILD_WRAP_CONTROL) ? "No Begin UserControl line in %s" : "No class name for %s", path); + return false; + } + + int32_t codeOff = basClassCodePos(text, len); + + code = text + codeOff; + entry.firstFileLine = countLines(text, codeOff) + 1; + } + + if (wrap == BAS_BUILD_WRAP_FORM) { + // A .frm without a Begin Form line would otherwise compile its + // code unwrapped, as if it were a module. + if (!basExtractFormName(text, name, BAS_MAX_IDENT)) { + snprintf(ps->error, sizeof(ps->error), "No Begin Form line in %s", path); + return false; + } + + // The BASIC code section follows the outer Begin Form block; the + // BEGINFORM line lists the controls (never longer than the text) + int32_t codeOff = basFindFormEndPos(text, len); + + code = text + codeOff; + entry.firstFileLine = countLines(text, codeOff) + 1; + ctrlList = (char *)malloc((size_t)len + 1); + + if (!ctrlList || basFrmControlList(text, len, ctrlList, len + 1) < 0) { + snprintf(ps->error, sizeof(ps->error), BAS_BUILD_ERR_MEMORY); + free(ctrlList); + return false; + } + } + + bool appended = basBuildAppendFile(ps, code, len - (int32_t)(code - text), wrap, name, ctrlList, &entry.startLine, &entry.lineCount); + + free(ctrlList); + + if (!appended) { + return false; + } + + arrput(ps->map, entry); + ps->fileCount++; + + // A UserControl's header: a CTLn resource, registered when the + // program starts + if (wrap == BAS_BUILD_WRAP_CONTROL) { + int32_t headerLen = (int32_t)(code - text); + char *header = (char *)malloc((size_t)headerLen + 1); + + if (!header) { + snprintf(ps->error, sizeof(ps->error), BAS_BUILD_ERR_MEMORY); + return false; + } + + memcpy(header, text, (size_t)headerLen); + header[headerLen] = '\0'; + arrput(ps->ctlTexts, header); + return true; + } + + if (wrap != BAS_BUILD_WRAP_FORM) { + return true; + } + + // The .frm text itself: a resource of the built program, registered + // with the form runtime of a run + char *frmPath = strdup(path); + char *frmText = (char *)malloc((size_t)len + 1); + + if (!frmPath || !frmText) { + snprintf(ps->error, sizeof(ps->error), BAS_BUILD_ERR_MEMORY); + free(frmPath); + free(frmText); + return false; + } + + memcpy(frmText, text, (size_t)len); + frmText[len] = '\0'; + arrput(ps->frmPaths, frmPath); + arrput(ps->frmTexts, frmText); + return true; +} + + const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) { - if (!outPath || !spec || !spec->module || !spec->selfPath) { + if (!outPath || !spec || !spec->module) { return "Invalid build request."; } @@ -246,8 +377,9 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) { bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, const char *ctrlList, int32_t *textLine, int32_t *textLines) { if (wrap != BAS_BUILD_WRAP_NONE) { - // One line: the directive, the name and a form's controls - const char *ctrls = (ctrlList != NULL) ? ctrlList : ""; + // One line: the directive, the name and a form's controls (a + // UserControl's mark) + const char *ctrls = (wrap == BAS_BUILD_WRAP_CONTROL) ? BAS_BUILD_CTL_MARK : ((ctrlList != NULL) ? ctrlList : ""); size_t cap = strlen(sWrapBegin[wrap]) + strlen(name) + strlen(ctrls) + BAS_BUILD_WRAP_SLACK; char *line = (char *)malloc(cap); @@ -288,21 +420,73 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, Ba } +BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err) { + memset(err, 0, sizeof(*err)); + + BasCompilerT *compiler = basCompilerCreate(source, len); + + if (!compiler) { + err->result = BAS_COMPILE_NO_MEMORY; + snprintf(err->message, sizeof(err->message), BAS_BUILD_ERR_MEMORY); + return NULL; + } + + basCompilerSetOptionExplicit(compiler, optionExplicit); + + if (validator) { + basCompilerSetValidator(compiler, validator); + } + + if (liveCompiler) { + *liveCompiler = compiler; + } + + bool compiled = basCompile(compiler); + BasModuleT *mod = NULL; + + if (liveCompiler) { + *liveCompiler = NULL; + } + + if (compiled) { + mod = basCompilerBuildModule(compiler); + + if (!mod) { + err->result = BAS_COMPILE_NO_MODULE; + snprintf(err->message, sizeof(err->message), BAS_BUILD_ERR_MODULE); + } + } else { + err->result = BAS_COMPILE_ERROR; + err->line = basCompilerErrorLine(compiler); + err->col = basCompilerErrorCol(compiler); + snprintf(err->message, sizeof(err->message), "%s", basCompilerError(compiler)); + } + + basCompilerDestroy(compiler); + return mod; +} + + bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out) { static const char *const sections[BAS_BUILD_PASS_COUNT] = { - [BAS_BUILD_PASS_MODULES] = BAS_INI_SECTION_MODULES, - [BAS_BUILD_PASS_CLASSES] = BAS_INI_SECTION_CLASSES, - [BAS_BUILD_PASS_FORMS] = BAS_INI_SECTION_FORMS + [BAS_BUILD_PASS_MODULES] = BAS_INI_SECTION_MODULES, + [BAS_BUILD_PASS_CLASSES] = BAS_INI_SECTION_CLASSES, + [BAS_BUILD_PASS_CONTROLS] = BAS_INI_SECTION_CONTROLS, + [BAS_BUILD_PASS_FORMS] = BAS_INI_SECTION_FORMS + }; + static const BasBuildWrapE wraps[BAS_BUILD_PASS_COUNT] = { + [BAS_BUILD_PASS_MODULES] = BAS_BUILD_WRAP_MODULE, + [BAS_BUILD_PASS_CLASSES] = BAS_BUILD_WRAP_CLASS, + [BAS_BUILD_PASS_CONTROLS] = BAS_BUILD_WRAP_CONTROL, + [BAS_BUILD_PASS_FORMS] = BAS_BUILD_WRAP_FORM }; memset(out, 0, sizeof(*out)); out->optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false); - // .bas modules first, then .cls class modules, then .frm code sections. + // .bas modules first, then .cls class modules, .ctl UserControls, + // then .frm code sections. for (int32_t pass = 0; pass < BAS_BUILD_PASS_COUNT; pass++) { - bool isForm = (pass == BAS_BUILD_PASS_FORMS); - bool isClass = (pass == BAS_BUILD_PASS_CLASSES); - for (int32_t i = 0; ; i++) { char key[BAS_BUILD_KEY_LEN]; snprintf(key, sizeof(key), "File%d", (int)i); @@ -312,95 +496,24 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje break; } - BasSourceMapT entry; - memset(&entry, 0, sizeof(entry)); - snprintf(entry.path, sizeof(entry.path), "%s%s%s", projectDir, BAS_BUILD_SEP, val); + char path[DVX_MAX_PATH]; + snprintf(path, sizeof(path), "%s%s%s", projectDir, BAS_BUILD_SEP, val); int32_t srcLen = 0; - char *src = platformReadFile(entry.path, &srcLen); + char *src = platformReadFile(path, &srcLen); if (!src) { - snprintf(out->error, sizeof(out->error), "Cannot read %s", entry.path); + snprintf(out->error, sizeof(out->error), "Cannot read %s", path); return false; } - const char *code = src; - char formName[BAS_MAX_IDENT] = ""; - BasBuildWrapE wrap = BAS_BUILD_WRAP_MODULE; + bool added = basBuildAddSource(out, path, src, srcLen, wraps[pass]); - entry.firstFileLine = 1; + free(src); - if (isClass) { - // The class code follows the .cls header - if (!basExtractClassName(src, srcLen, val, formName, BAS_MAX_IDENT)) { - snprintf(out->error, sizeof(out->error), "No class name for %s", entry.path); - free(src); - return false; - } - - int32_t codeOff = basClassCodePos(src, srcLen); - - code = src + codeOff; - entry.firstFileLine = countLines(src, codeOff) + 1; - wrap = BAS_BUILD_WRAP_CLASS; - } - - if (isForm) { - // A .frm without a Begin Form line would otherwise compile - // 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. - int32_t codeOff = basFindFormEndPos(src, srcLen); - - code = src + codeOff; - entry.firstFileLine = countLines(src, codeOff) + 1; - wrap = BAS_BUILD_WRAP_FORM; - } - - // A form's BEGINFORM line lists its controls (never longer than - // the .frm text that declares them) - char *ctrlList = isForm ? (char *)malloc((size_t)srcLen + 1) : NULL; - - if (isForm && (!ctrlList || basFrmControlList(src, srcLen, ctrlList, srcLen + 1) < 0)) { - snprintf(out->error, sizeof(out->error), "Out of memory."); - free(ctrlList); - free(src); + if (!added) { return false; } - - bool appended = basBuildAppendFile(out, code, (int32_t)strlen(code), wrap, formName, ctrlList, &entry.startLine, &entry.lineCount); - - free(ctrlList); - - if (!appended) { - free(src); - 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++; } } @@ -413,6 +526,43 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje } +const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec) { + if (!outPath || !spec || !spec->module) { + return "Invalid build request."; + } + + if (spec->ctlCount <= 0) { + return "A control project needs at least one UserControl (.ctl)."; + } + + uint8_t *modData = NULL; + int32_t modLen = 0; + uint8_t *dbgData = NULL; + int32_t dbgLen = 0; + const char *failure = prepareModule(spec, NULL, NULL, NULL, 0, &modData, &modLen, &dbgData, &dbgLen); + + if (!failure) { + failure = writeMarker(outPath); + } + + if (!failure && (dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen) != 0 || (dbgData && dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen) != 0))) { + failure = BAS_BUILD_ERR_WRITE_RES; + } + + if (!failure) { + failure = emitControls(outPath, spec); + } + + if (!failure) { + failure = copyHelpFile(outPath, spec); + } + + free(modData); + free(dbgData); + return failure; +} + + void basBuildDirOf(const char *path, char *out, int32_t outSize) { snprintf(out, outSize, "%s", path); @@ -426,6 +576,17 @@ void basBuildDirOf(const char *path, char *out, int32_t outSize) { } +const char *basBuildErrorText(const char *message) { + if (strncasecmp(message, BAS_BUILD_LINE_PREFIX, sizeof(BAS_BUILD_LINE_PREFIX) - 1) != 0) { + return message; + } + + const char *colon = strchr(message, ':'); + + return colon ? dvxSkipWs(colon + 1) : message; +} + + void basBuildFreeProject(BasProjectSourceT *out) { for (int32_t i = 0; i < (int32_t)arrlen(out->frmPaths); i++) { free(out->frmPaths[i]); @@ -435,14 +596,44 @@ void basBuildFreeProject(BasProjectSourceT *out) { free(out->frmTexts[i]); } + for (int32_t i = 0; i < (int32_t)arrlen(out->ctlTexts); i++) { + free(out->ctlTexts[i]); + } + arrfree(out->frmPaths); arrfree(out->frmTexts); + arrfree(out->ctlTexts); arrfree(out->map); free(out->source); memset(out, 0, sizeof(*out)); } +bool basBuildIsControlProject(PrefsHandleT *prefs) { + return basBuildIsControlType(prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_TYPE, "")); +} + + +bool basBuildIsControlType(const char *type) { + return type && strcasecmp(type, BAS_PROJECT_TYPE_CONTROL) == 0; +} + + +int32_t basBuildMapFileLine(const BasSourceMapT *map, const char *path, int32_t fileLine) { + for (int32_t i = 0; i < (int32_t)arrlen(map); i++) { + const BasSourceMapT *m = &map[i]; + + if (pathsEqual(m->path, path)) { + int32_t offset = fileLine - m->firstFileLine; + + return (offset >= 0 && offset < m->lineCount) ? m->startLine + offset : 0; + } + } + + return 0; +} + + 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]; @@ -480,6 +671,35 @@ bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projec } +void basBuildSpecFromProject(PrefsHandleT *prefs, const char *projectDir, BasBuildSpecT *spec) { + spec->projName = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, ""); + spec->author = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, ""); + spec->publisher = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, ""); + spec->version = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, ""); + spec->copyright = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, ""); + spec->description = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, ""); + spec->iconPath = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, ""); + spec->helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, ""); + spec->multiInstance = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_MULTIINSTANCE, false); + spec->projectDir = projectDir; +} + + +BasBuildWrapE basBuildWrapForPath(const char *path) { + const char *base = platformPathBaseName(path); + + if (dvxHasExt(base, BAS_FRM_EXT)) { + return BAS_BUILD_WRAP_FORM; + } + + if (dvxHasExt(base, BAS_CTL_EXT)) { + return BAS_BUILD_WRAP_CONTROL; + } + + return dvxHasExt(base, BAS_CLS_EXT) ? BAS_BUILD_WRAP_CLASS : BAS_BUILD_WRAP_MODULE; +} + + static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) { if (!spec->log) { return; @@ -534,8 +754,88 @@ static int32_t countLines(const char *text, int32_t len) { } -// Embed the project icon, or the NOICON fallback carried by the running -// executable when the project names none. Returns NULL on success, else +// A control file's resources after its module: the project's metadata +// (not its name: "name" is the first control's toolbox name), the help +// file's name, then for each UserControl its header (CTLn), its toolbox +// picture (icon24, the header's ToolboxIcon, a file in the project +// folder) and its toolbox name (the header's Description, else its own +// name), the second and later with "-n" suffixes. +static const char *emitControls(const char *outPath, const BasBuildSpecT *spec) { + int32_t rc = 0; + + rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author); + rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher); + rc |= appendText(outPath, BAS_RES_VERSION, spec->version); + rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright); + rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description); + + if (spec->helpFile && spec->helpFile[0]) { + rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); + } + + for (int32_t i = 0; rc == 0 && i < spec->ctlCount; i++) { + const char *text = spec->ctlSources[i]; + int32_t len = (int32_t)strlen(text); + int32_t index = i + BAS_BUILD_CTL_FIRST; + char value[DVX_MAX_PATH]; + char resName[BAS_CTL_RES_NAME_LEN]; + + if (basCtlHeaderValue(text, len, BAS_UC_KEY_TOOLBOXICON, value, (int32_t)sizeof(value)) && value[0]) { + char iconPath[DVX_MAX_PATH]; + int32_t iconLen = 0; + + snprintf(iconPath, sizeof(iconPath), "%s" BAS_BUILD_SEP "%s", spec->projectDir, value); + + void *iconData = platformReadFile(iconPath, &iconLen); + + if (!iconData) { + snprintf(sIconError, sizeof(sIconError), "Cannot read toolbox picture %s", iconPath); + return sIconError; + } + + snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "%s" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_ICON, (int)index); + rc |= dvxResAppend(outPath, resName, DVX_RES_ICON, iconData, (uint32_t)iconLen); + free(iconData); + } + + if (!basCtlHeaderValue(text, len, BAS_UC_KEY_DESCRIPTION, value, (int32_t)sizeof(value)) || !value[0]) { + basExtractCtlName(text, len, value, (int32_t)sizeof(value)); + } + + snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "%s" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_NAME, (int)index); + rc |= appendText(outPath, resName, value); + } + + if (rc != 0) { + return BAS_BUILD_ERR_WRITE_RES; + } + + return emitCtlTexts(outPath, spec); +} + + +// The UserControls' header texts, CTL0, CTL1 ... +static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec) { + if (spec->ctlCount > BAS_MAX_CTL_RESOURCES) { + return "Too many UserControls in one project."; + } + + for (int32_t i = 0; i < spec->ctlCount; i++) { + char resName[BAS_RES_FORM_NAME_LEN]; + + snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); + + if (appendText(outPath, resName, spec->ctlSources[i]) != 0) { + return BAS_BUILD_ERR_WRITE_RES; + } + } + + return NULL; +} + + +// Embed the project icon, or the NOICON fallback of the stub carrier +// when the project names none. Returns NULL on success, else // 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]; @@ -554,7 +854,7 @@ static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) { return sIconError; } } else { - DvxResHandleT *selfRes = dvxResOpen(spec->selfPath); + DvxResHandleT *selfRes = dvxResOpen(stubCarrier(spec)); if (selfRes) { uint32_t size = 0; @@ -601,6 +901,10 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); } + if (spec->multiInstance) { + rc |= appendText(outPath, BAS_RES_MULTIINSTANCE, BAS_RES_FLAG_ON); + } + rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen); if (dbgData && dbgLen > 0) { @@ -644,7 +948,11 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, outIdx++; } - return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES; + if (rc != 0) { + return BAS_BUILD_ERR_WRITE_RES; + } + + return emitCtlTexts(outPath, spec); } @@ -661,6 +969,22 @@ static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms } +// True when a and b name the same file the way DOS compares names: +// letters in any case, '/' and '\\' alike. +static bool pathsEqual(const char *a, const char *b) { + for (; *a && *b; a++, b++) { + bool sepA = (*a == '/' || *a == '\\'); + bool sepB = (*b == '/' || *b == '\\'); + + if (sepA != sepB || (!sepA && tolower((unsigned char)*a) != tolower((unsigned char)*b))) { + return false; + } + } + + return *a == *b; +} + + // Serialize the module (and, for debug builds, its debug info). Release // builds run strip -> obfuscate -> compact on a private copy so the // caller's module stays intact; obfuscation also fills obfFrms. @@ -727,10 +1051,32 @@ static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, i } -// Extract the STUB resource from the running executable and write it as +// The file carrying the STUB and NOICON resources. +static const char *stubCarrier(const BasBuildSpecT *spec) { + return (spec->selfPath && spec->selfPath[0]) ? spec->selfPath : BAS_BUILD_STUB_CARRIER; +} + + +// A control file starts with the resource-only marker (no code); its +// resources follow. +static const char *writeMarker(const char *outPath) { + FILE *f = fopen(outPath, "wb"); + + if (!f) { + return "Cannot create output file."; + } + + size_t written = fwrite(DVX_RES_ONLY_MAGIC, 1, (size_t)DVX_RES_ONLY_MAGIC_LEN, f); + int32_t closed = fclose(f); + + return (written == (size_t)DVX_RES_ONLY_MAGIC_LEN && closed == 0) ? NULL : "Failed writing output file."; +} + + +// Extract the STUB resource from the carrier (stubCarrier) and write it as // the output file; every resource is appended behind it. static const char *writeStub(const char *outPath, const BasBuildSpecT *spec) { - DvxResHandleT *selfRes = dvxResOpen(spec->selfPath); + DvxResHandleT *selfRes = dvxResOpen(stubCarrier(spec)); if (!selfRes) { return "Cannot open own executable to read embedded stub."; diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h index 46a9b173..130a7c9c 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.h +++ b/src/apps/kpunch/dvxbasic/basBuild.h @@ -36,6 +36,7 @@ #ifndef BAS_BUILD_H #define BAS_BUILD_H +#include "compiler/compiler.h" #include "runtime/vm.h" #include "dvxPrefs.h" #include "dvxTypes.h" @@ -65,8 +66,8 @@ typedef struct { const char *iconPath; // NULL/empty: embed the NOICON fallback const char *helpFile; // NULL/empty: no help file - // Path of the running executable; it carries the STUB and NOICON - // resources. + // A file carrying the STUB and NOICON resources (bascomp: itself); + // NULL or "" means BAS_BUILD_STUB_CARRIER. const char *selfPath; // Compiled module. Never modified. @@ -77,10 +78,21 @@ typedef struct { const char *const *frmSources; int32_t frmCount; + // .ctl header texts (the "Begin UserControl ... End" block of each, as + // basBuildAddSource keeps them), one per UserControl: CTLn resources. + const char *const *ctlSources; + int32_t ctlCount; + bool release; - BasBuildLogFnT log; // may be NULL + bool multiInstance; // [Settings] MultiInstance: more than one copy may run + BasBuildLogFnT log; // may be NULL } BasBuildSpecT; +// Where a build inside DVX finds the STUB and NOICON resources: the +// basdev library carries them (it is installed wherever programs can be +// compiled), relative to the DVX folder like every installed path. +#define BAS_BUILD_STUB_CARRIER "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "basdev" DVX_PATH_SEP "basdev.lib" + // Build outPath from spec. Returns NULL on success, otherwise a static // error message. On failure outPath may be left partially written. const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec); @@ -99,12 +111,14 @@ typedef struct { } BasSourceMapT; // A project's BASIC source as the compiler consumes it: every [Modules] -// file in order wrapped in BEGINMODULE "name" / ENDMODULE, then the code of every [Classes] file wrapped in -// BEGINCLASS "name" / ENDCLASS, then the code section of every [Forms] -// file wrapped in BEGINFORM "name" / ENDFORM, each file ending on a -// newline. frmPaths -// 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. +// file in order wrapped in BEGINMODULE "name" / ENDMODULE, then the code +// of every [Classes] file wrapped in BEGINCLASS "name" / ENDCLASS, then +// the code of every [Controls] file wrapped in BEGINCLASS "name" +// USERCONTROL / ENDCLASS, then the code section of every [Forms] file +// wrapped in BEGINFORM "name" / ENDFORM, each file ending on a newline. +// frmPaths and frmTexts list the .frm files (path and full text, as read) +// in the same order for the resource step, ctlTexts the .ctl headers; +// map locates each file's lines. typedef struct { char *source; // NUL-terminated, malloc'd int32_t sourceLen; @@ -112,8 +126,9 @@ typedef struct { int32_t lineCount; // newlines in source (next text starts on lineCount + 1) char **frmPaths; // stb_ds array of malloc'd paths char **frmTexts; // stb_ds array of malloc'd .frm texts + char **ctlTexts; // stb_ds array of malloc'd .ctl header texts (Begin UserControl ... End) BasSourceMapT *map; // stb_ds array, one entry per file - int32_t fileCount; // modules + classes + forms + int32_t fileCount; // modules + classes + controls + forms bool optionExplicit; // [Settings] OptionExplicit char error[BAS_BUILD_ERROR_LEN]; } BasProjectSourceT; @@ -123,13 +138,16 @@ typedef enum { BAS_BUILD_WRAP_NONE, // the text as it is (a lone program) BAS_BUILD_WRAP_MODULE, // a project's module: BEGINMODULE "name" / ENDMODULE (its Private CONST / ENUM stay in it) BAS_BUILD_WRAP_FORM, // a form's code: BEGINFORM "name" / ENDFORM - BAS_BUILD_WRAP_CLASS // a class module's code: BEGINCLASS "name" / ENDCLASS + BAS_BUILD_WRAP_CLASS, // a class module's code: BEGINCLASS "name" / ENDCLASS + BAS_BUILD_WRAP_CONTROL // a UserControl's code: BEGINCLASS "name" USERCONTROL / ENDCLASS } BasBuildWrapE; // Appends one file's text to *ps (a zeroed struct is a valid empty // project) and makes sure it ends on a newline, wrapped as wrap says in // the directives naming name; a form's BEGINFORM line also lists its -// controls, ctrlList (basFrmControlList's text; NULL or "" for none). +// controls, ctrlList (basFrmControlList's text; NULL or "" for none), +// and a UserControl's BEGINCLASS line its USERCONTROL mark (ctrlList is +// not used). // textLine and textLines (either may be // NULL) receive the 1-based line the text starts on and the number of // lines it occupies, wrapper lines excluded -- what an IDE source map @@ -147,18 +165,88 @@ void basBuildDirOf(const char *path, char *out, int32_t outSize); // Releases everything basBuildConcatProject allocated. void basBuildFreeProject(BasProjectSourceT *out); +// Why basBuildCompile failed. +typedef enum { + BAS_COMPILE_OK, // compiled + BAS_COMPILE_ERROR, // an error in the source (line / col set) + BAS_COMPILE_NO_MEMORY, // out of memory + BAS_COMPILE_NO_MODULE // compiled, but the module could not be built +} BasCompileResultE; + +typedef struct { + BasCompileResultE result; + int32_t line; // 1-based line of the source (0: none) + int32_t col; // 1-based column (0: not known) + char message[BAS_BUILD_ERROR_LEN]; // basCompilerError's text, or the failure +} BasCompileErrorT; + +// Compiles source (len bytes) into a module (the caller frees it with +// basModuleFree), with OPTION EXPLICIT for the whole program when +// optionExplicit is set and validator (may be NULL) checking +// CtrlName.Member references. *liveCompiler (may be NULL) holds the +// compiler while it runs: a validator reads its form scope from it. +// NULL on failure, with *err telling why. +BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err); + +// A compiler message without its "Line N, Col M: " prefix. +const char *basBuildErrorText(const char *message); + +// Appends one source file (path names it in the source map; text is its +// whole text, len bytes, NUL-terminated) the way a project is built: a +// module wrapped in BEGINMODULE, a class module's code after its header +// in BEGINCLASS, a UserControl's code after its header in BEGINCLASS ... +// USERCONTROL (the header is kept in ctlTexts), a form's code section in +// BEGINFORM with its controls (the .frm text is kept in frmPaths / +// frmTexts for the resources and the run), a lone program +// (BAS_BUILD_WRAP_NONE) as it is. Returns false +// with ps->error set on failure. +bool basBuildAddSource(BasProjectSourceT *ps, const char *path, const char *text, int32_t len, BasBuildWrapE wrap); + // 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); +// The inverse of basBuildMapLine: the 1-based line of the concatenated +// source holding line fileLine of path (compared as DOS compares names: +// letters in any case, '/' and '\\' alike), +// or 0 when path is not in the map or the line was not compiled (a class +// module's header, a .frm's layout). +int32_t basBuildMapFileLine(const BasSourceMapT *map, 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); +// Fills spec's project metadata (name, author, publisher, version, +// copyright, description, icon, help file, MultiInstance) and projectDir +// from an open .dbp; the strings stay owned by prefs. The other fields +// are left alone. +void basBuildSpecFromProject(PrefsHandleT *prefs, const char *projectDir, BasBuildSpecT *spec); + +// Build outPath, a control file (.wgt holding only resources: the marker +// DVX_RES_ONLY_MAGIC, then the module, each UserControl's header as CTLn +// with its toolbox icon24 and name resources -- "-2", "-3" ... for the +// second and later -- the project's metadata and help file) from spec, a +// control project's (no forms; ctlSources needed). NULL on success, +// else a static error message. +const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec); + +// True when the open .dbp is a control project ([Project] Type = Control). +bool basBuildIsControlProject(PrefsHandleT *prefs); + +// True when type, a [Project] Type value, makes a control project (any +// case). +bool basBuildIsControlType(const char *type); + +// The kind of project file path names, by its extension: a form (.frm), +// a class module (.cls), a UserControl (.ctl) or a module (anything +// else). +BasBuildWrapE basBuildWrapForPath(const char *path); + #ifdef __cplusplus } #endif diff --git a/src/apps/kpunch/dvxbasic/basRes.h b/src/apps/kpunch/dvxbasic/basRes.h index 1a18ef65..3c2a5380 100644 --- a/src/apps/kpunch/dvxbasic/basRes.h +++ b/src/apps/kpunch/dvxbasic/basRes.h @@ -37,7 +37,9 @@ #include // memchr, strlen #include // strncasecmp +#include "dvxRes.h" // DVX_RES_NAME_* resource names #include "formrt/frmParser.h" // FRM_KW_* .frm keyword spellings +#include "compiler/basEvents.h" // BAS_UC_OBJECT, a .ctl header's block keyword // dvxSkipWs is declared in platform/dvxPlat.h, which every translation // unit that includes this header also includes. We forward-declare it @@ -65,8 +67,12 @@ const char *dvxSkipWs(const char *s); #define BAS_INI_SECTION_SETTINGS "Settings" #define BAS_INI_KEY_STARTUPFORM "StartupForm" #define BAS_INI_KEY_OPTIONEXPLICIT "OptionExplicit" +#define BAS_INI_KEY_MULTIINSTANCE "MultiInstance" // True: more than one copy may run at once #define BAS_INI_SECTION_MODULES "Modules" #define BAS_INI_SECTION_CLASSES "Classes" +#define BAS_INI_SECTION_CONTROLS "Controls" // .ctl UserControl files +#define BAS_INI_KEY_TYPE "Type" // [Project] Type: Program (the default) or Control +#define BAS_PROJECT_TYPE_CONTROL "Control" // a control project: builds NAME.wgt #define BAS_INI_SECTION_FORMS "Forms" // ------------------------------------------------------------ @@ -88,6 +94,20 @@ const char *dvxSkipWs(const char *s); #define BAS_CLS_ATTR_PREFIX "VB_" #define BAS_CLS_ATTR_NAME "VB_Name" #define BAS_CLS_EXT ".cls" +#define BAS_CTL_EXT ".ctl" // a UserControl (header Begin UserControl Name ... End, then class code) + +// The "Key = Value" lines of a .ctl header's Begin UserControl block. +#define BAS_UC_KEY_WIDTH "Width" +#define BAS_UC_KEY_HEIGHT "Height" +#define BAS_UC_KEY_CANGETFOCUS "CanGetFocus" +#define BAS_UC_KEY_BACKCOLOR "BackColor" +#define BAS_UC_KEY_TOOLBOXICON "ToolboxIcon" +#define BAS_UC_KEY_DESCRIPTION "Description" +#define BAS_UC_KEY_DEFAULTEVENT "DefaultEvent" +#define BAS_QUOTE_PAIR_LEN 2 // a quoted header value's two quote characters +#define BAS_APP_EXT ".app" // what a program project builds +#define BAS_WGT_EXT ".wgt" // what a control project builds +#define BAS_FRM_EXT ".frm" // a form file // ------------------------------------------------------------ // Resource names inside a compiled .app DXE @@ -106,9 +126,11 @@ const char *dvxSkipWs(const char *s); #define BAS_RES_COPYRIGHT "copyright" #define BAS_RES_DESCRIPTION "description" #define BAS_RES_HELPFILE "helpfile" +#define BAS_RES_MULTIINSTANCE "multiinstance" // present: [Settings] MultiInstance = True +#define BAS_RES_FLAG_ON "1" // text of a flag resource that is present // Icon -- 32x32 application icon -#define BAS_RES_ICON32 "icon32" +#define BAS_RES_ICON32 DVX_RES_NAME_ICON // Binary payload -- bytecode + debug info #define BAS_RES_MODULE "MODULE" @@ -124,6 +146,22 @@ const char *dvxSkipWs(const char *s); #define BAS_RES_FORM_FMT "FORM%ld" #define BAS_RES_FORM_NAME_LEN 16 // buffer for a formatted FORMn resource name +// Per-UserControl header text ("Begin UserControl Name ... End" of its +// .ctl), CTL0, CTL1, ... dense like FORMn: in a program (its own +// controls) and in a control file (.wgt). BAS_MAX_CTL_RESOURCES bounds +// the writer and the readers alike. +#define BAS_RES_CTL_FMT "CTL%ld" +#define BAS_MAX_CTL_RESOURCES 64 + +// A widget file's toolbox resources (formrt basCtlResPath, the IDE +// toolbox, a control project's build): the 24 x 24 icon and the tooltip +// text, room for a resource name, and the "-n" suffix of the second and +// later widgets of one file ("icon24-2"). +#define BAS_CTL_RES_ICON DVX_RES_NAME_WIDGET_ICON +#define BAS_CTL_RES_NAME BAS_RES_NAME +#define BAS_CTL_RES_NAME_LEN DVX_RES_NAME_MAX +#define BAS_CTL_RES_SUFFIX_FMT "%s-%d" + // ------------------------------------------------------------ // Form resource scan limit // ------------------------------------------------------------ @@ -144,12 +182,15 @@ const char *dvxSkipWs(const char *s); static inline int32_t basClassCodePos(const char *text, int32_t len); static inline bool basClassHeaderLine(const char *s, int32_t len); static inline bool basClassNameAttr(const char *text, int32_t len, int32_t *outStart, int32_t *outLen); +static inline bool basCtlHeaderValue(const char *text, int32_t len, const char *key, char *out, int32_t outSize); static inline bool basExtractClassName(const char *text, int32_t len, const char *path, char *nameBuf, int32_t nameBufSize); +static inline bool basExtractCtlName(const char *text, int32_t len, char *nameBuf, int32_t nameBufSize); static inline bool basExtractFormName(const char *frmText, char *nameBuf, int32_t nameBufSize); static inline int32_t basFindFormEndPos(const char *text, int32_t len); static inline int32_t basFrmControlList(const char *frmText, int32_t frmLen, char *out, int32_t outSize); static inline bool basFrmListHas(const char *list, int32_t listLen, const char *name, int32_t nameLen); static inline bool basLineIsWord(const char *s, int32_t len, const char *word); +static inline int32_t basMatchBeginControl(const char *s, int32_t len); static inline int32_t basMatchBeginForm(const char *s, int32_t len); static inline int32_t basMatchKeywordWs(const char *s, int32_t len, const char *kw, int32_t kwLen); @@ -241,6 +282,82 @@ static inline bool basClassNameAttr(const char *text, int32_t len, int32_t *outS } +// The value of key in a .ctl header's "Begin UserControl ... End" +// block ("Key = Value" lines, the value without surrounding quotes) in +// out (outSize bytes). False when the block has no such line. +static inline bool basCtlHeaderValue(const char *text, int32_t len, const char *key, char *out, int32_t outSize) { + int32_t keyLen = (int32_t)strlen(key); + int32_t pos = 0; + bool inBlock = false; + + while (pos < len) { + int32_t lineStart = pos; + + while (pos < len && text[pos] != '\n') { + pos++; + } + + int32_t lineEnd = pos++; + + while (lineEnd > lineStart && (text[lineEnd - 1] == '\r' || text[lineEnd - 1] == ' ' || text[lineEnd - 1] == '\t')) { + lineEnd--; + } + + int32_t l = lineStart; + + while (l < lineEnd && (text[l] == ' ' || text[l] == '\t')) { + l++; + } + + if (!inBlock) { + inBlock = basMatchBeginControl(text + l, lineEnd - l) >= 0; + continue; + } + + if (frmIsEndLine(text + l, lineEnd - l)) { + return false; + } + + // Key, blanks, '=', blanks, the value + if (lineEnd - l <= keyLen || strncasecmp(text + l, key, keyLen) != 0) { + continue; + } + + int32_t v = l + keyLen; + + while (v < lineEnd && (text[v] == ' ' || text[v] == '\t')) { + v++; + } + + if (v >= lineEnd || text[v] != '=') { + continue; + } + + v++; + + while (v < lineEnd && (text[v] == ' ' || text[v] == '\t')) { + v++; + } + + // A quoted value loses its two quotes + int32_t valueLen = lineEnd - v; + + if (valueLen >= BAS_QUOTE_PAIR_LEN && text[v] == '"' && text[lineEnd - 1] == '"') { + v++; + valueLen -= BAS_QUOTE_PAIR_LEN; + } + + int32_t n = (valueLen < outSize - 1) ? valueLen : outSize - 1; + + memcpy(out, text + v, (size_t)n); + out[n] = '\0'; + return true; + } + + return false; +} + + // The class name of a .cls text: its Attribute VB_Name = "name" when the // header has one, else the base name of path (without directory and // extension). Returns false when neither gives a name. @@ -280,6 +397,48 @@ static inline bool basExtractClassName(const char *text, int32_t len, const char } +// The UserControl name of a .ctl text: the name on its header's "Begin +// UserControl " line (len bytes are looked at). False when there +// is none. +static inline bool basExtractCtlName(const char *text, int32_t len, char *nameBuf, int32_t nameBufSize) { + int32_t pos = 0; + + nameBuf[0] = '\0'; + + while (pos < len) { + int32_t lineStart = pos; + + while (pos < len && text[pos] != '\n') { + pos++; + } + + int32_t l = lineStart; + + while (l < pos && (text[l] == ' ' || text[l] == '\t')) { + l++; + } + + int32_t off = basMatchBeginControl(text + l, pos - l); + + if (off >= 0) { + int32_t n = 0; + + while (l + off + n < pos && n < nameBufSize - 1 && text[l + off + n] != ' ' && text[l + off + n] != '\t' && text[l + off + n] != '\r') { + n++; + } + + memcpy(nameBuf, text + l + off, (size_t)n); + nameBuf[n] = '\0'; + return n > 0; + } + + pos++; + } + + return false; +} + + // Extract the form name from a "Begin Form " line in .frm text. // Writes a NUL-terminated name into nameBuf (max nameBufSize bytes, // including the terminator) and returns true if a name was found. @@ -512,6 +671,22 @@ static inline bool basLineIsWord(const char *s, int32_t len, const char *word) { } +// Match "BeginUserControl" at s[0..len) (s already left- +// trimmed): a .ctl header. Returns the offset of the control's name, or +// -1. +static inline int32_t basMatchBeginControl(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 c = basMatchKeywordWs(s + b, len - b, BAS_UC_OBJECT, (int32_t)sizeof(BAS_UC_OBJECT) - 1); + + return (c < 0) ? -1 : b + c; +} + + // Match "BeginForm" 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. diff --git a/src/apps/kpunch/dvxbasic/basdev/basDebug.c b/src/apps/kpunch/dvxbasic/basdev/basDebug.c new file mode 100644 index 00000000..8f9ba16e --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basDebug.c @@ -0,0 +1,1039 @@ +// 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. + +// basDebug.c -- the debugger's view of a paused BASIC program (see basDebug.h) + +#include "basDebug.h" +#include "../compiler/compiler.h" +#include "../compiler/lexer.h" +#include "../formrt/formrt.h" +#include "../runtime/values.h" +#include "dvxPlat.h" + +#include +#include +#include +#include + +#define DBG_LINE_LEN 512 // one Immediate line +#define DBG_WRAP_LEN (DBG_LINE_LEN + 8) // the line plus a "PRINT " prefix +#define DBG_RHS_SRC_LEN (DBG_WRAP_LEN + BAS_MAX_IDENT * 2) // "DIM v AS type: v = rhs" +#define DBG_NAME_LEN (BAS_MAX_IDENT * 2) // a path's variable or field name +#define DBG_LINE_NUM_LEN 16 // decimal line number text +#define DBG_SNIPPET_SLICE 10000 // snippet steps per slice +#define DBG_SNIPPET_SLICES 100 // slices before a snippet is abandoned +#define DBG_QUOTE_OVERHEAD 3 // two quotes plus NUL around a string value +#define DBG_DIM_RESERVE 10 // room kept for ") [n]" after array dims +#define DBG_RHS_VAR "IDE_IMM_RHS" // scratch variable of an assignment's value +#define DBG_KW_PRINT "PRINT" +#define DBG_KW_DIM "DIM" +#define DBG_KW_LET "LET " +#define DBG_KW_LEN(s) ((int32_t)sizeof(s) - 1) +#define DBG_ERROR_PREFIX "Error: " +#define DBG_ERR_NOT_DONE "Statement did not finish; stopped." +#define DBG_ERR_TYPE "Cannot assign to this variable type" +#define DBG_ERR_NO_VAR "No such variable here" +#define DBG_ERR_MEMORY "Out of memory" +#define DBG_ERR_MODULE "Failed to build module" + +// Text a snippet printed, captured into a caller's buffer. +typedef struct { + char *buf; + int32_t cap; + int32_t len; +} DbgCaptureT; + +// Prototypes (alphabetical) +static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx); +int32_t basDbgAppendDropping(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen); +int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx); +int32_t basDbgCurProcIdx(const BasVmT *vm); +bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize); +bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx); +const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name); +void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize); +const char *basDbgInstanceTypeName(const BasVmT *vm, const BasObjectT *obj); +bool basDbgIsMangledName(const char *name); +int32_t basDbgLocals(BasVmT *vm, BasDbgRowFnT fn, void *ctx); +const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc); +BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr); +BasVmResultE basDbgRunSnippet(BasVmT *vm); +bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx); +BasVmT *basDbgSnippetVm(BasVmT *pausedVm, const char *src, BasModuleT **outMod, char *err, int32_t errSize, const char *captureName, int32_t *outCaptureIdx); +const char *basDbgTypeName(uint8_t dt); +BasValueT *basDbgVarSlot(BasVmT *vm, const BasDebugVarT *dv); +bool basDbgVarVisible(const BasVmT *vm, const BasDebugVarT *dv, int32_t curProcIdx); +static void capturePrint(void *ctx, const char *text, bool newline); +static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx); +static int32_t findMemberVarIdx(const BasVmT *vm, const BasObjectT *obj, const char *name); +static int32_t findUdtFieldIdx(const BasModuleT *mod, int32_t typeId, const char *fieldName); +static bool isPaused(const BasVmT *vm); +static bool kwMatch(const char *p, const char *kw); +static BasValueT *memberVarSlot(const BasVmT *vm, const BasDebugVarT *dv); +static const char *objectClassName(const BasVmT *vm, const BasObjectT *obj); +static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount); +static void printError(BasPrintFnT printFn, void *ctx, const char *msg); +static int32_t procIndexForPc(const BasVmT *vm, int32_t pc); +static void reportSnippetResult(BasVmT *vm, BasVmResultE result, BasPrintFnT printFn, void *ctx); +static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned); + + +// Evaluate rhs as a value of slot's type and store it there. lhs (lhsLen +// bytes) names the variable in the confirmation. +static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx) { + // Only scalars can be assigned; the RHS is evaluated as an expression + bool scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN || slot->type == BAS_TYPE_DATE || slot->type == BAS_TYPE_BYTE || slot->type == BAS_TYPE_CURRENCY; + + if (!scalar) { + printFn(ctx, DBG_ERR_TYPE, true); + return false; + } + + BasValueT newVal; + + if (!evalRhs(vm, rhs, slot->type, &newVal, printFn, ctx)) { + return false; + } + + // The new value replaces the old one (ownership moves to the slot) + basValRelease(slot); + *slot = newVal; + + char confirm[BAS_DBG_VALUE_LEN]; + + snprintf(confirm, sizeof(confirm), "%.*s = ", (int)lhsLen, lhs); + printFn(ctx, confirm, false); + basDbgFormatValue(vm, slot, confirm, sizeof(confirm)); + printFn(ctx, confirm, true); + return true; +} + + +int32_t basDbgAppendDropping(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen) { + if (textLen > cap - 1) { + text += textLen - (cap - 1); + textLen = cap - 1; + } + + int32_t drop = 0; + int32_t keep = len; + + while (keep + textLen > cap - 1) { + const char *nl = memchr(buf + drop, '\n', (size_t)keep); + + if (!nl) { + drop += keep; + keep = 0; + break; + } + + int32_t cut = (int32_t)(nl + 1 - (buf + drop)); + + drop += cut; + keep -= cut; + } + + memmove(buf, buf + drop, (size_t)keep); + memcpy(buf + keep, text, (size_t)textLen); + keep += textLen; + buf[keep] = '\0'; + return keep; +} + + +int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx) { + const char *cells[BAS_DBG_STACK_COLS]; + char lineBuf[DBG_LINE_NUM_LEN]; + int32_t rows = 0; + + // Current location first + if (curLine > 0) { + int32_t curProcIdx = basDbgCurProcIdx(vm); + + snprintf(lineBuf, sizeof(lineBuf), "%d", (int)curLine); + cells[0] = curProcIdx >= 0 ? vm->module->procs[curProcIdx].name : BAS_DBG_MODULE_FRAME; + cells[1] = lineBuf; + fn(ctx, cells, BAS_DBG_STACK_COLS); + rows++; + } + + // Walk the callers, innermost first. Frame d is suspended at the + // return address of frame d + 1; frame 0 is the implicit module frame. + for (int32_t d = vm->callDepth - 2; d >= 0; d--) { + cells[0] = d > 0 ? basDbgProcNameForPc(vm, vm->callStack[d + 1].returnPc) : BAS_DBG_MODULE_FRAME; + cells[1] = ""; + fn(ctx, cells, BAS_DBG_STACK_COLS); + rows++; + } + + return rows; +} + + +// Proc bodies carry no end address and module-level code can sit after a +// proc (a later file, form init code), so the frame depth -- not the pc +// -- says whether a proc is executing. +int32_t basDbgCurProcIdx(const BasVmT *vm) { + return vm->callDepth > 1 ? procIndexForPc(vm, vm->pc) : -1; +} + + +bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize) { + const char *end = NULL; + BasValueT *slot = isPaused(pausedVm) ? basDbgResolvePath(pausedVm, dvxSkipWs(expr), &end) : NULL; + + // A plain variable path reads the program directly + if (slot && end && *dvxSkipWs(end) == '\0') { + basDbgFormatValue(pausedVm, slot, out, outSize); + return true; + } + + if (!isPaused(pausedVm) || !pausedVm->module->debugVars) { + return false; + } + + // Anything else: PRINT it in a snippet and keep what it printed + char wrapped[DBG_WRAP_LEN]; + + snprintf(wrapped, sizeof(wrapped), "%s %s", DBG_KW_PRINT, expr); + + BasModuleT *mod = NULL; + char err[BAS_DBG_VALUE_LEN]; + BasVmT *tvm = basDbgSnippetVm(pausedVm, wrapped, &mod, err, sizeof(err), NULL, NULL); + + if (!tvm) { + return false; + } + + DbgCaptureT cap; + + cap.buf = out; + cap.cap = outSize; + cap.len = 0; + out[0] = '\0'; + basVmSetPrintCallback(tvm, capturePrint, &cap); + basDbgRunSnippet(tvm); + + // Strip the trailing newline + if (cap.len > 0 && out[cap.len - 1] == '\n') { + out[--cap.len] = '\0'; + } + + basVmDestroy(tvm); + basModuleFree(mod); + return cap.len > 0; +} + + +bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx) { + bool assigned = false; + + if (!line || *line == '\0' || tryAssign(pausedVm, line, printFn, ctx, &assigned)) { + return assigned; + } + + char wrapped[DBG_WRAP_LEN]; + + // A statement keyword runs as it is; anything else is printed + if (kwMatch(line, DBG_KW_PRINT) || kwMatch(line, DBG_KW_DIM) || kwMatch(line, DBG_KW_LET)) { + snprintf(wrapped, sizeof(wrapped), "%s", line); + } else { + snprintf(wrapped, sizeof(wrapped), "%s %s", DBG_KW_PRINT, line); + } + + BasModuleT *mod = NULL; + char err[BAS_DBG_VALUE_LEN]; + BasVmT *vm = basDbgSnippetVm(pausedVm, wrapped, &mod, err, sizeof(err), NULL, NULL); + + if (!vm) { + printError(printFn, ctx, err); + return false; + } + + basVmSetPrintCallback(vm, printFn, ctx); + reportSnippetResult(vm, basDbgRunSnippet(vm), printFn, ctx); + basVmDestroy(vm); + basModuleFree(mod); + return false; +} + + +const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name) { + if (!isPaused(vm) || !vm->module->debugVars) { + return NULL; + } + + int32_t curProcIdx = basDbgCurProcIdx(vm); + + for (int32_t i = 0; i < vm->module->debugVarCount; i++) { + const BasDebugVarT *dv = &vm->module->debugVars[i]; + + if (strcasecmp(dv->name, name) == 0 && basDbgVarVisible(vm, dv, curProcIdx)) { + return dv; + } + } + + return NULL; +} + + +void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize) { + switch (v->type) { + case BAS_TYPE_INTEGER: + case BAS_TYPE_BYTE: + snprintf(buf, bufSize, "%d", (int)v->intVal); + break; + + case BAS_TYPE_CURRENCY: { + BasStringT *text = basValFormatString(*v); + + snprintf(buf, bufSize, "%s", text->data); + basStringUnref(text); + break; + } + + case BAS_TYPE_LONG: + snprintf(buf, bufSize, "%ld", (long)v->longVal); + break; + + case BAS_TYPE_SINGLE: + snprintf(buf, bufSize, "%.6g", (double)v->sngVal); + break; + + case BAS_TYPE_DOUBLE: + snprintf(buf, bufSize, "%.10g", v->dblVal); + break; + + case BAS_TYPE_BOOLEAN: + snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False"); + break; + + case BAS_TYPE_DATE: { + BasStringT *text = basValFormatString(*v); + + snprintf(buf, bufSize, "#%s#", text->data); + basStringUnref(text); + break; + } + + case BAS_TYPE_STRING: + if (v->strVal) { + snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - DBG_QUOTE_OVERHEAD), v->strVal->data); + } else { + snprintf(buf, bufSize, "\"\""); + } + break; + + case BAS_TYPE_EMPTY: + snprintf(buf, bufSize, "Empty"); + break; + + case BAS_TYPE_NULL: + snprintf(buf, bufSize, "Null"); + break; + + case BAS_TYPE_MISSING: + snprintf(buf, bufSize, "Missing"); + break; + + case BAS_TYPE_OBJECT: + snprintf(buf, bufSize, "%s", v->objVal ? "<" BAS_DBG_OBJECT_TYPE ">" : "Nothing"); + break; + + case BAS_TYPE_INSTANCE: + if (v->instVal->classIdx == BAS_OBJ_COLLECTION) { + snprintf(buf, bufSize, BAS_DBG_COLLECTION_TYPE " (%d items)", (int)v->instVal->itemCount); + } else { + snprintf(buf, bufSize, "<%s>", basDbgInstanceTypeName(vm, v->instVal)); + } + break; + + case BAS_TYPE_ARRAY: { + BasArrayT *arr = v->arrVal; + + if (!arr) { + snprintf(buf, bufSize, "(uninitialized)"); + break; + } + + // A truncated append leaves the text cut there; the later ones + // then do nothing (dvxStrAppendf passes -1 on) + int32_t pos = dvxStrAppendf(buf, bufSize, 0, "%s(", basDbgTypeName(arr->elementType)); + + for (int32_t d = 0; d < arr->dims && pos < bufSize - DBG_DIM_RESERVE; d++) { + if (d > 0) { + pos = dvxStrAppendf(buf, bufSize, pos, ", "); + } + + pos = dvxStrAppendf(buf, bufSize, pos, "%d To %d", (int)arr->lbound[d], (int)arr->ubound[d]); + } + + dvxStrAppendf(buf, bufSize, pos, ") [%d]", (int)arr->totalElements); + break; + } + + default: + snprintf(buf, bufSize, "..."); + break; + } +} + + +const char *basDbgInstanceTypeName(const BasVmT *vm, const BasObjectT *obj) { + const char *cls = objectClassName(vm, obj); + + if (cls) { + return cls; + } + + return obj->classIdx == BAS_OBJ_COLLECTION ? BAS_DBG_COLLECTION_TYPE : BAS_DBG_OBJECT_TYPE; +} + + +// A STATIC variable's internal name ("DoCount$Count") has a $ in the +// middle; a string variable's name ends with $ and is shown. +bool basDbgIsMangledName(const char *name) { + const char *dollar = strchr(name, '$'); + + return dollar && dollar[1] != '\0'; +} + + +int32_t basDbgLocals(BasVmT *vm, BasDbgRowFnT fn, void *ctx) { + if (!isPaused(vm) || !vm->module->debugVars) { + return 0; + } + + int32_t curProcIdx = basDbgCurProcIdx(vm); + int32_t rows = 0; + + for (int32_t i = 0; i < vm->module->debugVarCount; i++) { + const BasDebugVarT *dv = &vm->module->debugVars[i]; + + if (!basDbgVarVisible(vm, dv, curProcIdx) || basDbgIsMangledName(dv->name)) { + continue; + } + + char typeBuf[BAS_DBG_TYPE_LEN]; + char valueBuf[BAS_DBG_LOCAL_VALUE_LEN]; + BasValueT val; + BasValueT *slot = basDbgVarSlot(vm, dv); + + if (slot) { + val = *slot; + } else { + memset(&val, 0, sizeof(val)); + } + + // Type column -- arrays show "type()" with the element type, an + // object its class, a Variant "Variant/" and what it holds + const char *valType = (val.type == BAS_TYPE_INSTANCE) ? basDbgInstanceTypeName(vm, val.instVal) : basDbgTypeName(val.type); + + if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) { + snprintf(typeBuf, sizeof(typeBuf), "%s()", basDbgTypeName(val.arrVal->elementType)); + } else if (dv->dataType == BAS_TYPE_VARIANT) { + snprintf(typeBuf, sizeof(typeBuf), "%s/%s", basDbgTypeName(BAS_TYPE_VARIANT), valType); + } else if (val.type == BAS_TYPE_INSTANCE) { + snprintf(typeBuf, sizeof(typeBuf), "%s", valType); + } else { + snprintf(typeBuf, sizeof(typeBuf), "%s", basDbgTypeName(dv->dataType)); + } + + basDbgFormatValue(vm, &val, valueBuf, sizeof(valueBuf)); + + const char *cells[BAS_DBG_LOCAL_COLS] = { dv->name, typeBuf, valueBuf }; + + fn(ctx, cells, BAS_DBG_LOCAL_COLS); + rows++; + } + + return rows; +} + + +const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc) { + int32_t idx = procIndexForPc(vm, pc); + + return idx >= 0 ? vm->module->procs[idx].name : BAS_DBG_MODULE_FRAME; +} + + +BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr) { + const char *p = path; + const char *nameStart = p; + + // The variable name (identifier plus optional type suffix) + while (basIsIdentChar(*p)) { + p++; + } + + if (p == nameStart) { + return NULL; + } + + if (basIsTypeSuffixChar(*p)) { + p++; + } + + char varName[DBG_NAME_LEN]; + int32_t nameLen = (int32_t)(p - nameStart); + + if (nameLen >= (int32_t)sizeof(varName)) { + return NULL; + } + + memcpy(varName, nameStart, nameLen); + varName[nameLen] = '\0'; + + const BasDebugVarT *dv = basDbgFindVar(vm, varName); + BasValueT *slot = dv ? basDbgVarSlot(vm, dv) : NULL; + + if (!slot) { + return NULL; + } + + // Optional array subscript: (idx1, idx2, ...) + if (*p == '(') { + if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) { + return NULL; + } + + int32_t indices[BAS_ARRAY_MAX_DIMS]; + int32_t numIndices = 0; + + p = parseIndexList(p + 1, indices, &numIndices); + + int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices); + + if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) { + return NULL; + } + + slot = basValDeref(&slot->arrVal->elements[flatIdx]); + } + + // Optional UDT field or class member variable: .fieldName + if (*p == '.') { + p++; + + bool isInstance = slot->type == BAS_TYPE_INSTANCE; + + if (!isInstance && (slot->type != BAS_TYPE_UDT || !slot->udtVal)) { + return NULL; + } + + const char *fieldStart = p; + + while (basIsIdentChar(*p)) { + p++; + } + + char fieldName[DBG_NAME_LEN]; + int32_t fieldLen = (int32_t)(p - fieldStart); + + if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) { + return NULL; + } + + memcpy(fieldName, fieldStart, fieldLen); + fieldName[fieldLen] = '\0'; + + if (isInstance) { + int32_t memberIdx = findMemberVarIdx(vm, slot->instVal, fieldName); + + if (memberIdx < 0) { + return NULL; + } + + slot = &slot->instVal->fields[memberIdx]; + } else { + int32_t fieldIdx = findUdtFieldIdx(vm->module, slot->udtVal->typeId, fieldName); + + if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) { + return NULL; + } + + slot = &slot->udtVal->fields[fieldIdx]; + } + } + + if (endPtr) { + *endPtr = p; + } + + return slot; +} + + +BasVmResultE basDbgRunSnippet(BasVmT *vm) { + BasVmResultE result = BAS_VM_STEP_LIMIT; + + basVmSetStepLimit(vm, DBG_SNIPPET_SLICE); + + for (int32_t slice = 0; slice < DBG_SNIPPET_SLICES && result == BAS_VM_STEP_LIMIT; slice++) { + result = basVmRun(vm); + } + + return result; +} + + +bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx) { + const char *start = dvxSkipWs(path); + const char *end = NULL; + BasValueT *slot = isPaused(pausedVm) ? basDbgResolvePath(pausedVm, start, &end) : NULL; + + if (!slot || *dvxSkipWs(end) != '\0') { + printError(printFn, ctx, DBG_ERR_NO_VAR); + return false; + } + + return assignSlot(pausedVm, slot, start, (int32_t)(end - start), expr, printFn, ctx); +} + + +BasVmT *basDbgSnippetVm(BasVmT *pausedVm, const char *src, BasModuleT **outMod, char *err, int32_t errSize, const char *captureName, int32_t *outCaptureIdx) { + *outMod = NULL; + err[0] = '\0'; + + BasCompilerT *compiler = basCompilerCreate(src, (int32_t)strlen(src)); + + if (!compiler) { + snprintf(err, errSize, DBG_ERR_MEMORY); + return NULL; + } + + // The paused program's visible variables become the snippet's + // globals, in order: varMap[g] is the debug variable of global g + int32_t varMap[BAS_VM_MAX_GLOBALS]; + int32_t varMapCount = 0; + + if (isPaused(pausedVm) && pausedVm->module->debugVars) { + const BasModuleT *mod = pausedVm->module; + int32_t curProcIdx = basDbgCurProcIdx(pausedVm); + + for (int32_t i = 0; i < mod->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) { + const BasDebugVarT *dv = &mod->debugVars[i]; + + if (!basDbgVarVisible(pausedVm, dv, curProcIdx) || basDbgIsMangledName(dv->name)) { + continue; + } + + if (basCompilerDeclareGlobal(compiler, dv->name, dv->dataType)) { + varMap[varMapCount] = i; + varMapCount++; + } + } + } + + if (!basCompile(compiler)) { + snprintf(err, errSize, "%s", basCompilerError(compiler)); + basCompilerDestroy(compiler); + return NULL; + } + + if (captureName) { + *outCaptureIdx = basCompilerGlobalSlot(compiler, captureName); + } + + BasModuleT *mod = basCompilerBuildModule(compiler); + + basCompilerDestroy(compiler); + + if (!mod) { + snprintf(err, errSize, DBG_ERR_MODULE); + return NULL; + } + + BasVmT *vm = basVmCreate(); + + if (!vm) { + basModuleFree(mod); + snprintf(err, errSize, DBG_ERR_MEMORY); + return NULL; + } + + basVmLoadModule(vm, mod); + + // Copy the paused program's values in + for (int32_t g = 0; g < varMapCount; g++) { + BasValueT *slot = basDbgVarSlot(pausedVm, &pausedVm->module->debugVars[varMap[g]]); + + if (slot) { + vm->globals[g] = basValCopy(*slot); + } + } + + *outMod = mod; + return vm; +} + + +const char *basDbgTypeName(uint8_t dt) { + const char *scalar = basValScalarTypeName(dt); + + if (scalar) { + return scalar; + } + + switch (dt) { + case BAS_TYPE_ARRAY: + return "Array"; + + case BAS_TYPE_UDT: + return "UDT"; + + case BAS_TYPE_OBJECT: + case BAS_TYPE_INSTANCE: + return BAS_DBG_OBJECT_TYPE; + + case BAS_TYPE_EMPTY: + return "Empty"; + + case BAS_TYPE_NULL: + return "Null"; + + case BAS_TYPE_VARIANT: + return "Variant"; + + case BAS_TYPE_MISSING: + return "Missing"; + + default: + return "?"; + } +} + + +BasValueT *basDbgVarSlot(BasVmT *vm, const BasDebugVarT *dv) { + if (!vm) { + return NULL; + } + + if (dv->scope == SCOPE_LOCAL && vm->callDepth > 0) { + BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1]; + + // A frame only initializes and releases its own procedure's + // localCount slots; anything past them is stale. A ByRef + // parameter shows (and sets) the caller's variable. + if (dv->index >= 0 && dv->index < frame->localCount) { + return basValDeref(&frame->locals[dv->index]); + } + } else if (dv->scope == SCOPE_GLOBAL) { + if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) { + return &vm->globals[dv->index]; + } + } else if (dv->scope == SCOPE_FORM && vm->currentFormVars) { + if (dv->index >= 0 && dv->index < vm->currentFormVarCount) { + return &vm->currentFormVars[dv->index]; + } + } else if (dv->scope == SCOPE_MEMBER) { + return memberVarSlot(vm, dv); + } + + return NULL; +} + + +bool basDbgVarVisible(const BasVmT *vm, const BasDebugVarT *dv, int32_t curProcIdx) { + if (dv->scope == SCOPE_MEMBER) { + return memberVarSlot(vm, dv) != NULL; + } + + // Locals and a procedure's SHARED / STATIC names belong to that + // procedure; inside a procedure a module variable shows only when it + // is DIM SHARED (QBasic scope). + if (dv->procIndex >= 0) { + if (dv->procIndex != curProcIdx) { + return false; + } + } else if (curProcIdx >= 0 && !dv->shared) { + return false; + } + + if (dv->scope != SCOPE_FORM) { + return true; + } + + // Form variables of different forms share slot numbers: the form + // whose code runs must match + if (!vm->currentFormVars) { + return false; + } + + if (!dv->formName[0]) { + return true; + } + + return vm->currentForm && strcasecmp(dv->formName, ((const BasFormT *)vm->currentForm)->name) == 0; +} + + +static void capturePrint(void *ctx, const char *text, bool newline) { + DbgCaptureT *cap = (DbgCaptureT *)ctx; + + if (text) { + int32_t tl = (int32_t)strlen(text); + + if (cap->len + tl < cap->cap - 1) { + memcpy(cap->buf + cap->len, text, tl); + cap->len += tl; + } + } + + if (newline && cap->len < cap->cap - 1) { + cap->buf[cap->len++] = '\n'; + } + + cap->buf[cap->len] = '\0'; +} + + +// Evaluate the right-hand side of an assignment as a BASIC expression +// (it may use the paused program's variables) converted to targetType. +// Reports an error to printFn and returns false. +static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx) { + char src[DBG_RHS_SRC_LEN]; + + snprintf(src, sizeof(src), "%s %s AS %s: %s = %s", DBG_KW_DIM, DBG_RHS_VAR, basDbgTypeName(targetType), DBG_RHS_VAR, rhs); + + BasModuleT *mod = NULL; + char err[BAS_DBG_VALUE_LEN]; + int32_t idx = -1; + BasVmT *svm = basDbgSnippetVm(vm, src, &mod, err, sizeof(err), DBG_RHS_VAR, &idx); + + if (!svm) { + printError(printFn, ctx, err); + return false; + } + + BasVmResultE result = basDbgRunSnippet(svm); + bool ok = (result == BAS_VM_HALTED || result == BAS_VM_OK) && idx >= 0 && idx < BAS_VM_MAX_GLOBALS; + + if (ok) { + *outVal = basValCopy(svm->globals[idx]); + } else { + reportSnippetResult(svm, result, printFn, ctx); + } + + basVmDestroy(svm); + basModuleFree(mod); + return ok; +} + + +// Member variable slot of class instance obj called name, from the debug +// variable table (-1 when there is none). +static int32_t findMemberVarIdx(const BasVmT *vm, const BasObjectT *obj, const char *name) { + const char *cls = objectClassName(vm, obj); + + for (int32_t i = 0; cls && i < vm->module->debugVarCount; i++) { + const BasDebugVarT *dv = &vm->module->debugVars[i]; + + if (dv->scope == SCOPE_MEMBER && strcasecmp(dv->formName, cls) == 0 && strcasecmp(dv->name, name) == 0 && dv->index < obj->fieldCount) { + return dv->index; + } + } + + return -1; +} + + +// Field index within a UDT from the debug type table. +static int32_t findUdtFieldIdx(const BasModuleT *mod, int32_t typeId, const char *fieldName) { + for (int32_t t = 0; t < mod->debugUdtDefCount; t++) { + if (mod->debugUdtDefs[t].typeId != typeId) { + continue; + } + + for (int32_t f = 0; f < mod->debugUdtDefs[t].fieldCount; f++) { + if (strcasecmp(mod->debugUdtDefs[t].fields[f].name, fieldName) == 0) { + return f; + } + } + + return -1; + } + + return -1; +} + + +// A paused program to read: a VM with a module. +static bool isPaused(const BasVmT *vm) { + return vm && vm->module; +} + + +static bool kwMatch(const char *p, const char *kw) { + return strncasecmp(p, kw, strlen(kw)) == 0; +} + + +// Member variable dv (SCOPE_MEMBER) of the object the innermost +// procedure runs for, or NULL when that procedure is not one of dv's +// class. +static BasValueT *memberVarSlot(const BasVmT *vm, const BasDebugVarT *dv) { + if (vm->callDepth <= 0) { + return NULL; + } + + const BasValueT *me = &vm->callStack[vm->callDepth - 1].me; + + if (me->type != BAS_TYPE_INSTANCE) { + return NULL; + } + + BasObjectT *obj = me->instVal; + const char *cls = objectClassName(vm, obj); + + if (!cls || strcasecmp(cls, dv->formName) != 0 || dv->index < 0 || dv->index >= obj->fieldCount) { + return NULL; + } + + return &obj->fields[dv->index]; +} + + +// The class module name of class instance obj, or NULL (a Collection). +static const char *objectClassName(const BasVmT *vm, const BasObjectT *obj) { + const BasModuleT *mod = vm ? vm->module : NULL; + + return (mod && obj->classIdx >= 0 && obj->classIdx < mod->classCount) ? mod->classes[obj->classIdx].name : NULL; +} + + +// Parse "i1, i2, ...)" starting just after the '('. Returns the position +// after the closing ')' (or where parsing stopped). +static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount) { + int32_t n = 0; + + while (*p && *p != ')' && n < BAS_ARRAY_MAX_DIMS) { + const char *start = p; + + p = dvxSkipWs(p); + indices[n++] = atoi(p); + + // Skip past the number + if (*p == '-') { + p++; + } + + while (*p >= '0' && *p <= '9') { + p++; + } + + p = dvxSkipWs(p); + + if (*p == ',') { + p++; + } + + // Anything unparseable would otherwise spin forever + if (p == start) { + break; + } + } + + if (*p == ')') { + p++; + } + + *outCount = n; + return p; +} + + +static void printError(BasPrintFnT printFn, void *ctx, const char *msg) { + printFn(ctx, DBG_ERROR_PREFIX, false); + printFn(ctx, msg, true); +} + + +// Index of the compiled proc whose codeAddr is the greatest one <= pc (-1 +// before every proc). Only meaningful for a pc known to be inside a proc +// (see basDbgCurProcIdx). +static int32_t procIndexForPc(const BasVmT *vm, int32_t pc) { + int32_t best = -1; + int32_t bestAddr = -1; + + for (int32_t i = 0; i < vm->module->procCount; i++) { + int32_t addr = vm->module->procs[i].codeAddr; + + if (addr <= pc && addr > bestAddr) { + bestAddr = addr; + best = i; + } + } + + return best; +} + + +// Report a snippet run that did not finish normally. +static void reportSnippetResult(BasVmT *vm, BasVmResultE result, BasPrintFnT printFn, void *ctx) { + if (result == BAS_VM_HALTED || result == BAS_VM_OK || result == BAS_VM_BREAKPOINT) { + return; + } + + printError(printFn, ctx, result == BAS_VM_STEP_LIMIT ? DBG_ERR_NOT_DONE : basVmGetError(vm)); +} + + +// "var = expr" (LET optional) when paused: assigns to the program's +// variable. Returns true when line was handled as an assignment (*assigned +// tells whether it succeeded); false lets the caller run it as a snippet. +static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned) { + if (!isPaused(vm)) { + return false; + } + + const char *p = dvxSkipWs(line); + + if (kwMatch(p, DBG_KW_LET)) { + p = dvxSkipWs(p + DBG_KW_LEN(DBG_KW_LET)); + } + + // The left-hand side must resolve to a live slot of the program + const char *afterLhs = NULL; + BasValueT *slot = basDbgResolvePath(vm, p, &afterLhs); + + if (!slot || !afterLhs) { + return false; + } + + const char *rhs = dvxSkipWs(afterLhs); + + // Must have '=' (but not '==') and something after it + if (*rhs != '=' || rhs[1] == '=') { + return false; + } + + rhs = dvxSkipWs(rhs + 1); + + if (*rhs == '\0') { + return false; + } + + *assigned = assignSlot(vm, slot, p, (int32_t)(afterLhs - p), rhs, printFn, ctx); + return true; +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basDebug.h b/src/apps/kpunch/dvxbasic/basdev/basDebug.h new file mode 100644 index 00000000..652b0ffd --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basDebug.h @@ -0,0 +1,148 @@ +// 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. + +// basDebug.h -- the debugger's view of a paused BASIC program +// +// Which variables are visible where the program is paused, their slots +// and display text, variable paths (a(1, 2).field), the call stack, and +// the Immediate / Watch evaluation: an expression or statement is +// compiled as a snippet whose globals are the paused program's visible +// variables (their values copied in) and run in bounded slices. +// +// One implementation for the IDE (ideMain.c) and the basdev library +// (basDevBas.c). A "paused VM" argument is the program's VM while it is +// paused in the debugger, or NULL when there is none: then only +// expressions without the program's variables can be evaluated. + +#ifndef BAS_DEBUG_H +#define BAS_DEBUG_H + +#include "../runtime/vm.h" + +#include +#include + +// Frame name of module-level code (the call stack's outermost frame) +#define BAS_DBG_MODULE_FRAME "(module)" +// Type names shown for a Collection and for any other object +#define BAS_DBG_COLLECTION_TYPE "Collection" +#define BAS_DBG_OBJECT_TYPE "Object" +// Watch / Immediate value text +#define BAS_DBG_VALUE_LEN 256 +// Locals value column +#define BAS_DBG_LOCAL_VALUE_LEN BAS_MAX_IDENT +// Locals type column ("Integer()", "Variant/ClassName") +#define BAS_DBG_TYPE_LEN (BAS_MAX_IDENT + 16) +// Text of a watch that cannot be evaluated +#define BAS_DBG_EVAL_ERROR "" +// The columns of a basDbgLocals row and of a basDbgCallStack row +#define BAS_DBG_LOCAL_COLS 3 // name, type, value +#define BAS_DBG_STACK_COLS 2 // procedure, line ("" for a caller) + +// One display row: cellCount NUL-terminated cells. +typedef void (*BasDbgRowFnT)(void *ctx, const char *const *cells, int32_t cellCount); + +// Append text (textLen bytes) to the NUL-terminated output buf of cap +// bytes holding len bytes, dropping whole leading lines while it would +// not fit so the newest text always survives (an oversize chunk keeps +// its tail). Returns the new length. +int32_t basDbgAppendDropping(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen); + +// The call stack where vm is paused, innermost first: the current +// procedure (BAS_DBG_MODULE_FRAME at module level) with curLine when it +// is > 0, then each caller with an empty line cell. Returns the rows. +int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx); + +// The compiled procedure the paused vm is in, or -1 at module level. +int32_t basDbgCurProcIdx(const BasVmT *vm); + +// A watch: a variable path is read directly, anything else is evaluated +// as an expression in the paused program (pausedVm NULL: fails). out +// receives the value text; false (out undefined) when it cannot be +// evaluated. +bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize); + +// An Immediate-window line: "var = expr" (LET optional) assigns to the +// paused program's variable; a line starting with PRINT, DIM or LET runs +// as it is; anything else is printed (PRINT expr). Everything it shows +// -- the output, "name = value" after an assignment, "Error: ..." -- goes +// to printFn. True when a variable of the program was changed. +bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx); + +// The variable called name that is visible where vm is paused, or NULL. +const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name); + +// Display text of v ("text" in quotes, #date#, arrays with their bounds, +// , Collection (n items) ...). vm names class instances. +void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize); + +// What a class instance or Collection is: its class name, "Collection". +const char *basDbgInstanceTypeName(const BasVmT *vm, const BasObjectT *obj); + +// Internal names ("Proc$Var" of a STATIC) the debugger hides. +bool basDbgIsMangledName(const char *name); + +// The variables visible where vm is paused, one row each: name, type +// ("Integer()", "Variant/String", a class name ...) and value. Returns +// the rows. +int32_t basDbgLocals(BasVmT *vm, BasDbgRowFnT fn, void *ctx); + +// Name of the procedure holding pc (BAS_DBG_MODULE_FRAME before every +// procedure). +const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc); + +// The live slot a variable path names where vm is paused: var, var(i, +// ...), var.field, var(i).field (a UDT field or a class instance's member +// variable). NULL when it does not resolve. *endPtr (may be NULL) is +// set past the path. +BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr); + +// Run a snippet VM (basDbgSnippetVm) in bounded slices, so an endless +// loop cannot hang the host. BAS_VM_STEP_LIMIT means it was abandoned. +BasVmResultE basDbgRunSnippet(BasVmT *vm); + +// Set variable path (see basDbgResolvePath) of the paused program to the +// value of expr, converted to its type (scalars only). printFn receives +// "path = value" or why it failed. True when it was set. +bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx); + +// Compile src as a snippet and return a VM ready to run it; with a +// paused VM, its visible variables are the snippet's globals, holding +// copies of their values. *outCaptureIdx (when captureName is given) +// receives the global slot of that snippet variable (-1: none). NULL on +// failure with the error text in err. The caller destroys the VM and +// frees *outMod. +BasVmT *basDbgSnippetVm(BasVmT *pausedVm, const char *src, BasModuleT **outMod, char *err, int32_t errSize, const char *captureName, int32_t *outCaptureIdx); + +// BASIC name of a BAS_TYPE_* data type ("Integer", "String" ...). +const char *basDbgTypeName(uint8_t dt); + +// The live slot of debug variable dv where vm is paused, or NULL. +BasValueT *basDbgVarSlot(BasVmT *vm, const BasDebugVarT *dv); + +// Whether dv is in scope where vm is paused: locals of the current +// procedure, module variables (inside a procedure only DIM SHARED ones), +// the variables of the form whose code runs, and the member variables of +// the object a class procedure runs for. +bool basDbgVarVisible(const BasVmT *vm, const BasDebugVarT *dv, int32_t curProcIdx); + +#endif // BAS_DEBUG_H diff --git a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c new file mode 100644 index 00000000..d6b33857 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c @@ -0,0 +1,2013 @@ +// 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. + +// basDevBas.c -- DVX BASIC entry points of the basdev library +// +// Called through DECLARE LIBRARY (src/include/basic/compiler.bas and +// debug.bas), so the signatures use only BASIC-marshalable types: +// strings and LONG in, LONG or STRING out, True (-1) / False (0). A +// returned string stays valid until the next call that returns one. +// +// Source sets, compiled modules, projects and runs are handles (1, 2, +// ...; 0 means none). Each belongs to the program that made it; whatever a program +// leaves open is freed when it ends. +// +// A run is a child program inside the calling program: its own VM and +// form runtime in the same process, driven by RunPoll from a Timer. +// RunPoll never blocks for longer than it is told. Two things nest: +// * A breakpoint or step that stops in the child's module-level code +// just makes RunPoll report RUN_PAUSED. +// * One that stops inside a child's event handler (or its forms' Load +// code, which runs inside RunStart) pauses inside that handler: the +// child keeps the DVX event pump going, so the caller's Timer and +// menus still run and can step, continue or stop it; RunPoll called +// from there returns at once. RunStart (Load code) does not return +// until such a pause is over. +// Freeing a run whose code is on the stack ends it at once; its memory +// is released by the next call into this library once that code has +// unwound (or when the program ends). + +#include "basDebug.h" +#include "basHelpMap.h" +#include "basProcs.h" +#include "basProject.h" +#include "basSource.h" +#include "basValidate.h" +#include "../basBuild.h" +#include "../basRes.h" +#include "../compiler/lexer.h" +#include "../formrt/basRunHost.h" +#include "../formrt/formrt.h" +#include "dvxDlg.h" +#include "dvxMem.h" +#include "dvxPlat.h" +#include "dvxTypes.h" + +#include "stb_ds_wrap.h" + +#include +#include +#include +#include +#include +#include + +DVX_ABI_STAMP(basdev); + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define ROW_CELL_SEP '\t' // between the cells of a row (ListView SetRows) +#define ROW_END '\n' // after each row +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" +#define DEV_OUTPUT_MAX 32768 // PRINT text kept for RunOutput$ (oldest lines dropped) +#define DEV_MESSAGE_LEN (BAS_DBG_VALUE_LEN * 2) +#define DEV_INPUT_PROMPT "Enter value:" +#define DEV_KIND_SUB "Sub" +#define DEV_KIND_FUNCTION "Function" +#define DEV_LINE_SEPS " ,\t\r\n" // between DbgSetBreakpoints line numbers +#define DEV_DECIMAL 10 +#define DEV_KIND_AUTO (-1) // CompSourceAdd: the kind follows the file name +#define DEV_NO_INDEX (-1) // no such keyword / file +#define DEV_TOKEN_ROW_MAX (BAS_MAX_TOKEN_LEN + DEV_MESSAGE_LEN) // one CompTokens$ row +#define DEV_TEXT_BLANK ' ' // what a TAB or line break inside a cell becomes +#define DEV_DESIGN_BUSY "The design UserControls' code is running; try again when it is done." + +// The states RunState reports (debug.bas RUN_*) +#define RUN_READY 0 +#define RUN_RUNNING 1 +#define RUN_EVENTS 2 +#define RUN_PAUSED 3 +#define RUN_ENDED 4 +#define RUN_ERROR 5 + +typedef enum { + DevSourceE, + DevModuleE, + DevRunE, + DevProjectE +} DevKindE; + +// Where a run is +typedef enum { + PhaseReadyE, // made, not started + PhaseModuleE, // module-level code + PhaseEventsE, // waiting for events (a form is open) + PhaseFinalE, // Class_Terminates of the module variables + PhaseEndedE, + PhaseErrorE +} DevPhaseE; + +// A source set: the concatenated program and its source map +typedef struct { + BasProjectSourceT ps; + char dbpPath[DVX_MAX_PATH]; // "" when not read from a project + char projectDir[DVX_MAX_PATH]; + char name[BAS_MAX_IDENT]; // [Project] Name + char startupForm[BAS_MAX_IDENT]; + char helpFile[DVX_MAX_PATH]; // relative to projectDir +} DevSourceT; + +typedef struct { + BasModuleT *mod; + const BasFormRtT *designer; // the program whose design forms run its UserControls (CompDesignControls), NULL: none + int32_t runs; // runs using it + bool freeRequested; // CompModuleFree while a run or a designer used it +} DevModuleT; + +typedef struct { + BasFormRtT *rt; // NULL once the program ended (output, error stay) + BasFormRtT *parentRt; // the program that made the run + int32_t module; // module handle it runs + DevPhaseE phase; + bool paused; + bool stopRequested; + bool freeRequested; + int32_t busy; // calls of this library into the child on the stack + int32_t pausedLine; // concatenated line where paused (0: unknown) + int32_t *breakpoints; // stb_ds: sorted concatenated lines + BasBreakTitleT *breakTitles; // stb_ds: captions tagged while paused + char *output; // DEV_OUTPUT_MAX bytes + int32_t outputLen; + int32_t errorLine; + char error[BAS_VM_ERROR_MSG_LEN]; + char title[BAS_MAX_IDENT]; + char startupForm[BAS_MAX_IDENT]; +} DevRunT; + +typedef struct { + void *obj; // DevSourceT / DevModuleT / DevRunT / PrjStateT; NULL = free slot + int32_t appId; // owner: freed when it ends + uint8_t kind; // DevKindE +} DevHandleT; + +// CompTokens$ in progress: the source the rows quote +typedef struct { + const char *src; +} DevTokensT; + +// Text a caller's print callback gathers +typedef struct { + char *buf; + int32_t cap; + int32_t len; +} DevCaptureT; + +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// the library loads before the shell, and the host tests have none. +typedef void (*DevCleanupFnT)(int32_t appId); +typedef void (*DevCleanupRegFnT)(DevCleanupFnT fn); + +// ============================================================ +// Module state +// ============================================================ + +static DevHandleT *sHandles = NULL; // stb_ds; handle = index + 1 +static bool sCleanupHooked = false; +static char *sText = NULL; // stb_ds: the last string result +static char *sCompError = NULL; // stb_ds: why the last Comp* call failed +static int32_t sCompErrorLine = 0; // concatenated line of the last compile error +static int32_t sCompErrorCol = 0; +static int32_t sCompFileLine = 0; // the same line in its file +static char sCompErrorFile[DVX_MAX_PATH]; +static char sDbgMessage[DEV_MESSAGE_LEN]; // the last Dbg* / Run* message + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static void cellAppend(const char *text, int32_t len); +static void classEventRow(void *ctx, const char *name, const char *params); +static void collectAppend(DevCaptureT *cap, const char *text, int32_t len); +static void collectPrint(void *ctx, const char *text, bool newline); +int32_t CompBuildApp(int32_t mdl, int32_t src, const char *outPath, int32_t release); +const char *CompClassEvents(const char *classText); +int32_t CompCompile(int32_t src, int32_t optionExplicit, int32_t validate); +int32_t CompConcatLine(int32_t src, const char *path, int32_t fileLine); +int32_t CompDesignControls(int32_t mdl, int32_t src, const char *appDir); +int32_t CompDesignRelease(void); +const char *CompError(void); +static void compErrorAppend(const char *s); +int32_t CompErrorCol(void); +const char *CompErrorFile(void); +int32_t CompErrorFileLine(void); +int32_t CompErrorLine(void); +static void compFail(const char *msg); +const char *CompHelpCtrlTopic(const char *typeName); +const char *CompHelpTopic(const char *word); +int32_t CompInCode(const char *source, int32_t position); +int32_t CompIsIdent(const char *name); +const char *CompKeyword(int32_t idx); +int32_t CompKeywordClass(int32_t idx); +int32_t CompKeywordCount(void); +int32_t CompKeywordReserved(int32_t idx); +const char *CompMapFile(int32_t src, int32_t line); +int32_t CompMapLine(int32_t src, int32_t line); +void CompModuleFree(int32_t mdl); +const char *CompProcList(const char *source); +const char *CompProcs(int32_t mdl); +const char *CompRenameInCode(const char *source, const char *oldName, const char *newName, int32_t codeOnly); +int32_t CompSourceAdd(int32_t src, const char *path, const char *text, int32_t kind); +int32_t CompSourceCreate(void); +const char *CompSourceFiles(int32_t src); +void CompSourceFree(int32_t src); +int32_t CompSourceFromProject(int32_t src, const char *dbpPath); +const char *CompSourceText(int32_t src); +const char *CompTokens(const char *source); +const char *CompWithEvents(const char *source); +static int32_t currentAppId(void); +const char *DbgCallStack(int32_t run); +void DbgContinue(int32_t run); +const char *DbgEval(int32_t run, const char *expr); +const char *DbgExec(int32_t run, const char *line); +int32_t DbgLine(int32_t run); +const char *DbgLocals(int32_t run); +const char *DbgMessage(void); +void DbgPause(int32_t run); +const char *DbgProc(int32_t run); +void DbgRunToLine(int32_t run, int32_t line); +void DbgSetBreakpoints(int32_t run, const char *lines); +int32_t DbgSetVar(int32_t run, const char *path, const char *expr); +void DbgStepInto(int32_t run); +void DbgStepOut(int32_t run); +void DbgStepOver(int32_t run); +static bool designRelease(const BasFormRtT *owner); +static const char *devAppDir(const DevSourceT *s, const char *appDir, const BasFormRtT *rt); +static bool devDoEvents(void *ctx); +static bool devInput(void *ctx, const char *prompt, char *buf, int32_t bufSize); +static void devOnBreak(void *ctx, int32_t line); +static void devPrint(void *ctx, const char *text, bool newline); +static const char *devTitle(const DevSourceT *s); +static int32_t handleNew(DevKindE kind, void *obj); +static void *handleOf(int32_t handle, DevKindE kind); +static void handleRelease(int32_t idx); +static void moduleRelease(DevModuleT *m); +int32_t ProjAddFile(int32_t prj, const char *fileName); +void ProjClose(int32_t prj); +const char *ProjCtlHeader(const char *text, const char *key); +const char *ProjCtlKeys(void); +const char *ProjCtlNew(const char *name); +const char *ProjCtlSetHeader(const char *text, const char *key, const char *value); +const char *ProjFile(int32_t prj, int32_t idx); +int32_t ProjFileCount(int32_t prj); +int32_t ProjFileFind(int32_t prj, const char *fileName); +int32_t ProjFileKind(int32_t prj, int32_t idx); +const char *ProjFilePath(int32_t prj, int32_t idx); +const char *ProjGet(int32_t prj, const char *key); +static int32_t projHandle(PrjStateT *p); +int32_t ProjNew(const char *name, const char *folder); +int32_t ProjOpen(const char *dbpPath); +const char *ProjPath(int32_t prj); +int32_t ProjRemoveFile(int32_t prj, int32_t idx); +int32_t ProjRenameFile(int32_t prj, int32_t idx, const char *newName); +int32_t ProjSave(int32_t prj); +int32_t ProjSet(int32_t prj, const char *key, const char *value); +static void rowAdd(void *ctx, const char *const *cells, int32_t cellCount); +int32_t RunCreate(int32_t mdl, int32_t src, const char *appDir, const char *args); +static void runDestroyHost(DevRunT *run); +static void runEnd(DevRunT *run); +const char *RunError(int32_t run); +int32_t RunErrorLine(int32_t run); +static void runFinal(DevRunT *run); +void RunFree(int32_t run); +static bool runOnStack(const DevRunT *run); +const char *RunOutput(int32_t run); +static DevRunT *runPaused(int32_t handle); +int32_t RunPoll(int32_t run, int32_t ms); +static void runResume(DevRunT *run); +static void runSlices(DevRunT *run, int32_t ms); +int32_t RunStart(int32_t run, const char *startupForm, int32_t breakOnStart); +int32_t RunState(int32_t run); +void RunStop(int32_t run); +static int32_t stateOf(const DevRunT *run); +static void sweepDeferred(void); +static void textAppend(const char *s, int32_t len); +static const char *textOf(const char *s); +static void textReset(void); +static void tokenRow(void *ctx, BasSrcKindE kind, int32_t line, int32_t col, int32_t pos, int32_t len); +static void withEventsRow(void *ctx, const char *name, const char *className); + + +// Shell app-cleanup hook: free what a program that ended left open (its +// code has unwound, so nothing of it is on the stack). Its runtime let +// go of its design controls when it ended (basUcRuntimeEnd), so their +// modules -- its own, CompDesignControls takes no other -- are free to go. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + if (sHandles[i].obj && sHandles[i].appId == appId) { + if (sHandles[i].kind == DevModuleE) { + ((DevModuleT *)sHandles[i].obj)->designer = NULL; + } + + handleRelease(i); + } + } +} + + +// Append text as one cell of a row: a TAB or line break inside it +// becomes a blank so the row keeps its shape. +static void cellAppend(const char *text, int32_t len) { + for (int32_t i = 0; i < len; i++) { + char c = (text[i] == ROW_CELL_SEP || text[i] == ROW_END || text[i] == '\r') ? DEV_TEXT_BLANK : text[i]; + + textAppend(&c, 1); + } +} + + +static void classEventRow(void *ctx, const char *name, const char *params) { + const char *cells[] = { name, params }; + + rowAdd(ctx, cells, DVX_ARRAY_LEN(cells)); +} + + +static void collectAppend(DevCaptureT *cap, const char *text, int32_t len) { + cap->len = basDbgAppendDropping(cap->buf, cap->cap, cap->len, text, len); +} + + +static void collectPrint(void *ctx, const char *text, bool newline) { + DevCaptureT *cap = (DevCaptureT *)ctx; + + if (text) { + collectAppend(cap, text, (int32_t)strlen(text)); + } + + if (newline) { + collectAppend(cap, "\n", 1); + } +} + + +int32_t CompBuildApp(int32_t mdl, int32_t src, const char *outPath, int32_t release) { + DevModuleT *m = (DevModuleT *)handleOf(mdl, DevModuleE); + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + PrefsHandleT *prefs = NULL; + char dir[DVX_MAX_PATH]; + BasBuildSpecT spec; + + if (!m || !s || !outPath || !outPath[0]) { + compFail("No compiled program, source set or output file."); + return BAS_FALSE; + } + + memset(&spec, 0, sizeof(spec)); + + // A project's metadata (name, icon, help file ...) as bascomp reads it + if (s->dbpPath[0] && basBuildOpenProject(s->dbpPath, &prefs, dir, sizeof(dir))) { + basBuildSpecFromProject(prefs, s->projectDir, &spec); + } else { + spec.projName = s->name; + spec.projectDir = s->projectDir; + } + + spec.module = m->mod; + spec.frmSources = (const char *const *)s->ps.frmTexts; + spec.frmCount = (int32_t)arrlen(s->ps.frmTexts); + spec.ctlSources = (const char *const *)s->ps.ctlTexts; + spec.ctlCount = (int32_t)arrlen(s->ps.ctlTexts); + spec.release = release != 0; + + // A control project builds a control file (.wgt), a program an .app + const char *failure = (prefs && basBuildIsControlProject(prefs)) ? basBuildControl(outPath, &spec) : basBuildApp(outPath, &spec); + + if (prefs) { + prefsClose(prefs); + } + + if (failure) { + compFail(failure); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + +// One row per Event a class module declares: name, parameter list. +const char *CompClassEvents(const char *classText) { + textReset(); + basSourceClassEvents(classText ? classText : "", classEventRow, NULL); + return sText ? sText : ""; +} + + +int32_t CompCompile(int32_t src, int32_t optionExplicit, int32_t validate) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + + compFail(""); + sCompErrorLine = 0; + sCompErrorCol = 0; + sCompFileLine = 0; + sCompErrorFile[0] = '\0'; + + if (!s || s->ps.sourceLen == 0) { + compFail("Nothing to compile."); + return BAS_NO_HANDLE; + } + + BasValidatorT v; + BasCtrlValidatorT cb; + + memset(&v, 0, sizeof(v)); + basValidateCallbacks(&v, &cb); + + if (validate) { + // The program's own UserControls are control types of its forms + for (int32_t i = 0; i < (int32_t)arrlen(s->ps.ctlTexts); i++) { + char name[BAS_MAX_IDENT]; + + if (basExtractCtlName(s->ps.ctlTexts[i], (int32_t)strlen(s->ps.ctlTexts[i]), name, (int32_t)sizeof(name))) { + basValidateAddType(&v, name); + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(s->ps.frmTexts); i++) { + basValidateScanFrm(&v, s->ps.frmTexts[i], (int32_t)strlen(s->ps.frmTexts[i]), NULL, NULL); + } + } + + // A control type no loaded widget library knows fails at once + if (arrlen(v.badTypes) > 0) { + char line[DEV_MESSAGE_LEN]; + + arrsetlen(sCompError, 0); + + for (int32_t i = 0; i < (int32_t)arrlen(v.badTypes); i++) { + const BasBadTypeT *b = &v.badTypes[i]; + + snprintf(line, sizeof(line), BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName); + compErrorAppend(line); + } + + compErrorAppend(BAS_VALIDATE_BAD_TYPE_HINT); + arrput(sCompError, '\0'); + basValidateFree(&v); + return BAS_NO_HANDLE; + } + + BasCompileErrorT err; + BasModuleT *mod = basBuildCompile(s->ps.source, s->ps.sourceLen, optionExplicit || s->ps.optionExplicit, validate ? &cb : NULL, &v.compiler, &err); + + basValidateFree(&v); + + if (!mod) { + const char *path = NULL; + + compFail(basBuildErrorText(err.message)); + sCompErrorLine = err.line; + sCompErrorCol = err.col; + + if (err.line > 0 && basBuildMapLine(s->ps.map, err.line, &path, &sCompFileLine)) { + snprintf(sCompErrorFile, sizeof(sCompErrorFile), "%s", path); + } + + return BAS_NO_HANDLE; + } + + DevModuleT *m = (DevModuleT *)calloc(1, sizeof(DevModuleT)); + int32_t h = m ? handleNew(DevModuleE, m) : BAS_NO_HANDLE; + + if (h == BAS_NO_HANDLE) { + free(m); + basModuleFree(mod); + compFail("Out of memory."); + return BAS_NO_HANDLE; + } + + m->mod = mod; + return h; +} + + +int32_t CompConcatLine(int32_t src, const char *path, int32_t fileLine) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + + return s ? basBuildMapFileLine(s->ps.map, path, fileLine) : 0; +} + + +// The UserControls of source set src (its .ctl texts), classes of module +// mdl, registered for the calling program's design forms in place of the +// ones it registered before, as the DVX BASIC IDE's designer registers a +// project's (basCtlLoadDesign): their code runs without the module-level +// code, App.Path appDir (else the project's folder, else the caller's). +// The module stays until they go; it must be the calling program's own +// (another program's would be freed when that one ends). The number +// registered; CompError$ says why when not every control was. +int32_t CompDesignControls(int32_t mdl, int32_t src, const char *appDir) { + DevModuleT *m = (DevModuleT *)handleOf(mdl, DevModuleE); + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + BasFormRtT *owner = basFormRtCurrent(); + + compFail(""); + + if (!m || m->freeRequested || !s || !owner || sHandles[mdl - 1].appId != currentAppId()) { + compFail("No compiled program or source set."); + return 0; + } + + if (!designRelease(owner)) { + compFail(DEV_DESIGN_BUSY); + return 0; + } + + char path[DVX_MAX_PATH]; + int32_t count = (int32_t)arrlen(s->ps.ctlTexts); + + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", devAppDir(s, appDir, owner), devTitle(s)); + m->designer = owner; + + int32_t added = basCtlLoadDesign(owner, m->mod, (const char *const *)s->ps.ctlTexts, count, path); + + if (added < count) { + compFail("A UserControl of the set is not one of the module's, or a control type of its name is installed already."); + } + + return added; +} + + +// The calling program's design controls (CompDesignControls) let go. +// False while their code runs. +int32_t CompDesignRelease(void) { + BasFormRtT *owner = basFormRtCurrent(); + + compFail(""); + + if (owner && !designRelease(owner)) { + compFail(DEV_DESIGN_BUSY); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + +const char *CompError(void) { + return sCompError ? sCompError : ""; +} + + +static void compErrorAppend(const char *s) { + for (const char *c = s; *c; c++) { + arrput(sCompError, *c); + } +} + + +int32_t CompErrorCol(void) { + return sCompErrorCol; +} + + +const char *CompErrorFile(void) { + return sCompErrorFile; +} + + +int32_t CompErrorFileLine(void) { + return sCompFileLine; +} + + +int32_t CompErrorLine(void) { + return sCompErrorLine; +} + + +// Record why a Comp* call failed. +static void compFail(const char *msg) { + arrsetlen(sCompError, 0); + compErrorAppend(msg); + arrput(sCompError, '\0'); +} + + +const char *CompHelpCtrlTopic(const char *typeName) { + char topic[BAS_MAX_IDENT + sizeof(BAS_HELP_CTRL_PREFIX)]; + + basHelpCtrlTopic(typeName ? typeName : "", topic, (int32_t)sizeof(topic)); + return textOf(topic); +} + + +const char *CompHelpTopic(const char *word) { + const char *topic = basHelpKeywordTopic(word); + + return topic ? topic : ""; +} + + +// position is 1-based, as INSTR and MID$ count. +int32_t CompInCode(const char *source, int32_t position) { + const char *src = source ? source : ""; + + return basSourceInCode(src, (int32_t)strlen(src), position - 1) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t CompIsIdent(const char *name) { + return basIsValidIdent(name) ? BAS_TRUE : BAS_FALSE; +} + + +const char *CompKeyword(int32_t idx) { + return (idx >= 0 && idx < basLexerKeywordCount()) ? basLexerKeywordAt(idx) : ""; +} + + +// The keyword's BasKeywordClassE (compiler.bas COMP_KW_*), -1 for no such +// keyword. +int32_t CompKeywordClass(int32_t idx) { + return (idx >= 0 && idx < basLexerKeywordCount()) ? (int32_t)basLexerKeywordClass(idx) : DEV_NO_INDEX; +} + + +int32_t CompKeywordCount(void) { + return basLexerKeywordCount(); +} + + +int32_t CompKeywordReserved(int32_t idx) { + return (idx >= 0 && idx < basLexerKeywordCount() && basLexerKeywordReserved(idx)) ? BAS_TRUE : BAS_FALSE; +} + + +const char *CompMapFile(int32_t src, int32_t line) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + const char *path = NULL; + int32_t fileLine = 0; + + return (s && basBuildMapLine(s->ps.map, line, &path, &fileLine)) ? textOf(path) : ""; +} + + +int32_t CompMapLine(int32_t src, int32_t line) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + const char *path = NULL; + int32_t fileLine = 0; + + return (s && basBuildMapLine(s->ps.map, line, &path, &fileLine)) ? fileLine : 0; +} + + +void CompModuleFree(int32_t mdl) { + if (handleOf(mdl, DevModuleE)) { + handleRelease(mdl - 1); + } +} + + +// One row per SUB / FUNCTION block of source: kind, name, first line, +// last line (1-based). +const char *CompProcList(const char *source) { + const char *pos = source ? source : ""; + int32_t line = 1; + + textReset(); + + while (*pos) { + bool isSub = false; + const char *afterKw = basProcDeclAt(dvxSkipWs(pos), &isSub); + const char *next = strchr(pos, '\n'); + + if (afterKw) { + char name[BAS_MAX_IDENT]; + char row[DEV_MESSAGE_LEN]; + int32_t endOffset = 0; + const char *end = basProcSkipToEnd(pos, isSub, &endOffset); + + basProcNameFromDecl(afterKw, name, sizeof(name)); + + int32_t n = snprintf(row, sizeof(row), "%s%c%s%c%" PRId32 "%c%" PRId32 "%c", isSub ? DEV_KIND_SUB : DEV_KIND_FUNCTION, ROW_CELL_SEP, name, ROW_CELL_SEP, line, ROW_CELL_SEP, line + endOffset, ROW_END); + + textAppend(row, n); + line += endOffset + 1; + pos = end; + continue; + } + + pos = next ? next + 1 : pos + strlen(pos); + line++; + } + + return sText ? sText : ""; +} + + +// One row per compiled SUB / FUNCTION: name, kind, owning form. +const char *CompProcs(int32_t mdl) { + DevModuleT *m = (DevModuleT *)handleOf(mdl, DevModuleE); + + textReset(); + + for (int32_t i = 0; m && i < m->mod->procCount; i++) { + const BasProcEntryT *p = &m->mod->procs[i]; + char row[DEV_MESSAGE_LEN]; + int32_t n = snprintf(row, sizeof(row), "%s%c%s%c%s%c", p->name, ROW_CELL_SEP, p->isFunction ? DEV_KIND_FUNCTION : DEV_KIND_SUB, ROW_CELL_SEP, p->formName, ROW_END); + + textAppend(row, n); + } + + return sText ? sText : ""; +} + + +// source with the form or control oldName renamed (the text unchanged +// when nothing refers to it). +const char *CompRenameInCode(const char *source, const char *oldName, const char *newName, int32_t codeOnly) { + const char *src = source ? source : ""; + char *renamed = basSourceRename(src, oldName, newName, codeOnly != 0); + + if (!renamed) { + return textOf(src); + } + + const char *text = textOf(renamed); + + free(renamed); + return text; +} + + +int32_t CompSourceAdd(int32_t src, const char *path, const char *text, int32_t kind) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + + if (!s || !path || !text) { + compFail("No source set."); + return BAS_FALSE; + } + + BasBuildWrapE wrap = (kind == DEV_KIND_AUTO) ? basBuildWrapForPath(path) : (BasBuildWrapE)kind; + + if (wrap < BAS_BUILD_WRAP_NONE || wrap > BAS_BUILD_WRAP_CONTROL) { + compFail("Unknown kind of source file."); + return BAS_FALSE; + } + + if (!basBuildAddSource(&s->ps, path, text, (int32_t)strlen(text), wrap)) { + compFail(s->ps.error); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + +int32_t CompSourceCreate(void) { + DevSourceT *s = (DevSourceT *)calloc(1, sizeof(DevSourceT)); + int32_t h = s ? handleNew(DevSourceE, s) : BAS_NO_HANDLE; + + if (h == BAS_NO_HANDLE) { + free(s); + return BAS_NO_HANDLE; + } + + snprintf(s->projectDir, sizeof(s->projectDir), "."); + return h; +} + + +// The files of the set in compile order, one row each: the path as the +// source map names it, the first line of its code in the file, and the +// line of the concatenated source that line is. +const char *CompSourceFiles(int32_t src) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + + textReset(); + + for (int32_t i = 0; s && i < (int32_t)arrlen(s->ps.map); i++) { + const BasSourceMapT *m = &s->ps.map[i]; + char row[DVX_MAX_PATH + DEV_MESSAGE_LEN]; + int32_t n = snprintf(row, sizeof(row), "%s%c%" PRId32 "%c%" PRId32 "%c", m->path, ROW_CELL_SEP, m->firstFileLine, ROW_CELL_SEP, m->startLine, ROW_END); + + textAppend(row, n); + } + + return sText ? sText : ""; +} + + +void CompSourceFree(int32_t src) { + if (handleOf(src, DevSourceE)) { + handleRelease(src - 1); + } +} + + +// Replaces the set with the project dbpPath names, read from disk. +int32_t CompSourceFromProject(int32_t src, const char *dbpPath) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + PrefsHandleT *prefs = NULL; + + if (!s || !dbpPath || !basBuildOpenProject(dbpPath, &prefs, s->projectDir, sizeof(s->projectDir))) { + compFail("Cannot open the project file."); + return BAS_FALSE; + } + + basBuildFreeProject(&s->ps); + + bool ok = basBuildConcatProject(prefs, s->projectDir, &s->ps); + + snprintf(s->dbpPath, sizeof(s->dbpPath), "%s", dbpPath); + snprintf(s->name, sizeof(s->name), "%s", prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, "")); + snprintf(s->startupForm, sizeof(s->startupForm), "%s", prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, "")); + snprintf(s->helpFile, sizeof(s->helpFile), "%s", prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "")); + prefsClose(prefs); + + if (!ok) { + compFail(s->ps.error); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + +const char *CompSourceText(int32_t src) { + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + + return (s && s->ps.source) ? s->ps.source : ""; +} + + +// One row per token of source: line, column, length, kind, text. +const char *CompTokens(const char *source) { + DevTokensT tokens; + + tokens.src = source ? source : ""; + textReset(); + basSourceTokens(tokens.src, (int32_t)strlen(tokens.src), tokenRow, &tokens); + return sText ? sText : ""; +} + + +// One row per WithEvents variable of source: name, class. +const char *CompWithEvents(const char *source) { + textReset(); + basSourceWithEvents(source ? source : "", withEventsRow, NULL); + return sText ? sText : ""; +} + + +// The app the calling code belongs to (0 outside DVX's app tracking): the +// owner of the handles it makes. +static int32_t currentAppId(void) { + return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; +} + + +const char *DbgCallStack(int32_t run) { + DevRunT *r = runPaused(run); + + textReset(); + + if (r) { + basDbgCallStack(r->rt->vm, r->pausedLine, rowAdd, NULL); + } + + return sText ? sText : ""; +} + + +void DbgContinue(int32_t run) { + DevRunT *r = runPaused(run); + + if (r) { + basVmClearStepState(r->rt->vm); + runResume(r); + } +} + + +const char *DbgEval(int32_t run, const char *expr) { + DevRunT *r = runPaused(run); + char out[BAS_DBG_VALUE_LEN]; + + if (!r || !expr) { + return ""; + } + + return textOf(basDbgEval(r->rt->vm, expr, out, sizeof(out)) ? out : BAS_DBG_EVAL_ERROR); +} + + +const char *DbgExec(int32_t run, const char *line) { + DevRunT *r = runPaused(run); + char out[BAS_DBG_VALUE_LEN]; + DevCaptureT cap; + + cap.buf = out; + cap.cap = sizeof(out); + cap.len = 0; + out[0] = '\0'; + basDbgExec(r ? r->rt->vm : NULL, line ? line : "", collectPrint, &cap); + return textOf(out); +} + + +int32_t DbgLine(int32_t run) { + DevRunT *r = runPaused(run); + + return r ? r->pausedLine : 0; +} + + +const char *DbgLocals(int32_t run) { + DevRunT *r = runPaused(run); + + textReset(); + + if (r) { + basDbgLocals(r->rt->vm, rowAdd, NULL); + } + + return sText ? sText : ""; +} + + +const char *DbgMessage(void) { + return sDbgMessage; +} + + +// Break at the next line the program runs. +void DbgPause(int32_t run) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + if (r && r->rt && !r->paused) { + basVmStepInto(r->rt->vm); + } +} + + +const char *DbgProc(int32_t run) { + DevRunT *r = runPaused(run); + + if (!r) { + return ""; + } + + int32_t idx = basDbgCurProcIdx(r->rt->vm); + + return idx >= 0 ? r->rt->vm->module->procs[idx].name : BAS_DBG_MODULE_FRAME; +} + + +void DbgRunToLine(int32_t run, int32_t line) { + DevRunT *r = runPaused(run); + + if (r) { + basVmRunToCursor(r->rt->vm, line); + runResume(r); + } +} + + +// The breakpoints: concatenated line numbers separated by commas, blanks +// or new lines ("" removes them all). +void DbgSetBreakpoints(int32_t run, const char *lines) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + if (!r) { + return; + } + + arrsetlen(r->breakpoints, 0); + + for (const char *p = lines ? lines : ""; *p; ) { + char *end = NULL; + int32_t line = (int32_t)strtol(p, &end, DEV_DECIMAL); + + if (end == p) { + p++; + continue; + } + + // Kept sorted as they come in (the VM searches them) + if (line > 0) { + int32_t at = (int32_t)arrlen(r->breakpoints); + + while (at > 0 && r->breakpoints[at - 1] > line) { + at--; + } + + arrins(r->breakpoints, at, line); + } + + p = end; + } + + // The VM keeps the array, so it learns every change of it + if (r->rt) { + basVmSetBreakpoints(r->rt->vm, r->breakpoints, (int32_t)arrlen(r->breakpoints)); + } +} + + +int32_t DbgSetVar(int32_t run, const char *path, const char *expr) { + DevRunT *r = runPaused(run); + DevCaptureT cap; + + cap.buf = sDbgMessage; + cap.cap = sizeof(sDbgMessage); + cap.len = 0; + sDbgMessage[0] = '\0'; + + bool ok = basDbgSetVar(r ? r->rt->vm : NULL, path ? path : "", expr ? expr : "", collectPrint, &cap); + + dvxTrimRight(sDbgMessage); + return ok ? BAS_TRUE : BAS_FALSE; +} + + +void DbgStepInto(int32_t run) { + DevRunT *r = runPaused(run); + + if (r) { + basVmStepInto(r->rt->vm); + runResume(r); + } +} + + +void DbgStepOut(int32_t run) { + DevRunT *r = runPaused(run); + + if (r) { + basVmStepOut(r->rt->vm); + runResume(r); + } +} + + +void DbgStepOver(int32_t run) { + DevRunT *r = runPaused(run); + + if (r) { + basVmStepOver(r->rt->vm); + runResume(r); + } +} + + +// Designer owner's UserControls let go (basCtlUnloadDesign), and the +// module they ran given back (freed when CompModuleFree came meanwhile). +// False, nothing changed, while their code runs. +static bool designRelease(const BasFormRtT *owner) { + if (!basCtlUnloadDesign(owner)) { + return false; + } + + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + DevModuleT *m = (sHandles[i].obj && sHandles[i].kind == DevModuleE) ? (DevModuleT *)sHandles[i].obj : NULL; + + if (m && m->designer == owner) { + m->designer = NULL; + moduleRelease(m); + } + } + + return true; +} + + +// The App.Path of source set s's code run for program rt (RunCreate, +// CompDesignControls): appDir when given, else the project's folder, else +// rt's own. +static const char *devAppDir(const DevSourceT *s, const char *appDir, const BasFormRtT *rt) { + return (appDir && appDir[0]) ? appDir : (s && s->dbpPath[0]) ? s->projectDir : rt->vm->appPath; +} + + +// The child's DoEvents (and its pause loop inside a handler): pump DVX so +// the caller's own events -- the Timer that steps or continues -- run. +// False ends the child's code: it was stopped or freed, DVX is shutting +// down, or the caller is idle with no form left open. +static bool devDoEvents(void *ctx) { + DevRunT *run = (DevRunT *)ctx; + AppContextT *ac = run->rt->ctx; + + if (run->stopRequested || !ac->running) { + return false; + } + + if (run->parentRt && run->parentRt->eventDepth == 0 && !basFormRtKeepsRunning(run->parentRt)) { + return false; + } + + run->busy++; + bool ok = dvxUpdate(ac); + run->busy--; + + return ok && !run->stopRequested; +} + + +// INPUT: what was printed so far, with the prompt, is in RunOutput$; the +// answer comes from an input box. +static bool devInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) { + DevRunT *run = (DevRunT *)ctx; + + if (prompt) { + devPrint(run, prompt, false); + } + + return dvxInputBox(run->rt->ctx, run->title, prompt ? prompt : DEV_INPUT_PROMPT, NULL, buf, bufSize); +} + + +// A breakpoint or step paused the child (line -1: a handler that paused +// has returned, not a pause). +static void devOnBreak(void *ctx, int32_t line) { + DevRunT *run = (DevRunT *)ctx; + + if (line < 0 || run->stopRequested) { + return; + } + + run->paused = true; + run->pausedLine = line; + basVmSetDebugPaused(run->rt->vm, true); + basRunHostTagBreak(run->rt, &run->breakTitles, true); +} + + +static void devPrint(void *ctx, const char *text, bool newline) { + DevRunT *run = (DevRunT *)ctx; + + if (text) { + run->outputLen = basDbgAppendDropping(run->output, DEV_OUTPUT_MAX, run->outputLen, text, (int32_t)strlen(text)); + } + + if (newline) { + run->outputLen = basDbgAppendDropping(run->output, DEV_OUTPUT_MAX, run->outputLen, "\n", 1); + } +} + + +// The name of source set s's program: its project's, else the default. +static const char *devTitle(const DevSourceT *s) { + return (s && s->name[0]) ? s->name : BAS_DEFAULT_APP_NAME; +} + + +static int32_t handleNew(DevKindE kind, void *obj) { + if (!sCleanupHooked) { + DevCleanupRegFnT reg = (DevCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sHandles) && sHandles[idx].obj) { + idx++; + } + + if (idx == (int32_t)arrlen(sHandles)) { + DevHandleT blank; + + memset(&blank, 0, sizeof(blank)); + arrput(sHandles, blank); + + if (idx == (int32_t)arrlen(sHandles)) { + return BAS_NO_HANDLE; + } + } + + sHandles[idx].obj = obj; + sHandles[idx].kind = (uint8_t)kind; + sHandles[idx].appId = currentAppId(); + return idx + 1; +} + + +static void *handleOf(int32_t handle, DevKindE kind) { + if (handle <= BAS_NO_HANDLE || handle > (int32_t)arrlen(sHandles)) { + return NULL; + } + + DevHandleT *h = &sHandles[handle - 1]; + + return (h->obj && h->kind == kind) ? h->obj : NULL; +} + + +// Free handle slot idx and what it holds (a run whose code is on the +// stack is stopped and freed later by sweepDeferred). +static void handleRelease(int32_t idx) { + DevHandleT *h = &sHandles[idx]; + + switch (h->kind) { + case DevSourceE: { + DevSourceT *s = (DevSourceT *)h->obj; + + basBuildFreeProject(&s->ps); + free(s); + break; + } + + case DevProjectE: { + PrjStateT *p = (PrjStateT *)h->obj; + + prjClose(p); + free(p); + break; + } + + case DevModuleE: { + DevModuleT *m = (DevModuleT *)h->obj; + + // A run still executing it, or its controls on design forms, + // keep it until they end + if (m->runs > 0 || m->designer) { + m->freeRequested = true; + return; + } + + basModuleFree(m->mod); + free(m); + break; + } + + default: { + DevRunT *run = (DevRunT *)h->obj; + + run->freeRequested = true; + run->stopRequested = true; + + if (runOnStack(run)) { + if (run->rt) { + basVmStop(run->rt->vm); + } + + return; + } + + runEnd(run); + arrfree(run->breakpoints); + arrfree(run->breakTitles); + free(run->output); + free(run); + break; + } + } + + h->obj = NULL; +} + + +// A run or a designer let go of a module: freed once nothing needs it +// and CompModuleFree asked for it (one module handle holds it, so the +// search ends there and never compares the freed pointer). +static void moduleRelease(DevModuleT *m) { + if (m->runs > 0 || m->designer || !m->freeRequested) { + return; + } + + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + if (sHandles[i].kind == DevModuleE && sHandles[i].obj == m) { + handleRelease(i); + return; + } + } +} + + +// Add fileName (in the project's folder; its kind from the extension). +// Its index, or -1 when it is already in the project. +int32_t ProjAddFile(int32_t prj, const char *fileName) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + if (!p || !fileName || !fileName[0]) { + compFail("No project or file name."); + return DEV_NO_INDEX; + } + + if (prjFileIndexByPath(p, fileName) >= 0) { + compFail("The file is already in the project."); + return DEV_NO_INDEX; + } + + return prjAddFile(p, fileName, prjKindForPath(fileName)); +} + + +void ProjClose(int32_t prj) { + if (handleOf(prj, DevProjectE)) { + handleRelease(prj - 1); + } +} + + +// The value of a header key of a UserControl's text ("" when the text +// has no header or the key is none). +const char *ProjCtlHeader(const char *text, const char *key) { + char value[BAS_UC_TEXT_MAX] = ""; + + prjCtlHeaderGet(text, key, value, (int32_t)sizeof(value)); + return textOf(value); +} + + +// One row per UserControl header key in the order the IDE's Properties +// window lists them (prjCtlKeyAt): the key and the kind of value it takes. +const char *ProjCtlKeys(void) { + static const char *const sKinds[] = { + [PrjCtlNameE] = "Name", + [PrjCtlIntE] = "Integer", + [PrjCtlBoolE] = "Boolean", + [PrjCtlColorE] = "Color", + [PrjCtlTextE] = "Text" + }; + const PrjCtlKeyT *k; + + textReset(); + + for (int32_t i = 0; (k = prjCtlKeyAt(i)) != NULL; i++) { + const char *cells[] = { k->key, sKinds[k->kind] }; + + rowAdd(NULL, cells, DVX_ARRAY_LEN(cells)); + } + + return sText ? sText : ""; +} + + +// The text of a new UserControl called name ("" with CompError$ set when +// the name is no identifier). +const char *ProjCtlNew(const char *name) { + char *text = prjCtlNew(name); + + if (!text) { + compFail("A UserControl's name must be an identifier."); + return textOf(""); + } + + compFail(""); + + const char *out = textOf(text); + + free(text); + return out; +} + + +// The UserControl's text with a header key set ("" with CompError$ set +// when the key or value does not suit). +const char *ProjCtlSetHeader(const char *text, const char *key, const char *value) { + char *changed = (text && value) ? prjCtlHeaderSet(text, key, value) : NULL; + + if (!changed) { + compFail("Not a UserControl header key, or a value it does not take."); + return textOf(""); + } + + compFail(""); + + const char *out = textOf(changed); + + free(changed); + return out; +} + + +// The file's path as the project lists it (relative to its folder). +const char *ProjFile(int32_t prj, int32_t idx) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + return (p && idx >= 0 && idx < prjFileCount(p)) ? p->files[idx].path : ""; +} + + +int32_t ProjFileCount(int32_t prj) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + return p ? prjFileCount(p) : 0; +} + + +int32_t ProjFileFind(int32_t prj, const char *fileName) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + return (p && fileName) ? prjFileIndexByPath(p, fileName) : DEV_NO_INDEX; +} + + +// COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or COMP_KIND_CONTROL; +// -1 for no such file. +int32_t ProjFileKind(int32_t prj, int32_t idx) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + return (p && idx >= 0 && idx < prjFileCount(p)) ? (int32_t)prjKindWrap((PrjFileKindE)p->files[idx].kind) : DEV_NO_INDEX; +} + + +const char *ProjFilePath(int32_t prj, int32_t idx) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + char path[DVX_MAX_PATH] = ""; + + if (p) { + prjFullPath(p, idx, path, (int32_t)sizeof(path)); + } + + return textOf(path); +} + + +// A [Project] / [Settings] value by its .dbp key ("" for an unknown key). +const char *ProjGet(int32_t prj, const char *key) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + char value[PRJ_MAX_FIELD] = ""; + + if (p) { + prjFieldGet(p, key, value, (int32_t)sizeof(value)); + } + + return textOf(value); +} + + +// A new project handle holding p (freed when no handle can be made). +static int32_t projHandle(PrjStateT *p) { + int32_t h = p ? handleNew(DevProjectE, p) : BAS_NO_HANDLE; + + if (h == BAS_NO_HANDLE) { + if (p) { + prjClose(p); + } + + free(p); + compFail("Out of memory."); + } + + return h; +} + + +// A new project called name in folder (its .dbp, name.dbp in folder, is +// written by ProjSave). +int32_t ProjNew(const char *name, const char *folder) { + if (!name || !name[0] || !folder) { + compFail("A project needs a name and a folder."); + return BAS_NO_HANDLE; + } + + PrjStateT *p = (PrjStateT *)malloc(sizeof(PrjStateT)); + + if (p) { + prjNew(p, name, folder[0] ? folder : "."); + } + + return projHandle(p); +} + + +int32_t ProjOpen(const char *dbpPath) { + PrjStateT *p = (PrjStateT *)malloc(sizeof(PrjStateT)); + + if (!p) { + compFail("Out of memory."); + return BAS_NO_HANDLE; + } + + prjInit(p); + + if (!dbpPath || !prjLoad(p, dbpPath)) { + free(p); + compFail("Cannot open the project file."); + return BAS_NO_HANDLE; + } + + return projHandle(p); +} + + +// The project file's (.dbp) path. +const char *ProjPath(int32_t prj) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + return p ? p->projectPath : ""; +} + + +// Remove file idx from the project (the file itself stays on disk). +int32_t ProjRemoveFile(int32_t prj, int32_t idx) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + if (!p || idx < 0 || idx >= prjFileCount(p)) { + return BAS_FALSE; + } + + prjRemoveFile(p, idx); + return BAS_TRUE; +} + + +// Rename file idx on disk and in the project (the .dbp is saved at +// once). False with CompError$ saying why. +int32_t ProjRenameFile(int32_t prj, int32_t idx, const char *newName) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + char err[DEV_MESSAGE_LEN]; + + if (!p || idx < 0 || idx >= prjFileCount(p) || !newName) { + compFail("No such project file."); + return BAS_FALSE; + } + + if (!prjRenameFile(p, idx, newName, err, (int32_t)sizeof(err))) { + compFail(err); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + +int32_t ProjSave(int32_t prj) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + if (!p || !prjSave(p)) { + compFail("Cannot write the project file."); + return BAS_FALSE; + } + + p->dirty = false; + return BAS_TRUE; +} + + +// Set a value by its .dbp key. False for an unknown key. +int32_t ProjSet(int32_t prj, const char *key, const char *value) { + PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE); + + return (p && prjFieldSet(p, key, value ? value : "")) ? BAS_TRUE : BAS_FALSE; +} + + +static void rowAdd(void *ctx, const char *const *cells, int32_t cellCount) { + (void)ctx; + char sep = ROW_CELL_SEP; + char end = ROW_END; + + for (int32_t c = 0; c < cellCount; c++) { + if (c > 0) { + textAppend(&sep, 1); + } + + textAppend(cells[c], (int32_t)strlen(cells[c])); + } + + textAppend(&end, 1); +} + + +int32_t RunCreate(int32_t mdl, int32_t src, const char *appDir, const char *args) { + DevModuleT *m = (DevModuleT *)handleOf(mdl, DevModuleE); + DevSourceT *s = (DevSourceT *)handleOf(src, DevSourceE); + BasFormRtT *parent = basFormRtCurrent(); + + sweepDeferred(); + sDbgMessage[0] = '\0'; + + if (!m || m->freeRequested || !parent) { + snprintf(sDbgMessage, sizeof(sDbgMessage), "No compiled program to run."); + return BAS_NO_HANDLE; + } + + DevRunT *run = (DevRunT *)calloc(1, sizeof(DevRunT)); + char *output = (char *)malloc(DEV_OUTPUT_MAX); + + if (!run || !output) { + free(run); + free(output); + snprintf(sDbgMessage, sizeof(sDbgMessage), "Out of memory."); + return BAS_NO_HANDLE; + } + + char helpPath[DVX_MAX_PATH] = ""; + const char *hostError = NULL; + BasRunSpecT spec; + + run->output = output; + run->output[0] = '\0'; + run->parentRt = parent; + run->module = mdl; + run->phase = PhaseReadyE; + snprintf(run->title, sizeof(run->title), "%s", devTitle(s)); + snprintf(run->startupForm, sizeof(run->startupForm), "%s", s ? s->startupForm : ""); + + if (s && s->helpFile[0]) { + snprintf(helpPath, sizeof(helpPath), "%s" DVX_PATH_SEP "%s", s->projectDir, s->helpFile); + } + + // App.Path: the folder given, else the project's, else the caller's + memset(&spec, 0, sizeof(spec)); + spec.appDir = devAppDir(s, appDir, parent); + spec.args = args; + spec.title = run->title; + spec.helpFile = helpPath[0] ? helpPath : NULL; + spec.printFn = devPrint; + spec.printCtx = run; + spec.inputFn = devInput; + spec.inputCtx = run; + spec.doEventsFn = devDoEvents; + spec.doEventsCtx = run; + spec.suppressErrorDialog = true; + + run->rt = basRunHostCreate(parent->ctx, m->mod, &spec, &hostError); + + int32_t h = run->rt ? handleNew(DevRunE, run) : BAS_NO_HANDLE; + + if (h == BAS_NO_HANDLE) { + if (run->rt) { + basRunHostDestroy(run->rt); + } + + snprintf(sDbgMessage, sizeof(sDbgMessage), "%s", hostError ? hostError : "Out of memory."); + free(run->output); + free(run); + return BAS_NO_HANDLE; + } + + m->runs++; + basVmSetBreakpointCallback(run->rt->vm, devOnBreak, run); + + for (int32_t i = 0; s && i < (int32_t)arrlen(s->ps.frmTexts); i++) { + basRunHostRegisterFrm(run->rt, s->ps.frmTexts[i], (int32_t)strlen(s->ps.frmTexts[i])); + } + + // Its own UserControls, before its forms load + for (int32_t i = 0; s && i < (int32_t)arrlen(s->ps.ctlTexts); i++) { + basFormRtRegisterCtl(run->rt, s->ps.ctlTexts[i], (int32_t)strlen(s->ps.ctlTexts[i])); + } + + return h; +} + + +// Destroy the child's VM and form runtime (its windows close). +static void runDestroyHost(DevRunT *run) { + if (!run->rt) { + return; + } + + DevModuleT *m = (DevModuleT *)handleOf(run->module, DevModuleE); + + // Design controls the child made (CompDesignControls) go with it, and + // so does its hold on their module + designRelease(run->rt); + basRunHostTagBreak(run->rt, &run->breakTitles, false); + basRunHostDestroy(run->rt); + run->rt = NULL; + + if (m) { + m->runs--; + moduleRelease(m); + } +} + + +// The child stopped running: keep its error, close it. +static void runEnd(DevRunT *run) { + if (run->rt && run->rt->vm->errorMsg[0] && !run->stopRequested) { + snprintf(run->error, sizeof(run->error), "%s", run->rt->vm->errorMsg); + run->errorLine = basVmGetErrorLine(run->rt->vm); + run->phase = PhaseErrorE; + } else if (run->phase != PhaseErrorE) { + run->phase = PhaseEndedE; + } + + run->paused = false; + runDestroyHost(run); +} + + +const char *RunError(int32_t run) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + return r ? r->error : ""; +} + + +int32_t RunErrorLine(int32_t run) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + return r ? r->errorLine : 0; +} + + +// Module code and events are over: a normal end releases the module +// variables, so their objects' Class_Terminates run (VB). +static void runFinal(DevRunT *run) { + if (run->phase != PhaseFinalE && !run->stopRequested && !run->rt->terminated && basVmFinish(run->rt->vm)) { + run->phase = PhaseFinalE; + return; + } + + runEnd(run); +} + + +void RunFree(int32_t run) { + if (handleOf(run, DevRunE)) { + handleRelease(run - 1); + } + + sweepDeferred(); +} + + +// Whether the child's code is on the stack (a handler of it, or a call +// of this library into it). +static bool runOnStack(const DevRunT *run) { + return run->busy > 0 || (run->rt && run->rt->eventDepth > 0); +} + + +// What the program printed since the last call. +const char *RunOutput(int32_t run) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + if (!r) { + return ""; + } + + const char *text = textOf(r->output); + + r->output[0] = '\0'; + r->outputLen = 0; + return text; +} + + +// The run when it is paused in the debugger, else NULL. +static DevRunT *runPaused(int32_t handle) { + DevRunT *r = (DevRunT *)handleOf(handle, DevRunE); + + return (r && r->rt && r->paused) ? r : NULL; +} + + +int32_t RunPoll(int32_t run, int32_t ms) { + sweepDeferred(); + + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + if (!r) { + return RUN_ENDED; + } + + // Nothing to do while paused, not started or over; and nothing may + // run the child while its own code is on the stack (this call came + // from inside it) + if (!r->rt || r->paused || r->phase == PhaseReadyE || runOnStack(r)) { + return stateOf(r); + } + + if (r->stopRequested) { + runEnd(r); + } else if (r->phase == PhaseEventsE) { + // Waiting for events: the pump runs the child's handlers. It is + // over when no form is left, it ENDed or an error ended it. + if (!basRunHostAlive(r->rt)) { + if (r->rt->vm->errorMsg[0]) { + runEnd(r); + } else { + runFinal(r); + } + } + } else { + runSlices(r, ms); + } + + return stateOf(r); +} + + +// Leave the pause: the step state is set; a paused handler carries on. +static void runResume(DevRunT *run) { + run->paused = false; + run->pausedLine = 0; + basRunHostTagBreak(run->rt, &run->breakTitles, false); + basVmResume(run->rt->vm); +} + + +// Run module-level code (or the final Class_Terminates) for about ms +// milliseconds: at least one slice, never more once the time is up. +static void runSlices(DevRunT *run, int32_t ms) { + PlatformTicksT start = platformClock(); + + for (;;) { + BasVmT *vm = run->rt->vm; + + if (vm->errorMsg[0]) { + runEnd(run); + return; + } + + run->busy++; + BasVmResultE result = basRunHostSlice(run->rt); + run->busy--; + + if (run->stopRequested) { + runEnd(run); + return; + } + + if (result == BAS_VM_BREAKPOINT) { + devOnBreak(run, vm->currentLine > 0 ? vm->currentLine : 0); + return; + } + + if (result == BAS_VM_HALTED) { + if (run->phase == PhaseModuleE && basRunHostAlive(run->rt)) { + run->phase = PhaseEventsE; + } else { + runFinal(run); + } + + return; + } + + if (result != BAS_VM_STEP_LIMIT) { + runEnd(run); + return; + } + + if ((int32_t)PLATFORM_TICKS_TO_MS(platformClock() - start) >= ms) { + return; + } + } +} + + +int32_t RunStart(int32_t run, const char *startupForm, int32_t breakOnStart) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + if (!r || !r->rt || r->phase != PhaseReadyE) { + return BAS_FALSE; + } + + if (breakOnStart) { + basVmStepInto(r->rt->vm); + } + + // Forms first (their Load code may pause here), then module code in + // slices from RunPoll + r->phase = PhaseModuleE; + basVmSetStepLimit(r->rt->vm, BAS_VM_DEFAULT_STEP_SLICE); + r->busy++; + basRunHostLoadForms(r->rt, (startupForm && startupForm[0]) ? startupForm : r->startupForm); + r->busy--; + + if (r->stopRequested) { + runEnd(r); + } + + return BAS_TRUE; +} + + +int32_t RunState(int32_t run) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + return r ? stateOf(r) : RUN_ENDED; +} + + +void RunStop(int32_t run) { + DevRunT *r = (DevRunT *)handleOf(run, DevRunE); + + if (!r || !r->rt) { + return; + } + + r->stopRequested = true; + r->paused = false; + basVmStop(r->rt->vm); + basRunHostTagBreak(r->rt, &r->breakTitles, false); + + if (!runOnStack(r)) { + runEnd(r); + } +} + + +static int32_t stateOf(const DevRunT *run) { + if (run->paused) { + return RUN_PAUSED; + } + + switch (run->phase) { + case PhaseReadyE: + return RUN_READY; + + case PhaseModuleE: + case PhaseFinalE: + return RUN_RUNNING; + + case PhaseEventsE: + return RUN_EVENTS; + + case PhaseErrorE: + return RUN_ERROR; + + default: + return RUN_ENDED; + } +} + + +// Finish what had to wait until a child's code left the stack: stopped +// runs end, freed runs are released. +static void sweepDeferred(void) { + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + DevRunT *run = (sHandles[i].obj && sHandles[i].kind == DevRunE) ? (DevRunT *)sHandles[i].obj : NULL; + + if (!run || runOnStack(run)) { + continue; + } + + if (run->freeRequested) { + handleRelease(i); + } else if (run->stopRequested && run->rt) { + runEnd(run); + } + } +} + + +static void textAppend(const char *s, int32_t len) { + if (sText && arrlen(sText) > 0) { + arrsetlen(sText, arrlen(sText) - 1); // drop the NUL + } + + for (int32_t i = 0; i < len; i++) { + arrput(sText, s[i]); + } + + arrput(sText, '\0'); +} + + +static const char *textOf(const char *s) { + textReset(); + textAppend(s, (int32_t)strlen(s)); + return sText ? sText : ""; +} + + +static void textReset(void) { + arrsetlen(sText, 0); + arrput(sText, '\0'); +} + + +// cppcheck-suppress constParameterCallback ; a token callback: the context stays a plain void * for callers that keep state +static void tokenRow(void *ctx, BasSrcKindE kind, int32_t line, int32_t col, int32_t pos, int32_t len) { + const DevTokensT *tokens = (const DevTokensT *)ctx; + char row[DEV_TOKEN_ROW_MAX]; + int32_t n = snprintf(row, sizeof(row), "%" PRId32 "%c%" PRId32 "%c%" PRId32 "%c%s%c", line, ROW_CELL_SEP, col, ROW_CELL_SEP, len, ROW_CELL_SEP, basSourceKindName(kind), ROW_CELL_SEP); + char end = ROW_END; + + textAppend(row, n); + cellAppend(tokens->src + pos, len); + textAppend(&end, 1); +} + + +static void withEventsRow(void *ctx, const char *name, const char *className) { + const char *cells[] = { name, className }; + + rowAdd(ctx, cells, DVX_ARRAY_LEN(cells)); +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c new file mode 100644 index 00000000..a826eaf1 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c @@ -0,0 +1,337 @@ +// 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. + +// basHelpMap.c -- the DVX BASIC help topic of a word or control type +// (see basHelpMap.h) + +#include "basHelpMap.h" +#include "dvxTypes.h" + +#include +#include +#include +#include + +typedef struct { + const char *keyword; + const char *topic; +} BasHelpMapEntryT; + +static const BasHelpMapEntryT sHelpMap[] = { + // Data types + {"Boolean", "lang.datatypes"}, + {"Double", "lang.datatypes"}, + {"Integer", "lang.datatypes"}, + {"Long", "lang.datatypes"}, + {"Single", "lang.datatypes"}, + {"String", "lang.datatypes"}, + + // Declarations + {"ByRef", "lang.declarations"}, + {"ByVal", "lang.declarations"}, + {"Const", "lang.declarations"}, + {"Declare", "lang.declarations"}, + {"Dim", "lang.declarations"}, + {"Option", "lang.declarations"}, + {"ReDim", "lang.declarations"}, + {"Shared", "lang.declarations"}, + {"Static", "lang.declarations"}, + {"Type", "lang.declarations"}, + + // Operators + {"And", "lang.operators"}, + {"Like", "lang.operators"}, + {"Mod", "lang.operators"}, + {"Not", "lang.operators"}, + {"Or", "lang.operators"}, + {"Xor", "lang.operators"}, + + // Conditionals + {"Case", "lang.conditionals"}, + {"Else", "lang.conditionals"}, + {"ElseIf", "lang.conditionals"}, + {"If", "lang.conditionals"}, + {"Select", "lang.conditionals"}, + {"Then", "lang.conditionals"}, + + // Loops + {"Do", "lang.loops"}, + {"For", "lang.loops"}, + {"Loop", "lang.loops"}, + {"Next", "lang.loops"}, + {"Step", "lang.loops"}, + {"Until", "lang.loops"}, + {"Wend", "lang.loops"}, + {"While", "lang.loops"}, + + // Procedures + {"Call", "lang.procedures"}, + {"Def", "lang.procedures"}, + {"End", "lang.procedures"}, + {"Function", "lang.procedures"}, + {"Sub", "lang.procedures"}, + + // Flow control + {"Exit", "lang.flow"}, + {"GoSub", "lang.flow"}, + {"GoTo", "lang.flow"}, + {"On", "lang.flow"}, + {"Resume", "lang.flow"}, + {"Return", "lang.flow"}, + + // I/O statements + {"Data", "lang.io"}, + {"Input", "lang.io"}, + {"Print", "lang.io"}, + {"Read", "lang.io"}, + {"Rem", "lang.io"}, + {"Write", "lang.io"}, + + // Misc statements + {"DoEvents", "lang.misc"}, + {"Err", "lang.misc"}, + {"Error", "lang.misc"}, + {"Error$", "lang.misc"}, + {"Load", "lang.misc"}, + {"Shell", "lang.misc"}, + {"Sleep", "lang.misc"}, + {"Unload", "lang.misc"}, + + // Sound, joysticks and frames + {"Beep", "lang.sound"}, + {"FrameWait", "lang.sound"}, + {"Play", "lang.sound"}, + {"Sound", "lang.sound"}, + {"Stick", "lang.sound"}, + {"Strig", "lang.sound"}, + + // Console and graphics + {"Circle", "lang.screen"}, + {"Cls", "lang.screen"}, + {"Color", "lang.screen"}, + {"CsrLin", "lang.screen"}, + {"Draw", "lang.screen"}, + {"FullScreen", "lang.screen"}, + {"InKey$", "lang.screen"}, + {"Locate", "lang.screen"}, + {"Paint", "lang.screen"}, + {"Palette", "lang.screen"}, + {"PCopy", "lang.screen"}, + {"Pen", "lang.screen"}, + {"PMap", "lang.screen"}, + {"Point", "lang.screen"}, + {"Pos", "lang.screen"}, + {"PReset", "lang.screen"}, + {"PSet", "lang.screen"}, + {"Screen", "lang.screen"}, + {"VarPtr$", "lang.screen"}, + {"View", "lang.screen"}, + {"Width", "lang.screen"}, + {"Window", "lang.screen"}, + + // File I/O + {"Close", "lang.fileio"}, + {"Eof", "lang.func.fileio"}, + {"FileAttr", "lang.func.fileio"}, + {"FreeFile", "lang.func.fileio"}, + {"Loc", "lang.func.fileio"}, + {"Lof", "lang.func.fileio"}, + {"Open", "lang.fileio"}, + {"Reset", "lang.fileio"}, + {"Seek", "lang.func.fileio"}, + + // String functions + {"Asc", "lang.func.string"}, + {"Chr", "lang.func.string"}, + {"Chr$", "lang.func.string"}, + {"Environ", "lang.func.string"}, + {"Environ$", "lang.func.string"}, + {"Format", "lang.func.string"}, + {"Format$", "lang.func.string"}, + {"InStr", "lang.func.string"}, + {"LCase", "lang.func.string"}, + {"LCase$", "lang.func.string"}, + {"LTrim", "lang.func.string"}, + {"LTrim$", "lang.func.string"}, + {"Left", "lang.func.string"}, + {"Left$", "lang.func.string"}, + {"Len", "lang.func.string"}, + {"Mid", "lang.func.string"}, + {"Mid$", "lang.func.string"}, + {"RTrim", "lang.func.string"}, + {"RTrim$", "lang.func.string"}, + {"Right", "lang.func.string"}, + {"Right$", "lang.func.string"}, + {"Spc", "lang.func.string"}, + {"Space", "lang.func.string"}, + {"Space$", "lang.func.string"}, + {"String$", "lang.func.string"}, + {"Tab", "lang.func.string"}, + {"Trim", "lang.func.string"}, + {"Trim$", "lang.func.string"}, + {"UCase", "lang.func.string"}, + {"UCase$", "lang.func.string"}, + + // Math functions + {"Abs", "lang.func.math"}, + {"Atn", "lang.func.math"}, + {"Cos", "lang.func.math"}, + {"Exp", "lang.func.math"}, + {"Fix", "lang.func.math"}, + {"Int", "lang.func.math"}, + {"Log", "lang.func.math"}, + {"Randomize", "lang.func.math"}, + {"Rnd", "lang.func.math"}, + {"Sgn", "lang.func.math"}, + {"Sin", "lang.func.math"}, + {"Sqr", "lang.func.math"}, + {"Tan", "lang.func.math"}, + {"Timer", "lang.func.math"}, + + // Conversion functions + {"CBool", "lang.func.conversion"}, + {"CDbl", "lang.func.conversion"}, + {"CInt", "lang.func.conversion"}, + {"CLng", "lang.func.conversion"}, + {"CSng", "lang.func.conversion"}, + {"CStr", "lang.func.conversion"}, + {"Hex", "lang.func.conversion"}, + {"Hex$", "lang.func.conversion"}, + {"Str", "lang.func.conversion"}, + {"Str$", "lang.func.conversion"}, + {"Val", "lang.func.conversion"}, + + // Misc functions + {"InputBox", "lang.func.misc"}, + {"InputBox$", "lang.func.misc"}, + {"LoadPicture", "lang.func.misc"}, + {"MsgBox", "lang.func.misc"}, + + // SQL functions + {"SQLAffected", "lang.sql"}, + {"SQLClose", "lang.sql"}, + {"SQLEof", "lang.sql"}, + {"SQLError", "lang.sql"}, + {"SQLError$", "lang.sql"}, + {"SQLExec", "lang.sql"}, + {"SQLField", "lang.sql"}, + {"SQLField$", "lang.sql"}, + {"SQLFieldCount", "lang.sql"}, + {"SQLFieldDbl", "lang.sql"}, + {"SQLFieldInt", "lang.sql"}, + {"SQLFreeResult", "lang.sql"}, + {"SQLNext", "lang.sql"}, + {"SQLOpen", "lang.sql"}, + {"SQLQuery", "lang.sql"}, + + // App object + {"App", "lang.app"}, + + // INI functions + {"IniRead", "lang.ini"}, + {"IniWrite", "lang.ini"}, + + // Constants + {"False", "lang.constants"}, + {"True", "lang.constants"}, + {"vbCancel", "lang.constants"}, + {"vbCritical", "lang.constants"}, + {"vbModal", "lang.constants"}, + {"vbOK", "lang.constants"}, + {"vbOKCancel", "lang.constants"}, + {"vbYesNo", "lang.constants"}, + + // Variants + {"Array", "lang.variants"}, + {"CVar", "lang.variants"}, + {"Empty", "lang.variants"}, + {"IsArray", "lang.variants"}, + {"IsEmpty", "lang.variants"}, + {"IsMissing", "lang.procedures"}, + {"IsNull", "lang.variants"}, + {"IsNumeric", "lang.variants"}, + {"IsObject", "lang.variants"}, + {"Null", "lang.variants"}, + {"TypeName", "lang.variants"}, + {"VarType", "lang.variants"}, + {"Variant", "lang.variants"}, + + // Objects and class modules + {"Class_Initialize", "lang.classes"}, + {"Class_Terminate", "lang.classes"}, + {"Event", "lang.events"}, + {"Friend", "lang.classes"}, + {"Implements", "lang.interfaces"}, + {"Is", "lang.classes"}, + {"New", "lang.classes"}, + {"Nothing", "lang.classes"}, + {"Private", "lang.classes"}, + {"Property", "lang.classes"}, + {"Public", "lang.classes"}, + {"RaiseEvent", "lang.events"}, + {"Set", "lang.classes"}, + {"TypeOf", "lang.classes"}, + {"With", "lang.classes"}, + {"WithEvents", "lang.events"}, + + // Collections + {"Collection", "lang.collection"}, + {"Each", "lang.collection"}, + + // Form/control statements + {"Me", "lang.forms"}, +}; + +#define HELP_MAP_COUNT DVX_ARRAY_LEN(sHelpMap) + +// ============================================================ +// Prototypes +// ============================================================ + +void basHelpCtrlTopic(const char *typeName, char *buf, int32_t bufSize); +const char *basHelpKeywordTopic(const char *word); + + +void basHelpCtrlTopic(const char *typeName, char *buf, int32_t bufSize) { + if (!buf || bufSize <= 0) { + return; + } + + int32_t off = snprintf(buf, (size_t)bufSize, "%s", BAS_HELP_CTRL_PREFIX); + + for (int32_t i = 0; typeName && typeName[i] && off < bufSize - 1; i++) { + buf[off++] = (char)tolower((unsigned char)typeName[i]); + } + + buf[off < bufSize ? off : bufSize - 1] = '\0'; +} + + +const char *basHelpKeywordTopic(const char *word) { + for (int32_t i = 0; word && i < HELP_MAP_COUNT; i++) { + if (strcasecmp(sHelpMap[i].keyword, word) == 0) { + return sHelpMap[i].topic; + } + } + + return NULL; +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basHelpMap.h b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.h new file mode 100644 index 00000000..2a8454fb --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.h @@ -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. + +// basHelpMap.h -- the DVX BASIC help topic of a word or control type +// +// What F1 opens in a code editor: the topic of the DVX BASIC help (the +// IDE's help file, dvxbasic.hlp) that explains a keyword, built-in +// function, statement or constant, and the topic of a control type +// (ctrl., the rule every control's help follows, so +// a new control's help is found without a table entry). The IDE uses +// these; BASIC programs reach them through compiler.bas +// (CompHelpTopic$, CompHelpCtrlTopic$). + +#ifndef BAS_HELP_MAP_H +#define BAS_HELP_MAP_H + +#include + +// The prefix of every control type's help topic +#define BAS_HELP_CTRL_PREFIX "ctrl." + +// The topic of control type typeName (BAS_HELP_CTRL_PREFIX and the name +// in lower case) in buf (bufSize bytes). +void basHelpCtrlTopic(const char *typeName, char *buf, int32_t bufSize); + +// The topic explaining word (any case), or NULL when it has none. +const char *basHelpKeywordTopic(const char *word); + +#endif // BAS_HELP_MAP_H diff --git a/src/apps/kpunch/dvxbasic/basdev/basProcs.c b/src/apps/kpunch/dvxbasic/basdev/basProcs.c new file mode 100644 index 00000000..8c38cedd --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basProcs.c @@ -0,0 +1,116 @@ +// 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. + +// basProcs.c -- the SUB / FUNCTION blocks of BASIC source text (see basProcs.h) + +#include "basProcs.h" +#include "dvxPlat.h" + +#include +#include + +#define PROC_KW_SUB "SUB " +#define PROC_KW_FUNCTION "FUNCTION " +#define PROC_KW_PUBLIC "PUBLIC " +#define PROC_KW_PRIVATE "PRIVATE " +#define PROC_KW_LEN(s) ((int32_t)sizeof(s) - 1) + +// Prototypes (alphabetical) +const char *basProcDeclAt(const char *p, bool *outIsSub); +void basProcNameFromDecl(const char *afterKw, char *out, int32_t outSize); +const char *basProcSkipToEnd(const char *declLine, bool isSub, int32_t *outEndLineOffset); +static bool kwMatch(const char *p, const char *kw); +static const char *skipLine(const char *p); + + +const char *basProcDeclAt(const char *p, bool *outIsSub) { + if (kwMatch(p, PROC_KW_PUBLIC)) { + p = dvxSkipWs(p + PROC_KW_LEN(PROC_KW_PUBLIC)); + } else if (kwMatch(p, PROC_KW_PRIVATE)) { + p = dvxSkipWs(p + PROC_KW_LEN(PROC_KW_PRIVATE)); + } + + if (kwMatch(p, PROC_KW_SUB)) { + *outIsSub = true; + return p + PROC_KW_LEN(PROC_KW_SUB); + } + + if (kwMatch(p, PROC_KW_FUNCTION)) { + *outIsSub = false; + return p + PROC_KW_LEN(PROC_KW_FUNCTION); + } + + return NULL; +} + + +void basProcNameFromDecl(const char *afterKw, char *out, int32_t outSize) { + const char *p = dvxSkipWs(afterKw); + int32_t n = 0; + + while (*p && *p != '(' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && n < outSize - 1) { + out[n++] = *p++; + } + + out[n] = '\0'; +} + + +const char *basProcSkipToEnd(const char *declLine, bool isSub, int32_t *outEndLineOffset) { + const char *endTag = isSub ? BAS_PROC_KW_END_SUB : BAS_PROC_KW_END_FUNCTION; + const char *scan = skipLine(declLine); + int32_t line = 1; + bool found = false; + + while (*scan) { + if (kwMatch(dvxSkipWs(scan), endTag)) { + found = true; + scan = skipLine(scan); + break; + } + + scan = skipLine(scan); + line++; + } + + *outEndLineOffset = found ? line : line - 1; + return scan; +} + + +static bool kwMatch(const char *p, const char *kw) { + return strncasecmp(p, kw, strlen(kw)) == 0; +} + + +// Advance past the end of the current line (or to the NUL). +static const char *skipLine(const char *p) { + while (*p && *p != '\n') { + p++; + } + + if (*p == '\n') { + p++; + } + + return p; +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basProcs.h b/src/apps/kpunch/dvxbasic/basdev/basProcs.h new file mode 100644 index 00000000..f724e213 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basProcs.h @@ -0,0 +1,55 @@ +// 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. + +// basProcs.h -- the SUB / FUNCTION blocks of BASIC source text +// +// The IDE shows one procedure at a time and fills its Object / Event +// lists from these blocks; the basdev library lists them for a BASIC +// IDE (CompProcList$). A block starts at a SUB or FUNCTION line (Public +// or Private in front allowed) and ends at its END SUB / END FUNCTION +// line. The scan is textual, so it works on code that does not compile. + +#ifndef BAS_PROCS_H +#define BAS_PROCS_H + +#include +#include + +// The lines that end a SUB / FUNCTION block (matched without regard to case) +#define BAS_PROC_KW_END_SUB "END SUB" +#define BAS_PROC_KW_END_FUNCTION "END FUNCTION" + +// When p (already past leading blanks) starts a SUB or FUNCTION line, +// returns the text after the keyword and sets *outIsSub; else NULL. +const char *basProcDeclAt(const char *p, bool *outIsSub); + +// Copy the procedure name that follows a SUB / FUNCTION keyword (up to +// '(' or a blank) into out. +void basProcNameFromDecl(const char *afterKw, char *out, int32_t outSize); + +// From the start of a SUB / FUNCTION line, the position just past its +// END SUB / END FUNCTION line (or the end of the text). +// *outEndLineOffset receives how many lines after the declaration the +// END line is (the last line's when it is missing). +const char *basProcSkipToEnd(const char *declLine, bool isSub, int32_t *outEndLineOffset); + +#endif // BAS_PROCS_H diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.c b/src/apps/kpunch/dvxbasic/basdev/basProject.c new file mode 100644 index 00000000..34c47172 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.c @@ -0,0 +1,1048 @@ +// 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. + +// basProject.c -- the DVX BASIC project (.dbp) model (see basProject.h) +// +// The .dbp file is INI format: +// +// [Project] +// Name = MyProject +// Type = Control (a control project; none for a program) +// +// [Modules] +// File0 = MAIN.BAS +// File1 = UTILS.BAS +// +// [Classes] +// File0 = STACK.CLS +// +// [Controls] +// File0 = ICONVIEW.CTL +// +// [Forms] +// File0 = FORM1.FRM +// +// [Settings] +// StartupForm = Form1 +// +// All file paths are relative to the directory containing the .dbp file. +// Each load and save uses a handle of its own (dvxPrefs), so project +// files never mix with a program's other settings. + +#include "basProject.h" +#include "../compiler/lexer.h" +#include "../formrt/formrt.h" +#include "dvxPlat.h" + +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include +#include +#include +#include + +#define PRJ_INI_KEY_LEN 16 // "FileNNN" INI key buffer size +#define PRJ_FILE_KEY_FMT "File%" PRId32 // INI key for an indexed module/class/form path + +_Static_assert(DVX_MAX_PATH <= PRJ_MAX_FIELD && PRJ_MAX_STRING <= PRJ_MAX_FIELD, "every [Project] field must fit PRJ_MAX_FIELD"); + +#define PRJ_TRUE_TEXT "true" // a flag setting as prjFieldGet gives it (and the .dbp stores it) +#define PRJ_FALSE_TEXT "false" +#define PRJ_YES_TEXT "yes" // also a true flag (as the .dbp reader takes it) +#define PRJ_ONE_TEXT "1" +#define PRJ_BASIC_TRUE "-1" // BASIC's True as text + +// A UserControl header: the indent and key width of a line it adds +// (" Key = Value", the longest key is DefaultEvent), the +// flag and color texts it writes, and how a color is read. +#define PRJ_CTL_INDENT " " +#define PRJ_CTL_KEY_WIDTH 12 +#define PRJ_CTL_TRUE "True" +#define PRJ_CTL_FALSE "False" +#define PRJ_CTL_COLOR_FMT "&H%08" PRIX32 "&" +#define PRJ_CTL_HEX_PREFIX "&H" +#define PRJ_CTL_HEX_BASE 16 +#define PRJ_CTL_DEC_BASE 10 +#define PRJ_CTL_LINE_EXTRA 8 // a line's indent, " = ", quotes and newline beyond key and value +#define PRJ_CTL_KEY_NAME BAS_PROP_NAME // the control's name: the word after Begin UserControl + +// A new UserControl (prjCtlNew): its header block around %s, its name, +// then the code before its Paint handler. +#define PRJ_CTL_NEW_HEAD BAS_CLS_KW_VERSION " DVX 1.00\n" FRM_KW_BEGIN " " BAS_UC_OBJECT " %s\n" FRM_KW_END "\n\nOption Explicit\n\n" +#define PRJ_CTL_PAINT BAS_UC_OBJECT BAS_EVENT_HANDLER_SEP BAS_EVT_PAINT + +// A [Project] or [Settings] value of PrjStateT: a text buffer (size bytes) +// or a flag (size 0). always: written even when empty / false, the rest +// only when set, so projects that never use a setting stay unchanged. +typedef struct { + const char *section; + const char *key; + size_t offset; + int32_t size; + bool always; +} PrjFieldT; + +#define PRJ_TEXT(sec, k, field, alw) { sec, k, offsetof(PrjStateT, field), (int32_t)sizeof(((PrjStateT *)0)->field), alw } +#define PRJ_FLAG(sec, k, field, alw) { sec, k, offsetof(PrjStateT, field), 0, alw } + +// The one list of .dbp settings: load, save, prjFieldGet / prjFieldSet +static const PrjFieldT sFields[] = { + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, name, true), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_TYPE, type, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, author, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, publisher, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, version, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, copyright, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, description, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, iconPath, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, helpFile, false), + PRJ_TEXT(BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COMMANDARGS, commandArgs, false), + PRJ_TEXT(BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, startupForm, true), + PRJ_FLAG(BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, optionExplicit, true), + PRJ_FLAG(BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_MULTIINSTANCE, multiInstance, false) +}; + +// Per file kind: the .dbp section listing its files, their extension +// and how the build wraps them (basBuildWrapForPath's kinds). +typedef struct { + const char *section; + const char *ext; + BasBuildWrapE wrap; +} PrjKindInfoT; + +static const PrjKindInfoT sKinds[PrjKindCountE] = { + [PrjKindModuleE] = { BAS_INI_SECTION_MODULES, PRJ_BAS_EXT, BAS_BUILD_WRAP_MODULE }, + [PrjKindClassE] = { BAS_INI_SECTION_CLASSES, BAS_CLS_EXT, BAS_BUILD_WRAP_CLASS }, + [PrjKindControlE] = { BAS_INI_SECTION_CONTROLS, BAS_CTL_EXT, BAS_BUILD_WRAP_CONTROL }, + [PrjKindFormE] = { BAS_INI_SECTION_FORMS, PRJ_FRM_EXT, BAS_BUILD_WRAP_FORM } +}; + +// The keys of a UserControl's header, in the order the IDE lists them, +// with where the parsed header (basUcHeaderParse) keeps each value. +typedef struct { + PrjCtlKeyT key; // first: a PrjCtlKeyT handed out is its row + size_t field; // offset in BasUcHeaderT: int32_t, bool or char[] (NUL-terminated) by kind +} PrjCtlFieldT; + +#define PRJ_CTL_FIELD(k, kind, f) { { k, kind }, offsetof(BasUcHeaderT, f) } + +static const PrjCtlFieldT sCtlKeys[] = { + PRJ_CTL_FIELD(PRJ_CTL_KEY_NAME, PrjCtlNameE, name), + PRJ_CTL_FIELD(BAS_UC_KEY_WIDTH, PrjCtlIntE, width), + PRJ_CTL_FIELD(BAS_UC_KEY_HEIGHT, PrjCtlIntE, height), + PRJ_CTL_FIELD(BAS_UC_KEY_CANGETFOCUS, PrjCtlBoolE, canGetFocus), + PRJ_CTL_FIELD(BAS_UC_KEY_BACKCOLOR, PrjCtlColorE, backColor), + PRJ_CTL_FIELD(BAS_UC_KEY_TOOLBOXICON, PrjCtlTextE, toolboxIcon), + PRJ_CTL_FIELD(BAS_UC_KEY_DESCRIPTION, PrjCtlTextE, description), + PRJ_CTL_FIELD(BAS_UC_KEY_DEFAULTEVENT, PrjCtlTextE, defaultEvent) +}; + +// Where a header key's line is in a .ctl text (offsets into it). For +// Name the key's line is the Begin UserControl line and the value the +// name on it. +typedef struct { + int32_t endLine; // start of the block's End line + int32_t keyLine; // start of the key's line (-1: none) + int32_t keyNext; // start of the line after it + int32_t valuePos; // just past the key's '=' (Name: the name's first character) + int32_t valueEnd; // end of the key's line before its line break (Name: just past the name) +} PrjCtlSpotT; + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind); +int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine); +bool prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize); +void prjClose(PrjStateT *prj); +static bool prjCtlFormat(const PrjCtlKeyT *k, const char *value, char *out, int32_t outSize); +bool prjCtlHeaderGet(const char *text, const char *key, char *out, int32_t outSize); +char *prjCtlHeaderSet(const char *text, const char *key, const char *value); +const PrjCtlKeyT *prjCtlKeyAt(int32_t idx); +const PrjCtlKeyT *prjCtlKeyFind(const char *key); +static bool prjCtlLocate(const char *text, const PrjCtlKeyT *k, PrjCtlSpotT *spot); +char *prjCtlNew(const char *name); +static void prjDeriveDir(PrjStateT *prj, const char *dbpPath); +static const PrjFieldT *prjFieldFind(const char *key); +bool prjFieldGet(const PrjStateT *prj, const char *key, char *out, int32_t outSize); +bool prjFieldSet(PrjStateT *prj, const char *key, const char *value); +int32_t prjFileCount(const PrjStateT *prj); +int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path); +void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize); +void prjInit(PrjStateT *prj); +bool prjIsControlProject(const PrjStateT *prj); +PrjFileKindE prjKindForPath(const char *path); +BasBuildWrapE prjKindWrap(PrjFileKindE kind); +bool prjLoad(PrjStateT *prj, const char *dbpPath); +void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx); +static void prjLoadFields(PrjStateT *prj, PrefsHandleT *h); +static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind); +int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine); +bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine); +void prjNew(PrjStateT *prj, const char *name, const char *directory); +static bool prjParseFlag(const char *text); +bool prjPathExists(const char *path); +char *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing); +void prjRemoveFile(PrjStateT *prj, int32_t idx); +bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize); +bool prjSave(const PrjStateT *prj); +static void prjSaveFields(PrefsHandleT *h, const PrjStateT *prj, const char *section); +static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKindE kind); +void prjSingleLine(char *dst, int32_t dstSize, const char *src); + + +int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind) { + PrjFileT entry; + memset(&entry, 0, sizeof(entry)); + snprintf(entry.path, sizeof(entry.path), "%s", relativePath); + entry.kind = (uint8_t)kind; + arrput(prj->files, entry); + prj->dirty = true; + return prjFileCount(prj) - 1; +} + + +int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine) { + int32_t codePos = basClassCodePos(text, len); + int32_t line = 1; + + for (int32_t i = 0; i < codePos; i++) { + if (text[i] == '\n') { + line++; + } + } + + *outFirstLine = line; + return codePos; +} + + +bool prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize) { + const char *src = text ? text : ""; + int32_t len = (int32_t)strlen(src); + + if (prj->files[fileIdx].kind == PrjKindControlE) { + return basExtractCtlName(src, len, out, outSize); + } + + return basExtractClassName(src, len, prj->files[fileIdx].path, out, outSize); +} + + +void prjClose(PrjStateT *prj) { + for (int32_t i = 0; i < prjFileCount(prj); i++) { + free(prj->files[i].buffer); + } + + arrfree(prj->files); + arrfree(prj->sourceMap); + prjInit(prj); +} + + +// value as header key k writes it into out: false when it does not suit +// the key (an empty value is "", the key's line removed). +static bool prjCtlFormat(const PrjCtlKeyT *k, const char *value, char *out, int32_t outSize) { + const char *v = dvxSkipWs(value); + char *end = NULL; + + out[0] = '\0'; + + // An empty value removes the key's line; a control keeps its name + if (v[0] == '\0') { + return k->kind != PrjCtlNameE; + } + + switch (k->kind) { + case PrjCtlNameE: + if (strlen(v) >= BAS_MAX_IDENT || !basIsValidIdent(v)) { + return false; + } + + snprintf(out, (size_t)outSize, "%s", v); + return true; + + case PrjCtlIntE: { + int64_t n = strtoll(v, &end, PRJ_CTL_DEC_BASE); + + if (!isdigit((unsigned char)v[0]) || n > INT32_MAX || *dvxSkipWs(end) != '\0') { + return false; + } + + snprintf(out, (size_t)outSize, "%" PRId32, (int32_t)n); + return true; + } + + case PrjCtlBoolE: + snprintf(out, (size_t)outSize, "%s", prjParseFlag(v) ? PRJ_CTL_TRUE : PRJ_CTL_FALSE); + return true; + + case PrjCtlColorE: { + bool hex = strncasecmp(v, PRJ_CTL_HEX_PREFIX, sizeof(PRJ_CTL_HEX_PREFIX) - 1) == 0; + const char *digits = hex ? v + sizeof(PRJ_CTL_HEX_PREFIX) - 1 : v; + uint64_t color = strtoull(digits, &end, hex ? PRJ_CTL_HEX_BASE : PRJ_CTL_DEC_BASE); + + if (hex && *end == '&') { + end++; + } + + if (end == digits || !isxdigit((unsigned char)digits[0]) || *dvxSkipWs(end) != '\0' || color > UINT32_MAX) { + return false; + } + + snprintf(out, (size_t)outSize, PRJ_CTL_COLOR_FMT, (uint32_t)color); + return true; + } + + default: + if (strpbrk(v, "\"\r\n") != NULL) { + return false; + } + + snprintf(out, (size_t)outSize, "\"%s\"", v); + return true; + } +} + + +bool prjCtlHeaderGet(const char *text, const char *key, char *out, int32_t outSize) { + const PrjCtlKeyT *k = prjCtlKeyFind(key); + BasUcHeaderT header; + + if (!k || !text || !basUcHeaderParse(text, (int32_t)strlen(text), &header)) { + return false; + } + + const PrjCtlFieldT *f = (const PrjCtlFieldT *)k; + const char *at = (const char *)&header + f->field; + + switch (k->kind) { + case PrjCtlIntE: + snprintf(out, (size_t)outSize, "%" PRId32, *(const int32_t *)at); + break; + + case PrjCtlBoolE: + snprintf(out, (size_t)outSize, "%s", *(const bool *)at ? PRJ_CTL_TRUE : PRJ_CTL_FALSE); + break; + + case PrjCtlColorE: + if (*(const int32_t *)at == BAS_UC_NO_COLOR) { + out[0] = '\0'; + } else { + snprintf(out, (size_t)outSize, PRJ_CTL_COLOR_FMT, (uint32_t)*(const int32_t *)at); + } + break; + + default: + snprintf(out, (size_t)outSize, "%s", at); + break; + } + + return true; +} + + +char *prjCtlHeaderSet(const char *text, const char *key, const char *value) { + const PrjCtlKeyT *k = prjCtlKeyFind(key); + PrjCtlSpotT spot; + + if (!k || !text || !value) { + return NULL; + } + + int32_t valueLen = (int32_t)strlen(value) + PRJ_CTL_LINE_EXTRA; + char *formatted = (char *)calloc(1, (size_t)valueLen); + + if (!formatted || !prjCtlFormat(k, value, formatted, valueLen) || !prjCtlLocate(text, k, &spot)) { + free(formatted); + return NULL; + } + + // The text before and after the changed part, and what goes between: + // the key's value replaced, its line dropped (an empty value), or a + // new line before the block's End + bool has = spot.keyLine >= 0; + bool set = formatted[0] != '\0'; + int32_t textLen = (int32_t)strlen(text); + int32_t head = has ? (set ? spot.valuePos : spot.keyLine) : spot.endLine; + int32_t tail = has ? (set ? spot.valueEnd : spot.keyNext) : spot.endLine; + int32_t size = textLen + valueLen + PRJ_CTL_KEY_WIDTH + PRJ_CTL_LINE_EXTRA; + char *out = (char *)malloc((size_t)size); + + if (!out) { + free(formatted); + return NULL; + } + + int32_t n = head; + + memcpy(out, text, (size_t)head); + + if (has && set) { + n += snprintf(out + n, (size_t)(size - n), (k->kind == PrjCtlNameE) ? "%s" : " %s", formatted); + } else if (set) { + n += snprintf(out + n, (size_t)(size - n), PRJ_CTL_INDENT "%-*s = %s\n", PRJ_CTL_KEY_WIDTH, k->key, formatted); + } + + snprintf(out + n, (size_t)(size - n), "%s", text + tail); + free(formatted); + return out; +} + + +const PrjCtlKeyT *prjCtlKeyAt(int32_t idx) { + return (idx >= 0 && idx < DVX_ARRAY_LEN(sCtlKeys)) ? &sCtlKeys[idx].key : NULL; +} + + +const PrjCtlKeyT *prjCtlKeyFind(const char *key) { + for (int32_t i = 0; key && i < DVX_ARRAY_LEN(sCtlKeys); i++) { + if (strcasecmp(sCtlKeys[i].key.key, key) == 0) { + return &sCtlKeys[i].key; + } + } + + return NULL; +} + + +// Where key k's line and the End line of text's Begin UserControl block +// are (the key's line found the way basCtlHeaderValue finds it, Name on +// the Begin line). False when the text has no such block or the block +// no End. +static bool prjCtlLocate(const char *text, const PrjCtlKeyT *k, PrjCtlSpotT *spot) { + const char *key = k->key; + int32_t len = (int32_t)strlen(text); + int32_t keyLen = (int32_t)strlen(key); + int32_t pos = 0; + bool inBlock = false; + + memset(spot, 0, sizeof(*spot)); + spot->keyLine = -1; + + while (pos < len) { + int32_t lineStart = pos; + + while (pos < len && text[pos] != '\n') { + pos++; + } + + int32_t lineEnd = pos; + int32_t trimEnd = pos; + int32_t l = lineStart; + + pos += (pos < len) ? 1 : 0; + + if (lineEnd > lineStart && text[lineEnd - 1] == '\r') { + lineEnd--; + } + + while (trimEnd > lineStart && (text[trimEnd - 1] == '\r' || text[trimEnd - 1] == ' ' || text[trimEnd - 1] == '\t')) { + trimEnd--; + } + + while (l < trimEnd && (text[l] == ' ' || text[l] == '\t')) { + l++; + } + + if (!inBlock) { + int32_t nameOff = basMatchBeginControl(text + l, trimEnd - l); + + inBlock = nameOff >= 0; + + if (inBlock && k->kind == PrjCtlNameE) { + spot->keyLine = lineStart; + spot->keyNext = pos; + spot->valuePos = l + nameOff; + spot->valueEnd = l + nameOff + (int32_t)strcspn(text + l + nameOff, " \t\r\n"); + } + + continue; + } + + if (frmIsEndLine(text + l, trimEnd - l)) { + spot->endLine = lineStart; + return true; + } + + if (spot->keyLine >= 0 || trimEnd - l <= keyLen || strncasecmp(text + l, key, (size_t)keyLen) != 0) { + continue; + } + + const char *eq = dvxSkipWs(text + l + keyLen); + + if (*eq == '=' && eq < text + trimEnd) { + spot->keyLine = lineStart; + spot->keyNext = pos; + spot->valuePos = (int32_t)(eq - text) + 1; + spot->valueEnd = lineEnd; + } + } + + return false; +} + + +char *prjCtlNew(const char *name) { + char stub[BAS_MAX_IDENT * 2]; + char size[PRJ_INI_KEY_LEN]; + int32_t stubLen = snprintf(stub, sizeof(stub), PRJ_HANDLER_FMT, PRJ_PRIVATE, PRJ_CTL_PAINT, ""); + size_t headSize = sizeof(PRJ_CTL_NEW_HEAD) + BAS_MAX_IDENT + (size_t)stubLen; + char *head = (name && strlen(name) < BAS_MAX_IDENT && basIsValidIdent(name)) ? (char *)malloc(headSize) : NULL; + + if (!head) { + return NULL; + } + + // The header's Width and Height: the size a control takes without them + snprintf(head, headSize, PRJ_CTL_NEW_HEAD "%s", name, stub); + snprintf(size, sizeof(size), "%d", (int)BAS_UC_DEFAULT_SIZE); + + char *sized = prjCtlHeaderSet(head, BAS_UC_KEY_WIDTH, size); + char *text = sized ? prjCtlHeaderSet(sized, BAS_UC_KEY_HEIGHT, size) : NULL; + + free(head); + free(sized); + return text; +} + + +// Set prj->projectDir to the directory containing dbpPath, or "." when +// dbpPath has no directory component. +static void prjDeriveDir(PrjStateT *prj, const char *dbpPath) { + snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath); + char *sep = platformPathDirEnd(prj->projectDir); + + if (sep) { + *sep = '\0'; + } else { + prj->projectDir[0] = '.'; + prj->projectDir[1] = '\0'; + } +} + + +static const PrjFieldT *prjFieldFind(const char *key) { + for (int32_t i = 0; key && i < DVX_ARRAY_LEN(sFields); i++) { + if (strcasecmp(sFields[i].key, key) == 0) { + return &sFields[i]; + } + } + + return NULL; +} + + +bool prjFieldGet(const PrjStateT *prj, const char *key, char *out, int32_t outSize) { + const PrjFieldT *f = prjFieldFind(key); + + if (!f) { + return false; + } + + const char *at = (const char *)prj + f->offset; + + if (f->size == 0) { + snprintf(out, (size_t)outSize, "%s", *(const bool *)at ? PRJ_TRUE_TEXT : PRJ_FALSE_TEXT); + } else { + snprintf(out, (size_t)outSize, "%s", at); + } + + return true; +} + + +bool prjFieldSet(PrjStateT *prj, const char *key, const char *value) { + const PrjFieldT *f = prjFieldFind(key); + + if (!f || !value) { + return false; + } + + char *at = (char *)prj + f->offset; + + if (f->size == 0) { + *(bool *)at = prjParseFlag(value); + } else { + prjSingleLine(at, f->size, value); + } + + prj->dirty = true; + return true; +} + + +int32_t prjFileCount(const PrjStateT *prj) { + return (int32_t)arrlen(prj->files); +} + + +int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path) { + for (int32_t i = 0; i < prjFileCount(prj); i++) { + if (strcasecmp(prj->files[i].path, path) == 0) { + return i; + } + } + + return -1; +} + + +void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) { + if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) { + outPath[0] = '\0'; + return; + } + + snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", prj->projectDir, prj->files[fileIdx].path); +} + + +void prjInit(PrjStateT *prj) { + memset(prj, 0, sizeof(*prj)); + prj->activeFileIdx = -1; +} + + +bool prjIsControlProject(const PrjStateT *prj) { + return basBuildIsControlType(prj->type); +} + + +PrjFileKindE prjKindForPath(const char *path) { + BasBuildWrapE wrap = basBuildWrapForPath(path); + + for (int32_t kind = 0; kind < PrjKindCountE; kind++) { + if (sKinds[kind].wrap == wrap) { + return (PrjFileKindE)kind; + } + } + + return PrjKindModuleE; +} + + +BasBuildWrapE prjKindWrap(PrjFileKindE kind) { + return sKinds[kind].wrap; +} + + +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); + + if (!h) { + return false; + } + + // Release whatever project was open (file buffers, arrays) before + // starting from a blank state. + prjClose(prj); + snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath); + + // Derive project directory + prjDeriveDir(prj, dbpPath); + + prjLoadFields(prj, h); + + // [Modules], [Classes], [Controls] and [Forms] sections -- File0, + // File1, ... (no cap) + for (int32_t kind = 0; kind < PrjKindCountE; kind++) { + prjLoadFileSection(prj, h, (PrjFileKindE)kind); + } + + prefsClose(h); + prj->dirty = false; + return true; +} + + +// prjLoadAllFiles -- read all project files into memory buffers +// and extract form names from .frm files. +void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) { + for (int32_t i = 0; i < prjFileCount(prj); i++) { + if (prj->files[i].buffer) { + continue; // already loaded + } + + // An unreadable or oversized file stays unloaded; the IDE reports + // it when the file is opened or compiled. + char *buf = prjReadFileText(prj, i, NULL); + + if (!buf) { + continue; + } + + prj->files[i].buffer = buf; + + // Extract form name from .frm files + if (prj->files[i].kind == PrjKindFormE) { + basExtractFormName(buf, prj->files[i].formName, sizeof(prj->files[i].formName)); + } + + // Yield between files to keep the UI responsive + if (ctx) { + dvxUpdate(ctx); + } + } +} + + +// The [Project] and [Settings] values that are in h; the rest keep +// prjInit's empty / false. +static void prjLoadFields(PrjStateT *prj, PrefsHandleT *h) { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sFields); i++) { + const PrjFieldT *f = &sFields[i]; + char *at = (char *)prj + f->offset; + + if (f->size == 0) { + *(bool *)at = prefsGetBool(h, f->section, f->key, false); + } else { + snprintf(at, (size_t)f->size, "%s", prefsGetString(h, f->section, f->key, at)); + } + } +} + + +// Load File0, File1, ... entries from the INI section of kind, registering +// each as a project file. The loop ends at the first missing key (no +// fixed cap). +static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind) { + for (int32_t i = 0; ; i++) { + char key[PRJ_INI_KEY_LEN]; + snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, i); + + const char *val = prefsGetString(h, sKinds[kind].section, key, NULL); + + if (!val) { + break; + } + + prjAddFile(prj, val, kind); + } +} + + +int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine) { + if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) { + return 0; + } + + return basBuildMapFileLine(prj->sourceMap, prj->files[fileIdx].path, fileLine); +} + + +bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) { + const char *path = NULL; + int32_t localLine = 0; + + if (!basBuildMapLine(prj->sourceMap, concatLine, &path, &localLine)) { + return false; + } + + int32_t fileIdx = prjFileIndexByPath(prj, path); + + if (fileIdx < 0) { + return false; + } + + *outFileIdx = fileIdx; + *outLocalLine = localLine; + return true; +} + + +void prjNew(PrjStateT *prj, const char *name, const char *directory) { + prjInit(prj); + snprintf(prj->name, sizeof(prj->name), "%s", name); + snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory); + snprintf(prj->projectPath, sizeof(prj->projectPath), "%s" DVX_PATH_SEP "%s" BAS_DBP_EXT, directory, name); + prj->dirty = true; +} + + +// A flag setting's text: true, yes, 1 or -1 (BASIC's True) in any case, +// blanks before it ignored. +static bool prjParseFlag(const char *text) { + const char *value = dvxSkipWs(text); + + return strcasecmp(value, PRJ_TRUE_TEXT) == 0 || strcasecmp(value, PRJ_YES_TEXT) == 0 || strcmp(value, PRJ_ONE_TEXT) == 0 || strcmp(value, PRJ_BASIC_TRUE) == 0; +} + + +bool prjPathExists(const char *path) { + FILE *f = fopen(path, "rb"); + + if (!f) { + return false; + } + + fclose(f); + return true; +} + + +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; + } + + int32_t rawSize = (fseek(f, 0, SEEK_END) == 0) ? (int32_t)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 prjRemoveFile(PrjStateT *prj, int32_t idx) { + if (idx < 0 || idx >= prjFileCount(prj)) { + return; + } + + // A removed startup form is no longer a valid startup form. + if (prj->files[idx].kind == PrjKindFormE && 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); + arrdel(prj->files, idx); + + // Adjust active file index + if (prj->activeFileIdx == idx) { + prj->activeFileIdx = -1; + } else if (prj->activeFileIdx > idx) { + prj->activeFileIdx--; + } + + prj->dirty = true; +} + + +bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize) { + PrjFileT *file = &prj->files[idx]; + const char *ext = sKinds[file->kind].ext; + const char *dot = strrchr(newName, '.'); + char name[DVX_MAX_PATH]; + + if (strpbrk(newName, "/\\:") != NULL) { + snprintf(err, errSize, "Give a file name only; the file stays in the project's directory."); + return false; + } + + if (dot && strcasecmp(dot, ext) != 0) { + snprintf(err, errSize, "%s must keep the %s extension.", file->path, ext); + return false; + } + + if (snprintf(name, sizeof(name), "%s%s", newName, dot ? "" : ext) >= (int32_t)sizeof(name)) { + snprintf(err, errSize, "The name is too long."); + return false; + } + + const char *invalid = platformValidateFilename(name); + + if (invalid) { + snprintf(err, errSize, "%s", invalid); + return false; + } + + if (strcasecmp(name, file->path) == 0) { + return true; + } + + if (prjFileIndexByPath(prj, name) >= 0) { + snprintf(err, errSize, "%s is already in the project.", name); + return false; + } + + char oldPath[DVX_MAX_PATH]; + char oldFull[DVX_MAX_PATH]; + char newFull[DVX_MAX_PATH * 2]; + + snprintf(oldPath, sizeof(oldPath), "%s", file->path); + prjFullPath(prj, idx, oldFull, sizeof(oldFull)); + snprintf(newFull, sizeof(newFull), "%s" DVX_PATH_SEP "%s", prj->projectDir, name); + + if (prjPathExists(newFull)) { + snprintf(err, errSize, "%s already exists.", name); + return false; + } + + // A file never saved exists only in memory: nothing to rename on disk + bool onDisk = prjPathExists(oldFull); + + if (onDisk && rename(oldFull, newFull) != 0) { + snprintf(err, errSize, "Could not rename %s to %s.", oldPath, name); + return false; + } + + snprintf(file->path, sizeof(file->path), "%s", name); + + if (!prjSave(prj)) { + snprintf(file->path, sizeof(file->path), "%s", oldPath); + + if (onDisk) { + rename(newFull, oldFull); + } + + snprintf(err, errSize, "Could not write the project file; %s keeps its name.", oldPath); + return false; + } + + // The source map is keyed by path + for (int32_t i = 0; i < (int32_t)arrlen(prj->sourceMap); i++) { + if (strcmp(prj->sourceMap[i].path, oldPath) == 0) { + snprintf(prj->sourceMap[i].path, sizeof(prj->sourceMap[i].path), "%s", name); + } + } + + prj->dirty = false; + return true; +} + + +bool prjSave(const PrjStateT *prj) { + if (prj->projectPath[0] == '\0') { + return false; + } + + PrefsHandleT *h = prefsCreate(); + + if (!h) { + return false; + } + + prjSaveFields(h, prj, BAS_INI_SECTION_PROJECT); + + // [Modules], [Classes], [Controls] and [Forms] sections + for (int32_t kind = 0; kind < PrjKindCountE; kind++) { + prjSaveFileSection(h, prj, (PrjFileKindE)kind); + } + + prjSaveFields(h, prj, BAS_INI_SECTION_SETTINGS); + + bool ok = prefsSaveAs(h, prj->projectPath); + prefsClose(h); + return ok; +} + + +// Write section's values of sFields as one INI line each: the ones +// marked always, and the rest when set. +static void prjSaveFields(PrefsHandleT *h, const PrjStateT *prj, const char *section) { + char line[PRJ_MAX_FIELD]; + + for (int32_t i = 0; i < DVX_ARRAY_LEN(sFields); i++) { + const PrjFieldT *f = &sFields[i]; + const char *at = (const char *)prj + f->offset; + + if (strcmp(f->section, section) != 0) { + continue; + } + + if (f->size == 0) { + if (f->always || *(const bool *)at) { + prefsSetBool(h, f->section, f->key, *(const bool *)at); + } + } else if (f->always || at[0]) { + prjSingleLine(line, (int32_t)sizeof(line), at); + prefsSetString(h, f->section, f->key, line); + } + } +} + + +// Write the project files of kind into its INI section as File0, File1, +// ... (readers stop at the first missing key). +static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKindE kind) { + int32_t idx = 0; + + for (int32_t i = 0; i < prjFileCount(prj); i++) { + if (prj->files[i].kind == kind) { + char key[PRJ_INI_KEY_LEN]; + snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, idx++); + prefsSetString(h, sKinds[kind].section, key, prj->files[i].path); + } + } +} + + +void prjSingleLine(char *dst, int32_t dstSize, const char *src) { + int32_t i = 0; + + for (; i < dstSize - 1 && src[i]; i++) { + dst[i] = (src[i] == '\r' || src[i] == '\n') ? ' ' : src[i]; + } + + dst[i] = '\0'; +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.h b/src/apps/kpunch/dvxbasic/basdev/basProject.h new file mode 100644 index 00000000..592f1308 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.h @@ -0,0 +1,287 @@ +// 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. + +// basProject.h -- the DVX BASIC project (.dbp) model +// +// A project as a development tool edits it: its file list (modules, +// class modules, UserControls, forms) with in-memory edit buffers, its +// [Project] settings (name, type, author, icon, help file ...) and +// [Settings] (startup form, Option Explicit, more than one copy), load +// and save with the .dbp format, renaming a file on disk and in the +// project together, the source map of the last compile, and the header +// of a UserControl (.ctl) read and changed key by key. The DVX BASIC +// IDE uses it, and BASIC programs reach it through compiler.bas (Proj*). +// The window and dialogs around it stay in the IDE (ideProject.h). + +#ifndef BAS_PROJECT_H +#define BAS_PROJECT_H + +#include "../basBuild.h" +#include "../basRes.h" +#include "dvxApp.h" +#include "dvxPrefs.h" +#include "dvxTypes.h" + +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define PRJ_MAX_NAME 32 +#define PRJ_MAX_SOURCE 65536 // a project file must be smaller than this to be loaded or edited +#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 + +// Project, form and module file extensions; a class module's is +// BAS_CLS_EXT, a UserControl's BAS_CTL_EXT (basRes.h). A file's kind +// follows its extension (see prjKindForPath). +#define BAS_DBP_EXT ".dbp" +#define PRJ_FRM_EXT BAS_FRM_EXT +#define PRJ_BAS_EXT ".bas" + +// An empty procedure as the IDE writes an event handler: its scope ("" or +// PRJ_PRIVATE), name and parameter list. A UserControl's own handlers +// (UserControl_Paint ...) are Private: Public ones would be methods. +#define PRJ_HANDLER_FMT "%sSub %s (%s)\n\nEnd Sub\n" +#define PRJ_PRIVATE "Private " + +// ============================================================ +// Project file kinds +// ============================================================ + +// The order is the compile order: modules, then class modules, then +// UserControls, then forms (see basBuildConcatProject). +typedef enum { + PrjKindModuleE, // .bas: [Modules] + PrjKindClassE, // .cls: [Classes] + PrjKindControlE, // .ctl: [Controls] (a UserControl) + PrjKindFormE, // .frm: [Forms] + PrjKindCountE +} PrjFileKindE; + +// ============================================================ +// Project file entry +// ============================================================ + +typedef struct { + // Fields are ordered by alignment to minimize struct padding. + char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded) + char path[DVX_MAX_PATH]; // relative path (8.3 DOS name) + char formName[BAS_MAX_IDENT]; // form object name (from "Begin Form ") + uint8_t kind; // PrjFileKindE + bool modified; // true = buffer has unsaved changes +} PrjFileT; + +// ============================================================ +// Project state +// ============================================================ + +typedef struct { + // Fields are ordered by alignment to minimize struct padding. + PrjFileT *files; // stb_ds dynamic array (prjFileCount) + BasSourceMapT *sourceMap; // stb_ds array: path = files[].path, firstFileLine = first code line + int32_t activeFileIdx; // index of file open in editor (-1 = none) + char name[PRJ_MAX_NAME]; + char type[PRJ_MAX_NAME]; // [Project] Type: "" a program, BAS_PROJECT_TYPE_CONTROL a control project (builds NAME.wgt) + char projectPath[DVX_MAX_PATH]; // full path to .dbp file + char projectDir[DVX_MAX_PATH]; // directory containing .dbp + char startupForm[BAS_MAX_IDENT]; + // Project metadata (for binary generation) + char author[PRJ_MAX_STRING]; + char publisher[PRJ_MAX_STRING]; + char version[PRJ_MAX_NAME]; + char copyright[PRJ_MAX_STRING]; + char description[PRJ_MAX_DESC]; + char iconPath[DVX_MAX_PATH]; // relative path to icon BMP + char helpFile[DVX_MAX_PATH]; // relative path to .hlp file + char commandArgs[PRJ_MAX_ARGS]; // COMMAND$ for runs from the IDE (not built into the program) + bool optionExplicit; // require DIM before use + bool multiInstance; // the built program may run more than once at a time + bool dirty; +} PrjStateT; + +// ============================================================ +// Project management +// ============================================================ + +// Append a file (path relative to the project's directory) of kind; its +// index. +int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind); + +// Offset of the first line of code in the text of a class module (len +// bytes), just past its header; *outFirstLine receives that line's +// 1-based number. +int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine); + +// The class name of project file fileIdx given its text: a class +// module's Attribute VB_Name, else its file name; a UserControl's name +// (Begin UserControl Name). False when there is none. +bool prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize); + +// Free everything the project holds; it is left empty (prjInit). +void prjClose(PrjStateT *prj); + +// A [Project] or [Settings] value by its .dbp key (BAS_INI_KEY_*, any +// case: Name, Type, Author, Publisher, Version, Copyright, Description, +// Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit, +// MultiInstance) as text in out; a flag reads "true" or "false". False +// for an unknown key. +bool prjFieldGet(const PrjStateT *prj, const char *key, char *out, int32_t outSize); + +// Set a value by its key (a flag from true / yes / 1 / -1, anything else +// false; text on one line, cut to its buffer). False for an unknown key. +bool prjFieldSet(PrjStateT *prj, const char *key, const char *value); +int32_t prjFileCount(const PrjStateT *prj); + +// Index of the project file whose relative path is path (any case), or -1. +int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path); + +// The full path of file fileIdx ("" for no such file). +void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize); +void prjInit(PrjStateT *prj); + +// The kind of source file path names, by its file name's extension (the +// build's rule, basBuildWrapForPath). +PrjFileKindE prjKindForPath(const char *path); + +// How the build wraps a file of kind (BAS_BUILD_WRAP_MODULE / CLASS / +// CONTROL / FORM). +BasBuildWrapE prjKindWrap(PrjFileKindE kind); + +// True when the project is a control project ([Project] Type = Control): +// it builds a control file (NAME.wgt) instead of a program. +bool prjIsControlProject(const PrjStateT *prj); + +// Read the .dbp dbpPath into prj (what it held before is freed). False +// when the file does not exist or cannot be read (prj unchanged). +bool prjLoad(PrjStateT *prj, const char *dbpPath); + +// Read every file not loaded yet into its edit buffer (and a form's +// name); with ctx the screen keeps updating between files. +void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx); + +// The line of the concatenated source of the last compile holding line +// fileLine of file fileIdx, or 0 when the file is not in the map (not +// compiled since it was added) or the line is not compiled code (e.g. a +// class module's header). +int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine); + +// The file and line a line of the concatenated source of the last +// compile came from. False when the line belongs to no project file. +bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine); + +// A new, empty project called name in directory (its .dbp is +// directory\name.dbp, not written until prjSave). +void prjNew(PrjStateT *prj, const char *name, const char *directory); + +// 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); + +// Remove file idx from the project (the file on disk stays). +void prjRemoveFile(PrjStateT *prj, int32_t idx); + +// Rename project file idx to newName, a file name in the project's +// directory whose extension, when given, is the one of the file's kind +// (none: it is added). The file on disk (when it has been saved) is +// renamed and the project file (.dbp) written at once so the two agree; +// on any failure both are put back. An existing file is never +// overwritten. A name that differs only in case changes nothing (DOS +// names ignore case). False with a message in err (errSize bytes). +bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize); + +// Write the .dbp (crash-safe: the old file is replaced only once the new +// one is complete). The file buffers are not written. +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); + +// ============================================================ +// UserControl headers (.ctl) +// ============================================================ +// +// A .ctl file starts with its header, VERSION DVX 1.00 and the block +// "Begin UserControl Name" ... "End" of "Key = Value" lines (keys +// BAS_UC_KEY_*); the class code follows. These read and change one key +// of the block -- or the control's name, key "Name", on its Begin line -- +// and leave every other line of the text as it is. + +// How a header key's value is written. +typedef enum { + PrjCtlNameE, // the control's name (Name, on the Begin UserControl line) + PrjCtlIntE, // a whole number (Width, Height) + PrjCtlBoolE, // True or False (CanGetFocus) + PrjCtlColorE, // &HBBGGRR& (BackColor), "" for none + PrjCtlTextE // a text in quotes (ToolboxIcon, Description, DefaultEvent), "" for none +} PrjCtlValueE; + +typedef struct { + const char *key; // BAS_UC_KEY_* + PrjCtlValueE kind; +} PrjCtlKeyT; + +// The text of a new UserControl called name: VERSION DVX 1.00, its +// header (Begin UserControl name with Width and Height ... End), Option +// Explicit and an empty UserControl_Paint. malloc'd; NULL when name is +// no identifier or memory runs out. +char *prjCtlNew(const char *name); + +// The idx-th header key (0 = the first), NULL past the end: Name, +// Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, +// DefaultEvent. +const PrjCtlKeyT *prjCtlKeyAt(int32_t idx); + +// The header key named key (any case), NULL when it is none. +const PrjCtlKeyT *prjCtlKeyFind(const char *key); + +// The value of header key key of a UserControl's text as the control +// takes it -- the default when the header leaves the key out; a flag +// True or False, a color &HBBGGRR& or "" -- in out (outSize bytes). +// False when the text has no Begin UserControl block or key is not a +// header key. +bool prjCtlHeaderGet(const char *text, const char *key, char *out, int32_t outSize); + +// A copy of a UserControl's text with header key key set to value: its +// line changed, or added before the block's End; "" removes the line (the +// default applies). The value is written the way its kind is (a text in +// quotes, a color &HBBGGRR&, a flag True or False); Name replaces the +// name on the Begin UserControl line. malloc'd; NULL when the text has +// no Begin UserControl block, key is not a header key, the value does not +// suit it (not a whole number, not a color, a text with a quote or a line +// break, a name that is no identifier or "") or memory runs out. +char *prjCtlHeaderSet(const char *text, const char *key, const char *value); + +#endif // BAS_PROJECT_H diff --git a/src/apps/kpunch/dvxbasic/basdev/basSource.c b/src/apps/kpunch/dvxbasic/basdev/basSource.c new file mode 100644 index 00000000..0cba0df0 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basSource.c @@ -0,0 +1,385 @@ +// 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. + +// basSource.c -- BASIC source text read with the compiler's lexer (see +// basSource.h) + +#include "basSource.h" +#include "../compiler/compiler.h" + +#include +#include +#include +#include + +#define SRC_PARAMS_MAX 256 // an Event's parameter list text kept for the callback +#define SRC_QUOTE '"' +#define SRC_DOT '.' +#define SRC_UNDERSCORE '_' + +static const char *const sKindNames[BasSrcKindCountE] = { + [BasSrcKeywordE] = "Keyword", + [BasSrcIdentE] = "Identifier", + [BasSrcStringE] = "String", + [BasSrcNumberE] = "Number", + [BasSrcDateE] = "Date", + [BasSrcSymbolE] = "Symbol", + [BasSrcCommentE] = "Comment", + [BasSrcErrorE] = "Error" +}; + +// ============================================================ +// Prototypes +// ============================================================ + +void basSourceClassEvents(const char *src, BasSourceEventFnT fn, void *ctx); +bool basSourceInCode(const char *src, int32_t srcLen, int32_t pos); +const char *basSourceKindName(BasSrcKindE kind); +char *basSourceRename(const char *src, const char *oldName, const char *newName, bool codeOnly); +int32_t basSourceTokens(const char *src, int32_t srcLen, BasSourceTokenFnT fn, void *ctx); +void basSourceWithEvents(const char *src, BasSourceWithEventsFnT fn, void *ctx); +static BasSrcKindE kindOf(const BasLexerT *lex); +static void lexGuard(BasLexerT *lex); +static void lexNext(BasLexerT *lex); +static void lexStart(BasLexerT *lex, const char *src, int32_t srcLen); +static bool renameMatchAt(const char *src, int32_t srcLen, int32_t i, const char *oldName, int32_t oldLen, bool codeOnly); + + +// The [Public] Event Name[(params)] statements, read with the compiler's +// words so strings and comments never match. +void basSourceClassEvents(const char *src, BasSourceEventFnT fn, void *ctx) { + BasLexerT lex; + bool stmtStart = true; + + lexStart(&lex, src, (int32_t)strlen(src)); + + while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR) { + if (stmtStart && lex.token.type == TOK_PUBLIC) { + lexNext(&lex); + } + + BasTokenTypeE tt = lex.token.type; + bool isEvent = stmtStart && tt == TOK_IDENT && strcasecmp(lex.token.text, BAS_KW_EVENT) == 0; + + if (isEvent) { + lexNext(&lex); + } + + if (isEvent && lex.token.type == TOK_IDENT) { + char name[BAS_MAX_IDENT]; + char params[SRC_PARAMS_MAX] = ""; + + snprintf(name, sizeof(name), "%s", lex.token.text); + lexNext(&lex); + + if (lex.token.type == TOK_LPAREN) { + int32_t start = lex.token.pos + 1; + int32_t depth = 1; + + while (depth > 0) { + lexNext(&lex); + + if (lex.token.type == TOK_EOF || lex.token.type == TOK_NEWLINE || lex.token.type == TOK_ERROR) { + break; + } + + depth += (lex.token.type == TOK_LPAREN) - (lex.token.type == TOK_RPAREN); + } + + int32_t len = lex.token.pos - start; + + if (len > (int32_t)sizeof(params) - 1) { + len = (int32_t)sizeof(params) - 1; + } + + if (len > 0) { + memcpy(params, src + start, (size_t)len); + params[len] = '\0'; + } + } + + fn(ctx, name, params); + tt = lex.token.type; + } + + stmtStart = tt == TOK_NEWLINE || tt == TOK_COLON; + lexNext(&lex); + } +} + + +// Lexes the line holding pos from its start: pos is code unless a string +// literal (or a string without its closing quote) or a comment covers it. +bool basSourceInCode(const char *src, int32_t srcLen, int32_t pos) { + if (!src || pos < 0 || pos >= srcLen) { + return false; + } + + int32_t lineStart = pos; + + while (lineStart > 0 && src[lineStart - 1] != '\n' && src[lineStart - 1] != '\r') { + lineStart--; + } + + BasLexerT lex; + int32_t rel = pos - lineStart; + + lexStart(&lex, src + lineStart, srcLen - lineStart); + + while (lex.token.type != TOK_EOF && lex.token.pos <= rel) { + BasSrcKindE kind = kindOf(&lex); + + if (kind == BasSrcCommentE) { + return false; + } + + if (lex.token.type == TOK_NEWLINE) { + return true; + } + + if (rel < lex.pos) { + return kind != BasSrcStringE && !(kind == BasSrcErrorE && lex.source[lex.token.pos] == SRC_QUOTE); + } + + lexNext(&lex); + } + + return true; +} + + +const char *basSourceKindName(BasSrcKindE kind) { + return (kind >= 0 && kind < BasSrcKindCountE) ? sKindNames[kind] : ""; +} + + +char *basSourceRename(const char *src, const char *oldName, const char *newName, bool codeOnly) { + if (!src || !oldName || !newName || !oldName[0]) { + return NULL; + } + + int32_t oldLen = (int32_t)strlen(oldName); + int32_t newLen = (int32_t)strlen(newName); + int32_t srcLen = (int32_t)strlen(src); + int32_t count = 0; + + // First pass: count the references to size the result + for (int32_t i = 0; i <= srcLen - oldLen; i++) { + if (renameMatchAt(src, srcLen, i, oldName, oldLen, codeOnly)) { + count++; + } + } + + if (count == 0) { + return NULL; + } + + // The new length in 64 bits: many references to a long new name can + // pass what an int32_t offset (and a 32-bit size_t) holds + int64_t outLen = (int64_t)srcLen + (int64_t)count * (newLen - oldLen); + + if (outLen >= INT32_MAX) { + return NULL; + } + + char *out = (char *)malloc((size_t)outLen + 1); + + if (!out) { + return NULL; + } + + int32_t op = 0; + + for (int32_t i = 0; i < srcLen; ) { + if (i <= srcLen - oldLen && renameMatchAt(src, srcLen, i, oldName, oldLen, codeOnly)) { + memcpy(out + op, newName, (size_t)newLen); + op += newLen; + i += oldLen; + continue; + } + + out[op++] = src[i++]; + } + + out[op] = '\0'; + return out; +} + + +int32_t basSourceTokens(const char *src, int32_t srcLen, BasSourceTokenFnT fn, void *ctx) { + BasLexerT lex; + int32_t count = 0; + + if (!src) { + return 0; + } + + lexStart(&lex, src, srcLen); + + while (lex.token.type != TOK_EOF) { + BasSrcKindE kind = kindOf(&lex); + + if (kind != BasSrcKindCountE) { + fn(ctx, kind, lex.token.line, lex.token.col, lex.token.pos, lex.pos - lex.token.pos); + count++; + } + + lexNext(&lex); + } + + return count; +} + + +// One declarator per pass: [WithEvents] name [As type]; commas inside an +// array's bounds do not start a declarator. +void basSourceWithEvents(const char *src, BasSourceWithEventsFnT fn, void *ctx) { + BasLexerT lex; + bool stmtStart = true; + + lexStart(&lex, src, (int32_t)strlen(src)); + + while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR) { + BasTokenTypeE tt = lex.token.type; + + if (!stmtStart || (tt != TOK_DIM && tt != TOK_PRIVATE && tt != TOK_PUBLIC)) { + stmtStart = tt == TOK_NEWLINE || tt == TOK_COLON; + lexNext(&lex); + continue; + } + + do { + lexNext(&lex); + + if (lex.token.type == TOK_IDENT && strcasecmp(lex.token.text, BAS_KW_WITHEVENTS) == 0) { + lexNext(&lex); + + if (lex.token.type == TOK_IDENT) { + char name[BAS_MAX_IDENT]; + + snprintf(name, sizeof(name), "%s", lex.token.text); + lexNext(&lex); + + if (lex.token.type == TOK_AS) { + lexNext(&lex); + + if (lex.token.type == TOK_IDENT) { + fn(ctx, name, lex.token.text); + } + } + } + } + + int32_t depth = 0; + + while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR && lex.token.type != TOK_NEWLINE && lex.token.type != TOK_COLON && (depth > 0 || lex.token.type != TOK_COMMA)) { + depth += (lex.token.type == TOK_LPAREN) - (lex.token.type == TOK_RPAREN); + lexNext(&lex); + } + } while (lex.token.type == TOK_COMMA); + + stmtStart = false; + } +} + + +// The kind of the lexer's current token; BasSrcKindCountE for a line end +// (a comment ends its line too: its line-end token starts at the ' or +// REM). +static BasSrcKindE kindOf(const BasLexerT *lex) { + BasTokenTypeE type = lex->token.type; + + switch (type) { + case TOK_INT_LIT: + case TOK_LONG_LIT: + case TOK_FLOAT_LIT: + case TOK_CUR_LIT: + return BasSrcNumberE; + + case TOK_STRING_LIT: + return BasSrcStringE; + + case TOK_DATE_LIT: + return BasSrcDateE; + + case TOK_IDENT: + return BasSrcIdentE; + + case TOK_ERROR: + return BasSrcErrorE; + + case TOK_EOF: + return BasSrcKindCountE; + + case TOK_NEWLINE: { + char c = lex->token.pos < lex->sourceLen ? lex->source[lex->token.pos] : '\n'; + + return (c == '\n' || c == '\r') ? BasSrcKindCountE : BasSrcCommentE; + } + + default: + return (type >= TOK_FIRST_KEYWORD && type <= TOK_LAST_KEYWORD) ? BasSrcKeywordE : BasSrcSymbolE; + } +} + + +// A token that did not move the lexer (an error on a character it cannot +// read) is stepped over, so every scan of any text ends. +static void lexGuard(BasLexerT *lex) { + if (lex->token.type == TOK_ERROR && lex->pos == lex->token.pos && lex->pos < lex->sourceLen) { + lex->pos++; + lex->col++; + } +} + + +static void lexNext(BasLexerT *lex) { + basLexerNext(lex); + lexGuard(lex); +} + + +static void lexStart(BasLexerT *lex, const char *src, int32_t srcLen) { + basLexerInit(lex, src, srcLen); + lexGuard(lex); +} + + +// oldName at src[i]: a whole word start followed by '.' or '_', and code +// when codeOnly. +static bool renameMatchAt(const char *src, int32_t srcLen, int32_t i, const char *oldName, int32_t oldLen, bool codeOnly) { + if (strncasecmp(&src[i], oldName, (size_t)oldLen) != 0) { + return false; + } + + char after = src[i + oldLen]; + + if (after != SRC_DOT && after != SRC_UNDERSCORE) { + return false; + } + + if (i > 0 && basIsIdentChar(src[i - 1])) { + return false; + } + + return !codeOnly || basSourceInCode(src, srcLen, i); +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basSource.h b/src/apps/kpunch/dvxbasic/basdev/basSource.h new file mode 100644 index 00000000..203cf486 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basSource.h @@ -0,0 +1,92 @@ +// 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. + +// basSource.h -- BASIC source text read with the compiler's lexer +// +// What a code editor needs to know about source text without compiling +// it: its tokens, whether a position is code (not inside a string +// literal or a comment), renaming a form or control in code, the +// WithEvents variables a file declares and the Events a class module +// declares. The IDE uses these, and BASIC programs reach them through +// compiler.bas (CompTokens$, CompInCode, CompRenameInCode$, +// CompWithEvents$, CompClassEvents$). Everything works line by line on +// text that need not compile. + +#ifndef BAS_SOURCE_H +#define BAS_SOURCE_H + +#include "../compiler/lexer.h" + +#include +#include + +// What a token of BasSourceTokenFnT is +typedef enum { + BasSrcKeywordE, // a reserved word (IF, DIM, PRINT ...) + BasSrcIdentE, // a name (also the words a program may reuse: INTEGER, CLS ...) + BasSrcStringE, // a string literal, quotes included + BasSrcNumberE, // a number literal (&H and & / # suffixes included) + BasSrcDateE, // a #date# literal + BasSrcSymbolE, // an operator or punctuation (. , ; : ( ) + = ...) + BasSrcCommentE, // ' or REM up to the end of the line + BasSrcErrorE, // text the lexer cannot read (e.g. a string without its closing quote) + BasSrcKindCountE +} BasSrcKindE; + +// One token: its kind, 1-based line and column, and the source text it +// covers (src + pos, len bytes). +typedef void (*BasSourceTokenFnT)(void *ctx, BasSrcKindE kind, int32_t line, int32_t col, int32_t pos, int32_t len); + +// One WithEvents variable: its name and its class. +typedef void (*BasSourceWithEventsFnT)(void *ctx, const char *name, const char *className); + +// One Event of a class module: its name and its parameter list text ("" +// when it has none). +typedef void (*BasSourceEventFnT)(void *ctx, const char *name, const char *params); + +// The Events a class module declares ([Public] Event Name[(params)]). +void basSourceClassEvents(const char *src, BasSourceEventFnT fn, void *ctx); + +// True when byte offset pos of src is code: not inside a string literal +// or a comment. Only the line holding pos is read. +bool basSourceInCode(const char *src, int32_t srcLen, int32_t pos); + +// The name of a BasSrcKindE as BASIC lists it ("Keyword", "Identifier" ...). +const char *basSourceKindName(BasSrcKindE kind); + +// Rename references to a form or control oldName in src: oldName (any +// case) as a whole word followed by '.' (Command1.Caption, +// Form1.Command1.Text) or '_' (Command1_Click, Form1_Load). With +// codeOnly, strings and comments are left alone. Returns the new text +// (malloc'd), or NULL when nothing matched, the result would not fit an +// int32_t length or memory runs out. +char *basSourceRename(const char *src, const char *oldName, const char *newName, bool codeOnly); + +// Every token of src (srcLen bytes) in order; line ends are not +// reported. Returns how many tokens were reported. +int32_t basSourceTokens(const char *src, int32_t srcLen, BasSourceTokenFnT fn, void *ctx); + +// The WithEvents variables src declares (Dim / Private / Public +// WithEvents name As Class; several declarators per statement allowed). +void basSourceWithEvents(const char *src, BasSourceWithEventsFnT fn, void *ctx); + +#endif // BAS_SOURCE_H diff --git a/src/apps/kpunch/dvxbasic/basdev/basValidate.c b/src/apps/kpunch/dvxbasic/basdev/basValidate.c new file mode 100644 index 00000000..bf0633a2 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basValidate.c @@ -0,0 +1,308 @@ +// The MIT License (MIT) +// +// Copyright (C) 2026 Scott Duensing +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// basValidate.c -- compile-time CtrlName.Member validator (see basValidate.h) + +#include "basValidate.h" +#include "../formrt/formrt.h" +#include "../formrt/frmParser.h" +#include "dvxWgt.h" + +#include "stb_ds_wrap.h" + +#include +#include +#include + +#define VALIDATE_FORM_TYPE FRM_KW_FORM // type name of a form +#define VALIDATE_MENU_TYPE FRM_KW_MENU // type name of a menu item + +// One basValidateScanFrm parse: the validator and the code hook. +typedef struct { + BasValidatorT *v; + BasFrmCodeFnT onCode; + void *codeCtx; +} ValidateScanT; + +// Prototypes (alphabetical) +static void addEntry(BasValidatorT *v, const char *name, const char *wgtType); +void basValidateAddType(BasValidatorT *v, const char *typeName); +void basValidateCallbacks(BasValidatorT *v, BasCtrlValidatorT *out); +void basValidateFree(BasValidatorT *v); +bool basValidateIsMethodValid(void *ctx, const char *wgtType, const char *methodName); +bool basValidateIsPropValid(void *ctx, const char *wgtType, const char *propName); +const char *basValidateLookupCtrlType(void *ctx, const char *ctrlName); +void basValidateScanFrm(BasValidatorT *v, const char *text, int32_t len, BasFrmCodeFnT onCode, void *codeCtx); +static bool isFormOrPicture(const char *wgtType); +static const WgtIfaceT *liveIface(const BasValidatorT *v, const char *wgtType); +static void onCtrlBegin(void *ud, const char *typeName, const char *name); +static bool onFormBegin(void *ud, const char *name); +static void onFormEnd(void *ud, const char *trailingSrc, int32_t trailingLen); +static void onMenuBegin(void *ud, const char *name, int32_t level); +static bool ownType(const BasValidatorT *v, const char *typeName); + + +static void addEntry(BasValidatorT *v, const char *name, const char *wgtType) { + BasCtrlMapEntryT e; + + memset(&e, 0, sizeof(e)); + snprintf(e.name, sizeof(e.name), "%s", name ? name : ""); + snprintf(e.wgtType, sizeof(e.wgtType), "%s", wgtType ? wgtType : ""); + snprintf(e.formName, sizeof(e.formName), "%s", v->currentForm); + arrput(v->entries, e); +} + + +void basValidateAddType(BasValidatorT *v, const char *typeName) { + BasOwnTypeT t; + + snprintf(t.name, sizeof(t.name), "%s", typeName ? typeName : ""); + arrput(v->ownTypes, t); +} + + +void basValidateCallbacks(BasValidatorT *v, BasCtrlValidatorT *out) { + out->lookupCtrlType = basValidateLookupCtrlType; + out->isMethodValid = basValidateIsMethodValid; + out->isPropValid = basValidateIsPropValid; + out->ctx = v; +} + + +void basValidateFree(BasValidatorT *v) { + arrfree(v->entries); + arrfree(v->badTypes); + arrfree(v->ownTypes); + v->entries = NULL; + v->badTypes = NULL; + v->ownTypes = NULL; +} + + +bool basValidateIsMethodValid(void *ctx, const char *wgtType, const char *methodName) { + if (!wgtType || !methodName) { + return true; // be permissive on malformed input + } + + if (basFormRtCommonMethodId(methodName) != BAS_CM_NONE) { + return true; + } + + // Drawing methods of forms and PictureBoxes + if (isFormOrPicture(wgtType) && basFormRtIsDrawMember(methodName, false)) { + return true; + } + + // Form-level methods + if (strcasecmp(wgtType, VALIDATE_FORM_TYPE) == 0) { + return basFormRtIsFormMethod(methodName); + } + + // Menu items have no methods beyond common + if (strcasecmp(wgtType, VALIDATE_MENU_TYPE) == 0) { + return false; + } + + const WgtIfaceT *iface = liveIface((const BasValidatorT *)ctx, wgtType); + + if (!iface) { + return true; // unknown type -- skip validation + } + + // A known type with no interface methods accepts only the common + // set (already checked above); being permissive here let typos + // like btnGo.Boggle compile. + for (int32_t i = 0; i < iface->methodCount; i++) { + if (strcasecmp(iface->methods[i].name, methodName) == 0) { + return true; + } + } + + return false; +} + + +bool basValidateIsPropValid(void *ctx, const char *wgtType, const char *propName) { + if (!wgtType || !propName) { + return true; + } + + // Drawing properties of forms and PictureBoxes + if (isFormOrPicture(wgtType) && basFormRtIsDrawMember(propName, true)) { + return true; + } + + // Form-level properties (the runtime's own table) + if (strcasecmp(wgtType, VALIDATE_FORM_TYPE) == 0) { + return basFormRtFindFormProp(propName) != NULL; + } + + // Menu items (the runtime's own table) + if (strcasecmp(wgtType, VALIDATE_MENU_TYPE) == 0) { + return basFormRtFindMenuProp(propName) != NULL; + } + + // Properties every non-menu control accepts (the runtime's own table) + if (basFormRtFindCommonProp(propName)) { + return true; + } + + const WgtIfaceT *iface = liveIface((const BasValidatorT *)ctx, wgtType); + + // Same reasoning as basValidateIsMethodValid: an empty interface + // property table must not turn validation off for the whole type. + return !iface || wgtIfaceFindProp(iface, propName) != NULL; +} + + +const char *basValidateLookupCtrlType(void *ctx, const char *ctrlName) { + BasValidatorT *v = (BasValidatorT *)ctx; + const char *found = NULL; + bool ambiguous = false; + + if (!v || !ctrlName) { + return NULL; + } + + const char *scope = v->compiler ? basCompilerFormScope(v->compiler) : ""; + + for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) { + const BasCtrlMapEntryT *e = &v->entries[i]; + + if (strcasecmp(e->name, ctrlName) != 0) { + continue; + } + + if (scope[0] && strcasecmp(e->formName, scope) == 0) { + return e->wgtType; + } + + ambiguous = ambiguous || (found && strcasecmp(found, e->wgtType) != 0); + found = e->wgtType; + } + + return ambiguous ? NULL : found; +} + + +void basValidateScanFrm(BasValidatorT *v, const char *text, int32_t len, BasFrmCodeFnT onCode, void *codeCtx) { + ValidateScanT scan; + FrmParserCbsT cbs; + + scan.v = v; + scan.onCode = onCode; + scan.codeCtx = codeCtx; + + memset(&cbs, 0, sizeof(cbs)); + cbs.userData = &scan; + cbs.onFormBegin = onFormBegin; + cbs.onCtrlBegin = onCtrlBegin; + cbs.onMenuBegin = onMenuBegin; + cbs.onFormEnd = onFormEnd; + frmParse(text, len, &cbs); +} + + +// Forms and PictureBoxes take the drawing members. +static bool isFormOrPicture(const char *wgtType) { + return strcasecmp(wgtType, VALIDATE_FORM_TYPE) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0; +} + + +// The interface a loaded widget library registers for the BASIC type +// name wgtType, or NULL when none does or the program defines the type +// itself (v may be NULL): a registered control of that name is then an +// earlier build of it (the IDE's designer) or another program's, so its +// members say nothing about this one's. +static const WgtIfaceT *liveIface(const BasValidatorT *v, const char *wgtType) { + const char *wgtName = (v && ownType(v, wgtType)) ? NULL : wgtFindByBasName(wgtType); + + return wgtName ? wgtGetIface(wgtName) : NULL; +} + + +static void onCtrlBegin(void *ud, const char *typeName, const char *name) { + BasValidatorT *v = ((ValidateScanT *)ud)->v; + + if (!v) { + return; + } + + addEntry(v, name, typeName); + + // Flag a type no loaded widget library registers and the program + // does not define (the runtime would silently drop the control and + // fail at its first use). Forms and menus are structural, not + // widgets. + if (typeName && typeName[0] && strcasecmp(typeName, VALIDATE_FORM_TYPE) != 0 && strcasecmp(typeName, VALIDATE_MENU_TYPE) != 0 && !wgtFindByBasName(typeName) && !ownType(v, typeName)) { + BasBadTypeT bad; + + memset(&bad, 0, sizeof(bad)); + snprintf(bad.ctrlName, sizeof(bad.ctrlName), "%s", name ? name : "?"); + snprintf(bad.typeName, sizeof(bad.typeName), "%s", typeName); + snprintf(bad.formName, sizeof(bad.formName), "%s", v->currentForm); + arrput(v->badTypes, bad); + } +} + + +static bool onFormBegin(void *ud, const char *name) { + BasValidatorT *v = ((ValidateScanT *)ud)->v; + + if (v) { + snprintf(v->currentForm, sizeof(v->currentForm), "%s", name ? name : ""); + addEntry(v, v->currentForm, VALIDATE_FORM_TYPE); + } + + return true; +} + + +static void onFormEnd(void *ud, const char *trailingSrc, int32_t trailingLen) { + ValidateScanT *scan = (ValidateScanT *)ud; + + if (scan->onCode) { + scan->onCode(scan->codeCtx, trailingSrc, trailingLen); + } +} + + +static void onMenuBegin(void *ud, const char *name, int32_t level) { + (void)level; + BasValidatorT *v = ((ValidateScanT *)ud)->v; + + if (v) { + addEntry(v, name, VALIDATE_MENU_TYPE); + } +} + + +// True when the program defines control type typeName itself. +static bool ownType(const BasValidatorT *v, const char *typeName) { + for (int32_t i = 0; i < (int32_t)arrlen(v->ownTypes); i++) { + if (strcasecmp(v->ownTypes[i].name, typeName) == 0) { + return true; + } + } + + return false; +} diff --git a/src/apps/kpunch/dvxbasic/basdev/basValidate.h b/src/apps/kpunch/dvxbasic/basdev/basValidate.h new file mode 100644 index 00000000..491f6b67 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basValidate.h @@ -0,0 +1,116 @@ +// 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. + +// basValidate.h -- compile-time CtrlName.Member validator +// +// Inside DVX the widget libraries are loaded, so their interface tables +// say which properties and methods every control type has. Together +// with a scan of the program's .frm files (control name -> widget type) +// the compiler can reject typos like LblStatus.Caphtion at compile time +// instead of at the first click. Controls made at run time +// (CreateControl) are not in the map and are not checked. +// +// Used by the IDE (ideMain.c) and the basdev library (basDevBas.c). +// The command-line compiler (bascomp) does not validate: it runs outside +// DVX where no widget library is loaded, so it cannot know the types; the +// runtime reports such typos when the line runs. + +#ifndef BAS_VALIDATE_H +#define BAS_VALIDATE_H + +#include "../compiler/compiler.h" + +#include +#include + +// One line of the unknown-type report (form, control, type). +#define BAS_VALIDATE_BAD_TYPE_FMT "Form '%s' control '%s': type '%s' is not registered." + +// Advice printed after the unknown-type lines. +#define BAS_VALIDATE_BAD_TYPE_HINT "DVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown)." + +// A control, form or menu item a scanned .frm declares. +typedef struct { + char name[BAS_MAX_IDENT]; + char wgtType[BAS_MAX_IDENT]; // "Form" for the form itself, "Menu" for a menu item, else the type's BASIC name + char formName[BAS_MAX_IDENT]; // form that declares it (a form: its own name) +} BasCtrlMapEntryT; + +// A control whose type no loaded widget library registers. +typedef struct { + char ctrlName[BAS_MAX_IDENT]; + char typeName[BAS_MAX_IDENT]; + char formName[BAS_MAX_IDENT]; +} BasBadTypeT; + +// A control type the program defines itself (a UserControl of the +// project). +typedef struct { + char name[BAS_MAX_IDENT]; +} BasOwnTypeT; + +// Validator state: zero it (memset) before the first scan, release it +// with basValidateFree. +typedef struct { + BasCtrlMapEntryT *entries; // stb_ds: every declared name + BasBadTypeT *badTypes; // stb_ds: unregistered control types + BasOwnTypeT *ownTypes; // stb_ds: the program's own control types (basValidateAddType) + const BasCompilerT *compiler; // compile in progress (its form scope), or NULL + char currentForm[BAS_MAX_IDENT]; // the last Begin Form seen +} BasValidatorT; + +// Called with the code that follows a scanned form's outer End. +typedef void (*BasFrmCodeFnT)(void *ctx, const char *trailingSrc, int32_t trailingLen); + +// Add a control type the program defines itself (a UserControl of the +// project), before the .frm files are scanned: controls of that type are +// not reported as unknown, and their members are not checked (a +// registered control of that name -- the IDE designer's or another +// program's -- may be an older build of it; the runtime reports a wrong +// member when the line runs). +void basValidateAddType(BasValidatorT *v, const char *typeName); + +// The compiler's callbacks for v (fills out; v must outlive the compile). +void basValidateCallbacks(BasValidatorT *v, BasCtrlValidatorT *out); + +// Release what the scans collected (v stays usable). +void basValidateFree(BasValidatorT *v); + +// True when methodName is a method of controls of type wgtType (a form: +// "Form"; a menu item: "Menu"). Unknown types are not checked (true). +bool basValidateIsMethodValid(void *ctx, const char *wgtType, const char *methodName); + +// True when propName is a property of controls of type wgtType. +bool basValidateIsPropValid(void *ctx, const char *wgtType, const char *propName); + +// The widget type of ctrlName for the compiler's member checks, or NULL +// to skip them: a name declared on the form whose code is being compiled +// wins; any other name is looked up across every form and skipped when +// different forms give it different types. +const char *basValidateLookupCtrlType(void *ctx, const char *ctrlName); + +// Parse one .frm text (len bytes): v (may be NULL: collect nothing) +// gets its form, controls and menu items, and onCode (may be NULL) the +// code after the form's outer End. +void basValidateScanFrm(BasValidatorT *v, const char *text, int32_t len, BasFrmCodeFnT onCode, void *codeCtx); + +#endif // BAS_VALIDATE_H diff --git a/src/apps/kpunch/dvxbasic/basdev/basdev.dep b/src/apps/kpunch/dvxbasic/basdev/basdev.dep new file mode 100644 index 00000000..5d3708cb --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basdev.dep @@ -0,0 +1,2 @@ +basrt +libdvx diff --git a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs new file mode 100644 index 00000000..b3f7fc2e --- /dev/null +++ b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs @@ -0,0 +1,127 @@ +# 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. + +.section Technical Reference (for Developers)|Libraries +.topic lib.basdev +.title BASIC Development Library +.toc 0 BASIC Development Library +.index basdev +.index BASIC Development Library +.index basBuildCompile +.index basBuildAddSource +.index basBuildMapFileLine +.index basValidateScanFrm +.index basDbgLocals +.index basDbgEval +.index basDbgExec +.index basRunHostCreate +.index basRunHostSlice +.index basSourceTokens +.index basSourceRename +.index basHelpKeywordTopic +.index basProject.h +.index PrjStateT +.index prjLoad +.index compiler.bas +.index debug.bas + +.h1 BASIC Development Library + +basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h) and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub and debug.bas share. + +Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h + +.h2 Compiling (basBuild.h) + +.table + Function Description + -------- ----------- + basBuildAddSource Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map. + basBuildConcatProject Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource. + basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY or BAS_COMPILE_NO_MODULE. + basBuildErrorText A compiler message without its "Line N, Col M: " prefix. + basBuildMapLine Line of the concatenated source to file and file line. + basBuildMapFileLine File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike). + basBuildSpecFromProject Fill a BasBuildSpecT's project metadata from an open .dbp. + basBuildApp Build an .app from a module, the .frm texts and the .ctl headers (ctlSources). A NULL selfPath takes the stub from BAS_BUILD_STUB_CARRIER (this library). + basBuildControl Build a control file (.wgt, resources only) from a control project's module and .ctl headers: the module, each header as CTLn with its toolbox icon24 and name resources, the project's metadata and help file. The IDE's Make Control and CompBuildApp of a control project. + basBuildIsControlProject True when an open .dbp is a control project ([Project] Type = Control). + basBuildWrapForPath The kind of a project file by its extension. +.endtable + +.h2 Validating Control Members (basValidate.h) + +A BasValidatorT (zeroed) collects the forms, controls and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there. + +.h2 Debugging (basDebug.h) + +Every function takes the paused program's VM (or NULL when there is none). + +.table + Function Description + -------- ----------- + basDbgCurProcIdx The compiled procedure the paused VM is in (-1: module level). + basDbgProcNameForPc Procedure name holding a code address (BAS_DBG_MODULE_FRAME before every procedure). + basDbgVarVisible Whether a debug variable is in scope where the VM is paused (QBasic rules: SHARED, STATIC, form and member variables). + basDbgFindVar The visible debug variable of a name. + basDbgVarSlot The live value slot of a debug variable. + basDbgResolvePath The live slot of var, var(i, j), var.field or var(i).field (a TYPE field or a class member). + basDbgFormatValue Display text of a value (strings quoted, #dates#, array bounds, class names, Collection item counts). + basDbgLocals The visible variables as rows (name, type, value) through a BasDbgRowFnT. + basDbgCallStack The call stack as rows (procedure, line). + basDbgEval A watch: a variable path read directly, any other expression compiled and run as a snippet. + basDbgExec An Immediate-window line: assignment to the paused program's variable, a statement, or an expression to print. + basDbgSetVar Set a variable path to the value of an expression (scalars). + basDbgSnippetVm Compile a snippet whose globals are the paused program's visible variables, with their values copied in. + basDbgRunSnippet Run a snippet in bounded slices (an endless loop is abandoned). + basDbgAppendDropping Append to a fixed output buffer, dropping the oldest lines. +.endtable + +.h2 Procedures (basProcs.h) + +basProcDeclAt, basProcSkipToEnd and basProcNameFromDecl find the SUB / FUNCTION blocks of source text without compiling it (BAS_PROC_KW_END_SUB / BAS_PROC_KW_END_FUNCTION end them). The IDE's one-procedure-at-a-time editor and CompProcList$ use them. + +.h2 Source Text (basSource.h) + +Source text read with the compiler's lexer, line by line, on text that need not compile: basSourceTokens reports every token with its kind (BasSrcKindE: keyword, identifier, string, number, date, symbol, comment, error), line, column and source span; basSourceInCode tells whether an offset is code (not in a string literal or a comment); basSourceRename renames a form or control (oldName followed by '.' or '_' as a whole word), optionally only in code; basSourceWithEvents and basSourceClassEvents report the WithEvents variables and the Events of a class module. The IDE's rename (ideRenameInCode, also after the Menu Editor renames items) and its Object / Event lists use them; compiler.bas has CompTokens$, CompInCode, CompRenameInCode$, CompWithEvents$ and CompClassEvents$. + +.h2 Help Topics (basHelpMap.h) + +basHelpKeywordTopic gives the DVX BASIC help topic of a keyword or built-in function (NULL for none) and basHelpCtrlTopic the topic of a control type (BAS_HELP_CTRL_PREFIX and the type name in lower case): what F1 opens in the IDE, and CompHelpTopic$ / CompHelpCtrlTopic$. + +.h2 Projects (basProject.h) + +The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl) or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjGet$ and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags. + +A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$ and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order). + +.h2 Running a Program (basRunHost.h, in basrt.lib) + +basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path and its CONFIG / DATA folders, COMMAND$, the print, input and DoEvents callbacks, the screen window's name, the help file and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs. + +VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine. + +.h2 From BASIC + +compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound. + +CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it. diff --git a/src/apps/kpunch/dvxbasic/basrt.dhs b/src/apps/kpunch/dvxbasic/basrt.dhs index fe2b31b5..62b94bbb 100644 --- a/src/apps/kpunch/dvxbasic/basrt.dhs +++ b/src/apps/kpunch/dvxbasic/basrt.dhs @@ -39,6 +39,7 @@ Headers: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h .link lib.basrt.values Value System (values.h) .link lib.basrt.vm Virtual Machine (vm.h) +.link lib.basrt.frmmodel The .frm Model (frmModel.h) .topic lib.basrt.values .title Value System @@ -385,6 +386,7 @@ void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui); hideForm void (*)(ctx, formRef) Hide a form (keep in memory). msgBox int32_t (*)(ctx, message, flags) Display a message box. inputBox BasStringT *(*)(ctx, prompt, title, defaultText) Display an input box. + loadFormText void *(*)(ctx, text) LoadFormText: a form from .frm text (its own copy when the name is loaded), NULL when it does not load. ctx void * User pointer passed to all callbacks. .endtable @@ -429,16 +431,33 @@ void basVmSetExternCallbacks(BasVmT *vm, .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. +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, a BINARY STRING (binaryMask bit i set for argument i) as the char pointer followed by an int32_t length, SINGLE as one 4-byte float, DOUBLE as 8 bytes, everything else as one int32_t. When binaryMask has BAS_EXTERN_BINARY_RET the result is a BINARY STRING: an int32_t * is pushed last and the result is that many bytes at the returned pointer. DOUBLE and SINGLE returns are read from the x87 ST(0) register; INTEGER returns keep only the low 16 bits. .table - Field Signature Description - ----- --------- ----------- - resolveExtern void *(*)(ctx, libName, funcName) Resolve a native function by library and symbol name. Cached after first call. - 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. + Field Signature Description + ----- --------- ----------- + resolveExtern void *(*)(ctx, libName, funcName) Resolve a native function by library and symbol name. Cached after first call. + callExtern BasValueT (*)(ctx, funcPtr, libName, funcName, args, argc, retType, binaryMask) Call a resolved native function, marshalling arguments and return value. + ctx void * User pointer passed to both callbacks. .endtable +A native SUB is called with OP_CALL_EXTERN like a FUNCTION and leaves a dummy result on the stack; a call written as a statement drops it, as it drops a FUNCTION's result. + +.h3 Terminal Controls for Libraries + +A library's BASIC functions can show a byte stream on a Terminal control the way SerAttach and CommAttach do (the connection library's ConnAttach is one). The form runtime (formrt.h, exported from basrt) keeps the binding safe: + +.code +typedef int32_t (*BasTermWriteFnT)(void *ctx, const uint8_t *data, int32_t len); +typedef void (*BasTermUnbindFnT)(void *ctx); + +WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind); +void basFormRtTermUnbind(void *ctx); +void basFormRtTermForget(void *ctx); +.endcode + +basFormRtTermBind finds the Terminal control ctrlName on the running program's forms, ends whatever fed it before (a serial port, a packet link, another binding) and ctx's own earlier binding, sends what is typed there to writeFn, and returns the widget for the library to write what arrives to (NULL when there is no Terminal of that name). When the control goes away -- its form unloads, it is removed, another binding takes it -- the runtime ends the binding and calls onUnbind(ctx): the library must forget the widget. basFormRtTermUnbind ends a binding the library no longer wants; basFormRtTermForget drops it without touching the widget, for a library whose program was ended (its windows are already gone). A library outside basrt looks these up with dlsym when it first needs them. + .h2 Form Context Set the active form context during event dispatch. @@ -449,6 +468,55 @@ void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count); .endcode +.h2 Collections for the Host + +A host hands BASIC a Collection of its own objects (the form runtime's Form.Controls) with these; basValInstance of the new Collection takes its one reference. + +.code +BasObjectT *basVmCollectionNew(BasVmT *vm); +int32_t basVmCollectionAdd(BasObjectT *coll, BasValueT item, const char *key); // key NULL: none; 0 or the BASIC error number +.endcode + +.h2 Control Types and the Design Surface + +formrt/formDesign.c holds what a form designer needs, shared by the DVX BASIC IDE's designer and by designer.bas (DECLARE LIBRARY "basrt": CtlTypeCount, CtlTypeName, CtlTypeFlags, CtlTypeNamePrefix, CtlTypeDefaultEvent, CtlTypeTooltip, CtlTypeIconFile, CtlPropCount, CtlPropName, CtlPropType, CtlPropEnum, CtlMethodList, CtlEventList, CtlEventParams, CtlTypeEventParams, and FrmFormName, FrmControlNames, FrmCode, ClsClassName over the basRes.h scanners). CtlTypeTooltip and CtlTypeIconFile fall back to a UserControl's header (its Description; its ToolboxIcon file, basUcIconPath) when the type has no widget file: + +.table + Function Description + -------- ----------- + basCtlIface(typeName) The widget interface of a BASIC type name, or NULL. + basCtlPropAt(typeName, idx, out) / basCtlPropFind(...) A type's properties as BasCtlPropT (name, type, readable, writable, enum names): the runtime table's ("Form", "Menu" or the common one), the widget's own winning on a name, then the widget's others. The IDE's properties window uses basCtlPropFind. + basCtlMethodAt / basCtlEventAt(typeName, idx) A type's methods (the widget's, then the common ones) and events (the master BAS_EVENT_LIST's for that kind of object, then the widget's); the IDE's event list uses basCtlEventAt. + basCtlEventParams(typeName, eventName) The parameter list of a handler of a type's event: the widget's own (WgtEventDescT params), else the master list's; typeName NULL searches every widget after the master list, BAS_UC_OBJECT ("UserControl", the object in a UserControl's own code) has BAS_UC_EVENT_LIST's alone. The IDE's handler skeletons, CtlEventParams and CtlTypeEventParams. + basCtlDefaultEvent, basCtlNamePrefix, basCtlIsContainer As the toolbox and designer need them. + basCtlResPath(typeName, resBase, resName, size) The widget file and the (suffixed) resource name of its toolbox icon (BAS_CTL_RES_ICON) or tooltip (BAS_CTL_RES_NAME). + basCtlTypeAt(rt, idx) The idx-th control type rt sees (basUcIfaceVisible; rt NULL: a C host) in alphabetical order of BASIC name, NULL past the end. The IDE's toolbox and CtlTypeName list the types in this order; the registry's own (wgtIfaceAt) is the order the widget files loaded in. + basDsgnHandleOrigin / basDsgnHandleAt / basDsgnCursorAt Selection handle geometry: where handle h of a widget is, the resize handle under a point, the mouse pointer there. + basDsgnPaintHandles(ctx, win, w) Paints a widget's eight selection handles over a window's painted content. + basDsgnPropertyChanged(w, propName) The code of UserControl widget w called PropertyChanged: a form in DesignMode holding it raises DesignPropertyChanged (CtrlName, PropertyName). formCtl.c calls it beside the C hook (basUcSetPropChangedHook). + basDsgnWidgetHit(w, x, y) True when a point is on a visible widget. +.endtable + +Form.DesignMode replaces the form window's mouse, key, paint and cursor handlers while it is True (the controls get no input, and fireCtrlEvent drops their events), raises DesignMouse, DesignKey and DesignPropertyChanged, and paints DesignSelection's handles after the widgets. Form.Controls, Control.Parent / Container and MoveBefore (basFormRtMoveControl) are dispatched there too. + +.h2 UserControls for a Host + +formrt/formCtl.c registers UserControls (widget classes written in BASIC) with the widget system. A program's own controls (basFormRtRegisterCtl, the stub and the IDE's runs) and a designer's (basCtlLoadDesign) get private registry keys, the class name plus a serial, so the same project running and being designed, or two programs, never share one; a control file's (basCtlLoadFile) keeps its name. Resolve names through these instead of wgtGetIface: + +.table + Function Description + -------- ----------- + basCtlLoadDesign(rt, module, headers, count, path) Designer rt's controls (rt: a BASIC program designing forms, compiler.bas CompDesignControls; NULL: a C host, the IDE): the .ctl headers' classes of module (the caller keeps it until basCtlUnloadDesign or rt's end), run without the module-level code, App.Path the folder of path. Returns how many were registered (a name a native or installed control has, or rt's own control has, is not). The set rt registered before is let go first. + basCtlUnloadDesign(rt) Lets designer rt's controls go (their widgets keep their last picture and run no more code). False while their code runs. basUcRuntimeEnd lets a program's go when it ends. + basUcIconPath(iface, out, outSize) The ToolboxIcon picture file of a control file's or a designer's UserControl: the header's name in the folder of the path it was registered with (the project's, for a designer's). False when it names none or iface is no such control. + basUcResolveType(rt, typeName) The registry key of a BASIC type name in rt's program: its own UserControl first (its program's or the ones it registered for design; rt NULL: the C host's design controls), else wgtResolveTypeName -- never another owner's private control (NULL then). + basUcOwnIface(rt, typeName) The interface of rt's own UserControl (its program's or a design one; rt NULL: the C host's design controls), NULL for none. basCtlIface looks here first, for the current program (basFormRtCurrent). + basUcIfaceVisible(rt, iface) False when iface is a private UserControl of another owner; a list of control types (basCtlTypeAt) leaves those out. + basUcIfaceHeader(iface) The parsed header (BasUcHeaderT: name, size, BackColor, ToolboxIcon, Description, DefaultEvent) of a UserControl's interface, NULL for any other. + basUcSetPropChangedHook(fn) fn(widget, propName) runs when a control's code calls PropertyChanged: a C designer reads its property sheet again (a BASIC one gets DesignPropertyChanged, basDsgnPropertyChanged). + basDsgnSetDesignWindow(win, on) / basDsgnIsDesignWindow Marks a window as a designer's (unmark it when it is destroyed): UserControls on it see UserMode False and get no user input. +.endtable + .h2 Stack Access Push and pop values on the evaluation stack for host integration. @@ -466,7 +534,9 @@ Both return true on success, false on stack overflow/underflow. const char *basVmGetError(const BasVmT *vm); .endcode -Returns the current error message string. Valid after basVmRun returns BAS_VM_ERROR. +Returns the current error message string. Valid after basVmRun returns BAS_VM_ERROR, and after a call of a SUB from the host (an event handler) that an untrapped error stopped. + +The form runtime ends the program on every untrapped error, in module-level code and in event handlers alike (basFormRtRuntimeError for errors the runtime finds, the same path for the VM's own): the error is logged to DVX.LOG (summary line, details indented), the VM halts (ended), the runtime is marked terminated so its event loop ends, and the BASIC Runtime Error box shows it -- unless the host set suppressErrorDialog (the IDE), which reports it itself. The VM's message ("Runtime error 11 on line 8: Division by zero") stays readable with basVmGetError. .h2 Sub/Function Calls from Host @@ -500,18 +570,34 @@ void basVmStepInto(BasVmT *vm); void basVmStepOver(BasVmT *vm); void basVmStepOut(BasVmT *vm); void basVmRunToCursor(BasVmT *vm, int32_t line); +void basVmClearStepState(BasVmT *vm); +void basVmSetBreakpointCallback(BasVmT *vm, + BasBreakpointFnT fn, void *ctx); +void basVmSetDebugPaused(BasVmT *vm, bool paused); +void basVmResume(BasVmT *vm); +void basVmStop(BasVmT *vm); int32_t basVmGetCurrentLine(const BasVmT *vm); +int32_t basVmGetErrorLine(const BasVmT *vm); +void basVmSetAppPaths(BasVmT *vm, const char *path, + const char *config, const char *data); .endcode .table - Function Description - -------- ----------- - basVmSetBreakpoints Set the breakpoint list (sorted array of source line numbers, host-owned memory). - basVmStepInto Break at the next OP_LINE instruction. - basVmStepOver Break when call depth returns to the current level. - basVmStepOut Break when call depth drops below the current level. - basVmRunToCursor Break when execution reaches the specified source line. - basVmGetCurrentLine Get the current source line number (from the last OP_LINE instruction). + Function Description + -------- ----------- + basVmSetBreakpoints Set the breakpoint list (sorted array of source line numbers, host-owned memory). + basVmStepInto Break at the next OP_LINE instruction. + basVmStepOver Break when call depth returns to the current level. + basVmStepOut Break when call depth drops below the current level. + basVmRunToCursor Break when execution reaches the specified source line. + basVmClearStepState Cancel a pending step or run-to-cursor request. + basVmSetBreakpointCallback The pause notification for SUBs the host calls (see below). + basVmSetDebugPaused Break mode on or off: while on, basVmCallSub refuses to run event handlers. + basVmResume Leave break mode; a handler paused at a breakpoint carries on. + basVmStop Stop a debugged program: step state cleared, breakpoints detached, break mode ended, the running code unwinds. + basVmGetCurrentLine Get the current source line number (from the last OP_LINE instruction). + basVmGetErrorLine The source line where the current runtime error was raised (0: none). + basVmSetAppPaths Set App.Path, App.Config and App.Data. .endtable The breakpoint callback notifies the host when a breakpoint fires during nested sub calls: @@ -538,6 +624,18 @@ typedef struct { int32_t procCount; BasFormVarInfoT *formVarInfo; int32_t formVarInfoCount; + BasSlotInitT *globalInits; + int32_t globalInitCount; + BasSlotInitT *localInits; + int32_t localInitCount; + BasLineNumberT *lineNumbers; + int32_t lineNumberCount; + BasClassInfoT *classes; + int32_t classCount; + BasEnumInfoT *enums; + int32_t enumCount; + BasWithEventsT *withEvents; + int32_t withEventsCount; BasDebugVarT *debugVars; int32_t debugVarCount; BasDebugUdtDefT *debugUdtDefs; @@ -560,8 +658,142 @@ typedef struct { procCount Number of procedures. formVarInfo Per-form variable counts and init code addresses. formVarInfoCount Number of forms with form-scoped variables. + globalInits The starting type of each typed global slot (kept in a release build). + globalInitCount Number of global slot entries. + localInits The starting type of each typed local slot, per procedure (BasProcEntryT initFirst / initCount). + localInitCount Number of local slot entries. + lineNumbers The program's QBasic line numbers in source order (ERL reports them). + lineNumberCount Number of line numbers. + classes Class modules and UserControls (BasClassInfoT): members, events, Implements maps. + classCount Number of classes. + enums The Enums UserControl properties are declared As (BasEnumInfoT: member names and values in declaration order; a class member's enumIdx names one), so a host shows and stores such a property by member name. Kept in a release build. + enumCount Number of Enums. + withEvents WithEvents variables and their handlers. + withEventsCount Number of WithEvents variables. debugVars Variable names and metadata for the debugger. debugVarCount Number of debug variable entries. debugUdtDefs UDT type definitions for the debugger. debugUdtDefCount Number of debug UDT definitions. .endtable + +.topic lib.basrt.frmmodel +.title The .frm Model +.toc 1 The .frm Model +.index FrmModelT +.index FrmCtrlT +.index FrmMenuItemT +.index frmModelLoad +.index frmModelWrite +.index frmModelText +.index frmModelFromForm + +.h1 The .frm Model + +A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields and every other "Key = Value" pair as text), its menu items and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c). + +Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib) + +.h2 Types + +.table + Type Description + ---- ----------- + FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded). + FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies and pastes start it at NULL). + FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu). + FrmPropT One stored "Key = Value": name, value. + FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE). +.endtable + +Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); keys the widget does not know are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut). + +.h2 Loading and Writing + +.table + Function Description + -------- ----------- + frmModelNew(name) A blank form with the runtime form's defaults. + frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it. + frmModelFree(m) Frees the model, its controls and menus. + frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text). + frmModelWriteSize(m) A buffer size that always holds the text. + frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code. + frmModelSetCode(m, code) Replaces the code section. + frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent). + frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window), its menus; no code. What FormText$ writes. +.endtable + +.h2 Properties + +.table + Function Description + -------- ----------- + frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0" or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties). + frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out. + frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones). + frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it). + frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule. + frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties. + frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table. + frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none. +.endtable + +.h2 Editing Controls + +.table + Function Description + -------- ----------- + frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it. + frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree. + frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's. + frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow. + frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last). + frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers and too deep a nesting). + frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before). + frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups. + frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order. + frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule. + frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard). + frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model. + frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents. +.endtable + +.h2 Menus + +The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree. + +.table + Function Description + -------- ----------- + frmMenuItemInit(mi) A blank item (enabled, visible, level 0). + frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree. + frmMenuRemove(&items, idx) Removes an item with its subtree. + frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX. + frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL). + frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits. + frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item). + frmModelMenuValidate(m, items, isIdent, reason, size) The checks before items replace m's menus (levels; every name set, an identifier by isIdent, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason. + frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing). + frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow. +.endtable + +.code +// Load a form, rename a control, add a label inside a frame, save. +FrmModelT *m = frmModelLoad(text, len); + +if (m) { + int32_t at = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX); + FrmCtrlT *frame = (at >= 0) ? m->controls[at] : NULL; + FrmCtrlT *lbl = frmModelAddCtrl(m, "Label", "", frame); + int32_t outLen = 0; + + frmModelRenameCtrl(m, "Command1", "cmdOk"); + frmModelSetCtrlProp(m, lbl, "Caption", "Name:"); + + char *out = frmModelText(m, &outLen); + + // write out[0 .. outLen) to the file + free(out); + frmModelFree(m); +} +.endcode diff --git a/src/apps/kpunch/dvxbasic/compiler/ast.h b/src/apps/kpunch/dvxbasic/compiler/ast.h index c78f0d31..cc9c196e 100644 --- a/src/apps/kpunch/dvxbasic/compiler/ast.h +++ b/src/apps/kpunch/dvxbasic/compiler/ast.h @@ -93,7 +93,7 @@ typedef enum { NK_RESULT, // the result slot of the FUNCTION being compiled (its name inside it) NK_ME, // Me: the form, or the instance in a class module NK_LOADFORM, // name: the form of that name (OP_LOAD_FORM) - NK_NEW, // name: New ClassName / New Collection; aux = class number after resolve + NK_NEW, // name: New ClassName / New Collection / New FormName; aux after resolve = class number, BAS_CLASS_COLLECTION, or BAS_CLASS_NONE for a form NK_OMIT, // an argument left out (f(a, , b)); the resolver replaces it NK_WITHREF, // BasMemberT: the object a leading '.' names inside WITH ... END WITH; target = the BasWithT NK_NAMEDARG, // BasMemberT: an argument given by name (Name:=value): name, base = the value; the resolver puts it in its parameter's place @@ -402,6 +402,7 @@ typedef struct { int32_t tok; // TOK_INTEGER ... (a keyword type), else TOK_IDENT int32_t fixedLen;// STRING * n bool isNew; // AS NEW + bool isBinary;// BINARY STRING (DECLARE LIBRARY only): pointer + length } BasTypeSpecT; typedef struct { diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h index c5b8392d..eff2d6b7 100644 --- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h +++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h @@ -23,7 +23,7 @@ // basEvents.h -- canonical list of BASIC event suffixes // // A procedure whose name ends with one of these suffixes is treated as an -// event handler (Ctrl_Load, Form_Click, etc). Referenced by the stripper +// event handler (Form1_Load, Command1_Click, etc). Referenced by the stripper // (to retain handlers in release builds), the obfuscator (to preserve event // naming in rewritten forms), and the IDE (to populate the Object/Event // dropdowns and generate handler stubs). Keep them in one place so adding @@ -35,19 +35,27 @@ // params -- the handler's parameter list exactly as the runtime // passes it (formrt.c fireCtrlEvent / FireEventWithCancel), // "" for events with no arguments. Control-array handlers -// receive an extra leading "Index As Integer". +// receive an extra leading "Index As Integer". NULL for a +// widget's own event (BAS_EVT_SCOPE_WIDGET): its parameters +// are written once, in the widget's WgtEventDescT (the +// form runtime's basCtlEventParams reads them there). +// The widget-own events are listed here only so the release-build +// stripper and the obfuscator keep their handler names. // Consumers expand it with an X macro so no parallel list can drift. #ifndef BAS_EVENTS_H #define BAS_EVENTS_H #include +#include #include +#include typedef enum { BAS_EVT_SCOPE_CTRL = 1 << 0, // fired on controls (widgets, Timer) BAS_EVT_SCOPE_FORM = 1 << 1, // fired on the form itself - BAS_EVT_SCOPE_DRAW = 1 << 2 // fired on the controls the drawing methods draw on (PictureBox) + BAS_EVT_SCOPE_DRAW = 1 << 2, // fired on the controls the drawing methods draw on (PictureBox) + BAS_EVT_SCOPE_WIDGET = 1 << 3 // fired only by the widgets whose interface lists it (wgtFireEvent) } BasEventScopeE; #define BAS_EVT_SCOPE_BOTH (BAS_EVT_SCOPE_CTRL | BAS_EVT_SCOPE_FORM) @@ -78,6 +86,13 @@ typedef enum { #define BAS_EVT_DRAGDROP "DragDrop" #define BAS_EVT_DRAGOVER "DragOver" #define BAS_EVT_PAINT "Paint" +#define BAS_EVT_COLUMNCLICK "ColumnClick" +#define BAS_EVT_GUTTERCLICK "GutterClick" +#define BAS_EVT_DESIGNMOUSE "DesignMouse" +#define BAS_EVT_DESIGNKEY "DesignKey" +#define BAS_EVT_DSGNPROPCHG "DesignPropertyChanged" +#define BAS_EVT_LINKCLICK "LinkClick" +#define BAS_EVT_NEWARGS "NewArgs" #define BAS_EVENT_LIST(X) \ X(BAS_EVT_LOAD, BAS_EVT_SCOPE_FORM, "") \ @@ -103,7 +118,63 @@ typedef enum { X(BAS_EVT_VALIDATE, BAS_EVT_SCOPE_CTRL, "Cancel As Integer") \ X(BAS_EVT_DRAGDROP, BAS_EVT_SCOPE_BOTH, "Source As Control, X As Integer, Y As Integer") \ X(BAS_EVT_DRAGOVER, BAS_EVT_SCOPE_BOTH, "Source As Control, X As Integer, Y As Integer, State As Integer") \ - X(BAS_EVT_PAINT, BAS_EVT_SCOPE_FORM | BAS_EVT_SCOPE_DRAW, "") + X(BAS_EVT_PAINT, BAS_EVT_SCOPE_FORM | BAS_EVT_SCOPE_DRAW, "") \ + X(BAS_EVT_COLUMNCLICK, BAS_EVT_SCOPE_WIDGET, NULL) \ + X(BAS_EVT_GUTTERCLICK, BAS_EVT_SCOPE_WIDGET, NULL) \ + X(BAS_EVT_DESIGNMOUSE, BAS_EVT_SCOPE_FORM, "Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer") \ + X(BAS_EVT_DESIGNKEY, BAS_EVT_SCOPE_FORM, "KeyCode As Integer, Shift As Integer") \ + X(BAS_EVT_DSGNPROPCHG, BAS_EVT_SCOPE_FORM, "CtrlName As String, PropertyName As String") \ + X(BAS_EVT_LINKCLICK, BAS_EVT_SCOPE_WIDGET, NULL) \ + X(BAS_EVT_NEWARGS, BAS_EVT_SCOPE_FORM, "") + +// A UserControl (a widget class written in BASIC, a .ctl file): the name +// of its own object in its code (UserControl.Refresh), the word after the +// class name on its BEGINCLASS line, and the events the runtime calls on +// it. BAS_UC_EVENT_LIST is the one table of those events: the handler is +// the class procedure UserControl_ with exactly the parameters +// listed (VB5's names and order); the compiler finds each class's +// handlers by it and keeps their procedure numbers in the module +// (BasClassInfoT.ucProcs, indexed by BasUcEventE). +#define BAS_UC_OBJECT "UserControl" +#define BAS_UC_CLASS_KW "USERCONTROL" +#define BAS_UC_EVT_INIT "Initialize" +#define BAS_UC_EVT_TERM "Terminate" +#define BAS_UC_EVT_WHEEL "MouseWheel" +#define BAS_UC_MOUSE_PARAMS "Button As Integer, Shift As Integer, X As Single, Y As Single" +#define BAS_UC_KEY_PARAMS "KeyCode As Integer, Shift As Integer" + +#define BAS_UC_EVENT_LIST(X) \ + X(BAS_UC_EVT_INITIALIZE, BAS_UC_EVT_INIT, "") \ + X(BAS_UC_EVT_TERMINATE, BAS_UC_EVT_TERM, "") \ + X(BAS_UC_EVT_RESIZE, BAS_EVT_RESIZE, "") \ + X(BAS_UC_EVT_PAINT, BAS_EVT_PAINT, "") \ + X(BAS_UC_EVT_CLICK, BAS_EVT_CLICK, "") \ + X(BAS_UC_EVT_DBLCLICK, BAS_EVT_DBLCLICK, "") \ + X(BAS_UC_EVT_MOUSEDOWN, BAS_EVT_MOUSEDOWN, BAS_UC_MOUSE_PARAMS) \ + X(BAS_UC_EVT_MOUSEMOVE, BAS_EVT_MOUSEMOVE, BAS_UC_MOUSE_PARAMS) \ + X(BAS_UC_EVT_MOUSEUP, BAS_EVT_MOUSEUP, BAS_UC_MOUSE_PARAMS) \ + X(BAS_UC_EVT_MOUSEWHEEL, BAS_UC_EVT_WHEEL, "Delta As Integer") \ + X(BAS_UC_EVT_KEYDOWN, BAS_EVT_KEYDOWN, BAS_UC_KEY_PARAMS) \ + X(BAS_UC_EVT_KEYUP, BAS_EVT_KEYUP, BAS_UC_KEY_PARAMS) \ + X(BAS_UC_EVT_KEYPRESS, BAS_EVT_KEYPRESS, "KeyAscii As Integer") \ + X(BAS_UC_EVT_GOTFOCUS, BAS_EVT_GOTFOCUS, "") \ + X(BAS_UC_EVT_LOSTFOCUS, BAS_EVT_LOSTFOCUS, "") + +#define BAS_UC_EVENT_ENUM(id, name, params) id, + +typedef enum { + BAS_UC_EVENT_LIST(BAS_UC_EVENT_ENUM) + BAS_UC_EVT_COUNT +} BasUcEventE; + +// One row of BAS_UC_EVENT_LIST: the event name (the handler is +// UserControl_) and its parameter list. +typedef struct { + const char *name; + const char *params; +} BasUcEventInfoT; + +#define BAS_UC_EVENT_ROW(id, name, params) { name, params }, // Drag and drop values, shared by the compiler's predefined vb* // constants and the form runtime: the DragMode property, the Drag @@ -117,6 +188,29 @@ typedef enum { #define BAS_DRAG_LEAVE 1 // vbLeave: the drag went off it #define BAS_DRAG_OVER 2 // vbOver: the drag moved over it +// The object-valued members of forms and controls: Form.Controls (the +// collection of its controls), Control.Parent (its form), +// Control.Container (the control or form it is in), Screen / Form. +// ActiveControl (the control with the focus) and Form.Icon (a Picture). +// Before another '.' they are these members, not controls of that name. +#define BAS_FORM_CONTROLS "Controls" +#define BAS_CTRL_PARENT "Parent" +#define BAS_CTRL_CONTAINER "Container" +#define BAS_FORM_ACTIVECONTROL "ActiveControl" +#define BAS_FORM_ICON "Icon" + +// The Screen object: in Screen.ActiveControl.Name the first name is the +// object, not a form to load. +#define BAS_SCREEN_OBJ_NAME "Screen" + +// The members that give control and form names as text (Name, a form's +// DesignSelection; Controls also takes a name). A release build keeps +// the names of a program that uses one of them, or the DesignMouse or +// DesignPropertyChanged event, so its code sees the names it was written +// with. +#define BAS_CTRL_NAME "Name" +#define BAS_FORM_DESIGNSEL "DesignSelection" + // Clipboard formats (Clipboard.GetFormat / GetText / SetText): the DVX // clipboard holds text. #define BAS_CF_TEXT 1 // vbCFText @@ -152,6 +246,8 @@ typedef enum { #define BAS_ROP_PATPAINT 0x00FB0A09 // vbPatPaint #define BAS_ROP_PATINVERT 0x005A0049 // vbPatInvert #define BAS_ROP_DSTINVERT 0x00550009 // vbDstInvert +#define BAS_ROP_GRAYCOPY 0x01000000 // vbGrayCopy (DVX): the picture greyed, as a disabled one +#define BAS_ROP_TRANSPCOPY 0x02000000 // vbTransparentCopy (DVX): every pixel but the key colour #define BAS_ROP_TABLE_SHIFT 16 // the truth table's place in an opcode #define BAS_ROP_TABLE_MASK 0xFF @@ -159,16 +255,86 @@ typedef enum { #define BAS_PIC_TYPE_NONE 0 // vbPicTypeNone #define BAS_PIC_TYPE_BITMAP 1 // vbPicTypeBitmap +// Form.WindowState (VB's values; vbNormal is also the file attribute 0). +#define BAS_WINSTATE_NORMAL 0 // vbNormal +#define BAS_WINSTATE_MINIMIZED 1 // vbMinimized +#define BAS_WINSTATE_MAXIMIZED 2 // vbMaximized + +// Screen.Arrange: VB's MDIForm.Arrange values (3, vbArrangeIcons, has no +// DVX meaning) plus DVX's grid. +#define BAS_ARRANGE_CASCADE 0 // vbCascade +#define BAS_ARRANGE_TILE_HORZ 1 // vbTileHorizontal: windows stacked top to bottom +#define BAS_ARRANGE_TILE_VERT 2 // vbTileVertical: windows side by side +#define BAS_ARRANGE_TILE_GRID 4 // vbTileGrid: rows and columns + +// MousePointer of controls, forms and Screen (VB's values for the +// pointers DVX has; no custom pointer). +#define BAS_POINTER_DEFAULT 0 // vbDefault: the control's own pointer +#define BAS_POINTER_ARROW 1 // vbArrow +#define BAS_POINTER_CROSSHAIR 2 // vbCrosshair +#define BAS_POINTER_SIZENESW 6 // vbSizeNESW +#define BAS_POINTER_SIZENS 7 // vbSizeNS +#define BAS_POINTER_SIZENWSE 8 // vbSizeNWSE +#define BAS_POINTER_SIZEWE 9 // vbSizeWE +#define BAS_POINTER_HOURGLASS 11 // vbHourglass +#define BAS_POINTER_NODROP 12 // vbNoDrop + // One row of BAS_EVENT_LIST, for consumers that want a runtime table // (e.g. `static const BasEventInfoT sEvents[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) };`). typedef struct { const char *suffix; uint8_t scope; // BasEventScopeE flags - const char *params; // handler parameter list, "" when none + const char *params; // handler parameter list, "" when none, NULL for a widget's own event } BasEventInfoT; #define BAS_EVENT_ROW(suffix, scope, params) { suffix, scope, params }, +// The parameter list the master table gives a handler of event eventName: +// "" when none, NULL for a widget's own event (its WgtEventDescT has it) +// and for an event not in the table. The form runtime's basCtlEventParams +// (IDE handler skeletons, designer.bas CtlEventParams$) adds the widget +// side. +static inline const char *basEventParams(const char *eventName) { + static const BasEventInfoT sEventRows[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) }; + + for (int32_t i = 0; eventName && i < (int32_t)(sizeof(sEventRows) / sizeof(sEventRows[0])); i++) { + if (strcasecmp(sEventRows[i].suffix, eventName) == 0) { + return sEventRows[i].params; + } + } + + return NULL; +} + + +// True when name is one of the object-valued members above (the +// compiler then reads obj.name.member as that member's member). +static inline bool basIsObjectMember(const char *name) { + return strcasecmp(name, BAS_FORM_CONTROLS) == 0 || strcasecmp(name, BAS_CTRL_PARENT) == 0 || strcasecmp(name, BAS_CTRL_CONTAINER) == 0 || strcasecmp(name, BAS_FORM_ACTIVECONTROL) == 0 || strcasecmp(name, BAS_FORM_ICON) == 0; +} + + +// The number of parameters in a parameter list as the tables above write +// it ("" none, else one more than its commas). +static inline int32_t basParamListCount(const char *params) { + int32_t n = (params != NULL && params[0] != '\0') ? 1 : 0; + + for (const char *p = params; p != NULL && *p; p++) { + n += (*p == ',') ? 1 : 0; + } + + return n; +} + + +// The row of UserControl event evt (BasUcEventE), NULL past the end. +static inline const BasUcEventInfoT *basUcEventInfo(int32_t evt) { + static const BasUcEventInfoT sUcRows[BAS_UC_EVT_COUNT] = { BAS_UC_EVENT_LIST(BAS_UC_EVENT_ROW) }; + + return (evt >= 0 && evt < BAS_UC_EVT_COUNT) ? &sUcRows[evt] : NULL; +} + + // Expands to just the suffix string followed by a comma, for a plain // NULL-terminated const char *[] view of the list. #define BAS_EVENT_SUFFIX(suffix, scope, params) suffix, @@ -187,4 +353,9 @@ bool basEventSuffixMatch(const char *suffix); // the event name inside name. Defined in strip.c. bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix); +// The same split for any suffix: "_" with neither part empty +// (a form's handler of a widget's or UserControl's own event). Defined in +// strip.c. +bool basSplitHandlerName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix); + #endif // BAS_EVENTS_H diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.c b/src/apps/kpunch/dvxbasic/compiler/compiler.c index 0916068c..e8dfa18a 100644 --- a/src/apps/kpunch/dvxbasic/compiler/compiler.c +++ b/src/apps/kpunch/dvxbasic/compiler/compiler.c @@ -31,6 +31,8 @@ #include "front.h" #include "basEvents.h" +#include "dvxTypes.h" +#include "textHelp.h" #include "../runtime/dates.h" #include "thirdparty/stb_ds_wrap.h" @@ -68,6 +70,23 @@ static const PredefConstT sPredefConsts[] = { // Show mode { "vbModal", 1 }, + // Form.WindowState (vbNormal is below), Screen.Arrange, Screen.MousePointer + { "vbMinimized", BAS_WINSTATE_MINIMIZED }, + { "vbMaximized", BAS_WINSTATE_MAXIMIZED }, + { "vbCascade", BAS_ARRANGE_CASCADE }, + { "vbTileHorizontal", BAS_ARRANGE_TILE_HORZ }, + { "vbTileVertical", BAS_ARRANGE_TILE_VERT }, + { "vbTileGrid", BAS_ARRANGE_TILE_GRID }, + { "vbDefault", BAS_POINTER_DEFAULT }, + { "vbArrow", BAS_POINTER_ARROW }, + { "vbCrosshair", BAS_POINTER_CROSSHAIR }, + { "vbSizeNESW", BAS_POINTER_SIZENESW }, + { "vbSizeNS", BAS_POINTER_SIZENS }, + { "vbSizeNWSE", BAS_POINTER_SIZENWSE }, + { "vbSizeWE", BAS_POINTER_SIZEWE }, + { "vbHourglass", BAS_POINTER_HOURGLASS }, + { "vbNoDrop", BAS_POINTER_NODROP }, + // File attributes { "vbNormal", 0 }, { "vbReadOnly", 1 }, @@ -108,6 +127,59 @@ static const PredefConstT sPredefConsts[] = { // Clipboard formats { "vbCFText", BAS_CF_TEXT }, + // CallByName's call types + { "vbMethod", BAS_CALLTYPE_METHOD }, + { "vbGet", BAS_CALLTYPE_GET }, + { "vbLet", BAS_CALLTYPE_LET }, + { "vbSet", BAS_CALLTYPE_SET }, + + // KeyDown key codes and Shift bits: DVX's own codes (a letter or other + // character is its ASCII code, Ctrl+A to Ctrl+Z are 1 to 26) + { "vbKeyBack", KEY_BACKSPACE }, + { "vbKeyTab", KEY_TAB }, + { "vbKeyReturn", KEY_ENTER }, + { "vbKeyEscape", KEY_ESCAPE }, + { "vbKeySpace", KEY_SPACE }, + { "vbKeyPageUp", KEY_PGUP }, + { "vbKeyPageDown", KEY_PGDN }, + { "vbKeyEnd", KEY_END }, + { "vbKeyHome", KEY_HOME }, + { "vbKeyLeft", KEY_LEFT }, + { "vbKeyUp", KEY_UP }, + { "vbKeyRight", KEY_RIGHT }, + { "vbKeyDown", KEY_DOWN }, + { "vbKeyInsert", KEY_INSERT }, + { "vbKeyDelete", KEY_DELETE }, + { "vbKeyF1", KEY_F1 }, + { "vbKeyF2", KEY_F2 }, + { "vbKeyF3", KEY_F3 }, + { "vbKeyF4", KEY_F4 }, + { "vbKeyF5", KEY_F5 }, + { "vbKeyF6", KEY_F6 }, + { "vbKeyF7", KEY_F7 }, + { "vbKeyF8", KEY_F8 }, + { "vbKeyF9", KEY_F9 }, + { "vbKeyF10", KEY_F10 }, + { "vbKeyF11", KEY_F11 }, + { "vbKeyF12", KEY_F12 }, + { "vbShiftMask", KEY_MOD_SHIFT }, + { "vbCtrlMask", KEY_MOD_CTRL }, + { "vbAltMask", KEY_MOD_ALT }, + + // TextArea syntax colour indices (SetSyntaxColor, the Colorize event) + { "vbSyntaxText", TEXT_SYNTAX_DEFAULT }, + { "vbSyntaxKeyword", TEXT_SYNTAX_KEYWORD }, + { "vbSyntaxString", TEXT_SYNTAX_STRING }, + { "vbSyntaxComment", TEXT_SYNTAX_COMMENT }, + { "vbSyntaxNumber", TEXT_SYNTAX_NUMBER }, + { "vbSyntaxOperator", TEXT_SYNTAX_OPERATOR }, + { "vbSyntaxType", TEXT_SYNTAX_TYPE }, + + // The Button argument of the mouse events + { "vbLeftButton", MOUSE_LEFT }, + { "vbRightButton", MOUSE_RIGHT }, + { "vbMiddleButton", MOUSE_MIDDLE }, + // Colours: RGB values (&HBBGGRR, as RGB() makes them) { "vbBlack", 0x000000 }, { "vbRed", 0x0000FF }, @@ -143,6 +215,14 @@ static const PredefConstT sPredefConsts[] = { { "vbPatPaint", BAS_ROP_PATPAINT }, { "vbPatInvert", BAS_ROP_PATINVERT }, { "vbDstInvert", BAS_ROP_DSTINVERT }, + { "vbGrayCopy", BAS_ROP_GRAYCOPY }, + { "vbTransparentCopy", BAS_ROP_TRANSPCOPY }, + + // DrawArrow directions (the libdvx arrow glyph's) + { "dvxArrowUp", ScrollArrowUpE }, + { "dvxArrowDown", ScrollArrowDownE }, + { "dvxArrowLeft", ScrollArrowLeftE }, + { "dvxArrowRight", ScrollArrowRightE }, { "vbLPSmall", BAS_LP_SMALL }, { "vbLPLarge", BAS_LP_LARGE }, { "vbLPSmallShell", BAS_LP_SMALL_SHELL }, @@ -178,6 +258,7 @@ static const PredefConstT sPredefConsts[] = { // Function prototypes (alphabetical) static bool addPredefConsts(BasCompilerT *c); +static bool appendEnum(const BasCompilerT *c, BasModuleT *mod, const BasSymT *e, int32_t *outIdx); static void applyMemberPatches(BasCompilerT *c); bool basCompile(BasCompilerT *c); BasModuleT *basCompilerBuildModule(BasCompilerT *c); @@ -194,13 +275,17 @@ void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v); void basErrorAt(BasCompilerT *c, int32_t line, int32_t col, const char *fmt, ...); void basErrorNode(BasCompilerT *c, const BasNodeT *n, const char *fmt, ...); static bool buildClasses(const BasCompilerT *c, BasModuleT *mod); +static bool buildClassEvents(const BasClassDefT *def, BasClassInfoT *info); +static bool buildEnums(const BasCompilerT *c, BasModuleT *mod); static bool buildGlobalInits(const BasCompilerT *c, BasModuleT *mod); static bool buildInterfaceMap(const BasCompilerT *c, int32_t classIdx, int32_t iface, BasImplementsT *impl); static bool buildProcs(const BasCompilerT *c, BasModuleT *mod); +static void buildUserControl(const BasCompilerT *c, int32_t classIdx, const BasModuleT *mod, BasClassInfoT *info); static bool buildWithEvents(const BasCompilerT *c, BasModuleT *mod); static int32_t classMemberNamed(const BasClassDefT *cls, const char *name, uint8_t kind); static void collectDebugGlobals(BasCompilerT *c); static bool emittableProc(const BasSymT *s); +static const char *enumMemberName(const BasSymT *s, const BasSymT *e); static void errorV(BasCompilerT *c, int32_t line, int32_t col, const char *fmt, va_list ap); static bool globalStartsTyped(const BasSymT *s); static bool joinParamNames(const BasProcInfoT *pi, char **out); @@ -228,6 +313,53 @@ static bool addPredefConsts(BasCompilerT *c) { } +// Appends ENUM e to mod->enums with its members (the module scope's +// "Enum.Member" constants, enumMemberName) in declaration order. *outIdx +// receives its index, BAS_ENUM_NONE for an ENUM without members. False +// when memory runs out. +static bool appendEnum(const BasCompilerT *c, BasModuleT *mod, const BasSymT *e, int32_t *outIdx) { + int32_t count = 0; + + *outIdx = BAS_ENUM_NONE; + + for (const BasSymT *s = c->moduleTab.first; s != NULL; s = s->orderNext) { + count += (enumMemberName(s, e) != NULL) ? 1 : 0; + } + + if (count == 0) { + return true; + } + + BasEnumMemberT *members = (BasEnumMemberT *)calloc((size_t)count, sizeof(BasEnumMemberT)); + BasEnumInfoT *grown = (members != NULL) ? (BasEnumInfoT *)realloc(mod->enums, (size_t)(mod->enumCount + 1) * sizeof(BasEnumInfoT)) : NULL; + + if (grown == NULL) { + free(members); + return false; + } + + BasEnumInfoT *en = &grown[mod->enumCount]; + + en->members = members; + en->memberCount = 0; + mod->enums = grown; + *outIdx = mod->enumCount++; + + for (const BasSymT *s = c->moduleTab.first; s != NULL; s = s->orderNext) { + const char *name = enumMemberName(s, e); + + if (name != NULL) { + BasEnumMemberT *m = &en->members[en->memberCount++]; + + snprintf(m->name, sizeof(m->name), "%s", name); + m->value = s->u.cval->intVal; + } + } + + return true; +} + + // Fills in the member operands of the early-bound instructions: the // member's index in its class's public member table, complete now that // every class is compiled. @@ -317,7 +449,7 @@ BasModuleT *basCompilerBuildModule(BasCompilerT *c) { BasModuleT *mod = basCodeGenBuildModule(&c->cg); // Each build step frees mod when it fails - if (mod == NULL || !buildGlobalInits(c, mod) || !buildProcs(c, mod) || !buildClasses(c, mod) || !buildWithEvents(c, mod)) { + if (mod == NULL || !buildGlobalInits(c, mod) || !buildProcs(c, mod) || !buildClasses(c, mod) || !buildEnums(c, mod) || !buildWithEvents(c, mod)) { return NULL; } @@ -391,15 +523,6 @@ void basCompilerDestroy(BasCompilerT *c) { arrfree(c->withEvents[i].handlers); } - // A procedure left open by an error still owns its labels' lists - if (c->inProc) { - for (const BasSymT *l = c->localLabels.first; l != NULL; l = l->orderNext) { - arrfree(l->u.label->patches); - arrfree(l->u.label->patchDepths); - arrfree(l->u.label->dataPatches); - } - } - for (int32_t i = 0; i < (int32_t)arrlen(c->loops); i++) { arrfree(c->loops[i].exitPatches); } @@ -509,6 +632,13 @@ static bool buildClasses(const BasCompilerT *c, BasModuleT *mod) { info->initCodeAddr = def->initCodeAddr; info->terminateProc = (def->terminateSym != NULL) ? procIndexAt(mod, def->terminateSym->u.proc->codeAddr) : -1; + buildUserControl(c, k, mod, info); + + if (!buildClassEvents(def, info)) { + basModuleFree(mod); + return false; + } + if (ni > 0) { info->implements = (BasImplementsT *)calloc(ni, sizeof(BasImplementsT)); @@ -589,6 +719,99 @@ static bool buildClasses(const BasCompilerT *c, BasModuleT *mod) { } +// A class's Events as the module keeps them (BasClassEventT). False when +// memory runs out. +// gcc -fanalyzer loses the parameter lists stored at a variable index of +// the new table and reports them leaked when the table is handed to info +// (a false positive: basModuleFree frees them through info). +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wanalyzer-malloc-leak" +static bool buildClassEvents(const BasClassDefT *def, BasClassInfoT *info) { + int32_t count = (int32_t)arrlen(def->events); + + if (count == 0) { + return true; + } + + BasClassEventT *events = (BasClassEventT *)calloc((size_t)count, sizeof(BasClassEventT)); + + if (events == NULL) { + return false; + } + + for (int32_t i = 0; i < count; i++) { + const BasEventDefT *ev = &def->events[i]; + BasClassEventT *out = &events[i]; + + snprintf(out->name, sizeof(out->name), "%s", ev->name); + out->paramCount = DVX_MIN(ev->paramCount, BAS_VM_MAX_CALL_ARGS); + + for (int32_t p = 0; p < out->paramCount; p++) { + out->paramTypes[p] = ev->paramTypes[p]; + out->byValParams |= (ev->paramFlags[p] & PARAM_BYVAL) ? (1u << p) : 0u; + } + + if (ev->paramNames != NULL && ev->paramNames[0] != '\0') { + char *names = strdup(ev->paramNames); + + if (names == NULL) { + basClassEventsFree(events, i); + return false; + } + + out->paramNames = names; + } + } + + info->eventCount = count; + info->events = events; + return true; +} +#pragma GCC diagnostic pop + + +// The ENUMs UserControl properties are declared As (mod->enums, each one +// once) and every class member's enumIdx (BAS_ENUM_NONE for the members +// of other classes and for other values). Frees mod and returns false +// when memory runs out. +static bool buildEnums(const BasCompilerT *c, BasModuleT *mod) { + const BasSymT **syms = NULL; // stb_ds: the ENUM of each mod->enums entry + bool ok = true; + + for (int32_t k = 0; ok && k < mod->classCount; k++) { + const BasClassDefT *def = &c->classes[k]; + BasClassInfoT *info = &mod->classes[k]; + + for (int32_t i = 0; ok && i < info->memberCount; i++) { + const BasSymT *e = def->isUserControl ? def->members[i].enumSym : NULL; + int32_t idx = BAS_ENUM_NONE; + + for (int32_t n = 0; e != NULL && n < (int32_t)arrlen(syms); n++) { + idx = (syms[n] == e) ? n : idx; + } + + if (e != NULL && idx == BAS_ENUM_NONE) { + ok = appendEnum(c, mod, e, &idx); + + if (idx != BAS_ENUM_NONE) { + arrput(syms, e); + } + } + + info->members[i].enumIdx = idx; + } + } + + arrfree(syms); + + if (!ok) { + basModuleFree(mod); + } + + return ok; +} + + // Global scalars that start as their type's zero rather than a cleared // slot's INTEGER 0 (STRING, and the other typed scalars), a table that // survives stripping unlike the debug variables. Frees mod and returns @@ -752,6 +975,32 @@ static bool buildProcs(const BasCompilerT *c, BasModuleT *mod) { } +// A UserControl class's flag, the slot of its UserControl object and the +// procedures of its UserControl_ handlers; for another class no +// slot. +static void buildUserControl(const BasCompilerT *c, int32_t classIdx, const BasModuleT *mod, BasClassInfoT *info) { + const BasClassDefT *def = &c->classes[classIdx]; + char qualified[BAS_MAX_IDENT]; + + info->ucSlot = -1; + + if (!def->isUserControl || !basQualify(c, classIdx, BAS_UC_OBJECT, qualified, (int32_t)sizeof(qualified))) { + return; + } + + const BasSymT *slot = basScopeFind(&c->moduleTab, qualified); + + info->flags = BAS_CLASS_USERCONTROL; + info->ucSlot = (slot != NULL && slot->kind == SK_VAR) ? slot->index : -1; + + for (int32_t e = 0; e < BAS_UC_EVT_COUNT; e++) { + const BasSymT *sym = basUserControlProc(c, classIdx, e); + + info->ucProcs[e] = (sym != NULL) ? procIndexAt(mod, sym->u.proc->codeAddr) : -1; + } +} + + // The WithEvents variables, handlers by their index in mod->procs. Frees // mod and returns false when memory runs out. static bool buildWithEvents(const BasCompilerT *c, BasModuleT *mod) { @@ -869,6 +1118,19 @@ static bool emittableProc(const BasSymT *s) { } +// The member name of module symbol s when s is a member of ENUM e (the +// constant "Enum.Member" resolve.c's addEnumMember made), else NULL. +static const char *enumMemberName(const BasSymT *s, const BasSymT *e) { + size_t len = strlen(e->name); + + if (s->kind != SK_CONST || strncmp(s->name, e->name, len) != 0 || s->name[len] != BAS_CLASS_SEP[0]) { + return NULL; + } + + return s->name + len + 1; +} + + // Records the first error: "Line N, Col M: text" ("Line N: text" when the // column is not known). static void errorV(BasCompilerT *c, int32_t line, int32_t col, const char *fmt, va_list ap) { @@ -917,14 +1179,10 @@ static bool joinParamNames(const BasProcInfoT *pi, char **out) { return false; } - (*out)[0] = '\0'; + size_t used = 0; for (int32_t p = 0; p < pi->paramCount; p++) { - if (p > 0) { - strcat(*out, BAS_PARAM_NAME_SEP); - } - - strcat(*out, pi->paramNames[p]); + used += (size_t)snprintf(*out + used, len - used, "%s%s", p > 0 ? BAS_PARAM_NAME_SEP : "", pi->paramNames[p]); } return true; diff --git a/src/apps/kpunch/dvxbasic/compiler/fold.c b/src/apps/kpunch/dvxbasic/compiler/fold.c index 053d5a54..564d3e5f 100644 --- a/src/apps/kpunch/dvxbasic/compiler/fold.c +++ b/src/apps/kpunch/dvxbasic/compiler/fold.c @@ -334,7 +334,7 @@ static bool constFromValue(BasCompilerT *c, BasValueT v, BasConstValT *out, cons case BAS_TYPE_STRING: if (v.strVal->len >= BAS_MAX_CONST_STR) { - basErrorNode(c, at, "Constant string longer than %d characters", (int)(BAS_MAX_CONST_STR - 1)); + basErrorNode(c, at, "Constant string longer than %d characters", BAS_MAX_CONST_STR - 1); return false; } diff --git a/src/apps/kpunch/dvxbasic/compiler/front.h b/src/apps/kpunch/dvxbasic/compiler/front.h index 1ffc3643..1a27c65c 100644 --- a/src/apps/kpunch/dvxbasic/compiler/front.h +++ b/src/apps/kpunch/dvxbasic/compiler/front.h @@ -59,6 +59,10 @@ #define BAS_MAX_EXPR_DEPTH 64 #define BAS_MAX_BLOCK_DEPTH 32 +// blockDepth while a top-level unit's own statement is parsed (a +// procedure must be one: its scope lives in the unit arena). +#define BAS_UNIT_BLOCK_DEPTH 1 + // Longest TYPE field chain in a store target (a.b.c ...). #define BAS_MAX_FIELD_CHAIN BAS_LAYOUT_MAX_DEPTH @@ -145,6 +149,10 @@ // Built-in function flags. #define BI_PURE 0x01 // result depends on the arguments alone: allowed in constant expressions #define BI_FOLD 0x02 // folded in ordinary code when its arguments are literals +#define BI_OBJECT_ARG 0x04 // the first argument is an object (a bare control or form name is the object) + +// The built-in function a statement may call too (CallByName obj, ...). +#define BAS_CALLBYNAME_NAME "CallByName" // A built-in function (BasBuiltinT table in resolveExpr.c). An // OP_BUILTIN entry names its BasBuiltinFnE in func and takes its argument @@ -236,6 +244,7 @@ struct BasCompilerS { bool parseInProc; // inside a SUB / FUNCTION / Property body bool parseProcIsFunction; // ... a FUNCTION or Property Get (its name sets its result) bool parseProcIsStatic; // ... declared STATIC (its DIMs declare STATIC variables) + bool parseInDeclareLib; // inside a DECLARE LIBRARY block (BINARY STRING allowed) const char *parseProcName; // the procedure being parsed (its own name sets its result) const char *parseClassName; // the class module being parsed (module arena; parseInClass) BasWithT *parseWith; // the innermost WITH block being parsed (its '.name's) @@ -388,6 +397,7 @@ void basDeclareMemberVars(BasCompilerT *c, const BasStmtT *dim, int32_t classIdx void basDeclareMemberVarsEnd(BasCompilerT *c); void basDeclareProc(BasCompilerT *c, BasProcT *proc, int32_t classIdx); void basDeclareTypeName(BasCompilerT *c, const char *name); +void basDeclareUserControlVar(BasCompilerT *c, int32_t classIdx); BasSymT *basEnsureVariable(BasCompilerT *c, const char *name, const BasNodeT *at); int32_t basExprClass(const BasCompilerT *c, const BasNodeT *n); int32_t basExprUdtType(const BasCompilerT *c, const BasNodeT *n); @@ -435,6 +445,7 @@ BasScopeE basStorageScope(const BasSymT *sym); void basSymChange(BasCompilerT *c, BasSymT *sym, const BasNodeT *at, uint8_t dataType, int32_t udtTypeId, uint32_t flags); void basSymChangesUndo(BasCompilerT *c); BasNodeT *basTypedArg(BasCompilerT *c, const char *procName, int32_t idx, uint8_t dataType, bool byVal, BasNodeT *arg); +BasSymT *basUserControlProc(const BasCompilerT *c, int32_t classIdx, int32_t evt); bool basValidateCtrlMember(BasCompilerT *c, const char *ctrlName, const char *member, bool isProp, const BasNodeT *at); BasNodeT *basWithObject(BasCompilerT *c, const BasNodeT *ref); @@ -467,6 +478,7 @@ void basGenJumpTo(BasCompilerT *c, uint8_t op, int32_t target); void basGenLine(BasCompilerT *c, const BasNodeT *s); void basGenLoadTemp(BasCompilerT *c, int32_t slot); void basGenLoadVar(BasCompilerT *c, const BasSymT *sym); +void basGenNew(BasCompilerT *c, const char *name, int32_t objClass); void basGenObjMember(BasCompilerT *c, uint8_t op, const BasCallT *call); void basGenPatchHere(BasCompilerT *c, int32_t operandPos); void basGenPushCStr(BasCompilerT *c, const char *text); diff --git a/src/apps/kpunch/dvxbasic/compiler/gen.c b/src/apps/kpunch/dvxbasic/compiler/gen.c index 318522fb..b32a67ad 100644 --- a/src/apps/kpunch/dvxbasic/compiler/gen.c +++ b/src/apps/kpunch/dvxbasic/compiler/gen.c @@ -38,6 +38,9 @@ #include #include +// BAS_FN_NEW_FORM's argument: the form's name. +#define BAS_NEW_FORM_ARGS 1 + // Function prototypes (alphabetical) void basGenAddrOf(BasCompilerT *c, const BasSymT *sym); @@ -47,6 +50,7 @@ int32_t basGenJump(BasCompilerT *c, uint8_t op); void basGenJumpTo(BasCompilerT *c, uint8_t op, int32_t target); void basGenLoadTemp(BasCompilerT *c, int32_t slot); void basGenLoadVar(BasCompilerT *c, const BasSymT *sym); +void basGenNew(BasCompilerT *c, const char *name, int32_t objClass); void basGenPatchHere(BasCompilerT *c, int32_t operandPos); void basGenPushCStr(BasCompilerT *c, const char *text); void basGenPushInt16(BasCompilerT *c, int16_t v); @@ -112,6 +116,7 @@ void basGenCall(BasCompilerT *c, BasSymT *sym, int32_t argc) { basEmitU16(&c->cg, pi->externFuncIdx); basEmit8(&c->cg, (uint8_t)argc); basEmit8(&c->cg, sym->dataType); + basEmit32(&c->cg, (int32_t)pi->externBinaryMask); return; } @@ -251,6 +256,23 @@ void basGenLoadVar(BasCompilerT *c, const BasSymT *sym) { } +// New name: a new object of class objClass (a class number or +// BAS_CLASS_COLLECTION), or with BAS_CLASS_NONE a new copy of the form +// name. +void basGenNew(BasCompilerT *c, const char *name, int32_t objClass) { + if (objClass == BAS_CLASS_NONE) { + basGenPushCStr(c, name); + basEmit8(&c->cg, OP_BUILTIN); + basEmit8(&c->cg, BAS_FN_NEW_FORM); + basEmit8(&c->cg, BAS_NEW_FORM_ARGS); + return; + } + + basEmit8(&c->cg, OP_NEW_OBJ); + basEmitU16(&c->cg, classOperand(objClass)); +} + + // Lands the forward jump whose operand is at operandPos here. void basGenPatchHere(BasCompilerT *c, int32_t operandPos) { basPatch32(&c->cg, operandPos, basCodePos(&c->cg) - (operandPos + BAS_CODE_ADDR_SIZE)); @@ -604,8 +626,7 @@ static void genDeclarator(BasCompilerT *c, const BasDeclT *d) { } if (d->spec != NULL && d->spec->isNew) { - basEmit8(&c->cg, OP_NEW_OBJ); - basEmitU16(&c->cg, classOperand(sym->objClass)); + basGenNew(c, d->spec->name, sym->objClass); basGenStore(c, sym); return; } @@ -728,16 +749,10 @@ static void genProc(BasCompilerT *c, const BasProcT *proc) { basEmit8(&c->cg, OP_RET); } - // Parameters, locals and the temporaries share the frame + // Parameters, locals and the temporaries share the frame (the labels' + // patch lists are freed when basResolveUnitEnd closes the scope) pi->localCount = c->nextLocalSlot; c->debugProcCount++; - - // A label's patch lists live in the heap, its record in the unit arena - for (BasSymT *l = c->localLabels.first; l != NULL; l = l->orderNext) { - arrfree(l->u.label->patches); - arrfree(l->u.label->patchDepths); - arrfree(l->u.label->dataPatches); - } } @@ -772,7 +787,7 @@ static void genProcInits(BasCompilerT *c, const BasSymT *sym) { pi->localInits = NULL; if (count > 0) { - pi->localInits = (BasSlotInitT *)basArenaAlloc(&c->modArena, (int32_t)(count * (int32_t)sizeof(BasSlotInitT))); + pi->localInits = (BasSlotInitT *)basArenaAlloc(&c->modArena, count * (int32_t)sizeof(BasSlotInitT)); if (pi->localInits == NULL) { basErrorAt(c, 0, 0, "Out of memory"); diff --git a/src/apps/kpunch/dvxbasic/compiler/genExpr.c b/src/apps/kpunch/dvxbasic/compiler/genExpr.c index 10cf5958..90cf396a 100644 --- a/src/apps/kpunch/dvxbasic/compiler/genExpr.c +++ b/src/apps/kpunch/dvxbasic/compiler/genExpr.c @@ -152,8 +152,7 @@ void basGenExpr(BasCompilerT *c, const BasNodeT *n) { break; case NK_NEW: - basEmit8(&c->cg, OP_NEW_OBJ); - basEmitU16(&c->cg, (uint16_t)((const BasNameT *)n)->aux); + basGenNew(c, ((const BasNameT *)n)->name, ((const BasNameT *)n)->aux); break; case NK_ELEM: diff --git a/src/apps/kpunch/dvxbasic/compiler/genStmt.c b/src/apps/kpunch/dvxbasic/compiler/genStmt.c index 9310d480..c577af5e 100644 --- a/src/apps/kpunch/dvxbasic/compiler/genStmt.c +++ b/src/apps/kpunch/dvxbasic/compiler/genStmt.c @@ -376,6 +376,8 @@ void basGenStatement(BasCompilerT *c, const BasNodeT *s) { if (st->name != NULL) { basGenPushCStr(c, st->name); basEmit8(&c->cg, OP_LOAD_FORM); + } else if (st->a != NULL) { + basGenExpr(c, st->a); } else { basEmit8(&c->cg, OP_ME_REF); } diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c index 91b4313f..d1cd0806 100644 --- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c +++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c @@ -25,6 +25,7 @@ // See obfuscate.h for the high-level description. #include "obfuscate.h" +#include "strip.h" #include "basEvents.h" #include "lexer.h" #include "../basRes.h" @@ -33,15 +34,17 @@ #include #include #include +#include #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_MAP_INIT_CAP 16 // initial name-map capacity (doubles on growth) #define BAS_REM_KEYWORD_LEN 3 // "REM" -// .frm properties whose value names another control on the form; only -// these values follow a control rename. +// .frm properties whose value names another control or a menu of the +// form; only these values follow a rename. static const char *sNameRefKeys[] = { + "ContextMenu", "DataSource", "MasterSource", NULL @@ -74,6 +77,7 @@ static const char *nameMapAdd(NameMapT *m, const char *name); static void nameMapFree(NameMapT *m); static void nameMapInit(NameMapT *m); static const char *nameMapLookup(const NameMapT *m, const char *name); +static bool namesUsedAsData(const BasModuleT *mod); static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize, bool *outTruncated); 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); @@ -122,7 +126,11 @@ bool basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr } } - for (int32_t i = 0; ok && i < names.count; i++) { + // A program that reads names as text keeps them all (see + // namesUsedAsData); comments are still stripped below. + bool keepNames = namesUsedAsData(mod); + + for (int32_t i = 0; ok && !keepNames && i < names.count; i++) { if (!nameMapLookup(&reserved, names.entries[i].orig)) { ok = nameMapAdd(&map, names.entries[i].orig) != NULL; } @@ -387,7 +395,7 @@ static const char *nameMapAdd(NameMapT *m, const char *name) { } 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); char *origCopy = strdup(name); char *mappedCopy = strdup(mapped); @@ -438,6 +446,43 @@ static const char *nameMapLookup(const NameMapT *m, const char *name) { } +// True when the program uses control and form names as text: it reads +// the Name property, a form's Controls or DesignSelection (the member +// names are string constants of the module), or handles DesignMouse or +// DesignPropertyChanged, whose CtrlName is a name. Renaming would then +// change what its code sees. A string literal of the same spelling +// counts too, which only keeps names that could have been renamed. +static bool namesUsedAsData(const BasModuleT *mod) { + static const char *const sNameMembers[] = { BAS_CTRL_NAME, BAS_FORM_CONTROLS, BAS_FORM_DESIGNSEL }; + static const char *const sNameEvents[] = { BAS_EVT_DESIGNMOUSE, BAS_EVT_DSGNPROPCHG }; + + for (int32_t i = 0; i < mod->constCount; i++) { + for (int32_t m = 0; mod->constants[i] && m < (int32_t)(sizeof(sNameMembers) / sizeof(sNameMembers[0])); m++) { + if (strcasecmp(mod->constants[i]->data, sNameMembers[m]) == 0) { + return true; + } + } + } + + for (int32_t i = 0; i < mod->procCount; i++) { + char prefix[BAS_MAX_IDENT]; + const char *suffix; + + if (!basSplitEventName(mod->procs[i].name, prefix, (int32_t)sizeof(prefix), &suffix)) { + continue; + } + + for (int32_t e = 0; e < (int32_t)(sizeof(sNameEvents) / sizeof(sNameEvents[0])); e++) { + if (strcasecmp(suffix, sNameEvents[e]) == 0) { + return true; + } + } + } + + return false; +} + + // Copy the next whitespace-delimited token into buf and return a pointer // 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) { @@ -661,11 +706,12 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) { } } - // _ handlers follow the control rename. + // _ handlers follow the control rename (a form's + // for any event: a UserControl raises its own, as the stripper keeps) char prefix[BAS_MAX_IDENT]; const char *suffix; - if (!basSplitEventName(proc->name, prefix, (int32_t)sizeof(prefix), &suffix)) { + if (!basSplitProcHandler(proc, prefix, (int32_t)sizeof(prefix), &suffix)) { continue; } diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.h b/src/apps/kpunch/dvxbasic/compiler/obfuscate.h index efcc2d29..bec47bd0 100644 --- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.h +++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.h @@ -52,9 +52,12 @@ typedef struct { // - Procedure names matching _ // - 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. +// Only the values of properties that name another control or a menu +// (ContextMenu, DataSource, MasterSource) follow a rename; a Caption or +// Text that happens to equal a control name is left alone. A program +// that uses names as text (the Name property, Controls, DesignSelection, +// a DesignMouse handler) keeps every name: its code must see the names +// it was written with. // // frmTexts / frmLens: input .frm buffers (one per form). // outFrms: caller-allocated array of frmCount entries; function fills diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h index ec82f99d..f272ca53 100644 --- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h +++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h @@ -449,7 +449,13 @@ typedef enum { // second callback. This allows BASIC programs to use any library // (serial, security, third-party) without recompiling the runtime. -#define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType] +#define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType] [uint32 binaryMask] + +// OP_CALL_EXTERN binaryMask: bit i set = argument i was declared BINARY +// STRING (passed as pointer + int32 length); BAS_EXTERN_BINARY_RET set = +// the result is BINARY STRING (the callee gets a trailing int32_t * for +// its length). Arguments use bits 0 .. BAS_MAX_PARAMS - 1. +#define BAS_EXTERN_BINARY_RET 0x80000000u #define OP_GET_BLUE 0xD0 // pop LONG color; push (color>>16) & 0xFF @@ -606,9 +612,20 @@ typedef enum { BAS_FN_ERROR_TEXT = 81, // ERROR$([n]) / ERROR([n]) -> the text of error n (none: the current error's) BAS_FN_LIKE = 82, // a LIKE pattern: (a, pattern) -> a matches the pattern (Option Compare decides case) BAS_FN_FILEATTR = 83, // FILEATTR(n, attribute) -> the open mode of file n (BAS_FILEATTR_MODE) or its system handle (BAS_FILEATTR_HANDLE) - BAS_FN_QBCOLOR = 84 // QBCOLOR(n) -> QBasic colour n (0..15) as an RGB value + BAS_FN_QBCOLOR = 84, // QBCOLOR(n) -> QBasic colour n (0..15) as an RGB value + BAS_FN_NEW_FORM = 85, // New FormName (compiler-generated): (name) -> a new copy of the form (its own window, controls and form-level variables) + BAS_FN_CALLBYNAME = 86, // CALLBYNAME(object, name, callType [, args...]) -> the member's value or result (Empty for vbLet / vbSet) + BAS_FN_LOADFORMTEXT = 87 // LOADFORMTEXT(frmText) -> a form loaded from .frm text } BasBuiltinFnE; +// CallByName's call types (VB's vbMethod, vbGet, vbLet, vbSet) and its +// fixed arguments (object, name, call type) before the member's own. +#define BAS_CALLTYPE_METHOD 1 +#define BAS_CALLTYPE_GET 2 +#define BAS_CALLTYPE_LET 4 +#define BAS_CALLTYPE_SET 8 +#define BAS_CALLBYNAME_FIXED 3 + // BAS_FN_PARAMARRAY's first argument: the element type (low byte) and // this flag for an array that starts at 1 (ARRAY under OPTION BASE 1). #define BAS_PARAMARRAY_TYPE_MASK 0xFF @@ -879,9 +896,9 @@ typedef enum { #define BAS_OPERAND_I32 4 #define BAS_OPERAND_MEMBER 5 // [uint16 class] [uint16 member] [uint8 argc] #define BAS_OPERAND_CALL 6 // [int32 addr] [uint8 argc] [uint8 baseSlot] -#define BAS_OPERAND_CALL_EXTERN 6 // [uint16 lib] [uint16 func] [uint8 argc] [uint8 retType] #define BAS_OPERAND_FOR 7 // [uint16 varIdx] [uint8 scope] [int32 offset] #define BAS_OPERAND_F64 8 +#define BAS_OPERAND_CALL_EXTERN 10 // [uint16 lib] [uint16 func] [uint8 argc] [uint8 retType] [uint32 binaryMask] #define BAS_OPERAND_UNKNOWN (-1) // Code addresses (OP_CALL / OP_CALL_ME targets, procedure and set-up @@ -923,7 +940,7 @@ static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArg *maxArgs = 0; return true; - case BAS_FN_QBCOLOR: + case BAS_FN_QBCOLOR: case BAS_FN_NEW_FORM: case BAS_FN_LOADFORMTEXT: case BAS_FN_STRREVERSE: case BAS_FN_DATEVALUE: case BAS_FN_TIMEVALUE: case BAS_FN_YEAR: case BAS_FN_MONTH: case BAS_FN_DAY: case BAS_FN_HOUR: case BAS_FN_MINUTE: case BAS_FN_SECOND: @@ -1050,6 +1067,11 @@ static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArg *maxArgs = BAS_BUILTIN_MAX_ARGS; return true; + case BAS_FN_CALLBYNAME: + *minArgs = BAS_CALLBYNAME_FIXED; + *maxArgs = BAS_BUILTIN_MAX_ARGS; + return true; + case BAS_FN_PARAMARRAY: *minArgs = 1; *maxArgs = BAS_BUILTIN_MAX_ARGS; diff --git a/src/apps/kpunch/dvxbasic/compiler/parse.c b/src/apps/kpunch/dvxbasic/compiler/parse.c index 4a3c1c3b..efaa858b 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parse.c +++ b/src/apps/kpunch/dvxbasic/compiler/parse.c @@ -240,7 +240,7 @@ BasNodeT *basParseUnit(BasCompilerT *c) { // name buffer they are copied into); the lexer accepts longer tokens. static void checkIdentLength(BasCompilerT *c) { if (c->lex.token.type == TOK_IDENT && c->lex.token.textLen >= BAS_MAX_IDENT) { - basParseError(c, "Identifier too long (maximum is %d characters)", (int)(BAS_MAX_IDENT - 1)); + basParseError(c, "Identifier too long (maximum is %d characters)", BAS_MAX_IDENT - 1); } } diff --git a/src/apps/kpunch/dvxbasic/compiler/parseDecl.c b/src/apps/kpunch/dvxbasic/compiler/parseDecl.c index f1a6993c..6fdacc75 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parseDecl.c +++ b/src/apps/kpunch/dvxbasic/compiler/parseDecl.c @@ -469,6 +469,15 @@ static void declPassNames(BasCompilerT *c, DeclPointT **points) { if (c->lex.token.type == TOK_STRING_LIT) { if (isClass) { basDeclareClass(c, c->lex.token.text); + + // BEGINCLASS "Name" USERCONTROL: a .ctl's class + int32_t classIdx = basClassFind(c, c->lex.token.text); + + declAdvance(c); + + if (classIdx >= 0 && basParseCheckWord(c, BAS_UC_CLASS_KW)) { + c->classes[classIdx].isUserControl = true; + } } else { basDeclareForm(c, c->lex.token.text); } @@ -592,6 +601,12 @@ static void declSignatureAt(BasCompilerT *c, int32_t *classIdx, bool *isPrivate) if (basParseCheckWord(c, BAS_WRAP_BEGINCLASS)) { declAdvance(c); *classIdx = (c->lex.token.type == TOK_STRING_LIT) ? basClassFind(c, c->lex.token.text) : BAS_CLASS_NONE; + + // A UserControl's own object is its first member variable + if (*classIdx != BAS_CLASS_NONE && c->classes[*classIdx].isUserControl) { + basDeclareUserControlVar(c, *classIdx); + } + return; } @@ -973,8 +988,12 @@ static BasNodeT *parseDeclareLibrary(BasCompilerT *c) { return NULL; } + c->parseInDeclareLib = true; + BasProcT *proc = basParseProcHeader(c, kind, false); + c->parseInDeclareLib = false; + if (proc == NULL) { return NULL; } @@ -1377,6 +1396,11 @@ static BasNodeT *parseProcDef(BasCompilerT *c, uint8_t memberKind, bool isPrivat return NULL; } + if (c->blockDepth > BAS_UNIT_BLOCK_DEPTH) { + basParseError(c, "%s cannot be defined inside IF, FOR, DO, WHILE, SELECT CASE or WITH", kwName); + return NULL; + } + BasProcT *proc = basParseProcHeader(c, memberKind, true); if (proc == NULL) { @@ -1675,6 +1699,25 @@ static BasTypeSpecT *parseTypeSpec(BasCompilerT *c, bool allowNew) { basParseAdvance(c); return t; + // BINARY STRING: a native function's string as bytes + length + case TOK_BINARY: + if (!c->parseInDeclareLib) { + basParseError(c, "BINARY STRING is only allowed in a DECLARE LIBRARY block"); + return NULL; + } + + basParseAdvance(c); + + if (!basParseCheck(c, TOK_STRING_KW)) { + basParseErrorExpected(c, "STRING after BINARY"); + return NULL; + } + + t->tok = TOK_STRING_KW; + t->isBinary = true; + basParseAdvance(c); + return t; + default: basParseError(c, BAS_TYPE_EXPECTED); return NULL; diff --git a/src/apps/kpunch/dvxbasic/compiler/parseExpr.c b/src/apps/kpunch/dvxbasic/compiler/parseExpr.c index b0f30c2e..83a08a84 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parseExpr.c +++ b/src/apps/kpunch/dvxbasic/compiler/parseExpr.c @@ -1178,11 +1178,12 @@ static BasNodeT *parsePrimary(BasCompilerT *c) { } case TOK_NEW: { - // New ClassName / New Collection + // New ClassName / New Collection / New FormName (a copy of + // the form) basParseAdvance(c); - if (!basParseCheck(c, TOK_IDENT) || (basClassFind(c, c->lex.token.text) < 0 && strcasecmp(c->lex.token.text, BAS_COLLECTION_TYPE_NAME) != 0)) { - basParseErrorExpected(c, "class name (or Collection) after New"); + if (!basParseCheck(c, TOK_IDENT) || (basClassFind(c, c->lex.token.text) < 0 && strcasecmp(c->lex.token.text, BAS_COLLECTION_TYPE_NAME) != 0 && !basIsFormName(c, c->lex.token.text))) { + basParseErrorExpected(c, "class name, form name or Collection after New"); return NULL; } diff --git a/src/apps/kpunch/dvxbasic/compiler/parseStmt.c b/src/apps/kpunch/dvxbasic/compiler/parseStmt.c index 6ac36938..06d306a9 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parseStmt.c +++ b/src/apps/kpunch/dvxbasic/compiler/parseStmt.c @@ -683,7 +683,9 @@ BasNodeT *basParseStatement(BasCompilerT *c) { } case TOK_UNLOAD: { - // Unload Me / Unload FormName + // Unload Me / Unload FormName / Unload formExpr (a Form + // variable, obj.Window ...): the resolver tells a form's name + // from a variable. basParseAdvance(c); BasStmtT *s = newStmt(c, ST_UNLOAD); @@ -693,13 +695,14 @@ BasNodeT *basParseStatement(BasCompilerT *c) { } if (!basParseMatch(c, TOK_ME)) { - if (!basParseCheck(c, TOK_IDENT)) { + bool literal = basParseCheck(c, TOK_INT_LIT) || basParseCheck(c, TOK_LONG_LIT) || basParseCheck(c, TOK_FLOAT_LIT) || basParseCheck(c, TOK_STRING_LIT) || basParseCheck(c, TOK_DATE_LIT) || basParseCheck(c, TOK_CUR_LIT); + + if (literal || basParseAtEnd(c)) { basParseErrorExpected(c, "form name"); break; } - s->name = basParseTokenName(c); - basParseAdvance(c); + s->a = basParseExpr(c); } n = &s->h; @@ -801,6 +804,11 @@ BasNodeT *basParseStatement(BasCompilerT *c) { basParseAdvance(c); + // A UserControl's class: the declaration pass marked it + if (isClass && basParseCheckWord(c, BAS_UC_CLASS_KW)) { + basParseAdvance(c); + } + // A form's controls follow its name BasNodeT *tail = NULL; diff --git a/src/apps/kpunch/dvxbasic/compiler/resolve.c b/src/apps/kpunch/dvxbasic/compiler/resolve.c index e2ce4355..113d0169 100644 --- a/src/apps/kpunch/dvxbasic/compiler/resolve.c +++ b/src/apps/kpunch/dvxbasic/compiler/resolve.c @@ -71,7 +71,7 @@ typedef struct { // Function prototypes (alphabetical) -static bool addClassMember(BasCompilerT *c, const char *name, BasSymT *sym, uint8_t kind, bool isPrivate, const BasNodeT *at); +static bool addClassMember(BasCompilerT *c, const char *name, BasSymT *sym, uint8_t kind, const BasSymT *enumSym, bool isPrivate, const BasNodeT *at); static bool addEnumMember(BasCompilerT *c, const BasSymT *enumSym, const char *member, bool filePrivate, int32_t value, const BasNodeT *at); static BasSymT *addModuleSym(BasCompilerT *c, const char *name, uint8_t kind, uint8_t dataType); static bool aliasTargetShared(const BasCompilerT *c, const BasSymT *alias); @@ -88,6 +88,7 @@ void basDeclareForm(BasCompilerT *c, const char *name); void basDeclareLibrary(BasCompilerT *c, const BasStmtT *s); void basDeclareProc(BasCompilerT *c, BasProcT *proc, int32_t classIdx); void basDeclareTypeName(BasCompilerT *c, const char *name); +void basDeclareUserControlVar(BasCompilerT *c, int32_t classIdx); int32_t basFieldIndex(const BasTypeDefT *td, const char *name); bool basIsDefFnName(const char *name); bool basIsFormName(const BasCompilerT *c, const char *name); @@ -102,6 +103,7 @@ void basResolveUnitEnd(BasCompilerT *c, BasNodeT *unit); BasSymT *basStaticVariable(BasCompilerT *c, const char *name, uint8_t dataType, bool isTyped, int32_t objClass, int32_t fixedLen, const BasNodeT *at, BasSymT **outSlot); void basSymChange(BasCompilerT *c, BasSymT *sym, const BasNodeT *at, uint8_t dataType, int32_t udtTypeId, uint32_t flags); void basSymChangesUndo(BasCompilerT *c); +BasSymT *basUserControlProc(const BasCompilerT *c, int32_t classIdx, int32_t evt); static bool buildSignature(BasCompilerT *c, const BasProcT *proc, uint32_t flags, SignatureT *sig); static int32_t callLeavesValue(const BasNodeT *call); static BasSymT *classEventProc(BasCompilerT *c, const char *eventName, const BasNodeT *at); @@ -109,6 +111,7 @@ static void copySignature(BasSymT *sym, const SignatureT *sig); static void debugLocals(BasCompilerT *c, int32_t procIndex); static BasNodeT *defaultMemberTarget(BasCompilerT *c, BasNodeT *obj, bool host); static void defineLabel(BasCompilerT *c, BasStmtT *s); +static uint32_t externBinaryMask(const BasProcT *proc); static BasSymT *findLabel(BasCompilerT *c, const char *name); static BasSymT *findOrAddProc(BasCompilerT *c, const char *name, uint8_t kind, uint8_t returnType, const BasNodeT *at); static bool fractionalLiteral(const BasNodeT *n); @@ -121,7 +124,9 @@ static bool numericLiteral(const BasNodeT *n); static bool openProcScope(BasCompilerT *c, const char *name, uint8_t kind); static bool plainValue(const BasNodeT *n); static BasProcInfoT *procInfo(BasCompilerT *c, BasSymT *sym); +static int32_t procLeavesValue(const BasSymT *sym); static bool procSymbolName(BasCompilerT *c, int32_t classIdx, const char *name, uint8_t kind, char *out, int32_t outSize, const BasNodeT *at); +static const BasSymT *procValueEnum(const BasCompilerT *c, const BasProcT *proc); static BasSymT *refLabel(BasCompilerT *c, const char *name, const BasNodeT *at); static bool registerParams(BasCompilerT *c, BasProcT *proc, const SignatureT *sig); static void resolveAppend(BasCompilerT *c, BasStmtT *s); @@ -163,9 +168,12 @@ static void resolveSelect(BasCompilerT *c, BasSelectT *sel); static void resolveShared(BasCompilerT *c, BasStmtT *s); static void resolveStatic(BasCompilerT *c, BasStmtT *s); static void resolveTypeDef(BasCompilerT *c, BasStmtT *s); +static void resolveUnload(BasCompilerT *c, BasStmtT *s); +static void resolveUserControls(BasCompilerT *c); static void resolveWith(BasCompilerT *c, BasWithT *w); static void resolveWithEvents(BasCompilerT *c); static bool sameSignature(const BasSymT *sym, const SignatureT *sig); +static const BasSymT *specEnum(const BasCompilerT *c, const BasTypeSpecT *t); static void staticAllocation(BasCompilerT *c, BasSymT *local, BasSymT *slot, const BasDeclT *d, int32_t udt); static bool staticBound(BasCompilerT *c, BasNodeT *expr, BasConstValT *out); static BasConstValT *storeConst(BasCompilerT *c, const BasConstValT *v, BasArenaT *arena); @@ -173,9 +181,10 @@ static bool symDefined(const BasCompilerT *c, const BasSymT *sym); // Records a public member of the class being compiled (its name as -// programs write it after the dot). Private members are not recorded. -// Two members may share a name only when both are Property procedures. -static bool addClassMember(BasCompilerT *c, const char *name, BasSymT *sym, uint8_t kind, bool isPrivate, const BasNodeT *at) { +// programs write it after the dot, the ENUM its value is declared As or +// NULL). Private members are not recorded. Two members may share a name +// only when both are Property procedures. +static bool addClassMember(BasCompilerT *c, const char *name, BasSymT *sym, uint8_t kind, const BasSymT *enumSym, bool isPrivate, const BasNodeT *at) { BasClassDefT *cls = &c->classes[c->classIdx]; sym->ownerClass = c->classIdx; @@ -208,9 +217,10 @@ static bool addClassMember(BasCompilerT *c, const char *name, BasSymT *sym, uint BasClassMemberDefT def; memset(&def, 0, sizeof(def)); - def.name = basArenaStrDup(&c->modArena, name, (int32_t)strlen(name)); - def.sym = sym; - def.kind = kind; + def.name = basArenaStrDup(&c->modArena, name, (int32_t)strlen(name)); + def.sym = sym; + def.enumSym = enumSym; + def.kind = kind; arrput(cls->members, def); return true; } @@ -221,7 +231,7 @@ static bool addClassMember(BasCompilerT *c, const char *name, BasSymT *sym, uint // then ambiguous and must be qualified, as in VB). static bool addEnumMember(BasCompilerT *c, const BasSymT *enumSym, const char *member, bool filePrivate, int32_t value, const BasNodeT *at) { char qualified[BAS_MAX_IDENT]; - int n = snprintf(qualified, sizeof(qualified), "%s.%s", enumSym->name, member); + int n = snprintf(qualified, sizeof(qualified), "%s" BAS_CLASS_SEP "%s", enumSym->name, member); if (n < 0 || n >= (int)sizeof(qualified)) { basErrorNode(c, at, "ENUM member name '%s.%s' is too long", enumSym->name, member); @@ -355,6 +365,13 @@ static bool appendSafe(const BasNodeT *n) { } case NK_BUILTIN: { + const BasBuiltinT *b = (const BasBuiltinT *)((const BasCallT *)n)->target; + + // CallByName and LoadFormText run BASIC code (a member, Form_Load) + if (b != NULL && b->opcode == OP_BUILTIN && (b->func == BAS_FN_CALLBYNAME || b->func == BAS_FN_LOADFORMTEXT)) { + return false; + } + for (const BasNodeT *a = ((const BasCallT *)n)->args; a != NULL; a = a->next) { if (!appendSafe(a)) { return false; @@ -460,8 +477,15 @@ int32_t basClassFind(BasCompilerT *c, const char *name) { } -// Closes the scope openProcScope opened. +// Closes the scope openProcScope opened. Its labels' records are in the +// unit arena, their patch lists in the heap. void basCloseProcScope(BasCompilerT *c) { + for (const BasSymT *l = c->localLabels.first; l != NULL; l = l->orderNext) { + arrfree(l->u.label->patches); + arrfree(l->u.label->patchDepths); + arrfree(l->u.label->dataPatches); + } + c->inProc = false; c->inDefFn = false; c->curProc = NULL; @@ -570,9 +594,10 @@ void basDeclareLibrary(BasCompilerT *c, const BasStmtT *s) { } sym->dataType = returnType; - sym->flags |= SF_DEFINED | SF_EXTERN; - sym->u.proc->externLibIdx = libIdx; - sym->u.proc->externFuncIdx = basAddConstant(&c->cg, externName, (int32_t)strlen(externName)); + sym->flags |= SF_DEFINED | SF_EXTERN; + sym->u.proc->externLibIdx = libIdx; + sym->u.proc->externFuncIdx = basAddConstant(&c->cg, externName, (int32_t)strlen(externName)); + sym->u.proc->externBinaryMask = externBinaryMask(proc); copySignature(sym, &sig); } } @@ -677,6 +702,7 @@ void basDeclareProc(BasCompilerT *c, BasProcT *proc, int32_t classIdx) { sym->dataType = returnType; sym->ownerClass = classIdx; sym->objClass = cls; + sym->line = proc->h.line; sym->flags &= ~(SF_PRIVATE | SF_PROPERTY | SF_DEFINED | SF_FRIEND); sym->flags |= (proc->isPrivate ? SF_PRIVATE : 0) | (proc->isFriend ? SF_FRIEND : 0) | ((proc->memberKind == BAS_MEMBER_GET || proc->memberKind == BAS_MEMBER_LET || proc->memberKind == BAS_MEMBER_SET) ? SF_PROPERTY : 0); copySignature(sym, &sig); @@ -699,6 +725,24 @@ void basDeclareTypeName(BasCompilerT *c, const char *name) { } +// Declaration pass: the implicit UserControl member variable of a +// UserControl class (the control's own surface and host side, an Object +// the runtime sets before UserControl_Initialize). It takes the class's +// first member slot. +void basDeclareUserControlVar(BasCompilerT *c, int32_t classIdx) { + BasMemberVarDeclT decl; + + memset(&decl, 0, sizeof(decl)); + decl.name = BAS_UC_OBJECT; + decl.classIdx = classIdx; + decl.udtTypeId = -1; + decl.objClass = BAS_CLASS_NONE; + decl.dataType = BAS_TYPE_OBJECT; + decl.flags = SF_TYPED | SF_PRIVATE; + arrput(c->memberVarDecls, decl); +} + + int32_t basFieldIndex(const BasTypeDefT *td, const char *name) { for (int32_t i = 0; i < td->fieldCount; i++) { if (strcasecmp(td->fields[i].name, name) == 0) { @@ -773,6 +817,7 @@ void basResolveModuleEnd(BasCompilerT *c) { if (!c->hasError) { resolveImplements(c); resolveWithEvents(c); + resolveUserControls(c); } for (const BasSymT *l = c->moduleLabels.first; l != NULL && !c->hasError; l = l->orderNext) { @@ -898,7 +943,10 @@ void basResolveStatement(BasCompilerT *c, BasNodeT *s) { case ST_END: case ST_DATA: case ST_LOAD: + break; + case ST_UNLOAD: + resolveUnload(c, (BasStmtT *)s); break; case ST_MIDASSIGN: @@ -1040,6 +1088,13 @@ bool basResolveTypeSpec(BasCompilerT *c, const BasTypeSpecT *t, bool prescan, ui return true; } + // A form of the project stands for a Form object (that form or a copy + // of it made with New) + if (basIsFormName(c, t->name)) { + *outType = BAS_TYPE_OBJECT; + return true; + } + for (int32_t i = 0; i < DVX_ARRAY_LEN(sObjectTypeNames); i++) { if (strcasecmp(t->name, sObjectTypeNames[i]) == 0) { *outType = BAS_TYPE_OBJECT; @@ -1065,7 +1120,9 @@ void basResolveUnit(BasCompilerT *c, BasNodeT *unit) { // After the generator: a procedure's frame size checked, its locals given -// to the debugger, its scope closed; a form's or class's scope closed. +// to the debugger, its scope closed; a form's or class's scope closed. A +// procedure scope never outlives its unit (its symbols are in the unit +// arena), so one an error left open (a DEF FN, a procedure) closes too. void basResolveUnitEnd(BasCompilerT *c, BasNodeT *unit) { // A form's or class's code ends after its closing marker is generated if (unit->kind == ST_ENDFORM && c->inForm) { @@ -1077,7 +1134,7 @@ void basResolveUnitEnd(BasCompilerT *c, BasNodeT *unit) { return; } - if (unit->kind == ST_ENDCLASS && c->inClass && !c->inProc) { + if (unit->kind == ST_ENDCLASS && c->inClass) { c->inClass = false; c->classIdx = BAS_CLASS_NONE; c->fileIdx = c->outerFileIdx; @@ -1085,13 +1142,13 @@ void basResolveUnitEnd(BasCompilerT *c, BasNodeT *unit) { return; } - if (unit->kind != ST_PROC || !c->inProc) { + if (!c->inProc) { return; } - BasProcT *proc = (BasProcT *)unit; + const BasProcT *proc = (const BasProcT *)unit; - if (!c->hasError && proc->sym != NULL) { + if (unit->kind == ST_PROC && !c->hasError && proc->sym != NULL) { int32_t localCount = proc->sym->u.proc->localCount; // A call frame holds BAS_VM_MAX_LOCALS slots: parameters, locals @@ -1194,6 +1251,29 @@ void basSymChangesUndo(BasCompilerT *c) { } +// The handler of UserControl event evt (BasUcEventE) in UserControl class +// classIdx: its procedure UserControl_, NULL when it has none. +BasSymT *basUserControlProc(const BasCompilerT *c, int32_t classIdx, int32_t evt) { + const BasUcEventInfoT *info = basUcEventInfo(evt); + char member[BAS_MAX_IDENT]; + char qualified[BAS_MAX_IDENT]; + + if (info == NULL) { + return NULL; + } + + snprintf(member, sizeof(member), "%s_%s", BAS_UC_OBJECT, info->name); + + if (!basQualify(c, classIdx, member, qualified, (int32_t)sizeof(qualified))) { + return NULL; + } + + BasSymT *sym = basScopeFind(&c->moduleTab, qualified); + + return (sym != NULL && (sym->kind == SK_SUB || sym->kind == SK_FUNCTION) && (sym->flags & SF_DEFINED)) ? sym : NULL; +} + + // The signature of a parameter list: types (AS, suffix or DEFxxx; an // untyped ParamArray holds Variants), ByVal, Optional, defaults (with // SIG_DEFINE folded to constants of the parameter's type). False after @@ -1311,7 +1391,7 @@ static bool buildSignature(BasCompilerT *c, const BasProcT *proc, uint32_t flags // The parameters' names, for arguments given by name if (i > 0) { - sig->info.paramNames = (const char **)basArenaAlloc(&c->modArena, (int32_t)(i * (int32_t)sizeof(const char *))); + sig->info.paramNames = (const char **)basArenaAlloc(&c->modArena, i * (int32_t)sizeof(const char *)); if (sig->info.paramNames == NULL) { basErrorNode(c, &proc->h, "Out of memory"); @@ -1348,7 +1428,7 @@ static bool buildSignature(BasCompilerT *c, const BasProcT *proc, uint32_t flags // called as a statement). static int32_t callLeavesValue(const BasNodeT *call) { if (call->kind == NK_FCALL) { - return (((const BasSymT *)((const BasCallT *)call)->target)->kind == SK_FUNCTION) ? 1 : 0; + return procLeavesValue((const BasSymT *)((const BasCallT *)call)->target); } return (call->kind == NK_METHOD) ? 1 : 0; @@ -1466,6 +1546,25 @@ static void defineLabel(BasCompilerT *c, BasStmtT *s) { // The label named name of the current procedure (a local label inside a // SUB / FUNCTION, a module label outside), or NULL. +// A DECLARE LIBRARY procedure's OP_CALL_EXTERN binary mask: bit i for +// each BINARY STRING parameter, BAS_EXTERN_BINARY_RET for a BINARY +// STRING result. +static uint32_t externBinaryMask(const BasProcT *proc) { + uint32_t mask = (proc->retSpec != NULL && proc->retSpec->isBinary) ? BAS_EXTERN_BINARY_RET : 0; + uint32_t bit = 1; + + for (const BasNodeT *pn = proc->params; pn != NULL; pn = pn->next, bit <<= 1) { + const BasParamT *p = (const BasParamT *)pn; + + if (p->spec != NULL && p->spec->isBinary) { + mask |= bit; + } + } + + return mask; +} + + static BasSymT *findLabel(BasCompilerT *c, const char *name) { return basScopeFind(c->inProc && !c->inDefFn ? &c->localLabels : &c->moduleLabels, name); } @@ -1715,6 +1814,14 @@ static BasProcInfoT *procInfo(BasCompilerT *c, BasSymT *sym) { } +// 1 when calling procedure sym leaves a value on the stack: a FUNCTION's +// result, or the dummy result every native (DECLARE LIBRARY) call pushes, +// SUBs too; else 0. +static int32_t procLeavesValue(const BasSymT *sym) { + return (sym->kind == SK_FUNCTION || (sym->flags & SF_EXTERN)) ? 1 : 0; +} + + // The symbol name of procedure name of member kind in class classIdx // (BAS_CLASS_NONE: not in a class): "Class.name", "Class.name.Let" / // ".Set" for Property Let / Set; else name. False after the error when @@ -1738,6 +1845,30 @@ static bool procSymbolName(BasCompilerT *c, int32_t classIdx, const char *name, } +// The ENUM a Property procedure's value is declared As: a Property Get's +// type, a Property Let's or Set's last parameter's; NULL for another type +// and for a SUB or FUNCTION. +static const BasSymT *procValueEnum(const BasCompilerT *c, const BasProcT *proc) { + const BasParamT *last = NULL; + + for (const BasNodeT *p = proc->params; p != NULL; p = p->next) { + last = (const BasParamT *)p; + } + + switch (proc->memberKind) { + case BAS_MEMBER_GET: + return specEnum(c, proc->retSpec); + + case BAS_MEMBER_LET: + case BAS_MEMBER_SET: + return (last != NULL) ? specEnum(c, last->spec) : NULL; + + default: + return NULL; + } +} + + // The label a GOTO / GOSUB / ON ERROR names: defined there or later in the // same procedure (or module), else an error at the end of it. static BasSymT *refLabel(BasCompilerT *c, const char *name, const BasNodeT *at) { @@ -2056,7 +2187,7 @@ static void resolveBeginClass(BasCompilerT *c, BasStmtT *s) { // BEGINFORM "Name": module-level DIMs make variables of the form. static void resolveBeginForm(BasCompilerT *c, BasStmtT *s) { if ((int32_t)strlen(s->name) >= BAS_MAX_IDENT) { - basErrorNode(c, &s->h, "Form name too long (maximum is %d characters)", (int)(BAS_MAX_IDENT - 1)); + basErrorNode(c, &s->h, "Form name too long (maximum is %d characters)", BAS_MAX_IDENT - 1); return; } @@ -2165,7 +2296,7 @@ static void resolveCallStmt(BasCompilerT *c, BasStmtT *s) { } s->a = basResolveCallee(c, call, true); - s->ival2 = (sym->kind == SK_FUNCTION) ? 1 : 0; + s->ival2 = procLeavesValue(sym); return; } @@ -2173,6 +2304,24 @@ static void resolveCallStmt(BasCompilerT *c, BasStmtT *s) { BasNameT *n = (BasNameT *)callee; BasSymT *sym = basLookup(c, n->name); + // CallByName obj, name, callType, args: the function, its value + // dropped (VB's statement form) + if (sym == NULL && strcasecmp(n->name, BAS_CALLBYNAME_NAME) == 0) { + BasCallT *byName = (BasCallT *)basNodeNew(&c->unitArena, (int32_t)sizeof(BasCallT), NK_CALL, callee->line, callee->col); + + if (byName == NULL) { + return; + } + + byName->callee = callee; + byName->args = s->b; + byName->argc = (int16_t)s->ival2; + s->a = basResolveExpr(c, &byName->h, BAS_USE_VALUE); + s->b = NULL; + s->ival2 = 1; + return; + } + if (sym == NULL) { basErrorNode(c, callee, "Undefined Sub: %s", n->name); return; @@ -2200,7 +2349,7 @@ static void resolveCallStmt(BasCompilerT *c, BasStmtT *s) { call->argc = (int16_t)argc; s->a = &call->h; s->b = NULL; - s->ival2 = (sym->kind == SK_FUNCTION) ? 1 : 0; + s->ival2 = procLeavesValue(sym); return; } @@ -2276,8 +2425,11 @@ static void resolveDeclarator(BasCompilerT *c, BasDeclT *d, bool isShared, bool fixedLen = d->spec->fixedLen; } - if (asNew && (objClass == BAS_CLASS_NONE || d->isArray)) { - basErrorNode(c, &d->h, d->isArray ? "New cannot be used with an array" : "New needs a class name (or Collection)"); + // As New FormName: a copy of the form (an untyped Form object) + bool newForm = asNew && d->spec->name != NULL && basIsFormName(c, d->spec->name); + + if (asNew && ((objClass == BAS_CLASS_NONE && !newForm) || d->isArray)) { + basErrorNode(c, &d->h, d->isArray ? "New cannot be used with an array" : "New needs a class name, a form name or Collection"); return; } @@ -2352,7 +2504,7 @@ static void resolveDeclarator(BasCompilerT *c, BasDeclT *d, bool isShared, bool d->sym = sym; - if (isMember && !addClassMember(c, d->name, sym, BAS_MEMBER_VAR, isPrivate, &d->h)) { + if (isMember && !addClassMember(c, d->name, sym, BAS_MEMBER_VAR, d->isArray ? NULL : specEnum(c, d->spec), isPrivate, &d->h)) { return; } @@ -2703,6 +2855,8 @@ static void resolveEnum(BasCompilerT *c, BasStmtT *s) { // Event Name(params) in a class module: no Optional or ParamArray // parameters (VB); an event and a member may not share a name. +_Static_assert(BAS_MAX_PARAMS <= BAS_VM_MAX_CALL_ARGS, "an event's parameters must fit a RaiseEvent call"); + static void resolveEvent(BasCompilerT *c, BasProcT *ev) { BasClassDefT *cls = &c->classes[c->classIdx]; @@ -2731,10 +2885,21 @@ static void resolveEvent(BasCompilerT *c, BasProcT *ev) { return; } + // The parameters' names, comma-separated (a UserControl's host sees them) + char names[BAS_VM_MAX_CALL_ARGS * BAS_MAX_IDENT]; + int32_t namesLen = 0; + + names[0] = '\0'; + + for (const BasNodeT *p = ev->params; p != NULL; p = p->next) { + namesLen += snprintf(names + namesLen, sizeof(names) - (size_t)namesLen, "%s%s", (namesLen > 0) ? BAS_PARAM_NAME_SEP : "", ((const BasParamT *)p)->name); + } + BasEventDefT def; memset(&def, 0, sizeof(def)); def.name = basArenaStrDup(&c->modArena, ev->name, (int32_t)strlen(ev->name)); + def.paramNames = basArenaStrDup(&c->modArena, names, namesLen); def.paramCount = sig.info.paramCount; memcpy(def.paramTypes, sig.info.paramTypes, sizeof(def.paramTypes)); @@ -3165,11 +3330,6 @@ static void resolveMemberCallStmt(BasCompilerT *c, BasStmtT *s) { method->args = s->b; method->argc = (int16_t)s->ival2; } else { - if (s->ival2 > BAS_VM_MAX_CALL_ARGS) { - basErrorNode(c, &s->h, "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); - return; - } - basLateArgs(c, method, s->b, s->ival2, !host); } @@ -3303,7 +3463,7 @@ static void resolveProc(BasCompilerT *c, BasProcT *proc) { procSym->flags |= SF_DEFINED | (returnTyped ? SF_TYPED : 0) | (proc->isPrivate ? SF_PRIVATE : 0) | (proc->isFriend ? SF_FRIEND : 0) | (isProperty ? SF_PROPERTY : 0); copySignature(procSym, &sig); - if (c->inClass && !addClassMember(c, proc->name, procSym, kind, proc->isPrivate, &proc->h)) { + if (c->inClass && !addClassMember(c, proc->name, procSym, kind, procValueEnum(c, proc), proc->isPrivate, &proc->h)) { return; } @@ -3699,6 +3859,51 @@ static void resolveTypeDef(BasCompilerT *c, BasStmtT *s) { } +// UNLOAD target: a bare name that is not a variable or a function is a +// form's name (loaded by name, as VB); anything else -- a Form variable, +// obj.Window, a function giving a form -- is evaluated for the form. +static void resolveUnload(BasCompilerT *c, BasStmtT *s) { + if (s->a == NULL) { + return; + } + + if (s->a->kind == NK_NAME) { + const char *name = ((BasNameT *)s->a)->name; + const BasSymT *sym = basLookup(c, name); + + if (sym == NULL || (sym->kind != SK_VAR && sym->kind != SK_FUNCTION)) { + s->name = name; + s->a = NULL; + return; + } + } + + s->a = basResolveExpr(c, s->a, BAS_USE_OBJECT); +} + + +// End of the program: every UserControl event handler a control class +// has is a SUB with exactly the event's parameters (the runtime calls it +// with them). +static void resolveUserControls(BasCompilerT *c) { + for (int32_t k = 0; k < (int32_t)arrlen(c->classes) && !c->hasError; k++) { + if (!c->classes[k].isUserControl) { + continue; + } + + for (int32_t e = 0; e < BAS_UC_EVT_COUNT && !c->hasError; e++) { + const BasSymT *sym = basUserControlProc(c, k, e); + const BasUcEventInfoT *info = basUcEventInfo(e); + int32_t want = basParamListCount(info->params); + + if (sym != NULL && (sym->kind != SK_SUB || sym->u.proc->paramCount != want)) { + basErrorAt(c, sym->line, 0, "%s_%s must be a Sub with %d parameter(s)%s%s", BAS_UC_OBJECT, info->name, (int)want, (want > 0) ? ": " : "", info->params); + } + } + } +} + + // WITH object ... END WITH: a control, form, Me or TYPE record (a // variable, element or field) is named again at each '.'; any other object // is evaluated once into a hidden variable (VB), its class kept for early @@ -3800,6 +4005,15 @@ static bool sameSignature(const BasSymT *sym, const SignatureT *sig) { } +// The ENUM a type spec names (As EnumName), as basResolveTypeSpec finds +// it; NULL for no spec and any other type. +static const BasSymT *specEnum(const BasCompilerT *c, const BasTypeSpecT *t) { + const BasSymT *sym = (t != NULL && t->name != NULL) ? basModuleFind(c, t->name) : NULL; + + return (sym != NULL && sym->kind == SK_ENUM) ? sym : NULL; +} + + // A STATIC array or TYPE (local, its procedure's alias of slot): the // slot becomes an array of the declarator's constant bounds (none: a // dynamic array the procedure REDIMs) or a TYPE instance, allocated once diff --git a/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c b/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c index 22ccc777..fd291f51 100644 --- a/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c +++ b/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c @@ -235,6 +235,7 @@ static const BasBuiltinT sBuiltins[] = { {"YEAR", OP_BUILTIN, BAS_FN_YEAR, 0, 0, BAS_NODE_TYPE_UNKNOWN, BI_PURE, 0, NULL}, // Variants and objects + {"CALLBYNAME", OP_BUILTIN, BAS_FN_CALLBYNAME, 0, 0, BAS_NODE_TYPE_UNKNOWN, BI_OBJECT_ARG, 0, NULL}, {"CVAR", OP_BUILTIN, BAS_FN_CVAR, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"ISARRAY", OP_BUILTIN, BAS_FN_ISARRAY, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"ISEMPTY", OP_BUILTIN, BAS_FN_ISEMPTY, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, @@ -242,6 +243,7 @@ static const BasBuiltinT sBuiltins[] = { {"ISNULL", OP_BUILTIN, BAS_FN_ISNULL, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"ISNUMERIC", OP_BUILTIN, BAS_FN_ISNUMERIC, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"ISOBJECT", OP_BUILTIN, BAS_FN_ISOBJECT, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, + {"LOADFORMTEXT", OP_BUILTIN, BAS_FN_LOADFORMTEXT, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"LOADPICTURE", OP_BUILTIN, BAS_FN_LOADPICTURE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"TYPENAME", OP_BUILTIN, BAS_FN_TYPENAME, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}, {"VARTYPE", OP_BUILTIN, BAS_FN_VARTYPE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL} @@ -608,11 +610,8 @@ int32_t basExprClass(const BasCompilerT *c, const BasNodeT *n) { case NK_ME: return (((const BasNameT *)n)->aux == OP_ME_OBJ) ? c->classIdx : BAS_CLASS_NONE; - case NK_NEW: { - int32_t cls = ((const BasNameT *)n)->aux; - - return (cls == BAS_NEW_COLLECTION) ? BAS_CLASS_COLLECTION : cls; - } + case NK_NEW: + return ((const BasNameT *)n)->aux; case NK_REF: return basExprClass(c, ((const BasOpT *)n)->a); @@ -694,12 +693,18 @@ uint8_t basImplicitType(const BasCompilerT *c, const char *name, bool *outTyped) // call: as basResolveMethodArgs gives them, those given by name (after the // positional ones) followed by a string of their names that OP_CALL_NAMED // reads, call->aux2 how many (0: none). A control or form method takes -// no names. False after the error. +// no names. False after the error. The argument limit is checked first: +// names holds a name (under BAS_MAX_IDENT characters) and a separator or +// the terminator per argument. bool basLateArgs(BasCompilerT *c, BasCallT *call, BasNodeT *args, int32_t argc, bool byRef) { - char names[BAS_MAX_PARAMS * BAS_MAX_IDENT]; - int32_t named = 0; + char names[BAS_VM_MAX_CALL_ARGS * BAS_MAX_IDENT]; + int32_t namesLen = 0; + int32_t named = 0; - names[0] = '\0'; + if (argc > BAS_VM_MAX_CALL_ARGS) { + basErrorNode(c, &call->h, "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + return false; + } for (BasNodeT **link = &args; *link != NULL; link = &(*link)->next) { BasNodeT *a = *link; @@ -713,11 +718,7 @@ bool basLateArgs(BasCompilerT *c, BasCallT *call, BasNodeT *args, int32_t argc, return false; } - if (named > 0) { - strcat(names, BAS_PARAM_NAME_SEP); - } - - strcat(names, ((const BasMemberT *)a)->name); + namesLen += snprintf(names + namesLen, sizeof(names) - (size_t)namesLen, "%s%s", named > 0 ? BAS_PARAM_NAME_SEP : "", ((const BasMemberT *)a)->name); // The value takes the named argument's place in the list BasNodeT *value = ((BasMemberT *)a)->base; @@ -742,10 +743,9 @@ bool basLateArgs(BasCompilerT *c, BasCallT *call, BasNodeT *args, int32_t argc, } // The node outlives this buffer: the names go in the unit arena - int32_t len = (int32_t)strlen(names); - char *kept = basArenaStrDup(&c->unitArena, names, len); + char *kept = basArenaStrDup(&c->unitArena, names, namesLen); - tail->next = (kept != NULL) ? basNodeStr(c, kept, len, &call->h) : NULL; + tail->next = (kept != NULL) ? basNodeStr(c, kept, namesLen, &call->h) : NULL; if (tail->next == NULL) { basErrorNode(c, &call->h, "Out of memory"); @@ -1201,8 +1201,9 @@ BasNodeT *basResolveCallee(BasCompilerT *c, BasCallT *call, bool statement) { return basBindMember(c, obj, cls, m->name, call->args, call->argc, statement ? BAS_ACCESS_STATEMENT : BAS_ACCESS_VALUE, &call->h); } - // name.Ctrl(i).Member: a control array element - if (call->h.flags & NF_MEMBERBASE) { + // name.Ctrl(i).Member: a control array element (name.Controls(i) is + // an item of the form's collection of controls) + if ((call->h.flags & NF_MEMBERBASE) && strcasecmp(m->name, BAS_FORM_CONTROLS) != 0) { if (call->argc != 1 || call->args->kind == NK_OMIT) { basErrorNode(c, &call->h, "Control array index expected"); return NULL; @@ -1314,7 +1315,13 @@ BasNodeT *basResolveExpr(BasCompilerT *c, BasNodeT *n, uint8_t use) { return NULL; } - nw->aux = (cls >= 0) ? cls : BAS_NEW_COLLECTION; + // A class, a form (BAS_CLASS_NONE: a copy of it) or Collection + if (cls >= 0) { + nw->aux = cls; + } else { + nw->aux = basIsFormName(c, nw->name) ? BAS_CLASS_NONE : BAS_CLASS_COLLECTION; + } + return n; } @@ -1540,7 +1547,7 @@ BasNodeT *basResolveMemberParts(BasCompilerT *c, BasMemberT *m, bool isProp, Bas // the form the first name names or holds; objVar.Member.x: a member // of the object a variable of a known class holds (a control of the // form, Img.Picture.Width, is the control below) - if ((m->h.flags & NF_MEMBERBASE) && (sym == NULL || sym->kind != SK_CONTROL)) { + if ((m->h.flags & NF_MEMBERBASE) && (sym == NULL || sym->kind != SK_CONTROL) && (sym != NULL || strcasecmp(bn->name, BAS_SCREEN_OBJ_NAME) != 0)) { *outObj = (sym != NULL && sym->kind == SK_VAR) ? basNodeVar(c, sym, base) : basLoadFormNode(c, bn->name, base); if (*outObj != NULL && (*outObj)->kind == NK_VAR) { @@ -1655,14 +1662,13 @@ BasNodeT *basResolveMemberParts(BasCompilerT *c, BasMemberT *m, bool isProp, Bas BasNodeT *basResolveMethodArgs(BasCompilerT *c, BasNodeT *args, int32_t argc, bool byRef) { BasNodeT *head = NULL; BasNodeT *tail = NULL; - int32_t i = 0; if (argc > BAS_VM_MAX_CALL_ARGS) { basErrorNode(c, args, "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); return NULL; } - for (BasNodeT *a = args; a != NULL; i++) { + for (BasNodeT *a = args; a != NULL;) { BasNodeT *next = a->next; BasNodeT *r; @@ -1700,10 +1706,12 @@ BasNodeT *basResolveObjectMember(BasCompilerT *c, BasNodeT *obj, const char *nam return basBindMember(c, obj, memberClass, name, NULL, 0, BAS_ACCESS_READ, at); } - // Before another '.', a member of a form is one of its controls; of a + // Before another '.', a member of a form is one of its controls (but + // the object-valued members Controls / Parent / Container); of a // control, a property whose value has members (Img.Picture.Width) bool control = obj != NULL && (obj->kind == NK_FINDCTRL || obj->kind == NK_FINDCTRLIDX); - BasMemberT *m = (BasMemberT *)basNodeNew(&c->unitArena, (int32_t)sizeof(BasMemberT), ((flags & NF_MEMBERBASE) && !control) ? NK_FINDCTRL : NK_PROP, at->line, at->col); + bool findIt = (flags & NF_MEMBERBASE) && !control && !basIsObjectMember(name); + BasMemberT *m = (BasMemberT *)basNodeNew(&c->unitArena, (int32_t)sizeof(BasMemberT), findIt ? NK_FINDCTRL : NK_PROP, at->line, at->col); if (m == NULL) { return NULL; @@ -2016,11 +2024,18 @@ static bool canIndexValue(const BasSymT *sym) { static BasSymT *classSymbol(BasCompilerT *c, int32_t cls, const char *name, const char *suffix) { char qualified[BAS_MAX_IDENT]; - if (!basQualify(c, cls, name, qualified, (int32_t)sizeof(qualified)) || strlen(qualified) + strlen(suffix) >= sizeof(qualified)) { + if (!basQualify(c, cls, name, qualified, (int32_t)sizeof(qualified))) { return NULL; } - strcat(qualified, suffix); + size_t len = strlen(qualified); + size_t suffixLen = strlen(suffix); + + if (len + suffixLen >= sizeof(qualified)) { + return NULL; + } + + memcpy(qualified + len, suffix, suffixLen + 1); BasSymT *sym = basScopeFind(&c->moduleTab, qualified); @@ -2455,7 +2470,8 @@ static BasNodeT *resolveBuiltin(BasCompilerT *c, const BasBuiltinT *b, BasCallT // OP_BUILTIN function is Missing: the VM gives it its default for (BasNodeT *a = call->args; a != NULL;) { BasNodeT *next = a->next; - BasNodeT *r = (a->kind == NK_OMIT && b->opcode == OP_BUILTIN) ? basNodeSpecial(c, BAS_SPECIAL_MISSING, a) : basResolveExpr(c, a, BAS_USE_VALUE); + uint8_t use = (a == call->args && (b->flags & BI_OBJECT_ARG)) ? BAS_USE_OBJECT : BAS_USE_VALUE; + BasNodeT *r = (a->kind == NK_OMIT && b->opcode == OP_BUILTIN) ? basNodeSpecial(c, BAS_SPECIAL_MISSING, a) : basResolveExpr(c, a, use); if (r == NULL) { return NULL; @@ -2609,7 +2625,16 @@ static BasNodeT *resolveName(BasCompilerT *c, BasNameT *n, uint8_t use) { return NULL; } - // A FUNCTION named without an argument list is a call with none (QB) + // Inside its own body a FUNCTION's name without an argument list is + // its result so far (VB: F = F + 1); F() calls it again + if (sym != NULL && sym->kind == SK_FUNCTION && c->inProc && sym == c->curProc) { + n->h.kind = NK_RESULT; + n->sym = sym; + return (BasNodeT *)n; + } + + // Elsewhere a FUNCTION named without an argument list is a call with + // none (QB) if (sym != NULL && sym->kind == SK_FUNCTION) { BasCallT *call = (BasCallT *)basNodeNew(&c->unitArena, (int32_t)sizeof(BasCallT), NK_FCALL, n->h.line, n->h.col); int32_t argc = 0; @@ -2821,34 +2846,13 @@ static uint8_t suffixType(const char *name, bool *outHasSuffix) { // The name programs write for a scalar type (messages). static const char *typeDisplayName(uint8_t dataType) { + const char *scalar = basValScalarTypeName(dataType); + + if (scalar) { + return scalar; + } + switch (dataType) { - case BAS_TYPE_INTEGER: - return "Integer"; - - case BAS_TYPE_LONG: - return "Long"; - - case BAS_TYPE_SINGLE: - return "Single"; - - case BAS_TYPE_DOUBLE: - return "Double"; - - case BAS_TYPE_STRING: - return "String"; - - case BAS_TYPE_BOOLEAN: - return "Boolean"; - - case BAS_TYPE_DATE: - return "Date"; - - case BAS_TYPE_BYTE: - return "Byte"; - - case BAS_TYPE_CURRENCY: - return "Currency"; - case BAS_TYPE_ARRAY: return "an array"; diff --git a/src/apps/kpunch/dvxbasic/compiler/scope.h b/src/apps/kpunch/dvxbasic/compiler/scope.h index d6d89a18..058c7ad3 100644 --- a/src/apps/kpunch/dvxbasic/compiler/scope.h +++ b/src/apps/kpunch/dvxbasic/compiler/scope.h @@ -135,6 +135,7 @@ typedef struct { int16_t requiredParams; // up to the last non-Optional parameter uint16_t externLibIdx; // DECLARE LIBRARY: constant pool indices uint16_t externFuncIdx; + uint32_t externBinaryMask; // DECLARE LIBRARY: BINARY STRING parameters (bit i) and result (BAS_EXTERN_BINARY_RET) uint8_t paramTypes[BAS_MAX_PARAMS]; uint8_t paramFlags[BAS_MAX_PARAMS]; // PARAM_* BasConstValT *defaults; // arena: per parameter (only PARAM_DEFAULT ones used, BAS_DEFAULT_PENDING until evaluated); NULL when none @@ -217,9 +218,10 @@ typedef struct { // A public member of a class module. typedef struct { - const char *name; // as programs write it (obj.name), or "Iface.name" - BasSymT *sym; // procedure or member variable - uint8_t kind; // BasMemberKindE + const char *name; // as programs write it (obj.name), or "Iface.name" + BasSymT *sym; // procedure or member variable + const BasSymT *enumSym; // the ENUM its value is declared As (a variable's or Property Get's type, a Property Let's or Set's last parameter's); NULL: another type + uint8_t kind; // BasMemberKindE } BasClassMemberDefT; // An Event a class module declares. @@ -228,6 +230,7 @@ typedef struct { int32_t paramCount; uint8_t paramTypes[BAS_MAX_PARAMS]; uint8_t paramFlags[BAS_MAX_PARAMS]; // PARAM_BYVAL, PARAM_TYPED + const char *paramNames; // module arena: the parameters' names, comma-separated ("" for none) } BasEventDefT; typedef struct { @@ -243,6 +246,7 @@ typedef struct { int32_t implementsLine; bool defined; bool isInterface; // some class Implements it + bool isUserControl; // BEGINCLASS "Name" USERCONTROL: a .ctl, a widget class written in BASIC } BasClassDefT; // A class member variable the declaration pass read (registered once diff --git a/src/apps/kpunch/dvxbasic/compiler/strip.c b/src/apps/kpunch/dvxbasic/compiler/strip.c index f102a71b..7e92bde0 100644 --- a/src/apps/kpunch/dvxbasic/compiler/strip.c +++ b/src/apps/kpunch/dvxbasic/compiler/strip.c @@ -54,6 +54,8 @@ const char *basEventSuffixes[] = { // Function prototypes (alphabetical) bool basEventSuffixMatch(const char *suffix); bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix); +bool basSplitHandlerName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix); +bool basSplitProcHandler(const BasProcEntryT *proc, char *prefix, int32_t prefixSize, const char **outSuffix); void basStripModule(BasModuleT *mod); static int compareNamesCI(const void *a, const void *b); static bool sortedHasName(const char **names, int32_t count, const char *name); @@ -71,9 +73,14 @@ bool basEventSuffixMatch(const char *suffix) { bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix) { + return basSplitHandlerName(name, prefix, prefixSize, outSuffix) && basEventSuffixMatch(*outSuffix); +} + + +bool basSplitHandlerName(const char *name, char *prefix, int32_t prefixSize, const char **outSuffix) { const char *underscore = strrchr(name, '_'); - if (!underscore || !basEventSuffixMatch(underscore + 1)) { + if (!underscore || underscore == name || underscore[1] == '\0') { return false; } @@ -90,6 +97,15 @@ bool basSplitEventName(const char *name, char *prefix, int32_t prefixSize, const } +bool basSplitProcHandler(const BasProcEntryT *proc, char *prefix, int32_t prefixSize, const char **outSuffix) { + if (proc->formName[0] != '\0') { + return basSplitHandlerName(proc->name, prefix, prefixSize, outSuffix); + } + + return basSplitEventName(proc->name, prefix, prefixSize, outSuffix); +} + + void basStripModule(BasModuleT *mod) { if (!mod) { return; @@ -138,7 +154,7 @@ void basStripModule(BasModuleT *mod) { 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); + mangle[i] = name[0] != '\0' && !basSplitProcHandler(&mod->procs[i], prefix, (int32_t)sizeof(prefix), &suffix) && !sortedHasName(consts, nConst, name); if (!mangle[i]) { kept[nKept++] = name; diff --git a/src/apps/kpunch/dvxbasic/compiler/strip.h b/src/apps/kpunch/dvxbasic/compiler/strip.h index ec499d16..bc1b4b19 100644 --- a/src/apps/kpunch/dvxbasic/compiler/strip.h +++ b/src/apps/kpunch/dvxbasic/compiler/strip.h @@ -41,4 +41,11 @@ // string constants (SetEvent dispatch targets). void basStripModule(BasModuleT *mod); +// A procedure the form runtime finds by name as an event handler: split +// into its object prefix and event name. A form's Control_Event +// procedure may handle any event (a widget's or a UserControl's own), so +// any "_" counts there; elsewhere only the master list's +// events (basSplitEventName). +bool basSplitProcHandler(const BasProcEntryT *proc, char *prefix, int32_t prefixSize, const char **outSuffix); + #endif // DVXBASIC_STRIP_H diff --git a/src/apps/kpunch/dvxbasic/compiler/typecheck.c b/src/apps/kpunch/dvxbasic/compiler/typecheck.c index 58af1be5..3c759913 100644 --- a/src/apps/kpunch/dvxbasic/compiler/typecheck.c +++ b/src/apps/kpunch/dvxbasic/compiler/typecheck.c @@ -164,13 +164,12 @@ static uint8_t binaryType(uint8_t tok, uint8_t a, uint8_t b) { switch (tok) { case TOK_PLUS: - if (a == BAS_TYPE_STRING && b == BAS_TYPE_STRING) { + case TOK_MINUS: + case TOK_STAR: + if (tok == TOK_PLUS && a == BAS_TYPE_STRING && b == BAS_TYPE_STRING) { return BAS_TYPE_STRING; } - // fall through - case TOK_MINUS: - case TOK_STAR: if (isLong || currency) { return isLong ? BAS_TYPE_LONG : BAS_TYPE_CURRENCY; } diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs index a911cd9d..8d1b9afb 100644 --- a/src/apps/kpunch/dvxbasic/ctrlover.dhs +++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs @@ -38,6 +38,19 @@ .index Drag and Drop .index Default Property .index Default Properties +.index Parent +.index Container +.index MoveBefore +.index Tag +.index MousePointer +.index vbArrow +.index vbCrosshair +.index vbSizeNS +.index vbSizeWE +.index vbSizeNWSE +.index vbSizeNESW +.index vbNoDrop +.index ScrollToControl .h1 Common Properties, Events, and Methods @@ -67,6 +80,10 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth 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. DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below. DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. + Parent Form R The form the control is on (Text1.Parent.Caption). + Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file. + MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11) or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error. + Container Object R/W The container control the control is in (a Frame, VBox, TabPage ...), or its form. Set Text1.Container = Frame1 moves the control into Frame1, after the controls there; Set Text1.Container = Me moves it onto the form itself. The new container must be a container control on the same form, not the control itself or one inside it. .endtable Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767. @@ -90,7 +107,7 @@ Other controls, forms and menu items have no default property: using one as a va .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, whether it is loaded from a .frm file or created at runtime with CreateControl (whose handlers SetEvent names). .table Event Parameters Description @@ -101,7 +118,7 @@ These events are wired on every control loaded from a .frm file. Controls create GotFocus (none) Fires when the control receives keyboard focus. LostFocus (none) Fires when the control loses keyboard focus. KeyPress KeyAscii As Integer Fires when a printable key is pressed. KeyAscii is the ASCII code. - KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down. KeyCode is the scan code; Shift indicates modifier keys. + KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down, after the control has handled it. KeyCode is the key (vbKeyDelete, vbKeyReturn, ... or a character's code; Ctrl+A to Ctrl+Z are 1 to 26); Shift holds vbShiftMask, vbCtrlMask and vbAltMask bits (see Key Code Constants in the language reference). KeyUp KeyCode As Integer, Shift As Integer Fires when a key is released. MouseDown Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control. MouseUp Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control. @@ -119,6 +136,8 @@ These events are wired on every control loaded from a .frm file. Controls create SetFocus (none) Gives keyboard focus to this control. Refresh (none) Forces the control to repaint. Drag [Action] Starts or ends dragging this control: vbBeginDrag (1, the default) starts, vbEndDrag (2) drops it where the mouse is, 0 cancels the drag without a drop. + MoveBefore [Control] Moves the control just before another control (into that control's container when it is in another one), or with none to the end of its own container. The layout places controls in this order, and Controls lists them in it. + ScrollToControl Control Of a ScrollPane: scrolls so that a control inside it (the control, or its name as a string) is in view, also one that cannot take the focus (a Label). On another control, or with a control that is not inside it, it is an error. .endtable .h2 Drag and Drop @@ -167,6 +186,8 @@ End Sub .index Point method .index Print method .index PaintPicture +.index DrawFocusRect +.index Focus rectangle .index Raster Operations .index vbSrcCopy .index TextWidth @@ -187,6 +208,18 @@ End Sub .index ScaleTop .index ScaleWidth .index ScaleHeight +.index DrawBevel +.index DrawButtonFace +.index DrawArrow +.index DrawScrollBar +.index DrawScrollArrow +.index ScrollBarClick +.index PrintAccel +.index TextWidthAccel +.index AccelKey +.index vbGrayCopy +.index vbTransparentCopy +.index dvxArrowUp .h1 Drawing on Forms and PictureBoxes @@ -203,7 +236,8 @@ Drawings stay until they are drawn over or cleared. In a form's own code, write PSet [Step](x, y) [, color] Draws a point (DrawWidth wide). Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF] Draws a line, a box (B) or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0. Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]] Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles). - Point(x, y) The colour of a pixel, -1 outside the picture. + DrawFocusRect x, y, width, height [, color] Draws the dotted rectangle DVX shows around the control that has the keyboard focus (every other pixel of its edge), for things a program draws itself, such as the focused link of a help topic. A negative width or height counts from the other edge. + Point(x, y) The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly. Print items Writes text at CurrentX, CurrentY in ForeColor, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,. PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]] Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations). Scale [(x1, y1)-(x2, y2)] Gives the drawing coordinates of its own: (x1, y1) is the top-left corner and (x2, y2) the bottom-right one (y2 below y1 makes y go up). Scale alone goes back to pixels. @@ -213,6 +247,43 @@ Drawings stay until they are drawn over or cleared. In a form's own code, write TextHeight(text) Height of a line of text in the scale's units. .endtable +.h2 Drawing the DVX Look + +These methods draw the parts DVX's own controls are made of, in the colours of the current theme, for controls a program draws itself (a PictureBox that acts as a list or a toolbar). Coordinates and lengths are in the scale's units; colours are RGB values. + +.table + Method Description + ------ ----------- + DrawBevel x, y, width, height [, sunken [, face [, border]]] A raised box like a window or button edge, or with sunken True a sunken one like a text box's. face is the fill colour (the theme's face colour when left out; -1 leaves the inside as it is, to frame what is already drawn); border is the edge width in pixels (2). + DrawButtonFace x, y, width, height [, pressed [, face]] A button face, up or with pressed True pushed down. + DrawArrow x, y, size, direction [, color] A solid triangle centred on x, y, size pixel rows tall, pointing dvxArrowUp (0), dvxArrowDown (1), dvxArrowLeft (2) or dvxArrowRight (3), in ForeColor unless a colour is given. + DrawScrollBar x, y, length, position, visible, total [, horizontal [, control]] A scroll bar with its arrows and its thumb, for a list of total rows of which visible are shown from row position, with its top-left corner at x, y. It stands upright (16 pixels wide, length tall, arrows at the top and bottom) unless horizontal is True: then it lies across (length wide, 16 pixels tall, arrows at the left and right ends). With control True it is the narrower bar of a control such as ListBox (14 pixels). + DrawScrollArrow x, y, size, direction One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft or dvxArrowRight, in the theme's scroll bar colours. + ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]]) The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing. + PrintAccel text [, enabled] Prints a caption at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text. + TextWidthAccel(text) Width of a caption without its & signs, in the scale's units. + AccelKey(text) The access key of a caption in lower case ("f" for "&File"), "" when it has none. +.endtable + +.code +Sub Picture1_Paint() + Picture1.DrawBevel 0, 0, Picture1.ScaleWidth, Picture1.ScaleHeight, True, vbWhite + Picture1.DrawButtonFace 4, 4, 60, 20, pressed + Picture1.CurrentX = 10: Picture1.CurrentY = 8 + Picture1.PrintAccel "&Go", Picture1.Enabled + Picture1.DrawScrollBar Picture1.ScaleWidth - 18, 2, Picture1.ScaleHeight - 22, topRow, 8, 40 + Picture1.DrawScrollBar 2, Picture1.ScaleHeight - 18, Picture1.ScaleWidth - 22, leftCol, 10, 60, True +End Sub + +Sub Picture1_MouseDown(Button As Integer, X As Integer, Y As Integer) + ' A click on the bar along the bottom scrolls sideways + If Y >= Picture1.ScaleHeight - 18 And X < Picture1.ScaleWidth - 20 Then + leftCol = Picture1.ScrollBarClick(Picture1.ScaleWidth - 22, X - 2, leftCol, 10, 60, True) + Picture1.Refresh + End If +End Sub +.endcode + Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. Coordinates, radii, CurrentX and CurrentY and PaintPicture's rectangles are in the scale's units (a radius in its horizontal units); DrawWidth is always in pixels. A colour or angle out of range stops the program with an error. .h2 Raster Operations @@ -235,8 +306,12 @@ PaintPicture's opcode says how each copied pixel is combined with the pixel alre vbPatPaint &HFB0A09 Pattern OR NOT picture OR destination vbPatInvert &H5A0049 Pattern XOR destination vbDstInvert &H550009 NOT destination + vbGrayCopy &H1000000 The picture greyed, as DVX shows a disabled picture (DVX only) + vbTransparentCopy &H2000000 Every pixel of the picture but the key colour: a colour given after the opcode, else the colour of the picture's top-left pixel (DVX only) .endtable +vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite. + Any of the 256 raster operations of Windows can be given by its number; the bits work on the pixel values, so with fewer than 32 bits a pixel the results follow the display's colour format. .code @@ -409,7 +484,18 @@ End Sub .index Menu .index Menu Bar .index Submenu +.index Shortcut +.index ShortcutText +.index Shortcut Keys .index Separator +.index PopupMenu +.index CreateMenu +.index AddMenuItem +.index AddMenuSeparator +.index AddSubMenu +.index RemoveMenuItem +.index DestroyMenu +.index Recent files menu .h1 Menu System @@ -457,23 +543,81 @@ In the .frm file a menu item takes these keys: Key Type Description ---------- ------- ------------------------------------------- Caption String The text displayed. Use & for accelerator key. Set to "-" for a separator. - Checked Boolean Whether the menu item shows a checkmark. - RadioCheck Boolean Show the check as a radio dot instead of a checkmark. + Checked Boolean Whether the menu item shows a checkmark. An item with a Checked line (True or False) is a check item: choosing it turns its checkmark on or off before its Click event runs, so the handler reads the new state. + RadioCheck Boolean Show the check as a radio dot instead of a checkmark. Radio items next to each other form a group: choosing one checks it and clears the others. + Shortcut String A key that runs the item from anywhere in the form, shown at the right of the item: "F2", "Ctrl+X", "Alt+Enter", "Shift+Del" (Ctrl, Alt and Shift, then a letter, digit, F1 to F12, or Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up). + ShortcutText String Key text shown at the right of the item without making a shortcut, for keys a control handles itself (for example "Del" on a Delete item whose list deletes on Del in its KeyDown handler, so a text box keeps its own Del). Shown instead of the Shortcut's text when both are set. 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. + Visible Boolean On a top-level menu, False makes it a popup menu (for ContextMenu) instead of a menu bar entry. On an item or separator inside a menu, False hides it until code sets Visible = True. .endtable +A Shortcut key works while the form's window has the focus, before the focused control sees the key, so give one only to keys no control there needs (F-keys, Ctrl or Alt combinations). F1 always opens the help and never reaches a shortcut. An unknown Shortcut stops the program with an error when the form loads. + +.code +Begin Menu mnuRename + Caption = "&Rename..." + Shortcut = "F2" +End +Begin Menu mnuDelete + Caption = "&Delete" + ShortcutText = "Del" +End +.endcode + 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. + Caption String R/W The item's text, with & before its access key. Setting it keeps the item's key text (Shortcut or ShortcutText), and reading it gives the text without it. + Checked Boolean R/W Whether the menu item shows a checkmark. Setting it on an item that has neither Checked nor RadioCheck in the .frm makes it a check item (as in VB, any item can be checked). Enabled Boolean R/W Whether the menu item is enabled. + Visible Boolean R/W Whether the item (or separator) is in its menu. A hidden item keeps its caption, checkmark and place: shown again, it returns where it was. A submenu cannot be hidden (hide or remove its items, or set Enabled = False). + Shortcut String R/W The key that runs the item ("Ctrl+K", written as in the .frm file, "" for none). Setting it changes the key and the key text shown in the item; "" removes both. An unknown key is an error. .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. +RadioCheck and ShortcutText 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. The top-level menus of the menu bar (File, Edit) have no run-time properties. + +.h2 Menus at Run Time + +These methods work on any control of a form, or on the form itself (Me.AddMenuItem ...). They name menus by their .frm names: a top-level menu of the menu bar, a submenu, a popup menu from the .frm, or one made with CreateMenu. + +.table + Method Parameters Description + ---------------- --------------------------------- ------------------------------------------- + PopupMenu menu$ [, x, y] Shows the menu as a popup at the mouse, or at x, y in the control. + CreateMenu menu$ Makes an empty popup menu of that name (nothing happens if one exists). + AddMenu menu$, caption$ Adds a menu at the end of the form's menu bar, which appears with the first one (so a form made with CreateForm can have a menu bar). Add items to it by its name. The window keeps its size; the controls get the height the bar takes. + RemoveMenu menu$ Removes a menu of the menu bar with all its items; with the last one the bar goes too. Not for popup menus (DestroyMenu) or items (RemoveMenuItem). + AddMenuItem menu$, item$, caption$ [, shortcut$] Adds an item at the end of the menu. Choosing it runs the Sub item_Click, and item.Caption, .Checked, .Enabled, .Visible and .Shortcut work as for .frm items. shortcut$ gives it a key, as Shortcut in the .frm file. + AddMenuRadioItem menu$, item$, caption$ [, checked] Adds a radio item: radio items next to each other form a group, and choosing one checks it and clears the others. + AddMenuSeparator menu$ [, separator$] Adds a separator line at the end of the menu. With a name it can be hidden and removed like an item. + AddSubMenu menu$, submenu$, caption$ Adds a submenu at the end of the menu; add items to it by its name. + RemoveMenuItem item$ Removes an item or named separator (shown or hidden) from its menu. An unknown name does nothing. A submenu cannot be removed. + DestroyMenu menu$ Frees a popup menu made with CreateMenu (or from the .frm) and every item in it. +.endtable + +A recent-files list at the end of the File menu, rebuilt whenever it changes: + +.code +Dim recentCount As Integer + +Sub ShowRecent(files() As String, count As Integer) + Dim i As Integer + For i = 1 To recentCount + Me.RemoveMenuItem "mnuRecent" & i + Next + Me.RemoveMenuItem "mnuRecentSep" + If count > 0 Then Me.AddMenuSeparator "mnuFile", "mnuRecentSep" + For i = 1 To count + Me.AddMenuItem "mnuFile", "mnuRecent" & i, "&" & i & " " & files(i) + Next + recentCount = count +End Sub +.endcode + +Each item made this way needs its own Click procedure (Sub mnuRecent1_Click and so on), or fixed items in the .frm switched on and off with Visible serve the same purpose. .h2 Nesting @@ -489,7 +633,7 @@ A level-0 menu that contains children becomes a top-level menu header. A non-lev .h2 Event Dispatch -Each clickable menu item (not headers, not separators) receives a unique numeric ID at load time. When clicked, the form's onMenu handler maps the ID to the menu item's name and fires MenuName_Click. +Choosing a menu item (not a header, separator or submenu) fires MenuName_Click, whether it is on the menu bar or in a popup menu. .code Sub mnuOpen_Click () @@ -619,6 +763,7 @@ End Sub .item Controls are nested with Begin TypeName Name / End. .item Container controls (Frame, VBox, HBox, Toolbar, TabStrip, ScrollPane, Splitter, WrapBox) can have child controls nested inside them. .item Properties are assigned as Key = Value. String values are optionally quoted. +.item A Key = Value line belongs to the innermost block that is still open, also when it comes after the blocks nested inside it. DVX writes a block's properties before the blocks inside it. .item Everything after the form's closing End is BASIC source code. .item Comments in the form section use ' (single quote). .item Blank lines are ignored in the form section. diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs index 502c8afa..f6e427dd 100644 --- a/src/apps/kpunch/dvxbasic/form.dhs +++ b/src/apps/kpunch/dvxbasic/form.dhs @@ -32,6 +32,30 @@ .index Unload .index Show .index Hide +.index Padding +.index Spacing +.index vbModal +.index New (forms) +.index WindowState +.index vbMinimized +.index NewArgs +.index COMMAND$ (new arguments) +.index vbMaximized +.index Several copies of a form +.index Controls (form) +.index DesignMode +.index DesignSelection +.index DesignMouse +.index DesignKey +.index DesignPropertyChanged +.index Design surface +.index Icon (form) +.index Tag (form) +.index MousePointer (form) +.index ActiveControl +.index FitWidth +.index FitHeight +.index SaveScreenshot .h1 Form @@ -57,8 +81,25 @@ The Form is the top-level container representing a DVX window. It is declared in 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. + Padding Integer 0 Space in pixels between the window's edges and the controls (the content box's Padding); 0 is the default of 4 pixels. + Spacing Integer 0 Space in pixels between one control and the next (the content box's Spacing); 0 is the default of 4 pixels. + WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen) or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. + Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form. + DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files. + Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. + Tag String "" Any text the program wants to keep with the form, as for controls. + MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties). + ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files. + DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files. .endtable +Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size. + +.code +frmTerminal.Show +frmTerminal.WindowState = vbMinimized ' starts as an icon +.endcode + .h2 Form Events .table @@ -67,16 +108,34 @@ The Form is the top-level container representing a DVX window. It is declared in Load (none) Fires after the form and all controls are created. This is the default event. Unload (none) Fires when the form is being closed or unloaded. QueryUnload Cancel As Integer Fires before Unload. Set Cancel = 1 to abort the close. - Resize (none) Fires when the window is resized by the user. + Resize (none) Fires when the window changes size: resized by the user, maximized or restored (by the user or WindowState). Activate (none) Fires when the window gains focus. Deactivate (none) Fires when the window loses focus. Paint (none) Fires when the form is shown or resized and on Refresh, unless AutoRedraw is True (see Drawing on Forms and PictureBoxes). DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form. DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver. + NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below). + DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released or double-clicked in the form. See Designing Forms below. + DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown). + DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below. .endtable See Drag and Drop under Common Properties, Events, and Methods. While a control is dragged over a form with no DragDrop event procedure, the mouse pointer shows the "no drop" sign. +.h2 New Arguments + +A program with a NewArgs event procedure on any of its forms can be handed new arguments while it runs, instead of DVX starting another copy of it. COMMAND$ (and COMMAND$(1), COMMAND$(2), ...) then give the new arguments, and NewArgs fires on every loaded form that has the procedure. When the arguments arrive while one of the program's event procedures is still running -- for example while it shows a message box or a dialog, or a form with vbModal -- they are held, and NewArgs fires once that procedure has finished; if several arrive meanwhile, only the last is given. + +DVX hands new arguments to the help viewer: F1 and the Help menus of programs (and HelpView in BASIC) send "helpfile topic" to the program installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP when it is already running. A help viewer written in BASIC and installed there gets them in NewArgs. Starting a program that is already running -- from Program Manager, the Run dialog, a document opened in it or ShellRun -- gives it the new arguments when it allows only one copy and has a NewArgs procedure: its window comes to the front and NewArgs fires. Without a NewArgs procedure DVX says it is already running; a program that allows more than one copy (Project Properties) starts another copy instead. + +.code +Sub Form1_NewArgs () + ' F1 in another program while this help viewer is open + If COMMAND$(1) <> openFile Then OpenHelpFile COMMAND$(1) + GoTopic HlpStartTopic(hlp, COMMAND$(2)) +End Sub +.endcode + .h2 Form Methods .table @@ -85,29 +144,77 @@ See Drag and Drop under Common Properties, Events, and Methods. While a control Load FormName Load the form (creates the window and controls, fires Load event). Unload FormName Unload the form (fires Unload, destroys window). FormName.Show Make the form visible. - FormName.Show 1 Show as modal dialog (blocks until closed). + FormName.Show 1 Show as a modal dialog (vbModal): the statement returns when the form is hidden or unloaded, so the code after it can read what the user chose. FormName.Hide Hide the form without unloading it. + FormName.FitWidth Make the window just wide enough for its controls, keeping its height (AutoSize fits both). + FormName.FitHeight Make the window just tall enough for its controls, keeping its width. + FormName.SaveScreenshot file$ Save a picture of what the window shows inside its frame to a file: BMP when the name ends in .bmp, PNG otherwise. An error when the form has no window or the file cannot be written. .endtable +.h2 Several Copies of a Form + +A form can be open more than once, the way a program opens a second document window. New FormName makes another copy of the form: its own window, controls, menus and form-level variables, built from the same .frm. Its Load event runs, and it starts hidden like any loaded form, so call Show. Keep the copy in a variable declared As the form's name (or As Form, As Object): + +.code +Dim f As Form1 ' a Form1 copy, or the form itself +Set f = New Form1 ' a new copy +f.Caption = "Second window" +f.Text1.Text = "Hello" ' this copy's Text1 +f.Show + +Dim g As New Form1 ' declares and makes a copy at once +.endcode + +The code of the form serves every copy. In it, Me, the controls named on their own (Text1) and the form-level variables are those of the copy whose event is running, so each window keeps its own state. Unload f closes that copy only. + +The form's name (Form1.Caption, Form1.Show, Load Form1) always means the first copy loaded -- the one the program starts with -- as in Visual Basic. If that copy was unloaded, the name means the next one still open. + +.h2 Designing Forms + +A form designer written in BASIC shows the form being designed as a real form and turns on its design mode. While DesignMode is True, the controls on the form take no mouse clicks or keys and fire no events (Timers too are quiet); the form instead tells the designer what the user does: + +.list +.item DesignMouse (Action, Button, X, Y, CtrlName, Handle): Action is DSGN_MOUSE_DOWN (1, a button was pressed), DSGN_MOUSE_MOVE (2), DSGN_MOUSE_UP (3) or DSGN_MOUSE_DBLCLICK (4, a double click: open the code, say). Button is the button pressed or released (1 left, 2 right, 4 middle; for a move, the buttons held). X and Y are where, in the form. CtrlName is the control there -- the innermost one when containers hold others -- or "" for the form itself. Handle is the selection handle of the selected control under the mouse: DSGN_HANDLE_E (3, the right edge: drag to change the width), DSGN_HANDLE_SE (4, the corner) or DSGN_HANDLE_S (5, the bottom edge), or DSGN_HANDLE_NONE (-1). +.item DesignKey (KeyCode, Shift): a key, for Delete, the arrow keys and so on. +.item DesignPropertyChanged (CtrlName, PropertyName): the code of a UserControl on the form -- one of the project's own, put on the designer's toolbox with CompDesignControls (compiler.bas) -- called UserControl.PropertyChanged PropertyName: read its properties again. Such controls run their code on the form with UserControl.UserMode False. +.endlist + +DesignSelection names the selected control; the form draws its eight selection handles over the controls (the three that resize in black) and shows the resize mouse pointers over them. The constants are in designer.bas, which also lists the control types, their properties and events (see Include Libraries in the language reference). Together with CreateControl, RemoveControl, CallByName, Controls, Container, MoveBefore, LoadFormText and the .frm model of designer.bas (FrmOpen, FrmText$, FrmCtlText$ / FrmCtlPaste to copy and paste controls, FormText$ to write the live form as .frm text), a designer has what the DVX BASIC IDE's own designer uses. + +.code +Sub Canvas_DesignMouse(Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer) + If Action = DSGN_MOUSE_DOWN Then + If Handle <> DSGN_HANDLE_NONE Then + resizing = True ' drag the selected control's size + Else + Canvas.DesignSelection = CtrlName + ShowProperties CtrlName + End If + ElseIf Action = DSGN_MOUSE_UP Then + resizing = False + End If +End Sub +.endcode + .h2 Drawing -Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on. +Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on. .h2 Example .code -Sub Form_Load () +Sub Form1_Load () Form1.Caption = "Hello World" Print "Form loaded!" End Sub -Sub Form_QueryUnload (Cancel As Integer) +Sub Form1_QueryUnload (Cancel As Integer) If MsgBox("Really quit?", 4) <> 6 Then Cancel = 1 End If End Sub -Sub Form_Resize () +Sub Form1_Resize () Print "Window resized" End Sub .endcode diff --git a/src/apps/kpunch/dvxbasic/formrt/basNativeDos.c b/src/apps/kpunch/dvxbasic/formrt/basNativeDos.c index 5935c145..e6db031b 100644 --- a/src/apps/kpunch/dvxbasic/formrt/basNativeDos.c +++ b/src/apps/kpunch/dvxbasic/formrt/basNativeDos.c @@ -31,7 +31,7 @@ #include "formrt.h" -// Shared asm body: save ESP in EDI, push nativeArgs[ECX-1..0] (right to +// Shared asm body: save ESP in EDI, push words[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 \ @@ -47,9 +47,10 @@ "movl %%edi, %%esp\n\t" -// Push the already-marshalled cdecl argument words and call a native -// function via funcPtr. Returns the EAX result word; when fpReturn is -// true the x87 ST(0) result is handed back through a "=t" output and +// Push the already-marshalled cdecl argument words (32-bit here, so the +// words are the stack slots and their kinds do not matter) and call a +// native function via funcPtr. Returns the EAX result word; for a +// floating-point return 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. // @@ -65,23 +66,25 @@ // "memory" stops GCC caching the string args the callee may // dereference. The "=t" output exists only on the fpReturn path, so // 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 rawResult = 0; - int32_t cnt = nativeCount; - double fpValue = 0.0; +BasNativeWordT basNativeCall(void *funcPtr, const BasNativeWordT *words, const uint8_t *kinds, int32_t count, BasNativeRetE ret, double *fpResult) { + BasNativeWordT rawResult = 0; + int32_t cnt = count; + double fpValue = 0.0; - if (fpReturn) { + (void)kinds; + + if (ret != BasNativeRetIntE) { __asm__ __volatile__( NATIVE_CALL_ASM : "=a"(rawResult), "=t"(fpValue), [cnt] "+c"(cnt) - : [fn] "b"(funcPtr), "S"(nativeArgs) + : [fn] "b"(funcPtr), "S"(words) : "edi", "edx", "cc", "memory" ); } else { __asm__ __volatile__( NATIVE_CALL_ASM : "=a"(rawResult), [cnt] "+c"(cnt) - : [fn] "b"(funcPtr), "S"(nativeArgs) + : [fn] "b"(funcPtr), "S"(words) : "edi", "edx", "cc", "memory" ); } diff --git a/src/apps/kpunch/dvxbasic/formrt/basNativeHost.c b/src/apps/kpunch/dvxbasic/formrt/basNativeHost.c index cd5ae7f1..f5d490ed 100644 --- a/src/apps/kpunch/dvxbasic/formrt/basNativeHost.c +++ b/src/apps/kpunch/dvxbasic/formrt/basNativeHost.c @@ -23,44 +23,142 @@ // basNativeHost.c -- DECLARE LIBRARY native call trampoline (host build) // // 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 -// here: nothing is called. The marshalled argument words are recorded -// 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. +// host. By default nothing is called: the marshalled argument words are +// recorded and the result is whatever basNativeHostSetResult last set +// (zero by default), so the test suite can check basExternCall's +// marshalling and return narrowing. basNativeHostSetLive makes the +// calls for real with the x86-64 System V convention, for a test that +// runs a program using libraries: the integer and pointer arguments go +// in the integer registers and the floating-point ones in the SSE +// registers, each class in its own order -- which is where a callee +// with those parameters in any interleaving looks for them. Integer +// arguments past the six registers go on the stack in order, where the +// callee looks for them too; more floating-point arguments than the +// registers hold (their stack places would interleave with the integer +// ones) are refused. Selected by the Makefile object list of the host +// basrt archive. #include "formrt.h" +#include "dvxPlat.h" #include -static uint32_t sLastArgs[BAS_EXTERN_MAX_NATIVE_WORDS]; -static int32_t sLastCount = 0; -static uint32_t sRawResult = 0; -static double sFpResult = 0.0; +#define HOST_INT_REGS 6 // rdi rsi rdx rcx r8 r9 +#define HOST_SSE_REGS 8 // xmm0-7 +#define HOST_STACK_WORDS 8 // integer arguments past the registers +#define HALF_BITS 32 +#define LOW_HALF_MASK 0xFFFFFFFFu + +// Six register words, eight SSE values, then the stack words +typedef BasNativeWordT (*HostIntFnT)(BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, double, double, double, double, double, double, double, double, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT); +typedef double (*HostDblFnT)(BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, double, double, double, double, double, double, double, double, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT); +typedef float (*HostSngFnT)(BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, double, double, double, double, double, double, double, double, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT, BasNativeWordT); + +static BasNativeWordT sLastWords[BAS_EXTERN_MAX_NATIVE_WORDS]; +static int32_t sLastCount = 0; +static bool sLive = false; +static uint32_t sRawResult = 0; +static double sFpResult = 0.0; + +// ============================================================ +// Prototypes +// ============================================================ + +BasNativeWordT basNativeCall(void *funcPtr, const BasNativeWordT *words, const uint8_t *kinds, int32_t count, BasNativeRetE ret, double *fpResult); +int32_t basNativeHostLastArgs(BasNativeWordT *out, int32_t maxCount); +void basNativeHostSetLive(bool live); +void basNativeHostSetResult(uint32_t rawResult, double fpResult); +static uint64_t joinHalves(BasNativeWordT lo, BasNativeWordT hi); +static BasNativeWordT liveCall(void *funcPtr, const BasNativeWordT *words, const uint8_t *kinds, int32_t count, BasNativeRetE ret, double *fpResult); -uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) { - (void)funcPtr; - (void)fpReturn; +BasNativeWordT basNativeCall(void *funcPtr, const BasNativeWordT *words, const uint8_t *kinds, int32_t count, BasNativeRetE ret, double *fpResult) { + sLastCount = DVX_MIN(count, BAS_EXTERN_MAX_NATIVE_WORDS); + memcpy(sLastWords, words, (size_t)sLastCount * sizeof(sLastWords[0])); - sLastCount = DVX_MIN(nativeCount, BAS_EXTERN_MAX_NATIVE_WORDS); - memcpy(sLastArgs, nativeArgs, (size_t)sLastCount * sizeof(sLastArgs[0])); + if (sLive) { + return liveCall(funcPtr, words, kinds, count, ret, fpResult); + } *fpResult = sFpResult; return sRawResult; } -int32_t basNativeHostLastArgs(uint32_t *out, int32_t maxCount) { +int32_t basNativeHostLastArgs(BasNativeWordT *out, int32_t maxCount) { int32_t n = DVX_MIN(sLastCount, maxCount); - memcpy(out, sLastArgs, (size_t)n * sizeof(sLastArgs[0])); + memcpy(out, sLastWords, (size_t)n * sizeof(sLastWords[0])); return sLastCount; } +void basNativeHostSetLive(bool live) { + sLive = live; +} + + void basNativeHostSetResult(uint32_t rawResult, double fpResult) { sRawResult = rawResult; sFpResult = fpResult; } + + +// A two-word (i386 layout) value as its 64 bits. +static uint64_t joinHalves(BasNativeWordT lo, BasNativeWordT hi) { + return ((uint64_t)(hi & LOW_HALF_MASK) << HALF_BITS) | (uint64_t)(lo & LOW_HALF_MASK); +} + + +// The real call: integer-class words into ints[] and then stack[], +// floating-point ones into sse[] (a SINGLE as a double whose low 32 bits +// are the float, which is what a float parameter reads from its +// register). +static BasNativeWordT liveCall(void *funcPtr, const BasNativeWordT *words, const uint8_t *kinds, int32_t count, BasNativeRetE ret, double *fpResult) { + BasNativeWordT ints[HOST_INT_REGS]; + double sse[HOST_SSE_REGS]; + BasNativeWordT stack[HOST_STACK_WORDS]; + int32_t intCount = 0; + int32_t sseCount = 0; + int32_t stackCount = 0; + + memset(ints, 0, sizeof(ints)); + memset(sse, 0, sizeof(sse)); + memset(stack, 0, sizeof(stack)); + *fpResult = 0.0; + + for (int32_t i = 0; i < count; i++) { + bool isSse = kinds[i] != BasNativeIntE && kinds[i] != BasNativeInt64LoE; + uint64_t bits = words[i]; + + if (kinds[i] == BasNativeDoubleLoE || kinds[i] == BasNativeInt64LoE) { + bits = (i + 1 < count) ? joinHalves(words[i], words[i + 1]) : bits; + i++; + } + + if (isSse ? sseCount >= HOST_SSE_REGS : stackCount >= HOST_STACK_WORDS) { + dvxLog("BASIC: external call with too many arguments for the host"); + return 0; + } + + if (isSse) { + memcpy(&sse[sseCount++], &bits, sizeof(bits)); + } else if (intCount < HOST_INT_REGS) { + ints[intCount++] = (BasNativeWordT)bits; + } else { + stack[stackCount++] = (BasNativeWordT)bits; + } + } + + if (ret == BasNativeRetDoubleE) { + *fpResult = ((HostDblFnT)funcPtr)(ints[0], ints[1], ints[2], ints[3], ints[4], ints[5], sse[0], sse[1], sse[2], sse[3], sse[4], sse[5], sse[6], sse[7], stack[0], stack[1], stack[2], stack[3], stack[4], stack[5], stack[6], stack[7]); + return 0; + } + + if (ret == BasNativeRetSingleE) { + *fpResult = ((HostSngFnT)funcPtr)(ints[0], ints[1], ints[2], ints[3], ints[4], ints[5], sse[0], sse[1], sse[2], sse[3], sse[4], sse[5], sse[6], sse[7], stack[0], stack[1], stack[2], stack[3], stack[4], stack[5], stack[6], stack[7]); + return 0; + } + + return ((HostIntFnT)funcPtr)(ints[0], ints[1], ints[2], ints[3], ints[4], ints[5], sse[0], sse[1], sse[2], sse[3], sse[4], sse[5], sse[6], sse[7], stack[0], stack[1], stack[2], stack[3], stack[4], stack[5], stack[6], stack[7]); +} diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c new file mode 100644 index 00000000..83508819 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c @@ -0,0 +1,213 @@ +// 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. + +// basRunHost.c -- set up and drive one BASIC program (see basRunHost.h) + +#include "basRunHost.h" +#include "../basRes.h" +#include "dvxPlat.h" +#include "dvxWm.h" + +#include "stb_ds_wrap.h" + +#include +#include +#include + +#define RUN_HOST_CONFIG_DIR "CONFIG" +#define RUN_HOST_DATA_DIR "DATA" +#define RUN_HOST_ERR_VM "Out of memory creating the BASIC runtime." +#define RUN_HOST_ERR_FORMRT "Out of memory creating the form runtime." +#define RUN_HOST_ERR_OTHER "Runtime error" + +// Prototypes (alphabetical) +bool basRunHostAlive(const BasFormRtT *rt); +BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunSpecT *spec, const char **outError); +void basRunHostDestroy(BasFormRtT *rt); +void basRunHostLoadForms(BasFormRtT *rt, const char *startupForm); +bool basRunHostRegisterFrm(BasFormRtT *rt, const char *text, int32_t len); +BasVmResultE basRunHostSlice(BasFormRtT *rt); +void basRunHostTagBreak(BasFormRtT *rt, BasBreakTitleT **saved, bool paused); + + +bool basRunHostAlive(const BasFormRtT *rt) { + return basFormRtKeepsRunning(rt) && !rt->vm->ended && rt->vm->errorMsg[0] == '\0'; +} + + +BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunSpecT *spec, const char **outError) { + BasVmT *vm = basVmCreate(); + + if (!vm) { + *outError = RUN_HOST_ERR_VM; + return NULL; + } + + basVmLoadModule(vm, module); + + // App.Path is the program's folder (read-only on a CD); App.Config and + // App.Data are writable folders beneath it, made on demand. + const char *dir = spec->appDir ? spec->appDir : ""; + char config[DVX_MAX_PATH]; + char data[DVX_MAX_PATH]; + + snprintf(config, sizeof(config), "%s" DVX_PATH_SEP RUN_HOST_CONFIG_DIR, dir); + snprintf(data, sizeof(data), "%s" DVX_PATH_SEP RUN_HOST_DATA_DIR, dir); + basVmSetAppPaths(vm, dir, config, data); + platformMkdirRecursive(config); + platformMkdirRecursive(data); + basVmSetCommand(vm, spec->args); + + if (spec->printFn) { + basVmSetPrintCallback(vm, spec->printFn, spec->printCtx); + } + + if (spec->inputFn) { + basVmSetInputCallback(vm, spec->inputFn, spec->inputCtx); + } + + if (spec->doEventsFn) { + basVmSetDoEventsCallback(vm, spec->doEventsFn, spec->doEventsCtx); + } + + // DECLARE LIBRARY functions + BasExternCallbacksT extCb; + + memset(&extCb, 0, sizeof(extCb)); + extCb.resolveExtern = basExternResolve; + extCb.callExtern = basExternCall; + basVmSetExternCallbacks(vm, &extCb); + + BasFormRtT *rt = basFormRtCreate(ctx, vm, module); + + if (!rt) { + basVmDestroy(vm); + *outError = RUN_HOST_ERR_FORMRT; + return NULL; + } + + rt->suppressErrorDialog = spec->suppressErrorDialog; + + if (spec->title) { + snprintf(rt->title, sizeof(rt->title), "%s", spec->title); + } + + if (spec->helpFile) { + snprintf(rt->helpFile, sizeof(rt->helpFile), "%s", spec->helpFile); + } + + return rt; +} + + +void basRunHostDestroy(BasFormRtT *rt) { + BasVmT *vm = rt->vm; + + basFormRtDestroy(rt); + basVmDestroy(vm); +} + + +void basRunHostLoadForms(BasFormRtT *rt, const char *startupForm) { + BasFormRtT *prev = basFormRtSetCurrent(rt); + + basFormRtLoadAllForms(rt, startupForm); + basFormRtSetCurrent(prev); +} + + +bool basRunHostRegisterFrm(BasFormRtT *rt, const char *text, int32_t len) { + char formName[BAS_MAX_IDENT]; + + if (!basExtractFormName(text, formName, sizeof(formName))) { + return false; + } + + basFormRtRegisterFrm(rt, formName, text, len); + return true; +} + + +BasVmResultE basRunHostSlice(BasFormRtT *rt) { + BasVmT *vm = rt->vm; + BasFormRtT *prev = basFormRtSetCurrent(rt); + BasVmResultE result = basVmRun(vm); + + basFormRtSetCurrent(prev); + + if (result == BAS_VM_HALTED || result == BAS_VM_STEP_LIMIT || result == BAS_VM_BREAKPOINT) { + return result; + } + + // A VM-internal error (division by zero ...) leaves no message or + // error line of its own: take them from the VM's state + if (vm->errorMsg[0] == '\0') { + const char *msg = basVmGetError(vm); + + snprintf(vm->errorMsg, sizeof(vm->errorMsg), "%s", msg ? msg : RUN_HOST_ERR_OTHER); + } + + if (vm->errorLine == 0 && vm->currentLine > 0) { + vm->errorLine = vm->currentLine; + } + + return result; +} + + +void basRunHostTagBreak(BasFormRtT *rt, BasBreakTitleT **saved, bool paused) { + if (!paused) { + for (int32_t t = 0; t < (int32_t)arrlen(*saved); t++) { + for (int32_t i = 0; rt && i < (int32_t)arrlen(rt->forms); i++) { + BasFormT *form = rt->forms[i]; + + if (form == (*saved)[t].form && form->window) { + dvxSetTitle(rt->ctx, form->window, (*saved)[t].title); + } + } + } + + arrsetlen(*saved, 0); + return; + } + + if (!rt || arrlen(*saved) > 0) { + return; + } + + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + BasFormT *form = rt->forms[i]; + + if (!form->window) { + continue; + } + + BasBreakTitleT entry; + char tagged[MAX_TITLE_LEN]; + + entry.form = form; + snprintf(entry.title, sizeof(entry.title), "%s", form->window->title); + arrput(*saved, entry); + snprintf(tagged, sizeof(tagged), "%s%s", entry.title, BAS_RUN_BREAK_TAG); + dvxSetTitle(rt->ctx, form->window, tagged); + } +} diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h new file mode 100644 index 00000000..84103830 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h @@ -0,0 +1,99 @@ +// 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. + +// basRunHost.h -- set up and drive one BASIC program (VM + form runtime) +// +// Every host of a compiled module does the same setup: a VM with the +// module, App.Path / App.Config / App.Data, COMMAND$, the I/O and +// DECLARE LIBRARY callbacks, a form runtime and the program's .frm +// texts. The standalone stub (basstub.c), the IDE (ideMain.c) and the +// basdev library (a BASIC program running another as a child) all call +// these functions, so the three behave alike. + +#ifndef BAS_RUN_HOST_H +#define BAS_RUN_HOST_H + +#include "formrt.h" +#include "../runtime/vm.h" +#include "dvxApp.h" + +#include +#include + +// What a program is run with. Any pointer may be NULL (none / default). +typedef struct { + const char *appDir; // App.Path; App.Config / App.Data are its CONFIG and DATA folders (made) + const char *args; // COMMAND$ + const char *title; // name of the program's screen window + const char *helpFile; // full path of the help file F1 opens + BasPrintFnT printFn; // PRINT to no file + void *printCtx; + BasInputFnT inputFn; // INPUT from the keyboard + void *inputCtx; + BasDoEventsFnT doEventsFn; // DoEvents and the pause loop of a breakpoint in a handler + void *doEventsCtx; + bool suppressErrorDialog; // the host reports runtime errors itself +} BasRunSpecT; + +// A form's caption saved while " [break]" is appended to it. +typedef struct { + const BasFormT *form; + char title[MAX_TITLE_LEN]; +} BasBreakTitleT; + +// What a debugger appends to the captions of a paused program's forms. +#define BAS_RUN_BREAK_TAG " [break]" + +// True while the program's event-loop phase should go on: a form (or its +// screen window) is open, it did not END and no runtime error ended it. +bool basRunHostAlive(const BasFormRtT *rt); + +// A VM running module (borrowed: it must outlive the runtime) and its +// form runtime, set up from spec. Returns the runtime (rt->vm is the +// VM), or NULL with *outError set when memory runs out. A module the VM +// refuses (too many globals) leaves rt->vm->module NULL; running it then +// fails with the reason in basVmGetError. +BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunSpecT *spec, const char **outError); + +// Destroy the runtime made by basRunHostCreate and its VM. +void basRunHostDestroy(BasFormRtT *rt); + +// Load every registered form and show the startup form (named, else the +// first), with rt current while their Load code runs. +void basRunHostLoadForms(BasFormRtT *rt, const char *startupForm); + +// Register the .frm text (len bytes, NUL-terminated) under the form name +// its Begin Form line gives. False when it names none. +bool basRunHostRegisterFrm(BasFormRtT *rt, const char *text, int32_t len); + +// Run the program's module code once (basVmRun under the VM's step +// limit) with rt current. After a result other than BAS_VM_HALTED, +// BAS_VM_STEP_LIMIT and BAS_VM_BREAKPOINT the VM's error message and +// error line are always set (from the VM's error and the current line +// when the failing code left them empty). +BasVmResultE basRunHostSlice(BasFormRtT *rt); + +// Tag every open form's caption with BAS_RUN_BREAK_TAG (paused) or put +// the saved captions back. *saved (stb_ds) holds the originals. +void basRunHostTagBreak(BasFormRtT *rt, BasBreakTitleT **saved, bool paused); + +#endif // BAS_RUN_HOST_H diff --git a/src/apps/kpunch/dvxbasic/formrt/formCtl.c b/src/apps/kpunch/dvxbasic/formrt/formCtl.c new file mode 100644 index 00000000..dfafbcce --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/formCtl.c @@ -0,0 +1,1877 @@ +// 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. + +// formCtl.c -- DVX BASIC form runtime: UserControls, widget classes +// written in BASIC (the bridge between a control's class code and the +// widget system) +// +// Registration: a UserControl class (BAS_CLASS_USERCONTROL in the module) +// plus its .ctl header becomes one widget class (WidgetClassT, a copy of +// the template below with its focus flag), one interface (WgtIfaceT with +// a dynamic interface, WgtDynIfaceT: the class's Property Get / Let and +// Public variables are the properties, its Public Subs and Functions the +// methods, its Events the events) and a type name (the class name, also +// its BASIC name). A program registers its own controls when it starts +// (basFormRtRegisterCtl) and unregisters them when it ends; a control +// file installed in widgets/ (basCtlLoadFile) registers its controls for +// every program at boot; a designer registers the controls of the +// project it edits (basCtlLoadDesign) until it lets them go +// (basCtlUnloadDesign) -- run like an installed file's, without the +// module-level code. +// +// An instance: the widget (this class) with one child, a border-less +// canvas that fills it -- the UserControl object, a form-runtime control +// object over the canvas whose drawing members are formGfx.c's, as a +// PictureBox's. The class object holds the UserControl object in its +// UserControl member variable. The widget takes the mouse itself +// (WCLASS_NO_HIT_RECURSE); its class methods run the UserControl_ +// procedures (mouse down / move / up with Click and DblClick, the wheel, +// keys, focus). The host's universal mouse, click, key and scroll +// callbacks fire only for what the control's code raises +// (WCLASS_RAISES_EVENTS), as a VB container sees only a UserControl's +// own events; GotFocus / LostFocus reach the host as for any control. Layout only records a new size: Resize and Paint run from +// the class's poll method, outside the layout and paint passes, as a +// PictureBox's Paint runs from events. +// +// RaiseEvent in control code reaches the VM's event host (ucEventHost), +// which fires the event on the widget (wgtFireEvent): the BASIC host's +// CtrlName_Event handler, a C host's onEvent, or for Click, DblClick, +// Change ... the universal callback. ByRef numeric and Boolean arguments +// come back as the host left them. +// +// Running code: a program's own controls run in its VM with its rules (an +// untrapped error ends the program). An installed control file has one VM +// shared by every program that uses its controls: it has no DoEvents host +// (its code never yields), a call from another task while it runs code is +// refused (the event is dropped, a paint keeps the old picture; logged +// once), and an untrapped error is shown and the control carries on. +// +// Lifetime: a widget destroyed while its code runs (a host handler that +// unloads the form) stays allocated, cut off from its widgets, until the +// last call into it returns. + +#include "formrt.h" +#include "basRunHost.h" +#include "../basRes.h" +#include "../runtime/serialize.h" +#include "canvas/canvas.h" +#include "dvxRes.h" +#include "dvxWgtP.h" +#include "dvxPlat.h" +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include +#include +#include + +// The UserControl object's own members beside the common SetFocus and +// Refresh and the drawing members. +#define UC_MEMBER_HASFOCUS "HasFocus" +#define UC_MEMBER_USERMODE "UserMode" +#define UC_MEMBER_PROPCHANGED "PropertyChanged" +#define UC_MEMBER_INVALIDATE "Invalidate" + +// A press's place in a run of presses (multiClickDetect): two or more +// is a double click. +#define UC_DOUBLE_CLICK 2 + +// The mouse event handlers' four arguments, a key handler's two. +#define UC_MOUSE_ARGS 4 +#define UC_KEY_ARGS 2 + +// WGT_METHOD_ON_MOUSE_EVENT gives a pressed or released button as its +// ordinal (1 left, 2 right, 3 middle); BASIC handlers get VB's button +// bits (vbLeftButton, vbRightButton, vbMiddleButton). +#define UC_MOUSE_ORDINALS 4 + +static const int32_t sButtonBits[UC_MOUSE_ORDINALS] = { 0, MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE }; + +// Printable keys also give KeyPress (the dispatcher's own rule). +#define UC_KEY_PRINT_FIRST KEY_ASCII_PRINT_FIRST +#define UC_KEY_PRINT_LAST KEY_ASCII_PRINT_LAST + +// BackColor in the header: &H hex or decimal. +#define UC_HEX_BASE 16 +#define UC_DECIMAL_BASE 10 + +// The canvas widget's type name (its interface makes the UserControl +// object a drawing surface). +#define UC_CANVAS_TYPE "canvas" + +// A program's own control is registered under its name plus a serial +// ("IconView#3"), so two programs -- or two copies of one -- each have +// theirs; its BASIC name stays the class name. A designer's +// (basCtlLoadDesign) are keyed the same way, so the project a designer +// edits and the same project running never share a key. Room for the +// serial. +#define UC_PROGRAM_KEY_FMT "%s#%" PRIu32 +#define UC_PROGRAM_KEY_EXTRA 12 +#define UC_TYPE_KEY_MAX (BAS_MAX_IDENT + UC_PROGRAM_KEY_EXTRA) + +// An installed control file: its module, and the runtime and VM its code +// runs in (made with its first control). A designer's project controls +// (basCtlLoadDesign) are one too. +typedef struct { + char path[DVX_MAX_PATH]; + BasModuleT *module; + BasFormRtT *rt; + const BasFormRtT *designer; // a designer's: the program designing with it (NULL: a C host, the IDE) + uint32_t task; // the task running its code while depth > 0 + int32_t depth; // calls into its VM in progress + bool failed; // its runtime could not be made + bool refusedLogged; // a refused call was logged + bool design; // a designer's (basCtlLoadDesign): the module is the caller's, its module-level code never runs +} UcFileT; + +// The names of one of the module's Enums as an ENUM property's value +// names (WgtPropDescT.enumNames), kept with the class. +typedef struct { + const char **names; // in value order, NULL-terminated (malloc'd); NULL: its values are not 0, 1, 2 ... + int32_t enumIdx; // index into the module's enums +} UcEnumNamesT; + +// One registered UserControl class. +typedef struct { + BasUcHeaderT header; + char typeKey[UC_TYPE_KEY_MAX]; // its name in the widget registry + BasFormRtT *rt; // the program's runtime (NULL for a file's) + UcFileT *file; // the installed control file (NULL for a program's) + int32_t classIdx; + int32_t typeId; + WidgetClassT wclass; + WgtDynIfaceT dyn; + WgtIfaceT iface; + WgtPropDescT *props; // stb_ds + WgtMethodDescT *methods; // stb_ds + WgtEventDescT *events; // stb_ds + char **texts; // stb_ds: the events' parameter texts (owned) + UcEnumNamesT *enumNames; // stb_ds: the Enums of its properties (owned) + int32_t live; // its controls not freed yet + bool retired; // unregistered (its program ended); freed with its last control +} UcClassT; + +// One control (widget). +typedef struct { + UcClassT *cls; + BasObjectT *obj; + WidgetT *self; + WidgetT *canvas; + BasControlT surface; // the UserControl object + BasStringT *hold; // text of the last STRING value given to the host + int32_t minW; + int32_t minH; + int32_t lastW; // the size Resize last ran for (-1: none yet) + int32_t lastH; + int32_t depth; // calls into it in progress + int32_t pressClicks; // the last left press's place in a run (multiClickDetect) + bool clickArmed; // a left press that a release over the control makes a Click + bool sizeDirty; // layout gave it a new size: Resize and Paint at the next poll + bool paintPending; // Refresh asked for Paint while it ran + bool inPaint; + bool dead; // its widget is gone; freed when depth is 0 + bool orphan; // its program ended first: no object, no code, only the widget left +} UcInstT; + +static UcClassT **sClasses = NULL; // stb_ds +static uint32_t sProgramKeys = 0; // serials of program-owned type keys +static UcInstT **sInsts = NULL; // stb_ds +static UcFileT **sFiles = NULL; // stb_ds +static BasUcPropChangedFnT sPropChanged = NULL; +static uint32_t (*sTaskIdFn)(void) = NULL; +static bool sTaskIdResolved = false; + +// Function prototypes (alphabetical) +int32_t basCtlLoadDesign(const BasFormRtT *rt, BasModuleT *module, const char *const *headers, int32_t count, const char *path); +bool basCtlLoadFile(const char *path); +bool basCtlUnloadDesign(const BasFormRtT *rt); +bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len); +bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out); +bool basUcIconPath(const WgtIfaceT *iface, char *out, int32_t outSize); +const BasUcHeaderT *basUcIfaceHeader(const WgtIfaceT *iface); +bool basUcIfaceVisible(const BasFormRtT *rt, const WgtIfaceT *iface); +bool basUcIsControlType(const char *typeName); +const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName); +BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref); +const char *basUcResolveType(const BasFormRtT *rt, const char *typeName); +void basUcRuntimeEnd(BasFormRtT *rt); +void basUcSetPropChangedHook(BasUcPropChangedFnT fn); +bool basUcSurfaceCall(BasFormRtT *rt, void *ref, const char *name, BasValueT *args, int32_t argc, BasValueT *out); +bool basUcSurfaceGet(BasFormRtT *rt, void *ref, const char *name, BasValueT *out); +bool basUcSurfaceSet(BasFormRtT *rt, void *ref, const char *name, BasValueT value); +static void ucBlur(WidgetT *w); +static bool ucBuildIface(UcClassT *cls, const BasModuleT *module); +static void ucCalcMinSize(WidgetT *w, const BitmapFontT *font); +static bool ucCallEvent(UcInstT *inst, int32_t evt, const BasValueT *args, int32_t argc); +static const BasClassInfoT *ucClassInfo(const UcClassT *cls); +static UcClassT *ucClassOf(const WgtIfaceT *iface); +static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface); +static void ucDestroy(WidgetT *w); +static void ucDropObject(UcInstT *inst); +static bool ucDynCall(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret); +static bool ucDynGet(WidgetT *w, int32_t propIdx, WgtValueT *out); +static bool ucDynSet(WidgetT *w, int32_t propIdx, const WgtValueT *value); +static bool ucEnter(UcInstT *inst, BasFormRtT **outPrev); +static bool ucEnumNames(UcClassT *cls, const BasModuleT *module, int32_t enumIdx, const char ***outNames); +static void ucEventHost(void *ctx, const BasObjectT *obj, int32_t eventIdx, BasValueT *args, int32_t argc); +static bool ucFileReady(UcFileT *file, WidgetT *parent); +static UcInstT *ucFindInst(const BasObjectT *obj); +static void ucFocus(WidgetT *w); +static void ucFreeClass(UcClassT *cls); +static void ucFreeInst(UcInstT *inst); +static void ucHold(UcInstT *inst); +static bool ucInput(const UcInstT *inst); +static UcInstT *ucInstOf(const WidgetT *w); +static void ucKey(WidgetT *w, int32_t key, int32_t mod); +static void ucKeyUp(WidgetT *w, int32_t keyCode, int32_t shift); +static uint8_t ucKindOf(uint8_t basType); +static void ucLayout(WidgetT *w, const BitmapFontT *font); +static void ucLeave(UcInstT *inst, BasFormRtT *prev, bool ok); +static bool ucMember(UcInstT *inst, const char *name, int32_t op, const BasValueT *args, int32_t argc, BasValueT *result); +static BasModuleT *ucModule(const UcClassT *cls); +static void ucMouseEvent(WidgetT *w, WgtMouseActionE action, int32_t button, int32_t x, int32_t y, int32_t shift); +static void ucMousePress(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +static const char *ucNameTaken(const BasFormRtT *rt, const UcFileT *file, const char *name); +static UcClassT *ucOwnClass(const BasFormRtT *rt, const char *name); +static const BasFormRtT *ucOwner(const UcClassT *cls); +static void ucPaintNow(UcInstT *inst); +static void ucPoll(WidgetT *w, WindowT *win); +static bool ucPrivate(const UcClassT *cls); +static bool ucPrivateIface(const WgtIfaceT *iface); +static UcClassT *ucRegister(BasFormRtT *rt, UcFileT *file, const BasModuleT *module, const BasUcHeaderT *header); +static bool ucRelease(UcInstT *inst); +static void ucRetireClass(UcClassT *cls); +static BasFormRtT *ucRt(const UcClassT *cls); +static UcInstT *ucSurfaceInst(const BasFormRtT *rt, const void *ref); +static uint32_t ucTaskId(void); +static WgtValueT ucToWgt(UcInstT *inst, BasValueT v, uint8_t want); +static bool ucUserMode(const UcInstT *inst); +static bool ucWheel(WidgetT *w, int32_t delta); + +// The class methods every UserControl class has (its flags are its own). +static const WidgetClassT sClassTemplate = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_NO_HIT_RECURSE | WCLASS_NEEDS_POLL | WCLASS_RAISES_EVENTS, + .handlers = { + [WGT_METHOD_CALC_MIN_SIZE] = (void *)ucCalcMinSize, + [WGT_METHOD_LAYOUT] = (void *)ucLayout, + [WGT_METHOD_ON_MOUSE] = (void *)ucMousePress, + [WGT_METHOD_ON_KEY] = (void *)ucKey, + [WGT_METHOD_DESTROY] = (void *)ucDestroy, + [WGT_METHOD_POLL] = (void *)ucPoll, + [WGT_METHOD_ON_BLUR] = (void *)ucBlur, + [WGT_METHOD_ON_WHEEL] = (void *)ucWheel, + [WGT_METHOD_ON_MOUSE_EVENT] = (void *)ucMouseEvent, + [WGT_METHOD_ON_KEY_UP] = (void *)ucKeyUp, + [WGT_METHOD_ON_FOCUS] = (void *)ucFocus, + } +}; + + +int32_t basCtlLoadDesign(const BasFormRtT *rt, BasModuleT *module, const char *const *headers, int32_t count, const char *path) { + UcFileT *file = (basCtlUnloadDesign(rt) && module) ? (UcFileT *)calloc(1, sizeof(UcFileT)) : NULL; + int32_t added = 0; + + if (!file) { + return 0; + } + + snprintf(file->path, sizeof(file->path), "%s", path ? path : ""); + file->module = module; + file->designer = rt; + file->design = true; + arrput(sFiles, file); + + for (int32_t i = 0; i < count; i++) { + BasUcHeaderT header; + + if (headers[i] && basUcHeaderParse(headers[i], (int32_t)strlen(headers[i]), &header) && ucRegister(NULL, file, module, &header)) { + added++; + } + } + + return added; +} + + +bool basCtlLoadFile(const char *path) { + DvxResHandleT *res = path ? dvxResOpen(path) : NULL; + + if (!res) { + dvxLog("BASIC control file %s: no resources", path ? path : "?"); + return false; + } + + uint32_t modSize = 0; + uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize); + BasModuleT *module = modData ? basModuleDeserialize(modData, (int32_t)modSize) : NULL; + UcFileT *file = module ? (UcFileT *)calloc(1, sizeof(UcFileT)) : NULL; + int32_t count = 0; + + free(modData); + + if (!file) { + dvxLog("BASIC control file %s: no usable module", path); + dvxResClose(res); + + if (module) { + basModuleFree(module); + } + + return false; + } + + snprintf(file->path, sizeof(file->path), "%s", path); + file->module = module; + arrput(sFiles, file); + + // One class per CTLn header, in order (the toolbox's icon24 / name + // resources are numbered the same way) + for (int32_t i = 0; i < BAS_MAX_CTL_RESOURCES; i++) { + char resName[BAS_RES_FORM_NAME_LEN]; + uint32_t size = 0; + BasUcHeaderT header; + + snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); + + char *text = (char *)dvxResRead(res, resName, &size); + + if (!text) { + break; + } + + bool parsed = basUcHeaderParse(text, (int32_t)size, &header); + + free(text); + + if (parsed && ucRegister(NULL, file, module, &header)) { + count++; + } else { + dvxLog("BASIC control file %s: %s is not a usable control", path, resName); + } + } + + dvxResClose(res); + return count > 0; +} + + +bool basCtlUnloadDesign(const BasFormRtT *rt) { + for (int32_t f = (int32_t)arrlen(sFiles) - 1; f >= 0; f--) { + UcFileT *file = sFiles[f]; + + if (!file->design || file->designer != rt) { + continue; + } + + // Its code is running (a control's MsgBox pumps events): not now + if (file->depth > 0) { + dvxLog("BASIC controls of %s: still running, kept", file->path); + return false; + } + + for (int32_t i = (int32_t)arrlen(sClasses) - 1; i >= 0; i--) { + UcClassT *cls = sClasses[i]; + + if (cls->file == file) { + arrdel(sClasses, i); + ucRetireClass(cls); + } + } + + // Out of the list first: the end of its runtime lets go of design + // controls that runtime registered (basUcRuntimeEnd), which may + // shorten the list + arrdel(sFiles, f); + + if (file->rt) { + basRunHostDestroy(file->rt); + } + + free(file); + f = DVX_MIN(f, (int32_t)arrlen(sFiles)); + } + + return true; +} + + +bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len) { + BasUcHeaderT header; + + if (!basUcHeaderParse(text, len, &header)) { + basFormRtRuntimeError(rt, "Bad control", "A control's text has no Begin UserControl block."); + return false; + } + + return ucRegister(rt, NULL, rt->module, &header) != NULL; +} + + +bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out) { + char value[FRM_MAX_LINE_LEN]; + + memset(out, 0, sizeof(*out)); + out->width = BAS_UC_DEFAULT_SIZE; + out->height = BAS_UC_DEFAULT_SIZE; + out->backColor = BAS_UC_NO_COLOR; + out->canGetFocus = true; + + if (!basExtractCtlName(text, len, out->name, (int32_t)sizeof(out->name))) { + return false; + } + + if (basCtlHeaderValue(text, len, BAS_UC_KEY_WIDTH, value, (int32_t)sizeof(value))) { + out->width = atoi(value); + } + + if (basCtlHeaderValue(text, len, BAS_UC_KEY_HEIGHT, value, (int32_t)sizeof(value))) { + out->height = atoi(value); + } + + if (basCtlHeaderValue(text, len, BAS_UC_KEY_CANGETFOCUS, value, (int32_t)sizeof(value))) { + out->canGetFocus = wgtParseBool(value, true); + } + + // &HBBGGRR& or decimal + if (basCtlHeaderValue(text, len, BAS_UC_KEY_BACKCOLOR, value, (int32_t)sizeof(value))) { + bool hex = (value[0] == '&' && (value[1] == 'H' || value[1] == 'h')); + + out->backColor = (int32_t)strtol(hex ? value + 2 : value, NULL, hex ? UC_HEX_BASE : UC_DECIMAL_BASE); + } + + basCtlHeaderValue(text, len, BAS_UC_KEY_TOOLBOXICON, out->toolboxIcon, (int32_t)sizeof(out->toolboxIcon)); + basCtlHeaderValue(text, len, BAS_UC_KEY_DESCRIPTION, out->description, (int32_t)sizeof(out->description)); + basCtlHeaderValue(text, len, BAS_UC_KEY_DEFAULTEVENT, out->defaultEvent, (int32_t)sizeof(out->defaultEvent)); + return true; +} + + +// The header's ToolboxIcon file of a control file's or a designer's +// UserControl: the name taken in the folder of the path the control was +// registered with (the project's, for a designer's). +bool basUcIconPath(const WgtIfaceT *iface, char *out, int32_t outSize) { + const UcClassT *cls = ucClassOf(iface); + + if (!cls || !cls->file || !cls->header.toolboxIcon[0]) { + return false; + } + + const char *sep = platformPathDirEnd(cls->file->path); + int32_t dirLen = sep ? (int32_t)(sep - cls->file->path) + 1 : 0; + int32_t len = snprintf(out, (size_t)outSize, "%.*s%s", (int)dirLen, cls->file->path, cls->header.toolboxIcon); + + return len >= 0 && len < outSize; +} + + +const BasUcHeaderT *basUcIfaceHeader(const WgtIfaceT *iface) { + const UcClassT *cls = ucClassOf(iface); + + return cls ? &cls->header : NULL; +} + + +// False when iface is a private UserControl (ucPrivate) of an owner other +// than rt: a program's own are its alone, a designer's its designer's. +bool basUcIfaceVisible(const BasFormRtT *rt, const WgtIfaceT *iface) { + const UcClassT *cls = ucClassOf(iface); + + return !cls || !ucPrivate(cls) || ucOwner(cls) == rt; +} + + +bool basUcIsControlType(const char *typeName) { + for (int32_t i = 0; typeName && i < (int32_t)arrlen(sClasses); i++) { + if (strcasecmp(sClasses[i]->header.name, typeName) == 0) { + return true; + } + } + + return false; +} + + +const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName) { + const UcClassT *own = typeName ? ucOwnClass(rt, typeName) : NULL; + + return own ? &own->iface : NULL; +} + + +BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref) { + UcInstT *inst = ucSurfaceInst(rt, ref); + + return inst ? &inst->surface : NULL; +} + + +// The widget type name a BASIC control type name means in rt's program: +// its own control of that name first, else the registry's -- never +// another owner's private control (ucNameTaken keeps an installed or +// native control's name from every private one). +const char *basUcResolveType(const BasFormRtT *rt, const char *typeName) { + const UcClassT *own = typeName ? ucOwnClass(rt, typeName) : NULL; + + if (own) { + return own->typeKey; + } + + const char *key = wgtResolveTypeName(typeName); + + return (key && ucPrivateIface(wgtGetIface(key))) ? NULL : key; +} + + +void basUcRuntimeEnd(BasFormRtT *rt) { + // The controls it registered for its design forms, then its own + basCtlUnloadDesign(rt); + + for (int32_t i = (int32_t)arrlen(sClasses) - 1; i >= 0; i--) { + if (sClasses[i]->rt == rt) { + UcClassT *cls = sClasses[i]; + + arrdel(sClasses, i); + ucRetireClass(cls); + } + } + + if (arrlen(sClasses) == 0) { + arrfree(sClasses); + } +} + + +void basUcSetPropChangedHook(BasUcPropChangedFnT fn) { + sPropChanged = fn; +} + + +bool basUcSurfaceCall(BasFormRtT *rt, void *ref, const char *name, BasValueT *args, int32_t argc, BasValueT *out) { + UcInstT *inst = ucSurfaceInst(rt, ref); + + if (!inst) { + return false; + } + + *out = basValEmpty(); + + // A control whose widget is gone draws nothing (its Terminate) + if (inst->dead || !name) { + return true; + } + + int32_t common = basFormRtCommonMethodId(name); + + if (common == BAS_CM_SETFOCUS) { + wgtSetFocused(inst->self); + } else if (common == BAS_CM_REFRESH) { + ucHold(inst); + ucPaintNow(inst); + ucRelease(inst); + } else if (strcasecmp(name, UC_MEMBER_INVALIDATE) == 0) { + // Paint once at the next poll, however often it is asked for + inst->paintPending = true; + } else if (strcasecmp(name, UC_MEMBER_PROPCHANGED) == 0) { + // The C designer's hook, and a design form holding the control + // (DesignPropertyChanged) + if (argc > 0) { + BasStringT *text = basValFormatString(args[0]); + + if (sPropChanged) { + sPropChanged(inst->self, text->data); + } + + basDsgnPropertyChanged(inst->self, text->data); + basStringUnref(text); + } + } else if (!basFormRtDrawMethod(rt, &inst->surface, name, args, argc, out)) { + basFormRtRuntimeError(rt, "UserControl has no such method", "Method: %s\nControl: %s", name, inst->cls->header.name); + } + + return true; +} + + +bool basUcSurfaceGet(BasFormRtT *rt, void *ref, const char *name, BasValueT *out) { + UcInstT *inst = ucSurfaceInst(rt, ref); + + if (!inst) { + return false; + } + + *out = basValEmpty(); + + if (inst->dead || !name) { + return true; + } + + const WidgetT *self = inst->self; + + if (strcasecmp(name, BAS_PROP_WIDTH) == 0) { + *out = basValLong(self->w > 0 ? self->w : inst->minW); + } else if (strcasecmp(name, BAS_PROP_HEIGHT) == 0) { + *out = basValLong(self->h > 0 ? self->h : inst->minH); + } else if (strcasecmp(name, BAS_PROP_MINWIDTH) == 0) { + *out = basValLong(inst->minW); + } else if (strcasecmp(name, BAS_PROP_MINHEIGHT) == 0) { + *out = basValLong(inst->minH); + } else if (strcasecmp(name, BAS_PROP_ENABLED) == 0) { + *out = basValBool(widgetIsEnabledChain(self)); + } else if (strcasecmp(name, UC_MEMBER_HASFOCUS) == 0) { + *out = basValBool(wgtGetFocused() == self); + } else if (strcasecmp(name, BAS_UC_KEY_CANGETFOCUS) == 0) { + *out = basValBool(inst->cls->header.canGetFocus); + } else if (strcasecmp(name, UC_MEMBER_USERMODE) == 0) { + *out = basValBool(ucUserMode(inst)); + } else if (strcasecmp(name, BAS_PROP_NAME) == 0) { + *out = basValStringFromC(self->name); + } else if (!basFormRtDrawGetProp(rt, &inst->surface, name, out)) { + basFormRtRuntimeError(rt, "UserControl has no such property", "Property: %s\nControl: %s", name, inst->cls->header.name); + } + + return true; +} + + +bool basUcSurfaceSet(BasFormRtT *rt, void *ref, const char *name, BasValueT value) { + UcInstT *inst = ucSurfaceInst(rt, ref); + + if (!inst) { + return false; + } + + if (inst->dead || !name) { + return true; + } + + // The natural size: the host lays the control out again + if (strcasecmp(name, BAS_PROP_MINWIDTH) == 0 || strcasecmp(name, BAS_PROP_MINHEIGHT) == 0) { + int32_t v = DVX_MAX(basValToInt32(value), 0); + + if (strcasecmp(name, BAS_PROP_MINWIDTH) == 0) { + inst->minW = v; + } else { + inst->minH = v; + } + + wgtInvalidate(inst->self); + } else if (!basFormRtDrawSetProp(rt, &inst->surface, name, value)) { + basFormRtRuntimeError(rt, "UserControl property cannot be set", "Property: %s\nControl: %s\nIt is read-only or not a UserControl property.", name, inst->cls->header.name); + } + + return true; +} + + +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static void ucBlur(WidgetT *w) { + UcInstT *inst = ucInstOf(w); + + if (inst && ucInput(inst)) { + ucCallEvent(inst, BAS_UC_EVT_LOSTFOCUS, NULL, 0); + } +} + + +// The class's interface from its module: properties (Public variables, +// Property Get / Let / Set without other arguments) with their widget +// types (an Enum's ENUM with its member names when ucEnumNames can name +// its values), methods (Public Subs and Functions, any arguments), events +// (with their parameter lists as BASIC writes them). Friend members, +// interface implementations and names the form runtime keeps for every +// control (Width, Visible, Tag ...) are left out. False when memory runs +// out. +static bool ucBuildIface(UcClassT *cls, const BasModuleT *module) { + const BasClassInfoT *info = &module->classes[cls->classIdx]; + + for (int32_t i = 0; i < info->memberCount; i++) { + const BasClassMemberT *m = &info->members[i]; + bool isProc = m->kind != BAS_MEMBER_VAR; + int32_t params = isProc ? module->procs[m->index].paramCount : 0; + + if ((m->flags & BAS_MEMBER_FRIEND) || strchr(m->name, BAS_MEMBER_QUALIFIER[0])) { + continue; + } + + if (basFormRtFindCommonProp(m->name)) { + dvxLog("UserControl %s: member %s is a property every control has; the control's own is not used", cls->header.name, m->name); + continue; + } + + if (m->kind == BAS_MEMBER_SUB || m->kind == BAS_MEMBER_FUNCTION) { + WgtMethodDescT md = { m->name, WGT_SIG_DYNAMIC, WGT_DYN_ACCESS }; + + arrput(cls->methods, md); + continue; + } + + // A property's value type: a variable's or Get's own, a Let's or + // Set's last parameter + bool getter = (m->kind == BAS_MEMBER_VAR || m->kind == BAS_MEMBER_GET); + uint8_t type = getter ? m->dataType : ((params > 0) ? m->paramTypes[params - 1] : BAS_TYPE_VARIANT); + uint8_t kind = ucKindOf(type); + const char **names = NULL; + + if (params > (getter ? 0 : 1) || kind == WGT_IFACE_NONE) { + continue; + } + + if (m->enumIdx != BAS_ENUM_NONE && !ucEnumNames(cls, module, m->enumIdx, &names)) { + return false; + } + + kind = names ? WGT_IFACE_ENUM : kind; + + WgtPropDescT *p = NULL; + + for (int32_t k = 0; k < (int32_t)arrlen(cls->props); k++) { + if (strcasecmp(cls->props[k].name, m->name) == 0) { + p = &cls->props[k]; + } + } + + if (!p) { + WgtPropDescT pd = { m->name, kind, NULL, NULL, names }; + + arrput(cls->props, pd); + p = &cls->props[arrlen(cls->props) - 1]; + } + + if (getter) { + p->getFn = WGT_DYN_ACCESS; + p->type = kind; + p->enumNames = names; + } + + if (m->kind != BAS_MEMBER_GET) { + p->setFn = WGT_DYN_ACCESS; + } + } + + for (int32_t i = 0; i < info->eventCount; i++) { + const BasClassEventT *ev = &info->events[i]; + char text[BAS_VM_MAX_CALL_ARGS * BAS_MAX_IDENT * 2]; + int32_t n = 0; + const char *name = ev->paramNames ? ev->paramNames : ""; + + text[0] = '\0'; + + for (int32_t k = 0; k < ev->paramCount; k++) { + size_t nameLen = strcspn(name, BAS_PARAM_NAME_SEP); + const char *typeName = basValScalarTypeName(ev->paramTypes[k]); + + n += snprintf(text + n, sizeof(text) - (size_t)n, "%s%s%.*s As %s", (k > 0) ? ", " : "", (ev->byValParams & (1u << k)) ? "ByVal " : "", (int)nameLen, name, typeName ? typeName : "Variant"); + name += nameLen + (name[nameLen] != '\0'); + } + + char *owned = strdup(text); + + if (!owned) { + return false; + } + + WgtEventDescT ed = { ev->name, owned }; + + arrput(cls->texts, owned); + arrput(cls->events, ed); + } + + cls->iface.basName = cls->header.name; + cls->iface.props = cls->props; + cls->iface.propCount = (int32_t)arrlen(cls->props); + cls->iface.methods = cls->methods; + cls->iface.methodCount = (int32_t)arrlen(cls->methods); + cls->iface.events = cls->events; + cls->iface.eventCount = (int32_t)arrlen(cls->events); + cls->iface.defaultEvent = cls->header.defaultEvent[0] ? cls->header.defaultEvent : NULL; + cls->iface.createSig = WGT_CREATE_PARENT; + cls->iface.dyn = &cls->dyn; + cls->dyn.create = ucCreate; + cls->dyn.getProp = ucDynGet; + cls->dyn.setProp = ucDynSet; + cls->dyn.callMethod = ucDynCall; + cls->dyn.ctx = cls; + return true; +} + + +// The natural size the control's code set (MinWidth / MinHeight; the +// header's Width / Height at first). +static void ucCalcMinSize(WidgetT *w, const BitmapFontT *font) { + UcInstT *inst = ucInstOf(w); + + (void)font; + w->calcMinW = inst ? inst->minW : 0; + w->calcMinH = inst ? inst->minH : 0; +} + + +// Runs UserControl_ of the control with args. True when the class +// has the handler and it was called. inst may be gone afterwards (its +// form unloaded by a host handler) unless the caller holds it. +static bool ucCallEvent(UcInstT *inst, int32_t evt, const BasValueT *args, int32_t argc) { + int32_t proc = ucClassInfo(inst->cls)->ucProcs[evt]; + BasFormRtT *prev = NULL; + + if (proc < 0 || inst->dead || !inst->obj || !ucEnter(inst, &prev)) { + return false; + } + + // Kept while its code runs: a host handler may destroy its widget + ucHold(inst); + + bool ok = basVmCallProc(ucRt(inst->cls)->vm, inst->obj, proc, args, argc, NULL, 0); + + ucLeave(inst, prev, ok); + ucRelease(inst); + return true; +} + + +static const BasClassInfoT *ucClassInfo(const UcClassT *cls) { + return &ucModule(cls)->classes[cls->classIdx]; +} + + +// The registered class whose interface iface is, NULL for none. +static UcClassT *ucClassOf(const WgtIfaceT *iface) { + for (int32_t i = 0; iface && i < (int32_t)arrlen(sClasses); i++) { + if (&sClasses[i]->iface == iface) { + return sClasses[i]; + } + } + + return NULL; +} + + +// The dynamic interface's create: the widget, its canvas, the class +// object with its UserControl, then UserControl_Initialize. Resize and +// Paint follow the first layout (ucPoll). +static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface) { + UcClassT *cls = (UcClassT *)iface->dyn->ctx; + + if (!parent || (cls->file && !ucFileReady(cls->file, parent))) { + return NULL; + } + + BasFormRtT *rt = ucRt(cls); + UcInstT *inst = (UcInstT *)calloc(1, sizeof(UcInstT)); + WidgetT *self = inst ? widgetAlloc(parent, cls->typeId) : NULL; + + if (!self) { + free(inst); + return NULL; + } + + self->data = inst; + inst->cls = cls; + inst->self = self; + inst->minW = DVX_MAX(cls->header.width, 1); + inst->minH = DVX_MAX(cls->header.height, 1); + inst->lastW = -1; + inst->lastH = -1; + inst->canvas = wgtCanvas(self, inst->minW, inst->minH); + arrput(sInsts, inst); + cls->live++; + + if (!inst->canvas) { + wgtDestroy(self); + return NULL; + } + + wgtCanvasSetBorderStyle(inst->canvas, CANVAS_BORDER_NONE); + snprintf(inst->surface.name, sizeof(inst->surface.name), "%s", BAS_UC_OBJECT); + snprintf(inst->surface.typeName, sizeof(inst->surface.typeName), "%s", BAS_UC_OBJECT); + inst->surface.index = -1; + inst->surface.widget = inst->canvas; + inst->surface.iface = wgtGetIface(UC_CANVAS_TYPE); + + BasFormRtT *prev = NULL; + bool ok = ucEnter(inst, &prev); + + if (ok) { + const BasClassInfoT *info = ucClassInfo(cls); + + inst->obj = basVmNewObject(rt->vm, cls->classIdx); + ok = inst->obj != NULL; + + if (ok) { + basValRelease(&inst->obj->fields[info->ucSlot]); + inst->obj->fields[info->ucSlot] = basValObject(&inst->surface); + } + + ucLeave(inst, prev, ok); + } + + if (!ok) { + wgtDestroy(self); + return NULL; + } + + if (cls->header.backColor != BAS_UC_NO_COLOR) { + basFormRtDrawSetProp(rt, &inst->surface, BAS_PROP_BACKCOLOR, basValLong(cls->header.backColor)); + } + + ucHold(inst); + ucCallEvent(inst, BAS_UC_EVT_INITIALIZE, NULL, 0); + + // A widget its own Initialize destroyed is gone + return ucRelease(inst) ? self : NULL; +} + + +// The widget is being destroyed (its canvas already is): UserControl_ +// Terminate, then the object goes. While code of it runs, the control +// stays allocated, cut off from the widgets, until the calls return. The +// widget keeps the template class for the rest of wgtDestroy: the class +// of a retired control (its program ended, its designer let it go) is +// freed with its last control, here. +static void ucDestroy(WidgetT *w) { + UcInstT *inst = (UcInstT *)w->data; + + if (!inst) { + return; + } + + w->data = NULL; + w->wclass = &sClassTemplate; + inst->self = NULL; + inst->canvas = NULL; + inst->surface.widget = NULL; + inst->dead = true; + basFormRtDrawFree(&inst->surface); + + BasFormRtT *rt = ucRt(inst->cls); + + // Kept through Terminate; freed here unless a call into it is running + ucHold(inst); + + // VB's END semantics: no Terminate once the program has ended + if (inst->obj && rt && !rt->terminated && !rt->vm->ended) { + int32_t proc = ucClassInfo(inst->cls)->ucProcs[BAS_UC_EVT_TERMINATE]; + BasFormRtT *prev = NULL; + + if (proc >= 0 && ucEnter(inst, &prev)) { + bool ok = basVmCallProc(rt->vm, inst->obj, proc, NULL, 0, NULL, 0); + + ucLeave(inst, prev, ok); + } + } + + ucRelease(inst); +} + + +// The control lets go of its class object (its UserControl member +// cleared first, so the object does not keep the surface). +static void ucDropObject(UcInstT *inst) { + int32_t slot = ucClassInfo(inst->cls)->ucSlot; + + basValRelease(&inst->obj->fields[slot]); + inst->obj->fields[slot] = basValEmpty(); + basObjectUnref(inst->obj); + inst->obj = NULL; +} + + +// The dynamic interface's method call: the arguments as BASIC values of +// their own kinds, the result as a widget value of its own kind (a Sub's +// is no value). +// cppcheck-suppress constParameterCallback ; a dynamic interface hook: WgtDynIfaceT fixes the signature +static bool ucDynCall(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + UcInstT *inst = ucInstOf(w); + BasValueT in[BAS_VM_MAX_CALL_ARGS]; + BasValueT result; + + if (!inst || argc < 0 || argc > BAS_VM_MAX_CALL_ARGS) { + return false; + } + + memset(in, 0, sizeof(in)); + + for (int32_t i = 0; i < argc; i++) { + in[i] = basFormRtFromWgt(&args[i]); + } + + bool ok = ucMember(inst, inst->cls->methods[methodIdx].name, BAS_MEMBER_OP_GET, in, argc, &result); + + for (int32_t i = 0; i < argc; i++) { + basValRelease(&in[i]); + } + + // A Sub (or a Function returning Empty) gives no value + if (ret) { + *ret = ucToWgt(inst, result, WGT_IFACE_NONE); + + if (result.type == BAS_TYPE_EMPTY) { + ret->type = WGT_IFACE_NONE; + } + } + + basValRelease(&result); + return ok; +} + + +// cppcheck-suppress constParameterCallback ; a dynamic interface hook: WgtDynIfaceT fixes the signature +static bool ucDynGet(WidgetT *w, int32_t propIdx, WgtValueT *out) { + UcInstT *inst = ucInstOf(w); + BasValueT v; + + if (!inst) { + return false; + } + + bool ok = ucMember(inst, inst->cls->props[propIdx].name, BAS_MEMBER_OP_GET, NULL, 0, &v); + + if (ok) { + *out = ucToWgt(inst, v, inst->cls->props[propIdx].type); + } + + basValRelease(&v); + return ok; +} + + +// cppcheck-suppress constParameterCallback ; a dynamic interface hook: WgtDynIfaceT fixes the signature +static bool ucDynSet(WidgetT *w, int32_t propIdx, const WgtValueT *value) { + UcInstT *inst = ucInstOf(w); + + if (!inst) { + return false; + } + + BasValueT v = basFormRtFromWgt(value); + bool ok = ucMember(inst, inst->cls->props[propIdx].name, BAS_MEMBER_OP_LET, &v, 1, NULL); + + basValRelease(&v); + return ok; +} + + +// Before the control's code runs: an installed file's VM is refused to a +// second task while it runs code (logged once); a program that has ended +// runs none. *outPrev receives the runtime to give back to ucLeave. +static bool ucEnter(UcInstT *inst, BasFormRtT **outPrev) { + UcFileT *file = inst->cls->file; + BasFormRtT *rt = ucRt(inst->cls); + + if (!rt || rt->terminated) { + return false; + } + + if (file) { + uint32_t task = ucTaskId(); + + if (file->depth > 0 && file->task != task) { + if (!file->refusedLogged) { + dvxLog("BASIC control file %s: a call from another task was refused while its code ran", file->path); + file->refusedLogged = true; + } + + return false; + } + + file->depth++; + file->task = task; + } + + *outPrev = basFormRtCodeEnter(rt); + return true; +} + + +// The value names of the module's Enum enumIdx for an ENUM property into +// *outNames: its member names in value order, NULL-terminated, kept with +// the class (made once per Enum). A widget's enumerated value is its +// name's index (WgtPropDescT.enumNames), so an Enum whose values are not +// 0, 1, 2 ... each once (gaps, negative values, two names for one value) +// has none: *outNames NULL, the property stays a whole number. False +// when memory runs out. +static bool ucEnumNames(UcClassT *cls, const BasModuleT *module, int32_t enumIdx, const char ***outNames) { + const BasEnumInfoT *en = &module->enums[enumIdx]; + int32_t count = en->memberCount; + + *outNames = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(cls->enumNames); i++) { + if (cls->enumNames[i].enumIdx == enumIdx) { + *outNames = cls->enumNames[i].names; + return true; + } + } + + const char **names = (const char **)calloc((size_t)count + 1, sizeof(const char *)); + + if (!names) { + return false; + } + + for (int32_t i = 0; i < count && names; i++) { + int32_t v = en->members[i].value; + + if (v < 0 || v >= count || names[v]) { + free(names); + names = NULL; + } else { + names[v] = en->members[i].name; + } + } + + UcEnumNamesT entry = { names, enumIdx }; + + arrput(cls->enumNames, entry); + *outNames = names; + return true; +} + + +// RaiseEvent in a control's code: the event on its widget, for the host. +// Numeric and Boolean arguments the host changed come back (ByRef). +static void ucEventHost(void *ctx, const BasObjectT *obj, int32_t eventIdx, BasValueT *args, int32_t argc) { + UcInstT *inst = ucFindInst(obj); + + (void)ctx; + + if (!inst || inst->dead || argc > BAS_VM_MAX_CALL_ARGS) { + return; + } + + const BasClassInfoT *info = ucClassInfo(inst->cls); + + if (eventIdx < 0 || eventIdx >= info->eventCount) { + return; + } + + const BasClassEventT *ev = &info->events[eventIdx]; + WgtValueT vals[BAS_VM_MAX_CALL_ARGS]; + BasStringT *holds[BAS_VM_MAX_CALL_ARGS]; + + for (int32_t i = 0; i < argc; i++) { + vals[i] = basFormRtToWgt(args[i], (i < ev->paramCount) ? ucKindOf(ev->paramTypes[i]) : WGT_IFACE_NONE, &holds[i]); + } + + ucHold(inst); + wgtFireEvent(inst->self, ev->name, vals, argc); + ucRelease(inst); + + for (int32_t i = 0; i < argc; i++) { + if (vals[i].type != WGT_IFACE_NONE && !(ev->byValParams & (1u << i))) { + basValRelease(&args[i]); + args[i] = basFormRtFromWgt(&vals[i]); + } + + if (holds[i]) { + basStringUnref(holds[i]); + } + } +} + + +// An installed control file's runtime, made with its first control: a VM +// for its module (App.Path its folder), no DoEvents host, the module's +// code run once. False when it cannot be made (then never again). +static bool ucFileReady(UcFileT *file, WidgetT *parent) { + if (file->rt || file->failed) { + return file->rt != NULL; + } + + AppContextT *ctx = wgtGetContext(parent); + BasRunSpecT spec; + char dir[DVX_MAX_PATH]; + const char *error = NULL; + + snprintf(dir, sizeof(dir), "%s", file->path); + + char *sep = platformPathDirEnd(dir); + + if (sep) { + *sep = '\0'; + } + + memset(&spec, 0, sizeof(spec)); + spec.appDir = dir; + spec.title = platformPathBaseName(file->path); + + // Making a runtime must not change which program is current + BasFormRtT *current = basFormRtCurrent(); + + file->rt = ctx ? basRunHostCreate(ctx, file->module, &spec, &error) : NULL; + basFormRtSetCurrent(current); + + if (!file->rt || !file->rt->vm->module) { + dvxLog("BASIC control file %s: no runtime (%s)", file->path, error ? error : (file->rt ? basVmGetError(file->rt->vm) : "no display")); + + if (file->rt) { + basRunHostDestroy(file->rt); + file->rt = NULL; + } + + file->failed = true; + return false; + } + + basVmSetEventHost(file->rt->vm, ucEventHost, NULL); + + // A designer's controls never run the module-level code: it is the + // program's start + if (file->design) { + return true; + } + + BasFormRtT *prev = basFormRtCodeEnter(file->rt); + BasVmResultE result = basVmRun(file->rt->vm); + + basFormRtCodeLeave(file->rt, prev); + + if (result != BAS_VM_HALTED && result != BAS_VM_OK) { + basFormRtCodeFailed(file->rt); + } + + // Whatever the module code did, the control works on + file->rt->terminated = false; + file->rt->vm->ended = false; + file->rt->vm->errorMsg[0] = '\0'; + file->rt->vm->errorNumber = 0; + return true; +} + + +// The control whose class object obj is, NULL for none (an object of the +// class a program made itself). +static UcInstT *ucFindInst(const BasObjectT *obj) { + for (int32_t i = 0; obj && i < (int32_t)arrlen(sInsts); i++) { + if (sInsts[i]->obj == obj) { + return sInsts[i]; + } + } + + return NULL; +} + + +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static void ucFocus(WidgetT *w) { + UcInstT *inst = ucInstOf(w); + + if (inst && ucInput(inst)) { + ucCallEvent(inst, BAS_UC_EVT_GOTFOCUS, NULL, 0); + } +} + + +// A class's memory (out of the registry, its controls gone). +static void ucFreeClass(UcClassT *cls) { + for (int32_t i = 0; i < (int32_t)arrlen(cls->texts); i++) { + free(cls->texts[i]); + } + + for (int32_t i = 0; i < (int32_t)arrlen(cls->enumNames); i++) { + free(cls->enumNames[i].names); + } + + arrfree(cls->texts); + arrfree(cls->enumNames); + arrfree(cls->props); + arrfree(cls->methods); + arrfree(cls->events); + free(cls); +} + + +// A control's last reference: its object (its UserControl member +// cleared first) and its memory. +static void ucFreeInst(UcInstT *inst) { + for (int32_t i = 0; i < (int32_t)arrlen(sInsts); i++) { + if (sInsts[i] == inst) { + arrdel(sInsts, i); + break; + } + } + + if (arrlen(sInsts) == 0) { + arrfree(sInsts); + } + + if (inst->obj) { + ucDropObject(inst); + } + + if (inst->hold) { + basStringUnref(inst->hold); + } + + basFormRtDrawFree(&inst->surface); + + UcClassT *cls = inst->cls; + + free(inst); + cls->live--; + + if (cls->retired && cls->live == 0) { + ucFreeClass(cls); + } +} + + +// A call into the control starts: it stays allocated until ucRelease. +static void ucHold(UcInstT *inst) { + inst->depth++; +} + + +// True when the user's input reaches the control's code: it is alive and +// not on a design window (UserMode). +static bool ucInput(const UcInstT *inst) { + return !inst->dead && ucUserMode(inst); +} + + +static UcInstT *ucInstOf(const WidgetT *w) { + UcInstT *inst = w ? (UcInstT *)w->data : NULL; + + return (inst && !inst->dead && !inst->orphan) ? inst : NULL; +} + + +// A key went down on the focused control: KeyDown, then KeyPress for a +// printable key (the dispatcher's own order). +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static void ucKey(WidgetT *w, int32_t key, int32_t mod) { + UcInstT *inst = ucInstOf(w); + + if (!inst || !ucInput(inst)) { + return; + } + + BasValueT args[UC_KEY_ARGS]; + + args[0] = basValInteger((int16_t)key); + args[1] = basValInteger((int16_t)mod); + ucHold(inst); + ucCallEvent(inst, BAS_UC_EVT_KEYDOWN, args, UC_KEY_ARGS); + + if (!inst->dead && key >= UC_KEY_PRINT_FIRST && key <= UC_KEY_PRINT_LAST) { + ucCallEvent(inst, BAS_UC_EVT_KEYPRESS, args, 1); + } + + ucRelease(inst); +} + + +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static void ucKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) { + UcInstT *inst = ucInstOf(w); + + if (!inst || !ucInput(inst)) { + return; + } + + BasValueT args[UC_KEY_ARGS]; + + args[0] = basValInteger((int16_t)keyCode); + args[1] = basValInteger((int16_t)shift); + ucCallEvent(inst, BAS_UC_EVT_KEYUP, args, UC_KEY_ARGS); +} + + +// The widget value type a BASIC type is shown as: whole numbers INT, +// Boolean BOOL, Single FLOAT, Double and Currency DOUBLE, String, Date +// and Variant STRING; WGT_IFACE_NONE for objects, arrays and TYPEs (not +// a widget value). +static uint8_t ucKindOf(uint8_t basType) { + switch (basType) { + case BAS_TYPE_INTEGER: + case BAS_TYPE_LONG: + case BAS_TYPE_BYTE: + return WGT_IFACE_INT; + + case BAS_TYPE_BOOLEAN: + return WGT_IFACE_BOOL; + + case BAS_TYPE_SINGLE: + return WGT_IFACE_FLOAT; + + case BAS_TYPE_DOUBLE: + case BAS_TYPE_CURRENCY: + return WGT_IFACE_DOUBLE; + + case BAS_TYPE_STRING: + case BAS_TYPE_DATE: + case BAS_TYPE_VARIANT: + return WGT_IFACE_STRING; + + default: + return WGT_IFACE_NONE; + } +} + + +// The canvas fills the control; a new size waits for the next poll +// (Resize and Paint run outside the layout pass). +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static void ucLayout(WidgetT *w, const BitmapFontT *font) { + UcInstT *inst = ucInstOf(w); + + (void)font; + + if (!inst) { + return; + } + + inst->canvas->x = w->x; + inst->canvas->y = w->y; + inst->canvas->w = w->w; + inst->canvas->h = w->h; + + if (w->w != inst->lastW || w->h != inst->lastH) { + inst->sizeDirty = true; + } +} + + +// After the control's code ran (ucEnter): the runtime goes back; an +// untrapped error ends a program, and in an installed file's code is shown +// and then forgotten (the control carries on). +static void ucLeave(UcInstT *inst, BasFormRtT *prev, bool ok) { + UcFileT *file = inst->cls->file; + BasFormRtT *rt = ucRt(inst->cls); + + basFormRtCodeLeave(rt, prev); + + if (!ok) { + basFormRtCodeFailed(rt); + } + + if (!file) { + return; + } + + file->depth--; + + if (rt->terminated || rt->vm->ended) { + rt->terminated = false; + rt->vm->ended = false; + rt->vm->errorMsg[0] = '\0'; + rt->vm->errorNumber = 0; + } +} + + +// A member of the control's class object by name (property read or +// write, method call), with the control kept and its code bracketed. +static bool ucMember(UcInstT *inst, const char *name, int32_t op, const BasValueT *args, int32_t argc, BasValueT *result) { + BasFormRtT *prev = NULL; + bool ok = false; + + if (result) { + *result = basValEmpty(); + } + + ucHold(inst); + + if (inst->obj && ucEnter(inst, &prev)) { + ok = basVmCallMember(ucRt(inst->cls)->vm, inst->obj, name, op, args, argc, result); + ucLeave(inst, prev, ok); + } + + ucRelease(inst); + return ok; +} + + +static BasModuleT *ucModule(const UcClassT *cls) { + return cls->file ? cls->file->module : cls->rt->module; +} + + +// Every press, release and move (hover too) on the control: MouseDown / +// MouseUp / MouseMove with VB's button bits, then DblClick on a second +// press and Click on a left release over the control after a single one. +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static void ucMouseEvent(WidgetT *w, WgtMouseActionE action, int32_t button, int32_t x, int32_t y, int32_t shift) { + UcInstT *inst = ucInstOf(w); + + if (!inst || !ucInput(inst)) { + return; + } + + static const int32_t events[] = { + [WgtMouseDownE] = BAS_UC_EVT_MOUSEDOWN, + [WgtMouseUpE] = BAS_UC_EVT_MOUSEUP, + [WgtMouseMoveE] = BAS_UC_EVT_MOUSEMOVE + }; + + bool held = (action == WgtMouseMoveE); + int32_t bits = held ? button : ((button > 0 && button < UC_MOUSE_ORDINALS) ? sButtonBits[button] : 0); + bool left = !held && bits == MOUSE_LEFT; + BasValueT args[UC_MOUSE_ARGS]; + + args[0] = basValInteger((int16_t)bits); + args[1] = basValInteger((int16_t)shift); + args[2] = basValSingle((float)x); + args[3] = basValSingle((float)y); + ucHold(inst); + ucCallEvent(inst, events[action], args, UC_MOUSE_ARGS); + + if (left && action == WgtMouseDownE) { + inst->clickArmed = inst->pressClicks < UC_DOUBLE_CLICK; + + if (!inst->clickArmed && !inst->dead) { + ucCallEvent(inst, BAS_UC_EVT_DBLCLICK, NULL, 0); + } + } else if (left && action == WgtMouseUpE) { + bool inside = !inst->dead && x >= 0 && y >= 0 && x < w->w && y < w->h; + + if (inst->clickArmed && inside) { + ucCallEvent(inst, BAS_UC_EVT_CLICK, NULL, 0); + } + + inst->clickArmed = false; + } + + ucRelease(inst); +} + + +// A left press on the control: the focus when it takes it, the mouse +// held (moves and the release come here while the button is down) and +// the press counted for DblClick. +static void ucMousePress(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + UcInstT *inst = ucInstOf(w); + + (void)root; + + if (!inst || !ucInput(inst)) { + return; + } + + if (inst->cls->header.canGetFocus) { + sFocusedWidget = w; + } + + sDragWidget = w; + inst->pressClicks = multiClickDetect(vx, vy); +} + + +// Why a control may not be registered under name, NULL when it may: a +// native or installed control (a program's own controls are private to +// it, a designer's yield to them), or for a program (rt) or a designer +// (file a design one, owned by its designer) a control of its own of +// that name already. +static const char *ucNameTaken(const BasFormRtT *rt, const UcFileT *file, const char *name) { + bool isPrivate = rt || (file && file->design); + + if (isPrivate && ucOwnClass(file ? file->designer : rt, name)) { + return "The program registers this control twice."; + } + + for (int32_t i = 0; i < wgtIfaceCount(); i++) { + const char *key = NULL; + const WgtIfaceT *iface = wgtIfaceAt(i, &key); + bool named = (key && strcasecmp(key, name) == 0) || (iface && iface->basName && strcasecmp(iface->basName, name) == 0); + + if (named && !ucPrivateIface(iface)) { + return "A control type of this name is installed already."; + } + } + + return NULL; +} + + +// The control class of that name rt owns: its program's own or one it +// registered for its design forms (basCtlLoadDesign); for rt NULL a C +// designer's (the IDE's). NULL for none. +static UcClassT *ucOwnClass(const BasFormRtT *rt, const char *name) { + for (int32_t i = 0; i < (int32_t)arrlen(sClasses); i++) { + const UcClassT *cls = sClasses[i]; + + if (ucPrivate(cls) && ucOwner(cls) == rt && strcasecmp(cls->header.name, name) == 0) { + return sClasses[i]; + } + } + + return NULL; +} + + +// The owner of a private class (ucPrivate): its program, or the program +// designing with it (NULL: a C designer). +static const BasFormRtT *ucOwner(const UcClassT *cls) { + return cls->file ? cls->file->designer : cls->rt; +} + + +// Paint: the surface cleared first when AutoRedraw was turned off +// (basFormRtDrawWantsPaint), the handler run (a Refresh inside it asks +// for another one at the next poll), the canvas shown. The caller holds +// inst (ucHold): the handler may destroy its widget. +static void ucPaintNow(UcInstT *inst) { + if (inst->inPaint) { + inst->paintPending = true; + return; + } + + inst->paintPending = false; + inst->inPaint = true; + + if (basFormRtDrawWantsPaint(&inst->surface)) { + ucCallEvent(inst, BAS_UC_EVT_PAINT, NULL, 0); + } + + inst->inPaint = false; + + if (!inst->dead) { + wgtInvalidatePaint(inst->canvas); + } +} + + +// Between frames: a size the layout gave the control makes a canvas of +// that size (cleared to its BackColor), then Resize and Paint; a Refresh +// that came during Paint paints again. +static void ucPoll(WidgetT *w, WindowT *win) { + UcInstT *inst = ucInstOf(w); + + (void)win; + + if (!inst) { + return; + } + + ucHold(inst); + + if (inst->sizeDirty && w->w > 0 && w->h > 0) { + inst->sizeDirty = false; + inst->lastW = w->w; + inst->lastH = w->h; + wgtCanvasResize(inst->canvas, w->w, w->h); + basFormRtDrawClear(ucRt(inst->cls), &inst->surface); + ucCallEvent(inst, BAS_UC_EVT_RESIZE, NULL, 0); + inst->paintPending = true; + } + + if (inst->paintPending && !inst->dead) { + ucPaintNow(inst); + } + + ucRelease(inst); +} + + +// True for a program's own control or a designer's (basCtlLoadDesign): +// neither keeps its name from a program's own control, which the program +// resolves first (basUcResolveType). +static bool ucPrivate(const UcClassT *cls) { + return cls->rt || (cls->file && cls->file->design); +} + + +// ucPrivate for the class of interface iface (false for any other). +static bool ucPrivateIface(const WgtIfaceT *iface) { + const UcClassT *cls = ucClassOf(iface); + + return cls && ucPrivate(cls); +} + + +// Registers the UserControl header names, a class of module: rt's +// program's own (file NULL), an installed file's or a designer's (a +// design file; it and a program's get a private registry key, the +// class name plus a serial). NULL, with the +// reason shown (a program) or logged (a file), when the class is not a +// UserControl of the module, its name is taken or memory runs out. +static UcClassT *ucRegister(BasFormRtT *rt, UcFileT *file, const BasModuleT *module, const BasUcHeaderT *header) { + const char *problem = NULL; + int32_t classIdx = -1; + + for (int32_t i = 0; module && i < module->classCount; i++) { + if (strcasecmp(module->classes[i].name, header->name) == 0 && (module->classes[i].flags & BAS_CLASS_USERCONTROL)) { + classIdx = i; + } + } + + if (classIdx < 0) { + problem = "The program has no UserControl class of this name."; + } else { + problem = ucNameTaken(rt, file, header->name); + } + + UcClassT *cls = problem ? NULL : (UcClassT *)calloc(1, sizeof(UcClassT)); + + if (cls) { + cls->header = *header; + cls->rt = rt; + cls->file = file; + cls->classIdx = classIdx; + cls->wclass = sClassTemplate; + + if (header->canGetFocus) { + cls->wclass.flags |= WCLASS_FOCUSABLE; + } + + if (!ucBuildIface(cls, module)) { + problem = "Out of memory."; + } + } else if (!problem) { + problem = "Out of memory."; + } + + if (problem) { + if (rt) { + basFormRtRuntimeError(rt, "Control cannot be registered", "Control: %s\n%s", header->name, problem); + } else { + dvxLog("BASIC control %s: %s", header->name, problem); + } + + if (cls) { + cls->typeId = -1; + ucFreeClass(cls); + } + + return NULL; + } + + if (rt || file->design) { + snprintf(cls->typeKey, sizeof(cls->typeKey), UC_PROGRAM_KEY_FMT, header->name, ++sProgramKeys); + } else { + snprintf(cls->typeKey, sizeof(cls->typeKey), "%s", header->name); + } + + cls->typeId = wgtRegisterClass(&cls->wclass); + wgtRegisterIface(cls->typeKey, &cls->iface); + wgtIfaceSetType(cls->typeKey, cls->typeId); + arrput(sClasses, cls); + + if (rt) { + basVmSetEventHost(rt->vm, ucEventHost, NULL); + } + + return cls; +} + + +// A call into the control ended (ucHold): a control whose widget went +// meanwhile is freed with the last one. False when it was freed. +static bool ucRelease(UcInstT *inst) { + inst->depth--; + + if (inst->dead && inst->depth == 0) { + ucFreeInst(inst); + return false; + } + + return !inst->dead; +} + + +// The class's program ended: it leaves the widget registry, its controls +// that are still there (a window whose destruction waits) let go of +// their objects while the program's VM is still there and stay only as +// widgets; the class's memory goes with the last of them. +static void ucRetireClass(UcClassT *cls) { + wgtUnregisterClass(cls->typeId); + wgtUnregisterIface(cls->typeKey); + + for (int32_t i = 0; i < (int32_t)arrlen(sInsts); i++) { + UcInstT *inst = sInsts[i]; + + if (inst->cls == cls && inst->obj) { + ucDropObject(inst); + inst->orphan = true; + } + } + + cls->retired = true; + cls->rt = NULL; + cls->file = NULL; + + if (cls->live == 0) { + ucFreeClass(cls); + } +} + + +// The runtime the class's code runs in (an installed file's: NULL before +// its first control). +static BasFormRtT *ucRt(const UcClassT *cls) { + return cls->file ? cls->file->rt : cls->rt; +} + + +// The control whose UserControl object ref is, when its code runs in rt. +static UcInstT *ucSurfaceInst(const BasFormRtT *rt, const void *ref) { + for (int32_t i = 0; ref && i < (int32_t)arrlen(sInsts); i++) { + if (ref == &sInsts[i]->surface && ucRt(sInsts[i]->cls) == rt) { + return sInsts[i]; + } + } + + return NULL; +} + + +// The running task (the task switcher's, found at run time; 0 without +// one). +static uint32_t ucTaskId(void) { + if (!sTaskIdResolved) { + sTaskIdResolved = true; + sTaskIdFn = (uint32_t (*)(void))dlsym(NULL, DVX_SYM("tsCurrentId")); + } + + return sTaskIdFn ? sTaskIdFn() : 0; +} + + +// A BASIC value as the widget value the host gets: its text held by the +// control until the next value it gives. +static WgtValueT ucToWgt(UcInstT *inst, BasValueT v, uint8_t want) { + if (inst->hold) { + basStringUnref(inst->hold); + inst->hold = NULL; + } + + return basFormRtToWgt(v, want, &inst->hold); +} + + +// UserMode: False on a design window, True in a running program. +static bool ucUserMode(const UcInstT *inst) { + return !basDsgnIsDesignWindow(inst->self ? inst->self->window : NULL); +} + + +// The wheel over the control: UserControl_MouseWheel takes it when the +// class has one. +// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature +static bool ucWheel(WidgetT *w, int32_t delta) { + UcInstT *inst = ucInstOf(w); + BasValueT arg = basValInteger((int16_t)delta); + + return inst && ucInput(inst) && ucCallEvent(inst, BAS_UC_EVT_MOUSEWHEEL, &arg, 1); +} diff --git a/src/apps/kpunch/dvxbasic/formrt/formDesign.c b/src/apps/kpunch/dvxbasic/formrt/formDesign.c new file mode 100644 index 00000000..7cebaabf --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/formDesign.c @@ -0,0 +1,1351 @@ +// 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. + +// formDesign.c -- DVX BASIC form runtime: control types, the live control +// tree and the design surface +// +// What a form designer needs, for the C IDE's designer and for one +// written in BASIC alike: +// +// * The control type registry: the BASIC control types (the widget +// interfaces with a BASIC name), their flags, name prefix, default +// event, toolbox tooltip and icon, properties (the runtime's common / +// form / menu tables plus the widget's own, the widget's winning on a +// name clash as setProp does), methods and events. basCtl* serve C +// (IDE toolbox, properties window, event lists); the Ctl* functions +// are the DECLARE LIBRARY entry points of designer.bas. +// * The live control tree: Form.Controls (a Collection of the form's +// controls), Control.Parent (its form), Control.Container (the +// container control or form it is in; assigning it moves the control) +// and Control.MoveBefore (reorder / reparent in the layout). +// * The design surface: while Form.DesignMode is True the form's window +// takes the mouse and keys itself (controls get no input and fire no +// events) and raises DesignMouse (Action, Button, X, Y, CtrlName, +// Handle), DesignKey (KeyCode, Shift) and, when a UserControl's code +// on it calls PropertyChanged, DesignPropertyChanged (CtrlName, +// PropertyName); DesignSelection names the control whose selection +// handles are painted over the widgets. The handle geometry, hit test +// and painting (basDsgn*) are the ones the IDE's designer uses. + +#include "formrt.h" +#include "../basRes.h" +#include "../compiler/basEvents.h" +#include "dvxApp.h" +#include "dvxCur.h" +#include "dvxDraw.h" +#include "dvxPlat.h" +#include "dvxRes.h" +#include "dvxVideo.h" +#include "dvxWgtP.h" +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 + +// Selection handles: gray levels of a resize handle (black) and of a +// decorative one, and the handle positions (0, 1/2 or 2/2 of the control +// size across and down, clockwise from the top-left corner). +#define DSGN_HANDLE_ON_GRAY 0 +#define DSGN_HANDLE_OFF_GRAY 128 +#define DSGN_HANDLE_HALVES 2 + +// A widget's toolbox tooltip text +#define DSGN_TOOLTIP_LEN 256 + +// A control array element's label in DesignMouse / DesignSelection. +#define DSGN_LABEL_FMT "%s(%d)" +#define DSGN_LABEL_LEN (BAS_MAX_IDENT + 16) + +// Separator of the lists CtlMethodList$ / CtlEventList$ / CtlPropEnum$ +// return. +#define DSGN_LIST_SEP '|' + +// The events of the master list a UserControl's host gets from the form +// runtime itself; the rest it gets only when the control's code raises +// them (they are then among its own events). +static const char *const sUcHostEvents[] = { BAS_EVT_GOTFOCUS, BAS_EVT_LOSTFOCUS, BAS_EVT_VALIDATE, BAS_EVT_DRAGDROP, BAS_EVT_DRAGOVER }; + +// The design surface of one form: the window handlers it replaced while +// in design mode, the selection and the last mouse buttons seen. +typedef struct { + BasFormT *form; + BasFormRtT *rt; + void (*prevOnMouse)(WindowT *win, int32_t x, int32_t y, int32_t buttons); + void (*prevOnKey)(WindowT *win, int32_t key, int32_t mod); + void (*prevOnPaint)(WindowT *win, RectT *dirtyArea); + int32_t (*prevOnCursor)(WindowT *win, int32_t x, int32_t y); + int32_t prevButtons; + bool designMode; + char selection[DSGN_LABEL_LEN]; // "" = none +} DsgnSurfaceT; + +static const int32_t sHandleFx[BAS_DSGN_HANDLE_COUNT] = { 0, 1, 2, 2, 2, 1, 0, 0 }; +static const int32_t sHandleFy[BAS_DSGN_HANDLE_COUNT] = { 0, 0, 0, 1, 2, 2, 2, 1 }; + +static DsgnSurfaceT *sSurfaces = NULL; // stb_ds +static WindowT **sDesignWindows = NULL; // stb_ds: designer windows (basDsgnSetDesignWindow) +static char *sText = NULL; // stb_ds: the last string result + +// ============================================================ +// Prototypes +// ============================================================ + +const char *basCtlDefaultEvent(const char *typeName); +const char *basCtlEventAt(const char *typeName, int32_t idx); +const char *basCtlEventParams(const char *typeName, const char *eventName); +const WgtIfaceT *basCtlIface(const char *typeName); +bool basCtlIsContainer(const char *typeName); +const char *basCtlMethodAt(const char *typeName, int32_t idx); +const char *basCtlNamePrefix(const char *typeName); +bool basCtlPropAt(const char *typeName, int32_t idx, BasCtlPropT *out); +bool basCtlPropFind(const char *typeName, const char *propName, BasCtlPropT *out); +const char *basCtlResPath(const char *typeName, const char *resBase, char *resName, int32_t resNameSize); +const WgtIfaceT *basCtlTypeAt(const BasFormRtT *rt, int32_t idx); +int32_t basDsgnCursorAt(const WidgetT *w, int32_t x, int32_t y); +int32_t basDsgnHandleAt(const WidgetT *w, int32_t x, int32_t y); +void basDsgnHandleOrigin(const WidgetT *w, int32_t h, int32_t *outX, int32_t *outY); +bool basDsgnHandleResizes(int32_t h); +bool basDsgnIsDesignWindow(const WindowT *win); +void basDsgnPaintHandles(AppContextT *ctx, WindowT *win, const WidgetT *w); +void basDsgnPropertyChanged(const WidgetT *w, const char *propName); +void basDsgnSetDesignWindow(WindowT *win, bool on); +bool basDsgnWidgetHit(const WidgetT *w, int32_t x, int32_t y); +bool basFormRtDesignBlocks(const BasFormT *form); +BasValueT basFormRtDesignControlsItem(BasFormRtT *rt, BasFormT *form, BasValueT *args, int32_t argc); +BasValueT basFormRtDesignCtrlProp(BasFormRtT *rt, BasControlT *ctrl, const char *propName); +void basFormRtDesignForget(const BasFormT *form); +BasValueT basFormRtDesignFormProp(BasFormRtT *rt, BasFormT *form, const char *propName); +void basFormRtDesignSetFormProp(BasFormRtT *rt, BasFormT *form, const char *propName, BasValueT value); +bool basFormRtMoveControl(BasFormRtT *rt, BasControlT *ctrl, void *containerRef, void *beforeRef); +const char *ClsClassName(const char *clsText, const char *path); +static void ctlAppend(const char *s); +const char *CtlEventList(const char *typeName); +const char *CtlEventParams(const char *eventName); +static const char *ctlListOut(const char *(*itemAt)(const char *, int32_t), const char *typeName); +const char *CtlMethodList(const char *typeName); +int32_t CtlPropCount(const char *typeName); +const char *CtlPropEnum(const char *typeName, int32_t idx); +static bool ctlPropFromDesc(const BasPropDescT *pd, BasCtlPropT *out); +static bool ctlPropFromIface(const WgtPropDescT *p, BasCtlPropT *out); +const char *CtlPropName(const char *typeName, int32_t idx); +int32_t CtlPropType(const char *typeName, int32_t idx); +static const BasPropDescT *ctlRuntimeTable(const char *typeName, int32_t *count, bool *outControl); +static int ctlTypeCompare(const void *a, const void *b); +int32_t CtlTypeCount(void); +const char *CtlTypeDefaultEvent(const char *typeName); +const char *CtlTypeEventParams(const char *typeName, const char *eventName); +int32_t CtlTypeFlags(const char *typeName); +int32_t CtlTypeIconFile(const char *typeName, const char *outFile); +const char *CtlTypeName(int32_t idx); +const char *CtlTypeNamePrefix(const char *typeName); +static const WgtIfaceT **ctlTypes(const BasFormRtT *rt); +const char *CtlTypeTooltip(const char *typeName); +static BasControlT *dsgnCtrlAt(const BasFormT *form, int32_t x, int32_t y); +static void dsgnCtrlLabel(const BasControlT *ctrl, char *out, int32_t outSize); +static int32_t dsgnOnCursor(WindowT *win, int32_t x, int32_t y); +static void dsgnOnKey(WindowT *win, int32_t key, int32_t mod); +static void dsgnOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); +static void dsgnOnPaint(WindowT *win, RectT *dirtyArea); +static BasControlT *dsgnSelected(const DsgnSurfaceT *s); +static void dsgnSetMode(DsgnSurfaceT *s, bool on); +static DsgnSurfaceT *dsgnSurface(const BasFormT *form, bool create); +static DsgnSurfaceT *dsgnSurfaceOfWindow(const WindowT *win); +const char *FrmCode(const char *frmText); +const char *FrmControlNames(const char *frmText); +const char *FrmFormName(const char *frmText); +static void relinkBefore(WidgetT *parent, WidgetT *w, WidgetT *before); +static const char *textOut(const char *s); + + +// The default event of a control type: Load for the form, the widget's +// own (Click when it names none) for the rest. The IDE's designer and +// CtlTypeDefaultEvent$. +const char *basCtlDefaultEvent(const char *typeName) { + if (typeName && strcasecmp(typeName, FRM_KW_FORM) == 0) { + return BAS_EVT_LOAD; + } + + const WgtIfaceT *iface = basCtlIface(typeName); + + return (iface && iface->defaultEvent) ? iface->defaultEvent : BAS_EVT_CLICK; +} + + +// The idx-th event (0 = the first) of a control type, NULL past the end: +// the master list's events of that kind of object (the form's for "Form", +// the controls' plus Paint for a PictureBox), then the widget's own that +// the list does not have; a menu item has Click; "UserControl" (the +// object in a UserControl's own code) the events the runtime calls on it +// (BAS_UC_EVENT_LIST); a UserControl its own events, then the master +// list's its host gets anyway (sUcHostEvents). The IDE's event list and +// CtlEventList$. +const char *basCtlEventAt(const char *typeName, int32_t idx) { + static const BasEventInfoT sEvents[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) }; + + if (!typeName || idx < 0) { + return NULL; + } + + if (strcasecmp(typeName, FRM_KW_MENU) == 0) { + return (idx == 0) ? BAS_EVT_CLICK : NULL; + } + + if (strcasecmp(typeName, BAS_UC_OBJECT) == 0) { + const BasUcEventInfoT *uc = basUcEventInfo(idx); + + return uc ? uc->name : NULL; + } + + const WgtIfaceT *ucIface = basUcIsControlType(typeName) ? basCtlIface(typeName) : NULL; + + if (ucIface) { + if (idx < ucIface->eventCount) { + return ucIface->events[idx].name; + } + + idx -= ucIface->eventCount; + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(sUcHostEvents); i++) { + if (!wgtIfaceFindEvent(ucIface, sUcHostEvents[i]) && idx-- == 0) { + return sUcHostEvents[i]; + } + } + + return NULL; + } + + bool isForm = strcasecmp(typeName, FRM_KW_FORM) == 0; + bool draws = strcasecmp(typeName, BAS_PICTUREBOX_TYPE) == 0; + uint8_t scope = isForm ? BAS_EVT_SCOPE_FORM : (uint8_t)(BAS_EVT_SCOPE_CTRL | (draws ? BAS_EVT_SCOPE_DRAW : 0)); + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(sEvents); i++) { + if ((sEvents[i].scope & scope) && idx-- == 0) { + return sEvents[i].suffix; + } + } + + const WgtIfaceT *iface = isForm ? NULL : basCtlIface(typeName); + + for (int32_t i = 0; iface && i < iface->eventCount; i++) { + bool listed = false; + + for (int32_t j = 0; j < (int32_t)DVX_ARRAY_LEN(sEvents) && !listed; j++) { + listed = (sEvents[j].scope & scope) && strcasecmp(sEvents[j].suffix, iface->events[i].name) == 0; + } + + if (!listed && idx-- == 0) { + return iface->events[i].name; + } + } + + return NULL; +} + + +// The parameter list of a handler for event eventName of a control type +// ("" when none): the widget's own (its WgtEventDescT params) when the +// type's interface lists the event with them, else the BASIC master +// list's. With no type (NULL) every registered widget's own events are +// searched after the master list. "UserControl" (the object in a +// UserControl's own code) has BAS_UC_EVENT_LIST's alone. The one place +// the sources meet: the IDE's handler skeletons and designer.bas +// CtlEventParams$ / CtlTypeEventParams$. +const char *basCtlEventParams(const char *typeName, const char *eventName) { + if (typeName && strcasecmp(typeName, BAS_UC_OBJECT) == 0) { + for (int32_t i = 0; eventName && basUcEventInfo(i); i++) { + if (strcasecmp(basUcEventInfo(i)->name, eventName) == 0) { + return basUcEventInfo(i)->params; + } + } + + return ""; + } + + const WgtEventDescT *ev = wgtIfaceFindEvent(basCtlIface(typeName), eventName); + + if (ev && ev->params) { + return ev->params; + } + + const char *params = basEventParams(eventName); + + if (params) { + return params; + } + + for (int32_t i = 0; !typeName && i < wgtIfaceCount(); i++) { + ev = wgtIfaceFindEvent(wgtIfaceAt(i, NULL), eventName); + + if (ev && ev->params) { + return ev->params; + } + } + + return ""; +} + + +// The widget interface of a BASIC control type name, or NULL: the +// running program's own UserControl of that name first (its own or its +// design controls; outside a program, the C designer's), else the +// registry's -- not another owner's private one. +const WgtIfaceT *basCtlIface(const char *typeName) { + const BasFormRtT *rt = basFormRtCurrent(); + const WgtIfaceT *own = basUcOwnIface(rt, typeName); + const char *wgtName = (typeName && !own) ? wgtFindByBasName(typeName) : NULL; + const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; + + return own ? own : (basUcIfaceVisible(rt, iface) ? iface : NULL); +} + + +// True when controls of the type hold other controls. +bool basCtlIsContainer(const char *typeName) { + const WgtIfaceT *iface = basCtlIface(typeName); + + return iface && iface->isContainer; +} + + +// The idx-th method of a control type, NULL past the end: the widget's +// own, then the common ones every control has; the form's are its own +// (basFormRtFormMethodAt) and the common ones (forms and PictureBoxes +// also have the drawing methods); a menu item has none. +const char *basCtlMethodAt(const char *typeName, int32_t idx) { + if (!typeName || idx < 0 || strcasecmp(typeName, FRM_KW_MENU) == 0) { + return NULL; + } + + if (strcasecmp(typeName, FRM_KW_FORM) == 0) { + if (idx < basFormRtFormMethodCount()) { + return basFormRtFormMethodAt(idx); + } + + idx -= basFormRtFormMethodCount(); + } else { + const WgtIfaceT *iface = basCtlIface(typeName); + + if (iface && idx < iface->methodCount) { + return iface->methods[idx].name; + } + + idx -= iface ? iface->methodCount : 0; + } + + return basFormRtCommonMethodName(idx); +} + + +// The prefix of new control names of a type (Command for CommandButton): +// the widget's own, else the type name. +const char *basCtlNamePrefix(const char *typeName) { + const WgtIfaceT *iface = basCtlIface(typeName); + + return (iface && iface->namePrefix) ? iface->namePrefix : typeName; +} + + +// The idx-th property (0 = the first) of a control type ("Form" the +// form's, "Menu" a menu item's): the runtime table's in table order (a +// widget property of the same name takes the row), then the widget's +// others. False past the end. +bool basCtlPropAt(const char *typeName, int32_t idx, BasCtlPropT *out) { + int32_t rows = 0; + bool control = false; + const BasPropDescT *table = ctlRuntimeTable(typeName, &rows, &control); + const WgtIfaceT *iface = control ? basCtlIface(typeName) : NULL; + + if (idx < 0) { + return false; + } + + if (idx < rows) { + const WgtPropDescT *own = iface ? wgtIfaceFindProp(iface, table[idx].name) : NULL; + + return own ? ctlPropFromIface(own, out) : ctlPropFromDesc(&table[idx], out); + } + + idx -= rows; + + for (int32_t i = 0; iface && i < iface->propCount; i++) { + bool inTable = false; + + for (int32_t j = 0; j < rows && !inTable; j++) { + inTable = strcasecmp(table[j].name, iface->props[i].name) == 0; + } + + if (!inTable && idx-- == 0) { + return ctlPropFromIface(&iface->props[i], out); + } + } + + return false; +} + + +// Property propName of a control type ("Form", "Menu" as basCtlPropAt): +// the widget's own wins over the runtime table, as at run time. False +// when the type has no such property. The IDE's properties window and +// CtlPropType use it. +bool basCtlPropFind(const char *typeName, const char *propName, BasCtlPropT *out) { + int32_t rows = 0; + bool control = false; + const BasPropDescT *table = ctlRuntimeTable(typeName, &rows, &control); + const WgtIfaceT *iface = control ? basCtlIface(typeName) : NULL; + const WgtPropDescT *own = (iface && propName) ? wgtIfaceFindProp(iface, propName) : NULL; + + if (own) { + return ctlPropFromIface(own, out); + } + + for (int32_t i = 0; propName && i < rows; i++) { + if (strcasecmp(table[i].name, propName) == 0) { + return ctlPropFromDesc(&table[i], out); + } + } + + return false; +} + + +// The widget file of a control type and, in resName, the name of its +// resource resBase there ("icon24", or "icon24-2" for the second widget +// of a file). NULL when the type has no widget file. The toolbox, and +// CtlTypeIconFile / CtlTypeTooltip$. +const char *basCtlResPath(const char *typeName, const char *resBase, char *resName, int32_t resNameSize) { + const char *wgtName = typeName ? wgtFindByBasName(typeName) : NULL; + const char *path = wgtName ? wgtIfaceGetPath(wgtName) : NULL; + + if (!path) { + return NULL; + } + + int32_t pathIdx = wgtIfaceGetPathIndex(wgtName); + + if (pathIdx <= 1) { + snprintf(resName, (size_t)resNameSize, "%s", resBase); + } else { + snprintf(resName, (size_t)resNameSize, BAS_CTL_RES_SUFFIX_FMT, resBase, (int)pathIdx); + } + + return path; +} + + +// The idx-th control type rt sees (0 = the first, rt NULL: a C host's) in +// alphabetical order, NULL past the end. +const WgtIfaceT *basCtlTypeAt(const BasFormRtT *rt, int32_t idx) { + const WgtIfaceT **types = ctlTypes(rt); + const WgtIfaceT *iface = (idx >= 0 && idx < (int32_t)arrlen(types)) ? types[idx] : NULL; + + arrfree(types); + return iface; +} + + +// The mouse cursor over the resize handles of selected widget w (NULL: +// none): the resize cursor of the handle under (x, y), else 0 (the +// default). +int32_t basDsgnCursorAt(const WidgetT *w, int32_t x, int32_t y) { + switch (basDsgnHandleAt(w, x, y)) { + case BAS_DSGN_HANDLE_SE: + return CURSOR_RESIZE_DIAG_NWSE; + + case BAS_DSGN_HANDLE_E: + return CURSOR_RESIZE_H; + + case BAS_DSGN_HANDLE_S: + return CURSOR_RESIZE_V; + + default: + return 0; + } +} + + +// The resize handle (E, SE or S) of a selected widget under (x, y), else +// BAS_DSGN_HANDLE_NONE. SE is tested first: it overlaps E and S. +int32_t basDsgnHandleAt(const WidgetT *w, int32_t x, int32_t y) { + static const int32_t sOrder[] = { BAS_DSGN_HANDLE_SE, BAS_DSGN_HANDLE_E, BAS_DSGN_HANDLE_S }; + + for (int32_t i = 0; w && w->visible && i < (int32_t)DVX_ARRAY_LEN(sOrder); i++) { + int32_t hx; + int32_t hy; + + basDsgnHandleOrigin(w, sOrder[i], &hx, &hy); + + if (x >= hx && x < hx + BAS_DSGN_HANDLE_SIZE && y >= hy && y < hy + BAS_DSGN_HANDLE_SIZE) { + return sOrder[i]; + } + } + + return BAS_DSGN_HANDLE_NONE; +} + + +// Top-left corner of selection handle h around widget w. +void basDsgnHandleOrigin(const WidgetT *w, int32_t h, int32_t *outX, int32_t *outY) { + *outX = w->x + w->w * sHandleFx[h] / DSGN_HANDLE_HALVES - BAS_DSGN_HANDLE_SIZE / 2; + *outY = w->y + w->h * sHandleFy[h] / DSGN_HANDLE_HALVES - BAS_DSGN_HANDLE_SIZE / 2; +} + + +// True for the handles that resize (E, SE, S); the layout owns position. +bool basDsgnHandleResizes(int32_t h) { + return h == BAS_DSGN_HANDLE_E || h == BAS_DSGN_HANDLE_SE || h == BAS_DSGN_HANDLE_S; +} + + +// True when win is a design window: one the IDE marked +// (basDsgnSetDesignWindow) or a form in Form.DesignMode. UserControls on +// it see UserMode False. +bool basDsgnIsDesignWindow(const WindowT *win) { + for (int32_t i = 0; win && i < (int32_t)arrlen(sDesignWindows); i++) { + if (sDesignWindows[i] == win) { + return true; + } + } + + return win != NULL && dsgnSurfaceOfWindow(win) != NULL; +} + + +// Paints the eight selection handles of widget w into win's content +// (after the widgets painted): the resize handles black, the others gray. +void basDsgnPaintHandles(AppContextT *ctx, WindowT *win, const WidgetT *w) { + if (!ctx || !win || !win->contentBuf || !w || !w->visible || w->w <= 0 || w->h <= 0) { + return; + } + + DisplayT cd; + + dvxContentDisplay(ctx, win, &cd); + + const BlitOpsT *ops = dvxGetBlitOps(ctx); + uint32_t onColor = packColor(&cd, DSGN_HANDLE_ON_GRAY, DSGN_HANDLE_ON_GRAY, DSGN_HANDLE_ON_GRAY); + uint32_t offColor = packColor(&cd, DSGN_HANDLE_OFF_GRAY, DSGN_HANDLE_OFF_GRAY, DSGN_HANDLE_OFF_GRAY); + + for (int32_t i = 0; i < BAS_DSGN_HANDLE_COUNT; i++) { + int32_t hx; + int32_t hy; + + basDsgnHandleOrigin(w, i, &hx, &hy); + rectFill(&cd, ops, hx, hy, BAS_DSGN_HANDLE_SIZE, BAS_DSGN_HANDLE_SIZE, basDsgnHandleResizes(i) ? onColor : offColor); + } +} + + +// PropertyChanged in the code of UserControl widget w: a form in design +// mode holding it raises DesignPropertyChanged (CtrlName, PropertyName). +void basDsgnPropertyChanged(const WidgetT *w, const char *propName) { + const DsgnSurfaceT *s = w ? dsgnSurfaceOfWindow(w->window) : NULL; + const BasControlT *ctrl = NULL; + + for (int32_t i = 0; s && !ctrl && i < (int32_t)arrlen(s->form->controls); i++) { + ctrl = (s->form->controls[i]->widget == w) ? s->form->controls[i] : NULL; + } + + if (!ctrl) { + return; + } + + char label[DSGN_LABEL_LEN]; + BasValueT args[BAS_DSGN_PROPCHG_ARGS]; + + dsgnCtrlLabel(ctrl, label, sizeof(label)); + args[0] = basValStringFromC(label); + args[1] = basValStringFromC(propName ? propName : ""); + basFormRtFireEventArgs(s->rt, s->form, s->form->name, BAS_EVT_DSGNPROPCHG, args, BAS_DSGN_PROPCHG_ARGS); + basValRelease(&args[0]); + basValRelease(&args[1]); +} + + +// Mark win as a design window (on) or no longer one; a destroyed window +// must be unmarked. +void basDsgnSetDesignWindow(WindowT *win, bool on) { + for (int32_t i = 0; i < (int32_t)arrlen(sDesignWindows); i++) { + if (sDesignWindows[i] == win) { + arrdel(sDesignWindows, i); + break; + } + } + + if (on && win) { + arrput(sDesignWindows, win); + } + + if (arrlen(sDesignWindows) == 0) { + arrfree(sDesignWindows); + } +} + + +// True when (x, y) (window content coordinates) is on visible widget w. +bool basDsgnWidgetHit(const WidgetT *w, int32_t x, int32_t y) { + return w && w->visible && x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h; +} + + +// True while form is in design mode: its controls fire no events. +bool basFormRtDesignBlocks(const BasFormT *form) { + const DsgnSurfaceT *s = dsgnSurface(form, false); + + return s && s->designMode; +} + + +// Form.Controls(index) / Form.Controls(name): the control, as Controls' +// Item finds it (control array elements have no name key). +BasValueT basFormRtDesignControlsItem(BasFormRtT *rt, BasFormT *form, BasValueT *args, int32_t argc) { + int32_t count = (int32_t)arrlen(form->controls); + + if (argc == 1 && args[0].type == BAS_TYPE_STRING) { + for (int32_t i = 0; i < count; i++) { + if (form->controls[i]->index < 0 && strcasecmp(form->controls[i]->name, args[0].strVal->data) == 0) { + return basValObject(form->controls[i]); + } + } + } else if (argc == 1) { + int32_t idx = (int32_t)basValToNumber(args[0]); + + if (idx >= 1 && idx <= count) { + return basValObject(form->controls[idx - 1]); + } + } + + basFormRtRuntimeError(rt, "No such control", "Form: %s\nControls takes an index from 1 to Count or the name of a control.", form->name); + return basValInteger(0); +} + + +// Control.Parent (the form the control is on) and Control.Container (the +// container control it is in, else its form). +BasValueT basFormRtDesignCtrlProp(BasFormRtT *rt, BasControlT *ctrl, const char *propName) { + BasFormT *form = ctrl->form; + + if (strcasecmp(propName, BAS_PROP_CONTAINER) == 0 && ctrl->widget) { + for (WidgetT *p = ctrl->widget->parent; p && p != form->contentBox && p != form->root; p = p->parent) { + BasControlT *c = basFormRtResolveCtrl(rt, p->userData); + + if (c && c->form == form && c->widget == p) { + return basValObject(c); + } + } + } + + return basValObject(form); +} + + +// The form is going away: drop its design surface. +void basFormRtDesignForget(const BasFormT *form) { + for (int32_t i = 0; i < (int32_t)arrlen(sSurfaces); i++) { + if (sSurfaces[i].form == form) { + arrdel(sSurfaces, i); + return; + } + } +} + + +// Form.Controls (a new Collection of the form's controls, in creation +// order, each under its name; control array elements have no key), +// Form.DesignMode and Form.DesignSelection. +BasValueT basFormRtDesignFormProp(BasFormRtT *rt, BasFormT *form, const char *propName) { + const DsgnSurfaceT *s = dsgnSurface(form, false); + + if (strcasecmp(propName, BAS_PROP_DESIGNMODE) == 0) { + return basValBool(s && s->designMode); + } + + if (strcasecmp(propName, BAS_PROP_DESIGNSELECTION) == 0) { + return basValStringFromC(s ? s->selection : ""); + } + + BasObjectT *coll = basVmCollectionNew(rt->vm); + + if (!coll) { + return basValInteger(0); + } + + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + BasControlT *c = form->controls[i]; + + basVmCollectionAdd(coll, basValObject(c), (c->index < 0) ? c->name : NULL); + } + + return basValInstance(coll); +} + + +// Form.DesignMode = on / off and Form.DesignSelection = control name +// ("" = none). +void basFormRtDesignSetFormProp(BasFormRtT *rt, BasFormT *form, const char *propName, BasValueT value) { + DsgnSurfaceT *s = dsgnSurface(form, true); + + if (!s) { + return; + } + + s->rt = rt; + + if (strcasecmp(propName, BAS_PROP_DESIGNMODE) == 0) { + dsgnSetMode(s, basValIsTruthy(value)); + } else { + BasStringT *name = basValFormatString(value); + + snprintf(s->selection, sizeof(s->selection), "%s", name->data); + basStringUnref(name); + } + + if (form->window) { + dvxInvalidateWindow(rt->ctx, form->window); + } +} + + +// Moves ctrl into the container containerRef names (a container control +// or the form; NULL keeps its container) just before the control +// beforeRef names (NULL: at the end). Set Container and MoveBefore. +// False, with a run-time error raised, when the target is not a +// container on the control's form or ctrl would go inside itself. +bool basFormRtMoveControl(BasFormRtT *rt, BasControlT *ctrl, void *containerRef, void *beforeRef) { + BasFormT *form = ctrl->form; + BasControlT *before = beforeRef ? basFormRtResolveCtrl(rt, beforeRef) : NULL; + BasControlT *target = containerRef ? basFormRtResolveCtrl(rt, containerRef) : NULL; + WidgetT *parent = NULL; + + if (!ctrl->widget || ctrl == &form->formCtrl || (beforeRef && (!before || before->form != form || !before->widget || before == ctrl))) { + basFormRtRuntimeError(rt, "Cannot move the control", "Control: %s\nA control moves before another control of its form.", ctrl->name); + return false; + } + + if (target == &form->formCtrl) { + parent = form->contentBox ? form->contentBox : form->root; + } else if (target) { + parent = (target->form == form && target->widget && target->iface && target->iface->isContainer) ? target->widget : NULL; + } else { + parent = before ? before->widget->parent : ctrl->widget->parent; + } + + // Not a container of this form, or the control itself / one inside it + for (WidgetT *p = parent; p; p = p->parent) { + if (p == ctrl->widget) { + parent = NULL; + break; + } + } + + if (!parent || (before && before->widget->parent != parent)) { + basFormRtRuntimeError(rt, "Cannot move the control", "Control: %s\nThe container must be a container control or the form, the control's own form, not the control or one inside it.", ctrl->name); + return false; + } + + relinkBefore(parent, ctrl->widget, before ? before->widget : NULL); + + // Controls keeps the layout order of a container's controls + int32_t from = -1; + int32_t to = (int32_t)arrlen(form->controls); + + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + from = (form->controls[i] == ctrl) ? i : from; + to = (form->controls[i] == before) ? i : to; + } + + if (from >= 0) { + arrdel(form->controls, from); + to = (to > from) ? to - 1 : to; + arrins(form->controls, DVX_MIN(to, (int32_t)arrlen(form->controls)), ctrl); + } + + wgtInvalidate(parent); + return true; +} + + +// designer.bas: the name of a class module from its text (its VB_Name +// attribute, else path's file name without the extension). +const char *ClsClassName(const char *clsText, const char *path) { + char name[BAS_MAX_IDENT]; + + basExtractClassName(clsText, (int32_t)strlen(clsText), path, name, sizeof(name)); + return textOut(name); +} + + +static void ctlAppend(const char *s) { + for (const char *p = s; *p; p++) { + arrput(sText, *p); + } +} + + +// designer.bas: the events of a control type, "|"-separated. +const char *CtlEventList(const char *typeName) { + return ctlListOut(basCtlEventAt, typeName); +} + + +// designer.bas: an event handler's parameter list ("" for none). +const char *CtlEventParams(const char *eventName) { + return textOut(basCtlEventParams(NULL, eventName)); +} + + +// The items of a list function joined with DSGN_LIST_SEP. +static const char *ctlListOut(const char *(*itemAt)(const char *, int32_t), const char *typeName) { + const char *item; + + arrsetlen(sText, 0); + + for (int32_t i = 0; (item = itemAt(typeName, i)) != NULL; i++) { + if (i > 0) { + arrput(sText, DSGN_LIST_SEP); + } + + ctlAppend(item); + } + + arrput(sText, '\0'); + return sText; +} + + +// designer.bas: the methods of a control type, "|"-separated. +const char *CtlMethodList(const char *typeName) { + return ctlListOut(basCtlMethodAt, typeName); +} + + +// designer.bas: the number of properties of a control type. +int32_t CtlPropCount(const char *typeName) { + BasCtlPropT p; + int32_t n = 0; + + while (basCtlPropAt(typeName, n, &p)) { + n++; + } + + return n; +} + + +// designer.bas: the value names of an enumerated property, "|"-separated +// in value order ("" for any other property). +const char *CtlPropEnum(const char *typeName, int32_t idx) { + BasCtlPropT p; + + arrsetlen(sText, 0); + + for (int32_t i = 0; basCtlPropAt(typeName, idx, &p) && p.enumNames && p.enumNames[i]; i++) { + if (i > 0) { + arrput(sText, DSGN_LIST_SEP); + } + + ctlAppend(p.enumNames[i]); + } + + arrput(sText, '\0'); + return sText; +} + + +static bool ctlPropFromDesc(const BasPropDescT *pd, BasCtlPropT *out) { + out->name = pd->name; + out->enumNames = NULL; + out->type = pd->type; + out->readable = pd->readable; + out->writable = pd->writable; + return true; +} + + +static bool ctlPropFromIface(const WgtPropDescT *p, BasCtlPropT *out) { + out->name = p->name; + out->enumNames = (p->type == WGT_IFACE_ENUM) ? (const char *const *)p->enumNames : NULL; + out->type = p->type; + out->readable = p->getFn != NULL; + out->writable = p->setFn != NULL; + return true; +} + + +// designer.bas: the name of property idx (0 = the first) of a type. +const char *CtlPropName(const char *typeName, int32_t idx) { + BasCtlPropT p; + + return textOut(basCtlPropAt(typeName, idx, &p) ? p.name : ""); +} + + +// designer.bas: the type of property idx (CTL_PROP_* value type, plus +// CTL_PROP_READONLY / CTL_PROP_WRITEONLY), -1 past the end. +int32_t CtlPropType(const char *typeName, int32_t idx) { + BasCtlPropT p; + + if (!basCtlPropAt(typeName, idx, &p)) { + return BAS_CTL_PROP_NONE; + } + + return p.type | (p.writable ? 0 : BAS_CTL_PROP_READONLY) | (p.readable ? 0 : BAS_CTL_PROP_WRITEONLY); +} + + +// The runtime property table of a type: the form's, a menu item's, else +// the common one every control has (*outControl true: the widget's own +// properties count too). +static const BasPropDescT *ctlRuntimeTable(const char *typeName, int32_t *count, bool *outControl) { + *outControl = false; + + if (typeName && strcasecmp(typeName, FRM_KW_FORM) == 0) { + return basFormRtFormProps(count); + } + + if (typeName && strcasecmp(typeName, FRM_KW_MENU) == 0) { + return basFormRtMenuProps(count); + } + + *outControl = true; + return basFormRtCommonProps(count); +} + + +// ctlTypes' order: by BASIC name, ignoring case. +static int ctlTypeCompare(const void *a, const void *b) { + const WgtIfaceT *ia = *(const WgtIfaceT *const *)a; + const WgtIfaceT *ib = *(const WgtIfaceT *const *)b; + + return strcasecmp(ia->basName, ib->basName); +} + + +// designer.bas: the number of BASIC control types. +int32_t CtlTypeCount(void) { + const WgtIfaceT **types = ctlTypes(basFormRtCurrent()); + int32_t n = (int32_t)arrlen(types); + + arrfree(types); + return n; +} + + +// designer.bas: a type's default event. +const char *CtlTypeDefaultEvent(const char *typeName) { + return textOut(basCtlDefaultEvent(typeName)); +} + + +// designer.bas: the parameter list of a handler for event eventName of a +// control type ("" for none): a UserControl's own events with their own +// parameters, "UserControl" the events of the object in its own code. +const char *CtlTypeEventParams(const char *typeName, const char *eventName) { + return textOut(basCtlEventParams(typeName, eventName)); +} + + +// designer.bas: CTL_FLAG_CONTAINER (holds controls) and CTL_FLAG_DESIGNABLE +// (can be created with CreateControl, so placed in a designer); -1 for an +// unknown type. +int32_t CtlTypeFlags(const char *typeName) { + const WgtIfaceT *iface = basCtlIface(typeName); + + if (!iface) { + return BAS_CTL_TYPE_UNKNOWN; + } + + return (iface->isContainer ? BAS_CTL_FLAG_CONTAINER : 0) | ((iface->createSig != WGT_CREATE_PARENT_DATA) ? BAS_CTL_FLAG_DESIGNABLE : 0); +} + + +// designer.bas: writes a type's toolbox icon (24 x 24) to outFile, for +// ImageButton.Picture: its widget file's, else a UserControl's header's +// ToolboxIcon file (a project's control, registered for design). True +// when written. +int32_t CtlTypeIconFile(const char *typeName, const char *outFile) { + char resName[BAS_CTL_RES_NAME_LEN]; + char iconPath[DVX_MAX_PATH]; + const char *path = basCtlResPath(typeName, BAS_CTL_RES_ICON, resName, sizeof(resName)); + + if (path) { + return ResExtract(path, resName, outFile); + } + + return (outFile && basUcIconPath(basCtlIface(typeName), iconPath, sizeof(iconPath)) && platformCopyFile(iconPath, outFile)) ? BAS_TRUE : BAS_FALSE; +} + + +// designer.bas: the name of control type idx (0 = the first), "" past the +// end. +const char *CtlTypeName(int32_t idx) { + const WgtIfaceT *iface = basCtlTypeAt(basFormRtCurrent(), idx); + + return textOut(iface ? iface->basName : ""); +} + + +// designer.bas: the prefix of new control names of a type. +const char *CtlTypeNamePrefix(const char *typeName) { + return textOut(basCtlIface(typeName) ? basCtlNamePrefix(typeName) : ""); +} + + +// The control types rt sees (named, basUcIfaceVisible) in alphabetical +// order, as a new stb_ds array: the registry's own order is the order the +// widget files loaded in, which follows the disk. +static const WgtIfaceT **ctlTypes(const BasFormRtT *rt) { + const WgtIfaceT **types = NULL; + + for (int32_t i = 0; i < wgtIfaceCount(); i++) { + const WgtIfaceT *iface = wgtIfaceAt(i, NULL); + + if (iface && iface->basName && iface->basName[0] && basUcIfaceVisible(rt, iface)) { + arrput(types, iface); + } + } + + // qsort's array may not be NULL, even for no elements + if (types) { + qsort(types, arrlenu(types), sizeof(types[0]), ctlTypeCompare); + } + + return types; +} + + +// designer.bas: a type's toolbox tooltip: its widget file's name text, +// else a UserControl's Description (a project's control, registered for +// design), else the type name. +const char *CtlTypeTooltip(const char *typeName) { + char resName[BAS_CTL_RES_NAME_LEN]; + char tip[DSGN_TOOLTIP_LEN]; + const char *path = basCtlResPath(typeName, BAS_CTL_RES_NAME, resName, sizeof(resName)); + const WgtIfaceT *iface = basCtlIface(typeName); + const BasUcHeaderT *header = basUcIfaceHeader(iface); + + tip[0] = '\0'; + + if (path) { + dvxResLoadText(path, resName, tip, sizeof(tip)); + } else if (header) { + snprintf(tip, sizeof(tip), "%s", header->description); + } + + return textOut(tip[0] ? tip : (iface ? typeName : "")); +} + + +// The control under (x, y) on a form in design mode, the innermost when +// containers nest (the deepest widget; the later one of two at the same +// depth), NULL for the form itself. +static BasControlT *dsgnCtrlAt(const BasFormT *form, int32_t x, int32_t y) { + BasControlT *hit = NULL; + int32_t hitDepth = -1; + + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + BasControlT *c = form->controls[i]; + int32_t depth = 0; + + if (!basDsgnWidgetHit(c->widget, x, y)) { + continue; + } + + for (const WidgetT *p = c->widget->parent; p; p = p->parent) { + depth++; + } + + if (depth >= hitDepth) { + hit = c; + hitDepth = depth; + } + } + + return hit; +} + + +// A control's name as DesignMouse and DesignSelection give it: Name, or +// Name(Index) in a control array; "" for none. +static void dsgnCtrlLabel(const BasControlT *ctrl, char *out, int32_t outSize) { + if (!ctrl) { + out[0] = '\0'; + } else if (ctrl->index >= 0) { + snprintf(out, (size_t)outSize, DSGN_LABEL_FMT, ctrl->name, (int)ctrl->index); + } else { + snprintf(out, (size_t)outSize, "%s", ctrl->name); + } +} + + +// The resize cursors over the selected control's handles. +static int32_t dsgnOnCursor(WindowT *win, int32_t x, int32_t y) { + const DsgnSurfaceT *s = dsgnSurfaceOfWindow(win); + const BasControlT *sel = s ? dsgnSelected(s) : NULL; + + return basDsgnCursorAt(sel ? sel->widget : NULL, x, y); +} + + +// A key in a form in design mode: DesignKey (KeyCode, Shift). +static void dsgnOnKey(WindowT *win, int32_t key, int32_t mod) { + DsgnSurfaceT *s = dsgnSurfaceOfWindow(win); + + if (s) { + BasValueT args[BAS_DSGN_KEY_ARGS]; + + args[0] = basValLong(key); + args[1] = basValLong(mod); + basFormRtFireEventArgs(s->rt, s->form, s->form->name, BAS_EVT_DESIGNKEY, args, BAS_DSGN_KEY_ARGS); + } +} + + +// The mouse in a form in design mode: DesignMouse (Action, Button, X, Y, +// CtrlName, Handle) for a press (a double click when it is one), a move +// and a release. CtrlName is the control under the mouse ("" = the +// form), Handle the selected control's resize handle there (-1 none). +static void dsgnOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { + DsgnSurfaceT *s = dsgnSurfaceOfWindow(win); + + if (!s) { + return; + } + + int32_t action = BAS_DSGN_MOUSE_MOVE; + int32_t button = buttons; + + if (buttons & ~s->prevButtons) { + action = (multiClickDetect(x, y) >= BAS_DSGN_DBLCLICK_COUNT) ? BAS_DSGN_MOUSE_DBLCLICK : BAS_DSGN_MOUSE_DOWN; + button = buttons & ~s->prevButtons; + } else if (s->prevButtons & ~buttons) { + action = BAS_DSGN_MOUSE_UP; + button = s->prevButtons & ~buttons; + } + + s->prevButtons = buttons; + + // The handle under the mouse belongs to the selection, which may be + // over another control + const BasControlT *sel = dsgnSelected(s); + int32_t handle = sel ? basDsgnHandleAt(sel->widget, x, y) : BAS_DSGN_HANDLE_NONE; + char label[DSGN_LABEL_LEN]; + + dsgnCtrlLabel(dsgnCtrlAt(s->form, x, y), label, sizeof(label)); + + BasValueT args[BAS_DSGN_MOUSE_ARGS]; + + args[0] = basValLong(action); + args[1] = basValLong(button); + args[2] = basValLong(x); + args[3] = basValLong(y); + args[4] = basValStringFromC(label); + args[5] = basValLong(handle); + basFormRtFireEventArgs(s->rt, s->form, s->form->name, BAS_EVT_DESIGNMOUSE, args, BAS_DSGN_MOUSE_ARGS); + basValRelease(&args[4]); +} + + +// Paint in design mode: the widgets (always whole, so the handles drawn +// last time go), then the selection's handles over them. +static void dsgnOnPaint(WindowT *win, RectT *dirtyArea) { + DsgnSurfaceT *s = dsgnSurfaceOfWindow(win); + + if (!s) { + return; + } + + win->paintNeeded = PAINT_FULL; + + if (s->prevOnPaint) { + s->prevOnPaint(win, dirtyArea); + } + + const BasControlT *sel = dsgnSelected(s); + + if (sel) { + basDsgnPaintHandles(s->rt->ctx, win, sel->widget); + } +} + + +// The control DesignSelection names, NULL for none or one that is gone. +static BasControlT *dsgnSelected(const DsgnSurfaceT *s) { + char label[DSGN_LABEL_LEN]; + + for (int32_t i = 0; s->selection[0] && i < (int32_t)arrlen(s->form->controls); i++) { + dsgnCtrlLabel(s->form->controls[i], label, sizeof(label)); + + if (strcasecmp(label, s->selection) == 0) { + return s->form->controls[i]; + } + } + + return NULL; +} + + +// Design mode on: the form's window handlers are replaced (its controls +// get no input); off: they come back. +static void dsgnSetMode(DsgnSurfaceT *s, bool on) { + WindowT *win = s->form->window; + + if (!win || on == s->designMode) { + return; + } + + s->designMode = on; + + if (on) { + s->prevOnMouse = win->onMouse; + s->prevOnKey = win->onKey; + s->prevOnPaint = win->onPaint; + s->prevOnCursor = win->onCursorQuery; + s->prevButtons = 0; + win->onMouse = dsgnOnMouse; + win->onKey = dsgnOnKey; + win->onPaint = dsgnOnPaint; + win->onCursorQuery = dsgnOnCursor; + } else { + win->onMouse = s->prevOnMouse; + win->onKey = s->prevOnKey; + win->onPaint = s->prevOnPaint; + win->onCursorQuery = s->prevOnCursor; + } +} + + +// The design surface of form (made when create is true), NULL for none. +static DsgnSurfaceT *dsgnSurface(const BasFormT *form, bool create) { + for (int32_t i = 0; i < (int32_t)arrlen(sSurfaces); i++) { + if (sSurfaces[i].form == form) { + return &sSurfaces[i]; + } + } + + if (!create) { + return NULL; + } + + DsgnSurfaceT s; + + memset(&s, 0, sizeof(s)); + s.form = (BasFormT *)form; + arrput(sSurfaces, s); + return &sSurfaces[arrlen(sSurfaces) - 1]; +} + + +// The design surface in design mode whose form owns win, NULL for none. +static DsgnSurfaceT *dsgnSurfaceOfWindow(const WindowT *win) { + for (int32_t i = 0; i < (int32_t)arrlen(sSurfaces); i++) { + if (sSurfaces[i].designMode && sSurfaces[i].form->window == win) { + return &sSurfaces[i]; + } + } + + return NULL; +} + + +// designer.bas: the code section of .frm text (what follows the form's +// End line). +const char *FrmCode(const char *frmText) { + return textOut(frmText + basFindFormEndPos(frmText, (int32_t)strlen(frmText))); +} + + +// designer.bas: the names of the controls and menu items .frm text +// declares, "|"-separated in file order (a control array's name once per +// element). +const char *FrmControlNames(const char *frmText) { + int32_t len = (int32_t)strlen(frmText); + char *list = (char *)malloc((size_t)len + 1); + + arrsetlen(sText, 0); + + if (list && basFrmControlList(frmText, len, list, len + 1) >= 0) { + bool inName = false; + + for (const char *p = list; *p; p++) { + if (*p == '"') { + inName = !inName; + + if (inName && arrlen(sText) > 0) { + arrput(sText, DSGN_LIST_SEP); + } + } else if (inName) { + arrput(sText, *p); + } + } + } + + free(list); + arrput(sText, '\0'); + return sText; +} + + +// designer.bas: the form name of .frm text ("" when it has no Begin Form +// line). +const char *FrmFormName(const char *frmText) { + char name[BAS_MAX_IDENT]; + + basExtractFormName(frmText, name, sizeof(name)); + return textOut(name); +} + + +// Puts child widget w of parent (or of another parent: it moves) just +// before sibling before (NULL: last). The layout follows the child list. +static void relinkBefore(WidgetT *parent, WidgetT *w, WidgetT *before) { + widgetRemoveChild(w->parent, w); + widgetAddChild(parent, w); + + if (!before) { + return; + } + + // Move before and every sibling after it (but w) to the end, in order + WidgetT *c = before; + + while (c && c != w) { + WidgetT *next = c->nextSibling; + + widgetRemoveChild(parent, c); + widgetAddChild(parent, c); + c = next; + } +} + + +// s as the string result (a copy that outlives s). +static const char *textOut(const char *s) { + arrsetlen(sText, 0); + ctlAppend(s ? s : ""); + arrput(sText, '\0'); + return sText; +} diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c index 63b18510..003d6606 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formGfx.c +++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c @@ -57,6 +57,8 @@ #include "timer/timer.h" #include "../compiler/basEvents.h" #include "dvxApp.h" +#include "dvxDraw.h" +#include "dvxWgtP.h" #include "dvxVideo.h" #include "dvxWm.h" #include "thirdparty/stb_ds_wrap.h" @@ -69,15 +71,12 @@ #define SCREEN_SCALE_MAX 3 // largest whole-number window scale #define SCREEN_PIXEL_SINGLE 1 // a framebuffer pixel one window pixel across / down (square) #define SCREEN_FULL_BPP 8 // full screen: the screen's own palette in the DAC -#define SCREEN_SCAN_ALT_KP_ENTER 0xA6 // BIOS scan code of Alt with the keypad Enter #define SCREEN_KEY_BYTE 0xFF #define SCREEN_KEY_CR 13 #define SCREEN_TITLE_DEFAULT "BASIC Program" -#define DRAW_CANVAS_WGT "canvas" #define DRAW_PROP_IMAGE_W "ImageWidth" #define DRAW_PROP_IMAGE_H "ImageHeight" -#define DRAW_CANVAS_BORDER 2 // the canvas widget's sunken frame (each side) #define DRAW_DEFAULT_FORE 0x000000 #define DRAW_DEFAULT_BACK 0xFFFFFF #define DRAW_RED_SHIFT 16 @@ -93,6 +92,30 @@ #define DRAW_ROP_SOURCE_BIT 2 // the source bit #define DRAW_ROP_DEST_BIT 1 // the destination bit #define DRAW_BITS_PER_BYTE 8 +#define DRAW_FOCUS_ARGS 4 // DrawFocusRect x, y, width, height +#define DRAW_FOCUS_COLOR_ARGS 5 // ... , color +#define DRAW_FOCUS_COLOR_ARG 4 +#define DRAW_RECT_ARGS 4 // DrawBevel / DrawButtonFace x, y, width, height +#define DRAW_RECT_FLAG_ARG 4 // ... , sunken / pressed +#define DRAW_RECT_FACE_ARG 5 // ... , face colour +#define DRAW_BEVEL_WIDTH_ARG 6 // DrawBevel ... , border width +#define DRAW_BEVEL_MAX_ARGS 7 +#define DRAW_BUTTON_MAX_ARGS 6 +#define DRAW_BEVEL_WIDTH 2 // DVX's bevels are 2 pixels +#define DRAW_ARROW_ARGS 4 // DrawArrow / DrawScrollArrow x, y, size, direction +#define DRAW_ARROW_DIR_ARG 3 +#define DRAW_ARROW_COLOR_ARG 4 // DrawArrow ... , colour +#define DRAW_SB_ARGS 6 // DrawScrollBar x, y, length, pos, visible, total +#define DRAW_SB_HORZ_ARG 6 // ... , horizontal +#define DRAW_SB_CONTROL_ARG 7 // ... , control (a control's bar, as ListBox's) +#define DRAW_SB_CLICK_ARGS 5 // ScrollBarClick(length, offset, pos, visible, total) +#define DRAW_SB_CLICK_HORZ_ARG 5 // ... , horizontal +#define DRAW_SB_CLICK_CTRL_ARG 6 // ... , control +#define DRAW_SB_LINE_ROWS 1 // the rows a control bar's arrow moves +#define DRAW_FACE_NONE (-1) // DrawBevel face: leave the inside as it is +#define DRAW_ACCEL_ENABLED_ARG 1 // PrintAccel text [, enabled] +#define DRAW_ACCEL_MAX_ARGS 2 +#define DRAW_PAINT_KEY_ARG 10 // PaintPicture ... , vbTransparentCopy, key colour // The scale units (BAS_SCALE_*) in twips: 15 to a pixel, as VB on a // standard (96 DPI) display, whatever the real display is; characters are @@ -126,9 +149,6 @@ static const double sTwipsPerUnit[BAS_SCALE_COUNT][DRAW_DOWN + 1] = { [BAS_SCALE_HIMETRIC] = { DRAW_TWIPS_PER_INCH / (DRAW_MM_PER_INCH * DRAW_HIMETRIC_PER_MM), DRAW_TWIPS_PER_INCH / (DRAW_MM_PER_INCH * DRAW_HIMETRIC_PER_MM) } }; -// A canvas integer property getter (ImageWidth / ImageHeight) -typedef int32_t (*DrawIntGetterT)(const WidgetT *w); - // The program screen's window struct BasScreenWinT { WindowT *win; @@ -140,6 +160,12 @@ struct BasScreenWinT { uint8_t deskPal[BAS_SCREEN_PAL_ENTRIES * BAS_SCREEN_PAL_CHANNELS]; // 256-colour desktop palette the table was made for }; +// DrawFocusRect's dots: where and in which pixel value +typedef struct { + WidgetT *canvas; + uint32_t pixel; +} DrawFocusDotT; + // One Form's or PictureBox's drawing state struct BasDrawT { BasGfxT gfx; @@ -155,18 +181,28 @@ struct BasDrawT { // Drawing members (the IDE accepts them on forms and PictureBoxes) typedef enum { + DRAW_M_ACCELKEY, DRAW_M_CIRCLE, DRAW_M_CLS, + DRAW_M_DRAWARROW, + DRAW_M_DRAWBEVEL, + DRAW_M_DRAWBUTTONFACE, + DRAW_M_DRAWFOCUSRECT, + DRAW_M_DRAWSCROLLARROW, + DRAW_M_DRAWSCROLLBAR, DRAW_M_LINE, DRAW_M_PAINTPICTURE, DRAW_M_POINT, DRAW_M_PRINT, + DRAW_M_PRINTACCEL, DRAW_M_PSET, DRAW_M_SCALE, DRAW_M_SCALEX, DRAW_M_SCALEY, + DRAW_M_SCROLLBARCLICK, DRAW_M_TEXTHEIGHT, DRAW_M_TEXTWIDTH, + DRAW_M_TEXTWIDTHACCEL, DRAW_M_COUNT } DrawMethodE; @@ -188,18 +224,28 @@ typedef enum { } DrawPropE; static const char *const sDrawMethods[DRAW_M_COUNT] = { - [DRAW_M_CIRCLE] = "Circle", - [DRAW_M_CLS] = "Cls", - [DRAW_M_LINE] = "Line", - [DRAW_M_PAINTPICTURE] = "PaintPicture", - [DRAW_M_POINT] = "Point", - [DRAW_M_PRINT] = "Print", - [DRAW_M_PSET] = "PSet", - [DRAW_M_SCALE] = "Scale", - [DRAW_M_SCALEX] = "ScaleX", - [DRAW_M_SCALEY] = "ScaleY", - [DRAW_M_TEXTHEIGHT] = "TextHeight", - [DRAW_M_TEXTWIDTH] = "TextWidth" + [DRAW_M_ACCELKEY] = "AccelKey", + [DRAW_M_CIRCLE] = "Circle", + [DRAW_M_CLS] = "Cls", + [DRAW_M_DRAWARROW] = "DrawArrow", + [DRAW_M_DRAWBEVEL] = "DrawBevel", + [DRAW_M_DRAWBUTTONFACE] = "DrawButtonFace", + [DRAW_M_DRAWFOCUSRECT] = "DrawFocusRect", + [DRAW_M_DRAWSCROLLARROW] = "DrawScrollArrow", + [DRAW_M_DRAWSCROLLBAR] = "DrawScrollBar", + [DRAW_M_LINE] = "Line", + [DRAW_M_PAINTPICTURE] = "PaintPicture", + [DRAW_M_POINT] = "Point", + [DRAW_M_PRINT] = "Print", + [DRAW_M_PRINTACCEL] = "PrintAccel", + [DRAW_M_PSET] = "PSet", + [DRAW_M_SCALE] = "Scale", + [DRAW_M_SCALEX] = "ScaleX", + [DRAW_M_SCALEY] = "ScaleY", + [DRAW_M_SCROLLBARCLICK] = "ScrollBarClick", + [DRAW_M_TEXTHEIGHT] = "TextHeight", + [DRAW_M_TEXTWIDTH] = "TextWidth", + [DRAW_M_TEXTWIDTHACCEL] = "TextWidthAccel" }; static const char *const sDrawProps[DRAW_P_COUNT] = { @@ -223,10 +269,16 @@ static const char *const sDrawProps[DRAW_P_COUNT] = { // ============================================================ static bool canvasSize(WidgetT *canvas, int32_t *w, int32_t *h); +static int32_t drawCls(BasDrawT *d); static int32_t drawFind(const char *const *names, int32_t count, const char *name); +static void drawFocusDot(void *ctx, int32_t x, int32_t y); +static int32_t drawFocusRectOn(BasDrawT *d, const BasValueT *args, int32_t argc); static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas); static bool drawIsSurface(const BasControlT *ctrl); +static bool drawMeasure(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result); +static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, uint32_t opcode, const BasValueT *args, int32_t argc, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy); static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc); +static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across); static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode); static uint32_t drawRasterOp(uint32_t rop, uint32_t pattern, uint32_t source, uint32_t dest); static int32_t drawScale(BasDrawT *d, const BasValueT *args); @@ -235,6 +287,7 @@ static int32_t drawScaleArg(const BasValueT *args, int32_t argc, int32_t idx, static bool drawScaleLength(BasDrawT *d, const BasValueT *args, int32_t argc, bool across, double *out); static bool drawSetScaleProp(BasFormRtT *rt, BasDrawT *d, int32_t prop, BasValueT value); static void drawSync(BasDrawT *d); +static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc); static double drawToPixelLength(BasDrawT *d, double length, int32_t scale, bool across); static void drawToPixels(const BasGfxT *g, double x, double y, double w, double h, int32_t *px, int32_t *py, int32_t *pw, int32_t *ph); static uint32_t readPixel(const uint8_t *src, int32_t bytesPerPixel); @@ -257,6 +310,16 @@ static uint32_t surfToColor(const void *ctx, uint32_t pixel); static uint32_t surfToPixel(void *ctx, uint32_t color); +void basFormRtDrawClear(BasFormRtT *rt, BasControlT *ctrl) { + BasDrawT *d = drawIsSurface(ctrl) ? drawFor(rt, ctrl, true) : NULL; + + if (d && d->canvas) { + drawSync(d); + drawCls(d); + } +} + + void basFormRtDrawFree(BasControlT *ctrl) { free(ctrl->draw); ctrl->draw = NULL; @@ -367,6 +430,15 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba return true; } + // AccelKey / TextWidthAccel / ScrollBarClick only measure + if (method == DRAW_M_ACCELKEY || method == DRAW_M_TEXTWIDTHACCEL || method == DRAW_M_SCROLLBARCLICK) { + if (!drawMeasure(rt, drawFor(rt, ctrl, false), (DrawMethodE)method, args, argc, result)) { + basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAccelKey and TextWidthAccel take a caption; ScrollBarClick takes the bar's length, the click's offset from its top (or left end), the position, the rows shown, the rows in all and optionally True for a horizontal bar.", name); + } + + return true; + } + // ScaleX / ScaleY need no drawing area either if (method == DRAW_M_SCALEX || method == DRAW_M_SCALEY) { BasDrawT *scaled = drawFor(rt, ctrl, false); @@ -394,9 +466,7 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba switch ((DrawMethodE)method) { case DRAW_M_CLS: - err = basGfxClear(&d->gfx, basValLong((int32_t)d->gfx.back)); - d->gfx.lastX = 0.0; - d->gfx.lastY = 0.0; + err = drawCls(d); break; case DRAW_M_PSET: @@ -428,6 +498,19 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba drawPaintPicture(rt, ctrl, d, args, argc); break; + case DRAW_M_DRAWFOCUSRECT: + err = (argc == DRAW_FOCUS_ARGS || argc == DRAW_FOCUS_COLOR_ARGS) ? drawFocusRectOn(d, args, argc) : BAS_ERR_ILLEGAL_FUNC_CALL; + break; + + case DRAW_M_DRAWARROW: + case DRAW_M_DRAWBEVEL: + case DRAW_M_DRAWBUTTONFACE: + case DRAW_M_DRAWSCROLLARROW: + case DRAW_M_DRAWSCROLLBAR: + case DRAW_M_PRINTACCEL: + err = drawThemed(rt, d, (DrawMethodE)method, args, argc); + break; + case DRAW_M_SCALE: err = argc == BAS_WINDOW_ARGS ? drawScale(d, args) : BAS_ERR_ILLEGAL_FUNC_CALL; break; @@ -603,26 +686,25 @@ WindowT *basFormRtScreenWindow(const BasFormRtT *rt) { // The drawing buffer size of a canvas (its ImageWidth / ImageHeight). static bool canvasSize(WidgetT *canvas, int32_t *w, int32_t *h) { - const WgtIfaceT *iface = wgtGetIface(DRAW_CANVAS_WGT); - - *w = 0; - *h = 0; - - for (int32_t i = 0; iface && i < iface->propCount; i++) { - const WgtPropDescT *p = &iface->props[i]; - DrawIntGetterT get = (DrawIntGetterT)p->getFn; - - if (strcmp(p->name, DRAW_PROP_IMAGE_W) == 0) { - *w = get(canvas); - } else if (strcmp(p->name, DRAW_PROP_IMAGE_H) == 0) { - *h = get(canvas); - } - } + WgtValueT v; + *w = wgtGetProp(canvas, DRAW_PROP_IMAGE_W, &v) ? v.i : 0; + *h = wgtGetProp(canvas, DRAW_PROP_IMAGE_H, &v) ? v.i : 0; return *w > 0 && *h > 0; } +// Cls: the drawing cleared to BackColor, CurrentX / CurrentY back to 0. +// 0 or a BASIC error. +static int32_t drawCls(BasDrawT *d) { + int32_t err = basGfxClear(&d->gfx, basValLong((int32_t)d->gfx.back)); + + d->gfx.lastX = 0.0; + d->gfx.lastY = 0.0; + return err; +} + + static int32_t drawFind(const char *const *names, int32_t count, const char *name) { for (int32_t i = 0; name && i < count; i++) { if (strcasecmp(names[i], name) == 0) { @@ -634,6 +716,49 @@ static int32_t drawFind(const char *const *names, int32_t count, const char *nam } +// cppcheck-suppress constParameterCallback ; a DrawDotFnT: the context stays a plain void * for callers that keep state +static void drawFocusDot(void *ctx, int32_t x, int32_t y) { + const DrawFocusDotT *fd = (const DrawFocusDotT *)ctx; + + wgtCanvasSetPixel(fd->canvas, x, y, fd->pixel); +} + + +// DrawFocusRect x, y, width, height [, color]: the dotted rectangle DVX +// draws around a focused control (libdvx's pattern), in the object's +// scale, in ForeColor unless a color is given. 0 or a BASIC error. +static int32_t drawFocusRectOn(BasDrawT *d, const BasValueT *args, int32_t argc) { + int32_t px = 0; + int32_t py = 0; + int32_t pw = 0; + int32_t ph = 0; + uint32_t color = (argc == DRAW_FOCUS_COLOR_ARGS) ? (uint32_t)basValToNumber(args[DRAW_FOCUS_COLOR_ARG]) : d->gfx.fore; + DrawFocusDotT fd; + + drawToPixels(&d->gfx, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), basValToNumber(args[3]), &px, &py, &pw, &ph); + + // A negative width or height counts from the other edge + if (pw < 0) { + px += pw; + pw = -pw; + } + + if (ph < 0) { + py += ph; + ph = -ph; + } + + if (pw <= 0 || ph <= 0) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + fd.canvas = d->canvas; + fd.pixel = surfToPixel(d, color); + drawFocusRectDots(px, py, pw, ph, drawFocusDot, &fd); + return 0; +} + + // The control's drawing state, made on first use; with needCanvas a form // also gets its drawing area (a canvas the size of its content, added to // its controls). @@ -671,7 +796,7 @@ static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas) { int32_t w = form->contentBox->w > 0 ? form->contentBox->w : (form->window ? form->window->contentW : form->frmWidth); int32_t h = form->contentBox->h > 0 ? form->contentBox->h : (form->window ? form->window->contentH : form->frmHeight); - d->canvas = wgtCanvas(form->contentBox, w - 2 * DRAW_CANVAS_BORDER, h - 2 * DRAW_CANVAS_BORDER); + d->canvas = wgtCanvas(form->contentBox, w - 2 * CANVAS_BORDER_WIDTH, h - 2 * CANVAS_BORDER_WIDTH); if (d->canvas) { wgtInvalidate(d->canvas); @@ -692,6 +817,105 @@ static bool drawIsSurface(const BasControlT *ctrl) { } +// The measuring members (no drawing area needed): AccelKey(caption) -- +// the access key letter of a caption ("f" for "&File", "" when none); +// TextWidthAccel(caption) -- its width in the object's scale without +// the & markers; ScrollBarClick(length, offset, pos, visible, total +// [, horizontal [, control]]) -- the new position after a click offset +// along a bar DrawScrollBar drew that long (down a vertical bar, across a +// horizontal one; arrows one row, trough a page, thumb unchanged; with +// control the control bar's rule, widgetScrollbarApplyHit). False for +// bad arguments. +static bool drawMeasure(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result) { + if (method == DRAW_M_SCROLLBARCLICK) { + if (argc < DRAW_SB_CLICK_ARGS || argc > DRAW_SB_CLICK_CTRL_ARG + 1) { + return false; + } + + if (d) { + drawSync(d); + } + + const BasGfxT *g = d ? &d->gfx : NULL; + bool across = argc > DRAW_SB_CLICK_HORZ_ARG && basValIsTruthy(args[DRAW_SB_CLICK_HORZ_ARG]); + int32_t length = drawPixelSpan(g, basValToNumber(args[0]), across); + int32_t offset = drawPixelSpan(g, basValToNumber(args[1]), across); + + int32_t pos = basValToInt32(args[2]); + int32_t grab = 0; + + if (argc > DRAW_SB_CLICK_CTRL_ARG && basValIsTruthy(args[DRAW_SB_CLICK_CTRL_ARG])) { + widgetScrollbarApplyHit(length, offset, basValToInt32(args[4]), basValToInt32(args[3]), DRAW_SB_LINE_ROWS, basValToInt32(args[3]), &pos, &grab); + } else { + pos = wmVScrollbarAtClick(length, offset, pos, basValToInt32(args[3]), basValToInt32(args[4])); + } + + *result = basValLong(pos); + return true; + } + + if (argc != 1) { + return false; + } + + BasStringT *text = basValFormatString(args[0]); + + if (method == DRAW_M_ACCELKEY) { + char key = accelParse(text->data); + + result->type = BAS_TYPE_STRING; + result->strVal = basStringNew(&key, key ? 1 : 0); + } else { + double px = (double)textWidthAccel(&rt->ctx->font, text->data); + + if (d) { + drawSync(d); + px = basGfxToUnitsX(&d->gfx, px); + } + + *result = basValDouble(fabs(px)); + } + + basStringUnref(text); + return true; +} + + +// PaintPicture with vbGrayCopy (libdvx rectCopyGrayscale, a disabled +// picture) or vbTransparentCopy (rectCopyTransparent: every pixel but the +// key colour, the argument after the opcode or else the source's top-left +// pixel), from an image file's pixels or a PictureBox, at the source +// rectangle's size. +static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, uint32_t opcode, const BasValueT *args, int32_t argc, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy) { + DisplayT dst; + DisplayT srcDisp; + int32_t bpp = rt->ctx->display.format.bytesPerPixel; + + if (!pixels) { + if (!src || !wgtCanvasGetDisplay(src, &srcDisp)) { + return; + } + + pixels = srcDisp.backBuf; + pitch = srcDisp.pitch; + } + + if (!wgtCanvasGetDisplay(d->canvas, &dst)) { + return; + } + + if (opcode == BAS_ROP_GRAYCOPY) { + rectCopyGrayscale(&dst, &rt->ctx->blitOps, dx, dy, pixels, pitch, sx, sy, sw, sh); + } else { + uint32_t key = (argc > DRAW_PAINT_KEY_ARG) ? surfToPixel(d, (uint32_t)basValToNumber(args[DRAW_PAINT_KEY_ARG])) : readPixel(pixels + sy * pitch + sx * bpp, bpp); + + rectCopyTransparent(&dst, &rt->ctx->blitOps, dx, dy, pixels, pitch, sx, sy, sw, sh, key); + } + + wgtInvalidatePaint(d->canvas); +} + + // PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, // srcHeight [, opcode]]]: the source rectangle is stretched to the // destination (nearest pixel), each pixel combined with the one there and @@ -717,7 +941,7 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas void *ref = name ? basFormRtFindCtrl(rt, ctrl->form, name->data) : args[0].objVal; BasControlT *pic = basFormRtResolveCtrl(rt, ref); - src = pic && drawIsSurface(pic) && pic != &pic->form->formCtrl ? pic->widget : NULL; + src = pic && drawIsSurface(pic) && (!pic->form || pic != &pic->form->formCtrl) ? pic->widget : NULL; if (!src && name) { pixels = dvxLoadImage(rt->ctx, name->data, &srcW, &srcH, &pitch); @@ -728,7 +952,7 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas } if (!pixels && (!src || !canvasSize(src, &srcW, &srcH))) { - basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG or GIF image file, or a picture from LoadPicture."); + basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture."); return; } @@ -755,7 +979,16 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas dh = sh; } - uint32_t rop = (argc >= DRAW_PAINT_ROP_ARGS) ? (((uint32_t)basValToInt32(args[DRAW_PAINT_ROP_ARGS - 1]) >> BAS_ROP_TABLE_SHIFT) & BAS_ROP_TABLE_MASK) : (BAS_ROP_SRCCOPY >> BAS_ROP_TABLE_SHIFT); + uint32_t opcode = (argc >= DRAW_PAINT_ROP_ARGS) ? (uint32_t)basValToInt32(args[DRAW_PAINT_ROP_ARGS - 1]) : BAS_ROP_SRCCOPY; + + // DVX's copies: greyed, or colour-keyed (at the source's size) + if (opcode == BAS_ROP_GRAYCOPY || opcode == BAS_ROP_TRANSPCOPY) { + drawPaintKeyed(rt, d, src, pixels, pitch, opcode, args, argc, sx, sy, sw, sh, dx, dy); + dvxFreeImage(pixels); + return; + } + + uint32_t rop = (opcode >> BAS_ROP_TABLE_SHIFT) & BAS_ROP_TABLE_MASK; bool copy = rop == (BAS_ROP_SRCCOPY >> BAS_ROP_TABLE_SHIFT); uint32_t pattern = surfToPixel(d, d->gfx.fillColor); uint32_t mask = (bpp >= (int32_t)sizeof(uint32_t)) ? UINT32_MAX : ((1u << (bpp * DRAW_BITS_PER_BYTE)) - 1); @@ -790,6 +1023,21 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas } +// A length in the object's scale as whole pixels along one axis (across +// or down), measured from 0 so the scale's origin does not shift it. No +// scale (g NULL): the length is pixels. +static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across) { + int32_t px = 0; + int32_t py = 0; + int32_t pw = 0; + int32_t ph = 0; + + drawToPixels(g, 0.0, 0.0, length, length, &px, &py, &pw, &ph); + + return across ? pw : ph; +} + + // One object Print item at CurrentX / CurrentY in ForeColor (the DVX // font, transparent background). Columns, zones and lines are counted // in pixels from the left edge, CurrentX / CurrentY kept in the scale. @@ -812,7 +1060,8 @@ static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode) { return; case BAS_OBJ_PRINT_TAB: { - int32_t target = basValToInt32(value) - 1; + // The 1-based column made 0-based in 64 bits (TAB(INT32_MIN)) + int64_t target = (int64_t)basValToInt32(value) - 1; if (target < column) { g->lastY += basGfxToUnitsY(g, charH); @@ -1020,6 +1269,125 @@ static void drawSync(BasDrawT *d) { } +// The members that draw DVX's own look with libdvx, in the object's +// scale, colours as RGB: +// DrawBevel x, y, w, h [, sunken [, face [, width]]] -- drawBevel (face -1: no fill) +// DrawButtonFace x, y, w, h [, pressed [, face]] -- drawPressableBevel +// DrawArrow x, y, size, direction [, color] -- drawArrowGlyph +// DrawScrollArrow x, y, size, direction -- drawScrollbarArrowButton +// DrawScrollBar x, y, length, pos, visible, total [, horizontal [, control]] -- wmDrawScrollbarAt (control: widgetDrawScrollbarV / H) +// PrintAccel text [, enabled] -- drawWidgetTextAccel +// (the face defaults to the theme's, the arrow and text to ForeColor). +// 0 or a BASIC error. +static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc) { + const ColorSchemeT *cs = &rt->ctx->colors; + const BlitOpsT *ops = &rt->ctx->blitOps; + BasGfxT *g = &d->gfx; + DisplayT disp; + int32_t px = 0; + int32_t py = 0; + int32_t pw = 0; + int32_t ph = 0; + + if (!wgtCanvasGetDisplay(d->canvas, &disp)) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + if (method == DRAW_M_PRINTACCEL) { + if (argc < 1 || argc > DRAW_ACCEL_MAX_ARGS) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + BasStringT *text = basValFormatString(args[0]); + bool enabled = argc <= DRAW_ACCEL_ENABLED_ARG || basValIsTruthy(args[DRAW_ACCEL_ENABLED_ARG]); + + drawWidgetTextAccel(&disp, ops, &rt->ctx->font, basGfxRound(basGfxToPixelX(g, g->lastX)), basGfxRound(basGfxToPixelY(g, g->lastY)), text->data, surfToPixel(d, g->fore), 0, false, enabled, cs); + g->lastX += basGfxToUnitsX(g, (double)textWidthAccel(&rt->ctx->font, text->data)); + basStringUnref(text); + wgtInvalidatePaint(d->canvas); + return 0; + } + + if (method == DRAW_M_DRAWSCROLLBAR) { + if (argc < DRAW_SB_ARGS || argc > DRAW_SB_CONTROL_ARG + 1) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + bool across = argc > DRAW_SB_HORZ_ARG && basValIsTruthy(args[DRAW_SB_HORZ_ARG]); + bool control = argc > DRAW_SB_CONTROL_ARG && basValIsTruthy(args[DRAW_SB_CONTROL_ARG]); + double length = basValToNumber(args[2]); + int32_t pos = basValToInt32(args[3]); + int32_t visible = basValToInt32(args[4]); + int32_t total = basValToInt32(args[5]); + + drawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), length, length, &px, &py, &pw, &ph); + + // A control's bar (ListBox's, WGT_SB_W wide) or a popup list's + if (control && across) { + widgetDrawScrollbarH(&disp, ops, cs, px, py, pw, total, visible, pos); + } else if (control) { + widgetDrawScrollbarV(&disp, ops, cs, px, py, ph, total, visible, pos); + } else { + wmDrawScrollbarAt(&disp, ops, cs, across ? ScrollbarHorizontalE : ScrollbarVerticalE, px, py, across ? pw : ph, pos, visible, total); + } + wgtInvalidatePaint(d->canvas); + return 0; + } + + if (method == DRAW_M_DRAWARROW || method == DRAW_M_DRAWSCROLLARROW) { + // Only DrawArrow takes a colour after the direction + int32_t maxArgs = (method == DRAW_M_DRAWARROW) ? DRAW_ARROW_COLOR_ARG + 1 : DRAW_ARROW_ARGS; + int32_t dir = argc >= DRAW_ARROW_ARGS ? basValToInt32(args[DRAW_ARROW_DIR_ARG]) : -1; + + if (argc < DRAW_ARROW_ARGS || argc > maxArgs || dir < ScrollArrowUpE || dir > ScrollArrowRightE) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + // DrawScrollArrow's size is a length across; DrawArrow's is glyph rows + drawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), 0.0, &px, &py, &pw, &ph); + + if (method == DRAW_M_DRAWSCROLLARROW) { + drawScrollbarArrowButton(&disp, ops, cs, px, py, pw, (ScrollArrowDirE)dir); + } else { + uint32_t color = (argc > DRAW_ARROW_COLOR_ARG) ? (uint32_t)basValToNumber(args[DRAW_ARROW_COLOR_ARG]) : g->fore; + + drawArrowGlyph(&disp, ops, px, py, basValToInt32(args[2]), (ScrollArrowDirE)dir, surfToPixel(d, color)); + } + + wgtInvalidatePaint(d->canvas); + return 0; + } + + // DrawBevel / DrawButtonFace + int32_t maxArgs = (method == DRAW_M_DRAWBEVEL) ? DRAW_BEVEL_MAX_ARGS : DRAW_BUTTON_MAX_ARGS; + + if (argc < DRAW_RECT_ARGS || argc > maxArgs) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + bool down = argc > DRAW_RECT_FLAG_ARG && basValIsTruthy(args[DRAW_RECT_FLAG_ARG]); + bool noFill = argc > DRAW_RECT_FACE_ARG && basValToNumber(args[DRAW_RECT_FACE_ARG]) == DRAW_FACE_NONE; + uint32_t face = (argc > DRAW_RECT_FACE_ARG) ? surfToPixel(d, (uint32_t)basValToNumber(args[DRAW_RECT_FACE_ARG])) : cs->windowFace; + + drawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), basValToNumber(args[3]), &px, &py, &pw, &ph); + + if (method == DRAW_M_DRAWBUTTONFACE) { + drawPressableBevel(&disp, ops, px, py, pw, ph, down, face, cs); + } else { + BevelStyleT style; + + style.highlight = down ? cs->windowShadow : cs->windowHighlight; + style.shadow = down ? cs->windowHighlight : cs->windowShadow; + style.face = noFill ? BEVEL_NO_FILL : face; + style.width = (argc > DRAW_BEVEL_WIDTH_ARG) ? basValToInt32(args[DRAW_BEVEL_WIDTH_ARG]) : DRAW_BEVEL_WIDTH; + drawBevel(&disp, ops, px, py, pw, ph, &style); + } + + wgtInvalidatePaint(d->canvas); + return 0; +} + + // A rectangle in a scale -> pixels (no scale: the values are pixels). // A length in scale (BAS_SCALE_*) across or down as pixels: vbUser is the // object's current scale (a unit ScaleMode or a user scale). @@ -1338,7 +1706,7 @@ static void screenOnKey(WindowT *win, int32_t key, int32_t mod) { return; } - if ((mod & KEY_MOD_ALT) && (key == KEY_ENTER || key == (KEY_SCAN_ENTER | KEY_EXT_FLAG) || key == (SCREEN_SCAN_ALT_KP_ENTER | KEY_EXT_FLAG))) { + if ((mod & KEY_MOD_ALT) && (key == KEY_ENTER || key == (KEY_SCAN_ENTER | KEY_EXT_FLAG) || key == (KEY_SCAN_ALT_KP_ENTER | KEY_EXT_FLAG))) { screenFullScreen(rt, scr, !scr->fullScreen); return; } diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c index 51b2f9d0..3a18199a 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c @@ -78,16 +78,21 @@ DVX_ABI_STAMP(basrt); #define MENU_ID_BASE 10000 // Work-buffer / item-count caps for the BASIC dialog wrappers. -#define BAS_CHOICE_MAX_ITEMS 64 #define BAS_CHOICE_BUF_LEN 1024 #define BAS_FILTER_BUF_LEN 1024 #define BAS_INPUTBOX_BUF_LEN 512 // InputBox result text (both BASIC entry points) +#define MENU_ADD_ITEM_ARGS 3 // AddMenuItem / AddMenuRadioItem menu$, item$, caption$ (then the optional one) +#define MENU_ADD_MENU_ARGS 2 // AddMenu menu$, caption$ +#define DLG_INT_STEP_DEFAULT 1 // DlgIntInput's spinner step // Runtime-error report buffers: the formatted detail block, the summary // line, and the modal box that shows both. #define BAS_ERR_DETAIL_LEN 512 #define BAS_ERR_SUMMARY_LEN 128 #define BAS_ERR_BOX_LEN (BAS_ERR_SUMMARY_LEN + BAS_ERR_DETAIL_LEN) +#define BAS_ERR_TITLE "BASIC Runtime Error" +#define BAS_ERR_SUMMARY "Runtime error" // when the error names none +#define BAS_ERR_UNKNOWN "Unknown error" // dlsym name buffer: DVX_SYM_PREFIX + the longest BASIC identifier. #define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2) @@ -98,6 +103,7 @@ DVX_ABI_STAMP(basrt); // Largest pixel size a control size property accepts; values are // clamped to [0, BAS_CTRL_MAX_DIM] before they become size hints. #define BAS_CTRL_MAX_DIM 32767 +#define BAS_SIZE_PERCENT_MAX 100 // SizePercent's largest share of the container // DlgColor: the RGB() bits of a color, and the result when cancelled. #define BAS_RGB_MASK 0xFFFFFF @@ -107,9 +113,22 @@ DVX_ABI_STAMP(basrt); // that sizes it: Resize(w, h). #define WGT_METHOD_RESIZE "Resize" +// Most arguments a control method call or a control event passes through +// libdvx (a fixed WGT_SIG_* takes at most WGT_SIG_MAX_ARGS). +#define BAS_WGT_ARGS_MAX 16 + // The Clipboard object's name and methods. #define BAS_CLIPBOARD_NAME "Clipboard" + +// The Screen object: the desktop (its size and depth, the debug layout +// borders, the mouse pointer) and its Arrange method. +#define BAS_SCREEN_NAME BAS_SCREEN_OBJ_NAME +#define BAS_SCREEN_ARRANGE "Arrange" +#define BAS_SCREEN_BITSPERPIXEL "BitsPerPixel" +#define BAS_SCREEN_DEBUGLAYOUT "DebugLayout" +#define BAS_SCREEN_MOUSEPOINTER "MousePointer" #define BAS_FORM_TYPE_NAME "Form" // TypeName / TypeOf of a form +#define BAS_WINSTATE_NONE (-1) // BasFormT.frmWindowState: no WindowState waits for Show #define BAS_PICTURE_TYPE_NAME "Picture" // TypeName of a LoadPicture object #define BAS_PIC_PROP_HEIGHT "Height" // Picture object properties (read-only) #define BAS_PIC_PROP_TYPE "Type" @@ -133,8 +152,15 @@ DVX_ABI_STAMP(basrt); // ResGetText result cap (static return buffer). #define BAS_RES_TEXT_LEN 1024 +// HelpCompile's messages (HelpCompileMessages$) +#define HELP_MESSAGE_SEP '\n' +#define HELP_COMPILER_MISSING "The help compiler is not available, or no input or output file was given." + // Module-level form runtime pointer for onFormClose callback static BasFormRtT *sFormRt = NULL; +// How many programs' code calls are on the stack (handlers, sliced +// module code): a runtime created inside one does not become current. +static int32_t sCodeDepth = 0; // True only while frmParse is running inside basFormRtLoadFrm. The // parser calls basFormRtLoadForm via onFormBegin to create the bare form @@ -146,16 +172,26 @@ static BasFormRtT *sFormRt = NULL; // 'Load Form2' from Form1_Load takes the normal cached-.frm path. static bool sLoadingFrm = false; -// Tracks whether the most recent DlgInputBox call was cancelled. -// BASIC callers query this via DlgInputCancelled so they can tell the -// difference between the user hitting Cancel and the user clicking OK -// on an empty field. +// True from 'New FormName' (basFormRtNewForm) until basFormRtLoadForm has +// made the new copy: the form's name must not find the copy already +// loaded. Cleared as the copy is made, so forms its Load event loads are +// found by name again. +static bool sNewFormCopy = false; + +// Tracks whether the most recent DlgInputBox / DlgIntInput / +// DlgIntInputStep call was cancelled. BASIC callers query this via +// DlgInputCancelled so they can tell the difference between the user +// hitting Cancel and the user clicking OK on an empty field or on the +// default number. static bool sLastInputBoxCancelled = false; // The Clipboard object: basFormRtFindCtrl hands out this address for the // name Clipboard when no control or form has it; only its address is used. static const uint8_t sClipboardObj = 0; +// The Screen object, handed out the same way for the name Screen. +static const uint8_t sScreenObj = 0; + // VB's default properties: what a control of each type stands for where a // value is needed (Print Text1, If Check1 = 1, Text1 = "x"), used when the // control has that property. @@ -233,6 +269,17 @@ typedef struct { int (*getRts)(int); int (*setDtr)(int, bool); int (*setRts)(int, bool); + __typeof__(rs232ClearTxBuffer) *clearTxBuffer; + __typeof__(rs232GetBps) *getBps; + __typeof__(rs232GetData) *getData; + __typeof__(rs232GetHandshake) *getHandshake; + __typeof__(rs232GetLsr) *getLsr; + __typeof__(rs232GetMsr) *getMsr; + __typeof__(rs232GetParity) *getParity; + __typeof__(rs232GetStop) *getStop; + __typeof__(rs232GetTxBuffered) *getTxBuffered; + __typeof__(rs232Set) *set; + __typeof__(rs232SetBreak) *setBreak; } SerApiT; static SerApiT sSerApi; @@ -242,16 +289,27 @@ static bool sSerApiResolved = false; // Per-COM raw serial state: open tracking and terminal attach #define SER_RECV_BUF 4096 #define SER_BASIC_TRUE (-1) // BASIC's True, for the Ser functions that return one +#define SER_LINE_ERRORS (RS232_LSR_OVERRUN | RS232_LSR_PARITY_ERR | RS232_LSR_FRAME_ERR | RS232_LSR_BREAK) typedef struct { bool opened; // SerOpen succeeded and SerClose has not run bool attached; WidgetT *term; - int32_t com; // 0-based COM port index + int32_t com; // 0-based COM port index + int32_t lastError; // RS232_ result of the last SerOpen / SerSet } SerAttachT; static SerAttachT sSerAttach[RS232_NUM_PORTS]; +// A library's byte stream bound to a Terminal control (basFormRtTermBind) +typedef struct { + WidgetT *term; + void *ctx; + BasTermUnbindFnT onUnbind; +} TermBindT; + +static TermBindT *sTermBinds = NULL; // stb_ds + // SecLink function pointers (resolved lazily) typedef struct { @@ -265,6 +323,8 @@ typedef struct { int (*getPending)(void *); int (*setPassphrase)(void *, const char *); int (*setChannelPlain)(void *, uint8_t, bool); + int (*handshakeStep)(void *); + int32_t (*getDropped)(void *); } CommSecLinkApiT; static CommSecLinkApiT sCommApi; @@ -274,6 +334,7 @@ typedef struct { __typeof__(printUiOpenPort) *open; __typeof__(printJobWrite) *write; __typeof__(printJobClose) *close; + __typeof__(printUiWindow) *window; } PrintApiT; static PrintApiT sPrintApi; @@ -321,6 +382,8 @@ typedef struct { WidgetT *attachedTerm; // AnsiTerm widget if attached int32_t termChannel; // channel for terminal I/O bool termEncrypt; // encrypt terminal traffic + bool stepping; // CommHandshakeStep exchange running + int32_t lastError; // SECLINK_ result of the last call } CommSlotT; // Dynamic array of heap-allocated comm slots (stb_ds), allocated on @@ -332,9 +395,12 @@ static CommSlotT **sCommSlots = NULL; // hlpcCompile function pointer (resolved lazily); typed from the // hlpcCompile.h prototype so the signature has one source of truth. typedef __typeof__(hlpcCompile) *HlpcCompileFnT; +typedef __typeof__(hlpcSetMessageFn) *HlpcSetMessageFnT; -static HlpcCompileFnT sHlpcCompile = NULL; -static bool sHlpcResolved = false; +static HlpcCompileFnT sHlpcCompile = NULL; +static HlpcSetMessageFnT sHlpcSetMessage = NULL; +static bool sHlpcResolved = false; +static char *sHelpMessages = NULL; // stb_ds: the last HelpCompile's messages, one line each // shellOpenHelp function pointer (resolved lazily); typed from the // shellApp.h prototype. It reuses a running help viewer like F1 does. @@ -360,6 +426,9 @@ static bool sShellIdleResolved = false; static DvxResHandleT *sResHandles[RES_MAX_HANDLES]; +// The last ResGetData result, valid until the next call +static uint8_t *sResData = NULL; + // ============================================================ // .frm parsing context (used by frmLoad_* callbacks) @@ -368,8 +437,11 @@ static DvxResHandleT *sResHandles[RES_MAX_HANDLES]; typedef struct { char caption[FRM_MAX_LINE_LEN]; char name[BAS_MAX_IDENT]; + char shortcut[BAS_MENU_KEY_TEXT_MAX]; // Shortcut: key that runs the item + char shortcutText[BAS_MENU_KEY_TEXT_MAX]; // ShortcutText: key text shown only int32_t level; bool checked; + bool checkable; // the .frm gave Checked (True or False): a check item, which toggles when chosen bool radioCheck; bool enabled; bool visible; // level==0: true = menu bar entry, false = popup-only @@ -426,23 +498,43 @@ typedef enum { FORM_PROP_LAYOUT, FORM_PROP_CONTEXTMENU, FORM_PROP_HELPTOPIC, + FORM_PROP_PADDING, + FORM_PROP_SPACING, + FORM_PROP_WINDOWSTATE, + FORM_PROP_CONTROLS, + FORM_PROP_DESIGNMODE, + FORM_PROP_DESIGNSELECTION, + FORM_PROP_ICON, + FORM_PROP_TAG, + FORM_PROP_MOUSEPOINTER, + FORM_PROP_ACTIVECONTROL, FORM_PROP_COUNT } BasFormPropE; static const BasPropDescT sFormProps[FORM_PROP_COUNT] = { - [FORM_PROP_NAME] = { BAS_PROP_NAME, WGT_IFACE_STRING, true, false }, - [FORM_PROP_CAPTION] = { BAS_PROP_CAPTION, WGT_IFACE_STRING, true, true }, - [FORM_PROP_WIDTH] = { BAS_PROP_WIDTH, WGT_IFACE_INT, true, true }, - [FORM_PROP_HEIGHT] = { BAS_PROP_HEIGHT, WGT_IFACE_INT, true, true }, - [FORM_PROP_LEFT] = { BAS_PROP_LEFT, WGT_IFACE_INT, true, true }, - [FORM_PROP_TOP] = { BAS_PROP_TOP, WGT_IFACE_INT, true, true }, - [FORM_PROP_VISIBLE] = { BAS_PROP_VISIBLE, WGT_IFACE_BOOL, true, true }, - [FORM_PROP_RESIZABLE] = { BAS_PROP_RESIZABLE, WGT_IFACE_BOOL, true, true }, - [FORM_PROP_AUTOSIZE] = { BAS_PROP_AUTOSIZE, WGT_IFACE_BOOL, true, true }, - [FORM_PROP_CENTERED] = { BAS_PROP_CENTERED, WGT_IFACE_BOOL, true, true }, - [FORM_PROP_LAYOUT] = { BAS_PROP_LAYOUT, WGT_IFACE_STRING, true, false }, - [FORM_PROP_CONTEXTMENU] = { BAS_PROP_CONTEXTMENU, WGT_IFACE_STRING, true, true }, - [FORM_PROP_HELPTOPIC] = { BAS_PROP_HELPTOPIC, WGT_IFACE_STRING, true, true }, + [FORM_PROP_NAME] = { BAS_PROP_NAME, WGT_IFACE_STRING, true, false }, + [FORM_PROP_CAPTION] = { BAS_PROP_CAPTION, WGT_IFACE_STRING, true, true }, + [FORM_PROP_WIDTH] = { BAS_PROP_WIDTH, WGT_IFACE_INT, true, true }, + [FORM_PROP_HEIGHT] = { BAS_PROP_HEIGHT, WGT_IFACE_INT, true, true }, + [FORM_PROP_LEFT] = { BAS_PROP_LEFT, WGT_IFACE_INT, true, true }, + [FORM_PROP_TOP] = { BAS_PROP_TOP, WGT_IFACE_INT, true, true }, + [FORM_PROP_VISIBLE] = { BAS_PROP_VISIBLE, WGT_IFACE_BOOL, true, true }, + [FORM_PROP_RESIZABLE] = { BAS_PROP_RESIZABLE, WGT_IFACE_BOOL, true, true }, + [FORM_PROP_AUTOSIZE] = { BAS_PROP_AUTOSIZE, WGT_IFACE_BOOL, true, true }, + [FORM_PROP_CENTERED] = { BAS_PROP_CENTERED, WGT_IFACE_BOOL, true, true }, + [FORM_PROP_LAYOUT] = { BAS_PROP_LAYOUT, WGT_IFACE_STRING, true, false }, + [FORM_PROP_CONTEXTMENU] = { BAS_PROP_CONTEXTMENU, WGT_IFACE_STRING, true, true }, + [FORM_PROP_HELPTOPIC] = { BAS_PROP_HELPTOPIC, WGT_IFACE_STRING, true, true }, + [FORM_PROP_PADDING] = { BAS_PROP_PADDING, WGT_IFACE_INT, true, true }, + [FORM_PROP_SPACING] = { BAS_PROP_SPACING, WGT_IFACE_INT, true, true }, + [FORM_PROP_WINDOWSTATE] = { BAS_PROP_WINDOWSTATE, WGT_IFACE_INT, true, true }, + [FORM_PROP_CONTROLS] = { BAS_PROP_CONTROLS, BAS_PROP_OBJECT, true, false }, + [FORM_PROP_DESIGNMODE] = { BAS_PROP_DESIGNMODE, WGT_IFACE_BOOL, true, true }, + [FORM_PROP_DESIGNSELECTION] = { BAS_PROP_DESIGNSELECTION, WGT_IFACE_STRING, true, true }, + [FORM_PROP_ICON] = { BAS_PROP_ICON, WGT_IFACE_STRING, true, true }, + [FORM_PROP_TAG] = { BAS_PROP_TAG, WGT_IFACE_STRING, true, true }, + [FORM_PROP_MOUSEPOINTER] = { BAS_PROP_MOUSEPOINTER, WGT_IFACE_INT, true, true }, + [FORM_PROP_ACTIVECONTROL] = { BAS_PROP_ACTIVECONTROL, BAS_PROP_OBJECT, true, false }, }; // Common control property ids (every non-menu control, consulted after @@ -474,50 +566,86 @@ typedef enum { CTRL_PROP_LISTCOUNT, CTRL_PROP_DRAGMODE, CTRL_PROP_DRAGICON, + CTRL_PROP_PARENT, + CTRL_PROP_CONTAINER, + CTRL_PROP_TAG, + CTRL_PROP_MOUSEPOINTER, CTRL_PROP_COUNT } BasCtrlPropE; static const BasPropDescT sCtrlProps[CTRL_PROP_COUNT] = { - [CTRL_PROP_NAME] = { BAS_PROP_NAME, WGT_IFACE_STRING, true, false }, - [CTRL_PROP_LEFT] = { BAS_PROP_LEFT, WGT_IFACE_INT, true, true }, - [CTRL_PROP_TOP] = { BAS_PROP_TOP, WGT_IFACE_INT, true, true }, - [CTRL_PROP_WIDTH] = { BAS_PROP_WIDTH, WGT_IFACE_INT, true, true }, - [CTRL_PROP_HEIGHT] = { BAS_PROP_HEIGHT, WGT_IFACE_INT, true, true }, - [CTRL_PROP_MINWIDTH] = { BAS_PROP_MINWIDTH, WGT_IFACE_INT, true, true }, - [CTRL_PROP_MINHEIGHT] = { BAS_PROP_MINHEIGHT, WGT_IFACE_INT, true, true }, - [CTRL_PROP_MAXWIDTH] = { BAS_PROP_MAXWIDTH, WGT_IFACE_INT, true, true }, - [CTRL_PROP_MAXHEIGHT] = { BAS_PROP_MAXHEIGHT, WGT_IFACE_INT, true, true }, - [CTRL_PROP_WEIGHT] = { BAS_PROP_WEIGHT, WGT_IFACE_INT, true, true }, - [CTRL_PROP_VISIBLE] = { BAS_PROP_VISIBLE, WGT_IFACE_BOOL, true, true }, - [CTRL_PROP_ENABLED] = { BAS_PROP_ENABLED, WGT_IFACE_BOOL, true, true }, - [CTRL_PROP_READONLY] = { BAS_PROP_READONLY, WGT_IFACE_BOOL, true, true }, - [CTRL_PROP_BACKCOLOR] = { BAS_PROP_BACKCOLOR, WGT_IFACE_INT, true, true }, - [CTRL_PROP_FORECOLOR] = { BAS_PROP_FORECOLOR, WGT_IFACE_INT, true, true }, - [CTRL_PROP_CAPTION] = { BAS_PROP_CAPTION, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_TEXT] = { BAS_PROP_TEXT, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_TOOLTIPTEXT] = { BAS_PROP_TOOLTIPTEXT, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_CONTEXTMENU] = { BAS_PROP_CONTEXTMENU, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_HELPTOPIC] = { BAS_PROP_HELPTOPIC, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_DATASOURCE] = { BAS_PROP_DATASOURCE, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_DATAFIELD] = { BAS_PROP_DATAFIELD, WGT_IFACE_STRING, true, true }, - [CTRL_PROP_LISTCOUNT] = { BAS_PROP_LISTCOUNT, WGT_IFACE_INT, true, false }, - [CTRL_PROP_DRAGMODE] = { BAS_PROP_DRAGMODE, WGT_IFACE_INT, true, true }, - [CTRL_PROP_DRAGICON] = { BAS_PROP_DRAGICON, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_NAME] = { BAS_PROP_NAME, WGT_IFACE_STRING, true, false }, + [CTRL_PROP_LEFT] = { BAS_PROP_LEFT, WGT_IFACE_INT, true, true }, + [CTRL_PROP_TOP] = { BAS_PROP_TOP, WGT_IFACE_INT, true, true }, + [CTRL_PROP_WIDTH] = { BAS_PROP_WIDTH, WGT_IFACE_INT, true, true }, + [CTRL_PROP_HEIGHT] = { BAS_PROP_HEIGHT, WGT_IFACE_INT, true, true }, + [CTRL_PROP_MINWIDTH] = { BAS_PROP_MINWIDTH, WGT_IFACE_INT, true, true }, + [CTRL_PROP_MINHEIGHT] = { BAS_PROP_MINHEIGHT, WGT_IFACE_INT, true, true }, + [CTRL_PROP_MAXWIDTH] = { BAS_PROP_MAXWIDTH, WGT_IFACE_INT, true, true }, + [CTRL_PROP_MAXHEIGHT] = { BAS_PROP_MAXHEIGHT, WGT_IFACE_INT, true, true }, + [CTRL_PROP_WEIGHT] = { BAS_PROP_WEIGHT, WGT_IFACE_INT, true, true }, + [CTRL_PROP_VISIBLE] = { BAS_PROP_VISIBLE, WGT_IFACE_BOOL, true, true }, + [CTRL_PROP_ENABLED] = { BAS_PROP_ENABLED, WGT_IFACE_BOOL, true, true }, + [CTRL_PROP_READONLY] = { BAS_PROP_READONLY, WGT_IFACE_BOOL, true, true }, + [CTRL_PROP_BACKCOLOR] = { BAS_PROP_BACKCOLOR, WGT_IFACE_INT, true, true }, + [CTRL_PROP_FORECOLOR] = { BAS_PROP_FORECOLOR, WGT_IFACE_INT, true, true }, + [CTRL_PROP_CAPTION] = { BAS_PROP_CAPTION, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_TEXT] = { BAS_PROP_TEXT, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_TOOLTIPTEXT] = { BAS_PROP_TOOLTIPTEXT, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_CONTEXTMENU] = { BAS_PROP_CONTEXTMENU, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_HELPTOPIC] = { BAS_PROP_HELPTOPIC, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_DATASOURCE] = { BAS_PROP_DATASOURCE, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_DATAFIELD] = { BAS_PROP_DATAFIELD, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_LISTCOUNT] = { BAS_PROP_LISTCOUNT, WGT_IFACE_INT, true, false }, + [CTRL_PROP_DRAGMODE] = { BAS_PROP_DRAGMODE, WGT_IFACE_INT, true, true }, + [CTRL_PROP_DRAGICON] = { BAS_PROP_DRAGICON, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_PARENT] = { BAS_PROP_PARENT, BAS_PROP_OBJECT, true, false }, + [CTRL_PROP_CONTAINER] = { BAS_PROP_CONTAINER, BAS_PROP_OBJECT, true, true }, + [CTRL_PROP_TAG] = { BAS_PROP_TAG, WGT_IFACE_STRING, true, true }, + [CTRL_PROP_MOUSEPOINTER] = { BAS_PROP_MOUSEPOINTER, WGT_IFACE_INT, true, true }, }; // Menu item property ids (MenuName.Prop). The .frm-only menu keys -// (Caption, RadioCheck, Visible) are not readable or writable at run time. +// (RadioCheck, ShortcutText) are not readable or writable at run time. +// Caption is the label without its key text; Shortcut is the key that +// runs the item ("" = none). typedef enum { MENU_PROP_NAME, + MENU_PROP_CAPTION, MENU_PROP_CHECKED, MENU_PROP_ENABLED, + MENU_PROP_VISIBLE, + MENU_PROP_SHORTCUT, MENU_PROP_COUNT } BasMenuPropE; static const BasPropDescT sMenuProps[MENU_PROP_COUNT] = { - [MENU_PROP_NAME] = { BAS_PROP_NAME, WGT_IFACE_STRING, true, false }, - [MENU_PROP_CHECKED] = { BAS_PROP_CHECKED, WGT_IFACE_BOOL, true, true }, - [MENU_PROP_ENABLED] = { BAS_PROP_ENABLED, WGT_IFACE_BOOL, true, true }, + [MENU_PROP_NAME] = { BAS_PROP_NAME, WGT_IFACE_STRING, true, false }, + [MENU_PROP_CAPTION] = { BAS_PROP_CAPTION, WGT_IFACE_STRING, true, true }, + [MENU_PROP_CHECKED] = { BAS_PROP_CHECKED, WGT_IFACE_BOOL, true, true }, + [MENU_PROP_ENABLED] = { BAS_PROP_ENABLED, WGT_IFACE_BOOL, true, true }, + [MENU_PROP_VISIBLE] = { BAS_PROP_VISIBLE, WGT_IFACE_BOOL, true, true }, + [MENU_PROP_SHORTCUT] = { BAS_PROP_SHORTCUT, WGT_IFACE_STRING, true, true }, +}; + +// Screen object property ids (Screen.Prop). +typedef enum { + SCREEN_PROP_WIDTH, + SCREEN_PROP_HEIGHT, + SCREEN_PROP_BITSPERPIXEL, + SCREEN_PROP_DEBUGLAYOUT, + SCREEN_PROP_MOUSEPOINTER, + SCREEN_PROP_ACTIVECONTROL, + SCREEN_PROP_COUNT +} BasScreenPropE; + +static const BasPropDescT sScreenProps[SCREEN_PROP_COUNT] = { + [SCREEN_PROP_WIDTH] = { BAS_PROP_WIDTH, WGT_IFACE_INT, true, false }, + [SCREEN_PROP_HEIGHT] = { BAS_PROP_HEIGHT, WGT_IFACE_INT, true, false }, + [SCREEN_PROP_BITSPERPIXEL] = { BAS_SCREEN_BITSPERPIXEL, WGT_IFACE_INT, true, false }, + [SCREEN_PROP_DEBUGLAYOUT] = { BAS_SCREEN_DEBUGLAYOUT, WGT_IFACE_BOOL, true, true }, + [SCREEN_PROP_MOUSEPOINTER] = { BAS_SCREEN_MOUSEPOINTER, WGT_IFACE_INT, true, true }, + [SCREEN_PROP_ACTIVECONTROL] = { BAS_PROP_ACTIVECONTROL, BAS_PROP_OBJECT, true, false }, }; @@ -533,6 +661,7 @@ static const char *const sCommonMethodNames[BAS_CM_COUNT] = { [BAS_CM_ADDMENUITEM] = "AddMenuItem", [BAS_CM_ADDMENUSEPARATOR] = "AddMenuSeparator", [BAS_CM_ADDSUBMENU] = "AddSubMenu", + [BAS_CM_REMOVEMENUITEM] = "RemoveMenuItem", [BAS_CM_DESTROYMENU] = "DestroyMenu", [BAS_CM_ADDBUTTON] = "AddButton", [BAS_CM_ADDTEXTBUTTON] = "AddTextButton", @@ -540,6 +669,39 @@ static const char *const sCommonMethodNames[BAS_CM_COUNT] = { [BAS_CM_CLEAR] = "Clear", [BAS_CM_BUTTONCOUNT] = "ButtonCount", [BAS_CM_DRAG] = "Drag", + [BAS_CM_MOVEBEFORE] = "MoveBefore", + [BAS_CM_ADDMENU] = "AddMenu", + [BAS_CM_REMOVEMENU] = "RemoveMenu", + [BAS_CM_ADDMENURADIOITEM] = "AddMenuRadioItem", + [BAS_CM_SCROLLTOCONTROL] = "ScrollToControl", +}; + +// The form object's own methods (BAS_FORM_METHOD_*); Show and Hide are +// statements the compiler turns into calls of their own. +static const char *const sFormMethodNames[] = { + BAS_FORM_METHOD_SHOW, + BAS_FORM_METHOD_HIDE, + BAS_FORM_METHOD_FITWIDTH, + BAS_FORM_METHOD_FITHEIGHT, + BAS_FORM_METHOD_SAVESCREENSHOT +}; + +// MousePointer values (BAS_POINTER_*) and the DVX pointer each shows. +typedef struct { + int32_t pointer; + int32_t cursor; +} BasPointerCursorT; + +static const BasPointerCursorT sPointerCursors[] = { + { BAS_POINTER_DEFAULT, CURSOR_ARROW }, + { BAS_POINTER_ARROW, CURSOR_ARROW }, + { BAS_POINTER_CROSSHAIR, CURSOR_CROSSHAIR }, + { BAS_POINTER_SIZENESW, CURSOR_RESIZE_DIAG_NESW }, + { BAS_POINTER_SIZENS, CURSOR_RESIZE_V }, + { BAS_POINTER_SIZENWSE, CURSOR_RESIZE_DIAG_NWSE }, + { BAS_POINTER_SIZEWE, CURSOR_RESIZE_H }, + { BAS_POINTER_HOURGLASS, CURSOR_BUSY }, + { BAS_POINTER_NODROP, CURSOR_NO_DROP } }; @@ -547,12 +709,17 @@ static const char *const sCommonMethodNames[BAS_CM_COUNT] = { // Prototypes // ============================================================ +static BasValueT activeControlOf(BasFormRtT *rt, const BasFormT *onlyForm); static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, bool centered, int32_t width, int32_t height); -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 binaryMask); void *basExternResolve(void *ctx, const char *libName, const char *funcName); void basFormRtBindVm(BasFormRtT *rt); BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc); +BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt); +void basFormRtCodeFailed(BasFormRtT *rt); +void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev); int32_t basFormRtCommonMethodId(const char *name); +const char *basFormRtCommonMethodName(int32_t id); const BasPropDescT *basFormRtCommonProps(int32_t *count); BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module); WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout); @@ -560,6 +727,7 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef); void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height); WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox); +BasFormRtT *basFormRtCurrent(void); const char *basFormRtDefaultProp(void *ctx, void *objRef); void basFormRtDestroy(BasFormRtT *rt); void basFormRtEventLoop(BasFormRtT *rt); @@ -571,18 +739,26 @@ const BasPropDescT *basFormRtFindMenuProp(const char *name); bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName); bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount); static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName); +const char *basFormRtFormMethodAt(int32_t idx); +int32_t basFormRtFormMethodCount(void); const BasPropDescT *basFormRtFormProps(int32_t *count); +BasValueT basFormRtFromWgt(const WgtValueT *v); BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName); bool basFormRtHelpTopic(const BasFormRtT *rt, char *topic, int32_t topicSize); void basFormRtHideForm(void *ctx, void *formRef); static void basFormRtInitFormVars(BasFormRtT *rt, BasFormT *form, bool runInit); static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText); +bool basFormRtIsFormMethod(const char *name); void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName); void *basFormRtLoadForm(void *ctx, const char *formName); +void *basFormRtLoadFormText(void *ctx, const char *text); BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen); void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr); +bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods); const BasPropDescT *basFormRtMenuProps(int32_t *count); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title); +void basFormRtNewArgs(BasFormRtT *rt, const char *args); +void *basFormRtNewForm(void *ctx, const char *formName); const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v); void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen); void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName); @@ -590,38 +766,47 @@ BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref); void basFormRtRunSimple(BasFormRtT *rt); void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...); void basFormRtSerialShutdown(void); +BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt); void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName); void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); void basFormRtShowForm(void *ctx, void *formRef, bool modal); static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form); -#ifdef DVX_HOST_TEST +WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind); +void basFormRtTermForget(void *ctx); +void basFormRtTermUnbind(void *ctx); void basFormRtTestReset(void); -#endif +WgtValueT basFormRtToWgt(BasValueT v, uint8_t want, BasStringT **hold); const char *basFormRtTypeName(void *ctx, void *objRef); void basFormRtUnloadForm(void *ctx, void *formRef); +bool basFormRtWantsNewArgs(BasFormRtT *rt); +static BasValueT basFromWgt(const WgtValueT *v); static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *proc, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount); +static BasValueT callIfaceMethod(BasFormRtT *rt, BasControlT *ctrl, const WgtMethodDescT *m, BasValueT *args, int32_t argc); static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc); void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); void CommClose(int32_t handle); void CommDetach(int32_t handle); +int32_t CommDropped(int32_t handle); static CommSlotT *commGetSlot(int32_t handle); int32_t CommHandshake(int32_t handle, const char *passphrase); +int32_t CommHandshakeStep(int32_t handle, const char *passphrase); static void commIdlePoll(void *ctx); int32_t CommIsReady(int32_t handle); +int32_t CommLastError(int32_t handle); +static int32_t commNote(CommSlotT *slot, int32_t rc); static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); int32_t CommPending(int32_t handle); int32_t CommPoll(int32_t handle); -const char *CommRecv(int32_t handle, int32_t channel); +const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen); int32_t CommRecvEncrypted(int32_t handle, int32_t channel); static bool commResolveApi(void); -int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt); +int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt); int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain); static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len); -WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); -WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData); +static BasValueT contentBoxProp(BasFormRtT *rt, BasFormT *form, const char *propName, const BasValueT *value); static const char *ctrlDisplayName(const BasControlT *ctrl); static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt); static const char *defaultPropOf(BasFormRtT *rt, void *ref); @@ -637,13 +822,17 @@ static void deviceSound(void *ctx, double hz, double ticks); static bool deviceSoundBusy(void *ctx); static int32_t deviceStick(void *ctx, int32_t n); static int32_t deviceStrig(void *ctx, int32_t n); +int32_t DlgButtons(const char *title, const char *message, const char *buttons, int32_t icon); int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx); int32_t DlgColor(const char *title, int32_t defaultColor); const char *DlgFileOpen(const char *title, const char *filter); +const char *DlgFileOpenIn(const char *title, const char *filter, const char *folder); const char *DlgFileSave(const char *title, const char *filter); +const char *DlgFileSaveIn(const char *title, const char *filter, const char *folder); const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText); int32_t DlgInputCancelled(void); int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal); +int32_t DlgIntInputStep(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t stepVal); int32_t DlgPromptSave(const char *title); static bool dragAccepts(BasFormRtT *rt, const BasControlT *obj); static void dragAppEnded(int32_t appId); @@ -662,6 +851,9 @@ static void dragOverUpdate(void); static bool dragShowIcon(AppContextT *ctx, const char *path); static BasControlT *dragTargetAt(BasFormRtT *rt); static const BasProcEntryT *eventHandler(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, bool *outOverridden); +static const char *fileDialog(const char *title, const char *filter, int32_t flags, const char *folder); +static BasControlT *findButtonWith(BasFormT *form, const char *propName); +static BasControlT *findCtrlByName(const char *name); static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w); static BasControlT *findCtrlInForm(BasFormT *form, const char *name); static WidgetT *findCtrlWidgetByName(const char *name); @@ -678,10 +870,13 @@ static void firePaintForm(BasFormRtT *rt, BasFormT *form); static void firePaintResized(BasFormRtT *rt, BasControlT *ctrl); static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize); static BasControlT *formEventObject(BasFormT *form, const char *ctrlName); +static bool formMethod(BasFormRtT *rt, BasFormT *frm, const char *name, BasValueT *args, int32_t argc); +static BasFormT *formOfWindow(const WindowT *win, BasFormRtT **outRt); static void formRtCommIdleSync(void); static void formRtDetachTermsForForm(BasFormT *form); static bool formRtDetachTermsForWidget(WidgetT *widget); static void formRtSerIdleSync(void); +int32_t FrameMissed(void); static void freeControl(BasControlT *ctrl); static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char *name); static void frmLoadOnCtrlEnd(void *userData); @@ -697,21 +892,37 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); int32_t HelpCompile(const char *inputFile, const char *outputFile); +static void helpCompileMessage(void *ctx, const char *line); +const char *HelpCompileMessages(void); void HelpView(const char *hlpFile); -static int32_t ifaceEnumIndex(const WgtPropDescT *p, const char *name); +static void helpViewOpen(const char *hlpFile, const char *topicId); +void HelpViewTopic(const char *hlpFile, const char *topicId); static const WgtMethodDescT *ifaceFindMethod(const WgtIfaceT *iface, const char *name, uint8_t sig); static const char *ifaceGetStringProp(const BasControlT *ctrl, const char *propName); static bool menuContainsMenu(const MenuT *tree, const MenuT *target); +static BasMenuIdMapT *menuIdRow(BasFormT *form, int32_t id); +static void menuIdRowDrop(BasFormRtT *rt, BasFormT *form, int32_t idx); +static bool menuItemLocate(BasFormT *form, int32_t id, MenuT **outMenu, int32_t *outIdx, bool *fromBar); +static void menuItemRegister(BasFormT *form, MenuItemT *item, int32_t id, const char *name); +static void menuItemSetVisible(BasFormRtT *rt, BasFormT *form, BasMenuIdMapT *row, bool visible); +static void menuItemShortcut(BasFormRtT *rt, BasFormT *form, BasMenuIdMapT *row, MenuItemT *item, const char *text); +static void menuItemTake(MenuT *menu, int32_t idx); +static void menuTreeForget(BasFormRtT *rt, BasFormT *form, MenuT *tree); +static bool menuTreeLocate(MenuT *menu, int32_t id, MenuT **outMenu, int32_t *outIdx); +static void newArgsFlush(BasFormRtT *rt); static int32_t nextMenuItemId(BasFormT *form); static void onFormActivate(WindowT *win); static void onFormClose(WindowT *win); +static int32_t onFormCursor(WindowT *win, int32_t x, int32_t y); static void onFormDeactivate(WindowT *win); +static void onFormKey(WindowT *win, int32_t key, int32_t mod); static void onFormMenu(WindowT *win, int32_t menuId); static void onFormResize(WindowT *win, int32_t newW, int32_t newH); static void onWidgetBlur(WidgetT *w); static void onWidgetChange(WidgetT *w); static void onWidgetClick(WidgetT *w); static void onWidgetDblClick(WidgetT *w); +static void onWidgetEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); static void onWidgetFocus(WidgetT *w); static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift); static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii); @@ -723,32 +934,41 @@ static void onWidgetScroll(WidgetT *w, int32_t delta); static bool onWidgetValidate(WidgetT *w); static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize); static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, int32_t height); +static char *pictureFileRead(const char *path, int32_t *outLen); static BasPictureT *pictureFind(const BasFormRtT *rt, const void *ref); static BasPictureT *pictureFor(BasFormRtT *rt, const char *path); static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char *propName); +static bool pointerCursor(int32_t pointer, int32_t *outCursor); +static void popupMenuAdd(BasFormT *form, const char *name, MenuT *menu, bool ownsMenu); static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu); static void printerBind(BasFormRtT *rt); static bool printerClose(void *ctx, void *job); static void *printerOpen(void *ctx, int32_t port); static bool printerWrite(void *ctx, void *job, const char *data, int32_t len); +int32_t PrintForm(const char *formName); +int32_t PrintFormDialog(const char *formName); +static int32_t printFormWindow(const char *formName, bool askFirst); static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl); static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl); +int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len); int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); int32_t ResAddText(const char *path, const char *name, const char *text); void ResClose(int32_t handle); int32_t ResCount(int32_t handle); int32_t ResExtract(const char *path, const char *name, const char *outFile); +const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen); const char *ResGetText(const char *path, const char *name); static void resizeIfaceBuffer(BasControlT *ctrl); const char *ResName(int32_t handle, int32_t index); +static BasControlT *resolveCtrlArg(BasFormRtT *rt, BasFormT *form, BasValueT arg); static BasControlT *resolveCtrlRef(BasFormRtT *rt, void *ref); static BasControlT *resolveCtrlRefIn(BasFormT **forms, void *ref); static BasFormT *resolveFormRef(BasFormRtT *rt, void *ref); static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar); -static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc); +static BasFormT *resolveOwningForm(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntryT *proc); static bool resolveShellIdle(void); -const char *resolveTypeName(const char *typeName); int32_t ResOpen(const char *path); +static void *resReadNamed(const char *path, const char *name, uint32_t *outSize); int32_t ResRemove(const char *path, const char *name); int32_t ResSize(int32_t handle, int32_t index); int32_t ResType(int32_t handle, int32_t index); @@ -757,38 +977,66 @@ static void rtEventEnter(BasFormRtT *rt); static void rtEventLeave(BasFormRtT *rt); static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name); static BasVmResultE runSliced(BasFormRtT *rt); +static void runtimeErrorEnd(BasFormRtT *rt, const char *summary, const char *details); +static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc); +static BasValueT screenProp(BasFormRtT *rt, const char *propName); +static void screenSetProp(BasFormRtT *rt, const char *propName, BasValueT value); void SerAttach(int32_t com, const char *termCtrlName); int32_t SerAvailable(int32_t com); void SerClose(int32_t com); void SerDetach(int32_t com); void SerFlush(int32_t com); +void SerFlushTx(int32_t com); int32_t SerGetBase(int32_t com); +int32_t SerGetBaud(int32_t com); int32_t SerGetCts(int32_t com); +int32_t SerGetDataBits(int32_t com); int32_t SerGetDcd(int32_t com); int32_t SerGetDsr(int32_t com); int32_t SerGetDtr(int32_t com); +int32_t SerGetHandshake(int32_t com); int32_t SerGetIrq(int32_t com); +const char *SerGetParity(int32_t com); +int32_t SerGetRi(int32_t com); int32_t SerGetRts(int32_t com); +int32_t SerGetStopBits(int32_t com); int32_t SerGetUart(int32_t com); static void serIdlePoll(void *ctx); +int32_t SerLastError(int32_t com); +int32_t SerLineErrors(int32_t com); static int32_t serLineGet(int (*const *get)(int), int32_t com); static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on); +static int32_t serNote(int32_t com, int32_t rc); int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); -const char *SerRead(int32_t com); +static char serParity(const char *parity); +const uint8_t *SerRead(int32_t com, int32_t *outLen); static bool serResolveApi(void); +int32_t SerSet(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); void SerSetBase(int32_t com, int32_t base); +void SerSetBreak(int32_t com, int32_t on); void SerSetDtr(int32_t com, int32_t on); void SerSetIrq(int32_t com, int32_t irq); void SerSetRts(int32_t com, int32_t on); static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len); -int32_t SerWrite(int32_t com, const char *data); +int32_t SerTxPending(int32_t com); +static int32_t serValueGet(int32_t (*const *get)(int32_t), int32_t com); +int32_t SerWrite(int32_t com, const uint8_t *data, int32_t len); static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value); static void setControlString(char *dst, int32_t dstSize, BasValueT value); +static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value); static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value); +static void setHeapString(char **dst, BasValueT value); static bool setIfaceProp(BasFormRtT *rt, const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value); +static bool setMousePointer(BasFormRtT *rt, int32_t *dst, BasValueT value); static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, BasValueT value); +static void setWindowState(BasFormRtT *rt, BasFormT *frm, int32_t state); +int32_t SizeChars(int32_t n); static int32_t sizeFromValue(BasValueT value); +int32_t SizePercent(int32_t n); +static int32_t sizePixels(const WidgetT *w, int32_t tagged, bool across); +static int32_t sizeTagFromValue(BasValueT value); +static int32_t splitPipeList(char *buf, const char **ptrs, int32_t maxPtrs); int32_t SQLAffected(int32_t db); static const BasSqlCallbacksT *sqlApi(void); void SQLClose(int32_t db); @@ -806,16 +1054,37 @@ int32_t SQLNext(int32_t rs); int32_t SQLOpen(const char *path); int32_t SQLQuery(int32_t db, const char *sql); static void sqlResolveApi(BasSqlCallbacksT *sql); +static int32_t termBindIndex(const void *ctx); static void unloadFormNow(BasFormRtT *rt, BasFormT *form); static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl); static void upperCopy(char *dst, int32_t dstSize, const char *src); -bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val); -bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); +static void vmErrorEnd(BasFormRtT *rt); +static WgtValueT wgtFromBas(BasValueT v, uint8_t want, BasStringT **hold); static bool widgetHasAncestor(const WidgetT *w, const WidgetT *ancestor); +static bool windowLive(const AppContextT *ctx, const WindowT *win, int32_t winId); +static int32_t windowStateOf(const BasFormT *frm); static void wireWidgetEvents(WidgetT *w, BasControlT *ctrl); static BasValueT zeroValue(void); +// Screen.ActiveControl / Form.ActiveControl: the control with the +// keyboard focus (on onlyForm when given), Nothing when the focus is on +// no control of the program (or of that form). +static BasValueT activeControlOf(BasFormRtT *rt, const BasFormT *onlyForm) { + WidgetT *focused = wgtGetFocused(); + + for (int32_t i = 0; focused && i < (int32_t)arrlen(rt->forms); i++) { + BasControlT *ctrl = (onlyForm && rt->forms[i] != onlyForm) ? NULL : findCtrlByWidget(rt->forms[i], focused); + + if (ctrl) { + return basValObject(ctrl); + } + } + + return basValObject(NULL); +} + + // Create the window and BasFormT for a new form and register it in // rt->forms. Shared by CreateForm and the bare-form path of LoadForm; // callers set any form-specific frm* fields afterwards. @@ -839,10 +1108,14 @@ static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, snprintf(form->name, BAS_MAX_IDENT, "%s", formName); snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", BAS_LAYOUT_VBOX); + form->frmWindowState = BAS_WINSTATE_NONE; + form->menuIdNext = MENU_ID_BASE; win->onClose = onFormClose; win->onResize = onFormResize; win->onFocus = onFormActivate; win->onBlur = onFormDeactivate; + win->onKey = onFormKey; + win->userData = form; form->window = win; form->root = root; form->contentBox = contentBox; @@ -850,6 +1123,9 @@ static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, form->vm = rt->vm; form->module = rt->module; + // The MousePointer of the controls, the form and Screen + win->onCursorQuery = onFormCursor; + // Synthetic control for form-level property access. Its name field // stays empty on purpose: form->name is the single source of truth // and readers go through ctrlDisplayName(). @@ -866,43 +1142,64 @@ static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, } -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 binaryMask) { (void)ctx; (void)libName; // The compiler coerces every argument to its DECLAREd parameter type // before OP_CALL_EXTERN, so each value's type IS the declared type // and selects the cdecl slot layout: DOUBLE takes two 32-bit words, - // SINGLE one word holding the float bit pattern, STRING a char*, - // everything else one int32 word. The VM rejects argc > - // BAS_VM_MAX_CALL_ARGS before reaching here. - uint32_t nativeArgs[BAS_EXTERN_MAX_NATIVE_WORDS]; - char *tempStrings[BAS_VM_MAX_CALL_ARGS]; - int32_t tempStringCount = 0; - int32_t nativeCount = 0; - bool oom = false; + // SINGLE one word holding the float bit pattern, STRING a char* (a + // BINARY STRING the char* and an int32 length), everything else one + // int32 word; a BINARY STRING result adds a trailing int32_t * for + // its length. The VM rejects argc > BAS_VM_MAX_CALL_ARGS before + // reaching here. + BasNativeWordT nativeArgs[BAS_EXTERN_MAX_NATIVE_WORDS]; + uint8_t kinds[BAS_EXTERN_MAX_NATIVE_WORDS] = { 0 }; + char *tempStrings[BAS_VM_MAX_CALL_ARGS]; + int32_t tempStringCount = 0; + int32_t nativeCount = 0; + int32_t binaryLen = 0; + bool binaryRet = (binaryMask & BAS_EXTERN_BINARY_RET) != 0; + bool oom = false; for (int32_t i = 0; i < argc && !oom; i++) { switch (args[i].type) { case BAS_TYPE_STRING: { + // A private copy of every byte and the terminator, so the + // callee may hold the pointer for the call and a BINARY + // STRING keeps its zero bytes BasStringT *s = basValFormatString(args[i]); - char *cstr = strdup(s->data); + int32_t len = s->len; + char *copy = (char *)malloc((size_t)len + 1); + + if (copy) { + memcpy(copy, s->data, (size_t)len + 1); + } basStringUnref(s); - if (!cstr) { + if (!copy) { oom = true; break; } - nativeArgs[nativeCount++] = (uint32_t)(uintptr_t)cstr; - tempStrings[tempStringCount++] = cstr; + kinds[nativeCount] = BasNativeIntE; + nativeArgs[nativeCount++] = (BasNativeWordT)copy; + tempStrings[tempStringCount++] = copy; + + if (binaryMask & (1u << i)) { + kinds[nativeCount] = BasNativeIntE; + nativeArgs[nativeCount++] = (BasNativeWordT)(intptr_t)len; + } + break; } case BAS_TYPE_SINGLE: { union { float f; uint32_t u; } conv; conv.f = args[i].sngVal; + kinds[nativeCount] = BasNativeSingleE; nativeArgs[nativeCount++] = conv.u; break; } @@ -911,7 +1208,9 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha case BAS_TYPE_DATE: { union { double d; uint32_t u[2]; } conv; conv.d = args[i].dblVal; + kinds[nativeCount] = BasNativeDoubleLoE; nativeArgs[nativeCount++] = conv.u[0]; + kinds[nativeCount] = BasNativeDoubleHiE; nativeArgs[nativeCount++] = conv.u[1]; break; } @@ -920,27 +1219,44 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha // An int64_t: the amount times BAS_CURRENCY_SCALE union { int64_t c; uint32_t u[2]; } conv; conv.c = args[i].curVal; + kinds[nativeCount] = BasNativeInt64LoE; nativeArgs[nativeCount++] = conv.u[0]; + kinds[nativeCount] = BasNativeInt64HiE; nativeArgs[nativeCount++] = conv.u[1]; break; } default: { - nativeArgs[nativeCount++] = (uint32_t)(int32_t)basValToNumber(args[i]); + // Sign-extended to the word, so a 64-bit callee reading a + // 32-bit parameter sees the same value. + kinds[nativeCount] = BasNativeIntE; + nativeArgs[nativeCount++] = (BasNativeWordT)(intptr_t)(int32_t)basValToNumber(args[i]); break; } } } + if (binaryRet) { + kinds[nativeCount] = BasNativeIntE; + nativeArgs[nativeCount++] = (BasNativeWordT)&binaryLen; + } + BasValueT result; memset(&result, 0, sizeof(result)); if (oom) { basFormRtRuntimeError(sFormRt, "Out of memory", "Out of memory marshalling arguments for external function '%s'.", funcName ? funcName : "?"); } else { - double dblResult = 0.0; - bool fpReturn = (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE || retType == BAS_TYPE_DATE); - uint32_t rawResult = basNativeCall(funcPtr, nativeArgs, nativeCount, fpReturn, &dblResult); + BasNativeRetE ret = BasNativeRetIntE; + + if (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_DATE) { + ret = BasNativeRetDoubleE; + } else if (retType == BAS_TYPE_SINGLE) { + ret = BasNativeRetSingleE; + } + + double dblResult = 0.0; + BasNativeWordT rawResult = basNativeCall(funcPtr, nativeArgs, kinds, nativeCount, ret, &dblResult); // The i386 ABI leaves the upper bits of a sub-32-bit return (bool, // int16_t) undefined, so narrow by the declared return type instead @@ -951,8 +1267,12 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha result = basValDate(dblResult); } else if (retType == BAS_TYPE_SINGLE) { result = basValSingle((float)dblResult); + } else if (retType == BAS_TYPE_STRING && binaryRet) { + // NULL or a negative length gives "" + result.type = BAS_TYPE_STRING; + result.strVal = basStringNew((const char *)rawResult, binaryLen); } else if (retType == BAS_TYPE_STRING) { - const char *str = (const char *)(uintptr_t)rawResult; + const char *str = (const char *)rawResult; result = basValStringFromC(str ? str : ""); } else if (retType == BAS_TYPE_BOOLEAN) { result = basValBool((uint8_t)rawResult != 0); @@ -1001,11 +1321,13 @@ void basFormRtBindVm(BasFormRtT *rt) { ui.msgBox = basFormRtMsgBox; ui.inputBox = basFormRtInputBox; ui.createForm = basFormRtCreateForm; + ui.newForm = basFormRtNewForm; ui.createCtrlEx = basFormRtCreateCtrlEx; ui.setEvent = basFormRtSetEvent; ui.removeCtrl = basFormRtRemoveCtrl; ui.typeName = basFormRtTypeName; ui.loadPicture = basFormRtLoadPicture; + ui.loadFormText = basFormRtLoadFormText; ui.defaultProp = basFormRtDefaultProp; ui.ctx = rt; @@ -1024,11 +1346,21 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, return clipboardMethod(rt, methodName, args, argc); } + if (ctrlRef == &sScreenObj) { + return screenMethod(rt, methodName, args, argc); + } + + BasValueT ucResult; + + if (basUcSurfaceCall(rt, ctrlRef, methodName, args, argc, &ucResult)) { + return ucResult; + } + if (pictureFind(rt, ctrlRef)) { basFormRtRuntimeError(rt, "Picture has no methods", "Method: %s\nA Picture object has the read-only properties Width, Height and Type.", - methodName ? methodName : "?"); + methodName); return zeroValue(); } @@ -1038,8 +1370,8 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, basFormRtRuntimeError(rt, "Method call on unknown control", "Control: %s\nMethod: %s\nControl was not found on any loaded form.", - (rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?", - methodName ? methodName : "?"); + rt->lastLookupName[0] ? rt->lastLookupName : "?", + methodName); return zeroValue(); } @@ -1047,11 +1379,21 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, basFormRtRuntimeError(rt, "Method call on control with no widget", "Method: %s\nControl: %s\nThe control is known but has no attached widget.", - methodName ? methodName : "?", + methodName, ctrlDisplayName(ctrl)); return zeroValue(); } + // Form.Controls(index or name): one of the form's controls + if (ctrl == &ctrl->form->formCtrl && strcasecmp(methodName, BAS_PROP_CONTROLS) == 0) { + return basFormRtDesignControlsItem(rt, ctrl->form, args, argc); + } + + // The form's own methods + if (ctrl == &ctrl->form->formCtrl && formMethod(rt, ctrl->form, methodName, args, argc)) { + return zeroValue(); + } + // Drawing methods of forms and PictureBoxes BasValueT drawResult; @@ -1060,200 +1402,17 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, } // Interface descriptor methods - const WgtIfaceT *iface = ctrl->iface; + const WgtMethodDescT *m = wgtIfaceFindMethod(ctrl->iface, methodName); - if (iface) { - for (int32_t i = 0; i < iface->methodCount; i++) { - if (strcasecmp(iface->methods[i].name, methodName) != 0) { - continue; - } + if (m) { + BasValueT result = callIfaceMethod(rt, ctrl, m, args, argc); - const WgtMethodDescT *m = &iface->methods[i]; - WidgetT *w = ctrl->widget; - - switch (m->sig) { - case WGT_SIG_VOID: - ((void (*)(WidgetT *))m->fn)(w); - return zeroValue(); - - case WGT_SIG_INT: - if (argc >= 1) { - ((void (*)(WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0])); - } - return zeroValue(); - - case WGT_SIG_BOOL: - if (argc >= 1) { - ((void (*)(WidgetT *, bool))m->fn)(w, basValIsTruthy(args[0])); - } - return zeroValue(); - - case WGT_SIG_STR: - if (argc >= 1) { - BasStringT *s = basValFormatString(args[0]); - ((void (*)(WidgetT *, const char *))m->fn)(w, s->data); - basStringUnref(s); - } - return zeroValue(); - - case WGT_SIG_INT_INT: - if (argc >= 2) { - ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1])); - } - - // A PictureBox resized by its Resize method is repainted - if (strcasecmp(m->name, WGT_METHOD_RESIZE) == 0) { - firePaintResized(rt, ctrl); - } - - return zeroValue(); - - case WGT_SIG_INT_BOOL: - if (argc >= 2) { - ((void (*)(WidgetT *, int32_t, bool))m->fn)(w, (int32_t)basValToNumber(args[0]), basValIsTruthy(args[1])); - } - return zeroValue(); - - case WGT_SIG_RET_INT: { - int32_t v = ((int32_t (*)(const WidgetT *))m->fn)(w); - return basValLong(v); - } - - case WGT_SIG_RET_BOOL: { - bool v = ((bool (*)(const WidgetT *))m->fn)(w); - return basValBool(v); - } - - case WGT_SIG_RET_BOOL_INT: { - if (argc >= 1) { - bool v = ((bool (*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0])); - return basValBool(v); - } - return basValBool(false); - } - - case WGT_SIG_INT3: - if (argc >= 3) { - ((void (*)(WidgetT *, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2])); - } - return zeroValue(); - - case WGT_SIG_INT4: - if (argc >= 4) { - ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]), (int32_t)basValToNumber(args[3])); - } - return zeroValue(); - - case WGT_SIG_RET_INT_INT_INT: { - if (argc >= 2) { - int32_t v = ((int32_t (*)(const WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1])); - return basValLong(v); - } - return zeroValue(); - } - - case WGT_SIG_INT5: - if (argc >= 5) { - ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]), (int32_t)basValToNumber(args[3]), (int32_t)basValToNumber(args[4])); - } - return zeroValue(); - - case WGT_SIG_RET_STR_INT: { - if (argc >= 1) { - const char *s = ((const char *(*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0])); - return basValStringFromC(s ? s : ""); - } - return basValStringFromC(""); - } - - case WGT_SIG_RET_STR_INT_INT: { - if (argc >= 2) { - const char *s = ((const char *(*)(const WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1])); - return basValStringFromC(s ? s : ""); - } - return basValStringFromC(""); - } - - case WGT_SIG_INT_INT_STR: - if (argc >= 3) { - BasStringT *s = basValFormatString(args[2]); - ((void (*)(WidgetT *, int32_t, int32_t, const char *))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), s->data); - basStringUnref(s); - } - return zeroValue(); - - case WGT_SIG_STR_BOOL_BOOL: { - if (argc >= 3) { - BasStringT *s = basValFormatString(args[0]); - bool v = ((bool (*)(WidgetT *, const char *, bool, bool))m->fn)(w, s->data, basValIsTruthy(args[1]), basValIsTruthy(args[2])); - basStringUnref(s); - return basValBool(v); - } - return basValBool(false); - } - - case WGT_SIG_STR_STR_BOOL: { - if (argc >= 3) { - BasStringT *s1 = basValFormatString(args[0]); - BasStringT *s2 = basValFormatString(args[1]); - int32_t v = ((int32_t (*)(WidgetT *, const char *, const char *, bool))m->fn)(w, s1->data, s2->data, basValIsTruthy(args[2])); - basStringUnref(s1); - basStringUnref(s2); - return basValLong(v); - } - return zeroValue(); - } - - case WGT_SIG_RET_STR: { - const char *s = ((const char *(*)(const WidgetT *))m->fn)(w); - return basValStringFromC(s ? s : ""); - } - - case WGT_SIG_STR_INT: - if (argc >= 2) { - BasStringT *s = basValFormatString(args[0]); - ((void (*)(WidgetT *, const char *, int32_t))m->fn)(w, s->data, (int32_t)basValToNumber(args[1])); - basStringUnref(s); - } - return zeroValue(); - - case WGT_SIG_INT_STR: - if (argc >= 2) { - BasStringT *s = basValFormatString(args[1]); - ((void (*)(WidgetT *, int32_t, const char *))m->fn)(w, (int32_t)basValToNumber(args[0]), s->data); - basStringUnref(s); - } - return zeroValue(); - - case WGT_SIG_STR_STR: - if (argc >= 2) { - BasStringT *a = basValFormatString(args[0]); - BasStringT *b = basValFormatString(args[1]); - ((void (*)(WidgetT *, const char *, const char *))m->fn)(w, a->data, b->data); - basStringUnref(a); - basStringUnref(b); - } - return zeroValue(); - - case WGT_SIG_STR_BOOL: - if (argc >= 2) { - BasStringT *s = basValFormatString(args[0]); - ((void (*)(WidgetT *, const char *, bool))m->fn)(w, s->data, basValIsTruthy(args[1])); - basStringUnref(s); - } - return zeroValue(); - - case WGT_SIG_RET_INT_STR: { - if (argc >= 1) { - BasStringT *s = basValFormatString(args[0]); - int32_t v = ((int32_t (*)(WidgetT *, const char *))m->fn)(w, s->data); - basStringUnref(s); - return basValLong(v); - } - return zeroValue(); - } - } + // A PictureBox resized by its Resize method is repainted + if (m->sig == WGT_SIG_INT_INT && strcasecmp(m->name, WGT_METHOD_RESIZE) == 0) { + firePaintResized(rt, ctrl); } + + return result; } // Common methods (SetFocus, Refresh) @@ -1261,6 +1420,30 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, } +BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt) { + BasFormRtT *prev = sFormRt; + + sFormRt = rt; + sCodeDepth++; + rtEventEnter(rt); + return prev; +} + + +void basFormRtCodeFailed(BasFormRtT *rt) { + if (rt->vm->errorMsg[0]) { + vmErrorEnd(rt); + } +} + + +void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev) { + sCodeDepth--; + sFormRt = prev; + rtEventLeave(rt); +} + + int32_t basFormRtCommonMethodId(const char *name) { for (int32_t i = 0; i < BAS_CM_COUNT; i++) { if (strcasecmp(sCommonMethodNames[i], name) == 0) { @@ -1272,6 +1455,11 @@ int32_t basFormRtCommonMethodId(const char *name) { } +const char *basFormRtCommonMethodName(int32_t id) { + return (id >= 0 && id < BAS_CM_COUNT) ? sCommonMethodNames[id] : NULL; +} + + const BasPropDescT *basFormRtCommonProps(int32_t *count) { *count = CTRL_PROP_COUNT; return sCtrlProps; @@ -1289,7 +1477,10 @@ BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) { rt->vm = vm; rt->module = module; - sFormRt = rt; + if (sCodeDepth == 0) { + sFormRt = rt; + } + basFormRtBindVm(rt); return rt; } @@ -1342,8 +1533,9 @@ void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, cons return NULL; } - // Resolve VB name to DVX widget type name - const char *wgtTypeName = resolveTypeName(typeName); + // Resolve VB name to DVX widget type name (the program's own + // UserControls first) + const char *wgtTypeName = basUcResolveType(rt, typeName); if (!wgtTypeName) { basFormRtRuntimeError(rt, @@ -1391,7 +1583,7 @@ void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, cons return NULL; } - WidgetT *widget = createWidget(wgtTypeName, parent); + WidgetT *widget = wgtCreateByName(wgtTypeName, parent); if (!widget) { basFormRtRuntimeError(rt, @@ -1515,6 +1707,11 @@ WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const ch // (BAS_PIC_PROP_DEFAULT), a control's its VB default property // (sDefaultProps) when it has that property; forms, menus and the // Clipboard have none. +BasFormRtT *basFormRtCurrent(void) { + return sFormRt; +} + + const char *basFormRtDefaultProp(void *ctx, void *objRef) { BasFormRtT *rt = (BasFormRtT *)ctx; @@ -1522,7 +1719,7 @@ const char *basFormRtDefaultProp(void *ctx, void *objRef) { return BAS_PIC_PROP_DEFAULT; } - BasControlT *ctrl = (objRef == &sClipboardObj) ? NULL : resolveCtrlRef(rt, objRef); + BasControlT *ctrl = (objRef == &sClipboardObj || objRef == &sScreenObj) ? NULL : resolveCtrlRef(rt, objRef); if (!ctrl || ctrl->menuId > 0 || (ctrl->form && ctrl == &ctrl->form->formCtrl)) { return NULL; @@ -1555,6 +1752,11 @@ void basFormRtDestroy(BasFormRtT *rt) { basFormRtScreenClose(rt); + // A busy cursor the program left on goes with it + if (rt->pointerBusy) { + dvxSetBusy(rt->ctx, false); + } + // Close serial/secLink and drop the idle pollers before tearing down the // forms whose terminal widgets they reference. Also invoked from the // stub's _appShutdown hook so a force-killed app gets the same cleanup. @@ -1588,6 +1790,9 @@ void basFormRtDestroy(BasFormRtT *rt) { arrfree(rt->pendingUnload); shfree(rt->procIndex); + // The program's own UserControl classes leave the widget registry + basUcRuntimeEnd(rt); + // Free .frm source cache for (int32_t i = 0; i < rt->frmCacheCount; i++) { free(rt->frmCache[i].frmSource); @@ -1600,6 +1805,7 @@ void basFormRtDestroy(BasFormRtT *rt) { } arrfree(rt->pictures); + free(rt->pendingArgs); free(rt); } @@ -1613,6 +1819,8 @@ void basFormRtEventLoop(BasFormRtT *rt) { if (!dvxUpdate(rt->ctx)) { break; } + + newArgsFlush(rt); } } @@ -1628,9 +1836,7 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { // Remember the last name asked for so a subsequent method/prop // call that lands on a NULL ctrl can still name the culprit. - if (rt && ctrlName) { - snprintf(rt->lastLookupName, sizeof(rt->lastLookupName), "%s", ctrlName); - } + snprintf(rt->lastLookupName, sizeof(rt->lastLookupName), "%s", ctrlName); // The current form first (a local miss is expected for cross-form // access; don't spam the log), then every other loaded form: dynamic @@ -1642,7 +1848,7 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { // form in that case. BasControlT *hit = form ? findNamedObjectInForm(form, ctrlName) : NULL; - if (hit || !rt) { + if (hit) { return hit; } @@ -1660,8 +1866,13 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { } } - // VB's Clipboard object, unless a control or form has the name. - return (ctrlName && strcasecmp(ctrlName, BAS_CLIPBOARD_NAME) == 0) ? (void *)&sClipboardObj : NULL; + // VB's Clipboard and Screen objects, unless a control or form has the + // name. + if (ctrlName && strcasecmp(ctrlName, BAS_CLIPBOARD_NAME) == 0) { + return (void *)&sClipboardObj; + } + + return (ctrlName && strcasecmp(ctrlName, BAS_SCREEN_NAME) == 0) ? (void *)&sScreenObj : NULL; } @@ -1729,12 +1940,27 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c } +const char *basFormRtFormMethodAt(int32_t idx) { + return (idx >= 0 && idx < DVX_ARRAY_LEN(sFormMethodNames)) ? sFormMethodNames[idx] : NULL; +} + + +int32_t basFormRtFormMethodCount(void) { + return DVX_ARRAY_LEN(sFormMethodNames); +} + + const BasPropDescT *basFormRtFormProps(int32_t *count) { *count = FORM_PROP_COUNT; return sFormProps; } +BasValueT basFormRtFromWgt(const WgtValueT *v) { + return basFromWgt(v); +} + + BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { BasFormRtT *rt = (BasFormRtT *)ctx; @@ -1744,14 +1970,24 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { return clipboardMethod(rt, propName, NULL, 0); } + if (ctrlRef == &sScreenObj) { + return screenProp(rt, propName); + } + const BasPictureT *pic = pictureFind(rt, ctrlRef); if (pic) { return pictureProp(rt, pic, propName); } + BasValueT ucValue; + + if (basUcSurfaceGet(rt, ctrlRef, propName, &ucValue)) { + return ucValue; + } + // No name: the control's default property (Text1 where a value is needed) - if (propName && !propName[0]) { + if (!propName[0]) { propName = defaultPropOf(rt, ctrlRef); if (!propName) { @@ -1765,8 +2001,8 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { basFormRtRuntimeError(rt, "Read of unknown control", "Control: %s\nProperty: %s\nControl was not found on any loaded form.", - (rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?", - propName ? propName : "?"); + rt->lastLookupName[0] ? rt->lastLookupName : "?", + propName); return zeroValue(); } @@ -1777,18 +2013,40 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { MenuItemT *item = resolveMenuItem(ctrl->form, ctrl->menuId, NULL); if (item) { - const BasPropDescT *pd = propName ? basFormRtFindMenuProp(propName) : NULL; + const BasPropDescT *pd = basFormRtFindMenuProp(propName); + BasMenuPropE propId = pd ? (BasMenuPropE)(pd - sMenuProps) : MENU_PROP_COUNT; + + switch (propId) { + case MENU_PROP_CAPTION: { + const char *sep = strchr(item->label, BAS_MENU_KEY_SEP); + BasStringT *text = basStringNew(item->label, sep ? (int32_t)(sep - item->label) : (int32_t)strlen(item->label)); + BasValueT out = basValString(text); + + basStringUnref(text); + return out; + } - switch (pd ? (BasMenuPropE)(pd - sMenuProps) : MENU_PROP_COUNT) { case MENU_PROP_CHECKED: return basValBool(item->checked); case MENU_PROP_ENABLED: return basValBool(item->enabled); + case MENU_PROP_VISIBLE: { + const BasMenuIdMapT *row = menuIdRow(ctrl->form, ctrl->menuId); + + return basValBool(!row || !row->hiddenIn); + } + case MENU_PROP_NAME: return basValStringFromC(ctrl->name); + case MENU_PROP_SHORTCUT: { + const BasMenuIdMapT *row = menuIdRow(ctrl->form, ctrl->menuId); + + return basValStringFromC(row ? row->shortcut : ""); + } + case MENU_PROP_COUNT: break; } @@ -1798,7 +2056,7 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { basFormRtRuntimeError(rt, "Unknown menu property", "Menu: %s\nProperty: %s\nValid: %s.", - ctrl->name, propName ? propName : "?", valid); + ctrl->name, propName, valid); return zeroValue(); } } @@ -1807,7 +2065,7 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { basFormRtRuntimeError(rt, "Property read on control with no widget", "Control: %s\nProperty: %s\nThe control is known but has no attached widget.", - ctrlDisplayName(ctrl), propName ? propName : "?"); + ctrlDisplayName(ctrl), propName); return zeroValue(); } @@ -1856,7 +2114,7 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { "Control: %s\nType: %s\nProperty: %s\nThe control has no readable property by that name.", ctrl->name, ctrl->typeName[0] ? ctrl->typeName : "?", - propName ? propName : "?"); + propName); return zeroValue(); } @@ -1980,6 +2238,17 @@ static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char * } +bool basFormRtIsFormMethod(const char *name) { + for (int32_t i = 0; name && i < DVX_ARRAY_LEN(sFormMethodNames); i++) { + if (strcasecmp(sFormMethodNames[i], name) == 0) { + return true; + } + } + + return false; +} + + void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) { if (!rt) { return; @@ -2013,8 +2282,8 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) { void *basFormRtLoadForm(void *ctx, const char *formName) { BasFormRtT *rt = (BasFormRtT *)ctx; - // Check if form already exists - for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + // Check if form already exists (not for New: that wants another copy) + for (int32_t i = 0; !sNewFormCopy && i < (int32_t)arrlen(rt->forms); i++) { if (strcasecmp(rt->forms[i]->name, formName) == 0) { return rt->forms[i]; } @@ -2025,7 +2294,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { // 'Unload X' compiles to a Load-then-Unload pair, and re-parsing // the cached .frm here would resurrect the form and unload it a // second time. Unload on the returned form is a no-op (unloading). - for (int32_t i = 0; i < (int32_t)arrlen(rt->pendingUnload); i++) { + for (int32_t i = 0; !sNewFormCopy && i < (int32_t)arrlen(rt->pendingUnload); i++) { if (strcasecmp(rt->pendingUnload[i]->name, formName) == 0) { return rt->pendingUnload[i]; } @@ -2053,7 +2322,10 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { } } - // No cache entry: create a bare form (first-time load without .frm file) + // No cache entry: create a bare form (first-time load without .frm + // file). A New copy is made here, so later loads find forms by name. + sNewFormCopy = false; + BasFormT *form = allocForm(rt, formName, false, true, BAS_DEFAULT_FORM_W, BAS_DEFAULT_FORM_H); if (!form) { @@ -2073,6 +2345,19 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { } +// BasUiCallbacksT.loadFormText: LoadFormText(text$). A form named like +// one already loaded is a copy of its own, as New makes. +void *basFormRtLoadFormText(void *ctx, const char *text) { + BasFormRtT *rt = (BasFormRtT *)ctx; + BasFormT *form; + + sNewFormCopy = true; + form = basFormRtLoadFrm(rt, text, (int32_t)strlen(text)); + sNewFormCopy = false; + return form; +} + + BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen) { if (!rt || !source || sourceLen <= 0) { return NULL; @@ -2116,7 +2401,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // missing controls. Created-by-us means it sits past the // pre-parse count; a pre-existing form matched by name is left // alone. Immediate free is safe even at eventDepth > 0: no VM - // frame can reference a form whose Form_Load never fired. + // frame can reference a form whose Load event never fired. if (ctx.form) { bool created = false; @@ -2160,14 +2445,29 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout); } + // Padding / Spacing from the .frm (0, the default, changes nothing) + if (form->frmPadding) { + BasValueT v = basValLong(form->frmPadding); + + contentBoxProp(rt, form, BAS_PROP_PADDING, &v); + } + + if (form->frmSpacing) { + BasValueT v = basValLong(form->frmSpacing); + + contentBoxProp(rt, form, BAS_PROP_SPACING, &v); + } + // Build menu bar + popup menus from accumulated menu items. // A top-level Menu with Visible=False becomes a standalone // popup (stored by name on the form). All other top-level // menus go on the window's menu bar. Nested items are added // to whichever top-level (bar or popup) is currently active. + // The form has its window: frmLoadOnFormBegin got it from + // basFormRtLoadForm, whose every form is made by allocForm. int32_t menuCount = (int32_t)arrlen(menuItems); - if (menuCount > 0 && form->window) { + if (menuCount > 0) { MenuBarT *bar = NULL; bool haveVisibleTop = false; @@ -2201,66 +2501,87 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen if (mi->level == 0) { if (mi->visible) { - // Top-level menu header on the bar + // Top-level menu header on the bar, named so the + // menu methods can add to it menuStack[0] = bar ? wmAddMenu(bar, mi->caption) : NULL; curTopPopup = NULL; + popupMenuAdd(form, mi->name, menuStack[0], false); } else { // Popup menu: allocate standalone, store by name. curTopPopup = wmCreateMenu(); menuStack[0] = curTopPopup; - - if (curTopPopup) { - BasFrmPopupMenuT entry; - memset(&entry, 0, sizeof(entry)); - snprintf(entry.name, BAS_MAX_IDENT, "%s", mi->name); - entry.menu = curTopPopup; - entry.ownsMenu = true; // resource-built popup root - arrput(form->popupMenus, entry); - form->popupMenuCount = (int32_t)arrlen(form->popupMenus); - } + popupMenuAdd(form, mi->name, curTopPopup, true); } } else if (!menuStack[mi->level - 1]) { // Parent was dropped: drop this item as well. - } else if (isSubParent) { - menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption); - } else if (isSep) { - wmAddMenuSeparator(menuStack[mi->level - 1]); } else { + // Every item (submenu, separator or command) gets its + // id and name: it can be renamed, hidden and removed. MenuT *parentMenu = menuStack[mi->level - 1]; int32_t id = MENU_ID_BASE + i; + int32_t before = parentMenu->itemCount; - if (mi->radioCheck) { - wmAddMenuRadioItem(parentMenu, mi->caption, id, mi->checked); - } else if (mi->checked) { - wmAddMenuCheckItem(parentMenu, mi->caption, id, true); + if (isSubParent) { + menuStack[mi->level] = wmAddSubMenu(parentMenu, mi->caption); + popupMenuAdd(form, mi->name, menuStack[mi->level], false); + } else if (isSep) { + wmAddMenuSeparator(parentMenu); } else { - wmAddMenuItem(parentMenu, mi->caption, id); - } + char label[FRM_MAX_LINE_LEN]; + int32_t key; + int32_t mods; - // Disable through the menu the item was added to, so - // bar and popup items are handled alike. - if (!mi->enabled) { - MenuItemT *added = wmMenuFindItemInMenu(parentMenu, id); + if (!basFormRtMenuLabel(mi->caption, mi->shortcut, mi->shortcutText, label, sizeof(label), &key, &mods)) { + basFormRtRuntimeError(rt, + "Unknown shortcut key", + "Menu: %s\nShortcut: %s\nUse a key such as F2, Ctrl+X, Alt+Enter or Shift+Del.", + mi->name, mi->shortcut); + } - if (added) { - added->enabled = false; + if (key != 0) { + if (!form->accel) { + form->accel = dvxCreateAccelTable(); + } + + dvxAddAccel(form->accel, key, mods, id); + } + + if (mi->radioCheck) { + wmAddMenuRadioItem(parentMenu, label, id, mi->checked); + } else if (mi->checkable) { + wmAddMenuCheckItem(parentMenu, label, id, mi->checked); + } else { + wmAddMenuItem(parentMenu, label, id); } } - // Store ID-to-name mapping for event dispatch. - // Popup menu items use the same table -- the - // window's onMenu handler looks up by ID - // regardless of which menu the click came from. - BasMenuIdMapT map; - memset(&map, 0, sizeof(map)); - map.id = id; - snprintf(map.name, BAS_MAX_IDENT, "%s", mi->name); - arrput(form->menuIdMap, map); - form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap); + if (parentMenu->itemCount > before) { + MenuItemT *added = &parentMenu->items[parentMenu->itemCount - 1]; + BasMenuIdMapT *row; + + added->enabled = mi->enabled; + menuItemRegister(form, added, id, mi->name); + row = menuIdRow(form, id); + + if (row) { + snprintf(row->shortcut, sizeof(row->shortcut), "%s", mi->shortcut); + } + } } } - form->window->onMenu = onFormMenu; + // Nested items written Visible = False start hidden + for (int32_t i = 0; i < menuCount; i++) { + BasMenuIdMapT *row = (menuItems[i].level > 0 && !menuItems[i].visible) ? menuIdRow(form, MENU_ID_BASE + i) : NULL; + + if (row) { + menuItemSetVisible(rt, form, row, false); + } + } + + form->menuIdNext = MENU_ID_BASE + menuCount; + form->window->onMenu = onFormMenu; + form->window->accelTable = form->accel; } arrfree(menuItems); @@ -2336,7 +2657,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Allocate per-form variable storage and run init code. // This must happen AFTER controls are created (so init code can - // reference controls by name) but BEFORE Form_Load fires. Skipped + // reference controls by name) but BEFORE the form's Load event fires. Skipped // when a Resize handler already unloaded the form -- its Unload has // fired, so no further code may run on it. if (form && !form->unloading) { @@ -2351,7 +2672,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Auto-refresh Data controls that have no MasterSource (masters/standalone). // Detail controls are refreshed automatically by the master-detail cascade // when the master's Reposition event fires. Skipped when a handler - // unloaded the form (e.g. 'Unload Me' inside Form_Load). + // unloaded the form (e.g. 'Unload Me' inside the form's Load handler). if (form && !form->unloading) { for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { BasControlT *dc = form->controls[i]; @@ -2400,7 +2721,7 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) { if (path[0]) { int32_t len = 0; - char *data = platformReadFile(path, &len); + char *data = pictureFileRead(path, &len); if (!data) { *outErr = BAS_ERR_FILE_NOT_FOUND; @@ -2432,6 +2753,33 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) { } +bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods) { + char keyText[BAS_MENU_KEY_TEXT_MAX]; + bool known = true; + + *outKey = 0; + *outMods = 0; + keyText[0] = '\0'; + + if (shortcut && shortcut[0]) { + known = dvxAccelParse(shortcut, outKey, outMods); + dvxAccelFormat(*outKey, *outMods, keyText, sizeof(keyText)); + } + + if (shortcutText && shortcutText[0]) { + snprintf(keyText, sizeof(keyText), "%s", shortcutText); + } + + if (keyText[0]) { + snprintf(out, outSize, "%s%c%s", caption, BAS_MENU_KEY_SEP, keyText); + } else { + snprintf(out, outSize, "%s", caption); + } + + return known; +} + + const BasPropDescT *basFormRtMenuProps(int32_t *count) { *count = MENU_PROP_COUNT; return sMenuProps; @@ -2445,6 +2793,33 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const cha } +// New FormName: another copy of the form, loaded from its .frm like the +// first (its own window, controls, menus and form-level variables; its +// init code and Load event run). The form's name keeps finding the +// first loaded copy. +void basFormRtNewArgs(BasFormRtT *rt, const char *args) { + if (!rt) { + return; + } + + free(rt->pendingArgs); + rt->pendingArgs = strdup(args ? args : ""); + newArgsFlush(rt); +} + + +void *basFormRtNewForm(void *ctx, const char *formName) { + BasFormRtT *rt = (BasFormRtT *)ctx; + + sNewFormCopy = true; + + void *form = basFormRtLoadForm(rt, formName); + + sNewFormCopy = false; + return form; +} + + const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v) { const BasPictureT *pic = (v.type == BAS_TYPE_OBJECT) ? pictureFind(rt, v.objVal) : NULL; @@ -2513,21 +2888,16 @@ void basFormRtRunSimple(BasFormRtT *rt) { } -// Report a non-recoverable runtime error. Logs to DVX.LOG with the -// summary on one line and the details indented beneath, then shows a -// modal MessageBox with the details, then halts the VM so execution -// stops instead of limping on with a corrupted state. +// Report a non-recoverable runtime error the form runtime found: log it, +// halt the VM, mark the runtime terminated and show the error box (see +// runtimeErrorEnd). The message the host reads (vm->errorMsg, "Summary\n +// Details") and the error site are recorded first. // // Used for errors that indicate a bug in the BASIC program: unknown // control type, missing control reference, unknown method, etc. // Silent no-ops here would hide these failures from the developer. void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...) { - // If we're already in a terminated state, swallow follow-on errors - // silently. The modal message box pumps events, which can re-enter - // the form runtime and trip the same missing-control / missing- - // method path for every queued event. Showing the dialog again - // for each one stacks modals and spams the log; the first report - // is enough to point the user at the root cause. + // Follow-on errors while terminated are swallowed (see runtimeErrorEnd). if (rt && rt->terminated) { return; } @@ -2539,50 +2909,10 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta vsnprintf(details, sizeof(details), detailFmt, ap); va_end(ap); - // Log the error only when we're also going to show the modal - // dialog. Hosts that suppress the dialog (the IDE) do their own - // reporting via the Output pane and editor navigation, and don't - // need a redundant log copy of every runtime error. - if (!rt || !rt->suppressErrorDialog) { - dvxLog(""); - dvxLog("BASIC RUNTIME ERROR: %s", summary ? summary : "(no summary)"); - - // Split details on newlines and indent each one. - const char *p = details; - - while (*p) { - const char *nl = strchr(p, '\n'); - int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p); - char line[BAS_ERR_DETAIL_LEN]; - - if (len >= (int32_t)sizeof(line)) { - len = sizeof(line) - 1; - } - - memcpy(line, p, len); - line[len] = '\0'; - dvxLog(" %s", line); - - if (!nl) { - break; - } - - p = nl + 1; - } + if (!summary) { + summary = BAS_ERR_SUMMARY; } - // Halt the VM and flip rt->terminated BEFORE showing the dialog. - // dvxMessageBox pumps events while it's open, and a single missing - // control typically has several queued events targeting it (load + - // activate + paint + ...). If we haven't marked the runtime as - // terminated yet, each re-entry into basFormRtRuntimeError passes - // the early-return check and stacks another dialog. Marking first - // means the re-entry sees terminated=true, returns early, and the - // user only has to dismiss one dialog. Setting vm->errorMsg makes - // basVmRun return BAS_VM_ERROR (not HALTED) so the IDE treats this - // as a runtime error, shows the diagnostic, and navigates the - // editor to vm->currentLine. ended=true also breaks the IDE's - // VB-style event pump loop. if (rt && rt->vm) { BasVmT *vm = rt->vm; @@ -2591,9 +2921,7 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta // having to parse the line number back out of the middle. // For the compiled stub, the host shows this as-is, which is // fine -- the details string is the interesting part. - snprintf(vm->errorMsg, sizeof(vm->errorMsg), - "%s\n%s", - summary ? summary : "Runtime error", details); + snprintf(vm->errorMsg, sizeof(vm->errorMsg), "%s\n%s", summary, details); // Snapshot PC and source line at the error site so the // debugger can locate the owning SUB and line. runSubLoop @@ -2611,22 +2939,9 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta // of the opcode that triggered the host callback. vm->errorPc = vm->currentOpPc > 0 ? vm->currentOpPc : vm->pc; vm->errorLine = vm->currentLine; - - vm->running = false; - vm->ended = true; } - if (rt) { - rt->terminated = true; - } - - if (rt && rt->ctx && !rt->suppressErrorDialog) { - char boxMsg[BAS_ERR_BOX_LEN]; - snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s", - summary ? summary : "Runtime error", - details); - dvxMessageBox(rt->ctx, "BASIC Runtime Error", boxMsg, MB_OK | MB_ICONERROR); - } + runtimeErrorEnd(rt, summary, details); } @@ -2663,6 +2978,14 @@ void basFormRtSerialShutdown(void) { } +BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt) { + BasFormRtT *prev = sFormRt; + + sFormRt = rt; + return prev; +} + + void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName) { BasControlT *ctrl = resolveCtrlRef((BasFormRtT *)ctx, ctrlRef); @@ -2693,8 +3016,12 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT BasFormRtT *rt = (BasFormRtT *)ctx; const char *path = basFormRtPicturePath(rt, value); + if (basUcSurfaceSet(rt, ctrlRef, propName, value)) { + return; + } + // No name: the control's default property (Text1 = "x") - if (propName && !propName[0]) { + if (!propName[0]) { propName = defaultPropOf(rt, ctrlRef); if (!propName) { @@ -2764,11 +3091,22 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { basFormRtFireEvent(rt, form, form->name, BAS_EVT_ACTIVATE); } + WindowT *prevModal = rt->ctx->modalWindow; + int32_t prevModalId = prevModal ? prevModal->id : 0; + if (!form->unloading) { if (form->frmAutoSize) { dvxFitWindow(rt->ctx, form->window); } + // A WindowState set while the form was hidden (.frm or code) + if (form->frmWindowState != BAS_WINSTATE_NONE) { + int32_t state = form->frmWindowState; + + form->frmWindowState = BAS_WINSTATE_NONE; + setWindowState(rt, form, state); + } + if (modal) { rt->ctx->modalWindow = form->window; } @@ -2779,6 +3117,23 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { firePaintForm(rt, form); } + // Show vbModal returns when the form is hidden or unloaded (as in VB), + // so the code after it can read what the user chose. The bracket + // keeps the form's memory valid for these tests; an unloaded form is + // detached at once. The window that was modal before gets the input + // gate back. + if (modal && !form->unloading) { + bool running = true; + + while (running && !form->detached && form->window->visible && !rt->terminated) { + running = dvxUpdate(rt->ctx); + } + + if (rt->ctx->modalWindow == NULL || rt->ctx->modalWindow == form->window) { + rt->ctx->modalWindow = windowLive(rt->ctx, prevModal, prevModalId) ? prevModal : NULL; + } + } + rtEventLeave(rt); } @@ -2791,6 +3146,7 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { // (basFormRtDestroy, basFormRtUnloadForm, onFormClose). static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) { rt->refCacheKey = NULL; + basFormRtDesignForget(form); // Detach serial/secLink terminals bound to this form's widgets while // both the controls array and the widgets are still alive. Skipping @@ -2845,6 +3201,64 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) { arrfree(form->popupMenus); form->popupMenus = NULL; form->popupMenuCount = 0; + + // The window may outlive the form (a deferred destroy): it must not + // reach the freed form or its menu shortcut keys. + if (form->window) { + form->window->userData = NULL; + form->window->accelTable = NULL; + } + + dvxFreeAccelTable(form->accel); + form->accel = NULL; +} + + +// Bind a library's byte stream to the Terminal control ctrlName (see +// formrt.h). NULL when the running program has no Terminal of that name. +WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind) { + BasControlT *ctrl = findCtrlByName(ctrlName); + + if (!ctrl || !ctrl->widget || strcasecmp(ctrl->typeName, BAS_TERMINAL_TYPE) != 0) { + return NULL; + } + + // One binding per terminal and per stream, as SerAttach / CommAttach + basFormRtTermUnbind(ctx); + + if (formRtDetachTermsForWidget(ctrl->widget)) { + formRtSerIdleSync(); + } + + TermBindT bind; + + bind.term = ctrl->widget; + bind.ctx = ctx; + bind.onUnbind = onUnbind; + arrput(sTermBinds, bind); + wgtAnsiTermSetComm(ctrl->widget, ctx, NULL, writeFn); + return ctrl->widget; +} + + +// Drop ctx's binding without touching the widget (it may be gone). +void basFormRtTermForget(void *ctx) { + int32_t i = termBindIndex(ctx); + + if (i >= 0) { + arrdel(sTermBinds, i); + } +} + + +// End ctx's binding: the Terminal stops sending to it. +void basFormRtTermUnbind(void *ctx) { + int32_t i = termBindIndex(ctx); + + if (i >= 0) { + wgtAnsiTermSetComm(sTermBinds[i].term, NULL, NULL, NULL); + arrdel(sTermBinds, i); + } } @@ -2863,13 +3277,20 @@ void basFormRtTestReset(void) { } } + free(sResData); + sResData = NULL; memset(sSerAttach, 0, sizeof(sSerAttach)); memset(&sSerApi, 0, sizeof(sSerApi)); + arrfree(sTermBinds); + sTermBinds = NULL; memset(&sCommApi, 0, sizeof(sCommApi)); sSerApiResolved = false; sCommApiResolved = false; + arrfree(sHelpMessages); + sHelpMessages = NULL; sHlpcCompile = NULL; + sHlpcSetMessage = NULL; sHlpcResolved = false; sShellOpenHelp = NULL; sShellOpenHelpResolved = false; @@ -2877,6 +3298,7 @@ void basFormRtTestReset(void) { sShellUnregisterIdle = NULL; sShellIdleResolved = false; sFormRt = NULL; + sCodeDepth = 0; sLoadingFrm = false; sLastInputBoxCancelled = false; memset(&sDrag, 0, sizeof(sDrag)); @@ -2885,6 +3307,11 @@ void basFormRtTestReset(void) { #endif // DVX_HOST_TEST +WgtValueT basFormRtToWgt(BasValueT v, uint8_t want, BasStringT **hold) { + return wgtFromBas(v, want, hold); +} + + // TypeName / TypeOf of a host object: "Form" for a form, the control's // VB type name ("TextBox" ...) for a control, NULL when unknown. const char *basFormRtTypeName(void *ctx, void *objRef) { @@ -2894,6 +3321,10 @@ const char *basFormRtTypeName(void *ctx, void *objRef) { return BAS_CLIPBOARD_NAME; } + if (objRef == (void *)&sScreenObj) { + return BAS_SCREEN_NAME; + } + if (pictureFind(rt, objRef)) { return BAS_PICTURE_TYPE_NAME; } @@ -2904,7 +3335,7 @@ const char *basFormRtTypeName(void *ctx, void *objRef) { return NULL; } - if (obj == &obj->form->formCtrl) { + if (obj->form && obj == &obj->form->formCtrl) { return BAS_FORM_TYPE_NAME; } @@ -2950,6 +3381,47 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { } +bool basFormRtWantsNewArgs(BasFormRtT *rt) { + for (int32_t i = 0; rt && i < (int32_t)arrlen(rt->forms); i++) { + BasFormT *form = rt->forms[i]; + bool overridden; + + if (eventHandler(rt, &form->formCtrl, form->name, BAS_EVT_NEWARGS, &overridden)) { + return true; + } + } + + return false; +} + + +// A widget value as a BASIC value: INT / ENUM Long, BOOL Boolean, FLOAT +// Single (the BASIC type of WGT_IFACE_FLOAT), DOUBLE Double, STRING a new +// string. +static BasValueT basFromWgt(const WgtValueT *v) { + switch (v->type) { + case WGT_IFACE_STRING: + return basValStringFromC(v->s ? v->s : ""); + + case WGT_IFACE_INT: + case WGT_IFACE_ENUM: + return basValLong(v->i); + + case WGT_IFACE_BOOL: + return basValBool(v->i != 0); + + case WGT_IFACE_FLOAT: + return basValSingle((float)v->f); + + case WGT_IFACE_DOUBLE: + return basValDouble(v->f); + + default: + return zeroValue(); + } +} + + static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) { int32_t methodId = basFormRtCommonMethodId(methodName); @@ -2982,6 +3454,32 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char return zeroValue(); } + if (methodId == BAS_CM_MOVEBEFORE) { + // MoveBefore [sibling]: just before sibling, in its container + // (left out or Nothing: last in the control's own container) + void *before = (argc >= 1 && args[0].type == BAS_TYPE_OBJECT) ? args[0].objVal : NULL; + + basFormRtMoveControl(rt, ctrl, NULL, before); + return zeroValue(); + } + + if (methodId == BAS_CM_SCROLLTOCONTROL) { + // ScrollToControl child: a scrolling container (ScrollPane) + // brings a control inside it into view, focusable or not + BasControlT *child = (argc >= 1) ? resolveCtrlArg(rt, ctrl->form, args[0]) : NULL; + + if (!wclsHas(ctrl->widget, WGT_METHOD_SCROLL_CHILD_INTO_VIEW) || !child || !child->widget || !widgetHasAncestor(child->widget, ctrl->widget)) { + basFormRtRuntimeError(rt, + "ScrollToControl needs a control inside a ScrollPane", + "Control: %s\nScrollToControl is a method of a scrolling container (ScrollPane); its argument is a control in it (the control, or its name).", + ctrlDisplayName(ctrl)); + return zeroValue(); + } + + wclsScrollChildIntoView(ctrl->widget, child->widget); + return zeroValue(); + } + if (methodId == BAS_CM_DRAG) { // Drag [action]: begin (the default), cancel, or drop where the // mouse is. Cancel and end only act on this control's own drag. @@ -3063,45 +3561,50 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char BasStringT *s = basValFormatString(args[0]); if (!findPopupMenu(menuForm, s->data)) { - BasFrmPopupMenuT entry; - memset(&entry, 0, sizeof(entry)); - snprintf(entry.name, BAS_MAX_IDENT, "%s", s->data); - entry.menu = wmCreateMenu(); - entry.ownsMenu = true; // CreateMenu root - arrput(menuForm->popupMenus, entry); - menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus); + popupMenuAdd(menuForm, s->data, wmCreateMenu(), true); } basStringUnref(s); return zeroValue(); } - if (methodId == BAS_CM_ADDMENUITEM) { - // AddMenuItem parentMenu$, itemName$, caption$ - if (!menuForm || argc < 3) { + if (methodId == BAS_CM_ADDMENUITEM || methodId == BAS_CM_ADDMENURADIOITEM) { + // AddMenuItem parentMenu$, itemName$, caption$ [, shortcut$] / + // AddMenuRadioItem parentMenu$, itemName$, caption$ [, checked] + // -- the parent is a popup, a submenu or a menu of the menu bar + if (!menuForm || argc < MENU_ADD_ITEM_ARGS) { return zeroValue(); } - BasStringT *parent = basValFormatString(args[0]); - BasStringT *itemN = basValFormatString(args[1]); - BasStringT *capN = basValFormatString(args[2]); - - BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data); + BasStringT *parent = basValFormatString(args[0]); + BasStringT *itemN = basValFormatString(args[1]); + BasStringT *capN = basValFormatString(args[2]); + BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data); + bool extra = argc > MENU_ADD_ITEM_ARGS; if (pm && pm->menu) { int32_t id = nextMenuItemId(menuForm); - wmAddMenuItem(pm->menu, capN->data, id); - BasMenuIdMapT map; - memset(&map, 0, sizeof(map)); - map.id = id; - snprintf(map.name, BAS_MAX_IDENT, "%s", itemN->data); - arrput(menuForm->menuIdMap, map); - menuForm->menuIdMapCount = (int32_t)arrlen(menuForm->menuIdMap); + if (methodId == BAS_CM_ADDMENURADIOITEM) { + wmAddMenuRadioItem(pm->menu, capN->data, id, extra && basValIsTruthy(args[MENU_ADD_ITEM_ARGS])); + } else { + wmAddMenuItem(pm->menu, capN->data, id); + } + + MenuItemT *added = &pm->menu->items[pm->menu->itemCount - 1]; + + menuItemRegister(menuForm, added, id, itemN->data); if (menuForm->window) { menuForm->window->onMenu = onFormMenu; } + + if (methodId == BAS_CM_ADDMENUITEM && extra) { + BasStringT *key = basValFormatString(args[MENU_ADD_ITEM_ARGS]); + + menuItemShortcut(rt, menuForm, menuIdRow(menuForm, id), added, key->data); + basStringUnref(key); + } } basStringUnref(parent); @@ -3110,20 +3613,92 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char return zeroValue(); } + if (methodId == BAS_CM_ADDMENU) { + // AddMenu menu$, caption$ -- a menu at the end of the form's menu + // bar (made with the first one), named for AddMenuItem etc. + if (!menuForm || !menuForm->window || argc < MENU_ADD_MENU_ARGS) { + return zeroValue(); + } + + BasStringT *menuN = basValFormatString(args[0]); + BasStringT *capN = basValFormatString(args[1]); + WindowT *win = menuForm->window; + + if (findPopupMenu(menuForm, menuN->data)) { + basFormRtRuntimeError(rt, + "Menu name already used", + "Menu: %s\nA form's menus need names of their own.", + menuN->data); + } else { + MenuBarT *bar = win->menuBar ? win->menuBar : wmAddMenuBar(win); + MenuT *menu = bar ? wmAddMenu(bar, capN->data) : NULL; + + if (menu) { + popupMenuAdd(menuForm, menuN->data, menu, false); + win->onMenu = onFormMenu; + dvxResizeWindow(rt->ctx, win, win->w, win->h); + } + } + + basStringUnref(menuN); + basStringUnref(capN); + return zeroValue(); + } + + if (methodId == BAS_CM_REMOVEMENU) { + // RemoveMenu menu$ -- a menu of the menu bar with everything in + // it; the bar goes with its last menu + if (!menuForm || !menuForm->window || argc < 1) { + return zeroValue(); + } + + BasStringT *menuN = basValFormatString(args[0]); + BasFrmPopupMenuT *pm = findPopupMenu(menuForm, menuN->data); + MenuT *menu = pm ? pm->menu : NULL; + WindowT *win = menuForm->window; + bool onBar = false; + + for (int32_t i = 0; menu && win->menuBar && i < win->menuBar->menuCount; i++) { + onBar = onBar || win->menuBar->menus[i] == menu; + } + + if (!onBar) { + basFormRtRuntimeError(rt, + "Not a menu of the menu bar", + "Menu: %s\nRemoveMenu removes a menu of the form's menu bar (AddMenu or the form's menus); use DestroyMenu for a popup menu and RemoveMenuItem for an item.", + menuN->data); + } else { + menuTreeForget(rt, menuForm, menu); + wmRemoveMenu(win, menu, &rt->ctx->display); + dvxResizeWindow(rt->ctx, win, win->w, win->h); + } + + basStringUnref(menuN); + return zeroValue(); + } + if (methodId == BAS_CM_ADDMENUSEPARATOR) { - // AddMenuSeparator parentMenu$ + // AddMenuSeparator parentMenu$ [, name$] -- a named separator can + // be hidden and removed like an item if (!menuForm || argc < 1) { return zeroValue(); } BasStringT *parent = basValFormatString(args[0]); + BasStringT *sepN = (argc >= 2) ? basValFormatString(args[1]) : NULL; BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data); if (pm && pm->menu) { wmAddMenuSeparator(pm->menu); + menuItemRegister(menuForm, &pm->menu->items[pm->menu->itemCount - 1], nextMenuItemId(menuForm), sepN ? sepN->data : NULL); } basStringUnref(parent); + + if (sepN) { + basStringUnref(sepN); + } + return zeroValue(); } @@ -3145,17 +3720,11 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char MenuT *sub = wmAddSubMenu(parentPm->menu, capN->data); if (sub) { - BasFrmPopupMenuT entry; - memset(&entry, 0, sizeof(entry)); - snprintf(entry.name, BAS_MAX_IDENT, "%s", childN->data); - // NB: sub is owned by parentPm->menu (wmFreeMenu - // recurses); we store a non-owning reference here so - // AddMenuItem etc. can look it up by name. Don't - // wmFreeMenu the child on DestroyMenu for this - // name; only the root popup owns the tree. - entry.menu = sub; - arrput(menuForm->popupMenus, entry); - menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus); + // sub is owned by parentPm->menu (wmFreeMenu recurses): + // the name is a non-owning reference for AddMenuItem etc., + // and names the submenu's item for Caption / Visible. + popupMenuAdd(menuForm, childN->data, sub, false); + menuItemRegister(menuForm, &parentPm->menu->items[parentPm->menu->itemCount - 1], nextMenuItemId(menuForm), childN->data); } } @@ -3165,6 +3734,47 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char return zeroValue(); } + if (methodId == BAS_CM_REMOVEMENUITEM) { + // RemoveMenuItem itemName$ -- an item or named separator of any + // menu of the form, shown or hidden (not a submenu) + if (!menuForm || argc < 1) { + return zeroValue(); + } + + BasStringT *itemN = basValFormatString(args[0]); + int32_t rowIdx = -1; + + for (int32_t i = 0; i < menuForm->menuIdMapCount; i++) { + if (strcasecmp(menuForm->menuIdMap[i].name, itemN->data) == 0) { + rowIdx = i; + break; + } + } + + MenuT *menu = NULL; + int32_t idx = -1; + + if (rowIdx >= 0 && !menuForm->menuIdMap[rowIdx].hiddenIn) { + menuItemLocate(menuForm, menuForm->menuIdMap[rowIdx].id, &menu, &idx, NULL); + } + + if (menu && menu->items[idx].subMenu) { + basFormRtRuntimeError(rt, + "A submenu cannot be removed", + "Menu: %s\nRemove the items in it, or set Enabled = False.", + itemN->data); + } else if (rowIdx >= 0) { + if (menu) { + menuItemTake(menu, idx); + } + + menuIdRowDrop(rt, menuForm, rowIdx); + } + + basStringUnref(itemN); + return zeroValue(); + } + // ======================================================== // Toolbar runtime methods // ======================================================== @@ -3304,52 +3914,9 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char MenuT *m = menuForm->popupMenus[foundIdx].menu; if (menuForm->popupMenus[foundIdx].ownsMenu) { - // Freeing an owning root: first null every widget/window - // contextMenu that points anywhere into this tree, then drop - // ALL popupMenus entries (root + non-owning child aliases) - // that reference it, then free the tree once. Otherwise those - // aliases and contextMenu pointers would dangle. - for (int32_t i = 0; i < (int32_t)arrlen(menuForm->controls); i++) { - WidgetT *cw = menuForm->controls[i]->widget; - - if (cw && menuContainsMenu(m, cw->contextMenu)) { - cw->contextMenu = NULL; - } - } - - if (menuForm->window && menuContainsMenu(m, menuForm->window->contextMenu)) { - menuForm->window->contextMenu = NULL; - } - - for (int32_t i = menuForm->popupMenuCount - 1; i >= 0; i--) { - if (menuContainsMenu(m, menuForm->popupMenus[i].menu)) { - arrdel(menuForm->popupMenus, i); - } - } - - // Drop the id->name rows (and their proxies) of every item - // in the tree, so the names stop resolving and repeated - // create/destroy cycles do not grow menuIdMap. A proxy - // may be what the running handler was fired for, so its - // free is deferred like any removed control. - for (int32_t i = menuForm->menuIdMapCount - 1; i >= 0; i--) { - if (!wmMenuFindItemInMenu(m, menuForm->menuIdMap[i].id)) { - continue; - } - - BasControlT *proxy = menuForm->menuIdMap[i].proxy; - - if (proxy && rt->eventDepth > 0) { - arrput(rt->pendingCtrlFree, proxy); - } else { - freeControl(proxy); - } - - arrdel(menuForm->menuIdMap, i); - rt->refCacheKey = NULL; - } - - menuForm->menuIdMapCount = (int32_t)arrlen(menuForm->menuIdMap); + // Freeing an owning root: forget every reference into the + // tree, then free it once + menuTreeForget(rt, menuForm, m); wmFreeMenu(m); } else { // Non-owning alias: drop just this entry; the root owns the @@ -3357,9 +3924,9 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char arrdel(menuForm->popupMenus, foundIdx); } - menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus); } + menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus); basStringUnref(s); return zeroValue(); } @@ -3400,6 +3967,44 @@ static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *p } +// An interface method call: the arguments converted with the BASIC rules +// to the signature's types (wgtSigInfo; a WGT_SIG_DYNAMIC method gets +// each argument as its own kind), the call through libdvx, the result +// back as a BASIC value. Missing arguments make no call and give the +// result type's zero; arguments past the signature's are ignored. +static BasValueT callIfaceMethod(BasFormRtT *rt, BasControlT *ctrl, const WgtMethodDescT *m, BasValueT *args, int32_t argc) { + const WgtSigInfoT *info = wgtSigInfo(m->sig); + int32_t n = info ? DVX_MIN(argc, (int32_t)info->argc) : argc; + WgtValueT vals[BAS_WGT_ARGS_MAX]; + BasStringT *holds[BAS_WGT_ARGS_MAX]; + WgtValueT ret; + + if (n > BAS_WGT_ARGS_MAX) { + basFormRtRuntimeError(rt, + "Too many arguments", + "Method: %s\nControl: %s\nA control method takes at most %d arguments.", + m->name, ctrlDisplayName(ctrl), BAS_WGT_ARGS_MAX); + return zeroValue(); + } + + for (int32_t i = 0; i < n; i++) { + vals[i] = wgtFromBas(args[i], info ? info->argTypes[i] : WGT_IFACE_NONE, &holds[i]); + } + + wgtIfaceCallMethod(ctrl->iface, ctrl->widget, m, vals, n, &ret); + + BasValueT result = basFromWgt(&ret); + + for (int32_t i = 0; i < n; i++) { + if (holds[i]) { + basStringUnref(holds[i]); + } + } + + return result; +} + + // The Clipboard object's methods (VB semantics; the DVX clipboard holds // text, vbCFText): Clear, GetFormat(format), GetText([format]) and // SetText text [, format]. Other formats are never present, read as "", @@ -3530,6 +4135,19 @@ void CommDetach(int32_t handle) { } +// Frames dropped since the link opened because they failed the tamper +// check or arrived unencrypted where that is refused; -1 for a bad handle. +int32_t CommDropped(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + if (!slot || !sCommApi.getDropped) { + return -1; + } + + return sCommApi.getDropped(slot->link); +} + + static CommSlotT *commGetSlot(int32_t handle) { if (handle < 1 || handle > (int32_t)arrlen(sCommSlots)) { return NULL; @@ -3551,11 +4169,35 @@ int32_t CommHandshake(int32_t handle, const char *passphrase) { return 0; } - if (sCommApi.setPassphrase(slot->link, passphrase) != 0) { + slot->stepping = false; + + if (commNote(slot, sCommApi.setPassphrase(slot->link, passphrase)) != SECLINK_SUCCESS) { return 0; } - return (sCommApi.handshake(slot->link) == 0) ? -1 : 0; + return (commNote(slot, sCommApi.handshake(slot->link)) == SECLINK_SUCCESS) ? -1 : 0; +} + + +// One step of a key exchange that does not wait (call it from a Timer +// until it is no longer COMM_PENDING): the passphrase is taken when an +// exchange starts. COMM_OK once the link is ready, COMM_PENDING while +// the exchange runs, else the COMM_ERR_ code that ended it. +int32_t CommHandshakeStep(int32_t handle, const char *passphrase) { + CommSlotT *slot = commGetSlot(handle); + int32_t rc; + + if (!slot || !sCommApi.handshakeStep || !sCommApi.setPassphrase) { + return SECLINK_ERR_PARAM; + } + + if (!slot->stepping && commNote(slot, sCommApi.setPassphrase(slot->link, passphrase)) != SECLINK_SUCCESS) { + return slot->lastError; + } + + rc = commNote(slot, sCommApi.handshakeStep(slot->link)); + slot->stepping = (rc == SECLINK_PENDING); + return rc; } @@ -3586,6 +4228,24 @@ int32_t CommIsReady(int32_t handle) { } +// The COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, +// CommSetPlain or CommPoll on the link (COMM_OK when it worked); +// COMM_ERR_PARAM for a bad handle. +int32_t CommLastError(int32_t handle) { + CommSlotT *slot = commGetSlot(handle); + + return slot ? slot->lastError : SECLINK_ERR_PARAM; +} + + +// Remember a secLink result (a count above 0 counts as success) and +// return it. +static int32_t commNote(CommSlotT *slot, int32_t rc) { + slot->lastError = (rc < 0 || rc == SECLINK_PENDING) ? rc : SECLINK_SUCCESS; + return rc; +} + + // SecLink receive callback: routes data into per-channel ring buffers, // allocating a channel's buffer on its first data (dropped on OOM). // secLink only delivers clear frames the channel's plaintext setting @@ -3690,23 +4350,25 @@ int32_t CommPoll(int32_t handle) { return 0; } - return sCommApi.poll(slot->link); + return commNote(slot, sCommApi.poll(slot->link)); } -const char *CommRecv(int32_t handle, int32_t channel) { - CommSlotT *slot = commGetSlot(handle); +// What arrived on channel (a BINARY STRING: every byte value), "" when +// nothing has. +const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen) { + static uint8_t buf[SECLINK_CHAN_BUF_SIZE]; + CommSlotT *slot = commGetSlot(handle); if (!slot || channel < 0 || channel >= SECLINK_NUM_CHANNELS) { - return ""; + return buf; } - CommChanBufT *cb = slot->channels[channel]; - static char buf[SECLINK_CHAN_BUF_SIZE]; + CommChanBufT *cb = slot->channels[channel]; int32_t count = 0; bool clear = false; - while (cb && count < (int32_t)sizeof(buf) - 1 && cb->tail != cb->head) { + while (cb && count < (int32_t)sizeof(buf) && cb->tail != cb->head) { clear = clear || (cb->clear[cb->tail / COMM_BITS_PER_BYTE] & (1u << (cb->tail % COMM_BITS_PER_BYTE))); buf[count++] = cb->data[cb->tail]; cb->tail = (cb->tail + 1) % SECLINK_CHAN_BUF_SIZE; @@ -3716,7 +4378,7 @@ const char *CommRecv(int32_t handle, int32_t channel) { cb->lastRecvClear = clear; } - buf[count] = '\0'; + *outLen = count; return buf; } @@ -3755,25 +4417,26 @@ static bool commResolveApi(void) { sCommApi.setPassphrase = dlsym(NULL, DVX_SYM("secLinkSetPassphrase")); sCommApi.setChannelPlain = dlsym(NULL, DVX_SYM("secLinkSetChannelPlain")); + sCommApi.handshakeStep = dlsym(NULL, DVX_SYM("secLinkHandshakeStep")); + sCommApi.getDropped = dlsym(NULL, DVX_SYM("secLinkGetDropped")); return sCommApi.open != NULL; } -int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt) { +// Send data (a BINARY STRING: every byte value) on channel. +int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt) { CommSlotT *slot = commGetSlot(handle); if (!slot || !data || !sCommApi.sendBuf || channel < 0 || channel >= SECLINK_NUM_CHANNELS) { return 0; } - int32_t len = (int32_t)strlen(data); - if (len == 0) { return -1; } - int32_t rc = sCommApi.sendBuf(slot->link, (const uint8_t *)data, len, (uint8_t)channel, encrypt != 0); + int32_t rc = commNote(slot, sCommApi.sendBuf(slot->link, data, len, (uint8_t)channel, encrypt != 0)); return (rc == 0) ? -1 : 0; } @@ -3788,7 +4451,7 @@ int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain) { return 0; } - return (sCommApi.setChannelPlain(slot->link, (uint8_t)channel, plain != 0) == 0) ? -1 : 0; + return (commNote(slot, sCommApi.setChannelPlain(slot->link, (uint8_t)channel, plain != 0)) == 0) ? -1 : 0; } @@ -3820,77 +4483,33 @@ static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len) { } -// Create a DVX widget by type name using its registered API. -// The API's create function signature varies by widget type, so -// we call with sensible defaults for each creation pattern. -WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { - const void *api = wgtGetApi(wgtTypeName, sizeof(WgtApiPrefixT)); +// A form's Padding / Spacing: the property of its content box (the box +// its Layout names), read, or set when value is not NULL. +static BasValueT contentBoxProp(BasFormRtT *rt, BasFormT *form, const char *propName, const BasValueT *value) { + const char *wgtName = wgtFindByBasName(form->frmLayout[0] ? form->frmLayout : BAS_LAYOUT_VBOX); + const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; + bool handled = false; - if (!api) { - return NULL; - } + if (iface && form->contentBox) { + if (value) { + handled = setIfaceProp(rt, iface, form->contentBox, propName, *value); + } else { + BasValueT result = getIfaceProp(iface, form->contentBox, propName, &handled); - const WgtIfaceT *iface = wgtGetIface(wgtTypeName); - return createWidgetByIface(iface, api, parent, true); -} - - -WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData) { - if (!api) { - return NULL; - } - - uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT; - WgtApiFnT create = ((const WgtApiPrefixT *)api)->create; - - typedef WidgetT *(*CreateParentFnT)(WidgetT *); - typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *); - typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t); - typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t); - typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t); - typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); - typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool); - - switch (sig) { - case WGT_CREATE_PARENT_TEXT: { - CreateParentTextFnT fn = (CreateParentTextFnT)create; - return fn(parent, ""); - } - case WGT_CREATE_PARENT_INT: { - CreateParentIntFnT fn = (CreateParentIntFnT)create; - return fn(parent, iface->createArgs[0]); - } - case WGT_CREATE_PARENT_INT_INT: { - CreateParentIntIntFnT fn = (CreateParentIntIntFnT)create; - return fn(parent, iface->createArgs[0], iface->createArgs[1]); - } - case WGT_CREATE_PARENT_INT_INT_INT: { - CreateParentIntIntIntFnT fn = (CreateParentIntIntIntFnT)create; - return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]); - } - case WGT_CREATE_PARENT_INT_BOOL: { - CreateParentIntBoolFnT fn = (CreateParentIntBoolFnT)create; - return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]); - } - case WGT_CREATE_PARENT_BOOL: { - CreateParentBoolFnT fn = (CreateParentBoolFnT)create; - return fn(parent, (bool)iface->createArgs[0]); - } - case WGT_CREATE_PARENT_DATA: { - if (!allowData) { - // Design-time: can't auto-create Image/ImageButton without pixel data - return NULL; + if (handled) { + return result; } - // create(parent, NULL, 0, 0, 0) -- empty widget, load content later via properties - typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t); - CreateDataFnT fn = (CreateDataFnT)create; - return fn(parent, NULL, 0, 0, 0); - } - default: { - CreateParentFnT fn = (CreateParentFnT)create; - return fn(parent); } } + + if (!handled) { + basFormRtRuntimeError(rt, + "Form property not available", + "Form: %s\nProperty: %s\nThe form's Layout (%s) has no %s.", + form->name, propName, form->frmLayout, propName); + } + + return zeroValue(); } @@ -3957,6 +4576,7 @@ static void destroyFormNow(BasFormRtT *rt, BasFormT *form) { } basFormRtDrawFree(&form->formCtrl); + free(form->formCtrl.tag); free(form); } @@ -4094,42 +4714,54 @@ static int32_t deviceStrig(void *ctx, int32_t n) { } +// A message box with the program's own buttons ("&Yes|&No|Yes to &All| +// &Cancel"): the index of the one chosen (Enter the first, Escape and +// closing the box the last), or -1 for no buttons or more than +// DVX_BUTTON_BOX_MAX. icon is a MsgBox icon value (vbQuestion ...) or 0. +int32_t DlgButtons(const char *title, const char *message, const char *buttons, int32_t icon) { + if (!sFormRt || !buttons) { + return -1; + } + + char buf[BAS_CHOICE_BUF_LEN]; + const char *labels[DVX_BUTTON_BOX_MAX + 1]; + + snprintf(buf, sizeof(buf), "%s", buttons); + + int32_t count = splitPipeList(buf, labels, DVX_BUTTON_BOX_MAX + 1); + + if (count < 1 || count > DVX_BUTTON_BOX_MAX) { + return -1; + } + + return dvxButtonBox(sFormRt->ctx, title, message ? message : "", icon & MB_ICON_MASK, labels, count); +} + + int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx) { if (!sFormRt || !items) { return -1; } - // Split pipe-delimited items string into an array - static char itemBuf[BAS_CHOICE_BUF_LEN]; - snprintf(itemBuf, sizeof(itemBuf), "%s", items); + // Split the pipe-delimited items into a list as long as they need + int32_t maxItems = 1; - const char *ptrs[BAS_CHOICE_MAX_ITEMS]; - int32_t count = 0; - char *tok = itemBuf; - - while (*tok && count < BAS_CHOICE_MAX_ITEMS) { - ptrs[count++] = tok; - char *sep = strchr(tok, '|'); - - if (sep) { - *sep = '\0'; - tok = sep + 1; - } else { - break; - } + for (const char *p = items; *p; p++) { + maxItems += (*p == '|'); } - if (count == 0) { - return -1; + char *buf = strdup(items); + const char **ptrs = (const char **)malloc(sizeof(const char *) * maxItems); + int32_t count = (buf && ptrs) ? splitPipeList(buf, ptrs, maxItems) : 0; + int32_t chosen = -1; + + if (count > 0 && !dvxChoiceDialog(sFormRt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) { + chosen = -1; } - int32_t chosen = 0; - - if (dvxChoiceDialog(sFormRt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) { - return chosen; - } - - return -1; + free(buf); + free(ptrs); + return chosen; } @@ -4147,50 +4779,24 @@ int32_t DlgColor(const char *title, int32_t defaultColor) { const char *DlgFileOpen(const char *title, const char *filter) { - if (!sFormRt) { - return ""; - } + return fileDialog(title, filter, FD_OPEN, NULL); +} - FileFilterT *filters = NULL; - char filterBuf[BAS_FILTER_BUF_LEN]; - int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf)); - static char path[DVX_MAX_PATH]; - path[0] = '\0'; - - bool ok = dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, count, path, sizeof(path)); - - free(filters); - - if (ok) { - return path; - } - - return ""; +// DlgFileOpen starting in folder ("" = the current folder). +const char *DlgFileOpenIn(const char *title, const char *filter, const char *folder) { + return fileDialog(title, filter, FD_OPEN, folder); } const char *DlgFileSave(const char *title, const char *filter) { - if (!sFormRt) { - return ""; - } + return fileDialog(title, filter, FD_SAVE, NULL); +} - FileFilterT *filters = NULL; - char filterBuf[BAS_FILTER_BUF_LEN]; - int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf)); - static char path[DVX_MAX_PATH]; - path[0] = '\0'; - - bool ok = dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, count, path, sizeof(path)); - - free(filters); - - if (ok) { - return path; - } - - return ""; +// DlgFileSave starting in folder ("" = the current folder). +const char *DlgFileSaveIn(const char *title, const char *filter, const char *folder) { + return fileDialog(title, filter, FD_SAVE, folder); } @@ -4219,13 +4825,17 @@ int32_t DlgInputCancelled(void) { int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal) { - if (!sFormRt) { - return defaultVal; - } + return DlgIntInputStep(title, prompt, defaultVal, minVal, maxVal, DLG_INT_STEP_DEFAULT); +} + +// The integer input box with the spinner moving by stepVal; the default +// value when cancelled (DlgInputCancelled tells). +int32_t DlgIntInputStep(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t stepVal) { int32_t result = defaultVal; - dvxIntInputBox(sFormRt->ctx, title, prompt, defaultVal, minVal, maxVal, 1, &result); - return result; + + sLastInputBoxCancelled = !sFormRt || !dvxIntInputBox(sFormRt->ctx, title, prompt, defaultVal, minVal, maxVal, stepVal, &result); + return sLastInputBoxCancelled ? defaultVal : result; } @@ -4243,7 +4853,7 @@ int32_t DlgPromptSave(const char *title) { static bool dragAccepts(BasFormRtT *rt, const BasControlT *obj) { bool overridden; - return obj && eventHandler(rt, obj, (obj == &obj->form->formCtrl) ? obj->form->name : obj->name, BAS_EVT_DRAGDROP, &overridden) != NULL; + return obj && obj->form && eventHandler(rt, obj, (obj == &obj->form->formCtrl) ? obj->form->name : obj->name, BAS_EVT_DRAGDROP, &overridden) != NULL; } @@ -4496,7 +5106,7 @@ static void dragOverUpdate(void) { // cannot be read (the outline is shown instead). static bool dragShowIcon(AppContextT *ctx, const char *path) { int32_t len = 0; - char *data = platformReadFile(path, &len); + char *data = pictureFileRead(path, &len); int32_t w = 0; int32_t h = 0; int32_t pitch = 0; @@ -4572,6 +5182,69 @@ static const BasProcEntryT *eventHandler(BasFormRtT *rt, const BasControlT *obj, } +// The file dialogs of commdlg.bas: flags FD_OPEN or FD_SAVE, starting in +// folder (NULL or "" = the current folder). The chosen path, or "" when +// cancelled. +static const char *fileDialog(const char *title, const char *filter, int32_t flags, const char *folder) { + static char path[DVX_MAX_PATH]; + FileFilterT *filters = NULL; + char filterBuf[BAS_FILTER_BUF_LEN]; + + path[0] = '\0'; + + if (!sFormRt) { + return path; + } + + int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf)); + bool ok = dvxFileDialog(sFormRt->ctx, title, flags, (folder && folder[0]) ? folder : NULL, filters, count, path, sizeof(path)); + + free(filters); + + if (!ok) { + path[0] = '\0'; + } + + return path; +} + + +// The first visible, enabled control on form whose Boolean property +// propName (a CommandButton's Default or Cancel) is True, or NULL. +static BasControlT *findButtonWith(BasFormT *form, const char *propName) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + BasControlT *ctrl = form->controls[i]; + const WgtPropDescT *p = wgtIfaceFindProp(ctrl->iface, propName); + WgtValueT v; + + if (p && p->type == WGT_IFACE_BOOL && wgtIfaceGetProp(ctrl->iface, ctrl->widget, p, &v) && v.i && widgetIsShown(ctrl->widget) && widgetIsEnabledChain(ctrl->widget)) { + return ctrl; + } + } + + return NULL; +} + + +// The running program's control called name, on any of its loaded forms +// (the first one found), or NULL. +static BasControlT *findCtrlByName(const char *name) { + if (!sFormRt || !name) { + return NULL; + } + + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { + BasControlT *hit = findCtrlInForm(sFormRt->forms[i], name); + + if (hit) { + return hit; + } + } + + return NULL; +} + + // Control whose live widget is exactly w, or NULL. static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w) { for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { @@ -4603,19 +5276,9 @@ static BasControlT *findCtrlInForm(BasFormT *form, const char *name) { // Finds a control's live widget by name across every loaded form. Shared by // CommAttach/SerAttach to locate the terminal widget to bind a serial channel. static WidgetT *findCtrlWidgetByName(const char *name) { - if (!sFormRt || !name) { - return NULL; - } + BasControlT *ctrl = findCtrlByName(name); - for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { - BasControlT *hit = findCtrlInForm(sFormRt->forms[i], name); - - if (hit) { - return hit->widget; - } - } - - return NULL; + return ctrl ? ctrl->widget : NULL; } @@ -4721,6 +5384,11 @@ static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *even return; } + // A form in design mode: its controls are being designed, not used + if (ctrl->form && basFormRtDesignBlocks(ctrl->form)) { + return; + } + // Re-entrancy guard: reject only same-event re-entry (runSubLoop // pumps events during long handlers; without this, a lingering // mouse-up can re-deliver Click on top of itself). But allow @@ -4928,6 +5596,68 @@ static BasControlT *formEventObject(BasFormT *form, const char *ctrlName) { } +// The form's own methods that reach the runtime as method calls: +// FitWidth / FitHeight (shrink-wrap one dimension to the controls, as +// AutoSize does both) and SaveScreenshot path$ (the window's picture to +// a file). False for any other name. +static bool formMethod(BasFormRtT *rt, BasFormT *frm, const char *name, BasValueT *args, int32_t argc) { + WindowT *win = frm->window; + + if (!name) { + return false; + } + + if (strcasecmp(name, BAS_FORM_METHOD_FITWIDTH) == 0) { + if (win) { + dvxFitWindowW(rt->ctx, win); + } + + return true; + } + + if (strcasecmp(name, BAS_FORM_METHOD_FITHEIGHT) == 0) { + if (win) { + dvxFitWindowH(rt->ctx, win); + } + + return true; + } + + if (strcasecmp(name, BAS_FORM_METHOD_SAVESCREENSHOT) != 0) { + return false; + } + + BasStringT *path = argc >= 1 ? basValFormatString(args[0]) : basStringNew("", 0); + + if (!win || path->len == 0 || dvxWindowScreenshot(rt->ctx, win, path->data) != 0) { + basFormRtRuntimeError(rt, + "Screenshot not saved", + "Form: %s\nFile: %s\nSaveScreenshot needs a form with a window and a file it can write (BMP when the name ends in .bmp, PNG otherwise).", + frm->name, path->data); + } + + basStringUnref(path); + return true; +} + + +// The live form a window belongs to (its userData, set by allocForm and +// cleared at teardown) and that form's runtime. Window callbacks use it: +// several BASIC programs run at once, each with its own runtime, so the +// window -- not the last runtime created -- says whose event it is. A +// form committed to unload gets no more events (NULL). +static BasFormT *formOfWindow(const WindowT *win, BasFormRtT **outRt) { + BasFormT *form = win ? (BasFormT *)win->userData : NULL; + + if (!form || form->detached || !form->vm || !form->vm->ui.ctx) { + return NULL; + } + + *outRt = (BasFormRtT *)form->vm->ui.ctx; + return form; +} + + // Register or unregister the secLink idle poller with the shell based on // whether any connection is still open. Idempotent on both sides, so this // is safe to call from every CommAttach and CommClose. commIdlePoll keeps @@ -4955,11 +5685,11 @@ static void formRtCommIdleSync(void) { } -// Detach any raw-serial or secLink terminal binding whose AnsiTerm widget -// lives on this form. Called from basFormRtTeardownForm before the form's -// controls array is freed and its widgets are destroyed; without this, -// serIdlePoll and the later Comm/Ser close paths would dereference the -// freed widget. The port/connection itself stays open -- only the +// Detach any raw-serial, secLink or library terminal binding whose +// AnsiTerm widget lives on this form. Called from basFormRtTeardownForm +// before the form's controls array is freed and its widgets are +// destroyed; without this, serIdlePoll, the later Comm/Ser close paths +// and the libraries would dereference the freed widget. The port/connection itself stays open -- only the // terminal binding is dropped, mirroring SerDetach/CommDetach. static void formRtDetachTermsForForm(BasFormT *form) { bool serDetached = false; @@ -4985,8 +5715,9 @@ static void formRtDetachTermsForForm(BasFormT *form) { } -// Detach any raw-serial or secLink terminal binding on a single widget. -// Returns true if a raw-serial binding was dropped -- the caller then +// Detach any raw-serial, secLink or library (basFormRtTermBind) terminal +// binding on a single widget. Returns true if a raw-serial binding was +// dropped -- the caller then // syncs the idle poller via formRtSerIdleSync. Shared by the per-form // teardown loop above and basFormRtRemoveCtrl's immediate widget destroy. static bool formRtDetachTermsForWidget(WidgetT *widget) { @@ -5008,6 +5739,18 @@ static bool formRtDetachTermsForWidget(WidgetT *widget) { } } + // Libraries' bindings (basFormRtTermBind): the library is told last, + // once the binding is gone, so it may bind again at once. + for (int32_t j = (int32_t)arrlen(sTermBinds) - 1; j >= 0; j--) { + if (sTermBinds[j].term == widget) { + TermBindT bind = sTermBinds[j]; + + wgtAnsiTermSetComm(widget, NULL, NULL, NULL); + arrdel(sTermBinds, j); + bind.onUnbind(bind.ctx); + } + } + return serDetached; } @@ -5038,6 +5781,14 @@ static void formRtSerIdleSync(void) { } +// game.bas FrameMissed(): the frames FRAMEWAIT skipped (they had already +// ended when it was called) since the program started waiting at its +// current rate (FRAMEWAIT's own pacer, GameFramePacerT.missed). +int32_t FrameMissed(void) { + return (sFormRt && sFormRt->frameHz > 0) ? (int32_t)sFormRt->framePacer.missed : 0; +} + + // Free a control and its heap-owned tooltip. NULL-safe: also used for // menuIdMap proxies, which are calloc'd with no tooltip (free(NULL) is // a no-op), so this is the single owner of control teardown. @@ -5048,6 +5799,7 @@ static void freeControl(BasControlT *ctrl) { free(ctrl->tooltip); free(ctrl->dragIcon); + free(ctrl->tag); basFormRtDrawFree(ctrl); free(ctrl); } @@ -5073,7 +5825,7 @@ static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char ctx->parentStack[0] = ctx->form->contentBox; } - const char *wgtTypeName = resolveTypeName(typeName); + const char *wgtTypeName = basUcResolveType(ctx->rt, typeName); bool isCtrlContainer = false; if (!wgtTypeName) { @@ -5093,7 +5845,7 @@ static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char } WidgetT *parent = ctx->parentStack[ctx->nestDepth - 1]; - WidgetT *widget = createWidget(wgtTypeName, parent); + WidgetT *widget = wgtCreateByName(wgtTypeName, parent); if (widget) { const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); @@ -5192,7 +5944,7 @@ static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value const WgtPropDescT *ip = ctrl->iface ? wgtIfaceFindProp(ctrl->iface, key) : NULL; if (ip) { - if (ip->setFn && !wgtApplyPropFromString(ctrl->widget, ip, scratch)) { + if (ip->setFn && !wgtIfaceSetPropText(ctrl->iface, ctrl->widget, ip, scratch)) { basFormRtRuntimeError(ctx->rt, "Bad property value in form", "Form: %s\nControl: %s\nProperty: %s\nValue: %s\nNot a valid value for this property.", @@ -5310,10 +6062,36 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value frmLoadQueueContextMenu(ctx, NULL, text); break; + case FORM_PROP_PADDING: + frm->frmPadding = num; + break; + + case FORM_PROP_SPACING: + frm->frmSpacing = num; + break; + + case FORM_PROP_WINDOWSTATE: + frm->frmWindowState = num; + break; + + case FORM_PROP_ICON: + case FORM_PROP_TAG: + case FORM_PROP_MOUSEPOINTER: { + BasValueT v = (pd->type == WGT_IFACE_INT) ? basValLong(num) : basValStringFromC(text); + + setFormProp(ctx->rt, frm, pd->name, v); + basValRelease(&v); + break; + } + case FORM_PROP_NAME: case FORM_PROP_VISIBLE: + case FORM_PROP_CONTROLS: + case FORM_PROP_DESIGNMODE: + case FORM_PROP_DESIGNSELECTION: + case FORM_PROP_ACTIVECONTROL: case FORM_PROP_COUNT: - // Name comes from the Begin line; Visible is runtime-only. + // Name comes from the Begin line; the others are runtime-only. break; } } @@ -5334,7 +6112,6 @@ static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level) mi.visible = true; arrput(ctx->menuItems, mi); ctx->curMenuItemIdx = (int32_t)arrlen(ctx->menuItems) - 1; - ctx->current = NULL; if (ctx->menuIdxDepth < FRM_MAX_NESTING) { ctx->menuIdxStack[ctx->menuIdxDepth++] = ctx->curMenuItemIdx; @@ -5371,15 +6148,20 @@ static void frmLoadOnMenuProp(void *userData, const char *key, const char *value if (strcasecmp(key, BAS_PROP_CAPTION) == 0) { snprintf(mip->caption, sizeof(mip->caption), "%s", text); } else if (strcasecmp(key, BAS_PROP_CHECKED) == 0) { - mip->checked = frmParseBool(text); + mip->checked = frmParseBool(text); + mip->checkable = true; } else if (strcasecmp(key, BAS_PROP_RADIOCHECK) == 0) { mip->radioCheck = frmParseBool(text); + } else if (strcasecmp(key, BAS_PROP_SHORTCUT) == 0) { + snprintf(mip->shortcut, sizeof(mip->shortcut), "%s", text); + } else if (strcasecmp(key, BAS_PROP_SHORTCUTTEXT) == 0) { + snprintf(mip->shortcutText, sizeof(mip->shortcutText), "%s", text); } else if (strcasecmp(key, BAS_PROP_ENABLED) == 0) { // Default-true: an unrecognized value leaves the item enabled. mip->enabled = frmParseBoolDefault(text, true); } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0) { - // Top-level only: Visible=False marks the menu as a popup - // (not part of the menu bar). Ignored on nested items. + // Visible=False on a top-level menu makes it a popup (not part + // of the menu bar); a nested item starts hidden. mip->visible = frmParseBoolDefault(text, true); } } @@ -5435,16 +6217,16 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha return basValLong(w->h); case CTRL_PROP_MINWIDTH: - return basValLong(w->minW & WGT_SIZE_VAL_MASK); + return basValLong(sizePixels(w, w->minW, true)); case CTRL_PROP_MINHEIGHT: - return basValLong(w->minH & WGT_SIZE_VAL_MASK); + return basValLong(sizePixels(w, w->minH, false)); case CTRL_PROP_MAXWIDTH: - return basValLong(w->maxW ? (w->maxW & WGT_SIZE_VAL_MASK) : 0); + return basValLong(sizePixels(w, w->maxW, true)); case CTRL_PROP_MAXHEIGHT: - return basValLong(w->maxH ? (w->maxH & WGT_SIZE_VAL_MASK) : 0); + return basValLong(sizePixels(w, w->maxH, false)); case CTRL_PROP_WEIGHT: return basValLong(w->weight); @@ -5489,7 +6271,9 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha case CTRL_PROP_LISTCOUNT: { // Any widget with item storage exposes a ListCount method. const WgtMethodDescT *m = ifaceFindMethod(ctrl->iface, BAS_PROP_LISTCOUNT, WGT_SIG_RET_INT); - return basValLong(m ? ((int32_t (*)(const WidgetT *))m->fn)(w) : 0); + WgtValueT count; + + return basValLong((m && wgtIfaceCallMethod(ctrl->iface, w, m, NULL, 0, &count)) ? count.i : 0); } case CTRL_PROP_DRAGMODE: @@ -5498,6 +6282,16 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha case CTRL_PROP_DRAGICON: return basValStringFromC(ctrl->dragIcon ? ctrl->dragIcon : ""); + case CTRL_PROP_PARENT: + case CTRL_PROP_CONTAINER: + return basFormRtDesignCtrlProp(sFormRt, ctrl, pd->name); + + case CTRL_PROP_TAG: + return basValStringFromC(ctrl->tag ? ctrl->tag : ""); + + case CTRL_PROP_MOUSEPOINTER: + return basValLong(ctrl->mousePointer); + case CTRL_PROP_COUNT: break; } @@ -5567,6 +6361,31 @@ static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName case FORM_PROP_HELPTOPIC: return basValStringFromC(frm->helpTopic); + case FORM_PROP_PADDING: + case FORM_PROP_SPACING: + return contentBoxProp(rt, frm, pd->name, NULL); + + case FORM_PROP_WINDOWSTATE: + return basValLong(windowStateOf(frm)); + + case FORM_PROP_CONTROLS: + case FORM_PROP_DESIGNMODE: + case FORM_PROP_DESIGNSELECTION: + return basFormRtDesignFormProp(rt, frm, pd->name); + + case FORM_PROP_ICON: + return basValObject(pictureFor(rt, frm->iconPath)); + + case FORM_PROP_TAG: + case FORM_PROP_MOUSEPOINTER: { + bool handled = false; + + return getCommonProp(&frm->formCtrl, pd->name, &handled); + } + + case FORM_PROP_ACTIVECONTROL: + return activeControlOf(rt, frm); + case FORM_PROP_COUNT: break; } @@ -5575,147 +6394,136 @@ static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName } -// Look up a property in the interface descriptor and call its -// getter, marshaling the return value to BasValueT. +// Look up a property in the interface descriptor and read it through +// libdvx as a BASIC value (a write-only property reads as 0). static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled) { - *handled = false; - const WgtPropDescT *p = wgtIfaceFindProp(iface, propName); + WgtValueT v; - if (!p) { + *handled = p != NULL; + + if (!p || !wgtIfaceGetProp(iface, w, p, &v)) { return zeroValue(); } - *handled = true; - - if (!p->getFn) { - return zeroValue(); - } - - switch (p->type) { - case WGT_IFACE_STRING: { - const char *s = ((const char *(*)(WidgetT *))p->getFn)(w); - return basValStringFromC(s ? s : ""); - } - - case WGT_IFACE_ENUM: { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); - return basValLong(v); - } - - case WGT_IFACE_INT: { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); - return basValLong(v); - } - - case WGT_IFACE_BOOL: { - bool v = ((bool (*)(const WidgetT *))p->getFn)(w); - return basValBool(v); - } - - case WGT_IFACE_FLOAT: { - float v = ((float (*)(const WidgetT *))p->getFn)(w); - return basValSingle(v); - } - } - - return zeroValue(); + return basFromWgt(&v); } +// The help compiler's errors and warnings are kept for +// HelpCompileMessages$ (the last compile's only). int32_t HelpCompile(const char *inputFile, const char *outputFile) { if (!sHlpcResolved) { - sHlpcResolved = true; - sHlpcCompile = dlsym(NULL, DVX_SYM("hlpcCompile")); + sHlpcResolved = true; + sHlpcCompile = dlsym(NULL, DVX_SYM("hlpcCompile")); + sHlpcSetMessage = dlsym(NULL, DVX_SYM("hlpcSetMessageFn")); } - if (!sHlpcCompile || !inputFile || !outputFile) { + // cppcheck-suppress unsignedLessThanZero ; stb_ds arrsetlen(a, 0) compares the capacity with 0 + arrsetlen(sHelpMessages, 0); + + if (!sHlpcCompile || !sHlpcSetMessage || !inputFile || !outputFile) { + helpCompileMessage(NULL, HELP_COMPILER_MISSING); return 0; } const char *inputs[1]; inputs[0] = inputFile; + sHlpcSetMessage(helpCompileMessage, NULL); + int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, HLPC_QUIET, NULL, NULL); + + sHlpcSetMessage(NULL, NULL); return (rc == 0) ? -1 : 0; } +// One line of the help compiler's output for HelpCompileMessages$. +static void helpCompileMessage(void *ctx, const char *line) { + (void)ctx; + + for (const char *c = line; *c; c++) { + arrput(sHelpMessages, *c); + } + + arrput(sHelpMessages, HELP_MESSAGE_SEP); +} + + +// The last HelpCompile's messages, one per line ("" when it had none). +const char *HelpCompileMessages(void) { + arrput(sHelpMessages, '\0'); + arrsetlen(sHelpMessages, arrlen(sHelpMessages) - 1); + return sHelpMessages; +} + + +// Open hlpFile in the Help Viewer (HelpViewTopic with no topic). void HelpView(const char *hlpFile) { + helpViewOpen(hlpFile, ""); +} + + +// HelpView and HelpViewTopic: shellOpenHelp with "file [topic]". A file +// name without a folder is looked for beside the program; "" is the +// system help (the DVX user guide). A path holding blanks is quoted so +// the topic stays a separate argument. +static void helpViewOpen(const char *hlpFile, const char *topicId) { if (!sShellOpenHelpResolved) { sShellOpenHelpResolved = true; sShellOpenHelp = dlsym(NULL, DVX_SYM("shellOpenHelp")); } - if (!sShellOpenHelp || !sFormRt || !sFormRt->ctx || !hlpFile) { + if (!sShellOpenHelp || !sFormRt || !sFormRt->ctx || !hlpFile || !topicId) { return; } - // If hlpFile has no directory component, resolve it against the calling - // app's directory. This matches the convention where apps bundle their - // help file alongside the .app. char resolved[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; bool hasDir = strchr(hlpFile, '/') != NULL || strchr(hlpFile, '\\') != NULL || (hlpFile[0] && hlpFile[1] == ':'); - if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) { + if (!hlpFile[0]) { + snprintf(resolved, sizeof(resolved), "%s", DVX_SYSTEM_HELP_FILE_LITERAL); + } else if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) { snprintf(resolved, sizeof(resolved), "%s" DVX_PATH_SEP "%s", sFormRt->vm->appPath, hlpFile); } else { snprintf(resolved, sizeof(resolved), "%s", hlpFile); } - sShellOpenHelp(sFormRt->ctx, resolved); + const char *quote = strchr(resolved, ' ') ? "\"" : ""; + + snprintf(args, sizeof(args), "%s%s%s%s%s", quote, resolved, quote, topicId[0] ? " " : "", topicId); + sShellOpenHelp(sFormRt->ctx, args); } -// Enumerator index for a named value of an ENUM property, or -1 when the -// name (or the property's name table) is absent. Shared by the runtime -// setter and the .frm/designer string path so both agree on unknowns. -static int32_t ifaceEnumIndex(const WgtPropDescT *p, const char *name) { - if (!p->enumNames || !name) { - return -1; - } - - for (int32_t en = 0; p->enumNames[en]; en++) { - if (strcasecmp(p->enumNames[en], name) == 0) { - return en; - } - } - - return -1; +// Open hlpFile in the Help Viewer at topicId ("" for its default topic). +void HelpViewTopic(const char *hlpFile, const char *topicId) { + helpViewOpen(hlpFile, topicId); } // Interface method by name and signature, or NULL. A NULL iface is a // miss, so callers need no separate guard. static const WgtMethodDescT *ifaceFindMethod(const WgtIfaceT *iface, const char *name, uint8_t sig) { - if (!iface) { - return NULL; - } + const WgtMethodDescT *m = wgtIfaceFindMethod(iface, name); - for (int32_t m = 0; m < iface->methodCount; m++) { - if (iface->methods[m].sig == sig && strcasecmp(iface->methods[m].name, name) == 0) { - return &iface->methods[m]; - } - } - - return NULL; + return (m && m->sig == sig) ? m : NULL; } -// Read a STRING interface property through its getter; NULL when the -// control has no such readable property. +// Read a STRING interface property; NULL when the control has no such +// readable property. static const char *ifaceGetStringProp(const BasControlT *ctrl, const char *propName) { - if (!ctrl->iface || !ctrl->widget) { - return NULL; - } - const WgtPropDescT *pd = wgtIfaceFindProp(ctrl->iface, propName); + WgtValueT v; - if (!pd || pd->type != WGT_IFACE_STRING || !pd->getFn) { + if (!pd || pd->type != WGT_IFACE_STRING || !wgtIfaceGetProp(ctrl->iface, ctrl->widget, pd, &v)) { return NULL; } - return ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget); + return v.s; } @@ -5741,20 +6549,291 @@ static bool menuContainsMenu(const MenuT *tree, const MenuT *target) { } +// The menuIdMap row of item id, or NULL. +static BasMenuIdMapT *menuIdRow(BasFormT *form, int32_t id) { + for (int32_t i = 0; i < form->menuIdMapCount; i++) { + if (form->menuIdMap[i].id == id) { + return &form->menuIdMap[i]; + } + } + + return NULL; +} + + +// Forget menuIdMap row idx: its name stops resolving. Its proxy may be +// what the running handler was fired for, so the free is deferred like +// any removed control's. +static void menuIdRowDrop(BasFormRtT *rt, BasFormT *form, int32_t idx) { + BasControlT *proxy = form->menuIdMap[idx].proxy; + + dvxRemoveAccel(form->accel, form->menuIdMap[idx].id); + + if (proxy && rt->eventDepth > 0) { + arrput(rt->pendingCtrlFree, proxy); + } else { + freeControl(proxy); + } + + arrdel(form->menuIdMap, idx); + form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap); + rt->refCacheKey = NULL; +} + + +// Where item id sits among the form's menus (the menu bar's, then the +// named popups', submenus included): the menu holding it and its index +// there. *fromBar (when given) tells a menu-bar item. False when no +// menu shows such an item (a hidden item is out of its menu). +static bool menuItemLocate(BasFormT *form, int32_t id, MenuT **outMenu, int32_t *outIdx, bool *fromBar) { + if (fromBar) { + *fromBar = false; + } + + if (form->window && form->window->menuBar) { + MenuBarT *bar = form->window->menuBar; + + for (int32_t i = 0; i < bar->menuCount; i++) { + if (menuTreeLocate(bar->menus[i], id, outMenu, outIdx)) { + if (fromBar) { + *fromBar = true; + } + + return true; + } + } + } + + for (int32_t i = 0; i < form->popupMenuCount; i++) { + if (menuTreeLocate(form->popupMenus[i].menu, id, outMenu, outIdx)) { + return true; + } + } + + return false; +} + + +// Give an item formrt just added its id and, when name is not NULL, its +// name (Click handler, properties, RemoveMenuItem). Ids grow in the +// order items are made (nextMenuItemId), so they grow down each menu. +static void menuItemRegister(BasFormT *form, MenuItemT *item, int32_t id, const char *name) { + item->id = id; + + if (!name) { + return; + } + + BasMenuIdMapT map; + memset(&map, 0, sizeof(map)); + map.id = id; + snprintf(map.name, BAS_MAX_IDENT, "%s", name); + arrput(form->menuIdMap, map); + form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap); +} + + +// A menu item's Visible: hiding takes the item out of its menu and keeps +// it in its row; showing puts it back before the first item with a +// greater id, which is its own place whatever else was hidden, shown or +// added meanwhile. A submenu stays (its menu tree is named and owned +// where it is). +static void menuItemSetVisible(BasFormRtT *rt, BasFormT *form, BasMenuIdMapT *row, bool visible) { + if (!visible && !row->hiddenIn) { + MenuT *menu = NULL; + int32_t idx = 0; + + if (menuItemLocate(form, row->id, &menu, &idx, NULL) && menu->items[idx].subMenu) { + basFormRtRuntimeError(rt, + "A submenu cannot be hidden", + "Menu: %s\nHide or remove the items in it, or set Enabled = False.", + row->name); + } else if (menu) { + row->hiddenItem = menu->items[idx]; + row->hiddenIn = menu; + menuItemTake(menu, idx); + } + + return; + } + + if (!visible || !row->hiddenIn) { + return; + } + + // A new last slot (the window manager grows the array), then the item + // moves to its place + MenuT *menu = row->hiddenIn; + int32_t last = menu->itemCount; + int32_t at = 0; + + wmAddMenuItem(menu, "", 0); + + if (menu->itemCount == last) { + return; + } + + while (at < last && menu->items[at].id < row->id) { + at++; + } + + memmove(&menu->items[at + 1], &menu->items[at], (last - at) * sizeof(MenuItemT)); + menu->items[at] = row->hiddenItem; + row->hiddenIn = NULL; +} + + +// A menu item's Shortcut (row, and item: its MenuItemT, shown or hidden): +// the form's accelerator for it and the key text its label shows; "" +// removes both. An unknown key is a runtime error and changes nothing. +static void menuItemShortcut(BasFormRtT *rt, BasFormT *form, BasMenuIdMapT *row, MenuItemT *item, const char *text) { + char caption[sizeof(item->label)]; + char label[sizeof(item->label)]; + const char *sep = strchr(item->label, BAS_MENU_KEY_SEP); + int32_t key = 0; + int32_t mods = 0; + + if (!basFormRtMenuLabel("", text, NULL, label, sizeof(label), &key, &mods)) { + basFormRtRuntimeError(rt, + "Unknown shortcut key", + "Menu: %s\nShortcut: %s\nUse a key such as F2, Ctrl+X, Alt+Enter or Shift+Del.", + row->name, text); + return; + } + + if (key != 0 && !form->accel) { + form->accel = dvxCreateAccelTable(); + + if (form->window) { + form->window->accelTable = form->accel; + } + } + + dvxRemoveAccel(form->accel, row->id); + + if (key != 0) { + dvxAddAccel(form->accel, key, mods, row->id); + } + + dvxAccelFormat(key, mods, row->shortcut, sizeof(row->shortcut)); + snprintf(caption, sizeof(caption), "%.*s", sep ? (int)(sep - item->label) : (int)strlen(item->label), item->label); + basFormRtMenuLabel(caption, row->shortcut, NULL, label, sizeof(label), &key, &mods); + wmMenuItemSetLabel(item, label); +} + + +// Take item idx out of menu (its submenu, if any, stays with whoever +// holds the item). +static void menuItemTake(MenuT *menu, int32_t idx) { + memmove(&menu->items[idx], &menu->items[idx + 1], (menu->itemCount - idx - 1) * sizeof(MenuItemT)); + menu->itemCount--; +} + + +// Before menu tree goes away (DestroyMenu, RemoveMenu): the context +// menus that point into it are cleared, the names of it and its +// submenus dropped, and the rows (and proxies) of its items, hidden ones +// included, so the names stop resolving and repeated create / destroy +// cycles do not grow menuIdMap. A proxy may be what the running handler +// was fired for, so its free is deferred like any removed control. +static void menuTreeForget(BasFormRtT *rt, BasFormT *form, MenuT *tree) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + WidgetT *cw = form->controls[i]->widget; + + if (cw && menuContainsMenu(tree, cw->contextMenu)) { + cw->contextMenu = NULL; + } + } + + if (form->window && menuContainsMenu(tree, form->window->contextMenu)) { + form->window->contextMenu = NULL; + } + + for (int32_t i = form->popupMenuCount - 1; i >= 0; i--) { + if (menuContainsMenu(tree, form->popupMenus[i].menu)) { + arrdel(form->popupMenus, i); + } + } + + form->popupMenuCount = (int32_t)arrlen(form->popupMenus); + + for (int32_t i = form->menuIdMapCount - 1; i >= 0; i--) { + const BasMenuIdMapT *row = &form->menuIdMap[i]; + + if (wmMenuFindItemInMenu(tree, row->id) || menuContainsMenu(tree, row->hiddenIn)) { + menuIdRowDrop(rt, form, i); + } + } +} + + +// Search one menu and its submenus for item id (menuItemLocate). +static bool menuTreeLocate(MenuT *menu, int32_t id, MenuT **outMenu, int32_t *outIdx) { + if (!menu) { + return false; + } + + for (int32_t i = 0; i < menu->itemCount; i++) { + if (menu->items[i].id == id) { + *outMenu = menu; + *outIdx = i; + return true; + } + + if (menuTreeLocate(menu->items[i].subMenu, id, outMenu, outIdx)) { + return true; + } + } + + return false; +} + + // Allocate the next menu-item ID for a form. IDs are unique across // menu bar + popups on the same form and reused by menuIdMap for // event dispatch. Starts from MENU_ID_BASE + the highest in-use ID // so runtime-added items don't collide with .frm-loaded items. -static int32_t nextMenuItemId(BasFormT *form) { - int32_t maxId = MENU_ID_BASE - 1; +// Ids are handed out in the order items are made, so within one menu they +// grow from top to bottom (menuItemSetVisible relies on it). +// Gives held launch arguments (basFormRtNewArgs) to the program once no +// handler is running and no dialog box or modal form is open: COMMAND$ +// becomes them and every loaded form's NewArgs fires. The forms are +// listed first; a handler may load or unload forms (the bracket keeps +// unloaded ones allocated until it is done). +static void newArgsFlush(BasFormRtT *rt) { + if (!rt->pendingArgs || rt->eventDepth > 0 || rt->ctx->modalWindow || rt->terminated) { + return; + } - for (int32_t i = 0; i < form->menuIdMapCount; i++) { - if (form->menuIdMap[i].id > maxId) { - maxId = form->menuIdMap[i].id; + char *args = rt->pendingArgs; + + rt->pendingArgs = NULL; + basVmSetCommand(rt->vm, args); + free(args); + + int32_t count = (int32_t)arrlen(rt->forms); + BasFormT **forms = (BasFormT **)malloc(sizeof(BasFormT *) * (count + 1)); + + if (!forms) { + return; + } + + memcpy(forms, rt->forms, sizeof(BasFormT *) * count); + rtEventEnter(rt); + + for (int32_t i = 0; i < count && !rt->terminated; i++) { + if (!forms[i]->unloading) { + basFormRtFireEvent(rt, forms[i], forms[i]->name, BAS_EVT_NEWARGS); } } - return maxId + 1; + rtEventLeave(rt); + free(forms); +} + + +static int32_t nextMenuItemId(BasFormT *form) { + return form->menuIdNext++; } @@ -5764,10 +6843,11 @@ static void onFormActivate(WindowT *win) { // bridge (icon refresh, focus bookkeeping) never runs for forms. widgetOnFocus(win); - BasFormT *form = findFormByWindow(sFormRt, win); + BasFormRtT *rt; + BasFormT *form = formOfWindow(win, &rt); if (form) { - basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_ACTIVATE); + basFormRtFireEvent(rt, form, form->name, BAS_EVT_ACTIVATE); } } @@ -5777,40 +6857,101 @@ static void onFormActivate(WindowT *win) { // QueryUnload/Unload/teardown path (including the `unloading` // re-entrancy guard and deferred frees) as the UNLOAD statement. static void onFormClose(WindowT *win) { - BasFormT *form = findFormByWindow(sFormRt, win); + BasFormRtT *rt; + BasFormT *form = formOfWindow(win, &rt); if (!form) { return; } - basFormRtUnloadForm(sFormRt, form); + basFormRtUnloadForm(rt, form); // If no forms are left, stop the VM. A QueryUnload cancel leaves the - // form in sFormRt->forms, so this stays false then; a committed - // unload is detached (removed) even when its frees are deferred. - if (arrlen(sFormRt->forms) == 0 && sFormRt->vm) { - sFormRt->vm->running = false; + // form in rt->forms, so this stays false then; a committed unload is + // detached (removed) even when its frees are deferred. + if (arrlen(rt->forms) == 0 && rt->vm) { + rt->vm->running = false; } } +// The pointer over a form: Screen.MousePointer, else the MousePointer of +// the control under it or of the nearest container around it that has +// one, else the form's; 0 (the widget's own) when none is set. +static int32_t onFormCursor(WindowT *win, int32_t x, int32_t y) { + BasFormT *form = (BasFormT *)win->userData; + BasFormRtT *rt = (form && form->vm) ? (BasFormRtT *)form->vm->ui.ctx : NULL; + int32_t pointer = rt ? rt->screenPointer : BAS_POINTER_DEFAULT; + int32_t cursor = CURSOR_ARROW; + + if (!form) { + return CURSOR_ARROW; + } + + for (WidgetT *w = win->widgetRoot ? widgetCallbackTarget(win->widgetRoot, x, y) : NULL; w && pointer == BAS_POINTER_DEFAULT; w = w->parent) { + const BasControlT *ctrl = findCtrlByWidget(form, w); + + pointer = ctrl ? ctrl->mousePointer : pointer; + } + + if (pointer == BAS_POINTER_DEFAULT) { + pointer = form->formCtrl.mousePointer; + } + + pointerCursor(pointer, &cursor); + return cursor; +} + + static void onFormDeactivate(WindowT *win) { // Chain the default widget focus handler: it clears sFocusedWidget // and fires the focused widget's onBlur (BASIC LostFocus event + // data-binding write-back), which this override otherwise suppresses. widgetOnBlur(win); - BasFormT *form = findFormByWindow(sFormRt, win); + BasFormRtT *rt; + BasFormT *form = formOfWindow(win, &rt); if (form) { - basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_DEACTIVATE); + basFormRtFireEvent(rt, form, form->name, BAS_EVT_DEACTIVATE); + } +} + + +// Keys of a form's window: Escape presses the Cancel button and Enter, +// after the focused control has had it, the Default button (unless the +// focus is on a button, which Enter pressed itself), as in VB and in DVX +// dialog boxes. Everything else goes to the focused control. +static void onFormKey(WindowT *win, int32_t key, int32_t mod) { + BasFormRtT *rt; + BasFormT *form = formOfWindow(win, &rt); + BasControlT *cancel = (form && key == KEY_ESCAPE) ? findButtonWith(form, BAS_PROP_CANCEL) : NULL; + + if (cancel) { + cancel->widget->onClick(cancel->widget); + return; + } + + WidgetT *focus = wgtGetFocused(); + bool focusPresses = focus && focus->wclass && (focus->wclass->flags & WCLASS_PRESS_RELEASE); + + widgetOnKey(win, key, mod); + + // The key's handler may have unloaded the form + form = (key == KEY_ENTER && !focusPresses) ? formOfWindow(win, &rt) : NULL; + + BasControlT *def = form ? findButtonWith(form, BAS_PROP_DEFAULT) : NULL; + + if (def) { + def->widget->onClick(def->widget); } } // Dispatch menu clicks as ControlName_Click events. static void onFormMenu(WindowT *win, int32_t menuId) { - BasFormT *form = findFormByWindow(sFormRt, win); + BasFormRtT *rt; + BasFormT *form = formOfWindow(win, &rt); if (!form) { return; @@ -5818,7 +6959,7 @@ static void onFormMenu(WindowT *win, int32_t menuId) { for (int32_t j = 0; j < form->menuIdMapCount; j++) { if (form->menuIdMap[j].id == menuId) { - fireEventArgsOut(sFormRt, form, form->menuIdMap[j].proxy, form->menuIdMap[j].name, BAS_EVT_CLICK, NULL, 0, NULL, 0); + fireEventArgsOut(rt, form, form->menuIdMap[j].proxy, form->menuIdMap[j].name, BAS_EVT_CLICK, NULL, 0, NULL, 0); return; } } @@ -5829,21 +6970,22 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) { // Let the widget system re-evaluate scrollbars for the new size widgetOnResize(win, newW, newH); - BasFormT *form = findFormByWindow(sFormRt, win); + BasFormRtT *rt; + BasFormT *form = formOfWindow(win, &rt); if (!form) { return; } // Rule-N bracket: Resize may unload the form; then no Paint follows - rtEventEnter(sFormRt); - basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_RESIZE); + rtEventEnter(rt); + basFormRtFireEvent(rt, form, form->name, BAS_EVT_RESIZE); if (!form->unloading && form->window && form->window->visible) { - firePaintForm(sFormRt, form); + firePaintForm(rt, form); } - rtEventLeave(sFormRt); + rtEventLeave(rt); } @@ -5934,6 +7076,48 @@ static void onWidgetDblClick(WidgetT *w) { } +// A widget fired a named event that has no universal callback +// (wgtFireEvent): a ListView's ColumnClick, a TextArea's GutterClick or +// Colorize, a HelpPane's LinkClick, an event a UserControl raises. +// CtrlName_Name runs with the arguments as BASIC values. What the handler +// leaves in a parameter is written back into args (ByRef); a String's +// text stays valid until the next event writes the same argument. +static void onWidgetEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + static BasStringT *sStrHold[BAS_WGT_ARGS_MAX]; + BasFormRtT *rt; + BasControlT *ctrl = ctrlFromWidget(w, &rt); + BasValueT in[BAS_WGT_ARGS_MAX]; + BasValueT out[BAS_WGT_ARGS_MAX]; + + if (!ctrl || argc < 0 || argc > BAS_WGT_ARGS_MAX || (argc > 0 && !args)) { + return; + } + + // out starts as the arguments themselves, so a handler that is + // missing or takes no parameters leaves them unchanged. + for (int32_t i = 0; i < argc; i++) { + in[i] = basFromWgt(&args[i]); + out[i] = basValCopy(in[i]); + } + + fireCtrlEventOut(rt, ctrl, name, in, argc, out, argc); + + for (int32_t i = 0; i < argc; i++) { + BasStringT *hold = NULL; + + args[i] = wgtFromBas(out[i], args[i].type, &hold); + + if (hold) { + basStringUnref(sStrHold[i]); + sStrHold[i] = hold; + } + + basValRelease(&in[i]); + basValRelease(&out[i]); + } +} + + static void onWidgetFocus(WidgetT *w) { BasFormRtT *rt; BasControlT *ctrl = ctrlFromWidget(w, &rt); @@ -6162,6 +7346,22 @@ static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, } +// The bytes of a picture file, or of the icon a DVX program, widget or +// library file carries (dvxResLoadIconData), for dvxLoadImageAlpha. NULL +// when the file cannot be read. Free with free(). +static char *pictureFileRead(const char *path, int32_t *outLen) { + uint32_t size = 0; + char *icon = (char *)dvxResLoadIconData(path, &size); + + if (!icon) { + return platformReadFile(path, outLen); + } + + *outLen = (int32_t)size; + return icon; +} + + // The Picture object (LoadPicture) ref is, else NULL. static BasPictureT *pictureFind(const BasFormRtT *rt, const void *ref) { for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) { @@ -6192,32 +7392,61 @@ static BasPictureT *pictureFor(BasFormRtT *rt, const char *path) { // its file name. Handle is not offered: DVX pictures have no system // handle. static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char *propName) { - const char *name = propName ? propName : "?"; - - if (strcmp(name, BAS_PIC_PROP_DEFAULT) == 0) { + if (strcmp(propName, BAS_PIC_PROP_DEFAULT) == 0) { return basValStringFromC(pic->path); } - if (strcasecmp(name, BAS_PIC_PROP_WIDTH) == 0) { + if (strcasecmp(propName, BAS_PIC_PROP_WIDTH) == 0) { return basValLong(basGfxRound(basFormRtDrawPixelsToUnits(pic->width, BAS_SCALE_HIMETRIC, true))); } - if (strcasecmp(name, BAS_PIC_PROP_HEIGHT) == 0) { + if (strcasecmp(propName, BAS_PIC_PROP_HEIGHT) == 0) { return basValLong(basGfxRound(basFormRtDrawPixelsToUnits(pic->height, BAS_SCALE_HIMETRIC, false))); } - if (strcasecmp(name, BAS_PIC_PROP_TYPE) == 0) { + if (strcasecmp(propName, BAS_PIC_PROP_TYPE) == 0) { return basValInteger(pic->path[0] ? BAS_PIC_TYPE_BITMAP : BAS_PIC_TYPE_NONE); } basFormRtRuntimeError(rt, "Unknown Picture property", "Property: %s\nValid: %s, %s, %s.", - name, BAS_PIC_PROP_HEIGHT, BAS_PIC_PROP_TYPE, BAS_PIC_PROP_WIDTH); + propName, BAS_PIC_PROP_HEIGHT, BAS_PIC_PROP_TYPE, BAS_PIC_PROP_WIDTH); return zeroValue(); } +// The DVX pointer (CURSOR_*) a MousePointer value shows; false for a +// value DVX has no pointer for. +static bool pointerCursor(int32_t pointer, int32_t *outCursor) { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sPointerCursors); i++) { + if (sPointerCursors[i].pointer == pointer) { + *outCursor = sPointerCursors[i].cursor; + return true; + } + } + + return false; +} + + +// Name a menu of the form (see BasFrmPopupMenuT): ownsMenu for a root the +// form frees. A NULL menu (a failed allocation) is not named. +static void popupMenuAdd(BasFormT *form, const char *name, MenuT *menu, bool ownsMenu) { + if (!menu) { + return; + } + + BasFrmPopupMenuT entry; + memset(&entry, 0, sizeof(entry)); + snprintf(entry.name, BAS_MAX_IDENT, "%s", name); + entry.menu = menu; + entry.ownsMenu = ownsMenu; + arrput(form->popupMenus, entry); + form->popupMenuCount = (int32_t)arrlen(form->popupMenus); +} + + static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu) { if (menu && form) { for (int32_t i = 0; i < form->popupMenuCount; i++) { @@ -6237,9 +7466,10 @@ static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu) { static void printerBind(BasFormRtT *rt) { __typeof__(printBasSetContext) *setContext = (__typeof__(setContext))dlsym(NULL, DVX_SYM("printBasSetContext")); - sPrintApi.open = (__typeof__(sPrintApi.open))dlsym(NULL, DVX_SYM("printUiOpenPort")); - sPrintApi.write = (__typeof__(sPrintApi.write))dlsym(NULL, DVX_SYM("printJobWrite")); - sPrintApi.close = (__typeof__(sPrintApi.close))dlsym(NULL, DVX_SYM("printJobClose")); + sPrintApi.open = (__typeof__(sPrintApi.open))dlsym(NULL, DVX_SYM("printUiOpenPort")); + sPrintApi.write = (__typeof__(sPrintApi.write))dlsym(NULL, DVX_SYM("printJobWrite")); + sPrintApi.close = (__typeof__(sPrintApi.close))dlsym(NULL, DVX_SYM("printJobClose")); + sPrintApi.window = (__typeof__(sPrintApi.window))dlsym(NULL, DVX_SYM("printUiWindow")); if (setContext) { setContext(rt->ctx); @@ -6278,6 +7508,37 @@ static bool printerWrite(void *ctx, void *job, const char *data, int32_t len) { } +// print.bas PrintForm: what a loaded form shows, on the printer in the +// settings (printUiWindow). True when it was printed. +int32_t PrintForm(const char *formName) { + return printFormWindow(formName, false); +} + + +// print.bas PrintFormDialog: PrintForm with the Print dialog first. +int32_t PrintFormDialog(const char *formName) { + return printFormWindow(formName, true); +} + + +static int32_t printFormWindow(const char *formName, bool askFirst) { + BasFormT *form = NULL; + + for (int32_t i = 0; sFormRt && formName && i < (int32_t)arrlen(sFormRt->forms); i++) { + if (strcasecmp(sFormRt->forms[i]->name, formName) == 0) { + form = sFormRt->forms[i]; + break; + } + } + + if (!form || !form->window || !sPrintApi.window) { + return 0; + } + + return sPrintApi.window(sFormRt->ctx, form->window, askFirst) ? -1 : 0; +} + + // refreshDetailControls -- cascade master-detail: refresh detail Data controls static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { if (!masterCtrl->widget) { @@ -6366,35 +7627,36 @@ static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl) { } +// ResAddData: a resource of any bytes (BINARY STRING); True on success. +int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len) { + if (!path || !name || !data || len < 0) { + return 0; + } + + return dvxResAppend(path, name, (uint32_t)type, data, (uint32_t)len) == 0 ? -1 : 0; +} + + int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) { - if (!path || !name || !srcFile) { + if (!srcFile) { return 0; } - int32_t fileSize = 0; - uint8_t *buf = (uint8_t *)platformReadFile(srcFile, &fileSize); + int32_t fileSize = 0; + uint8_t *buf = (uint8_t *)platformReadFile(srcFile, &fileSize); + int32_t result = (buf && fileSize > 0) ? ResAddData(path, name, type, buf, fileSize) : 0; - if (!buf || fileSize <= 0) { - free(buf); - return 0; - } - - int32_t result = dvxResAppend(path, name, (uint32_t)type, buf, (uint32_t)fileSize) == 0 ? -1 : 0; free(buf); return result; } int32_t ResAddText(const char *path, const char *name, const char *text) { - if (!path || !name) { - return 0; - } - if (!text) { text = ""; } - return dvxResAppend(path, name, DVX_RES_TEXT, text, (uint32_t)strlen(text) + 1) == 0 ? -1 : 0; + return ResAddData(path, name, DVX_RES_TEXT, (const uint8_t *)text, (int32_t)strlen(text) + 1); } @@ -6422,19 +7684,12 @@ int32_t ResCount(int32_t handle) { int32_t ResExtract(const char *path, const char *name, const char *outFile) { - if (!path || !name || !outFile) { - return 0; - } - - DvxResHandleT *h = dvxResOpen(path); - - if (!h) { + if (!outFile) { return 0; } uint32_t size = 0; - void *data = dvxResRead(h, name, &size); - dvxResClose(h); + void *data = resReadNamed(path, name, &size); if (!data) { return 0; @@ -6454,30 +7709,27 @@ int32_t ResExtract(const char *path, const char *name, const char *outFile) { } +// ResGetData$: every byte of a resource (BINARY STRING); "" when it is +// not there. +const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen) { + uint32_t size = 0; + + free(sResData); + sResData = (uint8_t *)resReadNamed(path, name, &size); + *outLen = sResData ? (int32_t)size : 0; + return sResData; +} + + const char *ResGetText(const char *path, const char *name) { static char sBuf[BAS_RES_TEXT_LEN]; + uint32_t size = 0; + void *data = resReadNamed(path, name, &size); + sBuf[0] = '\0'; - if (!path || !name) { - return sBuf; - } - - DvxResHandleT *h = dvxResOpen(path); - - if (!h) { - return sBuf; - } - - uint32_t size = 0; - void *data = dvxResRead(h, name, &size); - dvxResClose(h); - if (data) { - uint32_t copyLen = size; - - if (copyLen >= sizeof(sBuf)) { - copyLen = sizeof(sBuf) - 1; - } + uint32_t copyLen = DVX_MIN(size, (uint32_t)sizeof(sBuf) - 1); memcpy(sBuf, data, copyLen); sBuf[copyLen] = '\0'; @@ -6495,15 +7747,23 @@ const char *ResGetText(const char *path, const char *name) { static void resizeIfaceBuffer(BasControlT *ctrl) { WidgetT *w = ctrl->widget; - if (!w || w->minW <= 0 || w->minH <= 0) { + if (!w) { + return; + } + + int32_t wide = sizePixels(w, w->minW, true); + int32_t high = sizePixels(w, w->minH, false); + + if (wide <= 0 || high <= 0) { return; } const WgtMethodDescT *m = ifaceFindMethod(ctrl->iface, WGT_METHOD_RESIZE, WGT_SIG_INT_INT); + WgtValueT size[2]; - if (m) { - ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, w->minW, w->minH); - } + size[0] = wgtValueInt(wide); + size[1] = wgtValueInt(high); + wgtIfaceCallMethod(ctrl->iface, w, m, size, 2, NULL); } @@ -6519,6 +7779,21 @@ const char *ResName(int32_t handle, int32_t index) { } +// A control given as an argument: the control (an object) or its name on +// form; NULL when it is neither. +static BasControlT *resolveCtrlArg(BasFormRtT *rt, BasFormT *form, BasValueT arg) { + if (arg.type == BAS_TYPE_OBJECT) { + return resolveCtrlRef(rt, arg.objVal); + } + + BasStringT *name = basValFormatString(arg); + BasControlT *ctrl = form ? findCtrlInForm(form, name->data) : NULL; + + basStringUnref(name); + return ctrl; +} + + // Map a BASIC object value to the live BasControlT it names, or NULL. // BASIC holds raw pointers, so a ref is trusted only when it is still // one of: a loaded (or pending-unload) form or its formCtrl, a control @@ -6541,6 +7816,11 @@ static BasControlT *resolveCtrlRef(BasFormRtT *rt, void *ref) { hit = resolveCtrlRefIn(rt->pendingUnload, ref); } + // A UserControl's own object (its code's UserControl) + if (!hit) { + hit = basUcResolveSurface(rt, ref); + } + if (hit) { rt->refCacheKey = ref; rt->refCacheHit = hit; @@ -6582,7 +7862,7 @@ static BasControlT *resolveCtrlRefIn(BasFormT **forms, void *ref) { static BasFormT *resolveFormRef(BasFormRtT *rt, void *ref) { BasControlT *obj = resolveCtrlRef(rt, ref); - return (obj && obj == &obj->form->formCtrl) ? obj->form : NULL; + return (obj && obj->form && obj == &obj->form->formCtrl) ? obj->form : NULL; } @@ -6600,45 +7880,34 @@ static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar) return NULL; } - // Menu-bar items: walk every top-level menu (and its submenus) on the - // form's window menu bar. - if (form->window && form->window->menuBar) { - MenuBarT *bar = form->window->menuBar; + MenuT *menu; + int32_t idx; - for (int32_t i = 0; i < bar->menuCount; i++) { - MenuItemT *item = wmMenuFindItemInMenu(bar->menus[i], menuId); - - if (item) { - if (fromBar) { - *fromBar = true; - } - - return item; - } - } + if (menuItemLocate(form, menuId, &menu, &idx, fromBar)) { + return &menu->items[idx]; } - // Popup-only items: search each named popup/context menu the form owns. - for (int32_t i = 0; i < form->popupMenuCount; i++) { - MenuItemT *item = wmMenuFindItemInMenu(form->popupMenus[i].menu, menuId); + // A hidden item (Visible = False) is kept in its row + BasMenuIdMapT *row = menuIdRow(form, menuId); - if (item) { - return item; - } - } - - return NULL; + return (row && row->hiddenIn) ? &row->hiddenItem : NULL; } // Find the loaded form whose .frm declared this SUB. Returns NULL if // the SUB is module-global (no BEGINFORM scope) or the owning form // isn't currently loaded -- callers should fall back to ctrl->form. -static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) { +static BasFormT *resolveOwningForm(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntryT *proc) { if (!rt || !proc || !proc->formName[0]) { return NULL; } + // A copy made with New owns its own variables: the event's form when + // it is a copy of the procedure's form, else the first of that name + if (eventForm && strcasecmp(eventForm->name, proc->formName) == 0) { + return eventForm; + } + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) { return rt->forms[i]; @@ -6664,26 +7933,6 @@ static bool resolveShellIdle(void) { } -// Resolve a type name (VB-style or DVX widget name) to a DVX -// widget type name. First tries wgtFindByBasName for VB names -// like "CommandButton", then falls back to direct widget name. -const char *resolveTypeName(const char *typeName) { - // Try VB name first (e.g. "CommandButton" -> "button") - const char *wgtName = wgtFindByBasName(typeName); - - if (wgtName) { - return wgtName; - } - - // Try as direct widget type name (e.g. "button", "slider") - if (wgtGetApi(typeName, sizeof(WgtApiPrefixT))) { - return typeName; - } - - return NULL; -} - - int32_t ResOpen(const char *path) { if (!path) { return 0; @@ -6705,6 +7954,28 @@ int32_t ResOpen(const char *path) { } +// A named resource of the file at path, read in one go (malloc'd, the +// caller frees); NULL when the file or the resource is not there. +static void *resReadNamed(const char *path, const char *name, uint32_t *outSize) { + *outSize = 0; + + if (!path || !name) { + return NULL; + } + + DvxResHandleT *h = dvxResOpen(path); + + if (!h) { + return NULL; + } + + void *data = dvxResRead(h, name, outSize); + + dvxResClose(h); + return data; +} + + int32_t ResRemove(const char *path, const char *name) { if (!path || !name) { return 0; @@ -6757,16 +8028,17 @@ static bool rtCallHandler(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntr // declared it inside BEGINFORM...ENDFORM), not to the event form -- // those can differ when a SUB from form A is wired as an event // handler for a control on form B. - BasFormT *owningForm = resolveOwningForm(rt, proc); + BasFormT *owningForm = resolveOwningForm(rt, eventForm, proc); BasFormT *varsForm = owningForm ? owningForm : eventForm; rt->currentForm = eventForm; basVmSetCurrentForm(rt->vm, eventForm); basVmSetCurrentFormVars(rt->vm, varsForm ? varsForm->formVars : NULL, varsForm ? varsForm->formVarCount : 0); - bool ok; - - rtEventEnter(rt); + // The runtime-wide externs (dialogs, help, files) act for the program + // whose code runs: several programs share this library. + BasFormRtT *prevRt = basFormRtCodeEnter(rt); + bool ok; if (outArgs && outArgCount > 0) { ok = basVmCallSubWithArgsOut(rt->vm, codeAddr, args, argCount, outArgs, outArgCount); @@ -6779,7 +8051,14 @@ static bool rtCallHandler(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntr rt->currentForm = prevForm; basVmSetCurrentForm(rt->vm, prevForm); basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); - rtEventLeave(rt); + basFormRtCodeLeave(rt, prevRt); + + // An untrapped error stopped the handler: the program ends, as + // anywhere else (the message stays for the host to read). + if (!ok) { + basFormRtCodeFailed(rt); + } + return ok; } @@ -6805,7 +8084,10 @@ static void rtEventLeave(BasFormRtT *rt) { return; } + // Launch arguments that came while the program was busy are given + // once the outermost frame is done (and its frees are drained). if (arrlen(rt->pendingCtrlFree) == 0 && arrlen(rt->pendingUnload) == 0) { + newArgsFlush(rt); return; } @@ -6834,6 +8116,7 @@ static void rtEventLeave(BasFormRtT *rt) { } rt->ctx->dispatchDepth--; + newArgsFlush(rt); } @@ -6881,25 +8164,28 @@ static BasVmResultE runSliced(BasFormRtT *rt) { BasVmResultE result; do { - result = basVmRun(vm); + // This program's code runs (see rtCallHandler); the dvxUpdate + // between slices may run other programs' handlers. + BasFormRtT *prevRt = sFormRt; + + sFormRt = rt; + sCodeDepth++; + result = basVmRun(vm); + sCodeDepth--; + sFormRt = prevRt; if (result == BAS_VM_STEP_LIMIT) { if (!dvxUpdate(rt->ctx)) { break; } } else if (result != BAS_VM_HALTED && result != BAS_VM_OK) { - // basFormRtRuntimeError already surfaces its own error - // dialog and sets rt->terminated, so only show this generic - // box for VM-internal errors. Every non-HALTED/OK result is - // an error here: BAS_VM_ERROR carries a message, and the - // specific codes (division by zero, type mismatch, bad - // opcode, ...) must not end the program silently. - if (!rt->terminated) { - const char *errMsg = basVmGetError(vm); - char buf[BAS_ERR_BOX_LEN]; - snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg[0] ? errMsg : "Unknown error"); - dvxMessageBox(rt->ctx, "Error", buf, 0); - } + // Every non-HALTED/OK result is an error here: BAS_VM_ERROR + // carries a message, and the specific codes (division by + // zero, type mismatch, bad opcode, ...) must not end the + // program silently. An error basFormRtRuntimeError already + // reported has terminated the runtime; vmErrorEnd then does + // nothing. + vmErrorEnd(rt); break; } else { break; @@ -6910,6 +8196,188 @@ static BasVmResultE runSliced(BasFormRtT *rt) { } +// The end of every runtime error (basFormRtRuntimeError, vmErrorEnd): +// log it to DVX.LOG (the summary on one line, the details indented +// beneath), halt the VM, mark the runtime terminated and show the modal +// box with both, as VB shows an untrapped error and ends the program. +// Hosts that suppress the dialog (the IDE) report through the Output +// pane and editor navigation instead, so nothing is logged for them. +static void runtimeErrorEnd(BasFormRtT *rt, const char *summary, const char *details) { + if (!rt || !rt->suppressErrorDialog) { + dvxLog(""); + dvxLog("BASIC RUNTIME ERROR: %s", summary); + + // Split details on newlines and indent each one. + const char *p = details; + + while (*p) { + const char *nl = strchr(p, '\n'); + int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p); + char line[BAS_ERR_DETAIL_LEN]; + + if (len >= (int32_t)sizeof(line)) { + len = sizeof(line) - 1; + } + + memcpy(line, p, len); + line[len] = '\0'; + dvxLog(" %s", line); + + if (!nl) { + break; + } + + p = nl + 1; + } + } + + // Halt the VM and flip rt->terminated BEFORE showing the dialog. + // dvxMessageBox pumps events while it's open, and a single missing + // control typically has several queued events targeting it (load + + // activate + paint + ...). If we haven't marked the runtime as + // terminated yet, each re-entry passes the terminated check and + // stacks another dialog. Marking first means the re-entry returns + // early, and the user only has to dismiss one dialog. ended=true + // also breaks the IDE's VB-style event pump loop. + if (rt && rt->vm) { + rt->vm->running = false; + rt->vm->ended = true; + } + + if (rt) { + rt->terminated = true; + } + + if (rt && rt->ctx && !rt->suppressErrorDialog) { + char boxMsg[BAS_ERR_BOX_LEN]; + + snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s", summary, details); + dvxMessageBox(rt->ctx, BAS_ERR_TITLE, boxMsg, MB_OK | MB_ICONERROR); + } +} + + +// Screen.Arrange arrangement: VB's MDIForm.Arrange values over every +// window on the desktop, chosen by what they look like (DVX's "horizontal" +// tiling puts windows side by side, VB's stacks them). +static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc) { + if (!name || strcasecmp(name, BAS_SCREEN_ARRANGE) != 0 || argc != 1) { + basFormRtRuntimeError(rt, + "Unknown Screen method", + "Method: %s\nThe Screen object's method is Arrange arrangement.", + name ? name : "?"); + return zeroValue(); + } + + int32_t how = (int32_t)basValToNumber(args[0]); + + switch (how) { + case BAS_ARRANGE_CASCADE: + dvxCascadeWindows(rt->ctx); + break; + + case BAS_ARRANGE_TILE_HORZ: + dvxTileWindowsV(rt->ctx); + break; + + case BAS_ARRANGE_TILE_VERT: + dvxTileWindowsH(rt->ctx); + break; + + case BAS_ARRANGE_TILE_GRID: + dvxTileWindows(rt->ctx); + break; + + default: + basFormRtRuntimeError(rt, + "Invalid Arrange value", + "Arrange: %d\nUse vbCascade, vbTileHorizontal, vbTileVertical or vbTileGrid.", + (int)how); + break; + } + + return zeroValue(); +} + + +// Screen.Prop reads: the desktop's Width and Height in pixels (the unit of +// a form's Left, Top, Width and Height), its BitsPerPixel, DebugLayout and +// MousePointer. +static BasValueT screenProp(BasFormRtT *rt, const char *propName) { + const BasPropDescT *pd = findPropDesc(sScreenProps, SCREEN_PROP_COUNT, propName); + + if (!pd) { + char valid[BAS_PROP_LIST_LEN]; + formatPropNames(sScreenProps, SCREEN_PROP_COUNT, false, valid, sizeof(valid)); + basFormRtRuntimeError(rt, + "Unknown Screen property", + "Property: %s\nValid: %s.", + propName, valid); + return zeroValue(); + } + + switch ((BasScreenPropE)(pd - sScreenProps)) { + case SCREEN_PROP_WIDTH: + return basValLong(rt->ctx->display.width); + + case SCREEN_PROP_HEIGHT: + return basValLong(rt->ctx->display.height); + + case SCREEN_PROP_BITSPERPIXEL: + return basValLong(rt->ctx->display.format.bitsPerPixel); + + case SCREEN_PROP_DEBUGLAYOUT: + return basValBool(wgtGetDebugLayout()); + + case SCREEN_PROP_MOUSEPOINTER: + return basValLong(rt->ctx->busy ? BAS_POINTER_HOURGLASS : rt->screenPointer); + + case SCREEN_PROP_ACTIVECONTROL: + return activeControlOf(rt, NULL); + + case SCREEN_PROP_COUNT: + break; + } + + return zeroValue(); +} + + +// Screen.Prop = value: DebugLayout (the layout borders of every window) +// and MousePointer (vbHourglass: the busy cursor; another pointer: shown +// over every window of the program; vbDefault: each control's own). +static void screenSetProp(BasFormRtT *rt, const char *propName, BasValueT value) { + const BasPropDescT *pd = findPropDesc(sScreenProps, SCREEN_PROP_COUNT, propName); + + if (!pd || !pd->writable) { + char valid[BAS_PROP_LIST_LEN]; + formatPropNames(sScreenProps, SCREEN_PROP_COUNT, true, valid, sizeof(valid)); + basFormRtRuntimeError(rt, + "Screen property cannot be set", + "Property: %s\nSettable: %s.", + propName, valid); + return; + } + + if ((BasScreenPropE)(pd - sScreenProps) == SCREEN_PROP_DEBUGLAYOUT) { + wgtSetDebugLayout(rt->ctx, basValIsTruthy(value)); + return; + } + + // MousePointer, the other settable one + if (!setMousePointer(rt, &rt->screenPointer, value)) { + return; + } + + rt->pointerBusy = (rt->screenPointer == BAS_POINTER_HOURGLASS); + dvxSetBusy(rt->ctx, rt->pointerBusy); + + if (rt->pointerBusy) { + rt->screenPointer = BAS_POINTER_DEFAULT; + } +} + + void SerAttach(int32_t com, const char *termCtrlName) { if (!serResolveApi() || !sFormRt || !termCtrlName) { return; @@ -7003,6 +8471,14 @@ void SerFlush(int32_t com) { } +// Throw away what is waiting to be sent. +void SerFlushTx(int32_t com) { + if (serResolveApi() && sSerApi.clearTxBuffer) { + sSerApi.clearTxBuffer(com - 1); + } +} + + int32_t SerGetBase(int32_t com) { if (!serResolveApi() || !sSerApi.getBase) { return 0; @@ -7012,11 +8488,23 @@ int32_t SerGetBase(int32_t com) { } +// The speed of an open port (an SER_ERR_ code when it is not open). +int32_t SerGetBaud(int32_t com) { + return serValueGet(&sSerApi.getBps, com); +} + + int32_t SerGetCts(int32_t com) { return serLineGet(&sSerApi.getCts, com); } +// Data bits of an open port (5-8; an SER_ERR_ code when it is not open). +int32_t SerGetDataBits(int32_t com) { + return serValueGet(&sSerApi.getData, com); +} + + int32_t SerGetDcd(int32_t com) { return serLineGet(&sSerApi.getDcd, com); } @@ -7032,6 +8520,13 @@ int32_t SerGetDtr(int32_t com) { } +// The SER_HANDSHAKE_ mode of an open port (an SER_ERR_ code when it is +// not open). +int32_t SerGetHandshake(int32_t com) { + return serValueGet(&sSerApi.getHandshake, com); +} + + int32_t SerGetIrq(int32_t com) { if (!serResolveApi() || !sSerApi.getIrq) { return 0; @@ -7041,11 +8536,45 @@ int32_t SerGetIrq(int32_t com) { } +// The parity of an open port: "N", "E", "O", "M" or "S"; "" when it is +// not open. +const char *SerGetParity(int32_t com) { + static char text[2]; + char parity; + + text[0] = '\0'; + + if (!serResolveApi() || !sSerApi.getParity) { + return text; + } + + parity = sSerApi.getParity(com - 1); + text[0] = (parity > 0) ? parity : '\0'; + text[1] = '\0'; + return text; +} + + +// True while the other end rings (the ring indicator line is up). +int32_t SerGetRi(int32_t com) { + int32_t msr = serValueGet(&sSerApi.getMsr, com); + + return (msr > 0 && (msr & RS232_MSR_RI)) ? SER_BASIC_TRUE : 0; +} + + int32_t SerGetRts(int32_t com) { return serLineGet(&sSerApi.getRts, com); } +// Stop bits of an open port (1 or 2; an SER_ERR_ code when it is not +// open). +int32_t SerGetStopBits(int32_t com) { + return serValueGet(&sSerApi.getStop, com); +} + + int32_t SerGetUart(int32_t com) { if (!serResolveApi() || !sSerApi.getUartType) { return 0; @@ -7067,6 +8596,28 @@ static void serIdlePoll(void *ctx) { } +// The SER_ERR_ code of the last SerOpen or SerSet on the port (SER_OK +// when it worked). +int32_t SerLastError(int32_t com) { + int32_t idx = com - 1; + + if (idx < 0 || idx >= RS232_NUM_PORTS) { + return RS232_ERR_INVALID_PORT; + } + + return sSerAttach[idx].lastError; +} + + +// The SER_LINE_ errors (overrun, parity, framing, break) the port saw +// on received data when it last looked; 0 for none or a bad port. +int32_t SerLineErrors(int32_t com) { + int32_t lsr = serValueGet(&sSerApi.getLsr, com); + + return (lsr > 0) ? (lsr & SER_LINE_ERRORS) : 0; +} + + // A modem line of an open port as BASIC's True / False (False on an error // or a port that is not open). get points at the sSerApi field, read // after serResolveApi has filled it. @@ -7090,14 +8641,25 @@ static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on) { } +// Remember an rs232 result as the port's last error and return it. +static int32_t serNote(int32_t com, int32_t rc) { + int32_t idx = com - 1; + + if (idx >= 0 && idx < RS232_NUM_PORTS) { + sSerAttach[idx].lastError = rc; + } + + return rc; +} + + int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) { if (!serResolveApi()) { return 0; } - char parityChar = (parity && parity[0]) ? parity[0] : 'N'; - int32_t idx = com - 1; - int32_t rc = sSerApi.open(idx, baud, dataBits, parityChar, stopBits, handshake); + int32_t idx = com - 1; + int32_t rc = serNote(com, sSerApi.open(idx, baud, dataBits, serParity(parity), stopBits, handshake)); if (rc != 0) { return 0; @@ -7113,19 +8675,20 @@ int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, } -const char *SerRead(int32_t com) { - if (!serResolveApi()) { - return ""; +// A BASIC parity argument ("N", "E", ...; "" = "N") as the rs232 letter. +static char serParity(const char *parity) { + return (parity && parity[0]) ? parity[0] : 'N'; +} + + +// What arrived (a BINARY STRING: every byte value), "" when nothing has. +const uint8_t *SerRead(int32_t com, int32_t *outLen) { + static uint8_t buf[SER_RECV_BUF]; + + if (serResolveApi()) { + *outLen = sSerApi.read(com - 1, (char *)buf, (int)sizeof(buf)); } - static char buf[SER_RECV_BUF]; - int32_t n = sSerApi.read(com - 1, buf, (int)(sizeof(buf) - 1)); - - if (n <= 0) { - return ""; - } - - buf[n] = '\0'; return buf; } @@ -7155,11 +8718,34 @@ static bool serResolveApi(void) { sSerApi.getRts = dlsym(NULL, DVX_SYM("rs232GetRts")); sSerApi.setDtr = dlsym(NULL, DVX_SYM("rs232SetDtr")); sSerApi.setRts = dlsym(NULL, DVX_SYM("rs232SetRts")); + sSerApi.clearTxBuffer = dlsym(NULL, DVX_SYM("rs232ClearTxBuffer")); + sSerApi.getBps = dlsym(NULL, DVX_SYM("rs232GetBps")); + sSerApi.getData = dlsym(NULL, DVX_SYM("rs232GetData")); + sSerApi.getHandshake = dlsym(NULL, DVX_SYM("rs232GetHandshake")); + sSerApi.getLsr = dlsym(NULL, DVX_SYM("rs232GetLsr")); + sSerApi.getMsr = dlsym(NULL, DVX_SYM("rs232GetMsr")); + sSerApi.getParity = dlsym(NULL, DVX_SYM("rs232GetParity")); + sSerApi.getStop = dlsym(NULL, DVX_SYM("rs232GetStop")); + sSerApi.getTxBuffered = dlsym(NULL, DVX_SYM("rs232GetTxBuffered")); + sSerApi.set = dlsym(NULL, DVX_SYM("rs232Set")); + sSerApi.setBreak = dlsym(NULL, DVX_SYM("rs232SetBreak")); return sSerApi.open != NULL; } +// Change the speed, data bits, parity, stop bits and handshake of an +// open port (SerOpen's arguments). True when done; SerLastError says why +// not. +int32_t SerSet(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) { + if (!serResolveApi() || !sSerApi.set) { + return 0; + } + + return (serNote(com, sSerApi.set(com - 1, baud, dataBits, serParity(parity), stopBits, handshake)) == RS232_SUCCESS) ? SER_BASIC_TRUE : 0; +} + + void SerSetBase(int32_t com, int32_t base) { if (!serResolveApi() || !sSerApi.setBase) { return; @@ -7169,6 +8755,14 @@ void SerSetBase(int32_t com, int32_t base) { } +// Hold the transmit line in the break state (on <> 0) or release it. +void SerSetBreak(int32_t com, int32_t on) { + if (serResolveApi() && sSerApi.setBreak) { + sSerApi.setBreak(com - 1, on != 0); + } +} + + void SerSetDtr(int32_t com, int32_t on) { serLineSet(&sSerApi.setDtr, com, on); } @@ -7212,18 +8806,34 @@ static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len) { } -int32_t SerWrite(int32_t com, const char *data) { +// Bytes waiting to be sent (an SER_ERR_ code when the port is not open). +int32_t SerTxPending(int32_t com) { + return serValueGet(&sSerApi.getTxBuffered, com); +} + + +// A number an rs232 getter gives for a port (its negative RS232_ERR_ +// code on an error); get points at the sSerApi field. +static int32_t serValueGet(int32_t (*const *get)(int32_t), int32_t com) { + if (!serResolveApi() || !*get) { + return RS232_ERR_UNKNOWN; + } + + return (*get)(com - 1); +} + + +// Write data (a BINARY STRING: every byte value). +int32_t SerWrite(int32_t com, const uint8_t *data, int32_t len) { if (!serResolveApi() || !data) { return 0; } - int32_t len = (int32_t)strlen(data); - if (len == 0) { return -1; } - int32_t rc = sSerApi.writeBuf(com - 1, data, len); + int32_t rc = sSerApi.writeBuf(com - 1, (const char *)data, len); return (rc >= 0) ? -1 : 0; } @@ -7251,7 +8861,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val case CTRL_PROP_MINWIDTH: { int32_t old = w->minW; - w->minW = wgtPixels(sizeFromValue(value)); + w->minW = sizeTagFromValue(value); wgtInvalidate(w); resizeIfaceBuffer(ctrl); @@ -7266,7 +8876,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val case CTRL_PROP_MINHEIGHT: { int32_t old = w->minH; - w->minH = wgtPixels(sizeFromValue(value)); + w->minH = sizeTagFromValue(value); wgtInvalidate(w); resizeIfaceBuffer(ctrl); @@ -7277,19 +8887,15 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val break; } - case CTRL_PROP_MAXWIDTH: { - int32_t mw = sizeFromValue(value); - w->maxW = mw > 0 ? wgtPixels(mw) : 0; + case CTRL_PROP_MAXWIDTH: + w->maxW = sizeTagFromValue(value); wgtInvalidate(w); break; - } - case CTRL_PROP_MAXHEIGHT: { - int32_t mh = sizeFromValue(value); - w->maxH = mh > 0 ? wgtPixels(mh) : 0; + case CTRL_PROP_MAXHEIGHT: + w->maxH = sizeTagFromValue(value); wgtInvalidate(w); break; - } case CTRL_PROP_WEIGHT: w->weight = (int32_t)basValToNumber(value); @@ -7327,16 +8933,12 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val break; } - case CTRL_PROP_TOOLTIPTEXT: { + case CTRL_PROP_TOOLTIPTEXT: // wgtSetTooltip references ctrl->tooltip, which must outlive // the widget (freed by freeControl). - BasStringT *s = basValFormatString(value); - free(ctrl->tooltip); - ctrl->tooltip = (s->len > 0) ? strdup(s->data) : NULL; + setHeapString(&ctrl->tooltip, value); wgtSetTooltip(w, ctrl->tooltip); - basStringUnref(s); break; - } case CTRL_PROP_CONTEXTMENU: { // Set the auto-right-click menu. Empty string clears it. @@ -7364,16 +8966,26 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val ctrl->dragMode = ((int32_t)basValToNumber(value) == BAS_DRAGMODE_AUTOMATIC) ? BAS_DRAGMODE_AUTOMATIC : BAS_DRAGMODE_MANUAL; break; - case CTRL_PROP_DRAGICON: { - BasStringT *s = basValFormatString(value); - free(ctrl->dragIcon); - ctrl->dragIcon = (s->len > 0) ? strdup(s->data) : NULL; - basStringUnref(s); + case CTRL_PROP_DRAGICON: + setHeapString(&ctrl->dragIcon, value); + break; + + case CTRL_PROP_TAG: + setHeapString(&ctrl->tag, value); + break; + + case CTRL_PROP_MOUSEPOINTER: + setMousePointer(sFormRt, &ctrl->mousePointer, value); + break; + + case CTRL_PROP_CONTAINER: + // A failed move has reported its own error + basFormRtMoveControl(sFormRt, ctrl, (value.type == BAS_TYPE_OBJECT) ? value.objVal : NULL, NULL); break; - } case CTRL_PROP_NAME: case CTRL_PROP_LISTCOUNT: + case CTRL_PROP_PARENT: case CTRL_PROP_COUNT: // Read-only: let basFormRtSetProp report it. return false; @@ -7391,6 +9003,31 @@ static void setControlString(char *dst, int32_t dstSize, BasValueT value) { } +// Form.Icon = file$ or LoadPicture(file$): the picture of the minimized +// window ("" or LoadPicture() puts DVX's back). +static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value) { + const char *picPath = basFormRtPicturePath(rt, value); + BasStringT *text = picPath ? basStringNew(picPath, (int32_t)strlen(picPath)) : basValFormatString(value); + + if (text->len == 0) { + frm->iconPath[0] = '\0'; + + if (frm->window) { + wmSetIconData(frm->window, NULL, 0, 0, 0); + } + } else if (frm->window && dvxSetWindowIcon(rt->ctx, frm->window, text->data) != 0) { + basFormRtRuntimeError(rt, + "Icon not loaded", + "Form: %s\nFile: %s\nIcon takes a picture file DVX can read (.bmp, .png, .gif ...).", + frm->name, text->data); + } else { + snprintf(frm->iconPath, sizeof(frm->iconPath), "%s", text->data); + } + + basStringUnref(text); +} + + // FormName.Prop = value: window geometry/state plus the BasFormT flags. static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value) { WindowT *win = frm->window; @@ -7511,8 +9148,33 @@ static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, Bas setControlString(frm->helpTopic, sizeof(frm->helpTopic), value); break; + case FORM_PROP_PADDING: + case FORM_PROP_SPACING: + contentBoxProp(rt, frm, pd->name, &value); + break; + + case FORM_PROP_WINDOWSTATE: + setWindowState(rt, frm, num); + break; + + case FORM_PROP_DESIGNMODE: + case FORM_PROP_DESIGNSELECTION: + basFormRtDesignSetFormProp(rt, frm, pd->name, value); + break; + + case FORM_PROP_ICON: + setFormIcon(rt, frm, value); + break; + + case FORM_PROP_TAG: + case FORM_PROP_MOUSEPOINTER: + setCommonProp(&frm->formCtrl, pd->name, value); + break; + case FORM_PROP_NAME: case FORM_PROP_LAYOUT: + case FORM_PROP_CONTROLS: + case FORM_PROP_ACTIVECONTROL: case FORM_PROP_COUNT: // Read-only: rejected by the writable check above. break; @@ -7520,8 +9182,21 @@ static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, Bas } -// Look up a property in the interface descriptor and call its -// setter, marshaling the BasValueT to the native type. +// A heap-owned text property (Tag, ToolTipText, DragIcon): the value's +// text, NULL for "". +static void setHeapString(char **dst, BasValueT value) { + BasStringT *s = basValFormatString(value); + + free(*dst); + *dst = (s->len > 0) ? strdup(s->data) : NULL; + basStringUnref(s); +} + + +// Look up a property in the interface descriptor and write it through +// libdvx, the value converted with the BASIC rules to the property's type. +// An enumerated property also takes one of its value names; an unknown +// name is a program error, not a silent zero. static bool setIfaceProp(BasFormRtT *rt, const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value) { const WgtPropDescT *p = wgtIfaceFindProp(iface, propName); @@ -7533,47 +9208,40 @@ static bool setIfaceProp(BasFormRtT *rt, const WgtIfaceT *iface, WidgetT *w, con return true; // read-only, but recognized } - switch (p->type) { - case WGT_IFACE_STRING: { - BasStringT *s = basValFormatString(value); - ((void (*)(WidgetT *, const char *))p->setFn)(w, s->data); - basStringUnref(s); - break; - } + bool byName = p->type == WGT_IFACE_ENUM && value.type == BAS_TYPE_STRING && value.strVal; + BasStringT *hold = NULL; + WgtValueT v = wgtFromBas(value, byName ? WGT_IFACE_STRING : p->type, &hold); - case WGT_IFACE_ENUM: - if (value.type == BAS_TYPE_STRING && value.strVal) { - // Map name to index; an unknown name is a program bug, - // not a silent zero. - int32_t enumVal = ifaceEnumIndex(p, value.strVal->data); - - if (enumVal < 0) { - basFormRtRuntimeError(rt, - "Bad enum value for property", - "Property: %s\nValue: %s\nNot one of this property's named values.", - p->name, value.strVal->data); - break; - } - - ((void (*)(WidgetT *, int32_t))p->setFn)(w, enumVal); - } else { - ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value)); - } - break; - - case WGT_IFACE_INT: - ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value)); - break; - - case WGT_IFACE_BOOL: - ((void (*)(WidgetT *, bool))p->setFn)(w, basValIsTruthy(value)); - break; - - case WGT_IFACE_FLOAT: - ((void (*)(WidgetT *, float))p->setFn)(w, (float)basValToNumber(value)); - break; + if (!wgtIfaceSetProp(iface, w, p, &v) && byName) { + basFormRtRuntimeError(rt, + "Bad enum value for property", + "Property: %s\nValue: %s\nNot one of this property's named values.", + p->name, v.s); } + if (hold) { + basStringUnref(hold); + } + + return true; +} + + +// MousePointer = value into *dst; a value DVX has no pointer for is a +// runtime error (false, *dst unchanged). +static bool setMousePointer(BasFormRtT *rt, int32_t *dst, BasValueT value) { + int32_t pointer = (int32_t)basValToNumber(value); + int32_t cursor; + + if (!pointerCursor(pointer, &cursor)) { + basFormRtRuntimeError(rt, + "Invalid MousePointer", + "MousePointer: %d\nUse vbDefault (0), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11) or vbNoDrop (12).", + (int)pointer); + return false; + } + + *dst = pointer; return true; } @@ -7584,7 +9252,12 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba basFormRtRuntimeError(rt, "Clipboard has no properties", "Property: %s\nUse the Clipboard methods Clear, GetFormat, GetText and SetText.", - propName ? propName : "?"); + propName); + return; + } + + if (ctrlRef == &sScreenObj) { + screenSetProp(rt, propName, value); return; } @@ -7592,7 +9265,7 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba basFormRtRuntimeError(rt, "Picture properties are read-only", "Property: %s\nA Picture object's Width, Height and Type can only be read.", - propName ? propName : "?"); + propName); return; } @@ -7602,8 +9275,8 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba basFormRtRuntimeError(rt, "Assignment to unknown control", "Control: %s\nProperty: %s\nControl was not found on any loaded form.", - (rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?", - propName ? propName : "?"); + rt->lastLookupName[0] ? rt->lastLookupName : "?", + propName); return; } @@ -7615,12 +9288,28 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba bool fromBar = false; MenuItemT *item = resolveMenuItem(ctrl->form, ctrl->menuId, &fromBar); - const BasPropDescT *pd = (item && propName) ? basFormRtFindMenuProp(propName) : NULL; + const BasPropDescT *pd = item ? basFormRtFindMenuProp(propName) : NULL; BasMenuPropE propId = (pd && pd->writable) ? (BasMenuPropE)(pd - sMenuProps) : MENU_PROP_COUNT; if (item) { + BasMenuIdMapT *row = menuIdRow(ctrl->form, ctrl->menuId); + bool hidden = row && row->hiddenIn; + + if (propId == MENU_PROP_VISIBLE && row) { + menuItemSetVisible(rt, ctrl->form, row, basValIsTruthy(value)); + return; + } + if (propId == MENU_PROP_CHECKED) { - if (fromBar) { + // Any item can carry a check mark, as in VB: a plain one + // becomes a check item (a radio item stays radio). + if (item->type == MenuItemNormalE) { + item->type = MenuItemCheckE; + } + + if (hidden) { + item->checked = basValIsTruthy(value); + } else if (fromBar) { MenuBarT *bar = (ctrl->form->window) ? ctrl->form->window->menuBar : NULL; wmMenuItemSetChecked(bar, ctrl->menuId, basValIsTruthy(value)); } else { @@ -7637,12 +9326,32 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba return; } + if (propId == MENU_PROP_SHORTCUT && row) { + BasStringT *key = basValFormatString(value); + + menuItemShortcut(rt, ctrl->form, row, item, key->data); + basStringUnref(key); + return; + } + + // A new caption keeps the item's key text ("\tCtrl+O") + if (propId == MENU_PROP_CAPTION) { + const char *sep = strchr(item->label, BAS_MENU_KEY_SEP); + char label[sizeof(item->label)]; + BasStringT *text = basValFormatString(value); + + snprintf(label, sizeof(label), "%s%s", text->data, sep ? sep : ""); + basStringUnref(text); + wmMenuItemSetLabel(item, label); + return; + } + char valid[BAS_PROP_LIST_LEN]; formatPropNames(sMenuProps, MENU_PROP_COUNT, true, valid, sizeof(valid)); basFormRtRuntimeError(rt, "Unknown menu property", "Menu: %s\nProperty: %s\nValid: %s.", - ctrl->name, propName ? propName : "?", valid); + ctrl->name, propName, valid); return; } } @@ -7651,7 +9360,7 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba basFormRtRuntimeError(rt, "Property set on control with no widget", "Control: %s\nProperty: %s\nThe control is known but has no attached widget.", - ctrlDisplayName(ctrl), propName ? propName : "?"); + ctrlDisplayName(ctrl), propName); return; } @@ -7684,13 +9393,61 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba "Control: %s\nType: %s\nProperty: %s\nThe control has no writable property by that name.", ctrl->name, ctrl->typeName[0] ? ctrl->typeName : "?", - propName ? propName : "?"); + propName); } // A BASIC size-property value as a pixel count clamped to // [0, BAS_CTRL_MAX_DIM], so a negative or huge value can never become // a masked multi-million-pixel size hint. +// Form.WindowState = state (BAS_WINSTATE_*): a shown form's window changes +// at once (maximizing or restoring resizes it, which fires Resize); a hidden +// form keeps the state until it is shown. A minimized form that was +// maximized is maximized again when restored to vbMaximized. +static void setWindowState(BasFormRtT *rt, BasFormT *frm, int32_t state) { + WindowT *win = frm->window; + + if (state != BAS_WINSTATE_NORMAL && state != BAS_WINSTATE_MINIMIZED && state != BAS_WINSTATE_MAXIMIZED) { + basFormRtRuntimeError(rt, + "Invalid WindowState", + "Form: %s\nWindowState: %d\nUse vbNormal (0), vbMinimized (1) or vbMaximized (2).", + frm->name, (int)state); + return; + } + + if (!win) { + return; + } + + if (!win->visible) { + frm->frmWindowState = state; + return; + } + + if (state == BAS_WINSTATE_MINIMIZED) { + dvxMinimizeWindow(rt->ctx, win); + return; + } + + if (win->minimized) { + dvxRestoreWindow(rt->ctx, win); + } + + if (state == BAS_WINSTATE_MAXIMIZED) { + dvxMaximizeWindow(rt->ctx, win); + } else if (win->maximized) { + dvxRestoreWindow(rt->ctx, win); + } +} + + +// SizeChars (layout.bas): n characters wide, the size the layout gives +// n of the system font's characters. +int32_t SizeChars(int32_t n) { + return wgtChars(DVX_MIN(DVX_MAX(n, 0), BAS_CTRL_MAX_DIM)); +} + + static int32_t sizeFromValue(BasValueT value) { double v = basValToNumber(value); @@ -7706,6 +9463,64 @@ static int32_t sizeFromValue(BasValueT value) { } +// SizePercent (layout.bas): n percent of the container's size. +int32_t SizePercent(int32_t n) { + return wgtPercent(DVX_MIN(DVX_MAX(n, 0), BAS_SIZE_PERCENT_MAX)); +} + + +// A tagged size of w (MinWidth ... MaxHeight) in pixels, as the layout +// resolves it (a percentage of the container's current size). +static int32_t sizePixels(const WidgetT *w, int32_t tagged, bool across) { + const AppContextT *ctx = wgtGetContext(w); + const WidgetT *parent = w->parent; + int32_t within = parent ? (across ? parent->w : parent->h) : 0; + + return wgtResolveSize(tagged, within, ctx ? ctx->font.charWidth : FONT_CHAR_WIDTH); +} + + +// A size property's value as the widget stores it: pixels (0 to +// BAS_CTRL_MAX_DIM), or a size from SizeChars / SizePercent as it is. +static int32_t sizeTagFromValue(BasValueT value) { + double v = basValToNumber(value); + + if (v >= INT32_MIN && v <= INT32_MAX) { + uint32_t raw = (uint32_t)(int32_t)v; + uint32_t unit = raw & WGT_SIZE_TYPE_MASK; + + if (unit == WGT_SIZE_CHARS || unit == WGT_SIZE_PERCENT) { + return (int32_t)raw; + } + } + + return wgtPixels(sizeFromValue(value)); +} + + +// Split a "a|b|c" list in buf in place: ptrs receives up to maxPtrs +// entries (an empty list has none). Returns how many. +static int32_t splitPipeList(char *buf, const char **ptrs, int32_t maxPtrs) { + int32_t count = 0; + char *tok = buf; + + while (*tok && count < maxPtrs) { + char *sep = strchr(tok, '|'); + + ptrs[count++] = tok; + + if (!sep) { + break; + } + + *sep = '\0'; + tok = sep + 1; + } + + return count; +} + + int32_t SQLAffected(int32_t db) { const BasSqlCallbacksT *api = sqlApi(); @@ -7916,6 +9731,18 @@ static void sqlResolveApi(BasSqlCallbacksT *sql) { } +// Index of ctx's binding in sTermBinds, or -1. +static int32_t termBindIndex(const void *ctx) { + for (int32_t i = 0; i < (int32_t)arrlen(sTermBinds); i++) { + if (sTermBinds[i].ctx == ctx) { + return i; + } + } + + return -1; +} + + // The free half of basFormRtUnloadForm: frees the form's controls, // variables, window, and the BasFormT itself. QueryUnload/Unload have // already fired and detachFormForUnload has already removed the form @@ -7989,95 +9816,89 @@ static void upperCopy(char *dst, int32_t dstSize, const char *src) { } -bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val) { - if (!w || !p || !p->setFn || !val) { - return false; +// An untrapped error the VM raised, in module code or an event handler: +// reported through runtimeErrorEnd with the VM's own message ("Runtime +// error N on line L: text" becomes the summary and the details), which +// stays in vm->errorMsg for the host. Nothing once the runtime is +// terminated (an error basFormRtRuntimeError reported, or a follow-on). +static void vmErrorEnd(BasFormRtT *rt) { + if (rt->terminated) { + return; } - if (p->type == WGT_IFACE_ENUM) { - int32_t en = ifaceEnumIndex(p, val); + const char *msg = basVmGetError(rt->vm); + const char *sep = strstr(msg, BAS_VM_ERROR_TEXT_SEP); - if (en < 0) { - return false; - } + if (!msg[0]) { + runtimeErrorEnd(rt, BAS_ERR_SUMMARY, BAS_ERR_UNKNOWN); + } else if (sep) { + char summary[BAS_ERR_SUMMARY_LEN]; - ((void (*)(WidgetT *, int32_t))p->setFn)(w, en); - return true; + snprintf(summary, sizeof(summary), "%.*s", (int)(sep - msg), msg); + runtimeErrorEnd(rt, summary, sep + strlen(BAS_VM_ERROR_TEXT_SEP)); + } else { + runtimeErrorEnd(rt, BAS_ERR_SUMMARY, msg); } - - if (p->type == WGT_IFACE_INT) { - ((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val)); - return true; - } - - if (p->type == WGT_IFACE_BOOL) { - ((void (*)(WidgetT *, bool))p->setFn)(w, frmParseBool(val)); - return true; - } - - if (p->type == WGT_IFACE_STRING) { - ((void (*)(WidgetT *, const char *))p->setFn)(w, val); - return true; - } - - if (p->type == WGT_IFACE_FLOAT) { - ((void (*)(WidgetT *, float))p->setFn)(w, (float)strtod(val, NULL)); - return true; - } - - return false; } -bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize) { - if (!w || !p || !p->getFn || !out || outSize <= 0) { - return false; - } +// A BASIC value as a widget value of type want, with the BASIC +// conversions (basValToNumber, basValIsTruthy, basValFormatString). +// WGT_IFACE_NONE keeps the value's own kind (a dynamic method's +// arguments): String, Boolean, whole numbers INT, Single FLOAT, other +// numbers DOUBLE. +// A STRING's text is held by *hold (unref it after the call; NULL when +// nothing is held). +static WgtValueT wgtFromBas(BasValueT v, uint8_t want, BasStringT **hold) { + *hold = NULL; - if (p->type == WGT_IFACE_ENUM && p->enumNames) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); - const char *name = NULL; - - for (int32_t en = 0; p->enumNames[en]; en++) { - if (en == v) { - name = p->enumNames[en]; + if (want == WGT_IFACE_NONE) { + switch (v.type) { + case BAS_TYPE_STRING: + want = WGT_IFACE_STRING; + break; + + case BAS_TYPE_BOOLEAN: + want = WGT_IFACE_BOOL; + break; + + case BAS_TYPE_INTEGER: + case BAS_TYPE_LONG: + case BAS_TYPE_BYTE: + want = WGT_IFACE_INT; + break; + + case BAS_TYPE_SINGLE: + want = WGT_IFACE_FLOAT; + break; + + default: + want = WGT_IFACE_DOUBLE; break; - } } + } - if (!name) { - return false; + switch (want) { + case WGT_IFACE_STRING: + *hold = basValFormatString(v); + return wgtValueStr((*hold)->data); + + case WGT_IFACE_BOOL: + return wgtValueBool(basValIsTruthy(v)); + + case WGT_IFACE_FLOAT: + return wgtValueFloat(basValToNumber(v)); + + case WGT_IFACE_DOUBLE: + return wgtValueDouble(basValToNumber(v)); + + default: { + WgtValueT out = wgtValueInt((int32_t)basValToNumber(v)); + + out.type = want; + return out; } - - snprintf(out, outSize, "%s", name); - return true; } - - if (p->type == WGT_IFACE_INT) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); - snprintf(out, outSize, "%d", (int)v); - return true; - } - - if (p->type == WGT_IFACE_BOOL) { - bool v = ((bool (*)(const WidgetT *))p->getFn)(w); - snprintf(out, outSize, "%s", v ? "True" : "False"); - return true; - } - - if (p->type == WGT_IFACE_STRING) { - const char *s = ((const char *(*)(const WidgetT *))p->getFn)(w); - snprintf(out, outSize, "%s", s ? s : ""); - return true; - } - - if (p->type == WGT_IFACE_FLOAT) { - float v = ((float (*)(const WidgetT *))p->getFn)(w); - snprintf(out, outSize, "%g", (double)v); - return true; - } - - return false; } @@ -8096,6 +9917,41 @@ static bool widgetHasAncestor(const WidgetT *w, const WidgetT *ancestor) { // Wires the full set of widget event callbacks to the runtime dispatchers and // links the widget back to its BasControlT. Shared by the two dynamic // create-control paths and the .frm loader so every control behaves the same. +// True when win (with id winId) is still a window on the stack: the +// pointer is compared first (a destroyed window may be freed), then the +// id (a new window can reuse the address). +static bool windowLive(const AppContextT *ctx, const WindowT *win, int32_t winId) { + for (int32_t i = 0; win && i < ctx->stack.count; i++) { + if (ctx->stack.windows[i] == win) { + return win->id == winId; + } + } + + return false; +} + + +// Form.WindowState: the state a hidden form will be shown in when one was +// set, else its window's state. +static int32_t windowStateOf(const BasFormT *frm) { + const WindowT *win = frm->window; + + if (!win) { + return BAS_WINSTATE_NORMAL; + } + + if (!win->visible && frm->frmWindowState != BAS_WINSTATE_NONE) { + return frm->frmWindowState; + } + + if (win->minimized) { + return BAS_WINSTATE_MINIMIZED; + } + + return win->maximized ? BAS_WINSTATE_MAXIMIZED : BAS_WINSTATE_NORMAL; +} + + static void wireWidgetEvents(WidgetT *w, BasControlT *ctrl) { w->userData = ctrl; w->onClick = onWidgetClick; @@ -8111,6 +9967,7 @@ static void wireWidgetEvents(WidgetT *w, BasControlT *ctrl) { w->onMouseUp = onWidgetMouseUp; w->onMouseMove = onWidgetMouseMove; w->onScroll = onWidgetScroll; + w->onEvent = onWidgetEvent; } diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h index bdc68ac8..7b77a60e 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h @@ -38,6 +38,8 @@ #include "dvxApp.h" #include "dvxWgt.h" #include "frmParser.h" +#include "../compiler/basEvents.h" +#include "../basRes.h" // ============================================================ // Forward declarations @@ -54,6 +56,11 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f // Form and control names share the single identifier length BAS_MAX_IDENT // (vm.h) with the compiler, so name-keyed lookups never truncate. +// Longest menu Shortcut / ShortcutText ("Ctrl+Alt+Shift+Backspace"), and +// what separates it from the caption in an item's label ("&Open\tCtrl+O"). +#define BAS_MENU_KEY_TEXT_MAX 32 +#define BAS_MENU_KEY_SEP '\t' + // Window size for a form that gives no Width/Height; the IDE designer // uses the same values for a new form. #define BAS_DEFAULT_FORM_W 400 @@ -63,6 +70,7 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f // "WrapBox" are the box widgets' BASIC names). #define BAS_LAYOUT_VBOX "VBox" #define BAS_PICTUREBOX_TYPE "PictureBox" // takes the drawing members, as forms do +#define BAS_TERMINAL_TYPE "Terminal" // libraries bind byte streams to it (basFormRtTermBind) // ============================================================ // Property names @@ -76,14 +84,21 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f // and the data-binding properties the runtime reads from widget // interfaces are listed here too. +#define BAS_PROP_ACTIVECONTROL BAS_FORM_ACTIVECONTROL #define BAS_PROP_AUTOSIZE "AutoSize" #define BAS_PROP_BACKCOLOR "BackColor" +#define BAS_PROP_CANCEL "Cancel" #define BAS_PROP_CAPTION "Caption" #define BAS_PROP_CENTERED "Centered" #define BAS_PROP_CHECKED "Checked" +#define BAS_PROP_CONTAINER BAS_CTRL_CONTAINER #define BAS_PROP_CONTEXTMENU "ContextMenu" +#define BAS_PROP_CONTROLS BAS_FORM_CONTROLS #define BAS_PROP_DATAFIELD "DataField" #define BAS_PROP_DATASOURCE "DataSource" +#define BAS_PROP_DEFAULT "Default" +#define BAS_PROP_DESIGNMODE "DesignMode" +#define BAS_PROP_DESIGNSELECTION BAS_FORM_DESIGNSEL #define BAS_PROP_DETAILFIELD "DetailField" #define BAS_PROP_DRAGICON "DragIcon" #define BAS_PROP_DRAGMODE "DragMode" @@ -91,6 +106,7 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f #define BAS_PROP_FORECOLOR "ForeColor" #define BAS_PROP_HEIGHT "Height" #define BAS_PROP_HELPTOPIC "HelpTopic" +#define BAS_PROP_ICON BAS_FORM_ICON #define BAS_PROP_INDEX "Index" #define BAS_PROP_KEYCOLUMN "KeyColumn" #define BAS_PROP_LAYOUT "Layout" @@ -102,13 +118,20 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f #define BAS_PROP_MAXWIDTH "MaxWidth" #define BAS_PROP_MINHEIGHT "MinHeight" #define BAS_PROP_MINWIDTH "MinWidth" -#define BAS_PROP_NAME "Name" +#define BAS_PROP_MOUSEPOINTER "MousePointer" +#define BAS_PROP_NAME BAS_CTRL_NAME +#define BAS_PROP_PADDING "Padding" +#define BAS_PROP_PARENT BAS_CTRL_PARENT #define BAS_PROP_PICTURE "Picture" #define BAS_PROP_RADIOCHECK "RadioCheck" #define BAS_PROP_READONLY "ReadOnly" #define BAS_PROP_RECORDSOURCE "RecordSource" #define BAS_PROP_RESIZABLE "Resizable" +#define BAS_PROP_SHORTCUT "Shortcut" +#define BAS_PROP_SHORTCUTTEXT "ShortcutText" +#define BAS_PROP_SPACING "Spacing" #define BAS_PROP_TABINDEX "TabIndex" +#define BAS_PROP_TAG "Tag" #define BAS_PROP_TEXT "Text" #define BAS_PROP_TOOLTIPTEXT "ToolTipText" #define BAS_PROP_TOP "Top" @@ -116,6 +139,7 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f #define BAS_PROP_VISIBLE "Visible" #define BAS_PROP_WEIGHT "Weight" #define BAS_PROP_WIDTH "Width" +#define BAS_PROP_WINDOWSTATE "WindowState" // ============================================================ // Property descriptors @@ -130,11 +154,15 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f typedef struct { const char *name; - uint8_t type; // WGT_IFACE_STRING / WGT_IFACE_INT / WGT_IFACE_BOOL + uint8_t type; // WGT_IFACE_STRING / WGT_IFACE_INT / WGT_IFACE_BOOL / BAS_PROP_OBJECT bool readable; bool writable; } BasPropDescT; +// BasPropDescT.type of an object-valued property (Controls, Parent, +// Container): never in a .frm file. +#define BAS_PROP_OBJECT 0x10 + // Form-object properties (FormName.Prop), in table order. const BasPropDescT *basFormRtFormProps(int32_t *count); @@ -169,6 +197,7 @@ typedef enum { BAS_CM_ADDMENUITEM, BAS_CM_ADDMENUSEPARATOR, BAS_CM_ADDSUBMENU, + BAS_CM_REMOVEMENUITEM, BAS_CM_DESTROYMENU, BAS_CM_ADDBUTTON, BAS_CM_ADDTEXTBUTTON, @@ -176,6 +205,11 @@ typedef enum { BAS_CM_CLEAR, BAS_CM_BUTTONCOUNT, BAS_CM_DRAG, + BAS_CM_MOVEBEFORE, + BAS_CM_ADDMENU, + BAS_CM_REMOVEMENU, + BAS_CM_ADDMENURADIOITEM, + BAS_CM_SCROLLTOCONTROL, BAS_CM_COUNT } BasCommonMethodE; @@ -184,6 +218,119 @@ typedef enum { // Case-insensitive lookup; BAS_CM_NONE when name is not a common method. int32_t basFormRtCommonMethodId(const char *name); +// The name of common method id (BasCommonMethodE), NULL past the end. +const char *basFormRtCommonMethodName(int32_t id); + +// The form object's own methods (besides the common and drawing ones). +#define BAS_FORM_METHOD_SHOW "Show" +#define BAS_FORM_METHOD_HIDE "Hide" +#define BAS_FORM_METHOD_FITWIDTH "FitWidth" +#define BAS_FORM_METHOD_FITHEIGHT "FitHeight" +#define BAS_FORM_METHOD_SAVESCREENSHOT "SaveScreenshot" + +// The form object's own methods (above): the idx-th (NULL past the end), +// how many, and whether name is one of them. +const char *basFormRtFormMethodAt(int32_t idx); +int32_t basFormRtFormMethodCount(void); +bool basFormRtIsFormMethod(const char *name); + +// ============================================================ +// Control types (formDesign.c) +// ============================================================ +// +// The BASIC control types as a form designer sees them: the widget +// interfaces with a BASIC name, plus "Form" (FRM_KW_FORM, the form +// object) and "Menu" (FRM_KW_MENU, a menu item) for properties, methods +// and events. The C IDE's toolbox, properties window and event lists +// and designer.bas (the Ctl* functions) read the same answers. + +// One property of a control type. +typedef struct { + const char *name; + const char * const *enumNames; // WGT_IFACE_ENUM: NULL-terminated value names, else NULL + uint8_t type; // WGT_IFACE_* or BAS_PROP_OBJECT + bool readable; + bool writable; +} BasCtlPropT; + +// designer.bas values: CtlTypeFlags bits (-1: no such type) and +// CtlPropType's flags over the value type (-1: no such property). +#define BAS_CTL_TYPE_UNKNOWN (-1) +#define BAS_CTL_FLAG_CONTAINER 1 +#define BAS_CTL_FLAG_DESIGNABLE 2 +#define BAS_CTL_PROP_NONE (-1) +#define BAS_CTL_PROP_READONLY 0x100 +#define BAS_CTL_PROP_WRITEONLY 0x200 + +// A widget file's toolbox resources: BAS_CTL_RES_* (basRes.h). + +const char *basCtlDefaultEvent(const char *typeName); +const char *basCtlEventAt(const char *typeName, int32_t idx); +const char *basCtlEventParams(const char *typeName, const char *eventName); +const WgtIfaceT *basCtlIface(const char *typeName); +bool basCtlIsContainer(const char *typeName); +const char *basCtlMethodAt(const char *typeName, int32_t idx); +const char *basCtlNamePrefix(const char *typeName); +bool basCtlPropAt(const char *typeName, int32_t idx, BasCtlPropT *out); +bool basCtlPropFind(const char *typeName, const char *propName, BasCtlPropT *out); +const char *basCtlResPath(const char *typeName, const char *resBase, char *resName, int32_t resNameSize); + +// ============================================================ +// Design surface (formDesign.c) +// ============================================================ +// +// A designer window (the IDE's form designer, basDsgnSetDesignWindow) or +// a form in Form.DesignMode is a design window: the UserControls on it +// see UserMode False (their code draws, the user's input goes to the +// designer). +// +// The 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 IDE's designer and Form.DesignMode share the geometry, +// hit test and painting. + +#define BAS_DSGN_HANDLE_SIZE 6 + +typedef enum { + BAS_DSGN_HANDLE_NONE = -1, + BAS_DSGN_HANDLE_NW = 0, + BAS_DSGN_HANDLE_N, + BAS_DSGN_HANDLE_NE, + BAS_DSGN_HANDLE_E, // resize width + BAS_DSGN_HANDLE_SE, // resize both + BAS_DSGN_HANDLE_S, // resize height + BAS_DSGN_HANDLE_SW, + BAS_DSGN_HANDLE_W, + BAS_DSGN_HANDLE_COUNT +} BasDsgnHandleE; + +// DesignMouse's Action and the argument counts of DesignMouse / +// DesignKey / DesignPropertyChanged. +#define BAS_DSGN_MOUSE_DOWN 1 +#define BAS_DSGN_MOUSE_MOVE 2 +#define BAS_DSGN_MOUSE_UP 3 +#define BAS_DSGN_MOUSE_DBLCLICK 4 +#define BAS_DSGN_DBLCLICK_COUNT 2 // multiClickDetect count of a double click +#define BAS_DSGN_MOUSE_ARGS 6 +#define BAS_DSGN_KEY_ARGS 2 +#define BAS_DSGN_PROPCHG_ARGS 2 + +int32_t basDsgnCursorAt(const WidgetT *w, int32_t x, int32_t y); +int32_t basDsgnHandleAt(const WidgetT *w, int32_t x, int32_t y); +void basDsgnHandleOrigin(const WidgetT *w, int32_t h, int32_t *outX, int32_t *outY); +bool basDsgnHandleResizes(int32_t h); +bool basDsgnIsDesignWindow(const WindowT *win); +void basDsgnPaintHandles(AppContextT *ctx, WindowT *win, const WidgetT *w); +void basDsgnSetDesignWindow(WindowT *win, bool on); +bool basDsgnWidgetHit(const WidgetT *w, int32_t x, int32_t y); + +// PropertyChanged "propName" in the code of the UserControl whose widget w +// is (NULL: none): a form in Form.DesignMode holding it raises +// DesignPropertyChanged (CtrlName, PropertyName), so a designer written in +// BASIC reads its property sheet again (the C designer has +// basUcSetPropChangedHook). +void basDsgnPropertyChanged(const WidgetT *w, const char *propName); + // ============================================================ // Menu ID to name mapping for event dispatch // ============================================================ @@ -191,10 +338,16 @@ int32_t basFormRtCommonMethodId(const char *name); typedef struct { int32_t id; char name[BAS_MAX_IDENT]; - BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored) + BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored) + MenuT *hiddenIn; // Visible = False: the menu the item was taken out of (NULL: shown) + MenuItemT hiddenItem; // the item while it is out of its menu + char shortcut[BAS_MENU_KEY_TEXT_MAX]; // Shortcut: the key that runs it ("" = none) } BasMenuIdMapT; -// Named popup / context menu owned by a form. +// Named menu of a form: a popup / context menu (from the .frm or +// CreateMenu), or, without ownsMenu, a submenu or a menu-bar menu, so the +// menu methods (AddMenuItem, AddMenuSeparator, AddSubMenu, PopupMenu) can +// name it. typedef struct { char name[BAS_MAX_IDENT]; MenuT *menu; // wmCreateMenu root, or a submenu owned by its root @@ -239,6 +392,8 @@ typedef struct BasControlT { // while Click is still running) is allowed through. bool eventFiring; char firingEventName[BAS_MAX_IDENT]; + char *tag; // Tag: heap-owned text (NULL = "") + int32_t mousePointer; // MousePointer (BAS_POINTER_*; BAS_POINTER_DEFAULT: the widget's own) } BasControlT; // ============================================================ @@ -264,6 +419,10 @@ typedef struct BasFormT { bool frmCentered; bool frmAutoSize; char frmLayout[32]; // "VBox", "HBox", or "WrapBox" + int32_t frmPadding; // .frm Padding / Spacing (0 = the layout's default) + int32_t frmSpacing; + int32_t frmWindowState; // WindowState set while hidden (.frm or code): applied when shown + int32_t menuIdNext; // id of the next menu item made (ids grow in menu order) char helpTopic[BAS_MAX_IDENT]; // form-level help topic // Per-form variable storage (allocated at load, freed at unload) BasValueT *formVars; @@ -291,9 +450,14 @@ typedef struct BasFormT { // drop has taken them over (see dragHookForms). void (*dragPrevOnMouse)(WindowT *win, int32_t x, int32_t y, int32_t buttons); void (*dragPrevOnKey)(WindowT *win, int32_t key, int32_t mod); + // Keys of the menu items that have a Shortcut (the window's + // accelerator table; NULL when none has one). + AccelTableT *accel; // Synthetic control entry for the form itself, so that // FormName.Property works through the same getProp/setProp path. BasControlT formCtrl; + // Icon: the picture file shown for the minimized window ("" = DVX's) + char iconPath[DVX_MAX_PATH]; } BasFormT; // ============================================================ @@ -388,6 +552,17 @@ typedef struct { // (stb_ds array of heap objects; the object is that pointer), kept // until the runtime is destroyed. BasPictureT **pictures; + // Screen.MousePointer = vbHourglass is in force: the busy cursor is + // put back to the arrow when the runtime is destroyed. + bool pointerBusy; + // New launch arguments (basFormRtNewArgs) not yet given to the + // program: held while a handler, dialog box or modal form is running + // and given (COMMAND$ and NewArgs) when it is done. Only the latest + // is kept (NULL: none). + char *pendingArgs; + // Screen.MousePointer when it is not vbHourglass: the pointer over + // every window of the program (BAS_POINTER_DEFAULT: each control's). + int32_t screenPointer; } BasFormRtT; // ============================================================ @@ -412,6 +587,17 @@ void basFormRtRunSimple(BasFormRtT *rt); // Destroy the form runtime and all forms/controls. void basFormRtDestroy(BasFormRtT *rt); +// Make rt the runtime the library-wide externs (dialogs, help, controls +// found by name) act for, as event dispatch does while a program's code +// runs. Returns the previous one. basFormRtCreate makes the new runtime +// current only when no program's code is running, so a runtime created +// from inside another program's call leaves that program current. +BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt); + +// The runtime the library-wide externs act for now (NULL: none), e.g. +// the program whose DECLARE LIBRARY call is running. +BasFormRtT *basFormRtCurrent(void); + // True while the program is alive for its host's event loop: a form is // loaded or the program's screen window is open, and it was not stopped // (runtime error, screen window closed). @@ -440,6 +626,17 @@ void basFormRtTestReset(void); // Wire up the VM's UI callbacks to this form runtime. void basFormRtBindVm(BasFormRtT *rt); +// New launch arguments for the running program (the shell's onNewArgs: +// F1 hands the running Help Viewer "file [topic]"): COMMAND$ becomes args +// and NewArgs fires on every loaded form that handles it. While a +// handler, dialog box or modal form is running they are held (only the +// latest is kept) and given once it is done. +void basFormRtNewArgs(BasFormRtT *rt, const char *args); + +// True when a loaded form handles NewArgs (the stub then lets the shell +// hand the program new arguments instead of starting another copy). +bool basFormRtWantsNewArgs(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 @@ -447,11 +644,44 @@ void basFormRtBindVm(BasFormRtT *rt); // compiled-program stub both use it, so F1 follows the same rules. bool basFormRtHelpTopic(const BasFormRtT *rt, char *topic, int32_t topicSize); +// The label a menu item shows (the runtime and the IDE designer build it +// the same way): caption, then a tab and the key text -- shortcutText as +// written when it is not empty, else the Shortcut key as dvxAccelFormat +// writes it. *outKey / *outMods receive the Shortcut key for the window's +// accelerator table (0 when there is none). False when shortcut is +// neither empty nor a key (the label then has no key text). +bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods); + // Report a non-recoverable runtime error: log, halt the VM, mark the // 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. An +// untrapped error the VM raises -- in module code or in an event handler +// -- ends the program through the same path (VB shows it and ends). void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...); +// ---- Terminal controls for the BASIC functions of libraries ---- +// +// A library binds its byte stream to a Terminal control the way SerAttach +// and CommAttach do (the connection library's ConnAttach, for one; it +// finds these at run time). basFormRtTermBind finds the Terminal named +// ctrlName on the running program's forms, undoes whatever fed it before +// (a serial port, a packet link, another binding) and ctx's own earlier +// binding, and sends what is typed there to writeFn(ctx, ...). It +// returns the widget, for the library to write what arrives to, or NULL +// when the program has no Terminal of that name. The binding lasts until +// the library ends it with basFormRtTermUnbind (the widget stops calling +// writeFn) or the control goes away (its form unloads, it is removed, or +// another binding takes it): then onUnbind(ctx) tells the library to +// forget the widget. basFormRtTermForget drops a binding without +// touching its widget, for a library whose program was ended (its +// windows are gone already). +typedef int32_t (*BasTermWriteFnT)(void *ctx, const uint8_t *data, int32_t len); +typedef void (*BasTermUnbindFnT)(void *ctx); + +WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind); +void basFormRtTermForget(void *ctx); +void basFormRtTermUnbind(void *ctx); + // ---- UI callback implementations (match BasUiCallbacksT) ---- BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc); @@ -463,6 +693,7 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName); void basFormRtHideForm(void *ctx, void *formRef); void *basFormRtLoadForm(void *ctx, const char *formName); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title); +void *basFormRtNewForm(void *ctx, const char *formName); void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); void basFormRtShowForm(void *ctx, void *formRef, bool modal); const char *basFormRtTypeName(void *ctx, void *objRef); @@ -472,21 +703,52 @@ void basFormRtUnloadForm(void *ctx, void *formRef); void *basExternResolve(void *ctx, const char *libName, const char *funcName); -// Native cdecl trampoline behind basExternCall. basNativeDos.c carries -// the i386 asm; basNativeHost.c is the 64-bit host stub that calls -// 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); -uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult); +// Native call trampoline behind basExternCall. basNativeDos.c carries +// the i386 cdecl asm; basNativeHost.c is the 64-bit host build, which +// records the words and returns a set result, or (basNativeHostSetLive) +// makes the call with the x86-64 System V convention. Selected by the +// Makefile object list. +// +// The arguments are cdecl stack words in i386 layout: a pointer or an +// integer in one word, a DOUBLE / DATE or a 64-bit integer (CURRENCY) +// in two (low half first), a SINGLE in one word holding the float bits. +// A word is pointer-sized so a string pointer survives on the host; its +// kind says what it holds (the i386 trampoline ignores it, the host one +// needs it to pass each value in the right register). +typedef uintptr_t BasNativeWordT; -// Widest cdecl argument (DOUBLE) in 32-bit stack words, and the most -// words one extern call can marshal. +typedef enum { + BasNativeIntE, // integer or pointer + BasNativeSingleE, // float bits + BasNativeDoubleLoE, // double bits, low half (the high half follows) + BasNativeDoubleHiE, + BasNativeInt64LoE, // 64-bit integer, low half (the high half follows) + BasNativeInt64HiE +} BasNativeKindE; + +// What the callee returns: EAX / RAX, or a floating-point register. +typedef enum { + BasNativeRetIntE, + BasNativeRetDoubleE, + BasNativeRetSingleE +} BasNativeRetE; + +BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask); +BasNativeWordT basNativeCall(void *funcPtr, const BasNativeWordT *words, const uint8_t *kinds, int32_t count, BasNativeRetE ret, double *fpResult); + +// Widest cdecl argument (DOUBLE, BINARY STRING) in 32-bit stack words, +// the words a BINARY STRING result adds (its int32_t * length), 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) +#define BAS_EXTERN_RESULT_WORDS 1 +#define BAS_EXTERN_MAX_NATIVE_WORDS (BAS_VM_MAX_CALL_ARGS * BAS_EXTERN_MAX_WORDS_PER_ARG + BAS_EXTERN_RESULT_WORDS) // Host build only (basNativeHost.c): inspect the words the last -// basNativeCall was handed, and set the result the next calls return. +// basNativeCall was handed, set the result the next calls return, or +// make the calls for real (a test that runs a program using libraries). #ifdef DVX_HOST_TEST -int32_t basNativeHostLastArgs(uint32_t *out, int32_t maxCount); +int32_t basNativeHostLastArgs(BasNativeWordT *out, int32_t maxCount); +void basNativeHostSetLive(bool live); void basNativeHostSetResult(uint32_t rawResult, double fpResult); #endif @@ -497,28 +759,10 @@ void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *sour // ---- Widget creation ---- -// Create a widget by resolved (DVX) type name. Returns NULL if the type -// is unknown. createWidgetByIface dispatches on the interface -// descriptor's createSig; pass allowData=true for an empty pixel-data -// widget (runtime path), or false to refuse (design-time path). -WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); -WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData); - -// Resolve a VB-style name ("CommandButton") to a DVX widget name -// ("button"). Falls back to the same name if already canonical. Returns -// NULL if the type is unknown. -const char *resolveTypeName(const char *typeName); - -// Apply a "Key = Value" string pair to a widget property via the -// interface descriptor. Handles ENUM/INT/BOOL/STRING dispatch. Returns -// true if the property was set, false if unsupported type / no setter. -bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val); - -// Read a widget property via the interface getter and format it into -// out as a .frm-style value ("True", "False", enum name, or decimal -// string). Returns true if the property was read; false if the getter -// is NULL, the type is not serializable, or the enum value is unknown. -bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); +// Controls are created with libdvx's wgtCreateByName / wgtResolveTypeName +// and their properties read and written with wgtIfaceGetProp / +// wgtIfaceSetProp and the text forms wgtIfaceGetPropText / +// wgtIfaceSetPropText (dvxWgt.h, "Generic widget access"). // ---- Form window creation ---- @@ -541,6 +785,161 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen); +// ---- Code a runtime's VM runs outside its own event handlers ---- + +// Enter / leave code rt's VM runs for something other than its own event +// handlers (a UserControl's code): rt is current for the library-wide +// externs and its forms' teardown waits. basFormRtCodeEnter returns the +// runtime that was current, for basFormRtCodeLeave. After code that +// failed, basFormRtCodeFailed ends the program as an untrapped error in +// a handler does (nothing when the VM reported no error). +BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt); +void basFormRtCodeFailed(BasFormRtT *rt); +void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev); + +// The BASIC value of a widget value (INT / ENUM Long, BOOL Boolean, FLOAT +// Single, DOUBLE Double, STRING a new string) and the widget value of a +// BASIC value as type want (WGT_IFACE_NONE: its own kind; a STRING's text +// is held by *hold, unref it after use) -- the form runtime's one pair of +// conversions. +BasValueT basFormRtFromWgt(const WgtValueT *v); +WgtValueT basFormRtToWgt(BasValueT v, uint8_t want, BasStringT **hold); + +// ============================================================ +// UserControls (formCtl.c) +// ============================================================ +// +// A UserControl is a widget class written in BASIC: a class module +// compiled with the BAS_CLASS_USERCONTROL flag plus the "Begin +// UserControl Name ... End" header of its .ctl file. Registering it +// makes a widget class, an interface (its properties, methods and events) +// and a type name the .frm loader, the IDE and C programs +// (wgtCreateByName) use like any other widget. Each widget is one object +// of the class with a border-less canvas, the UserControl object its code +// draws on. + +// The header's keys are BAS_UC_KEY_* (basRes.h). +#define BAS_UC_DEFAULT_SIZE 64 // Width / Height of a header that gives none +#define BAS_UC_NO_COLOR (-1) // BackColor not given: the drawing default +#define BAS_UC_TEXT_MAX 128 // longest Description / ToolboxIcon / DefaultEvent kept + +// A .ctl header as parsed. +typedef struct { + char name[BAS_MAX_IDENT]; // the class (Begin UserControl ) + int32_t width; + int32_t height; + int32_t backColor; // BASIC &HBBGGRR, BAS_UC_NO_COLOR + bool canGetFocus; + char toolboxIcon[BAS_UC_TEXT_MAX]; // 24 x 24 toolbox picture file ("" none) + char description[BAS_UC_TEXT_MAX]; // toolbox tooltip / name resource + char defaultEvent[BAS_MAX_IDENT]; +} BasUcHeaderT; + +// Parse the "Begin UserControl Name ... End" block of a .ctl text (len +// bytes; what follows is not read). False when there is none. +bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out); + +// Register the UserControl the header text names (a class of rt's module) +// as a widget class owned by rt's program; it leaves the registry when +// the program ends (basFormRtDestroy). False, with the runtime error +// shown, when the class is not a UserControl of the module or its name +// is taken. +bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len); + +// A control file installed in widgets/ (a .wgt holding only resources: +// MODULE, CTLn header texts, icon24 / name): registers each of its +// UserControls for every program. Its code runs in a VM of its own, +// made when the first control of the file is created (its module code +// runs then, once). The DVX loader calls this, through dlsym, for every +// such file. False when the file holds no usable control. +bool basCtlLoadFile(const char *path); + +// A designer's UserControls (the project it edits): registers the +// controls of the count header texts (each a .ctl header, "Begin +// UserControl Name ... End"), classes of module, for designer rt -- a +// program designing forms (compiler.bas CompDesignControls), or NULL for +// a C host (the DVX BASIC IDE) -- so its toolbox, property sheet and +// forms can use them. Their code runs as an installed control file's +// does (a VM of their own, made with the first control, App.Path the +// folder of path; an untrapped error is shown and the control carries +// on) except that the module-level code never runs (it is the program's +// start). Each gets a private registry key, as a program's own controls +// do, so the designer finds them by BASIC name with basUcResolveType(rt, +// name) / basUcOwnIface(rt, name) even while the same project runs, and +// two designers never share them. module stays the caller's; it must +// outlive basCtlUnloadDesign (or rt's end). The set rt registered before +// is let go first. Returns the number of controls registered (a name a +// native or installed control has is not). +int32_t basCtlLoadDesign(const BasFormRtT *rt, BasModuleT *module, const char *const *headers, int32_t count, const char *path); + +// Let designer rt's UserControls go (rt NULL: a C host's): they leave the +// registry, and their widgets that are still there keep their last +// picture and run no more code. False (nothing changed) while their code +// runs. A designer program's go when it ends (basUcRuntimeEnd). +bool basCtlUnloadDesign(const BasFormRtT *rt); + +// The ToolboxIcon picture file of a control file's or a designer's +// UserControl (its header's ToolboxIcon in the folder of the path it was +// registered with, the project's folder for a designer's) in out. False +// when it names none, or iface is a program's own control or no +// UserControl's. +bool basUcIconPath(const WgtIfaceT *iface, char *out, int32_t outSize); + +// The header of the UserControl whose interface iface is (as registered: +// its ToolboxIcon, Description ...), NULL when iface is no UserControl's. +const BasUcHeaderT *basUcIfaceHeader(const WgtIfaceT *iface); + +// False when iface is a private UserControl of an owner other than rt: +// a program's own controls are its alone, a designer's (basCtlLoadDesign) +// its designer's (rt NULL: a C host's). Lists of control types +// (basCtlTypeAt) leave the others' out. +bool basUcIfaceVisible(const BasFormRtT *rt, const WgtIfaceT *iface); + +// The idx-th control type rt sees (0 = the first; basUcIfaceVisible) in +// alphabetical order of BASIC name, NULL past the end: the order the +// IDE's toolbox and CtlTypeName$ list them in (formDesign.c). +const WgtIfaceT *basCtlTypeAt(const BasFormRtT *rt, int32_t idx); + +// True when typeName is a UserControl (a control written in BASIC) that +// is registered: its host gets only the events its code raises, and +// GotFocus / LostFocus. +bool basUcIsControlType(const char *typeName); + +// The interface of rt's own UserControl typeName: its program's own or +// one it registered for design (basCtlLoadDesign); for rt NULL a C +// host's design controls. NULL when it has none. +const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName); + +// The runtime is being destroyed: its UserControl classes and the ones it +// registered for design are unregistered (their widgets are gone with the +// forms). +void basUcRuntimeEnd(BasFormRtT *rt); + +// The UserControl object ref names when it belongs to a control running +// in rt, else NULL. +BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref); + +// The widget type name (registry key) of a BASIC control type name in +// rt's program: its own UserControl of that name first (each program's +// controls and design controls are private to it; for rt NULL a C host's +// design controls, basCtlLoadDesign), else wgtResolveTypeName. +const char *basUcResolveType(const BasFormRtT *rt, const char *typeName); + +// The UserControl object's members (Width, Height, MinWidth, MinHeight, +// Enabled, HasFocus, CanGetFocus, UserMode, Name, SetFocus, Refresh, +// PropertyChanged and the drawing members): true when ref is one (an +// unknown member is a runtime error), false to let the caller go on. +bool basUcSurfaceCall(BasFormRtT *rt, void *ref, const char *name, BasValueT *args, int32_t argc, BasValueT *out); +bool basUcSurfaceGet(BasFormRtT *rt, void *ref, const char *name, BasValueT *out); +bool basUcSurfaceSet(BasFormRtT *rt, void *ref, const char *name, BasValueT value); + +// PropertyChanged "name" in a control's code calls fn (NULL: none) with +// the control's widget: a C designer re-reads its property sheet (a +// BASIC one gets DesignPropertyChanged, basDsgnPropertyChanged). +typedef void (*BasUcPropChangedFnT)(WidgetT *w, const char *propName); + +void basUcSetPropChangedHook(BasUcPropChangedFnT fn); + // ============================================================ // Between formrt.c and formGfx.c // ============================================================ @@ -555,6 +954,10 @@ bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, B // conversions (VB's 15 twips a pixel), shared with Picture.Width / Height. double basFormRtDrawPixelsToUnits(double pixels, int32_t scale, bool across); +// Cls on a form's or PictureBox's (or UserControl's) drawing: cleared to +// its BackColor. +void basFormRtDrawClear(BasFormRtT *rt, BasControlT *ctrl); + // Free a control's drawing state. void basFormRtDrawFree(BasControlT *ctrl); @@ -576,4 +979,36 @@ BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref); void basFormRtScreenBind(BasFormRtT *rt); void basFormRtScreenClose(BasFormRtT *rt); +// ============================================================ +// Between formrt.c and formDesign.c +// ============================================================ + +// True while form is in design mode (its controls fire no events). +bool basFormRtDesignBlocks(const BasFormT *form); + +// Control.Parent / Container, and Form.Controls / DesignMode / +// DesignSelection. +BasValueT basFormRtDesignCtrlProp(BasFormRtT *rt, BasControlT *ctrl, const char *propName); +BasValueT basFormRtDesignFormProp(BasFormRtT *rt, BasFormT *form, const char *propName); +void basFormRtDesignSetFormProp(BasFormRtT *rt, BasFormT *form, const char *propName, BasValueT value); + +// Form.Controls(index) (1 = the first, as the collection counts) or +// Form.Controls(name): that control; a run-time error when there is none. +BasValueT basFormRtDesignControlsItem(BasFormRtT *rt, BasFormT *form, BasValueT *args, int32_t argc); + +// The form is being torn down: forget its design surface. +void basFormRtDesignForget(const BasFormT *form); + +// Set Control.Container and Control.MoveBefore: ctrl into the container +// containerRef names (NULL: its own) before beforeRef (NULL: last). +bool basFormRtMoveControl(BasFormRtT *rt, BasControlT *ctrl, void *containerRef, void *beforeRef); + +// LoadFormText: a form (its own copy even when one of its name is +// loaded) from .frm text, NULL when the text does not load. +void *basFormRtLoadFormText(void *ctx, const char *text); + +// resource.bas ResExtract (formrt.c): resource name of file path written +// to outFile; True when written. +int32_t ResExtract(const char *path, const char *name, const char *outFile); + #endif // DVXBASIC_FORMRT_H diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.c b/src/apps/kpunch/dvxbasic/formrt/frmModel.c new file mode 100644 index 00000000..598f168a --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/frmModel.c @@ -0,0 +1,2405 @@ +// 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. + +// frmModel.c -- the .frm document model +// +// See frmModel.h. Loading goes through frmParse; writing produces the +// canonical text the DVX BASIC IDE saves: the form header in the order of +// the runtime's form property table, the menus, then each control +// followed by its children. Every value is written in the canonical form +// for its type (STRING quoted, BOOL True / False, INT a number, FLOAT and +// ENUM unquoted), so the runtime loader classifies it the same way +// whether it came from a fresh placement, a loaded file or an edit. + +#include "frmModel.h" +#include "dvxApp.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define FRM_PAD_BUF (FRM_MAX_PROP_INDENT + 1) // indent string of the deepest block +#define FRM_NEWLINE_ROOM 2 // "\n" before and after the code section +#define FRM_HEAD_LINES 3 // VERSION, Begin Form, the form's End +#define FRM_CTRL_FIXED_LINES 8 // Begin, Index, Caption, Text, HelpTopic, Visible, Enabled, End +#define FRM_MENU_LINES 9 // Begin, Caption, Checked, RadioCheck, Shortcut, ShortcutText, Enabled, Visible, End +#define FRM_OPTION_TYPE "OptionButton" // BASIC type whose Value is exclusive within its container +#define FRM_DATA_TYPE "Data" // Data control: DataSource / MasterSource name it +#define FRM_PASTE_FORM "FrmPaste" // scratch form that wraps pasted control blocks +#define FRM_VERSION_LINE FRM_KW_VERSION " DVX 1.00\n" +#define FRM_MENU_SEP_PREFIX "mnuSep" // generated separator name prefix +#define FRM_MENU_NAME_PREFIX "mnu" // generated item name prefix +#define FRM_INT_TEXT_MAX 16 // decimal int32_t text +#define FRM_TRUE_TEXT "True" +#define FRM_FALSE_TEXT "False" +#define FRM_PROP_LEVEL "Level" // a menu item's level (frmMenuPropText) + +// ============================================================ +// Built-in integer control properties (see FrmIntPropT) +// ============================================================ + +static const FrmIntPropT sIntProps[] = { + { BAS_PROP_LEFT, NULL, offsetof(FrmCtrlT, left), FRM_SAVE_IF_NONZERO }, + { BAS_PROP_TOP, NULL, offsetof(FrmCtrlT, top), FRM_SAVE_IF_NONZERO }, + { BAS_PROP_MINWIDTH, BAS_PROP_WIDTH, offsetof(FrmCtrlT, width), FRM_SAVE_ALWAYS }, + { BAS_PROP_MINHEIGHT, BAS_PROP_HEIGHT, offsetof(FrmCtrlT, height), FRM_SAVE_ALWAYS }, + { BAS_PROP_MAXWIDTH, NULL, offsetof(FrmCtrlT, maxWidth), FRM_SAVE_IF_POSITIVE }, + { BAS_PROP_MAXHEIGHT, NULL, offsetof(FrmCtrlT, maxHeight), FRM_SAVE_IF_POSITIVE }, + { BAS_PROP_WEIGHT, NULL, offsetof(FrmCtrlT, weight), FRM_SAVE_IF_POSITIVE }, + { BAS_PROP_DRAGMODE, NULL, offsetof(FrmCtrlT, dragMode), FRM_SAVE_IF_NONZERO } +}; + +#define FRM_INT_PROP_COUNT DVX_ARRAY_LEN(sIntProps) + +// Form properties of the runtime's table that are run-time state, never +// part of a .frm (besides the object-typed ones). +static const char *const sRuntimeOnlyFormProps[] = { + BAS_PROP_VISIBLE, + BAS_PROP_DESIGNMODE, + BAS_PROP_DESIGNSELECTION +}; + +// ============================================================ +// .frm loading context (used by the loadOn* callbacks) +// ============================================================ +// +// ctrlStack and menuStack mirror the parser's control and menu Begin / +// End nesting, so a property line goes to the innermost open block even +// after the blocks inside it, as the runtime's loader reads it. Every +// control block pushes its record (NULL when the allocation failed); +// enclosing keeps the record whose block held each control, and its +// parentName and parentIndex are set from it once the file is read: a +// container's Index line may come after its children. frmParse rejects +// a block deeper than FRM_MAX_NESTING before calling back, and the Form +// block holds one of those levels, so neither stack can overflow. + +typedef struct { + FrmModelT *m; + FrmCtrlT **enclosing; // stb_ds, parallel to m->controls (NULL: the form) + FrmCtrlT *ctrlStack[FRM_MAX_NESTING]; + int32_t ctrlDepth; + int32_t menuStack[FRM_MAX_NESTING]; // indices into m->menuItems + int32_t menuDepth; + bool sawForm; // a Begin Form line was read +} FrmLoadCtxT; + +// One name given to the controls of a pasted subtree (all members of a +// control array get the same one). +typedef struct { + char oldName[FRM_MAX_NAME]; + char newName[FRM_MAX_NAME]; +} FrmRenameT; + +// ============================================================ +// Prototypes +// ============================================================ + +static void buildPad(int32_t indent, char *pad); +static bool canTakeSubtree(const FrmModelT *m, const FrmCtrlT *parent, int32_t height); +static bool ctrlsAmbiguous(const FrmModelT *m); +static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args); +static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad); +static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...); +static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value); +static const BasPropDescT *formExtraProp(const char *name); +static bool formFieldText(const FrmModelT *m, const char *name, char *out, int32_t outSize); +static bool formPropSet(FrmModelT *m, const char *key, const char *value); +const char *frmCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen); +void frmCtrlFree(FrmCtrlT *ctrl); +const char *frmCtrlGetProp(const FrmCtrlT *ctrl, const char *name); +void frmCtrlInit(FrmCtrlT *ctrl); +FrmCtrlT *frmCtrlNew(const char *typeName, const char *name); +bool frmCtrlPropText(const FrmCtrlT *ctrl, const char *name, char *out, int32_t outSize); +bool frmCtrlRemoveProp(FrmCtrlT *ctrl, const char *name); +void frmCtrlSetProp(FrmCtrlT *ctrl, const char *name, const char *value); +void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value); +int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); +const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName); +const FrmIntPropT *frmIntPropAt(int32_t idx); +const FrmIntPropT *frmIntPropFind(const char *name); +int32_t frmIntPropGet(const FrmCtrlT *ctrl, const FrmIntPropT *ip); +void frmIntPropSet(FrmCtrlT *ctrl, const FrmIntPropT *ip, int32_t value); +void frmMenuAutoName(const FrmMenuItemT *items, int32_t idx, char *out, int32_t outSize); +int32_t frmMenuCheckLevels(const FrmMenuItemT *items); +bool frmMenuIndent(FrmMenuItemT *items, int32_t idx, int32_t delta); +int32_t frmMenuInsert(FrmMenuItemT **items, int32_t at, int32_t level); +void frmMenuItemInit(FrmMenuItemT *mi); +int32_t frmMenuMove(FrmMenuItemT **items, int32_t idx, int32_t dir); +bool frmMenuPropText(const FrmMenuItemT *mi, const char *key, char *out, int32_t outSize); +void frmMenuRemove(FrmMenuItemT **items, int32_t idx); +bool frmMenuSetPropText(FrmMenuItemT *mi, const char *key, const char *value); +void frmMenuStripBlank(FrmMenuItemT **items); +int32_t frmMenuSubtreeEnd(const FrmMenuItemT *items, int32_t idx); +FrmCtrlT *frmModelAddCtrl(FrmModelT *m, const char *typeName, const char *name, FrmCtrlT *parent); +void frmModelAutoName(const FrmModelT *m, const char *typeName, char *buf, int32_t bufSize); +int32_t frmModelCtrlDepth(const FrmModelT *m, const FrmCtrlT *ctrl); +int32_t frmModelCtrlPos(const FrmModelT *m, const FrmCtrlT *ctrl); +FrmModelT *frmModelCtrlsFromText(const char *text, int32_t len); +char *frmModelCtrlText(const FrmModelT *m, const FrmCtrlT *ctrl, int32_t *outLen); +int32_t frmModelFindCtrl(const FrmModelT *m, const char *name, int32_t index); +FrmCtrlT *frmModelFindParent(const FrmModelT *m, const FrmCtrlT *ctrl); +bool frmModelFormProp(const FrmModelT *m, const char *name, char *out, int32_t outSize); +void frmModelFree(FrmModelT *m); +FrmCtrlT *frmModelInsertTree(FrmModelT *m, FrmModelT *src, FrmCtrlT *srcRoot, FrmCtrlT *parent, const char *rootName, int32_t rootIndex); +bool frmModelIsDescendant(const FrmModelT *m, const FrmCtrlT *ctrl, const FrmCtrlT *ancestor); +FrmModelT *frmModelLoad(const char *text, int32_t len); +void frmModelMakeArrayMember(FrmModelT *m, FrmCtrlT *ctrl, int32_t index); +bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items); +int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize); +bool frmModelMoveCtrl(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *before); +bool frmModelNameInUse(const FrmModelT *m, const char *name, const char *exceptName, bool checkMenus); +FrmModelT *frmModelNew(const char *name); +bool frmModelNormalizeOrder(FrmModelT *m); +void frmModelOptionSelected(FrmModelT *m, FrmCtrlT *ctrl); +void frmModelRemoveCtrl(FrmModelT *m, FrmCtrlT *ctrl); +bool frmModelRenameCtrl(FrmModelT *m, const char *oldName, const char *newName); +bool frmModelReparent(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *parent); +bool frmModelSetCode(FrmModelT *m, const char *code); +bool frmModelSetCtrlProp(FrmModelT *m, FrmCtrlT *ctrl, const char *name, const char *value); +bool frmModelSetFormProp(FrmModelT *m, const char *name, const char *value); +bool frmModelSetMenuProp(FrmModelT *m, int32_t idx, const char *key, const char *value); +int32_t frmModelSibling(const FrmModelT *m, int32_t idx, int32_t dir); +int32_t frmModelSubtreeEnd(const FrmModelT *m, int32_t idx); +char *frmModelText(const FrmModelT *m, int32_t *outLen); +int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize); +int32_t frmModelWriteSize(const FrmModelT *m); +static bool isEmptyValue(uint8_t type, const char *value); +static void loadOnCtrlBegin(void *userData, const char *typeName, const char *name); +static void loadOnCtrlEnd(void *userData); +static void loadOnCtrlProp(void *userData, const char *key, const char *value); +static bool loadOnFormBegin(void *userData, const char *name); +static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen); +static void loadOnFormProp(void *userData, const char *key, const char *value); +static void loadOnMenuBegin(void *userData, const char *name, int32_t level); +static void loadOnMenuEnd(void *userData); +static void loadOnMenuProp(void *userData, const char *key, const char *value); +static void normalizeAppend(FrmCtrlT **src, FrmCtrlT **parents, bool *placed, int32_t count, const FrmCtrlT *parent, FrmCtrlT ***out, int32_t depth); +static bool propDrop(FrmPropT **props, const char *name); +static int32_t propFind(const FrmPropT *props, const char *name); +static void propStore(FrmPropT **props, const char *name, const char *value); +static const char *renamedMenu(const FrmModelT *m, const FrmMenuItemT *items, const char *oldName); +static FrmCtrlT **resolveParents(const FrmModelT *m); +static void retargetContextMenu(FrmModelT *m, const char *oldName, const char *newName); +static bool spliceRun(FrmCtrlT **arr, int32_t start, int32_t len, int32_t dest); +static int32_t subtreeHeight(const FrmModelT *m, int32_t idx); +static int32_t subtreeLines(const FrmModelT *m, int32_t idx); +static int32_t writeControls(const FrmModelT *m, FrmCtrlT **parents, char *buf, int32_t bufSize, int32_t pos, const FrmCtrlT *parent, int32_t indent); +static int32_t writeCtrlTree(const FrmModelT *m, FrmCtrlT **parents, const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent); +static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_t pos); + + +// Fill pad with the indentation of an indent-level block. pad must hold +// FRM_PAD_BUF bytes; deeper levels than the parser accepts are clamped. +static void buildPad(int32_t indent, char *pad) { + int32_t padLen = indent * FRM_INDENT_SPACES; + + if (padLen > FRM_MAX_PROP_INDENT) { + padLen = FRM_MAX_PROP_INDENT; + } + + memset(pad, ' ', padLen); + pad[padLen] = '\0'; +} + + +// True when parent (NULL = the form) can take a child subtree height +// levels tall: it holds controls and the subtree stays within the +// parser's nesting limit. +static bool canTakeSubtree(const FrmModelT *m, const FrmCtrlT *parent, int32_t height) { + int32_t depth = parent ? frmModelCtrlDepth(m, parent) : 0; + + if (parent && !basCtlIsContainer(parent->typeName)) { + return false; + } + + return depth + height <= FRM_MAX_NEST_DEPTH; +} + + +// True when two controls share a name and index: a child's parent (and +// the code) could name either, so the tree would not read back the same. +static bool ctrlsAmbiguous(const FrmModelT *m) { + int32_t count = (int32_t)arrlen(m->controls); + + for (int32_t i = 0; i < count; i++) { + for (int32_t j = i + 1; j < count; j++) { + if (m->controls[i]->index == m->controls[j]->index && strcasecmp(m->controls[i]->name, m->controls[j]->name) == 0) { + return true; + } + } + } + + return false; +} + + +static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args) { + if (!buf || pos < 0 || pos >= bufSize) { + return pos; + } + + int32_t avail = bufSize - pos; + int32_t written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args); + + if (written < 0) { + return pos; + } + + // Cut: vsnprintf wrote avail - 1 characters and the NUL + if (written >= avail) { + return bufSize - 1; + } + + return pos + written; +} + + +// "Begin Type Name" and every property line of one control, without the +// closing End so the writer can nest the children first. Interface-typed +// keys are written by their type; only keys the widget interface does not +// know go out as quoted strings. +static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad) { + pos = frmEmitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name); + + if (ctrl->index >= 0) { + pos = frmEmitClamped(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, BAS_PROP_INDEX, ctrl->index); + } + + const char *caption = frmCtrlGetProp(ctrl, BAS_PROP_CAPTION); + const char *text = frmCtrlGetProp(ctrl, BAS_PROP_TEXT); + + if (caption) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, caption); + } + + if (text) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_TEXT, WGT_IFACE_STRING, text); + } + + for (int32_t i = 0; i < FRM_INT_PROP_COUNT; i++) { + const FrmIntPropT *ip = &sIntProps[i]; + int32_t val = frmIntPropGet(ctrl, ip); + bool emit = true; + + if (ip->saveRule == FRM_SAVE_IF_NONZERO) { + emit = (val != 0); + } else if (ip->saveRule == FRM_SAVE_IF_POSITIVE) { + emit = (val > 0); + } + + if (emit) { + pos = frmEmitClamped(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, ip->name, val); + } + } + + if (ctrl->helpTopic[0]) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_HELPTOPIC, WGT_IFACE_STRING, ctrl->helpTopic); + } + + if (!ctrl->visible && !frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, FRM_FALSE_TEXT); + } + + if (!ctrl->enabled && !frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, FRM_FALSE_TEXT); + } + + // Keys the widget interface does not know: raw strings, quoted. + for (int32_t j = 0; j < (int32_t)arrlen(ctrl->props); j++) { + const char *name = ctrl->props[j].name; + + if (strcasecmp(name, BAS_PROP_CAPTION) == 0 || strcasecmp(name, BAS_PROP_TEXT) == 0 || frmIfaceProp(ctrl->typeName, name)) { + continue; + } + + pos = emitPropLine(buf, bufSize, pos, pad, name, WGT_IFACE_STRING, ctrl->props[j].value); + } + + // Interface properties that were set, typed, in interface order. + const WgtIfaceT *iface = basCtlIface(ctrl->typeName); + + for (int32_t j = 0; iface && j < iface->propCount; j++) { + const WgtPropDescT *p = &iface->props[j]; + const char *v = p->setFn ? frmCtrlGetProp(ctrl, p->name) : NULL; + + if (v && strcasecmp(p->name, BAS_PROP_CAPTION) != 0 && strcasecmp(p->name, BAS_PROP_TEXT) != 0) { + pos = emitPropLine(buf, bufSize, pos, pad, p->name, p->type, v); + } + } + + return pos; +} + + +// One formatted line indented to an indent-level block. +static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...) { + char pad[FRM_PAD_BUF]; + va_list args; + + buildPad(indent, pad); + pos = frmEmitClamped(buf, bufSize, pos, "%s", pad); + va_start(args, fmt); + pos = emitClampedV(buf, bufSize, pos, fmt, args); + va_end(args); + return frmEmitClamped(buf, bufSize, pos, "\n"); +} + + +// One "Key = Value" line in the canonical form for the value's type, one +// level deeper than the block's pad. A line longer than the parser keeps +// (FRM_MAX_LINE_LEN - 1 characters plus the newline) returns +// FRM_WRITE_LINE_TOO_LONG, which every later emit passes through. +static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value) { + int32_t end; + + switch (type) { + case WGT_IFACE_STRING: + end = frmEmitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value); + break; + case WGT_IFACE_BOOL: + end = frmEmitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? FRM_TRUE_TEXT : FRM_FALSE_TEXT); + break; + case WGT_IFACE_INT: + end = frmEmitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value)); + break; + default: + // FLOAT and ENUM: decimal text or the value name, unquoted + end = frmEmitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, value); + break; + } + + if (end < bufSize - 1 && end - pos > FRM_MAX_LINE_LEN) { + return FRM_WRITE_LINE_TOO_LONG; + } + + return end; +} + + +// The runtime form table entry of a design property the model keeps in +// FrmModelT.props (one without a field), NULL for a field, a run-time-only +// or object property, or an unknown name. +static const BasPropDescT *formExtraProp(const char *name) { + const BasPropDescT *pd = basFormRtFindFormProp(name); + char dummy[FRM_INT_TEXT_MAX]; + + if (!pd || !pd->writable || pd->type == BAS_PROP_OBJECT || formFieldText(NULL, pd->name, dummy, sizeof(dummy))) { + return NULL; + } + + for (int32_t i = 0; i < DVX_ARRAY_LEN(sRuntimeOnlyFormProps); i++) { + if (strcasecmp(pd->name, sRuntimeOnlyFormProps[i]) == 0) { + return NULL; + } + } + + return pd; +} + + +// The text of a form property kept in a field of FrmModelT. With m NULL +// it only answers whether name is such a field. +static bool formFieldText(const FrmModelT *m, const char *name, char *out, int32_t outSize) { + static const FrmModelT blank; + const char *text = NULL; + int32_t num = 0; + bool flag = false; + bool isNum = false; + bool isFlag = false; + + if (!m) { + m = ␣ + } + + if (strcasecmp(name, BAS_PROP_NAME) == 0) { + text = m->name; + } else if (strcasecmp(name, BAS_PROP_CAPTION) == 0) { + text = m->caption; + } else if (strcasecmp(name, BAS_PROP_LAYOUT) == 0) { + text = m->layout; + } else if (strcasecmp(name, BAS_PROP_HELPTOPIC) == 0) { + text = m->helpTopic; + } else if (strcasecmp(name, BAS_PROP_CONTEXTMENU) == 0) { + text = m->contextMenu; + } else if (strcasecmp(name, BAS_PROP_LEFT) == 0) { + num = m->left; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_TOP) == 0) { + num = m->top; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_WIDTH) == 0) { + num = m->width; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_HEIGHT) == 0) { + num = m->height; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_PADDING) == 0) { + num = m->padding; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_SPACING) == 0) { + num = m->spacing; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_WINDOWSTATE) == 0) { + num = m->windowState; + isNum = true; + } else if (strcasecmp(name, BAS_PROP_AUTOSIZE) == 0) { + flag = m->autoSize; + isFlag = true; + } else if (strcasecmp(name, BAS_PROP_RESIZABLE) == 0) { + flag = m->resizable; + isFlag = true; + } else if (strcasecmp(name, BAS_PROP_CENTERED) == 0) { + flag = m->centered; + isFlag = true; + } + + if (text) { + snprintf(out, outSize, "%s", text); + } else if (isNum) { + snprintf(out, outSize, "%" PRId32, num); + } else if (isFlag) { + snprintf(out, outSize, "%s", flag ? FRM_TRUE_TEXT : FRM_FALSE_TEXT); + } else { + return false; + } + + return true; +} + + +// Apply one form property from .frm text (unquoted), Name excepted. +// AutoSize is independent of Width / Height, as at run time. +static bool formPropSet(FrmModelT *m, const char *key, const char *value) { + if (strcasecmp(key, BAS_PROP_CAPTION) == 0) { + snprintf(m->caption, sizeof(m->caption), "%s", value); + } else if (strcasecmp(key, BAS_PROP_LAYOUT) == 0) { + snprintf(m->layout, sizeof(m->layout), "%s", value); + } else if (strcasecmp(key, BAS_PROP_AUTOSIZE) == 0) { + m->autoSize = frmParseBool(value); + } else if (strcasecmp(key, BAS_PROP_RESIZABLE) == 0) { + m->resizable = frmParseBool(value); + } else if (strcasecmp(key, BAS_PROP_CENTERED) == 0) { + m->centered = frmParseBool(value); + } else if (strcasecmp(key, BAS_PROP_LEFT) == 0) { + m->left = atoi(value); + } else if (strcasecmp(key, BAS_PROP_TOP) == 0) { + m->top = atoi(value); + } else if (strcasecmp(key, BAS_PROP_WIDTH) == 0) { + m->width = atoi(value); + } else if (strcasecmp(key, BAS_PROP_HEIGHT) == 0) { + m->height = atoi(value); + } else if (strcasecmp(key, BAS_PROP_PADDING) == 0) { + m->padding = atoi(value); + } else if (strcasecmp(key, BAS_PROP_SPACING) == 0) { + m->spacing = atoi(value); + } else if (strcasecmp(key, BAS_PROP_WINDOWSTATE) == 0) { + m->windowState = atoi(value); + } else if (strcasecmp(key, BAS_PROP_HELPTOPIC) == 0) { + snprintf(m->helpTopic, sizeof(m->helpTopic), "%s", value); + } else if (strcasecmp(key, BAS_PROP_CONTEXTMENU) == 0) { + snprintf(m->contextMenu, sizeof(m->contextMenu), "%s", value); + } else { + const BasPropDescT *pd = formExtraProp(key); + + if (!pd) { + return false; + } + + if (isEmptyValue(pd->type, value)) { + propDrop(&m->props, pd->name); + } else { + propStore(&m->props, pd->name, value); + } + } + + return true; +} + + +const char *frmCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen) { + // The code is text: a NUL byte ends it (the model keeps a C string, + // and code that is blank up to the NUL must read as no code at all) + size_t srcLen = (size_t)DVX_MAX(trailingLen, 0); + const char *nul = (const char *)memchr(trailingSrc, '\0', srcLen); + const char *codeStart = trailingSrc; + const char *codeEnd = nul ? nul : trailingSrc + srcLen; + + // Skip whole blank lines only; the first code line keeps its indent. + while (codeStart < codeEnd) { + const char *p = codeStart; + + while (p < codeEnd && (*p == ' ' || *p == '\t')) { + p++; + } + + if (p >= codeEnd) { + return NULL; + } + + if (*p != '\r' && *p != '\n') { + break; + } + + if (*p == '\r' && p + 1 < codeEnd && p[1] == '\n') { + p++; + } + + codeStart = p + 1; + } + + if (codeStart >= codeEnd) { + return NULL; + } + + *outLen = (int32_t)(codeEnd - codeStart); + return codeStart; +} + + +void frmCtrlFree(FrmCtrlT *ctrl) { + if (!ctrl) { + return; + } + + arrfree(ctrl->props); + free(ctrl); +} + + +const char *frmCtrlGetProp(const FrmCtrlT *ctrl, const char *name) { + int32_t i = propFind(ctrl->props, name); + + return (i >= 0) ? ctrl->props[i].value : NULL; +} + + +void frmCtrlInit(FrmCtrlT *ctrl) { + memset(ctrl, 0, sizeof(*ctrl)); + ctrl->index = FRM_NO_INDEX; + ctrl->parentIndex = FRM_NO_INDEX; + ctrl->visible = true; + ctrl->enabled = true; + ctrl->width = FRM_DEFAULT_CTRL_W; + ctrl->height = FRM_DEFAULT_CTRL_H; +} + + +FrmCtrlT *frmCtrlNew(const char *typeName, const char *name) { + FrmCtrlT *ctrl = (FrmCtrlT *)malloc(sizeof(FrmCtrlT)); + + if (!ctrl) { + return NULL; + } + + frmCtrlInit(ctrl); + snprintf(ctrl->typeName, sizeof(ctrl->typeName), "%s", typeName); + snprintf(ctrl->name, sizeof(ctrl->name), "%s", name); + return ctrl; +} + + +bool frmCtrlPropText(const FrmCtrlT *ctrl, const char *name, char *out, int32_t outSize) { + const FrmIntPropT *ip = frmIntPropFind(name); + const char *stored = frmCtrlGetProp(ctrl, name); + + if (strcasecmp(name, BAS_PROP_NAME) == 0) { + snprintf(out, outSize, "%s", ctrl->name); + } else if (strcasecmp(name, BAS_PROP_INDEX) == 0) { + snprintf(out, outSize, "%" PRId32, ctrl->index); + } else if (ip) { + snprintf(out, outSize, "%" PRId32, frmIntPropGet(ctrl, ip)); + } else if (strcasecmp(name, BAS_PROP_HELPTOPIC) == 0) { + snprintf(out, outSize, "%s", ctrl->helpTopic); + } else if (strcasecmp(name, BAS_PROP_VISIBLE) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) { + snprintf(out, outSize, "%s", ctrl->visible ? FRM_TRUE_TEXT : FRM_FALSE_TEXT); + } else if (strcasecmp(name, BAS_PROP_ENABLED) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) { + snprintf(out, outSize, "%s", ctrl->enabled ? FRM_TRUE_TEXT : FRM_FALSE_TEXT); + } else if (stored) { + snprintf(out, outSize, "%s", stored); + } else { + return false; + } + + return true; +} + + +bool frmCtrlRemoveProp(FrmCtrlT *ctrl, const char *name) { + return propDrop(&ctrl->props, name); +} + + +// A name no "key = value" line can carry is not stored (frmIsPropKey): +// the writer could only give it back as something else. +void frmCtrlSetProp(FrmCtrlT *ctrl, const char *name, const char *value) { + if (frmIsPropKey(name)) { + propStore(&ctrl->props, name, value); + } +} + + +void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value) { + const FrmIntPropT *ip = frmIntPropFind(key); + + if (ip) { + frmIntPropSet(ctrl, ip, atoi(value)); + } else if (strcasecmp(key, BAS_PROP_INDEX) == 0) { + ctrl->index = atoi(value); + } else if (strcasecmp(key, BAS_PROP_HELPTOPIC) == 0) { + snprintf(ctrl->helpTopic, sizeof(ctrl->helpTopic), "%s", value); + } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) { + ctrl->visible = frmParseBool(value); + } else if (strcasecmp(key, BAS_PROP_ENABLED) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) { + ctrl->enabled = frmParseBool(value); + } else if (strcasecmp(key, BAS_PROP_TABINDEX) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_TABINDEX)) { + // VB tab order: dropped (DVX has none). A widget that defines + // TabIndex itself (TabStrip: the active tab) keeps it below. + } else { + frmCtrlSetProp(ctrl, key, value); + } +} + + +int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) { + va_list args; + + va_start(args, fmt); + pos = emitClampedV(buf, bufSize, pos, fmt, args); + va_end(args); + return pos; +} + + +const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName) { + return wgtIfaceFindProp(basCtlIface(typeName), propName); +} + + +const FrmIntPropT *frmIntPropAt(int32_t idx) { + if (idx < 0 || idx >= FRM_INT_PROP_COUNT) { + return NULL; + } + + return &sIntProps[idx]; +} + + +const FrmIntPropT *frmIntPropFind(const char *name) { + for (int32_t i = 0; i < FRM_INT_PROP_COUNT; i++) { + const FrmIntPropT *ip = &sIntProps[i]; + + if (strcasecmp(ip->name, name) == 0 || (ip->alias && strcasecmp(ip->alias, name) == 0)) { + return ip; + } + } + + return NULL; +} + + +int32_t frmIntPropGet(const FrmCtrlT *ctrl, const FrmIntPropT *ip) { + return *(const int32_t *)((const char *)ctrl + ip->offset); +} + + +void frmIntPropSet(FrmCtrlT *ctrl, const FrmIntPropT *ip, int32_t value) { + *(int32_t *)((char *)ctrl + ip->offset) = value; +} + + +void frmMenuAutoName(const FrmMenuItemT *items, int32_t idx, char *out, int32_t outSize) { + const char *caption = items[idx].caption; + + if (caption[0] == '-') { + // Separator: one past the highest mnuSepN of the other items + int32_t sepNum = 1; + int32_t prefixLen = (int32_t)sizeof(FRM_MENU_SEP_PREFIX) - 1; + + for (int32_t i = 0; i < (int32_t)arrlen(items); i++) { + if (i != idx && strncasecmp(items[i].name, FRM_MENU_SEP_PREFIX, prefixLen) == 0) { + int32_t n = atoi(items[i].name + prefixLen); + + if (n >= sepNum) { + sepNum = n + 1; + } + } + } + + snprintf(out, outSize, FRM_MENU_SEP_PREFIX "%" PRId32, sepNum); + return; + } + + // Normal item: "mnu" and the caption's letters and digits + int32_t p = snprintf(out, outSize, "%s", FRM_MENU_NAME_PREFIX); + + for (const char *c = caption; *c && p < outSize - 1; c++) { + if ((*c >= 'A' && *c <= 'Z') || (*c >= 'a' && *c <= 'z') || (*c >= '0' && *c <= '9')) { + out[p++] = *c; + } + } + + out[p] = '\0'; +} + + +int32_t frmMenuCheckLevels(const FrmMenuItemT *items) { + int32_t count = (int32_t)arrlen(items); + + for (int32_t i = 0; i < count; i++) { + int32_t maxLevel = (i == 0) ? 0 : items[i - 1].level + 1; + + if (items[i].level > maxLevel || items[i].level < 0) { + return i; + } + } + + return FRM_NO_INDEX; +} + + +bool frmMenuIndent(FrmMenuItemT *items, int32_t idx, int32_t delta) { + int32_t count = (int32_t)arrlen(items); + + if (idx < 0 || idx >= count || (delta != 1 && delta != -1)) { + return false; + } + + int32_t subEnd = frmMenuSubtreeEnd(items, idx); + + if (delta < 0) { + if (items[idx].level <= 0) { + return false; + } + } else { + // At most one level below the item above, and the whole subtree + // must stay within the .frm nesting limit + if (idx == 0 || items[idx].level > items[idx - 1].level) { + return false; + } + + for (int32_t i = idx; i < subEnd; i++) { + if (items[i].level >= FRM_MAX_MENU_LEVEL) { + return false; + } + } + } + + for (int32_t i = idx; i < subEnd; i++) { + items[i].level += delta; + } + + return true; +} + + +int32_t frmMenuInsert(FrmMenuItemT **items, int32_t at, int32_t level) { + int32_t count = (int32_t)arrlen(*items); + FrmMenuItemT mi; + + at = DVX_MAX(0, DVX_MIN(at, count)); + + // Keep the tree: no deeper than one below the item above, no + // shallower than one above the item below (which becomes its child) + int32_t maxLevel = (at == 0) ? 0 : DVX_MIN((*items)[at - 1].level + 1, FRM_MAX_MENU_LEVEL); + int32_t minLevel = (at < count) ? DVX_MAX((*items)[at].level - 1, 0) : 0; + + frmMenuItemInit(&mi); + mi.level = DVX_MAX(minLevel, DVX_MIN(level, maxLevel)); + arrins(*items, at, mi); + return ((int32_t)arrlen(*items) == count + 1) ? at : FRM_NO_INDEX; +} + + +void frmMenuItemInit(FrmMenuItemT *mi) { + memset(mi, 0, sizeof(*mi)); + mi->enabled = true; + mi->visible = true; + mi->id = FRM_NEW_MENU_ID; +} + + +int32_t frmMenuMove(FrmMenuItemT **items, int32_t idx, int32_t dir) { + FrmMenuItemT *arr = *items; + int32_t count = (int32_t)arrlen(arr); + int32_t to; + + if (idx < 0 || idx >= count) { + return FRM_NO_INDEX; + } + + int32_t subEnd = frmMenuSubtreeEnd(arr, idx); + + if (dir > 0) { + // The item after the subtree must be the next sibling; a + // shallower one means this is the last child + if (subEnd >= count || arr[subEnd].level != arr[idx].level) { + return FRM_NO_INDEX; + } + + to = frmMenuSubtreeEnd(arr, subEnd); + } else { + int32_t prev = idx - 1; + + while (prev > 0 && arr[prev].level > arr[idx].level) { + prev--; + } + + // No previous sibling (the first child of its parent) + if (prev < 0 || arr[prev].level != arr[idx].level) { + return FRM_NO_INDEX; + } + + to = prev; + } + + // Move items[idx..subEnd) so it starts where to pointed before the move + int32_t size = subEnd - idx; + FrmMenuItemT *tmp = (FrmMenuItemT *)malloc((size_t)size * sizeof(FrmMenuItemT)); + + if (!tmp) { + return FRM_NO_INDEX; + } + + memcpy(tmp, &arr[idx], (size_t)size * sizeof(FrmMenuItemT)); + + if (to > idx) { + memmove(&arr[idx], &arr[subEnd], (size_t)(to - subEnd) * sizeof(FrmMenuItemT)); + to -= size; + } else { + memmove(&arr[to + size], &arr[to], (size_t)(idx - to) * sizeof(FrmMenuItemT)); + } + + memcpy(&arr[to], tmp, (size_t)size * sizeof(FrmMenuItemT)); + free(tmp); + return to; +} + + +bool frmMenuPropText(const FrmMenuItemT *mi, const char *key, char *out, int32_t outSize) { + const char *text = NULL; + const bool *flag = NULL; + + if (strcasecmp(key, BAS_PROP_NAME) == 0) { + text = mi->name; + } else if (strcasecmp(key, BAS_PROP_CAPTION) == 0) { + text = mi->caption; + } else if (strcasecmp(key, BAS_PROP_SHORTCUT) == 0) { + text = mi->shortcut; + } else if (strcasecmp(key, BAS_PROP_SHORTCUTTEXT) == 0) { + text = mi->shortcutText; + } else if (strcasecmp(key, BAS_PROP_CHECKED) == 0) { + flag = &mi->checked; + } else if (strcasecmp(key, BAS_PROP_RADIOCHECK) == 0) { + flag = &mi->radioCheck; + } else if (strcasecmp(key, BAS_PROP_ENABLED) == 0) { + flag = &mi->enabled; + } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0) { + flag = &mi->visible; + } + + if (text) { + snprintf(out, outSize, "%s", text); + } else if (flag) { + snprintf(out, outSize, "%s", *flag ? FRM_TRUE_TEXT : FRM_FALSE_TEXT); + } else if (strcasecmp(key, FRM_PROP_LEVEL) == 0) { + snprintf(out, outSize, "%" PRId32, mi->level); + } else { + return false; + } + + return true; +} + + +void frmMenuRemove(FrmMenuItemT **items, int32_t idx) { + if (idx < 0 || idx >= (int32_t)arrlen(*items)) { + return; + } + + arrdeln(*items, idx, frmMenuSubtreeEnd(*items, idx) - idx); +} + + +bool frmMenuSetPropText(FrmMenuItemT *mi, const char *key, const char *value) { + if (strcasecmp(key, BAS_PROP_NAME) == 0) { + snprintf(mi->name, sizeof(mi->name), "%s", value); + } else if (strcasecmp(key, BAS_PROP_CAPTION) == 0) { + snprintf(mi->caption, sizeof(mi->caption), "%s", value); + } else if (strcasecmp(key, BAS_PROP_CHECKED) == 0) { + mi->checked = frmParseBool(value); + mi->checkable = true; + } else if (strcasecmp(key, BAS_PROP_RADIOCHECK) == 0) { + mi->radioCheck = frmParseBool(value); + } else if (strcasecmp(key, BAS_PROP_SHORTCUT) == 0) { + snprintf(mi->shortcut, sizeof(mi->shortcut), "%s", value); + } else if (strcasecmp(key, BAS_PROP_SHORTCUTTEXT) == 0) { + snprintf(mi->shortcutText, sizeof(mi->shortcutText), "%s", value); + } else if (strcasecmp(key, BAS_PROP_ENABLED) == 0) { + // Default-true flags: the shared parser rule keeps the model and + // the runtime in agreement + mi->enabled = frmParseBoolDefault(value, true); + } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0) { + mi->visible = frmParseBoolDefault(value, true); + } else { + return false; + } + + return true; +} + + +// Walking backwards, so a blank parent whose children were all blank +// goes too; a blank parent with real children stays (removing it would +// leave them under the wrong parent) and fails the Name check instead. +void frmMenuStripBlank(FrmMenuItemT **items) { + for (int32_t i = (int32_t)arrlen(*items) - 1; i >= 0; i--) { + const FrmMenuItemT *mi = &(*items)[i]; + + if (mi->caption[0] == '\0' && mi->name[0] == '\0' && frmMenuSubtreeEnd(*items, i) == i + 1) { + arrdel(*items, i); + } + } +} + + +int32_t frmMenuSubtreeEnd(const FrmMenuItemT *items, int32_t idx) { + int32_t count = (int32_t)arrlen(items); + int32_t end = idx + 1; + + while (end < count && items[end].level > items[idx].level) { + end++; + } + + return end; +} + + +FrmCtrlT *frmModelAddCtrl(FrmModelT *m, const char *typeName, const char *name, FrmCtrlT *parent) { + char newName[FRM_MAX_NAME]; + + if ((parent && frmModelCtrlPos(m, parent) < 0) || !canTakeSubtree(m, parent, 1)) { + return NULL; + } + + if (name && name[0]) { + if (frmModelNameInUse(m, name, NULL, true)) { + return NULL; + } + + snprintf(newName, sizeof(newName), "%s", name); + } else { + frmModelAutoName(m, typeName, newName, sizeof(newName)); + } + + FrmCtrlT *ctrl = frmCtrlNew(typeName, newName); + + if (!ctrl) { + return NULL; + } + + frmCtrlSetProp(ctrl, BAS_PROP_CAPTION, ctrl->name); + + if (parent) { + snprintf(ctrl->parentName, sizeof(ctrl->parentName), "%s", parent->name); + ctrl->parentIndex = parent->index; + } + + // Appended, then put in tree order: the last child of its parent + arrput(m->controls, ctrl); + frmModelNormalizeOrder(m); + m->dirty = true; + return ctrl; +} + + +void frmModelAutoName(const FrmModelT *m, const char *typeName, char *buf, int32_t bufSize) { + // The widget interface's name prefix, else the type name itself + const char *prefix = basCtlNamePrefix(typeName); + int32_t highest = 0; + int32_t prefixLen = (int32_t)strlen(prefix); + int32_t count = (int32_t)arrlen(m->controls); + + for (int32_t i = 0; i < count; i++) { + if (strncasecmp(m->controls[i]->name, prefix, prefixLen) == 0) { + int32_t num = atoi(m->controls[i]->name + prefixLen); + + if (num > highest) { + highest = num; + } + } + } + + // The numbering only looks at controls; a menu item or the form may + // already own the next name, so keep counting until it is free. + int32_t num = highest + 1; + + snprintf(buf, bufSize, "%s%" PRId32, prefix, num); + + while (frmModelNameInUse(m, buf, NULL, true)) { + num++; + snprintf(buf, bufSize, "%s%" PRId32, prefix, num); + } +} + + +// The walk is capped so a parent cycle cannot loop forever. +int32_t frmModelCtrlDepth(const FrmModelT *m, const FrmCtrlT *ctrl) { + int32_t depth = 1; + FrmCtrlT *p = frmModelFindParent(m, ctrl); + + while (p && depth <= FRM_MAX_NEST_DEPTH) { + depth++; + p = frmModelFindParent(m, p); + } + + return depth; +} + + +int32_t frmModelCtrlPos(const FrmModelT *m, const FrmCtrlT *ctrl) { + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + if (m->controls[i] == ctrl) { + return i; + } + } + + return FRM_NO_INDEX; +} + + +FrmModelT *frmModelCtrlsFromText(const char *text, int32_t len) { + static const char head[] = FRM_KW_BEGIN " " FRM_KW_FORM " " FRM_PASTE_FORM "\n"; + static const char tail[] = "\n" FRM_KW_END "\n"; + int32_t total = (int32_t)(sizeof(head) - 1) + len + (int32_t)(sizeof(tail) - 1); + char *wrap = (char *)malloc((size_t)total + 1); + + if (!wrap || len < 0) { + free(wrap); + return NULL; + } + + memcpy(wrap, head, sizeof(head) - 1); + memcpy(wrap + sizeof(head) - 1, text, (size_t)len); + memcpy(wrap + sizeof(head) - 1 + len, tail, sizeof(tail)); + + FrmModelT *m = frmModelLoad(wrap, total); + + free(wrap); + + if (m && arrlen(m->controls) == 0) { + frmModelFree(m); + m = NULL; + } + + return m; +} + + +char *frmModelCtrlText(const FrmModelT *m, const FrmCtrlT *ctrl, int32_t *outLen) { + int32_t idx = frmModelCtrlPos(m, ctrl); + + *outLen = FRM_WRITE_TOO_LARGE; + + if (idx < 0) { + return NULL; + } + + int32_t size = subtreeLines(m, idx) * FRM_MAX_LINE_LEN + 1; + char *buf = (char *)malloc((size_t)size); + FrmCtrlT **parents = resolveParents(m); + + if (!buf || (arrlen(m->controls) > 0 && !parents)) { + free(buf); + arrfree(parents); + return NULL; + } + + int32_t pos = writeCtrlTree(m, parents, ctrl, buf, size, 0, 0); + + arrfree(parents); + + if (pos < 0) { + free(buf); + *outLen = pos; + return NULL; + } + + buf[pos] = '\0'; + *outLen = pos; + return buf; +} + + +int32_t frmModelFindCtrl(const FrmModelT *m, const char *name, int32_t index) { + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + const FrmCtrlT *c = m->controls[i]; + + if (strcasecmp(c->name, name) == 0 && (index == FRM_NO_INDEX || c->index == index)) { + return i; + } + } + + return FRM_NO_INDEX; +} + + +// An exact name + index match wins; otherwise the first control bearing +// the name (the loader and the editing functions keep both exact; a host +// that sets the fields itself may not). +FrmCtrlT *frmModelFindParent(const FrmModelT *m, const FrmCtrlT *ctrl) { + if (!ctrl->parentName[0]) { + return NULL; + } + + FrmCtrlT *first = NULL; + int32_t count = (int32_t)arrlen(m->controls); + + for (int32_t i = 0; i < count; i++) { + FrmCtrlT *c = m->controls[i]; + + if (c == ctrl || strcasecmp(c->name, ctrl->parentName) != 0) { + continue; + } + + if (c->index == ctrl->parentIndex) { + return c; + } + + if (!first) { + first = c; + } + } + + return first; +} + + +bool frmModelFormProp(const FrmModelT *m, const char *name, char *out, int32_t outSize) { + if (formFieldText(m, name, out, outSize)) { + return true; + } + + const BasPropDescT *pd = formExtraProp(name); + + if (!pd) { + return false; + } + + int32_t i = propFind(m->props, pd->name); + + if (i >= 0) { + snprintf(out, outSize, "%s", m->props[i].value); + } else if (pd->type == WGT_IFACE_BOOL) { + snprintf(out, outSize, "%s", FRM_FALSE_TEXT); + } else if (pd->type == WGT_IFACE_STRING) { + snprintf(out, outSize, "%s", ""); + } else { + snprintf(out, outSize, "%d", 0); + } + + return true; +} + + +void frmModelFree(FrmModelT *m) { + if (!m) { + return; + } + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + frmCtrlFree(m->controls[i]); + } + + arrfree(m->controls); + arrfree(m->menuItems); + arrfree(m->props); + free(m->code); + free(m); +} + + +FrmCtrlT *frmModelInsertTree(FrmModelT *m, FrmModelT *src, FrmCtrlT *srcRoot, FrmCtrlT *parent, const char *rootName, int32_t rootIndex) { + int32_t srcIdx = frmModelCtrlPos(src, srcRoot); + + if (srcIdx < 0 || (parent && frmModelCtrlPos(m, parent) < 0)) { + return NULL; + } + + frmModelNormalizeOrder(src); + srcIdx = frmModelCtrlPos(src, srcRoot); + + if (!canTakeSubtree(m, parent, subtreeHeight(src, srcIdx))) { + return NULL; + } + + int32_t end = frmModelSubtreeEnd(src, srcIdx); + int32_t n = end - srcIdx; + FrmCtrlT **parents = (FrmCtrlT **)calloc((size_t)n, sizeof(FrmCtrlT *)); + FrmRenameT *names = (FrmRenameT *)calloc((size_t)n, sizeof(FrmRenameT)); + int32_t nameCnt = 0; + + if (!parents || !names) { + free(parents); + free(names); + return NULL; + } + + for (int32_t k = 0; k < n; k++) { + parents[k] = frmModelFindParent(src, src->controls[srcIdx + k]); + } + + // Name each control as it moves over, so the free-name checks see the + // ones already moved + for (int32_t k = 0; k < n; k++) { + FrmCtrlT *c = src->controls[srcIdx + k]; + int32_t mapped = -1; + char newName[FRM_MAX_NAME]; + + for (int32_t j = 0; j < nameCnt; j++) { + if (strcasecmp(names[j].oldName, c->name) == 0) { + mapped = j; + break; + } + } + + if (k == 0 && rootName && rootName[0]) { + snprintf(newName, sizeof(newName), "%s", rootName); + } else if (mapped >= 0) { + snprintf(newName, sizeof(newName), "%s", names[mapped].newName); + } else if (frmModelNameInUse(m, c->name, NULL, true)) { + frmModelAutoName(m, c->typeName, newName, sizeof(newName)); + } else { + snprintf(newName, sizeof(newName), "%s", c->name); + } + + if (k == 0) { + c->index = (rootName && rootName[0]) ? rootIndex : FRM_NO_INDEX; + } + + if (mapped < 0) { + snprintf(names[nameCnt].oldName, FRM_MAX_NAME, "%s", c->name); + snprintf(names[nameCnt].newName, FRM_MAX_NAME, "%s", newName); + nameCnt++; + } + + snprintf(c->name, sizeof(c->name), "%s", newName); + c->userData = NULL; + arrput(m->controls, c); + } + + // Children follow their renamed parents; the root goes under parent + for (int32_t k = 0; k < n; k++) { + FrmCtrlT *c = src->controls[srcIdx + k]; + const FrmCtrlT *p = (k == 0) ? parent : parents[k]; + + snprintf(c->parentName, sizeof(c->parentName), "%s", p ? p->name : ""); + c->parentIndex = p ? p->index : FRM_NO_INDEX; + } + + arrdeln(src->controls, srcIdx, n); + free(parents); + free(names); + + frmModelNormalizeOrder(m); + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + if (m->controls[i] == srcRoot || frmModelIsDescendant(m, m->controls[i], srcRoot)) { + frmModelOptionSelected(m, m->controls[i]); + } + } + + m->dirty = true; + return srcRoot; +} + + +// Walk capped so a parent cycle terminates. +bool frmModelIsDescendant(const FrmModelT *m, const FrmCtrlT *ctrl, const FrmCtrlT *ancestor) { + FrmCtrlT *p = frmModelFindParent(m, ctrl); + + for (int32_t depth = 0; p && depth < FRM_MAX_NEST_DEPTH; depth++) { + if (p == ancestor) { + return true; + } + + p = frmModelFindParent(m, p); + } + + return false; +} + + +FrmModelT *frmModelLoad(const char *text, int32_t len) { + if (!text || len <= 0) { + return NULL; + } + + FrmModelT *m = frmModelNew(FRM_KW_FORM "1"); + + if (!m) { + return NULL; + } + + FrmLoadCtxT ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.m = m; + + FrmParserCbsT cbs; + + memset(&cbs, 0, sizeof(cbs)); + cbs.userData = &ctx; + cbs.onFormBegin = loadOnFormBegin; + cbs.onFormProp = loadOnFormProp; + cbs.onFormEnd = loadOnFormEnd; + cbs.onMenuBegin = loadOnMenuBegin; + cbs.onMenuEnd = loadOnMenuEnd; + cbs.onMenuProp = loadOnMenuProp; + cbs.onCtrlBegin = loadOnCtrlBegin; + cbs.onCtrlEnd = loadOnCtrlEnd; + cbs.onCtrlProp = loadOnCtrlProp; + + bool parsed = frmParse(text, len, &cbs) && ctx.sawForm; + + // Each control names its container as the container's block left it + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + const FrmCtrlT *parent = ctx.enclosing[i]; + + if (parent) { + snprintf(m->controls[i]->parentName, sizeof(m->controls[i]->parentName), "%s", parent->name); + m->controls[i]->parentIndex = parent->index; + } + } + + arrfree(ctx.enclosing); + + // Two controls with one name and index make the file malformed + if (!parsed || ctrlsAmbiguous(m)) { + frmModelFree(m); + return NULL; + } + + // One selected OptionButton per group: of several saved True, the + // last wins (as setting them in order would at run time) + for (int32_t i = (int32_t)arrlen(m->controls) - 1; i >= 0; i--) { + frmModelOptionSelected(m, m->controls[i]); + } + + m->dirty = false; + return m; +} + + +void frmModelMakeArrayMember(FrmModelT *m, FrmCtrlT *ctrl, int32_t index) { + int32_t oldIndex = ctrl->index; + int32_t count = (int32_t)arrlen(m->controls); + + for (int32_t i = 0; i < count; i++) { + FrmCtrlT *c = m->controls[i]; + + if (c != ctrl && c->parentIndex == oldIndex && strcasecmp(c->parentName, ctrl->name) == 0) { + c->parentIndex = index; + } + } + + ctrl->index = index; + m->dirty = true; +} + + +bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items) { + FrmMenuItemT *copy = NULL; + int32_t count = (int32_t)arrlen(items); + + if (count > 0) { + arrsetlen(copy, count); + + if ((int32_t)arrlen(copy) != count) { + arrfree(copy); + return false; + } + + memcpy(copy, items, (size_t)count * sizeof(FrmMenuItemT)); + } + + // Each binding is looked up against the original names, so swapped + // names cannot chain + const char *newName = renamedMenu(m, items, m->contextMenu); + + if (newName) { + snprintf(m->contextMenu, sizeof(m->contextMenu), "%s", newName); + } + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + const char *ref = frmCtrlGetProp(m->controls[i], BAS_PROP_CONTEXTMENU); + + newName = ref ? renamedMenu(m, items, ref) : NULL; + + if (newName) { + frmCtrlSetProp(m->controls[i], BAS_PROP_CONTEXTMENU, newName); + } + } + + arrfree(m->menuItems); + m->menuItems = copy; + m->dirty = true; + return true; +} + + +int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize) { + int32_t count = (int32_t)arrlen(items); + int32_t bad = frmMenuCheckLevels(items); + + if (bad >= 0) { + snprintf(reason, reasonSize, "%s", "A menu item is nested more than one level below the item above it."); + return bad; + } + + for (int32_t i = 0; i < count; i++) { + const char *name = items[i].name; + int32_t key; + int32_t mods; + + if (name[0] == '\0') { + snprintf(reason, reasonSize, "%s", "All menu items must have a Name."); + return i; + } + + if (isIdent && !isIdent(name)) { + snprintf(reason, reasonSize, "Menu name must be an identifier (letters, digits, underscores): %s", name); + return i; + } + + if (frmModelNameInUse(m, name, NULL, false)) { + snprintf(reason, reasonSize, "Menu name is already used by the form or a control: %s", name); + return i; + } + + if (items[i].shortcut[0] && !dvxAccelParse(items[i].shortcut, &key, &mods)) { + snprintf(reason, reasonSize, "Unknown shortcut key: %s (use a key such as F2, Ctrl+X, Alt+Enter or Shift+Del)", items[i].shortcut); + return i; + } + + for (int32_t j = i + 1; j < count; j++) { + if (strcasecmp(name, items[j].name) == 0) { + snprintf(reason, reasonSize, "Duplicate menu name: %s", name); + return j; + } + } + } + + return FRM_NO_INDEX; +} + + +bool frmModelMoveCtrl(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *before) { + FrmCtrlT *parent = frmModelFindParent(m, ctrl); + + if (frmModelCtrlPos(m, ctrl) < 0 || before == ctrl || (before && (frmModelCtrlPos(m, before) < 0 || frmModelFindParent(m, before) != parent))) { + return false; + } + + frmModelNormalizeOrder(m); + + int32_t idx = frmModelCtrlPos(m, ctrl); + int32_t end = frmModelSubtreeEnd(m, idx); + int32_t dest = before ? frmModelCtrlPos(m, before) : (parent ? frmModelSubtreeEnd(m, frmModelCtrlPos(m, parent)) : (int32_t)arrlen(m->controls)); + + if (!spliceRun(m->controls, idx, end - idx, dest)) { + return false; + } + + m->dirty = true; + return true; +} + + +bool frmModelNameInUse(const FrmModelT *m, const char *name, const char *exceptName, bool checkMenus) { + if (exceptName && strcasecmp(name, exceptName) == 0) { + return false; + } + + if (strcasecmp(m->name, name) == 0) { + return true; + } + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + if (strcasecmp(m->controls[i]->name, name) == 0) { + return true; + } + } + + for (int32_t i = 0; checkMenus && i < (int32_t)arrlen(m->menuItems); i++) { + if (strcasecmp(m->menuItems[i].name, name) == 0) { + return true; + } + } + + return false; +} + + +// The runtime form's defaults (AutoSize off unless the .frm says so), so +// a designer shows what the program will show. +FrmModelT *frmModelNew(const char *name) { + FrmModelT *m = (FrmModelT *)calloc(1, sizeof(FrmModelT)); + + if (!m) { + return NULL; + } + + m->width = BAS_DEFAULT_FORM_W; + m->height = BAS_DEFAULT_FORM_H; + m->centered = true; + m->resizable = true; + snprintf(m->layout, sizeof(m->layout), "%s", BAS_LAYOUT_VBOX); + snprintf(m->name, sizeof(m->name), "%s", name); + snprintf(m->caption, sizeof(m->caption), "%s", name); + return m; +} + + +bool frmModelNormalizeOrder(FrmModelT *m) { + int32_t count = (int32_t)arrlen(m->controls); + FrmCtrlT **src = m->controls; + FrmCtrlT **parents = resolveParents(m); + FrmCtrlT **out = NULL; + bool *placed = (bool *)calloc((size_t)count + 1, sizeof(bool)); + bool changed = false; + + if (!placed || (count > 0 && !parents)) { + free(placed); + arrfree(parents); + return false; + } + + normalizeAppend(src, parents, placed, count, NULL, &out, 1); + + // A parent cycle leaves members unplaced; keep them, in order. + for (int32_t i = 0; i < count; i++) { + if (!placed[i]) { + arrput(out, src[i]); + } + } + + if ((int32_t)arrlen(out) == count) { + for (int32_t i = 0; i < count; i++) { + changed = changed || src[i] != out[i]; + src[i] = out[i]; + } + } + + arrfree(out); + arrfree(parents); + free(placed); + return changed; +} + + +void frmModelOptionSelected(FrmModelT *m, FrmCtrlT *ctrl) { + const char *value = frmCtrlGetProp(ctrl, BAS_PROP_VALUE); + + if (strcasecmp(ctrl->typeName, FRM_OPTION_TYPE) != 0 || !value || !frmParseBool(value)) { + return; + } + + FrmCtrlT *parent = frmModelFindParent(m, ctrl); + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + FrmCtrlT *c = m->controls[i]; + const char *other = frmCtrlGetProp(c, BAS_PROP_VALUE); + + if (c != ctrl && other && frmParseBool(other) && strcasecmp(c->typeName, FRM_OPTION_TYPE) == 0 && frmModelFindParent(m, c) == parent) { + frmCtrlSetProp(c, BAS_PROP_VALUE, FRM_FALSE_TEXT); + } + } +} + + +// Every descendant goes with ctrl. Parents are resolved on the unchanged +// array, so a child of one control-array member goes only with that +// member. +void frmModelRemoveCtrl(FrmModelT *m, FrmCtrlT *ctrl) { + int32_t count = (int32_t)arrlen(m->controls); + bool *drop = (bool *)calloc((size_t)count + 1, sizeof(bool)); + + if (!drop || frmModelCtrlPos(m, ctrl) < 0) { + free(drop); + return; + } + + for (int32_t i = 0; i < count; i++) { + drop[i] = m->controls[i] == ctrl || frmModelIsDescendant(m, m->controls[i], ctrl); + } + + // Highest position first so the earlier ones stay valid + for (int32_t i = count - 1; i >= 0; i--) { + if (drop[i]) { + frmCtrlFree(m->controls[i]); + arrdel(m->controls, i); + } + } + + free(drop); + m->dirty = true; +} + + +bool frmModelRenameCtrl(FrmModelT *m, const char *oldName, const char *newName) { + int32_t count = (int32_t)arrlen(m->controls); + bool found = false; + bool data = false; + char old[FRM_MAX_NAME]; + + // oldName may point into a control that is renamed below + snprintf(old, sizeof(old), "%s", oldName); + + if (!newName[0] || frmModelNameInUse(m, newName, old, true)) { + return false; + } + + for (int32_t i = 0; i < count; i++) { + FrmCtrlT *c = m->controls[i]; + + if (strcasecmp(c->name, old) == 0) { + found = true; + data = data || strcasecmp(c->typeName, FRM_DATA_TYPE) == 0; + snprintf(c->name, sizeof(c->name), "%s", newName); + } + } + + if (!found) { + return false; + } + + for (int32_t i = 0; i < count; i++) { + FrmCtrlT *c = m->controls[i]; + + // Children name their container; they would be orphaned otherwise + if (strcasecmp(c->parentName, old) == 0) { + snprintf(c->parentName, sizeof(c->parentName), "%s", newName); + } + + // Controls bound to a renamed Data control follow it + for (int32_t j = 0; data && j < (int32_t)arrlen(c->props); j++) { + FrmPropT *p = &c->props[j]; + + if ((strcasecmp(p->name, BAS_PROP_DATASOURCE) == 0 || strcasecmp(p->name, BAS_PROP_MASTERSOURCE) == 0) && strcasecmp(p->value, old) == 0) { + snprintf(p->value, sizeof(p->value), "%s", newName); + } + } + } + + m->dirty = true; + return true; +} + + +bool frmModelReparent(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *parent) { + int32_t idx = frmModelCtrlPos(m, ctrl); + + if (idx < 0 || parent == ctrl || (parent && (frmModelCtrlPos(m, parent) < 0 || frmModelIsDescendant(m, parent, ctrl)))) { + return false; + } + + frmModelNormalizeOrder(m); + idx = frmModelCtrlPos(m, ctrl); + + if (!canTakeSubtree(m, parent, subtreeHeight(m, idx))) { + return false; + } + + // Moved to the end first, so tree order makes it the last child + int32_t end = frmModelSubtreeEnd(m, idx); + + if (!spliceRun(m->controls, idx, end - idx, (int32_t)arrlen(m->controls))) { + return false; + } + + snprintf(ctrl->parentName, sizeof(ctrl->parentName), "%s", parent ? parent->name : ""); + ctrl->parentIndex = parent ? parent->index : FRM_NO_INDEX; + frmModelNormalizeOrder(m); + m->dirty = true; + return true; +} + + +bool frmModelSetCode(FrmModelT *m, const char *code) { + char *copy = NULL; + + if (code && code[0]) { + copy = strdup(code); + + if (!copy) { + return false; + } + } + + free(m->code); + m->code = copy; + m->dirty = true; + return true; +} + + +bool frmModelSetCtrlProp(FrmModelT *m, FrmCtrlT *ctrl, const char *name, const char *value) { + if (strcasecmp(name, BAS_PROP_NAME) == 0) { + return frmModelRenameCtrl(m, ctrl->name, value); + } + + if (strcasecmp(name, BAS_PROP_INDEX) == 0) { + frmModelMakeArrayMember(m, ctrl, value[0] ? atoi(value) : FRM_NO_INDEX); + return true; + } + + frmCtrlSetPropText(ctrl, name, value); + frmModelOptionSelected(m, ctrl); + m->dirty = true; + return true; +} + + +bool frmModelSetFormProp(FrmModelT *m, const char *name, const char *value) { + if (strcasecmp(name, BAS_PROP_NAME) == 0) { + snprintf(m->name, sizeof(m->name), "%s", value); + } else if (!formPropSet(m, name, value)) { + return false; + } + + m->dirty = true; + return true; +} + + +bool frmModelSetMenuProp(FrmModelT *m, int32_t idx, const char *key, const char *value) { + if (idx < 0 || idx >= (int32_t)arrlen(m->menuItems)) { + return false; + } + + FrmMenuItemT *mi = &m->menuItems[idx]; + + if (strcasecmp(key, BAS_PROP_NAME) == 0) { + if (!value[0] || frmModelNameInUse(m, value, mi->name, true)) { + return false; + } + + retargetContextMenu(m, mi->name, value); + } + + if (!frmMenuSetPropText(mi, key, value)) { + return false; + } + + m->dirty = true; + return true; +} + + +int32_t frmModelSibling(const FrmModelT *m, int32_t idx, int32_t dir) { + FrmCtrlT **arr = m->controls; + int32_t count = (int32_t)arrlen(arr); + + if (idx < 0 || idx >= count) { + return FRM_NO_INDEX; + } + + FrmCtrlT *parent = frmModelFindParent(m, arr[idx]); + + if (dir > 0) { + int32_t end = frmModelSubtreeEnd(m, idx); + + return (end < count && frmModelFindParent(m, arr[end]) == parent) ? end : FRM_NO_INDEX; + } + + for (int32_t j = idx - 1; j >= 0 && arr[j] != parent; j--) { + if (frmModelFindParent(m, arr[j]) == parent) { + return j; + } + } + + return FRM_NO_INDEX; +} + + +int32_t frmModelSubtreeEnd(const FrmModelT *m, int32_t idx) { + int32_t count = (int32_t)arrlen(m->controls); + int32_t end = idx + 1; + + while (end < count && frmModelIsDescendant(m, m->controls[end], m->controls[idx])) { + end++; + } + + return end; +} + + +char *frmModelText(const FrmModelT *m, int32_t *outLen) { + int32_t size = m ? frmModelWriteSize(m) : 0; + char *buf = (size > 0) ? (char *)malloc((size_t)size) : NULL; + + *outLen = FRM_WRITE_TOO_LARGE; + + if (!buf) { + return NULL; + } + + int32_t len = frmModelWrite(m, buf, size); + + if (len < 0) { + free(buf); + *outLen = len; + return NULL; + } + + // Keep only what the text needs; the larger block stays valid if not + char *fitted = (char *)realloc(buf, (size_t)len + 1); + + if (fitted) { + buf = fitted; + } + + *outLen = len; + return buf; +} + + +int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize) { + if (!m || !buf || bufSize <= 0) { + return FRM_WRITE_TOO_LARGE; + } + + int32_t pos = frmEmitClamped(buf, bufSize, 0, FRM_VERSION_LINE); + + pos = frmEmitClamped(buf, bufSize, pos, FRM_KW_BEGIN " " FRM_KW_FORM " %s\n", m->name); + + // Form header in the runtime table's order. Name comes from the + // Begin line; Left / Top are implied by Centered and Width / Height + // by AutoSize; empty HelpTopic / ContextMenu and zero Padding / + // Spacing / WindowState are defaults. Layout is read-only at run + // time but is the loader's container type. The other design + // properties are written when set. + int32_t formPropCount = 0; + const BasPropDescT *formProps = basFormRtFormProps(&formPropCount); + + for (int32_t i = 0; i < formPropCount; i++) { + const BasPropDescT *pd = &formProps[i]; + char val[FRM_MAX_TEXT]; + + if (strcasecmp(pd->name, BAS_PROP_NAME) == 0) { + continue; + } + + if (m->centered && (strcasecmp(pd->name, BAS_PROP_LEFT) == 0 || strcasecmp(pd->name, BAS_PROP_TOP) == 0)) { + continue; + } + + if (m->autoSize && (strcasecmp(pd->name, BAS_PROP_WIDTH) == 0 || strcasecmp(pd->name, BAS_PROP_HEIGHT) == 0)) { + continue; + } + + if (!formFieldText(m, pd->name, val, sizeof(val))) { + int32_t at = formExtraProp(pd->name) ? propFind(m->props, pd->name) : FRM_NO_INDEX; + + if (at >= 0) { + pos = emitPropLine(buf, bufSize, pos, "", pd->name, pd->type, m->props[at].value); + } + + continue; + } + + if (!val[0] && (strcasecmp(pd->name, BAS_PROP_HELPTOPIC) == 0 || strcasecmp(pd->name, BAS_PROP_CONTEXTMENU) == 0)) { + continue; + } + + if ((strcasecmp(pd->name, BAS_PROP_PADDING) == 0 && m->padding == 0) || (strcasecmp(pd->name, BAS_PROP_SPACING) == 0 && m->spacing == 0) || (strcasecmp(pd->name, BAS_PROP_WINDOWSTATE) == 0 && m->windowState == 0)) { + continue; + } + + pos = emitPropLine(buf, bufSize, pos, "", pd->name, pd->type, val); + } + + pos = writeMenus(m, buf, bufSize, pos); + + // Controls, each followed by its own children + FrmCtrlT **parents = resolveParents(m); + + if (arrlen(m->controls) > 0 && !parents) { + return FRM_WRITE_TOO_LARGE; + } + + pos = writeControls(m, parents, buf, bufSize, pos, NULL, 1); + arrfree(parents); + + pos = frmEmitClamped(buf, bufSize, pos, FRM_KW_END "\n"); + + if (pos < 0) { + return pos; + } + + // frmEmitClamped parks pos at bufSize - 1 once the buffer is full, so + // reaching it means the form text was cut short + if (pos >= bufSize - 1) { + return FRM_WRITE_TOO_LARGE; + } + + if (m->code && m->code[0]) { + int32_t codeLen = (int32_t)strlen(m->code); + int32_t avail = bufSize - pos - FRM_NEWLINE_ROOM - 1; + + if (codeLen > avail) { + return FRM_WRITE_TOO_LARGE; + } + + buf[pos++] = '\n'; + memcpy(buf + pos, m->code, codeLen); + pos += codeLen; + + if (buf[pos - 1] != '\n') { + buf[pos++] = '\n'; + } + } + + buf[pos] = '\0'; + return pos; +} + + +int32_t frmModelWriteSize(const FrmModelT *m) { + int32_t formPropCount = 0; + int32_t lines; + + basFormRtFormProps(&formPropCount); + lines = FRM_HEAD_LINES + formPropCount + (int32_t)arrlen(m->menuItems) * FRM_MENU_LINES; + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + const FrmCtrlT *c = m->controls[i]; + const WgtIfaceT *iface = basCtlIface(c->typeName); + + lines += FRM_CTRL_FIXED_LINES + FRM_INT_PROP_COUNT + (int32_t)arrlen(c->props) + (iface ? iface->propCount : 0); + } + + return lines * FRM_MAX_LINE_LEN + (m->code ? (int32_t)strlen(m->code) : 0) + FRM_NEWLINE_ROOM + 1; +} + + +static bool isEmptyValue(uint8_t type, const char *value) { + if (type == WGT_IFACE_STRING) { + return value[0] == '\0'; + } + + if (type == WGT_IFACE_BOOL) { + return !frmParseBool(value); + } + + return atoi(value) == 0; +} + + +static void loadOnCtrlBegin(void *userData, const char *typeName, const char *name) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + FrmCtrlT *cp = frmCtrlNew(typeName, name); + + if (cp) { + arrput(ctx->m->controls, cp); + arrput(ctx->enclosing, (ctx->ctrlDepth > 0) ? ctx->ctrlStack[ctx->ctrlDepth - 1] : NULL); + } + + // Pushed even when the allocation failed so the matching End keeps + // the depth in step with the parser + ctx->ctrlStack[ctx->ctrlDepth++] = cp; +} + + +// Back to the enclosing control block: its lines may follow its children. +static void loadOnCtrlEnd(void *userData) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + + ctx->ctrlDepth--; +} + + +// The parser calls this only inside a control block. +static void loadOnCtrlProp(void *userData, const char *key, const char *value) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + FrmCtrlT *ctrl = ctx->ctrlStack[ctx->ctrlDepth - 1]; + char val[FRM_MAX_LINE_LEN]; + + if (!ctrl) { + return; + } + + snprintf(val, sizeof(val), "%s", value); + frmStripQuotes(val); + frmCtrlSetPropText(ctrl, key, val); +} + + +static bool loadOnFormBegin(void *userData, const char *name) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + + snprintf(ctx->m->name, sizeof(ctx->m->name), "%s", name); + snprintf(ctx->m->caption, sizeof(ctx->m->caption), "%s", name); + ctx->sawForm = true; + return true; +} + + +static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + int32_t codeLen = 0; + const char *codeStart = frmCodeSection(trailingSrc, trailingLen, &codeLen); + + if (!codeStart) { + return; + } + + ctx->m->code = (char *)malloc((size_t)codeLen + 1); + + if (ctx->m->code) { + memcpy(ctx->m->code, codeStart, codeLen); + ctx->m->code[codeLen] = '\0'; + } +} + + +static void loadOnFormProp(void *userData, const char *key, const char *value) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + char val[FRM_MAX_LINE_LEN]; + + snprintf(val, sizeof(val), "%s", value); + frmStripQuotes(val); + formPropSet(ctx->m, key, val); +} + + +static void loadOnMenuBegin(void *userData, const char *name, int32_t level) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + FrmMenuItemT mi; + + frmMenuItemInit(&mi); + snprintf(mi.name, sizeof(mi.name), "%s", name); + mi.level = level; + arrput(ctx->m->menuItems, mi); + ctx->menuStack[ctx->menuDepth++] = (int32_t)arrlen(ctx->m->menuItems) - 1; +} + + +// Back to the enclosing menu item: its lines may follow its submenu's. +static void loadOnMenuEnd(void *userData) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + + ctx->menuDepth--; +} + + +// The parser calls this only inside a menu block. The item is looked up +// by index: arrput for a nested menu may have moved the array. A Name +// key is not a menu property of the file (the Begin line names the item). +static void loadOnMenuProp(void *userData, const char *key, const char *value) { + FrmLoadCtxT *ctx = (FrmLoadCtxT *)userData; + char val[FRM_MAX_LINE_LEN]; + + if (strcasecmp(key, BAS_PROP_NAME) == 0) { + return; + } + + snprintf(val, sizeof(val), "%s", value); + frmStripQuotes(val); + frmMenuSetPropText(&ctx->m->menuItems[ctx->menuStack[ctx->menuDepth - 1]], key, val); +} + + +// Depth-first append of parent's children (in their current relative +// order) and their subtrees to *out. +static void normalizeAppend(FrmCtrlT **src, FrmCtrlT **parents, bool *placed, int32_t count, const FrmCtrlT *parent, FrmCtrlT ***out, int32_t depth) { + for (int32_t i = 0; i < count; i++) { + if (placed[i] || parents[i] != parent) { + continue; + } + + placed[i] = true; + arrput(*out, src[i]); + + if (depth < FRM_MAX_NEST_DEPTH) { + normalizeAppend(src, parents, placed, count, src[i], out, depth + 1); + } + } +} + + +static bool propDrop(FrmPropT **props, const char *name) { + int32_t i = propFind(*props, name); + + if (i < 0) { + return false; + } + + arrdel(*props, i); + return true; +} + + +static int32_t propFind(const FrmPropT *props, const char *name) { + for (int32_t i = 0; i < (int32_t)arrlen(props); i++) { + if (strcasecmp(props[i].name, name) == 0) { + return i; + } + } + + return FRM_NO_INDEX; +} + + +static void propStore(FrmPropT **props, const char *name, const char *value) { + int32_t i = propFind(*props, name); + FrmPropT prop; + + if (i >= 0) { + snprintf((*props)[i].value, sizeof((*props)[i].value), "%s", value); + return; + } + + snprintf(prop.name, sizeof(prop.name), "%s", name); + snprintf(prop.value, sizeof(prop.value), "%s", value); + arrput(*props, prop); +} + + +// The new name (in items) of the item of m's current menus called +// oldName, matched through the ids; NULL when that item was deleted or no +// item had that name. +static const char *renamedMenu(const FrmModelT *m, const FrmMenuItemT *items, const char *oldName) { + int32_t oldCount = (int32_t)arrlen(m->menuItems); + int32_t count = (int32_t)arrlen(items); + + for (int32_t k = 0; k < oldCount; k++) { + if (strcasecmp(m->menuItems[k].name, oldName) != 0) { + continue; + } + + for (int32_t i = 0; i < count; i++) { + if (items[i].id == k + 1) { + return items[i].name; + } + } + + return NULL; + } + + return NULL; +} + + +// The parent of every control, parallel to m->controls (stb_ds; NULL +// when there are no controls or out of memory). +static FrmCtrlT **resolveParents(const FrmModelT *m) { + FrmCtrlT **parents = NULL; + int32_t count = (int32_t)arrlen(m->controls); + + if (count == 0) { + return NULL; + } + + arrsetlen(parents, count); + + if ((int32_t)arrlen(parents) != count) { + arrfree(parents); + return NULL; + } + + for (int32_t i = 0; i < count; i++) { + parents[i] = frmModelFindParent(m, m->controls[i]); + } + + return parents; +} + + +// Point the form's and the controls' ContextMenu bindings at newName where +// they named oldName. +static void retargetContextMenu(FrmModelT *m, const char *oldName, const char *newName) { + if (strcasecmp(m->contextMenu, oldName) == 0) { + snprintf(m->contextMenu, sizeof(m->contextMenu), "%s", newName); + } + + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + const char *ref = frmCtrlGetProp(m->controls[i], BAS_PROP_CONTEXTMENU); + + if (ref && strcasecmp(ref, oldName) == 0) { + frmCtrlSetProp(m->controls[i], BAS_PROP_CONTEXTMENU, newName); + } + } +} + + +// Move arr[start..start + len) so it starts where dest pointed before the +// move (dest <= start or dest >= start + len). False (array untouched) +// when out of memory. +static bool spliceRun(FrmCtrlT **arr, int32_t start, int32_t len, int32_t dest) { + FrmCtrlT **tmp = (FrmCtrlT **)malloc((size_t)len * sizeof(FrmCtrlT *)); + + if (!tmp) { + return false; + } + + memcpy(tmp, &arr[start], (size_t)len * sizeof(FrmCtrlT *)); + + if (dest > start) { + memmove(&arr[start], &arr[start + len], (size_t)(dest - start - len) * sizeof(FrmCtrlT *)); + dest -= len; + } else { + memmove(&arr[dest + len], &arr[dest], (size_t)(start - dest) * sizeof(FrmCtrlT *)); + } + + memcpy(&arr[dest], tmp, (size_t)len * sizeof(FrmCtrlT *)); + free(tmp); + return true; +} + + +// Levels of the subtree at m->controls[idx] (1 = no children); the +// controls are in tree order. +static int32_t subtreeHeight(const FrmModelT *m, int32_t idx) { + int32_t end = frmModelSubtreeEnd(m, idx); + int32_t base = frmModelCtrlDepth(m, m->controls[idx]); + int32_t height = 1; + + for (int32_t i = idx + 1; i < end; i++) { + height = DVX_MAX(height, frmModelCtrlDepth(m, m->controls[i]) - base + 1); + } + + return height; +} + + +// Lines the subtree at m->controls[idx] can take when written. +static int32_t subtreeLines(const FrmModelT *m, int32_t idx) { + int32_t end = frmModelSubtreeEnd(m, idx); + int32_t lines = 0; + + for (int32_t i = idx; i < end; i++) { + const FrmCtrlT *c = m->controls[i]; + const WgtIfaceT *iface = basCtlIface(c->typeName); + + lines += FRM_CTRL_FIXED_LINES + FRM_INT_PROP_COUNT + (int32_t)arrlen(c->props) + (iface ? iface->propCount : 0); + } + + return lines; +} + + +// Write the controls whose resolved parent is parent, each followed by +// its own children. parents[i] is the parent of m->controls[i]; one whose +// parent is missing is written at the top level. +static int32_t writeControls(const FrmModelT *m, FrmCtrlT **parents, char *buf, int32_t bufSize, int32_t pos, const FrmCtrlT *parent, int32_t indent) { + for (int32_t i = 0; i < (int32_t)arrlen(m->controls); i++) { + if (parents[i] == parent) { + pos = writeCtrlTree(m, parents, m->controls[i], buf, bufSize, pos, indent); + } + } + + return pos; +} + + +// One control's block with its children inside. The depth cap keeps a +// parent cycle finite and matches the parser's nesting limit. +static int32_t writeCtrlTree(const FrmModelT *m, FrmCtrlT **parents, const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent) { + char pad[FRM_PAD_BUF]; + + buildPad(indent, pad); + pos = emitControlHead(ctrl, buf, bufSize, pos, pad); + + if (indent < FRM_MAX_NEST_DEPTH) { + pos = writeControls(m, parents, buf, bufSize, pos, ctrl, indent + 1); + } + + return frmEmitClamped(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad); +} + + +// Menu items as nested Begin Menu blocks. A level-L item's Begin line +// sits at indent L + 1 (inside the Form block), its properties one level +// deeper. +static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_t pos) { + int32_t menuCount = (int32_t)arrlen(m->menuItems); + int32_t curLevel = 0; + + for (int32_t i = 0; i < menuCount; i++) { + const FrmMenuItemT *mi = &m->menuItems[i]; + char pad[FRM_PAD_BUF]; + + // Close blocks back to this item's level + while (curLevel > mi->level) { + curLevel--; + pos = emitIndented(buf, bufSize, pos, curLevel + 1, FRM_KW_END); + } + + buildPad(mi->level + 1, pad); + pos = frmEmitClamped(buf, bufSize, pos, "%s" FRM_KW_BEGIN " " FRM_KW_MENU " %s\n", pad, mi->name); + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, mi->caption); + + if (mi->checked || mi->checkable) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CHECKED, WGT_IFACE_BOOL, mi->checked ? FRM_TRUE_TEXT : FRM_FALSE_TEXT); + } + + if (mi->radioCheck) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_RADIOCHECK, WGT_IFACE_BOOL, FRM_TRUE_TEXT); + } + + if (mi->shortcut[0]) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_SHORTCUT, WGT_IFACE_STRING, mi->shortcut); + } + + if (mi->shortcutText[0]) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_SHORTCUTTEXT, WGT_IFACE_STRING, mi->shortcutText); + } + + if (!mi->enabled) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, FRM_FALSE_TEXT); + } + + // Visible matters only on top-level menus: False makes a popup + if (mi->level == 0 && !mi->visible) { + pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, FRM_FALSE_TEXT); + } + + curLevel = mi->level + 1; + } + + while (curLevel > 0) { + curLevel--; + pos = emitIndented(buf, bufSize, pos, curLevel + 1, FRM_KW_END); + } + + return pos; +} diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.h b/src/apps/kpunch/dvxbasic/formrt/frmModel.h new file mode 100644 index 00000000..5fc3679a --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/frmModel.h @@ -0,0 +1,486 @@ +// 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. + +// frmModel.h -- the .frm document model (BASIC run-time library) +// +// A form file as data, with no widgets and no windows: the form's +// properties, its controls (type, name, control array index, parent, +// the built-in size and state fields and every other "Key = Value" +// pair as text), its menu items and its code section. frmModelLoad +// reads .frm text (through frmParse), frmModelWrite writes it back in +// the one canonical form the DVX BASIC IDE saves; the edit operations +// below keep the model consistent (control subtrees stay together, +// children follow a renamed or re-indexed container, menu levels stay a +// tree). The C IDE's designer, property sheet, menu editor, copy and +// paste all work on this model; designer.bas binds it for BASIC +// programs (frmModelBas.c). +// +// Controls are kept in tree order: each control is directly followed by +// its own subtree. The parent is named by parentName plus parentIndex, +// so a child of one member of a container control array belongs to that +// member only. Only values somebody set are stored and written: a +// property the model does not hold is the control's default and is not +// written. + +#ifndef DVXBASIC_FRMMODEL_H +#define DVXBASIC_FRMMODEL_H + +#include "formrt.h" +#include "frmParser.h" + +#include +#include + +// ============================================================ +// Limits +// ============================================================ +// +// Derived from the parser's limits so the model never writes a file the +// parser and the 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). A +// value is part of a line of at most FRM_MAX_LINE_LEN - 1 characters; +// whether it fits depends on the indent and key of its line, so the +// writer checks every property line and fails with +// FRM_WRITE_LINE_TOO_LONG rather than write a line the parser would cut. + +#define FRM_MAX_NAME BAS_MAX_IDENT +#define FRM_MAX_TEXT FRM_MAX_LINE_LEN +#define FRM_INDENT_SPACES 4 // .frm indent per block level +#define FRM_MAX_NEST_DEPTH (FRM_MAX_NESTING - 1) // deepest control block below the Form block +#define FRM_MAX_MENU_LEVEL (FRM_MAX_NESTING - 2) // deepest menu level (0 = menu bar) +#define FRM_MENU_STACK_DEPTH (FRM_MAX_MENU_LEVEL + 1) // menu levels 0..FRM_MAX_MENU_LEVEL +#define FRM_MAX_PROP_INDENT (FRM_MAX_NESTING * FRM_INDENT_SPACES) // indent of the deepest property line +#define FRM_NO_INDEX (-1) // control not in a control array; no such control / menu item +#define FRM_NEW_MENU_ID 0 // FrmMenuItemT.id of an item added since the ids were given out +#define FRM_DEFAULT_CTRL_W 100 // MinWidth of a new control +#define FRM_DEFAULT_CTRL_H 30 // MinHeight of a new control + +// frmModelWrite / frmModelText / frmModelCtrlText failures (never a +// partial result) +#define FRM_WRITE_TOO_LARGE (-1) // no model, or the text does not fit in bufSize +#define FRM_WRITE_LINE_TOO_LONG (-2) // a property line would exceed FRM_MAX_LINE_LEN - 1 characters + +// ============================================================ +// Types +// ============================================================ + +// One stored "Key = Value" pair (the value without quotes). +typedef struct { + char name[FRM_MAX_NAME]; + char value[FRM_MAX_TEXT]; +} FrmPropT; + +// A menu item. The items of a form are one flat array in file order; +// level gives the tree (0 = a menu on the menu bar or a popup menu, 1 = +// its items, 2+ = submenus). id is a free identity for editors: give +// every item id = position + 1 before editing, and an item that still +// has id > 0 afterwards is the one that was at position id - 1 +// (frmModelMenuReplace follows renames by it). + +typedef struct { + char caption[FRM_MAX_TEXT]; // "&File", "-" for a separator + char name[FRM_MAX_NAME]; // "mnuFile" + char shortcut[BAS_MENU_KEY_TEXT_MAX]; // Shortcut: key that runs the item ("" = none) + char shortcutText[BAS_MENU_KEY_TEXT_MAX]; // ShortcutText: key text only ("" = none) + int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu + int32_t id; // editor identity (see above) + bool checked; + bool checkable; // Checked is written (True or False): a check item + bool radioCheck; // radio bullet instead of a check mark + bool enabled; // default true + bool visible; // default true; false on a top-level item = popup only +} FrmMenuItemT; + +// A control. props holds every "Key = Value" pair that has no field of +// its own, including the widget interface properties, written in the +// canonical form for their interface type. userData is the caller's +// (the IDE keeps the control's live widget there): the model never +// reads it and copies, pastes and loads start it at NULL. + +typedef struct { + char name[FRM_MAX_NAME]; + char typeName[FRM_MAX_NAME]; + char parentName[FRM_MAX_NAME]; // empty = top-level (child of the form) + int32_t parentIndex; // parent's control array index (FRM_NO_INDEX = not an array member) + int32_t index; // control array index (FRM_NO_INDEX = not in an array) + int32_t left; // position hint (0 = layout-managed) + int32_t top; + int32_t width; // MinWidth + int32_t height; // MinHeight + int32_t maxWidth; // 0 = no cap + int32_t maxHeight; // 0 = no cap + int32_t weight; // layout weight (0 = fixed size) + int32_t dragMode; // DragMode (0 manual, 1 automatic) + char helpTopic[FRM_MAX_NAME]; // help topic for F1 + bool visible; // default true + bool enabled; // default true + FrmPropT *props; // stb_ds array + void *userData; // the caller's, see above +} FrmCtrlT; + +// The built-in integer control properties: one table drives loading, +// writing, the property text functions and the IDE's property grid. + +typedef enum { + FRM_SAVE_ALWAYS, + FRM_SAVE_IF_NONZERO, + FRM_SAVE_IF_POSITIVE +} FrmSaveRuleE; + +typedef struct { + const char *name; // .frm key and property name + const char *alias; // other .frm key accepted on load (NULL = none) + int32_t offset; // offsetof(FrmCtrlT, field) + FrmSaveRuleE saveRule; +} FrmIntPropT; + +// A form document. props holds the other form properties of the +// runtime's form table that were set (Icon, Tag, MousePointer ...). + +typedef struct FrmModelT { + char name[FRM_MAX_NAME]; + char caption[FRM_MAX_TEXT]; + int32_t width; + int32_t height; + int32_t left; // initial position (used when not centered) + int32_t top; + int32_t padding; // content box Padding / Spacing in pixels (0 = the layout's default) + int32_t spacing; + int32_t windowState; // WindowState when first shown (0 = normal, not written) + FrmCtrlT **controls; // stb_ds array of heap-allocated controls, in tree order + FrmMenuItemT *menuItems; // stb_ds array (NULL: no menus) + FrmPropT *props; // stb_ds array + char *code; // BASIC code section (malloc'd, NULL = none) + char layout[FRM_MAX_NAME]; // "VBox", "HBox", ... + char helpTopic[FRM_MAX_NAME]; // form-level help topic + char contextMenu[FRM_MAX_NAME]; // form popup menu name ("" = none) + bool centered; // centered on the screen (else left / top) + bool autoSize; // sized to fit its contents (else width / height) + bool resizable; + bool dirty; // changed by an edit operation since it was made or loaded +} FrmModelT; + +// Identifier rule for names (the compiler's basIsValidIdent): the model +// lives in the run-time library, which has no compiler. +typedef bool (*FrmIdentFnT)(const char *name); + +// ============================================================ +// Text +// ============================================================ + +// 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 and a NUL byte ends it. Returns its +// start and length, or NULL when there is no code. +const char *frmCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen); + +// Clamped formatted append into a fixed buffer: returns pos unchanged once +// it has reached bufSize, else the new position, capped at bufSize - 1 +// when the text was cut. +int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); + +// ============================================================ +// Model lifecycle and .frm text +// ============================================================ + +// A new form with the runtime form's defaults (VBox layout, centered, +// resizable, not auto-sized, Caption = name). NULL when out of memory. +FrmModelT *frmModelNew(const char *name); + +// Parse .frm text into a new model; NULL when the text is not a form +// (frmParse fails or there is no Begin Form line), two controls share a +// name and index (which one a child or the code names is ambiguous) or +// memory runs out. Of several OptionButtons of one container saved with +// Value True, the last keeps it. +FrmModelT *frmModelLoad(const char *text, int32_t len); + +void frmModelFree(FrmModelT *m); + +// Write the model as .frm text into buf (NUL-terminated). Returns the +// length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG. +int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize); + +// A buffer size that always holds frmModelWrite's text for m. +int32_t frmModelWriteSize(const FrmModelT *m); + +// The .frm text in a new malloc'd string, or NULL with *outLen set to an +// FRM_WRITE_* code. +char *frmModelText(const FrmModelT *m, int32_t *outLen); + +// Replace the code section (NULL or "" = none). False when out of memory. +bool frmModelSetCode(FrmModelT *m, const char *code); + +// ============================================================ +// Form properties +// ============================================================ + +// The text of form property name as the model holds it (Name, Caption, +// Layout, Width ... and the other design properties of the runtime's +// form table: an unset one gives "", "0" or "False"). False for a +// run-time-only property (Visible, DesignMode, DesignSelection, the +// object properties) or an unknown name. +bool frmModelFormProp(const FrmModelT *m, const char *name, char *out, int32_t outSize); + +// Set form property name from text (unquoted). Name renames the form +// (not checked: see frmModelNameInUse). Setting one of the other table +// properties to its empty value removes it. False for a run-time-only +// or unknown property. +bool frmModelSetFormProp(FrmModelT *m, const char *name, const char *value); + +// ============================================================ +// Controls +// ============================================================ + +// A blank control: not in an array, top-level, visible, enabled, the +// default size. +void frmCtrlInit(FrmCtrlT *ctrl); + +// A new heap control of a type with a name (blank, see frmCtrlInit). +FrmCtrlT *frmCtrlNew(const char *typeName, const char *name); + +// Free a heap control and its stored properties. +void frmCtrlFree(FrmCtrlT *ctrl); + +// A stored property's value, NULL when the control does not hold one. +const char *frmCtrlGetProp(const FrmCtrlT *ctrl, const char *name); + +// Store (add or replace) a property value; longer values are cut to +// FRM_MAX_TEXT - 1 characters. A name that cannot be a .frm key +// (frmIsPropKey) is not stored. +void frmCtrlSetProp(FrmCtrlT *ctrl, const char *name, const char *value); + +// Remove a stored property. False when it held none. +bool frmCtrlRemoveProp(FrmCtrlT *ctrl, const char *name); + +// The text of property name as the control holds it: Name, Index (-1 +// when not in an array), the built-in integers, HelpTopic, Visible / +// Enabled (when the widget does not define them itself) and the stored +// properties. False when the control holds no such property. +bool frmCtrlPropText(const FrmCtrlT *ctrl, const char *name, char *out, int32_t outSize); + +// Apply a property from text (unquoted) the way the .frm loader does: +// the built-in integers (and their aliases Width / Height), Index, +// HelpTopic, Visible / Enabled (unless the widget defines them), a VB +// TabIndex dropped (unless the widget defines it), anything else stored. +void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value); + +// The widget interface property propName of a control type, NULL when +// the type has none (or is not registered). +const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName); + +// Built-in integer property table: entry idx (NULL past the end), the +// entry for a .frm key (name or alias), and a control's value of it. +const FrmIntPropT *frmIntPropAt(int32_t idx); +const FrmIntPropT *frmIntPropFind(const char *name); +int32_t frmIntPropGet(const FrmCtrlT *ctrl, const FrmIntPropT *ip); +void frmIntPropSet(FrmCtrlT *ctrl, const FrmIntPropT *ip, int32_t value); + +// ============================================================ +// Controls of a model +// ============================================================ + +// The position of control name (control array member index; FRM_NO_INDEX +// matches the first control of that name), FRM_NO_INDEX when none. +int32_t frmModelFindCtrl(const FrmModelT *m, const char *name, int32_t index); + +// The position of ctrl in m->controls, FRM_NO_INDEX when it is not there. +int32_t frmModelCtrlPos(const FrmModelT *m, const FrmCtrlT *ctrl); + +// The control ctrl's parentName / parentIndex name: the exact name + +// index match, else the first control of that name. NULL for a +// top-level control or one whose parent no longer exists. +FrmCtrlT *frmModelFindParent(const FrmModelT *m, const FrmCtrlT *ctrl); + +// Block depth below the Form block (1 = top-level). A child may only be +// added to a control whose depth is below FRM_MAX_NEST_DEPTH. +int32_t frmModelCtrlDepth(const FrmModelT *m, const FrmCtrlT *ctrl); + +// True when ancestor is ctrl's parent, grandparent ... +bool frmModelIsDescendant(const FrmModelT *m, const FrmCtrlT *ctrl, const FrmCtrlT *ancestor); + +// One past the subtree of m->controls[idx]. +int32_t frmModelSubtreeEnd(const FrmModelT *m, int32_t idx); + +// True when name (any case) is the form's, a control's or (checkMenus) a +// menu item's name. exceptName is never in use, so a control array or +// the object being renamed does not collide with itself. +bool frmModelNameInUse(const FrmModelT *m, const char *name, const char *exceptName, bool checkMenus); + +// A free name for a new control of a type: the type's name prefix +// (Command, Label ...) and the next number. +void frmModelAutoName(const FrmModelT *m, const char *typeName, char *buf, int32_t bufSize); + +// Put the controls in tree order (each control directly followed by its +// subtree), siblings keeping their order. True when the order changed. +bool frmModelNormalizeOrder(FrmModelT *m); + +// Add a new control as the last child of parent (NULL = the form), named +// name ("" or NULL: a free name, see frmModelAutoName), with Caption = +// its name. NULL when the name is in use, parent cannot hold controls or +// is at the deepest level, or out of memory. +FrmCtrlT *frmModelAddCtrl(FrmModelT *m, const char *typeName, const char *name, FrmCtrlT *parent); + +// Remove ctrl and its subtree (they are freed). +void frmModelRemoveCtrl(FrmModelT *m, FrmCtrlT *ctrl); + +// Rename every control called oldName (a whole control array) to +// newName: their children follow, and DataSource / MasterSource that +// named a renamed Data control too. False (nothing changed) when there +// is no such control or newName is in use. Identifier rules are the +// caller's (FrmIdentFnT). +bool frmModelRenameCtrl(FrmModelT *m, const char *oldName, const char *newName); + +// Give ctrl control array index index (FRM_NO_INDEX: no array); its +// children follow it. +void frmModelMakeArrayMember(FrmModelT *m, FrmCtrlT *ctrl, int32_t index); + +// Move ctrl, with its subtree, among its siblings to just before sibling +// before (NULL: after the last sibling). False when before is not a +// sibling of ctrl. +bool frmModelMoveCtrl(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *before); + +// The position of the sibling next to m->controls[idx] in direction dir +// (+1 the one after its subtree, -1 the one before), FRM_NO_INDEX when +// there is none. +int32_t frmModelSibling(const FrmModelT *m, int32_t idx, int32_t dir); + +// Make ctrl, with its subtree, the last child of parent (NULL = the +// form). False when parent is ctrl or inside it, cannot hold controls, +// or the subtree would nest deeper than FRM_MAX_NEST_DEPTH. +bool frmModelReparent(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *parent); + +// Set a control property from text with every rule an editor needs: Name +// renames (frmModelRenameCtrl), Index makes an array member, anything +// else as frmCtrlSetPropText, then the OptionButton group rule +// (frmModelOptionSelected). False when the rename was refused. +bool frmModelSetCtrlProp(FrmModelT *m, FrmCtrlT *ctrl, const char *name, const char *value); + +// OptionButtons in one container are one group: when ctrl is an +// OptionButton whose Value is True, Value is cleared on the others of its +// group. Nothing for anything else. +void frmModelOptionSelected(FrmModelT *m, FrmCtrlT *ctrl); + +// ============================================================ +// Copy and paste +// ============================================================ + +// ctrl and its subtree as .frm control blocks (indent 0), the text a +// clipboard copy holds, in a new malloc'd string; NULL with *outLen set +// to an FRM_WRITE_* code on failure. +char *frmModelCtrlText(const FrmModelT *m, const FrmCtrlT *ctrl, int32_t *outLen); + +// Read one or more .frm control blocks (copied text) into a scratch +// model whose controls[] are the blocks and their children; NULL when +// the text holds no control. +FrmModelT *frmModelCtrlsFromText(const char *text, int32_t len); + +// Move srcRoot and its subtree out of src into m as the last child of +// parent (NULL = the form). The root is named rootName with index +// rootIndex (rootName NULL or "": its own name when free, else a free +// one, not in an array); a child whose name is in use in m gets a free +// one (members of one array together). Children follow their renamed +// parents. Returns the inserted root, NULL when parent cannot take it. +FrmCtrlT *frmModelInsertTree(FrmModelT *m, FrmModelT *src, FrmCtrlT *srcRoot, FrmCtrlT *parent, const char *rootName, int32_t rootIndex); + +// ============================================================ +// Menus +// ============================================================ +// +// The frmMenu* functions work on any stb_ds array of items (an editor's +// working copy as well as m->menuItems); they keep the levels a tree. + +// A blank item: enabled, visible, level 0, id FRM_NEW_MENU_ID. +void frmMenuItemInit(FrmMenuItemT *mi); + +// A name for item idx made from its caption: mnuSep1, mnuSep2 ... for a +// separator, else "mnu" and the caption's letters and digits. +void frmMenuAutoName(const FrmMenuItemT *items, int32_t idx, char *out, int32_t outSize); + +// One past the subtree (the item and the deeper items after it) of item +// idx. +int32_t frmMenuSubtreeEnd(const FrmMenuItemT *items, int32_t idx); + +// Insert a blank item at position at (clamped to the array) with level +// (clamped so the levels stay a tree). Returns its position, or +// FRM_NO_INDEX when out of memory. +int32_t frmMenuInsert(FrmMenuItemT **items, int32_t at, int32_t level); + +// Remove item idx and its subtree. +void frmMenuRemove(FrmMenuItemT **items, int32_t idx); + +// Move item idx, with its subtree, past its previous (dir -1) or next +// (dir +1) sibling. Returns its new position, FRM_NO_INDEX when it has +// no such sibling. +int32_t frmMenuMove(FrmMenuItemT **items, int32_t idx, int32_t dir); + +// Indent (delta +1) or outdent (delta -1) item idx with its subtree. An +// item can go one level below the item above it at most, and not past +// FRM_MAX_MENU_LEVEL; a level-0 item cannot be outdented. False when +// refused. +bool frmMenuIndent(FrmMenuItemT *items, int32_t idx, int32_t delta); + +// The first item that breaks the menu tree (the first item is not level +// 0, or an item is more than one level below the one above it), +// FRM_NO_INDEX when the levels are valid. +int32_t frmMenuCheckLevels(const FrmMenuItemT *items); + +// Remove items with neither caption nor name that have no children. +void frmMenuStripBlank(FrmMenuItemT **items); + +// The text of menu property key (Name, Caption, Shortcut, ShortcutText, +// Checked, RadioCheck, Enabled, Visible, Level). False for another key. +bool frmMenuPropText(const FrmMenuItemT *mi, const char *key, char *out, int32_t outSize); + +// Apply a menu property from text (unquoted), as the .frm loader does +// (Checked makes a check item). Name sets the name only (see +// frmModelSetMenuProp). False for another key (Level included: use +// frmMenuIndent). +bool frmMenuSetPropText(FrmMenuItemT *mi, const char *key, const char *value); + +// Check a menu array before it replaces m's: the levels, and every name +// is set, an identifier (isIdent; NULL skips this), not the form's or a +// control's, not used twice; every Shortcut is a key. Returns the first +// bad item with the reason in reason, FRM_NO_INDEX when all are good. +int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize); + +// Replace m's menu items by a copy of items. ContextMenu bindings of the +// form and its controls follow renamed items, matched by id (see +// FrmMenuItemT). False when out of memory (m unchanged). +bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items); + +// Set property key of menu item idx from text: Name renames the item +// (False when the name is in use; ContextMenu bindings follow), the rest +// as frmMenuSetPropText. +bool frmModelSetMenuProp(FrmModelT *m, int32_t idx, const char *key, const char *value); + +// ============================================================ +// Live forms +// ============================================================ + +// A model of a loaded form as it is now: the form's properties, its +// controls in their containers with every property that differs from a +// new control of the type, its menus. No code section. NULL when out of +// memory. (frmModelBas.c) +FrmModelT *frmModelFromForm(BasFormRtT *rt, BasFormT *form); + +#endif // DVXBASIC_FRMMODEL_H diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c b/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c new file mode 100644 index 00000000..3b556f9a --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c @@ -0,0 +1,920 @@ +// 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. + +// frmModelBas.c -- DVX BASIC entry points of the .frm model (designer.bas +// Frm*, FrmCtl*, FrmMenu*, FormText$) and the model of a live form +// (frmModelFromForm) +// +// Called through DECLARE LIBRARY "basrt", so the signatures use only +// BASIC-marshalable types: strings and LONG in, LONG or STRING out, True +// (-1) / False (0). A model is a handle (1, 2, ...; 0 means none) owned +// by the program that opened it and freed when that program ends. +// Controls and menu items are numbered from 0 in model order (controls: +// each one followed by its children); -1 means none (the form as a +// parent, the end as a position). A returned string stays valid until +// the next call returning one. + +#include "frmModel.h" +#include "dvxMem.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define FRM_VALUE_MAX FRM_MAX_TEXT +#define FRM_SCRATCH_SIZE 64 // the hidden window new controls are made in to read their defaults +#define FRM_SCRATCH_TITLE "" +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" +#define FRM_BAS_TRUE_TEXT "True" +#define FRM_BAS_FALSE_TEXT "False" + +typedef void (*FrmCleanupFnT)(int32_t appId); +typedef void (*FrmCleanupRegFnT)(FrmCleanupFnT fn); + +// One open model and the program it belongs to (m NULL: free slot) +typedef struct { + FrmModelT *m; + int32_t appId; +} BasFrmHandleT; + +// Building the model of a live form +typedef struct { + BasFormRtT *rt; + BasFormT *form; + FrmModelT *m; + WidgetT *scratch; // content box of a hidden window: default controls are made there +} FrmLiveT; + +// ============================================================ +// Module state +// ============================================================ + +static BasFrmHandleT *sModels = NULL; // stb_ds; handle = index + 1 +static char *sText = NULL; // stb_ds: the last string result +static bool sCleanupHooked = false; + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static BasControlT *ctrlForWidget(const BasFormT *form, const WidgetT *w); +static FrmCtrlT *ctrlOf(int32_t handle, int32_t i, FrmModelT **outModel); +const char *FormText(const char *formName); +int32_t FrmCtlAdd(int32_t handle, const char *typeName, const char *name, int32_t parent); +int32_t FrmCtlCount(int32_t handle); +int32_t FrmCtlFind(int32_t handle, const char *name, int32_t index); +const char *FrmCtlGet(int32_t handle, int32_t i, const char *prop); +int32_t FrmCtlIndex(int32_t handle, int32_t i); +int32_t FrmCtlMove(int32_t handle, int32_t i, int32_t before); +const char *FrmCtlName(int32_t handle, int32_t i); +int32_t FrmCtlParent(int32_t handle, int32_t i); +int32_t FrmCtlPaste(int32_t handle, const char *text, int32_t parent); +int32_t FrmCtlPropCount(int32_t handle, int32_t i); +const char *FrmCtlPropName(int32_t handle, int32_t i, int32_t j); +void FrmCtlRemove(int32_t handle, int32_t i); +int32_t FrmCtlRemoveProp(int32_t handle, int32_t i, const char *prop); +int32_t FrmCtlReparent(int32_t handle, int32_t i, int32_t parent); +int32_t FrmCtlSet(int32_t handle, int32_t i, const char *prop, const char *value); +const char *FrmCtlText(int32_t handle, int32_t i); +const char *FrmCtlType(int32_t handle, int32_t i); +void FrmFree(int32_t handle); +const char *FrmGet(int32_t handle, const char *prop); +const char *FrmGetCode(int32_t handle); +int32_t FrmMenuAdd(int32_t handle, int32_t at, int32_t level); +int32_t FrmMenuCount(int32_t handle); +const char *FrmMenuGet(int32_t handle, int32_t i, const char *prop); +int32_t FrmMenuIndent(int32_t handle, int32_t i, int32_t delta); +int32_t FrmMenuMove(int32_t handle, int32_t i, int32_t dir); +void FrmMenuRemove(int32_t handle, int32_t i); +int32_t FrmMenuSet(int32_t handle, int32_t i, const char *prop, const char *value); +FrmModelT *frmModelFromForm(BasFormRtT *rt, BasFormT *form); +int32_t FrmNew(const char *name); +int32_t FrmOpen(const char *text); +int32_t FrmOpenFile(const char *path); +int32_t FrmSave(int32_t handle, const char *path); +int32_t FrmSet(int32_t handle, const char *prop, const char *value); +int32_t FrmSetCode(int32_t handle, const char *code); +const char *FrmText(int32_t handle); +static int32_t handleNew(FrmModelT *m); +static void liveControls(FrmLiveT *live, const WidgetT *parentWidget, FrmCtrlT *parent); +static void liveCtrl(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *parent); +static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc); +static void liveForm(FrmLiveT *live); +static void liveMenu(FrmLiveT *live, const MenuT *menu, int32_t level); +static void liveMenuAdd(FrmLiveT *live, const char *name, const char *label, int32_t level); +static const char *liveMenuName(const BasFormT *form, const MenuT *menu); +static void liveMenus(FrmLiveT *live); +static FrmModelT *modelOf(int32_t handle); +static const char *textOf(const char *s); +static void valueText(BasValueT v, uint8_t type, char *out, int32_t outSize); + + +// Shell app-cleanup hook: free the models a program that ended left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sModels); i++) { + if (sModels[i].m && sModels[i].appId == appId) { + FrmFree(i + 1); + } + } +} + + +static BasControlT *ctrlForWidget(const BasFormT *form, const WidgetT *w) { + for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { + if (form->controls[i]->widget == w) { + return form->controls[i]; + } + } + + return NULL; +} + + +// Control i of an open model (NULL for none); *outModel (may be NULL) +// gets the model. +static FrmCtrlT *ctrlOf(int32_t handle, int32_t i, FrmModelT **outModel) { + FrmModelT *m = modelOf(handle); + + if (outModel) { + *outModel = m; + } + + return (m && i >= 0 && i < (int32_t)arrlen(m->controls)) ? m->controls[i] : NULL; +} + + +// The .frm text of loaded form formName (its first copy) as it is now. +const char *FormText(const char *formName) { + BasFormRtT *rt = basFormRtCurrent(); + BasFormT *form = NULL; + + for (int32_t i = 0; rt && formName && i < (int32_t)arrlen(rt->forms); i++) { + if (strcasecmp(rt->forms[i]->name, formName) == 0) { + form = rt->forms[i]; + break; + } + } + + FrmModelT *m = form ? frmModelFromForm(rt, form) : NULL; + int32_t len = 0; + char *text = m ? frmModelText(m, &len) : NULL; + + frmModelFree(m); + + const char *out = textOf(text ? text : ""); + + free(text); + return out; +} + + +int32_t FrmCtlAdd(int32_t handle, const char *typeName, const char *name, int32_t parent) { + FrmModelT *m = modelOf(handle); + FrmCtrlT *p = ctrlOf(handle, parent, NULL); + + if (!m || !typeName || (parent >= 0 && !p)) { + return FRM_NO_INDEX; + } + + FrmCtrlT *ctrl = frmModelAddCtrl(m, typeName, name, p); + + return ctrl ? frmModelCtrlPos(m, ctrl) : FRM_NO_INDEX; +} + + +int32_t FrmCtlCount(int32_t handle) { + FrmModelT *m = modelOf(handle); + + return m ? (int32_t)arrlen(m->controls) : 0; +} + + +int32_t FrmCtlFind(int32_t handle, const char *name, int32_t index) { + FrmModelT *m = modelOf(handle); + + return (m && name) ? frmModelFindCtrl(m, name, index) : FRM_NO_INDEX; +} + + +const char *FrmCtlGet(int32_t handle, int32_t i, const char *prop) { + FrmCtrlT *c = ctrlOf(handle, i, NULL); + char val[FRM_VALUE_MAX]; + + if (!c || !prop || !frmCtrlPropText(c, prop, val, sizeof(val))) { + val[0] = '\0'; + } + + return textOf(val); +} + + +int32_t FrmCtlIndex(int32_t handle, int32_t i) { + FrmCtrlT *c = ctrlOf(handle, i, NULL); + + return c ? c->index : FRM_NO_INDEX; +} + + +int32_t FrmCtlMove(int32_t handle, int32_t i, int32_t before) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + FrmCtrlT *b = ctrlOf(handle, before, NULL); + + if (!c || (before >= 0 && !b)) { + return BAS_FALSE; + } + + return frmModelMoveCtrl(m, c, b) ? BAS_TRUE : BAS_FALSE; +} + + +const char *FrmCtlName(int32_t handle, int32_t i) { + FrmCtrlT *c = ctrlOf(handle, i, NULL); + + return textOf(c ? c->name : ""); +} + + +int32_t FrmCtlParent(int32_t handle, int32_t i) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + FrmCtrlT *p = c ? frmModelFindParent(m, c) : NULL; + + return p ? frmModelCtrlPos(m, p) : FRM_NO_INDEX; +} + + +// The pasted root's position; its name is kept when free. +int32_t FrmCtlPaste(int32_t handle, const char *text, int32_t parent) { + FrmModelT *m = modelOf(handle); + FrmCtrlT *p = ctrlOf(handle, parent, NULL); + + if (!m || !text || (parent >= 0 && !p)) { + return FRM_NO_INDEX; + } + + FrmModelT *clip = frmModelCtrlsFromText(text, (int32_t)strlen(text)); + FrmCtrlT *root = clip ? frmModelInsertTree(m, clip, clip->controls[0], p, NULL, FRM_NO_INDEX) : NULL; + + frmModelFree(clip); + return root ? frmModelCtrlPos(m, root) : FRM_NO_INDEX; +} + + +int32_t FrmCtlPropCount(int32_t handle, int32_t i) { + FrmCtrlT *c = ctrlOf(handle, i, NULL); + + return c ? (int32_t)arrlen(c->props) : 0; +} + + +const char *FrmCtlPropName(int32_t handle, int32_t i, int32_t j) { + FrmCtrlT *c = ctrlOf(handle, i, NULL); + + return textOf((c && j >= 0 && j < (int32_t)arrlen(c->props)) ? c->props[j].name : ""); +} + + +void FrmCtlRemove(int32_t handle, int32_t i) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + + if (c) { + frmModelRemoveCtrl(m, c); + } +} + + +int32_t FrmCtlRemoveProp(int32_t handle, int32_t i, const char *prop) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + + if (!c || !prop || !frmCtrlRemoveProp(c, prop)) { + return BAS_FALSE; + } + + m->dirty = true; + return BAS_TRUE; +} + + +int32_t FrmCtlReparent(int32_t handle, int32_t i, int32_t parent) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + FrmCtrlT *p = ctrlOf(handle, parent, NULL); + + if (!c || (parent >= 0 && !p)) { + return BAS_FALSE; + } + + return frmModelReparent(m, c, p) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FrmCtlSet(int32_t handle, int32_t i, const char *prop, const char *value) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + + if (!c || !prop || !value) { + return BAS_FALSE; + } + + return frmModelSetCtrlProp(m, c, prop, value) ? BAS_TRUE : BAS_FALSE; +} + + +// Control i and its children as .frm text (what a copy puts on the +// Clipboard), "" on failure. +const char *FrmCtlText(int32_t handle, int32_t i) { + FrmModelT *m = NULL; + FrmCtrlT *c = ctrlOf(handle, i, &m); + int32_t len = 0; + char *text = c ? frmModelCtrlText(m, c, &len) : NULL; + const char *out = textOf(text ? text : ""); + + free(text); + return out; +} + + +const char *FrmCtlType(int32_t handle, int32_t i) { + FrmCtrlT *c = ctrlOf(handle, i, NULL); + + return textOf(c ? c->typeName : ""); +} + + +void FrmFree(int32_t handle) { + if (modelOf(handle)) { + frmModelFree(sModels[handle - 1].m); + sModels[handle - 1].m = NULL; + } +} + + +const char *FrmGet(int32_t handle, const char *prop) { + FrmModelT *m = modelOf(handle); + char val[FRM_VALUE_MAX]; + + if (!m || !prop || !frmModelFormProp(m, prop, val, sizeof(val))) { + val[0] = '\0'; + } + + return textOf(val); +} + + +const char *FrmGetCode(int32_t handle) { + FrmModelT *m = modelOf(handle); + + return textOf((m && m->code) ? m->code : ""); +} + + +int32_t FrmMenuAdd(int32_t handle, int32_t at, int32_t level) { + FrmModelT *m = modelOf(handle); + + if (!m) { + return FRM_NO_INDEX; + } + + int32_t idx = frmMenuInsert(&m->menuItems, at < 0 ? (int32_t)arrlen(m->menuItems) : at, level); + + m->dirty = true; + return idx; +} + + +int32_t FrmMenuCount(int32_t handle) { + FrmModelT *m = modelOf(handle); + + return m ? (int32_t)arrlen(m->menuItems) : 0; +} + + +const char *FrmMenuGet(int32_t handle, int32_t i, const char *prop) { + FrmModelT *m = modelOf(handle); + char val[FRM_VALUE_MAX]; + + if (!m || !prop || i < 0 || i >= (int32_t)arrlen(m->menuItems) || !frmMenuPropText(&m->menuItems[i], prop, val, sizeof(val))) { + val[0] = '\0'; + } + + return textOf(val); +} + + +int32_t FrmMenuIndent(int32_t handle, int32_t i, int32_t delta) { + FrmModelT *m = modelOf(handle); + + if (!m || !frmMenuIndent(m->menuItems, i, delta)) { + return BAS_FALSE; + } + + m->dirty = true; + return BAS_TRUE; +} + + +// The item's new position, -1 when it cannot move that way. +int32_t FrmMenuMove(int32_t handle, int32_t i, int32_t dir) { + FrmModelT *m = modelOf(handle); + int32_t idx = m ? frmMenuMove(&m->menuItems, i, dir) : FRM_NO_INDEX; + + if (idx >= 0) { + m->dirty = true; + } + + return idx; +} + + +void FrmMenuRemove(int32_t handle, int32_t i) { + FrmModelT *m = modelOf(handle); + + if (m && i >= 0 && i < (int32_t)arrlen(m->menuItems)) { + frmMenuRemove(&m->menuItems, i); + m->dirty = true; + } +} + + +int32_t FrmMenuSet(int32_t handle, int32_t i, const char *prop, const char *value) { + FrmModelT *m = modelOf(handle); + + return (m && prop && value && frmModelSetMenuProp(m, i, prop, value)) ? BAS_TRUE : BAS_FALSE; +} + + +FrmModelT *frmModelFromForm(BasFormRtT *rt, BasFormT *form) { + FrmLiveT live; + WidgetT *root = NULL; + + memset(&live, 0, sizeof(live)); + live.rt = rt; + live.form = form; + live.m = frmModelNew(form->name); + + if (!live.m) { + return NULL; + } + + // New controls of each type are made in a hidden window to read the + // values a control has before anybody sets one + WindowT *win = basFormRtCreateFormWindow(form->ctx, FRM_SCRATCH_TITLE, BAS_LAYOUT_VBOX, false, false, false, FRM_SCRATCH_SIZE, FRM_SCRATCH_SIZE, 0, 0, &root, &live.scratch); + + liveForm(&live); + liveMenus(&live); + + if (form->contentBox) { + liveControls(&live, form->contentBox, NULL); + } + + if (win) { + dvxDestroyWindow(form->ctx, win); + } + + live.m->dirty = false; + return live.m; +} + + +int32_t FrmNew(const char *name) { + return handleNew(frmModelNew(name && name[0] ? name : FRM_KW_FORM "1")); +} + + +int32_t FrmOpen(const char *text) { + return handleNew(text ? frmModelLoad(text, (int32_t)strlen(text)) : NULL); +} + + +int32_t FrmOpenFile(const char *path) { + int32_t len = 0; + char *text = path ? platformReadFile(path, &len) : NULL; + int32_t h = handleNew(text ? frmModelLoad(text, len) : NULL); + + free(text); + return h; +} + + +int32_t FrmSave(int32_t handle, const char *path) { + FrmModelT *m = modelOf(handle); + int32_t len = 0; + char *text = (m && path) ? frmModelText(m, &len) : NULL; + FILE *f = text ? fopen(path, "wb") : NULL; + bool ok = false; + + if (f) { + ok = fwrite(text, 1, (size_t)len, f) == (size_t)len; + ok = (fclose(f) == 0) && ok; + } + + free(text); + + if (ok) { + m->dirty = false; + } + + return ok ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FrmSet(int32_t handle, const char *prop, const char *value) { + FrmModelT *m = modelOf(handle); + + return (m && prop && value && frmModelSetFormProp(m, prop, value)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FrmSetCode(int32_t handle, const char *code) { + FrmModelT *m = modelOf(handle); + + return (m && frmModelSetCode(m, code)) ? BAS_TRUE : BAS_FALSE; +} + + +// The model as .frm text, "" when a property line is too long to write. +const char *FrmText(int32_t handle) { + FrmModelT *m = modelOf(handle); + int32_t len = 0; + char *text = m ? frmModelText(m, &len) : NULL; + const char *out = textOf(text ? text : ""); + + free(text); + return out; +} + + +// A handle for m (freed when the calling program ends), 0 when m is NULL. +static int32_t handleNew(FrmModelT *m) { + if (!m) { + return BAS_NO_HANDLE; + } + + if (!sCleanupHooked) { + FrmCleanupRegFnT reg = (FrmCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sModels) && sModels[idx].m) { + idx++; + } + + if (idx == (int32_t)arrlen(sModels)) { + BasFrmHandleT blank; + + memset(&blank, 0, sizeof(blank)); + arrput(sModels, blank); + + if (idx == (int32_t)arrlen(sModels)) { + frmModelFree(m); + return BAS_NO_HANDLE; + } + } + + sModels[idx].m = m; + sModels[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + return idx + 1; +} + + +// The controls under parentWidget: a widget that is no control (a layout +// box a container's Layout made) is looked through. +static void liveControls(FrmLiveT *live, const WidgetT *parentWidget, FrmCtrlT *parent) { + for (const WidgetT *c = parentWidget->firstChild; c; c = c->nextSibling) { + BasControlT *ctrl = ctrlForWidget(live->form, c); + + if (ctrl) { + liveCtrl(live, ctrl, parent); + } else { + liveControls(live, c, parent); + } + } +} + + +static void liveCtrl(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *parent) { + FrmModelT *m = live->m; + FrmCtrlT *fc = frmCtrlNew(ctrl->typeName, ctrl->name); + + if (!fc) { + return; + } + + arrput(m->controls, fc); + fc->index = ctrl->index; + + if (parent) { + snprintf(fc->parentName, sizeof(fc->parentName), "%s", parent->name); + fc->parentIndex = parent->index; + } + + snprintf(fc->helpTopic, sizeof(fc->helpTopic), "%s", ctrl->helpTopic); + + // The size fields (positions are the layout's: Left / Top stay 0) + for (int32_t i = 0; frmIntPropAt(i); i++) { + const FrmIntPropT *ip = frmIntPropAt(i); + + if (strcasecmp(ip->name, BAS_PROP_LEFT) != 0 && strcasecmp(ip->name, BAS_PROP_TOP) != 0 && basFormRtFindCommonProp(ip->name)) { + BasValueT v = basFormRtGetProp(live->rt, ctrl, ip->name); + + frmIntPropSet(fc, ip, (int32_t)basValToNumber(v)); + basValRelease(&v); + } + } + + liveDefaults(live, ctrl, fc); + + if (ctrl->iface && ctrl->iface->isContainer && ctrl->widget) { + liveControls(live, ctrl->widget, fc); + } +} + + +// The properties of ctrl that differ from a new control of its type: +// interface properties by comparison with a new control made in the +// scratch window, Caption / Text when the control keeps text and it +// differs, the other common properties when not empty. +static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc) { + WidgetT *w = ctrl->widget; + const WgtIfaceT *iface = ctrl->iface; + const char *wgt = NULL; + char val[FRM_VALUE_MAX]; + char old[FRM_VALUE_MAX]; + + // The widget's own type (a program's UserControl is registered under + // a key of its own, not its BASIC name) + if (w) { + wgtGetIfaceOf(w, &wgt); + } + + WidgetT *dflt = (wgt && live->scratch) ? wgtCreateByName(wgt, live->scratch) : NULL; + + for (int32_t i = 0; iface && dflt && i < iface->propCount; i++) { + const WgtPropDescT *p = &iface->props[i]; + + if (!p->getFn || !p->setFn || !wgtIfaceGetPropText(iface, w, p, val, sizeof(val))) { + continue; + } + + if (!wgtIfaceGetPropText(iface, dflt, p, old, sizeof(old)) || strcmp(val, old) != 0) { + frmCtrlSetProp(fc, p->name, val); + } + } + + // Caption / Text: only a widget that keeps the text it is given + const char *text = w ? wgtGetText(w) : NULL; + + if (dflt && text && !frmCtrlGetProp(fc, BAS_PROP_CAPTION) && !frmCtrlGetProp(fc, BAS_PROP_TEXT)) { + const char *base = wgtGetText(dflt); + const char *key = basFormRtDefaultProp(live->rt, ctrl); + + snprintf(val, sizeof(val), "%s", text); + snprintf(old, sizeof(old), "%s", base ? base : ""); + wgtSetText(dflt, val); + base = wgtGetText(dflt); + + if (strcmp(val, old) != 0 && base && strcmp(base, val) == 0) { + frmCtrlSetProp(fc, (key && strcasecmp(key, BAS_PROP_TEXT) == 0) ? BAS_PROP_TEXT : BAS_PROP_CAPTION, val); + } + } + + if (dflt) { + wgtDestroy(dflt); + } + + // The other common properties (the widget's own of the same name win) + int32_t count = 0; + const BasPropDescT *common = basFormRtCommonProps(&count); + + for (int32_t i = 0; i < count; i++) { + const BasPropDescT *pd = &common[i]; + bool fixed = strcasecmp(pd->name, BAS_PROP_NAME) == 0 || strcasecmp(pd->name, BAS_PROP_CAPTION) == 0 || strcasecmp(pd->name, BAS_PROP_TEXT) == 0 || strcasecmp(pd->name, BAS_PROP_HELPTOPIC) == 0 || frmIntPropFind(pd->name); + + if (fixed || !pd->writable || pd->type == BAS_PROP_OBJECT || wgtIfaceFindProp(iface, pd->name)) { + continue; + } + + BasValueT v = basFormRtGetProp(live->rt, ctrl, pd->name); + + valueText(v, pd->type, val, sizeof(val)); + basValRelease(&v); + + if (strcasecmp(pd->name, BAS_PROP_VISIBLE) == 0) { + fc->visible = frmParseBool(val); + } else if (strcasecmp(pd->name, BAS_PROP_ENABLED) == 0) { + fc->enabled = frmParseBool(val); + } else if (pd->type == WGT_IFACE_STRING ? val[0] != '\0' : (pd->type == WGT_IFACE_BOOL ? frmParseBool(val) : atoi(val) != 0)) { + frmCtrlSetProp(fc, pd->name, val); + } + } +} + + +// The form's design properties as the runtime reports them. +static void liveForm(FrmLiveT *live) { + BasFormT *form = live->form; + int32_t count = 0; + const BasPropDescT *props = basFormRtFormProps(&count); + char val[FRM_VALUE_MAX]; + + for (int32_t i = 0; i < count; i++) { + const BasPropDescT *pd = &props[i]; + + // Name is the Begin line's; the rest only when the model keeps it + if (strcasecmp(pd->name, BAS_PROP_NAME) == 0 || !frmModelFormProp(live->m, pd->name, val, sizeof(val))) { + continue; + } + + // Icon reads as a Picture; the .frm holds its file name + if (strcasecmp(pd->name, BAS_PROP_ICON) == 0) { + frmModelSetFormProp(live->m, pd->name, form->iconPath); + continue; + } + + BasValueT v = basFormRtGetProp(live->rt, &form->formCtrl, pd->name); + + valueText(v, pd->type, val, sizeof(val)); + basValRelease(&v); + frmModelSetFormProp(live->m, pd->name, val); + } +} + + +// The items of menu at level (separators, submenus with their items, +// commands) in menu order. +static void liveMenu(FrmLiveT *live, const MenuT *menu, int32_t level) { + for (int32_t i = 0; menu && i < menu->itemCount; i++) { + const MenuItemT *item = &menu->items[i]; + const BasMenuIdMapT *row = NULL; + + for (int32_t r = 0; item->id > 0 && r < live->form->menuIdMapCount; r++) { + if (live->form->menuIdMap[r].id == item->id) { + row = &live->form->menuIdMap[r]; + } + } + + if (item->separator) { + liveMenuAdd(live, row ? row->name : "", "-", level); + continue; + } + + if (item->subMenu) { + liveMenuAdd(live, liveMenuName(live->form, item->subMenu), item->label, level); + liveMenu(live, item->subMenu, level + 1); + continue; + } + + liveMenuAdd(live, row ? row->name : "", item->label, level); + + FrmMenuItemT *mi = &live->m->menuItems[arrlen(live->m->menuItems) - 1]; + + mi->checked = item->checked; + mi->checkable = item->type == MenuItemCheckE; + mi->radioCheck = item->type == MenuItemRadioE; + mi->enabled = item->enabled; + + if (row) { + snprintf(mi->shortcut, sizeof(mi->shortcut), "%s", row->shortcut); + } + + // Key text without a key that runs the item is ShortcutText + const char *sep = strchr(item->label, BAS_MENU_KEY_SEP); + + if (sep && !mi->shortcut[0]) { + snprintf(mi->shortcutText, sizeof(mi->shortcutText), "%s", sep + 1); + } + } +} + + +// Append an item; its caption is the label without the key text. +static void liveMenuAdd(FrmLiveT *live, const char *name, const char *label, int32_t level) { + FrmMenuItemT mi; + const char *sep = strchr(label, BAS_MENU_KEY_SEP); + int32_t len = sep ? (int32_t)(sep - label) : (int32_t)strlen(label); + + frmMenuItemInit(&mi); + snprintf(mi.name, sizeof(mi.name), "%s", name); + snprintf(mi.caption, sizeof(mi.caption), "%.*s", (int)len, label); + mi.level = DVX_MIN(level, FRM_MAX_MENU_LEVEL); + arrput(live->m->menuItems, mi); +} + + +// The name the runtime gave a menu-bar menu, submenu or popup menu, "" +// when it has none. +static const char *liveMenuName(const BasFormT *form, const MenuT *menu) { + for (int32_t i = 0; i < (int32_t)arrlen(form->popupMenus); i++) { + if (form->popupMenus[i].menu == menu) { + return form->popupMenus[i].name; + } + } + + return ""; +} + + +// The menu bar's menus, then the popup menus (Visible = False); items +// hidden at run time are left out (a .frm cannot hide one). Items +// without a name get one made from the caption. +static void liveMenus(FrmLiveT *live) { + const BasFormT *form = live->form; + const MenuBarT *bar = form->window ? form->window->menuBar : NULL; + + for (int32_t i = 0; bar && i < bar->menuCount; i++) { + liveMenuAdd(live, liveMenuName(form, bar->menus[i]), bar->menus[i]->label, 0); + liveMenu(live, bar->menus[i], 1); + } + + for (int32_t i = 0; i < (int32_t)arrlen(form->popupMenus); i++) { + const BasFrmPopupMenuT *pm = &form->popupMenus[i]; + + if (pm->ownsMenu) { + liveMenuAdd(live, pm->name, pm->menu ? pm->menu->label : "", 0); + live->m->menuItems[arrlen(live->m->menuItems) - 1].visible = false; + liveMenu(live, pm->menu, 1); + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(live->m->menuItems); i++) { + FrmMenuItemT *mi = &live->m->menuItems[i]; + + if (!mi->name[0]) { + frmMenuAutoName(live->m->menuItems, i, mi->name, sizeof(mi->name)); + } + } +} + + +static FrmModelT *modelOf(int32_t handle) { + return (handle >= 1 && handle <= (int32_t)arrlen(sModels)) ? sModels[handle - 1].m : NULL; +} + + +// Keep s as the last string result (valid until the next one). +static const char *textOf(const char *s) { + int32_t len = (int32_t)strlen(s); + + arrsetlen(sText, len + 1); + + if ((int32_t)arrlen(sText) != len + 1) { + return ""; + } + + memcpy(sText, s, (size_t)len + 1); + return sText; +} + + +// A runtime property value as .frm text for its type. +static void valueText(BasValueT v, uint8_t type, char *out, int32_t outSize) { + if (type == WGT_IFACE_STRING) { + BasStringT *s = basValFormatString(v); + + snprintf(out, outSize, "%s", s->data); + basStringUnref(s); + } else if (type == WGT_IFACE_BOOL) { + snprintf(out, outSize, "%s", basValIsTruthy(v) ? FRM_BAS_TRUE_TEXT : FRM_BAS_FALSE_TEXT); + } else { + snprintf(out, outSize, "%" PRId32, (int32_t)basValToNumber(v)); + } +} diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.c b/src/apps/kpunch/dvxbasic/formrt/frmParser.c index abc6adaf..49278839 100644 --- a/src/apps/kpunch/dvxbasic/formrt/frmParser.c +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.c @@ -26,6 +26,7 @@ #include "frmParser.h" #include "dvxPlat.h" +#include "dvxWgt.h" #include #include @@ -86,7 +87,7 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { char line[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)); + dvxLog("frm: line %d is longer than %d characters and was truncated", (int)lineNum, FRM_MAX_LINE_LEN - 1); lineLen = FRM_MAX_LINE_LEN - 1; } @@ -103,8 +104,8 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { } // "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import) - if (strncasecmp(trimmed, "VERSION ", sizeof("VERSION ") - 1) == 0) { - const char *ver = trimmed + sizeof("VERSION ") - 1; + if (strncasecmp(trimmed, FRM_KW_VERSION, FRM_KW_VERSION_LEN) == 0 && trimmed[FRM_KW_VERSION_LEN] == ' ') { + const char *ver = trimmed + FRM_KW_VERSION_LEN + 1; if (strncasecmp(ver, "DVX ", sizeof("DVX ") - 1) != 0) { double vbVer = atof(ver); @@ -123,7 +124,6 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { char typeName[FRM_MAX_TOKEN_LEN]; char ctrlName[FRM_MAX_TOKEN_LEN]; BlkTypeE blkType; - bool doPush = false; if (!readToken(&rest, typeName, FRM_MAX_TOKEN_LEN)) { continue; @@ -133,7 +133,8 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN); // 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. A + // control or menu block outside the form is skipped. if (strcasecmp(typeName, FRM_KW_FORM) == 0) { // A second Begin Form inside an open form is malformed: its // End would otherwise terminate the whole file early and @@ -143,17 +144,12 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { } blkType = BLK_FORM; - doPush = true; - } else if (strcasecmp(typeName, FRM_KW_MENU) == 0 && inForm) { - blkType = BLK_MENU; - doPush = true; - } else if (inForm) { - blkType = BLK_CTRL; - doPush = true; - } - - if (!doPush) { + } else if (!inForm) { continue; + } else if (strcasecmp(typeName, FRM_KW_MENU) == 0) { + blkType = BLK_MENU; + } else { + blkType = BLK_CTRL; } // Hard-error on overflow before any callback or state mutation so @@ -217,8 +213,6 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { } } else { // Outer Form End -- deliver any trailing text and stop. - inForm = false; - if (cb->onFormEnd) { cb->onFormEnd(cb->userData, pos, (int32_t)(end - pos)); } @@ -229,12 +223,14 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { continue; } - // Property assignment: Key = Value + // Property assignment: Key = Value. Any other line -- a bare + // Begin, "End = x", "VERSION=x" -- is malformed and ignored, so + // it never comes back from the writer as a block or VERSION line. char key[FRM_MAX_TOKEN_LEN]; char value[FRM_MAX_LINE_LEN]; frmParseKeyValue(trimmed, key, FRM_MAX_TOKEN_LEN, value, FRM_MAX_LINE_LEN); - if (key[0] == '\0' || !inForm) { + if (!inForm || !frmIsPropKey(key)) { continue; } @@ -264,20 +260,9 @@ bool frmParseBool(const char *val) { } +// The one .frm boolean rule lives in libdvx (property text uses it too). bool frmParseBoolDefault(const char *val, bool defaultValue) { - if (!val) { - return defaultValue; - } - - if (strcasecmp(val, "True") == 0 || strcmp(val, "-1") == 0) { - return true; - } - - if (strcasecmp(val, "False") == 0 || strcmp(val, "0") == 0) { - return false; - } - - return defaultValue; + return wgtParseBool(val, defaultValue); } diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.h b/src/apps/kpunch/dvxbasic/formrt/frmParser.h index 1578e7a2..44a8acd8 100644 --- a/src/apps/kpunch/dvxbasic/formrt/frmParser.h +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.h @@ -49,9 +49,11 @@ #define FRM_KW_FORM "Form" #define FRM_KW_END "End" #define FRM_KW_MENU "Menu" +#define FRM_KW_VERSION "VERSION" #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) +#define FRM_KW_VERSION_LEN ((int32_t)sizeof(FRM_KW_VERSION) - 1) // Callbacks supplied by the consumer. Any field may be NULL. typedef struct FrmParserCbsT { @@ -113,6 +115,14 @@ bool frmParseBoolDefault(const char *val, bool defaultValue); // The one End-line rule for the parser and every .frm scanner. static inline bool frmIsEndLine(const char *s, int32_t len); +// True when key can name a property: its "key = value" line reads back +// as that property. Not empty, no blank, control character or '=', no +// leading ' (a comment) and not a keyword of the block syntax (Begin, +// End, VERSION: such a line would read back as a block, End or VERSION +// line). The parser ignores any other "key = value" line and the model +// never stores such a key, so a written form always loads back the same. +static inline bool frmIsPropKey(const char *key); + 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) { @@ -128,4 +138,19 @@ static inline bool frmIsEndLine(const char *s, int32_t len) { return i == len || s[i] == '\''; } + +static inline bool frmIsPropKey(const char *key) { + if (key[0] == '\0' || key[0] == '\'' || strcasecmp(key, FRM_KW_BEGIN) == 0 || strcasecmp(key, FRM_KW_END) == 0 || strcasecmp(key, FRM_KW_VERSION) == 0) { + return false; + } + + for (const char *p = key; *p != '\0'; p++) { + if ((uint8_t)*p <= ' ' || *p == '=') { + return false; + } + } + + return true; +} + #endif // DVXBASIC_FRMPARSER_H diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c index ac3245d4..cdabff4e 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c @@ -20,23 +20,22 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. + // ideDesigner.c -- DVX BASIC form designer implementation // -// WYSIWYG design surface using real DVX widgets positioned -// identically to how the form runtime lays them out. Selection -// handles and grid dots are drawn in the window's onPaint callback. +// WYSIWYG design surface using real DVX widgets positioned identically +// to how the form runtime lays them out. The form itself is a .frm +// model (frmModel.h); this file keeps the live widgets in step with it +// (each control's userData is its widget) and turns mouse and keys into +// model edits. Selection handles are drawn in the window's onPaint. #include "ideDesigner.h" -#include "../formrt/formrt.h" -#include "../formrt/frmParser.h" -#include "../compiler/basEvents.h" #include "dvxDraw.h" #include "dvxVideo.h" #include "dvxWgtP.h" #include "dvxWm.h" #include "stb_ds_wrap.h" -#include #include #include #include @@ -46,161 +45,45 @@ // Constants // ============================================================ -#define DEFAULT_CTRL_W 100 -#define DEFAULT_CTRL_H 30 #define MIN_CTRL_SIZE 8 -#define DSGN_PAD_BUF (DSGN_MAX_PROP_INDENT + 1) // indent string of the deepest block -#define DSGN_NEWLINE_ROOM 2 // "\n" before and after the code section -#define DSGN_HANDLE_ON_GRAY 0 // gray level of a resize handle (black) -#define DSGN_HANDLE_OFF_GRAY 128 // gray level of a decorative handle -#define DSGN_HANDLE_HALVES 2 // handle positions are 0, 1/2 or 2/2 of the control size -#define DSGN_EMIT_FIXED_LINES 8 // Begin, Index, Caption, Text, HelpTopic, Visible, Enabled, End -#define DSGN_OPTION_TYPE "OptionButton" // BASIC type whose Value is exclusive within its container -#define DSGN_OPTION_VALUE "Value" - -// Handle position along each edge in halves of the control size, indexed -// by DsgnHandleE. Shared by the overlay painter and the hit test. -static const int32_t sHandleFx[HANDLE_COUNT] = { 0, 1, 2, 2, 2, 1, 0, 0 }; -static const int32_t sHandleFy[HANDLE_COUNT] = { 0, 0, 0, 1, 2, 2, 2, 1 }; - -// ============================================================ -// Built-in integer control properties (see DsgnIntPropT) -// ============================================================ - -static const DsgnIntPropT sIntProps[] = { - { BAS_PROP_LEFT, NULL, offsetof(DsgnControlT, left), DSGN_SAVE_IF_NONZERO }, - { BAS_PROP_TOP, NULL, offsetof(DsgnControlT, top), DSGN_SAVE_IF_NONZERO }, - { BAS_PROP_MINWIDTH, BAS_PROP_WIDTH, offsetof(DsgnControlT, width), DSGN_SAVE_ALWAYS }, - { BAS_PROP_MINHEIGHT, BAS_PROP_HEIGHT, offsetof(DsgnControlT, height), DSGN_SAVE_ALWAYS }, - { BAS_PROP_MAXWIDTH, NULL, offsetof(DsgnControlT, maxWidth), DSGN_SAVE_IF_POSITIVE }, - { BAS_PROP_MAXHEIGHT, NULL, offsetof(DsgnControlT, maxHeight), DSGN_SAVE_IF_POSITIVE }, - { BAS_PROP_WEIGHT, NULL, offsetof(DsgnControlT, weight), DSGN_SAVE_IF_POSITIVE }, - { BAS_PROP_DRAGMODE, NULL, offsetof(DsgnControlT, dragMode), DSGN_SAVE_IF_NONZERO } -}; - -#define DSGN_INT_PROP_COUNT DVX_ARRAY_LEN(sIntProps) - -// ============================================================ -// .frm parsing context (used by the loadOn* callbacks) -// ============================================================ -// -// parentStack mirrors the parser's control Begin/End nesting: every control -// block pushes its name (and its record, NULL when the allocation failed), -// so a child's parentName and parentIndex always come from the enclosing -// block whether or not that type's widget is loaded. frmParse rejects a -// block deeper than FRM_MAX_NESTING before calling back, and the Form block -// holds one of those levels, so the stack cannot overflow. - -typedef struct { - char name[DSGN_MAX_NAME]; - DsgnControlT *ctrl; -} DsgnLoadParentT; - -typedef struct { - DsgnFormT *form; - DsgnControlT *current; - int32_t curMenuItemIdx; - DsgnLoadParentT parentStack[FRM_MAX_NESTING]; - int32_t nestDepth; -} DsgnFrmLoadCtxT; +#define DSGN_NO_SELECTION (-1) // selectedIdx: the form itself // ============================================================ // Prototypes // ============================================================ -static void buildPad(int32_t indent, char *pad); -static WidgetT *containerChildParent(DsgnControlT *pc); -void dsgnApplyEnabled(const DsgnFormT *form); -void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize); -void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form); -const char *dsgnCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen); -int32_t dsgnControlDepth(const DsgnFormT *form, const DsgnControlT *ctrl); -void dsgnControlFree(DsgnControlT *ctrl); -const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name); -void dsgnControlInit(DsgnControlT *ctrl); -WidgetT *dsgnCreateControlWidget(const DsgnFormT *form, DsgnControlT *ctrl, WidgetT *parent); -WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent); +static WidgetT *containerChildParent(FrmCtrlT *pc); +static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent); +void dsgnApplyEnabled(const FrmModelT *form); +void dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form); +WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent); void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox); -const char *dsgnDefaultEvent(const char *typeName); -int32_t dsgnEmitControl(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent); -int32_t dsgnEmitControlSize(const DsgnControlT *ctrl); -const DsgnIntPropT *dsgnFindIntProp(const char *name); -DsgnControlT *dsgnFindParent(const DsgnFormT *form, const DsgnControlT *ctrl); -bool dsgnFormPropValue(const DsgnFormT *form, const char *name, char *out, int32_t outSize); +void dsgnFitFormWin(DsgnStateT *ds); void dsgnFree(DsgnStateT *ds); -bool dsgnIfaceHasProp(const char *typeName, const char *propName); -bool dsgnIfacePropValue(const DsgnControlT *ctrl, const WgtPropDescT *p, char *out, int32_t outSize); void dsgnInit(DsgnStateT *ds, AppContextT *ctx); -const DsgnIntPropT *dsgnIntPropAt(int32_t idx); -bool dsgnIsContainer(const char *typeName); bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen); -void dsgnMakeArrayMember(DsgnFormT *form, DsgnControlT *ctrl, int32_t index); -void dsgnMenuItemInit(DsgnMenuItemT *mi); -bool dsgnNameInUse(const DsgnFormT *form, const char *name, const char *exceptName, bool checkMenus); void dsgnNewForm(DsgnStateT *ds, const char *name); void dsgnOnKey(DsgnStateT *ds, int32_t key); void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag); -void dsgnOptionSelected(DsgnFormT *form, DsgnControlT *ctrl); void dsgnPaintOverlay(DsgnStateT *ds); void dsgnRebuildWidgets(DsgnStateT *ds); int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize); -const char *dsgnSelectedName(const DsgnStateT *ds); -void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value); -void dsgnSyncWidgetGeom(DsgnControlT *ctrl); -static bool effectiveEnabled(const DsgnFormT *form, const DsgnControlT *ctrl); -int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); -static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args); -static int32_t emitControlHead(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad); -static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...); -static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value); -const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName); -static void handleOrigin(const WidgetT *w, DsgnHandleE h, int32_t *outX, int32_t *outY); -static bool handleResizes(DsgnHandleE h); +WidgetT *dsgnSelectedWidget(const DsgnStateT *ds); +void dsgnSyncWidgetGeom(FrmCtrlT *ctrl); +static bool effectiveEnabled(const FrmModelT *form, const FrmCtrlT *ctrl); static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y); -static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y); -static int32_t intPropGet(const DsgnControlT *ctrl, const DsgnIntPropT *ip); -static void intPropSet(DsgnControlT *ctrl, const DsgnIntPropT *ip, int32_t val); -static bool isDescendant(const DsgnFormT *form, const DsgnControlT *ctrl, const DsgnControlT *ancestor); -static void loadOnCtrlBegin(void *userData, const char *typeName, const char *name); -static void loadOnCtrlEnd(void *userData); -static void loadOnCtrlProp(void *userData, const char *key, const char *value); -static bool loadOnFormBegin(void *userData, const char *name); -static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen); -static void loadOnFormProp(void *userData, const char *key, const char *value); -static void loadOnMenuBegin(void *userData, const char *name, int32_t level); -static void loadOnMenuEnd(void *userData); -static void loadOnMenuProp(void *userData, const char *key, const char *value); -static DsgnFormT *newFormDefaults(const char *name); -static void normalizeAppend(DsgnControlT **src, DsgnControlT **parents, bool *placed, int32_t count, const DsgnControlT *parent, DsgnControlT ***out, int32_t depth); -static void normalizeOrder(DsgnStateT *ds); -static WidgetT *parentWidgetFor(const DsgnFormT *form, const DsgnControlT *ctrl, WidgetT *contentBox); -static void relinkSiblingWidgets(const DsgnFormT *form, WidgetT *parent); -static bool rotateControls(DsgnControlT **arr, int32_t start, int32_t mid, int32_t end); -static int32_t saveControls(const DsgnFormT *form, DsgnControlT **parents, char *buf, int32_t bufSize, int32_t pos, const DsgnControlT *parent, int32_t indent); -static int32_t subtreeEnd(const DsgnFormT *form, int32_t idx); +static void normalizeSelected(DsgnStateT *ds); +static WidgetT *parentWidgetFor(const FrmModelT *form, const FrmCtrlT *ctrl, WidgetT *contentBox); +static void relinkSiblingWidgets(const FrmModelT *form, WidgetT *parent); static void trySwapNeighbor(DsgnStateT *ds, int32_t y, int32_t dir); -// Fill pad with the indentation of an indent-level block. pad must hold -// DSGN_PAD_BUF bytes; deeper levels than the parser accepts are clamped. -static void buildPad(int32_t indent, char *pad) { - int32_t padLen = indent * DSGN_INDENT_SPACES; - - if (padLen > DSGN_MAX_PROP_INDENT) { - padLen = DSGN_MAX_PROP_INDENT; - } - - memset(pad, ' ', padLen); - pad[padLen] = '\0'; -} - - // Widget that a container's children attach to. Containers with a // non-VBox Layout property nest their children inside a content box tagged // with the control pointer so it is created exactly once. -static WidgetT *containerChildParent(DsgnControlT *pc) { - WidgetT *parent = pc->widget; - const char *layout = dsgnControlGetPropValue(pc, BAS_PROP_LAYOUT); +static WidgetT *containerChildParent(FrmCtrlT *pc) { + WidgetT *parent = dsgnWidget(pc); + const char *layout = frmCtrlGetProp(pc, BAS_PROP_LAYOUT); if (!layout || !layout[0] || strcasecmp(layout, BAS_LAYOUT_VBOX) == 0) { return parent; @@ -224,63 +107,39 @@ static WidgetT *containerChildParent(DsgnControlT *pc) { } +// Create a real DVX widget for design-time display through libdvx's +// wgtCreateByName (the project's own UserControls first, the designer's +// registration of them: basUcResolveType); design-time refuses +// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no +// pixel data. +static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) { + const char *wgtName = basUcResolveType(NULL, vbTypeName); + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (!wgtName || (iface && iface->createSig == WGT_CREATE_PARENT_DATA)) { + return NULL; + } + + return wgtCreateByName(wgtName, parent); +} + + // Push every control's effective enabled state (its own flag and every // ancestor's) to its live widget, the way the runtime shows it. -void dsgnApplyEnabled(const DsgnFormT *form) { +void dsgnApplyEnabled(const FrmModelT *form) { int32_t count = (int32_t)arrlen(form->controls); for (int32_t i = 0; i < count; i++) { - DsgnControlT *ctrl = form->controls[i]; + FrmCtrlT *ctrl = form->controls[i]; - if (ctrl->widget) { - wgtSetEnabled(ctrl->widget, effectiveEnabled(form, ctrl)); + if (dsgnWidget(ctrl)) { + wgtSetEnabled(dsgnWidget(ctrl), effectiveEnabled(form, ctrl)); } } } -void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) { - // Look up the name prefix from the widget interface descriptor. - // Falls back to the type name itself if no prefix is registered. - const char *prefix = typeName; - const char *wgtName = wgtFindByBasName(typeName); - - if (wgtName) { - const WgtIfaceT *iface = wgtGetIface(wgtName); - - if (iface && iface->namePrefix) { - prefix = iface->namePrefix; - } - } - - int32_t highest = 0; - int32_t prefixLen = (int32_t)strlen(prefix); - int32_t count = ds->form ? (int32_t)arrlen(ds->form->controls) : 0; - - for (int32_t i = 0; i < count; i++) { - if (strncasecmp(ds->form->controls[i]->name, prefix, prefixLen) == 0) { - int32_t num = atoi(ds->form->controls[i]->name + prefixLen); - - if (num > highest) { - highest = num; - } - } - } - - // The numbering only looks at controls; a menu item or the form may - // already own the next name, so keep counting until it is free. - int32_t num = highest + 1; - - snprintf(buf, bufSize, "%s%d", prefix, (int)num); - - while (ds->form && dsgnNameInUse(ds->form, buf, NULL, true)) { - num++; - snprintf(buf, bufSize, "%s%d", prefix, (int)num); - } -} - - -void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) { +void dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form) { if (!win || !form) { return; } @@ -297,13 +156,13 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) { return; } - MenuT *menuStack[DSGN_MENU_STACK_DEPTH]; + MenuT *menuStack[FRM_MENU_STACK_DEPTH]; memset(menuStack, 0, sizeof(menuStack)); for (int32_t i = 0; i < menuCount; i++) { - const DsgnMenuItemT *mi = &form->menuItems[i]; - bool isSep = (mi->caption[0] == '-'); - bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level); + const FrmMenuItemT *mi = &form->menuItems[i]; + bool isSep = (mi->caption[0] == '-'); + bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level); if (mi->level == 0) { // Only show top-level menus with Visible=True on the bar; popup-only @@ -316,7 +175,7 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) { continue; } - MenuT *parentMenu = (mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH) ? menuStack[mi->level - 1] : NULL; + MenuT *parentMenu = (mi->level > 0 && mi->level < FRM_MENU_STACK_DEPTH) ? menuStack[mi->level - 1] : NULL; if (!parentMenu) { continue; @@ -328,113 +187,35 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) { menuStack[mi->level] = wmAddSubMenu(parentMenu, mi->caption); } else { int32_t id = DSGN_MENU_ID_BASE + i; + char label[FRM_MAX_TEXT + BAS_MENU_KEY_TEXT_MAX]; + int32_t key; + int32_t mods; + + // The preview shows the key text; its keys do nothing here + basFormRtMenuLabel(mi->caption, mi->shortcut, mi->shortcutText, label, sizeof(label), &key, &mods); if (mi->radioCheck) { - wmAddMenuRadioItem(parentMenu, mi->caption, id, mi->checked); - } else if (mi->checked) { - wmAddMenuCheckItem(parentMenu, mi->caption, id, true); + wmAddMenuRadioItem(parentMenu, label, id, mi->checked); + } else if (mi->checked || mi->checkable) { + wmAddMenuCheckItem(parentMenu, label, id, mi->checked); } else { - wmAddMenuItem(parentMenu, mi->caption, id); + wmAddMenuItem(parentMenu, label, id); } } } } -const char *dsgnCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen) { - const char *codeStart = trailingSrc; - const char *codeEnd = trailingSrc + DVX_MAX(trailingLen, 0); - - // Skip whole blank lines only; the first code line keeps its indent. - while (codeStart < codeEnd) { - const char *p = codeStart; - - while (p < codeEnd && (*p == ' ' || *p == '\t')) { - p++; - } - - if (p >= codeEnd) { - return NULL; - } - - if (*p != '\r' && *p != '\n') { - break; - } - - if (*p == '\r' && p + 1 < codeEnd && p[1] == '\n') { - p++; - } - - codeStart = p + 1; - } - - if (codeStart >= codeEnd) { - return NULL; - } - - *outLen = (int32_t)(codeEnd - codeStart); - return codeStart; -} - - -// Block depth of a control below the Form block: 1 for a top-level -// control, 2 for a child of a top-level container, and so on. The walk is -// capped so a parent cycle cannot loop forever. -int32_t dsgnControlDepth(const DsgnFormT *form, const DsgnControlT *ctrl) { - int32_t depth = 1; - DsgnControlT *p = dsgnFindParent(form, ctrl); - - while (p && depth <= DSGN_MAX_NEST_DEPTH) { - depth++; - p = dsgnFindParent(form, p); - } - - return depth; -} - - -void dsgnControlFree(DsgnControlT *ctrl) { - if (!ctrl) { - return; - } - - arrfree(ctrl->props); - free(ctrl); -} - - -const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name) { - for (int32_t i = 0; i < (int32_t)arrlen(ctrl->props); i++) { - if (strcasecmp(ctrl->props[i].name, name) == 0) { - return ctrl->props[i].value; - } - } - - return NULL; -} - - -void dsgnControlInit(DsgnControlT *ctrl) { - memset(ctrl, 0, sizeof(*ctrl)); - ctrl->index = DSGN_NO_INDEX; - ctrl->parentIndex = DSGN_NO_INDEX; - ctrl->visible = true; - ctrl->enabled = true; - ctrl->width = DEFAULT_CTRL_W; - ctrl->height = DEFAULT_CTRL_H; -} - - // Create one control's live design widget under parent and apply its // name, text, geometry, enabled state and interface properties. Visible is // deliberately not applied: a hidden widget gets no layout geometry, which // would make a Visible=False control invisible and unclickable on the // canvas. The runtime loader applies it instead. Returns the widget, or // NULL when the type cannot be created at design time. -WidgetT *dsgnCreateControlWidget(const DsgnFormT *form, DsgnControlT *ctrl, WidgetT *parent) { - WidgetT *w = dsgnCreateDesignWidget(ctrl->typeName, parent); +WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent) { + WidgetT *w = createDesignWidget(ctrl->typeName, parent); - ctrl->widget = w; + ctrl->userData = w; if (!w) { return NULL; @@ -442,8 +223,8 @@ WidgetT *dsgnCreateControlWidget(const DsgnFormT *form, DsgnControlT *ctrl, Widg wgtSetName(w, ctrl->name); - const char *caption = dsgnControlGetPropValue(ctrl, BAS_PROP_CAPTION); - const char *text = dsgnControlGetPropValue(ctrl, BAS_PROP_TEXT); + const char *caption = frmCtrlGetProp(ctrl, BAS_PROP_CAPTION); + const char *text = frmCtrlGetProp(ctrl, BAS_PROP_TEXT); if (caption) { wgtSetText(w, caption); @@ -456,22 +237,14 @@ WidgetT *dsgnCreateControlWidget(const DsgnFormT *form, DsgnControlT *ctrl, Widg dsgnSyncWidgetGeom(ctrl); wgtSetEnabled(w, effectiveEnabled(form, ctrl)); - const char *wgtName = wgtFindByBasName(ctrl->typeName); - const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; + const WgtIfaceT *iface = basCtlIface(ctrl->typeName); - if (iface) { - for (int32_t pi = 0; pi < iface->propCount; pi++) { - const WgtPropDescT *p = &iface->props[pi]; + for (int32_t pi = 0; iface && pi < iface->propCount; pi++) { + const WgtPropDescT *p = &iface->props[pi]; + const char *val = p->setFn ? frmCtrlGetProp(ctrl, p->name) : NULL; - if (!p->setFn) { - continue; - } - - const char *val = dsgnControlGetPropValue(ctrl, p->name); - - if (val) { - wgtApplyPropFromString(w, p, val); - } + if (val) { + wgtIfaceSetPropText(iface, w, p, val); } } @@ -479,368 +252,75 @@ WidgetT *dsgnCreateControlWidget(const DsgnFormT *form, DsgnControlT *ctrl, Widg } -// Create a real DVX widget for design-time display. Uses the shared -// createWidgetByIface switch from formrt; design-time refuses -// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no -// pixel data. -WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) { - const char *wgtName = resolveTypeName(vbTypeName); - - if (!wgtName) { - return NULL; - } - - const void *api = wgtGetApi(wgtName, sizeof(WgtApiPrefixT)); - - if (!api) { - return NULL; - } - - return createWidgetByIface(wgtGetIface(wgtName), api, parent, false); -} - - // Single pass in controls[] order: a container always precedes its children -// (the loader, the tree reorder, and the sibling-only canvas reorder all -// preserve that), so each child finds its parent's widget already created. +// (the model keeps tree order), so each child finds its parent's widget +// already created. void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { if (!ds->form || !contentBox) { return; } - ds->form->contentBox = contentBox; + ds->contentBox = contentBox; + contentBox->padding = wgtPixels(ds->form->padding); + contentBox->spacing = wgtPixels(ds->form->spacing); + int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < count; i++) { - DsgnControlT *ctrl = ds->form->controls[i]; + FrmCtrlT *ctrl = ds->form->controls[i]; - if (!ctrl->widget) { + if (!dsgnWidget(ctrl)) { dsgnCreateControlWidget(ds->form, ctrl, parentWidgetFor(ds->form, ctrl, contentBox)); } } } -const char *dsgnDefaultEvent(const char *typeName) { - // The Form type is not a widget, so it has no interface default event. - if (strcasecmp(typeName, FRM_KW_FORM) == 0) { - return BAS_EVT_LOAD; +void dsgnFitFormWin(DsgnStateT *ds) { + if (!ds->form || !ds->formWin) { + return; } - const char *wgtName = wgtFindByBasName(typeName); - - if (wgtName) { - const WgtIfaceT *iface = wgtGetIface(wgtName); - - if (iface && iface->defaultEvent) { - return iface->defaultEvent; - } - } - - return BAS_EVT_CLICK; -} - - -int32_t dsgnEmitControl(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent) { - char pad[DSGN_PAD_BUF]; - - buildPad(indent, pad); - pos = emitControlHead(ctrl, buf, bufSize, pos, pad); - return emitClamped(buf, bufSize, pos, "%sEnd\n", pad); -} - - -int32_t dsgnEmitControlSize(const DsgnControlT *ctrl) { - const char *wgtName = wgtFindByBasName(ctrl->typeName); - const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; - int32_t lines = DSGN_EMIT_FIXED_LINES + DSGN_INT_PROP_COUNT + (int32_t)arrlen(ctrl->props) + (iface ? iface->propCount : 0); - - return lines * FRM_MAX_LINE_LEN + 1; -} - - -const DsgnIntPropT *dsgnFindIntProp(const char *name) { - for (int32_t i = 0; i < DSGN_INT_PROP_COUNT; i++) { - const DsgnIntPropT *ip = &sIntProps[i]; - - if (strcasecmp(ip->name, name) == 0 || (ip->alias && strcasecmp(ip->alias, name) == 0)) { - return ip; - } - } - - return NULL; -} - - -// The control ctrl's parentName/parentIndex refer to, or NULL for a -// top-level control or one whose parent no longer exists. An exact -// name + index match wins; otherwise the first control bearing the name -// (a child placed before its container became a control array keeps the -// container's old index). -DsgnControlT *dsgnFindParent(const DsgnFormT *form, const DsgnControlT *ctrl) { - if (!ctrl->parentName[0]) { - return NULL; - } - - DsgnControlT *first = NULL; - int32_t count = (int32_t)arrlen(form->controls); - - for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = form->controls[i]; - - if (c == ctrl || strcasecmp(c->name, ctrl->parentName) != 0) { - continue; - } - - if (c->index == ctrl->parentIndex) { - return c; - } - - if (!first) { - first = c; - } - } - - return first; -} - - -// Text value of one runtime form property as the designer stores it. -// Returns false for properties the designer has no field for (Visible: -// runtime-only), so both the .frm writer and the property grid can walk -// basFormRtFormProps() and skip those. -bool dsgnFormPropValue(const DsgnFormT *form, const char *name, char *out, int32_t outSize) { - const char *text = NULL; - int32_t num = 0; - bool flag = false; - bool isNum = false; - bool isFlag = false; - - if (strcasecmp(name, BAS_PROP_NAME) == 0) { - text = form->name; - } else if (strcasecmp(name, BAS_PROP_CAPTION) == 0) { - text = form->caption; - } else if (strcasecmp(name, BAS_PROP_LAYOUT) == 0) { - text = form->layout; - } else if (strcasecmp(name, BAS_PROP_HELPTOPIC) == 0) { - text = form->helpTopic; - } else if (strcasecmp(name, BAS_PROP_CONTEXTMENU) == 0) { - text = form->contextMenu; - } else if (strcasecmp(name, BAS_PROP_LEFT) == 0) { - num = form->left; - isNum = true; - } else if (strcasecmp(name, BAS_PROP_TOP) == 0) { - num = form->top; - isNum = true; - } else if (strcasecmp(name, BAS_PROP_WIDTH) == 0) { - num = form->width; - isNum = true; - } else if (strcasecmp(name, BAS_PROP_HEIGHT) == 0) { - num = form->height; - isNum = true; - } else if (strcasecmp(name, BAS_PROP_AUTOSIZE) == 0) { - flag = form->autoSize; - isFlag = true; - } else if (strcasecmp(name, BAS_PROP_RESIZABLE) == 0) { - flag = form->resizable; - isFlag = true; - } else if (strcasecmp(name, BAS_PROP_CENTERED) == 0) { - flag = form->centered; - isFlag = true; - } - - if (text) { - snprintf(out, outSize, "%s", text); - } else if (isNum) { - snprintf(out, outSize, "%d", (int)num); - } else if (isFlag) { - snprintf(out, outSize, "%s", flag ? "True" : "False"); + if (ds->form->autoSize) { + dvxFitWindow(ds->ctx, ds->formWin); + ds->form->width = ds->formWin->w; + ds->form->height = ds->formWin->h; } else { - return false; + dvxResizeWindow(ds->ctx, ds->formWin, ds->form->width, ds->form->height); } - - return true; } void dsgnFree(DsgnStateT *ds) { - if (ds->form) { - for (int32_t i = 0; i < arrlen(ds->form->controls); i++) { - dsgnControlFree(ds->form->controls[i]); - } - - arrfree(ds->form->controls); - arrfree(ds->form->menuItems); - free(ds->form->code); - free(ds->form); - ds->form = NULL; - } -} - - -// Returns true if the widget type registers an interface property with the -// given name. The generic built-in Visible/Enabled designer rows yield to -// such a property so a widget can give those names its own meaning (e.g. the -// Timer's "Enabled" maps to its running state, VB-style) without producing a -// duplicate grid row or a conflicting saved line. -bool dsgnIfaceHasProp(const char *typeName, const char *propName) { - return findIfaceProp(typeName, propName) != NULL; -} - - -bool dsgnIfacePropValue(const DsgnControlT *ctrl, const WgtPropDescT *p, char *out, int32_t outSize) { - const char *stored = dsgnControlGetPropValue(ctrl, p->name); - - if (stored) { - snprintf(out, outSize, "%s", stored); - return true; - } - - return wgtPropValueToString(ctrl->widget, p, out, outSize); + frmModelFree(ds->form); + ds->form = NULL; } void dsgnInit(DsgnStateT *ds, AppContextT *ctx) { memset(ds, 0, sizeof(*ds)); - ds->selectedIdx = -1; - ds->activeHandle = HANDLE_NONE; + ds->selectedIdx = DSGN_NO_SELECTION; + ds->activeHandle = BAS_DSGN_HANDLE_NONE; ds->ctx = ctx; } -const DsgnIntPropT *dsgnIntPropAt(int32_t idx) { - if (idx < 0 || idx >= DSGN_INT_PROP_COUNT) { - return NULL; - } - - return &sIntProps[idx]; -} - - -bool dsgnIsContainer(const char *typeName) { - const char *wgtName = wgtFindByBasName(typeName); - - if (wgtName) { - const WgtIfaceT *iface = wgtGetIface(wgtName); - - if (iface) { - return iface->isContainer; - } - } - - return false; -} - - bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { if (!source || sourceLen <= 0) { return false; } dsgnFree(ds); - - // Same defaults as the runtime form (AutoSize off unless the .frm says - // so), so the designer shows what the program will show. - DsgnFormT *form = newFormDefaults("Form1"); - - if (!form) { - return false; - } - - DsgnFrmLoadCtxT ctx; - memset(&ctx, 0, sizeof(ctx)); - ctx.form = form; - ctx.curMenuItemIdx = -1; - - FrmParserCbsT cbs; - memset(&cbs, 0, sizeof(cbs)); - cbs.userData = &ctx; - cbs.onFormBegin = loadOnFormBegin; - cbs.onFormProp = loadOnFormProp; - cbs.onFormEnd = loadOnFormEnd; - cbs.onMenuBegin = loadOnMenuBegin; - cbs.onMenuEnd = loadOnMenuEnd; - cbs.onMenuProp = loadOnMenuProp; - cbs.onCtrlBegin = loadOnCtrlBegin; - cbs.onCtrlEnd = loadOnCtrlEnd; - cbs.onCtrlProp = loadOnCtrlProp; - - ds->form = form; - ds->selectedIdx = -1; - - if (!frmParse(source, sourceLen, &cbs)) { - // The callbacks may already have allocated controls/menuItems/code - // before a mid-stream failure; dsgnFree tears all of it down. - dsgnFree(ds); - return false; - } - - // One selected OptionButton per group: of several saved True, the - // last one wins (as setting them in order would at runtime), so the - // scan runs from the end - for (int32_t i = (int32_t)arrlen(form->controls) - 1; i >= 0; i--) { - dsgnOptionSelected(form, form->controls[i]); - } - - return true; -} - - -void dsgnMakeArrayMember(DsgnFormT *form, DsgnControlT *ctrl, int32_t index) { - int32_t oldIndex = ctrl->index; - int32_t count = (int32_t)arrlen(form->controls); - - for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = form->controls[i]; - - if (c != ctrl && c->parentIndex == oldIndex && strcasecmp(c->parentName, ctrl->name) == 0) { - c->parentIndex = index; - } - } - - ctrl->index = index; -} - - -void dsgnMenuItemInit(DsgnMenuItemT *mi) { - memset(mi, 0, sizeof(*mi)); - mi->enabled = true; - mi->visible = true; - mi->id = DSGN_NEW_MENU_ID; -} - - -bool dsgnNameInUse(const DsgnFormT *form, const char *name, const char *exceptName, bool checkMenus) { - if (exceptName && strcasecmp(name, exceptName) == 0) { - return false; - } - - if (strcasecmp(form->name, name) == 0) { - return true; - } - - for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { - if (strcasecmp(form->controls[i]->name, name) == 0) { - return true; - } - } - - if (checkMenus) { - for (int32_t i = 0; i < (int32_t)arrlen(form->menuItems); i++) { - if (strcasecmp(form->menuItems[i].name, name) == 0) { - return true; - } - } - } - - return false; + ds->form = frmModelLoad(source, sourceLen); + ds->selectedIdx = DSGN_NO_SELECTION; + return ds->form != NULL; } void dsgnNewForm(DsgnStateT *ds, const char *name) { dsgnFree(ds); - ds->form = newFormDefaults(name); - ds->selectedIdx = -1; + ds->form = frmModelNew(name); + ds->selectedIdx = DSGN_NO_SELECTION; } @@ -855,54 +335,9 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) { return; } - // Delete the selected control and every descendant, transitively. - // Parents are resolved on the unmodified array (dsgnFindParent), so a - // child of one control-array member goes only with that member. - DsgnControlT **delSet = NULL; - bool grew = true; - - arrput(delSet, ds->form->controls[ds->selectedIdx]); - - while (grew) { - grew = false; - - for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = ds->form->controls[i]; - DsgnControlT *parent = dsgnFindParent(ds->form, c); - bool selfIn = false; - bool parIn = false; - - if (!parent) { - continue; - } - - for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) { - selfIn = selfIn || delSet[s] == c; - parIn = parIn || delSet[s] == parent; - } - - if (parIn && !selfIn) { - arrput(delSet, c); - grew = true; - } - } - } - - // Delete every flagged control, highest index first so earlier - // indices stay valid. - for (int32_t i = count - 1; i >= 0; i--) { - for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) { - if (delSet[s] == ds->form->controls[i]) { - dsgnControlFree(ds->form->controls[i]); - arrdel(ds->form->controls, i); - break; - } - } - } - - arrfree(delSet); - ds->selectedIdx = -1; - ds->form->dirty = true; + // The selected control goes with its whole subtree + frmModelRemoveCtrl(ds->form, ds->form->controls[ds->selectedIdx]); + ds->selectedIdx = DSGN_NO_SELECTION; ds->treeStale = true; dsgnRebuildWidgets(ds); @@ -918,18 +353,18 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { if (drag) { if (ds->mode == DSGN_RESIZING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { - DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; - int32_t dx = x - ds->dragStartX; - int32_t dy = y - ds->dragStartY; + FrmCtrlT *ctrl = ds->form->controls[ds->selectedIdx]; + int32_t dx = x - ds->dragStartX; + int32_t dy = y - ds->dragStartY; switch (ds->activeHandle) { - case HANDLE_E: + case BAS_DSGN_HANDLE_E: ctrl->width = ds->dragOrigWidth + dx; break; - case HANDLE_S: + case BAS_DSGN_HANDLE_S: ctrl->height = ds->dragOrigHeight + dy; break; - case HANDLE_SE: + case BAS_DSGN_HANDLE_SE: ctrl->width = ds->dragOrigWidth + dx; ctrl->height = ds->dragOrigHeight + dy; break; @@ -972,16 +407,16 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { if (ds->activeTool[0] == '\0') { // Check grab handles of selected control first if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { - DsgnHandleE handle = hitTestHandles(ds->form->controls[ds->selectedIdx], x, y); + FrmCtrlT *ctrl = ds->form->controls[ds->selectedIdx]; + int32_t handle = basDsgnHandleAt(dsgnWidget(ctrl), x, y); - if (handle != HANDLE_NONE) { - DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; + if (handle != BAS_DSGN_HANDLE_NONE) { ds->mode = DSGN_RESIZING; - ds->activeHandle = handle; + ds->activeHandle = (BasDsgnHandleE)handle; ds->dragStartX = x; ds->dragStartY = y; - ds->dragOrigWidth = ctrl->widget ? ctrl->widget->w : ctrl->width; - ds->dragOrigHeight = ctrl->widget ? ctrl->widget->h : ctrl->height; + ds->dragOrigWidth = dsgnWidget(ctrl) ? dsgnWidget(ctrl)->w : ctrl->width; + ds->dragOrigHeight = dsgnWidget(ctrl) ? dsgnWidget(ctrl)->h : ctrl->height; return; } } @@ -990,140 +425,78 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { int32_t hit = hitTestControl(ds, x, y); // controls[] is put in tree order once here, not on every drag - // step: the swaps keep subtrees contiguous. + // step: the moves keep subtrees contiguous. if (hit >= 0) { ds->selectedIdx = hit; - normalizeOrder(ds); + normalizeSelected(ds); ds->mode = DSGN_REORDERING; ds->dragStartY = y; } else { - ds->selectedIdx = -1; + ds->selectedIdx = DSGN_NO_SELECTION; } return; } - // Non-pointer tool: place a new control - const char *typeName = ds->activeTool; - DsgnControlT *cp = (DsgnControlT *)malloc(sizeof(DsgnControlT)); - - if (!cp) { - return; - } - - dsgnControlInit(cp); - dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME); - snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName); - dsgnSetPropValue(cp, BAS_PROP_CAPTION, cp->name); - - // Nest inside the innermost container under the click that can still - // take a child: the .frm parser rejects blocks deeper than - // DSGN_MAX_NEST_DEPTH, so a full-depth container passes the new control - // on to its enclosing container. - WidgetT *parentWidget = ds->form->contentBox; + // Non-pointer tool: place a new control. It nests inside the + // innermost container under the click that can still take a child: + // the .frm parser rejects blocks deeper than FRM_MAX_NEST_DEPTH, so a + // full-depth container passes the new control on to its enclosing + // container. + WidgetT *parentWidget = ds->contentBox; + FrmCtrlT *parent = NULL; for (int32_t i = ctrlCount - 1; i >= 0; i--) { - DsgnControlT *pc = ds->form->controls[i]; + FrmCtrlT *pc = ds->form->controls[i]; + WidgetT *pw = dsgnWidget(pc); - if (!pc->widget || !dsgnIsContainer(pc->typeName)) { + if (!pw || !basCtlIsContainer(pc->typeName)) { continue; } - int32_t wx = pc->widget->x; - int32_t wy = pc->widget->y; - int32_t ww = pc->widget->w; - int32_t wh = pc->widget->h; - - if (x >= wx && x < wx + ww && y >= wy && y < wy + wh && dsgnControlDepth(ds->form, pc) < DSGN_MAX_NEST_DEPTH) { - snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name); - cp->parentIndex = pc->index; - parentWidget = containerChildParent(pc); + if (x >= pw->x && x < pw->x + pw->w && y >= pw->y && y < pw->y + pw->h && frmModelCtrlDepth(ds->form, pc) < FRM_MAX_NEST_DEPTH) { + parent = pc; + parentWidget = containerChildParent(pc); break; } } + // The model makes it the last child of its container, the same place + // its widget is appended + FrmCtrlT *cp = frmModelAddCtrl(ds->form, ds->activeTool, NULL, parent); + + if (!cp) { + return; + } + if (parentWidget) { dsgnCreateControlWidget(ds->form, cp, parentWidget); } - // Keep controls[] in tree order: the new control goes after the rest of - // its container's subtree (the same place its widget was appended). - arrput(ds->form->controls, cp); - ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1; - normalizeOrder(ds); + dsgnFitFormWin(ds); - // Leave the same state a normal selection press leaves (selectedIdx is - // already the new control). The immediate mouse-release then matches the - // REORDERING guard and resets to IDLE, instead of re-arming REORDERING and - // swallowing the user's next click. + // Leave the same state a normal selection press leaves. The immediate + // mouse-release then matches the REORDERING guard and resets to IDLE, + // instead of re-arming REORDERING and swallowing the user's next click. + ds->selectedIdx = frmModelCtrlPos(ds->form, cp); ds->activeTool[0] = '\0'; ds->mode = DSGN_REORDERING; ds->dragStartY = y; - ds->form->dirty = true; ds->treeStale = true; } -void dsgnOptionSelected(DsgnFormT *form, DsgnControlT *ctrl) { - const char *value = dsgnControlGetPropValue(ctrl, DSGN_OPTION_VALUE); - - if (strcasecmp(ctrl->typeName, DSGN_OPTION_TYPE) != 0 || !value || !frmParseBool(value)) { - return; - } - - DsgnControlT *parent = dsgnFindParent(form, ctrl); - - for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { - DsgnControlT *c = form->controls[i]; - const char *other = dsgnControlGetPropValue(c, DSGN_OPTION_VALUE); - - if (c != ctrl && other && frmParseBool(other) && strcasecmp(c->typeName, DSGN_OPTION_TYPE) == 0 && dsgnFindParent(form, c) == parent) { - dsgnSetPropValue(c, DSGN_OPTION_VALUE, "False"); - } - } -} - - -// Draw selection handles on the window's painted surface. -// Called after widgets have painted, using direct display drawing. +// Draw the selected control's handles on the window's painted surface +// (after the widgets painted), as Form.DesignMode does. void dsgnPaintOverlay(DsgnStateT *ds) { - if (!ds->form || !ds->ctx || !ds->formWin || !ds->formWin->contentBuf) { - return; - } - - int32_t count = (int32_t)arrlen(ds->form->controls); - - if (ds->selectedIdx < 0 || ds->selectedIdx >= count) { - return; - } - - DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; - - if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { - return; - } - - // Draw into the window's content buffer (same coordinate space as widgets) - DisplayT cd; - - dvxContentDisplay(ds->ctx, ds->formWin, &cd); - - const BlitOpsT *ops = dvxGetBlitOps(ds->ctx); - uint32_t onColor = packColor(&cd, DSGN_HANDLE_ON_GRAY, DSGN_HANDLE_ON_GRAY, DSGN_HANDLE_ON_GRAY); - uint32_t offColor = packColor(&cd, DSGN_HANDLE_OFF_GRAY, DSGN_HANDLE_OFF_GRAY, DSGN_HANDLE_OFF_GRAY); - - for (int32_t i = 0; i < HANDLE_COUNT; i++) { - int32_t hx; - int32_t hy; - - handleOrigin(ctrl->widget, (DsgnHandleE)i, &hx, &hy); - rectFill(&cd, ops, hx, hy, DSGN_HANDLE_SIZE, DSGN_HANDLE_SIZE, handleResizes((DsgnHandleE)i) ? onColor : offColor); + if (ds->form) { + basDsgnPaintHandles(ds->ctx, ds->formWin, dsgnSelectedWidget(ds)); } } void dsgnRebuildWidgets(DsgnStateT *ds) { - WidgetT *parent = ds->form ? ds->form->contentBox : NULL; + WidgetT *parent = ds->form ? ds->contentBox : NULL; if (!parent) { return; @@ -1136,449 +509,70 @@ void dsgnRebuildWidgets(DsgnStateT *ds) { int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < count; i++) { - ds->form->controls[i]->widget = NULL; + ds->form->controls[i]->userData = NULL; } dsgnCreateWidgets(ds, parent); + dsgnFitFormWin(ds); } int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) { - if (!ds->form || !buf || bufSize <= 0) { - return DSGN_SAVE_TOO_LARGE; - } - - int32_t pos = 0; - - pos = emitClamped(buf, bufSize, pos, "VERSION DVX 1.00\n"); - pos = emitClamped(buf, bufSize, pos, "Begin Form %s\n", ds->form->name); - - // Form header: every runtime form property the designer stores, in - // table order. Name comes from the Begin line; Left/Top are implied - // by Centered and Width/Height by AutoSize; HelpTopic and ContextMenu - // are optional. Layout is read-only at runtime but is the loader's - // container type. - int32_t formPropCount = 0; - const BasPropDescT *formProps = basFormRtFormProps(&formPropCount); - - for (int32_t i = 0; i < formPropCount; i++) { - const BasPropDescT *pd = &formProps[i]; - char valBuf[DSGN_MAX_TEXT]; - - if (strcasecmp(pd->name, BAS_PROP_NAME) == 0) { - continue; - } - - if (ds->form->centered && (strcasecmp(pd->name, BAS_PROP_LEFT) == 0 || strcasecmp(pd->name, BAS_PROP_TOP) == 0)) { - continue; - } - - if (ds->form->autoSize && (strcasecmp(pd->name, BAS_PROP_WIDTH) == 0 || strcasecmp(pd->name, BAS_PROP_HEIGHT) == 0)) { - continue; - } - - if (!dsgnFormPropValue(ds->form, pd->name, valBuf, sizeof(valBuf))) { - continue; - } - - if (!valBuf[0] && (strcasecmp(pd->name, BAS_PROP_HELPTOPIC) == 0 || strcasecmp(pd->name, BAS_PROP_CONTEXTMENU) == 0)) { - continue; - } - - pos = emitPropLine(buf, bufSize, pos, "", pd->name, pd->type, valBuf); - } - - // Menu items as nested Begin Menu blocks. A level-L item's Begin line - // sits at indent L + 1 (inside the Form block), its properties one - // level deeper. - int32_t menuCount = (int32_t)arrlen(ds->form->menuItems); - int32_t curLevel = 0; - - for (int32_t i = 0; i < menuCount; i++) { - const DsgnMenuItemT *mi = &ds->form->menuItems[i]; - char pad[DSGN_PAD_BUF]; - - // Close blocks back to this item's level - while (curLevel > mi->level) { - curLevel--; - pos = emitIndented(buf, bufSize, pos, curLevel + 1, "End"); - } - - buildPad(mi->level + 1, pad); - pos = emitClamped(buf, bufSize, pos, "%sBegin Menu %s\n", pad, mi->name); - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, mi->caption); - - if (mi->checked) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CHECKED, WGT_IFACE_BOOL, "True"); - } - - if (mi->radioCheck) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_RADIOCHECK, WGT_IFACE_BOOL, "True"); - } - - if (!mi->enabled) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, "False"); - } - - // Visible is only meaningful on top-level menus -- False marks it - // as a popup (not on the menu bar). Never serialize the default. - if (mi->level == 0 && !mi->visible) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, "False"); - } - - curLevel = mi->level + 1; - } - - // Close any remaining open menu blocks - while (curLevel > 0) { - curLevel--; - pos = emitIndented(buf, bufSize, pos, curLevel + 1, "End"); - } - - // Controls, each followed by its own children. Parents are resolved - // once here instead of on every recursion level. - int32_t ctrlCount = (int32_t)arrlen(ds->form->controls); - DsgnControlT **parents = NULL; - - for (int32_t i = 0; i < ctrlCount; i++) { - arrput(parents, dsgnFindParent(ds->form, ds->form->controls[i])); - } - - pos = saveControls(ds->form, parents, buf, bufSize, pos, NULL, 1); - arrfree(parents); - - pos = emitClamped(buf, bufSize, pos, "End\n"); - - if (pos < 0) { - return pos; - } - - // emitClamped parks pos at bufSize-1 once the buffer is full, so - // reaching it means the form text was cut short. - if (pos >= bufSize - 1) { - return DSGN_SAVE_TOO_LARGE; - } - - // Append code section if present - if (ds->form->code && ds->form->code[0]) { - int32_t codeLen = (int32_t)strlen(ds->form->code); - int32_t avail = bufSize - pos - DSGN_NEWLINE_ROOM - 1; - - if (codeLen > avail) { - return DSGN_SAVE_TOO_LARGE; - } - - buf[pos++] = '\n'; - memcpy(buf + pos, ds->form->code, codeLen); - pos += codeLen; - - // Ensure trailing newline - if (buf[pos - 1] != '\n') { - buf[pos++] = '\n'; - } - } - - buf[pos] = '\0'; - return pos; + return frmModelWrite(ds->form, buf, bufSize); } -const char *dsgnSelectedName(const DsgnStateT *ds) { - if (!ds->form) { - return ""; - } +WidgetT *dsgnSelectedWidget(const DsgnStateT *ds) { + bool selected = ds->form && ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls); - if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { - return ds->form->controls[ds->selectedIdx]->name; - } - - return ds->form->name; -} - - -void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value) { - for (int32_t i = 0; i < (int32_t)arrlen(ctrl->props); i++) { - if (strcasecmp(ctrl->props[i].name, name) == 0) { - snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", value); - return; - } - } - - DsgnPropT prop; - - snprintf(prop.name, sizeof(prop.name), "%s", name); - snprintf(prop.value, sizeof(prop.value), "%s", value); - arrput(ctrl->props, prop); + return selected ? dsgnWidget(ds->form->controls[ds->selectedIdx]) : NULL; } // Update the live widget's size hints from the design data. minW/minH set // the floor; maxW/maxH cap the size (0 = no cap). -void dsgnSyncWidgetGeom(DsgnControlT *ctrl) { - if (!ctrl->widget) { +void dsgnSyncWidgetGeom(FrmCtrlT *ctrl) { + WidgetT *w = dsgnWidget(ctrl); + + if (!w) { return; } if (ctrl->width > 0) { - ctrl->widget->minW = wgtPixels(ctrl->width); + w->minW = wgtPixels(ctrl->width); } if (ctrl->height > 0) { - ctrl->widget->minH = wgtPixels(ctrl->height); + w->minH = wgtPixels(ctrl->height); } - ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0; - ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0; - ctrl->widget->weight = ctrl->weight; + w->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0; + w->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0; + w->weight = ctrl->weight; } // A control is drawn enabled only when it and every ancestor are enabled, // matching the runtime (a disabled container disables its contents). -static bool effectiveEnabled(const DsgnFormT *form, const DsgnControlT *ctrl) { - bool enabled = ctrl->enabled; - DsgnControlT *p = dsgnFindParent(form, ctrl); +static bool effectiveEnabled(const FrmModelT *form, const FrmCtrlT *ctrl) { + bool enabled = ctrl->enabled; + FrmCtrlT *p = frmModelFindParent(form, ctrl); - for (int32_t depth = 0; enabled && p && depth < DSGN_MAX_NEST_DEPTH; depth++) { + for (int32_t depth = 0; enabled && p && depth < FRM_MAX_NEST_DEPTH; depth++) { enabled = p->enabled; - p = dsgnFindParent(form, p); + p = frmModelFindParent(form, p); } return enabled; } -// Clamped formatted append into a fixed buffer. Returns pos unchanged once -// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and -// returns the new position, capped at bufSize-1 on truncation. This avoids -// the snprintf-accumulation overrun where bufSize-pos wraps negative. -// Shared by ideDesigner.c and ideMain.c via ideDesigner.h. -int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) { - va_list args; - - va_start(args, fmt); - pos = emitClampedV(buf, bufSize, pos, fmt, args); - va_end(args); - return pos; -} - - -static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args) { - if (!buf || pos < 0 || pos >= bufSize) { - return pos; - } - - int32_t avail = bufSize - pos; - int32_t written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args); - - if (written < 0) { - return pos; - } - - if (written >= avail) { - // Truncated: vsnprintf wrote avail-1 chars + NUL. Advance to - // the last writable byte before the NUL so we never exceed bufSize-1. - return bufSize - 1; - } - - return pos + written; -} - - -// Emit "Begin Type Name" and every property line of one control, without -// the closing End so the .frm writer can nest children first. -// -// Every value is written in the canonical form for its type so the runtime -// loader can classify it the same way whether it came from a fresh -// placement, a loaded .frm, or the property grid: STRING quoted, BOOL as -// True/False, INT/FLOAT/ENUM unquoted. Interface-typed keys are never -// written from the raw props[] strings; only keys unknown to the widget -// interface go out as quoted strings. -static int32_t emitControlHead(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad) { - pos = emitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name); - - if (ctrl->index >= 0) { - pos = emitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, BAS_PROP_INDEX, (int)ctrl->index); - } - - const char *caption = dsgnControlGetPropValue(ctrl, BAS_PROP_CAPTION); - const char *text = dsgnControlGetPropValue(ctrl, BAS_PROP_TEXT); - - if (caption) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, caption); - } - - if (text) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_TEXT, WGT_IFACE_STRING, text); - } - - for (int32_t i = 0; i < DSGN_INT_PROP_COUNT; i++) { - const DsgnIntPropT *ip = &sIntProps[i]; - int32_t val = intPropGet(ctrl, ip); - bool emit; - - switch (ip->saveRule) { - case DSGN_SAVE_IF_NONZERO: - emit = (val != 0); - break; - case DSGN_SAVE_IF_POSITIVE: - emit = (val > 0); - break; - default: - emit = true; - break; - } - - if (emit) { - pos = emitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, ip->name, (int)val); - } - } - - if (ctrl->helpTopic[0]) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_HELPTOPIC, WGT_IFACE_STRING, ctrl->helpTopic); - } - - if (!ctrl->visible && !dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_VISIBLE)) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, "False"); - } - - if (!ctrl->enabled && !dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_ENABLED)) { - pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, "False"); - } - - // Keys the widget interface does not know: raw strings, quoted. - for (int32_t j = 0; j < (int32_t)arrlen(ctrl->props); j++) { - const char *name = ctrl->props[j].name; - - if (strcasecmp(name, BAS_PROP_CAPTION) == 0 || strcasecmp(name, BAS_PROP_TEXT) == 0) { - continue; - } - - if (findIfaceProp(ctrl->typeName, name)) { - continue; - } - - pos = emitPropLine(buf, bufSize, pos, pad, name, WGT_IFACE_STRING, ctrl->props[j].value); - } - - // Interface properties, typed. - const char *wgtName = wgtFindByBasName(ctrl->typeName); - const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; - - if (iface) { - for (int32_t j = 0; j < iface->propCount; j++) { - const WgtPropDescT *p = &iface->props[j]; - char valBuf[DSGN_MAX_TEXT]; - - if (!p->setFn) { - continue; - } - - if (strcasecmp(p->name, BAS_PROP_CAPTION) == 0 || strcasecmp(p->name, BAS_PROP_TEXT) == 0) { - continue; - } - - if (dsgnIfacePropValue(ctrl, p, valBuf, sizeof(valBuf))) { - pos = emitPropLine(buf, bufSize, pos, pad, p->name, p->type, valBuf); - } - } - } - - return pos; -} - - -// Emit one formatted line indented to an indent-level block. -static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...) { - char pad[DSGN_PAD_BUF]; - va_list args; - - buildPad(indent, pad); - pos = emitClamped(buf, bufSize, pos, "%s", pad); - va_start(args, fmt); - pos = emitClampedV(buf, bufSize, pos, fmt, args); - va_end(args); - return emitClamped(buf, bufSize, pos, "\n"); -} - - -// Emit one "Key = Value" line in the canonical form for the value's -// interface type, one level deeper than the block's pad. A line longer -// than the parser keeps (FRM_MAX_LINE_LEN - 1 characters plus the -// newline) returns DSGN_SAVE_LINE_TOO_LONG, which every later emit -// passes through unchanged. -static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value) { - int32_t end; - - switch (type) { - case WGT_IFACE_STRING: - end = emitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value); - break; - case WGT_IFACE_BOOL: - end = emitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? "True" : "False"); - break; - case WGT_IFACE_INT: - end = emitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value)); - break; - default: - // FLOAT and ENUM: decimal text or the enum value name, unquoted. - end = emitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, value); - break; - } - - if (end < bufSize - 1 && end - pos > FRM_MAX_LINE_LEN) { - return DSGN_SAVE_LINE_TOO_LONG; - } - - return end; -} - - -const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) { - if (!typeName || !typeName[0]) { - return NULL; - } - - const char *wgtName = wgtFindByBasName(typeName); - - if (!wgtName) { - return NULL; - } - - return wgtIfaceFindProp(wgtGetIface(wgtName), propName); -} - - -// Top-left corner of selection handle h around widget w. -static void handleOrigin(const WidgetT *w, DsgnHandleE h, int32_t *outX, int32_t *outY) { - *outX = w->x + w->w * sHandleFx[h] / DSGN_HANDLE_HALVES - DSGN_HANDLE_SIZE / 2; - *outY = w->y + w->h * sHandleFy[h] / DSGN_HANDLE_HALVES - DSGN_HANDLE_SIZE / 2; -} - - -// True for the handles that resize (E, SE, S); the layout owns position. -static bool handleResizes(DsgnHandleE h) { - return h == HANDLE_E || h == HANDLE_SE || h == HANDLE_S; -} - - static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) { int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = count - 1; i >= 0; i--) { - const DsgnControlT *ctrl = ds->form->controls[i]; - - if (!ctrl->widget || !ctrl->widget->visible) { - continue; - } - - // Use the widget's actual laid-out position - int32_t wx = ctrl->widget->x; - int32_t wy = ctrl->widget->y; - int32_t ww = ctrl->widget->w; - int32_t wh = ctrl->widget->h; - - if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) { + // The widget's actual laid-out position + if (basDsgnWidgetHit(dsgnWidget(ds->form->controls[i]), x, y)) { return i; } } @@ -1587,348 +581,19 @@ static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) { } -static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y) { - if (!ctrl->widget) { - return HANDLE_NONE; +// Put controls[] in tree order; selectedIdx follows the selected control +// and any change marks the Properties tree stale. +static void normalizeSelected(DsgnStateT *ds) { + int32_t count = (int32_t)arrlen(ds->form->controls); + FrmCtrlT *sel = (ds->selectedIdx >= 0 && ds->selectedIdx < count) ? ds->form->controls[ds->selectedIdx] : NULL; + + if (frmModelNormalizeOrder(ds->form)) { + ds->treeStale = true; } - for (int32_t i = 0; i < HANDLE_COUNT; i++) { - int32_t hx; - int32_t hy; - - if (!handleResizes((DsgnHandleE)i)) { - continue; - } - - handleOrigin(ctrl->widget, (DsgnHandleE)i, &hx, &hy); - - if (x >= hx && x < hx + DSGN_HANDLE_SIZE && y >= hy && y < hy + DSGN_HANDLE_SIZE) { - return (DsgnHandleE)i; - } + if (sel) { + ds->selectedIdx = frmModelCtrlPos(ds->form, sel); } - - return HANDLE_NONE; -} - - -static int32_t intPropGet(const DsgnControlT *ctrl, const DsgnIntPropT *ip) { - return *(const int32_t *)((const char *)ctrl + ip->offset); -} - - -static void intPropSet(DsgnControlT *ctrl, const DsgnIntPropT *ip, int32_t val) { - *(int32_t *)((char *)ctrl + ip->offset) = val; -} - - -// True if ancestor is ctrl's parent, grandparent, ... (walk capped so a -// parent cycle terminates). -static bool isDescendant(const DsgnFormT *form, const DsgnControlT *ctrl, const DsgnControlT *ancestor) { - DsgnControlT *p = dsgnFindParent(form, ctrl); - - for (int32_t depth = 0; p && depth < DSGN_MAX_NEST_DEPTH; depth++) { - if (p == ancestor) { - return true; - } - - p = dsgnFindParent(form, p); - } - - return false; -} - - -static void loadOnCtrlBegin(void *userData, const char *typeName, const char *name) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - if (!ctx->form) { - return; - } - - DsgnControlT *cp = (DsgnControlT *)malloc(sizeof(DsgnControlT)); - - if (cp) { - dsgnControlInit(cp); - snprintf(cp->name, DSGN_MAX_NAME, "%s", name); - snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName); - - // The enclosing block's Index line has already been read, so its - // index identifies the exact control-array member. - if (ctx->nestDepth > 0) { - const DsgnLoadParentT *pe = &ctx->parentStack[ctx->nestDepth - 1]; - - snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pe->name); - cp->parentIndex = pe->ctrl ? pe->ctrl->index : DSGN_NO_INDEX; - } - - arrput(ctx->form->controls, cp); - } - - ctx->current = cp; - - // Push even when the allocation failed so the matching CtrlEnd pop - // keeps the depth in step with the parser. - snprintf(ctx->parentStack[ctx->nestDepth].name, DSGN_MAX_NAME, "%s", name); - ctx->parentStack[ctx->nestDepth].ctrl = cp; - ctx->nestDepth++; -} - - -static void loadOnCtrlEnd(void *userData) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - ctx->nestDepth--; - ctx->current = NULL; -} - - -static void loadOnCtrlProp(void *userData, const char *key, const char *value) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - if (!ctx->current) { - return; - } - - // Sized for the raw value: the quotes are stripped after the copy. - char val[FRM_MAX_LINE_LEN]; - snprintf(val, sizeof(val), "%s", value); - frmStripQuotes(val); - - DsgnControlT *cc = ctx->current; - const DsgnIntPropT *ip = dsgnFindIntProp(key); - - if (ip) { - intPropSet(cc, ip, atoi(val)); - } else if (strcasecmp(key, BAS_PROP_INDEX) == 0) { - cc->index = atoi(val); - } else if (strcasecmp(key, BAS_PROP_HELPTOPIC) == 0) { - snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val); - } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0 && !dsgnIfaceHasProp(cc->typeName, BAS_PROP_VISIBLE)) { - cc->visible = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_ENABLED) == 0 && !dsgnIfaceHasProp(cc->typeName, BAS_PROP_ENABLED)) { - cc->enabled = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_TABINDEX) == 0 && !dsgnIfaceHasProp(cc->typeName, BAS_PROP_TABINDEX)) { - // VB tab order: ignored (DVX has none). A widget that defines - // TabIndex itself (TabStrip: the active tab) keeps it below. - } else { - dsgnSetPropValue(cc, key, val); - } -} - - -static bool loadOnFormBegin(void *userData, const char *name) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - if (!ctx->form) { - return false; - } - - snprintf(ctx->form->name, sizeof(ctx->form->name), "%s", name); - snprintf(ctx->form->caption, DSGN_MAX_TEXT, "%s", name); - ctx->current = NULL; - ctx->nestDepth = 0; - return true; -} - - -static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - int32_t codeLen = 0; - const char *codeStart = dsgnCodeSection(trailingSrc, trailingLen, &codeLen); - - if (!ctx->form || !codeStart) { - return; - } - - ctx->form->code = (char *)malloc(codeLen + 1); - - if (ctx->form->code) { - memcpy(ctx->form->code, codeStart, codeLen); - ctx->form->code[codeLen] = '\0'; - } -} - - -// Form-level properties. AutoSize is independent of Width/Height, as at -// runtime: an explicit AutoSize wins whatever order the lines come in. -static void loadOnFormProp(void *userData, const char *key, const char *value) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - if (!ctx->form) { - return; - } - - // Sized for the raw value: the quotes are stripped after the copy. - char val[FRM_MAX_LINE_LEN]; - snprintf(val, sizeof(val), "%s", value); - frmStripQuotes(val); - - DsgnFormT *ff = ctx->form; - - if (strcasecmp(key, BAS_PROP_CAPTION) == 0) { - snprintf(ff->caption, DSGN_MAX_TEXT, "%s", val); - } else if (strcasecmp(key, BAS_PROP_LAYOUT) == 0) { - snprintf(ff->layout, DSGN_MAX_NAME, "%s", val); - } else if (strcasecmp(key, BAS_PROP_AUTOSIZE) == 0) { - ff->autoSize = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_RESIZABLE) == 0) { - ff->resizable = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_CENTERED) == 0) { - ff->centered = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_LEFT) == 0) { - ff->left = atoi(val); - } else if (strcasecmp(key, BAS_PROP_TOP) == 0) { - ff->top = atoi(val); - } else if (strcasecmp(key, BAS_PROP_WIDTH) == 0) { - ff->width = atoi(val); - } else if (strcasecmp(key, BAS_PROP_HEIGHT) == 0) { - ff->height = atoi(val); - } else if (strcasecmp(key, BAS_PROP_HELPTOPIC) == 0) { - snprintf(ff->helpTopic, DSGN_MAX_NAME, "%s", val); - } else if (strcasecmp(key, BAS_PROP_CONTEXTMENU) == 0) { - snprintf(ff->contextMenu, DSGN_MAX_NAME, "%s", val); - } -} - - -static void loadOnMenuBegin(void *userData, const char *name, int32_t level) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - if (!ctx->form) { - return; - } - - DsgnMenuItemT mi; - dsgnMenuItemInit(&mi); - snprintf(mi.name, DSGN_MAX_NAME, "%s", name); - mi.level = level; - arrput(ctx->form->menuItems, mi); - ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1; - ctx->current = NULL; -} - - -static void loadOnMenuEnd(void *userData) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - ctx->curMenuItemIdx = -1; -} - - -static void loadOnMenuProp(void *userData, const char *key, const char *value) { - DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; - - if (!ctx->form || - ctx->curMenuItemIdx < 0 || - ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->form->menuItems)) { - return; - } - - // Resolve pointer fresh each write -- arrput on nested menus may - // have reallocated the array. - DsgnMenuItemT *mip = &ctx->form->menuItems[ctx->curMenuItemIdx]; - char val[FRM_MAX_LINE_LEN]; - snprintf(val, sizeof(val), "%s", value); - frmStripQuotes(val); - - if (strcasecmp(key, BAS_PROP_CAPTION) == 0) { - snprintf(mip->caption, DSGN_MAX_TEXT, "%s", val); - } else if (strcasecmp(key, BAS_PROP_CHECKED) == 0) { - mip->checked = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_RADIOCHECK) == 0) { - mip->radioCheck = frmParseBool(val); - } else if (strcasecmp(key, BAS_PROP_ENABLED) == 0) { - // Default-true flags: the shared parser rule keeps the loader and - // the runtime (formrt.c) in agreement. - mip->enabled = frmParseBoolDefault(val, true); - } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0) { - mip->visible = frmParseBoolDefault(val, true); - } -} - - -// Allocate a form carrying the defaults every new or loaded form starts -// from (the runtime form's defaults). Returns NULL when out of memory. -static DsgnFormT *newFormDefaults(const char *name) { - DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT)); - - if (!form) { - return NULL; - } - - form->width = BAS_DEFAULT_FORM_W; - form->height = BAS_DEFAULT_FORM_H; - form->centered = true; - form->resizable = true; - snprintf(form->layout, DSGN_MAX_NAME, "%s", BAS_LAYOUT_VBOX); - snprintf(form->name, sizeof(form->name), "%s", name); - snprintf(form->caption, DSGN_MAX_TEXT, "%s", name); - return form; -} - - -// Depth-first append of parent's children (in their current relative -// order) and their subtrees to *out. -static void normalizeAppend(DsgnControlT **src, DsgnControlT **parents, bool *placed, int32_t count, const DsgnControlT *parent, DsgnControlT ***out, int32_t depth) { - for (int32_t i = 0; i < count; i++) { - if (placed[i] || parents[i] != parent) { - continue; - } - - placed[i] = true; - arrput(*out, src[i]); - - if (depth < DSGN_MAX_NEST_DEPTH) { - normalizeAppend(src, parents, placed, count, src[i], out, depth + 1); - } - } -} - - -// Reorder controls[] depth-first (each control directly followed by its -// subtree), keeping siblings in their current order, so a subtree is one -// contiguous run. selectedIdx follows the selected control; any change -// marks the Properties tree stale. -static void normalizeOrder(DsgnStateT *ds) { - int32_t count = (int32_t)arrlen(ds->form->controls); - DsgnControlT **src = ds->form->controls; - DsgnControlT **parents = NULL; - DsgnControlT **out = NULL; - bool *placed = (bool *)calloc((size_t)count + 1, sizeof(bool)); - DsgnControlT *sel = (ds->selectedIdx >= 0 && ds->selectedIdx < count) ? src[ds->selectedIdx] : NULL; - - if (!placed) { - return; - } - - for (int32_t i = 0; i < count; i++) { - arrput(parents, dsgnFindParent(ds->form, src[i])); - } - - normalizeAppend(src, parents, placed, count, NULL, &out, 1); - - // A parent cycle leaves members unplaced; keep them, in order. - for (int32_t i = 0; i < count; i++) { - if (!placed[i]) { - arrput(out, src[i]); - } - } - - for (int32_t i = 0; i < count; i++) { - if (src[i] != out[i]) { - src[i] = out[i]; - ds->treeStale = true; - } - - if (src[i] == sel) { - ds->selectedIdx = i; - } - } - - arrfree(out); - arrfree(parents); - free(placed); } @@ -1936,15 +601,15 @@ static void normalizeOrder(DsgnStateT *ds) { // nearest container ancestor (a control nested in a non-container block is // placed in the enclosing container, as the runtime loader does), or the // form's content box. -static WidgetT *parentWidgetFor(const DsgnFormT *form, const DsgnControlT *ctrl, WidgetT *contentBox) { - DsgnControlT *p = dsgnFindParent(form, ctrl); +static WidgetT *parentWidgetFor(const FrmModelT *form, const FrmCtrlT *ctrl, WidgetT *contentBox) { + FrmCtrlT *p = frmModelFindParent(form, ctrl); - for (int32_t depth = 0; p && depth < DSGN_MAX_NEST_DEPTH; depth++) { - if (dsgnIsContainer(p->typeName)) { - return p->widget ? containerChildParent(p) : contentBox; + for (int32_t depth = 0; p && depth < FRM_MAX_NEST_DEPTH; depth++) { + if (basCtlIsContainer(p->typeName)) { + return dsgnWidget(p) ? containerChildParent(p) : contentBox; } - p = dsgnFindParent(form, p); + p = frmModelFindParent(form, p); } return contentBox; @@ -1953,11 +618,11 @@ static WidgetT *parentWidgetFor(const DsgnFormT *form, const DsgnControlT *ctrl, // Re-link parent's control widgets in controls[] order so the widget child // list (which the layout follows) matches the model after a reorder. -static void relinkSiblingWidgets(const DsgnFormT *form, WidgetT *parent) { +static void relinkSiblingWidgets(const FrmModelT *form, WidgetT *parent) { int32_t count = (int32_t)arrlen(form->controls); for (int32_t i = 0; i < count; i++) { - WidgetT *w = form->controls[i]->widget; + WidgetT *w = dsgnWidget(form->controls[i]); if (w && w->parent == parent) { widgetRemoveChild(parent, w); @@ -1969,127 +634,37 @@ static void relinkSiblingWidgets(const DsgnFormT *form, WidgetT *parent) { } -// Rotate arr[start..end) so the run starting at mid comes first. Returns -// false (array untouched) when out of memory. -static bool rotateControls(DsgnControlT **arr, int32_t start, int32_t mid, int32_t end) { - int32_t len = end - start; - DsgnControlT **tmp = (DsgnControlT **)malloc((size_t)len * sizeof(DsgnControlT *)); - - if (!tmp) { - return false; - } - - memcpy(tmp, &arr[mid], (size_t)(end - mid) * sizeof(DsgnControlT *)); - memcpy(&tmp[end - mid], &arr[start], (size_t)(mid - start) * sizeof(DsgnControlT *)); - memcpy(&arr[start], tmp, (size_t)len * sizeof(DsgnControlT *)); - free(tmp); - return true; -} - - -// Write the controls whose resolved parent is parent, each followed by its -// own children. parents[i] is dsgnFindParent of controls[i]; a control -// whose parent is missing resolves to NULL and is written at the top level -// (where the canvas and tree show it). The depth cap keeps a parent -// cycle finite and matches the parser's nesting limit. -static int32_t saveControls(const DsgnFormT *form, DsgnControlT **parents, char *buf, int32_t bufSize, int32_t pos, const DsgnControlT *parent, int32_t indent) { - int32_t count = (int32_t)arrlen(form->controls); - - for (int32_t i = 0; i < count; i++) { - const DsgnControlT *ctrl = form->controls[i]; - char pad[DSGN_PAD_BUF]; - - if (parents[i] != parent) { - continue; - } - - buildPad(indent, pad); - pos = emitControlHead(ctrl, buf, bufSize, pos, pad); - - if (indent < DSGN_MAX_NEST_DEPTH) { - pos = saveControls(form, parents, buf, bufSize, pos, ctrl, indent + 1); - } - - pos = emitClamped(buf, bufSize, pos, "%sEnd\n", pad); - } - - return pos; -} - - -// Index one past the subtree of controls[idx] (controls[] depth-first). -static int32_t subtreeEnd(const DsgnFormT *form, int32_t idx) { - int32_t count = (int32_t)arrlen(form->controls); - int32_t end = idx + 1; - - while (end < count && isDescendant(form, form->controls[end], form->controls[idx])) { - end++; - } - - return end; -} - - // Canvas drag-reorder step: move the selected control, with its subtree, // past the neighboring sibling (and that sibling's subtree) in direction dir // (+1 = down, -1 = up) once the pointer passes the sibling's midpoint. // Only siblings trade places, so a child never leaves its container. The // widgets are re-linked in place instead of rebuilt. static void trySwapNeighbor(DsgnStateT *ds, int32_t y, int32_t dir) { - DsgnFormT *form = ds->form; - DsgnControlT **arr = form->controls; - int32_t count = (int32_t)arrlen(arr); - int32_t selIdx = ds->selectedIdx; - DsgnControlT *ctrl = arr[selIdx]; - DsgnControlT *parent = dsgnFindParent(form, ctrl); - int32_t selEnd = subtreeEnd(form, selIdx); - int32_t other = -1; + FrmModelT *form = ds->form; + FrmCtrlT *ctrl = form->controls[ds->selectedIdx]; + int32_t other = frmModelSibling(form, ds->selectedIdx, dir); - if (!ctrl->widget) { + if (!dsgnWidget(ctrl) || other < 0 || !dsgnWidget(form->controls[other])) { return; } - if (dir > 0) { - if (selEnd < count && dsgnFindParent(form, arr[selEnd]) == parent) { - other = selEnd; - } - } else { - for (int32_t j = selIdx - 1; j >= 0 && arr[j] != parent; j--) { - if (dsgnFindParent(form, arr[j]) == parent) { - other = j; - break; - } - } - } - - if (other < 0 || !arr[other]->widget) { - return; - } - - int32_t mid = arr[other]->widget->y + arr[other]->widget->h / 2; + WidgetT *ow = dsgnWidget(form->controls[other]); + int32_t mid = ow->y + ow->h / 2; if ((dir > 0 && y <= mid) || (dir < 0 && y >= mid)) { return; } - if (dir > 0) { - int32_t otherEnd = subtreeEnd(form, other); + // Down: before the sibling after the other one (none: the end) + int32_t after = (dir > 0) ? frmModelSibling(form, other, 1) : other; + FrmCtrlT *before = (after >= 0) ? form->controls[after] : NULL; - if (!rotateControls(arr, selIdx, selEnd, otherEnd)) { - return; - } - - ds->selectedIdx = selIdx + (otherEnd - selEnd); - } else { - if (!rotateControls(arr, other, selIdx, selEnd)) { - return; - } - - ds->selectedIdx = other; + if (!frmModelMoveCtrl(form, ctrl, before)) { + return; } - relinkSiblingWidgets(form, ctrl->widget->parent); + ds->selectedIdx = frmModelCtrlPos(form, ctrl); + relinkSiblingWidgets(form, dsgnWidget(ctrl)->parent); ds->dragStartY = y; - form->dirty = true; ds->treeStale = true; } diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h index 7b0cc472..225025ea 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h @@ -22,19 +22,21 @@ // ideDesigner.h -- DVX BASIC form designer // -// Design-time data model and design surface for visual form editing. -// Controls are stored as pure data (not live widgets) and rendered -// onto a Canvas widget. The designer reads and writes .frm files. +// The design surface of the IDE: the form is a .frm document model +// (formrt/frmModel.h, the BASIC run-time library) and the designer keeps +// only what belongs to the screen -- the live widgets shown for the +// controls (each control's userData), the selection, and the mouse +// interaction. Loading, saving, copy and paste, the property sheet and +// the menu editor all go through the model. #ifndef IDE_DESIGNER_H #define IDE_DESIGNER_H #include "dvxApp.h" #include "dvxWgt.h" -#include "canvas/canvas.h" #include "../compiler/lexer.h" #include "../formrt/formrt.h" -#include "../formrt/frmParser.h" +#include "../formrt/frmModel.h" #include "stb_ds_wrap.h" @@ -42,174 +44,7 @@ #include #include -// ============================================================ -// Limits -// ============================================================ -// -// Name, text, and nesting limits are derived from the .frm limits in -// formrt.h and frmParser.h so the designer never produces a file the -// 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_INDENT_SPACES 4 // .frm indent per block level -#define DSGN_MAX_NEST_DEPTH (FRM_MAX_NESTING - 1) // deepest control block below the Form block -#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_MENU_ID_BASE 20000 // base ID for designer preview menu items -#define DSGN_NO_INDEX (-1) // control not in a control array -#define DSGN_NEW_MENU_ID 0 // DsgnMenuItemT.id of an item added in the menu editor - -// ============================================================ -// Design-time property (stored as key=value strings) -// ============================================================ - -typedef struct { - char name[DSGN_MAX_NAME]; - char value[DSGN_MAX_TEXT]; -} DsgnPropT; - -// ============================================================ -// 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 { - char caption[DSGN_MAX_TEXT]; // "&File", "-" for separator - char name[DSGN_MAX_NAME]; // "mnuFile" - int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu - int32_t id; // menu editor identity (see above) - bool checked; - bool radioCheck; // true = radio bullet instead of checkmark - bool enabled; // default true - bool visible; // default true; false on a top-level item = popup-only (not on menu bar) -} DsgnMenuItemT; - -// ============================================================ -// Design-time control -// ============================================================ -// -// props[] holds every non-built-in "Key = Value" pair from the .frm, -// including widget interface properties, so a control keeps its values -// before its live widget exists and across widget rebuilds. Interface -// properties are written back in the canonical form for their type -// (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 { - char name[DSGN_MAX_NAME]; - char typeName[DSGN_MAX_NAME]; - char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form) - 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 top; - int32_t width; - int32_t height; - int32_t maxWidth; // 0 = no cap (stretch to fill) - int32_t maxHeight; // 0 = no cap (stretch to fill) - int32_t weight; // layout weight (0 = fixed size, >0 = share extra space) - int32_t dragMode; // DragMode (0 manual, 1 automatic) - char helpTopic[DSGN_MAX_NAME]; // help topic ID for F1 - bool visible; // design-time visibility (default true) - bool enabled; // design-time enabled (default true) - DsgnPropT *props; // stb_ds array; freed by dsgnControlFree - WidgetT *widget; // live widget (created at design time for WYSIWYG) -} DsgnControlT; - -// ============================================================ -// Built-in integer control properties -// ============================================================ -// -// One table drives the .frm loader, the .frm writer, the property grid -// rows, and the grid editor for every int32_t field of DsgnControlT. - -typedef enum { - DSGN_SAVE_ALWAYS, - DSGN_SAVE_IF_NONZERO, - DSGN_SAVE_IF_POSITIVE -} DsgnSaveRuleE; - -typedef struct { - const char *name; // .frm key and property-grid row name - const char *alias; // alternate .frm key accepted on load (NULL = none) - int32_t offset; // offsetof(DsgnControlT, field) - DsgnSaveRuleE saveRule; -} DsgnIntPropT; - -// ============================================================ -// Design-time form -// ============================================================ - -typedef struct { - // Fields are ordered by alignment to minimize struct padding. - char name[BAS_MAX_IDENT]; - char caption[DSGN_MAX_TEXT]; - int32_t width; - int32_t height; - int32_t left; // initial X position (0 = default) - int32_t top; // initial Y position (0 = default) - DsgnControlT **controls; // stb_ds array of heap-allocated pointers - DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus) - WidgetT *contentBox; // VBox parent for live widgets - char *code; // BASIC code section (malloc'd, after End block) - char layout[DSGN_MAX_NAME]; // "VBox" or "HBox" (contentBox type) - 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 autoSize; // true = dvxFitWindow, false = use width/height - bool resizable; // true = user can resize at runtime - bool dirty; -} DsgnFormT; - -// ============================================================ -// Active tool -// ============================================================ -// -// Empty string = pointer (select/move) mode. -// Non-empty = basName of the widget type to place. - -// ============================================================ -// 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 { - HANDLE_NONE = -1, - HANDLE_NW = 0, - HANDLE_N, - HANDLE_NE, - HANDLE_E, // resize width - HANDLE_SE, // resize both - HANDLE_S, // resize height - HANDLE_SW, - HANDLE_W, - HANDLE_COUNT -} DsgnHandleE; // ============================================================ // Interaction mode @@ -226,11 +61,12 @@ typedef enum { // ============================================================ typedef struct { - DsgnFormT *form; + FrmModelT *form; + WidgetT *contentBox; // the form window's content box the live widgets live in (NULL: no window) int32_t selectedIdx; // -1 = form itself selected - char activeTool[DSGN_MAX_NAME]; // "" = pointer, else basName to place + char activeTool[FRM_MAX_NAME]; // "" = pointer, else the type to place DsgnModeE mode; - DsgnHandleE activeHandle; + BasDsgnHandleE activeHandle; // the resize handle being dragged (formrt.h) int32_t dragStartY; // mouse Y at drag start (for reorder) int32_t dragOrigWidth; // widget size at resize start int32_t dragOrigHeight; @@ -245,164 +81,70 @@ typedef struct { // API // ============================================================ +// The live widget of a control (NULL: none made). +static inline WidgetT *dsgnWidget(const FrmCtrlT *ctrl); + // Initialize designer state. void dsgnInit(DsgnStateT *ds, AppContextT *ctx); -// Create live widgets for all controls in the form. +// Create live widgets for all controls in the form that have none. // Call after dsgnLoadFrm or dsgnNewForm, with the form window's 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, +// data; Visible is left to the runtime). Sets and returns the 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); +WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent); // Push every control's effective enabled state (its own and its // ancestors') to the live widgets. -void dsgnApplyEnabled(const DsgnFormT *form); +void dsgnApplyEnabled(const FrmModelT *form); -// Destroy every live widget under the form's contentBox and recreate them in -// the current controls[] order (the widget child list mirrors that order). +// Destroy every live widget under the content box and recreate them in +// the current controls[] order (the widget child list mirrors that order), +// then size the form window (dsgnFitFormWin). void dsgnRebuildWidgets(DsgnStateT *ds); -// Load a .frm file into the designer. +// Size the form window: an AutoSize form's fitted to its controls (the +// form takes that size), any other form's to its Width x Height. Called +// whenever the controls or the form's properties change, so a control +// placed on an AutoSize form shows at once. +void dsgnFitFormWin(DsgnStateT *ds); + +// Load .frm text into the designer (frmModelLoad). False, with no form, +// when it is not a form. 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. -// Returns the number of bytes written (excluding null), or one of the -// DSGN_SAVE_* codes above; the caller must not overwrite the original -// file with the partial buffer. +// Save the designer form as .frm text (frmModelWrite): the length, or an +// FRM_WRITE_* code; the caller must not overwrite the original file then. int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize); -// Append one control's "Begin ... End" block (without its children) to buf -// at pos, indented by indent levels. Returns the new position, or -// 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); - -// 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. void dsgnNewForm(DsgnStateT *ds, const char *name); // Draw selection handles over the painted window surface. void dsgnPaintOverlay(DsgnStateT *ds); +// The selected control's widget, NULL when the form is selected. +WidgetT *dsgnSelectedWidget(const DsgnStateT *ds); + // Handle mouse click on the design surface. void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag); // Handle key press in design view. void dsgnOnKey(DsgnStateT *ds, int32_t key); -// Get the name of the selected control (or form name if nothing selected). -const char *dsgnSelectedName(const DsgnStateT *ds); - -// 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); - -// Set (or add) a control's raw string property. Values longer than -// DSGN_MAX_TEXT - 1 characters are truncated. -void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value); - -// Fetch the current value of a widget interface property as a string: the -// props[] copy when one exists, else the live widget. Returns false when -// neither holds a value. -bool dsgnIfacePropValue(const DsgnControlT *ctrl, const WgtPropDescT *p, char *out, int32_t outSize); - // Push a control's size fields (width/height/max/weight) to its live widget. -void dsgnSyncWidgetGeom(DsgnControlT *ctrl); - -// Built-in integer property table: entry by index (NULL past the end) and -// lookup by .frm key (name or alias). -const DsgnIntPropT *dsgnFindIntProp(const char *name); -const DsgnIntPropT *dsgnIntPropAt(int32_t idx); - -// Text value of a runtime form property (basFormRtFormProps) as the designer -// stores it; false when the designer has no field for it (runtime-only). -bool dsgnFormPropValue(const DsgnFormT *form, const char *name, char *out, int32_t outSize); - -// Get the default event name for a control type. -const char *dsgnDefaultEvent(const char *typeName); - -// Auto-generate a unique control name for the given type. -void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize); - -// Check if a control type is a container (can hold children). -bool dsgnIsContainer(const char *typeName); - -// True if name (case-insensitive) is already the form's name, a control's -// name, or (when checkMenus) a menu item's name. Entries equal to -// exceptName are ignored so a control array or the object being renamed -// does not collide with itself. -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). -void dsgnMenuItemInit(DsgnMenuItemT *mi); - -// Look up a widget type's interface property descriptor by name, or NULL if -// the type has no such interface property. Shared IDE-internal helper. -const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName); - -// True if the widget type registers an interface property of this name. The -// generic Visible/Enabled designer rows defer to such a property. -bool dsgnIfaceHasProp(const char *typeName, const char *propName); - -// Clamped formatted append into a fixed buffer. Returns pos unchanged once -// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and -// returns the new position, capped at bufSize-1 on truncation. -int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); +void dsgnSyncWidgetGeom(FrmCtrlT *ctrl); // Free designer resources. void dsgnFree(DsgnStateT *ds); -// Create a live design-time widget for a given VB type name. -WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent); - // Build a display-only menu bar on a window from the form's menuItems. // Used in the form designer to preview the menu layout. -void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form); +void dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form); // ============================================================ // Code rename support (implemented in ideMain.c) @@ -414,4 +156,9 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form); void ideRenameInCode(const char *oldName, const char *newName); + +static inline WidgetT *dsgnWidget(const FrmCtrlT *ctrl) { + return (WidgetT *)ctrl->userData; +} + #endif // IDE_DESIGNER_H diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c index 362f8cfc..c48da47a 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c @@ -59,13 +59,18 @@ #include "ideProperties.h" #include "../basBuild.h" +#include "../basdev/basDebug.h" +#include "../basdev/basHelpMap.h" +#include "../basdev/basProcs.h" +#include "../basdev/basSource.h" +#include "../basdev/basValidate.h" #include "../basRes.h" #include "../compiler/basEvents.h" #include "../compiler/compiler.h" +#include "../formrt/basRunHost.h" #include "../formrt/formrt.h" #include "../formrt/frmParser.h" #include "dvxRes.h" -#include "../../../../libs/kpunch/sql/dvxSql.h" #include "../../../../libs/kpunch/texthelp/textHelp.h" #include "../runtime/vm.h" #include "../runtime/values.h" @@ -87,72 +92,47 @@ #define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file #define IDE_MAX_OUTPUT 32768 -#define IDE_WORD_BUF 32 // single-word scratch buffer #define IDE_SOURCE_MARGIN 64 #define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names #define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *" -#define IDE_FORM_TITLE_BUF (DSGN_MAX_TEXT + 32) // "caption [Design] *" +#define IDE_FORM_TITLE_BUF (FRM_MAX_TEXT + 32) // "caption [Design] *" #define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message #define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report #define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys #define IDE_IMM_LINE_BUF 512 // one Immediate-window line -#define IDE_IMM_WRAP_BUF (IDE_IMM_LINE_BUF + 8) // line plus "PRINT " prefix #define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton #define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text #define IDE_RENAME_TITLE "Rename File" #define IDE_VALUE_BUF 256 // formatted variable value / watch text #define IDE_FIND_TEXT_BUF 256 // find / replace text #define IDE_LINE_NUM_BUF 16 // decimal line number text -#define IDE_TYPE_NAME_BUF (BAS_MAX_IDENT + 16) // "Integer()" / "Variant/ClassName" type column text #define IDE_RGB_LABEL_BUF 16 // "Green 255" #define IDE_TAB_WIDTH_BUF 8 // tab width text input #define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line -#define IDE_QUOTE_OVERHEAD 3 // two quotes plus NUL around a string value -#define IDE_ARRAY_DIM_RESERVE 10 // room kept for ") [n]" after array dims #define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log -#define IDE_SNIPPET_SLICE 10000 // Immediate/Watch snippet steps per slice -#define IDE_SNIPPET_MAX_SLICES 100 // slices before a snippet is abandoned - -// sSyntaxMap flag: a color-only (not reserved) word, left uncolored where -// it names a member (obj.Width). -#define IDE_SYNTAX_UNRESERVED 0x80 #define IDE_MAIN_TITLE "DVX BASIC" #define IDE_GENERAL_SECTION "(General)" #define IDE_GLOBAL_SECTION "(Global)" -#define IDE_MODULE_FRAME "(module)" -#define IDE_COLLECTION_TYPE "Collection" // debugger type name of a Collection -#define IDE_OBJECT_TYPE "Object" // debugger type name of any other object -#define IDE_BREAK_TAG " [break]" #define IDE_DEFAULT_FORM "Form1" #define IDE_HELP_FILE "dvxbasic.hlp" #define IDE_PREFS_FILE "dvxbasic.ini" -#define IDE_DBP_EXT ".dbp" -#define IDE_FORM_TYPE_NAME FRM_KW_FORM // designer/validator type name of a form -#define IDE_MENU_TYPE_NAME FRM_KW_MENU // validator type name of a menu item +#define IDE_FORM_TYPE_NAME FRM_KW_FORM // designer type name of a form #define IDE_MENU_EVENT BAS_EVT_CLICK // the only event a menu item fires #define IDE_FALLBACK_EVENT BAS_EVT_CLICK // default event when a type names none -#define IDE_IMM_RHS_VAR "IDE_IMM_RHS" // Immediate assignment scratch variable #define IDE_RENAME_TEMP_FMT "IDE_MNU_REN%d" // placeholder during menu renames -#define IDE_PASTE_FORM "IdePaste" // scratch form wrapping a pasted control #define IDE_NEW_CLASS_FMT "Class%d" // name of a new class module +#define IDE_NEW_CTL_FMT "UserControl%d" // name of a new UserControl +#define IDE_MAKE_EXE_LABEL "&Make Executable..." // File menu: a program project's build +#define IDE_MAKE_CTL_LABEL "&Make Control..." // File menu: a control project's build +#define IDE_UC_DEFAULT_EVENT BAS_UC_EVT_INIT // the UserControl object's event when none has code yet +#define IDE_CTL_NO_RUN "A control project is not run: make its control file (File > Make Control), or add its .ctl files to a program project." // Text of a new class module (%s = its name) #define IDE_NEW_CLASS_TEXT BAS_CLS_KW_VERSION " DVX 1.00 CLASS\n" BAS_CLS_KW_ATTRIBUTE " " BAS_CLS_ATTR_NAME " = \"%s\"\n\nOption Explicit\n" // Keyword tags for procedure scanning (case-insensitive prefix match) -#define KW_SUB "SUB " -#define KW_FUNCTION "FUNCTION " -#define KW_END_SUB "END SUB" -#define KW_END_FUNCTION "END FUNCTION" -#define KW_PUBLIC "PUBLIC " -#define KW_PRIVATE "PRIVATE " #define KW_REM "REM" -#define KW_LET "LET " -#define KW_PRINT "PRINT" -#define KW_DIM "DIM" -#define KW_LINE_PREFIX "Line " #define FRM_BEGIN_TAG "Begin " -#define FRM_END_TAG "End" #define KW_LEN(s) ((int32_t)sizeof(s) - 1) // Preference sections/keys used from more than one place @@ -173,13 +153,11 @@ #define IDE_MIN_TAB_WIDTH 1 #define IDE_MAX_TAB_WIDTH 8 -// Editor colors -#define IDE_BP_GUTTER_R 200 -#define IDE_BP_GUTTER_G 0 -#define IDE_BP_GUTTER_B 0 -#define IDE_DBG_LINE_R 255 -#define IDE_DBG_LINE_G 255 -#define IDE_DBG_LINE_B 128 +// Code editor: the built-in colorizer, and the line markers (0xRRGGBB) +// of a breakpoint (gutter dot) and the paused line (background) +#define IDE_SYNTAX_MODE "bas" +#define IDE_BP_GUTTER_RGB 0xC80000 +#define IDE_DBG_LINE_RGB 0xFFFF80 // Window geometry #define IDE_WIN_MARGIN 10 // gap from screen edge @@ -277,6 +255,7 @@ #define CMD_RECENT_BASE 160 // 160-175 reserved for recent files #define CMD_RECENT_MAX 16 // Keep cap: menu command-ID range is a fixed slice #define CMD_MAKE_EXE 176 +#define CMD_PRJ_NEW_CTL 177 #define CMD_HELP_CONTENTS 157 #define CMD_HELP_API 158 #define IDE_MAX_IMM 1024 @@ -363,16 +342,6 @@ typedef struct { int32_t offset; // 0-based line within section, or IDE_NO_LINE (gone) } IdeLineAnchorT; -typedef struct { - char *key; - uint8_t value; -} SyntaxMapEntryT; - -typedef struct { - const char *keyword; - const char *topic; -} HelpMapEntryT; - // Row storage for the debug ListViews. Cell text is strdup'd row-major // into strs; cells holds the same pointers in the layout // wgtListViewSetData expects. @@ -381,38 +350,20 @@ typedef struct { const char **cells; // stb_ds: borrowed pointers into strs } IdeListRowsT; -// Compile-time CtrlName.Member validator context -typedef struct { - char name[BAS_MAX_IDENT]; - char wgtType[BAS_MAX_IDENT]; // "Form" for the form itself, else iface basName - char formName[BAS_MAX_IDENT]; // form that declares it (a form: its own name) -} IdeCtrlMapEntryT; - -typedef struct { - char ctrlName[BAS_MAX_IDENT]; // control that has the bad type - char typeName[BAS_MAX_IDENT]; // the unrecognised type name - char formName[BAS_MAX_IDENT]; // enclosing form (for error message) -} IdeBadTypeT; - -typedef struct { - IdeCtrlMapEntryT *entries; // stb_ds: known controls - IdeBadTypeT *badTypes; // stb_ds: unknown widget types encountered - const BasCompilerT *compiler; // compile in progress (form scope), or NULL - char currentForm[BAS_MAX_IDENT]; // most recent Begin Form -} IdeValidatorCtxT; +// How concatProject ended +typedef enum { + ConcatOkE, + ConcatUnreadableE, // a file cannot be read (missing or too large) + ConcatTooLargeE, // the source outgrows the editor's limit + ConcatFailedE // the build refused a file (ps->error says why) +} IdeConcatE; // One .frm parse during compile: the validator map plus the code section typedef struct { - IdeValidatorCtxT *validator; // NULL = do not collect controls + BasValidatorT *validator; // NULL = do not collect controls char *code; // malloc'd code section (NULL = none) } IdeFrmScanT; -// A running form's caption saved while " [break]" is appended to it -typedef struct { - const BasFormT *form; - char title[MAX_TITLE_LEN]; -} IdeBreakTitleT; - // Preferences dialog state (used by several onPrefs* handlers) typedef struct { WindowT *win; // non-NULL while the dialog is open @@ -444,26 +395,23 @@ typedef struct { static bool activateFailed(void); static bool activateFile(int32_t fileIdx, IdeViewModeE view); static bool activeIsForm(void); +static void addClassEvent(void *ctx, const char *name, const char *params); static bool addFileToProject(const char *path); +static void addWithEvents(void *ctx, const char *name, const char *className); static void anchorCodeLine(IdeLineAnchorT *a, int32_t codeLine, int32_t cur, int32_t edLines, int32_t delta, const int32_t *starts); static void anchorEditedLine(IdeLineAnchorT *a, const IdeLineDestT *map, char **procs, int32_t genBase); static int32_t anchorLine(const IdeLineAnchorT *a, const int32_t *starts); static bool anyFileModified(void); static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, const char *ctrlList, BasSourceMapT *entry); -static int32_t appendDroppingLines(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen); static void applySyntaxColors(void); -static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); -static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf, int32_t errBufSize, const char *captureName, int32_t *outCaptureIdx); static void buildVmBreakpoints(void); static void buildWindow(void); static void classEventsOf(const char *className, IdeClassEventT **events); static bool classFollowsFile(int32_t fileIdx, const char *text); -static uint8_t classifyWord(const char *word, int32_t wordLen, bool afterDot); static bool classNameUsed(const char *name, const char *fileName); static void cleanupFormWin(void); static void clearAllBreakpoints(void); static void clearOutput(void); -static void clearVmStepState(void); static void closeFindDialog(void); static void closeProject(void); static void closeProjectUi(void); @@ -471,52 +419,48 @@ static int cmpEvtPtrs(const void *a, const void *b); static int cmpStrPtrs(const void *a, const void *b); static void compileAndRun(void); static bool compileProject(void); +static IdeConcatE concatProject(BasProjectSourceT *ps, BasValidatorT *validator, bool withForms, BasSourceMapT **map, int32_t *outFileIdx); static int32_t countLines(const char *text); static void createEventSubSkeleton(const char *objName, const char *evtName); +static char **ctlHeaders(void); static void currentObjName(char *out, int32_t outSize); -static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); static void debugNavigateToLine(int32_t concatLine); static void debugResume(const char *status); -static void debugSetBreakTitles(bool paused); static void debugStartOrResume(int32_t cmd); static void debugStop(void); static void debugUpdateWindows(void); -static bool debugVarVisible(const BasDebugVarT *dv, int32_t curProcIdx); static bool dedupeBreakpoints(void); static void deleteSelectedControl(void); +static void designCtlRefresh(void); +static void designCtlRelease(void); static void designerEditCmd(int32_t cmd); static void designerRelease(void); static bool doEventsCallback(void *ctx); static void dsgnCopySelected(void); static void dsgnPasteControl(void); static int32_t editCmdKey(int32_t cmd); +static bool editorIsControl(void); static int32_t editorLineToCodeLine(int32_t editorLine); static void editorSetText(const char *text, int32_t caretLine); static void editorSnapshot(int32_t lineCount); static void ensureProject(const char *filePath); static void ensureProjectWindow(bool raise); static void evaluateImmediate(const char *expr); -static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize); static void evtItemAdd(const char *name, bool bracketed); static void evtItemsReset(void); static bool evtLabelMatches(const char *label, const char *evtName); static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needleLen, bool caseSensitive); -static const BasDebugVarT *findDebugVar(const char *name); static bool findDialogBuild(bool showReplace); static bool findInEditorSections(int32_t fromIdx, const char *needle, bool caseSensitive, bool forward, const char *objFilter); static bool findInProject(const char *needle, bool caseSensitive, bool forward, FindScopeE scope); -static int32_t findMemberVarIdx(const BasObjectT *obj, const char *name); static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen); -static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName); static const IdeWithEventsT *findWithEvents(const char *name); static void flushOutput(void); -static void formatValue(const BasValueT *v, char *buf, int32_t bufSize); static void freeProcBufs(void); +static void freeTexts(char **texts); static void frmScan(const char *frmText, IdeFrmScanT *scan); static void frmScanOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen); static char *generalAppend(const char *general, const char *text); -static BasValueT *getDebugVarSlot(const BasDebugVarT *dv); -static const char *getEventParams(const char *evtName); static bool getFindForward(void); static bool getFindMatchCase(void); static FindScopeE getFindScope(void); @@ -530,23 +474,14 @@ static void handleViewCmd(int32_t cmd); static void handleWindowCmd(int32_t cmd); static bool hasProject(void); static bool hasUnsavedData(void); -static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize); -static const char *helpLookupKeyword(const char *word); static void helpQueryHandler(void *ctx); static void helpSetCtrlTopic(const char *typeName); static void ideErrorBox(const char *msg); static void idePump(void); -static bool immEvalRhs(const char *rhs, uint8_t targetType, BasValueT *outVal); static void immPrintCallback(void *ctx, const char *text, bool newline); -static void immReportSnippetResult(BasVmT *vm, BasVmResultE result); -static bool immTryAssign(const char *expr); -static void initSyntaxMap(void); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); -static const char *instanceTypeName(const BasObjectT *obj); static void invalidateCompiled(void); static bool isCtrlArrayInDesigner(const char *ctrlName); -static bool isInStringOrComment(const char *src, int32_t i); -static bool isMangledDebugName(const char *name); static bool isRemAt(const char *line, int32_t pos, int32_t lineLen); static bool isReplaceEnabled(void); static char *joinProcArrays(const char *general, char **procs); @@ -555,28 +490,27 @@ static bool kwMatch(const char *p, const char *kw); static int32_t lineCopy(const char *text, int32_t line, char *out, int32_t outSize); static int32_t lineStartOffset(const char *text, int32_t line); static void listRowsAdd(IdeListRowsT *rows, const char *text); +static void listRowsAddRow(void *ctx, const char *const *cells, int32_t cellCount); static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount); static void listRowsFree(IdeListRowsT *rows); static void listRowsReset(IdeListRowsT *rows); -static char *loadCompileSource(int32_t fileIdx, IdeValidatorCtxT *validator, char **outCtrlList); +static char *loadCompileSource(int32_t fileIdx, BasValidatorT *validator, char **outCtrlList); static bool loadDesignerFile(int32_t fileIdx); static void loadFile(void); static void loadFormCodeIntoEditor(void); -static void loadFrmFiles(BasFormRtT *rt); static void loadModuleIntoEditor(int32_t fileIdx, const char *source); +static void loadRunFiles(BasFormRtT *rt); static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText); static int32_t localToConcatLine(int32_t editorLine); static void logFlushLine(void); -static bool lookupWatchVar(const char *expr, BasValueT *outVal); static void makeExecutable(void); -static BasValueT *memberVarSlot(const BasDebugVarT *dv); static void navigateToBreakpoint(int32_t bpIdx); static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbgLine); static void navigateToEventSub(void); static void navigateToNamedEventSub(const char *ctrlName, const char *eventName); -static void newClassModule(void); +static void newClassFile(PrjFileKindE kind); static void newProject(void); -static const char *objectClassName(const BasObjectT *obj); +static const char *objectType(const char *objName); static void onBreakpointHit(void *ctx, int32_t line); static void onBreakpointListDblClick(WidgetT *w); static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift); @@ -587,6 +521,7 @@ static void onCodeWinClose(WindowT *win); static void onColorChange(WidgetT *w); static void onColorListChange(WidgetT *w); static void onContentFocus(WindowT *win); +static void onCtlHeaderSet(const char *key, const char *value); static void onEditorChange(WidgetT *w); static void onEvtDropdownChange(WidgetT *w); static void onFindClose(WindowT *win); @@ -625,6 +560,7 @@ static void onTbStepInto(WidgetT *w); static void onTbStepOut(WidgetT *w); static void onTbStepOver(WidgetT *w); static void onTbStop(WidgetT *w); +static void onUcPropChanged(WidgetT *w, const char *propName); static void onWatchClose(WindowT *win); static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift); static void onWatchListDblClick(WidgetT *w); @@ -637,9 +573,7 @@ static void openSinglePath(const char *path); static void outputAppend(const char *text, int32_t len); static void outputLog(const char *text); static char *packProcLayout(const char *source); -static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount); static void parseProcs(const char *source); -static DsgnControlT *pasteParseControl(const char *clip, int32_t clipLen); static bool prefsBuild(WindowT *win); static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value); static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLen, const char *value); @@ -647,14 +581,13 @@ static void prefsUpdateColorSwatch(void); static void printCallback(void *ctx, const char *text, bool newline); static bool procBodyEmpty(const char *procText); static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive); -static const char *procDeclAt(const char *p, bool *outIsSub); static int32_t procIndexByName(const char *objName, const char *evtName); static int32_t procIndexForCodeLine(int32_t codeLine); -static void procNameFromDecl(const char *afterKw, char *out, int32_t outSize); static void procNameOf(const char *procText, char *out, int32_t outSize); static int32_t *procStartLines(const char *general, char **procs); static const char *projectFileText(int32_t fileIdx, char **outOwned, bool *outMissing); static bool promptAndSave(void); +static void propsFollowActive(void); static void rebuildProcTable(void); static void recentAdd(const char *path); static void recentLoad(void); @@ -662,32 +595,27 @@ static void recentOpen(int32_t index); static void recentRebuildMenu(void); static void recentRemove(const char *path); static void recentSave(void); +static void refreshEditorMarkers(void); static void remapBreakpoints(const IdeLineAnchorT *anchors, const int32_t *starts); static void removeBreakpointsForFile(int32_t fileIdx); -static char *renameInBuffer(const char *src, const char *oldName, const char *newName); -static bool renameMatchAt(const char *src, int32_t i, const char *oldName, int32_t oldLen, bool skipComments); static void renameMenuItemsInCode(char **oldNames, int32_t oldCount); static void renameProjectFile(int32_t fileIdx); static bool renameProjectFileTo(int32_t fileIdx, const char *newName); static int32_t replaceInSections(const char *objFilter, bool caseSensitive); static void reportUnreadable(const char *path); -static BasValueT *resolveVarPath(const char *path, const char **endPtr); static void runCached(void); static void runModule(BasModuleT *mod); -static BasVmResultE runProgramSlices(BasVmT *vm); -static BasVmResultE runSnippetVm(BasVmT *vm); +static BasVmResultE runProgramSlices(BasFormRtT *rt); static bool saveActiveFile(void); static bool saveAllModified(void); static bool saveCurProc(bool discardSkeleton); static void saveFile(void); static bool saveProjectFile(void); -static void scanClassEvents(const char *text, IdeClassEventT **events); -static void scanWithEvents(const char *src); static bool sectionInScope(int32_t p, const char *objFilter); static int32_t sectionLineCount(const char *text); static void selectDropdowns(const char *objName, const char *evtName); static void setBreakpointProcName(IdeBreakpointT *bp, int32_t procIdx); -static bool setClassNameAttr(int32_t fileIdx, const char *name); +static bool setFileClassName(int32_t fileIdx, const char *name); static void setOutputText(const char *text); static void setStatus(const char *text); static void shiftBreakpointsForEdit(const char *oldText, const char *newText, int32_t delta); @@ -702,9 +630,10 @@ static void showOutputWindow(void); static void showPreferencesDialog(void); static void showProc(int32_t procIdx); static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive, bool forward); +static void showProperties(void); +static void showToolbox(void); static void showWatchWindow(void); static const char *skipLine(const char *p); -static const char *skipToEndProc(const char *declLine, bool isSub, int32_t *outEndLineOffset); static bool splitProcs(const char *source, char **outGeneral, char ***outProcs, IdeLineDestT **outMap); static bool stashCurrentFile(void); static void stashEditor(void); @@ -716,7 +645,6 @@ static void teardownFormWin(void); static void toggleBreakpoint(void); static void toggleBreakpointLine(int32_t editorLine); static int32_t toolbarBottom(void); -static const char *typeNameStr(uint8_t dt); static void updateBreakpointWindow(void); static void updateCallStackWindow(void); static void updateDirtyIndicators(void); @@ -725,292 +653,10 @@ static void updateLocalsWindow(void); static void updateMainTitle(void); static void updateProjectMenuState(void); static void updateWatchWindow(void); -static void validatorFree(IdeValidatorCtxT *ctx); -static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *methodName); -static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *propName); -static const char *validatorLookupCtrlType(void *ctx, const char *ctrlName); -static void validatorOnCtrlBegin(void *ud, const char *typeName, const char *name); -static bool validatorOnFormBegin(void *ud, const char *name); -static void validatorOnMenuBegin(void *ud, const char *name, int32_t level); -static int32_t vmCurProcIdx(void); -static int32_t vmProcIndexForPc(int32_t pc); -static const char *vmProcNameForPc(int32_t pc); static void watchEditSelected(void); -static void watchPrintCallback(void *ctx, const char *text, bool newline); static bool writeProjectFile(int32_t idx); int32_t appMain(DxeAppContextT *ctx); -// ============================================================ -// Keyword-to-topic lookup for context-sensitive help (F1) -// ============================================================ - -static const HelpMapEntryT sHelpMap[] = { - // Data types - {"Boolean", "lang.datatypes"}, - {"Double", "lang.datatypes"}, - {"Integer", "lang.datatypes"}, - {"Long", "lang.datatypes"}, - {"Single", "lang.datatypes"}, - {"String", "lang.datatypes"}, - - // Declarations - {"ByRef", "lang.declarations"}, - {"ByVal", "lang.declarations"}, - {"Const", "lang.declarations"}, - {"Declare", "lang.declarations"}, - {"Dim", "lang.declarations"}, - {"Option", "lang.declarations"}, - {"ReDim", "lang.declarations"}, - {"Shared", "lang.declarations"}, - {"Static", "lang.declarations"}, - {"Type", "lang.declarations"}, - - // Operators - {"And", "lang.operators"}, - {"Like", "lang.operators"}, - {"Mod", "lang.operators"}, - {"Not", "lang.operators"}, - {"Or", "lang.operators"}, - {"Xor", "lang.operators"}, - - // Conditionals - {"Case", "lang.conditionals"}, - {"Else", "lang.conditionals"}, - {"ElseIf", "lang.conditionals"}, - {"If", "lang.conditionals"}, - {"Select", "lang.conditionals"}, - {"Then", "lang.conditionals"}, - - // Loops - {"Do", "lang.loops"}, - {"For", "lang.loops"}, - {"Loop", "lang.loops"}, - {"Next", "lang.loops"}, - {"Step", "lang.loops"}, - {"Until", "lang.loops"}, - {"Wend", "lang.loops"}, - {"While", "lang.loops"}, - - // Procedures - {"Call", "lang.procedures"}, - {"Def", "lang.procedures"}, - {"End", "lang.procedures"}, - {"Function", "lang.procedures"}, - {"Sub", "lang.procedures"}, - - // Flow control - {"Exit", "lang.flow"}, - {"GoSub", "lang.flow"}, - {"GoTo", "lang.flow"}, - {"On", "lang.flow"}, - {"Resume", "lang.flow"}, - {"Return", "lang.flow"}, - - // I/O statements - {"Data", "lang.io"}, - {"Input", "lang.io"}, - {"Print", "lang.io"}, - {"Read", "lang.io"}, - {"Rem", "lang.io"}, - {"Write", "lang.io"}, - - // Misc statements - {"DoEvents", "lang.misc"}, - {"Err", "lang.misc"}, - {"Error", "lang.misc"}, - {"Error$", "lang.misc"}, - {"Load", "lang.misc"}, - {"Shell", "lang.misc"}, - {"Sleep", "lang.misc"}, - {"Unload", "lang.misc"}, - - // Sound, joysticks and frames - {"Beep", "lang.sound"}, - {"FrameWait", "lang.sound"}, - {"Play", "lang.sound"}, - {"Sound", "lang.sound"}, - {"Stick", "lang.sound"}, - {"Strig", "lang.sound"}, - - // Console and graphics - {"Circle", "lang.screen"}, - {"Cls", "lang.screen"}, - {"Color", "lang.screen"}, - {"CsrLin", "lang.screen"}, - {"Draw", "lang.screen"}, - {"FullScreen", "lang.screen"}, - {"InKey$", "lang.screen"}, - {"Locate", "lang.screen"}, - {"Paint", "lang.screen"}, - {"Palette", "lang.screen"}, - {"PCopy", "lang.screen"}, - {"Pen", "lang.screen"}, - {"PMap", "lang.screen"}, - {"Point", "lang.screen"}, - {"Pos", "lang.screen"}, - {"PReset", "lang.screen"}, - {"PSet", "lang.screen"}, - {"Screen", "lang.screen"}, - {"VarPtr$", "lang.screen"}, - {"View", "lang.screen"}, - {"Width", "lang.screen"}, - {"Window", "lang.screen"}, - - // File I/O - {"Close", "lang.fileio"}, - {"Eof", "lang.func.fileio"}, - {"FileAttr", "lang.func.fileio"}, - {"FreeFile", "lang.func.fileio"}, - {"Loc", "lang.func.fileio"}, - {"Lof", "lang.func.fileio"}, - {"Open", "lang.fileio"}, - {"Reset", "lang.fileio"}, - {"Seek", "lang.func.fileio"}, - - // String functions - {"Asc", "lang.func.string"}, - {"Chr", "lang.func.string"}, - {"Chr$", "lang.func.string"}, - {"Environ", "lang.func.string"}, - {"Environ$", "lang.func.string"}, - {"Format", "lang.func.string"}, - {"Format$", "lang.func.string"}, - {"InStr", "lang.func.string"}, - {"LCase", "lang.func.string"}, - {"LCase$", "lang.func.string"}, - {"LTrim", "lang.func.string"}, - {"LTrim$", "lang.func.string"}, - {"Left", "lang.func.string"}, - {"Left$", "lang.func.string"}, - {"Len", "lang.func.string"}, - {"Mid", "lang.func.string"}, - {"Mid$", "lang.func.string"}, - {"RTrim", "lang.func.string"}, - {"RTrim$", "lang.func.string"}, - {"Right", "lang.func.string"}, - {"Right$", "lang.func.string"}, - {"Spc", "lang.func.string"}, - {"Space", "lang.func.string"}, - {"Space$", "lang.func.string"}, - {"String$", "lang.func.string"}, - {"Tab", "lang.func.string"}, - {"Trim", "lang.func.string"}, - {"Trim$", "lang.func.string"}, - {"UCase", "lang.func.string"}, - {"UCase$", "lang.func.string"}, - - // Math functions - {"Abs", "lang.func.math"}, - {"Atn", "lang.func.math"}, - {"Cos", "lang.func.math"}, - {"Exp", "lang.func.math"}, - {"Fix", "lang.func.math"}, - {"Int", "lang.func.math"}, - {"Log", "lang.func.math"}, - {"Randomize", "lang.func.math"}, - {"Rnd", "lang.func.math"}, - {"Sgn", "lang.func.math"}, - {"Sin", "lang.func.math"}, - {"Sqr", "lang.func.math"}, - {"Tan", "lang.func.math"}, - {"Timer", "lang.func.math"}, - - // Conversion functions - {"CBool", "lang.func.conversion"}, - {"CDbl", "lang.func.conversion"}, - {"CInt", "lang.func.conversion"}, - {"CLng", "lang.func.conversion"}, - {"CSng", "lang.func.conversion"}, - {"CStr", "lang.func.conversion"}, - {"Hex", "lang.func.conversion"}, - {"Hex$", "lang.func.conversion"}, - {"Str", "lang.func.conversion"}, - {"Str$", "lang.func.conversion"}, - {"Val", "lang.func.conversion"}, - - // Misc functions - {"InputBox", "lang.func.misc"}, - {"InputBox$", "lang.func.misc"}, - {"LoadPicture", "lang.func.misc"}, - {"MsgBox", "lang.func.misc"}, - - // SQL functions - {"SQLAffected", "lang.sql"}, - {"SQLClose", "lang.sql"}, - {"SQLEof", "lang.sql"}, - {"SQLError", "lang.sql"}, - {"SQLError$", "lang.sql"}, - {"SQLExec", "lang.sql"}, - {"SQLField", "lang.sql"}, - {"SQLField$", "lang.sql"}, - {"SQLFieldCount", "lang.sql"}, - {"SQLFieldDbl", "lang.sql"}, - {"SQLFieldInt", "lang.sql"}, - {"SQLFreeResult", "lang.sql"}, - {"SQLNext", "lang.sql"}, - {"SQLOpen", "lang.sql"}, - {"SQLQuery", "lang.sql"}, - - // App object - {"App", "lang.app"}, - - // INI functions - {"IniRead", "lang.ini"}, - {"IniWrite", "lang.ini"}, - - // Constants - {"False", "lang.constants"}, - {"True", "lang.constants"}, - {"vbCancel", "lang.constants"}, - {"vbCritical", "lang.constants"}, - {"vbModal", "lang.constants"}, - {"vbOK", "lang.constants"}, - {"vbOKCancel", "lang.constants"}, - {"vbYesNo", "lang.constants"}, - - // Variants - {"Array", "lang.variants"}, - {"CVar", "lang.variants"}, - {"Empty", "lang.variants"}, - {"IsArray", "lang.variants"}, - {"IsEmpty", "lang.variants"}, - {"IsMissing", "lang.procedures"}, - {"IsNull", "lang.variants"}, - {"IsNumeric", "lang.variants"}, - {"IsObject", "lang.variants"}, - {"Null", "lang.variants"}, - {"TypeName", "lang.variants"}, - {"VarType", "lang.variants"}, - {"Variant", "lang.variants"}, - - // Objects and class modules - {"Class_Initialize", "lang.classes"}, - {"Class_Terminate", "lang.classes"}, - {"Event", "lang.events"}, - {"Friend", "lang.classes"}, - {"Implements", "lang.interfaces"}, - {"Is", "lang.classes"}, - {"New", "lang.classes"}, - {"Nothing", "lang.classes"}, - {"Private", "lang.classes"}, - {"Property", "lang.classes"}, - {"Public", "lang.classes"}, - {"RaiseEvent", "lang.events"}, - {"Set", "lang.classes"}, - {"TypeOf", "lang.classes"}, - {"With", "lang.classes"}, - {"WithEvents", "lang.events"}, - - // Collections - {"Collection", "lang.collection"}, - {"Each", "lang.collection"}, - - // Form/control statements - {"Me", "lang.forms"}, -}; - -#define HELP_MAP_COUNT DVX_ARRAY_LEN(sHelpMap) - // ============================================================ // Module state // ============================================================ @@ -1042,6 +688,7 @@ static WidgetT *sTbCode = NULL; static WidgetT *sTbDesign = NULL; static BasVmT *sVm = NULL; // VM instance (non-NULL while running) static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5) +static BasModuleT *sDesignModule = NULL; // the project's UserControls as the designer runs them (basCtlLoadDesign) static DsgnStateT sDesigner; static WindowT *sFormWin = NULL; // Form designer window (separate) static WindowT *sToolboxWin = NULL; @@ -1107,7 +754,7 @@ static MenuT *sFileMenu = NULL; static int32_t sFileMenuBase = 0; // item count before recent files static IdeDebugStateE sDbgState = DbgIdleE; -static IdeBreakTitleT *sBreakTitles = NULL; // stb_ds: captions tagged " [break]" +static BasBreakTitleT *sBreakTitles = NULL; // stb_ds: captions tagged " [break]" static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array static int32_t *sVmBreakpoints = NULL; // stb_ds array of concat line numbers (built at compile time) static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none) @@ -1136,8 +783,6 @@ static IdeListRowsT sWatchRows; // Syntax highlighting tables (shared by several functions) // ============================================================ -static SyntaxMapEntryT *sSyntaxMap = NULL; - // Color entry names for the Colors tab (matches TEXT_SYNTAX_* indices) static const char *sSyntaxColorNames[TEXT_SYNTAX_MAX] = { [TEXT_SYNTAX_DEFAULT] = "Default Text", @@ -1151,22 +796,6 @@ static const char *sSyntaxColorNames[TEXT_SYNTAX_MAX] = { static IdePrefsDlgT sPrefsDlg; -// Watch-expression scratch buffer (used by evalWatchExpr + watchPrintCallback) -static char sWatchPrintBuf[IDE_VALUE_BUF]; -static int32_t sWatchPrintLen; - -// ============================================================ -// Event table for the Object/Event dropdowns and handler skeletons -// ============================================================ -// -// Expanded from the master BAS_EVENT_LIST in compiler/basEvents.h so the -// dropdowns, the generated parameter lists and the runtime dispatcher -// can never disagree. - -static const BasEventInfoT sEventTable[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) }; - -#define IDE_EVENT_COUNT DVX_ARRAY_LEN(sEventTable) - // ============================================================ // App descriptor // ============================================================ @@ -1217,6 +846,7 @@ static bool activateFile(int32_t fileIdx, IdeViewModeE view) { showCodeView(fileIdx); } + propsFollowActive(); return true; } @@ -1282,6 +912,7 @@ static bool activateFile(int32_t fileIdx, IdeViewModeE view) { updateProjectMenuState(); updateDirtyIndicators(); + propsFollowActive(); return true; } @@ -1292,6 +923,19 @@ static bool activeIsForm(void) { } +// addClassEvent -- basSourceClassEvents callback: one Event of a class +// module into the IdeClassEventT array ctx points at +static void addClassEvent(void *ctx, const char *name, const char *params) { + IdeClassEventT **events = (IdeClassEventT **)ctx; + IdeClassEventT evt; + + memset(&evt, 0, sizeof(evt)); + snprintf(evt.name, sizeof(evt.name), "%s", name); + snprintf(evt.params, sizeof(evt.params), "%s", params); + arrput(*events, evt); +} + + // Adds path to the open project (or activates it when already listed). // Returns false when the file lives outside the project directory -- // PrjFileT.path is project-relative, so such a file could never be @@ -1336,10 +980,27 @@ static bool addFileToProject(const char *path) { invalidateCompiled(); prjRebuildTree(&sProject); + + if (sProject.files[prjFileCount(&sProject) - 1].kind == PrjKindControlE) { + designCtlRefresh(); + } + return true; } +// addWithEvents -- basSourceWithEvents callback: one WithEvents variable +// of the editor's file into sWithEvents +static void addWithEvents(void *ctx, const char *name, const char *className) { + IdeWithEventsT we; + + (void)ctx; + snprintf(we.name, sizeof(we.name), "%s", name); + snprintf(we.className, sizeof(we.className), "%s", className); + arrput(sWithEvents, we); +} + + // anchorCodeLine -- pin whole-file line codeLine of the editor's file to // the section holding it. The layout is the stored buffers with section // cur replaced by the edited text: its edLines lines start at cur's @@ -1438,9 +1099,10 @@ static bool anyFileModified(void) { // appendCompileFile -- append project file fileIdx's compile text (text, // len bytes: its canonical layout, a form's code section only) to ps the // way basBuildConcatProject does: a form's code wrapped in BEGINFORM with -// its controls (ctrlList, see basFrmControlList), a class module's code -// after its header wrapped in BEGINCLASS, a module in BEGINMODULE. Fills -// entry's line fields; false with ps->error set on failure. +// its controls (ctrlList, see basFrmControlList), a class module's or +// UserControl's code after its header wrapped in BEGINCLASS (a +// UserControl's with its mark), a module in BEGINMODULE. Fills entry's +// line fields; false with ps->error set on failure. static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, const char *ctrlList, BasSourceMapT *entry) { PrjFileT *file = &sProject.files[fileIdx]; const char *name = ""; @@ -1453,7 +1115,7 @@ static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char if (file->kind == PrjKindFormE) { name = file->formName; wrap = BAS_BUILD_WRAP_FORM; - } else if (file->kind == PrjKindClassE) { + } else if (file->kind == PrjKindClassE || file->kind == PrjKindControlE) { if (!prjClassName(&sProject, fileIdx, text, className, (int32_t)sizeof(className))) { snprintf(ps->error, sizeof(ps->error), "No class name for %s", file->path); return false; @@ -1461,49 +1123,13 @@ static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char codePos = prjClassCodePos(text, len, &entry->firstFileLine); name = className; - wrap = BAS_BUILD_WRAP_CLASS; + wrap = prjKindWrap((PrjFileKindE)file->kind); } return basBuildAppendFile(ps, text + codePos, len - codePos, wrap, name, ctrlList, &entry->startLine, &entry->lineCount); } -// appendDroppingLines -- append text to a NUL-terminated buffer of cap -// bytes holding len bytes, dropping whole leading lines while it would -// not fit so the newest text always survives (an oversize chunk keeps -// its tail). Returns the new length. -static int32_t appendDroppingLines(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen) { - if (textLen > cap - 1) { - text += textLen - (cap - 1); - textLen = cap - 1; - } - - int32_t drop = 0; - int32_t keep = len; - - while (keep + textLen > cap - 1) { - const char *nl = memchr(buf + drop, '\n', (size_t)keep); - - if (!nl) { - drop += keep; - keep = 0; - break; - } - - int32_t cut = (int32_t)(nl + 1 - (buf + drop)); - - drop += cut; - keep -= cut; - } - - memmove(buf, buf + drop, (size_t)keep); - memcpy(buf + keep, text, (size_t)textLen); - keep += textLen; - buf[keep] = '\0'; - return keep; -} - - static void applySyntaxColors(void) { if (!sEditor) { return; @@ -1513,184 +1139,6 @@ static void applySyntaxColors(void) { } -static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { - (void)ctx; - int32_t i = 0; - - while (i < lineLen) { - char ch = line[i]; - - // Comment: ' (REM is handled with the identifiers) - if (ch == '\'') { - while (i < lineLen) { - colors[i++] = TEXT_SYNTAX_COMMENT; - } - return; - } - - // String literal - if (ch == '"') { - colors[i++] = TEXT_SYNTAX_STRING; - - while (i < lineLen && line[i] != '"') { - colors[i++] = TEXT_SYNTAX_STRING; - } - - if (i < lineLen) { - colors[i++] = TEXT_SYNTAX_STRING; - } - - continue; - } - - // Number - if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) { - while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) { - colors[i++] = TEXT_SYNTAX_NUMBER; - } - - continue; - } - - // Date literal #...#, colored as a number (the compiler's rule: - // other '#'s are file numbers) - double serial; - int32_t dateLen = (ch == '#') ? basLexerDateLiteralLen(line + i, lineLen - i, &serial) : 0; - - if (dateLen > 0) { - memset(colors + i, TEXT_SYNTAX_NUMBER, (size_t)dateLen); - i += dateLen; - continue; - } - - // Identifier or keyword - if (isalpha((unsigned char)ch) || ch == '_') { - int32_t start = i; - - while (i < lineLen && basIsIdentChar(line[i])) { - i++; - } - - // A type suffix (Dir$, Len%) is colored with its word but is - // not part of the keyword lookup. - int32_t wordLen = i - start; - - if (i < lineLen && basIsTypeSuffixChar(line[i])) { - i++; - } - - if (isRemAt(line, start, lineLen)) { - for (int32_t j = start; j < lineLen; j++) { - colors[j] = TEXT_SYNTAX_COMMENT; - } - return; - } - - uint8_t c = classifyWord(line + start, wordLen, start > 0 && line[start - 1] == '.'); - - for (int32_t j = start; j < i; j++) { - colors[j] = c; - } - - continue; - } - - // Operators - if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') { - colors[i++] = TEXT_SYNTAX_OPERATOR; - continue; - } - - // Default (whitespace, parens, etc.) - colors[i++] = TEXT_SYNTAX_DEFAULT; - } -} - - -// buildSnippetVm -- compile a one-line snippet and return a VM ready to -// run it. When paused in the debugger, the paused VM's variables are -// added to the snippet's symbol table (as globals) and their current -// values copied in, so expressions can reference them by name. When -// captureName is given, *outCaptureIdx receives the global slot of that -// snippet variable (-1 when it does not exist). On failure returns NULL -// and copies the error text into errBuf. - -static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf, int32_t errBufSize, const char *captureName, int32_t *outCaptureIdx) { - *outMod = NULL; - errBuf[0] = '\0'; - - BasCompilerT *compiler = basCompilerCreate(src, (int32_t)strlen(src)); - - if (!compiler) { - snprintf(errBuf, errBufSize, "Out of memory"); - return NULL; - } - - // Track which debug vars we added and their assigned global indices - int32_t varMap[BAS_VM_MAX_GLOBALS]; // maps temp global idx -> debug var idx - int32_t varMapCount = 0; - bool paused = (sDbgState == DbgPausedE && sVm && sDbgModule && sDbgModule->debugVars); - - if (paused) { - int32_t curProcIdx = vmCurProcIdx(); - - for (int32_t i = 0; i < sDbgModule->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) { - const BasDebugVarT *dv = &sDbgModule->debugVars[i]; - - if (!debugVarVisible(dv, curProcIdx) || isMangledDebugName(dv->name)) { - continue; - } - - // The snippet's globals, in order: slot varMapCount - if (basCompilerDeclareGlobal(compiler, dv->name, dv->dataType)) { - varMap[varMapCount] = i; - varMapCount++; - } - } - } - - if (!basCompile(compiler)) { - snprintf(errBuf, errBufSize, "%s", basCompilerError(compiler)); - basCompilerDestroy(compiler); - return NULL; - } - - if (captureName) { - *outCaptureIdx = basCompilerGlobalSlot(compiler, captureName); - } - - BasModuleT *mod = basCompilerBuildModule(compiler); - basCompilerDestroy(compiler); - - if (!mod) { - snprintf(errBuf, errBufSize, "Failed to build module"); - return NULL; - } - - BasVmT *vm = basVmCreate(); - - if (!vm) { - basModuleFree(mod); - snprintf(errBuf, errBufSize, "Out of memory"); - return NULL; - } - - basVmLoadModule(vm, mod); - - // Copy values from the paused VM into the temp VM's globals - for (int32_t g = 0; g < varMapCount; g++) { - BasValueT *slot = getDebugVarSlot(&sDbgModule->debugVars[varMap[g]]); - - if (slot) { - vm->globals[g] = basValCopy(*slot); - } - } - - *outMod = mod; - return vm; -} - - // ============================================================ // buildVmBreakpoints -- convert IDE breakpoints to VM concat line numbers // ============================================================ @@ -1737,10 +1185,11 @@ static void buildWindow(void) { wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN); wmAddMenuItem(fileMenu, "&New Class Module", CMD_PRJ_NEW_CLASS); + wmAddMenuItem(fileMenu, "New &UserControl", CMD_PRJ_NEW_CTL); wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); wmAddMenuSeparator(fileMenu); - wmAddMenuItem(fileMenu, "&Make Executable...", CMD_MAKE_EXE); + wmAddMenuItem(fileMenu, IDE_MAKE_EXE_LABEL, CMD_MAKE_EXE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "R&ename File...", CMD_PRJ_RENAME); wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE); @@ -1929,7 +1378,7 @@ static void classEventsOf(const char *className, IdeClassEventT **events) { bool match = text && prjClassName(&sProject, i, text, name, (int32_t)sizeof(name)) && strcasecmp(name, className) == 0; if (match) { - scanClassEvents(text, events); + basSourceClassEvents(text, addClassEvent, events); } free(owned); @@ -1941,47 +1390,22 @@ static void classEventsOf(const char *className, IdeClassEventT **events) { } -// classFollowsFile -- whether class module fileIdx (whose text is given) -// is named after its file: it has no Attribute VB_Name, or one equal to -// the file's base name (as File > New Class Module writes it) +// classFollowsFile -- whether class module or UserControl fileIdx (whose +// text is given) is named after its file: a class module with no +// Attribute VB_Name or one equal to the file's base name, a UserControl +// whose Begin UserControl name is (as File > New Class Module / New +// UserControl write them) static bool classFollowsFile(int32_t fileIdx, const char *text) { - char attrName[BAS_MAX_IDENT]; + char ownName[BAS_MAX_IDENT]; char fileName[BAS_MAX_IDENT]; basExtractClassName("", 0, sProject.files[fileIdx].path, fileName, (int32_t)sizeof(fileName)); - return !basExtractClassName(text, (int32_t)strlen(text), NULL, attrName, (int32_t)sizeof(attrName)) || strcasecmp(attrName, fileName) == 0; + return !prjClassName(&sProject, fileIdx, text, ownName, (int32_t)sizeof(ownName)) || strcasecmp(ownName, fileName) == 0; } -// classifyWord -- the syntax color of a word. afterDot: it names a -// member (obj.Width), where only reserved words keep their color. -static uint8_t classifyWord(const char *word, int32_t wordLen, bool afterDot) { - char upper[IDE_WORD_BUF]; - - if (wordLen <= 0 || wordLen >= IDE_WORD_BUF) { - return TEXT_SYNTAX_DEFAULT; - } - - for (int32_t i = 0; i < wordLen; i++) { - upper[i] = (char)toupper((unsigned char)word[i]); - } - - upper[wordLen] = '\0'; - - initSyntaxMap(); - - int32_t idx = shgeti(sSyntaxMap, upper); - - if (idx < 0 || (afterDot && (sSyntaxMap[idx].value & IDE_SYNTAX_UNRESERVED))) { - return TEXT_SYNTAX_DEFAULT; - } - - return sSyntaxMap[idx].value & (uint8_t)~IDE_SYNTAX_UNRESERVED; -} - - -// classNameUsed -- true when a project file is named fileName or a form -// or class module of the project is called name +// classNameUsed -- true when a project file is named fileName or a form, +// class module or UserControl of the project is called name static bool classNameUsed(const char *name, const char *fileName) { bool used = false; @@ -1990,7 +1414,7 @@ static bool classNameUsed(const char *name, const char *fileName) { used = strcasecmp(file->path, fileName) == 0 || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0); - if (!used && file->kind == PrjKindClassE) { + if (!used && (file->kind == PrjKindClassE || file->kind == PrjKindControlE)) { bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); @@ -2008,6 +1432,7 @@ static bool classNameUsed(const char *name, const char *fileName) { // cleanupFormWin -- release designer-related state without destroying // the form window itself (the caller handles that). static void cleanupFormWin(void) { + basDsgnSetDesignWindow(sFormWin, false); sFormWin = NULL; sDesigner.formWin = NULL; @@ -2018,12 +1443,12 @@ static void cleanupFormWin(void) { int32_t count = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < count; i++) { - sDesigner.form->controls[i]->widget = NULL; + sDesigner.form->controls[i]->userData = NULL; } - - sDesigner.form->contentBox = NULL; } + sDesigner.contentBox = NULL; + if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; @@ -2058,19 +1483,6 @@ static void clearOutput(void) { } -// clearVmStepState -- cancel any pending step/run-to-cursor request -static void clearVmStepState(void) { - if (!sVm) { - return; - } - - sVm->debugBreak = false; - sVm->stepOverDepth = -1; - sVm->stepOutDepth = -1; - sVm->runToCursorLine = -1; -} - - static void closeFindDialog(void) { if (sFindWin) { dvxDestroyWindow(sAc, sFindWin); @@ -2097,6 +1509,7 @@ static void closeProject(void) { closeFindDialog(); closeProjectUi(); + designCtlRelease(); invalidateCompiled(); prjClose(&sProject); updateMainTitle(); @@ -2205,7 +1618,7 @@ static bool compileProject(void) { // Build source: either concatenate project files or use editor contents BasProjectSourceT project; - IdeValidatorCtxT validatorCtx; + BasValidatorT validatorCtx; const char *src = NULL; int32_t srcLen = 0; @@ -2221,63 +1634,24 @@ static bool compileProject(void) { arrfree(sProject.sourceMap); sProject.sourceMap = NULL; - // One pass per file kind in compile order: .bas modules first (so - // CONST declarations are available), then .cls class modules, - // then .frm code sections (see basBuildConcatProject). - for (int32_t kind = 0; kind < PrjKindCountE; kind++) { - for (int32_t i = 0; i < prjFileCount(&sProject); i++) { - if (sProject.files[i].kind != kind) { - continue; - } + int32_t failIdx = -1; + IdeConcatE concat = concatProject(&project, &validatorCtx, true, &sProject.sourceMap, &failIdx); - char *ctrlList = NULL; - char *fileSrc = loadCompileSource(i, &validatorCtx, &ctrlList); + if (concat == ConcatUnreadableE) { + sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE); + showCompileError("Compilation failed: cannot read file."); + } else if (concat == ConcatTooLargeE) { + sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path); + showCompileError("Compilation failed: source too large."); + } else if (concat == ConcatFailedE) { + setStatus(project.error); + dvxSetBusy(sAc, false); + } - if (!fileSrc) { - sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[i].path, (int)IDE_MAX_SOURCE); - showCompileError("Compilation failed: cannot read file."); - basBuildFreeProject(&project); - validatorFree(&validatorCtx); - return false; - } - - int32_t fileLen = (int32_t)strlen(fileSrc); - - // BEGINFORM/ENDFORM directives plus a trailing newline - if (project.sourceLen + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) { - sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[i].path); - showCompileError("Compilation failed: source too large."); - free(fileSrc); - basBuildFreeProject(&project); - validatorFree(&validatorCtx); - return false; - } - - // startLine is recorded AFTER the injected BEGINFORM / - // BEGINCLASS so the source map lines match what the editor - // shows. - BasSourceMapT mapEntry; - - memset(&mapEntry, 0, sizeof(mapEntry)); - snprintf(mapEntry.path, sizeof(mapEntry.path), "%s", sProject.files[i].path); - - bool ok = appendCompileFile(&project, i, fileSrc, fileLen, ctrlList, &mapEntry); - - free(fileSrc); - free(ctrlList); - - if (!ok) { - setStatus(project.error); - dvxSetBusy(sAc, false); - basBuildFreeProject(&project); - validatorFree(&validatorCtx); - return false; - } - - arrput(sProject.sourceMap, mapEntry); - - dvxUpdate(sAc); - } + if (concat != ConcatOkE) { + basBuildFreeProject(&project); + basValidateFree(&validatorCtx); + return false; } src = project.source; @@ -2300,55 +1674,51 @@ static bool compileProject(void) { // DXE. Those would silently drop the control at runtime and // produce a misleading "control not found" error on first access. if (arrlen(validatorCtx.badTypes) > 0) { - int32_t n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); + int32_t n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - IDE_OUTPUT_LINE_RESERVE; i++) { - IdeBadTypeT *b = &validatorCtx.badTypes[i]; - n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, " Form '%s' control '%s': type '%s' is not registered.\n", b->formName, b->ctrlName, b->typeName); + BasBadTypeT *b = &validatorCtx.badTypes[i]; + n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName); } - n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, "\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n"); + n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n"); sOutputLen = n; showCompileError("Compilation failed: unknown widget type."); basBuildFreeProject(&project); - validatorFree(&validatorCtx); + basValidateFree(&validatorCtx); return false; } - BasCompilerT *compiler = basCompilerCreate(src, srcLen); - - if (!compiler) { - basBuildFreeProject(&project); - validatorFree(&validatorCtx); - setStatus("Out of memory."); - dvxSetBusy(sAc, false); - return false; - } - - basCompilerSetOptionExplicit(compiler, sProject.optionExplicit); - // The validator lets the compiler reject CtrlName.Member typos at compile // time; it falls back silently for dynamically-created controls (not // in the map). BasCtrlValidatorT validator; - validator.lookupCtrlType = validatorLookupCtrlType; - validator.isMethodValid = validatorIsMethodValid; - validator.isPropValid = validatorIsPropValid; - validator.ctx = &validatorCtx; - validatorCtx.compiler = compiler; - basCompilerSetValidator(compiler, &validator); + BasCompileErrorT err; - if (!basCompile(compiler)) { + basValidateCallbacks(&validatorCtx, &validator); + + BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err); + + basBuildFreeProject(&project); + basValidateFree(&validatorCtx); + + if (!mod && err.result != BAS_COMPILE_ERROR) { + setStatus(err.message); + dvxSetBusy(sAc, false); + return false; + } + + if (!mod) { // Navigation may raise dialogs (a form that cannot be stashed) dvxSetBusy(sAc, false); // Translate global error line to local file/line for display int32_t errFileIdx = -1; - int32_t errLocalLine = basCompilerErrorLine(compiler); + int32_t errLocalLine = err.line; const char *errFile = ""; - if (basCompilerErrorLine(compiler) > 0 && prjFileCount(&sProject) > 0) { - prjMapLine(&sProject, basCompilerErrorLine(compiler), &errFileIdx, &errLocalLine); + if (err.line > 0 && prjFileCount(&sProject) > 0) { + prjMapLine(&sProject, err.line, &errFileIdx, &errLocalLine); if (errFileIdx >= 0 && errFileIdx < prjFileCount(&sProject)) { errFile = sProject.files[errFileIdx].path; @@ -2359,12 +1729,12 @@ static bool compileProject(void) { char procName[IDE_FULL_NAME_BUF] = {0}; int32_t procLine = errLocalLine; - if (basCompilerErrorLine(compiler) > 0 && errFileIdx >= 0) { + if (err.line > 0 && errFileIdx >= 0) { int32_t procIdx = navigateToCodeLine(errFileIdx, errLocalLine, false); // The caret goes to the column the error is at, when known - if (basCompilerErrorCol(compiler) > 0 && sEditor) { - wgtTextAreaSetCursor(sEditor, wgtTextAreaGetCursorLine(sEditor), basCompilerErrorCol(compiler)); + if (err.col > 0 && sEditor) { + wgtTextAreaSetCursor(sEditor, wgtTextAreaGetCursorLine(sEditor), err.col); } if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) { @@ -2373,44 +1743,17 @@ static bool compileProject(void) { } } - // Strip the "Line N, Col M: " prefix from the compiler's error - const char *msg = basCompilerError(compiler); + // Show the error (without its "Line N, Col M: " prefix) with the + // procedure name and proc-relative line + const char *msg = basBuildErrorText(err.message); - if (kwMatch(msg, KW_LINE_PREFIX)) { - while (*msg && *msg != ':') { - msg++; - } - - if (*msg == ':') { - msg++; - } - - msg = dvxSkipWs(msg); - } - - // Show the error with procedure name and proc-relative line if (procName[0]) { - sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); + sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); } else { - sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); + sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); } showCompileError("Compilation failed."); - basCompilerDestroy(compiler); - basBuildFreeProject(&project); - validatorFree(&validatorCtx); - return false; - } - - basBuildFreeProject(&project); - validatorFree(&validatorCtx); - - BasModuleT *mod = basCompilerBuildModule(compiler); - basCompilerDestroy(compiler); - - if (!mod) { - setStatus("Failed to build module."); - dvxSetBusy(sAc, false); return false; } @@ -2430,6 +1773,63 @@ static bool compileProject(void) { } +// concatProject -- the compile source of the project into ps: one pass per +// file kind in compile order -- .bas modules first (so CONST declarations +// are available), then .cls class modules, then .ctl UserControls, then +// (withForms) the .frm code sections (see basBuildConcatProject) -- each +// file's source map entry appended to *map and the forms' controls added +// to validator (may be NULL). On failure *outFileIdx names the file. +static IdeConcatE concatProject(BasProjectSourceT *ps, BasValidatorT *validator, bool withForms, BasSourceMapT **map, int32_t *outFileIdx) { + for (int32_t kind = 0; kind < PrjKindCountE; kind++) { + for (int32_t i = 0; i < prjFileCount(&sProject); i++) { + if (sProject.files[i].kind != kind || (kind == PrjKindFormE && !withForms)) { + continue; + } + + char *ctrlList = NULL; + char *fileSrc = loadCompileSource(i, validator, &ctrlList); + + *outFileIdx = i; + + if (!fileSrc) { + return ConcatUnreadableE; + } + + int32_t fileLen = (int32_t)strlen(fileSrc); + + // BEGINFORM/ENDFORM directives plus a trailing newline + if (ps->sourceLen + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) { + free(fileSrc); + free(ctrlList); + return ConcatTooLargeE; + } + + // startLine is recorded AFTER the injected BEGINFORM / + // BEGINCLASS so the source map lines match what the editor + // shows. + BasSourceMapT mapEntry; + + memset(&mapEntry, 0, sizeof(mapEntry)); + snprintf(mapEntry.path, sizeof(mapEntry.path), "%s", sProject.files[i].path); + + bool ok = appendCompileFile(ps, i, fileSrc, fileLen, ctrlList, &mapEntry); + + free(fileSrc); + free(ctrlList); + + if (!ok) { + return ConcatFailedE; + } + + arrput(*map, mapEntry); + dvxUpdate(sAc); + } + } + + return ConcatOkE; +} + + static int32_t countLines(const char *text) { if (!text || !text[0]) { return 1; @@ -2448,13 +1848,20 @@ static int32_t countLines(const char *text) { // createEventSubSkeleton -- append an empty "Sub Obj_Evt ... End Sub" to -// the proc buffers and show it in the editor. Not marked dirty yet; -// if the user adds no code it is discarded once the editor leaves it. +// the proc buffers and show it in the editor, with the parameters of the +// object's event: a control's (its type's, basCtlEventParams), a +// WithEvents variable's class Event's, or in a .ctl the UserControl +// object's own (Private: a Public one would be a method of the control). +// Not marked dirty yet; if the user adds no code it is discarded once the +// editor leaves it. static void createEventSubSkeleton(const char *objName, const char *evtName) { char subName[IDE_SKELETON_BUF]; char skeleton[IDE_SKELETON_BUF]; char classParams[IDE_EVENT_PARAMS_BUF]; - const char *params = getEventParams(evtName); + char arrayParams[IDE_EVENT_PARAMS_BUF]; + const char *objType = objectType(objName); + bool ucObject = strcmp(objType, BAS_UC_OBJECT) == 0; + const char *params = basCtlEventParams(objType[0] ? objType : NULL, evtName); const IdeWithEventsT *withEvents = findWithEvents(objName); // A WithEvents variable's handler takes its class Event's parameters @@ -2477,12 +1884,13 @@ static void createEventSubSkeleton(const char *objName, const char *evtName) { snprintf(subName, sizeof(subName), "%s" BAS_EVENT_HANDLER_SEP "%s", objName, evtName); // Control-array handlers take a leading Index argument - if (isCtrlArrayInDesigner(objName)) { - snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s%s)\n\nEnd Sub\n", subName, params[0] ? ", " : "", params); - } else { - snprintf(skeleton, sizeof(skeleton), "Sub %s (%s)\n\nEnd Sub\n", subName, params); + if (!ucObject && isCtrlArrayInDesigner(objName)) { + snprintf(arrayParams, sizeof(arrayParams), "Index As Integer%s%s", params[0] ? ", " : "", params); + params = arrayParams; } + snprintf(skeleton, sizeof(skeleton), PRJ_HANDLER_FMT, ucObject ? PRJ_PRIVATE : "", subName, params); + char *buf = strdup(skeleton); if (!buf) { @@ -2501,6 +1909,38 @@ static void createEventSubSkeleton(const char *objName, const char *evtName) { } +// ctlHeaders -- the header of every UserControl of the project (its text +// up to the class code: VERSION and the Begin UserControl ... End block), +// in project order, the way CTLn resources and the designer take them. +// The text is the same the compile used (loaded buffers, disk only for +// never-loaded files). stb_ds array of malloc'd texts (freeTexts). +static char **ctlHeaders(void) { + char **headers = NULL; + + for (int32_t i = 0; i < prjFileCount(&sProject); i++) { + if (sProject.files[i].kind != PrjKindControlE) { + continue; + } + + bool missing = false; + char *owned = NULL; + const char *text = projectFileText(i, &owned, &missing); + int32_t len = text ? basClassCodePos(text, (int32_t)strlen(text)) : 0; + char *header = text ? (char *)malloc((size_t)len + 1) : NULL; + + if (header) { + memcpy(header, text, (size_t)len); + header[len] = '\0'; + arrput(headers, header); + } + + free(owned); + } + + return headers; +} + + // currentObjName -- the object picked in the code window's Object // dropdown ("" when none) static void currentObjName(char *out, int32_t outSize) { @@ -2510,30 +1950,6 @@ static void currentObjName(char *out, int32_t outSize) { } -static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) { - AppContextT *ac = (AppContextT *)ctx; - - // Convert editor line to file code line - int32_t codeLine = editorLineToCodeLine(lineNum); - int32_t fileIdx = sEditorFileIdx; - - // Breakpoint: red gutter dot - for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { - if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { - *gutterColor = packColor(&ac->display, IDE_BP_GUTTER_R, IDE_BP_GUTTER_G, IDE_BP_GUTTER_B); - break; - } - } - - // Current debug line: yellow background (sDbgCurrentLine is editor-local) - if (sDbgState == DbgPausedE && lineNum == sDbgCurrentLine) { - return packColor(&ac->display, IDE_DBG_LINE_R, IDE_DBG_LINE_G, IDE_DBG_LINE_B); - } - - return 0; -} - - // debugNavigateToLine -- show the paused (or failing) statement. The // file and line come from the source map; the procedure is picked by // line range in that file once it is active. @@ -2558,63 +1974,19 @@ static void debugNavigateToLine(int32_t concatLine) { static void debugResume(const char *status) { sDbgCurrentLine = -1; sDbgState = DbgRunningE; - debugSetBreakTitles(false); + basRunHostTagBreak(sDbgFormRt, &sBreakTitles, false); if (sVm) { - sVm->debugPaused = false; - sVm->running = true; + basVmResume(sVm); } - if (sEditor) { - wgtInvalidatePaint(sEditor); - } + refreshEditorMarkers(); updateProjectMenuState(); setStatus(status); } -// debugSetBreakTitles -- tag every running form's caption with " [break]" -// while paused, and put the saved captions back on resume. -static void debugSetBreakTitles(bool paused) { - if (!paused) { - for (int32_t t = 0; t < (int32_t)arrlen(sBreakTitles); t++) { - for (int32_t i = 0; sDbgFormRt && i < (int32_t)arrlen(sDbgFormRt->forms); i++) { - BasFormT *form = sDbgFormRt->forms[i]; - - if (form == sBreakTitles[t].form && form->window) { - dvxSetTitle(sAc, form->window, sBreakTitles[t].title); - } - } - } - - arrsetlen(sBreakTitles, 0); - return; - } - - if (!sDbgFormRt || arrlen(sBreakTitles) > 0) { - return; - } - - for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) { - BasFormT *form = sDbgFormRt->forms[i]; - - if (!form->window) { - continue; - } - - IdeBreakTitleT saved; - char tagged[MAX_TITLE_LEN]; - - saved.form = form; - snprintf(saved.title, sizeof(saved.title), "%s", form->window->title); - arrput(sBreakTitles, saved); - snprintf(tagged, sizeof(tagged), "%s%s", saved.title, IDE_BREAK_TAG); - dvxSetTitle(sAc, form->window, tagged); - } -} - - static void debugStartOrResume(int32_t cmd) { if (sDbgState == DbgPausedE && sVm) { // Already paused -- apply the appropriate step command and resume @@ -2663,22 +2035,16 @@ static void debugStop(void) { sStopRequested = true; // Nothing may pause the VM again while it unwinds - clearVmStepState(); - if (sVm) { - sVm->running = false; - sVm->debugPaused = false; - basVmSetBreakpoints(sVm, NULL, 0); + basVmStop(sVm); } - debugSetBreakTitles(false); + basRunHostTagBreak(sDbgFormRt, &sBreakTitles, false); sDbgState = sVm ? DbgStoppingE : DbgIdleE; sDbgCurrentLine = -1; sDbgEnabled = false; - if (sEditor) { - wgtInvalidatePaint(sEditor); - } + refreshEditorMarkers(); updateProjectMenuState(); } @@ -2704,43 +2070,6 @@ static void debugUpdateWindows(void) { } -// debugVarVisible -- whether a debug variable is in scope where the VM is -// paused: locals of the current proc, module variables, the variables of the -// form whose code is running (form variables of different forms share -// slot numbers, so the form must match) and the member variables of the -// object a class procedure runs for. -static bool debugVarVisible(const BasDebugVarT *dv, int32_t curProcIdx) { - if (dv->scope == SCOPE_MEMBER) { - return memberVarSlot(dv) != NULL; - } - - // Locals and a procedure's SHARED / STATIC names belong to that - // procedure; inside a procedure a module variable shows only when it - // is DIM SHARED (QBasic scope). - if (dv->procIndex >= 0) { - if (dv->procIndex != curProcIdx) { - return false; - } - } else if (curProcIdx >= 0 && !dv->shared) { - return false; - } - - if (dv->scope != SCOPE_FORM) { - return true; - } - - if (!sVm->currentFormVars) { - return false; - } - - if (!dv->formName[0]) { - return true; - } - - return sVm->currentForm && strcasecmp(dv->formName, ((const BasFormT *)sVm->currentForm)->name) == 0; -} - - // dedupeBreakpoints -- drop a breakpoint that lands on the same line of // the same file as an earlier one (edits can join lines). Returns true // if any was dropped. @@ -2786,6 +2115,81 @@ static void deleteSelectedControl(void) { } +// designCtlRefresh -- the project's UserControls for the designer: their +// code (with the modules and class modules it may use, not the forms) +// compiled and registered (basCtlLoadDesign) in place of the set before, +// so the toolbox offers them and the forms being designed hold live +// instances that run the code with UserMode False. The designer's +// widgets, the toolbox and the properties window follow. A project whose +// controls do not compile has none in the designer until they do (the +// status line says so); while code of the old set runs nothing changes. +static void designCtlRefresh(void) { + char **headers = ctlHeaders(); + int32_t count = (int32_t)arrlen(headers); + BasModuleT *module = NULL; + BasProjectSourceT ps; + BasSourceMapT *map = NULL; + int32_t failIdx = -1; + + memset(&ps, 0, sizeof(ps)); + + if (count > 0 && concatProject(&ps, NULL, false, &map, &failIdx) == ConcatOkE) { + BasCompileErrorT err; + + module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err); + } + + basBuildFreeProject(&ps); + arrfree(map); + + if (!basCtlUnloadDesign(NULL)) { + basModuleFree(module); + freeTexts(headers); + return; + } + + basModuleFree(sDesignModule); + sDesignModule = module; + + int32_t registered = module ? basCtlLoadDesign(NULL, module, (const char *const *)headers, count, sProject.projectPath) : 0; + + freeTexts(headers); + + if (count > 0 && !module) { + setStatus("The project's UserControls do not compile; the designer cannot show them until they do (compile with F5 to see why)."); + } else if (registered < count) { + setStatus("A UserControl of the project has the name of an installed control; the designer cannot show it."); + } + + if (sFormWin && sDesigner.form) { + dsgnRebuildWidgets(&sDesigner); + dvxInvalidateWindow(sAc, sFormWin); + } + + if (sToolboxWin) { + tbxDestroy(sAc, sToolboxWin); + sToolboxWin = NULL; + showToolbox(); + } + + if (sPropsWin) { + prpRebuildTree(&sDesigner); + prpRefresh(&sDesigner); + } +} + + +// designCtlRelease -- the designer lets the project's UserControls go +// (the project closes). While their code runs (a message box of theirs +// is up) they stay, and so does their module. +static void designCtlRelease(void) { + if (basCtlUnloadDesign(NULL)) { + basModuleFree(sDesignModule); + sDesignModule = NULL; + } +} + + // designerEditCmd -- an Edit menu command applied to the designer's // selected control static void designerEditCmd(int32_t cmd) { @@ -2847,20 +2251,13 @@ static void dsgnCopySelected(void) { return; } - // Serialize the selected control to FRM text with the same writer the - // .frm saver uses, into a heap buffer sized for all of its lines. - const DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; - int32_t bufSize = dsgnEmitControlSize(ctrl); - char *buf = (char *)malloc((size_t)bufSize); + // The selected control and its children as .frm text, by the writer + // the .frm saver uses + int32_t len = 0; + char *buf = frmModelCtrlText(sDesigner.form, sDesigner.form->controls[sDesigner.selectedIdx], &len); - if (!buf) { - return; - } - - int32_t pos = dsgnEmitControl(ctrl, buf, bufSize, 0, 0); - - if (pos >= 0) { - dvxClipboardCopy(buf, pos); + if (buf) { + dvxClipboardCopy(buf, len); } else { setStatus("A property value of this control is too long to copy."); } @@ -2869,12 +2266,11 @@ static void dsgnCopySelected(void) { } -// dsgnPasteControl -- paste a control copied with dsgnCopySelected. The -// clipboard block is read by the designer's own .frm loader, so every -// property (position, Visible, Enabled, HelpTopic, quoted text) comes -// back exactly as a saved form would. +// dsgnPasteControl -- paste a control (and its children) copied with +// dsgnCopySelected. The clipboard blocks are read by the .frm model's +// loader, so every property comes back exactly as a saved form would. static void dsgnPasteControl(void) { - if (!sDesigner.form || !sDesigner.form->contentBox) { + if (!sDesigner.form || !sDesigner.contentBox) { return; } @@ -2885,22 +2281,23 @@ static void dsgnPasteControl(void) { return; } - DsgnControlT *ctrl = pasteParseControl(clip, clipLen); + FrmModelT *pasted = frmModelCtrlsFromText(clip, clipLen); - if (!ctrl) { + if (!pasted) { return; } // A control with the same name exists: continue its control array, // offer to start one, or rename the paste - char newName[DSGN_MAX_NAME]; - int32_t newIndex = DSGN_NO_INDEX; - bool nameExists = false; - int32_t highIdx = DSGN_NO_INDEX; - int32_t existCount = (int32_t)arrlen(sDesigner.form->controls); + FrmCtrlT *src = pasted->controls[0]; + char newName[FRM_MAX_NAME]; + int32_t newIndex = FRM_NO_INDEX; + bool nameExists = false; + int32_t highIdx = FRM_NO_INDEX; + int32_t existCount = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < existCount; i++) { - if (strcasecmp(sDesigner.form->controls[i]->name, ctrl->name) == 0) { + if (strcasecmp(sDesigner.form->controls[i]->name, src->name) == 0) { nameExists = true; if (sDesigner.form->controls[i]->index > highIdx) { @@ -2909,34 +2306,30 @@ static void dsgnPasteControl(void) { } } - if (nameExists && highIdx != DSGN_NO_INDEX) { - snprintf(newName, sizeof(newName), "%s", ctrl->name); + if (nameExists && highIdx != FRM_NO_INDEX) { + snprintf(newName, sizeof(newName), "%s", src->name); newIndex = highIdx + 1; } else if (nameExists && dvxMessageBox(sAc, "Paste", "A control with this name already exists.\nCreate a control array?", MB_YESNO | MB_ICONQUESTION) == ID_YES) { - // Convert existing control (and its children's parent link) to index 0 - for (int32_t i = 0; i < existCount; i++) { - if (strcasecmp(sDesigner.form->controls[i]->name, ctrl->name) == 0) { - dsgnMakeArrayMember(sDesigner.form, sDesigner.form->controls[i], 0); - break; - } - } - - snprintf(newName, sizeof(newName), "%s", ctrl->name); + // The existing control (and its children's parent link) becomes index 0 + frmModelMakeArrayMember(sDesigner.form, sDesigner.form->controls[frmModelFindCtrl(sDesigner.form, src->name, FRM_NO_INDEX)], 0); + snprintf(newName, sizeof(newName), "%s", src->name); newIndex = 1; } else { - dsgnAutoName(&sDesigner, ctrl->typeName, newName, sizeof(newName)); + frmModelAutoName(sDesigner.form, src->typeName, newName, sizeof(newName)); } - snprintf(ctrl->name, sizeof(ctrl->name), "%s", newName); - ctrl->index = newIndex; - ctrl->parentName[0] = '\0'; - ctrl->parentIndex = DSGN_NO_INDEX; - arrput(sDesigner.form->controls, ctrl); - dsgnOptionSelected(sDesigner.form, ctrl); - dsgnCreateControlWidget(sDesigner.form, ctrl, sDesigner.form->contentBox); + FrmCtrlT *ctrl = frmModelInsertTree(sDesigner.form, pasted, src, NULL, newName, newIndex); - sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1; - sDesigner.form->dirty = true; + frmModelFree(pasted); + + if (!ctrl) { + return; + } + + // Widgets for the pasted controls (the others have theirs) + dsgnCreateWidgets(&sDesigner, sDesigner.contentBox); + + sDesigner.selectedIdx = frmModelCtrlPos(sDesigner.form, ctrl); prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); @@ -2970,6 +2363,13 @@ static int32_t editCmdKey(int32_t cmd) { } +// editorIsControl -- true when the editor holds a UserControl (.ctl): +// its object is UserControl, with the UserControl_* events +static bool editorIsControl(void) { + return sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject) && sProject.files[sEditorFileIdx].kind == PrjKindControlE; +} + + // Converts a 1-based editor line (within the current proc's buffer) to its // 1-based line in the file's full code. For (General)/no-proc context // (sCurProcIdx out of range) the editor line is the code line. @@ -3010,6 +2410,7 @@ static void editorSetText(const char *text, int32_t caretLine) { } editorSnapshot(countLines(wgtGetText(sEditor))); + refreshEditorMarkers(); if (caretLine > IDE_CARET_LOAD) { wgtTextAreaSetCursor(sEditor, caretLine, 1); @@ -3097,7 +2498,8 @@ static void ensureProject(const char *filePath) { *dot = '\0'; } - prjNew(&sProject, baseName, dir, sPrefs); + prjNew(&sProject, baseName, dir); + prjApplyDefaults(&sProject, sPrefs); PrjFileKindE kind = prjKindForPath(filePath); @@ -3129,6 +2531,7 @@ static void ensureProject(const char *filePath) { prjLoadAllFiles(&sProject, sAc); updateMainTitle(); setStatus("Project created."); + designCtlRefresh(); updateProjectMenuState(); } @@ -3161,80 +2564,14 @@ static void ensureProjectWindow(bool raise) { // Immediate window. If the line doesn't start with a statement keyword, // wrap it in PRINT so expressions produce visible output. +// evaluateImmediate -- run a line typed into the Immediate window (an +// assignment to the paused program's variable, a statement, or an +// expression to print); an assignment refreshes the debug windows. static void evaluateImmediate(const char *expr) { - if (!expr || *expr == '\0') { - return; + if (basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL)) { + updateLocalsWindow(); + updateWatchWindow(); } - - // Try assignment first when paused - if (immTryAssign(expr)) { - return; - } - - char wrapped[IDE_IMM_WRAP_BUF]; - - // If it already starts with a statement keyword, use as-is - if (kwMatch(expr, KW_PRINT) || kwMatch(expr, KW_DIM) || kwMatch(expr, KW_LET)) { - snprintf(wrapped, sizeof(wrapped), "%s", expr); - } else { - snprintf(wrapped, sizeof(wrapped), "%s %s", KW_PRINT, expr); - } - - BasModuleT *mod = NULL; - char err[IDE_VALUE_BUF]; - BasVmT *vm = buildSnippetVm(wrapped, &mod, err, sizeof(err), NULL, NULL); - - if (!vm) { - immPrintCallback(NULL, "Error: ", false); - immPrintCallback(NULL, err, true); - return; - } - - basVmSetPrintCallback(vm, immPrintCallback, NULL); - immReportSnippetResult(vm, runSnippetVm(vm)); - basVmDestroy(vm); - basModuleFree(mod); -} - - -// evalWatchExpr -- compile and evaluate an expression using the paused VM's -// state. Used as a fallback when lookupWatchVar can't handle the -// expression. Returns the printed result in outBuf. - -static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize) { - if (sDbgState != DbgPausedE || !sVm || !sDbgModule || !sDbgModule->debugVars) { - return false; - } - - char wrapped[IDE_IMM_WRAP_BUF]; - snprintf(wrapped, sizeof(wrapped), "%s %s", KW_PRINT, expr); - - BasModuleT *mod = NULL; - char err[IDE_VALUE_BUF]; - BasVmT *tvm = buildSnippetVm(wrapped, &mod, err, sizeof(err), NULL, NULL); - - if (!tvm) { - return false; - } - - // Set up capture callback - sWatchPrintBuf[0] = '\0'; - sWatchPrintLen = 0; - basVmSetPrintCallback(tvm, watchPrintCallback, NULL); - - // Run - runSnippetVm(tvm); - - // Strip trailing newline - if (sWatchPrintLen > 0 && sWatchPrintBuf[sWatchPrintLen - 1] == '\n') { - sWatchPrintBuf[--sWatchPrintLen] = '\0'; - } - - snprintf(outBuf, outBufSize, "%s", sWatchPrintBuf); - - basVmDestroy(tvm); - basModuleFree(mod); - return sWatchPrintLen > 0; } @@ -3301,26 +2638,6 @@ static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needle } -// findDebugVar -- find a debug variable by name, respecting scope -static const BasDebugVarT *findDebugVar(const char *name) { - if (!sVm || !sDbgModule || !sDbgModule->debugVars) { - return NULL; - } - - int32_t curProcIdx = vmCurProcIdx(); - - for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) { - const BasDebugVarT *dv = &sDbgModule->debugVars[i]; - - if (strcasecmp(dv->name, name) == 0 && debugVarVisible(dv, curProcIdx)) { - return dv; - } - } - - return NULL; -} - - // findDialogBuild -- build the Find / Replace widgets into sFindWin. // Returns false if any widget could not be made. static bool findDialogBuild(bool showReplace) { @@ -3511,23 +2828,6 @@ static bool findInProject(const char *needle, bool caseSensitive, bool forward, } -// findMemberVarIdx -- member variable slot of class instance obj called -// name, from the debug variable table (-1 when there is none) -static int32_t findMemberVarIdx(const BasObjectT *obj, const char *name) { - const char *cls = objectClassName(obj); - - for (int32_t i = 0; cls && i < sDbgModule->debugVarCount; i++) { - const BasDebugVarT *dv = &sDbgModule->debugVars[i]; - - if (dv->scope == SCOPE_MEMBER && strcasecmp(dv->formName, cls) == 0 && strcasecmp(dv->name, name) == 0 && dv->index < obj->fieldCount) { - return dv->index; - } - } - - return -1; -} - - // Case-insensitive strstr replacement (strcasestr is a GNU extension) static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) { for (; *haystack; haystack++) { @@ -3540,26 +2840,6 @@ static const char *findSubstrNoCase(const char *haystack, const char *needle, in } -// findUdtFieldIdx -- field index within a UDT from the debug type table -static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName) { - for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) { - if (sDbgModule->debugUdtDefs[t].typeId != typeId) { - continue; - } - - for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) { - if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) { - return f; - } - } - - return -1; - } - - return -1; -} - - // findWithEvents -- the WithEvents variable of the code window's file // called name, NULL when there is none static const IdeWithEventsT *findWithEvents(const char *name) { @@ -3589,109 +2869,6 @@ static void flushOutput(void) { } -static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) { - switch (v->type) { - case BAS_TYPE_INTEGER: - case BAS_TYPE_BYTE: - snprintf(buf, bufSize, "%d", (int)v->intVal); - break; - - case BAS_TYPE_CURRENCY: { - BasStringT *text = basValFormatString(*v); - - snprintf(buf, bufSize, "%s", text->data); - basStringUnref(text); - break; - } - - case BAS_TYPE_LONG: - snprintf(buf, bufSize, "%ld", (long)v->longVal); - break; - - case BAS_TYPE_SINGLE: - snprintf(buf, bufSize, "%.6g", (double)v->sngVal); - break; - - case BAS_TYPE_DOUBLE: - snprintf(buf, bufSize, "%.10g", v->dblVal); - break; - - case BAS_TYPE_BOOLEAN: - snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False"); - break; - - case BAS_TYPE_DATE: { - BasStringT *text = basValFormatString(*v); - - snprintf(buf, bufSize, "#%s#", text->data); - basStringUnref(text); - break; - } - - case BAS_TYPE_STRING: - if (v->strVal) { - snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - IDE_QUOTE_OVERHEAD), v->strVal->data); - } else { - snprintf(buf, bufSize, "\"\""); - } - break; - - case BAS_TYPE_EMPTY: - snprintf(buf, bufSize, "Empty"); - break; - - case BAS_TYPE_NULL: - snprintf(buf, bufSize, "Null"); - break; - - case BAS_TYPE_MISSING: - snprintf(buf, bufSize, "Missing"); - break; - - case BAS_TYPE_OBJECT: - snprintf(buf, bufSize, "%s", v->objVal ? "<" IDE_OBJECT_TYPE ">" : "Nothing"); - break; - - case BAS_TYPE_INSTANCE: - if (v->instVal->classIdx == BAS_OBJ_COLLECTION) { - snprintf(buf, bufSize, IDE_COLLECTION_TYPE " (%d items)", (int)v->instVal->itemCount); - } else { - snprintf(buf, bufSize, "<%s>", instanceTypeName(v->instVal)); - } - break; - - case BAS_TYPE_ARRAY: { - BasArrayT *arr = v->arrVal; - - if (!arr) { - snprintf(buf, bufSize, "(uninitialized)"); - break; - } - - // emitClamped clamps pos to bufSize-1 on truncation, so a - // multi-dim array can't push pos past bufSize and make a later - // 'bufSize - pos' go negative (huge size_t -> OOB write). - int32_t pos = emitClamped(buf, bufSize, 0, "%s(", typeNameStr(arr->elementType)); - - for (int32_t d = 0; d < arr->dims && pos < bufSize - IDE_ARRAY_DIM_RESERVE; d++) { - if (d > 0) { - pos = emitClamped(buf, bufSize, pos, ", "); - } - - pos = emitClamped(buf, bufSize, pos, "%d To %d", (int)arr->lbound[d], (int)arr->ubound[d]); - } - - emitClamped(buf, bufSize, pos, ") [%d]", (int)arr->totalElements); - break; - } - - default: - snprintf(buf, bufSize, "..."); - break; - } -} - - // freeProcBufs -- release all procedure buffers static void freeProcBufs(void) { @@ -3711,29 +2888,31 @@ static void freeProcBufs(void) { } +// freeTexts -- free an stb_ds array of malloc'd texts and the array +static void freeTexts(char **texts) { + for (int32_t i = 0; i < (int32_t)arrlen(texts); i++) { + free(texts[i]); + } + + arrfree(texts); +} + + // frmScan -- one parse of .frm text: collects the code section into // scan->code (malloc'd) and, when scan->validator is set, the form's // controls for the compile-time member validator. static void frmScan(const char *frmText, IdeFrmScanT *scan) { - FrmParserCbsT cbs; - - memset(&cbs, 0, sizeof(cbs)); - cbs.userData = scan; - cbs.onFormBegin = validatorOnFormBegin; - cbs.onCtrlBegin = validatorOnCtrlBegin; - cbs.onMenuBegin = validatorOnMenuBegin; - cbs.onFormEnd = frmScanOnFormEnd; - frmParse(frmText, (int32_t)strlen(frmText), &cbs); + basValidateScanFrm(scan->validator, frmText, (int32_t)strlen(frmText), frmScanOnFormEnd, scan); } // frmScanOnFormEnd -- capture the code after the form's End exactly as -// the designer loads it (dsgnCodeSection), so compiled line numbers +// the designer loads it (frmCodeSection), so compiled line numbers // match what the editor shows. static void frmScanOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) { IdeFrmScanT *scan = (IdeFrmScanT *)userData; int32_t codeLen = 0; - const char *code = dsgnCodeSection(trailingSrc, trailingLen, &codeLen); + const char *code = frmCodeSection(trailingSrc, trailingLen, &codeLen); if (!code) { return; @@ -3776,50 +2955,6 @@ static char *generalAppend(const char *general, const char *text) { } -// getDebugVarSlot -- return a pointer to the actual BasValueT in the running VM -static BasValueT *getDebugVarSlot(const BasDebugVarT *dv) { - if (!sVm) { - return NULL; - } - - if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) { - BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1]; - - // A frame only initializes and releases its own procedure's - // localCount slots; anything past them is stale. A ByRef - // parameter shows (and sets) the caller's variable. - if (dv->index >= 0 && dv->index < frame->localCount) { - return basValDeref(&frame->locals[dv->index]); - } - } else if (dv->scope == SCOPE_GLOBAL) { - if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) { - return &sVm->globals[dv->index]; - } - } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) { - if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) { - return &sVm->currentFormVars[dv->index]; - } - } else if (dv->scope == SCOPE_MEMBER) { - return memberVarSlot(dv); - } - - return NULL; -} - - -// Parameter list the runtime passes to a handler of evtName ("" when -// none, or for events not in the master table). -static const char *getEventParams(const char *evtName) { - for (int32_t i = 0; i < IDE_EVENT_COUNT; i++) { - if (strcasecmp(sEventTable[i].suffix, evtName) == 0) { - return sEventTable[i].params; - } - } - - return ""; -} - - static bool getFindForward(void) { return !sDirGroup || wgtRadioGroupGetSelected(sDirGroup) != FindDirBackE; } @@ -3981,6 +3116,7 @@ static void handleProjectCmd(int32_t cmd) { if (hasProject()) { if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) { updateMainTitle(); + updateProjectMenuState(); if (sProjectWin) { prjRebuildTree(&sProject); @@ -3991,7 +3127,13 @@ static void handleProjectCmd(int32_t cmd) { case CMD_PRJ_NEW_CLASS: if (isIdle && hasProject()) { - newClassModule(); + newClassFile(PrjKindClassE); + } + break; + + case CMD_PRJ_NEW_CTL: + if (isIdle && hasProject()) { + newClassFile(PrjKindControlE); } break; @@ -4049,6 +3191,8 @@ static void handleProjectCmd(int32_t cmd) { sDesignerFileIdx--; } + bool rmControl = rmFile->kind == PrjKindControlE; + removeBreakpointsForFile(rmIdx); prjRemoveFile(&sProject, rmIdx); // adjusts activeFileIdx invalidateCompiled(); @@ -4056,6 +3200,10 @@ static void handleProjectCmd(int32_t cmd) { prjRebuildTree(&sProject); updateProjectMenuState(); updateDirtyIndicators(); + + if (rmControl) { + designCtlRefresh(); + } break; } @@ -4071,9 +3219,9 @@ static void handleRunCmd(int32_t cmd) { if (sDbgState == DbgPausedE) { // Resume from breakpoint -- clear debug mode so it runs free sDbgEnabled = false; - clearVmStepState(); if (sVm) { + basVmClearStepState(sVm); basVmSetBreakpoints(sVm, NULL, 0); } @@ -4087,7 +3235,10 @@ static void handleRunCmd(int32_t cmd) { case CMD_DEBUG: if (sDbgState == DbgPausedE) { // Already debugging -- resume, run to next breakpoint - clearVmStepState(); + if (sVm) { + basVmClearStepState(sVm); + } + debugResume("Debugging..."); } else if (sDbgState == DbgIdleE) { // Start in debug mode with breakpoints @@ -4289,26 +3440,11 @@ static void handleWindowCmd(int32_t cmd) { break; case CMD_WIN_TOOLBOX: - if (!sToolboxWin) { - sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom()); - - if (sToolboxWin) { - sToolboxWin->onMenu = onMenu; - sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; - } - } + showToolbox(); break; case CMD_WIN_PROPS: - if (!sPropsWin) { - sPropsWin = prpCreate(sAc, &sDesigner); - - if (sPropsWin) { - sPropsWin->y = toolbarBottom(); - sPropsWin->onMenu = onMenu; - sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; - } - } + showProperties(); break; case CMD_WIN_PROJECT: @@ -4331,31 +3467,6 @@ static bool hasUnsavedData(void) { } -// Build control help topic from type name. -// Topic IDs in .bhs files follow the pattern ctrl.. -// Generated dynamically so third-party widgets get help automatically. -static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize) { - int32_t off = snprintf(buf, bufSize, "ctrl."); - - for (int32_t i = 0; typeName[i] && off < bufSize - 1; i++) { - buf[off++] = tolower((unsigned char)typeName[i]); - } - - buf[off] = '\0'; -} - - -static const char *helpLookupKeyword(const char *word) { - for (int32_t i = 0; i < HELP_MAP_COUNT; i++) { - if (strcasecmp(sHelpMap[i].keyword, word) == 0) { - return sHelpMap[i].topic; - } - } - - return NULL; -} - - static void helpQueryHandler(void *ctx) { (void)ctx; @@ -4384,7 +3495,7 @@ static void helpQueryHandler(void *ctx) { char word[IDE_FULL_NAME_BUF]; if (wgtTextAreaGetWordAtCursor(area, word, sizeof(word)) > 0) { - const char *topic = helpLookupKeyword(word); + const char *topic = basHelpKeywordTopic(word); if (topic) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); @@ -4449,7 +3560,7 @@ static void helpQueryHandler(void *ctx) { static void helpSetCtrlTopic(const char *typeName) { - helpBuildCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic)); + basHelpCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic)); } @@ -4488,12 +3599,14 @@ void ideRenameInCode(const char *oldName, const char *newName) { // The editor may hold edits that are not in the proc buffers yet saveCurProc(false); + bool codeOnly = prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true); + // Rename in the per-procedure buffers (the code bound to the editor) bool editorChanged = false; bool shownChanged = false; if (sGeneralBuf) { - char *replaced = renameInBuffer(sGeneralBuf, oldName, newName); + char *replaced = basSourceRename(sGeneralBuf, oldName, newName, codeOnly); if (replaced) { free(sGeneralBuf); @@ -4504,7 +3617,7 @@ void ideRenameInCode(const char *oldName, const char *newName) { } for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) { - char *replaced = renameInBuffer(sProcBufs[i], oldName, newName); + char *replaced = basSourceRename(sProcBufs[i], oldName, newName, codeOnly); if (replaced) { free(sProcBufs[i]); @@ -4531,7 +3644,7 @@ void ideRenameInCode(const char *oldName, const char *newName) { stashFormCode(); sDesigner.form->dirty = true; } else if (sDesigner.form->code) { - char *replaced = renameInBuffer(sDesigner.form->code, oldName, newName); + char *replaced = basSourceRename(sDesigner.form->code, oldName, newName, codeOnly); if (replaced) { free(sDesigner.form->code); @@ -4555,7 +3668,7 @@ void ideRenameInCode(const char *oldName, const char *newName) { bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); - char *replaced = renameInBuffer(text, oldName, newName); + char *replaced = basSourceRename(text, oldName, newName, codeOnly); free(owned); @@ -4571,41 +3684,6 @@ void ideRenameInCode(const char *oldName, const char *newName) { } -// immEvalRhs -- evaluate the right-hand side of an Immediate-window -// assignment as a BASIC expression (it may use the paused program's -// variables) converted to the target's type. Reports any error in the -// Immediate window and returns false. -static bool immEvalRhs(const char *rhs, uint8_t targetType, BasValueT *outVal) { - char src[IDE_IMM_WRAP_BUF + BAS_MAX_IDENT * 2]; - - snprintf(src, sizeof(src), "%s %s AS %s: %s = %s", KW_DIM, IDE_IMM_RHS_VAR, typeNameStr(targetType), IDE_IMM_RHS_VAR, rhs); - - BasModuleT *mod = NULL; - char err[IDE_VALUE_BUF]; - int32_t idx = -1; - BasVmT *vm = buildSnippetVm(src, &mod, err, sizeof(err), IDE_IMM_RHS_VAR, &idx); - - if (!vm) { - immPrintCallback(NULL, "Error: ", false); - immPrintCallback(NULL, err, true); - return false; - } - - BasVmResultE result = runSnippetVm(vm); - bool ok = (result == BAS_VM_HALTED || result == BAS_VM_OK) && idx >= 0 && idx < BAS_VM_MAX_GLOBALS; - - if (ok) { - *outVal = basValCopy(vm->globals[idx]); - } else { - immReportSnippetResult(vm, result); - } - - basVmDestroy(vm); - basModuleFree(mod); - return ok; -} - - // immPrintCallback -- append program output to the Immediate window. // The window has a fixed capacity, so the oldest lines scroll off the // top once output would take the room kept for typing (IDE_IMM_HEADROOM). @@ -4630,11 +3708,11 @@ static void immPrintCallback(void *ctx, const char *text, bool newline) { immBuf[len] = '\0'; if (text) { - len = appendDroppingLines(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, text, (int32_t)strlen(text)); + len = basDbgAppendDropping(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, text, (int32_t)strlen(text)); } if (newline) { - appendDroppingLines(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, "\n", 1); + basDbgAppendDropping(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, "\n", 1); } wgtSetText(sImmediate, immBuf); @@ -4644,130 +3722,6 @@ static void immPrintCallback(void *ctx, const char *text, bool newline) { } -// immReportSnippetResult -- report a failed Immediate/Watch snippet run -static void immReportSnippetResult(BasVmT *vm, BasVmResultE result) { - if (result == BAS_VM_HALTED || result == BAS_VM_OK || result == BAS_VM_BREAKPOINT) { - return; - } - - immPrintCallback(NULL, "Error: ", false); - immPrintCallback(NULL, result == BAS_VM_STEP_LIMIT ? "Statement did not finish; stopped." : basVmGetError(vm), true); -} - - -// immTryAssign -- handle "varName = expr" when paused at a breakpoint. -// Looks up the variable, evaluates the RHS expression, and writes the -// value back to the running VM. Returns true if handled as an assignment. - -static bool immTryAssign(const char *expr) { - if (sDbgState != DbgPausedE || !sVm || !sDbgModule) { - return false; - } - - // Skip leading whitespace - const char *p = dvxSkipWs(expr); - - // Skip optional LET keyword - if (kwMatch(p, KW_LET)) { - p = dvxSkipWs(p + KW_LEN(KW_LET)); - } - - // Resolve the LHS to a live slot in the VM - const char *afterLhs = NULL; - BasValueT *slot = resolveVarPath(p, &afterLhs); - - if (!slot || !afterLhs) { - return false; - } - - // Build display name from LHS - char lhsName[IDE_VALUE_BUF]; - int32_t lhsLen = (int32_t)(afterLhs - p); - - if (lhsLen >= (int32_t)sizeof(lhsName)) { - lhsLen = (int32_t)sizeof(lhsName) - 1; - } - - memcpy(lhsName, p, lhsLen); - lhsName[lhsLen] = '\0'; - - // Skip whitespace after LHS - p = dvxSkipWs(afterLhs); - - // Must have '=' (but not '==') - if (*p != '=' || p[1] == '=') { - return false; - } - - p = dvxSkipWs(p + 1); // skip '=' - - if (*p == '\0') { - return false; - } - - // Only scalars can be assigned; the RHS is evaluated as an expression - BasValueT newVal; - bool scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN || slot->type == BAS_TYPE_DATE || slot->type == BAS_TYPE_BYTE || slot->type == BAS_TYPE_CURRENCY; - - if (!scalar) { - immPrintCallback(NULL, "Cannot assign to this variable type", true); - return true; - } - - if (!immEvalRhs(p, slot->type, &newVal)) { - return true; - } - - // Write the value directly to the slot - basValRelease(slot); - *slot = newVal; // transfer ownership -- don't release newVal - - // Show confirmation - char confirm[IDE_VALUE_BUF]; - snprintf(confirm, sizeof(confirm), "%s = ", lhsName); - immPrintCallback(NULL, confirm, false); - formatValue(slot, confirm, sizeof(confirm)); - immPrintCallback(NULL, confirm, true); - - // Update debug windows to reflect the change - updateLocalsWindow(); - updateWatchWindow(); - return true; -} - - -// Hash-based keyword/type lookup using stb_ds. -// Key = uppercase word, value = TEXT_SYNTAX_KEYWORD or TEXT_SYNTAX_TYPE. -// Built once on first use, then O(1) per lookup. - -static void initSyntaxMap(void) { - if (sSyntaxMap) { - return; - } - - sh_new_arena(sSyntaxMap); - - // Pull the canonical keyword list from the lexer -- one source of truth. - int32_t n = basLexerKeywordCount(); - - for (int32_t i = 0; i < n; i++) { - const char *kw = basLexerKeywordAt(i); - - // Skip dollar-suffixed aliases (DIR$, CURDIR$, etc.) -- the - // unsuffixed form is already in the list and the colorizer - // strips the $ before lookup. - if (kw[strlen(kw) - 1] == '$') { - continue; - } - - BasKeywordClassE cls = basLexerKeywordClass(i); - uint8_t color = (cls == BAS_KW_CLASS_TYPE || cls == BAS_KW_CLASS_LITERAL) ? TEXT_SYNTAX_TYPE : TEXT_SYNTAX_KEYWORD; - - shput(sSyntaxMap, kw, basLexerKeywordReserved(i) ? color : (uint8_t)(color | IDE_SYNTAX_UNRESERVED)); - } -} - - static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) { (void)ctx; @@ -4781,19 +3735,6 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS } -// instanceTypeName -- what a class instance or Collection is, for the -// debugger: its class name, or "Collection" -static const char *instanceTypeName(const BasObjectT *obj) { - const char *cls = objectClassName(obj); - - if (cls) { - return cls; - } - - return obj->classIdx == BAS_OBJ_COLLECTION ? IDE_COLLECTION_TYPE : IDE_OBJECT_TYPE; -} - - // invalidateCompiled -- the compiled module and source map describe the // project's previous file list; drop them (a running program keeps the // module it executes). @@ -4826,45 +3767,8 @@ static bool isCtrlArrayInDesigner(const char *ctrlName) { } -// Check if position i is inside a string literal or comment (' or REM). -// Scans from the start of the line containing i. -static bool isInStringOrComment(const char *src, int32_t i) { - // Find start of line - int32_t lineStart = i; - - while (lineStart > 0 && src[lineStart - 1] != '\n') { - lineStart--; - } - - const char *line = src + lineStart; - int32_t lineLen = (int32_t)strcspn(line, "\n"); - bool inString = false; - - for (int32_t j = 0; j < i - lineStart; j++) { - if (line[j] == '"') { - inString = !inString; - } else if (!inString && (line[j] == '\'' || isRemAt(line, j, lineLen))) { - // Rest of line is a comment - return true; - } - } - - return inString; -} - - -// Internal mangled names (e.g. "DoCount$Count" for Static vars) have a $ -// in the middle; string variable names end with $ and are fine. -static bool isMangledDebugName(const char *name) { - const char *dollar = strchr(name, '$'); - - return dollar && dollar[1] != '\0'; -} - - // isRemAt -- true if the word REM (a comment to end of line) starts at -// line[pos]: a whole word in any case. Shared by the colorizer, the -// rename scanner and breakpoint validation. +// line[pos]: a whole word in any case (breakpoint validation). static bool isRemAt(const char *line, int32_t pos, int32_t lineLen) { int32_t end = pos + KW_LEN(KW_REM); @@ -5004,6 +3908,14 @@ static void listRowsAdd(IdeListRowsT *rows, const char *text) { } +// listRowsAddRow -- a BasDbgRowFnT: one row of cells into ctx (IdeListRowsT) +static void listRowsAddRow(void *ctx, const char *const *cells, int32_t cellCount) { + for (int32_t i = 0; i < cellCount; i++) { + listRowsAdd((IdeListRowsT *)ctx, cells[i]); + } +} + + static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount) { wgtListViewSetData(list, rowCount > 0 ? rows->cells : NULL, rowCount); } @@ -5034,8 +3946,9 @@ static void listRowsReset(IdeListRowsT *rows) { // the result is just its code section, *outCtrlList gets its controls for // the BEGINFORM line (basFrmControlList, malloc'd; NULL for other files) // and the same single parse adds them to validator (when non-NULL). -// Returns NULL when the file cannot be read. -static char *loadCompileSource(int32_t fileIdx, IdeValidatorCtxT *validator, char **outCtrlList) { +// Returns NULL (and no control list) when the file cannot be read or +// memory runs out. +static char *loadCompileSource(int32_t fileIdx, BasValidatorT *validator, char **outCtrlList) { bool missing = false; char *owned = NULL; const char *text = projectFileText(fileIdx, &owned, &missing); @@ -5078,6 +3991,12 @@ static char *loadCompileSource(int32_t fileIdx, IdeValidatorCtxT *validator, cha char *packed = packProcLayout(code); + // Out of memory: the caller frees nothing when the source is NULL + if (!packed) { + free(*outCtrlList); + *outCtrlList = NULL; + } + free(scan.code); free(owned); return packed; @@ -5128,6 +4047,7 @@ static void loadFile(void) { { "BASIC Files (*.bas)" }, { "Form Files (*.frm)" }, { "Class Modules (*.cls)" }, + { "UserControls (*.ctl)" }, { "All Files (*.*)" } }; @@ -5166,32 +4086,6 @@ static void loadFormCodeIntoEditor(void) { } -// loadFrmFiles -- register every .frm listed in the current project with -// the form runtime. The text is the same the compile used (loaded -// buffers, disk only for never-loaded files). Registration only caches -// the text; the caller's basFormRtLoadAllForms then instantiates them, -// so a Form_Load that does "Load Form2" finds Form2's .frm regardless of -// project file order. -static void loadFrmFiles(BasFormRtT *rt) { - for (int32_t i = 0; i < prjFileCount(&sProject); i++) { - if (sProject.files[i].kind != PrjKindFormE) { - continue; - } - - bool missing = false; - char *owned = NULL; - const char *text = projectFileText(i, &owned, &missing); - char formName[BAS_MAX_IDENT]; - - if (text && basExtractFormName(text, formName, sizeof(formName))) { - basFormRtRegisterFrm(rt, formName, text, (int32_t)strlen(text)); - } - - free(owned); - } -} - - // loadModuleIntoEditor -- bind the editor to .bas file fileIdx holding source static void loadModuleIntoEditor(int32_t fileIdx, const char *source) { parseProcs(source); @@ -5201,6 +4095,37 @@ static void loadModuleIntoEditor(int32_t fileIdx, const char *source) { } +// loadRunFiles -- register the project's UserControls and forms with the +// form runtime of a run, as the built program does: each .ctl's header +// (its control class, before any form can use it) and each .frm's text. +// The text is the same the compile used (loaded buffers, disk only for +// never-loaded files). A form's registration only caches the text; the +// caller's basRunHostLoadForms then instantiates them, so a Load handler +// that does "Load Form2" finds Form2's .frm regardless of project file +// order. +static void loadRunFiles(BasFormRtT *rt) { + for (int32_t i = 0; i < prjFileCount(&sProject); i++) { + PrjFileT *file = &sProject.files[i]; + + if (file->kind != PrjKindFormE && file->kind != PrjKindControlE) { + continue; + } + + bool missing = false; + char *owned = NULL; + const char *text = projectFileText(i, &owned, &missing); + + if (text && file->kind == PrjKindControlE) { + basFormRtRegisterCtl(rt, text, (int32_t)strlen(text)); + } else if (text) { + basRunHostRegisterFrm(rt, text, (int32_t)strlen(text)); + } + + free(owned); + } +} + + static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText) { int32_t iconW = 0; int32_t iconH = 0; @@ -5244,22 +4169,9 @@ static void logFlushLine(void) { } -// lookupWatchVar -- evaluate a simple watch expression by reading the -// paused VM directly. Supports varName, varName(idx, ...), varName.field -// and varName(idx).field; anything else falls back to evalWatchExpr. -static bool lookupWatchVar(const char *expr, BasValueT *outVal) { - const char *end = NULL; - BasValueT *slot = resolveVarPath(dvxSkipWs(expr), &end); - - if (!slot || !end || *dvxSkipWs(end) != '\0') { - return false; - } - - *outVal = *slot; - return true; -} - - +// makeExecutable -- File > Make Executable: the program (.app), or for a +// control project (File > Make Control) its control file (.wgt, named +// after the project), through the build code bascomp uses static void makeExecutable(void) { if (!hasProject()) { setStatus("Save the project first."); @@ -5272,14 +4184,23 @@ static void makeExecutable(void) { } // Ask for output path - FileFilterT filters[] = { + bool control = prjIsControlProject(&sProject); + FileFilterT appFilters[] = { { "DVX Applications (*.app)" }, { "All Files (*.*)" } }; + FileFilterT ctlFilters[] = { + { "Control Files (*.wgt)" }, + { "All Files (*.*)" } + }; char outPath[DVX_MAX_PATH]; outPath[0] = '\0'; - if (!dvxFileDialog(sAc, "Make Executable", FD_SAVE, NULL, filters, DVX_ARRAY_LEN(filters), outPath, sizeof(outPath))) { + if (control) { + snprintf(outPath, sizeof(outPath), "%s" BAS_WGT_EXT, sProject.name); + } + + if (!dvxFileDialog(sAc, control ? "Make Control" : "Make Executable", FD_SAVE, NULL, control ? ctlFilters : appFilters, control ? DVX_ARRAY_LEN(ctlFilters) : DVX_ARRAY_LEN(appFilters), outPath, sizeof(outPath))) { return; } @@ -5296,7 +4217,7 @@ static void makeExecutable(void) { bool release = (modeChoice == 1); - setStatus(release ? "Building release executable..." : "Building debug executable..."); + setStatus(control ? "Building control file..." : (release ? "Building release executable..." : "Building debug executable...")); dvxSetBusy(sAc, true); dvxUpdate(sAc); @@ -5320,28 +4241,29 @@ static void makeExecutable(void) { BasBuildSpecT spec; memset(&spec, 0, sizeof(spec)); - spec.projName = sProject.name; - spec.author = sProject.author; - spec.publisher = sProject.publisher; - spec.version = sProject.version; - spec.copyright = sProject.copyright; - spec.description = sProject.description; - spec.projectDir = sProject.projectDir; - spec.iconPath = sProject.iconPath; - spec.helpFile = sProject.helpFile; - spec.selfPath = sCtx->appPath; - spec.module = sCachedModule; - spec.frmSources = (const char *const *)frmSources; - spec.frmCount = (int32_t)arrlen(frmSources); - spec.release = release; + spec.projName = sProject.name; + spec.author = sProject.author; + spec.publisher = sProject.publisher; + spec.version = sProject.version; + spec.copyright = sProject.copyright; + spec.description = sProject.description; + spec.projectDir = sProject.projectDir; + spec.iconPath = sProject.iconPath; + spec.helpFile = sProject.helpFile; + char **ctlSources = ctlHeaders(); - const char *failure = basBuildApp(outPath, &spec); + spec.module = sCachedModule; + spec.frmSources = (const char *const *)frmSources; + spec.frmCount = (int32_t)arrlen(frmSources); + spec.ctlSources = (const char *const *)ctlSources; + spec.ctlCount = (int32_t)arrlen(ctlSources); + spec.release = release; + spec.multiInstance = sProject.multiInstance; - for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) { - free(frmSources[i]); - } + const char *failure = control ? basBuildControl(outPath, &spec) : basBuildApp(outPath, &spec); - arrfree(frmSources); + freeTexts(frmSources); + freeTexts(ctlSources); dvxSetBusy(sAc, false); if (failure) { @@ -5355,31 +4277,6 @@ static void makeExecutable(void) { } -// memberVarSlot -- member variable dv (SCOPE_MEMBER) of the object the -// innermost procedure runs for, or NULL when that procedure is not a -// procedure of dv's class -static BasValueT *memberVarSlot(const BasDebugVarT *dv) { - if (sVm->callDepth <= 0) { - return NULL; - } - - const BasValueT *me = &sVm->callStack[sVm->callDepth - 1].me; - - if (me->type != BAS_TYPE_INSTANCE) { - return NULL; - } - - BasObjectT *obj = me->instVal; - const char *cls = objectClassName(obj); - - if (!cls || strcasecmp(cls, dv->formName) != 0 || dv->index < 0 || dv->index >= obj->fieldCount) { - return NULL; - } - - return &obj->fields[dv->index]; -} - - static void navigateToBreakpoint(int32_t bpIdx) { if (bpIdx < 0 || bpIdx >= (int32_t)arrlen(sBreakpoints)) { return; @@ -5446,7 +4343,7 @@ static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbg } wgtTextAreaGoToLine(sEditor, editorLine); - wgtInvalidatePaint(sEditor); + refreshEditorMarkers(); return sCurProcIdx; } @@ -5464,12 +4361,12 @@ static void navigateToEventSub(void) { const char *eventName = NULL; if (sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) { - DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; + FrmCtrlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; ctrlName = ctrl->name; - eventName = dsgnDefaultEvent(ctrl->typeName); + eventName = basCtlDefaultEvent(ctrl->typeName); } else { ctrlName = sDesigner.form->name; - eventName = dsgnDefaultEvent(IDE_FORM_TYPE_NAME); + eventName = basCtlDefaultEvent(IDE_FORM_TYPE_NAME); } navigateToNamedEventSub(ctrlName, eventName); @@ -5524,17 +4421,20 @@ static void navigateToNamedEventSub(const char *ctrlName, const char *eventName) } -// newClassModule -- add class module ClassN (the first N whose name no -// form or class of the project uses and whose file does not exist yet), -// holding the standard header, to the project and open it in the editor. -static void newClassModule(void) { - char name[BAS_MAX_IDENT]; - char fileName[DVX_MAX_PATH]; - char path[DVX_MAX_PATH * 2]; +// newClassFile -- add class module ClassN or UserControl UserControlN +// (kind; the first N whose name no form, class or control of the project +// uses and whose file does not exist yet), holding the standard header +// (a UserControl's: prjCtlNew), to the project and open it in the editor. +static void newClassFile(PrjFileKindE kind) { + bool control = (kind == PrjKindControlE); + const char *ext = control ? BAS_CTL_EXT : BAS_CLS_EXT; + char name[BAS_MAX_IDENT]; + char fileName[DVX_MAX_PATH]; + char path[DVX_MAX_PATH * 2]; for (int32_t n = 1; ; n++) { - snprintf(name, sizeof(name), IDE_NEW_CLASS_FMT, (int)n); - snprintf(fileName, sizeof(fileName), "%s%s", name, BAS_CLS_EXT); + snprintf(name, sizeof(name), control ? IDE_NEW_CTL_FMT : IDE_NEW_CLASS_FMT, (int)n); + snprintf(fileName, sizeof(fileName), "%s%s", name, ext); snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", sProject.projectDir, fileName); if (!classNameUsed(name, fileName) && !prjPathExists(path)) { @@ -5542,17 +4442,23 @@ static void newClassModule(void) { } } - char text[sizeof(IDE_NEW_CLASS_TEXT) + BAS_MAX_IDENT]; + char classText[sizeof(IDE_NEW_CLASS_TEXT) + BAS_MAX_IDENT]; + char *text = NULL; - snprintf(text, sizeof(text), IDE_NEW_CLASS_TEXT, name); + if (control) { + text = prjCtlNew(name); + } else { + snprintf(classText, sizeof(classText), IDE_NEW_CLASS_TEXT, name); + text = strdup(classText); + } bool wasDirty = sProject.dirty; - int32_t idx = prjAddFile(&sProject, fileName, PrjKindClassE); + int32_t idx = prjAddFile(&sProject, fileName, kind); - sProject.files[idx].buffer = strdup(text); + sProject.files[idx].buffer = text; sProject.files[idx].modified = true; - if (!sProject.files[idx].buffer || !activateFile(idx, ViewCodeE)) { + if (!text || !activateFile(idx, ViewCodeE)) { prjRemoveFile(&sProject, idx); sProject.dirty = wasDirty; return; @@ -5561,7 +4467,11 @@ static void newClassModule(void) { invalidateCompiled(); prjRebuildTree(&sProject); updateDirtyIndicators(); - setStatus("Class module added."); + setStatus(control ? "UserControl added." : "Class module added."); + + if (control) { + designCtlRefresh(); + } } @@ -5586,7 +4496,7 @@ static void newProject(void) { }; char dbpPath[DVX_MAX_PATH]; - snprintf(dbpPath, sizeof(dbpPath), "%s%s", name, IDE_DBP_EXT); + snprintf(dbpPath, sizeof(dbpPath), "%s%s", name, BAS_DBP_EXT); if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) { return; @@ -5606,7 +4516,8 @@ static void newProject(void) { dir[1] = '\0'; } - prjNew(&sProject, name, dir, sPrefs); + prjNew(&sProject, name, dir); + prjApplyDefaults(&sProject, sPrefs); snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath); saveProjectFile(); @@ -5617,12 +4528,36 @@ static void newProject(void) { } -// objectClassName -- the class module name of class instance obj in the -// running program, or NULL (a Collection) -static const char *objectClassName(const BasObjectT *obj) { - const BasModuleT *mod = sVm->module; +// objectType -- the designer type of an Object dropdown entry: the form +// (IDE_FORM_TYPE_NAME), a menu item (FRM_KW_MENU), a control's type, in a +// .ctl the UserControl object (BAS_UC_OBJECT), else "" (a WithEvents +// variable, or a name that is none of these) +static const char *objectType(const char *objName) { + if (editorIsControl()) { + return strcasecmp(objName, BAS_UC_OBJECT) == 0 ? BAS_UC_OBJECT : ""; + } - return (mod && obj->classIdx >= 0 && obj->classIdx < mod->classCount) ? mod->classes[obj->classIdx].name : NULL; + if (!sDesigner.form) { + return ""; + } + + if (strcasecmp(objName, sDesigner.form->name) == 0) { + return IDE_FORM_TYPE_NAME; + } + + for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { + if (strcasecmp(sDesigner.form->menuItems[i].name, objName) == 0) { + return FRM_KW_MENU; + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { + if (strcasecmp(sDesigner.form->controls[i]->name, objName) == 0) { + return sDesigner.form->controls[i]->typeName; + } + } + + return ""; } @@ -5642,7 +4577,7 @@ static void onBreakpointHit(void *ctx, int32_t line) { sDbgState = DbgPausedE; if (sVm) { - sVm->debugPaused = true; + basVmSetDebugPaused(sVm, true); } if (line > 0) { @@ -5650,7 +4585,7 @@ static void onBreakpointHit(void *ctx, int32_t line) { debugNavigateToLine(line); } - debugSetBreakTitles(true); + basRunHostTagBreak(sDbgFormRt, &sBreakTitles, true); debugUpdateWindows(); updateProjectMenuState(); setStatus("Paused."); @@ -5691,11 +4626,6 @@ static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) syncVmBreakpoints(); updateBreakpointWindow(); - - // Repaint editor to remove breakpoint dots - if (sEditor) { - wgtInvalidatePaint(sEditor); - } } @@ -5734,6 +4664,7 @@ static void onClose(WindowT *win) { // then whatever can exist without one (toolbox/properties). closeProject(); closeProjectUi(); + basUcSetPropChangedHook(NULL); if (sOutWin) { dvxDestroyWindow(sAc, sOutWin); @@ -5826,9 +4757,6 @@ static void onClose(WindowT *win) { free(sEditorPrevText); sEditorPrevText = NULL; - shfree(sSyntaxMap); - sSyntaxMap = NULL; - prefsClose(sPrefs); sPrefs = NULL; @@ -5891,6 +4819,69 @@ static void onContentFocus(WindowT *win) { } +// onCtlHeaderSet -- the Properties window changed header key key of the +// active UserControl: its text rewritten (prjCtlHeaderSet: that line, or +// the name on the Begin line; the code untouched) as an unsaved edit, the +// editor and the file's breakpoints following (a line added or removed +// moves every code line), the designer's controls refreshed. A new name +// must be free in the project. +static void onCtlHeaderSet(const char *key, const char *value) { + int32_t idx = sProject.activeFileIdx; + + if (idx < 0 || idx >= prjFileCount(&sProject) || sProject.files[idx].kind != PrjKindControlE) { + return; + } + + if (idx == sEditorFileIdx) { + stashEditor(); + } + + bool missing = false; + char *owned = NULL; + const char *text = projectFileText(idx, &owned, &missing); + char oldName[BAS_MAX_IDENT]; + bool rename = strcasecmp(key, BAS_PROP_NAME) == 0 && text && prjClassName(&sProject, idx, text, oldName, (int32_t)sizeof(oldName)) && strcasecmp(oldName, value) != 0; + char *changed = (text && !(rename && classNameUsed(value, ""))) ? prjCtlHeaderSet(text, key, value) : NULL; + + if (!changed) { + char msg[IDE_MSG_BUF]; + + snprintf(msg, sizeof(msg), rename ? "The project already has a form, class module or UserControl named %s, or it is not a valid name." : "%s cannot be set to that value.", rename ? value : key); + ideErrorBox(msg); + free(owned); + return; + } + + int32_t delta = countLines(changed) - countLines(text); + PrjFileT *file = &sProject.files[idx]; + + free(owned); + free(file->buffer); + file->buffer = changed; + file->modified = true; + + for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { + if (sBreakpoints[i].fileIdx == idx) { + sBreakpoints[i].codeLine += delta; + } + } + + if (delta != 0) { + syncVmBreakpoints(); + updateBreakpointWindow(); + } + + if (idx == sEditorFileIdx) { + loadModuleIntoEditor(idx, changed); + } + + invalidateCompiled(); + updateDirtyIndicators(); + designCtlRefresh(); + propsFollowActive(); +} + + static void onEditorChange(WidgetT *w) { (void)w; @@ -6042,49 +5033,13 @@ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { return 0; } - // Crosshair when placing a new control + // Crosshair when placing a new control, else the resize cursors over + // the selection's handles if (sDesigner.activeTool[0] != '\0') { return CURSOR_CROSSHAIR; } - if (!sDesigner.form->controls) { - return 0; - } - - int32_t count = (int32_t)arrlen(sDesigner.form->controls); - - if (sDesigner.selectedIdx < 0 || sDesigner.selectedIdx >= count) { - return 0; - } - - DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; - - if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { - return 0; - } - - int32_t cx = ctrl->widget->x; - int32_t cy = ctrl->widget->y; - int32_t cw = ctrl->widget->w; - int32_t ch = ctrl->widget->h; - int32_t hs = DSGN_HANDLE_SIZE; - - // SE handle (check first -- overlaps with E and S) - if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) { - return CURSOR_RESIZE_DIAG_NWSE; - } - - // E handle (right edge center) - if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch/2 - hs/2 && y < cy + ch/2 + hs/2) { - return CURSOR_RESIZE_H; - } - - // S handle (bottom center) - if (x >= cx + cw/2 - hs/2 && x < cx + cw/2 + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) { - return CURSOR_RESIZE_V; - } - - return 0; + return basDsgnCursorAt(dsgnSelectedWidget(&sDesigner), x, y); } @@ -6394,71 +5349,34 @@ static void onObjDropdownChange(WidgetT *w) { return; } - // Check if this is a form name - bool isForm = false; + // What the object is: the form, a menu item (only event is Click), a + // control, the UserControl object of a .ctl + const char *objType = objectType(selObj); + bool ucObject = strcmp(objType, BAS_UC_OBJECT) == 0; - if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) { - isForm = true; - } - - // Check if this is a menu item (only event is Click) - bool isMenuItem = false; - - if (!isForm && sDesigner.form) { - for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { - if (strcasecmp(sDesigner.form->menuItems[i].name, selObj) == 0) { - isMenuItem = true; - break; - } - } - } - - // The control's type and widget interface (its own events) - const WgtIfaceT *iface = NULL; - const char *ctrlType = NULL; - - if (!isForm && !isMenuItem && sDesigner.form) { - for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { - if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) { - const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i]->typeName); - - ctrlType = sDesigner.form->controls[i]->typeName; - - if (wgtName) { - iface = wgtGetIface(wgtName); - } - - break; - } - } - } - - // Standard events for this kind of object, from the master table - // (NULL-terminated stb_ds temp array); a WithEvents variable's are its - // class's Events + // The events of this kind of object (the master table's, then the + // widget's own: basCtlEventAt, as designer.bas lists them), a + // WithEvents variable's class's Events, or the UserControl events the + // compiler knows (NULL-terminated stb_ds temp array) const char **availEvents = NULL; if (withEvents) { for (int32_t i = 0; i < (int32_t)arrlen(classEvents); i++) { arrput(availEvents, classEvents[i].name); } - } else if (isMenuItem) { - arrput(availEvents, IDE_MENU_EVENT); + } else if (ucObject) { + for (int32_t i = 0; basUcEventInfo(i); i++) { + arrput(availEvents, basUcEventInfo(i)->name); + } } else { - bool draws = (ctrlType != NULL && strcasecmp(ctrlType, BAS_PICTUREBOX_TYPE) == 0); - uint8_t scope = isForm ? BAS_EVT_SCOPE_FORM : (BAS_EVT_SCOPE_CTRL | (draws ? BAS_EVT_SCOPE_DRAW : 0)); - - for (int32_t i = 0; i < IDE_EVENT_COUNT; i++) { - if (sEventTable[i].scope & scope) { - arrput(availEvents, sEventTable[i].suffix); - } + for (int32_t i = 0; basCtlEventAt(objType, i); i++) { + arrput(availEvents, basCtlEventAt(objType, i)); } } arrput(availEvents, NULL); - // Build the event list: standard events + widget-specific events - // Add standard events (common or form) + // Bracketed: no handler written yet for (int32_t i = 0; availEvents[i]; i++) { bool hasCode = false; @@ -6472,38 +5390,6 @@ static void onObjDropdownChange(WidgetT *w) { evtItemAdd(availEvents[i], !hasCode); } - // Add widget-specific events - if (iface) { - for (int32_t i = 0; i < iface->eventCount; i++) { - const char *evtName = iface->events[i].name; - - // Skip if already in the standard list - bool alreadyListed = false; - - for (int32_t j = 0; availEvents[j]; j++) { - if (strcasecmp(availEvents[j], evtName) == 0) { - alreadyListed = true; - break; - } - } - - if (alreadyListed) { - continue; - } - - bool hasCode = false; - - for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) { - if (strcasecmp(existingEvts[j], evtName) == 0) { - hasCode = true; - break; - } - } - - evtItemAdd(evtName, !hasCode); - } - } - // Add any existing events not in the standard/widget list (custom subs) for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) { bool alreadyListed = false; @@ -6561,18 +5447,13 @@ static void onObjDropdownChange(WidgetT *w) { onEvtDropdownChange(sEvtDropdown); } else { // No implemented events -- find and select the default event - // for this widget type, which will create its skeleton + // for this kind of object, which will create its skeleton const char *defEvt = withEventsDefault; - if (isForm) { - defEvt = dsgnDefaultEvent(IDE_FORM_TYPE_NAME); - } else if (!withEvents && sDesigner.form) { - for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { - if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) { - defEvt = dsgnDefaultEvent(sDesigner.form->controls[i]->typeName); - break; - } - } + if (ucObject) { + defEvt = IDE_UC_DEFAULT_EVENT; + } else if (!withEvents) { + defEvt = basCtlDefaultEvent(objType); } if (!defEvt) { @@ -6815,6 +5696,18 @@ static void onTbStop(WidgetT *w) { } +// onUcPropChanged -- PropertyChanged in the code of a UserControl on the +// designer: the properties window reads the values again +static void onUcPropChanged(WidgetT *w, const char *propName) { + (void)w; + (void)propName; + + if (sPropsWin && sDesigner.form) { + prpRefresh(&sDesigner); + } +} + + static void onWatchClose(WindowT *win) { dvxHideWindow(sAc, win); } @@ -6894,7 +5787,7 @@ static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { // openAnyPath -- open a project (.dbp) in place of the current one, or // a .bas/.frm (see openSinglePath) static void openAnyPath(const char *path) { - if (dvxHasExt(platformPathBaseName(path), IDE_DBP_EXT)) { + if (dvxHasExt(platformPathBaseName(path), BAS_DBP_EXT)) { if (promptAndSave()) { openProjectPath(path); } @@ -6989,6 +5882,7 @@ static void openProjectPath(const char *path) { ensureProjectWindow(false); updateMainTitle(); setStatus("Project loaded."); + designCtlRefresh(); updateProjectMenuState(); recentAdd(path); } @@ -7019,7 +5913,7 @@ static void openSinglePath(const char *path) { // outputAppend -- add program/report text to the Output buffer, dropping // the oldest lines when it is full so the newest text always shows static void outputAppend(const char *text, int32_t len) { - sOutputLen = appendDroppingLines(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, text, len); + sOutputLen = basDbgAppendDropping(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, text, len); sOutputPending = true; } @@ -7055,47 +5949,6 @@ static char *packProcLayout(const char *source) { } -// parseIndexList -- parse "i1, i2, ...)" starting just after the '('. -// Returns the position after the closing ')' (or where parsing stopped). -static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount) { - int32_t n = 0; - - while (*p && *p != ')' && n < BAS_ARRAY_MAX_DIMS) { - const char *start = p; - - p = dvxSkipWs(p); - indices[n++] = atoi(p); - - // Skip past the number - if (*p == '-') { - p++; - } - - while (*p >= '0' && *p <= '9') { - p++; - } - - p = dvxSkipWs(p); - - if (*p == ',') { - p++; - } - - // Anything unparseable would otherwise spin forever - if (p == start) { - break; - } - } - - if (*p == ')') { - p++; - } - - *outCount = n; - return p; -} - - // parseProcs -- split source into (General) + per-procedure buffers static void parseProcs(const char *source) { @@ -7108,39 +5961,6 @@ static void parseProcs(const char *source) { } -// pasteParseControl -- parse one "Begin ... End" block with -// the designer's .frm loader by wrapping it in a scratch form. Returns -// the loaded control record (the caller owns it), or NULL. -static DsgnControlT *pasteParseControl(const char *clip, int32_t clipLen) { - static const char head[] = FRM_BEGIN_TAG IDE_FORM_TYPE_NAME " " IDE_PASTE_FORM "\n"; - static const char tail[] = "\n" FRM_END_TAG "\n"; - int32_t len = (int32_t)(sizeof(head) - 1) + clipLen + (int32_t)(sizeof(tail) - 1); - char *text = (char *)malloc((size_t)len + 1); - - if (!text) { - return NULL; - } - - memcpy(text, head, sizeof(head) - 1); - memcpy(text + sizeof(head) - 1, clip, (size_t)clipLen); - memcpy(text + sizeof(head) - 1 + clipLen, tail, sizeof(tail)); - - DsgnStateT tmp; - DsgnControlT *ctrl = NULL; - - memset(&tmp, 0, sizeof(tmp)); - - if (dsgnLoadFrm(&tmp, text, len) && tmp.form && arrlen(tmp.form->controls) > 0) { - ctrl = tmp.form->controls[0]; - arrdel(tmp.form->controls, 0); - } - - dsgnFree(&tmp); - free(text); - return ctrl; -} - - // pathExists -- true when a file can be opened for reading // prefsBuild -- build the Preferences dialog's widgets into win and load // the current settings. Returns false if any widget could not be made. @@ -7368,8 +6188,8 @@ static void printCallback(void *ctx, const char *text, bool newline) { static bool procBodyEmpty(const char *procText) { bool isSub = false; const char *p = dvxSkipWs(procText); - const char *afterKw = procDeclAt(p, &isSub); - const char *endTag = (afterKw && !isSub) ? KW_END_FUNCTION : KW_END_SUB; + const char *afterKw = basProcDeclAt(p, &isSub); + const char *endTag = (afterKw && !isSub) ? BAS_PROC_KW_END_FUNCTION : BAS_PROC_KW_END_SUB; for (p = skipLine(p); *p && !kwMatch(dvxSkipWs(p), endTag); p = skipLine(p)) { const char *q = dvxSkipWs(p); @@ -7397,30 +6217,6 @@ static bool procBufContains(const char *hay, const char *needle, int32_t needleL } -// procDeclAt -- if p (already whitespace-skipped) starts a SUB or -// FUNCTION declaration (Public or Private in front allowed), returns the -// text after the keyword; else NULL. -static const char *procDeclAt(const char *p, bool *outIsSub) { - if (kwMatch(p, KW_PUBLIC)) { - p = dvxSkipWs(p + KW_LEN(KW_PUBLIC)); - } else if (kwMatch(p, KW_PRIVATE)) { - p = dvxSkipWs(p + KW_LEN(KW_PRIVATE)); - } - - if (kwMatch(p, KW_SUB)) { - *outIsSub = true; - return p + KW_LEN(KW_SUB); - } - - if (kwMatch(p, KW_FUNCTION)) { - *outIsSub = false; - return p + KW_LEN(KW_FUNCTION); - } - - return NULL; -} - - // procIndexByName -- sProcTable index of the proc objName/evtName, or -1 static int32_t procIndexByName(const char *objName, const char *evtName) { for (int32_t i = 0; i < (int32_t)arrlen(sProcTable); i++) { @@ -7446,26 +6242,12 @@ static int32_t procIndexForCodeLine(int32_t codeLine) { } -// procNameFromDecl -- copy the procedure name that follows a SUB/FUNCTION -// keyword (stops at '(' or whitespace). -static void procNameFromDecl(const char *afterKw, char *out, int32_t outSize) { - const char *p = dvxSkipWs(afterKw); - int32_t n = 0; - - while (*p && *p != '(' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && n < outSize - 1) { - out[n++] = *p++; - } - - out[n] = '\0'; -} - - // procNameOf -- the name a proc's text declares (its first line) static void procNameOf(const char *procText, char *out, int32_t outSize) { bool isSub = false; - const char *afterKw = procDeclAt(dvxSkipWs(procText), &isSub); + const char *afterKw = basProcDeclAt(dvxSkipWs(procText), &isSub); - procNameFromDecl(afterKw ? afterKw : procText, out, outSize); + basProcNameFromDecl(afterKw ? afterKw : procText, out, outSize); } @@ -7525,6 +6307,30 @@ static bool promptAndSave(void) { } +// propsFollowActive -- the Properties window shows the header of the +// active file when it is a UserControl (its newest text: the editor's +// when it holds the file), else the designer's selection +static void propsFollowActive(void) { + int32_t idx = sProject.activeFileIdx; + + if (idx < 0 || idx >= prjFileCount(&sProject) || sProject.files[idx].kind != PrjKindControlE) { + prpShowCtl(NULL, NULL); + return; + } + + if (idx == sEditorFileIdx) { + stashEditor(); + } + + bool missing = false; + char *owned = NULL; + const char *text = projectFileText(idx, &owned, &missing); + + prpShowCtl(text, onCtlHeaderSet); + free(owned); +} + + // rebuildProcTable -- one sProcTable entry per proc buffer (so table and // buffer indices always agree) with its object/event split and its line // range in the canonical layout. Procedure names are split on '_' into @@ -7534,10 +6340,13 @@ static bool promptAndSave(void) { static void rebuildProcTable(void) { arrsetlen(sProcTable, 0); - // Collect all known object names once; they only depend on sDesigner.form + // Collect all known object names once: the designer form's, or in a + // .ctl the UserControl object const char **objNames = NULL; // stb_ds temp array - if (sDesigner.form) { + if (editorIsControl()) { + arrput(objNames, BAS_UC_OBJECT); + } else if (sDesigner.form) { arrput(objNames, sDesigner.form->name); for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls); ci++) { @@ -7559,8 +6368,8 @@ static void rebuildProcTable(void) { procNameOf(text, procName, sizeof(procName)); - if (procDeclAt(dvxSkipWs(text), &isSub)) { - skipToEndProc(text, isSub, &endOffset); + if (basProcDeclAt(dvxSkipWs(text), &isSub)) { + basProcSkipToEnd(text, isSub, &endOffset); } IdeProcEntryT entry; @@ -7699,7 +6508,7 @@ static void recentRebuildMenu(void) { const char *name = platformPathBaseName(sRecentFiles[i]); char label[MAX_MENU_LABEL]; - snprintf(label, sizeof(label), "&%ld %s", (long)(i + 1), name); + snprintf(label, sizeof(label), "&%ld %s", (long)i + 1, name); wmAddMenuItem(sFileMenu, label, CMD_RECENT_BASE + i); } } @@ -7736,6 +6545,35 @@ static void recentSave(void) { } +// refreshEditorMarkers -- the code editor's line markers: a gutter dot on +// each breakpoint of the shown section, the paused line's background. +// Called whenever the breakpoints, the paused line or the shown section +// change (updateBreakpointWindow, editorSetText, the debugger). +static void refreshEditorMarkers(void) { + if (!sEditor) { + return; + } + + int32_t base = editorLineToCodeLine(1) - 1; // code line before editor line 1 + bool pausedHasBp = false; + + wgtTextAreaClearLineMarkers(sEditor); + + for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { + int32_t edLine = sBreakpoints[i].codeLine - base; + + if (sBreakpoints[i].fileIdx == sEditorFileIdx && edLine >= 1) { + wgtTextAreaSetLineMarker(sEditor, edLine, IDE_BP_GUTTER_RGB, TEXTAREA_MARKER_NONE); + pausedHasBp = pausedHasBp || edLine == sDbgCurrentLine; + } + } + + if (sDbgState == DbgPausedE && sDbgCurrentLine > 0) { + wgtTextAreaSetLineMarker(sEditor, sDbgCurrentLine, pausedHasBp ? IDE_BP_GUTTER_RGB : TEXTAREA_MARKER_NONE, IDE_DBG_LINE_RGB); + } +} + + // remapBreakpoints -- move the editor file's breakpoints to their anchors' // lines in the current layout (starts: procStartLines of the current // buffers; anchors parallel sBreakpoints). A breakpoint whose line is @@ -7789,85 +6627,9 @@ static void removeBreakpointsForFile(int32_t fileIdx) { } -// renameInBuffer -- word-boundary rename of OldName followed by '.' or -// '_'. The character before the match must be a non-identifier -// character to avoid replacing "Command1" inside "MyCommand1". Returns -// the new text (malloc'd), or NULL when nothing matched. -static char *renameInBuffer(const char *src, const char *oldName, const char *newName) { - if (!src || !oldName || !newName || !oldName[0]) { - return NULL; - } - - int32_t oldLen = (int32_t)strlen(oldName); - int32_t newLen = (int32_t)strlen(newName); - int32_t srcLen = (int32_t)strlen(src); - bool skipComments = prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true); - - // First pass: count replacements to compute output size - int32_t count = 0; - - for (int32_t i = 0; i <= srcLen - oldLen; i++) { - if (renameMatchAt(src, i, oldName, oldLen, skipComments)) { - count++; - } - } - - if (count == 0) { - return NULL; - } - - // Allocate output - int32_t outLen = srcLen + count * (newLen - oldLen); - char *out = (char *)malloc(outLen + 1); - - if (!out) { - return NULL; - } - - // Second pass: build output - int32_t op = 0; - - for (int32_t i = 0; i < srcLen; ) { - if (i <= srcLen - oldLen && renameMatchAt(src, i, oldName, oldLen, skipComments)) { - memcpy(out + op, newName, newLen); - op += newLen; - i += oldLen; - continue; - } - - out[op++] = src[i++]; - } - - out[op] = '\0'; - return out; -} - - -// renameMatchAt -- whether oldName at src[i] is a reference to rename: a -// whole identifier start followed by '.' or '_', outside comments and -// strings when skipComments. -static bool renameMatchAt(const char *src, int32_t i, const char *oldName, int32_t oldLen, bool skipComments) { - if (strncasecmp(&src[i], oldName, oldLen) != 0) { - return false; - } - - char after = src[i + oldLen]; - - if (after != '.' && after != '_') { - return false; - } - - if (i > 0 && basIsIdentChar(src[i - 1])) { - return false; - } - - return !skipComments || !isInStringOrComment(src, i); -} - - // renameMenuItemsInCode -- after the Menu Editor, rename the handlers of // menu items whose name changed. Items are matched by the identity the -// editor keeps (DsgnMenuItemT.id, the item's position + 1 on entry), so +// editor keeps (FrmMenuItemT.id, the item's position + 1 on entry), so // inserting, deleting and moving items never looks like a rename. All // old names go to unique placeholders first, so swapped names cannot // collapse two handlers into one. @@ -7877,10 +6639,10 @@ static void renameMenuItemsInCode(char **oldNames, int32_t oldCount) { int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems); for (int32_t mi = 0; mi < newCount; mi++) { - DsgnMenuItemT *item = &sDesigner.form->menuItems[mi]; + FrmMenuItemT *item = &sDesigner.form->menuItems[mi]; int32_t was = item->id - 1; - if (item->id == DSGN_NEW_MENU_ID || was < 0 || was >= oldCount || !oldNames[was] || !oldNames[was][0] || !item->name[0]) { + if (item->id == FRM_NEW_MENU_ID || was < 0 || was >= oldCount || !oldNames[was] || !oldNames[was][0] || !item->name[0]) { continue; } @@ -7930,9 +6692,9 @@ static void renameProjectFile(int32_t fileIdx) { // renameProjectFileTo -- rename project file fileIdx to newName on disk // and in the project (prjRenameFile, which puts everything back on -// failure; errors are shown). A class module named after its file -// (classFollowsFile) takes the new name as its class name; that name must -// then be free. A form keeps its Name. +// failure; errors are shown). A class module or UserControl named after +// its file (classFollowsFile) takes the new name as its class name; that +// name must then be free. A form keeps its Name. static bool renameProjectFileTo(int32_t fileIdx, const char *newName) { char err[IDE_MSG_BUF]; @@ -7946,7 +6708,7 @@ static bool renameProjectFileTo(int32_t fileIdx, const char *newName) { char oldClass[BAS_MAX_IDENT]; char newClass[BAS_MAX_IDENT]; - if (file->kind == PrjKindClassE) { + if (file->kind == PrjKindClassE || file->kind == PrjKindControlE) { bool missing = false; char *owned = NULL; const char *text = projectFileText(fileIdx, &owned, &missing); @@ -7957,13 +6719,13 @@ static bool renameProjectFileTo(int32_t fileIdx, const char *newName) { } if (nameClass && !basIsValidIdent(newClass)) { - snprintf(err, sizeof(err), "%s is not a valid class name: the class module is named after its file.", newClass); + snprintf(err, sizeof(err), "%s is not a valid class name: the class module or UserControl is named after its file.", newClass); ideErrorBox(err); return false; } if (nameClass && strcasecmp(newClass, oldClass) != 0 && classNameUsed(newClass, "")) { - snprintf(err, sizeof(err), "The project already has a form or class module named %s.", newClass); + snprintf(err, sizeof(err), "The project already has a form, class module or UserControl named %s.", newClass); ideErrorBox(err); return false; } @@ -7973,10 +6735,14 @@ static bool renameProjectFileTo(int32_t fileIdx, const char *newName) { return false; } - if (nameClass && !setClassNameAttr(fileIdx, newClass)) { + if (nameClass && !setFileClassName(fileIdx, newClass)) { ideErrorBox("The file was renamed, but its class name could not be changed."); } + if (nameClass && file->kind == PrjKindControlE) { + designCtlRefresh(); + } + invalidateCompiled(); updateProjectMenuState(); updateDirtyIndicators(); @@ -8034,133 +6800,11 @@ static int32_t replaceInSections(const char *objFilter, bool caseSensitive) { static void reportUnreadable(const char *path) { char msg[IDE_MSG_BUF]; - snprintf(msg, sizeof(msg), "Cannot load %s: unreadable or larger than %d bytes.", path, (int)(IDE_MAX_SOURCE - 1)); + snprintf(msg, sizeof(msg), "Cannot load %s: unreadable or larger than %d bytes.", path, IDE_MAX_SOURCE - 1); ideErrorBox(msg); } -// resolveVarPath -- parse a variable reference and resolve it to a -// pointer into the running VM's live data. Handles: -// varName -- scalar variable -// varName(i) -- array element -// varName.field -- UDT field -// varName(i).field -- array element UDT field -// Returns NULL if it can't be resolved. *endPtr is set past the path. - -static BasValueT *resolveVarPath(const char *path, const char **endPtr) { - const char *p = path; - - // Extract variable name (identifier plus optional type suffix) - const char *nameStart = p; - - while (basIsIdentChar(*p)) { - p++; - } - - if (p == nameStart) { - return NULL; - } - - if (basIsTypeSuffixChar(*p)) { - p++; - } - - char varName[IDE_FULL_NAME_BUF]; - int32_t nameLen = (int32_t)(p - nameStart); - - if (nameLen >= (int32_t)sizeof(varName)) { - return NULL; - } - - memcpy(varName, nameStart, nameLen); - varName[nameLen] = '\0'; - - // Look up the base variable - const BasDebugVarT *dv = findDebugVar(varName); - - if (!dv) { - return NULL; - } - - BasValueT *slot = getDebugVarSlot(dv); - - if (!slot) { - return NULL; - } - - // Parse optional array subscript: (idx1, idx2, ...) - if (*p == '(') { - if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) { - return NULL; - } - - int32_t indices[BAS_ARRAY_MAX_DIMS]; - int32_t numIndices = 0; - - p = parseIndexList(p + 1, indices, &numIndices); - - int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices); - - if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) { - return NULL; - } - - slot = basValDeref(&slot->arrVal->elements[flatIdx]); - } - - // Parse optional UDT field or class member variable: .fieldName - if (*p == '.') { - p++; // skip '.' - - bool isInstance = slot->type == BAS_TYPE_INSTANCE; - - if (!isInstance && (slot->type != BAS_TYPE_UDT || !slot->udtVal)) { - return NULL; - } - - const char *fieldStart = p; - - while (basIsIdentChar(*p)) { - p++; - } - - char fieldName[IDE_FULL_NAME_BUF]; - int32_t fieldLen = (int32_t)(p - fieldStart); - - if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) { - return NULL; - } - - memcpy(fieldName, fieldStart, fieldLen); - fieldName[fieldLen] = '\0'; - - if (isInstance) { - int32_t memberIdx = findMemberVarIdx(slot->instVal, fieldName); - - if (memberIdx < 0) { - return NULL; - } - - slot = &slot->instVal->fields[memberIdx]; - } else { - int32_t fieldIdx = findUdtFieldIdx(slot->udtVal->typeId, fieldName); - - if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) { - return NULL; - } - - slot = &slot->udtVal->fields[fieldIdx]; - } - } - - if (endPtr) { - *endPtr = p; - } - - return slot; -} - - static void runCached(void) { if (!sCachedModule) { setStatus("No compiled program. Press F5 to compile first."); @@ -8183,23 +6827,46 @@ static void runModule(BasModuleT *mod) { return; } - // Create VM - BasVmT *vm = basVmCreate(); - - if (!vm) { - setStatus("Out of memory creating VM."); + if (prjIsControlProject(&sProject)) { + setStatus(IDE_CTL_NO_RUN); return; } - // Create form runtime (bridges UI opcodes to DVX widgets) - BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod); + // The program's VM and form runtime. In the IDE, App.Config and + // App.Data live under the project directory so everything stays + // together during development; the program's screen window carries + // the project's name and F1 opens the project's help file. The IDE + // reports runtime errors through the Output pane and jumps the editor + // to the faulting line, so the form runtime's modal error box is + // suppressed (its nested event pump raced with events still queued + // by the program: "press OK twice"). + char helpPath[DVX_MAX_PATH] = ""; + const char *hostError = NULL; + BasRunSpecT spec; + + if (sProject.helpFile[0]) { + snprintf(helpPath, sizeof(helpPath), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.helpFile); + } + + memset(&spec, 0, sizeof(spec)); + spec.appDir = sProject.projectDir; + spec.args = sProject.commandArgs; + spec.title = sProject.name; + spec.helpFile = helpPath[0] ? helpPath : NULL; + spec.printFn = printCallback; + spec.inputFn = inputCallback; + spec.doEventsFn = doEventsCallback; + spec.suppressErrorDialog = true; + + BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError); if (!formRt) { - basVmDestroy(vm); - setStatus("Out of memory creating form runtime."); + setStatus(hostError); return; } + BasVmT *vm = formRt->vm; + setStatus("Running..."); closeFindDialog(); @@ -8231,64 +6898,7 @@ static void runModule(BasModuleT *mod) { dvxHideWindow(sAc, sProjectWin); } - basVmLoadModule(vm, mod); - - // Set App.Path/Config/Data. In the IDE, config and data live under - // the project directory so everything stays together during development. - // Standalone apps use the DVX root-level CONFIG/ and DATA/ directories - // since the app directory (on CD-ROM) is read-only. - snprintf(vm->appPath, sizeof(vm->appPath), "%s", sProject.projectDir); - snprintf(vm->appConfig, sizeof(vm->appConfig), "%s" DVX_PATH_SEP "CONFIG", sProject.projectDir); - snprintf(vm->appData, sizeof(vm->appData), "%s" DVX_PATH_SEP "DATA", sProject.projectDir); - basVmSetCommand(vm, sProject.commandArgs); - - platformMkdirRecursive(vm->appConfig); - platformMkdirRecursive(vm->appData); - - // Set I/O callbacks - basVmSetPrintCallback(vm, printCallback, NULL); - basVmSetInputCallback(vm, inputCallback, NULL); - basVmSetDoEventsCallback(vm, doEventsCallback, NULL); - vm->breakpointFn = onBreakpointHit; - vm->breakpointCtx = NULL; - - // Set SQL callbacks - BasSqlCallbacksT sqlCb; - memset(&sqlCb, 0, sizeof(sqlCb)); - sqlCb.sqlOpen = dvxSqlOpen; - sqlCb.sqlClose = dvxSqlClose; - sqlCb.sqlExec = dvxSqlExec; - sqlCb.sqlError = dvxSqlError; - sqlCb.sqlQuery = dvxSqlQuery; - sqlCb.sqlNext = dvxSqlNext; - sqlCb.sqlEof = dvxSqlEof; - sqlCb.sqlFieldCount = dvxSqlFieldCount; - sqlCb.sqlFieldName = dvxSqlFieldName; - sqlCb.sqlFieldText = dvxSqlFieldText; - sqlCb.sqlFieldByName = dvxSqlFieldByName; - sqlCb.sqlFieldInt = dvxSqlFieldInt; - sqlCb.sqlFieldDbl = dvxSqlFieldDbl; - sqlCb.sqlFreeResult = dvxSqlFreeResult; - sqlCb.sqlAffectedRows = dvxSqlAffectedRows; - basVmSetSqlCallbacks(vm, &sqlCb); - - // Set extern library callbacks (DECLARE LIBRARY support) - BasExternCallbacksT extCb; - extCb.resolveExtern = basExternResolve; - extCb.callExtern = basExternCall; - extCb.ctx = NULL; - basVmSetExternCallbacks(vm, &extCb); - - // The IDE surfaces runtime errors via the Output pane and jumps - // the editor to the faulting line, so suppress the form runtime's - // modal error dialog. Keeping the dialog on top of an already- - // modal-heavy IDE session was producing phantom "press OK twice" - // UX as the dialog's nested event pump raced with other queued - // events coming out of the user's program. - formRt->suppressErrorDialog = true; - - // The program's screen window (console / graphics) carries its name - snprintf(formRt->title, sizeof(formRt->title), "%s", sProject.name); + basVmSetBreakpointCallback(vm, onBreakpointHit, NULL); sVm = vm; sDbgFormRt = formRt; @@ -8296,11 +6906,6 @@ static void runModule(BasModuleT *mod) { sDbgState = DbgRunningE; sStopRequested = false; - // Set project help file on form runtime for F1 context help - if (sProject.helpFile[0]) { - snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.helpFile); - } - updateProjectMenuState(); // Set breakpoints BEFORE loading forms so breakpoints in form @@ -8315,13 +6920,13 @@ static void runModule(BasModuleT *mod) { // Load forms from project files (AFTER breakpoints are set so // init code breakpoints fire), then show the startup form. - loadFrmFiles(formRt); - basFormRtLoadAllForms(formRt, sProject.startupForm); + loadRunFiles(formRt); + basRunHostLoadForms(formRt, sProject.startupForm); // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE); - BasVmResultE result = runProgramSlices(vm); + BasVmResultE result = runProgramSlices(formRt); // VB-style event loop: after module-level code finishes, // keep processing events as long as any form is loaded. @@ -8332,12 +6937,12 @@ static void runModule(BasModuleT *mod) { if (result == BAS_VM_HALTED && basFormRtKeepsRunning(formRt)) { setStatus("Running (event loop)..."); - // vm->errorMsg outliving an event dispatch means an unhandled - // runtime error escaped a handler (ON ERROR clears the message - // when it takes over); without this test the program kept - // running after the error and the report below never fired + // An error message outliving an event dispatch means an + // unhandled runtime error escaped a handler (ON ERROR clears the + // message when it takes over); without this test the program + // kept running after the error and the report below never fired // until the user pressed Stop. - while (sWin && sAc->running && basFormRtKeepsRunning(formRt) && !sStopRequested && !vm->ended && vm->errorMsg[0] == '\0') { + while (sWin && sAc->running && !sStopRequested && basRunHostAlive(formRt)) { idePump(); } } @@ -8345,7 +6950,7 @@ static void runModule(BasModuleT *mod) { // A normal end releases the module variables: their objects' // Class_Terminates run (VB) if (result == BAS_VM_HALTED && sWin && sAc->running && !sStopRequested && !formRt->terminated && basVmFinish(vm)) { - runProgramSlices(vm); + runProgramSlices(formRt); } // A runtime error fired from a host callback (e.g. setting a @@ -8359,8 +6964,8 @@ static void runModule(BasModuleT *mod) { // Use vm->errorLine (snapshot at error time) rather than // vm->currentLine: the dialog/event pump can fire another handler // whose OP_LINE overwrites currentLine before we read it here. - bool hadRuntimeError = (vm->errorMsg[0] != '\0') && sWin; - int32_t errorLine = hadRuntimeError ? vm->errorLine : 0; + bool hadRuntimeError = basVmGetError(vm)[0] != '\0' && sWin; + int32_t errorLine = hadRuntimeError ? basVmGetErrorLine(vm) : 0; if (hadRuntimeError) { // Map the concatenated line to a file / proc-local line the @@ -8395,11 +7000,11 @@ static void runModule(BasModuleT *mod) { char report[IDE_REPORT_BUF]; if (procName[0]) { - snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", procName, (int)procLine, vm->errorMsg); + snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", procName, (int)procLine, basVmGetError(vm)); } else if (errFile[0]) { - snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, vm->errorMsg); + snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, basVmGetError(vm)); } else { - snprintf(report, sizeof(report), "\nRUNTIME ERROR:\nline %d: %s\n", (int)errLocalLine, vm->errorMsg); + snprintf(report, sizeof(report), "\nRUNTIME ERROR:\nline %d: %s\n", (int)errLocalLine, basVmGetError(vm)); } outputAppend(report, (int32_t)strlen(report)); @@ -8418,8 +7023,7 @@ static void runModule(BasModuleT *mod) { sDbgCurrentLine = -1; sDbgEnabled = false; - basFormRtDestroy(formRt); - basVmDestroy(vm); + basRunHostDestroy(formRt); // If the IDE was closed while the program was running, skip all UI // updates -- the windows are already destroyed -- and release the @@ -8480,9 +7084,10 @@ static void runModule(BasModuleT *mod) { // runProgramSlices -- run the program in step slices, yielding to DVX -// between them, pausing at breakpoints, until it halts, fails (its message -// left in vm->errorMsg) or is stopped. Returns the last result. -static BasVmResultE runProgramSlices(BasVmT *vm) { +// between them, pausing at breakpoints, until it halts, fails (its +// message and line left in the VM, see basRunHostSlice) or is stopped. +// Returns the last result. +static BasVmResultE runProgramSlices(BasFormRtT *rt) { BasVmResultE result = BAS_VM_HALTED; for (;;) { @@ -8491,22 +7096,19 @@ static BasVmResultE runProgramSlices(BasVmT *vm) { } if (sDbgState == DbgPausedE) { - // Paused at breakpoint/step -- spin on GUI events until user acts + // Paused at breakpoint/step -- spin on GUI events until the + // user continues, steps or stops (debugResume resumes the VM) idePump(); - - // User may have pressed F5 (continue), F8 (step), or Esc (stop) - if (sDbgState == DbgRunningE) { - vm->running = true; - } - continue; } - result = basVmRun(vm); + result = basRunHostSlice(rt); // Module-level pause: the same state as a pause inside a handler if (result == BAS_VM_BREAKPOINT) { - onBreakpointHit(NULL, vm->currentLine > 0 ? vm->currentLine : 0); + int32_t line = basVmGetCurrentLine(rt->vm); + + onBreakpointHit(NULL, line > 0 ? line : 0); continue; } @@ -8515,24 +7117,6 @@ static BasVmResultE runProgramSlices(BasVmT *vm) { continue; } - if (result == BAS_VM_HALTED) { - break; - } - - // Module-level runtime error. For VM-internal errors (e.g. - // division by zero) there's no vm->errorLine set, so fall - // back to vm->currentLine -- but copy it into errorLine so - // the shared post-loop handler below can format and navigate - // uniformly with the event-handler error path. - if (vm->errorMsg[0] == '\0') { - const char *msg = basVmGetError(vm); - snprintf(vm->errorMsg, sizeof(vm->errorMsg), "%s", msg ? msg : "Runtime error"); - } - - if (vm->errorLine == 0 && vm->currentLine > 0) { - vm->errorLine = vm->currentLine; - } - break; } @@ -8540,22 +7124,6 @@ static BasVmResultE runProgramSlices(BasVmT *vm) { } -// runSnippetVm -- run an Immediate/Watch snippet in bounded slices so an -// endless loop typed into the Immediate window cannot hang the IDE. -// BAS_VM_STEP_LIMIT means it was abandoned. -static BasVmResultE runSnippetVm(BasVmT *vm) { - BasVmResultE result = BAS_VM_STEP_LIMIT; - - basVmSetStepLimit(vm, IDE_SNIPPET_SLICE); - - for (int32_t slice = 0; slice < IDE_SNIPPET_MAX_SLICES && result == BAS_VM_STEP_LIMIT; slice++) { - result = basVmRun(vm); - } - - return result; -} - - // saveActiveFile -- write the active file (editor/designer state first // stashed into its buffer). Returns false when nothing was written. static bool saveActiveFile(void) { @@ -8830,100 +7398,6 @@ static bool saveProjectFile(void) { } -// scanClassEvents -- append the [Public] Event Name[(params)] statements -// of a class module's text to *events (the compiler's words, read with -// its lexer so strings and comments never match) -static void scanClassEvents(const char *text, IdeClassEventT **events) { - BasLexerT lex; - bool stmtStart = true; - - basLexerInit(&lex, text, (int32_t)strlen(text)); - - while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR) { - if (stmtStart && lex.token.type == TOK_PUBLIC) { - basLexerNext(&lex); - } - - BasTokenTypeE tt = lex.token.type; - bool isEvent = stmtStart && tt == TOK_IDENT && strcasecmp(lex.token.text, BAS_KW_EVENT) == 0; - - if (isEvent && basLexerNext(&lex) == TOK_IDENT) { - IdeClassEventT evt; - - memset(&evt, 0, sizeof(evt)); - snprintf(evt.name, sizeof(evt.name), "%s", lex.token.text); - - if (basLexerNext(&lex) == TOK_LPAREN) { - int32_t start = lex.token.pos + 1; - int32_t depth = 1; - - while (depth > 0 && basLexerNext(&lex) != TOK_EOF && lex.token.type != TOK_NEWLINE && lex.token.type != TOK_ERROR) { - depth += (lex.token.type == TOK_LPAREN) - (lex.token.type == TOK_RPAREN); - } - - int32_t len = lex.token.pos - start; - - snprintf(evt.params, sizeof(evt.params), "%.*s", (int)DVX_MIN(len, (int32_t)sizeof(evt.params) - 1), text + start); - } - - arrput(*events, evt); - tt = lex.token.type; - } - - stmtStart = tt == TOK_NEWLINE || tt == TOK_COLON; - basLexerNext(&lex); - } -} - - -// scanWithEvents -- collect the WithEvents variables src declares (Dim / -// Private / Public WithEvents name As Class, several declarators allowed) -// into sWithEvents, read with the compiler's lexer -static void scanWithEvents(const char *src) { - BasLexerT lex; - bool stmtStart = true; - - arrsetlen(sWithEvents, 0); - basLexerInit(&lex, src, (int32_t)strlen(src)); - - while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR) { - BasTokenTypeE tt = lex.token.type; - - if (!stmtStart || (tt != TOK_DIM && tt != TOK_PRIVATE && tt != TOK_PUBLIC)) { - stmtStart = tt == TOK_NEWLINE || tt == TOK_COLON; - basLexerNext(&lex); - continue; - } - - // One declarator per pass: [WithEvents] name [As type] - do { - bool withEvents = basLexerNext(&lex) == TOK_IDENT && strcasecmp(lex.token.text, BAS_KW_WITHEVENTS) == 0; - - if (withEvents && basLexerNext(&lex) == TOK_IDENT) { - IdeWithEventsT we; - - snprintf(we.name, sizeof(we.name), "%s", lex.token.text); - - if (basLexerNext(&lex) == TOK_AS && basLexerNext(&lex) == TOK_IDENT) { - snprintf(we.className, sizeof(we.className), "%s", lex.token.text); - arrput(sWithEvents, we); - } - } - - // On to the next declarator (commas inside bounds do not count) - int32_t depth = 0; - - while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR && lex.token.type != TOK_NEWLINE && lex.token.type != TOK_COLON && (depth > 0 || lex.token.type != TOK_COMMA)) { - depth += (lex.token.type == TOK_LPAREN) - (lex.token.type == TOK_RPAREN); - basLexerNext(&lex); - } - } while (lex.token.type == TOK_COMMA); - - stmtStart = false; - } -} - - // sectionInScope -- whether section p (-1 = General) of the editor file // belongs to object objFilter (NULL = every section) static bool sectionInScope(int32_t p, const char *objFilter) { @@ -9007,12 +7481,13 @@ static void setBreakpointProcName(IdeBreakpointT *bp, int32_t procIdx) { } -// setClassNameAttr -- give class module fileIdx the class name name in +// setFileClassName -- give class module fileIdx the class name name in // its Attribute VB_Name (when it has one; else the name already comes -// from the file name). The text becomes an unsaved edit, and the editor +// from the file name), or UserControl fileIdx the name on its Begin +// UserControl line. The text becomes an unsaved edit, and the editor // shows it when the file is open there. False when out of memory or the // file cannot be read. -static bool setClassNameAttr(int32_t fileIdx, const char *name) { +static bool setFileClassName(int32_t fileIdx, const char *name) { PrjFileT *file = &sProject.files[fileIdx]; if (!file->buffer) { @@ -9027,20 +7502,26 @@ static bool setClassNameAttr(int32_t fileIdx, const char *name) { int32_t start = 0; int32_t oldLen = 0; int32_t nameLen = (int32_t)strlen(name); + char *text = NULL; - if (!basClassNameAttr(file->buffer, len, &start, &oldLen)) { + if (file->kind == PrjKindControlE) { + text = prjCtlHeaderSet(file->buffer, BAS_PROP_NAME, name); + } else if (basClassNameAttr(file->buffer, len, &start, &oldLen)) { + text = (char *)malloc((size_t)(len - oldLen + nameLen + 1)); + + if (text) { + memcpy(text, file->buffer, (size_t)start); + memcpy(text + start, name, (size_t)nameLen); + memcpy(text + start + nameLen, file->buffer + start + oldLen, (size_t)(len - start - oldLen + 1)); + } + } else { return true; } - char *text = (char *)malloc((size_t)(len - oldLen + nameLen + 1)); - if (!text) { return false; } - memcpy(text, file->buffer, (size_t)start); - memcpy(text + start, name, (size_t)nameLen); - memcpy(text + start + nameLen, file->buffer + start + oldLen, (size_t)(len - start - oldLen + 1)); free(file->buffer); file->buffer = text; file->modified = true; @@ -9322,7 +7803,7 @@ static void showCodeWindow(void) { wgtDropdownSetItems(sEvtDropdown, NULL, 0); sEditor->weight = WGT_WEIGHT_FILL; - wgtTextAreaSetColorize(sEditor, basicColorize, NULL); + wgtTextAreaSetSyntaxMode(sEditor, IDE_SYNTAX_MODE); // Apply saved syntax colors uint32_t initColors[TEXT_SYNTAX_MAX]; @@ -9335,7 +7816,6 @@ static void showCodeWindow(void) { wgtTextAreaSetSyntaxColors(sEditor, initColors, TEXT_SYNTAX_MAX); - wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc); wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); @@ -9655,6 +8135,39 @@ static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensit } +// showProperties -- the Properties window (made when there is none) +static void showProperties(void) { + if (sPropsWin) { + return; + } + + sPropsWin = prpCreate(sAc, &sDesigner); + + if (sPropsWin) { + sPropsWin->y = toolbarBottom(); + sPropsWin->onMenu = onMenu; + sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; + propsFollowActive(); + } +} + + +// showToolbox -- the Toolbox window (made when there is none): every +// control type a form can hold, the project's own UserControls included +static void showToolbox(void) { + if (sToolboxWin) { + return; + } + + sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom()); + + if (sToolboxWin) { + sToolboxWin->onMenu = onMenu; + sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; + } +} + + static void showWatchWindow(void) { if (sWatchWin) { dvxShowWindow(sAc, sWatchWin); @@ -9722,32 +8235,6 @@ static const char *skipLine(const char *p) { } -// skipToEndProc -- from the start of a SUB/FUNCTION declaration line, -// return the position just past the matching END SUB/END FUNCTION line -// (or the end of the text). *outEndLineOffset receives the END line's -// distance in lines from the declaration (the last line's when missing). -static const char *skipToEndProc(const char *declLine, bool isSub, int32_t *outEndLineOffset) { - const char *endTag = isSub ? KW_END_SUB : KW_END_FUNCTION; - const char *scan = skipLine(declLine); - int32_t line = 1; - bool found = false; - - while (*scan) { - if (kwMatch(dvxSkipWs(scan), endTag)) { - found = true; - scan = skipLine(scan); - break; - } - - scan = skipLine(scan); - line++; - } - - *outEndLineOffset = found ? line : line - 1; - return scan; -} - - // splitProcs -- split source into a (General) buffer plus one buffer per // SUB/FUNCTION block. Every line outside a procedure (including any // between or after procedures) belongs to General, so nothing is lost @@ -9769,9 +8256,9 @@ static bool splitProcs(const char *source, char **outGeneral, char ***outProcs, while (ok && *pos) { bool isSub = false; - if (procDeclAt(dvxSkipWs(pos), &isSub)) { + if (basProcDeclAt(dvxSkipWs(pos), &isSub)) { int32_t endOffset = 0; - const char *end = skipToEndProc(pos, isSub, &endOffset); + const char *end = basProcSkipToEnd(pos, isSub, &endOffset); int32_t procLen = (int32_t)(end - pos); char *procBuf = (char *)malloc(procLen + 1); @@ -9872,7 +8359,7 @@ static bool stashCurrentFile(void) { // On failure keep the previous buffer rather than a truncated one if (frmLen < 0) { free(frmBuf); - ideErrorBox(frmLen == DSGN_SAVE_LINE_TOO_LONG ? "A property value is too long to save at its position in the form. Shorten it and save again." : "Form is too large to save; its code section does not fit."); + ideErrorBox(frmLen == FRM_WRITE_LINE_TOO_LONG ? "A property value is too long to save at its position in the form. Shorten it and save again." : "Form is too large to save; its code section does not fit."); return false; } @@ -9947,7 +8434,7 @@ static void switchToDesign(void) { return; } - DsgnFormT *form = sDesigner.form; + FrmModelT *form = sDesigner.form; // Create the form designer window using the shared form window builder char title[IDE_FORM_TITLE_BUF]; @@ -9969,6 +8456,10 @@ static void switchToDesign(void) { sFormWin->accelTable = sWin ? sWin->accelTable : NULL; sDesigner.formWin = sFormWin; + // UserControls placed here see UserMode False (their code draws, the + // input goes to the designer) from their Initialize on + basDsgnSetDesignWindow(sFormWin, true); + // Build preview menu bar from form's menu items dsgnBuildPreviewMenuBar(sFormWin, form); @@ -9989,36 +8480,12 @@ static void switchToDesign(void) { dvxSetTitle(sAc, sFormWin, title); } - // Size the form window - if (form->autoSize) { - dvxFitWindow(sAc, sFormWin); - form->width = sFormWin->w; - form->height = sFormWin->h; - } else { - dvxResizeWindow(sAc, sFormWin, form->width, form->height); - } - + dsgnFitFormWin(&sDesigner); sFormWin->onResize = onFormWinResize; // Create toolbox and properties windows - if (!sToolboxWin) { - sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom()); - - if (sToolboxWin) { - sToolboxWin->onMenu = onMenu; - sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; - } - } - - if (!sPropsWin) { - sPropsWin = prpCreate(sAc, &sDesigner); - - if (sPropsWin) { - sPropsWin->y = toolbarBottom(); - sPropsWin->onMenu = onMenu; - sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; - } - } + showToolbox(); + showProperties(); dvxInvalidateWindow(sAc, sFormWin); updateProjectMenuState(); @@ -10079,11 +8546,6 @@ static void toggleBreakpointLine(int32_t editorLine) { if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { arrdel(sBreakpoints, i); syncVmBreakpoints(); - - if (sEditor) { - wgtInvalidatePaint(sEditor); - } - updateBreakpointWindow(); return; } @@ -10133,7 +8595,7 @@ static void toggleBreakpointLine(int32_t editorLine) { // SUB/FUNCTION declaration or END SUB/FUNCTION bool isSub; - if (procDeclAt(p, &isSub) || kwMatch(p, KW_END_SUB) || kwMatch(p, KW_END_FUNCTION)) { + if (basProcDeclAt(p, &isSub) || kwMatch(p, BAS_PROC_KW_END_SUB) || kwMatch(p, BAS_PROC_KW_END_FUNCTION)) { return; } } @@ -10153,7 +8615,6 @@ static void toggleBreakpointLine(int32_t editorLine) { // The first breakpoint of the file starts the edit snapshots if (sEditor) { editorSnapshot(sEditorLineCount); - wgtInvalidatePaint(sEditor); } updateBreakpointWindow(); @@ -10165,64 +8626,9 @@ static int32_t toolbarBottom(void) { } -static const char *typeNameStr(uint8_t dt) { - switch (dt) { - case BAS_TYPE_INTEGER: - return "Integer"; - - case BAS_TYPE_BYTE: - return "Byte"; - - case BAS_TYPE_CURRENCY: - return "Currency"; - - case BAS_TYPE_LONG: - return "Long"; - - case BAS_TYPE_SINGLE: - return "Single"; - - case BAS_TYPE_DOUBLE: - return "Double"; - - case BAS_TYPE_STRING: - return "String"; - - case BAS_TYPE_BOOLEAN: - return "Boolean"; - - case BAS_TYPE_DATE: - return "Date"; - - case BAS_TYPE_ARRAY: - return "Array"; - - case BAS_TYPE_UDT: - return "UDT"; - - case BAS_TYPE_OBJECT: - case BAS_TYPE_INSTANCE: - return IDE_OBJECT_TYPE; - - case BAS_TYPE_EMPTY: - return "Empty"; - - case BAS_TYPE_NULL: - return "Null"; - - case BAS_TYPE_VARIANT: - return "Variant"; - - case BAS_TYPE_MISSING: - return "Missing"; - - default: - return "?"; - } -} - - static void updateBreakpointWindow(void) { + refreshEditorMarkers(); + if (!sBreakpointList || !sBreakpointWin || !sBreakpointWin->visible) { return; } @@ -10260,32 +8666,7 @@ static void updateCallStackWindow(void) { listRowsReset(&sCallRows); - if (sDbgState != DbgPausedE || !sVm || !sDbgModule) { - listRowsCommit(&sCallRows, sCallStackList, 0); - return; - } - - int32_t rowCount = 0; - - // Current location first - if (sDbgCurrentLine > 0) { - char lineBuf[IDE_LINE_NUM_BUF]; - - int32_t curProcIdx = vmCurProcIdx(); - - snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sDbgCurrentLine); - listRowsAdd(&sCallRows, curProcIdx >= 0 ? sDbgModule->procs[curProcIdx].name : IDE_MODULE_FRAME); - listRowsAdd(&sCallRows, lineBuf); - rowCount++; - } - - // Walk the callers, innermost first. Frame d is suspended at the - // return address of frame d + 1; frame 0 is the implicit module frame. - for (int32_t d = sVm->callDepth - 2; d >= 0; d--) { - listRowsAdd(&sCallRows, d > 0 ? vmProcNameForPc(sVm->callStack[d + 1].returnPc) : IDE_MODULE_FRAME); - listRowsAdd(&sCallRows, ""); - rowCount++; - } + int32_t rowCount = (sDbgState == DbgPausedE && sVm && sDbgModule) ? basDbgCallStack(sVm, sDbgCurrentLine, listRowsAddRow, &sCallRows) : 0; listRowsCommit(&sCallRows, sCallStackList, rowCount); } @@ -10334,7 +8715,8 @@ static void updateDirtyIndicators(void) { static void updateDropdowns(void) { // Rebuilds sProcTable as a side effect; the file's WithEvents // variables are objects too - scanWithEvents(getFullSource()); + arrsetlen(sWithEvents, 0); + basSourceWithEvents(getFullSource(), addWithEvents, NULL); // Reset dynamic arrays. sObjItems owns strdup'd copies -- free them // first so no entry survives a subsequent controls/menuItems mutation @@ -10357,7 +8739,9 @@ static void updateDropdowns(void) { // pointer we borrowed from &menuItems[i].name. arrput(sObjItems, strdup(IDE_GENERAL_SECTION)); - if (sDesigner.form) { + if (editorIsControl()) { + arrput(sObjItems, strdup(BAS_UC_OBJECT)); + } else if (sDesigner.form) { arrput(sObjItems, strdup(sDesigner.form->name)); for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { @@ -10366,7 +8750,7 @@ static void updateDropdowns(void) { // Add menu item names (non-separator, non-top-level-header-only) for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { - DsgnMenuItemT *mi = &sDesigner.form->menuItems[i]; + FrmMenuItemT *mi = &sDesigner.form->menuItems[i]; if (mi->name[0] && mi->caption[0] != '-') { arrput(sObjItems, strdup(mi->name)); @@ -10408,60 +8792,10 @@ static void updateLocalsWindow(void) { return; } + // Locals of the current proc, globals, and the current form's variables listRowsReset(&sLocalsRows); - if (sDbgState != DbgPausedE || !sVm || !sDbgModule) { - listRowsCommit(&sLocalsRows, sLocalsList, 0); - return; - } - - // Locals of the current proc, globals, and the current form's variables - int32_t curProcIdx = vmCurProcIdx(); - int32_t rowCount = 0; - - if (sDbgModule->debugVars) { - for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) { - BasDebugVarT *dv = &sDbgModule->debugVars[i]; - - if (!debugVarVisible(dv, curProcIdx) || isMangledDebugName(dv->name)) { - continue; - } - - char typeBuf[IDE_TYPE_NAME_BUF]; - char valueBuf[BAS_MAX_IDENT]; - - // Read the value first so we can use it for the type column - BasValueT val; - BasValueT *slot = getDebugVarSlot(dv); - - if (slot) { - val = *slot; - } else { - memset(&val, 0, sizeof(val)); - } - - // Type column -- arrays show "type()" with the element type, an - // object its class, a Variant "Variant/" and what it holds - const char *valType = (val.type == BAS_TYPE_INSTANCE) ? instanceTypeName(val.instVal) : typeNameStr(val.type); - - if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) { - snprintf(typeBuf, sizeof(typeBuf), "%s()", typeNameStr(val.arrVal->elementType)); - } else if (dv->dataType == BAS_TYPE_VARIANT) { - snprintf(typeBuf, sizeof(typeBuf), "%s/%s", typeNameStr(BAS_TYPE_VARIANT), valType); - } else if (val.type == BAS_TYPE_INSTANCE) { - snprintf(typeBuf, sizeof(typeBuf), "%s", valType); - } else { - snprintf(typeBuf, sizeof(typeBuf), "%s", typeNameStr(dv->dataType)); - } - - formatValue(&val, valueBuf, sizeof(valueBuf)); - - listRowsAdd(&sLocalsRows, dv->name); - listRowsAdd(&sLocalsRows, typeBuf); - listRowsAdd(&sLocalsRows, valueBuf); - rowCount++; - } - } + int32_t rowCount = (sDbgState == DbgPausedE && sVm && sDbgModule) ? basDbgLocals(sVm, listRowsAddRow, &sLocalsRows) : 0; listRowsCommit(&sLocalsRows, sLocalsList, rowCount); } @@ -10507,6 +8841,7 @@ static void updateProjectMenuState(void) { wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_OPEN, isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_OPEN, isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW_CLASS, project && isIdle); + wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW_CTL, project && isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, project); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_RENAME, isIdle && prjGetSelectedFileIdx() >= 0); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, isIdle && prjGetSelectedFileIdx() >= 0); @@ -10521,6 +8856,13 @@ static void updateProjectMenuState(void) { wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyFileModified()); wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, project && isIdle); + // A control project makes a control file + MenuItemT *makeItem = sFileMenu ? wmMenuFindItemInMenu(sFileMenu, CMD_MAKE_EXE) : NULL; + + if (makeItem) { + wmMenuItemSetLabel(makeItem, prjIsControlProject(&sProject) ? IDE_MAKE_CTL_LABEL : IDE_MAKE_EXE_LABEL); + } + // Edit menu wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, project); wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, project); @@ -10603,16 +8945,8 @@ static void updateWatchWindow(void) { valBuf[0] = '\0'; - if (sDbgState == DbgPausedE && sVm && sDbgModule) { - BasValueT val; - memset(&val, 0, sizeof(val)); - - if (lookupWatchVar(sWatchExprs[i], &val)) { - // Simple variable/field/subscript lookup succeeded - formatValue(&val, valBuf, sizeof(valBuf)); - } else if (!evalWatchExpr(sWatchExprs[i], valBuf, sizeof(valBuf))) { - snprintf(valBuf, sizeof(valBuf), ""); - } + if (sDbgState == DbgPausedE && sVm && sDbgModule && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf))) { + snprintf(valBuf, sizeof(valBuf), "%s", BAS_DBG_EVAL_ERROR); } listRowsAdd(&sWatchRows, sWatchExprs[i]); @@ -10623,266 +8957,6 @@ static void updateWatchWindow(void) { } -// ============================================================ -// Compile-time CtrlName.Member validator -// ============================================================ -// -// The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface -// returns live interface metadata. Combined with a static scan of -// the project's .frm files (control name -> widget type), we can -// reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at -// compile time instead of letting them surface as runtime errors -// at event-click time. Dynamically-created controls (via -// CreateControl at runtime) aren't in the map; lookupCtrlType -// returns NULL for them and validation is skipped. - -// validatorFree -- release a validator context filled during a compile -static void validatorFree(IdeValidatorCtxT *ctx) { - arrfree(ctx->entries); - arrfree(ctx->badTypes); - ctx->entries = NULL; - ctx->badTypes = NULL; -} - - -static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *methodName) { - (void)ctx; - - if (!wgtType || !methodName) { - return true; // be permissive on malformed input - } - - if (basFormRtCommonMethodId(methodName) != BAS_CM_NONE) { - return true; - } - - // Drawing methods of forms and PictureBoxes - if ((strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0) && basFormRtIsDrawMember(methodName, false)) { - return true; - } - - // Form-level methods - if (strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0) { - return strcasecmp(methodName, "Show") == 0 || strcasecmp(methodName, "Hide") == 0; - } - - // Menu items have no methods beyond common - if (strcasecmp(wgtType, IDE_MENU_TYPE_NAME) == 0) { - return false; - } - - // Widget-specific methods from the live iface - const char *wgtName = wgtFindByBasName(wgtType); - - if (!wgtName) { - return true; // unknown type -- skip validation - } - - const WgtIfaceT *iface = wgtGetIface(wgtName); - - if (!iface) { - return true; - } - - // A known type with no interface methods accepts only the common - // set (already checked above); being permissive here let typos - // like btnGo.Boggle compile. - for (int32_t i = 0; i < iface->methodCount; i++) { - if (strcasecmp(iface->methods[i].name, methodName) == 0) { - return true; - } - } - - return false; -} - - -static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *propName) { - (void)ctx; - - if (!wgtType || !propName) { - return true; - } - - // Drawing properties of forms and PictureBoxes - if ((strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0) && basFormRtIsDrawMember(propName, true)) { - return true; - } - - // Form-level properties (the runtime's own table) - if (strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0) { - return basFormRtFindFormProp(propName) != NULL; - } - - // Menu items (the runtime's own table) - if (strcasecmp(wgtType, IDE_MENU_TYPE_NAME) == 0) { - return basFormRtFindMenuProp(propName) != NULL; - } - - // Properties every non-menu control accepts (the runtime's own table) - if (basFormRtFindCommonProp(propName)) { - return true; - } - - // Widget-specific props from the live iface - const char *wgtName = wgtFindByBasName(wgtType); - - if (!wgtName) { - return true; - } - - const WgtIfaceT *iface = wgtGetIface(wgtName); - - if (!iface) { - return true; - } - - // Same reasoning as validatorIsMethodValid: an empty interface - // property table must not turn validation off for the whole type. - return wgtIfaceFindProp(iface, propName) != NULL; -} - - -// validatorLookupCtrlType -- widget type of ctrlName for the parser's -// member checks, or NULL to skip validation. Resolved the way the -// runtime finds controls: a name declared on the form whose code is being -// compiled (basParserFormScope) wins; any other name is looked up across -// every form and skipped when it has different types on different forms -// (Text1 as TextBox and ListBox) rather than checked against the wrong one. -static const char *validatorLookupCtrlType(void *ctx, const char *ctrlName) { - IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx; - const char *found = NULL; - bool ambiguous = false; - - if (!v || !ctrlName) { - return NULL; - } - - const char *scope = v->compiler ? basCompilerFormScope(v->compiler) : ""; - - for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) { - const IdeCtrlMapEntryT *e = &v->entries[i]; - - if (strcasecmp(e->name, ctrlName) != 0) { - continue; - } - - if (scope[0] && strcasecmp(e->formName, scope) == 0) { - return e->wgtType; - } - - ambiguous = ambiguous || (found && strcasecmp(found, e->wgtType) != 0); - found = e->wgtType; - } - - return ambiguous ? NULL : found; -} - - -static void validatorOnCtrlBegin(void *ud, const char *typeName, const char *name) { - IdeValidatorCtxT *v = ((IdeFrmScanT *)ud)->validator; - - if (!v) { - return; - } - - IdeCtrlMapEntryT e; - memset(&e, 0, sizeof(e)); - snprintf(e.name, BAS_MAX_IDENT, "%s", name ? name : ""); - snprintf(e.wgtType, BAS_MAX_IDENT, "%s", typeName ? typeName : ""); - snprintf(e.formName, BAS_MAX_IDENT, "%s", v->currentForm); - arrput(v->entries, e); - - // Also flag unknown widget types. wgtFindByBasName returns NULL - // for anything not registered by a .wgt DXE loaded by the IDE. - // Skip internal structural types (they're handled by layout code, - // not widget DXEs). - if (typeName && typeName[0]) { - bool structural = (strcasecmp(typeName, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(typeName, IDE_MENU_TYPE_NAME) == 0); - - if (!structural && !wgtFindByBasName(typeName)) { - IdeBadTypeT bad; - memset(&bad, 0, sizeof(bad)); - snprintf(bad.ctrlName, BAS_MAX_IDENT, "%s", name ? name : "?"); - snprintf(bad.typeName, BAS_MAX_IDENT, "%s", typeName); - snprintf(bad.formName, BAS_MAX_IDENT, "%s", v->currentForm); - arrput(v->badTypes, bad); - } - } -} - - -static bool validatorOnFormBegin(void *ud, const char *name) { - IdeValidatorCtxT *v = ((IdeFrmScanT *)ud)->validator; - - if (!v) { - return true; - } - - IdeCtrlMapEntryT e; - memset(&e, 0, sizeof(e)); - snprintf(v->currentForm, BAS_MAX_IDENT, "%s", name ? name : ""); - snprintf(e.name, BAS_MAX_IDENT, "%s", v->currentForm); - snprintf(e.wgtType, BAS_MAX_IDENT, "%s", IDE_FORM_TYPE_NAME); - snprintf(e.formName, BAS_MAX_IDENT, "%s", v->currentForm); - arrput(v->entries, e); - return true; -} - - -static void validatorOnMenuBegin(void *ud, const char *name, int32_t level) { - (void)level; - IdeValidatorCtxT *v = ((IdeFrmScanT *)ud)->validator; - - if (!v) { - return; - } - - IdeCtrlMapEntryT e; - memset(&e, 0, sizeof(e)); - snprintf(e.name, BAS_MAX_IDENT, "%s", name ? name : ""); - snprintf(e.wgtType, BAS_MAX_IDENT, "%s", IDE_MENU_TYPE_NAME); - snprintf(e.formName, BAS_MAX_IDENT, "%s", v->currentForm); - arrput(v->entries, e); -} - - -// vmCurProcIdx -- compiled proc the paused VM is in, or -1 at module -// level. Proc bodies carry no end address and module-level code can sit -// after a proc (a later file, form init code), so the frame depth -- not -// the pc -- says whether a proc is executing. -static int32_t vmCurProcIdx(void) { - return sVm->callDepth > 1 ? vmProcIndexForPc(sVm->pc) : -1; -} - - -// vmProcIndexForPc -- index of the compiled proc whose codeAddr is the -// greatest one <= pc (-1 before every proc). Only meaningful for a pc -// known to be inside a proc (see vmCurProcIdx). Requires sDbgModule. -static int32_t vmProcIndexForPc(int32_t pc) { - int32_t best = -1; - int32_t bestAddr = -1; - - for (int32_t i = 0; i < sDbgModule->procCount; i++) { - int32_t addr = sDbgModule->procs[i].codeAddr; - - if (addr <= pc && addr > bestAddr) { - bestAddr = addr; - best = i; - } - } - - return best; -} - - -static const char *vmProcNameForPc(int32_t pc) { - int32_t idx = vmProcIndexForPc(pc); - - return idx >= 0 ? sDbgModule->procs[idx].name : IDE_MODULE_FRAME; -} - - static void watchEditSelected(void) { if (!sWatchList || !sWatchInput) { return; @@ -10908,26 +8982,6 @@ static void watchEditSelected(void) { } -static void watchPrintCallback(void *ctx, const char *text, bool newline) { - (void)ctx; - - if (text) { - int32_t tl = (int32_t)strlen(text); - - if (sWatchPrintLen + tl < (int32_t)sizeof(sWatchPrintBuf) - 1) { - memcpy(sWatchPrintBuf + sWatchPrintLen, text, tl); - sWatchPrintLen += tl; - } - } - - if (newline && sWatchPrintLen < (int32_t)sizeof(sWatchPrintBuf) - 1) { - sWatchPrintBuf[sWatchPrintLen++] = '\n'; - } - - sWatchPrintBuf[sWatchPrintLen] = '\0'; -} - - // writeProjectFile -- write files[idx].buffer to disk (a NULL buffer is // an empty file), byte for byte. The text goes to a temporary sibling // file that platformReplaceFile swaps in only once every write @@ -10977,6 +9031,10 @@ static bool writeProjectFile(int32_t idx) { sDesigner.form->dirty = false; } + if (file->kind == PrjKindControlE) { + designCtlRefresh(); + } + return true; } @@ -10993,6 +9051,7 @@ int32_t appMain(DxeAppContextT *ctx) { prjInit(&sProject); buildWindow(); + basUcSetPropChangedHook(onUcPropChanged); // Load persisted settings shellEnsureConfigDir(sCtx); diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c index c1491295..ea2a0c53 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c @@ -47,16 +47,13 @@ #define ARROW_STR "-> " #define MED_MSG_BUF 128 // error-message scratch buffer -#define MED_SEP_PREFIX "mnuSep" // auto-generated separator name prefix -#define MED_SEP_PREFIX_LEN ((int32_t)(sizeof(MED_SEP_PREFIX) - 1)) -#define MED_NAME_PREFIX "mnu" // auto-generated item name prefix -#define MED_LABEL_BUF (DSGN_MAX_TEXT + DSGN_MENU_STACK_DEPTH * (int32_t)sizeof(ARROW_STR)) +#define MED_LABEL_BUF (FRM_MAX_TEXT + FRM_MENU_STACK_DEPTH * (int32_t)sizeof(ARROW_STR)) #define MED_DLG_W 360 #define MED_DLG_H 420 #define MED_ROW_SPACING 4 #define MED_CHECK_SPACING 12 #define MED_OK_SPACING 8 -#define MED_LABEL_W 60 +#define MED_LABEL_W 80 #define MED_ARROW_BTN_W 32 #define MED_OK_BTN_W 60 #define MED_DLG_TITLE "Menu Editor" @@ -68,15 +65,17 @@ typedef struct { // Fields are ordered by alignment to minimize struct padding. AppContextT *ctx; - DsgnFormT *form; + FrmModelT *form; // Working copy of menu items - DsgnMenuItemT *items; // stb_ds array + FrmMenuItemT *items; // stb_ds array int32_t selectedIdx; // Widgets WidgetT *captionInput; WidgetT *nameInput; + WidgetT *shortcutInput; + WidgetT *shortcutTextInput; WidgetT *checkedCb; WidgetT *radioCheckCb; WidgetT *enabledCb; @@ -100,10 +99,7 @@ static const char **sLabels = NULL; // ============================================================ static void applyFields(void); -static int32_t findSubtreeEnd(int32_t idx); -static void followMenuRenames(void); static void loadFields(void); -static void moveSubtree(int32_t from, int32_t subEnd, int32_t insertAt); static void onCancel(WidgetT *w); static void onCaptionChange(WidgetT *w); static void onClose(WindowT *win); @@ -117,10 +113,8 @@ static void onNameChange(WidgetT *w); static void onNext(WidgetT *w); static void onOk(WidgetT *w); static void onOutdent(WidgetT *w); +static void onShortcutChange(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) { @@ -130,56 +124,27 @@ static void applyFields(void) { return; } - DsgnMenuItemT *mi = &sMed.items[sMed.selectedIdx]; + FrmMenuItemT *mi = &sMed.items[sMed.selectedIdx]; const char *cap = wgtGetText(sMed.captionInput); const char *nam = wgtGetText(sMed.nameInput); if (cap) { - snprintf(mi->caption, DSGN_MAX_TEXT, "%s", cap); + snprintf(mi->caption, FRM_MAX_TEXT, "%s", cap); } if (nam) { - snprintf(mi->name, DSGN_MAX_NAME, "%s", nam); + snprintf(mi->name, FRM_MAX_NAME, "%s", nam); } + const char *key = wgtGetText(sMed.shortcutInput); + const char *keyText = wgtGetText(sMed.shortcutTextInput); + + snprintf(mi->shortcut, sizeof(mi->shortcut), "%s", key ? key : ""); + snprintf(mi->shortcutText, sizeof(mi->shortcutText), "%s", keyText ? keyText : ""); + // Auto-generate name from caption if name is empty or was auto-generated if ((mi->name[0] == '\0' || sMed.nameAutoGen) && mi->caption[0] != '\0') { - char autoName[DSGN_MAX_NAME]; - - if (mi->caption[0] == '-') { - // Separator: generate mnuSep1, mnuSep2, etc. - int32_t sepNum = 1; - int32_t itemCount = (int32_t)arrlen(sMed.items); - - for (int32_t i = 0; i < itemCount; i++) { - if (i != sMed.selectedIdx && strncasecmp(sMed.items[i].name, MED_SEP_PREFIX, MED_SEP_PREFIX_LEN) == 0) { - int32_t n = atoi(sMed.items[i].name + MED_SEP_PREFIX_LEN); - - if (n >= sepNum) { - sepNum = n + 1; - } - } - } - - snprintf(autoName, DSGN_MAX_NAME, MED_SEP_PREFIX "%d", (int)sepNum); - } else { - // Normal item: strip & and non-alphanumeric, prefix "mnu" - int32_t p = snprintf(autoName, DSGN_MAX_NAME, "%s", MED_NAME_PREFIX); - - for (const char *c = mi->caption; *c && p < DSGN_MAX_NAME - 1; c++) { - if (*c == '&') { - continue; - } - - if ((*c >= 'A' && *c <= 'Z') || (*c >= 'a' && *c <= 'z') || (*c >= '0' && *c <= '9')) { - autoName[p++] = *c; - } - } - - autoName[p] = '\0'; - } - - snprintf(mi->name, DSGN_MAX_NAME, "%s", autoName); + frmMenuAutoName(sMed.items, sMed.selectedIdx, mi->name, sizeof(mi->name)); wgtSetText(sMed.nameInput, mi->name); sMed.nameAutoGen = true; } @@ -197,43 +162,6 @@ static void applyFields(void) { } -static int32_t findSubtreeEnd(int32_t idx) { - int32_t count = (int32_t)arrlen(sMed.items); - int32_t level = sMed.items[idx].level; - int32_t end = idx + 1; - - while (end < count && sMed.items[end].level > level) { - end++; - } - - return end; -} - - -// 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) { int32_t count = (int32_t)arrlen(sMed.items); @@ -244,6 +172,8 @@ static void loadFields(void) { if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) { wgtSetText(sMed.captionInput, ""); wgtSetText(sMed.nameInput, ""); + wgtSetText(sMed.shortcutInput, ""); + wgtSetText(sMed.shortcutTextInput, ""); wgtCheckboxSetChecked(sMed.checkedCb, false); wgtCheckboxSetChecked(sMed.radioCheckCb, false); wgtCheckboxSetChecked(sMed.enabledCb, true); @@ -253,10 +183,12 @@ static void loadFields(void) { return; } - DsgnMenuItemT *mi = &sMed.items[sMed.selectedIdx]; + FrmMenuItemT *mi = &sMed.items[sMed.selectedIdx]; sMed.nameAutoGen = (mi->name[0] == '\0'); wgtSetText(sMed.captionInput, mi->caption); wgtSetText(sMed.nameInput, mi->name); + wgtSetText(sMed.shortcutInput, mi->shortcut); + wgtSetText(sMed.shortcutTextInput, mi->shortcutText); wgtCheckboxSetChecked(sMed.checkedCb, mi->checked); wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck); wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled); @@ -267,7 +199,7 @@ static void loadFields(void) { } -bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { +bool mnuEditorDialog(AppContextT *ctx, FrmModelT *form) { memset(&sMed, 0, sizeof(sMed)); sMed.ctx = ctx; sMed.form = form; @@ -278,7 +210,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { int32_t count = (int32_t)arrlen(form->menuItems); for (int32_t i = 0; i < count; i++) { - DsgnMenuItemT mi = form->menuItems[i]; + FrmMenuItemT mi = form->menuItems[i]; mi.id = i + 1; arrput(sMed.items, mi); @@ -286,8 +218,8 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { // If empty, start with one blank item so the user can type immediately if (arrlen(sMed.items) == 0) { - DsgnMenuItemT mi; - dsgnMenuItemInit(&mi); + FrmMenuItemT mi; + frmMenuItemInit(&mi); arrput(sMed.items, mi); } @@ -313,7 +245,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { capRow->spacing = wgtPixels(MED_ROW_SPACING); WidgetT *capLbl = wgtLabel(capRow, "Caption:"); capLbl->minW = wgtPixels(MED_LABEL_W); - sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT - 1); + sMed.captionInput = wgtTextInput(capRow, FRM_MAX_TEXT - 1); sMed.captionInput->weight = WGT_WEIGHT_FILL; sMed.captionInput->onChange = onCaptionChange; @@ -322,10 +254,23 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { namRow->spacing = wgtPixels(MED_ROW_SPACING); WidgetT *namLbl = wgtLabel(namRow, "Name:"); namLbl->minW = wgtPixels(MED_LABEL_W); - sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME - 1); + sMed.nameInput = wgtTextInput(namRow, FRM_MAX_NAME - 1); sMed.nameInput->weight = WGT_WEIGHT_FILL; sMed.nameInput->onChange = onNameChange; + // Shortcut row: the key that runs the item, and key text shown only + WidgetT *keyRow = wgtHBox(root); + keyRow->spacing = wgtPixels(MED_ROW_SPACING); + WidgetT *keyLbl = wgtLabel(keyRow, "Shortcut:"); + keyLbl->minW = wgtPixels(MED_LABEL_W); + sMed.shortcutInput = wgtTextInput(keyRow, BAS_MENU_KEY_TEXT_MAX - 1); + sMed.shortcutInput->weight = WGT_WEIGHT_FILL; + sMed.shortcutInput->onChange = onShortcutChange; + wgtLabel(keyRow, "Text:"); + sMed.shortcutTextInput = wgtTextInput(keyRow, BAS_MENU_KEY_TEXT_MAX - 1); + sMed.shortcutTextInput->weight = WGT_WEIGHT_FILL; + sMed.shortcutTextInput->onChange = onShortcutChange; + // Check row WidgetT *chkRow = wgtHBox(root); chkRow->spacing = wgtPixels(MED_CHECK_SPACING); @@ -421,31 +366,6 @@ 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) { (void)w; sMed.accepted = false; @@ -477,13 +397,8 @@ static void onDelete(WidgetT *w) { applyFields(); - // Delete selected item and its children - int32_t subEnd = findSubtreeEnd(sMed.selectedIdx); - - for (int32_t i = subEnd - 1; i >= sMed.selectedIdx; i--) { - arrdel(sMed.items, i); - } - + // The selected item goes with its children + frmMenuRemove(&sMed.items, sMed.selectedIdx); count = (int32_t)arrlen(sMed.items); if (sMed.selectedIdx >= count) { @@ -498,61 +413,28 @@ static void onDelete(WidgetT *w) { static void onIndent(WidgetT *w) { (void)w; - int32_t count = (int32_t)arrlen(sMed.items); - int32_t idx = sMed.selectedIdx; - - if (idx <= 0 || idx >= count) { - return; - } - applyFields(); - // Can only indent if previous item is at same or higher level - int32_t prevLevel = sMed.items[idx - 1].level; - - if (sMed.items[idx].level > prevLevel) { - return; // already deeper than prev + if (frmMenuIndent(sMed.items, sMed.selectedIdx, 1)) { + rebuildList(); } - - // The whole subtree moves one level down, so its deepest item must - // still be within the .frm nesting limit afterwards. - 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++) { - sMed.items[i].level++; - } - - rebuildList(); } +// A new item after the current item's subtree, at the same level. static void onInsert(WidgetT *w) { (void)w; int32_t count = (int32_t)arrlen(sMed.items); + bool valid = sMed.selectedIdx >= 0 && sMed.selectedIdx < count; applyFields(); - DsgnMenuItemT mi; - dsgnMenuItemInit(&mi); + int32_t at = frmMenuInsert(&sMed.items, valid ? frmMenuSubtreeEnd(sMed.items, sMed.selectedIdx) : count, valid ? sMed.items[sMed.selectedIdx].level : 0); - // Insert after the current item's subtree, at the same level - int32_t insertAt; - - if (sMed.selectedIdx >= 0 && sMed.selectedIdx < count) { - mi.level = sMed.items[sMed.selectedIdx].level; - insertAt = findSubtreeEnd(sMed.selectedIdx); - } else { - insertAt = count; + if (at >= 0) { + sMed.selectedIdx = at; } - arrins(sMed.items, insertAt, mi); - sMed.selectedIdx = insertAt; rebuildList(); loadFields(); wgtSetFocused(sMed.captionInput); @@ -577,59 +459,27 @@ static void onListClick(WidgetT *w) { static void onMoveDown(WidgetT *w) { (void)w; - int32_t count = (int32_t)arrlen(sMed.items); - int32_t idx = sMed.selectedIdx; - - if (idx < 0 || idx >= count) { - return; - } - applyFields(); - int32_t subEnd = findSubtreeEnd(idx); + int32_t to = frmMenuMove(&sMed.items, sMed.selectedIdx, 1); - // The item after our subtree must be our next sibling; a shallower - // 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) { - return; + if (to >= 0) { + sMed.selectedIdx = to; + rebuildList(); } - - // Move our subtree after the next sibling's subtree - int32_t nextEnd = findSubtreeEnd(subEnd); - - moveSubtree(idx, subEnd, nextEnd); - rebuildList(); } static void onMoveUp(WidgetT *w) { (void)w; - int32_t idx = sMed.selectedIdx; - - if (idx <= 0) { - return; - } - applyFields(); - // Find the start of the previous subtree at the same or lower level - int32_t prevIdx = idx - 1; + int32_t to = frmMenuMove(&sMed.items, sMed.selectedIdx, -1); - while (prevIdx > 0 && sMed.items[prevIdx].level > sMed.items[idx].level) { - prevIdx--; + if (to >= 0) { + sMed.selectedIdx = to; + rebuildList(); } - - // No previous sibling at the same level (e.g. selected item is the first - // child of its parent) -- Move Up would insert the subtree before its - // parent and corrupt nesting, so do nothing. - if (sMed.items[prevIdx].level != sMed.items[idx].level) { - return; - } - - // Move our subtree before the previous sibling's subtree - moveSubtree(idx, findSubtreeEnd(idx), prevIdx); - rebuildList(); } @@ -650,16 +500,12 @@ static void onNext(WidgetT *w) { if (sMed.selectedIdx < count - 1) { sMed.selectedIdx++; } else { - // Append new item - DsgnMenuItemT mi; - dsgnMenuItemInit(&mi); + // A new item at the end, at the last item's level + int32_t at = frmMenuInsert(&sMed.items, count, count > 0 ? sMed.items[count - 1].level : 0); - if (count > 0) { - mi.level = sMed.items[count - 1].level; + if (at >= 0) { + sMed.selectedIdx = at; } - - arrput(sMed.items, mi); - sMed.selectedIdx = (int32_t)arrlen(sMed.items) - 1; } rebuildList(); @@ -668,48 +514,18 @@ static void onNext(WidgetT *w) { } +// The levels form a tree, and every name is an identifier, unique among +// the items and not the form's or a control's (a name with spaces would +// be written as "Begin Menu File Menu" and mis-parsed); every shortcut is +// a key. Then the items replace the form's, ContextMenu bindings +// following renamed items. static void onOk(WidgetT *w) { (void)w; applyFields(); - stripBlankItems(); + frmMenuStripBlank(&sMed.items); - // Validate: the levels form a tree, and 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 badIdx = validateLevels(); - const char *reason = NULL; - 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++) { - const char *name = sMed.items[i].name; - - if (name[0] == '\0') { - badIdx = i; - reason = "All menu items must have a Name."; - } else if (!basIsValidIdent(name)) { - badIdx = i; - snprintf(msg, sizeof(msg), "Menu name must be an identifier (letters, digits, underscores): %s", name); - reason = msg; - } else if (dsgnNameInUse(sMed.form, name, NULL, false)) { - badIdx = i; - snprintf(msg, sizeof(msg), "Menu name is already used by the form or a control: %s", name); - reason = msg; - } - - for (int32_t j = i + 1; j < count && badIdx < 0; j++) { - if (strcasecmp(name, sMed.items[j].name) == 0) { - badIdx = j; - snprintf(msg, sizeof(msg), "Duplicate menu name: %s", name); - reason = msg; - } - } - } + char reason[MED_MSG_BUF]; + int32_t badIdx = frmModelMenuValidate(sMed.form, sMed.items, basIsValidIdent, reason, sizeof(reason)); if (badIdx >= 0) { dvxErrorBox(sMed.ctx, MED_DLG_TITLE, reason); @@ -720,14 +536,9 @@ static void onOk(WidgetT *w) { return; } - followMenuRenames(); - - // Copy working items back to form - arrfree(sMed.form->menuItems); - sMed.form->menuItems = NULL; - - for (int32_t i = 0; i < count; i++) { - arrput(sMed.form->menuItems, sMed.items[i]); + if (!frmModelMenuReplace(sMed.form, sMed.items)) { + dvxErrorBox(sMed.ctx, MED_DLG_TITLE, "Not enough memory to apply the menus."); + return; } sMed.accepted = true; @@ -737,26 +548,17 @@ static void onOk(WidgetT *w) { static void onOutdent(WidgetT *w) { (void)w; - int32_t count = (int32_t)arrlen(sMed.items); - int32_t idx = sMed.selectedIdx; - - if (idx < 0 || idx >= count) { - return; - } - - if (sMed.items[idx].level <= 0) { - return; - } - applyFields(); - int32_t subEnd = findSubtreeEnd(idx); - - for (int32_t i = idx; i < subEnd; i++) { - sMed.items[i].level--; + if (frmMenuIndent(sMed.items, sMed.selectedIdx, -1)) { + rebuildList(); } +} - rebuildList(); + +static void onShortcutChange(WidgetT *w) { + (void)w; + applyFields(); } @@ -804,57 +606,3 @@ static void rebuildList(void) { } -// 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; -} diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.h b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.h index ab9cbe40..230426e5 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.h +++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.h @@ -23,7 +23,7 @@ // ideMenuEditor.h -- DVX BASIC menu editor dialog // // Modal dialog for designing menu bars on forms. Edits the -// DsgnMenuItemT array on a DsgnFormT. Returns true if the +// FrmMenuItemT array on a FrmModelT. Returns true if the // user clicked OK and the menu data was modified. #ifndef IDE_MENUEDITOR_H @@ -37,8 +37,8 @@ // 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 +// by id, whatever was inserted, deleted or moved); FRM_NEW_MENU_ID marks // an item added in the dialog. -bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form); +bool mnuEditorDialog(AppContextT *ctx, FrmModelT *form); #endif // IDE_MENUEDITOR_H diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c index f146a8f9..58c7c807 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c @@ -20,29 +20,12 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// ideProject.c -- DVX BASIC project file management and project window +// ideProject.c -- DVX BASIC IDE: the Project window and the Project +// Properties dialog // -// The .dbp (DVX BASIC Project) file is INI-format: -// -// [Project] -// Name = MyProject -// -// [Modules] -// File0 = MAIN.BAS -// File1 = UTILS.BAS -// -// [Classes] -// File0 = STACK.CLS -// -// [Forms] -// File0 = FORM1.FRM -// -// [Settings] -// StartupForm = Form1 -// -// All file paths are relative to the directory containing the .dbp file. -// Uses the handle-based dvxPrefs API with a dedicated handle per load/save -// so project files don't interfere with the IDE's own preferences. +// The project model itself (PrjStateT, the .dbp format, load and save) +// is basdev/basProject.c, shared with BASIC programs through +// compiler.bas. #include "ideProject.h" #include "../basRes.h" @@ -77,8 +60,6 @@ #define PRJ_MSG_BUF 128 // error-message scratch buffer #define PRJ_MODIFIED_SUFFIX " *" // tree label suffix for an unsaved file #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_FILE_KEY_FMT "File%d" // INI key for an indexed module/class/form path #define PPD_WIDTH 380 #define PPD_HEIGHT 410 @@ -88,6 +69,19 @@ #define PPD_ROOT_SPACING 2 #define PPD_ROW_SPACING 4 +// Project Properties' Type rows: a program ([Project] Type empty) or a +// control project (BAS_PROJECT_TYPE_CONTROL) +typedef enum { + PpdTypeProgramE, + PpdTypeControlE, + PpdTypeCountE +} PpdTypeE; + +static const char *const sPpdTypes[PpdTypeCountE] = { + [PpdTypeProgramE] = "Program (makes an .app)", + [PpdTypeControlE] = "Control (makes a .wgt)" +}; + // ============================================================ // Module state // ============================================================ @@ -99,27 +93,22 @@ static PrjFileClickFnT sOnClick = NULL; static PrjSelChangeFnT sOnSelChange = NULL; static char **sLabels = NULL; // stb_ds array of strdup'd strings -// Per file kind: the .dbp section listing its files and its Project -// window group. -static const char *const sKindSections[PrjKindCountE] = { - [PrjKindModuleE] = BAS_INI_SECTION_MODULES, - [PrjKindClassE] = BAS_INI_SECTION_CLASSES, - [PrjKindFormE] = BAS_INI_SECTION_FORMS -}; - +// Per file kind: its Project window group. static const char *const sKindGroups[PrjKindCountE] = { - [PrjKindModuleE] = "Modules", - [PrjKindClassE] = "Classes", - [PrjKindFormE] = "Forms" + [PrjKindModuleE] = "Modules", + [PrjKindClassE] = "Classes", + [PrjKindControlE] = "Controls", + [PrjKindFormE] = "Forms" }; // Project window group order -static const PrjFileKindE sGroupOrder[PrjKindCountE] = { PrjKindFormE, PrjKindModuleE, PrjKindClassE }; +static const PrjFileKindE sGroupOrder[PrjKindCountE] = { PrjKindFormE, PrjKindModuleE, PrjKindClassE, PrjKindControlE }; static struct { bool done; bool accepted; WidgetT *name; + WidgetT *type; WidgetT *author; WidgetT *publisher; WidgetT *version; @@ -128,6 +117,7 @@ static struct { WidgetT *commandArgs; WidgetT *startupForm; WidgetT *optionExplicit; + WidgetT *multiInstance; const char **formNames; // stb_ds array of form name strings for startup dropdown WidgetT *helpFileInput; WidgetT *iconPreview; @@ -141,26 +131,25 @@ static struct { // Prototypes // ============================================================ -static void onPrjWinClose(WindowT *win); -static void onTreeItemDblClick(WidgetT *w); -static void onTreeSelChanged(WidgetT *w); -static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t fieldSize); -static void ppdLoadIconPreview(void); -static void ppdOnBrowseHelp(WidgetT *w); -static void ppdOnBrowseIcon(WidgetT *w); -static void ppdOnCancel(WidgetT *w); -static void ppdOnClose(WindowT *win); -static void ppdOnOk(WidgetT *w); -static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize); -static void prjDeriveDir(PrjStateT *prj, const char *dbpPath); -static int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path); -static const char *prjKindExt(PrjFileKindE kind); -static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind); -static void prjLoadString(PrefsHandleT *h, const char *section, const char *key, char *dst, int32_t dstSize); +static void onPrjWinClose(WindowT *win); +static void onTreeItemDblClick(WidgetT *w); +static void onTreeSelChanged(WidgetT *w); +static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t fieldSize); +static void ppdLoadIconPreview(void); +static void ppdOnBrowseHelp(WidgetT *w); +static void ppdOnBrowseIcon(WidgetT *w); +static void ppdOnCancel(WidgetT *w); +static void ppdOnClose(WindowT *win); +static void ppdOnOk(WidgetT *w); +static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize); +void prjApplyDefaults(PrjStateT *prj, PrefsHandleT *prefs); +WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange); +void prjDestroyWindow(AppContextT *ctx, WindowT *win); +int32_t prjGetSelectedFileIdx(void); static const char *prjMakeRelative(const PrjStateT *prj, const char *path); -static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKindE kind); -static void prjSaveOptional(PrefsHandleT *h, const char *key, const char *value); -static bool validateIcon(const char *fullPath, bool showErrors); +bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath); +void prjRebuildTree(PrjStateT *prj); +static bool validateIcon(const char *fullPath, bool showErrors); static void onPrjWinClose(WindowT *win) { @@ -361,47 +350,17 @@ static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize) { } -int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind) { - PrjFileT entry; - memset(&entry, 0, sizeof(entry)); - snprintf(entry.path, sizeof(entry.path), "%s", relativePath); - entry.kind = (uint8_t)kind; - arrput(prj->files, entry); - prj->dirty = true; - return prjFileCount(prj) - 1; -} - - -int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine) { - int32_t codePos = basClassCodePos(text, len); - int32_t line = 1; - - for (int32_t i = 0; i < codePos; i++) { - if (text[i] == '\n') { - line++; - } +void prjApplyDefaults(PrjStateT *prj, PrefsHandleT *prefs) { + if (!prefs) { + return; } - *outFirstLine = line; - return codePos; -} - - -bool prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize) { - const char *src = text ? text : ""; - - return basExtractClassName(src, (int32_t)strlen(src), prj->files[fileIdx].path, out, outSize); -} - - -void prjClose(PrjStateT *prj) { - for (int32_t i = 0; i < prjFileCount(prj); i++) { - free(prj->files[i].buffer); - } - - arrfree(prj->files); - arrfree(prj->sourceMap); - prjInit(prj); + snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, "")); + snprintf(prj->publisher, sizeof(prj->publisher), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, "")); + snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, IDE_PREF_DEFAULT_VERSION)); + snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, "")); + snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, "")); + prj->optionExplicit = prefsGetBool(prefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, false); } @@ -428,21 +387,6 @@ WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onCli } -// Set prj->projectDir to the directory containing dbpPath, or "." when -// dbpPath has no directory component. -static void prjDeriveDir(PrjStateT *prj, const char *dbpPath) { - snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath); - char *sep = platformPathDirEnd(prj->projectDir); - - if (sep) { - *sep = '\0'; - } else { - prj->projectDir[0] = '.'; - prj->projectDir[1] = '\0'; - } -} - - void prjDestroyWindow(AppContextT *ctx, WindowT *win) { if (win) { dvxDestroyWindow(ctx, win); @@ -466,33 +410,6 @@ 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 (strcasecmp(prj->files[i].path, path) == 0) { - return i; - } - } - - return -1; -} - - -void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) { - if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) { - outPath[0] = '\0'; - return; - } - - snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", prj->projectDir, prj->files[fileIdx].path); -} - - int32_t prjGetSelectedFileIdx(void) { if (!sTree) { return -1; @@ -508,146 +425,6 @@ int32_t prjGetSelectedFileIdx(void) { } -void prjInit(PrjStateT *prj) { - memset(prj, 0, sizeof(*prj)); - prj->activeFileIdx = -1; -} - - -// The file extension of a kind of project file. -static const char *prjKindExt(PrjFileKindE kind) { - if (kind == PrjKindFormE) { - return PRJ_FRM_EXT; - } - - return (kind == PrjKindClassE) ? BAS_CLS_EXT : PRJ_BAS_EXT; -} - - -PrjFileKindE prjKindForPath(const char *path) { - const char *base = platformPathBaseName(path); - - if (dvxHasExt(base, PRJ_FRM_EXT)) { - return PrjKindFormE; - } - - if (dvxHasExt(base, BAS_CLS_EXT)) { - return PrjKindClassE; - } - - return PrjKindModuleE; -} - - -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); - - if (!h) { - return false; - } - - // Release whatever project was open (file buffers, arrays) before - // starting from a blank state. - prjClose(prj); - snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath); - - // Derive project directory - prjDeriveDir(prj, dbpPath); - - // [Project] section - 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)); - prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, prj->publisher, sizeof(prj->publisher)); - 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)); - prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, prj->description, sizeof(prj->description)); - 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)); - prjLoadString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COMMANDARGS, prj->commandArgs, sizeof(prj->commandArgs)); - - // [Modules], [Classes] and [Forms] sections -- File0, File1, ... (no cap) - for (int32_t kind = 0; kind < PrjKindCountE; kind++) { - prjLoadFileSection(prj, h, (PrjFileKindE)kind); - } - - // [Settings] section - prjLoadString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm, sizeof(prj->startupForm)); - - prj->optionExplicit = prefsGetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false); - - prefsClose(h); - prj->dirty = false; - return true; -} - - -// prjLoadAllFiles -- read all project files into memory buffers -// and extract form names from .frm files. -void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) { - for (int32_t i = 0; i < prjFileCount(prj); i++) { - if (prj->files[i].buffer) { - continue; // already loaded - } - - // An unreadable or oversized file stays unloaded; the IDE reports - // it when the file is opened or compiled. - char *buf = prjReadFileText(prj, i, NULL); - - if (!buf) { - continue; - } - - prj->files[i].buffer = buf; - - // Extract form name from .frm files - if (prj->files[i].kind == PrjKindFormE) { - basExtractFormName(buf, prj->files[i].formName, sizeof(prj->files[i].formName)); - } - - // Yield between files to keep the UI responsive - if (ctx) { - dvxUpdate(ctx); - } - } -} - - -// Load File0, File1, ... entries from the INI section of kind, registering -// each as a project file. The loop ends at the first missing key (no -// fixed cap). -static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind) { - for (int32_t i = 0; ; i++) { - char key[PRJ_INI_KEY_LEN]; - snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)i); - - const char *val = prefsGetString(h, sKindSections[kind], key, NULL); - - if (!val) { - break; - } - - prjAddFile(prj, val, kind); - } -} - - -// 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 // (callers apply their own fallback). static const char *prjMakeRelative(const PrjStateT *prj, const char *path) { @@ -662,76 +439,6 @@ static const char *prjMakeRelative(const PrjStateT *prj, const char *path) { } -int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine) { - if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) { - return 0; - } - - for (int32_t i = 0; i < (int32_t)arrlen(prj->sourceMap); i++) { - const BasSourceMapT *m = &prj->sourceMap[i]; - - if (strcmp(m->path, prj->files[fileIdx].path) == 0) { - int32_t offset = fileLine - m->firstFileLine; - - return (offset >= 0 && offset < m->lineCount) ? m->startLine + offset : 0; - } - } - - return 0; -} - - -bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) { - const char *path = NULL; - int32_t localLine = 0; - - if (!basBuildMapLine(prj->sourceMap, concatLine, &path, &localLine)) { - return false; - } - - int32_t fileIdx = prjFileIndexByPath(prj, path); - - if (fileIdx < 0) { - return false; - } - - *outFileIdx = fileIdx; - *outLocalLine = localLine; - return true; -} - - -void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs) { - prjInit(prj); - snprintf(prj->name, sizeof(prj->name), "%s", name); - snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory); - snprintf(prj->projectPath, sizeof(prj->projectPath), "%s" DVX_PATH_SEP "%s.dbp", directory, name); - prj->dirty = true; - - // Apply defaults from preferences - if (prefs) { - snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, "")); - snprintf(prj->publisher, sizeof(prj->publisher), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, "")); - snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, IDE_PREF_DEFAULT_VERSION)); - snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, "")); - snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, "")); - prj->optionExplicit = prefsGetBool(prefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, false); - } -} - - -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) { if (!ctx || !prj) { return false; @@ -764,6 +471,21 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) root->spacing = wgtPixels(PPD_ROOT_SPACING); sPpd.name = ppdAddRow(root, "Name:", prj->name, sizeof(prj->name)); + + // What the project makes + { + WidgetT *typeRow = wgtHBox(root); + typeRow->spacing = wgtPixels(PPD_ROW_SPACING); + + WidgetT *typeLbl = wgtLabel(typeRow, "Type:"); + typeLbl->minW = wgtPixels(PPD_LABEL_W); + + sPpd.type = wgtDropdown(typeRow); + sPpd.type->weight = WGT_WEIGHT_FILL; + wgtDropdownSetItems(sPpd.type, (const char **)sPpdTypes, PpdTypeCountE); + wgtDropdownSetSelected(sPpd.type, prjIsControlProject(prj) ? PpdTypeControlE : PpdTypeProgramE); + } + sPpd.author = ppdAddRow(root, "Author:", prj->author, sizeof(prj->author)); sPpd.publisher = ppdAddRow(root, "Publisher:", prj->publisher, sizeof(prj->publisher)); sPpd.version = ppdAddRow(root, "Version:", prj->version, sizeof(prj->version)); @@ -867,6 +589,10 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)"); wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit); + // More than one copy of the built program at a time + sPpd.multiInstance = wgtCheckbox(root, "Allow more than one copy to run at once"); + wgtCheckboxSetChecked(sPpd.multiInstance, prj->multiInstance); + // Description: one line, since it is stored as a single INI value. sPpd.description = ppdAddRow(root, "Description:", prj->description, sizeof(prj->description)); @@ -917,6 +643,8 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) } prj->optionExplicit = wgtCheckboxIsChecked(sPpd.optionExplicit); + prj->multiInstance = wgtCheckboxIsChecked(sPpd.multiInstance); + prjFieldSet(prj, BAS_INI_KEY_TYPE, (wgtDropdownGetSelected(sPpd.type) == PpdTypeControlE) ? BAS_PROJECT_TYPE_CONTROL : ""); prj->dirty = true; } @@ -934,52 +662,6 @@ 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) { if (!sTree) { return; @@ -1035,199 +717,9 @@ void prjRebuildTree(PrjStateT *prj) { } -void prjRemoveFile(PrjStateT *prj, int32_t idx) { - if (idx < 0 || idx >= prjFileCount(prj)) { - return; - } - - // A removed startup form is no longer a valid startup form. - if (prj->files[idx].kind == PrjKindFormE && 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); - arrdel(prj->files, idx); - - // Adjust active file index - if (prj->activeFileIdx == idx) { - prj->activeFileIdx = -1; - } else if (prj->activeFileIdx > idx) { - prj->activeFileIdx--; - } - - prj->dirty = true; -} - - -bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize) { - PrjFileT *file = &prj->files[idx]; - const char *ext = prjKindExt((PrjFileKindE)file->kind); - const char *dot = strrchr(newName, '.'); - char name[DVX_MAX_PATH]; - - if (strpbrk(newName, "/\\:") != NULL) { - snprintf(err, errSize, "Give a file name only; the file stays in the project's directory."); - return false; - } - - if (dot && strcasecmp(dot, ext) != 0) { - snprintf(err, errSize, "%s must keep the %s extension.", file->path, ext); - return false; - } - - if (snprintf(name, sizeof(name), "%s%s", newName, dot ? "" : ext) >= (int32_t)sizeof(name)) { - snprintf(err, errSize, "The name is too long."); - return false; - } - - const char *invalid = platformValidateFilename(name); - - if (invalid) { - snprintf(err, errSize, "%s", invalid); - return false; - } - - if (strcasecmp(name, file->path) == 0) { - return true; - } - - if (prjFileIndexByPath(prj, name) >= 0) { - snprintf(err, errSize, "%s is already in the project.", name); - return false; - } - - char oldPath[DVX_MAX_PATH]; - char oldFull[DVX_MAX_PATH]; - char newFull[DVX_MAX_PATH * 2]; - - snprintf(oldPath, sizeof(oldPath), "%s", file->path); - prjFullPath(prj, idx, oldFull, sizeof(oldFull)); - snprintf(newFull, sizeof(newFull), "%s" DVX_PATH_SEP "%s", prj->projectDir, name); - - if (prjPathExists(newFull)) { - snprintf(err, errSize, "%s already exists.", name); - return false; - } - - // A file never saved exists only in memory: nothing to rename on disk - bool onDisk = prjPathExists(oldFull); - - if (onDisk && rename(oldFull, newFull) != 0) { - snprintf(err, errSize, "Could not rename %s to %s.", oldPath, name); - return false; - } - - snprintf(file->path, sizeof(file->path), "%s", name); - - if (!prjSave(prj)) { - snprintf(file->path, sizeof(file->path), "%s", oldPath); - - if (onDisk) { - rename(newFull, oldFull); - } - - snprintf(err, errSize, "Could not write the project file; %s keeps its name.", oldPath); - return false; - } - - // The source map is keyed by path - for (int32_t i = 0; i < (int32_t)arrlen(prj->sourceMap); i++) { - if (strcmp(prj->sourceMap[i].path, oldPath) == 0) { - snprintf(prj->sourceMap[i].path, sizeof(prj->sourceMap[i].path), "%s", name); - } - } - - prj->dirty = false; - return true; -} - - -bool prjSave(const PrjStateT *prj) { - if (prj->projectPath[0] == '\0') { - return false; - } - - PrefsHandleT *h = prefsCreate(); - - if (!h) { - return false; - } - - // [Project] section - prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, prj->name); - prjSaveOptional(h, BAS_INI_KEY_AUTHOR, prj->author); - prjSaveOptional(h, BAS_INI_KEY_PUBLISHER, prj->publisher); - prjSaveOptional(h, BAS_INI_KEY_VERSION, prj->version); - prjSaveOptional(h, BAS_INI_KEY_COPYRIGHT, prj->copyright); - prjSaveOptional(h, BAS_INI_KEY_DESCRIPTION, prj->description); - prjSaveOptional(h, BAS_INI_KEY_ICON, prj->iconPath); - prjSaveOptional(h, BAS_INI_KEY_HELPFILE, prj->helpFile); - prjSaveOptional(h, BAS_INI_KEY_COMMANDARGS, prj->commandArgs); - - // [Modules], [Classes] and [Forms] sections - for (int32_t kind = 0; kind < PrjKindCountE; kind++) { - prjSaveFileSection(h, prj, (PrjFileKindE)kind); - } - - // [Settings] section - prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm); - prefsSetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, prj->optionExplicit); - - bool ok = prefsSaveAs(h, prj->projectPath); - prefsClose(h); - return ok; -} - - -// Write the project files of kind into its INI section as File0, File1, -// ... (readers stop at the first missing key). -static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKindE kind) { - int32_t idx = 0; - - for (int32_t i = 0; i < prjFileCount(prj); i++) { - if (prj->files[i].kind == kind) { - char key[PRJ_INI_KEY_LEN]; - snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++); - prefsSetString(h, sKindSections[kind], key, prj->files[i].path); - } - } -} - - _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'; -} - - // validateIcon -- check that an image file is a valid PRJ_ICON_SIZE square icon. // Returns true if valid. Shows an error dialog and returns false if not. static bool validateIcon(const char *fullPath, bool showErrors) { diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.h b/src/apps/kpunch/dvxbasic/ide/ideProject.h index 740701ce..80c2e181 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.h +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.h @@ -25,12 +25,10 @@ #ifndef IDE_PROJECT_H #define IDE_PROJECT_H +#include "../basdev/basProject.h" +#include "../formrt/formrt.h" #include "dvxApp.h" #include "dvxPrefs.h" -#include "dvxTypes.h" -#include "../basBuild.h" -#include "../basRes.h" -#include "../formrt/formrt.h" #include #include @@ -39,14 +37,6 @@ // Constants // ============================================================ -#define PRJ_MAX_NAME 32 -#define PRJ_MAX_SOURCE 65536 // a project file must be smaller than this to be loaded or edited -#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 preferences dialog (ideMain.c). #define IDE_PREF_SECTION_DEFAULTS "defaults" @@ -59,127 +49,12 @@ #define IDE_PREF_SECTION_EDITOR "editor" #define IDE_PREF_KEY_OPTION_EXPLICIT "optionExplicit" -// Form and module file extensions; a class module's is BAS_CLS_EXT -// (basRes.h). A file's kind follows its extension (see prjKindForPath). -#define PRJ_FRM_EXT ".frm" -#define PRJ_BAS_EXT ".bas" +// The project model (PrjStateT, prjLoad, prjSave ...) is basProject.h in +// the basdev library; this is the IDE's window and dialogs around it. -// ============================================================ -// Project file kinds -// ============================================================ - -// The order is the compile order: modules, then class modules, then -// forms (see basBuildConcatProject). -typedef enum { - PrjKindModuleE, // .bas: [Modules] - PrjKindClassE, // .cls: [Classes] - PrjKindFormE, // .frm: [Forms] - PrjKindCountE -} PrjFileKindE; - -// ============================================================ -// Project file entry -// ============================================================ - -typedef struct { - // Fields are ordered by alignment to minimize struct padding. - char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded) - char path[DVX_MAX_PATH]; // relative path (8.3 DOS name) - char formName[BAS_MAX_IDENT]; // form object name (from "Begin Form ") - uint8_t kind; // PrjFileKindE - bool modified; // true = buffer has unsaved changes -} PrjFileT; - -// ============================================================ -// Project state -// ============================================================ - -typedef struct { - // Fields are ordered by alignment to minimize struct padding. - PrjFileT *files; // stb_ds dynamic array (prjFileCount) - BasSourceMapT *sourceMap; // stb_ds array: path = files[].path, firstFileLine = first code line - int32_t activeFileIdx; // index of file open in editor (-1 = none) - char name[PRJ_MAX_NAME]; - char projectPath[DVX_MAX_PATH]; // full path to .dbp file - char projectDir[DVX_MAX_PATH]; // directory containing .dbp - char startupForm[BAS_MAX_IDENT]; - // Project metadata (for binary generation) - char author[PRJ_MAX_STRING]; - char publisher[PRJ_MAX_STRING]; - char version[PRJ_MAX_NAME]; - char copyright[PRJ_MAX_STRING]; - char description[PRJ_MAX_DESC]; - char iconPath[DVX_MAX_PATH]; // relative path to icon BMP - char helpFile[DVX_MAX_PATH]; // relative path to .hlp file - char commandArgs[PRJ_MAX_ARGS]; // COMMAND$ for runs from the IDE (not built into the program) - bool optionExplicit; // require DIM before use - bool dirty; -} PrjStateT; - -// ============================================================ -// Project management -// ============================================================ - -int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind); - -// Offset of the first line of code in the text of a class module (len -// bytes), just past its header; *outFirstLine receives that line's -// 1-based number. -int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine); - -// The class name of project file fileIdx (a class module) given its -// text: its Attribute VB_Name, else its file name. False when neither -// gives one. -bool prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize); -void prjClose(PrjStateT *prj); -int32_t prjFileCount(const PrjStateT *prj); -void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize); -void prjInit(PrjStateT *prj); - -// The kind of source file path names, by its file name's extension. -PrjFileKindE prjKindForPath(const char *path); -bool prjLoad(PrjStateT *prj, const char *dbpPath); -void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx); -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); - -// Rename project file idx to newName, a file name in the project's -// directory whose extension, when given, is the one of the file's kind -// (none: it is added). The file on disk (when it has been saved) is -// renamed and the project file (.dbp) written at once so the two agree; -// on any failure both are put back. An existing file is never -// overwritten. A name that differs only in case changes nothing (DOS -// names ignore case). False with a message in err (errSize bytes). -bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize); -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 -// ============================================================ - -// Returns true if the line was found in the map. Sets outFileIdx and -// outLocalLine to the originating file and line within that file. -bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine); - -// The concatenated line holding line fileLine of file fileIdx, or 0 when -// the file is not in the map (not compiled since it was added) or the -// line is not compiled code (e.g. a class module's header). -int32_t prjMapFileLine(const PrjStateT *prj, int32_t fileIdx, int32_t fileLine); +// Fill a new project's metadata and Option Explicit from the IDE's +// [defaults] / [editor] preferences. +void prjApplyDefaults(PrjStateT *prj, PrefsHandleT *prefs); // ============================================================ // Project window UI diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c index 89ad31bb..17e818c7 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c @@ -25,9 +25,15 @@ // A floating window with a TreeView listing all controls on the // form (for selection and drag-reorder) and a ListView showing // editable properties of the selected control. Double-click a -// property value to edit it via an InputBox dialog. +// property value to edit it via an InputBox dialog. While a UserControl +// (.ctl) is the active file the window shows its header instead (Name, +// Width, Height ... DefaultEvent: prjCtlKeyAt), edited by kind: a number +// in a spin box, a flag toggled, BackColor in the color dialog, the +// default event picked from the control's Events, text in an input box. #include "ideProperties.h" +#include "../basdev/basProject.h" +#include "../basdev/basSource.h" #include "../formrt/formrt.h" #include "../formrt/frmParser.h" #include "dvxDlg.h" @@ -38,6 +44,7 @@ #include "treeView/treeView.h" #include +#include #include #include #include @@ -54,7 +61,7 @@ #define PRP_INT_BUF 32 // decimal int32_t text #define PRP_QUERY_BUF 512 // SQL probe scratch 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 FRM_MAX_TEXT for " [Design]" suffix #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 @@ -65,6 +72,12 @@ #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 PRP_EDIT_TITLE "Edit Property" +#define PRP_CTL_MIN_SIZE 1 // smallest UserControl Width / Height +#define PRP_CTL_DEF_COLOR 0xFFFFFF // BackColor the color dialog starts from when the header gives none +#define PRP_CTL_HEX_PREFIX "&H" // a hexadecimal color in the header +#define PRP_CTL_HEX_FMT PRP_CTL_HEX_PREFIX "%06" PRIX32 // a BASIC color as the header takes it +#define PRP_HEX_BASE 16 // Property grid editor kinds. A property is edited by its WGT_IFACE_* // type, or by one of these designer-only editors, numbered past the @@ -107,11 +120,13 @@ static AppContextT *sPrpCtx = NULL; static bool sUpdating = false; static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings static char **sCellData = NULL; // stb_ds array of strdup'd strings +static char *sCtlText = NULL; // the active UserControl's text while the window shows its header (prpShowCtl) +static PrpCtlSetFnT sCtlSet = NULL; // where a changed header value goes // Tree parent (NULL = the form) and block depth recorded for a control by // collectTreeOrder, parallel to the collected control array. typedef struct { - DsgnControlT *parent; + FrmCtrlT *parent; int32_t depth; } PrpParentT; @@ -120,19 +135,22 @@ typedef struct { // ============================================================ static void addPropRow(const char *name, const char *value); -static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, DsgnControlT *treeParent, int32_t depth); +static void collectTreeOrder(WidgetT *parent, FrmCtrlT **srcArr, int32_t srcCount, FrmCtrlT ***outArr, PrpParentT **outParents, FrmCtrlT *treeParent, int32_t depth); +static void ctlAddEvent(void *ctx, const char *name, const char *params); +static void ctlEditRow(int32_t row); +static void ctlShowRows(void); static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index); static void freeCellData(void); static void freeTreeLabels(void); -static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[DSGN_MAX_NAME]); +static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[FRM_MAX_NAME]); static uint8_t getPropType(const char *propName, const char *typeName); -static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME]); +static int32_t getTableNames(const char *dbName, char (**outNames)[FRM_MAX_NAME]); static void onPropDblClick(WidgetT *w); static void onPrpClose(WindowT *win); static void onTreeChange(WidgetT *w); static void onTreeItemClick(WidgetT *w); static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex); -static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize); +static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[FRM_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize); static void resolveDbPath(const char *dbName, char *out, int32_t outSize); static bool treeOrderMatches(void); static bool validateNewName(const char *name, int32_t maxLen, const char *exceptName); @@ -148,7 +166,7 @@ static void addPropRow(const char *name, const char *value) { // with the tree parent each one sits under and its block depth (1 = // top-level). The controls themselves are not modified; the caller decides // 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) { +static void collectTreeOrder(WidgetT *parent, FrmCtrlT **srcArr, int32_t srcCount, FrmCtrlT ***outArr, PrpParentT **outParents, FrmCtrlT *treeParent, int32_t depth) { for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { const char *label = (const char *)item->userData; @@ -156,10 +174,10 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src continue; } - char itemName[DSGN_MAX_NAME]; - int32_t itemIndex = DSGN_NO_INDEX; + char itemName[FRM_MAX_NAME]; + int32_t itemIndex = FRM_NO_INDEX; - parseTreeLabel(label, itemName, DSGN_MAX_NAME, &itemIndex); + parseTreeLabel(label, itemName, FRM_MAX_NAME, &itemIndex); for (int32_t i = 0; i < srcCount; i++) { if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) { @@ -182,16 +200,120 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src } +// basSourceClassEvents callback: one Event of the UserControl into the +// stb_ds name array ctx points at (the names are copies) +static void ctlAddEvent(void *ctx, const char *name, const char *params) { + char ***names = (char ***)ctx; + char *copy = strdup(name); + + (void)params; + + if (copy) { + arrput(*names, copy); + } +} + + +// Edit header row row of the UserControl shown: the new value goes to the +// IDE (sCtlSet), which rewrites the file and shows it again. +static void ctlEditRow(int32_t row) { + const char *key = sCellData[row * PRP_CELL_COLUMNS]; + const char *curValue = sCellData[row * PRP_CELL_COLUMNS + 1]; + const PrjCtlKeyT *k = prjCtlKeyFind(key); + char newValue[BAS_UC_TEXT_MAX]; + char prompt[PRP_PROMPT_BUF]; + + if (!k || !sCtlSet) { + return; + } + + snprintf(prompt, sizeof(prompt), "%s:", key); + + if (k->kind == PrjCtlBoolE) { + snprintf(newValue, sizeof(newValue), "%s", frmParseBool(curValue) ? "False" : "True"); + } else if (k->kind == PrjCtlIntE) { + int32_t n = atoi(curValue); + + if (!dvxIntInputBox(sPrpCtx, PRP_EDIT_TITLE, prompt, DVX_MAX(n, PRP_CTL_MIN_SIZE), PRP_CTL_MIN_SIZE, INT32_MAX, 1, &n)) { + return; + } + + snprintf(newValue, sizeof(newValue), "%" PRId32, n); + } else if (k->kind == PrjCtlColorE) { + // The header holds a BASIC color (&HBBGGRR), the dialog an RGB one + const char *hex = curValue + sizeof(PRP_CTL_HEX_PREFIX) - 1; + uint32_t rgb = curValue[0] ? wgtBasicColor((uint32_t)strtoul(hex, NULL, PRP_HEX_BASE)) : PRP_CTL_DEF_COLOR; + + if (!dvxColorDialog(sPrpCtx, key, &rgb)) { + return; + } + + snprintf(newValue, sizeof(newValue), PRP_CTL_HEX_FMT, wgtBasicColor(rgb)); + } else if (strcasecmp(key, BAS_UC_KEY_DEFAULTEVENT) == 0) { + // One of the control's Events (or none) + char **events = NULL; + + arrput(events, strdup(PRP_NONE_ITEM)); + basSourceClassEvents(sCtlText, ctlAddEvent, &events); + + int32_t count = (int32_t)arrlen(events); + int32_t chosen = 0; + + for (int32_t i = 1; i < count; i++) { + chosen = (strcasecmp(events[i], curValue) == 0) ? i : chosen; + } + + bool picked = count > 0 && events[0] && dvxChoiceDialog(sPrpCtx, key, "Select the default event:", (const char **)events, count, chosen, &chosen); + + snprintf(newValue, sizeof(newValue), "%s", (picked && chosen > 0) ? events[chosen] : ""); + + for (int32_t i = 0; i < count; i++) { + free(events[i]); + } + + arrfree(events); + + if (!picked) { + return; + } + } else if (!dvxInputBox(sPrpCtx, PRP_EDIT_TITLE, prompt, curValue, newValue, (int32_t)sizeof(newValue))) { + return; + } + + sCtlSet(key, newValue); +} + + +// The rows of the UserControl shown: Name and every header key with the +// value the control takes (its default when the header leaves it out). +static void ctlShowRows(void) { + if (!sPropList) { + return; + } + + freeCellData(); + + for (int32_t i = 0; prjCtlKeyAt(i); i++) { + char value[BAS_UC_TEXT_MAX] = ""; + + prjCtlHeaderGet(sCtlText, prjCtlKeyAt(i)->key, value, (int32_t)sizeof(value)); + addPropRow(prjCtlKeyAt(i)->key, value); + } + + wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS); +} + + // Walk tree items recursively to find the one matching a control name. static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) { for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { const char *label = (const char *)item->userData; if (label) { - char itemName[DSGN_MAX_NAME]; - int32_t itemIndex = DSGN_NO_INDEX; + char itemName[FRM_MAX_NAME]; + int32_t itemIndex = FRM_NO_INDEX; - parseTreeLabel(label, itemName, DSGN_MAX_NAME, &itemIndex); + parseTreeLabel(label, itemName, FRM_MAX_NAME, &itemIndex); if (itemIndex == index && strcmp(itemName, name) == 0) { return item; @@ -239,7 +361,7 @@ static void freeTreeLabels(void) { // through dlsym), and heap-allocates *outNames sized to the actual column // count. Caller must free *outNames. Returns the count of names (0 if // anything fails). -static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[DSGN_MAX_NAME]) { +static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[FRM_MAX_NAME]) { *outNames = NULL; if (!ds || !ds->form || !dataSourceName || !dataSourceName[0]) { @@ -252,14 +374,14 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam int32_t ctrlCount = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < ctrlCount; i++) { - DsgnControlT *ctrl = ds->form->controls[i]; + FrmCtrlT *ctrl = ds->form->controls[i]; if (strcasecmp(ctrl->typeName, PRP_DATA_TYPE) != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) { continue; } - dbName = dsgnControlGetPropValue(ctrl, "DatabaseName"); - recSrc = dsgnControlGetPropValue(ctrl, BAS_PROP_RECORDSOURCE); + dbName = frmCtrlGetProp(ctrl, "DatabaseName"); + recSrc = frmCtrlGetProp(ctrl, BAS_PROP_RECORDSOURCE); break; } @@ -320,7 +442,7 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam return 0; } - char (*names)[DSGN_MAX_NAME] = (char (*)[DSGN_MAX_NAME])calloc(colCount, DSGN_MAX_NAME); + char (*names)[FRM_MAX_NAME] = (char (*)[FRM_MAX_NAME])calloc(colCount, FRM_MAX_NAME); if (!names) { sqlFreeResult(rs); @@ -334,7 +456,7 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam const char *name = sqlFieldName(rs, i); if (name) { - snprintf(names[count++], DSGN_MAX_NAME, "%s", name); + snprintf(names[count++], FRM_MAX_NAME, "%s", name); } } @@ -376,23 +498,17 @@ static uint8_t getPropType(const char *propName, const char *typeName) { } } - const WgtPropDescT *p = findIfaceProp(typeName, propName); + // The control type's property (the form's when no control is + // selected): the widget's own over the runtime table, as at run time. + BasCtlPropT prop; - if (p) { - return p->setFn ? p->type : PRP_EDIT_READONLY; - } - - // Runtime tables: the form object when no control is selected, else - // the common control properties. Non-writable entries are read-only. - const BasPropDescT *pd = typeName[0] ? basFormRtFindCommonProp(propName) : basFormRtFindFormProp(propName); - - if (pd) { - return pd->writable ? pd->type : PRP_EDIT_READONLY; + if (basCtlPropFind(typeName[0] ? typeName : FRM_KW_FORM, propName, &prop)) { + return prop.writable ? prop.type : PRP_EDIT_READONLY; } // Designer storage fields not in the runtime tables (MinWidth alias // Width etc. are; this covers any table drift) and unknown keys. - return dsgnFindIntProp(propName) ? WGT_IFACE_INT : WGT_IFACE_STRING; + return frmIntPropFind(propName) ? WGT_IFACE_INT : WGT_IFACE_STRING; } @@ -401,7 +517,7 @@ static uint8_t getPropType(const char *propName, const char *typeName) { // Heap-allocates *outNames and grows it as tables are enumerated. Caller // must free *outNames. Returns the number of tables found (0 if anything // fails). -static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME]) { +static int32_t getTableNames(const char *dbName, char (**outNames)[FRM_MAX_NAME]) { *outNames = NULL; if (!dbName || !dbName[0]) { @@ -436,7 +552,7 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME } // Grow with realloc doubling - char (*names)[DSGN_MAX_NAME] = NULL; + char (*names)[FRM_MAX_NAME] = NULL; int32_t count = 0; int32_t cap = 0; @@ -449,7 +565,7 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME if (count >= cap) { 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)[FRM_MAX_NAME] = (char (*)[FRM_MAX_NAME])realloc(names, (size_t)newCap * FRM_MAX_NAME); if (!grown) { break; @@ -459,7 +575,7 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME cap = newCap; } - snprintf(names[count++], DSGN_MAX_NAME, "%s", name); + snprintf(names[count++], FRM_MAX_NAME, "%s", name); } sqlFreeResult(rs); @@ -470,15 +586,19 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME static void onPropDblClick(WidgetT *w) { - if (!sDs || !sDs->form || !sPropList || !sPrpCtx) { + int32_t row = wgtListViewGetSelected(w); + int32_t rowCount = (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS; + + if (!sPropList || !sPrpCtx || row < 0 || row >= rowCount) { return; } - int32_t row = wgtListViewGetSelected(w); + if (sCtlText) { + ctlEditRow(row); + return; + } - int32_t rowCount = (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS; - - if (row < 0 || row >= rowCount) { + if (!sDs || !sDs->form) { return; } @@ -505,37 +625,20 @@ static void onPropDblClick(WidgetT *w) { return; } - // Determine whose layout we're changing - char *layoutField = NULL; + // Whose layout: the form's, or the selected container's (a freshly + // placed one has none yet and shows the default) + FrmCtrlT *layoutCtrl = (sDs->selectedIdx >= 0) ? sDs->form->controls[sDs->selectedIdx] : NULL; + const char *curLayout = layoutCtrl ? frmCtrlGetProp(layoutCtrl, BAS_PROP_LAYOUT) : sDs->form->layout; - if (sDs->selectedIdx < 0) { - layoutField = sDs->form->layout; - } else { - DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx]; - - // A freshly placed container has no Layout entry yet; create - // the default so the grid row is editable. - if (!dsgnControlGetPropValue(ctrl, BAS_PROP_LAYOUT)) { - dsgnSetPropValue(ctrl, BAS_PROP_LAYOUT, BAS_LAYOUT_VBOX); - } - - for (int32_t pi = 0; pi < (int32_t)arrlen(ctrl->props); pi++) { - if (strcasecmp(ctrl->props[pi].name, BAS_PROP_LAYOUT) == 0) { - layoutField = ctrl->props[pi].value; - break; - } - } - - if (!layoutField) { - return; - } + if (!curLayout) { + curLayout = BAS_LAYOUT_VBOX; } // Find current selection int32_t defIdx = 0; for (int32_t i = 0; i < layoutCount; i++) { - if (strcasecmp(layoutField, layoutNames[i]) == 0) { + if (strcasecmp(curLayout, layoutNames[i]) == 0) { defIdx = i; break; } @@ -547,8 +650,11 @@ static void onPropDblClick(WidgetT *w) { return; } - snprintf(layoutField, DSGN_MAX_NAME, "%s", layoutNames[chosenIdx]); - sDs->form->dirty = true; + if (layoutCtrl) { + frmModelSetCtrlProp(sDs->form, layoutCtrl, BAS_PROP_LAYOUT, layoutNames[chosenIdx]); + } else { + frmModelSetFormProp(sDs->form, BAS_PROP_LAYOUT, layoutNames[chosenIdx]); + } // Rebuild the form designer to apply the new layout if (sDs->formWin && sDs->formWin->widgetRoot) { @@ -556,11 +662,10 @@ static void onPropDblClick(WidgetT *w) { wgtDestroyChildren(root); - // The root content box must use the FORM's layout, not whatever - // layoutField points at (it points at a selected container's - // Layout prop when one is selected). Per-container layouts are + // The root content box uses the form's layout, not the + // selected container's. Per-container layouts are // applied separately inside dsgnCreateWidgets. - sDs->form->contentBox = basFormRtCreateContentBox(root, sDs->form->layout); + sDs->contentBox = basFormRtCreateContentBox(root, sDs->form->layout); dsgnRebuildWidgets(sDs); dvxInvalidateWindow(sPrpCtx, sDs->formWin); } @@ -583,7 +688,7 @@ static void onPropDblClick(WidgetT *w) { return; } - char newValue[DSGN_MAX_TEXT]; + char newValue[FRM_MAX_TEXT]; if (propType == WGT_IFACE_BOOL) { // Toggle boolean on double-click -- no input box @@ -591,7 +696,7 @@ static void onPropDblClick(WidgetT *w) { snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True"); } else if (propType == WGT_IFACE_ENUM) { // Enum: cycle to next value on double-click - const WgtPropDescT *pd = findIfaceProp(ctrlTypeName, propName); + const WgtPropDescT *pd = frmIfaceProp(ctrlTypeName, propName); if (!pd || !pd->enumNames) { return; @@ -662,12 +767,12 @@ static void onPropDblClick(WidgetT *w) { const char *dataSrc = ""; if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { - DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx]; + FrmCtrlT *selCtrl = sDs->form->controls[sDs->selectedIdx]; if (strcasecmp(selCtrl->typeName, PRP_DATA_TYPE) == 0) { dataSrc = selCtrl->name; } else { - const char *dataSrcProp = dsgnControlGetPropValue(selCtrl, BAS_PROP_DATASOURCE); + const char *dataSrcProp = frmCtrlGetProp(selCtrl, BAS_PROP_DATASOURCE); if (dataSrcProp) { dataSrc = dataSrcProp; @@ -675,7 +780,7 @@ static void onPropDblClick(WidgetT *w) { } } - char (*fieldNames)[DSGN_MAX_NAME] = NULL; + char (*fieldNames)[FRM_MAX_NAME] = NULL; int32_t fieldCount = getDataFieldNames(sDs, dataSrc, &fieldNames); bool ok = propDropdownOrInput(sPrpCtx, propName, "Select column:", fieldNames, fieldCount, curValue, newValue, sizeof(newValue)); @@ -691,15 +796,15 @@ static void onPropDblClick(WidgetT *w) { const char *dbName = ""; if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { - DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx]; - const char *dbNameProp = dsgnControlGetPropValue(selCtrl, "DatabaseName"); + FrmCtrlT *selCtrl = sDs->form->controls[sDs->selectedIdx]; + const char *dbNameProp = frmCtrlGetProp(selCtrl, "DatabaseName"); if (dbNameProp) { dbName = dbNameProp; } } - char (*tableNames)[DSGN_MAX_NAME] = NULL; + char (*tableNames)[FRM_MAX_NAME] = NULL; int32_t tableCount = getTableNames(dbName, &tableNames); bool ok = propDropdownOrInput(sPrpCtx, propName, "Select table:", tableNames, tableCount, curValue, newValue, sizeof(newValue)); @@ -714,7 +819,7 @@ static void onPropDblClick(WidgetT *w) { snprintf(prompt, sizeof(prompt), "%s:", propName); int32_t intVal = atoi(curValue); - if (!dvxIntInputBox(sPrpCtx, "Edit Property", prompt, intVal, INT32_MIN, INT32_MAX, 1, &intVal)) { + if (!dvxIntInputBox(sPrpCtx, PRP_EDIT_TITLE, prompt, intVal, INT32_MIN, INT32_MAX, 1, &intVal)) { return; } @@ -727,13 +832,13 @@ static void onPropDblClick(WidgetT *w) { int32_t maxSize = (int32_t)sizeof(newValue); if (strcasecmp(propName, BAS_PROP_HELPTOPIC) == 0 || strcasecmp(propName, BAS_PROP_CONTEXTMENU) == 0) { - maxSize = DSGN_MAX_NAME; + maxSize = FRM_MAX_NAME; } snprintf(prompt, sizeof(prompt), "%s:", propName); snprintf(newValue, maxSize, "%s", curValue); - if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, maxSize)) { + if (!dvxInputBox(sPrpCtx, PRP_EDIT_TITLE, prompt, curValue, newValue, maxSize)) { return; } } @@ -741,123 +846,54 @@ static void onPropDblClick(WidgetT *w) { int32_t count = (int32_t)arrlen(sDs->form->controls); if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) { - DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx]; + FrmCtrlT *ctrl = sDs->form->controls[sDs->selectedIdx]; if (strcasecmp(propName, BAS_PROP_NAME) == 0) { - char oldName[DSGN_MAX_NAME]; + char oldName[FRM_MAX_NAME]; snprintf(oldName, sizeof(oldName), "%s", ctrl->name); - if (!validateNewName(newValue, DSGN_MAX_NAME, oldName)) { + if (!validateNewName(newValue, FRM_MAX_NAME, oldName)) { return; } - // Rename all members of a control array, not just the selected one - for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = sDs->form->controls[i]; - - if (strcasecmp(c->name, oldName) == 0) { - snprintf(c->name, DSGN_MAX_NAME, "%s", newValue); - - if (c->widget) { - wgtSetName(c->widget, c->name); - } - } + // The whole control array, its children and the Data bindings + // that name it follow (frmModelRenameCtrl) + if (!frmModelRenameCtrl(sDs->form, oldName, newValue)) { + return; } - // Children reference their container by parentName; if the - // renamed control was a container, update them too, or they - // would be orphaned -- dropped from the saved .frm and - // reparented to the form node in the tree. for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = sDs->form->controls[i]; + FrmCtrlT *c = sDs->form->controls[i]; - if (strcasecmp(c->parentName, oldName) == 0) { - snprintf(c->parentName, DSGN_MAX_NAME, "%s", newValue); - } - } - - // If this is a Data control, update DataSource and MasterSource - // references on all other controls that pointed to the old name - if (strcasecmp(ctrl->typeName, PRP_DATA_TYPE) == 0) { - for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = sDs->form->controls[i]; - - for (int32_t j = 0; j < (int32_t)arrlen(c->props); j++) { - if ((strcasecmp(c->props[j].name, BAS_PROP_DATASOURCE) == 0 || - strcasecmp(c->props[j].name, BAS_PROP_MASTERSOURCE) == 0) && - strcasecmp(c->props[j].value, oldName) == 0) { - snprintf(c->props[j].value, DSGN_MAX_TEXT, "%s", newValue); - } - } + if (dsgnWidget(c) && strcasecmp(c->name, newValue) == 0) { + wgtSetName(dsgnWidget(c), c->name); } } ideRenameInCode(oldName, newValue); prpRebuildTree(sDs); - } else if (dsgnFindIntProp(propName)) { - const DsgnIntPropT *ip = dsgnFindIntProp(propName); - - *(int32_t *)((char *)ctrl + ip->offset) = atoi(newValue); - dsgnSyncWidgetGeom(ctrl); - } else if (strcasecmp(propName, BAS_PROP_VISIBLE) == 0 && !dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_VISIBLE)) { - // Stored only: the design widget stays shown (and clickable), - // as dsgnCreateControlWidget leaves it; the runtime hides it. - ctrl->visible = frmParseBool(newValue); - } else if (strcasecmp(propName, BAS_PROP_ENABLED) == 0 && !dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_ENABLED)) { - // Descendants keep their own Enabled; they are drawn disabled - // only while an ancestor is. - ctrl->enabled = frmParseBool(newValue); - dsgnApplyEnabled(sDs->form); - } else if (strcasecmp(propName, BAS_PROP_HELPTOPIC) == 0) { - snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue); } else { - // Try widget iface setter first - bool ifaceHandled = false; + // The model stores it (props[] survives a widget rebuild and is + // what gets saved); the live widget mirrors it + const WgtPropDescT *p = frmIfaceProp(ctrl->typeName, propName); + WidgetT *live = dsgnWidget(ctrl); - if (ctrl->widget) { - const char *wgtName = wgtFindByBasName(ctrl->typeName); + frmModelSetCtrlProp(sDs->form, ctrl, propName, newValue); - if (wgtName) { - const WgtIfaceT *iface = wgtGetIface(wgtName); + const char *stored = frmCtrlGetProp(ctrl, propName); - if (iface) { - const WgtPropDescT *p = wgtIfaceFindProp(iface, propName); - - if (p && p->setFn) { - // props[] is the design-time store (it survives a - // widget rebuild and is what gets saved); the live - // widget mirrors it. A setter must copy the - // string: props[] is an stb_ds array that any - // later dsgnSetPropValue may reallocate. - dsgnSetPropValue(ctrl, propName, newValue); - - const char *stored = dsgnControlGetPropValue(ctrl, propName); - - if (stored) { - wgtApplyPropFromString(ctrl->widget, p, stored); - } - - ifaceHandled = true; - } - } - } + if (frmIntPropFind(propName)) { + dsgnSyncWidgetGeom(ctrl); + } else if (strcasecmp(propName, BAS_PROP_ENABLED) == 0 && !p) { + // Descendants keep their own Enabled; they are drawn + // disabled only while an ancestor is. Visible is stored + // only: the design widget stays shown (and clickable). + dsgnApplyEnabled(sDs->form); + } else if (live && stored && p && p->setFn) { + wgtIfaceSetPropText(basCtlIface(ctrl->typeName), live, p, stored); + } else if (live && stored && (strcasecmp(propName, BAS_PROP_CAPTION) == 0 || strcasecmp(propName, BAS_PROP_TEXT) == 0)) { + wgtSetText(live, stored); } - - if (!ifaceHandled) { - // Custom prop storage - dsgnSetPropValue(ctrl, propName, newValue); - - // Update widget text from the persistent props array - if (ctrl->widget && (strcasecmp(propName, BAS_PROP_CAPTION) == 0 || strcasecmp(propName, BAS_PROP_TEXT) == 0)) { - const char *stored = dsgnControlGetPropValue(ctrl, propName); - - if (stored) { - wgtSetText(ctrl->widget, stored); - } - } - } - - dsgnOptionSelected(sDs->form, ctrl); } sDs->form->dirty = true; @@ -874,56 +910,45 @@ static void onPropDblClick(WidgetT *w) { return; } - snprintf(sDs->form->name, sizeof(sDs->form->name), "%s", newValue); + frmModelSetFormProp(sDs->form, propName, newValue); ideRenameInCode(oldName, sDs->form->name); prpRebuildTree(sDs); - } else if (strcasecmp(propName, BAS_PROP_CAPTION) == 0) { - snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue); + } else { + // Padding / Spacing below 0 mean the default; an explicit + // Width / Height ends AutoSize + bool isPad = strcasecmp(propName, BAS_PROP_PADDING) == 0 || strcasecmp(propName, BAS_PROP_SPACING) == 0; + bool isSize = strcasecmp(propName, BAS_PROP_WIDTH) == 0 || strcasecmp(propName, BAS_PROP_HEIGHT) == 0; + char px[PRP_INT_BUF]; + + snprintf(px, sizeof(px), "%d", DVX_MAX(atoi(newValue), 0)); + frmModelSetFormProp(sDs->form, propName, isPad ? px : newValue); + + if (isSize) { + sDs->form->autoSize = false; + } + + if (strcasecmp(propName, BAS_PROP_CAPTION) == 0 && sDs->formWin) { + char winTitle[FRM_MAX_TEXT + PRP_TITLE_SUFFIX_PAD]; - if (sDs->formWin) { - char winTitle[DSGN_MAX_TEXT + PRP_TITLE_SUFFIX_PAD]; snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption); dvxSetTitle(sPrpCtx, sDs->formWin, winTitle); } - } else if (strcasecmp(propName, BAS_PROP_AUTOSIZE) == 0) { - sDs->form->autoSize = frmParseBool(newValue); - } else if (strcasecmp(propName, BAS_PROP_RESIZABLE) == 0) { - sDs->form->resizable = frmParseBool(newValue); - if (sDs->formWin) { + if (strcasecmp(propName, BAS_PROP_RESIZABLE) == 0 && sDs->formWin) { sDs->formWin->resizable = sDs->form->resizable; dvxInvalidateWindow(sPrpCtx, sDs->formWin); } - } else if (strcasecmp(propName, BAS_PROP_CENTERED) == 0) { - sDs->form->centered = frmParseBool(newValue); - } else if (strcasecmp(propName, BAS_PROP_LEFT) == 0) { - sDs->form->left = atoi(newValue); - } else if (strcasecmp(propName, BAS_PROP_TOP) == 0) { - sDs->form->top = atoi(newValue); - } else if (strcasecmp(propName, BAS_PROP_WIDTH) == 0) { - sDs->form->width = atoi(newValue); - sDs->form->autoSize = false; - } else if (strcasecmp(propName, BAS_PROP_HEIGHT) == 0) { - sDs->form->height = atoi(newValue); - sDs->form->autoSize = false; - } else if (strcasecmp(propName, BAS_PROP_HELPTOPIC) == 0) { - 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); + + if (isPad && sDs->contentBox) { + sDs->contentBox->padding = wgtPixels(sDs->form->padding); + sDs->contentBox->spacing = wgtPixels(sDs->form->spacing); + wgtInvalidate(sDs->contentBox); + } } sDs->form->dirty = true; - // Resize the form designer window - if (sDs->formWin) { - if (sDs->form->autoSize) { - dvxFitWindow(sPrpCtx, sDs->formWin); - sDs->form->width = sDs->formWin->w; - sDs->form->height = sDs->formWin->h; - } else { - dvxResizeWindow(sPrpCtx, sDs->formWin, sDs->form->width, sDs->form->height); - } - } + dsgnFitFormWin(sDs); } prpRefresh(sDs); @@ -938,7 +963,7 @@ static void onPrpClose(WindowT *win) { static void onTreeChange(WidgetT *w) { (void)w; - if (!sDs || !sDs->form || !sTree || sUpdating) { + if (sCtlText || !sDs || !sDs->form || !sTree || sUpdating) { return; } @@ -950,7 +975,7 @@ static void onTreeChange(WidgetT *w) { // Actual reorder happened -- rebuild the controls array from tree order. int32_t count = (int32_t)arrlen(sDs->form->controls); - DsgnControlT **newArr = NULL; + FrmCtrlT **newArr = NULL; PrpParentT *newParents = NULL; WidgetT *formItem = sTree->firstChild; @@ -967,9 +992,9 @@ static void onTreeChange(WidgetT *w) { bool valid = ((int32_t)arrlen(newArr) == count); for (int32_t i = 0; valid && i < count; i++) { - const DsgnControlT *parent = newParents[i].parent; + const FrmCtrlT *parent = newParents[i].parent; - valid = newParents[i].depth <= DSGN_MAX_NEST_DEPTH && (!parent || dsgnIsContainer(parent->typeName)); + valid = newParents[i].depth <= FRM_MAX_NEST_DEPTH && (!parent || basCtlIsContainer(parent->typeName)); } if (!valid) { @@ -980,14 +1005,14 @@ static void onTreeChange(WidgetT *w) { } for (int32_t i = 0; i < count; i++) { - const DsgnControlT *parent = newParents[i].parent; + const FrmCtrlT *parent = newParents[i].parent; - snprintf(newArr[i]->parentName, DSGN_MAX_NAME, "%s", parent ? parent->name : ""); - newArr[i]->parentIndex = parent ? parent->index : DSGN_NO_INDEX; + snprintf(newArr[i]->parentName, FRM_MAX_NAME, "%s", parent ? parent->name : ""); + newArr[i]->parentIndex = parent ? parent->index : FRM_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; + FrmCtrlT *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) { @@ -1040,10 +1065,10 @@ static void onTreeItemClick(WidgetT *w) { } // Extract name and control-array index from "Name(idx) (Type)" - char clickedName[DSGN_MAX_NAME]; - int32_t clickedIndex = DSGN_NO_INDEX; + char clickedName[FRM_MAX_NAME]; + int32_t clickedIndex = FRM_NO_INDEX; - parseTreeLabel(label, clickedName, DSGN_MAX_NAME, &clickedIndex); + parseTreeLabel(label, clickedName, FRM_MAX_NAME, &clickedIndex); int32_t count = (int32_t)arrlen(sDs->form->controls); @@ -1074,7 +1099,7 @@ static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize } outName[ni] = '\0'; - *outIndex = DSGN_NO_INDEX; + *outIndex = FRM_NO_INDEX; if (label[ni] == '(') { *outIndex = (int32_t)atoi(&label[ni + 1]); @@ -1087,7 +1112,7 @@ static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize // otherwise it offers a "(none)"-prefixed dropdown of names, preselecting // the entry matching curValue. Writes the chosen value (empty for "(none)") // into newValue. Returns false if the user cancelled. -static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize) { +static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[FRM_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize) { if (count <= 0) { // No names available -- fall back to text input char prompt[PRP_PROMPT_BUF]; @@ -1188,6 +1213,9 @@ void prpDestroy(AppContextT *ctx, WindowT *win) { freeCellData(); arrfree(sCellData); sCellData = NULL; + free(sCtlText); + sCtlText = NULL; + sCtlSet = NULL; if (win) { dvxDestroyWindow(ctx, win); @@ -1201,7 +1229,35 @@ void prpDestroy(AppContextT *ctx, WindowT *win) { void prpRebuildTree(DsgnStateT *ds) { - if (!sTree || !ds || !ds->form) { + if (!sTree) { + return; + } + + // A UserControl's header: its name alone + if (sCtlText) { + char name[BAS_MAX_IDENT] = ""; + + sUpdating = true; + freeTreeLabels(); + wgtDestroyChildren(sTree); + prjCtlHeaderGet(sCtlText, BAS_PROP_NAME, name, (int32_t)sizeof(name)); + + char *label = strdup(name); + WidgetT *item = label ? wgtTreeItem(sTree, label) : NULL; + + if (label) { + arrput(sTreeLabels, label); + } + + if (item) { + wgtTreeItemSetSelected(item, true); + } + + sUpdating = false; + return; + } + + if (!ds || !ds->form) { return; } @@ -1231,7 +1287,7 @@ void prpRebuildTree(DsgnStateT *ds) { WidgetT **treeItems = NULL; for (int32_t i = 0; i < count; i++) { - DsgnControlT *ctrl = ds->form->controls[i]; + FrmCtrlT *ctrl = ds->form->controls[i]; char buf[PRP_PROMPT_BUF]; if (ctrl->index >= 0) { @@ -1246,7 +1302,7 @@ void prpRebuildTree(DsgnStateT *ds) { // Find the tree parent: form item or the parent control's tree item // (controls[] lists a parent before its children). WidgetT *treeParent = formItem; - const DsgnControlT *parent = dsgnFindParent(ds->form, ctrl); + const FrmCtrlT *parent = frmModelFindParent(ds->form, ctrl); for (int32_t j = 0; parent && j < i; j++) { if (ds->form->controls[j] == parent) { @@ -1260,7 +1316,7 @@ void prpRebuildTree(DsgnStateT *ds) { item->onClick = onTreeItemClick; arrput(treeItems, item); - if (dsgnIsContainer(ctrl->typeName)) { + if (basCtlIsContainer(ctrl->typeName)) { wgtTreeItemSetExpanded(item, true); } @@ -1276,6 +1332,11 @@ void prpRebuildTree(DsgnStateT *ds) { void prpRefresh(DsgnStateT *ds) { + if (sCtlText) { + ctlShowRows(); + return; + } + if (!ds || !ds->form) { return; } @@ -1318,7 +1379,7 @@ void prpRefresh(DsgnStateT *ds) { int32_t count = (int32_t)arrlen(ds->form->controls); if (ds->selectedIdx >= 0 && ds->selectedIdx < count) { - DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; + FrmCtrlT *ctrl = ds->form->controls[ds->selectedIdx]; char buf[PRP_INT_BUF]; addPropRow(BAS_PROP_NAME, ctrl->name); @@ -1330,18 +1391,18 @@ void prpRefresh(DsgnStateT *ds) { addPropRow(PRP_ROW_TYPE, ctrl->typeName); - for (int32_t i = 0; dsgnIntPropAt(i); i++) { - const DsgnIntPropT *ip = dsgnIntPropAt(i); + for (int32_t i = 0; frmIntPropAt(i); i++) { + const FrmIntPropT *ip = frmIntPropAt(i); - snprintf(buf, sizeof(buf), "%d", (int)*(const int32_t *)((const char *)ctrl + ip->offset)); + snprintf(buf, sizeof(buf), "%" PRId32, frmIntPropGet(ctrl, ip)); addPropRow(ip->name, buf); } - if (!dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_VISIBLE)) { + if (!frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) { addPropRow(BAS_PROP_VISIBLE, ctrl->visible ? "True" : "False"); } - if (!dsgnIfaceHasProp(ctrl->typeName, BAS_PROP_ENABLED)) { + if (!frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) { addPropRow(BAS_PROP_ENABLED, ctrl->enabled ? "True" : "False"); } @@ -1353,50 +1414,48 @@ void prpRefresh(DsgnStateT *ds) { // 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. - if (dsgnIsContainer(ctrl->typeName) && !dsgnControlGetPropValue(ctrl, BAS_PROP_LAYOUT)) { + if (basCtlIsContainer(ctrl->typeName) && !frmCtrlGetProp(ctrl, BAS_PROP_LAYOUT)) { addPropRow(BAS_PROP_LAYOUT, BAS_LAYOUT_VBOX); } - // Widget interface properties (from the .wgt descriptor) - const char *wgtName = wgtFindByBasName(ctrl->typeName); + // Widget interface properties (from the .wgt descriptor, or a + // UserControl's dynamic interface: its properties' values come + // from its live design instance) + const WgtIfaceT *iface = basCtlIface(ctrl->typeName); - if (wgtName) { - const WgtIfaceT *iface = wgtGetIface(wgtName); + if (iface && dsgnWidget(ctrl)) { + for (int32_t i = 0; i < iface->propCount; i++) { + const WgtPropDescT *p = &iface->props[i]; - if (iface && ctrl->widget) { - for (int32_t i = 0; i < iface->propCount; i++) { - const WgtPropDescT *p = &iface->props[i]; + // Skip read-only runtime properties (no setter) + if (!p->setFn) { + continue; + } - // Skip read-only runtime properties (no setter) - if (!p->setFn) { - continue; + // Skip if already shown as a custom prop + bool already = false; + + for (int32_t j = 0; j < (int32_t)arrlen(ctrl->props); j++) { + if (strcasecmp(ctrl->props[j].name, p->name) == 0) { + already = true; + break; } + } - // Skip if already shown as a custom prop - bool already = false; + if (already) { + continue; + } - for (int32_t j = 0; j < (int32_t)arrlen(ctrl->props); j++) { - if (strcasecmp(ctrl->props[j].name, p->name) == 0) { - already = true; - break; - } - } + // Read the current value from the widget. Enum values + // that don't map to a name are shown as "?". + char valBuf[FRM_MAX_TEXT]; - if (already) { - continue; - } - - // Read the current value from the widget. Enum values - // that don't map to a name are shown as "?". - char valBuf[DSGN_MAX_TEXT]; - - if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) { - addPropRow(p->name, valBuf); - } else if (p->type == WGT_IFACE_ENUM) { - addPropRow(p->name, "?"); - } else { - addPropRow(p->name, ""); - } + if (wgtIfaceGetPropText(iface, dsgnWidget(ctrl), p, valBuf, sizeof(valBuf))) { + addPropRow(p->name, valBuf); + } else if (p->type == WGT_IFACE_ENUM) { + addPropRow(p->name, "?"); + } else { + addPropRow(p->name, ""); } } } @@ -1408,9 +1467,9 @@ void prpRefresh(DsgnStateT *ds) { const BasPropDescT *formProps = basFormRtFormProps(&formPropCount); for (int32_t i = 0; i < formPropCount; i++) { - char valBuf[DSGN_MAX_TEXT]; + char valBuf[FRM_MAX_TEXT]; - if (dsgnFormPropValue(ds->form, formProps[i].name, valBuf, sizeof(valBuf))) { + if (frmModelFormProp(ds->form, formProps[i].name, valBuf, sizeof(valBuf))) { addPropRow(formProps[i].name, valBuf); } } @@ -1420,6 +1479,31 @@ void prpRefresh(DsgnStateT *ds) { } +void prpShowCtl(const char *text, PrpCtlSetFnT onSet) { + free(sCtlText); + sCtlText = (text && sPrpWin) ? strdup(text) : NULL; + sCtlSet = sCtlText ? onSet : NULL; + + if (!sPrpWin) { + return; + } + + // Back to the designer: its selection, or nothing without a form + if (!sCtlText && !(sDs && sDs->form)) { + sUpdating = true; + freeTreeLabels(); + wgtDestroyChildren(sTree); + sUpdating = false; + freeCellData(); + wgtListViewSetData(sPropList, NULL, 0); + return; + } + + prpRebuildTree(sDs); + prpRefresh(sDs); +} + + // resolveDbPath -- resolve a DatabaseName against the project directory static void resolveDbPath(const char *dbName, char *out, int32_t outSize) { // If it's already an absolute path (starts with drive letter or /), use as-is @@ -1447,7 +1531,7 @@ static bool treeOrderMatches(void) { } int32_t count = (int32_t)arrlen(sDs->form->controls); - DsgnControlT **newArr = NULL; + FrmCtrlT **newArr = NULL; PrpParentT *newParents = NULL; // Order is detected via pointer identity (tree DFS order vs model @@ -1459,7 +1543,7 @@ static bool treeOrderMatches(void) { if (match) { for (int32_t i = 0; i < count; i++) { if (newArr[i] != sDs->form->controls[i] || - dsgnFindParent(sDs->form, sDs->form->controls[i]) != newParents[i].parent) { + frmModelFindParent(sDs->form, sDs->form->controls[i]) != newParents[i].parent) { match = false; break; } @@ -1489,7 +1573,7 @@ static bool validateNewName(const char *name, int32_t maxLen, const char *except return false; } - if (dsgnNameInUse(sDs->form, name, exceptName, true)) { + if (frmModelNameInUse(sDs->form, name, exceptName, true)) { snprintf(msg, sizeof(msg), "The name %s is already in use.", name); dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, msg); return false; diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.h b/src/apps/kpunch/dvxbasic/ide/ideProperties.h index 8196068e..73fb3ab9 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.h +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.h @@ -39,4 +39,15 @@ void prpRebuildTree(DsgnStateT *ds); // Refresh selection and properties display (lightweight, no tree rebuild). void prpRefresh(DsgnStateT *ds); +// A changed header value of the UserControl shown: key (Name, Width ..., +// prjCtlKeyAt) and its new text. +typedef void (*PrpCtlSetFnT)(const char *key, const char *value); + +// Show a UserControl's header (Name and the keys of prjCtlKeyAt) instead +// of the designer's selection: text is the .ctl's text (copied); a value +// the user changes goes to onSet, which rewrites the file and calls this +// again with the new text. text NULL goes back to the designer. Only +// while the window exists (prpDestroy forgets it). +void prpShowCtl(const char *text, PrpCtlSetFnT onSet); + #endif // IDE_PROPERTIES_H diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c index c90bdd6d..cea78b82 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c +++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c @@ -23,8 +23,13 @@ // ideToolbox.c -- DVX BASIC form designer toolbox window // // A small floating window with buttons for each registered widget -// type that has a basName (VB-style name). Built dynamically from -// the widget interface registry. +// type that has a basName (VB-style name), in alphabetical order of +// that name (basCtlTypeAt). Built dynamically from the widget interface +// registry: native widgets and installed BASIC controls (their .wgt +// file's icon24 / name resources), and the project's own UserControls +// the designer registered (their header's ToolboxIcon picture, a file +// in the project's folder, and Description); other programs' private +// controls are left out. #include "ideToolbox.h" #include "dvxApp.h" @@ -47,20 +52,13 @@ #define TBX_WIN_W 120 #define TBX_WIN_H 250 #define TBX_TOOLTIP_LEN 64 // tooltip text loaded from the .wgt "name" resource -#define TBX_RES_NAME_LEN 32 // resource name scratch buffer - -// .wgt resource contract: a toolbox icon and display name per interface; -// the second and later interfaces in one .wgt use a "-N" suffix. -#define TBX_RES_ICON "icon24" -#define TBX_RES_NAME "name" -#define TBX_RES_SUFFIX_FMT "%s-%d" // ============================================================ // Per-tool entry // ============================================================ typedef struct { - char typeName[DSGN_MAX_NAME]; + char typeName[FRM_MAX_NAME]; char tooltip[TBX_TOOLTIP_LEN]; } TbxToolEntryT; @@ -73,8 +71,9 @@ static TbxToolEntryT *sTbxTools = NULL; // stb_ds dynamic array static bool sBuilding = false; // tbxCreate is pumping events between buttons // Function prototypes -static void onTbxClose(WindowT *win); -static void onToolClick(WidgetT *w); +static void onTbxClose(WindowT *win); +static void onToolClick(WidgetT *w); +static uint8_t *projectToolIcon(AppContextT *ctx, const WgtIfaceT *iface, char *tooltip, int32_t tooltipSize, int32_t *outW, int32_t *outH, int32_t *outPitch); static void onTbxClose(WindowT *win) { @@ -96,7 +95,7 @@ static void onToolClick(WidgetT *w) { if (strcasecmp(sDs->activeTool, typeName) == 0) { sDs->activeTool[0] = '\0'; } else { - snprintf(sDs->activeTool, DSGN_MAX_NAME, "%s", typeName); + snprintf(sDs->activeTool, FRM_MAX_NAME, "%s", typeName); } sDs->mode = DSGN_IDLE; @@ -104,6 +103,23 @@ static void onToolClick(WidgetT *w) { } +// A project UserControl's toolbox picture (its header's ToolboxIcon, a +// file in the project's folder: basUcIconPath; NULL for none) and tooltip +// (its Description), when iface is one of the designer's own controls +// (basCtlLoadDesign); NULL and the tooltip untouched otherwise. +static uint8_t *projectToolIcon(AppContextT *ctx, const WgtIfaceT *iface, char *tooltip, int32_t tooltipSize, int32_t *outW, int32_t *outH, int32_t *outPitch) { + const BasUcHeaderT *header = (basUcOwnIface(NULL, iface->basName) == iface) ? basUcIfaceHeader(iface) : NULL; + char path[DVX_MAX_PATH]; + + if (!header) { + return NULL; + } + + snprintf(tooltip, (size_t)tooltipSize, "%s", header->description); + return basUcIconPath(iface, path, sizeof(path)) ? dvxLoadImage(ctx, path, outW, outH, outPitch) : NULL; +} + + 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. @@ -125,52 +141,47 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) { WidgetT *root = wgtInitWindow(ctx, win); root->spacing = 0; - // Enumerate all registered widget interfaces with a basName - int32_t ifaceCount = wgtIfaceCount(); - int32_t col = 0; - WidgetT *row = NULL; - WidgetT **buttons = NULL; // stb_ds array, parallel to sTbxTools + int32_t col = 0; + WidgetT *row = NULL; + WidgetT **buttons = NULL; // stb_ds array, parallel to sTbxTools sBuilding = true; - for (int32_t i = 0; i < ifaceCount && ctx->running; i++) { - const char *wgtName = NULL; - const WgtIfaceT *iface = wgtIfaceAt(i, &wgtName); + // The control types in alphabetical order (basCtlTypeAt), fetched by + // index each time: the event pump below may change the registry. + for (int32_t i = 0; ctx->running; i++) { + const WgtIfaceT *iface = basCtlTypeAt(NULL, i); + + if (!iface) { + break; + } // 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) { + if (iface->createSig == WGT_CREATE_PARENT_DATA) { continue; } // Add tool entry TbxToolEntryT entry; memset(&entry, 0, sizeof(entry)); - snprintf(entry.typeName, DSGN_MAX_NAME, "%s", iface->basName); + snprintf(entry.typeName, FRM_MAX_NAME, "%s", iface->basName); // Load icon data and tooltip from the .wgt file resources uint8_t *iconData = NULL; int32_t iconW = 0; int32_t iconH = 0; int32_t iconPitch = 0; - const char *wgtPath = wgtIfaceGetPath(wgtName); - int32_t pathIdx = wgtIfaceGetPathIndex(wgtName); + char iconResName[BAS_CTL_RES_NAME_LEN]; + char nameResName[BAS_CTL_RES_NAME_LEN]; + const char *wgtPath = basCtlResPath(iface->basName, BAS_CTL_RES_ICON, iconResName, sizeof(iconResName)); if (wgtPath) { - // Build suffixed resource names: "icon24", "icon24-2", etc. - char iconResName[TBX_RES_NAME_LEN]; - char nameResName[TBX_RES_NAME_LEN]; - - if (pathIdx <= 1) { - snprintf(iconResName, sizeof(iconResName), "%s", TBX_RES_ICON); - snprintf(nameResName, sizeof(nameResName), "%s", TBX_RES_NAME); - } else { - snprintf(iconResName, sizeof(iconResName), TBX_RES_SUFFIX_FMT, TBX_RES_ICON, (int)pathIdx); - snprintf(nameResName, sizeof(nameResName), TBX_RES_SUFFIX_FMT, TBX_RES_NAME, (int)pathIdx); - } - + basCtlResPath(iface->basName, BAS_CTL_RES_NAME, nameResName, sizeof(nameResName)); iconData = dvxResLoadIcon(ctx, wgtPath, iconResName, &iconW, &iconH, &iconPitch); dvxResLoadText(wgtPath, nameResName, entry.tooltip, sizeof(entry.tooltip)); + } else { + iconData = projectToolIcon(ctx, iface, entry.tooltip, (int32_t)sizeof(entry.tooltip), &iconW, &iconH, &iconPitch); } if (!entry.tooltip[0]) { diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs index 502ce34a..1a0c64cb 100644 --- a/src/apps/kpunch/dvxbasic/ideguide.dhs +++ b/src/apps/kpunch/dvxbasic/ideguide.dhs @@ -89,8 +89,10 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual .index Project Properties .index Add File .index New Class Module +.index New UserControl .index Save File .index Save All +.index Make Control .index Rename File .index Remove File .index Exit @@ -107,12 +109,14 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual --- 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, .frm or .cls 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. + Add File... Ctrl+O Add a .bas, .frm, .cls or .ctl 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. New Class Module Add a new, empty class module (Class1.cls, Class2.cls, ...) to the project and open it in the Code Editor. See Class Modules under Project System. + New UserControl Add a new UserControl, a control of your own (UserControl1.ctl, UserControl2.ctl, ...), to the project and open it in the Code Editor. See UserControls in the IDE. Save File Ctrl+S Save the active file to disk. Save All Save all modified files in the project. --- - Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build. + Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build. A release build leaves out debugging information and gives forms and controls short internal names, except in a program that uses names as text (the Name property, Controls, DesignSelection or a DesignMouse event procedure), which keeps them. + Make Control... In a control project (Project Properties, Type Control) this item replaces Make Executable: it compiles the project and saves its controls as a control file (.wgt, named after the project). See UserControls in the IDE. --- Rename File... Give the file selected in the Project Explorer a new name, on disk and in the project. See Renaming a File under Project System. Remove File Remove the selected file from the project (prompts to save if modified). @@ -120,7 +124,7 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual Exit Close the IDE (prompts to save unsaved changes). .endtable -A project (.dbp) or source file (.bas, .frm, .cls) 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. +A project (.dbp) or source file (.bas, .frm, .cls, .ctl) 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. @@ -227,7 +231,7 @@ Cut, Copy, Paste, Select All and Delete act on the window you last worked in: th Toolbar Checkbox: show or hide the toolbar. 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 (a form must be active). Allows designing menu bars with captions, names, levels, checked state, and enabled state. See Menu Editor below. + 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, shortcut keys, levels, checked state, and enabled state. See Menu Editor below. .endtable .link ide.menu.window Window Menu @@ -437,7 +441,7 @@ Opening a form in the designer does not mark it modified. A control whose Visibl .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. +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. Shortcut is a key that runs the item (F2, Ctrl+X, Alt+Enter ...), shown at its right; Text is key text shown without making a shortcut (see Menu Item Properties); OK rejects a Shortcut it does not know. 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 @@ -458,6 +462,8 @@ Forms have the following design-time properties: Name, Caption, Width, Height, L .index Class Modules in the IDE .index .cls Files .index Classes Section +.index Controls Section +.index Project Type .h1 Project System @@ -470,8 +476,10 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi .item Startup Form -- which form to show automatically when the program starts. .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 More Than One Copy -- "Allow more than one copy to run at once" ([Settings] MultiInstance = True): the compiled program can be started again while it is running, and each copy runs on its own (like the Clock and the File Manager). Without it, starting the program again only says that it is already running. .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 the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section and forms (.frm) in the [Forms] section. +.item Type -- what the project makes: a program (an .app, the default) or, with Type = Control, a control file (a .wgt holding the project's UserControls for every program). See UserControls in the IDE. +.item File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section, UserControls (.ctl) in the [Controls] section and forms (.frm) in the [Forms] section. .endlist The file sections all have the same shape: one FileN line per file, numbered from 0, and a Count line: @@ -493,7 +501,7 @@ Count = 1 .h2 How Projects are Compiled -When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module is likewise marked with its class name; a class module's header lines are not compiled. +When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the UserControls, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module and UserControl is likewise marked with its class name; the header lines of a class module or UserControl are not compiled. .h2 Compile-Time Validation @@ -503,7 +511,7 @@ Before the source is handed to the compiler, the IDE scans every .frm file and c .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. 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 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. +.item Menu items -- only Name (read), Checked and Enabled can be used on a menu item in code; Caption, Visible, RadioCheck, Shortcut and ShortcutText are design-time settings. .endlist These checks use the same metadata the Properties panel and Toolbox read from each installed control, so property and method names always match the current widget set without any separate list to maintain. @@ -517,22 +525,23 @@ These checks use the same metadata the Properties panel and Toolbox read from ea 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 : unreadable or larger than 65535 bytes." Carriage returns are removed when files are loaded. Save Project Writes the .dbp file to disk. Close Project Closes the project, prompting to save unsaved changes. - Add File Adds a .bas, .frm or .cls file to the project. Opening a file without a project auto-creates an implicit project. + Add File Adds a .bas, .frm, .cls or .ctl file to the project. Opening a file without a project auto-creates an implicit project. New Class Module Adds a new class module to the project and opens it (see Class Modules below). + New UserControl Adds a new UserControl to the project and opens it (see UserControls in the IDE). Rename File Gives a file a new name on disk and in the project file (see Renaming a File below). 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, help file, startup form) and the Arguments for runs from the IDE. + Project Properties Opens a dialog for editing project metadata (name, type, author, company, version, copyright, description, icon, help file, startup form), whether more than one copy may run at once, and the Arguments for runs from the IDE. .endtable .h2 Renaming a File -Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save. +Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls, a UserControl .ctl and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save. -A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form or class module uses. Code that uses the old class name is not changed. A form keeps its Name property; rename the form itself in the Properties window. +A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form, class module or UserControl uses. Code that uses the old class name is not changed. A UserControl named after its file (as File > New UserControl makes it) takes the new name too, on its Begin UserControl line; forms that use the old name are not changed. A form keeps its Name property; rename the form itself in the Properties window. .h2 Project Explorer -The Project Explorer is a tree view window listing all project files, in three groups: Forms, Modules and Classes. Double-click a file to open it: .bas and .cls files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands. +The Project Explorer is a tree view window listing all project files, in four groups: Forms, Modules, Classes and Controls (UserControls). Double-click a file to open it: .bas, .cls and .ctl files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands. .h2 Class Modules @@ -556,8 +565,100 @@ Attribute VB_Name = "Class1" Option Explicit .endcode -Leave the VERSION and Attribute lines at the top of the file; they are not compiled as code. Each class name must be different from the other class names and the form names in the project. +Leave the VERSION and Attribute lines at the top of the file; they are not compiled as code. Each class name must be different from the other class names, the UserControl names and the form names in the project. +.link ide.usercontrols UserControls in the IDE +.link ide.overview Back to Overview + +.topic ide.usercontrols +.title UserControls in the IDE +.toc 1 UserControls in the IDE +.index UserControls in the IDE +.index New UserControl +.index .ctl Files +.index Control Projects +.index Make Control +.index Header Properties +.index UserMode in the Designer + +.h1 UserControls in the IDE + +A UserControl is a control you write in BASIC (see UserControls: Your Own Controls in the BASIC Language Reference for how to write one). This topic covers making one in the IDE, editing it, trying it on a form, and turning it into a control file every program can use. + +.h2 Making a UserControl + +Choose File > New UserControl. The IDE adds a new UserControl named UserControl1 (UserControl2, and so on, when that name is taken) to the project's Controls group, saved in the project's directory as UserControl1.ctl, and opens it in the Code Editor. It is written to disk when you save it. A new UserControl starts like this: + +.code +VERSION DVX 1.00 +Begin UserControl UserControl1 + Width = 64 + Height = 64 +End + +Option Explicit + +Private Sub UserControl_Paint () + +End Sub +.endcode + +The lines up to End are the control's header; the code follows. To use a .ctl file you already have, choose File > Add File and pick it (the UserControls file type). Open, save, rename and remove .ctl files as you do class modules: a UserControl named after its file takes the new name when the file is renamed. + +.h2 Writing the Code + +In the Code Editor, the Object list of a UserControl holds UserControl (the control itself) beside (General). Its Function list has the events the control gets: Initialize, Terminate, Resize, Paint, Click, DblClick, MouseDown, MouseMove, MouseUp, MouseWheel, KeyDown, KeyPress, KeyUp, GotFocus and LostFocus. Picking one that has no code yet writes its procedure with the right parameters, for example: + +.code +Private Sub UserControl_MouseDown (Button As Integer, Shift As Integer, X As Single, Y As Single) + +End Sub +.endcode + +These procedures are Private: a Public one would be a method of the control. + +.h2 The Header in the Properties Window + +While a UserControl is the active file, the Properties window shows its header instead of a form's controls: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description and DefaultEvent, each with the value the control takes (its default when the header leaves it out). Double-click a row to change it: + +.table + Row How it is changed + --- ----------------- + Name Type a new name. It must be a valid name that no form, class module or other UserControl of the project uses. Forms that use the old name are not changed. + Width, Height Enter a whole number of pixels (at least 1): the control's natural size. + CanGetFocus Each double-click switches between True and False. + BackColor Pick a color in the color dialog. + ToolboxIcon Type the name of a 24 x 24 picture file in the project's folder. + Description Type the toolbox tooltip. + DefaultEvent Pick one of the control's Events (or none): the event the designer opens when a placed control is double-clicked. +.endtable + +The change is written into the header lines of the file (the rest of the file stays as it is) and saved with the file. The Code Editor shows the new header; breakpoints in the code move with the lines below it. You can also edit the header lines in the Code Editor by hand. + +.h2 Trying It on a Form + +While the project is open, its UserControls are in the Toolbox like the controls DVX comes with: with the picture of their ToolboxIcon (or their name on the button), and their Description as the tooltip. Place one on a form as any other control. The control on the design surface is a real one: its code runs, with UserControl.UserMode False, so Initialize, Resize and Paint draw it as it will look, while your clicks and keys go to the designer. + +.list +.item The Properties window lists the control's properties that can be set (its Public variables and Property Let procedures), with the values its code gives them. A value you change there is passed to its Property Let and saved in the .frm; when the control's code calls UserControl.PropertyChanged, the window shows the new values. +.item The Code Editor's Function list for the placed control has the events the control raises, then the events a form gets for any of its controls (GotFocus, LostFocus, Validate, DragDrop and DragOver); picking one writes its handler with the event's parameters (Sub Gauge1_Changed (ByVal Level As Integer)). +.item Double-clicking the control on the design surface opens its DefaultEvent's handler. +.endlist + +The designer compiles the project's UserControls (with its modules and class modules, but not its forms) when the project opens and again when a .ctl file is saved, added, removed or renamed, or its header is changed in the Properties window; the controls on the design surface are made again then. While the controls have a compile error the designer cannot show them (the status line says so); compile with F5 to see the error. The code of the project's modules (outside their procedures) does not run in the designer, so module variables start empty there. A UserControl whose name is the name of a control installed for every program cannot be shown in the designer, and the program stops when it is run ("Control cannot be registered"): give it another name. + +When the program runs, its UserControls are its own: they are registered again for the program, separately from the copies in the designer. + +.h2 Control Projects and Make Control + +A project whose controls every program should be able to use is a control project: in File > Project Properties set Type to Control. A control project needs no forms. File > Make Executable becomes File > Make Control: it compiles the project, asks where to save the control file (the project's name with .wgt) and for a debug or release build, and writes the file -- the same file the BASIC compiler makes from the project. Run (F5) only compiles a control project: it is not a program. + +To install the control file, copy it into DVX's WIDGETS folder (in a folder of its own, like the controls DVX comes with). When DVX starts next, every program can use its controls, and the Toolbox shows them with their ToolboxIcon pictures and Descriptions. + +A program project can hold UserControls too (list them under Controls): the program built from it (Make Executable) carries them, and only that program uses them. + +.link lang.usercontrol UserControls: Your Own Controls +.link ide.project Project System .link ide.overview Back to Overview .topic ide.properties @@ -577,9 +678,12 @@ The Properties panel (Window > Properties) has two sections: .item Property list -- a two-column ListView below the tree showing property names and values for the selected control. Double-click a property value to edit it via an InputBox dialog. Changes take effect immediately in the designer preview. .endlist -Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls). +Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls). A UserControl placed on a form lists the properties its code defines. + +While a UserControl (.ctl) is the active file, the panel shows the control's header instead: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description and DefaultEvent (see UserControls in the IDE). .link ide.designer Form Designer +.link ide.usercontrols UserControls in the IDE .link ide.overview Back to Overview .topic ide.toolbox @@ -591,7 +695,7 @@ Each control type exposes different properties (e.g., Caption, Text, Width, Heig .h1 Toolbox -The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox. +The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox: the controls DVX comes with, the controls written in BASIC that are installed for every program (control files in the WIDGETS folder), and the open project's own UserControls (see UserControls in the IDE). The buttons are in alphabetical order of the control's name. A button shows the control's picture, or its name when it has none; its tooltip is the control's description. Click a tool to select it (the active tool name is stored in the designer state), then click on the form to place a new instance. Click the same tool again to deselect it and return to pointer mode. @@ -600,41 +704,44 @@ Click a tool to select it (the active tool name is stored in the designer state) .table VB Name Description ------- ----------- - CommandButton Push button that triggers a Click event. - Label Static text label. - TextBox Single-line text input field. - TextArea Multi-line text editor. CheckBox On/off checkbox. - OptionButton Radio button (mutually exclusive within a group). - ListBox Scrollable list of items. ComboBox Drop-down combo box. - DropDown Simple drop-down list. - PictureBox Canvas for drawing and images. - Frame Grouping container with a labeled border. - VBox Vertical layout container. - HBox Horizontal layout container. - WrapBox Flow layout container that wraps items to the next row. - Splitter Resizable split between two child panes. - ScrollPane Scrollable container for large content. - TabStrip Tabbed container with multiple pages. - ListView Multi-column list with headers. - TreeView Hierarchical tree control. - ProgressBar Progress indicator bar. - HScrollBar Horizontal slider/scrollbar. - SpinButton Numeric up/down spinner. - Line Horizontal or vertical separator line. - Spacer Invisible spacing element for layout. - Timer Non-visual timer that fires periodic events. - Toolbar Toolbar container for buttons. - StatusBar Status bar at the bottom of a form. - Terminal ANSI terminal emulator control. + CommandButton Push button that triggers a Click event. Data Data control for binding to a database. DBGrid Data-bound grid for displaying database query results. + DropDown Simple drop-down list. + Frame Grouping container with a labeled border. + HBox Horizontal layout container. + HelpPane One topic of a help file, as the Help Viewer shows it. + HScrollBar Horizontal slider/scrollbar. + Label Static text label. + Line Horizontal or vertical separator line. + ListBox Scrollable list of items. + ListView Multi-column list with headers. + OptionButton Radio button (mutually exclusive within a group). + PictureBox Canvas for drawing and images. + ProgressBar Progress indicator bar. + ScrollPane Scrollable container for large content. + Spacer Invisible spacing element for layout. + SpinButton Numeric up/down spinner. + Splitter Resizable split between two child panes. + StatusBar Status bar at the bottom of a form. + TabPage One page of a TabStrip, holding the controls shown on that tab. + TabStrip Tabbed container with multiple pages. + Terminal ANSI terminal emulator control. + TextArea Multi-line text editor. + TextBox Single-line text input field. + Timer Non-visual timer that fires periodic events. + Toolbar Toolbar container for buttons. + TreeView Hierarchical tree control. + VBox Vertical layout container. + WrapBox Flow layout container that wraps items to the next row. .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.usercontrols UserControls in the IDE .link ide.overview Back to Overview .topic ide.debugger @@ -1223,6 +1330,7 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec .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 Frame: the Caption from the .frm file or set by code now shows on the frame's border, and reading Caption gives it back (it was ignored and read back ""). .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. @@ -1246,7 +1354,7 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec .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. +.item Menu items accept only Name (read-only), Checked and Enabled in code; Caption, Visible, RadioCheck, Shortcut and ShortcutText are design-time settings, and using them in code is a compile error in the IDE. .endlist .h2 IDE @@ -1270,6 +1378,9 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec .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 .frm whose container has properties after the controls inside it (Index, Caption ...; DVX writes them before) opens as it runs: the properties are kept and the controls stay in that container. Before, they were dropped, and such a file with another control of the container's name opened as a blank form. Menu items' properties after their submenus are kept too. FrmOpen reads .frm files the same way. +.item The Toolbox lists the controls in alphabetical order, as CtlTypeName$ does. +.item UserControls: File > New UserControl, a Controls group in the Project Explorer, the UserControl object and its events in the Code Editor, the header in the Properties window, the project's UserControls in the Toolbox and on the design surface (their code runs, with UserMode False), and control projects with File > Make Control. A program built with Make Executable carries its UserControls. See UserControls in the IDE. .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 diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs index a2c49796..99ec9863 100644 --- a/src/apps/kpunch/dvxbasic/langref.dhs +++ b/src/apps/kpunch/dvxbasic/langref.dhs @@ -516,6 +516,7 @@ REM This is a comment .index END ENUM .index DECLARE .index DECLARE LIBRARY +.index BINARY STRING .index SHARED .index STATIC .index OPTION @@ -586,7 +587,7 @@ A module-level variable is visible inside a SUB or FUNCTION only when it is decl Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n. -DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules. +DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules. DIM variable AS NEW FormName makes a new copy of a form of the project (see Several Copies of a Form in the Form topic); DIM variable AS FormName declares a variable for that form or a copy of it. .h2 Variable Scope and SHARED @@ -807,6 +808,18 @@ End Declare 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. A Byte parameter or result is 8 bits. A Currency parameter receives the amount as a 64-bit whole number of ten-thousandths; a DECLARE FUNCTION cannot return a Currency. +A String parameter or result declared AS STRING is text: it ends at the first CHR$(0). To pass or receive any bytes, CHR$(0) included (network data, sound samples, encrypted data, checksums of binary data), the declaration says BINARY STRING instead. To the program it is an ordinary String; LEN, MID$, ASC and the other string functions see every byte. BINARY STRING is allowed only in a DECLARE LIBRARY block, and the shipped include files already use it where a function works with bytes. + +.code +Declare Library "mylib" + Declare Function MyChecksum(ByVal bytes As Binary String) As Long + Declare Function MyRead$(ByVal handle As Long, ByVal maxLen As Long) As Binary String +End Declare + +packet$ = "A" + Chr$(0) + "B" +Print MyChecksum(packet$) +.endcode + .link lang.runtime See also: BASIC Runtime Libraries .h2 STATIC @@ -948,6 +961,7 @@ Stores an object reference -- a form, a control, an instance of a class module, .code SET variable = objectExpression SET variable = NEW ClassName +SET variable = NEW FormName SET variable = Nothing .endcode @@ -963,6 +977,10 @@ c.Text = "Hello" Dim list As Collection Set list = New Collection ' a new, empty Collection Set list = Nothing ' let it go + +Dim w As Form1 +Set w = New Form1 ' a second Form1 window +w.Show .endcode The value must be an object or Nothing; anything else raises error 424 (Object required). SET is not used for ordinary numeric or string assignment. @@ -1216,6 +1234,8 @@ Sub Greet(ByVal name As String) End Sub .endcode +A SUB, FUNCTION or Property procedure stands on its own in the program: defining one inside another procedure, or inside an IF, FOR, DO, WHILE, SELECT CASE or WITH block, is a compile error. + 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. A by-reference parameter is the caller's variable itself, as in Visual Basic: a write to it changes the variable at once (the caller's code, or another parameter naming the same variable, sees the new value straight away, even if the procedure later stops with an error), and what is stored is converted to the variable's own type (a STRING * n keeps its length). This holds for a plain variable, an array element (`bump a(3)` changes `a(3)` in place), a TYPE field (`bump rec.count`, `bump recs(2).count`, `bump rec.inner.count`) and, inside a class, one of the class's own member variables. While an array element (or a field of one) is passed by reference, its array cannot be given a new size or erased: REDIM of that array, or ERASE of it when it is a dynamic array, raises error 10 (This array is fixed or temporarily locked), as in Visual Basic (ERASE of a fixed-size array only clears its elements and is allowed). The array is free again as soon as the call returns. Assigning a whole TYPE value to the variable (or element) whose field is being passed copies the value into the same record, so the parameter sees the new value (as in Visual Basic). 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 @@ -1353,7 +1373,7 @@ Function Square(ByVal n As Double) As Double End Function .endcode -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. +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. Inside the function its name on its own reads the value set so far (Total = Total + n), as in Visual Basic; with an argument list -- F() or F(n - 1) -- it calls the function again. Use EXIT FUNCTION to return early. A FUNCTION can also be called as a statement, with or without parentheses; its result is discarded: @@ -1675,6 +1695,8 @@ Each SUB and FUNCTION has its own ON ERROR handler: .item The label named by ON ERROR GOTO must be in the same procedure. .endlist +An error that no handler traps ends the program, as in Visual Basic, wherever it happens: in module-level code or in an event handler (a Click, a Timer, a form's Load ...). DVX shows it in a BASIC Runtime Error box -- the error number, the line and the message -- and writes it to the DVX log; the program's windows close once the box is dismissed. In the IDE the Output window shows the error and the editor goes to its line. + ON ERROR RESUME NEXT, as in Visual Basic, needs no handler: a statement that fails is abandoned and the program goes on at the next one, with ERR (Err.Number) and Err.Description telling what went wrong. An error in a procedure that has no handler of its own leaves that procedure, and the program goes on after the statement that called it. The statement clears ERR, and so does the end of the procedure that used it; check ERR right after the statements that may fail: .code @@ -1764,6 +1786,7 @@ ErrorHandler: 449 Argument not optional (a method call through an object left out a parameter that is not OPTIONAL) 450 Wrong number of arguments or invalid property assignment 451 Object not a collection (FOR EACH over an object that is not a Collection) + 453 Library or function not installed (a DECLARE LIBRARY function whose library is not on this computer; see Include Libraries) 457 This key is already associated with an element of this collection (Collection.Add with a key in use) 481 Invalid picture (LOADPICTURE of a file that is not a picture DVX can show) 1000 No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner) @@ -1928,7 +1951,7 @@ If STRIG(0) Then FireShot .h2 FRAMEWAIT -Waits until the next frame of a steady clock of rate frames a second begins, keeping the desktop and the program's forms running meanwhile. A game loop that calls it once a frame runs at the same speed on every computer. Frames the program was too busy for are skipped, not caught up. rate must be at least 1; rates above 1000 count as 1000. Changing the rate starts the clock over. +Waits until the next frame of a steady clock of rate frames a second begins, keeping the desktop and the program's forms running meanwhile. A game loop that calls it once a frame runs at the same speed on every computer. Frames the program was too busy for are skipped, not caught up; FrameMissed() in game.bas counts them. rate must be at least 1; rates above 1000 count as 1000. Changing the rate starts the clock over. .code FRAMEWAIT rate @@ -3077,7 +3100,7 @@ Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") .h2 LOADPICTURE -LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG or GIF) and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. +LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG or GIF) -- or the icon of a DVX program, widget or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481). The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3) or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2) or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5. @@ -3134,6 +3157,8 @@ A program gets arguments when a document of a type associated with it is opened .index CreateControl .index SetEvent .index RemoveControl +.index LoadFormText +.index LOADFORMTEXT .index With .index vbModal .index Control Arrays @@ -3149,9 +3174,10 @@ DVX BASIC supports Visual Basic-style forms and controls for building graphical LOAD FormName UNLOAD FormName UNLOAD Me +UNLOAD formVariable .endcode -LOAD creates the form and its controls in memory. It fires Form_Load when the form is first loaded. UNLOAD destroys the form, firing Form_QueryUnload (which can cancel the close) and then Form_Unload. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. +LOAD creates the form and its controls in memory. It fires the form's Load event (Form1_Load for a form named Form1) when the form is first loaded. UNLOAD destroys the form, firing its QueryUnload event (which can cancel the close) and then its Unload event. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. UNLOAD also takes a Form variable, or anything else that gives a form (for example a form made with CreateForm and kept in an object's property), and unloads that form. .h2 Showing and Hiding Forms @@ -3266,7 +3292,7 @@ In addition to loading forms from .frm files, you can build a form entirely from SET formVar = CreateForm(name$, width%, height%) .endcode -Creates a new form with the given name and initial size. Returns a form reference. +Creates a new form with the given name and initial size. Returns a form reference. If a form of that name is already loaded, CreateForm returns that form; to open another copy of a form designed in a .frm file, use New FormName (see Several Copies of a Form in the Form topic). .h3 CreateControl @@ -3292,6 +3318,29 @@ REMOVECONTROL formRef, ctrlName$ 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. +.h3 LoadFormText + +.code +SET formVar = LOADFORMTEXT(frmText$) +.endcode + +Loads a form from the text of a .frm file -- one a program made, read from a file or got from somewhere else -- and returns it, loaded but hidden like Load (its Load event runs; call Show). A form whose name is already loaded becomes another copy of its own, as with New. The form's controls fire events to procedures named after them in the running program (Gen1_Load, Go_Click), as for any form; a code section after the form's End is not compiled. Text that is not a form raises error 5 (Illegal function call). Form designers use it to show a .frm file being edited; Controls, CallByName and the designer.bas functions then read and change it. + +.code +Dim f As Object +Dim n As Integer +Dim t As String + +n = FreeFile +Open "PREVIEW.FRM" For Binary As #n +t = Input$(LOF(n), #n) +Close #n + +Set f = LoadFormText(t) +f.Show +Print f.Controls.Count; f.Controls(1).Name +.endcode + .code Dim frm As Form Dim lbl As Control @@ -3365,14 +3414,14 @@ End If .h2 Event Handler Convention -Event handlers are named ControlName_EventName and defined as SUBs: +Event handlers are named ControlName_EventName and defined as SUBs. A form's own events are named after the form: the Load event of a form named Form1 is Form1_Load. (Visual Basic's Form_Load spelling is not used: every procedure name is shared by the whole program, so each form's handlers need names of their own.) .code Sub Command1_Click() MsgBox "Button clicked!" End Sub -Sub Form_Load() +Sub Form1_Load() Me.Caption = "My App" End Sub @@ -3398,12 +3447,13 @@ End Sub Scroll Control was scrolled (mouse wheel or scrollbar) GotFocus Control received input focus LostFocus Control lost input focus - Form_Load Form is being loaded (fires once, after controls are created) - Form_QueryUnload Form is about to close; set Cancel = 1 to abort - Form_Unload Form is being unloaded - Form_Resize Form was resized - Form_Activate Form gained focus - Form_Deactivate Form lost focus + Form1_Load Form is being loaded (fires once, after controls are created) + Form1_QueryUnload Form is about to close; set Cancel = 1 to abort + Form1_Unload Form is being unloaded + Form1_Resize Form was resized + Form1_Activate Form gained focus + Form1_Deactivate Form lost focus + Form1_NewArgs The running program was handed new arguments (COMMAND$ changed) Timer1_Timer Timer control interval elapsed (Timer control's default event) .endtable @@ -3428,6 +3478,8 @@ End Sub .index Default Member .index Reference Counting .index Late Binding +.index CallByName +.index CALLBYNAME .index Me in Classes .index FRIEND .index Friend @@ -3599,6 +3651,24 @@ Arguments of a class's procedures follow the rules of SUB and FUNCTION calls whe An object parameter is itself passed like any other parameter: without BYVAL, SET on the parameter changes the caller's variable (a SUB can make a new object for the caller); with BYVAL, the procedure gets its own reference to the same object, and SET on it does not affect the caller. +.h2 CALLBYNAME + +.code +result = CALLBYNAME(object, memberName$, callType [, arguments...]) +CALLBYNAME object, memberName$, callType [, arguments...] +.endcode + +Uses the member of an object whose name the program has in a string, as if it had been written after a dot: a form, a control, a class instance or a Collection. callType says what to do: vbGet reads a property (with arguments, a property that takes them), vbLet assigns it the last argument, vbSet assigns it an object (the last argument), vbMethod calls a method or function with the arguments. The function gives the property's value or the method's result (Empty for vbLet and vbSet); as a statement the result is dropped. Another callType raises error 5 (Illegal function call), as does vbLet or vbSet without a value; a member the object does not have raises the error the same member written after a dot would. A form designer's property window uses it to read and set the properties designer.bas lists for a control type. + +.code +Dim prop As String +prop = "Caption" +Print CallByName(Command1, prop, vbGet) ' Command1.Caption +CallByName Command1, prop, vbLet, "OK" ' Command1.Caption = "OK" +CallByName List1, "AddItem", vbMethod, "one" ' List1.AddItem "one" +Print CallByName(acct, "Balance", vbGet) ' a class instance's property +.endcode + .h2 Default Member A class can name one property or method as its default member, used when the object is written with parentheses directly after it. Put this line in the class, inside the procedure (as Visual Basic writes it) or on its own at the top of the class: @@ -3663,6 +3733,7 @@ NoMoney: .link lang.events See also: Events .link lang.interfaces See also: Interfaces (Implements) +.link lang.usercontrol See also: UserControls: Your Own Controls .link lang.collection See also: Collection Object .link lang.variants See also: Variants .link lang.declarations See also: SET and PUBLIC / PRIVATE @@ -4108,6 +4179,11 @@ Print "Running from: " & App.Path .index Clipboard.GetText .index Clipboard.SetText .index vbCFText +.index vbKeyDelete +.index vbKeyReturn +.index vbKeyBack +.index vbCtrlMask +.index Key Codes .h1 Clipboard Object @@ -4129,6 +4205,78 @@ If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText() A control or form of your own named Clipboard takes the name's place. +.topic lang.screenobj +.title Screen Object +.toc 1 Screen Object +.index Screen Object +.index Screen.Width +.index Screen.Height +.index Screen.BitsPerPixel +.index Screen.DebugLayout +.index Screen.MousePointer +.index Screen.Arrange +.index Arrange +.index MousePointer +.index vbHourglass +.index vbCascade +.index vbTileHorizontal +.index vbTileVertical +.index vbTileGrid + +.h1 Screen Object + +The Screen object is the DVX desktop, as in Visual Basic: its size, so a program can place its forms (for example in a corner of the screen), its colour depth, the mouse pointer, and the arrangement of the windows on it. + +.table + Property Type R/W Description + -------------------- ------- --- ----------- + Screen.Width Long R Width of the desktop in pixels + Screen.Height Long R Height of the desktop in pixels + Screen.BitsPerPixel Long R Colour depth of the display: 8, 15, 16, 24 or 32 bits per pixel + Screen.MousePointer Integer R/W vbHourglass (11) for the busy pointer while a long job runs; another pointer (vbArrow, vbCrosshair, vbSizeNS, vbSizeWE, vbSizeNWSE, vbSizeNESW, vbNoDrop: see Mouse Constants) over every form of the program, whatever their controls' MousePointer; vbDefault (0) back to each control's own + Screen.ActiveControl Control R The control of the program that has the keyboard focus, Nothing when none has it (another program's window has the focus) + Screen.DebugLayout Boolean R/W True draws a coloured border around every layout container (VBox, HBox, Frame ...) in every window, to see how a form is laid out; False removes them +.endtable + +The sizes are in pixels, the unit of a form's Left, Top, Width and Height in DVX (Visual Basic uses twips there). + +.code +' Put the form at the right edge, 40 pixels from the top +Me.Left = Screen.Width - Me.Width - 40 +Me.Top = 40 +.endcode + +Set the hourglass before a long job and put the arrow back after it. The pointer is the whole desktop's, so the program should not leave it busy; when the program ends, a busy pointer it set goes back to the arrow. + +.code +Screen.MousePointer = vbHourglass +CountWords +Screen.MousePointer = vbDefault +.endcode + +.h2 Arrange + +Screen.Arrange arrangement puts every window on the desktop that is shown and not minimized (those of other programs too) in order, as the Window menu of a program does: + +.table + Constant Value Arrangement + ---------------- ----- ----------- + vbCascade 0 Overlapping, each one a little down and to the right of the one before + vbTileHorizontal 1 Stacked from top to bottom, each the full width of the screen + vbTileVertical 2 Side by side from left to right, each the full height of the screen + vbTileGrid 4 In rows and columns +.endtable + +The values are those of Visual Basic's MDIForm.Arrange (vbTileGrid is DVX's own; 3, Visual Basic's vbArrangeIcons, is not used). + +.code +Sub mnuCascade_Click () + Screen.Arrange vbCascade +End Sub +.endcode + +Any other property, method or value stops the program with an error. The QBasic statement SCREEN and function SCREEN(row, column) are unaffected: Screen followed by a dot is the object. A control or form of your own named Screen takes the name's place. + .topic lang.ini .title INI Functions .toc 1 INI Functions @@ -4184,6 +4332,9 @@ IniWrite App.Config & "\app.ini", "Display", "FontSize", Str$(fontSize) .index vbNo .index vbRetry .index vbNormal +.index vbMinimized +.index vbMaximized +.index vbDefault .index vbReadOnly .index vbHidden .index vbSystem @@ -4212,6 +4363,21 @@ IniWrite App.Config & "\app.ini", "Display", "FontSize", Str$(fontSize) .index vbLeave .index vbOver .index vbCFText +.index vbLeftButton +.index vbRightButton +.index vbMiddleButton +.index vbSyntaxText +.index vbSyntaxKeyword +.index vbSyntaxString +.index vbSyntaxComment +.index vbSyntaxNumber +.index vbSyntaxOperator +.index vbSyntaxType +.index Mouse Constants +.index vbMethod +.index vbGet +.index vbLet +.index vbSet .index vbUser .index vbTwips .index vbPoints @@ -4302,6 +4468,22 @@ Add an icon flag to the button style (with + or OR) to display an icon in the me vbModal 1 Show form as modal dialog .endtable +.h2 Window State, Arrangement and Mouse Pointer + +A form's WindowState and Screen.Arrange (see Form and Screen Object); the MousePointer values are under Mouse Constants. + +.table + Constant Value Description + -------- ----- ----------- + vbNormal 0 WindowState: a normal window + vbMinimized 1 WindowState: shown only as an icon + vbMaximized 2 WindowState: filling the screen + vbCascade 0 Arrange: overlapping windows + vbTileHorizontal 1 Arrange: windows stacked top to bottom + vbTileVertical 2 Arrange: windows side by side + vbTileGrid 4 Arrange: windows in rows and columns +.endtable + .h2 File Attribute Flags Bit values used with GETATTR and SETATTR. @@ -4374,6 +4556,90 @@ The DragMode property, the Drag method and the State argument of DragOver. vbCFText 1 Text, the format the DVX clipboard holds .endtable +.h2 Mouse Constants + +The Button argument of the MouseDown, MouseUp and MouseMove events (several bits when several buttons are down), and the values of MousePointer (controls, forms and Screen). + +.table + Constant Value Description + -------- ----- ----------- + vbLeftButton 1 The left button + vbRightButton 2 The right button + vbMiddleButton 4 The middle button + vbDefault 0 MousePointer: the control's own pointer + vbArrow 1 MousePointer: the arrow + vbCrosshair 2 MousePointer: a cross, for placing things + vbSizeNESW 6 MousePointer: two-way arrow, top right to bottom left + vbSizeNS 7 MousePointer: two-way arrow, up and down + vbSizeNWSE 8 MousePointer: two-way arrow, top left to bottom right + vbSizeWE 9 MousePointer: two-way arrow, left and right + vbHourglass 11 MousePointer: busy + vbNoDrop 12 MousePointer: a circle with a line through it +.endtable + +Visual Basic's other pointers (vbIbeam, vbUpArrow, custom pointers) have no DVX shape; DVX has no MouseIcon. + +.h2 Syntax Colour Constants + +The kinds of text a TextArea colours: the index of SetSyntaxColor and GetSyntaxColor, and the digits of the Colors argument of the Colorize event ("0" is vbSyntaxText ... "6" vbSyntaxType). + +.table + Constant Value Description + -------- ----- ----------- + vbSyntaxText 0 Ordinary text (ForeColor) + vbSyntaxKeyword 1 Keywords + vbSyntaxString 2 Strings + vbSyntaxComment 3 Comments + vbSyntaxNumber 4 Numbers + vbSyntaxOperator 5 Operators + vbSyntaxType 6 Type names +.endtable + +.h2 CallByName Constants + +.table + Constant Value Description + -------- ----- ----------- + vbMethod 1 Call a method (or a function of a class) + vbGet 2 Read a property + vbLet 4 Assign a property a value + vbSet 8 Assign a property an object (Set) +.endtable + +.h2 Key Code Constants + +The KeyCode a KeyDown event gets, and the bits of its Shift argument. These are DVX's codes, so compare KeyCode with the constants rather than with numbers from Visual Basic. A character key gives its character's code (Asc("a") with no Shift), and Ctrl with a letter gives 1 to 26 (Ctrl+A is 1, Ctrl+C 3, Ctrl+V 22, Ctrl+X 24). + +.table + Constant Value Description + -------- ----- ----------- + vbKeyBack 8 Backspace + vbKeyTab 9 Tab + vbKeyReturn 13 Enter + vbKeyEscape 27 Esc + vbKeySpace 32 Space bar + vbKeyPageUp, vbKeyPageDown &H149, &H151 Page Up, Page Down + vbKeyEnd, vbKeyHome &H14F, &H147 End, Home + vbKeyLeft, vbKeyUp &H14B, &H148 Arrow keys + vbKeyRight, vbKeyDown &H14D, &H150 Arrow keys + vbKeyInsert, vbKeyDelete &H152, &H153 Ins, Del + vbKeyF1 to vbKeyF10 &H13B to &H144 Function keys F1 to F10 + vbKeyF11, vbKeyF12 &H185, &H186 Function keys F11 and F12 + vbShiftMask 3 Shift argument: a Shift key is down + vbCtrlMask 4 Shift argument: Ctrl is down + vbAltMask 8 Shift argument: Alt is down +.endtable + +.code +Sub List1_KeyDown(KeyCode As Integer, Shift As Integer) + If KeyCode = vbKeyDelete Then + DeleteSelected + ElseIf KeyCode = 1 And (Shift And vbCtrlMask) <> 0 Then + SelectEverything ' Ctrl+A + End If +End Sub +.endcode + .h2 Scale and Picture Constants The scale units of ScaleMode and of the ScaleX / ScaleY methods, and the values of a Picture object's Type (see Drawing on Forms and PictureBoxes, and LOADPICTURE). @@ -4460,11 +4726,27 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index Include Libraries .index commdlg.bas .index DlgColor +.index DlgButtons .index Color Dialog .index sql.bas .index comm.bas .index resource.bas +.index ResGetData$ +.index ResAddData +.index RES_NAME_ICON +.index DlgIntInput +.index DlgIntInputStep +.index DlgInputCancelled +.index Number input dialog .index help.bas +.index HelpViewTopic +.index hlpfile.bas +.index HlpOpen +.index HlpSearch +.index HlpLoadTopic +.index HelpPane (help files) +.index Help Files +.index Help viewer in BASIC .index DECLARE LIBRARY .index CommHandshake .index CommRecvEncrypted @@ -4480,6 +4762,9 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index HostLookup$ .index game.bas .index GameKeyDown +.index GameKeyClear +.index FrameMissed +.index GameWaitRetrace .index joystick.bas .index JoyCount .index JoyX @@ -4487,6 +4772,11 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index JoyAxis .index JoyButton .index JoyHat +.index JoyCalibration +.index JoyIsKeyboard +.index JoyKeyIndex +.index JoyKeyCode +.index JoyKeyNameOf$ .index Joystick .index sound.bas .index PlayWave @@ -4499,6 +4789,26 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index SoundVolume .index SoundInfo$ .index SoundError$ +.index PlayTriangle +.index SoundLoad +.index SoundCreate +.index SoundPlay +.index SoundFree +.index SoundLength +.index SoundSetVolume +.index SoundSetPan +.index SoundSetPitch +.index SoundSetFrequency +.index SoundGetVolume +.index SoundCaps +.index SoundPlayEvent +.index SoundStreamOpen +.index SoundStreamWrite +.index SoundStreamClose +.index FmAlloc +.index FmWrite +.index FmRawAcquire +.index FM synthesizer .index Sound .index music.bas .index MusicLoad @@ -4519,8 +4829,189 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic .index MusicProgram .index MusicControl .index MusicPitchBend +.index MusicInfo$ +.index MusicGetVolume +.index MidiAcquire +.index MidiPut +.index MidiPortInfo$ .index Music .index MIDI +.index conn.bas +.index ConnCreate +.index ConnOpen +.index ConnPoll +.index ConnAttach +.index ConnSendFiles +.index ConnReceiveFiles +.index Connections +.index Modem +.index Telnet +.index xfer.bas +.index XferStatus +.index XferAnswer +.index File Transfers +.index ZMODEM +.index prefs.bas +.index PrefsLoad +.index PrefsCreate +.index PrefsSaveAs +.index Settings Files +.index ShellConfigFile$ +.index DlgFileOpenIn$ +.index DlgFileSaveIn$ +.index FileOpsMakePath +.index FileOpsReplaceFile +.index FileOpsValidName +.index system.bas +.index SysSetVideoMode +.index SysSetColor +.index SysGetColor +.index SysLoadTheme +.index SysSetWallpaper +.index SysSetMouse +.index PrefsDvxIni$ +.index PrefsChoiceLabel$ +.index PrefsGetChoice +.index Control Panel settings +.index Library not installed +.index Error 453 +.index compiler.bas +.index debug.bas +.index designer.bas +.index CtlTypeCount +.index CtlTypeName$ +.index CtlTypeFlags +.index CtlPropCount +.index CtlPropName$ +.index CtlPropType +.index CtlEventList$ +.index CtlEventParams$ +.index CtlTypeEventParams$ +.index Form designers +.index Code editors +.index CompSourceCreate +.index CompSourceFromProject +.index CompCompile +.index CompError$ +.index CompBuildApp +.index CompDesignControls +.index CompDesignRelease +.index CompProcList$ +.index ProjCtlNew$ +.index ProjCtlHeader$ +.index ProjCtlSetHeader$ +.index ProjCtlKeys$ +.index RunCreate +.index RunStart +.index RunPoll +.index RunOutput$ +.index DbgSetBreakpoints +.index DbgStepOver +.index DbgLocals$ +.index DbgEval$ +.index DbgExec$ +.index DbgSetVar +.index Compiling programs +.index Debugging programs +.index listhelp.bas +.index ListSelCreate +.index ListSelNavigate +.index ListTypeAhead +.index ListIconMetric +.index ListIconLabel$ +.index Lists you draw yourself +.index Icon views you draw yourself +.index security.bas +.index SecRandom$ +.index SecDhCreate +.index SecCipherCreate +.index SecSha256$ +.index SecHmac$ +.index Encryption +.index SerSet +.index SerLastError +.index CommHandshakeStep +.index CommLastError +.index PrintChoose +.index PrintForm +.index PrintPageSetting +.index PrintPortStatus +.index TcpPeer$ +.index HostName$ +.index XferCrc32 +.index layout.bas +.index SizeChars +.index SizePercent +.index Sizes in characters +.index MOUSE_WHEEL_STEP_MAX +.index MOUSE_DBLCLICK_DEFAULT_MS +.index SysFontGlyphRow +.index System font glyphs +.index ShellProgramsFolder$ +.index ShellItemIcon$ +.index ShellItemKeyValid +.index ShellGroupsLoad +.index ShellItemGet$ +.index Program groups +.index PROGMAN.INI +.index PrefsGetChoiceFrom +.index ShellRunningId +.index ShellRunningName$ +.index ShellRunningFind +.index ShellRunningActivate +.index ShellRunningClose +.index ShellRunningKill +.index ShellAppIsHung +.index ShellAppMemory +.index ShellAppId +.index ShellSetPriority +.index ShellPriority +.index SHELL_PRIORITY_NORMAL +.index ShellCommandProgram$ +.index ShellCommandArgs$ +.index ShellArgCount +.index ShellArg$ +.index Running programs +.index Program priority +.index SysScreenshot +.index SysMouseX +.index SysMouseButtons +.index SysSetMousePos +.index SysShiftState +.index SysSendKey +.index SendKeys +.index SysMemoryTotalKb +.index SysMemoryFreeKb +.index SYS_VIDEO_DEFAULT_WIDTH +.index PrefsKeyName$ +.index PrefsVideoWidth +.index PrefsChoiceDefault +.index FileOpsSetModified +.index FileOpsPathIsInside +.index FileOpsPathIsAbsolute +.index FileOpsPathSep$ +.index FileOpsFilterMatch +.index FileOpsDriveType +.index FileOpsTotalBytes +.index FileOpsBootDrive$ +.index FileOpsIsWritable +.index FileOpsReadOnlySystem +.index FileOpsTempDir$ +.index FileOpsJobClearAttr +.index FileOpsJobBytesDone +.index FILEOPS_ATTR_EDITABLE +.index HelpCompileMessages$ +.index CompTokens$ +.index CompInCode +.index CompRenameInCode$ +.index CompWithEvents$ +.index CompClassEvents$ +.index CompHelpTopic$ +.index CompKeywordClass +.index ProjOpen +.index ProjNew +.index ProjGet$ +.index Project files .h1 Include Libraries @@ -4529,17 +5020,30 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory .table File Provides ---- -------- - commdlg.bas File Open/Save dialogs, input dialogs, choice dialogs, save-prompt dialogs, the color dialog + commdlg.bas File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, message boxes with your own buttons (DlgButtons) sql.bas SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.) - 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; port settings read back and changed, why a call failed, breaks, ring and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...) + conn.bas Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy or Telnet, shown on a Terminal control, with capture, answerback and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...) + xfer.bas File transfers with XMODEM, XMODEM-1K, YMODEM and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...) + prefs.bas Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...) 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 - 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 - game.bas Games: whether a key is being held down right now (GameKeyDown) - joystick.bas Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...) - sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...) - music.bas Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, ...) + help.bas Compile help source files and see the compiler's messages, and open the DVX help viewer, at a topic if you like (HelpCompile, HelpCompileMessages$, HelpView, HelpViewTopic; HelpView "" opens the DVX user guide) + hlpfile.bas Read compiled help files yourself, for a help viewer of your own: topics, table of contents, keyword index, full-text search, and each topic's headings, text, links and pictures; show a topic in a HelpPane control (HlpOpen, HlpTocTitle$, HlpIndexKeyword$, HlpSearch, HlpLoadTopic, ...) + net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...) + print.bas Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...) + listhelp.bas Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...) + security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...) + game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace) + joystick.bas Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...) + sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...) + music.bas Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$) + fileops.bas Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving and deleting whole folders at once or as a job with progress, Cancel and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...) + system.bas What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...) + layout.bas Control sizes in characters or in percent of the container instead of pixels (SizeChars, SizePercent) + compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...) + debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...) + designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...) + shell.bas DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, this program's settings folder, the DVX log; Program Manager's program groups with their items, shortcut keys and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellConfigFile$, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...) .endtable .h2 Using an Include Library @@ -4554,6 +5058,37 @@ If path <> "" Then End If .endcode +.h2 When a Library Is Not Installed + +Some libraries are optional parts of DVX (the network, printing, sound, joysticks ...). Calling a function of a library that is not on the computer raises run-time error 453, "Library or function not installed", which ON ERROR can trap -- for example to show a message instead of a page of settings, as Control Panel does: + +.code +Dim status As String +On Error Resume Next +status = NetStatus$("message") +If Err.Number = 453 Then + lblNet.Caption = "The network is not installed." +Else + lblNet.Caption = status +End If +On Error GoTo 0 +.endcode + +.h2 Message Boxes with Your Own Buttons + +MsgBox offers fixed sets of buttons (OK, Yes and No, ...). With commdlg.bas in the project, DlgButtons(title, message, buttons, icon) shows a message box with the buttons you list, separated by |, with & before each button's access key. It returns the index of the button chosen: 0 for the first, which Enter presses. Esc and closing the box choose the last button, so make the last one the choice that changes nothing. icon is vbQuestion, vbInformation, vbExclamation, vbCritical or 0. Up to six buttons; -1 is returned when the list is empty or longer. + +.code +Select Case DlgButtons("Copy", "notes.txt already exists. Replace it?", "&Yes|&No|Yes to &All|&Cancel", vbQuestion) + Case 0 + ' replace this one + Case 2 + ' replace this and every other one + Case Else + ' keep it (No), or stop (Cancel) +End Select +.endcode + .h2 Color Dialog With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels. @@ -4567,6 +5102,31 @@ If c <> -1 Then End If .endcode +.h2 Number Input Dialog + +With commdlg.bas in the project, DlgIntInput(title, prompt, default, min, max) asks for a whole number with a spin box, and DlgIntInputStep(title, prompt, default, min, max, step) does the same with the arrows moving by step. Both return the number entered, or default when the user cancels; DlgInputCancelled() (True after a Cancel, False after OK) tells the two apart, as it does for DlgInputBox$. + +.code +n = DlgIntInputStep("Zoom", "Zoom in percent:", 100, 25, 400, 25) + +If Not DlgInputCancelled() Then + SetZoom n +End If +.endcode + +.h2 Resources in Files + +With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget and library files: icons, text and any bytes. ResOpen, ResCount, ResName$, ResType and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT or RES_TYPE_ICON). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters. + +.code +Dim settings As String +settings = Chr$(1) + Chr$(0) + Chr$(255) + +If ResAddData("mygame.app", "state", RES_TYPE_BINARY, settings) Then + Print Len(ResGetData$("mygame.app", "state")) ' 3 +End If +.endcode + .note info DVX BASIC does not have an $INCLUDE directive. Libraries are added to the project so that the compiler sees their DECLARE LIBRARY blocks. .endnote @@ -4588,6 +5148,41 @@ End If After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True. +CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL. + +.code +Sub Timer1_Timer () + Select Case CommHandshakeStep(link, txtPass.Text) + Case COMM_PENDING + lblState.Caption = "Connecting..." + Case COMM_OK + Timer1.Enabled = False + lblState.Caption = "Connected" + Case COMM_ERR_AUTH + Timer1.Enabled = False + lblState.Caption = "Wrong passphrase" + Case Else + Timer1.Enabled = False + lblState.Caption = "The other side does not answer" + End Select +End Sub +.endcode + +.h2 Serial Port Settings and Errors + +With comm.bas, SerOpen returns False when a port cannot be opened; SerLastError(com) then says why, as an SER_ERR_ constant (SER_ERR_NO_UART: there is no port there, SER_ERR_IRQ_NOT_FOUND, SER_ERR_INVALID_BAUD, ...), or SER_OK when the last SerOpen or SerSet worked. + +.table + Function Description + -------- ----------- + SerSet(com, baud, dataBits, parity, stopBits, handshake) Change the settings of an open port (SerOpen's arguments). True when done. + SerGetBaud(com), SerGetDataBits(com), SerGetParity$(com), SerGetStopBits(com), SerGetHandshake(com) The settings of an open port; parity is "N", "E", "O", "M" or "S". When the port is not open the numbers are an SER_ERR_ code and the parity "". + SerTxPending(com), SerFlushTx com Bytes written but not sent yet; throw them away. + SerSetBreak com, state Send a break: True holds the line in the break state, False releases it (hold it a quarter to half a second). + SerGetRi(com) True while the other end rings. + SerLineErrors(com) Errors seen on received data, added up: SER_LINE_OVERRUN (data came faster than it was read), SER_LINE_PARITY, SER_LINE_FRAMING, SER_LINE_BREAK; 0 for none. +.endtable + .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. @@ -4604,6 +5199,258 @@ End Sub For printer output a line at a time, see LPRINT and OPEN "LPT1:". +A program that prints its own way has more: + +.table + Function Description + -------- ----------- + PrintPageCount(text) How many pages text takes with the saved page setup and printer. + PrintTextPages(text, title, firstPage, lastPage, copies) Print text without asking: those pages (0 and 0 for all), copies times. + PrintChoose(title, pageCount) Show the Print dialog only (the page range when pageCount is above 0). True when the user chose Print; then PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst and PrintChoiceLast say what was chosen (pages 0 and 0: all). + PrintForm(formName) / PrintFormDialog(formName) Print what a loaded form shows, scaled to the page (a printer model that prints pictures); the second shows the Print dialog first. + PrintPortPresent(port), PrintPortStatus(port) Whether printer port LPTn (1 to 3) exists, and its state: PRINT_STATUS_ values added up (PRINT_STATUS_PAPER_OUT, PRINT_STATUS_SELECTED, PRINT_STATUS_NOT_BUSY, ...). + PrintStatusProblem$(status, afterWrite, where) What is wrong with a printer in that state, in words ("" when nothing is), for printer where such as "LPT1". +.endtable + +.code +If PrintChoose("Report", 0) Then + If PrintDestIsFile(PrintChoiceDest()) Then + Open PrintChoiceFile$() For Output As #1 + Else + Open "LPT" & (PrintChoiceDest() + 1) & ":" For Output As #1 + End If + Print #1, "Report" + Close #1 +End If +.endcode + +The page setup can be read and changed without the Page Setup dialog. Like the printer settings these take a handle from prefs.bas: + +.table + Function Description + -------- ----------- + PrintPageSetting(prefs, field) A number of the page setup: "Lines" (lines per page, 0 = the printer's usual length), "Columns", "MarginTop", "MarginBottom", "MarginLeft", "MarginRight", "TabWidth", or "Wrap" (True: long lines wrap at a space); -1 for an unknown field. + PrintPageText$(prefs, field) "Header" or "Footer". In them &f is the document name, &p the page, &n the number of pages, &d the date, &t the time, && an ampersand; &l, &c and &r put what follows left, centred or right. + PrintPageSetupProblem$(lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer) "" when this page setup can be used, else why not. + PrintPageSettingsStore(prefs, lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer) Put a page setup into the settings (then PrefsSave). False for one PrintPageSetupProblem$ objects to. + PrintPageLines(prefs, model) Lines per page the setup gives on printer model. +.endtable + +The limits are CONSTs: PRINT_LINES_MIN to PRINT_LINES_MAX, PRINT_COLUMNS_MIN to PRINT_COLUMNS_MAX, PRINT_TAB_MAX, PRINT_COPIES_MAX, with the defaults PRINT_COLUMNS_DEFAULT, PRINT_MARGIN_TB_DEFAULT, PRINT_MARGIN_LR_DEFAULT and PRINT_TAB_DEFAULT. + +A program can also change the printer settings the way the Control Panel's Printer page does. They are kept in DVX's settings file, so these calls take a handle from prefs.bas (add prefs.bas to the project too). Printer models and destinations are numbered from 0. + +.table + Function Description + -------- ----------- + PrintModelCount() / PrintModelLabel$(model) How many printer models there are, and the name of each. + PrintDestCount() / PrintDestLabel$(dest) How many places the printer can be connected to (LPT1, LPT2, LPT3, a file), and the name of each. + PrintDestIsFile(dest) True for the destination that prints into a file. + PrintSettingModel(prefs) The printer model in the settings. + PrintSettingDest(prefs) Where the printer is connected. + PrintSettingFile$(prefs) The file printed into when the destination is a file. + PrintSettingsProblem$(dest, filePath) "" when this choice can be used, else why not (printing to a file needs its name). + PrintSettingsStore(prefs, model, dest, filePath) Put the choice into the settings, keeping the page setup as it is; then save with PrefsSave. +.endtable + +.code +Dim prefs As Long + +Sub Form1_Load () + Dim i As Long + prefs = PrefsLoad(PrefsDvxIni$()) + For i = 0 To PrintModelCount() - 1 + cboModel.AddItem PrintModelLabel$(i) + Next i + cboModel.ListIndex = PrintSettingModel(prefs) +End Sub + +Sub cmdOK_Click () + If PrintSettingsStore(prefs, cboModel.ListIndex, PrintSettingDest(prefs), PrintSettingFile$(prefs)) Then + PrefsSave prefs + End If + PrefsClose prefs +End Sub +.endcode + +.h2 Files and Folders + +With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name. + +Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsBaseName$ and FileOpsExtension$. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering. + +Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName (whether a name can be used as a file name here; FileOpsError$ says why not), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$. + +Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none). + +FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends. + +.code +Dim job As Long + +Sub cmdCopy_Click () + job = FileOpsJobCreate(FILEOPS_COPY, "C:\BACKUP") + If FileOpsJobAddSource(job, "C:\DOCS") Then Timer1.Enabled = True +End Sub + +Sub Timer1_Timer () + Select Case FileOpsJobRunFor(job, 50) + Case FILEOPS_STEP_ASK + If MsgBox(FileOpsJobMessage$(job), vbYesNo + vbQuestion) = vbYes Then + FileOpsJobAnswer job, FILEOPS_ANSWER_YES + Else + FileOpsJobAnswer job, FILEOPS_ANSWER_NO + End If + Case FILEOPS_STEP_ERROR + FileOpsJobAnswer job, FILEOPS_ANSWER_SKIP + Case FILEOPS_STEP_DONE, FILEOPS_STEP_CANCELLED + Timer1.Enabled = False + FileOpsJobFree job + Case Else + ProgressBar1.Value = FileOpsJobPercent(job) + End Select +End Sub +.endcode + +.h2 Working with DVX + +With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC. + +Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), or opens a document in the program set for its type; it returns True when it started. ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (0 when it did not start; DVX has already said why). ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel. + +Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$. + +File types. ShellAssocProgram$(extension) tells which program opens files with that extension (with ",arguments" after it when it is given any), ShellAssocCommand$(document) just the program that opens a document. ShellAssocCount() file types are set; ShellAssocExt$(index) and ShellAssocValue$(index) read them (index from 0). ShellAssocSet(extension, program) sets one and ShellAssocRemove(extension) removes one, saved at once; ShellAssocNormalize$(extension) is the extension as stored (".txt" gives "TXT"; "" when it cannot be one). + +Running programs. ShellRunningCount() says how many programs are running, and ShellRunningId(index) is the id of each one (index from 0; the same id ShellRunProgram returns). ShellAppId() is this program's own id. ShellRunningName$(id) and ShellRunningPath$(id) are the name a running program shows and the path of its .app ("" when it is not running), and ShellRunningFind(program) is the id of a running copy of a program, 0 when it is not running. ShellRunningActivate id brings a program to the front the way Task Manager's Switch To does (a minimized window is restored). ShellRunningClose id asks a program to close as if the user had closed its windows: it may ask to save its work and the user may cancel, and it is asked once your code has returned to DVX, so check from a Timer whether it is still running. ShellRunningKill id ends a program at once without asking it, the way Task Manager's End Task does: whatever it had not saved is lost, so keep it for a program that does not respond. ShellAppIsHung(id) is True when a program has been busy for 5 seconds or longer ("Not responding" in Task Manager). A program cannot end itself with ShellRunningKill (END does that). ShellAppMemory(id) is how many bytes of memory a program is using. ShellChangeCount() changes whenever a program starts or ends: check it from a Timer and update what you show when it is not the value you saw last. ShellTaskManager opens the Task Manager, ShellSystemInfo$() describes the computer (several lines), and ShellExitDvx exits DVX: every program is asked to close, each asking about unsaved work, and a program that stays open stops the exit (ask the user first). + +.code +Sub cmdNotepad_Click () + Dim id As Long + id = ShellRunningFind("APPS\KPUNCH\NOTEPAD\NOTEPAD.APP") + If id = 0 Then + ShellRun "APPS\KPUNCH\NOTEPAD\NOTEPAD.APP", "" + Else + ShellRunningActivate id + End If +End Sub + +Sub FillTaskList () + Dim i As Long, id As Long + lstTasks.Clear + For i = 0 To ShellRunningCount() - 1 + id = ShellRunningId(i) + If ShellAppIsHung(id) Then + lstTasks.AddItem ShellRunningName$(id) & " (not responding)" + Else + lstTasks.AddItem ShellRunningName$(id) + End If + Next i +End Sub +.endcode + +Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). COMMAND$(n) gives this program's own arguments the same way. + +Keys for every program. HotkeyAdd(keyText) makes a key such as "Ctrl+Alt+E" work whichever program is in front and returns its id (0 when it is not a key, needs Ctrl or Alt, or another program uses it). The program asks for the keys pressed from a Timer: HotkeyNext() returns the id of the oldest one not asked for yet, 0 when there is none. HotkeyRemove id stops one; the rest stop when the program ends. HotkeyText$(keyText) writes a key the way DVX does ("ctrl+alt+e" gives "Ctrl+Alt+E"; "" when it is not a key), handy for checking what the user typed. ShellItemKeyValid(keyText) is Program Manager's rule for an item's shortcut: Ctrl together with Alt or Shift, so it never takes a key a program uses itself ("" for no shortcut is valid too). + +This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none). + +Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE. + +.code +Dim groups As Long, main As Long, i As Long +groups = ShellGroupsLoad(ShellConfigFile$("groups.ini")) +main = ShellGroupFind(groups, "Main") +i = ShellItemAdd(groups, main, "Notepad", "APPS\KPUNCH\NOTEPAD\NOTEPAD.APP") +If Not ShellItemSet(groups, main, i, "key", "Ctrl+Alt+N") Then MsgBox "Not a shortcut." +picIcon.Picture = LOADPICTURE(ShellItemIcon$(ShellItemGet$(groups, main, i, "icon"), ShellItemGet$(groups, main, i, "program"))) +If Not ShellGroupsSave(groups, ShellConfigFile$("groups.ini")) Then MsgBox "Not saved." +ShellGroupsFree groups +.endcode + +.code +Sub Form1_Load () + Dim copies As Long + copies = ShellInstanceCount() + Me.Left = 40 + (copies - 1) * 20 + Me.Top = 30 + (copies - 1) * 20 +End Sub +.endcode + +A desktop's Timer keeps its running count current and starts the programs whose keys were pressed: + +.code +Sub Timer1_Timer () + Dim id As Long + If ShellChangeCount() <> lastChange Then + lastChange = ShellChangeCount() + lblRunning.Caption = ShellRunningCount() & " programs running" + End If + id = HotkeyNext() + Do While id <> 0 + StartItemWithKey id + id = HotkeyNext() + Loop +End Sub +.endcode + +.h2 Changing DVX's Settings + +With system.bas in the project, a program changes what Control Panel changes, at once for the whole desktop. Add prefs.bas too: the settings are saved through a prefs.bas handle. + +Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written. + +Colors: the system colors are numbered from 0 to SysColorCount() - 1. SysColorLabel$(id) is the text to show for one ("Desktop", "Window Face" ...), SysColorName$(id) the name it is stored as, and SysColorId(colorName) the id of a name (-1 for none) -- to draw in the user's colors, for example SysGetColor(SysColorId("desktop")). SysGetColor and SysSetColor read and set a color as an RGB() value; each SysSetColor redraws the whole screen. SysResetColors goes back to DVX's own colors. SysLoadTheme(path) and SysSaveTheme(path) read and write a theme file (.THM, kept in CONFIG\THEMES); both return True when they worked. + +Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use. + +Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower) and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back). + +The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl and Alt keys held now (vbShiftMask, vbCtrlMask and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value. + +Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told). + +System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own. + +Saving: SysColorsStore, SysWallpaperStore and SysMouseStore write the colors, the wallpaper and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel. + +.code +Dim snap As Long +Dim oldW As Long, oldH As Long, oldBpp As Long + +Sub Form1_Load () + snap = PrefsCreate() + SysColorsStore snap + SysWallpaperStore snap + SysMouseStore snap + oldW = Screen.Width + oldH = Screen.Height + oldBpp = Screen.BitsPerPixel +End Sub + +Sub cmdOK_Click () + Dim ini As Long + ini = PrefsLoad(PrefsDvxIni$()) + SysColorsStore ini + SysWallpaperStore ini + SysMouseStore ini + PrefsSetVideoMode ini, Screen.Width, Screen.Height, Screen.BitsPerPixel + If Not PrefsSave(ini) Then MsgBox "The settings could not be saved." + PrefsClose ini + Unload Me +End Sub + +Sub cmdCancel_Click () + SysColorsLoad snap + SysWallpaperLoad snap + SysMouseLoad snap + If Screen.Width <> oldW Or Screen.Height <> oldH Or Screen.BitsPerPixel <> oldBpp Then + SysSetVideoMode oldW, oldH, oldBpp + End If + Unload Me +End Sub +.endcode + .h2 Held Keys for Games With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it: @@ -4615,6 +5462,20 @@ Sub Timer1_Timer () End Sub .endcode +GameKeyClear forgets the keys held now, so each reads as up until it is pressed again (after a pause screen, say); like GameKeyDown it only works while one of the program's windows is active. FrameMissed() counts the frames FRAMEWAIT skipped because the program was too slow to draw them in time, since it began waiting at its present rate (0 before the first FRAMEWAIT), so a game can draw less when it falls behind. GameWaitRetrace waits until the screen starts drawing its next picture from the top; a full-screen game that draws right after it does not tear. + +.code +Do + MoveEverything + If FrameMissed() > lastMissed Then + lastMissed = FrameMissed() + detail = detail - 1 + End If + DrawEverything + FRAMEWAIT 60 +Loop Until quitting +.endcode + .h2 Joysticks With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy) and JoyButtons(joy) describe the joystick. @@ -4630,6 +5491,21 @@ End Sub When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas. +joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo. + +.code +Sub cmdCenter_Click () + ' The stick rests in the middle: remember the readings + centerX = JoyRawAxis(1, 1) +End Sub + +Sub cmdFinish_Click () + If Not JoySetCalibration(1, 1, lowX, centerX, highX) Then + MsgBox "Move the stick all the way to each side." + End If +End Sub +.endcode + .link lang.sound See: STICK and STRIG .h2 Sound @@ -4652,6 +5528,50 @@ End Sub WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND and PLAY work too, without sound.bas. +PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator) and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits" and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played. + +A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends. + +.code +Dim Shared laser As Long + +Sub Form1_Load () + laser = SoundLoad("LASER.WAV") +End Sub + +Sub Fire (x As Integer) + Dim v As Long + v = SoundPlay(laser, 80, (x - 160) * 100 \ 160, False) + SoundSetPitch v, 90 + Int(Rnd * 20) +End Sub +.endcode + +A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed: + +.code +Sub Timer1_Timer () + Dim taken As Long + taken = SoundStreamWrite(stream, pending) + pending = Mid$(pending, taken + 1) + If Len(pending) = 0 Then pending = MakeMoreSound() +End Sub +.endcode + +On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo" and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block). + +sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle) and SoundSettingVolume(handle) read the sound driver, MIDI choice and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts). + +.code +Dim ini As Long +Dim i As Long + +ini = PrefsLoad(PrefsDvxIni$()) + +For i = 0 To SoundEventCount() - 1 + lstEvents.AddItem SoundEventLabel$(i) & ": " & SoundEventFile$(ini, i) +Next i +.endcode + .link lang.sound See: BEEP, SOUND and PLAY .h2 Music @@ -4663,7 +5583,7 @@ MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FA .code Dim Shared tune As Long -Sub Form_Load () +Sub Form1_Load () tune = MusicLoad("THEME.MOD") If tune = MUSIC_FAILED Then @@ -4679,7 +5599,7 @@ A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a .code Dim Shared keys As Long -Sub Form_Load () +Sub Form1_Load () keys = MusicMidiOpen() MusicProgram keys, 1, 73 End Sub @@ -4695,6 +5615,10 @@ Sub tmrNoteOff_Timer () End Sub .endcode +MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi" or "live"), "where" ("stream", "mpu401", "fm" or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value). + +A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present" and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again) and MidiRelease gives the port back. + .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: @@ -4702,7 +5626,7 @@ With net.bas in the project, a program talks to other computers over the network .code Dim conn As Long -Sub Form_Load () +Sub Form1_Load () conn = TcpConnect("bbs.example.org", 23) If conn = 0 Then @@ -4720,15 +5644,21 @@ End Sub Function Description -------- ----------- HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found. + HostName$(address) The name of an address; the address itself when no name is known for it. 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. + TcpSend(handle, bytes) Send text or any other bytes; 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. + TcpShutdown(handle) Tell the other side nothing more will be sent; what it sends can still be read. + TcpPeer$(handle) "address:port" of the other side ("" when not connected). + TcpLocalPort(handle) The port a handle uses on this computer (the one TcpListen(0) picked). + TcpSetNoDelay(handle, enable) True: send small pieces at once instead of gathering them (typing, games). + TcpSetKeepAlive(handle, enable) True: check now and then that the other side of an idle connection is still there. 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. @@ -4737,7 +5667,55 @@ End Sub 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. +The strings sent and received may hold any bytes, CHR$(0) too, so binary protocols work as well as text. Every connection a program opens is closed for it when the program ends. + +A program can also edit the network settings the way the Control Panel's Network page does, and show the network's state. The settings are kept in DVX's settings file, so NetConfigLoad and NetConfigStore take a handle from prefs.bas (add prefs.bas to the project too). NetConfigLoad gives a settings handle (0 when none is free); read and change its fields by name, check them, store them, and restart the network only when they changed: + +.code +Dim prefs As Long +Dim cfg As Long + +Sub Form1_Load () + prefs = PrefsLoad(PrefsDvxIni$()) + cfg = NetConfigLoad(prefs) + txtName.Text = NetConfigGet$(cfg, "hostname") + lblState.Caption = NetStatus$("message") +End Sub + +Sub cmdOK_Click () + NetConfigSet cfg, "hostname", txtName.Text + + If NetConfigProblem$(cfg) <> "" Then + MsgBox NetConfigProblem$(cfg) + Exit Sub + End If + + NetConfigStore cfg, prefs + PrefsSave prefs + + If NetConfigChanged(cfg) Then NetRestart + NetConfigFree cfg + PrefsClose prefs +End Sub +.endcode + +.table + Function Description + -------- ----------- + NetConfigLoad(prefs) A settings handle holding the network settings of prefs (0 for the usual settings), or 0 when none is free. + NetConfigGet$(cfg, field) One setting as text: "dhcp" ("-1" when the address comes from the network automatically, "0" when it is fixed), "address", "netmask", "gateway", "dns1", "dns2", "hostname", "vector" (the packet driver's interrupt, 0 = find it). + NetConfigSet(cfg, field, value) Change one setting (same names; "dhcp" and "vector" take numbers). True when the field exists. + NetConfigProblem$(cfg) "" when the settings can be used, else why not in plain words. + NetConfigChanged(cfg) True when the settings differ from what NetConfigLoad read. + NetConfigStore(cfg, prefs) Put the settings into prefs (then save it with PrefsSave). True when done. + NetConfigFree cfg Free the settings handle. + NetVectorCount() / NetVector(index) The packet driver interrupts to choose from: index 0 is 0 (find it), then each one the settings accept. + NetRestart() Stop the network and start it with the saved settings. True when it is running. + NetStop Stop the network (every program's connections are closed) until NetRestart. + NetStatus$(field) The network's state as text: "message" (in plain words), "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", "gateway", "dns1", "dns2", "hostname", "driver" (the network card's driver), "mac" (its hardware address), "vector", "sockets", "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes". "" for an unknown field. +.endtable + +Settings handles a program leaves open are freed for it when it 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. @@ -4750,4 +5728,328 @@ If msg <> "" And Not CommRecvEncrypted(link, 1) Then End If .endcode +.h2 Connections and Terminals + +With conn.bas in the project, a program talks to another computer the way the Terminal program does, whatever is in between: a serial port, a modem (it sends its setup command, dials, dials again when the line is busy and notices when the carrier is lost), the secure link to the DVX host proxy (encrypted, and authenticated with a passphrase), or Telnet over the network. ConnCreate makes a connection (a number); ConnSetSerial, ConnSetModem, ConnSetHost and ConnSetPassphrase set it up, and ConnOpen(conn, type) starts it. Opening only starts: dialing and the key exchange go on while the program runs, so call ConnPoll often from a Timer. ConnState says where it is (CONN_CLOSED, CONN_OPENING, CONN_OPEN) and ConnStatus$ what is happening or why it ended; ConnStatusSerial changes whenever ConnStatus$ does. + +ConnAttach conn, "Term1" puts what arrives on the Terminal control Term1 (ConnPoll moves it there) and sends what is typed in it. ConnPoll then also appends what arrives to a capture file (ConnCapture), answers the other side's ENQ with an answerback (ConnSetAnswerback), and notices a ZMODEM sender starting a download: ConnZmodemRequest is True once, and the program answers with ConnReceiveFiles in the same Timer event. ConnSetEcho shows what is typed on the Terminal too. Without a Terminal, read with ConnRead$ and send with ConnWrite (any bytes, CHR$(0) too). ConnOpen returns CONN_OK or a CONN_ERR_ code; ConnErrorText$ puts the code in words. ConnSetXferWindow conn, bytes limits how much a ZMODEM sender may send before it waits for an answer (0 = no limit, up to XFER_WINDOW_MAX; -1, the default, picks by the line). ConnSendBreak holds a serial line in the break state for a moment; ConnClose hangs up and ConnFree forgets the connection. + +.code +Dim Shared conn As Long +Dim Shared download As Long + +Sub Form1_Load () + conn = ConnCreate() + ConnSetSerial conn, 2, 115200, 8, "N", 1, CONN_FLOW_RTSCTS + ConnAttach conn, "Term1" + ConnOpen conn, CONN_SERIAL +End Sub + +Sub Timer1_Timer () + ConnPoll conn + If ConnZmodemRequest(conn) Then download = ConnReceiveFiles(conn, XFER_ZMODEM, "DOWNLOAD", XFER_EXIST_RENAME, False) +End Sub +.endcode + +.h2 File Transfers + +With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say) and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it. + +XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name) gives the 8.3 name a receive stores a sender's file name as: no folder or device name, only characters every disk accepts. + +.h2 Lists You Draw Yourself + +With listhelp.bas in the project, a list or icon view a program draws itself -- in a UserControl or a PictureBox -- follows the same rules as ListBox and ListView. ListSelCreate(multi, count) makes a selection for count items (several at a time when multi is True) and returns a handle; it remembers which items are selected and which one is current (the one the keyboard moves). Items are numbered from 0. Give it the clicks and keys, and ask it what to draw: + +.table + Function Description + -------- ----------- + ListSelClick sel, index, shift A click on an item, with the MouseDown event's Shift: alone it selects only that item, with Ctrl it turns the item on or off, with Shift it selects the items from the last click up to it. + ListSelKey(sel, keyCode, shift) Ctrl+A selects every item (returns LISTSEL_KEY_ALL), Space turns the current item on or off (LISTSEL_KEY_TOGGLE); LISTSEL_KEY_NONE for other keys. + ListSelNavigate(sel, keyCode, shift, columns, pageRows) The arrow keys, Home, End, PgUp and PgDn, in a list (columns 1) or a grid with columns items per row, pageRows rows a page; with Shift they select from the last click. The new current item, or -1 when nothing moved. + ListSelMove sel, index, shift Make an item current as the keyboard does (after type-ahead). + ListSelCursor(sel), ListSelSetCursor sel, index The current item (-1: none); make one item current and the only one selected. + ListSelIsSelected(sel, index), ListSelSelectedCount(sel) Whether an item is selected; how many are. + ListSelSetItem sel, index, selected; ListSelSetAll sel, selected Select or clear one item, or all. + ListSelInsert sel, index; ListSelRemove sel, index An item was added or removed: the others keep their selection. + ListSelReorder sel, fromIndex, dest An item was dragged to dest: its selection goes with it. + ListReorderDest(fromIndex, toSlot, count) Where an item dragged from fromIndex lands when dropped in front of slot toSlot (count: after the last item); -1 when the drop changes nothing. + ListSelReset sel, count; ListSelCount(sel); ListSelFree sel The list now holds count new items; how many it holds; free the handle. + ListNavigateIndex(keyCode, current, count, pageSize), ListGridNavigate(keyCode, current, count, columns, pageRows) The same keys without a selection handle. + ListTypeAhead(ch, items, current) The next item after current whose text starts with the letter ch, any case (going round to the first); items holds the texts separated by CHR$(10). -1 when none does. + ListTypeAheadMatch(ch, text) True when text starts with the letter ch. +.endtable + +.code +Sub UserControl_KeyDown (KeyCode As Integer, Shift As Integer) + If ListSelKey(sel, KeyCode, Shift) <> LISTSEL_KEY_NONE Then UserControl.Refresh + If ListSelNavigate(sel, KeyCode, Shift, columns, rowsShown) >= 0 Then UserControl.Refresh +End Sub + +Sub UserControl_KeyPress (KeyAscii As Integer) + Dim i As Long + i = ListTypeAhead(Chr$(KeyAscii), itemTexts, ListSelCursor(sel)) + If i >= 0 Then + ListSelMove sel, i, 0 + UserControl.Refresh + End If +End Sub +.endcode + +Selections a program leaves open are freed when it ends. + +An icon view drawn with listhelp.bas looks and behaves like Program Manager's: the same cells for large icons, small icons and the list, the same frame and scroll bar, a large icon's name on two lines, and the same rules for the keys, dragging and dropping. The grid functions take the view mode (LISTICON_VIEW_LARGE, LISTICON_VIEW_SMALL or LISTICON_VIEW_LIST), the size of a character (TextWidth("W") and TextHeight("W")), the size of the whole view, how many items it shows, and rowsTaken: LISTICON_BY_ORDER when the items fill the grid in order, else how many rows items placed in cells of their own take. Draw the view's frame with DrawBevel (LISTICON_FRAME wide; with the face -1 after the items, so a row cut off at the bottom stops at the frame) and its scroll bar with DrawScrollBar as a control's bar (its control argument True); the grid leaves room for it. + +.table + Function Description + -------- ----------- + ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken) One value of the grid: a cell's size (LISTICON_CELL_W, LISTICON_CELL_H), where its picture goes and how big it is (LISTICON_PICTURE_X, LISTICON_PICTURE_Y, LISTICON_PICTURE_SIZE), where its name goes and how many characters it shows (LISTICON_TEXT_X, LISTICON_TEXT_Y, LISTICON_NAME_CHARS; a large icon's two lines are each centred in the cell), the list's detail column (LISTICON_DETAIL_X, 0 in the other views); the inside of the frame less the scroll bar (LISTICON_INNER_X, LISTICON_INNER_Y, LISTICON_INNER_W, LISTICON_INNER_H), the columns, the rows shown whole and in all (LISTICON_COLUMNS, LISTICON_ROWS_SHOWN, LISTICON_ROWS), the largest top row (LISTICON_TOP_ROW_MAX), True when the scroll bar is needed (LISTICON_SCROLLBAR), where the text of an empty view goes (LISTICON_EMPTY_X, LISTICON_EMPTY_Y), the frame's width (LISTICON_FRAME) and the rows a wheel notch scrolls (LISTICON_WHEEL_ROWS). + ListIconCellAt(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) The cell under x, y with the view scrolled to topRow, numbered row * columns + column; -1 outside the grid. + ListIconLabel$(name, maxChars, lineNo) Line 1 or 2 of a large icon's name, maxChars characters a line (LISTICON_NAME_CHARS): the first line ends at the last blank that fits, else it is cut; the second is the rest, cut. "" for a line the name does not need. + ListIconNavigate(keyCode, current, cells, columns, pageRows) The item the arrow keys, Home, End, PgUp or PgDn move to when the items stand in cells of their own, gaps allowed: cells holds each item's column and row, all separated by commas ("0,0,2,0,1,1"). -1 for another key or when no item is that way. + ListIconScrollPress(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) A left press on the scroll bar: the top row after it (an arrow moves a row, the trough a page, the thumb nothing); -1 when x, y is not on the scroll bar. + ListIconScrollGrab(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) How far below the top of the thumb a press is; -1 when it is not on the thumb. + ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y) The top row with the thumb, held grab pixels below its top, dragged to y. While the button is down the control gets the mouse even outside it. + ListIconDragStarted(dx, dy) True when a press has moved far enough to start a drag. + ListIconDragScroll(mode, cw, ch, w, h, count, rowsTaken, topRow, y) While an item is dragged: the top row after the view scrolls a row because the mouse is above or below the grid. + ListIconDropCell(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) The cell a dragged item would be dropped on (above or below the grid: the nearest row); -1 for none. + ListIconDropDest(fromIndex, cell, count) Items in order: the place an item dragged from fromIndex takes when dropped on a cell -- that cell's, the last place for a cell past the end; -1 when the drop changes nothing. +.endtable + +.code +Sub UserControl_Paint () + Dim cw As Long, ch As Long + cw = UserControl.TextWidth("W") + ch = UserControl.TextHeight("W") + UserControl.DrawBevel 0, 0, UserControl.Width, UserControl.Height, True, vbWhite, ListIconMetric(LISTICON_VIEW_LARGE, LISTICON_FRAME, cw, ch, UserControl.Width, UserControl.Height, itemCount, LISTICON_BY_ORDER) + ' ... each item in its cell, its name in two lines: + UserControl.Print ListIconLabel$(itemName, ListIconMetric(LISTICON_VIEW_LARGE, LISTICON_NAME_CHARS, cw, ch, UserControl.Width, UserControl.Height, itemCount, LISTICON_BY_ORDER), 1); +End Sub +.endcode + +.h2 Security + +With security.bas in the project, a program has the building blocks of DVX's encrypted serial link for its own protocols and files. All of them take and give strings that may hold any bytes, CHR$(0) too. + +.table + Function Description + -------- ----------- + SecRandom$(count) count random bytes, good for keys and salts (RND is not). SecAddEntropy bytes stirs something unpredictable (key timings, mouse positions) into the generator. + SecDhCreate() A key exchange with fresh keys (a few seconds on a slow computer); a handle, or 0. + SecDhPublicKey$(dh) This side's public key (SEC_DH_KEY_SIZE bytes), to send to the other side. + SecDhComputeSecret(dh, otherPublicKey) Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM for a key that is not acceptable. + SecDhSecret$(dh) The secret both sides now share, which nobody listening can work out. Make keys from it with SecHkdf$; never use it as a key itself. SecDhFree dh frees the handle. + SecCipherCreate(key) A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. + SecCipherCrypt$(cipher, bytes) Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it. + SecSha256$(bytes) The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle. + SecHmac$(key, bytes) The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd and SecHmacEnd$ for pieces. + SecHkdf$(salt, secret, info, length) length bytes (up to SEC_HKDF_MAX_OUT) of keys made from secret; a different info gives different keys. + SecEqual(a, b) True when a and b hold the same bytes, compared in the same time wherever they differ (for signature checks). +.endtable + +.code +' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key +Dim dh As Long, cipher As Long, key As String +dh = SecDhCreate() +TcpSend conn, SecDhPublicKey$(dh) +' ... receive the other side's key into theirs ... +If SecDhComputeSecret(dh, theirs) = SEC_OK Then + key = SecHkdf$("", SecDhSecret$(dh), "chat cipher", SEC_XTEA_KEY_SIZE) + cipher = SecCipherCreate(key) +End If +SecDhFree dh +.endcode + +Handles a program leaves open are freed, and their keys wiped, when it ends. + +.h2 Settings Files + +With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings. + +.code +Dim ini As Long +ini = PrefsLoad(ShellConfigFile$("MYAPP.INI")) +userName = PrefsGet$(ini, "User", "Name", "Unknown") +PrefsSetInt ini, "Window", "Left", Me.Left +If Not PrefsSave(ini) Then MsgBox "The settings could not be saved." +PrefsClose ini +.endcode + +PrefsDvxIni$() is the path of DVX's own settings file, DVX.INI, which Control Panel changes; other libraries (system.bas, joystick.bas, net.bas, print.bas and sound.bas) read and store their part of it through a prefs.bas handle. PrefsSetVideoMode handle, wide, high, bpp stores the screen mode DVX starts in, and PrefsVideoWidth(handle), PrefsVideoHeight(handle) and PrefsVideoBpp(handle) read it (DVX's own start-up mode, SYS_VIDEO_DEFAULT_* in system.bas, for a value that is not there). PrefsKeyName$(handle, section, index) is the name of key number index of a section (from 0, in the order of the file; "" past the last key), to go through every key of a section. + +Some settings are stored by name, from a fixed list of choices: the mouse acceleration (PREFS_CHOICE_ACCEL), the wallpaper mode (PREFS_CHOICE_WALLPAPER) and the scroll wheel direction (PREFS_CHOICE_WHEEL). For each list, PrefsChoiceCount(table) says how many choices it has (numbered from 0), PrefsChoiceLabel$(table, i) is the text to show, PrefsChoiceName$(table, i) the name stored in the file and PrefsChoiceValue(table, i) the value it stands for (what SysSetMouse takes). PrefsChoiceIndex(table, choiceName) and PrefsChoiceIndexOfValue(table, value) find a choice (-1 when there is none), and PrefsGetChoice(handle, section, key, table) reads one from a file, giving DVX's default choice when the key is missing or unknown. PrefsChoiceDefault(table) is that default choice, what a Defaults button selects. For a list of your own, PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) does the same with the names separated by "|" ("large|small|list" stores choice 1 as "small"), giving defaultName's choice for a missing or unknown key. + +.code +For i = 0 To PrefsChoiceCount(PREFS_CHOICE_ACCEL) - 1 + cboAccel.AddItem PrefsChoiceLabel$(PREFS_CHOICE_ACCEL, i) +Next i +cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel()) +.endcode + +.h2 Help Files + +With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting. + +With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none) and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$ and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them. + +To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$ and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$. + +.code +Dim hlp As Long +Dim history(63) As Long, histPos As Long + +Sub Form1_Load () + hlp = HlpOpen(App.Path + "\MYAPP.HLP") + For i = 0 To HlpTocCount(hlp) - 1 + If HlpTocDepth(hlp, i) = 0 Then + Toc.AddItem HlpTocTitle$(hlp, i) + Else + Toc.AddChildItem lastTop, HlpTocTitle$(hlp, i) + End If + If HlpTocDepth(hlp, i) = 0 Then lastTop = Toc.ItemCount() - 1 + Toc.SetItemTag Toc.ItemCount() - 1, Str$(HlpTocTopic(hlp, i)) + Next i + GoTopic HlpStartTopic(hlp, "") +End Sub + +Sub GoTopic (ByVal topic As Long) + histPos = histPos + 1 + history(histPos) = topic + HelpPane1.ShowTopic hlp, topic + Me.Caption = HlpTopicTitle$(hlp, topic) + " - My Help" +End Sub + +Sub HelpPane1_LinkClick (Topic As String) + GoTopic HlpFindTopic(hlp, Topic) +End Sub + +Sub Toc_Change () + If Val(Toc.GetItemTag(Toc.ListIndex)) >= 0 Then GoTopic Val(Toc.GetItemTag(Toc.ListIndex)) +End Sub +.endcode + +A help viewer written in BASIC can also take the place of DVX's own: installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP with a NewArgs event procedure, it is handed "helpfile topic" when F1 is pressed while it is open (see New Arguments in the Form topic). + +.h2 Sizes in Characters + +Control sizes (Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight) are pixels. With layout.bas in the project, SizeChars(n) is a size of n characters of the system font and SizePercent(n) one of n percent of the container (0 to 100); give them to a size property and the layout works out the pixels each time it arranges the form, so a box for an address keeps its width whatever the font. Reading the property back gives the pixels it stands for at that moment. + +.code +txtAddress.MinWidth = SizeChars(16) +lstFiles.MaxHeight = SizePercent(50) +.endcode + +.h2 Compiling and Debugging Programs + +With compiler.bas in the project, a program compiles BASIC programs -- what a development tool written in BASIC needs. A source set holds the files of one program: CompSourceFromProject(src, dbpPath) reads a whole project (.dbp) from disk, or CompSourceAdd(src, path, text, kind) adds files one at a time, in the order a project compiles: modules, then class modules, then UserControls, then forms (COMP_KIND_AUTO tells them apart by the file name; a form's whole .frm text is added, and the code after its layout is compiled; a UserControl's whole .ctl text, its header and its code). CompCompile(src, optionExplicit, validate) gives a compiled module, or 0 with CompError$() saying what is wrong and CompErrorFile$() / CompErrorFileLine() where. With validate True, Label1.Captoin and other misspelled control members, and controls of an unknown type, are found while compiling instead of when the line runs (the set's own UserControls are known types). + +Line numbers of compiled programs (errors, breakpoints, the line a program is paused on) count through all the files of the source set. CompMapFile$(src, line) and CompMapLine(src, line) give the file and the line in it; CompConcatLine(src, path, fileLine) goes the other way. CompSourceFiles$ lists the files in compile order. CompBuildApp(module, src, outPath, release) builds an .app the way the IDE's Make Executable does, with the project's name, icon and help file, its forms and its UserControls. For a control project (its Type setting is "Control") it builds a control file instead (outPath NAME.wgt), the way the IDE's Make Control does: the project's UserControls with their toolbox pictures and names, there for every program once the file is copied into DVX's WIDGETS folder (DVX loads it when it starts). CompProcList$(text) lists the SUB and FUNCTION blocks of source text with their first and last lines (also for code that does not compile yet), CompIsIdent checks a name and CompKeyword$ lists the language's keywords, CompKeywordClass the kind of each (COMP_KW_OTHER, COMP_KW_TYPE or COMP_KW_LITERAL) and CompKeywordReserved whether a program may also use it as a name of its own. + +Project files: ProjOpen(dbpPath) opens a project file (.dbp) and ProjNew(name, folder) starts a new one; ProjSave writes it and ProjClose frees it. ProjFileCount, ProjFile$ (the name as the project lists it), ProjFilePath$ (the full path), ProjFileKind (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or COMP_KIND_CONTROL) and ProjFileFind list its files; ProjAddFile, ProjRemoveFile and ProjRenameFile (on disk and in the project together, saved at once) change the list. ProjGet$ and ProjSet read and set its settings by their names in the file: Name, Type ("Control" for a control project, empty for a program), Author, Publisher, Version, Copyright, Description, Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit and MultiInstance (the last two "true" or "false"). This is the project model the DVX BASIC IDE uses. + +A UserControl's header (the Begin UserControl block at the top of its .ctl file) is read and changed the way the IDE's Properties window does it, the rest of the text left as it is: ProjCtlKeys$() lists the keys in the window's order with the kind of value each takes (Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent; the kinds Name, Integer, Boolean, Color and Text), ProjCtlHeader$(text, key) reads one (its default when the header leaves it out), ProjCtlSetHeader$(text, key, value) gives the text with one set (an empty value removes it; Name renames the control) and ProjCtlNew$(name) the text of a new UserControl, as File > New UserControl makes it. + +With debug.bas too, a program runs a compiled module as a child program inside itself: RunCreate(module, src, appDir, args) prepares it, RunStart(run, startupForm, breakOnStart) loads its forms, and RunPoll(run, ms), called from a Timer, runs it for at most ms milliseconds at a time and returns what it is doing: RUN_RUNNING, RUN_EVENTS (waiting for events on its forms), RUN_PAUSED, RUN_ENDED or RUN_ERROR (RunError$ and RunErrorLine tell why and where). RunOutput$ collects what it PRINTs; INPUT asks with an input box. RunStop ends it and RunFree forgets it. + +DbgSetBreakpoints run, "12, 40" sets breakpoints (line numbers of the source set). While the program is paused, DbgLine and DbgProc$ say where, DbgLocals$ and DbgCallStack$ give the variables and the calls as rows for a ListView's SetRows, DbgEval$(run, expr) shows a variable or expression as the IDE's Watch window does, DbgExec$(run, line) runs an Immediate-window line (x = 5 changes a variable, anything else is printed) and DbgSetVar changes a variable; DbgStepInto, DbgStepOver, DbgStepOut, DbgRunToLine and DbgContinue go on, DbgPause stops it at the next line. The program's form captions show [break] while it is paused. + +A program paused in its module-level code waits until you step or continue it. One paused inside one of its event handlers -- or in its forms' Load code, which runs inside RunStart -- waits right there while your Timer and menus keep working; RunPoll called meanwhile returns at once, and RunStart returns only after such a pause. + +.code +' compiler.bas and debug.bas in the project; tmrRun is a Timer +Dim Shared src As Long, mdl As Long, kid As Long + +Sub StartDebugging + src = CompSourceCreate() + If Not CompSourceFromProject(src, "C:\PROJ\HELLO.DBP") Then MsgBox CompError$(): Exit Sub + mdl = CompCompile(src, True, True) + If mdl = 0 Then MsgBox CompErrorFile$() & " line" & Str$(CompErrorFileLine()) & ": " & CompError$(): Exit Sub + kid = RunCreate(mdl, src, "", "") + DbgSetBreakpoints kid, Str$(CompConcatLine(src, "C:\PROJ\MAIN.BAS", 12)) + If RunStart(kid, "", False) Then tmrRun.Enabled = True +End Sub + +Sub tmrRun_Timer + Select Case RunPoll(kid, 40) + Case RUN_PAUSED + lstLocals.SetRows DbgLocals$(kid) + lblWhere.Caption = CompMapFile$(src, DbgLine(kid)) & ":" & Str$(CompMapLine(src, DbgLine(kid))) + Case RUN_ENDED, RUN_ERROR + tmrRun.Enabled = False + txtOutput.Text = txtOutput.Text & RunOutput$(kid) & RunError$(kid) + End Select +End Sub +.endcode + +.h2 Form Designers and Code Editors + +A form designer or a code editor written in BASIC uses the same pieces as the DVX BASIC IDE. designer.bas describes the control types, which CtlTypeName$(0 to CtlTypeCount - 1) gives in alphabetical order: the toolbox lists the types CtlTypeFlags marks CTL_FLAG_DESIGNABLE (with CtlTypeTooltip$ and the icon CtlTypeIconFile writes), new controls are named CtlTypeNamePrefix$ plus a number, and the properties window lists CtlPropName$ for 0 to CtlPropCount - 1 with CtlPropType (CTL_PROP_STRING ... CTL_PROP_ENUM, plus CTL_PROP_READONLY) and CtlPropEnum$ for an enumerated property's choices. "Form" and "Menu" stand for the form itself and a menu item. The event list is CtlEventList$, and CtlEventParams$ writes an event procedure's parameter list; CtlTypeEventParams$(typeName, eventName) does it for one type, whose events may have parameters of their own (a UserControl's events), and "UserControl" stands for the object in a UserControl's own code (UserControl_MouseDown has Button, Shift, X and Y). For a project's files, FrmFormName$ and FrmControlNames$ read a .frm file's form and control names (the Object list), FrmCode$ its code, and ClsClassName$ a class module's name. + +The form being designed is a real form: CreateControl and RemoveControl add and remove controls, CallByName reads and sets a property by its name, Me.Controls lists the controls, Container (Set ctl.Container = Frame1) and MoveBefore move them, and LoadFormText shows a .frm file. Its DesignMode (see the Form control) stops the controls from working and reports the mouse and keys in DesignMouse and DesignKey, with the selection handles drawn for DesignSelection. + +A project's own UserControls go on the toolbox too, as in the IDE. Compile the project -- its forms may be left out -- and give the module and the source set to CompDesignControls(module, src, appDir) from compiler.bas: the UserControls are then control types of this program alone, listed by CtlTypeName$ with their Description as the tooltip and their ToolboxIcon as the icon, with their properties, methods and events, and known to CreateControl and LoadFormText. On the forms they run their code -- not the project's module-level code -- with UserControl.UserMode False on a form in DesignMode; App.Path is appDir, or the project's folder for "". When a control's code calls UserControl.PropertyChanged, the form raises DesignPropertyChanged (CtrlName, PropertyName), so the properties window can read them again. Call CompDesignControls again after a UserControl changed (the ones before are let go first) and CompDesignRelease when the project closes; the end of the program lets them go too. The module stays in use while they are there, even after CompModuleFree, and must be one this program compiled. + +.code +' compiler.bas and designer.bas in the project; Canvas is the form +' being designed +Sub LoadProjectControls(dbpPath As String) + Dim src As Long + Dim mdl As Long + Dim made As Long + src = CompSourceCreate() + If CompSourceFromProject(src, dbpPath) Then mdl = CompCompile(src, True, False) + If mdl = 0 Then + MsgBox CompError$() + Else + made = CompDesignControls(mdl, src, "") + If CompError$() <> "" Then MsgBox CompError$() + CompModuleFree mdl + End If + CompSourceFree src +End Sub + +Sub Canvas_DesignPropertyChanged(CtrlName As String, PropertyName As String) + If CtrlName = Canvas.DesignSelection Then RefreshPropertyList +End Sub +.endcode + +A .frm file can also be worked on as data, the way the DVX BASIC IDE does it, without showing it. FrmOpen(text$), FrmOpenFile(path$) or FrmNew(name$) gives a handle to a form model (0 when the text is not a form); FrmText$ writes it back as .frm text in the form the IDE saves and FrmSave writes it to a file; FrmFree frees it (a program's open handles are freed when it ends). FrmGet$ and FrmSet read and change the form's properties (Caption, Width, AutoSize, Layout, Icon, Tag ...; Name renames the form), FrmGetCode$ and FrmSetCode its code. The controls are numbered from 0 in file order, each one followed by the controls inside it: FrmCtlCount, FrmCtlFind, FrmCtlName$, FrmCtlType$, FrmCtlIndex (its control array index, -1 when it is not in an array) and FrmCtlParent (the container, -1 for the form) describe them; FrmCtlGet$ and FrmCtlSet read and set any property as text (Name renames the control and its whole control array, the controls inside it follow; Index makes it an array member; an OptionButton set to True clears the others in its container), FrmCtlRemoveProp takes a set one out, and FrmCtlPropCount / FrmCtlPropName$ list the ones that were set. FrmCtlAdd adds a control (its name "" gives the next free one, Command1, Label2 ...) as the last control of a container (-1: the form), FrmCtlRemove removes one with what is inside it, FrmCtlMove puts one before another of the same container and FrmCtlReparent into another container. FrmCtlText$ gives a control with what is inside it as .frm text, what a designer puts on the Clipboard to copy it, and FrmCtlPaste puts such text into a container: a name that is already taken is replaced by a free one. The menu items are numbered from 0 in menu order with a Level (0 for a menu on the menu bar or a popup menu, 1 for its items, 2 and more for submenus): FrmMenuCount, FrmMenuGet$ / FrmMenuSet (Name, Caption, Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, Level), FrmMenuAdd, FrmMenuRemove (with the items under it), FrmMenuMove (past the previous or next item of its level) and FrmMenuIndent (one level in or out, with the items under it). Only what was set is written: a property a control has by default is left out of the file. + +FormText$(formName$) writes a loaded form as it is now as .frm text -- one shown with LoadFormText, made with CreateForm and CreateControl, or one of the program's own: its properties, its controls in their containers with each property that differs from a new control of that type, and its menus (a form loaded this way has no code). A designer that keeps its form live saves it with FormText$, and FrmOpen(FormText$(...)) reads it as a model. + +compiler.bas reads source text as the compiler does, for a code editor: CompTokens$(source) lists its tokens (line, column, length, kind and text, the kind one of Keyword, Identifier, String, Number, Date, Symbol, Comment and Error); CompInCode(source, position) tells whether a character is code rather than inside a string or a comment; CompRenameInCode$(source, oldName, newName, codeOnly) renames a form, control or menu item where code refers to it (Command1.Caption, Command1_Click), leaving strings and comments alone with codeOnly; CompWithEvents$ lists the WithEvents variables a file declares and CompClassEvents$ the Events of a class module, for the Object and Event lists; CompHelpTopic$(word) and CompHelpCtrlTopic$(typeName) are the help topics F1 opens (HelpViewTopic on dvxbasic.hlp beside the IDE). + +A code editor is a TextArea: SetSyntaxMode "bas" colours BASIC as the IDE does (SetSyntaxColor changes the colours), SetLineMarker marks breakpoints and the current line, GutterClick reports clicks on the line numbers, SelStart / SelLength / SelText and Cut, Copy, Paste, SelectAll and Undo give the Edit menu, FindNext / ReplaceSelection / ReplaceAll find and replace, and LineCount / GetLine / CursorLine / CursorCol read the text (see TextArea). + +.code +' designer.bas in the project: put a copy of the first control of a +' .frm file into a Frame of the same form and save it +Sub CopyIntoFrame(path As String) + Dim h As Long + Dim box As Long + h = FrmOpenFile(path) + If h = 0 Then Exit Sub + box = FrmCtlAdd(h, "Frame", "", -1) + If FrmCtlSet(h, box, "Caption", "Copies") And FrmCtlPaste(h, FrmCtlText$(h, 0), box) >= 0 Then + If Not FrmSave(h, path) Then MsgBox "Could not save " & path + End If + FrmFree h +End Sub +.endcode + +.code +' designer.bas in the project: fill a properties list for a control +Sub ShowProperties(ctl As Object, typeName As String) + Dim i As Integer + Dim v As String + lstProps.Clear + For i = 0 To CtlPropCount(typeName) - 1 + If (CtlPropType(typeName, i) And CTL_PROP_TYPEMASK) <> CTL_PROP_OBJECT Then + v = CallByName(ctl, CtlPropName$(typeName, i), vbGet) + lstProps.AddItem CtlPropName$(typeName, i) & " = " & v + End If + Next +End Sub +.endcode + .link lang.declarations See also: DECLARE LIBRARY diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c index 5d75fb9c..b630db65 100644 --- a/src/apps/kpunch/dvxbasic/runtime/serialize.c +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c @@ -70,6 +70,12 @@ // line int32 (the source line) // number int32 (the QBasic line number there) // +// enumCount int32, then that many Enums UserControl properties +// are declared As (class members' enumIdx): +// memberCount int32 (at least 1), then that many members in declaration order: +// name uint16 len + bytes (1 .. BAS_MAX_IDENT - 1 bytes, no NUL; no two alike, ignoring case) +// value int32 +// // classCount int32, then that many class modules: // name uint16 len + bytes // varCount int32 @@ -78,6 +84,7 @@ // memberCount int32, then that many members: // name uint16 len + bytes // index int32 +// enumIdx int32 (a UserControl's property: its Enum in the table above; else -1) // kind uint8 // flags uint8 // dataType uint8 @@ -90,6 +97,15 @@ // implementsCount int32, then that many interfaces: // iface int32 (class number) // mapLen int32 (the interface's memberCount * BAS_IMPL_WAYS), then that many member indices (int32, -1 = none) +// eventCount int32, then that many Events (RaiseEvent numbers them in this order): +// name uint16 len + bytes +// paramCount uint8, then that many parameter types (uint8) +// byValParams uint32 (bit i: parameter i is ByVal) +// paramNames uint16 len + bytes (comma-separated; 0: none) +// flags uint8 (BAS_CLASS_*) +// ucSlot int32 (BAS_CLASS_USERCONTROL: the UserControl member variable, else -1) +// ucProcCount uint8 (BAS_UC_EVT_COUNT for a UserControl, else 0), then that many +// UserControl_ procedures (int32 procs[] index, -1 = none) // // withEventsCount int32, then that many WithEvents variables: // slot int32 @@ -114,7 +130,7 @@ // so both sides pull from the same definition. #define BAS_MOD_MAGIC "DBXM" #define BAS_MOD_MAGIC_LEN 4 -#define BAS_MOD_VERSION 18 // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND; 3: BEEP / SOUND / PLAY / STICK / STRIG / FRAMEWAIT opcodes (runtime and stub rebuilt together); 4: DATE type, OP_CONV_DATE, OP_BUILTIN; 5: BAS_FN_PARAMARRAY; 6: program screen built-ins (BAS_FN_CLS .. BAS_FN_LINE_INPUT); 7: Variants, objects and class modules (class table, OP_PUSH_SPECIAL .. OP_PROP_ASSIGN, BAS_FN_VARTYPE .. BAS_FN_EACH_ITEM); 8: events, Implements, Missing (class implements lists, withEvents table, OP_RAISE_EVENT, OP_BIND_EVENTS, BAS_FN_ISMISSING, BAS_FN_LSET_TYPE, BAS_SPECIAL_MISSING); 9: screen pages and modes (SCREEN and PAINT argument blocks, BAS_FN_PCOPY .. BAS_FN_VARPTR_STR); 10: parameter defaults (member defaultParams), WithEvents in class modules (withEvents ownerClass), BAS_FN_ELEMENT_EXISTS; 11: BAS_FN_REF_TYPE, BAS_FN_HOLD_REF (typed method-call references, array locks); 12: OP_RAISE_EVENT adopts BAS_FN_HOLD_REF references (RaiseEvent passes TYPE fields by reference, their arrays locked); 13: one reference value (OP_PUSH_*_ADDR and OP_PUSH_ARR_ADDR carry the target's type, OP_PUSH_FIELD_ADDR, OP_REF_FIXLEN; BAS_FN_ELEMENT_EXISTS / REF_TYPE / HOLD_REF retired, BAS_FN_LOADPICTURE renumbered 70); 14: 32-bit code addresses (OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE / OP_GOSUB / OP_ON_ERROR / FOR offsets int32, OP_CALL / OP_CALL_ME int32 addresses: no 64 KB code limit), the new compiler front end (BAS_FN_RECORD_ASSIGN: TYPE values assigned into the record there); 15: compile-time member binding (OP_OBJ_GET / OP_OBJ_PUT / OP_OBJ_CALL, interface maps per Implements entry); 16: superinstructions OP_ADD_K16 / OP_SUB_K16 / OP_MUL_K16 / OP_IDIV_K16 / OP_MOD_K16 (INTEGER literal operand) and OP_STORE_LOCAL_CONV / OP_STORE_GLOBAL_CONV (conversion and store in one); 17: gap closure (OP_NULL_PROP; OP_FILE_PRINT retired for BAS_FN_FILE_PRINT, BAS_FN_CLOSE, BAS_FN_FILE_WIDTH, BAS_FN_PEN, BAS_FN_PEN_SET; LoadPicture size and colour arguments; member typedParams and BAS_MEMBER_FRIEND); 18: typed slot inits (procedure localInits / initFirst / initCount; globalInits for every typed scalar), Byte and Currency (BAS_TYPE_BYTE, BAS_TYPE_CURRENCY, OP_PUSH_CUR, OP_CONV_BYTE replacing OP_CONV_STR_INT, OP_CONV_CUR, Currency data pool entries), RGB as &HBBGGRR and BAS_FN_QBCOLOR, named arguments (OP_CALL_NAMED, member paramNames), QBasic line numbers (line number table, OP_RESTORE label operand, OP_RESUME_LABEL) +#define BAS_MOD_VERSION 22 // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND; 3: BEEP / SOUND / PLAY / STICK / STRIG / FRAMEWAIT opcodes (runtime and stub rebuilt together); 4: DATE type, OP_CONV_DATE, OP_BUILTIN; 5: BAS_FN_PARAMARRAY; 6: program screen built-ins (BAS_FN_CLS .. BAS_FN_LINE_INPUT); 7: Variants, objects and class modules (class table, OP_PUSH_SPECIAL .. OP_PROP_ASSIGN, BAS_FN_VARTYPE .. BAS_FN_EACH_ITEM); 8: events, Implements, Missing (class implements lists, withEvents table, OP_RAISE_EVENT, OP_BIND_EVENTS, BAS_FN_ISMISSING, BAS_FN_LSET_TYPE, BAS_SPECIAL_MISSING); 9: screen pages and modes (SCREEN and PAINT argument blocks, BAS_FN_PCOPY .. BAS_FN_VARPTR_STR); 10: parameter defaults (member defaultParams), WithEvents in class modules (withEvents ownerClass), BAS_FN_ELEMENT_EXISTS; 11: BAS_FN_REF_TYPE, BAS_FN_HOLD_REF (typed method-call references, array locks); 12: OP_RAISE_EVENT adopts BAS_FN_HOLD_REF references (RaiseEvent passes TYPE fields by reference, their arrays locked); 13: one reference value (OP_PUSH_*_ADDR and OP_PUSH_ARR_ADDR carry the target's type, OP_PUSH_FIELD_ADDR, OP_REF_FIXLEN; BAS_FN_ELEMENT_EXISTS / REF_TYPE / HOLD_REF retired, BAS_FN_LOADPICTURE renumbered 70); 14: 32-bit code addresses (OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE / OP_GOSUB / OP_ON_ERROR / FOR offsets int32, OP_CALL / OP_CALL_ME int32 addresses: no 64 KB code limit), the new compiler front end (BAS_FN_RECORD_ASSIGN: TYPE values assigned into the record there); 15: compile-time member binding (OP_OBJ_GET / OP_OBJ_PUT / OP_OBJ_CALL, interface maps per Implements entry); 16: superinstructions OP_ADD_K16 / OP_SUB_K16 / OP_MUL_K16 / OP_IDIV_K16 / OP_MOD_K16 (INTEGER literal operand) and OP_STORE_LOCAL_CONV / OP_STORE_GLOBAL_CONV (conversion and store in one); 17: gap closure (OP_NULL_PROP; OP_FILE_PRINT retired for BAS_FN_FILE_PRINT, BAS_FN_CLOSE, BAS_FN_FILE_WIDTH, BAS_FN_PEN, BAS_FN_PEN_SET; LoadPicture size and colour arguments; member typedParams and BAS_MEMBER_FRIEND); 18: typed slot inits (procedure localInits / initFirst / initCount; globalInits for every typed scalar), Byte and Currency (BAS_TYPE_BYTE, BAS_TYPE_CURRENCY, OP_PUSH_CUR, OP_CONV_BYTE replacing OP_CONV_STR_INT, OP_CONV_CUR, Currency data pool entries), RGB as &HBBGGRR and BAS_FN_QBCOLOR, named arguments (OP_CALL_NAMED, member paramNames), QBasic line numbers (line number table, OP_RESTORE label operand, OP_RESUME_LABEL); 19: form instancing (BAS_FN_NEW_FORM: New FormName), BAS_FN_CALLBYNAME, BAS_FN_LOADFORMTEXT; 20: binary-safe DECLARE LIBRARY strings (OP_CALL_EXTERN carries a uint32 BINARY STRING mask); 21: UserControls (class events with parameter names and types, class flags, the UserControl member slot and UserControl_ procedures); 22: Enum member names of UserControl properties (enums table, class member enumIdx) // 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 @@ -130,10 +146,14 @@ #define BAS_FORMVAR_MIN_ENTRY 14 #define BAS_SLOTINIT_MIN_ENTRY 5 #define BAS_LINENUM_MIN_ENTRY 8 +#define BAS_ENUM_MIN_ENTRY 11 +#define BAS_ENUM_MEMBER_MIN_ENTRY 7 #define BAS_CLASS_MIN_ENTRY 22 -#define BAS_MEMBER_MIN_ENTRY 25 +#define BAS_MEMBER_MIN_ENTRY 29 #define BAS_IMPLEMENTS_MIN_ENTRY 8 #define BAS_IMPL_MAP_MIN_ENTRY 4 +#define BAS_EVENT_MIN_ENTRY 9 +#define BAS_UC_PROC_ENTRY 4 #define BAS_WITHEVENTS_MIN_ENTRY 17 #define BAS_HANDLER_MIN_ENTRY 8 #define BAS_DEBUGVAR_MIN_ENTRY 15 @@ -159,6 +179,7 @@ typedef struct { // Function prototypes (alphabetical) +void basClassEventsFree(BasClassEventT *events, int32_t count); void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen); uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen); BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen); @@ -175,15 +196,19 @@ static void bufWriteU16(BufT *b, uint16_t v); static void bufWriteU8(BufT *b, uint8_t v); static bool classRangesValid(const BasModuleT *mod, const BasClassInfoT *cls); static bool countFits(ReaderT *r, int32_t count, int32_t minBytesPerEntry); +static bool enumsValid(const BasModuleT *mod); static bool initsValid(const BasSlotInitT *inits, int32_t first, int32_t count, int32_t total, int32_t slots); static bool moduleRangesValid(const BasModuleT *mod); +static bool readClassEvents(ReaderT *r, BasClassInfoT *cls); static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry); +static bool readEnums(ReaderT *r, BasModuleT *mod); static bool readLineNumbers(ReaderT *r, BasModuleT *mod); static bool readSlotInits(ReaderT *r, BasSlotInitT **outInits, int32_t *outCount); static float rF32(ReaderT *r); static double rF64(ReaderT *r); static int32_t rI32(ReaderT *r); static int64_t rI64(ReaderT *r); +static void rIdent(ReaderT *r, char *dst); static bool rOk(ReaderT *r, int32_t need); static char *rStr(ReaderT *r, int32_t *outLen); static void rStrInto(ReaderT *r, char *dst, int32_t cap); @@ -191,6 +216,18 @@ static uint16_t rU16(ReaderT *r); static uint8_t rU8(ReaderT *r); static bool withEventsRangesValid(const BasModuleT *mod, const BasWithEventsT *we); + +// A class's event table (BasClassInfoT.events) and its parameter name +// lists: the module's own free and the compiler's build share it. +void basClassEventsFree(BasClassEventT *events, int32_t count) { + for (int32_t k = 0; k < count; k++) { + free(events[k].paramNames); + } + + free(events); +} + + void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen) { if (!mod || !data || dataLen < BAS_DBG_HEADER_SIZE) { return; @@ -469,9 +506,9 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) { } } - // The typed globals' and locals' starting values and the line numbers - // (survive strip) - if (!readSlotInits(&r, &mod->globalInits, &mod->globalInitCount) || !readSlotInits(&r, &mod->localInits, &mod->localInitCount) || !readLineNumbers(&r, mod)) { + // The typed globals' and locals' starting values, the line numbers and + // the UserControl properties' Enums (survive strip) + if (!readSlotInits(&r, &mod->globalInits, &mod->globalInitCount) || !readSlotInits(&r, &mod->localInits, &mod->localInitCount) || !readLineNumbers(&r, mod) || !readEnums(&r, mod)) { basModuleFree(mod); return NULL; } @@ -515,6 +552,7 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) { rStrInto(&r, m->name, BAS_MAX_IDENT); m->index = rI32(&r); + m->enumIdx = rI32(&r); m->kind = rU8(&r); m->flags = rU8(&r); m->dataType = rU8(&r); @@ -586,6 +624,26 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) { impl->map[k] = rI32(&r); } } + + if (!r.err && !readClassEvents(&r, cls)) { + basModuleFree(mod); + return NULL; + } + + cls->flags = rU8(&r); + cls->ucSlot = rI32(&r); + + int32_t ucProcCount = rU8(&r); + + // A UserControl lists every event's procedure, another class none + if (ucProcCount != (((cls->flags & BAS_CLASS_USERCONTROL) != 0) ? BAS_UC_EVT_COUNT : 0) || !countFits(&r, ucProcCount, BAS_UC_PROC_ENTRY)) { + r.err = true; + break; + } + + for (int32_t i = 0; i < BAS_UC_EVT_COUNT; i++) { + cls->ucProcs[i] = (i < ucProcCount) ? rI32(&r) : -1; + } } } @@ -701,10 +759,18 @@ void basModuleFree(BasModuleT *mod) { } free(mod->classes[i].implements); + + basClassEventsFree(mod->classes[i].events, mod->classes[i].eventCount); } free(mod->classes); + for (int32_t i = 0; i < mod->enumCount; i++) { + free(mod->enums[i].members); + } + + free(mod->enums); + for (int32_t i = 0; i < mod->withEventsCount; i++) { free(mod->withEvents[i].handlers); } @@ -831,6 +897,20 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) { bufWriteI32(&b, mod->lineNumbers[i].number); } + // The Enums of UserControl properties (a host shows them by name) + bufWriteI32(&b, mod->enumCount); + + for (int32_t e = 0; e < mod->enumCount; e++) { + const BasEnumInfoT *en = &mod->enums[e]; + + bufWriteI32(&b, en->memberCount); + + for (int32_t i = 0; i < en->memberCount; i++) { + bufWriteStr(&b, en->members[i].name, (int32_t)strlen(en->members[i].name)); + bufWriteI32(&b, en->members[i].value); + } + } + // Class modules (runtime-essential: New and member access) bufWriteI32(&b, mod->classCount); @@ -848,6 +928,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) { bufWriteStr(&b, m->name, (int32_t)strlen(m->name)); bufWriteI32(&b, m->index); + bufWriteI32(&b, m->enumIdx); bufWriteU8(&b, m->kind); bufWriteU8(&b, m->flags); bufWriteU8(&b, m->dataType); @@ -875,6 +956,28 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) { bufWriteI32(&b, impl->map[k]); } } + + bufWriteI32(&b, cls->eventCount); + + for (int32_t i = 0; i < cls->eventCount; i++) { + const BasClassEventT *ev = &cls->events[i]; + + bufWriteStr(&b, ev->name, (int32_t)strlen(ev->name)); + bufWriteU8(&b, (uint8_t)ev->paramCount); + bufWrite(&b, ev->paramTypes, ev->paramCount); + bufWriteI32(&b, (int32_t)ev->byValParams); + bufWriteStr(&b, (ev->paramNames != NULL) ? ev->paramNames : "", (ev->paramNames != NULL) ? (int32_t)strlen(ev->paramNames) : 0); + } + + bool isControl = (cls->flags & BAS_CLASS_USERCONTROL) != 0; + + bufWriteU8(&b, cls->flags); + bufWriteI32(&b, isControl ? cls->ucSlot : -1); + bufWriteU8(&b, isControl ? BAS_UC_EVT_COUNT : 0); + + for (int32_t i = 0; isControl && i < BAS_UC_EVT_COUNT; i++) { + bufWriteI32(&b, cls->ucProcs[i]); + } } // WithEvents variables (runtime-essential: RaiseEvent's handlers) @@ -1021,13 +1124,55 @@ static bool classRangesValid(const BasModuleT *mod, const BasClassInfoT *cls) { return false; } + // Known flags; a UserControl's object in one of its member variables + // and its event procedures in the procedure table + if ((cls->flags & ~BAS_CLASS_FLAGS_KNOWN) != 0) { + return false; + } + + if (cls->flags & BAS_CLASS_USERCONTROL) { + if (cls->ucSlot < 0 || cls->ucSlot >= cls->varCount) { + return false; + } + + for (int32_t i = 0; i < BAS_UC_EVT_COUNT; i++) { + if (cls->ucProcs[i] < -1 || cls->ucProcs[i] >= mod->procCount) { + return false; + } + } + } else if (cls->ucSlot != -1) { + return false; + } + + // Event parameter types are value types the VM knows + for (int32_t i = 0; i < cls->eventCount; i++) { + const BasClassEventT *ev = &cls->events[i]; + + if (ev->paramCount < BAS_MEMBER_PARAM_BITS && (ev->byValParams >> ev->paramCount) != 0) { + return false; + } + + for (int32_t k = 0; k < ev->paramCount; k++) { + if (ev->paramTypes[k] > BAS_TYPE_LAST) { + return false; + } + } + } + + // Members of known kinds in range; an Enum only for a UserControl's + // property, one of the module's for (int32_t i = 0; i < cls->memberCount; i++) { - const BasClassMemberT *m = &cls->members[i]; - int32_t limit = (m->kind == BAS_MEMBER_VAR) ? cls->varCount : mod->procCount; + const BasClassMemberT *m = &cls->members[i]; + int32_t limit = (m->kind == BAS_MEMBER_VAR) ? cls->varCount : mod->procCount; + bool isProp = m->kind != BAS_MEMBER_SUB && m->kind != BAS_MEMBER_FUNCTION; if (m->kind > BAS_MEMBER_SET || m->index < 0 || m->index >= limit) { return false; } + + if (m->enumIdx != BAS_ENUM_NONE && (m->enumIdx < 0 || m->enumIdx >= mod->enumCount || !isProp || !(cls->flags & BAS_CLASS_USERCONTROL))) { + return false; + } } // An interface map names one entry per way for every member of the @@ -1058,6 +1203,25 @@ static bool countFits(ReaderT *r, int32_t count, int32_t minBytesPerEntry) { } +// The Enums read from a file: no two members of one Enum share a name +// (ignoring case), so a name a host stores reads back as one value. +static bool enumsValid(const BasModuleT *mod) { + for (int32_t e = 0; e < mod->enumCount; e++) { + const BasEnumInfoT *en = &mod->enums[e]; + + for (int32_t i = 0; i < en->memberCount; i++) { + for (int32_t k = i + 1; k < en->memberCount; k++) { + if (strcasecmp(en->members[i].name, en->members[k].name) == 0) { + return false; + } + } + } + } + + return true; +} + + // True when inits[first .. first + count - 1] lie inside the total // entries and each names one of slots slots and a type a slot can start // as (a scalar, Variant or Object). @@ -1108,7 +1272,7 @@ static bool moduleRangesValid(const BasModuleT *mod) { } } - if (!initsValid(mod->globalInits, 0, mod->globalInitCount, mod->globalInitCount, mod->globalCount)) { + if (!initsValid(mod->globalInits, 0, mod->globalInitCount, mod->globalInitCount, mod->globalCount) || !enumsValid(mod)) { return false; } @@ -1128,6 +1292,52 @@ static bool moduleRangesValid(const BasModuleT *mod) { } +// A class's Events (BasClassEventT). False only when memory runs out +// (a malformed table sets r->err). +static bool readClassEvents(ReaderT *r, BasClassInfoT *cls) { + int32_t eventCount = readCount(r, BAS_EVENT_MIN_ENTRY); + + if (eventCount <= 0) { + return true; + } + + cls->events = (BasClassEventT *)calloc(eventCount, sizeof(BasClassEventT)); + + if (!cls->events) { + return false; + } + + cls->eventCount = eventCount; + + for (int32_t i = 0; i < eventCount && !r->err; i++) { + BasClassEventT *ev = &cls->events[i]; + int32_t namesLen = 0; + + rStrInto(r, ev->name, BAS_MAX_IDENT); + ev->paramCount = rU8(r); + + if (ev->paramCount > BAS_VM_MAX_CALL_ARGS) { + r->err = true; + break; + } + + for (int32_t k = 0; k < ev->paramCount; k++) { + ev->paramTypes[k] = rU8(r); + } + + ev->byValParams = (uint32_t)rI32(r); + ev->paramNames = rStr(r, &namesLen); + + if (namesLen == 0) { + free(ev->paramNames); + ev->paramNames = NULL; + } + } + + return true; +} + + // Read an int32 count and validate it with countFits. static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) { int32_t count = rI32(r); @@ -1141,6 +1351,53 @@ static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) { } +// The UserControl properties' Enums (count, then each one's members) into +// mod. False only when memory runs out; a malformed table (an Enum +// without members, a bad member name: rIdent) sets r->err. Each Enum's +// members are read into their own array before it joins the table (the +// caller frees mod, and with it every array in the table, on failure). +static bool readEnums(ReaderT *r, BasModuleT *mod) { + int32_t count = readCount(r, BAS_ENUM_MIN_ENTRY); + + if (count <= 0) { + return true; + } + + mod->enums = (BasEnumInfoT *)calloc(count, sizeof(BasEnumInfoT)); + + if (mod->enums == NULL) { + return false; + } + + mod->enumCount = count; + + for (int32_t e = 0; e < count && !r->err; e++) { + int32_t memberCount = readCount(r, BAS_ENUM_MEMBER_MIN_ENTRY); + + if (memberCount <= 0) { + r->err = true; + break; + } + + BasEnumMemberT *members = (BasEnumMemberT *)calloc(memberCount, sizeof(BasEnumMemberT)); + + if (members == NULL) { + return false; + } + + for (int32_t i = 0; i < memberCount; i++) { + rIdent(r, members[i].name); + members[i].value = rI32(r); + } + + mod->enums[e].members = members; + mod->enums[e].memberCount = memberCount; + } + + return true; +} + + // The line number table (count, then line / number pairs) into mod. // False when memory runs out or an entry is out of range. static bool readLineNumbers(ReaderT *r, BasModuleT *mod) { @@ -1246,6 +1503,23 @@ static int64_t rI64(ReaderT *r) { } +// Read a name into dst (BAS_MAX_IDENT bytes) that must fit it whole: +// 1 .. BAS_MAX_IDENT - 1 bytes, no NUL among them (an Enum member's, +// which a host stores and looks up by its spelling). Anything else sets +// r->err and leaves dst empty. +static void rIdent(ReaderT *r, char *dst) { + int32_t len = 0; + char *s = rStr(r, &len); + + if (!s || len == 0 || len >= BAS_MAX_IDENT || (int32_t)strlen(s) != len) { + r->err = true; + } + + snprintf(dst, BAS_MAX_IDENT, "%s", r->err ? "" : s); + free(s); +} + + static bool rOk(ReaderT *r, int32_t need) { // r->len and r->pos are both non-negative and r->pos is within // [0, len] (it only advances after this check), so r->len - r->pos diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.h b/src/apps/kpunch/dvxbasic/runtime/serialize.h index 2cb8fa4a..ef33da57 100644 --- a/src/apps/kpunch/dvxbasic/runtime/serialize.h +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.h @@ -54,6 +54,10 @@ const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); // Free a module (compiled or deserialized) and all its allocated members. void basModuleFree(BasModuleT *mod); +// Free a class's event table and its parameter name lists (count +// entries); the compiler frees a half-built table with it. +void basClassEventsFree(BasClassEventT *events, int32_t count); + // ============================================================ // Debug info serialization (separate resource) // ============================================================ diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c index ecf3cd8d..dd56b82d 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.c +++ b/src/apps/kpunch/dvxbasic/runtime/values.c @@ -64,18 +64,6 @@ #define BAS_INT_DIGITS_MAX 10 // digits of the largest int32 magnitude #define BAS_REF_CELL_POOL 16 // released reference cells kept for reuse (a ByRef TYPE field of an array element needs one per call) -// Freed strings of a pooled capacity (BAS_STRING_POOL_MIN_CAP doubled up -// to BAS_STRING_POOL_CLASSES times: 16 .. 128 bytes) are kept for reuse, -// at most BAS_STRING_POOL_DEPTH per capacity. None under AddressSanitizer, -// so it still sees a use of a freed string. -#define BAS_STRING_POOL_CLASSES 4 -#define BAS_STRING_POOL_MIN_CAP 16 -#if defined(__SANITIZE_ADDRESS__) -#define BAS_STRING_POOL_DEPTH 0 -#else -#define BAS_STRING_POOL_DEPTH 32 -#endif - // ============================================================ // String system // ============================================================ @@ -101,18 +89,28 @@ bool basValMismatchUsed = false; static BasRefCellT *sCellPool[BAS_REF_CELL_POOL]; static int32_t sCellPoolCount = 0; -// The string pool: per capacity class a list linked through the first -// bytes of each freed string's data. -static BasStringT *sStringPool[BAS_STRING_POOL_CLASSES]; -static int32_t sStringPoolCount[BAS_STRING_POOL_CLASSES]; +// A string in the pool: once free, its first bytes (refCount and len, +// dead until basStringAlloc sets them again) link to the next one. The +// link sits at the start of the heap block, so it is pointer-aligned on +// every target and leak checkers follow it: a pooled string is +// reachable, not lost. +typedef struct BasStringFreeS { + struct BasStringFreeS *next; +} BasStringFreeT; + +_Static_assert(sizeof(BasStringFreeT) <= offsetof(BasStringT, cap), "the string pool link must not overlay cap"); + +// The string pool: a list of freed strings per capacity class. +static BasStringFreeT *sStringPool[BAS_STRING_POOL_CLASSES]; +static int32_t sStringPoolCount[BAS_STRING_POOL_CLASSES]; // Function prototypes (alphabetical) BasArrayT *basArrayClone(const BasArrayT *arr); void basArrayFree(BasArrayT *arr); -int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims); +int32_t basArrayIndex(const BasArrayT *arr, const int32_t *indices, int32_t ndims); void basArrayLock(BasArrayT *arr); -BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType); +BasArrayT *basArrayNew(int32_t dims, const int32_t *lbounds, const int32_t *ubounds, uint8_t elementType); BasArrayT *basArrayNewEmpty(uint8_t elementType); BasArrayT *basArrayRef(BasArrayT *arr); void basArrayUnlock(BasArrayT *arr); @@ -137,6 +135,7 @@ int32_t basStringCompare(const BasStringT *a, const BasStringT *b); int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b); BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b); BasStringT *basStringNew(const char *text, int32_t len); +void basStringPoolDrain(void); BasStringT *basStringRef(BasStringT *s); BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len); void basStringUnref(BasStringT *s); @@ -166,6 +165,7 @@ BasValueT basValNull(void); BasValueT basValObject(void *obj); void basValRelease(BasValueT *v); bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out); +const char *basValScalarTypeName(uint8_t type); BasValueT basValSingle(float v); BasValueT basValString(BasStringT *s); BasValueT basValStringFromC(const char *text); @@ -243,7 +243,7 @@ void basArrayFree(BasArrayT *arr) { } -int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims) { +int32_t basArrayIndex(const BasArrayT *arr, const int32_t *indices, int32_t ndims) { if (!arr || ndims != arr->dims) { return -1; } @@ -280,7 +280,7 @@ void basArrayLock(BasArrayT *arr) { } -BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType) { +BasArrayT *basArrayNew(int32_t dims, const int32_t *lbounds, const int32_t *ubounds, uint8_t elementType) { if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS) { return NULL; } @@ -793,9 +793,11 @@ BasStringT *basStringAlloc(int32_t cap) { cap = BAS_STRING_POOL_MIN_CAP << cls; if (sStringPoolCount[cls] > 0) { - s = sStringPool[cls]; - memcpy(&sStringPool[cls], s->data, sizeof(s)); + BasStringFreeT *node = sStringPool[cls]; + + sStringPool[cls] = node->next; sStringPoolCount[cls]--; + s = (BasStringT *)node; } } @@ -950,6 +952,20 @@ BasStringT *basStringNew(const char *text, int32_t len) { } +void basStringPoolDrain(void) { + for (int32_t cls = 0; cls < BAS_STRING_POOL_CLASSES; cls++) { + while (sStringPool[cls] != NULL) { + BasStringFreeT *node = sStringPool[cls]; + + sStringPool[cls] = node->next; + free(node); + } + + sStringPoolCount[cls] = 0; + } +} + + BasStringT *basStringRef(BasStringT *s) { // The empty string is immortal and shared by every "" value; counting // it would only overflow the counter eventually (basStringUnref skips @@ -993,8 +1009,10 @@ void basStringUnref(BasStringT *s) { int32_t cls = stringPoolClass(s->cap, true); if (cls >= 0 && sStringPoolCount[cls] < BAS_STRING_POOL_DEPTH) { - memcpy(s->data, &sStringPool[cls], sizeof(s)); - sStringPool[cls] = s; + BasStringFreeT *node = (BasStringFreeT *)s; + + node->next = sStringPool[cls]; + sStringPool[cls] = node; sStringPoolCount[cls]++; return; } @@ -1428,6 +1446,41 @@ bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out) { } +const char *basValScalarTypeName(uint8_t type) { + switch (type) { + case BAS_TYPE_INTEGER: + return "Integer"; + + case BAS_TYPE_BYTE: + return "Byte"; + + case BAS_TYPE_LONG: + return "Long"; + + case BAS_TYPE_CURRENCY: + return "Currency"; + + case BAS_TYPE_SINGLE: + return "Single"; + + case BAS_TYPE_DOUBLE: + return "Double"; + + case BAS_TYPE_DATE: + return "Date"; + + case BAS_TYPE_STRING: + return "String"; + + case BAS_TYPE_BOOLEAN: + return "Boolean"; + + default: + return NULL; + } +} + + BasValueT basValSingle(float v) { BasValueT val; val.type = BAS_TYPE_SINGLE; diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h index 6d005286..fcc80a66 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.h +++ b/src/apps/kpunch/dvxbasic/runtime/values.h @@ -54,6 +54,18 @@ typedef struct { // Capacity multiplier when basStringAppend has to grow a string. #define BAS_STRING_GROWTH 2 +// Freed strings of a pooled capacity (BAS_STRING_POOL_MIN_CAP doubled up +// to BAS_STRING_POOL_CLASSES times: 16 .. 128 bytes) are kept for reuse, +// at most BAS_STRING_POOL_DEPTH per capacity. None under AddressSanitizer, +// so it still sees a use of a freed string. +#define BAS_STRING_POOL_CLASSES 4 +#define BAS_STRING_POOL_MIN_CAP 16 +#if defined(__SANITIZE_ADDRESS__) +#define BAS_STRING_POOL_DEPTH 0 +#else +#define BAS_STRING_POOL_DEPTH 32 +#endif + // Allocate a new string from a C string. refCount starts at 1. BasStringT *basStringNew(const char *text, int32_t len); @@ -72,6 +84,13 @@ BasStringT *basStringRef(BasStringT *s); // Decrement reference count. Frees if count reaches zero. void basStringUnref(BasStringT *s); +// Free the released strings the allocator keeps for reuse. basVmDestroy +// calls it: the runtime stays loaded while programs come and go (DOS, the +// IDE's repeated runs), so the end of a program is where its cached +// string blocks go back to the heap. Only free strings are in the pool, +// so a VM still running elsewhere loses nothing but the cache. +void basStringPoolDrain(void); + // Concatenate two strings. Returns a new string (refCount 1). BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b); @@ -122,7 +141,7 @@ typedef struct { BasArrayT *basArrayClone(const BasArrayT *arr); // Allocate a new array. refCount starts at 1. -BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType); +BasArrayT *basArrayNew(int32_t dims, const int32_t *lbounds, const int32_t *ubounds, uint8_t elementType); // Allocate a one-dimensional array with no elements (LBOUND 0, UBOUND -1), // what SPLIT and FILTER return when there is nothing to hold. refCount @@ -145,7 +164,7 @@ void basArrayLock(BasArrayT *arr); void basArrayUnlock(BasArrayT *arr); // Compute flat index from multi-dimensional indices. Returns -1 if out of bounds. -int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims); +int32_t basArrayIndex(const BasArrayT *arr, const int32_t *indices, int32_t ndims); // ============================================================ // Reference-counted user-defined type instance @@ -448,6 +467,10 @@ BasStringT *basValPrintString(BasValueT val); // Check if a value is truthy (non-zero number, non-empty string). bool basValIsTruthy(BasValueT v); +// The name programs write for scalar type type (BAS_TYPE_INTEGER -> +// "Integer" ... "Boolean"), NULL for any other type. +const char *basValScalarTypeName(uint8_t type); + // Compare two values. Returns -1, 0, or 1. // Numeric types are compared numerically. Strings lexicographically. // A DATE compared with a string compares with the date the string holds. diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c index 5f6dee46..df1df665 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.c +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c @@ -202,12 +202,6 @@ #define BAS_QBCOLOR_COUNT 16 #define BAS_CHAR_MASK 0xFF -// Accesses of an object member: a read, or an assignment made with Let -// (obj.p = v) or Set (Set obj.p = v). -#define BAS_MEMBER_OP_GET 0 -#define BAS_MEMBER_OP_LET 1 -#define BAS_MEMBER_OP_SET 2 - // findMember: rank of a member that cannot serve the access. #define BAS_MEMBER_NO_RANK 2 @@ -230,6 +224,7 @@ // How a Collection's item array grows. #define BAS_COLL_MIN_CAP 4 #define BAS_COLL_GROWTH 2 +#define BAS_COLL_KEY_ARGS 2 // Add item, key // TYPENAME / TYPEOF names that are not class or control names, and the // suffix of an array's type name ("Integer()"). @@ -386,8 +381,10 @@ static uint32_t builtinNullMask(uint8_t fn); static int32_t builtinObject(BasVmT *vm, uint8_t fn, BasValueT *args, BasValueT *out); static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *out); static BasVmResultE callBound(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, uint8_t resultMode); +static BasVmResultE callByName(BasVmT *vm, BasValueT *args, int32_t argc); static BasVmResultE callMember(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, bool wantResult); static BasVmResultE callNamed(BasVmT *vm, BasValueT objVal, BasValueT nameVal, BasValueT *args, int32_t argc, int32_t named, BasValueT namesVal); +static bool callProcAt(BasVmT *vm, int32_t codeAddr, BasObjectT *me, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount); static int32_t clampComponent(int32_t c); static bool classImplements(const BasVmT *vm, int32_t classIdx, int32_t iface); static bool classListens(const BasVmT *vm, int32_t classIdx); @@ -484,6 +481,8 @@ static int32_t gcFind(const BasGcT *gc, const void *ptr); static bool gcGrow(BasGcT *gc); static bool gcRelease(BasVmT *vm, const BasGcT *gc, int32_t returnPc); static void gcScan(BasGcT *gc, int32_t idx, bool mark); +static bool hostCallFinish(BasVmT *vm, BasVmResultE rc, int32_t savedPc, int32_t savedDepth, bool savedRunning, int32_t savedSp, BasValueT *result); +static bool hostCallReady(const BasVmT *vm); static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx); static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr); static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val); @@ -546,6 +545,7 @@ static void popCallFrame(BasVmT *vm); static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen); static bool popPair(BasVmT *vm, BasValueT *a, BasValueT *b); static bool popStringArg(BasVmT *vm, BasValueT *out); +static BasValueT popUnchecked(BasVmT *vm); static void primeModuleFrame(BasVmT *vm); static int32_t *printColumnOf(BasVmT *vm); static bool printerDevice(const char *name, int32_t *outPort); @@ -560,6 +560,7 @@ static void putBounded(char *out, int32_t *idx, int32_t limit, char c) static BasVmResultE putValue(BasVmT *vm, BasPutSinkT *sink, int32_t mode, BasValueT val, const BasStringT *layout); static void queueTerminate(void *ctx, BasObjectT *obj); static BasVmResultE raiseError(BasVmT *vm, int32_t errNum); +static BasVmResultE raiseToHost(BasVmT *vm, BasObjectT *obj, int32_t eventIdx, const BasValueT *args, int32_t argc); static BasStringT *readField(FILE *fp); static int16_t readInt16(BasVmT *vm); static int32_t readInt32(BasVmT *vm); @@ -706,8 +707,10 @@ static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, bo } } - int32_t lbounds[BAS_ARRAY_MAX_DIMS]; - int32_t ubounds[BAS_ARRAY_MAX_DIMS]; + // Zeroed: validArrayDims keeps dims in 1 .. BAS_ARRAY_MAX_DIMS and the + // loop sets every bound in use, which cppcheck cannot see through + int32_t lbounds[BAS_ARRAY_MAX_DIMS] = { 0 }; + int32_t ubounds[BAS_ARRAY_MAX_DIMS] = { 0 }; // Pop bounds in reverse order (last dim first) for (int32_t d = dims - 1; d >= 0; d--) { @@ -752,7 +755,7 @@ static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, bo memcpy(idx, lbounds, sizeof(int32_t) * dims); for (int32_t flat = 0; flat < arr->totalElements; flat++) { - int32_t oldFlat = oldArr ? basArrayIndex((BasArrayT *)oldArr, idx, dims) : -1; + int32_t oldFlat = oldArr ? basArrayIndex(oldArr, idx, dims) : -1; if (oldFlat >= 0) { BasValueT v = basValCopy(oldArr->elements[oldFlat]); @@ -882,10 +885,47 @@ static BasValueT arrValue(BasArrayT *arr) { } +bool basVmCallMember(BasVmT *vm, BasObjectT *obj, const char *name, int32_t op, const BasValueT *args, int32_t argc, BasValueT *result) { + if (result) { + *result = basValEmpty(); + } + + int32_t given = (op == BAS_MEMBER_OP_GET) ? argc : argc - 1; + + if (!hostCallReady(vm) || !obj || !name || given < 0 || argc > BAS_VM_MAX_CALL_ARGS) { + return false; + } + + BasValueT callArgs[BAS_VM_MAX_CALL_ARGS + 1]; + BasValueT *value = (op == BAS_MEMBER_OP_GET) ? NULL : &callArgs[given]; + + for (int32_t i = 0; i < argc; i++) { + callArgs[i] = basValCopy(args[i]); + } + + int32_t savedPc = vm->pc; + int32_t savedDepth = vm->callDepth; + int32_t savedSp = vm->sp; + bool savedRunning = vm->running; + BasVmResultE rc = objectAccess(vm, obj, name, callArgs, given, value, op); + + return hostCallFinish(vm, rc, savedPc, savedDepth, savedRunning, savedSp, result); +} + + +bool basVmCallProc(BasVmT *vm, BasObjectT *obj, int32_t procIdx, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount) { + if (!vm || !vm->module || procIdx < 0 || procIdx >= vm->module->procCount) { + return false; + } + + return callProcAt(vm, vm->module->procs[procIdx].codeAddr, obj, args, argCount, outArgs, outCount); +} + + bool basVmCallSub(BasVmT *vm, int32_t codeAddr) { // Thin wrapper: no args, no out-args. basVmCallSubWithArgsOut handles // NULL args / zero counts and contains the full validation + frame setup. - return basVmCallSubWithArgsOut(vm, codeAddr, NULL, 0, NULL, BAS_MEMBER_OP_GET); + return basVmCallSubWithArgsOut(vm, codeAddr, NULL, 0, NULL, 0); } @@ -896,45 +936,39 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount) { - if (!vm || !vm->module) { - return false; - } + return callProcAt(vm, codeAddr, NULL, args, argCount, outArgs, outCount); +} - if (vm->debugPaused) { - return false; - } - if (codeAddr < 0 || codeAddr >= vm->module->codeLen) { - return false; - } +void basVmClearStepState(BasVmT *vm) { + vm->debugBreak = false; + vm->stepOverDepth = -1; + vm->stepOutDepth = -1; + vm->runToCursorLine = -1; +} - if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1 || argCount < 0 || argCount > BAS_VM_MAX_LOCALS) { - return false; - } - int32_t savedPc = vm->pc; - int32_t savedCallDepth = vm->callDepth; - bool savedRunning = vm->running; - BasCallFrameT *frame = pushFrame(vm, savedPc, codeAddr, argCount); +// Appends item to Collection coll under key (NULL: none). Returns 0 or +// the BASIC error number (a key in use). +int32_t basVmCollectionAdd(BasObjectT *coll, BasValueT item, const char *key) { + BasValueT args[BAS_COLL_KEY_ARGS]; + int32_t argc = key ? BAS_COLL_KEY_ARGS : 1; - // The first outCount parameters are references to the host's outArgs, - // which start as the arguments: what the SUB stores in them is there - // at once. runSubLoop pops every frame it pushed before returning, so - // no reference outlives the host's buffer. - for (int32_t i = 0; i < argCount; i++) { - if (outArgs && i < outCount) { - basValRelease(&outArgs[i]); - outArgs[i] = basValCopy(args[i]); - frame->locals[i] = refValue(BAS_REF_SLOT, NULL, &outArgs[i], BAS_TYPE_VARIANT); - } else { - frame->locals[i] = basValCopy(args[i]); - } - } + args[0] = item; + args[1] = key ? basValStringFromC(key) : basValEmpty(); - vm->pc = codeAddr; - vm->running = true; + int32_t err = collectionAdd(coll, args, argc); - return runSubLoop(vm, savedPc, savedCallDepth, savedRunning); + basValRelease(&args[1]); + return err; +} + + +// A new empty Collection owned by vm, for the host to fill and hand to +// BASIC (basValInstance takes the one reference). NULL when out of memory. +BasObjectT *basVmCollectionNew(BasVmT *vm) { + vm->gcAllocs++; + return basObjectNew(BAS_OBJ_COLLECTION, 0, &vm->liveObjects); } @@ -991,6 +1025,7 @@ void basVmDestroy(BasVmT *vm) { free(vm->externCache); free(vm->eventSinks); free(vm); + basStringPoolDrain(); } @@ -1116,11 +1151,21 @@ void basVmForgetForm(BasVmT *vm, void *formRef, BasValueT *formVars, int32_t for } +int32_t basVmGetCurrentLine(const BasVmT *vm) { + return vm->currentLine; +} + + const char *basVmGetError(const BasVmT *vm) { return vm ? vm->errorMsg : ""; } +int32_t basVmGetErrorLine(const BasVmT *vm) { + return vm->errorLine; +} + + void basVmLoadModule(BasVmT *vm, BasModuleT *module) { if (!module) { vm->module = NULL; @@ -1188,12 +1233,40 @@ void basVmLoadModule(BasVmT *vm, BasModuleT *module) { } +BasObjectT *basVmNewObject(BasVmT *vm, int32_t classIdx) { + if (!hostCallReady(vm) || classIdx < 0 || classIdx >= vm->module->classCount) { + return NULL; + } + + int32_t savedPc = vm->pc; + int32_t savedDepth = vm->callDepth; + int32_t savedSp = vm->sp; + bool savedRunning = vm->running; + BasValueT obj = basValEmpty(); + BasVmResultE rc = newObject(vm, (uint16_t)classIdx); + + if (!hostCallFinish(vm, rc, savedPc, savedDepth, savedRunning, savedSp, &obj) || obj.type != BAS_TYPE_INSTANCE) { + basValRelease(&obj); + return NULL; + } + + // The stack's reference is the caller's now + return obj.instVal; +} + + void basVmRequestStop(BasVmT *vm) { vm->stopRequested = true; vm->running = false; } +void basVmResume(BasVmT *vm) { + vm->debugPaused = false; + vm->running = true; +} + + BasVmResultE basVmRun(BasVmT *vm) { // basVmLoadModule refused the module (too many globals): report why // instead of returning a clean HALTED for a program that never ran. @@ -1270,6 +1343,19 @@ void basVmRunToCursor(BasVmT *vm, int32_t line) { } +void basVmSetAppPaths(BasVmT *vm, const char *path, const char *config, const char *data) { + snprintf(vm->appPath, sizeof(vm->appPath), "%s", path); + snprintf(vm->appConfig, sizeof(vm->appConfig), "%s", config); + snprintf(vm->appData, sizeof(vm->appData), "%s", data); +} + + +void basVmSetBreakpointCallback(BasVmT *vm, BasBreakpointFnT fn, void *ctx) { + vm->breakpointFn = fn; + vm->breakpointCtx = ctx; +} + + void basVmSetBreakpoints(BasVmT *vm, int32_t *lines, int32_t count) { if (!vm) { return; @@ -1298,6 +1384,11 @@ void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count) { } +void basVmSetDebugPaused(BasVmT *vm, bool paused) { + vm->debugPaused = paused; +} + + void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device) { vm->device = *device; } @@ -1309,6 +1400,12 @@ void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx) { } +void basVmSetEventHost(BasVmT *vm, BasEventHostFnT fn, void *ctx) { + vm->eventHostFn = fn; + vm->eventHostCtx = ctx; +} + + void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext) { vm->ext = *ext; } @@ -3317,8 +3414,8 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { vm->sp -= argc; memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT)); - pop(vm, &methodNameVal); - pop(vm, &ctrlRefVal); + methodNameVal = popUnchecked(vm); + ctrlRefVal = popUnchecked(vm); return namedMemberOp(vm, ctrlRefVal, methodNameVal, args, argc, NULL, BAS_MEMBER_OP_GET, true); } @@ -3340,11 +3437,11 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { return BAS_VM_STACK_UNDERFLOW; } - pop(vm, &namesVal); - vm->sp -= argc; + namesVal = popUnchecked(vm); + vm->sp -= argc; memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT)); - pop(vm, &methodNameVal); - pop(vm, &ctrlRefVal); + methodNameVal = popUnchecked(vm); + ctrlRefVal = popUnchecked(vm); return callNamed(vm, ctrlRefVal, methodNameVal, args, argc, named, namesVal); } @@ -3546,11 +3643,11 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { return BAS_VM_STACK_UNDERFLOW; } - pop(vm, &value); + value = popUnchecked(vm); vm->sp -= argc; memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT)); - pop(vm, &nameVal); - pop(vm, &objVal); + nameVal = popUnchecked(vm); + objVal = popUnchecked(vm); return namedMemberOp(vm, objVal, nameVal, args, argc, &value, (flags & BAS_ASSIGN_SET) ? BAS_MEMBER_OP_SET : BAS_MEMBER_OP_LET, false); } @@ -3858,6 +3955,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { uint16_t funcNameIdx = readUint16(vm); uint8_t argc = readUint8(vm); uint8_t retType = readUint8(vm); + uint32_t binaryMask = (uint32_t)readInt32(vm); const char *libName = (libNameIdx < (uint16_t)vm->module->constCount) ? vm->module->constants[libNameIdx]->data : ""; const char *funcName = (funcNameIdx < (uint16_t)vm->module->constCount) ? vm->module->constants[funcNameIdx]->data : ""; void *funcPtr = NULL; @@ -3916,7 +4014,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) { BasValueT result = basValInteger(0); if (vm->ext.callExtern) { - result = vm->ext.callExtern(vm->ext.ctx, funcPtr, libName, funcName, args, argCount, retType); + result = vm->ext.callExtern(vm->ext.ctx, funcPtr, libName, funcName, args, argCount, retType, binaryMask); } for (int32_t i = 0; i < argCount; i++) { @@ -4103,6 +4201,14 @@ void basVmStepOver(BasVmT *vm) { } +void basVmStop(BasVmT *vm) { + basVmClearStepState(vm); + basVmSetBreakpoints(vm, NULL, 0); + vm->running = false; + vm->debugPaused = false; +} + + // The member an early-bound access of member of class cls reaches on obj: // that member when obj is of class cls, else the one the interface map of // obj's class names for it (way: BAS_IMPL_READ, or BAS_IMPL_STORE for a @@ -4968,6 +5074,29 @@ static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t ar return 0; } + case BAS_FN_NEW_FORM: + if (!vm->ui.newForm) { + return BAS_ERR_NO_FORM_RUNTIME; + } + + *out = basValObject(vm->ui.newForm(vm->ui.ctx, argString(&args[0])->data)); + return 0; + + case BAS_FN_LOADFORMTEXT: { + if (!vm->ui.loadFormText) { + return BAS_ERR_NO_FORM_RUNTIME; + } + + void *form = vm->ui.loadFormText(vm->ui.ctx, argString(&args[0])->data); + + if (!form) { + return BAS_ERR_ILLEGAL_FUNC_CALL; + } + + *out = basValObject(form); + return 0; + } + case BAS_FN_PARAMARRAY: { // (elementType, values...): the values (references for // arguments passed by reference) move into a 0-based array @@ -5025,6 +5154,38 @@ static BasVmResultE callBound(BasVmT *vm, BasObjectT *obj, const BasClassMemberT } +// CALLBYNAME(object, name, callType [, args...]): the member name of the +// object (a class instance, Collection, form or control) read (vbGet), +// assigned the last argument (vbLet / vbSet, the result Empty) or called +// (vbMethod) at run time, as obj.name would be. Takes over args. +static BasVmResultE callByName(BasVmT *vm, BasValueT *args, int32_t argc) { + BasValueT nameVal = basValToString(args[1]); + int32_t callType = basValToInt32(args[2]); + BasValueT *rest = &args[BAS_CALLBYNAME_FIXED]; + int32_t restc = argc - BAS_CALLBYNAME_FIXED; + BasVmResultE rc; + + basValRelease(&args[1]); + basValRelease(&args[2]); + + if (callType == BAS_CALLTYPE_METHOD || callType == BAS_CALLTYPE_GET) { + rc = memberByName(vm, args[0], nameVal.strVal->data, rest, restc, NULL, BAS_MEMBER_OP_GET, callType == BAS_CALLTYPE_METHOD || restc > 0); + } else if ((callType == BAS_CALLTYPE_LET || callType == BAS_CALLTYPE_SET) && restc > 0) { + BasValueT value = rest[restc - 1]; + + rc = memberByName(vm, args[0], nameVal.strVal->data, rest, restc - 1, &value, (callType == BAS_CALLTYPE_SET) ? BAS_MEMBER_OP_SET : BAS_MEMBER_OP_LET, false); + rc = (rc == BAS_VM_OK && !push(vm, basValEmpty())) ? BAS_VM_STACK_OVERFLOW : rc; + } else { + basValRelease(&args[0]); + dropArgs(rest, restc); + rc = raiseError(vm, BAS_ERR_ILLEGAL_FUNC_CALL); + } + + basValRelease(&nameVal); + return rc; +} + + // Runs procedure member m of obj called by name (late binding): the // arguments (taken over) become its parameters, omitted Optional ones // their type's zero, the rest of a ParamArray's packed into an array. An @@ -5151,6 +5312,43 @@ static BasVmResultE callNamed(BasVmT *vm, BasValueT objVal, BasValueT nameVal, B } +// Runs the procedure at codeAddr from the host to completion with Me = +// me (NULL: none): the basVmCallSubWithArgsOut / basVmCallProc contract. +static bool callProcAt(BasVmT *vm, int32_t codeAddr, BasObjectT *me, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount) { + if (!hostCallReady(vm) || codeAddr < 0 || codeAddr >= vm->module->codeLen || argCount < 0 || argCount > BAS_VM_MAX_LOCALS) { + return false; + } + + int32_t savedPc = vm->pc; + int32_t savedCallDepth = vm->callDepth; + bool savedRunning = vm->running; + BasCallFrameT *frame = pushFrame(vm, savedPc, codeAddr, argCount); + + // The first outCount parameters are references to the host's outArgs, + // which start as the arguments: what the SUB stores in them is there + // at once. runSubLoop pops every frame it pushed before returning, so + // no reference outlives the host's buffer. + for (int32_t i = 0; i < argCount; i++) { + if (outArgs && i < outCount) { + basValRelease(&outArgs[i]); + outArgs[i] = basValCopy(args[i]); + frame->locals[i] = refValue(BAS_REF_SLOT, NULL, &outArgs[i], BAS_TYPE_VARIANT); + } else { + frame->locals[i] = basValCopy(args[i]); + } + } + + if (me) { + frame->me = basValInstance(basObjectRef(me)); + } + + vm->pc = codeAddr; + vm->running = true; + + return runSubLoop(vm, savedPc, savedCallDepth, savedRunning); +} + + // Clamps an RGB channel value into [0, BAS_RGB_COMPONENT_MAX]. static int32_t clampComponent(int32_t c) { if (c < 0) { @@ -6363,6 +6561,12 @@ static BasVmResultE execBuiltin(BasVmT *vm) { memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT)); argc = builtinDefaults(fn, args, argc); + // CallByName runs the member the way obj.member does: its result (a + // BASIC procedure's when it returns) is pushed there + if (fn == BAS_FN_CALLBYNAME) { + return callByName(vm, args, argc); + } + // The date functions are numbered NOW through WEEKDAYNAME; the program // screen's are the basBuiltinIsScreen ones. BasValueT result = basValInteger(0); @@ -6533,9 +6737,9 @@ static BasVmResultE execConvert(BasVmT *vm, uint8_t op) { return BAS_VM_STACK_UNDERFLOW; } - BasValueT *top = &vm->stack[vm->sp - 1]; - BasValueT conv = *top; - int32_t n = 0; + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv; + int32_t n = 0; // An object (its default value was taken before the instruction when // it has one), array or TYPE value is no number or text @@ -8085,7 +8289,7 @@ static BasVmResultE execObjMember(BasVmT *vm, uint8_t op) { vm->sp -= operands; memcpy(args, &vm->stack[vm->sp], (size_t)operands * sizeof(BasValueT)); - pop(vm, &objVal); + objVal = popUnchecked(vm); // Nothing (or no object at all) has no members if (!basValIsObject(objVal) || (objVal.type == BAS_TYPE_OBJECT && !objVal.objVal) || (objVal.type == BAS_TYPE_INSTANCE && !objVal.instVal)) { @@ -8184,6 +8388,18 @@ static BasVmResultE execRaiseEvent(BasVmT *vm) { return BAS_VM_ERROR; } + // A UserControl's event goes to its widget's container first + const BasClassInfoT *raiser = objectClass(vm, me.instVal); + + if (vm->eventHostFn && raiser && (raiser->flags & BAS_CLASS_USERCONTROL)) { + BasVmResultE hostRc = raiseToHost(vm, me.instVal, eventIdx, args, argc); + + if (hostRc != BAS_VM_OK) { + dropArgs(args, argc); + return hostRc; + } + } + for (int32_t i = 0; i < vm->eventSinkCount && count < room; i++) { const BasEventSinkT *sink = &vm->eventSinks[i]; const BasWithEventsT *we = &vm->module->withEvents[sink->binding]; @@ -8602,15 +8818,20 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { return BAS_VM_STACK_UNDERFLOW; } - startPos = basValToInt32(startVal) - 1; + // Start is tested before it is made 0-based: INT32_MIN - 1 + // would overflow + int32_t start = basValToInt32(startVal); + basValRelease(&startVal); - if (startPos < 0) { + if (start < 1) { basValRelease(&fv); basValRelease(&sv); runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call"); return BAS_VM_ERROR; } + + startPos = start - 1; } int32_t pos = 0; @@ -8764,22 +8985,24 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { return BAS_VM_STACK_UNDERFLOW; } - BasValueT sv = basValToString(strVal); - BasValueT rv = basValToString(replVal); - int32_t start = basValToInt32(startVal) - 1; // 1-based to 0-based - int32_t len = basValToInt32(lenVal); + BasValueT sv = basValToString(strVal); + BasValueT rv = basValToString(replVal); + int32_t startArg = basValToInt32(startVal); // 1-based, tested before it is made 0-based (INT32_MIN - 1 would overflow) + int32_t len = basValToInt32(lenVal); basValRelease(&strVal); basValRelease(&startVal); basValRelease(&lenVal); basValRelease(&replVal); - if (start < 0 || (len < 0 && len != BAS_MID_LEN_OMITTED)) { + if (startArg < 1 || (len < 0 && len != BAS_MID_LEN_OMITTED)) { basValRelease(&sv); basValRelease(&rv); runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call"); return BAS_VM_ERROR; } + int32_t start = startArg - 1; + BasStringT *src = sv.strVal; BasStringT *repl = rv.strVal; int32_t srcLen = src->len; @@ -9812,10 +10035,48 @@ static void gcScan(BasGcT *gc, int32_t idx, bool mark) { } +// Ends a host call (basVmNewObject, basVmCallMember) that started with +// rc and left the VM at savedPc / savedDepth / savedSp: the frames it +// pushed run to completion, then the VM is as it was. The value the call +// left on the stack goes to *result (NULL: dropped). False when the call +// failed (the VM's error is set). +static bool hostCallFinish(BasVmT *vm, BasVmResultE rc, int32_t savedPc, int32_t savedDepth, bool savedRunning, int32_t savedSp, BasValueT *result) { + bool ok = (rc == BAS_VM_OK); + + if (ok && vm->callDepth > savedDepth) { + vm->running = true; + ok = runSubLoop(vm, savedPc, savedDepth, savedRunning); + } + + // A failed access may leave the frame it pushed + while (vm->callDepth > savedDepth) { + discardFrame(vm); + } + + vm->pc = savedPc; + vm->running = savedRunning; + + if (ok && result && vm->sp > savedSp) { + *result = vm->stack[savedSp]; + vm->stack[savedSp] = basValEmpty(); + } + + dropValues(vm, vm->sp - savedSp); + return ok; +} + + +// True when the host may call into the VM now: a module is loaded, the +// debugger has not paused it and a call frame is free. +static bool hostCallReady(const BasVmT *vm) { + return vm && vm->module && !vm->debugPaused && vm->callDepth < BAS_VM_CALL_STACK_SIZE - 1; +} + + // Flat index of args[0..argc) in arr. Returns 0 and sets *outIdx, else // the BASIC error number. static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx) { - int32_t indices[BAS_ARRAY_MAX_DIMS]; + int32_t indices[BAS_ARRAY_MAX_DIMS] = { 0 }; // zeroed for cppcheck: basArrayIndex reads only the argc set below if (argc > BAS_ARRAY_MAX_DIMS) { return BAS_ERR_SUBSCRIPT_RANGE; @@ -9826,7 +10087,7 @@ static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, i indices[i] = basValToInt32(args[i]); } - *outIdx = basArrayIndex((BasArrayT *)arr, indices, argc); + *outIdx = basArrayIndex(arr, indices, argc); return (*outIdx < 0) ? BAS_ERR_SUBSCRIPT_RANGE : 0; } @@ -10358,11 +10619,13 @@ static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b bool known = !(aNull && bNull); + // True OR Null, False IMP Null and Null IMP True are all True + bool orTrue = op == OP_OR && other == BAS_TRUE_BITS; + bool impTrue = op == OP_IMP && ((bNull && other == 0) || (aNull && other == BAS_TRUE_BITS)); + if (known && op == OP_AND && other == 0) { result = intResult(0); - } else if (known && op == OP_OR && other == BAS_TRUE_BITS) { - result = intResult(BAS_TRUE_BITS); - } else if (known && op == OP_IMP && ((bNull && other == 0) || (aNull && other == BAS_TRUE_BITS))) { + } else if (known && (orTrue || impTrue)) { result = intResult(BAS_TRUE_BITS); } @@ -10914,7 +11177,9 @@ static const BasClassInfoT *objectClass(const BasVmT *vm, const BasObjectT *obj) // obj(args) (OP_LOAD_ARRAY) or obj(args) = value (OP_STORE_ARRAY) on a // class instance or Collection: its default member. The operands are on -// the stack (the value on top for a store). +// the stack (the value on top for a store): every caller found the +// instance beneath them first (vm->sp > argc, or > argc + 1 with the +// value), so they cannot underflow. static BasVmResultE objectDefault(BasVmT *vm, int32_t argc, bool isStore) { BasValueT args[BAS_VM_MAX_CALL_ARGS]; BasValueT value; @@ -10926,12 +11191,12 @@ static BasVmResultE objectDefault(BasVmT *vm, int32_t argc, bool isStore) { } if (isStore) { - pop(vm, &value); + value = popUnchecked(vm); } vm->sp -= argc; memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT)); - pop(vm, &objVal); + objVal = popUnchecked(vm); BasVmResultE rc = objectAccess(vm, objVal.instVal, NULL, args, argc, isStore ? &value : NULL, isStore ? BAS_MEMBER_OP_LET : BAS_MEMBER_OP_GET); @@ -11429,6 +11694,13 @@ static bool popStringArg(BasVmT *vm, BasValueT *out) { } +// Pops the top of the stack after the caller's own depth check made sure +// it is there (pop() tests the depth itself). +static BasValueT popUnchecked(BasVmT *vm) { + return vm->stack[--vm->sp]; +} + + // Primes callStack[0] as the implicit main (module-level) frame. Module // code runs in frame 0; without callDepth >= 1 the error dispatcher's // unwind never inspects that frame, so a module-level ON ERROR GOTO never @@ -11759,6 +12031,32 @@ static BasVmResultE raiseError(BasVmT *vm, int32_t errNum) { } +// A UserControl's RaiseEvent: the event host gets the argument values +// (references read), and what it leaves in a ByRef parameter's place is +// stored through the reference the code passed. +static BasVmResultE raiseToHost(BasVmT *vm, BasObjectT *obj, int32_t eventIdx, const BasValueT *args, int32_t argc) { + BasValueT vals[BAS_VM_MAX_CALL_ARGS]; + BasVmResultE rc = BAS_VM_OK; + + for (int32_t i = 0; i < argc; i++) { + vals[i] = basValCopy(args[i]); + derefArg(&vals[i]); + } + + vm->eventHostFn(vm->eventHostCtx, obj, eventIdx, vals, argc); + + for (int32_t i = 0; i < argc; i++) { + if (args[i].type == BAS_TYPE_REF && rc == BAS_VM_OK) { + rc = refStore(vm, &args[i], vals[i]); + } else { + basValRelease(&vals[i]); + } + } + + return rc; +} + + // INPUT #: reads one field the way WRITE # wrote it -- leading blanks are // skipped, a quoted field runs to the closing quote, an unquoted field runs // to the next comma or end of line, and the field's delimiter (comma or @@ -12646,7 +12944,6 @@ fastStore: fastLoadArray: { int32_t dims = ip[1]; - int32_t flat; if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS || sp - vm->stack <= dims) { goto fastFallback; @@ -12654,7 +12951,9 @@ fastLoadArray: { a = sp - 1 - dims; - if ((flat = fastElement(a, dims)) < 0) { + int32_t flat = fastElement(a, dims); + + if (flat < 0) { goto fastFallback; } @@ -12672,7 +12971,6 @@ fastLoadArray: { fastStoreArray: { int32_t dims = ip[1]; - int32_t flat; if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS || sp - vm->stack <= dims + 1 || !fastHeld(&sp[-1])) { goto fastFallback; @@ -12680,7 +12978,9 @@ fastStoreArray: { a = sp - 2 - dims; - if ((flat = fastElement(a, dims)) < 0) { + int32_t flat = fastElement(a, dims); + + if (flat < 0) { goto fastFallback; } @@ -12701,7 +13001,13 @@ fastStoreArray: { } fastLoadField: - if (sp <= vm->stack || !(slot = fastField(&sp[-1], fetchU16(ip + 1)))) { + if (sp <= vm->stack) { + goto fastFallback; + } + + slot = fastField(&sp[-1], fetchU16(ip + 1)); + + if (!slot) { goto fastFallback; } @@ -12711,7 +13017,13 @@ fastLoadField: FAST_NEXT(); fastStoreField: - if (sp - vm->stack < 2 || !fastHeld(&sp[-1]) || !(slot = fastField(&sp[-2], fetchU16(ip + 1))) || !fastDroppable(slot)) { + if (sp - vm->stack < 2 || !fastHeld(&sp[-1])) { + goto fastFallback; + } + + slot = fastField(&sp[-2], fetchU16(ip + 1)); + + if (!slot || !fastDroppable(slot)) { goto fastFallback; } @@ -13440,12 +13752,10 @@ static bool sendSpool(BasVmT *vm, BasFileChannelT *ch) { FILE *fp = (FILE *)ch->handle; char in[BAS_SPOOL_CHUNK]; char out[BAS_SPOOL_CHUNK * BAS_PRINTER_NEWLINE_LEN]; - bool ok = true; + bool ok = fseek(fp, 0, SEEK_SET) == 0; // from the start of the spool; a failed seek fails the job bool prevCr = false; size_t n; - rewind(fp); - while (ok && (n = fread(in, 1, sizeof(in), fp)) > 0) { int32_t len = 0; @@ -13734,6 +14044,12 @@ static void sweepObjects(BasVmT *vm) { // TypeName of a value of a non-object type (typeId names a TYPE). static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId) { + const char *scalar = basValScalarTypeName(type); + + if (scalar) { + return scalar; + } + switch (type) { case BAS_TYPE_EMPTY: return "Empty"; @@ -13741,33 +14057,6 @@ static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId) { case BAS_TYPE_NULL: return "Null"; - case BAS_TYPE_INTEGER: - return "Integer"; - - case BAS_TYPE_BYTE: - return "Byte"; - - case BAS_TYPE_LONG: - return "Long"; - - case BAS_TYPE_CURRENCY: - return "Currency"; - - case BAS_TYPE_SINGLE: - return "Single"; - - case BAS_TYPE_DOUBLE: - return "Double"; - - case BAS_TYPE_DATE: - return "Date"; - - case BAS_TYPE_STRING: - return "String"; - - case BAS_TYPE_BOOLEAN: - return "Boolean"; - case BAS_TYPE_VARIANT: return "Variant"; diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h index 31e7e86b..7187e97b 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.h +++ b/src/apps/kpunch/dvxbasic/runtime/vm.h @@ -39,6 +39,7 @@ #define DVXBASIC_VM_H #include "values.h" +#include "../compiler/basEvents.h" #include "../compiler/opcodes.h" #include "dvxTypes.h" #include "dvxPlat.h" @@ -133,6 +134,15 @@ typedef bool (*BasDoEventsFnT)(void *ctx); // (highlight current line, update locals/call stack) and return. typedef void (*BasBreakpointFnT)(void *ctx, int32_t line); +// RaiseEvent in the code of a UserControl class (BAS_CLASS_USERCONTROL): +// after the VM has queued its WithEvents handlers it hands the event to +// the host (the form runtime's control bridge), which passes it to the +// widget's container. obj is the raising object, eventIdx its class's +// event number, args the argument values (argc of them, the caller's +// copies); the host may change them, and what it leaves in a ByRef +// parameter's place is stored back into the variable the code passed. +typedef void (*BasEventHostFnT)(void *ctx, const BasObjectT *obj, int32_t eventIdx, BasValueT *args, int32_t argc); + // ============================================================ // UI callbacks (host-provided, for form/control system) // ============================================================ @@ -185,6 +195,11 @@ typedef BasStringT *(*BasUiInputBoxFnT)(void *ctx, const char *prompt, const cha // Create a form programmatically (no .frm file). Returns form reference. typedef void *(*BasUiCreateFormFnT)(void *ctx, const char *formName, int32_t width, int32_t height); +// New FormName: load a new copy of a form of the program (its own +// window, controls, menus and form-level variables; Load fires). Returns +// the copy's form reference, NULL when it could not be made. +typedef void *(*BasUiNewFormFnT)(void *ctx, const char *formName); + // Create a control with an explicit parent container. parentRef may be NULL // to use the form's root content box. typedef void *(*BasUiCreateCtrlExFnT)(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef); @@ -212,6 +227,10 @@ typedef const char *(*BasUiDefaultPropFnT)(void *ctx, void *objRef); // cannot be read. typedef void *(*BasUiLoadPictureFnT)(void *ctx, const char *path, int32_t *outErr); +// LoadFormText(text): a form loaded from .frm text (its own copy when a +// form of its name is loaded), NULL when the text does not load. +typedef void *(*BasUiLoadFormTextFnT)(void *ctx, const char *text); + // Collected UI callbacks typedef struct { BasUiGetPropFnT getProp; @@ -227,12 +246,14 @@ typedef struct { BasUiMsgBoxFnT msgBox; BasUiInputBoxFnT inputBox; BasUiCreateFormFnT createForm; + BasUiNewFormFnT newForm; BasUiCreateCtrlExFnT createCtrlEx; BasUiSetEventFnT setEvent; BasUiRemoveCtrlFnT removeCtrl; BasUiTypeNameFnT typeName; BasUiLoadPictureFnT loadPicture; BasUiDefaultPropFnT defaultProp; + BasUiLoadFormTextFnT loadFormText; void *ctx; // passed as first arg to all callbacks } BasUiCallbacksT; @@ -359,8 +380,10 @@ typedef void *(*BasResolveExternFnT)(void *ctx, const char *libName, const char // retType is the expected return type (BAS_TYPE_*). // libName is the DECLARE LIBRARY name (e.g. "dvx"); the host may use // it to inject hidden parameters (e.g. AppContextT for "dvx" library). +// binaryMask marks the BINARY STRING arguments (bit i) and result +// (BAS_EXTERN_BINARY_RET): those pass as pointer + length. // Returns the native function's return value as a BasValueT. -typedef BasValueT (*BasCallExternFnT)(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); +typedef BasValueT (*BasCallExternFnT)(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask); typedef struct { BasResolveExternFnT resolveExtern; @@ -526,6 +549,12 @@ typedef enum { // compiler's "Class.Member" symbols). #define BAS_MEMBER_QUALIFIER "." +// Accesses of an object member: a read (or a call), or an assignment +// made with Let (obj.p = v) or Set (Set obj.p = v). +#define BAS_MEMBER_OP_GET 0 +#define BAS_MEMBER_OP_LET 1 +#define BAS_MEMBER_OP_SET 2 + // BasClassMemberT flags. #define BAS_MEMBER_DEFAULT 0x01 // the default member (Attribute Name.VB_UserMemId = 0) #define BAS_MEMBER_PARAMARRAY 0x02 // the last parameter is a ParamArray @@ -540,11 +569,30 @@ typedef enum { _Static_assert(BAS_VM_MAX_CALL_ARGS <= BAS_MEMBER_PARAM_BITS, "defaultParams needs a bit per parameter"); +// BasClassMemberT.enumIdx of a member whose value is no Enum. +#define BAS_ENUM_NONE (-1) + +// One member of an Enum: its name as declared (without the Enum's name) +// and its value. +typedef struct { + char name[BAS_MAX_IDENT]; + int32_t value; +} BasEnumMemberT; + +// An Enum a UserControl property is declared As (BasClassMemberT.enumIdx): +// its members in declaration order, so a host can show and store the +// property by member name. Part of the module format. +typedef struct { + BasEnumMemberT *members; // malloc'd, at least one + int32_t memberCount; +} BasEnumInfoT; + // A public member: found by its index when the compiler knew the // object's class, by name when a program uses obj.Name late bound. typedef struct { char name[BAS_MAX_IDENT]; int32_t index; // BAS_MEMBER_VAR: member variable slot; else index into procs[] + int32_t enumIdx; // a UserControl's property (variable, Get, Let, Set): index into the module's enums of the Enum its value is declared As, else BAS_ENUM_NONE uint8_t kind; // BasMemberKindE uint8_t flags; // BAS_MEMBER_* uint8_t dataType; // BAS_MEMBER_VAR: declared type (stores convert to it) @@ -576,6 +624,21 @@ typedef struct { int32_t mapLen; // the interface's member count * BAS_IMPL_WAYS } BasImplementsT; +// An Event a class declares; RaiseEvent's operand is its index. Part of +// the module format (a UserControl's events reach the widget's host by +// these names and parameters). +typedef struct { + char name[BAS_MAX_IDENT]; + int32_t paramCount; + uint8_t paramTypes[BAS_VM_MAX_CALL_ARGS]; // BAS_TYPE_* (BAS_TYPE_OBJECT for an object parameter) + uint32_t byValParams; // bit i: parameter i is ByVal + char *paramNames; // the parameters' names, comma-separated, malloc'd; NULL: none +} BasClassEventT; + +// BasClassInfoT.flags. +#define BAS_CLASS_USERCONTROL 0x01 // a UserControl (.ctl): a widget class written in BASIC +#define BAS_CLASS_FLAGS_KNOWN BAS_CLASS_USERCONTROL + typedef struct { char name[BAS_MAX_IDENT]; int32_t varCount; // member variables per instance @@ -585,6 +648,11 @@ typedef struct { int32_t memberCount; BasImplementsT *implements; // interfaces it Implements (malloc'd) int32_t implementsCount; + BasClassEventT *events; // the Events it declares (malloc'd) + int32_t eventCount; + uint8_t flags; // BAS_CLASS_* + int32_t ucSlot; // BAS_CLASS_USERCONTROL: member variable holding the UserControl object, else -1 + int32_t ucProcs[BAS_UC_EVT_COUNT]; // BAS_CLASS_USERCONTROL: index into procs[] of UserControl_ (BasUcEventE), -1 = none } BasClassInfoT; // The handler of one event for a WithEvents variable. Part of the module @@ -657,6 +725,8 @@ typedef struct { int32_t lineNumberCount; BasClassInfoT *classes; // class modules (runtime-essential) int32_t classCount; + BasEnumInfoT *enums; // the Enums UserControl properties are declared As (survives stripping) + int32_t enumCount; BasWithEventsT *withEvents; // WithEvents variables (runtime-essential) int32_t withEventsCount; BasDebugVarT *debugVars; // variable names for debugger @@ -768,6 +838,8 @@ typedef struct { void *doEventsCtx; BasBreakpointFnT breakpointFn; void *breakpointCtx; + BasEventHostFnT eventHostFn; // UserControl events to the host (NULL: none) + void *eventHostCtx; // UI callbacks (set by host for form/control support) BasUiCallbacksT ui; @@ -863,6 +935,9 @@ BasVmResultE basVmStep(BasVmT *vm); // False when the copy cannot be allocated (COMMAND$ is then empty). bool basVmSetCommand(BasVmT *vm, const char *args); +// Set App.Path, App.Config and App.Data (copied, cut to DVX_MAX_PATH). +void basVmSetAppPaths(BasVmT *vm, const char *path, const char *config, const char *data); + // Give the host a turn: show what changed on the program screen, then // the DoEvents callback. False when the program must stop (the host said // so, or basVmRequestStop). @@ -908,6 +983,13 @@ bool basVmFinish(BasVmT *vm); // procedures: ByRef parameters, ParamArray arguments, pending arguments). void basVmForgetForm(BasVmT *vm, void *formRef, BasValueT *formVars, int32_t formVarCount); +// A new empty Collection object owned by vm (the host's Form.Controls), +// and item appended to one under key (NULL: none; basVmCollectionAdd +// returns 0 or the BASIC error number of a key in use). basValInstance +// of the new Collection takes its one reference. +int32_t basVmCollectionAdd(BasObjectT *coll, BasValueT item, const char *key); +BasObjectT *basVmCollectionNew(BasVmT *vm); + // Set the current form context (called by host during event dispatch). void basVmSetCurrentForm(BasVmT *vm, void *formRef); void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count); @@ -944,6 +1026,31 @@ void basVmStepOut(BasVmT *vm); // Run to cursor: break when reaching the specified source line. void basVmRunToCursor(BasVmT *vm, int32_t line); +// Cancel a pending step / run-to-cursor request. +void basVmClearStepState(BasVmT *vm); + +// The source line of the last OP_LINE executed (0: none yet). +int32_t basVmGetCurrentLine(const BasVmT *vm); + +// The source line where the current runtime error was raised (0: none). +int32_t basVmGetErrorLine(const BasVmT *vm); + +// Leave break mode: event handlers may run again and a handler paused +// at a breakpoint carries on (its pause loop ends). +void basVmResume(BasVmT *vm); + +// The host's pause notification: fn(ctx, line) when a breakpoint or step +// pauses inside a SUB the host called (line -1 once that SUB returns). +void basVmSetBreakpointCallback(BasVmT *vm, BasBreakpointFnT fn, void *ctx); + +// Break mode on or off: while on, basVmCallSub refuses to run handlers. +void basVmSetDebugPaused(BasVmT *vm, bool paused); + +// Stop a program under the debugger: no step request or breakpoint may +// pause it again (breakpoints are detached), break mode ends and the +// running code unwinds. +void basVmStop(BasVmT *vm); + // Call a SUB by code address from the host. // Pushes a call frame, runs until the SUB returns, then restores // the previous execution state. Returns true if the SUB was called @@ -957,4 +1064,31 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i // returns, also after an error. bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount); +// ---- Class objects from the host (the UserControl bridge) ---- + +// A new instance of class classIdx, set up as New sets one up (member +// variables, then Class_Initialize), run to completion. The caller owns +// the one reference returned (basObjectUnref). NULL when the class does +// not exist, the VM is paused in the debugger or the set-up code failed +// (the VM's error is set then). +BasObjectT *basVmNewObject(BasVmT *vm, int32_t classIdx); + +// obj.name as late-bound code would use it, run to completion: op +// BAS_MEMBER_OP_GET reads or calls it (a Sub leaves Empty) with the argc +// arguments in args; BAS_MEMBER_OP_LET / _SET assign args[argc - 1] with +// the arguments before it. args stay the caller's (they are copied). +// *result (may be NULL) receives the value (Empty for a Sub or an +// assignment); the caller releases it. False on an error (no such +// member, wrong arguments, an untrapped runtime error in the code: the +// VM's error is set) or when the VM is paused in the debugger. +bool basVmCallMember(BasVmT *vm, BasObjectT *obj, const char *name, int32_t op, const BasValueT *args, int32_t argc, BasValueT *result); + +// Run procedure procIdx (a SUB) with Me = obj (NULL: none), as +// basVmCallSubWithArgsOut runs one: its first outCount parameters are +// passed by reference to outArgs. +bool basVmCallProc(BasVmT *vm, BasObjectT *obj, int32_t procIdx, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount); + +// Set the UserControl event host (see BasEventHostFnT). +void basVmSetEventHost(BasVmT *vm, BasEventHostFnT fn, void *ctx); + #endif // DVXBASIC_VM_H diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c index 3fb4c79a..4f74edfe 100644 --- a/src/apps/kpunch/dvxbasic/stub/bascomp.c +++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c @@ -22,16 +22,17 @@ // bascomp.c -- DVX BASIC command-line compiler // -// Compiles a .dbp project into a standalone .app file. +// Compiles a .dbp project into a standalone .app file, or a control +// project ([Project] Type = Control) into a control file (.wgt) that +// installs its UserControls for every program. // -// Usage: BASCOMP project.dbp [-o output.app] [-release] +// Usage: BASCOMP project.dbp [-o output.app|output.wgt] [-release] // // The project file and all referenced source files (.bas, .frm) // are loaded relative to the directory containing the .dbp file. // The app stub is the STUB resource embedded in this executable, and // the fallback icon its NOICON resource. -#include "../compiler/compiler.h" #include "../runtime/vm.h" #include "../../../../libs/kpunch/libdvx/dvxPrefs.h" #include "../../../../libs/kpunch/libdvx/dvxTypes.h" @@ -82,7 +83,7 @@ static void usage(void) { fprintf(stderr, "DVX BASIC Compiler\n\n"); fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n"); fprintf(stderr, " project.dbp DVX BASIC project file\n"); - fprintf(stderr, " -o output.app Output file (default: project name + .app)\n"); + fprintf(stderr, " -o output.app Output file (default: project name + .app, or + .wgt for a control project)\n"); fprintf(stderr, " -release Strip debug information\n"); } @@ -128,28 +129,28 @@ int main(int argc, char **argv) { // Read project metadata. prefsGetString returns "" for a present but // empty Name=, which is treated like a missing one. - const char *projName = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, ""); - 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 *version = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, ""); - const char *copyright = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, ""); - const char *description = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, ""); - 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, ""); + BasBuildSpecT spec; + + memset(&spec, 0, sizeof(spec)); + basBuildSpecFromProject(prefs, projectDir, &spec); + + const char *projName = spec.projName; + bool control = basBuildIsControlProject(prefs); + const char *ext = control ? BAS_WGT_EXT : BAS_APP_EXT; // Derive output path: the project Name, else the .dbp file's own - // base name (never a bare ".app"). + // base name (never a bare ".app" / ".wgt"). char outBuf[DVX_MAX_PATH]; if (!outputPath) { if (projName[0]) { - snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName); + snprintf(outBuf, sizeof(outBuf), "%s/%s%s", projectDir, projName, ext); } 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); + snprintf(outBuf, sizeof(outBuf), "%s/%.*s%s", projectDir, (int)baseLen, base, ext); } outputPath = outBuf; @@ -177,60 +178,39 @@ int main(int argc, char **argv) { // Compile printf("Compiling %d file(s)...\n", (int)fileCount); - BasCompilerT *compiler = basCompilerCreate(project.source, project.sourceLen); + BasCompileErrorT err; + BasModuleT *mod = basBuildCompile(project.source, project.sourceLen, project.optionExplicit, NULL, NULL, &err); - if (!compiler) { - fprintf(stderr, "Error: out of memory.\n"); - goto failProject; - } - - basCompilerSetOptionExplicit(compiler, project.optionExplicit); - - if (!basCompile(compiler)) { + if (!mod) { const char *errPath = NULL; int32_t errLine = 0; - if (basBuildMapLine(project.map, basCompilerErrorLine(compiler), &errPath, &errLine)) { - fprintf(stderr, "Compile error at %s:%d: %s\n", errPath, (int)errLine, basCompilerError(compiler)); + if (err.result != BAS_COMPILE_ERROR) { + fprintf(stderr, "Error: %s\n", err.message); + } else if (basBuildMapLine(project.map, err.line, &errPath, &errLine)) { + fprintf(stderr, "Compile error at %s:%d: %s\n", errPath, (int)errLine, err.message); } else { - fprintf(stderr, "Compile error at line %d: %s\n", (int)basCompilerErrorLine(compiler), basCompilerError(compiler)); + fprintf(stderr, "Compile error at line %d: %s\n", (int)err.line, err.message); } - basCompilerDestroy(compiler); - goto failProject; - } - - BasModuleT *mod = basCompilerBuildModule(compiler); - basCompilerDestroy(compiler); - - if (!mod) { - fprintf(stderr, "Error: failed to build module.\n"); goto failProject; } printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount); - BasBuildSpecT spec; - memset(&spec, 0, sizeof(spec)); - spec.projName = projName; - spec.author = author; - spec.publisher = publisher; - spec.version = version; - spec.copyright = copyright; - spec.description = description; - spec.projectDir = projectDir; - spec.iconPath = iconPath; - spec.helpFile = helpFile; - spec.selfPath = selfPath(argv[0]); - spec.module = mod; // Raw .frm texts as concatenation read them; the shared build // 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.log = buildLog; + spec.projName = projName; + spec.selfPath = selfPath(argv[0]); + spec.module = mod; + spec.frmSources = (const char *const *)project.frmTexts; + spec.frmCount = (int32_t)arrlen(project.frmTexts); + spec.ctlSources = (const char *const *)project.ctlTexts; + spec.ctlCount = (int32_t)arrlen(project.ctlTexts); + spec.release = release; + spec.log = buildLog; - const char *failure = basBuildApp(outputPath, &spec); + const char *failure = control ? basBuildControl(outputPath, &spec) : basBuildApp(outputPath, &spec); basModuleFree(mod); basBuildFreeProject(&project); diff --git a/src/apps/kpunch/dvxbasic/stub/basrun.c b/src/apps/kpunch/dvxbasic/stub/basrun.c index 0b850945..5ee74a16 100644 --- a/src/apps/kpunch/dvxbasic/stub/basrun.c +++ b/src/apps/kpunch/dvxbasic/stub/basrun.c @@ -284,9 +284,7 @@ static int32_t runModule(BasModuleT *mod, const OptionsT *opt) { basBuildDirOf(opt->program, dir, sizeof(dir)); const char *dataDir = opt->dataDir ? opt->dataDir : dir; - snprintf(vm->appPath, sizeof(vm->appPath), "%s", dir); - snprintf(vm->appConfig, sizeof(vm->appConfig), "%s", dataDir); - snprintf(vm->appData, sizeof(vm->appData), "%s", dataDir); + basVmSetAppPaths(vm, dir, dataDir, dataDir); basVmSetCommand(vm, opt->args); // Run in slices so a --steps cap can be enforced across the whole run. diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c index 3c2a7373..ce2a35ad 100644 --- a/src/apps/kpunch/dvxbasic/stub/basstub.c +++ b/src/apps/kpunch/dvxbasic/stub/basstub.c @@ -39,6 +39,7 @@ #include "../basRes.h" #include "../runtime/vm.h" #include "../formrt/formrt.h" +#include "../formrt/basRunHost.h" #include "basModuleRes.h" #include @@ -74,6 +75,7 @@ void appShutdown(void); static bool stubDoEvents(void *ctx); static void stubHelpQuery(void *ctx); static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize); +static void stubNewArgs(void *ctx, const char *args); static void stubPrint(void *ctx, const char *text, bool newline); int32_t appMain(DxeAppContextT *ctx); @@ -121,6 +123,15 @@ static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) } +// The shell hands the running program new launch arguments (F1 with a +// program installed as the Help Viewer, see shellOpenHelp): COMMAND$ and +// NewArgs (basFormRtNewArgs). +static void stubNewArgs(void *ctx, const char *args) { + (void)ctx; + basFormRtNewArgs(sRt, args); +} + + static void stubPrint(void *ctx, const char *text, bool newline) { (void)ctx; (void)text; @@ -155,6 +166,16 @@ int32_t appMain(DxeAppContextT *ctx) { free(appName); } + // [Settings] MultiInstance: the shell reads this running copy's + // descriptor when the program is started again, and then runs another. + uint32_t multiSize = 0; + void *multiFlag = dvxResRead(res, BAS_RES_MULTIINSTANCE, &multiSize); + + if (multiFlag) { + appDescriptor.multiInstance = true; + free(multiFlag); + } + // Set help file path if present uint32_t helpNameSize = 0; char *helpName = (char *)dvxResRead(res, BAS_RES_HELPFILE, &helpNameSize); @@ -174,70 +195,42 @@ int32_t appMain(DxeAppContextT *ctx) { return 1; } - // Create VM - BasVmT *vm = basVmCreate(); + // The VM and form runtime. App.Path is the .app's directory + // (read-only on CD); App.Config and App.Data are writable + // subdirectories made on demand -- the IDE runs projects the same way. + // The program's screen window (console / graphics) carries its name; + // COMMAND$ is what it was started with. + ShellAppT *self = shellGetApp(ctx->appId); + const char *hostError = NULL; + BasRunSpecT spec; - if (!vm) { - dvxMessageBox(sAc, "Error", "Out of memory creating the BASIC runtime.", 0); + memset(&spec, 0, sizeof(spec)); + spec.appDir = ctx->appDir; + spec.args = ctx->args; + spec.title = self ? self->name : NULL; + spec.printFn = stubPrint; + spec.inputFn = stubInput; + spec.doEventsFn = stubDoEvents; + + BasFormRtT *rt = basRunHostCreate(sAc, mod, &spec, &hostError); + + if (!rt) { + dvxMessageBox(sAc, "Error", hostError, 0); basModuleFree(mod); dvxResClose(res); return 1; } - 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); + if (!rt->vm->module) { + dvxMessageBox(sAc, "Error", basVmGetError(rt->vm), 0); + basRunHostDestroy(rt); basModuleFree(mod); dvxResClose(res); return 1; } - basVmSetPrintCallback(vm, stubPrint, NULL); - basVmSetInputCallback(vm, stubInput, NULL); - basVmSetDoEventsCallback(vm, stubDoEvents, NULL); - - // Set app paths. App.Path is the .app's directory (read-only on CD); - // App.Config and App.Data are writable subdirectories created on - // demand. The IDE does the same split for project debugging, so - // behavior matches between compiled apps and in-IDE runs. - snprintf(vm->appPath, sizeof(vm->appPath), "%s", ctx->appDir); - snprintf(vm->appConfig, sizeof(vm->appConfig), "%s" DVX_PATH_SEP "CONFIG", ctx->appDir); - snprintf(vm->appData, sizeof(vm->appData), "%s" DVX_PATH_SEP "DATA", ctx->appDir); - platformMkdirRecursive(vm->appConfig); - platformMkdirRecursive(vm->appData); - - // COMMAND$: the arguments the program was started with - basVmSetCommand(vm, ctx->args); - - // Set extern call callbacks (required for DECLARE LIBRARY functions) - BasExternCallbacksT extCb; - memset(&extCb, 0, sizeof(extCb)); - extCb.resolveExtern = basExternResolve; - extCb.callExtern = basExternCall; - basVmSetExternCallbacks(vm, &extCb); - - // Create form runtime - BasFormRtT *rt = basFormRtCreate(sAc, vm, mod); - - if (!rt) { - dvxMessageBox(sAc, "Error", "Out of memory creating the form runtime.", 0); - basVmDestroy(vm); - basModuleFree(mod); - dvxResClose(res); - return 1; - } - - // The program's screen window (console / graphics) carries its name - ShellAppT *self = shellGetApp(ctx->appId); - - if (self) { - snprintf(rt->title, sizeof(rt->title), "%s", self->name); - } - // Register .frm source text for lazy loading for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) { char resName[BAS_RES_FORM_NAME_LEN]; @@ -266,16 +259,26 @@ int32_t appMain(DxeAppContextT *ctx) { frmText = frmTextZ; frmText[frmSize] = '\0'; + basRunHostRegisterFrm(rt, frmText, (int32_t)frmSize); + free(frmText); + } - // Extract form name from "Begin Form " line. - char frmName[BAS_MAX_IDENT] = ""; - basExtractFormName(frmText, frmName, BAS_MAX_IDENT); + // The program's own UserControls (CTLn), registered before any form + // loads so its .frm files can use them; they go when it ends + for (int32_t i = 0; i < BAS_MAX_CTL_RESOURCES; i++) { + char resName[BAS_RES_FORM_NAME_LEN]; + uint32_t ctlSize = 0; - if (frmName[0]) { - basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize); + snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); + + char *ctlText = (char *)dvxResRead(res, resName, &ctlSize); + + if (!ctlText) { + break; } - free(frmText); + basFormRtRegisterCtl(rt, ctlText, (int32_t)ctlSize); + free(ctlText); } dvxResClose(res); @@ -286,7 +289,14 @@ int32_t appMain(DxeAppContextT *ctx) { ctx->helpQueryCtx = ctx; // Load all cached forms and show the startup form - basFormRtLoadAllForms(rt, NULL); + basRunHostLoadForms(rt, NULL); + + // A program with a NewArgs handler takes new launch arguments while it + // runs; without one the shell starts another copy instead. + if (basFormRtWantsNewArgs(rt)) { + ctx->onNewArgs = stubNewArgs; + ctx->newArgsCtx = ctx; + } // Run bytecode + VB-style event loop basFormRtRunSimple(rt); @@ -294,9 +304,10 @@ int32_t appMain(DxeAppContextT *ctx) { // Cleanup ctx->onHelpQuery = NULL; ctx->helpQueryCtx = NULL; + ctx->onNewArgs = NULL; + ctx->newArgsCtx = NULL; sRt = NULL; - basFormRtDestroy(rt); - basVmDestroy(vm); + basRunHostDestroy(rt); basModuleFree(mod); return 0; diff --git a/src/apps/kpunch/dvxbasic/test_compact.c b/src/apps/kpunch/dvxbasic/test_compact.c index e12f0bf3..3698e829 100644 --- a/src/apps/kpunch/dvxbasic/test_compact.c +++ b/src/apps/kpunch/dvxbasic/test_compact.c @@ -29,6 +29,7 @@ #include "compiler/compiler.h" #include "compiler/strip.h" #include "compiler/compact.h" +#include "runtime/serialize.h" #include "runtime/vm.h" #include "compiler/opcodes.h" #include "runtime/values.h" @@ -56,6 +57,7 @@ static bool runAndCapture(const char *source, bool compact, CaptureT *cap static void testCompact(const char *name, const char *source); static void testCraftedTables(void); static void testOperandKinds(void); +static void testStripHandlers(void); static int32_t sTotal = 0; @@ -70,7 +72,7 @@ static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeL int32_t removed = basCompactBytecode(mod); if (removed != 0 || mod->codeLen != codeLen || memcmp(mod->code, code, codeLen) != 0) { - printf("FAIL: %s (removed %d)\n", name, (int32_t)removed); + printf("FAIL: %s (removed %d)\n", name, (int)removed); sFailed++; } else { printf("PASS: %s\n", name); @@ -244,6 +246,7 @@ static void testOperandKinds(void) { case BAS_OPERAND_U8_U16: case BAS_OPERAND_I32: case BAS_OPERAND_MEMBER: + case BAS_OPERAND_CALL: case BAS_OPERAND_FOR: case BAS_OPERAND_CALL_EXTERN: case BAS_OPERAND_F64: @@ -251,7 +254,7 @@ static void testOperandKinds(void) { break; default: - printf("FAIL: opcode 0x%02X reports operand size %d\n", (int32_t)op, (int32_t)size); + printf("FAIL: opcode 0x%02X reports operand size %d\n", (int)op, (int)size); ok = false; break; } @@ -272,10 +275,46 @@ static void testOperandKinds(void) { +// Release stripping keeps the names the form runtime looks up: every +// "_" procedure of a form (any event, a UserControl +// raises its own) and a module's procedures named after a master-list +// event; the rest become F1, F2 ... +static void testStripHandlers(void) { + static const char *source = + "BEGINFORM \"F\" \"IconView1\"\n" + "SUB IconView1_ItemOpen(i AS INTEGER)\n" + "END SUB\n" + "ENDFORM\n" + "SUB Plain_Helper()\n" + "END SUB\n" + "SUB Mod_Click()\n" + "END SUB\n"; + + sTotal++; + + BasCompilerT *compiler = basCompilerCreate(source, (int32_t)strlen(source)); + BasModuleT *mod = basCompile(compiler) ? basCompilerBuildModule(compiler) : NULL; + bool ok = mod != NULL; + + basCompilerDestroy(compiler); + + if (mod) { + basStripModule(mod); + ok = basModuleFindProc(mod, "IconView1_ItemOpen") != NULL && basModuleFindProc(mod, "Mod_Click") != NULL && basModuleFindProc(mod, "Plain_Helper") == NULL; + basModuleFree(mod); + } + + printf("%s: strip keeps a form's handlers of any event\n", ok ? "PASS" : "FAIL"); + sFailed += ok ? 0 : 1; +} + + int main(void) { printf("DVX BASIC Bytecode Compaction Tests\n"); printf("====================================\n\n"); + testStripHandlers(); + // ---- Basic control flow ---- testCompact("FOR loop", diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c index 734e44b5..9325f838 100644 --- a/src/apps/kpunch/dvxbasic/test_compiler.c +++ b/src/apps/kpunch/dvxbasic/test_compiler.c @@ -28,12 +28,14 @@ #include "runtime/vm.h" #include "runtime/values.h" #include "runtime/screen.h" +#include "runtime/serialize.h" #include "testCapture.h" // The DVX 8x16 font, so screen text draws real glyphs #define DVX_FONT_IMPL #include "dvxFont.h" +#include #include #include #include @@ -68,6 +70,9 @@ static int32_t gFailCount = 0; #define TEST_SCALE_LINE_MAX 96 #define TEST_LONG_STRING_LEN 300 // past BAS_MAX_STRING_LEN #define TEST_LONG_STRING_SRC 512 +#define TEST_NAMED_EXTRA 4 // named arguments past BAS_VM_MAX_CALL_ARGS +#define TEST_NAMED_PAD (BAS_MAX_IDENT - 1 - 3) // zeros after "Nnn": the longest identifier +#define TEST_NAMED_SRC_LEN 2048 // a call with BAS_VM_MAX_CALL_ARGS + TEST_NAMED_EXTRA of them #define TEST_TOKEN_NAME_SWEEP 512 // covers the whole token enum #define TEST_SWAP_LINES 600 // element SWAPs sharing the temporaries #define TEST_EXTERN_REC_LEN 128 // one recorded extern call @@ -113,7 +118,7 @@ static void runProgramPlay(const char *name, const char *source, const char *exp static BasVmT *screenRun(const char *name, const char *source, CaptureT *cap, TestKeysT *keys); static void screenRunFree(BasVmT *vm); static uint8_t shownAt(const BasScreenT *scr, int32_t x, int32_t y); -static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); +static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask); static bool stubIsMethodValid(void *ctx, const char *wgtType, const char *methodName); static bool stubIsPropValid(void *ctx, const char *wgtType, const char *propName); static const char *stubLookupCtrlType(void *ctx, const char *ctrlName); @@ -141,6 +146,8 @@ static void testReferences(void); static void testScreenModesPages(void); static void testStringsAndDates(void); static void testTokenNames(void); +static void testUserControl(void); +static void ucEventHost(void *ctx, const BasObjectT *obj, int32_t eventIdx, BasValueT *args, int32_t argc); // Headless stand-in for the form runtime: MsgBox answers OK so programs // that show one still run to completion (every other form/control @@ -162,7 +169,7 @@ static double bestCompileSeconds(int32_t procs) { int32_t len = snprintf(src, TEST_SCALE_LINE_MAX, "PRINT F%d(1)\n", (int32_t)(procs - 1)); for (int32_t i = 0; i < procs; i++) { - len += snprintf(src + len, TEST_SCALE_LINE_MAX, "FUNCTION F%d(BYVAL a AS LONG) AS LONG\nF%d = a%s\nEND FUNCTION\n", (int32_t)i, (int32_t)i, (i > 0) ? " + 1" : ""); + len += snprintf(src + len, TEST_SCALE_LINE_MAX, "FUNCTION F%d(BYVAL a AS LONG) AS LONG\nF%d = a%s\nEND FUNCTION\n", (int)i, (int)i, (i > 0) ? " + 1" : ""); } double best = -1.0; @@ -570,8 +577,9 @@ static void runProgramPlay(const char *name, const char *source, const char *exp // Compile and run a program that uses the program screen; the VM is -// returned for the checks (screenRunFree ends it). keys (optional) are -// typed at the first DoEvents. +// returned for the checks (screenRunFree ends it), NULL when the program +// did not compile or made no screen. keys (optional) are typed at the +// first DoEvents. static BasVmT *screenRun(const char *name, const char *source, CaptureT *cap, TestKeysT *keys) { printf("=== %s ===\n", name); @@ -612,6 +620,13 @@ static BasVmT *screenRun(const char *name, const char *source, CaptureT *cap, Te gFailCount++; } + if (vm->screen == NULL) { + printf("FAIL: the program made no screen\n\n"); + gFailCount++; + screenRunFree(vm); + return NULL; + } + return vm; } @@ -633,7 +648,7 @@ static uint8_t shownAt(const BasScreenT *scr, int32_t x, int32_t y) { // Extern call stub: appends "Func(t,t,...)" to the capture (ctx), one // type letter per argument, and returns 0. -static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) { +static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask) { CaptureT *c = (CaptureT *)ctx; char rec[TEST_EXTERN_REC_LEN]; int32_t n = 0; @@ -665,7 +680,7 @@ static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, c break; case BAS_TYPE_STRING: - t = '$'; + t = (binaryMask & (1u << i)) ? 'b' : '$'; break; case BAS_TYPE_BOOLEAN: @@ -679,7 +694,7 @@ static BasValueT stubCallExtern(void *ctx, void *funcPtr, const char *libName, c n += snprintf(rec + n, sizeof(rec) - n, "%s%c", i > 0 ? "," : "", t); } - snprintf(rec + n, sizeof(rec) - n, ")"); + snprintf(rec + n, sizeof(rec) - n, ")%s", (binaryMask & BAS_EXTERN_BINARY_RET) ? "b" : ""); captureAppend(c, rec); return basValInteger(0); } @@ -1480,7 +1495,7 @@ static void testCompilerFixes(void) { n += snprintf(src + n, TEST_LIMIT_SRC_CAP - n, "SWAP a(1), a(2)\n"); } - n += snprintf(src + n, TEST_LIMIT_SRC_CAP - n, "PRINT a(1); a(2)\n"); + snprintf(src + n, TEST_LIMIT_SRC_CAP - n, "PRINT a(1); a(2)\n"); runProgram("Compiler temporaries are reused", src, "1 0 \n"); free(src); @@ -1494,6 +1509,17 @@ static void testCompilerFixes(void) { "G\n", "F(I,D,$,S,L,B) G(S)" ); + + // BINARY STRING parameters and results reach the host flagged + runExternProgram("Extern BINARY STRING parameters and result flagged", + "DECLARE LIBRARY \"x\"\n" + " DECLARE FUNCTION H$(BYVAL a AS BINARY STRING, BYVAL b AS LONG, BYVAL c AS STRING, BYVAL d AS BINARY STRING) AS BINARY STRING\n" + " DECLARE FUNCTION K(BYVAL a AS STRING) AS BINARY STRING\n" + "END DECLARE\n" + "s$ = H$(\"a\" + CHR$(0), 2, \"c\", 4)\n" + "s$ = K(\"z\")\n", + "H(b,L,$,b)b K($)b" + ); } @@ -1513,7 +1539,7 @@ static void testCompilerLimits(void) { // One more distinct string constant than the pool can index. for (int32_t i = 0; i < TEST_LIMIT_CONSTS; i++) { - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "A$ = \"c%d\"\n", (int32_t)i); + len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "A$ = \"c%d\"\n", (int)i); } expectCompileError("Constant pool overflow rejected", src, "Too many string constants"); @@ -1528,7 +1554,7 @@ static void testCompilerLimits(void) { for (int32_t i = 0; i < TEST_LIMIT_FORBODY; i++) { len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "L = L + 1\n"); } - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "NEXT\nGOSUB Far\nFarSub 5\nERROR 5\nPRINT L\nEND\nHandler:\nPRINT \"handled\"; ERR\nRESUME NEXT\nFar:\nL = L + %d\nRETURN\nSUB FarSub(n)\nPRINT \"far\"; n\nEND SUB\n", (int32_t)TEST_LIMIT_GOSUB_ADD); + snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "NEXT\nGOSUB Far\nFarSub 5\nERROR 5\nPRINT L\nEND\nHandler:\nPRINT \"handled\"; ERR\nRESUME NEXT\nFar:\nL = L + %d\nRETURN\nSUB FarSub(n)\nPRINT \"far\"; n\nEND SUB\n", (int32_t)TEST_LIMIT_GOSUB_ADD); char expected[TEST_LIMIT_EXPECT_LEN]; @@ -1538,7 +1564,7 @@ static void testCompilerLimits(void) { // More module-level variables than the VM has global slots. len = 0; for (int32_t i = 0; i < TEST_LIMIT_GLOBALS; i++) { - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int32_t)i); + len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int)i); } expectCompileError("Too many globals rejected", src, "Too many module-level variables"); @@ -1546,31 +1572,31 @@ static void testCompilerLimits(void) { // a runtime "Invalid local variable index". len = snprintf(src, TEST_LIMIT_SRC_CAP, "SUB S\n"); for (int32_t i = 0; i < TEST_LIMIT_LOCALS; i++) { - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int32_t)i); + len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int)i); } - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "END SUB\nS\n"); + snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "END SUB\nS\n"); expectCompileError("Too many locals rejected", src, "Too many local variables in S"); // A frame just under the limit whose statements take hidden // temporaries: the SWAP's element temps must fit too. len = snprintf(src, TEST_LIMIT_SRC_CAP, "SUB S\n"); for (int32_t i = 0; i < BAS_VM_MAX_LOCALS - 1; i++) { - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int32_t)i); + len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int)i); } - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "SWAP v0, v1\nDIM b(2, 2)\nSWAP b(1, 1), b(2, 2)\nEND SUB\nS\n"); + snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "SWAP v0, v1\nDIM b(2, 2)\nSWAP b(1, 1), b(2, 2)\nEND SUB\nS\n"); expectCompileError("Temporaries past the frame rejected", src, "Too many local variables in S"); // READ into many array elements reuses each target's temporaries, so // the SUB stays well inside its frame. len = snprintf(src, TEST_LIMIT_SRC_CAP, "S\nSUB S\nDIM a(%d) AS INTEGER\nREAD a(1)", (int32_t)TEST_LIMIT_READ_ELEMS); for (int32_t i = 2; i <= TEST_LIMIT_READ_ELEMS; i++) { - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, ", a(%d)", (int32_t)i); + len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, ", a(%d)", (int)i); } len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "\nPRINT a(1); a(%d)\nEND SUB\nDATA 1", (int32_t)TEST_LIMIT_READ_ELEMS); for (int32_t i = 2; i <= TEST_LIMIT_READ_ELEMS; i++) { - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, ", %d", (int32_t)i); + len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, ", %d", (int)i); } - len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "\n"); + snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "\n"); runProgram("READ many elements in a SUB", src, "1 40 \n"); free(src); @@ -1602,7 +1628,7 @@ static void testConsoleGraphics(void) { BasScreenT *scr = vm->screen; checkTrue("host got only the first PRINT", strcmp(cap.buf, "before\n") == 0); - checkTrue("screen made in SCREEN 0 80x25", scr && scr->mode == BAS_SCREEN_MODE_TEXT && scr->cols == 80 && scr->rows == 25 && scr->width == 640 && scr->height == 400); + checkTrue("screen made in SCREEN 0 80x25", scr->mode == BAS_SCREEN_MODE_TEXT && scr->cols == 80 && scr->rows == 25 && scr->width == 640 && scr->height == 400); rowText(scr, 0, row); checkTrue("row 1 text", strcmp(row, "HI12") == 0); rowText(scr, 1, row); @@ -2328,6 +2354,23 @@ static void testConstEnumParamArray(void) { "ParamArray is not allowed in DEF FN" ); + expectCompileError("BINARY STRING outside DECLARE LIBRARY rejected", + "SUB S(BYVAL x AS BINARY STRING)\nEND SUB\n", + "BINARY STRING is only allowed in a DECLARE LIBRARY block" + ); + + expectCompileError("BINARY STRING in DIM rejected", + "DIM x AS BINARY STRING\n", + "BINARY STRING is only allowed in a DECLARE LIBRARY block" + ); + + expectCompileError("BINARY needs STRING", + "DECLARE LIBRARY \"x\"\n" + "DECLARE SUB F(BYVAL b AS BINARY LONG)\n" + "END DECLARE\n", + "STRING after BINARY" + ); + expectCompileError("ParamArray in DECLARE LIBRARY rejected", "DECLARE LIBRARY \"x\"\n" "DECLARE SUB F(ParamArray b())\n" @@ -3915,7 +3958,7 @@ static void testFrontEnd(void) { printf("FAIL: generated program did not compile\n\n"); gFailCount++; } else if (small > 0.0 && large > small * TEST_SCALE_LIMIT) { - printf("FAIL: %d procedures took %.4fs, %d took %.4fs\n\n", (int)TEST_SCALE_PROCS, small, (int)(TEST_SCALE_PROCS * TEST_SCALE_FACTOR), large); + printf("FAIL: %d procedures took %.4fs, %d took %.4fs\n\n", TEST_SCALE_PROCS, small, TEST_SCALE_PROCS * TEST_SCALE_FACTOR, large); gFailCount++; } else { printf("OK (%.4fs, %.4fs)\n\n", small, large); @@ -4881,7 +4924,7 @@ static void testKeywordApi(void) { } if (ok) { - printf("OK (%d keywords)\n\n", (int32_t)n); + printf("OK (%d keywords)\n\n", (int)n); } else { printf("FAIL: keyword API\n\n"); gFailCount++; @@ -4898,7 +4941,7 @@ static void testLexerLimits(void) { src[len++] = 'x'; } - len += snprintf(src + len, sizeof(src) - len, "\"\n"); + snprintf(src + len, sizeof(src) - len, "\"\n"); expectCompileError("Over-long string literal rejected", src, "String literal too long"); } @@ -5736,7 +5779,34 @@ static void testObjectsAndVariants(void) { expectCompileError("NEW of a non-class rejected", "x = NEW Foo\n", - "class name (or Collection) after New" + "class name, form name or Collection after New" + ); + + // New FormName: a copy of a form of the project (headless, the + // missing form runtime raises error 1000 where the copy is made) + runProgram("NEW and AS NEW of a form name", + "BEGINFORM \"Form1\"\n" + "ENDFORM\n" + "DIM f AS Form1\n" + "ON ERROR RESUME NEXT\n" + "SET f = NEW Form1\n" + "PRINT ERR\n" + "ERR.CLEAR\n" + "MakeCopy\n" + "PRINT ERR\n" + "SUB MakeCopy\n" + " ON ERROR RESUME NEXT\n" + " DIM g AS NEW Form1\n" + " PRINT ERR;\n" + "END SUB\n", + "1000 \n1000 0 \n" + ); + + expectCompileError("AS NEW of a form name in an array rejected", + "BEGINFORM \"Form1\"\n" + "ENDFORM\n" + "DIM f(2) AS NEW Form1\n", + "New cannot be used with an array" ); expectCompileError("AS NEW needs a class", @@ -7449,6 +7519,181 @@ static void testTokenNames(void) { } +// A UserControl class (BEGINCLASS "Name" USERCONTROL): the module keeps +// its flag, the slot of its UserControl object, its UserControl_ +// procedures and its events with their parameters; the host makes +// objects, reads, writes and calls members and runs the event procedures +// with Me; RaiseEvent reaches the event host, which writes ByRef +// arguments back. +static void testUserControl(void) { + static const char *source = + "BEGINCLASS \"Gauge\" USERCONTROL\n" + "Public Event ItemOpen(ByVal Index AS INTEGER, Title AS STRING)\n" + "Public Event Ping()\n" + "Private mLevel AS DOUBLE\n" + "Public Caption AS STRING\n" + "Public Property Get Level() AS DOUBLE\n" + " Level = mLevel\n" + "End Property\n" + "Public Property Let Level(v AS DOUBLE)\n" + " mLevel = v\n" + "End Property\n" + "Public Function Open(i AS INTEGER) AS STRING\n" + " DIM n AS STRING\n" + " n = \"item\"\n" + " RaiseEvent ItemOpen(i, n)\n" + " Open = n\n" + "End Function\n" + "Private Sub UserControl_Initialize()\n" + " mLevel = 1.25\n" + " PRINT \"init\"; TypeName(UserControl)\n" + "End Sub\n" + "Private Sub UserControl_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n" + " PRINT \"down\"; Button; X\n" + "End Sub\n" + "ENDCLASS\n"; + + printf("=== UserControl: module, host calls, events ===\n"); + + BasCompilerT *compiler = basCompilerCreate(source, (int32_t)strlen(source)); + bool ok = basCompile(compiler); + BasModuleT *mod = ok ? basCompilerBuildModule(compiler) : NULL; + + if (!ok) { + printf("FAIL: compile: %s\n", basCompilerError(compiler)); + } + + basCompilerDestroy(compiler); + checkTrue("UserControl compiles", mod != NULL); + + if (mod == NULL) { + gFailCount++; + return; + } + + const BasClassInfoT *cls = &mod->classes[0]; + + checkTrue("class flag", cls->flags == BAS_CLASS_USERCONTROL); + checkTrue("UserControl slot is the first member variable", cls->ucSlot == 0); + checkTrue("Initialize and MouseDown handlers found", cls->ucProcs[BAS_UC_EVT_INITIALIZE] >= 0 && cls->ucProcs[BAS_UC_EVT_MOUSEDOWN] >= 0); + checkTrue("no Paint handler", cls->ucProcs[BAS_UC_EVT_PAINT] == -1); + checkTrue("two events", cls->eventCount == 2); + checkTrue("event parameters", cls->eventCount == 2 && strcmp(cls->events[0].name, "ItemOpen") == 0 && cls->events[0].paramCount == 2 && cls->events[0].paramTypes[0] == BAS_TYPE_INTEGER && cls->events[0].paramTypes[1] == BAS_TYPE_STRING && cls->events[0].byValParams == 1u && cls->events[0].paramNames && strcmp(cls->events[0].paramNames, "Index,Title") == 0); + checkTrue("event without parameters", cls->eventCount == 2 && cls->events[1].paramCount == 0 && cls->events[1].paramNames == NULL); + + // The module format keeps all of it + int32_t len = 0; + uint8_t *data = basModuleSerialize(mod, &len); + BasModuleT *back = data ? basModuleDeserialize(data, len) : NULL; + + free(data); + checkTrue("round trip", back != NULL && back->classes[0].flags == BAS_CLASS_USERCONTROL && back->classes[0].ucSlot == 0 && memcmp(back->classes[0].ucProcs, cls->ucProcs, sizeof(cls->ucProcs)) == 0 && back->classes[0].eventCount == 2 && strcmp(back->classes[0].events[0].paramNames, "Index,Title") == 0 && back->classes[0].events[0].byValParams == 1u); + + if (back) { + basModuleFree(back); + } + + // The host's side + CaptureT cap; + int32_t hostCalls = 0; + BasVmT *vm = basVmCreate(); + + captureReset(&cap); + basVmSetPrintCallback(vm, capturePrint, &cap); + basVmLoadModule(vm, mod); + basVmSetEventHost(vm, ucEventHost, &hostCalls); + basVmRun(vm); + + BasObjectT *obj = basVmNewObject(vm, 0); + BasValueT ctl = basValObject(&hostCalls); + BasValueT res; + + checkTrue("object made", obj != NULL); + + if (obj) { + basValRelease(&obj->fields[cls->ucSlot]); + obj->fields[cls->ucSlot] = ctl; + checkTrue("UserControl_Initialize runs with Me", basVmCallProc(vm, obj, cls->ucProcs[BAS_UC_EVT_INITIALIZE], NULL, 0, NULL, 0) && strcmp(cap.buf, "initObject\n") == 0); + checkTrue("Property Get", basVmCallMember(vm, obj, "Level", BAS_MEMBER_OP_GET, NULL, 0, &res) && res.type == BAS_TYPE_DOUBLE && res.dblVal == 1.25); + basValRelease(&res); + + BasValueT v = basValDouble(0.1); + + checkTrue("Property Let", basVmCallMember(vm, obj, "Level", BAS_MEMBER_OP_LET, &v, 1, NULL)); + checkTrue("Double kept", basVmCallMember(vm, obj, "Level", BAS_MEMBER_OP_GET, NULL, 0, &res) && res.dblVal == 0.1); + basValRelease(&res); + + BasValueT txt = basValStringFromC("hello"); + + checkTrue("public variable", basVmCallMember(vm, obj, "Caption", BAS_MEMBER_OP_LET, &txt, 1, NULL) && basVmCallMember(vm, obj, "Caption", BAS_MEMBER_OP_GET, NULL, 0, &res) && res.type == BAS_TYPE_STRING && strcmp(res.strVal->data, "hello") == 0); + basValRelease(&res); + basValRelease(&txt); + + BasValueT idx = basValInteger(3); + + checkTrue("method raising an event: the host writes back the ByRef argument", basVmCallMember(vm, obj, "Open", BAS_MEMBER_OP_GET, &idx, 1, &res) && hostCalls == 1 && res.type == BAS_TYPE_STRING && strcmp(res.strVal->data, "item3") == 0); + basValRelease(&res); + checkTrue("unknown member fails", !basVmCallMember(vm, obj, "Nope", BAS_MEMBER_OP_GET, NULL, 0, &res)); + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + + BasValueT mouse[4]; + + mouse[0] = basValInteger(1); + mouse[1] = basValInteger(0); + mouse[2] = basValSingle(7.0f); + mouse[3] = basValSingle(9.0f); + captureReset(&cap); + checkTrue("MouseDown handler", basVmCallProc(vm, obj, cls->ucProcs[BAS_UC_EVT_MOUSEDOWN], mouse, 4, NULL, 0) && strcmp(cap.buf, "down1 7 \n") == 0); + basObjectUnref(obj); + } + + basVmDestroy(vm); + basModuleFree(mod); + + expectCompileError("UserControl: a handler with the wrong parameters", + "BEGINCLASS \"C\" USERCONTROL\n" + "Private Sub UserControl_KeyDown(KeyCode AS INTEGER)\n" + "End Sub\n" + "ENDCLASS\n", + "UserControl_KeyDown must be a Sub with 2 parameter(s)"); + + // Enum-typed properties (the module's Enum table; also a fuzz seed) + runProgram("UserControl: Enum properties", + "Enum ShapeE\n" + " Square\n" + " Circle\n" + "End Enum\n" + "BEGINCLASS \"Shaped\" USERCONTROL\n" + "Public Look AS ShapeE\n" + "Public Property Get Kind() AS ShapeE\n" + " Kind = Look\n" + "End Property\n" + "Public Property Let Kind(v AS ShapeE)\n" + " Look = v\n" + "End Property\n" + "ENDCLASS\n" + "PRINT Circle; ShapeE.Square\n", + "1 0 \n"); +} + + +// The test's event host: counts the calls and answers ItemOpen by +// appending the index to the ByRef name. +static void ucEventHost(void *ctx, const BasObjectT *obj, int32_t eventIdx, BasValueT *args, int32_t argc) { + (void)obj; + (*(int32_t *)ctx)++; + + if (eventIdx == 0 && argc == 2 && args[1].type == BAS_TYPE_STRING) { + char text[TEST_ROW_TEXT_LEN]; + + snprintf(text, sizeof(text), "%s%" PRId32, args[1].strVal->data, basValToInt32(args[0])); + basValRelease(&args[1]); + args[1] = basValStringFromC(text); + } +} + + int main(void) { printf("DVX BASIC Compiler Tests\n"); printf("========================\n\n"); @@ -10627,6 +10872,73 @@ int main(void) { "SUB cannot be defined inside SUB or FUNCTION" ); + // A procedure inside a block statement is an error (QA bug 1: its + // scope outlived the block's unit arena -- a use-after-free on the + // next name lookup; the first source is the fuzzer's reproducer) + { + static const char *const procInBlock[] = { + "FOR i=0TO 3\nSUB L\nEND SUB\nNEXT\nP", + "IF 1 THEN\nSUB A\nEND SUB\nEND IF\nA\n", + "IF 1 THEN SUB A\nA\n", + "DO\nFUNCTION F\nF = 1\nEND FUNCTION\nLOOP\nPRINT F\n", + "WHILE 0\nPUBLIC SUB A\nEND SUB\nWEND\nA\n", + "SELECT CASE 1\nCASE 1\nSUB A\nEND SUB\nEND SELECT\nA\n", + "DIM o AS Object\nWITH o\nSUB A\nEND SUB\nEND WITH\nA\n", + "FOR i = 1 TO 2\nIF i THEN\nDO\nPROPERTY GET P\nEND PROPERTY\nLOOP\nEND IF\nNEXT\nPRINT P\n", + "FOR i = 1 TO 2\nSUB A\n" + }; + + for (int32_t i = 0; i < (int32_t)(sizeof(procInBlock) / sizeof(procInBlock[0])); i++) { + char name[64]; + + snprintf(name, sizeof(name), "Procedure inside a block rejected %d", (int)i); + expectCompileError(name, procInBlock[i], "cannot be defined inside IF, FOR, DO, WHILE, SELECT CASE or WITH"); + } + } + + // Named arguments of a late-bound call: the most the VM takes, each + // with the longest name, fit the names buffer; more are an error found + // before any name is copied (clang-tidy strcat triage: 17+ long names + // overflowed the buffer in an expression call) + { + static const struct { + const char *head; + const char *tail; + const char *fitName; + const char *overName; + } forms[] = { + { "x = o.M(", ")", "Most named arguments (expression)", "Too many named arguments (expression)" }, + { "o.M ", "", "Most named arguments (statement)", "Too many named arguments (statement)" } + }; + char src[TEST_NAMED_SRC_LEN]; + + for (int32_t f = 0; f < (int32_t)(sizeof(forms) / sizeof(forms[0])); f++) { + for (int32_t total = BAS_VM_MAX_CALL_ARGS; total <= BAS_VM_MAX_CALL_ARGS + TEST_NAMED_EXTRA; total += TEST_NAMED_EXTRA) { + int32_t len = snprintf(src, sizeof(src), "DIM o AS Object\n%s", forms[f].head); + + for (int32_t i = 0; i < total; i++) { + len += snprintf(src + len, sizeof(src) - len, "%sN%02d%0*d:=1", i > 0 ? ", " : "", (int)i, TEST_NAMED_PAD, 0); + } + + snprintf(src + len, sizeof(src) - len, "%s\n", forms[f].tail); + + if (total > BAS_VM_MAX_CALL_ARGS) { + expectCompileError(forms[f].overName, src, "Too many arguments"); + } else { + compileOnly(forms[f].fitName, src); + } + } + } + } + + // A DEF FN that stops at an error leaves no procedure scope behind + expectCompileError("DEF FN error closes its scope", + "x = 1\n" + "DEF FNa(ParamArray v()) = 1\n" + "PRINT x\n", + "ParamArray is not allowed in DEF FN" + ); + // PRINT#1 / CLOSE#1 / LINE INPUT#1 with no space before '#' runProgram("PRINT# without a space", "OPEN \"test_hash.txt\" FOR OUTPUT AS #1\n" @@ -11901,6 +12213,7 @@ int main(void) { testObjectsAndVariants(); testScreenModesPages(); testStringsAndDates(); + testUserControl(); printf("All tests complete. Failures: %d\n", (int)gFailCount); return gFailCount > 0 ? 1 : 0; diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c index 64b00f32..37c82d68 100644 --- a/src/apps/kpunch/dvxbasic/test_suite.c +++ b/src/apps/kpunch/dvxbasic/test_suite.c @@ -74,6 +74,10 @@ // Failure detail text of a check. #define TEST_DETAIL_LEN 512 #define TEST_IMPL_CORRUPTIONS 3 // testInterfaceMaps: bad entry, short map, bad interface +#define TEST_ENUM_CORRUPTIONS 7 // testUserControlEnums: index past the table, below none, on a method, in another class, an Enum without members, an empty name, two names alike +#define TEST_ENUM_WIRE_NAME "\x06\x00" "Circle" // the member Circle on the wire: uint16 length, then the name +#define TEST_ENUM_WIRE_LEN 8 // its bytes +#define TEST_ENUM_WIRE_NUL 5 // a byte of the name, made NUL static CaptureT *sUiCap = NULL; static char sUiRefs[UI_STUB_REF_COUNT][UI_STUB_REF_LEN]; @@ -236,13 +240,14 @@ static bool doEventsAbortStub(void *ctx); static bool doEventsCountdownStub(void *ctx); static bool doEventsResumeStub(void *ctx); static void expectTrue(const char *name, bool ok, const char *detail); -static BasValueT externCallStub(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); +static BasValueT externCallStub(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask); static void *externResolveStub(void *ctx, const char *libName, const char *funcName); static void handEmit16(HandCodeT *h, int16_t v); static void handEmit32(HandCodeT *h, int32_t v); static void handEmit8(HandCodeT *h, uint8_t b); static void handEmitU16(HandCodeT *h, uint16_t v); static bool inputStub(void *ctx, const char *prompt, char *buf, int32_t bufSize); +static int32_t memberIndex(const BasClassInfoT *cls, const char *memberName, uint8_t kind); static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName); static MiniFormT *miniCreateForm(MiniRtT *rt, const char *name, int32_t formVarCount); static void miniFireCtrlEvent(MiniRtT *rt, MiniCtrlT *ctrl, const char *eventName); @@ -276,6 +281,7 @@ static void testInterfaceMaps(const char *name); static void testLexerTokens(const char *name, const char *source, const ExpectedTokenT *expected, int32_t expectedCount); static void testLoadModuleRefusal(const char *name); static void testObfuscateCaption(const char *name); +static void testObfuscateKeepsNames(const char *name, const char *source); static void testOpcodeGuards(void); static void testReleaseRoundTrip(const char *name); static void testRuntimeError(const char *name, const char *source, const char *needle); @@ -285,7 +291,9 @@ static void testSleepPumpsEvents(const char *name); static void testStrAppend(void); static void testSubEq(const char *name, const char *source, const char *subName, const char *expected); static void testUiEq(const char *name, const char *source, const char *expected); +static void testUserControlEnums(const char *name); static void testValuesApi(void); +static bool ucEnumTableOk(const BasModuleT *mod); static BasValueT uiStubCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc); static void *uiStubCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName); static void *uiStubCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef); @@ -413,7 +421,13 @@ static void deviceBeepStub(void *ctx) { static bool deviceDoEventsStub(void *ctx) { (void)ctx; sDeviceEvents++; - return sDeviceEventsLeft == 0 || --sDeviceEventsLeft > 0; + + if (sDeviceEventsLeft == 0) { + return true; + } + + sDeviceEventsLeft--; + return sDeviceEventsLeft > 0; } @@ -488,9 +502,7 @@ static bool doEventsCountdownStub(void *ctx) { // DoEvents stub that immediately resumes a VM paused at a breakpoint. static bool doEventsResumeStub(void *ctx) { - BasVmT *vm = (BasVmT *)ctx; - - vm->running = true; + basVmResume((BasVmT *)ctx); return true; } @@ -505,7 +517,7 @@ static void expectTrue(const char *name, bool ok, const char *detail) { // Extern-call stub: returns the int32 sum of the arguments. -static BasValueT externCallStub(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) { +static BasValueT externCallStub(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask) { int32_t sum = 0; (void)ctx; @@ -513,6 +525,7 @@ static BasValueT externCallStub(void *ctx, void *funcPtr, const char *libName, c (void)libName; (void)funcName; (void)retType; + (void)binaryMask; for (int32_t i = 0; i < argc; i++) { sum += basValToInt32(args[i]); @@ -574,6 +587,19 @@ static bool inputStub(void *ctx, const char *prompt, char *buf, int32_t bufSize) } +// Index of cls's public member memberName of kind (BasMemberKindE), -1 +// when it has none. +static int32_t memberIndex(const BasClassInfoT *cls, const char *memberName, uint8_t kind) { + for (int32_t i = 0; i < cls->memberCount; i++) { + if (strcmp(cls->members[i].name, memberName) == 0 && cls->members[i].kind == kind) { + return i; + } + } + + return -1; +} + + static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName) { // Store stable heap pointers: arrput may realloc f->ctrls, so returning // an interior address would dangle once a second control is added. @@ -1424,30 +1450,30 @@ static void testDebuggerApi(void) { bool ok = r == BAS_VM_BREAKPOINT && vm->currentLine == 2; char detail[256]; - snprintf(detail, sizeof(detail), "stop1 r=%d line=%d", (int32_t)r, (int32_t)vm->currentLine); + snprintf(detail, sizeof(detail), "stop1 r=%d line=%d", (int32_t)r, (int)vm->currentLine); expectTrue("debugger-breakpoint-hit", ok, detail); basVmStepInto(vm); vm->running = true; r = basVmRun(vm); - snprintf(detail, sizeof(detail), "stop2 r=%d line=%d", (int32_t)r, (int32_t)vm->currentLine); + snprintf(detail, sizeof(detail), "stop2 r=%d line=%d", (int32_t)r, (int)vm->currentLine); expectTrue("debugger-step-into", r == BAS_VM_BREAKPOINT && vm->currentLine == 3, detail); vm->running = true; r = basVmRun(vm); - snprintf(detail, sizeof(detail), "stop3 r=%d line=%d", (int32_t)r, (int32_t)vm->currentLine); + snprintf(detail, sizeof(detail), "stop3 r=%d line=%d", (int32_t)r, (int)vm->currentLine); expectTrue("debugger-breakpoint-in-sub", r == BAS_VM_BREAKPOINT && vm->currentLine == 9, detail); basVmStepOut(vm); vm->running = true; r = basVmRun(vm); - snprintf(detail, sizeof(detail), "stop4 r=%d line=%d", (int32_t)r, (int32_t)vm->currentLine); + snprintf(detail, sizeof(detail), "stop4 r=%d line=%d", (int32_t)r, (int)vm->currentLine); expectTrue("debugger-step-out", r == BAS_VM_BREAKPOINT && vm->currentLine == 4, detail); basVmRunToCursor(vm, 6); vm->running = true; r = basVmRun(vm); - snprintf(detail, sizeof(detail), "stop5 r=%d line=%d", (int32_t)r, (int32_t)vm->currentLine); + snprintf(detail, sizeof(detail), "stop5 r=%d line=%d", (int32_t)r, (int)vm->currentLine); expectTrue("debugger-run-to-cursor", r == BAS_VM_BREAKPOINT && vm->currentLine == 6, detail); vm->running = true; @@ -1479,7 +1505,7 @@ static void testDebuggerApi(void) { basVmStepOver(vm); vm->running = true; r = basVmRun(vm); - snprintf(detail, sizeof(detail), "r=%d line=%d out=[%s]", (int32_t)r, (int32_t)vm->currentLine, cap.buf); + snprintf(detail, sizeof(detail), "r=%d line=%d out=[%s]", (int32_t)r, (int)vm->currentLine, cap.buf); expectTrue("debugger-step-over", r == BAS_VM_BREAKPOINT && vm->currentLine == 4 && strcmp(cap.buf, "in\n") == 0, detail); basVmDestroy(vm); @@ -1509,19 +1535,18 @@ static void testDebuggerApi(void) { basVmSetPrintCallback(vm, capturePrint, &cap); basVmSetBreakpoints(vm, subBp, 1); basVmSetDoEventsCallback(vm, doEventsResumeStub, vm); - vm->breakpointFn = breakpointRecordStub; - vm->breakpointCtx = NULL; - sBpCount = 0; + basVmSetBreakpointCallback(vm, breakpointRecordStub, NULL); + sBpCount = 0; bool called = basVmCallSub(vm, proc->codeAddr); - snprintf(detail, sizeof(detail), "called=%d bpCount=%d out=[%s]", (int32_t)called, (int32_t)sBpCount, cap.buf); + snprintf(detail, sizeof(detail), "called=%d bpCount=%d out=[%s]", (int)called, (int)sBpCount, cap.buf); expectTrue("debugger-callsub-resume", called && sBpCount == 2 && sBpLines[0] == 9 && sBpLines[1] == -1 && strcmp(cap.buf, "in\n") == 0, detail); // While paused at a breakpoint the VM rejects re-entrant sub calls. - vm->debugPaused = true; + basVmSetDebugPaused(vm, true); expectTrue("debugger-callsub-paused-rejected", !basVmCallSub(vm, proc->codeAddr), "call while paused must be refused"); - vm->debugPaused = false; + basVmSetDebugPaused(vm, false); // A doEvents callback reporting shutdown aborts the paused sub. basVmSetDoEventsCallback(vm, doEventsAbortStub, NULL); @@ -1572,7 +1597,7 @@ static void testDebuggerApi(void) { basVmSetDoEventsCallback(vm, doEventsCountdownStub, &countdown); called = basVmCallSub(vm, proc->codeAddr); - snprintf(detail, sizeof(detail), "called=%d countdown=%d", (int32_t)called, (int32_t)countdown); + snprintf(detail, sizeof(detail), "called=%d countdown=%d", (int)called, (int)countdown); expectTrue("debugger-callsub-yield-abort", called && countdown == 0, detail); basVmDestroy(vm); @@ -1955,7 +1980,7 @@ static void testExternCalls(void) { BasVmResultE r = basVmRun(vm); - snprintf(detail, sizeof(detail), "r=%d out=[%s] resolves=%d", (int32_t)r, cap.buf, (int32_t)sExternResolves); + snprintf(detail, sizeof(detail), "r=%d out=[%s] resolves=%d", (int32_t)r, cap.buf, (int)sExternResolves); expectTrue("extern-call-and-cache", r == BAS_VM_HALTED && strcmp(cap.buf, "5 \n30 \n") == 0 && sExternResolves == 1, detail); basVmDestroy(vm); @@ -2583,6 +2608,10 @@ static void testObfuscateCaption(const char *name) { "End\n" "Begin Label Note\n" "Caption = Don't panic ' kept\n" + "ContextMenu = \"mnuPop\"\n" + "End\n" + "Begin Menu mnuPop\n" + "Caption = \"Pop\"\n" "End\n" "End\n" "' comment\n" @@ -2609,7 +2638,7 @@ static void testObfuscateCaption(const char *name) { memcpy(text, obf[0].data, (size_t)obf[0].len); text[obf[0].len] = '\0'; - if (!strstr(text, "Caption = \"Calculator\"") || !strstr(text, "Caption = \"Total\"") || !strstr(text, "Begin Form C1") || !strstr(text, "Begin Label C2") || !strstr(text, "DataSource = \"C2\"")) { + if (!strstr(text, "Caption = \"Calculator\"") || !strstr(text, "Caption = \"Total\"") || !strstr(text, "Begin Form C1") || !strstr(text, "Begin Label C2") || !strstr(text, "DataSource = \"C2\"") || !strstr(text, "Begin Menu C4") || !strstr(text, "ContextMenu = \"C4\"")) { snprintf(detail, sizeof(detail), "frm rewrite wrong:\n%s", text); ok = false; } @@ -2634,6 +2663,40 @@ static void testObfuscateCaption(const char *name) { } +// A program that uses names as text (here the Name property, or a +// DesignMouse or DesignPropertyChanged handler) keeps its form and +// control names in a release build: its code must see the names it was +// written with. +static void testObfuscateKeepsNames(const char *name, const char *source) { + const char *frm = + "VERSION DVX 1.00\n" + "Begin Form Calculator\n" + "Begin Label Total\n" + "End\n" + "End\n"; + BasModuleT *mod = compileSource(name, source); + + if (!mod) { + return; + } + + const char *frmTexts[1] = { frm }; + int32_t frmLens[1] = { (int32_t)strlen(frm) }; + BasObfFrmT obf[1] = { { NULL, 0 } }; + bool ok = basObfuscateNames(mod, frmTexts, frmLens, 1, obf) && obf[0].data; + + ok = ok && memmem(obf[0].data, (size_t)obf[0].len, "Begin Form Calculator", strlen("Begin Form Calculator")) && memmem(obf[0].data, (size_t)obf[0].len, "Begin Label Total", strlen("Begin Label Total")); + + for (int32_t i = 0; ok && i < mod->procCount; i++) { + ok = strncmp(mod->procs[i].name, "C", 1) != 0 || strncmp(mod->procs[i].name, "Calculator", strlen("Calculator")) == 0; + } + + free(obf[0].data); + basModuleFree(mod); + expectTrue(name, ok, "names were renamed"); +} + + // VM guard paths the compiler never emits: every pop on an empty stack, // every push on a full one, and the bad-index / bad-type operand guards. static void testOpcodeGuards(void) { @@ -3438,7 +3501,7 @@ static void testSerializeCorrupt(const char *name) { for (int32_t cut = 0; cut < len; cut++) { if (basModuleDeserialize(bad, cut) != NULL) { - snprintf(detail, sizeof(detail), "truncation at %d accepted", (int32_t)cut); + snprintf(detail, sizeof(detail), "truncation at %d accepted", (int)cut); truncOk = false; break; } @@ -3536,7 +3599,7 @@ static void testSleepPumpsEvents(const char *name) { BasVmResultE r = basVmRun(vm); - snprintf(detail, sizeof(detail), "r=%d countdown=%d out=[%s]", (int32_t)r, (int32_t)countdown, cap.buf); + snprintf(detail, sizeof(detail), "r=%d countdown=%d out=[%s]", (int32_t)r, (int)countdown, cap.buf); expectTrue(name, r == BAS_VM_HALTED && countdown == 0 && cap.len == 0, detail); basVmDestroy(vm); @@ -3650,6 +3713,193 @@ static void testUiEq(const char *name, const char *source, const char *expected) } +// The Enums of a UserControl's properties (BasModuleT.enums, member +// enumIdx): an Enum a property (Public variable, Property Get / Let) is +// declared As is kept once, its members' names and values in declaration +// order (a Private Enum's members by their own names); methods, other +// types and other classes' members have none. The table survives a +// release build (strip, name obfuscation, compaction) and the module +// format, whose reader refuses a broken one. +static void testUserControlEnums(const char *name) { + const char *source = + "Public Enum ShapeE\n" + " Square\n" + " Circle\n" + " Star\n" + "End Enum\n" + "BEGINCLASS \"Gauge\" USERCONTROL\n" + "Private Enum SizeE\n" + " Small = 10\n" + " Large = 20\n" + "End Enum\n" + "Public Look AS ShapeE\n" + "Private mSize AS SizeE\n" + "Public Property Get Size() AS SizeE\n" + " Size = mSize\n" + "End Property\n" + "Public Property Let Size(v AS SizeE)\n" + " mSize = v\n" + "End Property\n" + "Public Property Let Other(v AS ShapeE)\n" + "End Property\n" + "Public Function Pick(s AS ShapeE) AS ShapeE\n" + " Pick = s\n" + "End Function\n" + "Public Count AS LONG\n" + "ENDCLASS\n" + "BEGINCLASS \"Plain\"\n" + "Public Look AS ShapeE\n" + "ENDCLASS\n"; + char detail[TEST_DETAIL_LEN]; + BasModuleT *mod = compileSource(name, source); + + if (!mod) { + return; + } + + bool ok = ucEnumTableOk(mod); + + snprintf(detail, sizeof(detail), "enum table wrong after compile (%d enums)", (int)mod->enumCount); + + // A release build keeps it and the .frm's value name + basStripModule(mod); + + if (ok) { + const char *frm = + "Begin Form Form1\n" + "Begin Gauge Gauge1\n" + "Look = Circle\n" + "End\n" + "End\n"; + const char *frmValue = "Look = Circle"; + const char *frmTexts[1] = { frm }; + const int32_t frmLens[1] = { (int32_t)strlen(frm) }; + BasObfFrmT obf[1]; + + memset(obf, 0, sizeof(obf)); + ok = basObfuscateNames(mod, frmTexts, frmLens, 1, obf) && obf[0].data != NULL && memmem(obf[0].data, (size_t)obf[0].len, frmValue, strlen(frmValue)) != NULL; + snprintf(detail, sizeof(detail), "release build lost the .frm's value name"); + free(obf[0].data); + } + + basCompactBytecode(mod); + + if (ok) { + ok = ucEnumTableOk(mod); + snprintf(detail, sizeof(detail), "release build changed the enum table"); + } + + // The module format keeps it + int32_t len = 0; + uint8_t *data = ok ? basModuleSerialize(mod, &len) : NULL; + BasModuleT *back = data ? basModuleDeserialize(data, len) : NULL; + + if (ok) { + ok = back != NULL && ucEnumTableOk(back); + snprintf(detail, sizeof(detail), "round trip lost the enum table"); + } + + basModuleFree(back); + + // A bad member name on the wire: a NUL in it, a length past the longest name + static const struct { + int32_t offset; + uint8_t value; + const char *what; + } wireBad[] = { + { TEST_ENUM_WIRE_NUL, '\0', "with a NUL" }, + { 0, BAS_MAX_IDENT, "longer than BAS_MAX_IDENT - 1" } + }; + uint8_t *at = data ? (uint8_t *)memmem(data, (size_t)len, TEST_ENUM_WIRE_NAME, TEST_ENUM_WIRE_LEN) : NULL; + + if (ok) { + ok = at != NULL; + snprintf(detail, sizeof(detail), "member name not found on the wire"); + } + + for (int32_t k = 0; ok && k < DVX_ARRAY_LEN(wireBad); k++) { + uint8_t saved = at[wireBad[k].offset]; + + at[wireBad[k].offset] = wireBad[k].value; + + BasModuleT *bad = basModuleDeserialize(data, len); + + at[wireBad[k].offset] = saved; + ok = bad == NULL; + snprintf(detail, sizeof(detail), "a member name %s accepted", wireBad[k].what); + basModuleFree(bad); + } + + free(data); + + // A broken table in the module: each is refused at load (the table checked above) + if (ok) { + BasClassInfoT *gauge = &mod->classes[0]; + BasClassInfoT *plain = &mod->classes[mod->classCount - 1]; + int32_t look = memberIndex(gauge, "Look", BAS_MEMBER_VAR); + int32_t pick = memberIndex(gauge, "Pick", BAS_MEMBER_FUNCTION); + int32_t other = memberIndex(plain, "Look", BAS_MEMBER_VAR); + int32_t shape = (look >= 0) ? gauge->members[look].enumIdx : BAS_ENUM_NONE; + + for (int32_t k = 0; ok && k < TEST_ENUM_CORRUPTIONS; k++) { + BasEnumInfoT *en = &mod->enums[shape]; + BasEnumMemberT savedName = en->members[1]; + int32_t savedCount = en->memberCount; + int32_t savedLook = gauge->members[look].enumIdx; + int32_t savedPick = gauge->members[pick].enumIdx; + int32_t savedOther = plain->members[other].enumIdx; + + switch (k) { + case 0: + gauge->members[look].enumIdx = mod->enumCount; + break; + + case 1: + gauge->members[look].enumIdx = BAS_ENUM_NONE - 1; + break; + + case 2: + gauge->members[pick].enumIdx = shape; + break; + + case 3: + plain->members[other].enumIdx = shape; + break; + + case 4: + en->memberCount = 0; + break; + + case 5: + en->members[1].name[0] = '\0'; + break; + + default: + snprintf(en->members[1].name, sizeof(en->members[1].name), "SQUARE"); + break; + } + + int32_t badLen = 0; + uint8_t *badData = basModuleSerialize(mod, &badLen); + BasModuleT *bad = badData ? basModuleDeserialize(badData, badLen) : NULL; + + ok = badData != NULL && bad == NULL; + snprintf(detail, sizeof(detail), "broken enum table %d accepted", (int)k); + free(badData); + basModuleFree(bad); + en->members[1] = savedName; + en->memberCount = savedCount; + gauge->members[look].enumIdx = savedLook; + gauge->members[pick].enumIdx = savedPick; + plain->members[other].enumIdx = savedOther; + } + } + + basModuleFree(mod); + expectTrue(name, ok, detail); +} + + // Direct values.c API edges: refused allocations, substring/compare // boundaries, numeric parsing forms, deep UDT clones and reference // counting through basValCopy. @@ -3665,6 +3915,35 @@ static void testValuesApi(void) { expectTrue("values-empty-string-uncounted", basEmptyString->refCount == emptyRefs, "empty string refCount drifted"); + // The string pool (QA bug 6: its link sat at offset 12, unaligned on + // a 64-bit host, so leak checkers reported every pooled string as + // lost): a released string of a pooled size is linked through the + // start of its block, keeps its capacity, comes back with its header + // reset, and the drain empties the pool. No pool under ASan. + if (BAS_STRING_POOL_DEPTH > 0) { + basStringPoolDrain(); + + BasStringT *first = basStringAlloc(1); + BasStringT *second = basStringAlloc(1); + void *link = NULL; + + basStringUnref(first); + basStringUnref(second); + memcpy(&link, second, sizeof(link)); + expectTrue("values-string-pool-link", link == (void *)first && second->cap == BAS_STRING_POOL_MIN_CAP, "pool link not at the block start, or cap overwritten"); + + BasStringT *again = basStringAlloc(1); + + expectTrue("values-string-pool-reuse", again == second && again->refCount == 1 && again->len == 0 && again->cap == BAS_STRING_POOL_MIN_CAP, "pooled string not reused with a fresh header"); + basStringUnref(again); + basStringPoolDrain(); + + BasStringT *fresh = basStringAlloc(1); + + expectTrue("values-string-pool-drained", fresh->refCount == 1 && fresh->cap == BAS_STRING_POOL_MIN_CAP, "string after a drain"); + basStringUnref(fresh); + } + // A TYPE with no fields is valid; a negative field count is not. BasUdtT *noFields = basUdtNew(0, 0); @@ -3672,17 +3951,17 @@ static void testValuesApi(void) { basUdtUnref(noFields); // Extreme subscripts are out of range, not a signed-overflow wrap. - int32_t extLb[1] = { 1 }; - int32_t extUb[1] = { 3 }; - int32_t extIdx[1] = { INT32_MIN }; - BasArrayT *extArr = basArrayNew(1, extLb, extUb, BAS_TYPE_INTEGER); + const int32_t extLb[1] = { 1 }; + const int32_t extUb[1] = { 3 }; + const int32_t extIdx[1] = { INT32_MIN }; + BasArrayT *extArr = basArrayNew(1, extLb, extUb, BAS_TYPE_INTEGER); expectTrue("values-array-index-extreme", extArr && basArrayIndex(extArr, extIdx, 1) == -1, "INT32_MIN subscript accepted"); basArrayUnref(extArr); // Arrays: bad bounds and overflowing element counts are refused. - int32_t lb[2] = { 0, 0 }; - int32_t ub[2] = { -1, 0 }; + const int32_t lb[2] = { 0, 0 }; + int32_t ub[2] = { -1, 0 }; expectTrue("values-array-empty-dim", basArrayNew(1, lb, ub, BAS_TYPE_INTEGER) == NULL, "ubound < lbound accepted"); @@ -3699,7 +3978,7 @@ static void testValuesApi(void) { expectTrue("values-array-string-init", arr != NULL && arr->totalElements == 9 && arr->elements[8].strVal != NULL, "string elements not seeded"); // Out-of-bounds index computation. - int32_t idx[2] = { 3, 0 }; + const int32_t idx[2] = { 3, 0 }; expectTrue("values-array-index-oob", arr && basArrayIndex(arr, idx, 2) == -1, "out-of-bounds index accepted"); @@ -3840,6 +4119,42 @@ static void testValuesApi(void) { } +// The table testUserControlEnums expects: Gauge's Look and Other the Enum +// ShapeE (Square 0, Circle 1, Star 2), its Size Get and Let the Enum +// SizeE (Small 10, Large 20); its Pick and Count and Plain's Look none. +static bool ucEnumTableOk(const BasModuleT *mod) { + if (mod->enumCount != 2 || mod->classCount != 2) { + return false; + } + + const BasClassInfoT *gauge = &mod->classes[0]; + const BasClassInfoT *plain = &mod->classes[1]; + int32_t look = memberIndex(gauge, "Look", BAS_MEMBER_VAR); + int32_t other = memberIndex(gauge, "Other", BAS_MEMBER_LET); + int32_t sizeGet = memberIndex(gauge, "Size", BAS_MEMBER_GET); + int32_t sizeLet = memberIndex(gauge, "Size", BAS_MEMBER_LET); + int32_t pick = memberIndex(gauge, "Pick", BAS_MEMBER_FUNCTION); + int32_t count = memberIndex(gauge, "Count", BAS_MEMBER_VAR); + int32_t plainLook = memberIndex(plain, "Look", BAS_MEMBER_VAR); + + if (look < 0 || other < 0 || sizeGet < 0 || sizeLet < 0 || pick < 0 || count < 0 || plainLook < 0) { + return false; + } + + int32_t shape = gauge->members[look].enumIdx; + int32_t size = gauge->members[sizeGet].enumIdx; + + if (shape < 0 || size < 0 || shape == size || gauge->members[other].enumIdx != shape || gauge->members[sizeLet].enumIdx != size || gauge->members[pick].enumIdx != BAS_ENUM_NONE || gauge->members[count].enumIdx != BAS_ENUM_NONE || plain->members[plainLook].enumIdx != BAS_ENUM_NONE) { + return false; + } + + const BasEnumInfoT *sh = &mod->enums[shape]; + const BasEnumInfoT *sz = &mod->enums[size]; + + return sh->memberCount == 3 && strcmp(sh->members[0].name, "Square") == 0 && sh->members[0].value == 0 && strcmp(sh->members[1].name, "Circle") == 0 && sh->members[1].value == 1 && strcmp(sh->members[2].name, "Star") == 0 && sh->members[2].value == 2 && sz->memberCount == 2 && strcmp(sz->members[0].name, "Small") == 0 && sz->members[0].value == 10 && strcmp(sz->members[1].name, "Large") == 0 && sz->members[1].value == 20; +} + + static BasValueT uiStubCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) { char line[CAPTURE_MAX]; @@ -3944,7 +4259,7 @@ static int32_t uiStubMsgBox(void *ctx, const char *message, int32_t flags, const char line[CAPTURE_MAX]; (void)ctx; - snprintf(line, sizeof(line), "msgbox [%s] f=%d [%s]\n", message, (int32_t)flags, title); + snprintf(line, sizeof(line), "msgbox [%s] f=%d [%s]\n", message, (int)flags, title); captureAppend(sUiCap, line); return 6; } @@ -5094,6 +5409,15 @@ int main(void) { "MID$(s, 2, 3) = \"XYZ\"\n" "PRINT s\n", "aXYZef\n"); + + // The lowest start is an error, not a signed overflow when it is made + // 0-based (fuzzBasModule: INSTR at INT32_MIN; MID$ = the same) + TEST_RUNTIME_ERROR("instr-start-int32-min", + "DIM n AS DOUBLE\nn = -2147483648#\nPRINT INSTR(n, \"abc\", \"b\")\n", + "Illegal function call"); + TEST_RUNTIME_ERROR("mid-assign-start-int32-min", + "DIM n AS DOUBLE\nDIM s AS STRING\nn = -2147483648#\ns = \"abc\"\nMID$(s, n, 1) = \"x\"\n", + "Illegal function call"); TEST_EQ("str-leading-space-positive", "PRINT \"[\" + STR$(5) + \"]\"\n", "[ 5]\n"); @@ -6673,6 +6997,28 @@ int main(void) { "Unload Form2\n", "unload Me\nunload Form2\n"); + // A Form variable or any expression giving a form is unloaded itself, + // not a form of that name. + TEST_UI_EQ("ui-unload-expr", + "DIM f AS Form\n" + "SET f = CreateForm(\"Dyn\", 1, 1)\n" + "Unload f\n" + "Unload CreateForm(\"Dyn2\", 1, 1)\n", + "unload Dyn\nunload Dyn2\n"); + + TEST_UI_EQ("ui-unload-number", + "ON ERROR GOTO bad\n" + "DIM n AS INTEGER\n" + "Unload n\n" + "END\n" + "bad:\n" + "PRINT ERR\n", + "91 \n"); + + TEST_COMPILE_ERROR("ui-unload-nothing", + "Unload\n", + "form name"); + TEST_COMPILE_ERROR("ui-me-ctrl-idx-arity", "PRINT Me.Btn(1, 2).Caption\n", "Control array index expected"); @@ -6848,16 +7194,16 @@ int main(void) { int32_t manyPos = 0; for (int32_t ch = 1; ch <= 15; ch++) { - manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "OPEN \"ts_ch%d.tmp\" FOR OUTPUT AS #%d\n", (int32_t)ch, (int32_t)ch); + manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "OPEN \"ts_ch%d.tmp\" FOR OUTPUT AS #%d\n", (int)ch, (int)ch); } manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "ON ERROR GOTO H\nDIM n AS INTEGER\nn = FREEFILE\nPRINT \"free\"; n\nEND\nH:\nPRINT ERR\n"); for (int32_t ch = 1; ch <= 15; ch++) { - manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "CLOSE #%d\nKILL \"ts_ch%d.tmp\"\n", (int32_t)ch, (int32_t)ch); + manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "CLOSE #%d\nKILL \"ts_ch%d.tmp\"\n", (int)ch, (int)ch); } - manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "END\n"); + snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "END\n"); TEST_EQ("freefile-exhausted", manySrc, "67 \n"); } @@ -7337,6 +7683,34 @@ int main(void) { "DIM a() AS PtT\nREDIM a(2) AS PtT\na(1).x = 5\nREDIM PRESERVE a(4) AS PtT\na(4).x = 6\nPRINT a(1).x; a(4).x; a(3).x\n", "5 6 0 \n"); + // CallByName: a class instance's property, Let, method and function + // by a name chosen at run time, a Collection's members, the statement + // form, and a call type that is none of vbMethod / vbGet / vbLet / vbSet. + TEST_EQ("callbyname-class", + "DIM a AS NEW Acc\nDIM c AS NEW Collection\nDIM m AS STRING\n" + "m = \"Add\"\nCALLBYNAME a, m, vbMethod, 5\nCallByName a, \"Add\", vbMethod, 7\n" + "CallByName a, \"Label\", vbLet, \"sum\"\n" + "PRINT CallByName(a, \"Label\", vbGet); CallByName(a, \"Total\", vbMethod); CallByName(a, \"Total\", vbGet, 10)\n" + "c.Add \"x\"\nPRINT CallByName(c, \"Count\", vbGet); CallByName(c, \"Item\", vbGet, 1); IsEmpty(CallByName(a, \"Label\", vbLet, \"z\")); a.Label\n" + "BEGINCLASS \"Acc\"\n" + "Private vals As New Collection\n" + "Public Label As String\n" + "Public Sub Add(v As Integer)\n vals.Add v\nEnd Sub\n" + "Public Function Total(Optional start As Integer) As Long\n DIM t AS LONG\n t = start\n FOR EACH v IN vals\n t = t + v\n NEXT\n Total = t\nEnd Function\n" + "ENDCLASS\n", + "sum12 22 \n1 xTrue z\n"); + TEST_RUNTIME_ERROR("callbyname-bad-type", "DIM c AS NEW Collection\nPRINT CallByName(c, \"Count\", 3)\n", "Illegal function call"); + TEST_RUNTIME_ERROR("callbyname-let-no-value", "DIM c AS NEW Collection\nCallByName c, \"Count\", vbLet\n", "Illegal function call"); + + // Inside its own body a FUNCTION's bare name is its result so far + // (VB), with or without parameters; F() and F(n) call it again. + TEST_EQ("function-name-reads-result", + "FUNCTION F() AS STRING\n F = \"x\"\n IF F = \"\" THEN F = \"e\" ELSE F = F + \"y\"\nEND FUNCTION\n" + "FUNCTION G(BYVAL n AS LONG) AS LONG\n G = n\n IF n > 1 THEN G = G * G(n - 1)\nEND FUNCTION\n" + "FUNCTION H() AS LONG\n STATIC k AS LONG\n k = k + 1\n H = k\n IF k < 3 THEN H = H + H()\nEND FUNCTION\n" + "PRINT F(); G(4); H\n", + "xy24 6 \n"); + // GETATTR of a missing file is File not found, like FILELEN. TEST_RUNTIME_ERROR("getattr-missing", "PRINT GETATTR(\"ts_no_such_file.tmp\")\n", "File not found"); @@ -7353,12 +7727,16 @@ int main(void) { testDeviceStatements(); testStrAppend(); testObfuscateCaption("obfuscate-caption-kept"); + testObfuscateKeepsNames("obfuscate-keeps-names-name", "SUB Total_Click\n PRINT Total.Name\nEND SUB\n"); + testObfuscateKeepsNames("obfuscate-keeps-names-designmouse", "SUB Calculator_DesignMouse(a AS INTEGER, b AS INTEGER, x AS INTEGER, y AS INTEGER, n AS STRING, h AS INTEGER)\nEND SUB\n"); + testObfuscateKeepsNames("obfuscate-keeps-names-designpropchg", "SUB Calculator_DesignPropertyChanged(n AS STRING, p AS STRING)\nEND SUB\n"); // Debugger pause vs ON ERROR, and the release payload path. testBreakpointNotTrapped("breakpoint-not-trapped"); testReleaseRoundTrip("release-round-trip"); testClassRoundTrip("class-round-trip"); testInterfaceMaps("interface-map-round-trip"); + testUserControlEnums("usercontrol-enum-table"); testEarlyBoundOpcodes(); // Hand-assembled p-code, lexer token stream and raw codegen output. diff --git a/src/apps/kpunch/dvxbasic/userctl.dhs b/src/apps/kpunch/dvxbasic/userctl.dhs new file mode 100644 index 00000000..9ab01928 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/userctl.dhs @@ -0,0 +1,219 @@ +# 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. + +.topic lang.usercontrol +.title UserControls: Your Own Controls +.toc 1 UserControls: Your Own Controls +.index UserControl +.index USERCONTROL +.index .ctl +.index Controls (project section) +.index Writing a control + +.h1 UserControls: Your Own Controls + +A UserControl is a control you write in DVX BASIC. Once written, it is used exactly like the controls DVX comes with: you put it on a form in the designer (or in a .frm file with Begin), set its properties, call its methods and write handlers for its events. Programs written in C can create and use it too. + +A UserControl lives in a .ctl file. In the IDE, File > New UserControl makes one (see UserControls in the IDE). The file starts with a header that describes the control, then holds the control's code. The code is a class: every control on a form is one object of the class, with its own copies of the class's variables. + +.code +VERSION DVX 1.00 +Begin UserControl Gauge + Width = 120 ' natural (smallest) size in pixels + Height = 30 + CanGetFocus = True + BackColor = &H00FFFFFF& + ToolboxIcon = "gauge.bmp" ' 24 x 24 picture for the toolbox + Description = "A gauge" ' the toolbox tooltip + DefaultEvent = "Changed" +End +Option Explicit + +Public Event Changed(ByVal Level As Integer) + +Private mLevel As Integer + +Public Property Get Level() As Integer + Level = mLevel +End Property + +Public Property Let Level(ByVal Value As Integer) + mLevel = Value + RaiseEvent Changed(mLevel) + UserControl.Refresh +End Property + +Private Sub UserControl_Paint() + UserControl.Cls + UserControl.Line (0, 0)-(mLevel * UserControl.Width \ 100, UserControl.Height - 1), RGB(0, 128, 0), BF +End Sub +.endcode + +A form uses it like any control: + +.code +Begin Form Form1 + Begin Gauge Gauge1 + Level = 40 + End +End + +Sub Gauge1_Changed (ByVal Level As Integer) + lblLevel.Caption = Str$(Level) +End Sub +.endcode + +.h2 The Header + +The header is a Begin UserControl block: the control's name (the type name forms use) and its settings. Every setting is optional. In the IDE, the Properties window shows and changes them while the .ctl file is the active file. + +.table + Setting Meaning + ------- ------- + Width, Height The natural size in pixels: the smallest size the form's layout gives it (64 x 64 when not given). The code changes it with UserControl.MinWidth and MinHeight. + CanGetFocus True (the default): a click or Tab gives it the keyboard focus. False: it never has the focus and gets no keys. + BackColor The colour Cls clears it to (white when not given). + ToolboxIcon A 24 x 24 picture file in the project folder, shown in the designer's toolbox. + Description The toolbox tooltip (its name when not given). + DefaultEvent The event the designer opens when the control is double-clicked. +.endtable + +.h2 Properties, Methods and Events + +The control's public members are what programs use: + +.list +.item Public Property Get / Let (or Set) pairs and Public variables are its properties. A property with only Property Get is read-only, with only Property Let write-only. A property whose type is Integer, Long or Byte is a whole number, Boolean is True / False, Single and Double (and Currency) are numbers, String, Date and Variant are text; a property of an object type, an array or a TYPE cannot be a property of a control. +.item A property declared As an ENUM is a choice of its members, as a built-in control's Alignment is: the designer's Properties window shows the member's name and a double-click steps to the next member, the form file stores the name (Look = Circle), and code sets it with a member, its number or its name as text (Shape1.Look = Circle, Shape1.Look = 1, Shape1.Look = "Circle"). This needs the members numbered 0, 1, 2 and so on, each number once, as an ENUM numbers them when no values are given; an ENUM with other values (Small = 10) makes a whole-number property. +.item Public Subs and Functions are its methods. They take any arguments; a Function's value is the method's result. +.item Public Events are its events. RaiseEvent sends one to the program that has the control: its handler CtrlName_EventName runs with the arguments, and a ByRef parameter the handler changes (a number, a Boolean or a String) comes back changed. +.endlist + +Names that every control already has (Name, Left, Top, Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Weight, Visible, Enabled, ReadOnly, BackColor, ForeColor, Caption, Text, ToolTipText, ContextMenu, HelpTopic, DataSource, DataField, Tag, MousePointer ...) belong to the form: a control's own property of such a name is not used. Give your properties other names (Title instead of Caption, Value or Level instead of Text). + +A property's default is the value it has after UserControl_Initialize ran. The form's .frm sets only the properties the designer user changed, through your Property Let, after Initialize. + +.h2 The UserControl Object + +Inside the control's code, UserControl is the control itself: the surface it draws on and the side the program sees. + +.table + Member Description + ------ ----------- + Width, Height Its current size in pixels (read-only; the form's layout decides it). + MinWidth, MinHeight Its natural size: the form lays it out again when you change them. + Enabled False while the control, or a container it is in, is disabled (read-only). + HasFocus True while it has the keyboard focus (read-only). + CanGetFocus The header's CanGetFocus (read-only). + UserMode False while it sits in a form designer (the IDE's, or a form with Form.DesignMode = True), True in a running program. In the designer the code draws it, but the user's clicks and keys go to the designer. + Name The name the program gave the control ("" when a C program gave none). + SetFocus Gives it the keyboard focus. + Refresh Runs UserControl_Paint now and shows the result. + Invalidate Asks for UserControl_Paint at the next chance, once however often it is called before then: change many things (add a hundred items) and the control is drawn once. + PropertyChanged name Tells a designer that a property changed, so its property sheet shows the new value. + Cls, Line, PSet, Circle, Print, Point, PaintPicture, DrawFocusRect, TextWidth, TextHeight, Scale, ScaleX, ScaleY Drawing, as on a PictureBox. + DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, TextWidthAccel, AccelKey Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox. + CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox. +.endtable + +The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control. + +The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes. + +.h2 The Control's Events + +The program runs these procedures of the control's code when they exist. Each is a Private Sub with exactly these parameters: + +.table + Procedure When + --------- ---- + UserControl_Initialize () The control was made (before the form's property values are set). + UserControl_Terminate () The control is going away (its form unloads). It cannot draw any more. + UserControl_Resize () Its size changed (and once when it first appears). + UserControl_Paint () It must be drawn (after Resize, after Refresh). + UserControl_Click () The left button was pressed and let go over it. + UserControl_DblClick () A second quick left press. + UserControl_MouseDown (Button As Integer, Shift As Integer, X As Single, Y As Single) A button went down over it. + UserControl_MouseMove (Button As Integer, Shift As Integer, X As Single, Y As Single) The mouse moved over it (Button 0), or with the left button held after a press on it, anywhere. + UserControl_MouseUp (Button As Integer, Shift As Integer, X As Single, Y As Single) A button went up. + UserControl_MouseWheel (Delta As Integer) The wheel turned over it (Delta is positive downwards). + UserControl_KeyDown (KeyCode As Integer, Shift As Integer) A key went down while it has the focus. + UserControl_KeyPress (KeyAscii As Integer) A character key was typed. + UserControl_KeyUp (KeyCode As Integer, Shift As Integer) A key went up. + UserControl_GotFocus () It got the keyboard focus. + UserControl_LostFocus () It lost the keyboard focus. +.endtable + +Button is vbLeftButton, vbRightButton or vbMiddleButton (in MouseMove, the buttons held). Shift holds vbShiftMask, vbCtrlMask and vbAltMask. X and Y are pixels from the control's top-left corner. The compiler reports a handler with other parameters. + +These events are the control's own: the program does not see them. The program sees only the events the control raises. To let a program react to clicks, raise your own Click (Public Event Click, then RaiseEvent Click in UserControl_Click); an event named Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll reaches the program's CtrlName_Click (and so on) handler as for any control. Give those events Visual Basic's parameters: Button, Shift, X, Y for MouseDown, MouseUp and MouseMove; KeyCode, Shift for KeyDown and KeyUp; KeyAscii for KeyPress; Delta for Scroll. GotFocus and LostFocus reach the program by themselves. + +.h2 Using Controls in a Program + +In the IDE, File > New UserControl (or File > Add File with a .ctl file) adds a control to the open project; while the project is open the control is in the Toolbox, ready to be placed on the project's forms. In the project file, the .ctl file is listed in the [Controls] section of the project's .dbp: + +.code +[Controls] +File0 = gauge.ctl +Count = 1 +.endcode + +When the program starts, its controls are registered before any form is loaded, so its forms can use them; they go away when the program ends. They belong to the program: other programs (and other copies of the same program) can have controls of the same name, and each program uses its own. The control's code runs in the program, with the program's rules: an error the code does not handle ends the program, and DoEvents works as anywhere. + +.h2 Installing a Control for Every Program + +A control project holds only controls (no forms): its .dbp says + +.code +[Project] +Name = GAUGE +Type = Control + +[Controls] +File0 = gauge.ctl +Count = 1 +.endcode + +Building it -- in the IDE, File > Make Control (set the project's Type to Control in File > Project Properties first); or with the BASIC compiler, bascomp gauge.dbp (on DVX, SYSTEM\BASCOMP.EXE) -- makes gauge.wgt, a control file. Copied into DVX's WIDGETS folder (in a folder of its own, like the controls DVX comes with), it is loaded when DVX starts: every program can use its controls, the designer's toolbox shows them with their ToolboxIcon and Description, and C programs create them by name. A control project may hold several controls and the modules they need; its help file goes beside the control file as for any widget. + +All the programs using an installed control file share one copy of its code and its module variables: module-level code of the control project runs once, when the first of its controls is made. A control's own variables (its class members) are separate for each control. Because the code is shared: + +.list +.item DoEvents does nothing in an installed control's code, and its code never lets another program run in between. +.item While its code runs for one program, another program that needs it (to paint or for an event) is turned away: the control keeps its old picture and the event is dropped (DVX.LOG notes it once). +.item An error its code does not handle is shown, and the control carries on; it does not end the program that was using it. +.endlist + +.h2 Limits + +.list +.item A control cannot hold other controls; it draws what it shows on its UserControl surface. +.item Properties are numbers, Booleans, text and ENUM choices; a property of an ENUM whose members are not numbered 0, 1, 2 ... is a whole number in the Properties window. +.item CanGetFocus is fixed by the header (read-only in code). +.item UserControl_Terminate cannot draw: the surface is already gone. +.item A program cannot have a control of the same name as one installed in WIDGETS (the program stops with "Control cannot be registered"). +.endlist + +.link ide.usercontrols See also: UserControls in the IDE +.link lang.classes See also: Objects and Class Modules +.link lang.events See also: Events +.link ctrl.drawing See also: Drawing on Forms and PictureBoxes diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.c b/src/apps/kpunch/dvxhelp/dvxhelp.c index f9296d62..ef0c9841 100644 --- a/src/apps/kpunch/dvxhelp/dvxhelp.c +++ b/src/apps/kpunch/dvxhelp/dvxhelp.c @@ -23,43 +23,31 @@ // dvxhelp.c -- DVX Help Viewer application // // A callback-only DXE app that displays .hlp help files with a TreeView -// TOC sidebar and scrollable content area. Supports headings, text, -// links, images, lists, horizontal rules, notes, and code blocks. +// TOC sidebar and a HelpView (the HelpPane widget) showing the topic. +// The help file is read with the hlpfile library; the HelpView renders +// topics (the same widget BASIC programs use). // -// The content area is rebuilt per-topic by reading records from the .hlp -// file and creating private widget types (HelpText, HelpHeading, HelpLink) -// registered via wgtRegisterClass at startup. -// -// Search is full text: the trigram index the help compiler writes narrows -// the candidate topics, then each candidate's text is checked for every -// search word. -// -// Navigation history supports back/forward with a fixed-size circular stack. +// Navigation history supports back/forward with a fixed-size stack. -#include "hlpformat.h" #include "dvxApp.h" #include "dvxDlg.h" #include "dvxWgt.h" #include "dvxWgtP.h" #include "dvxWm.h" -#include "dvxDraw.h" -#include "dvxVideo.h" #include "dvxPlat.h" #include "shellApp.h" -#include "box/box.h" +#include "hlpfile/hlpFile.h" #include "button/button.h" +#include "helpView/helpView.h" #include "splitter/splitter.h" #include "treeView/treeView.h" -#include "scrollPane/scrlPane.h" #include "toolbar/toolbar.h" -#include "image/image.h" #include #include #include #include #include -#include #include "dvxMem.h" @@ -72,49 +60,17 @@ #define HELP_CASCADE_OFFSET 16 #define HELP_SPLITTER_POS 160 #define HELP_TOC_MIN_W 100 -#define HELP_CONTENT_PAD 6 -#define HELP_PARA_SPACING 12 -#define HELP_HEADING1_TOP 24 -#define HELP_HEADING1_PAD 6 -#define HELP_HEADING2_TOP 16 -#define HELP_HEADING2_PAD 4 -#define HELP_HEADING2_RULE_GAP 1 // gap between heading-2 text and its underline -#define HELP_HEADING2_RULE_H (HELP_HEADING2_RULE_GAP + 1) -#define HELP_HEADING3_TOP 8 -#define HELP_LINK_PAD_X 2 -#define HELP_LINK_PAD_Y 1 -#define HELP_NOTE_PAD 6 -#define HELP_NOTE_PREFIX_W 4 -#define HELP_CODE_PAD 4 -#define HELP_RULE_H 5 // horizontal-rule widget min height -#define HELP_RULE_LINE_Y 2 // rule line offset within the widget #define HELP_TITLE_MAX 300 // window-title buffer length -#define HELP_MIN_WRAP_COLS 10 // narrowest column count text is wrapped at #define HELP_MAX_TOC_DEPTH 16 -#define HELP_BULLET "\x07 " // list item prefix (bullet glyph + space) -#define HELP_BULLET_LEN 2 #define HELP_DEFAULT_FILE "dvxhelp.hlp" // viewer's own help, opened without arguments #define HELP_APP_TITLE "DVX Help" #define HELP_OWN_TOPIC "help.overview" // F1 inside the viewer: help on the viewer -// Horizontal space inside the content ScrollPane that text cannot use: -// the pane border on both sides (matches the ScrollPane widget), its -// vertical scrollbar and the content VBox padding. One character more is -// kept free when wrapping. -#define HELP_SCROLL_BORDER 2 -#define HELP_WRAP_OVERHEAD (HELP_SCROLL_BORDER * 2 + WGT_SB_W + HELP_CONTENT_PAD * 2) - // MAX_HISTORY -- fixed back-button history depth. 64 entries is plenty for a // help viewer; the oldest is dropped when exceeded (see the memmove below). #define MAX_HISTORY 64 #define MAX_SEARCH_BUF 128 -#define MAX_SEARCH_TERMS (MAX_SEARCH_BUF / 2) - -// Trigram posting counts are stored in a byte, so a trigram used by more -// topics than that has a truncated posting list and cannot rule topics out. -#define HELP_TRIGRAM_LEN 3 -#define HELP_POSTING_SATURATED UINT8_MAX #define CMD_BACK 100 #define CMD_FORWARD 101 @@ -126,59 +82,6 @@ #define TOPIC_TO_USERDATA(idx) ((void *)(intptr_t)((idx) + 1)) #define USERDATA_TO_TOPIC(ud) ((int32_t)(intptr_t)(ud) - 1) -// ============================================================ -// Private widget data types -// ============================================================ - -// Flowing text that is word-wrapped to the viewport: paragraphs, list -// items and notes. -typedef struct { - char *text; // original flowing text (no newlines) - char *wrapped; // cached wrapped text (with \n), NULL = needs wrap - int32_t lineCount; // line count of wrapped text - int32_t wrapWidth; // pixel width text was wrapped at (-1 = dirty) - uint8_t noteType; // HLP_NOTE_* (only notes read it) -} HelpWrapDataT; - -typedef struct { - char *text; - int32_t level; -} HelpHeadingDataT; - -typedef struct { - char *displayText; - char *targetTopicId; -} HelpLinkDataT; - -typedef struct { - char *text; - int32_t lineCount; -} HelpCodeDataT; - -// Callback for forEachRecord; returning false stops the walk. -typedef bool (*HelpRecordFnT)(const HlpRecordHdrT *hdr, const char *payload, void *userData); - -// Search words for topicMatchRecord. -typedef struct { - char *terms[MAX_SEARCH_TERMS]; - bool found[MAX_SEARCH_TERMS]; - int32_t termCount; - int32_t foundCount; -} HelpSearchT; - -// Everything read from one .hlp file. Grouped so a replacement file can -// be opened aside and swapped in only when it loads. -typedef struct { - FILE *fp; - HlpHeaderT header; - HlpTopicDirT *topicDir; - char *stringTable; - HlpTocEntryT *tocEntries; - HlpIndexEntryT *indexEntries; - uint8_t *searchData; // search section, NULL if absent - uint32_t trigramCount; -} HelpFileT; - // ============================================================ // Module state // ============================================================ @@ -187,16 +90,13 @@ static AppContextT *sAc = NULL; static DxeAppContextT *sCtx = NULL; static WindowT *sWin = NULL; static WidgetT *sTocTree = NULL; -static WidgetT *sContentScroll = NULL; -static WidgetT *sContentBox = NULL; +static WidgetT *sView = NULL; static WidgetT *sBtnBack = NULL; static WidgetT *sBtnForward = NULL; static AccelTableT *sAccel = NULL; +static HlpFileT *sFile = NULL; // the open help file static char sHlpPath[DVX_MAX_PATH]; // path of the open .hlp file -// .hlp file data (the open file; zeroed when none is open) -static HelpFileT sFile; - // Viewer modal dialogs in progress. Their list boxes show strings from // sFile, so new launch arguments that arrive meanwhile (F1 inside a // dialog) are held in sPendingArgs and applied once the last one closes. @@ -207,21 +107,6 @@ static char *sPendingArgs = NULL; static int32_t sHistory[MAX_HISTORY]; static int32_t sHistoryPos = -1; static int32_t sHistoryCount = 0; -static int32_t sCurrentTopic = -1; - -// Viewport wrap width -- the width text should wrap at, derived from the -// ScrollPane's inner area. Updated when the window is first laid out and -// on resize. Wrapping widgets use this instead of w->parent->w so that -// wide tables/code don't inflate the wrap width. -static int32_t sWrapWidth = 0; - -// Custom widget type IDs -static int32_t sHelpTextTypeId = -1; -static int32_t sHelpHeadingTypeId = -1; -static int32_t sHelpLinkTypeId = -1; -static int32_t sHelpNoteTypeId = -1; -static int32_t sHelpCodeTypeId = -1; -static int32_t sHelpRuleTypeId = -1; // ============================================================ // Prototypes @@ -229,141 +114,31 @@ static int32_t sHelpRuleTypeId = -1; static void applyNewArgs(const char *args); void appShutdown(void); -static void attachContentData(WidgetT *w, void *data, bool complete, void (*destroy)(WidgetT *w)); -static void buildContentWidgets(void); -static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); -static void closeHelpFile(void); -static bool containsNoCase(const char *hay, const char *needle); -static int32_t countLines(const char *text); static void destroyViewer(bool now); -static void displayRecord(const HlpRecordHdrT *hdr, const char *payload); -static const char *doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t padPixels); -static int32_t findTopicByIdStr(const char *topicId); static WidgetT *findTreeItemByTopic(WidgetT *node, int32_t topicIdx); -static const HlpTrigramEntryT *findTrigram(const uint8_t *tri); -static bool forEachRecord(int32_t topicIdx, HelpRecordFnT fn, void *userData); -static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font); -static void helpCodeDestroy(WidgetT *w); -static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font); -static void helpHeadingDestroy(WidgetT *w); -static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font); -static void helpLinkDestroy(WidgetT *w); -static void helpLinkFollow(WidgetT *w); -static void helpLinkOnKey(WidgetT *w, int32_t key, int32_t mod); -static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font); -static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font); -static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font); -static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void helpWrapDestroy(WidgetT *w); static void historyPush(int32_t topicIdx); -static const char *hlpString(uint32_t offset); static void indexDialog(void); -static bool isFirstChild(WidgetT *w); -static bool isSearchAlnum(uint8_t c); -static int32_t maxLineWidth(const BitmapFontT *font, const char *text); static void modalEnd(void); static void navigateBack(void); static void navigateForward(void); static void navigateToTopic(int32_t topicIdx); -static bool nextSiblingHasOwnBorder(WidgetT *w); static void onClose(WindowT *win); static void onIndex(WidgetT *w); +static void onLink(WidgetT *w, const char *target); static void onMenu(WindowT *win, int32_t menuId); static void onNavBack(WidgetT *w); static void onNavForward(WidgetT *w); static void onNewArgs(void *ctx, const char *args); -static void onResize(WindowT *win, int32_t newW, int32_t newH); static void onSearch(WidgetT *w); static void onTocChange(WidgetT *w); -static bool openHelpFile(const char *path); -static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg); static const char *parseArgs(const DxeAppContextT *ctx, const char *args, char *path, size_t pathSize); static void populateToc(void); -static bool readSearchIndex(void); -static void registerWidgetClasses(void); static void searchDialog(void); -static int32_t searchTopics(char *query, uint8_t *matches); static void showSearchResults(const uint8_t *matches, int32_t matchCount); static void showTopic(int32_t topicIdx); -static int32_t startTopic(const char *topicId); -static bool topicMatchRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); -static void unregisterWidgetClasses(void); static void updateNavButtons(void); -static void updateWrapWidth(void); -static char *wrapText(const char *text, int32_t colWidth); int32_t appMain(DxeAppContextT *ctx); -// ============================================================ -// Widget classes -// ============================================================ - -static const WidgetClassT sClassHelpText = { - .version = WGT_CLASS_VERSION, - .flags = 0, - .handlers = { - [WGT_METHOD_PAINT] = (void *)helpTextPaint, - [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpTextCalcMinSize, - [WGT_METHOD_DESTROY] = (void *)helpWrapDestroy, - } -}; - -static const WidgetClassT sClassHelpHeading = { - .version = WGT_CLASS_VERSION, - .flags = 0, - .handlers = { - [WGT_METHOD_PAINT] = (void *)helpHeadingPaint, - [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpHeadingCalcMinSize, - [WGT_METHOD_DESTROY] = (void *)helpHeadingDestroy, - } -}; - -static const WidgetClassT sClassHelpLink = { - .version = WGT_CLASS_VERSION, - .flags = WCLASS_FOCUSABLE, - .handlers = { - [WGT_METHOD_PAINT] = (void *)helpLinkPaint, - [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpLinkCalcMinSize, - [WGT_METHOD_ON_MOUSE] = (void *)helpLinkOnMouse, - [WGT_METHOD_ON_KEY] = (void *)helpLinkOnKey, - [WGT_METHOD_DESTROY] = (void *)helpLinkDestroy, - } -}; - -static const WidgetClassT sClassHelpNote = { - .version = WGT_CLASS_VERSION, - .flags = 0, - .handlers = { - [WGT_METHOD_PAINT] = (void *)helpNotePaint, - [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpNoteCalcMinSize, - [WGT_METHOD_DESTROY] = (void *)helpWrapDestroy, - } -}; - -static const WidgetClassT sClassHelpCode = { - .version = WGT_CLASS_VERSION, - .flags = 0, - .handlers = { - [WGT_METHOD_PAINT] = (void *)helpCodePaint, - [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpCodeCalcMinSize, - [WGT_METHOD_DESTROY] = (void *)helpCodeDestroy, - } -}; - -static const WidgetClassT sClassHelpRule = { - .version = WGT_CLASS_VERSION, - .flags = 0, - .handlers = { - [WGT_METHOD_PAINT] = (void *)helpRulePaint, - [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpRuleCalcMinSize, - } -}; - // ============================================================ // App descriptor // ============================================================ @@ -390,133 +165,38 @@ static void applyNewArgs(const char *args) { } if (!shellPathsMatch(path, sHlpPath)) { - HelpFileT oldFile = sFile; + HlpFileT *newFile = hlpFileOpen(path); - memset(&sFile, 0, sizeof(sFile)); - - if (!openHelpFile(path)) { - sFile = oldFile; + if (!newFile) { dvxErrorBox(sAc, HELP_APP_TITLE, "Could not open help file."); return; } - HelpFileT newFile = sFile; - - // The TOC and content refer to the old file; drop them first. + // The TOC refers to the old file; the HelpView keeps copies. widgetDestroyChildren(sTocTree); - widgetDestroyChildren(sContentBox); - sFile = oldFile; - closeHelpFile(); + wgtHelpViewClear(sView); + hlpFileClose(sFile); sFile = newFile; snprintf(sHlpPath, sizeof(sHlpPath), "%s", path); sHistoryPos = -1; sHistoryCount = 0; - sCurrentTopic = -1; populateToc(); wgtInvalidate(sTocTree); - wgtInvalidate(sContentScroll); dvxSetTitle(sAc, sWin, HELP_APP_TITLE); updateNavButtons(); } - navigateToTopic(startTopic(topicId)); + navigateToTopic(hlpFileStartTopic(sFile, topicId)); } // Shell reap hook. The shell may reap the viewer with its window still -// open; the window (and every widget of the private classes) must be gone -// before the classes are unregistered, since the DXE is unloaded next. +// open; the window must be gone before the DXE is unloaded. void appShutdown(void) { destroyViewer(true); - unregisterWidgetClasses(); -} - - -// Finishes adding a content widget. When the data or one of its strings -// could not be allocated (complete is false), the partial data is freed -// through destroy and the widget removed, so the paint and measure -// handlers never see missing text. -static void attachContentData(WidgetT *w, void *data, bool complete, void (*destroy)(WidgetT *w)) { - w->data = data; - - if (complete) { - return; - } - - destroy(w); - widgetAllocRollback(w); -} - - -static void buildContentWidgets(void) { - if (!sContentBox || sCurrentTopic < 0) { - return; - } - - // Destroy old content - widgetDestroyChildren(sContentBox); - forEachRecord(sCurrentTopic, buildRecord, NULL); - - if (sContentScroll) { - wgtScrollPaneScrollToTop(sContentScroll); - wgtInvalidate(sContentScroll); - } -} - - -// forEachRecord callback that adds one record's widget(s) to the content. -static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { - (void)userData; - displayRecord(hdr, payload); - return true; -} - - -static void closeHelpFile(void) { - if (sFile.fp) { - fclose(sFile.fp); - } - - dvxFree(sFile.topicDir); - dvxFree(sFile.stringTable); - dvxFree(sFile.tocEntries); - dvxFree(sFile.indexEntries); - dvxFree(sFile.searchData); - memset(&sFile, 0, sizeof(sFile)); -} - - -// Case-insensitive substring test. -static bool containsNoCase(const char *hay, const char *needle) { - size_t needleLen = strlen(needle); - - for (const char *p = hay; *p; p++) { - if (strncasecmp(p, needle, needleLen) == 0) { - return true; - } - } - - return false; -} - - -static int32_t countLines(const char *text) { - int32_t count = 1; - - if (!text) { - return count; - } - - for (const char *p = text; *p; p++) { - if (*p == '\n') { - count++; - } - } - - return count; } @@ -524,7 +204,8 @@ static int32_t countLines(const char *text) { // as no callback is on the stack) and releases the help file and the // accelerator table, which the window does not own. static void destroyViewer(bool now) { - closeHelpFile(); + hlpFileClose(sFile); + sFile = NULL; dvxFree(sPendingArgs); sPendingArgs = NULL; @@ -541,234 +222,12 @@ static void destroyViewer(bool now) { } dvxFreeAccelTable(sAccel); - sAccel = NULL; - sWin = NULL; - sTocTree = NULL; - sContentScroll = NULL; - sContentBox = NULL; - sBtnBack = NULL; - sBtnForward = NULL; -} - - -static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) { - if (!sContentBox) { - return; - } - - switch (hdr->type) { - case HLP_REC_TEXT: - case HLP_REC_LIST_ITEM: - case HLP_REC_NOTE: { - WidgetT *w = widgetAlloc(sContentBox, (hdr->type == HLP_REC_NOTE) ? sHelpNoteTypeId : sHelpTextTypeId); - - if (!w) { - break; - } - - HelpWrapDataT *wd = (HelpWrapDataT *)dvxCalloc(1, sizeof(HelpWrapDataT)); - - if (wd) { - if (hdr->type == HLP_REC_LIST_ITEM) { - size_t pLen = strlen(payload); - wd->text = (char *)dvxMalloc(HELP_BULLET_LEN + pLen + 1); - - if (wd->text) { - memcpy(wd->text, HELP_BULLET, HELP_BULLET_LEN); - memcpy(wd->text + HELP_BULLET_LEN, payload, pLen + 1); - } - } else { - wd->text = dvxStrdup(payload); - } - - wd->wrapWidth = -1; - wd->noteType = hdr->flags; - } - - attachContentData(w, wd, wd && wd->text, helpWrapDestroy); - break; - } - - case HLP_REC_HEADING1: - case HLP_REC_HEADING2: - case HLP_REC_HEADING3: { - WidgetT *w = widgetAlloc(sContentBox, sHelpHeadingTypeId); - - if (!w) { - break; - } - - HelpHeadingDataT *hd = (HelpHeadingDataT *)dvxCalloc(1, sizeof(HelpHeadingDataT)); - - if (hd) { - hd->text = dvxStrdup(payload); - hd->level = hdr->type - HLP_REC_HEADING1 + 1; - } - - attachContentData(w, hd, hd && hd->text, helpHeadingDestroy); - break; - } - - case HLP_REC_LINK: { - // Payload: target topic ID \0 display text \0 - size_t targetLen = strlen(payload); - - // Reject only a payload with no terminator within bounds so - // displayText doesn't point past the allocation. A payload of - // exactly targetLen + 1 bytes is the writer's legal encoding of - // a link with empty display text (displayText then points at the - // NUL the reader appended at payload[length]). - if (targetLen + 1 > hdr->length) { - break; - } - - const char *targetTopicId = payload; - const char *displayText = payload + targetLen + 1; - - // Empty display text: fall back to the target ID, matching the - // compiler's own HTML emitter. - if (displayText[0] == '\0') { - displayText = targetTopicId; - } - - WidgetT *w = widgetAlloc(sContentBox, sHelpLinkTypeId); - - if (!w) { - break; - } - - HelpLinkDataT *ld = (HelpLinkDataT *)dvxCalloc(1, sizeof(HelpLinkDataT)); - - if (ld) { - ld->displayText = dvxStrdup(displayText); - ld->targetTopicId = dvxStrdup(targetTopicId); - } - - attachContentData(w, ld, ld && ld->displayText && ld->targetTopicId, helpLinkDestroy); - break; - } - - case HLP_REC_IMAGE: { - if (hdr->length >= sizeof(HlpImageRefT)) { - const HlpImageRefT *imgRef = (const HlpImageRefT *)payload; - uint32_t absOffset = sFile.header.imagePoolOffset + imgRef->imageOffset; - - // Save file position -- the record walk reads records - // sequentially and we must not disturb its position. - long savedPos = ftell(sFile.fp); - - uint8_t *bmpData = (uint8_t *)dvxMalloc(imgRef->imageSize); - - if (bmpData) { - fseek(sFile.fp, absOffset, SEEK_SET); - size_t bytesRead = fread(bmpData, 1, imgRef->imageSize, sFile.fp); - - if (bytesRead == imgRef->imageSize) { - int32_t imgW = 0; - int32_t imgH = 0; - int32_t imgP = 0; - bool hasAlpha = false; - uint32_t keyColor = 0; - uint8_t *pixels = dvxLoadImageAlpha(sAc, bmpData, (int32_t)imgRef->imageSize, &imgW, &imgH, &imgP, &hasAlpha, &keyColor); - - if (pixels) { - WidgetT *parent = sContentBox; - - if (hdr->flags == HLP_IMG_CENTER || hdr->flags == HLP_IMG_RIGHT) { - WidgetT *row = wgtHBox(sContentBox); - - if (row) { - row->align = (hdr->flags == HLP_IMG_CENTER) ? AlignCenterE : AlignEndE; - parent = row; - } - } - - // wgtImage takes ownership of pixels even on - // failure (it frees them itself before returning - // NULL), so no cleanup is needed here. - WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP); - - if (imgWgt && hasAlpha) { - wgtImageSetTransparent(imgWgt, true, keyColor); - } - } - } - - dvxFree(bmpData); - } - - // Restore file position for sequential record reading - fseek(sFile.fp, savedPos, SEEK_SET); - } - - break; - } - - case HLP_REC_HRULE: { - widgetAlloc(sContentBox, sHelpRuleTypeId); - break; - } - - // Tables render as preformatted text, identical to code blocks. - case HLP_REC_TABLE: - case HLP_REC_CODE: { - WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId); - - if (!w) { - break; - } - - HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT)); - - if (cd) { - cd->text = dvxStrdup(payload); - cd->lineCount = countLines(cd->text); - } - - attachContentData(w, cd, cd && cd->text, helpCodeDestroy); - break; - } - } -} - - -// Wraps the widget's text at the given pixel width minus padPixels and -// caches the result -- only re-wraps when pixelW changes. Returns the -// wrapped string and updates lineCount. -static const char *doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t padPixels) { - if (wd->wrapped && wd->wrapWidth == pixelW) { - return wd->wrapped; - } - - int32_t cols = (pixelW - padPixels) / charW; - - if (cols < HELP_MIN_WRAP_COLS) { - cols = HELP_MIN_WRAP_COLS; - } - - dvxFree(wd->wrapped); - wd->wrapped = wrapText(wd->text, cols); - wd->wrapWidth = pixelW; - wd->lineCount = countLines(wd->wrapped); - return wd->wrapped; -} - - -static int32_t findTopicByIdStr(const char *topicId) { - if (!topicId || !sFile.topicDir) { - return -1; - } - - // Linear search -- topic directory is typically small - for (uint32_t i = 0; i < sFile.header.topicCount; i++) { - const char *id = hlpString(sFile.topicDir[i].topicIdStr); - - if (strcmp(id, topicId) == 0) { - return (int32_t)i; - } - } - - return -1; + sAccel = NULL; + sWin = NULL; + sTocTree = NULL; + sView = NULL; + sBtnBack = NULL; + sBtnForward = NULL; } @@ -789,343 +248,6 @@ static WidgetT *findTreeItemByTopic(WidgetT *node, int32_t topicIdx) { } -// Binary search of the (sorted) trigram table. Returns NULL when no -// topic contains the trigram. -static const HlpTrigramEntryT *findTrigram(const uint8_t *tri) { - const HlpTrigramEntryT *entries = (const HlpTrigramEntryT *)(sFile.searchData + sizeof(HlpSearchHeaderT)); - int32_t lo = 0; - int32_t hi = (int32_t)sFile.trigramCount - 1; - - while (lo <= hi) { - int32_t mid = lo + (hi - lo) / 2; - int32_t cmp = memcmp(entries[mid].trigram, tri, HELP_TRIGRAM_LEN); - - if (cmp == 0) { - return &entries[mid]; - } - - if (cmp < 0) { - lo = mid + 1; - } else { - hi = mid - 1; - } - } - - return NULL; -} - - -// Reads a topic's content records in order and passes each one (payload -// NUL-terminated) to fn until fn returns false or the topic ends. -// Returns false on a read or allocation failure. -static bool forEachRecord(int32_t topicIdx, HelpRecordFnT fn, void *userData) { - const HlpTopicDirT *topic = &sFile.topicDir[topicIdx]; - uint32_t bytesRead = 0; - char *payload = NULL; - uint32_t payloadSz = 0; - bool ok = true; - - if (fseek(sFile.fp, topic->contentOffset, SEEK_SET) != 0) { - return false; - } - - while (bytesRead < topic->contentSize) { - HlpRecordHdrT recHdr; - - if (fread(&recHdr, sizeof(recHdr), 1, sFile.fp) != 1) { - ok = false; - break; - } - - bytesRead += sizeof(recHdr); - - if (recHdr.type == HLP_REC_END) { - break; - } - - // One buffer serves every record of the topic, grown as needed. - if (recHdr.length + 1 > payloadSz) { - char *grown = (char *)dvxRealloc(payload, recHdr.length + 1); - - if (!grown) { - ok = false; - break; - } - - payload = grown; - payloadSz = recHdr.length + 1; - } - - if (fread(payload, 1, recHdr.length, sFile.fp) != recHdr.length) { - ok = false; - break; - } - - payload[recHdr.length] = '\0'; - bytesRead += recHdr.length; - - if (!fn(&recHdr, payload, userData)) { - break; - } - } - - dvxFree(payload); - return ok; -} - - -static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font) { - HelpCodeDataT *cd = (HelpCodeDataT *)w->data; - w->calcMinW = maxLineWidth(font, cd->text) + HELP_CONTENT_PAD * 2 + HELP_CODE_PAD * 2; - w->calcMinH = cd->lineCount * font->charHeight + HELP_CODE_PAD * 2; -} - - -static void helpCodeDestroy(WidgetT *w) { - HelpCodeDataT *cd = (HelpCodeDataT *)w->data; - - if (cd) { - dvxFree(cd->text); - dvxFree(cd); - w->data = NULL; - } -} - - -static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - HelpCodeDataT *cd = (HelpCodeDataT *)w->data; - - int32_t boxX = w->x + HELP_CONTENT_PAD; - int32_t boxW = w->w - HELP_CONTENT_PAD * 2; - - // Sunken background - rectFill(d, ops, boxX, w->y, boxW, w->h, colors->contentBg); - drawHLine(d, ops, boxX, w->y, boxW, colors->windowShadow); - drawHLine(d, ops, boxX, w->y + w->h - 1, boxW, colors->windowHighlight); - - paintLines(d, ops, font, boxX + HELP_CODE_PAD, w->y + HELP_CODE_PAD, cd->text, cd->lineCount, colors->contentFg); -} - - -static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) { - HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; - int32_t textH = font->charHeight; - bool first = isFirstChild(w); - - w->calcMinW = 0; - - if (hd->level == 1) { - int32_t top = first ? 0 : HELP_HEADING1_TOP; - w->calcMinH = top + textH + HELP_HEADING1_PAD * 2; - } else if (hd->level == 2) { - int32_t top = first ? 0 : HELP_HEADING2_TOP; - int32_t extra = nextSiblingHasOwnBorder(w) ? 0 : HELP_HEADING2_RULE_H; - w->calcMinH = top + textH + HELP_HEADING2_PAD + extra; - } else { - int32_t top = first ? 0 : HELP_HEADING3_TOP; - w->calcMinH = top + textH; - } -} - - -static void helpHeadingDestroy(WidgetT *w) { - HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; - - if (hd) { - dvxFree(hd->text); - dvxFree(hd); - w->data = NULL; - } -} - - -static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; - int32_t textLen = (int32_t)strlen(hd->text); - bool first = isFirstChild(w); - - if (hd->level == 1) { - int32_t top = first ? 0 : HELP_HEADING1_TOP; - int32_t barY = w->y + top; - int32_t barH = w->h - top; - rectFill(d, ops, w->x, barY, w->w, barH, colors->activeTitleBg); - int32_t textY = barY + HELP_HEADING1_PAD; - drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->activeTitleFg, 0, false); - } else if (hd->level == 2) { - int32_t top = first ? 0 : HELP_HEADING2_TOP; - int32_t textY = w->y + top; - drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false); - - // Suppress the underline when the next sibling paints its own - // top border, so we don't end up with two nearly-adjacent lines. - // Tables and code blocks both use sHelpCodeTypeId; .hr uses - // sHelpRuleTypeId. - if (!nextSiblingHasOwnBorder(w)) { - int32_t lineY = textY + font->charHeight + HELP_HEADING2_RULE_GAP; - drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); - } - } else { - int32_t top = first ? 0 : HELP_HEADING3_TOP; - int32_t textY = w->y + top; - drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false); - } -} - - -static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = 0; - w->calcMinH = font->charHeight + HELP_LINK_PAD_Y * 2; -} - - -static void helpLinkDestroy(WidgetT *w) { - HelpLinkDataT *ld = (HelpLinkDataT *)w->data; - - if (ld) { - dvxFree(ld->displayText); - dvxFree(ld->targetTopicId); - dvxFree(ld); - w->data = NULL; - } -} - - -// Navigates to the link's target topic. This rebuilds the content, which -// destroys w, so callers must not touch w afterwards. -static void helpLinkFollow(WidgetT *w) { - HelpLinkDataT *ld = (HelpLinkDataT *)w->data; - int32_t topicIdx = findTopicByIdStr(ld->targetTopicId); - - if (topicIdx >= 0) { - navigateToTopic(topicIdx); - } -} - - -static void helpLinkOnKey(WidgetT *w, int32_t key, int32_t mod) { - (void)mod; - - if (key == KEY_ENTER || key == KEY_SPACE) { - helpLinkFollow(w); - } -} - - -static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { - (void)root; - (void)vx; - (void)vy; - helpLinkFollow(w); -} - - -static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - HelpLinkDataT *ld = (HelpLinkDataT *)w->data; - int32_t textLen = (int32_t)strlen(ld->displayText); - int32_t x = w->x + HELP_CONTENT_PAD + HELP_LINK_PAD_X; - int32_t y = w->y + HELP_LINK_PAD_Y; - int32_t lineW = textLen * font->charWidth; - - // Use active title color as link color - uint32_t linkColor = colors->activeTitleBg; - - drawTextN(d, ops, font, x, y, ld->displayText, textLen, linkColor, 0, false); - - // Underline - drawHLine(d, ops, x, y + font->charHeight, lineW, linkColor); - - if (w == sFocusedWidget) { - drawFocusRect(d, ops, x - HELP_LINK_PAD_X, w->y, lineW + HELP_LINK_PAD_X * 2, w->h, colors->contentFg); - } -} - - -static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font) { - HelpWrapDataT *wd = (HelpWrapDataT *)w->data; - updateWrapWidth(); - - if (sWrapWidth > 0) { - doWrap(wd, sWrapWidth, font->charWidth, HELP_NOTE_PAD * 2 + HELP_NOTE_PREFIX_W); - } - - w->calcMinW = 0; - w->calcMinH = wd->lineCount * font->charHeight + HELP_NOTE_PAD * 2; -} - - -static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - HelpWrapDataT *wd = (HelpWrapDataT *)w->data; - - uint32_t barColor; - - if (wd->noteType == HLP_NOTE_WARNING) { - barColor = colors->activeTitleBg; - } else if (wd->noteType == HLP_NOTE_TIP) { - barColor = colors->menuHighlightBg; - } else { - barColor = colors->windowShadow; - } - - int32_t boxX = w->x + HELP_CONTENT_PAD; - int32_t boxW = w->w - HELP_CONTENT_PAD * 2; - - rectFill(d, ops, boxX, w->y, boxW, w->h, colors->windowFace); - rectFill(d, ops, boxX, w->y, HELP_NOTE_PREFIX_W, w->h, barColor); - - int32_t textX = boxX + HELP_NOTE_PREFIX_W + HELP_NOTE_PAD; - const char *text = wd->wrapped ? wd->wrapped : wd->text; - paintLines(d, ops, font, textX, w->y + HELP_NOTE_PAD, text, wd->lineCount, colors->contentFg); -} - - -static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font) { - (void)font; - w->calcMinW = 0; - w->calcMinH = HELP_RULE_H; -} - - -static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - (void)font; - int32_t lineY = w->y + HELP_RULE_LINE_Y; - drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); - drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY + 1, w->w - HELP_CONTENT_PAD * 2, colors->windowHighlight); -} - - -// Paragraphs and list items. -static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font) { - HelpWrapDataT *wd = (HelpWrapDataT *)w->data; - updateWrapWidth(); - - if (sWrapWidth > 0) { - doWrap(wd, sWrapWidth, font->charWidth, 0); - } - - w->calcMinW = 0; - w->calcMinH = wd->lineCount * font->charHeight; -} - - -static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - HelpWrapDataT *wd = (HelpWrapDataT *)w->data; - const char *text = wd->wrapped ? wd->wrapped : wd->text; - paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, text, wd->lineCount, colors->contentFg); -} - - -static void helpWrapDestroy(WidgetT *w) { - HelpWrapDataT *wd = (HelpWrapDataT *)w->data; - - if (wd) { - dvxFree(wd->wrapped); - dvxFree(wd->text); - dvxFree(wd); - w->data = NULL; - } -} - - static void historyPush(int32_t topicIdx) { // Truncate forward history when navigating to a new topic sHistoryPos++; @@ -1141,85 +263,35 @@ static void historyPush(int32_t topicIdx) { } -static const char *hlpString(uint32_t offset) { - if (!sFile.stringTable || offset >= sFile.header.stringTableSize) { - return ""; - } - - return sFile.stringTable + offset; -} - - static void indexDialog(void) { - if (!sFile.indexEntries || sFile.header.indexCount == 0) { + int32_t count = hlpFileIndexCount(sFile); + + if (count == 0) { dvxInfoBox(sAc, HELP_APP_TITLE, "No index available."); return; } // Build list of keyword strings - const char **items = (const char **)dvxMalloc(sFile.header.indexCount * sizeof(const char *)); + const char **items = (const char **)dvxMalloc(count * sizeof(const char *)); if (!items) { return; } - for (uint32_t i = 0; i < sFile.header.indexCount; i++) { - items[i] = hlpString(sFile.indexEntries[i].keywordStr); + for (int32_t i = 0; i < count; i++) { + items[i] = hlpFileString(sFile, hlpFileIndex(sFile, i)->keywordStr); } int32_t selected = -1; - bool ok = dvxChoiceDialog(sAc, "Index", "Select a topic:", items, (int32_t)sFile.header.indexCount, 0, &selected); - if (ok && selected >= 0 && (uint32_t)selected < sFile.header.indexCount) { - navigateToTopic((int32_t)sFile.indexEntries[selected].topicIdx); + if (dvxChoiceDialog(sAc, "Index", "Select a topic:", items, count, 0, &selected) && selected >= 0 && selected < count) { + navigateToTopic((int32_t)hlpFileIndex(sFile, selected)->topicIdx); } dvxFree(items); } -static bool isFirstChild(WidgetT *w) { - return w->parent && w->parent->firstChild == w; -} - - -// The help compiler builds trigrams only from ASCII letters and digits -// (after lowercasing), so queries are split the same way. -static bool isSearchAlnum(uint8_t c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z'); -} - - -static int32_t maxLineWidth(const BitmapFontT *font, const char *text) { - int32_t maxW = 0; - int32_t lineLen = 0; - - if (!text) { - return 0; - } - - for (const char *p = text; ; p++) { - if (*p == '\n' || *p == '\0') { - int32_t w = lineLen * font->charWidth; - - if (w > maxW) { - maxW = w; - } - - lineLen = 0; - - if (*p == '\0') { - break; - } - } else { - lineLen++; - } - } - - return maxW; -} - - // Ends a viewer modal section (see sModalDepth). Leaving the outermost one // applies the launch arguments that arrived while it was open. static void modalEnd(void) { @@ -1258,11 +330,7 @@ static void navigateForward(void) { static void navigateToTopic(int32_t topicIdx) { - if (topicIdx < 0 || (uint32_t)topicIdx >= sFile.header.topicCount) { - return; - } - - if (topicIdx == sCurrentTopic) { + if (topicIdx < 0 || topicIdx >= hlpFileTopicCount(sFile) || topicIdx == wgtHelpViewGetTopic(sView)) { return; } @@ -1271,15 +339,6 @@ static void navigateToTopic(int32_t topicIdx) { } -// True if w's next sibling is a code or rule widget, which draws its own top -// border -- the heading then omits its bottom spacing to avoid a double rule. -static bool nextSiblingHasOwnBorder(WidgetT *w) { - return w->nextSibling && - (w->nextSibling->type == sHelpCodeTypeId || - w->nextSibling->type == sHelpRuleTypeId); -} - - static void onClose(WindowT *win) { (void)win; destroyViewer(false); @@ -1294,6 +353,14 @@ static void onIndex(WidgetT *w) { } +// A link in the topic was followed. Showing its topic rebuilds the +// HelpView's content (the link included). +static void onLink(WidgetT *w, const char *target) { + (void)w; + navigateToTopic(hlpFileFindTopic(sFile, target)); +} + + static void onMenu(WindowT *win, int32_t menuId) { switch (menuId) { case CMD_BACK: @@ -1349,22 +416,6 @@ static void onNewArgs(void *ctx, const char *args) { } -// The text widgets wrap while being measured, which happens before the -// layout pass gives the ScrollPane its new width. After the normal resize -// handling, re-measure once more if the wrap width changed so text is not -// left wrapped at the old width. -static void onResize(WindowT *win, int32_t newW, int32_t newH) { - int32_t oldWrap = sWrapWidth; - - widgetOnResize(win, newW, newH); - updateWrapWidth(); - - if (sWrapWidth != oldWrap && sContentScroll) { - wgtInvalidate(sContentScroll); - } -} - - static void onSearch(WidgetT *w) { (void)w; sModalDepth++; @@ -1376,135 +427,8 @@ static void onSearch(WidgetT *w) { static void onTocChange(WidgetT *w) { WidgetT *selected = wgtTreeViewGetSelected(w); - if (!selected || !selected->userData) { - return; - } - - int32_t topicIdx = USERDATA_TO_TOPIC(selected->userData); - - if (topicIdx < 0 || topicIdx >= (int32_t)sFile.header.topicCount) { - return; - } - - navigateToTopic(topicIdx); -} - - -static bool openHelpFile(const char *path) { - sFile.fp = fopen(path, "rb"); - - if (!sFile.fp) { - return false; - } - - // Read header from end of file, then validate magic and version - if (fseek(sFile.fp, -(long)sizeof(HlpHeaderT), SEEK_END) != 0 || - fread(&sFile.header, sizeof(HlpHeaderT), 1, sFile.fp) != 1 || - sFile.header.magic != HLP_MAGIC || - sFile.header.version != HLP_VERSION) { - closeHelpFile(); - return false; - } - - // Read topic directory - sFile.topicDir = (HlpTopicDirT *)dvxMalloc(sFile.header.topicCount * sizeof(HlpTopicDirT)); - - if (!sFile.topicDir) { - closeHelpFile(); - return false; - } - - fseek(sFile.fp, sFile.header.topicDirOffset, SEEK_SET); - - if (fread(sFile.topicDir, sizeof(HlpTopicDirT), sFile.header.topicCount, sFile.fp) != sFile.header.topicCount) { - closeHelpFile(); - return false; - } - - // Read string table - sFile.stringTable = (char *)dvxMalloc(sFile.header.stringTableSize); - - if (!sFile.stringTable) { - closeHelpFile(); - return false; - } - - fseek(sFile.fp, sFile.header.stringTableOffset, SEEK_SET); - - if (fread(sFile.stringTable, 1, sFile.header.stringTableSize, sFile.fp) != sFile.header.stringTableSize) { - closeHelpFile(); - return false; - } - - // Force-terminate the table so a truncated/corrupt file can't make any - // hlpString() consumer walk past the allocation. - if (sFile.header.stringTableSize > 0) { - sFile.stringTable[sFile.header.stringTableSize - 1] = '\0'; - } - - // Read TOC entries - if (sFile.header.tocCount > 0) { - sFile.tocEntries = (HlpTocEntryT *)dvxMalloc(sFile.header.tocCount * sizeof(HlpTocEntryT)); - - if (!sFile.tocEntries) { - closeHelpFile(); - return false; - } - - fseek(sFile.fp, sFile.header.tocOffset, SEEK_SET); - - if (fread(sFile.tocEntries, sizeof(HlpTocEntryT), sFile.header.tocCount, sFile.fp) != sFile.header.tocCount) { - closeHelpFile(); - return false; - } - } - - // Read keyword index - if (sFile.header.indexCount > 0) { - sFile.indexEntries = (HlpIndexEntryT *)dvxMalloc(sFile.header.indexCount * sizeof(HlpIndexEntryT)); - - if (!sFile.indexEntries) { - closeHelpFile(); - return false; - } - - fseek(sFile.fp, sFile.header.indexOffset, SEEK_SET); - - if (fread(sFile.indexEntries, sizeof(HlpIndexEntryT), sFile.header.indexCount, sFile.fp) != sFile.header.indexCount) { - closeHelpFile(); - return false; - } - } - - // A missing or damaged search index only makes searching slower - // (every topic is scanned), so it does not fail the open. - if (!readSearchIndex()) { - dvxFree(sFile.searchData); - sFile.searchData = NULL; - sFile.trigramCount = 0; - } - - return true; -} - - -static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg) { - if (!text) { - return; - } - - const char *line = text; - - for (int32_t i = 0; i < lineCount; i++) { - const char *end = strchr(line, '\n'); - int32_t len = end ? (int32_t)(end - line) : (int32_t)strlen(line); - - if (len > 0) { - drawTextN(d, ops, font, x, y, line, len, fg, 0, false); - } - - y += font->charHeight; - line = end ? end + 1 : line + len; + if (selected && selected->userData) { + navigateToTopic(USERDATA_TO_TOPIC(selected->userData)); } } @@ -1527,10 +451,6 @@ static const char *parseArgs(const DxeAppContextT *ctx, const char *args, char * static void populateToc(void) { - if (!sTocTree || !sFile.tocEntries) { - return; - } - // Track parent nodes at each depth level WidgetT *parents[HELP_MAX_TOC_DEPTH]; @@ -1538,84 +458,43 @@ static void populateToc(void) { parents[i] = sTocTree; } - for (uint32_t i = 0; i < sFile.header.tocCount; i++) { - HlpTocEntryT *entry = &sFile.tocEntries[i]; - const char *title = hlpString(entry->titleStr); - int32_t depth = entry->depth; + for (int32_t i = 0; i < hlpFileTocCount(sFile); i++) { + const HlpTocEntryT *entry = hlpFileToc(sFile, i); + int32_t depth = entry->depth; if (depth >= HELP_MAX_TOC_DEPTH) { depth = HELP_MAX_TOC_DEPTH - 1; } - WidgetT *parent = parents[depth]; - WidgetT *item = wgtTreeItem(parent, title); + WidgetT *item = wgtTreeItem(parents[depth], hlpFileString(sFile, entry->titleStr)); - if (item) { - // Store topic index in userData (+1 so topic 0 != NULL); a - // section header without a page of its own keeps NULL. - if (entry->topicIdx != HLP_NO_TOPIC) { - item->userData = TOPIC_TO_USERDATA(entry->topicIdx); - } + if (!item) { + continue; + } - // Update parent for next deeper level - if (depth + 1 < HELP_MAX_TOC_DEPTH) { - parents[depth + 1] = item; - } + // Store topic index in userData (+1 so topic 0 != NULL); a + // section header without a page of its own keeps NULL. + if (entry->topicIdx != HLP_NO_TOPIC) { + item->userData = TOPIC_TO_USERDATA(entry->topicIdx); + } - // Expand by default if flagged - if (entry->flags & HLP_TOC_EXPANDED) { - wgtTreeItemSetExpanded(item, true); - } + // Update parent for next deeper level + if (depth + 1 < HELP_MAX_TOC_DEPTH) { + parents[depth + 1] = item; + } + + // Expand by default if flagged + if (entry->flags & HLP_TOC_EXPANDED) { + wgtTreeItemSetExpanded(item, true); } } } -// Loads the trigram search section into sFile.searchData. Returns false when -// the section is missing or does not hold together (the caller then drops -// it). Posting lists are range-checked when used. -static bool readSearchIndex(void) { - if (sFile.header.searchSize < sizeof(HlpSearchHeaderT)) { - return false; - } - - sFile.searchData = (uint8_t *)dvxMalloc(sFile.header.searchSize); - - if (!sFile.searchData) { - return false; - } - - if (fseek(sFile.fp, sFile.header.searchOffset, SEEK_SET) != 0 || - fread(sFile.searchData, 1, sFile.header.searchSize, sFile.fp) != sFile.header.searchSize) { - return false; - } - - HlpSearchHeaderT searchHdr; - memcpy(&searchHdr, sFile.searchData, sizeof(searchHdr)); - - uint32_t maxEntries = (sFile.header.searchSize - (uint32_t)sizeof(HlpSearchHeaderT)) / (uint32_t)sizeof(HlpTrigramEntryT); - - if (searchHdr.trigramCount > maxEntries) { - return false; - } - - sFile.trigramCount = searchHdr.trigramCount; - return true; -} - - -static void registerWidgetClasses(void) { - sHelpTextTypeId = wgtRegisterClass(&sClassHelpText); - sHelpHeadingTypeId = wgtRegisterClass(&sClassHelpHeading); - sHelpLinkTypeId = wgtRegisterClass(&sClassHelpLink); - sHelpNoteTypeId = wgtRegisterClass(&sClassHelpNote); - sHelpCodeTypeId = wgtRegisterClass(&sClassHelpCode); - sHelpRuleTypeId = wgtRegisterClass(&sClassHelpRule); -} - - static void searchDialog(void) { - if (!sFile.topicDir || sFile.header.topicCount == 0) { + int32_t topicCount = hlpFileTopicCount(sFile); + + if (topicCount == 0) { return; } @@ -1625,13 +504,13 @@ static void searchDialog(void) { return; } - uint8_t *matches = (uint8_t *)dvxCalloc(sFile.header.topicCount, sizeof(uint8_t)); + uint8_t *matches = (uint8_t *)dvxMalloc(topicCount); if (!matches) { return; } - int32_t matchCount = searchTopics(searchBuf, matches); + int32_t matchCount = hlpFileSearch(sFile, searchBuf, matches); if (matchCount > 0) { showSearchResults(matches, matchCount); @@ -1643,135 +522,6 @@ static void searchDialog(void) { } -// Full-text search. query holds whitespace-separated words (it is -// lowercased and split in place); a topic matches when its title or text -// contains every word, or when an index keyword contains the whole query. -// Words too short for the trigram index cannot narrow the candidates, so -// when no word has HELP_TRIGRAM_LEN letters or digits in a row only titles -// and keywords are searched (topic text is scanned only without an index). -// Sets matches[topic] for each match and returns the match count, or -1 -// on an allocation or read failure. -static int32_t searchTopics(char *query, uint8_t *matches) { - uint32_t topicCount = sFile.header.topicCount; - HelpSearchT search; - char wholeQuery[MAX_SEARCH_BUF]; - - memset(&search, 0, sizeof(search)); - - for (char *p = query; *p; p++) { - if (*p >= 'A' && *p <= 'Z') { - *p = (char)(*p - 'A' + 'a'); - } - } - - snprintf(wholeQuery, sizeof(wholeQuery), "%s", dvxSkipWs(query)); - dvxTrimRight(wholeQuery); - - for (char *tok = strtok(query, " \t"); tok && search.termCount < MAX_SEARCH_TERMS; tok = strtok(NULL, " \t")) { - search.terms[search.termCount++] = tok; - } - - if (search.termCount == 0) { - return 0; - } - - // Every topic starts as a candidate; each trigram of each word that is - // not in a topic's posting list rules that topic out. - uint8_t *inPosting = (uint8_t *)dvxMalloc(topicCount); - - if (!inPosting) { - return -1; - } - - memset(matches, 1, topicCount); - - bool titlesOnly = sFile.searchData != NULL; - - for (int32_t t = 0; t < search.termCount && sFile.searchData; t++) { - const uint8_t *term = (const uint8_t *)search.terms[t]; - - for (int32_t i = 0; term[i] && term[i + 1] && term[i + 2]; i++) { - if (!isSearchAlnum(term[i]) || !isSearchAlnum(term[i + 1]) || !isSearchAlnum(term[i + 2])) { - continue; - } - - titlesOnly = false; - - const HlpTrigramEntryT *entry = findTrigram(&term[i]); - - if (!entry) { - memset(matches, 0, topicCount); - break; - } - - uint32_t postBytes = (uint32_t)entry->postingCount * (uint32_t)sizeof(uint16_t); - - if (entry->postingCount == HELP_POSTING_SATURATED || entry->postingOffset > sFile.header.searchSize || postBytes > sFile.header.searchSize - entry->postingOffset) { - continue; - } - - memset(inPosting, 0, topicCount); - - for (uint32_t k = 0; k < entry->postingCount; k++) { - uint16_t topicIdx; - memcpy(&topicIdx, sFile.searchData + entry->postingOffset + k * sizeof(uint16_t), sizeof(topicIdx)); - - if (topicIdx < topicCount) { - inPosting[topicIdx] = 1; - } - } - - for (uint32_t k = 0; k < topicCount; k++) { - matches[k] &= inPosting[k]; - } - } - } - - dvxFree(inPosting); - - // The trigram index only proves the letters occur somewhere in the - // topic; confirm each candidate really contains every word. - int32_t matchCount = 0; - - for (uint32_t k = 0; k < topicCount; k++) { - if (!matches[k]) { - continue; - } - - memset(search.found, 0, sizeof(search.found)); - search.foundCount = 0; - - const char *title = hlpString(sFile.topicDir[k].titleStr); - - for (int32_t t = 0; t < search.termCount; t++) { - if (containsNoCase(title, search.terms[t])) { - search.found[t] = true; - search.foundCount++; - } - } - - if (search.foundCount < search.termCount && !titlesOnly && !forEachRecord((int32_t)k, topicMatchRecord, &search)) { - return -1; - } - - matches[k] = (search.foundCount == search.termCount); - matchCount += matches[k]; - } - - // Keyword index entries that contain the whole query also match. - for (uint32_t i = 0; i < sFile.header.indexCount; i++) { - uint16_t topicIdx = sFile.indexEntries[i].topicIdx; - - if (topicIdx < topicCount && !matches[topicIdx] && containsNoCase(hlpString(sFile.indexEntries[i].keywordStr), wholeQuery)) { - matches[topicIdx] = 1; - matchCount++; - } - } - - return matchCount; -} - - // Goes straight to a single match, otherwise lets the user pick one. static void showSearchResults(const uint8_t *matches, int32_t matchCount) { const char **items = (const char **)dvxMalloc(matchCount * sizeof(const char *)); @@ -1784,10 +534,10 @@ static void showSearchResults(const uint8_t *matches, int32_t matchCount) { return; } - for (uint32_t k = 0; k < sFile.header.topicCount && n < matchCount; k++) { + for (int32_t k = 0; k < hlpFileTopicCount(sFile) && n < matchCount; k++) { if (matches[k]) { - items[n] = hlpString(sFile.topicDir[k].titleStr); - topics[n] = (int32_t)k; + items[n] = hlpFileTopicTitle(sFile, k); + topics[n] = k; n++; } } @@ -1796,6 +546,7 @@ static void showSearchResults(const uint8_t *matches, int32_t matchCount) { if (n > 1) { char prompt[MAX_SEARCH_BUF]; + snprintf(prompt, sizeof(prompt), "%ld topics found:", (long)n); if (!dvxChoiceDialog(sAc, "Search Results", prompt, items, n, 0, &selected)) { @@ -1816,186 +567,30 @@ static void showSearchResults(const uint8_t *matches, int32_t matchCount) { // three navigation paths so Back/Forward don't leave a stale title/selection. // Does NOT push history -- callers manage sHistory. static void showTopic(int32_t topicIdx) { - sCurrentTopic = topicIdx; - buildContentWidgets(); + char winTitle[HELP_TITLE_MAX]; + + wgtHelpViewShowTopic(sView, sFile, topicIdx); updateNavButtons(); - const char *title = hlpString(sFile.topicDir[topicIdx].titleStr); - char winTitle[HELP_TITLE_MAX]; - snprintf(winTitle, sizeof(winTitle), "%s - " HELP_APP_TITLE, title); + snprintf(winTitle, sizeof(winTitle), "%s - " HELP_APP_TITLE, hlpFileTopicTitle(sFile, topicIdx)); dvxSetTitle(sAc, sWin, winTitle); - if (sTocTree) { - WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx); + WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx); - if (item) { - // Suppress onChange to avoid re-entering navigation - void (*savedOnChange)(WidgetT *) = sTocTree->onChange; - sTocTree->onChange = NULL; - wgtTreeViewSetSelected(sTocTree, item); - sTocTree->onChange = savedOnChange; - } + if (item) { + // Suppress onChange to avoid re-entering navigation + void (*savedOnChange)(WidgetT *) = sTocTree->onChange; + + sTocTree->onChange = NULL; + wgtTreeViewSetSelected(sTocTree, item); + sTocTree->onChange = savedOnChange; } } -// The topic to open for a "file [topicId]" request: the named topic, else -// the file's default topic, else the first topic (-1 for an empty file). -static int32_t startTopic(const char *topicId) { - int32_t topic = topicId ? findTopicByIdStr(topicId) : -1; - - if (topic < 0) { - topic = findTopicByIdStr(hlpString(sFile.header.defaultTopicStr)); - } - - if (topic < 0 && sFile.header.topicCount > 0) { - topic = 0; - } - - return topic; -} - - -// forEachRecord callback for searchTopics: marks the search words found in -// one record. Only the record types the help compiler indexes are -// searched. Stops the walk once every word has been found. -static bool topicMatchRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { - HelpSearchT *search = (HelpSearchT *)userData; - - switch (hdr->type) { - case HLP_REC_TEXT: - case HLP_REC_HEADING1: - case HLP_REC_HEADING2: - case HLP_REC_HEADING3: - case HLP_REC_LIST_ITEM: - case HLP_REC_NOTE: - case HLP_REC_CODE: - break; - - default: - return true; - } - - for (int32_t t = 0; t < search->termCount; t++) { - if (!search->found[t] && containsNoCase(payload, search->terms[t])) { - search->found[t] = true; - search->foundCount++; - } - } - - return search->foundCount < search->termCount; -} - - -static void unregisterWidgetClasses(void) { - wgtUnregisterClass(sHelpTextTypeId); - wgtUnregisterClass(sHelpHeadingTypeId); - wgtUnregisterClass(sHelpLinkTypeId); - wgtUnregisterClass(sHelpNoteTypeId); - wgtUnregisterClass(sHelpCodeTypeId); - wgtUnregisterClass(sHelpRuleTypeId); - sHelpTextTypeId = -1; - sHelpHeadingTypeId = -1; - sHelpLinkTypeId = -1; - sHelpNoteTypeId = -1; - sHelpCodeTypeId = -1; - sHelpRuleTypeId = -1; -} - - static void updateNavButtons(void) { - if (sBtnBack) { - wgtSetEnabled(sBtnBack, sHistoryPos > 0); - } - - if (sBtnForward) { - wgtSetEnabled(sBtnForward, sHistoryPos < sHistoryCount - 1); - } -} - - -// Update sWrapWidth from the ScrollPane's current size. -static void updateWrapWidth(void) { - if (!sContentScroll) { - return; - } - - int32_t avail = sContentScroll->w - HELP_WRAP_OVERHEAD - sAc->font.charWidth; - - if (avail > 0) { - sWrapWidth = avail; - } -} - - -// Word-wrap flowing text to fit within colWidth characters. -// Returns a dvxMalloc'd string with \n at wrap points. -// Caller must dvxFree the result. -static char *wrapText(const char *text, int32_t colWidth) { - if (!text || !text[0] || colWidth < HELP_MIN_WRAP_COLS) { - return dvxStrdup(text ? text : ""); - } - - int32_t textLen = (int32_t)strlen(text); - char *result = (char *)dvxMalloc(textLen + textLen / colWidth + 16); - - if (!result) { - return dvxStrdup(text); - } - - int32_t outPos = 0; - int32_t col = 0; - const char *p = text; - - while (*p) { - // Skip leading whitespace at start of line - if (col == 0) { - while (*p == ' ') { - p++; - } - } - - if (!*p) { - break; - } - - // Find end of word - const char *wordStart = p; - - while (*p && *p != ' ' && *p != '\n') { - p++; - } - - int32_t wordLen = (int32_t)(p - wordStart); - - // Check if word fits on current line - if (col > 0 && col + 1 + wordLen > colWidth) { - result[outPos++] = '\n'; - col = 0; - } else if (col > 0) { - result[outPos++] = ' '; - col++; - } - - memcpy(result + outPos, wordStart, wordLen); - outPos += wordLen; - col += wordLen; - - // Skip spaces between words - while (*p == ' ') { - p++; - } - - // Explicit newline in source = paragraph break - if (*p == '\n') { - result[outPos++] = '\n'; - col = 0; - p++; - } - } - - result[outPos] = '\0'; - return result; + wgtSetEnabled(sBtnBack, sHistoryPos > 0); + wgtSetEnabled(sBtnForward, sHistoryPos < sHistoryCount - 1); } @@ -2003,15 +598,13 @@ int32_t appMain(DxeAppContextT *ctx) { sAc = ctx->shellCtx; sCtx = ctx; - // Register private widget classes - registerWidgetClasses(); - const char *topicId = parseArgs(ctx, ctx->args, sHlpPath, sizeof(sHlpPath)); - if (!openHelpFile(sHlpPath)) { + sFile = hlpFileOpen(sHlpPath); + + if (!sFile) { dvxSetBusy(sAc, false); dvxErrorBox(sAc, HELP_APP_TITLE, "Could not open help file."); - unregisterWidgetClasses(); return -1; } @@ -2026,8 +619,8 @@ int32_t appMain(DxeAppContextT *ctx) { sWin = dvxCreateWindow(sAc, HELP_APP_TITLE, winX, winY, winW, winH, true); if (!sWin) { - closeHelpFile(); - unregisterWidgetClasses(); + hlpFileClose(sFile); + sFile = NULL; return -1; } @@ -2056,10 +649,7 @@ int32_t appMain(DxeAppContextT *ctx) { sWin->accelTable = sAccel; } - // Widget tree. wgtInitWindow installs the widget resize handler; - // onResize wraps it. WidgetT *root = wgtInitWindow(sAc, sWin); - sWin->onResize = onResize; // Toolbar WidgetT *toolbar = wgtToolbar(root); @@ -2078,7 +668,7 @@ int32_t appMain(DxeAppContextT *ctx) { WidgetT *btnSearch = wgtButton(toolbar, "Search"); btnSearch->onClick = onSearch; - // Splitter with TOC tree and content scroll pane + // Splitter with TOC tree and the topic WidgetT *splitter = wgtSplitter(root, true); splitter->weight = WGT_WEIGHT_FILL; @@ -2087,19 +677,14 @@ int32_t appMain(DxeAppContextT *ctx) { sTocTree->minW = wgtPixels(HELP_TOC_MIN_W); sTocTree->onChange = onTocChange; - sContentScroll = wgtScrollPane(splitter); - sContentScroll->weight = WGT_WEIGHT_FILL; - - sContentBox = wgtVBox(sContentScroll); - sContentBox->spacing = wgtPixels(HELP_PARA_SPACING); - sContentBox->padding = wgtPixels(HELP_CONTENT_PAD); + sView = wgtHelpView(splitter); + sView->weight = WGT_WEIGHT_FILL; + wgtHelpViewSetLinkCallback(sView, onLink); wgtSplitterSetPos(splitter, HELP_SPLITTER_POS); - // Populate TOC tree populateToc(); - - navigateToTopic(startTopic(topicId)); + navigateToTopic(hlpFileStartTopic(sFile, topicId)); // F1 inside the viewer (or one of its dialogs) means help on the viewer parseArgs(ctx, "", ctx->helpFile, sizeof(ctx->helpFile)); diff --git a/src/apps/kpunch/dvxhelp/help.dhs b/src/apps/kpunch/dvxhelp/help.dhs index 28eac0d1..7b1673c9 100644 --- a/src/apps/kpunch/dvxhelp/help.dhs +++ b/src/apps/kpunch/dvxhelp/help.dhs @@ -217,7 +217,7 @@ Multiple input files are merged into a single help file. This allows per-widget 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). +Source lines longer than 4094 characters, and lines that contain a NUL byte, are errors. 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. diff --git a/src/apps/kpunch/dvxhelp/helpedit/helpedit.frm b/src/apps/kpunch/dvxhelp/helpedit/helpedit.frm index 2794a080..d94e6bf4 100644 --- a/src/apps/kpunch/dvxhelp/helpedit/helpedit.frm +++ b/src/apps/kpunch/dvxhelp/helpedit/helpedit.frm @@ -97,6 +97,8 @@ End ' help viewer's help ("file topic", opened at that topic). CONST HELP_FILE = "helpedit.hlp" CONST FORMAT_HELP = "dvxhelp.hlp help.format" +CONST HASH_MUL = 31 +CONST HASH_MOD = 16777213 DIM SHARED currentFile AS STRING DIM SHARED hlpFile AS STRING @@ -130,14 +132,16 @@ IF COMMAND$(1) <> "" THEN END IF +' A hash of the editor's text (kept below HASH_MOD so h * HASH_MUL +' always fits a LONG). FUNCTION TextHash() AS LONG DIM h AS LONG DIM t AS STRING + DIM i AS LONG t = Editor.Text h = 0 - DIM i AS INTEGER FOR i = 1 TO LEN(t) - h = h * 31 + ASC(MID$(t, i, 1)) + h = (h * HASH_MUL + ASC(MID$(t, i, 1))) MOD HASH_MOD NEXT i TextHash = h END FUNCTION diff --git a/src/apps/kpunch/fileMgr/icon32.bmp b/src/apps/kpunch/fileMgr/ICON32.BMP similarity index 100% rename from src/apps/kpunch/fileMgr/icon32.bmp rename to src/apps/kpunch/fileMgr/ICON32.BMP diff --git a/src/apps/kpunch/fileMgr/fileMgr.bas b/src/apps/kpunch/fileMgr/fileMgr.bas new file mode 100644 index 00000000..6d232314 --- /dev/null +++ b/src/apps/kpunch/fileMgr/fileMgr.bas @@ -0,0 +1,218 @@ + +' 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. + +' fileMgr.bas -- File Manager: what outlives the main window. +' +' Copy, move and delete run as jobs, each with its own progress window +' (FmJob, a form made in code with a Timer, a progress bar and a Cancel +' button). The timer works on its job for FM_SLICE_MS at a time, so the +' File Manager and the rest of DVX stay usable; questions ("already +' exists", "read-only", errors) are asked from the timer with a small +' modal dialog. The jobs live here, not in the main form, because they +' keep running when the main window is closed: the program ends when the +' last window (a job's or the main one) closes. Only the timer's handler, +' JobTick, is in the main form's code (fileMgr.frm): a handler runs with +' the variables of the form whose code it is, and a finished job shows +' the main window's folder again. +' +' The Properties dialog leaves its answer here too. + +OPTION EXPLICIT + +CONST FM_TITLE = "File Manager" + +' Job progress windows +CONST FM_JOB_WIN_W = 360 +CONST FM_JOB_WIN_H = 130 +CONST FM_JOB_PREFIX = "FmJob" ' window names: FmJob1, FmJob2, ... +CONST FM_SLICE_MS = 60 ' work per timer tick +CONST FM_JOB_TICK = 1 ' timer interval: as often as DVX can + +' The questions a job asks, with the answers their buttons give (in order) +CONST FM_OVERWRITE_BUTTONS = "&Yes|&No|Yes to &All|&Cancel" +CONST FM_ERROR_BUTTONS = "&Retry|&Skip|&Cancel" + +' The attribute bits the Properties dialog edits +CONST FM_ATTR_EDITABLE = FILEOPS_ATTR_READONLY OR FILEOPS_ATTR_HIDDEN OR FILEOPS_ATTR_SYSTEM OR FILEOPS_ATTR_ARCHIVE + +' True while the main window is open (a job that ends then refreshes it) +DIM SHARED fmMainOpen AS BOOLEAN + +' The running jobs, keyed by window name, and how many were started +DIM SHARED fmJobs AS NEW Collection +DIM SHARED fmJobCount AS LONG + +' The Properties dialog's answer: OK chosen, and the attribute bits ticked +DIM SHARED fmPropsAccepted AS BOOLEAN +DIM SHARED fmPropsAttr AS LONG + + +SUB ShowError(BYVAL title AS STRING, BYVAL message AS STRING) + MsgBox message, vbOKOnly + vbCritical, title +END SUB + + +' The running job whose window is called windowName, or Nothing. +FUNCTION FindJob(BYVAL windowName AS STRING) AS FmJob + DIM job AS FmJob + + FOR EACH job IN fmJobs + IF job.WindowName = windowName THEN + SET FindJob = job + EXIT FUNCTION + END IF + NEXT job +END FUNCTION + + +' Start a copy, move or delete (FILEOPS_COPY / _MOVE / _DELETE) of paths +' (a Collection of full paths) into destDir, with its progress window. +SUB JobStart(BYVAL op AS LONG, BYVAL paths AS Collection, BYVAL destDir AS STRING) + DIM handle AS LONG + DIM anyAdded AS BOOLEAN + DIM p AS VARIANT + DIM job AS FmJob + DIM win AS Form + DIM ctrl AS Control + DIM row AS Control + + handle = FileOpsJobCreate(op, destDir) + + IF handle = 0 THEN + ShowError FM_TITLE, "Not enough memory to start the operation." + EXIT SUB + END IF + + ' Every source is added; the ones that cannot be are reported + FOR EACH p IN paths + IF FileOpsJobAddSource(handle, p) THEN + anyAdded = True + ELSE + ShowError FM_TITLE, p & " cannot be found, or the destination path would be too long. It is skipped." + END IF + NEXT p + + IF NOT anyAdded THEN + FileOpsJobFree handle + EXIT SUB + END IF + + fmJobCount = fmJobCount + 1 + + SET job = NEW FmJob + job.Handle = handle + job.Verb = CHOOSE(op + 1, "Copying", "Moving", "Deleting") + job.WindowName = FM_JOB_PREFIX & CStr(fmJobCount) + + ' The progress window: what is being done, the bar, how many files, + ' and Cancel; its timer runs the job + SET win = CreateForm(job.WindowName, FM_JOB_WIN_W, FM_JOB_WIN_H) + SET job.Window = win + win.Caption = job.Verb + win.Resizable = False + + SET ctrl = CreateControl(win, "Label", "lblWhat") + ctrl.Weight = 1 + SET ctrl = CreateControl(win, "ProgressBar", "pbJob") + SET ctrl = CreateControl(win, "Label", "lblCounts") + SET row = CreateControl(win, "HBox", "boxCancel") + row.Alignment = "Center" + SET ctrl = CreateControl(win, "CommandButton", "cmdCancel", row) + ctrl.Caption = "&Cancel" + SetEvent ctrl, "Click", "JobCancelClick" + SetEvent win, "QueryUnload", "JobQueryUnload" + + fmJobs.Add job, job.WindowName + JobShowProgress job + + SET ctrl = CreateControl(win, "Timer", "tmrJob") + ctrl.Interval = FM_JOB_TICK + SetEvent ctrl, "Timer", "JobTick" + ctrl.Enabled = True + + win.Show +END SUB + + +' Put the job's question to the user (the last button is Cancel) and hand +' the answer back. +SUB JobAsk(BYVAL job AS FmJob, BYVAL buttons AS STRING, BYVAL answers AS STRING) + DIM pick AS INTEGER + + JobShowProgress job + pick = DlgButtons(FM_TITLE, FileOpsJobMessage$(job.Handle), buttons, vbQuestion) + FileOpsJobAnswer job.Handle, Val(Split(answers, "|")(pick)) +END SUB + + +' The Cancel button of a job's window (Me is that window). +SUB JobCancelClick + DIM job AS FmJob + + SET job = FindJob(Me.Name) + + IF NOT job IS NOTHING THEN + FileOpsJobCancel job.Handle + END IF +END SUB + + +' The job has stopped: log it, close its window, free it, and show the +' folder as it is now. +SUB JobFinish(BYVAL job AS FmJob, BYVAL stepResult AS LONG) + IF stepResult = FILEOPS_STEP_DONE THEN + ShellLog "FileMgr: " & job.Verb & " finished" + ELSE + ShellLog "FileMgr: " & job.Verb & " cancelled" + END IF + + fmJobs.Remove job.WindowName + Unload job.Window + FileOpsJobFree job.Handle + + IF fmMainOpen THEN + FmRefresh + END IF +END SUB + + +' The close box of a job's window cancels the job; the window closes when +' the job has stopped. +SUB JobQueryUnload(Cancel AS INTEGER) + DIM job AS FmJob + + SET job = FindJob(Me.Name) + + IF NOT job IS NOTHING THEN + FileOpsJobCancel job.Handle + Cancel = 1 + END IF +END SUB + + +' Progress line, file count and bar of the job's window. +SUB JobShowProgress(BYVAL job AS FmJob) + job.Window.lblWhat.Caption = job.Verb & " " & FileOpsJobCurrent$(job.Handle) + job.Window.lblCounts.Caption = CStr(FileOpsJobFilesDone(job.Handle)) & " of " & CStr(FileOpsJobFilesTotal(job.Handle)) & " files" + job.Window.pbJob.Value = FileOpsJobPercent(job.Handle) +END SUB diff --git a/src/apps/kpunch/fileMgr/fileMgr.c b/src/apps/kpunch/fileMgr/fileMgr.c deleted file mode 100644 index 5536678e..00000000 --- a/src/apps/kpunch/fileMgr/fileMgr.c +++ /dev/null @@ -1,1740 +0,0 @@ -// 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. - -// fileMgr.c -- File Manager -// -// A thin user interface over the file operations library (fileops.lib): -// drive bar, path bar, folder tree, sortable file list and status bar, -// with open/run, copy, move, rename, delete, new folder, properties, -// selection and filter commands. -// -// One window per instance (multiInstance: File > New Window starts another -// instance). Cut/Copy/Paste of files between File Manager windows goes -// through the DVX clipboard as text: a header line (FM_CLIP_COPY or -// FM_CLIP_CUT) followed by one full path per line. DVX has no -// drag-and-drop of files between programs, so this is the way to move -// files between windows; pasting the same text into a text editor gives -// the list of paths. -// -// Copy, move and delete run as cooperative tasks (tsCreate), each with its -// own progress window and Cancel button, so the File Manager (and the rest -// of DVX) stays usable. The task advances the job in time slices and -// yields in between; questions ("file exists", "read-only", errors) are -// asked from the task with a small modal dialog. -// -// The tree shows the path from the drive root to the current folder, the -// folders beside each step, and the folders inside the current one. -// Choosing a folder in the tree lists it and adds its sub-folders under it -// in place; navigating any other way rebuilds the tree. - -#include "dvxApp.h" -#include "dvxDlg.h" -#include "dvxWgt.h" -#include "dvxWgtP.h" -#include "dvxWm.h" -#include "dvxPlat.h" -#include "shellApp.h" -#include "shellAssoc.h" -#include "taskSwch.h" -#include "stb_ds_wrap.h" - -#include "box/box.h" -#include "button/button.h" -#include "checkbox/checkbox.h" -#include "label/label.h" -#include "listView/listView.h" -#include "progressBar/progress.h" -#include "splitter/splitter.h" -#include "statusBar/statBar.h" -#include "textInput/textInpt.h" -#include "toolbar/toolbar.h" -#include "treeView/treeView.h" - -#include "fileops/fileOps.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// ============================================================ -// Constants -// ============================================================ - -#define FM_TITLE "File Manager" -#define FM_WIN_W 600 -#define FM_WIN_H 400 -#define FM_WIN_X 40 -#define FM_WIN_Y 30 -#define FM_CASCADE 20 // offset between successive File Manager windows -#define FM_TREE_W 170 // initial width of the folder tree pane -#define FM_TITLE_MAX (DVX_MAX_PATH + 32) -#define FM_STATUS_MAX 128 -#define FM_PROMPT_MAX (DVX_MAX_PATH + 96) -#define FM_NAME_MAX DVX_MAX_PATH -#define FM_DEFAULT_FOLDER "NEWFOLDR" - -// Widget names (for scripts and tests) -#define FM_NAME_PATH "fmPath" -#define FM_NAME_TREE "fmTree" -#define FM_NAME_LIST "fmList" -#define FM_NAME_STATUS "fmStatus" - -// File list columns -#define FM_COL_NAME 0 -#define FM_COL_SIZE 1 -#define FM_COL_TYPE 2 -#define FM_COL_DATE 3 -#define FM_COL_COUNT 4 -#define FM_COL_NAME_CHARS 14 -#define FM_COL_SIZE_CHARS 12 -#define FM_COL_TYPE_CHARS 10 -#define FM_COL_DATE_CHARS 16 -#define FM_PATH_CHARS 40 // visible width of the path bar - -// Clipboard format for files -#define FM_CLIP_COPY "DVX-FILES COPY" -#define FM_CLIP_CUT "DVX-FILES CUT" -#define FM_CLIP_EOL '\n' - -// Job tasks: stack for the task (modal dialogs run on it) and the time -// slice between yields -#define FM_JOB_STACK (TS_DEFAULT_STACK_SIZE * 2) -#define FM_SLICE_MS 60 -#define FM_JOB_WIN_W 360 -#define FM_JOB_WIN_H 130 - -// Question dialog - -// Properties dialog -#define FM_PROPS_W 340 - -// Menu command IDs -#define CMD_FILE_OPEN 100 -#define CMD_FILE_NEW_FOLDER 101 -#define CMD_FILE_COPY_TO 102 -#define CMD_FILE_MOVE_TO 103 -#define CMD_FILE_RENAME 104 -#define CMD_FILE_DELETE 105 -#define CMD_FILE_PROPERTIES 106 -#define CMD_FILE_NEW_WINDOW 107 -#define CMD_FILE_EXIT 108 -#define CMD_EDIT_CUT 200 -#define CMD_EDIT_COPY 201 -#define CMD_EDIT_PASTE 202 -#define CMD_EDIT_SELECT_ALL 203 -#define CMD_EDIT_INVERT 204 -#define CMD_VIEW_REFRESH 300 -#define CMD_VIEW_FILTER 301 -#define CMD_VIEW_UP 302 -#define CMD_VIEW_SORT_NAME 310 -#define CMD_VIEW_SORT_SIZE 311 -#define CMD_VIEW_SORT_TYPE 312 -#define CMD_VIEW_SORT_DATE 313 -#define CMD_VIEW_SORT_DESC 314 -#define CMD_HELP_CONTENTS 400 -#define CMD_HELP_ABOUT 401 - -// Deepest folder chain the tree shows (each level is at least two -// characters of the path) -#define FM_TREE_DEPTH_MAX (DVX_MAX_PATH / 2) -#define FM_CUR_DIR "." - -// ============================================================ -// Types -// ============================================================ - -// Formatted text for the non-name columns of one row -typedef struct { - char size[FILEOPS_SIZE_TEXT_MAX]; - char type[FILEOPS_TYPE_TEXT_MAX]; - char date[FILEOPS_DATE_TEXT_MAX]; -} FmRowTextT; - -// One running copy/move/delete and its progress window -typedef struct { - FileOpsJobT *job; - WindowT *win; - WidgetT *what; - WidgetT *counts; - WidgetT *bar; - const char *verb; // "Copying" / "Moving" / "Deleting" - int32_t taskId; -} FmJobUiT; - -// The Properties dialog's attribute check boxes and what OK read from them -typedef struct { - WidgetT *readOnly; - WidgetT *hidden; - WidgetT *system; - WidgetT *archive; - uint8_t chosen; -} FmPropsDlgT; - -typedef struct { - WindowT *win; - WidgetT *driveBar; - WidgetT *pathInput; - WidgetT *tree; - WidgetT *list; - WidgetT *statusSel; - WidgetT *statusFree; - AccelTableT *accel; - char curDir[DVX_MAX_PATH]; - char filter[FILEOPS_FILTER_MAX]; - FileOpsEntryT *entries; // current listing - int32_t entryCount; - const char **cells; // entryCount * FM_COL_COUNT, handed to the list view - FmRowTextT *rowText; - char **treePaths; // stb_ds: path of every tree item (item->userData) - FileOpsSortE sortCol; - bool sortDesc; - bool treeActive; // the tree (not the list) has focus - bool syncingTree; // tree selection is being set by the program -} FmStateT; - -// ============================================================ -// Prototypes -// ============================================================ - -static void addDriveButtons(void); -void appShutdown(void); -static void buildMenus(void); -static void buildWindow(void); -static void clipboardPut(bool cut); -static void clipboardSetPaths(const char *header, char **paths); -static void cmdDelete(void); -static void cmdFilter(void); -static void cmdInvert(void); -static void cmdNewFolder(void); -static void cmdOpen(void); -static void cmdPaste(void); -static void cmdProperties(void); -static void cmdRename(void); -static void cmdSelectAll(void); -static void cmdSort(FileOpsSortE col, bool descending); -static void cmdTransferTo(FileOpsOpE op); -static void cmdUp(void); -static int32_t countFileMgrWindows(const AppContextT *ac); -static void freeListing(void); -static void freeSelection(char **paths); -static char **getSelection(void); -static bool jobAddSources(FileOpsJobT *job, char **paths); -static void jobAsk(FmJobUiT *j, const char * const *labels, const FileOpsAnswerE *answers, int32_t count); -static void jobFinish(FmJobUiT *j, FileOpsStepE result); -static void jobOnCancel(WidgetT *w); -static void jobOnClose(WindowT *win); -static void jobShowProgress(FmJobUiT *j); -static void jobStart(FileOpsOpE op, char **paths, const char *destDir); -static void jobTask(void *arg); -static void listOnDblClick(WidgetT *w); -static void listOnFocus(WidgetT *w); -static void listOnHeader(WidgetT *w, int32_t col, ListViewSortE dir); -static void listOnKey(WidgetT *w, int32_t keyCode, int32_t shift); -static void listOnSelChange(WidgetT *w); -static bool loadListing(const char *dir); -static bool navigate(const char *path, bool fromTree); -static void onClose(WindowT *win); -static void onDriveClick(WidgetT *w); -static void onMenu(WindowT *win, int32_t menuId); -static void onPathKey(WidgetT *w, int32_t keyCode, int32_t shift); -static void openEntry(const FileOpsEntryT *e); -static bool propsOnOk(WindowT *dlg, void *userCtx); -static void refresh(void); -static void selectByName(const char *name); -static void selectionAdd(char ***paths, const char *path); -static void setCells(void); -static void showError(const char *title, const char *message); -static void treeAddChildren(WidgetT *parentItem, const char *dir, const char *expandPath, WidgetT **outNext); -static WidgetT *treeAddItem(WidgetT *parent, const char *path, const char *label); -static void treeFreePaths(void); -static void treeOnChange(WidgetT *w); -static void treeOnFocus(WidgetT *w); -static void treeOnKey(WidgetT *w, int32_t keyCode, int32_t shift); -static void treeRebuild(void); -static void updateStatus(void); -static void updateTitle(void); -int32_t appMain(DxeAppContextT *ctx); - -// ============================================================ -// App descriptor and module state -// ============================================================ - -SHELL_APP_DESCRIPTOR( - .name = FM_TITLE, - .hasMainLoop = false, - .multiInstance = true, - .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_NORMAL -); - -static DxeAppContextT *sCtx = NULL; -static FmStateT sState; -static FmJobUiT **sJobs = NULL; // stb_ds: running jobs - -static const char * const sOverwriteLabels[] = { "&Yes", "&No", "Yes to &All", "&Cancel" }; -static const char * const sErrorLabels[] = { "&Retry", "&Skip", "&Cancel" }; - -static const FileOpsAnswerE sOverwriteAnswers[] = { FileOpsAnswerYesE, FileOpsAnswerNoE, FileOpsAnswerAllE, FileOpsAnswerCancelE }; -static const FileOpsAnswerE sErrorAnswers[] = { FileOpsAnswerRetryE, FileOpsAnswerSkipE, FileOpsAnswerCancelE }; - -// The list view keeps a pointer to the column array. -static const ListViewColT sColumns[FM_COL_COUNT] = { - { "Name", wgtChars(FM_COL_NAME_CHARS), ListViewAlignLeftE }, - { "Size", wgtChars(FM_COL_SIZE_CHARS), ListViewAlignRightE }, - { "Type", wgtChars(FM_COL_TYPE_CHARS), ListViewAlignLeftE }, - { "Modified", wgtChars(FM_COL_DATE_CHARS), ListViewAlignLeftE } -}; - - -// One button per drive that exists. The DOS probe never touches the -// media, so empty floppy and CD-ROM drives cost nothing here. -static void addDriveButtons(void) { - PlatformDriveT drives[PLATFORM_DRIVE_MAX]; - int32_t count = platformDriveList(drives, PLATFORM_DRIVE_MAX); - - for (int32_t i = 0; i < count; i++) { - char label[PLATFORM_DRIVE_ROOT_MAX]; - - // "C:" for a DOS drive, "/" for a single-root file system - snprintf(label, sizeof(label), "%s", drives[i].root); - - if (label[1] == ':') { - label[2] = '\0'; - } - - WidgetT *b = wgtButton(sState.driveBar, label); - - b->userData = (void *)(intptr_t)drives[i].root[0]; - b->onClick = onDriveClick; - } -} - - -// Force-kill (Task Manager, shell shutdown): stop every job task and -// release its files; the shell destroys the windows. -void appShutdown(void) { - for (int32_t i = 0; i < (int32_t)arrlen(sJobs); i++) { - tsKill((uint32_t)sJobs[i]->taskId); - fileOpsJobFree(sJobs[i]->job); - free(sJobs[i]); - } - - arrfree(sJobs); - freeListing(); - treeFreePaths(); - - if (sState.accel) { - dvxFreeAccelTable(sState.accel); - sState.accel = NULL; - } -} - - -static void buildMenus(void) { - WindowT *win = sState.win; - MenuBarT *menuBar = wmAddMenuBar(win); - - if (!menuBar) { - return; - } - - MenuT *file = wmAddMenu(menuBar, "&File"); - wmAddMenuItem(file, "&Open\tEnter", CMD_FILE_OPEN); - wmAddMenuItem(file, "New &Folder...", CMD_FILE_NEW_FOLDER); - wmAddMenuSeparator(file); - wmAddMenuItem(file, "&Copy To...\tF8", CMD_FILE_COPY_TO); - wmAddMenuItem(file, "&Move To...\tF7", CMD_FILE_MOVE_TO); - wmAddMenuItem(file, "&Rename...\tF2", CMD_FILE_RENAME); - wmAddMenuItem(file, "&Delete\tDel", CMD_FILE_DELETE); - wmAddMenuItem(file, "P&roperties\tAlt+Enter", CMD_FILE_PROPERTIES); - wmAddMenuSeparator(file); - wmAddMenuItem(file, "New &Window", CMD_FILE_NEW_WINDOW); - wmAddMenuSeparator(file); - wmAddMenuItem(file, "E&xit", CMD_FILE_EXIT); - - MenuT *edit = wmAddMenu(menuBar, "&Edit"); - wmAddMenuItem(edit, "Cu&t\tCtrl+X", CMD_EDIT_CUT); - wmAddMenuItem(edit, "&Copy\tCtrl+C", CMD_EDIT_COPY); - wmAddMenuItem(edit, "&Paste\tCtrl+V", CMD_EDIT_PASTE); - wmAddMenuSeparator(edit); - wmAddMenuItem(edit, "Select &All\tCtrl+A", CMD_EDIT_SELECT_ALL); - wmAddMenuItem(edit, "&Invert Selection", CMD_EDIT_INVERT); - - MenuT *view = wmAddMenu(menuBar, "&View"); - wmAddMenuItem(view, "&Refresh\tF5", CMD_VIEW_REFRESH); - wmAddMenuItem(view, "&Filter...", CMD_VIEW_FILTER); - wmAddMenuItem(view, "&Up One Level\tBackspace", CMD_VIEW_UP); - wmAddMenuSeparator(view); - wmAddMenuRadioItem(view, "Sort by &Name", CMD_VIEW_SORT_NAME, true); - wmAddMenuRadioItem(view, "Sort by &Size", CMD_VIEW_SORT_SIZE, false); - wmAddMenuRadioItem(view, "Sort by &Type", CMD_VIEW_SORT_TYPE, false); - wmAddMenuRadioItem(view, "Sort by &Date", CMD_VIEW_SORT_DATE, false); - wmAddMenuSeparator(view); - wmAddMenuCheckItem(view, "D&escending", CMD_VIEW_SORT_DESC, false); - - MenuT *help = wmAddMenu(menuBar, "&Help"); - wmAddMenuItem(help, "&Contents\tF1", CMD_HELP_CONTENTS); - wmAddMenuSeparator(help); - wmAddMenuItem(help, "&About File Manager...", CMD_HELP_ABOUT); - - // Keys that never mean anything to the path bar go in the accelerator - // table; Del, Backspace and Ctrl+A/C/V/X belong to the text box when - // it has focus, so the tree and list handle those themselves. - sState.accel = dvxCreateAccelTable(); - dvxAddAccel(sState.accel, KEY_F2, 0, CMD_FILE_RENAME); - dvxAddAccel(sState.accel, KEY_F5, 0, CMD_VIEW_REFRESH); - dvxAddAccel(sState.accel, KEY_F7, 0, CMD_FILE_MOVE_TO); - dvxAddAccel(sState.accel, KEY_F8, 0, CMD_FILE_COPY_TO); - dvxAddAccel(sState.accel, KEY_ENTER, ACCEL_ALT, CMD_FILE_PROPERTIES); - dvxAddAccel(sState.accel, KEY_SCAN_ENTER | KEY_EXT_FLAG, ACCEL_ALT, CMD_FILE_PROPERTIES); - win->accelTable = sState.accel; -} - - -static void buildWindow(void) { - AppContextT *ac = sCtx->shellCtx; - WidgetT *root = wgtInitWindow(ac, sState.win); - - sState.driveBar = wgtToolbar(root); - addDriveButtons(); - - WidgetT *pathRow = wgtHBox(root); - wgtLabel(pathRow, "&Path:"); - sState.pathInput = wgtTextInput(pathRow, DVX_MAX_PATH); - sState.pathInput->weight = WGT_WEIGHT_FILL; - sState.pathInput->prefW = wgtChars(FM_PATH_CHARS); - sState.pathInput->onKeyDown = onPathKey; - wgtSetName(sState.pathInput, FM_NAME_PATH); - - WidgetT *split = wgtSplitter(root, true); - split->weight = WGT_WEIGHT_FILL; - - sState.tree = wgtTreeView(split); - sState.tree->onChange = treeOnChange; - sState.tree->onFocus = treeOnFocus; - sState.tree->onKeyDown = treeOnKey; - wgtSetName(sState.tree, FM_NAME_TREE); - - sState.list = wgtListView(split); - sState.list->onDblClick = listOnDblClick; - sState.list->onChange = listOnSelChange; - sState.list->onFocus = listOnFocus; - sState.list->onKeyDown = listOnKey; - wgtSetName(sState.list, FM_NAME_LIST); - wgtListViewSetColumns(sState.list, sColumns, FM_COL_COUNT); - wgtListViewSetMultiSelect(sState.list, true); - wgtListViewSetHeaderClickCallback(sState.list, listOnHeader); - wgtSplitterSetPos(split, FM_TREE_W); - - WidgetT *status = wgtStatusBar(root); - sState.statusSel = wgtLabel(status, ""); - sState.statusSel->weight = WGT_WEIGHT_FILL; - sState.statusFree = wgtLabel(status, ""); - wgtSetName(sState.statusSel, FM_NAME_STATUS); -} - - -// Put the selection on the DVX clipboard as a file list. -static void clipboardPut(bool cut) { - char **paths = getSelection(); - - if (arrlen(paths) > 0) { - clipboardSetPaths(cut ? FM_CLIP_CUT : FM_CLIP_COPY, paths); - } - - freeSelection(paths); -} - - -// Clipboard text: the header line, then one path per line. -static void clipboardSetPaths(const char *header, char **paths) { - char *text = NULL; - - for (const char *p = header; *p; p++) { - arrput(text, *p); - } - - for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { - arrput(text, FM_CLIP_EOL); - - for (const char *p = paths[i]; *p; p++) { - arrput(text, *p); - } - } - - dvxClipboardCopy(text, (int32_t)arrlen(text)); - arrfree(text); -} - - -static void cmdDelete(void) { - char **paths = getSelection(); - int32_t count = (int32_t)arrlen(paths); - char msg[FM_PROMPT_MAX]; - - if (count == 0) { - freeSelection(paths); - return; - } - - if (count == 1) { - snprintf(msg, sizeof(msg), "Delete %s?", paths[0]); - } else { - snprintf(msg, sizeof(msg), "Delete these %" PRId32 " items?", count); - } - - if (dvxMessageBox(sCtx->shellCtx, "Confirm Delete", msg, MB_YESNO | MB_ICONQUESTION) == ID_YES) { - jobStart(FileOpsDeleteE, paths, NULL); - } - - freeSelection(paths); -} - - -static void cmdFilter(void) { - char buf[FILEOPS_FILTER_MAX]; - - if (!dvxInputBox(sCtx->shellCtx, "Filter", "Show only files matching:", sState.filter, buf, sizeof(buf))) { - return; - } - - snprintf(sState.filter, sizeof(sState.filter), "%s", buf[0] ? buf : PLATFORM_FILTER_ALL); - refresh(); -} - - -static void cmdInvert(void) { - for (int32_t i = 0; i < sState.entryCount; i++) { - wgtListViewSetItemSelected(sState.list, i, !wgtListViewIsItemSelected(sState.list, i)); - } - - updateStatus(); -} - - -static void cmdNewFolder(void) { - char name[FM_NAME_MAX]; - char err[FILEOPS_MSG_MAX]; - - if (!dvxInputBox(sCtx->shellCtx, "New Folder", "Name of the new folder:", FM_DEFAULT_FOLDER, name, sizeof(name))) { - return; - } - - if (!fileOpsMakeDir(sState.curDir, name, err, sizeof(err))) { - showError("New Folder", err); - return; - } - - dvxLog("FileMgr: created folder %s", name); - refresh(); - selectByName(name); -} - - -// Enter / double-click / File > Open: a folder is opened in place, a -// program is started, a document opens in its associated program. -static void cmdOpen(void) { - if (sState.treeActive) { - return; - } - - int32_t row = wgtListViewGetSelected(sState.list); - - if (row >= 0 && row < sState.entryCount) { - openEntry(&sState.entries[row]); - } -} - - -// Paste the clipboard's file list into the current folder. -static void cmdPaste(void) { - int32_t len = 0; - const char *clip = dvxClipboardGet(&len); - bool cut = false; - size_t headLen; - - if (!clip) { - return; - } - - if (strncmp(clip, FM_CLIP_CUT, strlen(FM_CLIP_CUT)) == 0 && clip[strlen(FM_CLIP_CUT)] == FM_CLIP_EOL) { - cut = true; - headLen = strlen(FM_CLIP_CUT); - } else if (strncmp(clip, FM_CLIP_COPY, strlen(FM_CLIP_COPY)) == 0 && clip[strlen(FM_CLIP_COPY)] == FM_CLIP_EOL) { - headLen = strlen(FM_CLIP_COPY); - } else { - showError("Paste", "The clipboard holds no files. Use Copy or Cut in a File Manager window first."); - return; - } - - char **paths = NULL; - const char *p = clip + headLen + 1; - const char *end = clip + len; - - while (p < end) { - const char *eol = memchr(p, FM_CLIP_EOL, (size_t)(end - p)); - size_t n = eol ? (size_t)(eol - p) : (size_t)(end - p); - char *path = (char *)malloc(n + 1); - - if (path) { - memcpy(path, p, n); - path[n] = '\0'; - arrput(paths, path); - } - - p += n + 1; - } - - jobStart(cut ? FileOpsMoveE : FileOpsCopyE, paths, sState.curDir); - - // A cut is used up by its paste. - if (cut) { - dvxClipboardClear(); - } - - freeSelection(paths); -} - - -// Size, dates and editable attributes of the selection. Attribute changes -// apply to every selected item. -static void cmdProperties(void) { - AppContextT *ac = sCtx->shellCtx; - char **paths = getSelection(); - int32_t count = (int32_t)arrlen(paths); - - if (count == 0) { - freeSelection(paths); - return; - } - - uint64_t bytes = 0; - int32_t files = 0; - int32_t folders = 0; - bool allRead = true; - - for (int32_t i = 0; i < count; i++) { - uint64_t b; - int32_t f; - int32_t d; - struct stat st; - - allRead = fileOpsTreeSize(paths[i], &b, &f, &d) && allRead; - bytes += b; - files += f; - folders += d + (stat(paths[i], &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0); - } - - uint8_t attr = 0; - struct stat st; - char line[FM_PROMPT_MAX]; - char sizeText[FILEOPS_SIZE_TEXT_MAX]; - char dateText[FILEOPS_DATE_TEXT_MAX]; - char location[DVX_MAX_PATH]; - bool haveStat = stat(paths[0], &st) == 0; - - fileOpsGetAttr(paths[0], &attr); - - if (!fileOpsPathParent(paths[0], location, sizeof(location))) { - snprintf(location, sizeof(location), "%s", paths[0]); - } - - WindowT *win = dvxDialogCreate(ac, "Properties", FM_PROPS_W); - - if (!win) { - freeSelection(paths); - return; - } - - WidgetT *root = win->widgetRoot; - - if (count == 1) { - snprintf(line, sizeof(line), "Name: %s", platformPathBaseName(paths[0])); - } else { - snprintf(line, sizeof(line), "Name: %" PRId32 " items", count); - } - - wgtLabel(root, line); - snprintf(line, sizeof(line), "Location: %s", location); - wgtLabel(root, line); - - fileOpsFormatSize(bytes, sizeText, sizeof(sizeText)); - snprintf(line, sizeof(line), "Size: %s bytes%s", sizeText, allRead ? "" : " (some items could not be read)"); - wgtLabel(root, line); - snprintf(line, sizeof(line), "Contains: %" PRId32 " files, %" PRId32 " folders", files, folders); - wgtLabel(root, line); - - if (count == 1 && haveStat) { - fileOpsFormatDate(st.st_mtime, dateText, sizeof(dateText)); - snprintf(line, sizeof(line), "Modified: %s", dateText); - wgtLabel(root, line); - } - - WidgetT *frame = wgtFrame(root, "Attributes"); - FmPropsDlgT pd = {0}; - - pd.readOnly = wgtCheckbox(frame, "&Read-only"); - pd.hidden = wgtCheckbox(frame, "&Hidden"); - pd.system = wgtCheckbox(frame, "&System"); - pd.archive = wgtCheckbox(frame, "&Archive"); - wgtCheckboxSetChecked(pd.readOnly, (attr & FILEOPS_ATTR_READONLY) != 0); - wgtCheckboxSetChecked(pd.hidden, (attr & FILEOPS_ATTR_HIDDEN) != 0); - wgtCheckboxSetChecked(pd.system, (attr & FILEOPS_ATTR_SYSTEM) != 0); - wgtCheckboxSetChecked(pd.archive, (attr & FILEOPS_ATTR_ARCHIVE) != 0); - dvxDialogAddOkCancel(win, "&OK", propsOnOk, &pd); - - bool apply = dvxDialogRun(ac, win); - uint8_t chosen = pd.chosen; - - if (apply) { - bool failed = false; - - for (int32_t i = 0; i < count; i++) { - uint8_t old = 0; - - if (!fileOpsGetAttr(paths[i], &old) || !fileOpsSetAttr(paths[i], (uint8_t)((old & ~FILEOPS_ATTR_EDITABLE) | chosen))) { - failed = true; - } - } - - if (failed) { - showError("Properties", "The attributes of some items could not be changed."); - } - - refresh(); - } - - freeSelection(paths); -} - - -static void cmdRename(void) { - char **paths = getSelection(); - char name[FM_NAME_MAX]; - char err[FILEOPS_MSG_MAX]; - char dir[DVX_MAX_PATH]; - char prompt[FM_PROMPT_MAX]; - - if (arrlen(paths) != 1) { - if (arrlen(paths) > 1) { - showError("Rename", "Select one file or folder to rename."); - } - - freeSelection(paths); - return; - } - - const char *oldName = platformPathBaseName(paths[0]); - - snprintf(prompt, sizeof(prompt), "New name for %s:", oldName); - - if (fileOpsPathParent(paths[0], dir, sizeof(dir)) && dvxInputBox(sCtx->shellCtx, "Rename", prompt, oldName, name, sizeof(name))) { - bool renamedCurrent = fileOpsPathSame(paths[0], sState.curDir); - - if (!fileOpsRename(dir, oldName, name, err, sizeof(err))) { - showError("Rename", err); - } else if (renamedCurrent) { - // The tree's current folder itself was renamed. - char newPath[DVX_MAX_PATH]; - - if (fileOpsPathJoin(newPath, sizeof(newPath), dir, name)) { - snprintf(sState.curDir, sizeof(sState.curDir), "%s", newPath); - } - } - - refresh(); - } - - freeSelection(paths); -} - - -static void cmdSelectAll(void) { - wgtListViewSelectAll(sState.list); - updateStatus(); -} - - -static void cmdSort(FileOpsSortE col, bool descending) { - static const int32_t sortCmds[] = { CMD_VIEW_SORT_NAME, CMD_VIEW_SORT_SIZE, CMD_VIEW_SORT_TYPE, CMD_VIEW_SORT_DATE }; - - sState.sortCol = col; - sState.sortDesc = descending; - - if (sState.win->menuBar) { - for (int32_t i = 0; i < DVX_ARRAY_LEN(sortCmds); i++) { - wmMenuItemSetChecked(sState.win->menuBar, sortCmds[i], i == (int32_t)col); - } - - wmMenuItemSetChecked(sState.win->menuBar, CMD_VIEW_SORT_DESC, descending); - } - - fileOpsListSort(sState.entries, sState.entryCount, col, descending); - setCells(); -} - - -// Copy To / Move To: ask for the destination folder. -static void cmdTransferTo(FileOpsOpE op) { - char **paths = getSelection(); - int32_t count = (int32_t)arrlen(paths); - const char *verb = op == FileOpsMoveE ? "Move" : "Copy"; - char prompt[FM_PROMPT_MAX]; - char dest[DVX_MAX_PATH]; - char norm[DVX_MAX_PATH]; - - if (count == 0) { - freeSelection(paths); - return; - } - - if (count == 1) { - snprintf(prompt, sizeof(prompt), "%s %s to folder:", verb, platformPathBaseName(paths[0])); - } else { - snprintf(prompt, sizeof(prompt), "%s %" PRId32 " items to folder:", verb, count); - } - - if (dvxInputBox(sCtx->shellCtx, verb, prompt, sState.curDir, dest, sizeof(dest))) { - if (fileOpsPathNormalize(dest, norm, sizeof(norm))) { - jobStart(op, paths, norm); - } else { - snprintf(prompt, sizeof(prompt), "%s is not an existing folder, or its drive is not ready.", dest); - showError(verb, prompt); - } - } - - freeSelection(paths); -} - - -static void cmdUp(void) { - char parent[DVX_MAX_PATH]; - - if (fileOpsPathParent(sState.curDir, parent, sizeof(parent))) { - navigate(parent, false); - } -} - - -// File Manager windows already open (other instances), used to cascade. -static int32_t countFileMgrWindows(const AppContextT *ac) { - int32_t n = 0; - - for (int32_t i = 0; i < ac->stack.count; i++) { - if (strncmp(ac->stack.windows[i]->title, FM_TITLE, strlen(FM_TITLE)) == 0) { - n++; - } - } - - return n; -} - - -// Drop the list view's reference first: it keeps the cell array. -static void freeListing(void) { - if (sState.list) { - wgtListViewSetData(sState.list, NULL, 0); - } - - fileOpsListFree(sState.entries, sState.entryCount); - free(sState.cells); - free(sState.rowText); - sState.entries = NULL; - sState.entryCount = 0; - sState.cells = NULL; - sState.rowText = NULL; -} - - -static void freeSelection(char **paths) { - for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { - free(paths[i]); - } - - arrfree(paths); -} - - -// Full paths the file commands act on: the tree's folder when the tree has -// focus, else the selected list rows (or the row under the cursor). -static char **getSelection(void) { - char **paths = NULL; - char path[DVX_MAX_PATH]; - - if (sState.treeActive) { - if (!fileOpsPathIsRoot(sState.curDir)) { - selectionAdd(&paths, sState.curDir); - } - - return paths; - } - - for (int32_t i = 0; i < sState.entryCount; i++) { - if (wgtListViewIsItemSelected(sState.list, i) && fileOpsPathJoin(path, sizeof(path), sState.curDir, sState.entries[i].name)) { - selectionAdd(&paths, path); - } - } - - int32_t row = wgtListViewGetSelected(sState.list); - - if (arrlen(paths) == 0 && row >= 0 && row < sState.entryCount && fileOpsPathJoin(path, sizeof(path), sState.curDir, sState.entries[row].name)) { - selectionAdd(&paths, path); - } - - return paths; -} - - -// Add every path to the job; report the ones that cannot be added. -static bool jobAddSources(FileOpsJobT *job, char **paths) { - bool any = false; - - for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { - if (fileOpsJobAddSource(job, paths[i])) { - any = true; - } else { - char msg[FM_PROMPT_MAX]; - - snprintf(msg, sizeof(msg), "%s cannot be found, or the destination path would be too long. It is skipped.", paths[i]); - showError(FM_TITLE, msg); - } - } - - return any; -} - - -// Put the job's question to the user (last button = Cancel) and hand the -// answer back. -static void jobAsk(FmJobUiT *j, const char * const *labels, const FileOpsAnswerE *answers, int32_t count) { - jobShowProgress(j); - - int32_t pick = dvxButtonBox(sCtx->shellCtx, FM_TITLE, fileOpsJobMessage(j->job), MB_ICONQUESTION, labels, count); - - fileOpsJobAnswer(j->job, answers[pick]); -} - - -// Runs on the job's task: close its window, free it, refresh the listing. -static void jobFinish(FmJobUiT *j, FileOpsStepE result) { - AppContextT *ac = sCtx->shellCtx; - - dvxLog("FileMgr: %s %s", j->verb, result == FileOpsStepDoneE ? "finished" : "cancelled"); - - for (int32_t i = 0; i < (int32_t)arrlen(sJobs); i++) { - if (sJobs[i] == j) { - arrdel(sJobs, i); - break; - } - } - - if (j->win) { - dvxDestroyWindow(ac, j->win); - } - - fileOpsJobFree(j->job); - free(j); - - if (sState.win) { - refresh(); - } -} - - -static void jobOnCancel(WidgetT *w) { - FmJobUiT *j = (FmJobUiT *)w->userData; - - fileOpsJobCancel(j->job); -} - - -// The close gadget cancels; the task closes the window when it stops. -static void jobOnClose(WindowT *win) { - FmJobUiT *j = (FmJobUiT *)win->userData; - - fileOpsJobCancel(j->job); -} - - -static void jobShowProgress(FmJobUiT *j) { - FileOpsProgressT p; - char line[FM_PROMPT_MAX]; - - if (!j->win) { - return; - } - - fileOpsJobProgress(j->job, &p); - snprintf(line, sizeof(line), "%s %s", j->verb, p.current); - wgtSetText(j->what, line); - snprintf(line, sizeof(line), "%" PRId32 " of %" PRId32 " files", p.filesDone, p.filesTotal); - wgtSetText(j->counts, line); - wgtProgressBarSetValue(j->bar, p.percent); -} - - -// Start a copy, move or delete of paths (into destDir) on its own task. -static void jobStart(FileOpsOpE op, char **paths, const char *destDir) { - AppContextT *ac = sCtx->shellCtx; - FileOpsJobT *job = fileOpsJobCreate(op, destDir); - FmJobUiT *j = (FmJobUiT *)calloc(1, sizeof(FmJobUiT)); - - if (!job || !j) { - fileOpsJobFree(job); - free(j); - showError(FM_TITLE, "Not enough memory to start the operation."); - return; - } - - if (!jobAddSources(job, paths)) { - fileOpsJobFree(job); - free(j); - return; - } - - j->job = job; - j->verb = op == FileOpsCopyE ? "Copying" : op == FileOpsMoveE ? "Moving" : "Deleting"; - j->win = dvxCreateWindowCentered(ac, j->verb, FM_JOB_WIN_W, FM_JOB_WIN_H, false); - - if (j->win) { - WidgetT *root = wgtInitWindow(ac, j->win); - - j->what = wgtLabel(root, ""); - j->bar = wgtProgressBar(root); - j->counts = wgtLabel(root, ""); - - WidgetT *row = wgtHBox(root); - WidgetT *cancel = wgtButton(row, "&Cancel"); - - row->align = AlignCenterE; - j->what->weight = WGT_WEIGHT_FILL; - cancel->userData = j; - cancel->onClick = jobOnCancel; - j->win->userData = j; - j->win->onClose = jobOnClose; - } - - jobShowProgress(j); - - j->taskId = tsCreate(j->verb, jobTask, j, FM_JOB_STACK, TS_PRIORITY_NORMAL); - - if (j->taskId < 0) { - if (j->win) { - dvxDestroyWindow(ac, j->win); - } - - fileOpsJobFree(job); - free(j); - showError(FM_TITLE, "Not enough memory to start the operation."); - return; - } - - arrput(sJobs, j); -} - - -// The job's task: step through the job in time slices, yielding between -// them, and ask the user when the job needs an answer. -static void jobTask(void *arg) { - FmJobUiT *j = (FmJobUiT *)arg; - PlatformTicksT slice = PLATFORM_MS_TO_TICKS(FM_SLICE_MS); - PlatformTicksT sliceEnd; - FileOpsStepE step; - - sCtx->shellCtx->currentAppId = sCtx->appId; - sliceEnd = platformClock() + slice; - - for (step = fileOpsJobStep(j->job); step != FileOpsStepDoneE && step != FileOpsStepCancelledE; step = fileOpsJobStep(j->job)) { - if (step == FileOpsStepAskE) { - jobAsk(j, sOverwriteLabels, sOverwriteAnswers, DVX_ARRAY_LEN(sOverwriteLabels)); - sliceEnd = platformClock() + slice; - } else if (step == FileOpsStepErrorE) { - jobAsk(j, sErrorLabels, sErrorAnswers, DVX_ARRAY_LEN(sErrorLabels)); - sliceEnd = platformClock() + slice; - } else if (platformClock() >= sliceEnd) { - jobShowProgress(j); - tsYield(); - sliceEnd = platformClock() + slice; - } - } - - jobFinish(j, step); -} - - -static void listOnDblClick(WidgetT *w) { - (void)w; - cmdOpen(); -} - - -static void listOnFocus(WidgetT *w) { - (void)w; - sState.treeActive = false; -} - - -// Header click: sort by that column; the same column again reverses. The -// list's own text sort is cleared (sizes and dates need real ordering). -static void listOnHeader(WidgetT *w, int32_t col, ListViewSortE dir) { - static const FileOpsSortE colSort[FM_COL_COUNT] = { FileOpsSortNameE, FileOpsSortSizeE, FileOpsSortTypeE, FileOpsSortDateE }; - - (void)dir; - wgtListViewSetSort(w, -1, ListViewSortNoneE); - - if (col < 0 || col >= FM_COL_COUNT) { - return; - } - - FileOpsSortE sort = colSort[col]; - - cmdSort(sort, sort == sState.sortCol ? !sState.sortDesc : false); -} - - -static void listOnKey(WidgetT *w, int32_t keyCode, int32_t shift) { - (void)w; - (void)shift; - - switch (keyCode) { - case KEY_DELETE: - cmdDelete(); - break; - - case KEY_BACKSPACE: - cmdUp(); - break; - - case KEY_CTRL_A: - cmdSelectAll(); - break; - - case KEY_CTRL_C: - clipboardPut(false); - break; - - case KEY_CTRL_X: - clipboardPut(true); - break; - - case KEY_CTRL_V: - cmdPaste(); - break; - - default: - break; - } -} - - -static void listOnSelChange(WidgetT *w) { - (void)w; - updateStatus(); -} - - -// Read dir into the list (sorted), replacing the current listing. False -// (listing unchanged) when it cannot be read. -static bool loadListing(const char *dir) { - FileOpsEntryT *entries = NULL; - int32_t count = 0; - - if (!fileOpsListDir(dir, sState.filter, &entries, &count)) { - return false; - } - - freeListing(); - sState.entries = entries; - sState.entryCount = count; - sState.cells = (const char **)calloc((size_t)(count > 0 ? count : 1) * FM_COL_COUNT, sizeof(const char *)); - sState.rowText = (FmRowTextT *)calloc((size_t)(count > 0 ? count : 1), sizeof(FmRowTextT)); - - if (!sState.cells || !sState.rowText) { - freeListing(); - return false; - } - - fileOpsListSort(sState.entries, sState.entryCount, sState.sortCol, sState.sortDesc); - setCells(); - return true; -} - - -// Go to a folder. fromTree: the user chose it in the tree, which then -// only grows its sub-folders; otherwise the tree is rebuilt. -static bool navigate(const char *path, bool fromTree) { - char norm[DVX_MAX_PATH]; - char msg[FM_PROMPT_MAX]; - - if (!fileOpsPathNormalize(path, norm, sizeof(norm)) || !loadListing(norm)) { - snprintf(msg, sizeof(msg), "Cannot open %s.\nThe folder does not exist or the drive is not ready.", path); - showError(FM_TITLE, msg); - return false; - } - - snprintf(sState.curDir, sizeof(sState.curDir), "%s", norm); - wgtSetText(sState.pathInput, sState.curDir); - updateTitle(); - updateStatus(); - - if (fromTree) { - WidgetT *item = wgtTreeViewGetSelected(sState.tree); - - if (item && !item->firstChild) { - treeAddChildren(item, sState.curDir, NULL, NULL); - wgtTreeItemSetExpanded(item, true); - } - } else { - treeRebuild(); - } - - return true; -} - - -// Closing with operations still running asks first; the jobs then stop -// and close their own windows (the program ends with the last one). -static void onClose(WindowT *win) { - AppContextT *ac = sCtx->shellCtx; - - if (arrlen(sJobs) > 0) { - if (dvxMessageBox(ac, FM_TITLE, "File operations are still running.\nStop them and close?", MB_YESNO | MB_ICONQUESTION) != ID_YES) { - return; - } - - for (int32_t i = 0; i < (int32_t)arrlen(sJobs); i++) { - fileOpsJobCancel(sJobs[i]->job); - } - } - - freeListing(); - treeFreePaths(); - sState.list = NULL; - sState.win = NULL; - dvxDestroyWindow(ac, win); - - if (sState.accel) { - dvxFreeAccelTable(sState.accel); - sState.accel = NULL; - } -} - - -// Drive button: that drive's current folder ("C:" resolves to it). -static void onDriveClick(WidgetT *w) { - char spec[PLATFORM_DRIVE_ROOT_MAX]; - char letter = (char)(intptr_t)w->userData; - - if (letter == '/') { - snprintf(spec, sizeof(spec), "/"); - } else { - snprintf(spec, sizeof(spec), "%c:", letter); - } - - navigate(spec, false); -} - - -static void onMenu(WindowT *win, int32_t menuId) { - (void)win; - - switch (menuId) { - case CMD_FILE_OPEN: - cmdOpen(); - break; - - case CMD_FILE_NEW_FOLDER: - cmdNewFolder(); - break; - - case CMD_FILE_COPY_TO: - cmdTransferTo(FileOpsCopyE); - break; - - case CMD_FILE_MOVE_TO: - cmdTransferTo(FileOpsMoveE); - break; - - case CMD_FILE_RENAME: - cmdRename(); - break; - - case CMD_FILE_DELETE: - cmdDelete(); - break; - - case CMD_FILE_PROPERTIES: - cmdProperties(); - break; - - case CMD_FILE_NEW_WINDOW: - shellLoadAppWithArgs(sCtx->shellCtx, sCtx->appPath, sState.curDir); - break; - - case CMD_FILE_EXIT: - onClose(sState.win); - break; - - case CMD_EDIT_CUT: - clipboardPut(true); - break; - - case CMD_EDIT_COPY: - clipboardPut(false); - break; - - case CMD_EDIT_PASTE: - cmdPaste(); - break; - - case CMD_EDIT_SELECT_ALL: - cmdSelectAll(); - break; - - case CMD_EDIT_INVERT: - cmdInvert(); - break; - - case CMD_VIEW_REFRESH: - refresh(); - break; - - case CMD_VIEW_FILTER: - cmdFilter(); - break; - - case CMD_VIEW_UP: - cmdUp(); - break; - - case CMD_VIEW_SORT_NAME: - cmdSort(FileOpsSortNameE, sState.sortDesc); - break; - - case CMD_VIEW_SORT_SIZE: - cmdSort(FileOpsSortSizeE, sState.sortDesc); - break; - - case CMD_VIEW_SORT_TYPE: - cmdSort(FileOpsSortTypeE, sState.sortDesc); - break; - - case CMD_VIEW_SORT_DATE: - cmdSort(FileOpsSortDateE, sState.sortDesc); - break; - - case CMD_VIEW_SORT_DESC: - cmdSort(sState.sortCol, !sState.sortDesc); - break; - - case CMD_HELP_CONTENTS: - shellOpenHelp(sCtx->shellCtx, sCtx->helpFile); - break; - - case CMD_HELP_ABOUT: - dvxInfoBox(sCtx->shellCtx, "About File Manager", "File Manager\n\nBrowse, copy, move, rename and delete files and folders.\n\nCopyright 2026 Scott Duensing"); - break; - - default: - break; - } -} - - -// Enter in the path bar goes to the typed folder. -static void onPathKey(WidgetT *w, int32_t keyCode, int32_t shift) { - (void)shift; - - if (keyCode == KEY_ENTER) { - navigate(wgtGetText(w), false); - } -} - - -static void openEntry(const FileOpsEntryT *e) { - AppContextT *ac = sCtx->shellCtx; - char path[DVX_MAX_PATH]; - char program[DVX_MAX_PATH]; - char msg[FM_PROMPT_MAX]; - - if (!fileOpsPathJoin(path, sizeof(path), sState.curDir, e->name)) { - return; - } - - if (e->isDir) { - navigate(path, false); - return; - } - - if (dvxHasExt(e->name, ".app")) { - shellLoadApp(ac, path); - return; - } - - if (!shellAssocLookup(fileOpsExtension(e->name), program, sizeof(program))) { - snprintf(msg, sizeof(msg), "No program is set to open %s files.\nFile types are set in the Control Panel.", fileOpsExtension(e->name)[0] ? fileOpsExtension(e->name) : "these"); - showError(FM_TITLE, msg); - return; - } - - if (!shellAssocOpenDocument(ac, path)) { - snprintf(msg, sizeof(msg), "%s could not be opened.", e->name); - showError(FM_TITLE, msg); - } -} - - -// Properties OK: read the check boxes while they still exist. -static bool propsOnOk(WindowT *dlg, void *userCtx) { - FmPropsDlgT *pd = (FmPropsDlgT *)userCtx; - - (void)dlg; - pd->chosen = wgtCheckboxIsChecked(pd->readOnly) ? FILEOPS_ATTR_READONLY : 0; - pd->chosen |= wgtCheckboxIsChecked(pd->hidden) ? FILEOPS_ATTR_HIDDEN : 0; - pd->chosen |= wgtCheckboxIsChecked(pd->system) ? FILEOPS_ATTR_SYSTEM : 0; - pd->chosen |= wgtCheckboxIsChecked(pd->archive) ? FILEOPS_ATTR_ARCHIVE : 0; - return true; -} - - -// Re-read the current folder, keeping the row under the cursor. -static void refresh(void) { - char keep[FM_NAME_MAX]; - int32_t row = wgtListViewGetSelected(sState.list); - - keep[0] = '\0'; - - if (row >= 0 && row < sState.entryCount) { - snprintf(keep, sizeof(keep), "%s", sState.entries[row].name); - } - - if (!loadListing(sState.curDir)) { - // The folder is gone (deleted, renamed, disk removed): go up. - char parent[DVX_MAX_PATH]; - - if (fileOpsPathParent(sState.curDir, parent, sizeof(parent)) && navigate(parent, false)) { - return; - } - - freeListing(); - } - - selectByName(keep); - treeRebuild(); - updateTitle(); - updateStatus(); -} - - -// Put the list cursor (and selection) on the entry called name, if any. -static void selectByName(const char *name) { - for (int32_t i = 0; i < sState.entryCount; i++) { - if (strcasecmp(sState.entries[i].name, name) == 0) { - wgtListViewSetSelected(sState.list, i); - wgtListViewSetItemSelected(sState.list, i, true); - updateStatus(); - return; - } - } -} - - -// Adds a copy of path to a selection list; a copy that cannot be made is -// left out, so every entry is a string. -static void selectionAdd(char ***paths, const char *path) { - char *copy = strdup(path); - - if (copy) { - arrput(*paths, copy); - } -} - - -// Fill the cell array from the listing and hand it to the list view. -static void setCells(void) { - for (int32_t i = 0; i < sState.entryCount; i++) { - const FileOpsEntryT *e = &sState.entries[i]; - FmRowTextT *t = &sState.rowText[i]; - const char **cell = &sState.cells[i * FM_COL_COUNT]; - - t->size[0] = '\0'; - - if (!e->isDir) { - fileOpsFormatSize(e->size, t->size, sizeof(t->size)); - } - - fileOpsTypeName(e, t->type, sizeof(t->type)); - fileOpsFormatDate(e->mtime, t->date, sizeof(t->date)); - - cell[FM_COL_NAME] = e->name; - cell[FM_COL_SIZE] = t->size; - cell[FM_COL_TYPE] = t->type; - cell[FM_COL_DATE] = t->date; - } - - wgtListViewSetData(sState.list, sState.cells, sState.entryCount); - wgtListViewClearSelection(sState.list); - updateStatus(); -} - - -static void showError(const char *title, const char *message) { - dvxMessageBox(sCtx->shellCtx, title, message, MB_OK | MB_ICONERROR); -} - - -// Add dir's sub-folders under parentItem. When expandPath is one of them, -// *outNext receives its item. -static void treeAddChildren(WidgetT *parentItem, const char *dir, const char *expandPath, WidgetT **outNext) { - FileOpsEntryT *entries = NULL; - int32_t count = 0; - char path[DVX_MAX_PATH]; - - if (!fileOpsListDir(dir, NULL, &entries, &count)) { - return; - } - - fileOpsListSort(entries, count, FileOpsSortNameE, false); - - for (int32_t i = 0; i < count && entries[i].isDir; i++) { - if (!fileOpsPathJoin(path, sizeof(path), dir, entries[i].name)) { - continue; - } - - WidgetT *item = treeAddItem(parentItem, path, entries[i].name); - - if (item && outNext && expandPath && fileOpsPathSame(path, expandPath)) { - *outNext = item; - } - } - - fileOpsListFree(entries, count); -} - - -// One tree item remembering its folder path. -static WidgetT *treeAddItem(WidgetT *parent, const char *path, const char *label) { - char *copy = strdup(path); - - if (!copy) { - return NULL; - } - - WidgetT *item = wgtTreeItem(parent, label); - - if (!item) { - free(copy); - return NULL; - } - - arrput(sState.treePaths, copy); - item->userData = copy; - return item; -} - - -static void treeFreePaths(void) { - for (int32_t i = 0; i < (int32_t)arrlen(sState.treePaths); i++) { - free(sState.treePaths[i]); - } - - arrfree(sState.treePaths); - sState.treePaths = NULL; -} - - -static void treeOnChange(WidgetT *w) { - if (sState.syncingTree) { - return; - } - - WidgetT *item = wgtTreeViewGetSelected(w); - - if (item && item->userData && !fileOpsPathSame((const char *)item->userData, sState.curDir)) { - navigate((const char *)item->userData, true); - } -} - - -static void treeOnFocus(WidgetT *w) { - (void)w; - sState.treeActive = true; -} - - -static void treeOnKey(WidgetT *w, int32_t keyCode, int32_t shift) { - (void)w; - (void)shift; - - if (keyCode == KEY_DELETE) { - cmdDelete(); - } else if (keyCode == KEY_BACKSPACE) { - cmdUp(); - } -} - - -// Root, then each folder on the way to the current one with its sibling -// folders, then the current folder's own sub-folders. -static void treeRebuild(void) { - sState.syncingTree = true; - - while (sState.tree->firstChild) { - wgtDestroy(sState.tree->firstChild); - } - - treeFreePaths(); - - // Walk up from the current folder to the root, remembering each step - // (one block: the chain is too big for a task stack). - char (*chain)[DVX_MAX_PATH] = malloc(sizeof(*chain) * FM_TREE_DEPTH_MAX); - int32_t depth = 0; - - if (chain) { - snprintf(chain[0], sizeof(chain[0]), "%s", sState.curDir); - depth = 1; - - while (depth < FM_TREE_DEPTH_MAX && fileOpsPathParent(chain[depth - 1], chain[depth], sizeof(chain[depth]))) { - depth++; - } - } - - WidgetT *item = depth > 0 ? treeAddItem(sState.tree, chain[depth - 1], chain[depth - 1]) : NULL; - - for (int32_t level = depth - 1; item && level > 0; level--) { - WidgetT *next = NULL; - - treeAddChildren(item, chain[level], chain[level - 1], &next); - wgtTreeItemSetExpanded(item, true); - item = next; - } - - if (item) { - treeAddChildren(item, sState.curDir, NULL, NULL); - wgtTreeItemSetExpanded(item, true); - wgtTreeViewSetSelected(sState.tree, item); - } - - free(chain); - sState.syncingTree = false; -} - - -static void updateStatus(void) { - char text[FM_STATUS_MAX]; - char sizeText[FILEOPS_SIZE_TEXT_MAX]; - int32_t selected = 0; - uint64_t bytes = 0; - uint64_t totalBytes; - uint64_t freeBytes; - - for (int32_t i = 0; i < sState.entryCount; i++) { - if (wgtListViewIsItemSelected(sState.list, i)) { - selected++; - bytes += sState.entries[i].size; - } - } - - fileOpsFormatSize(bytes, sizeText, sizeof(sizeText)); - - if (selected > 0) { - snprintf(text, sizeof(text), "%" PRId32 " of %" PRId32 " selected (%s bytes)", selected, sState.entryCount, sizeText); - } else { - snprintf(text, sizeof(text), "%" PRId32 " items", sState.entryCount); - } - - wgtSetText(sState.statusSel, text); - - if (platformDiskSpace(sState.curDir, &totalBytes, &freeBytes)) { - fileOpsFormatSize(freeBytes, sizeText, sizeof(sizeText)); - snprintf(text, sizeof(text), "%s bytes free", sizeText); - } else { - text[0] = '\0'; - } - - wgtSetText(sState.statusFree, text); -} - - -static void updateTitle(void) { - char title[FM_TITLE_MAX]; - bool filtered = strcmp(sState.filter, PLATFORM_FILTER_ALL) != 0; - - snprintf(title, sizeof(title), FM_TITLE " - %s%s%s", sState.curDir, filtered ? DVX_PATH_SEP : "", filtered ? sState.filter : ""); - dvxSetTitle(sCtx->shellCtx, sState.win, title); -} - - -// Arguments: an optional starting folder (default: the current one). -int32_t appMain(DxeAppContextT *ctx) { - AppContextT *ac = ctx->shellCtx; - int32_t offset = countFileMgrWindows(ac) * FM_CASCADE; - char start[DVX_MAX_PATH]; - - sCtx = ctx; - memset(&sState, 0, sizeof(sState)); - snprintf(sState.filter, sizeof(sState.filter), "%s", PLATFORM_FILTER_ALL); - sState.sortCol = FileOpsSortNameE; - - int32_t winX = FM_WIN_X + offset; - int32_t winY = FM_WIN_Y + offset; - - if (winX + FM_WIN_W > ac->display.width || winY + FM_WIN_H > ac->display.height) { - winX = FM_WIN_X; - winY = FM_WIN_Y; - } - - sState.win = dvxCreateWindow(ac, FM_TITLE, winX, winY, FM_WIN_W, FM_WIN_H, true); - - if (!sState.win) { - return -1; - } - - sState.win->onClose = onClose; - sState.win->onMenu = onMenu; - - buildMenus(); - buildWindow(); - - if (ctx->args[0]) { - snprintf(start, sizeof(start), "%s", ctx->args); - } else if (!getcwd(start, sizeof(start))) { - snprintf(start, sizeof(start), "%s", FM_CUR_DIR); - } - - if (!navigate(start, false)) { - PlatformDriveT drive; - - if (platformDriveList(&drive, 1) == 1) { - navigate(drive.root, false); - } - } - - wgtSetFocused(sState.list); - sState.treeActive = false; - return 0; -} diff --git a/src/apps/kpunch/fileMgr/fileMgr.dbp b/src/apps/kpunch/fileMgr/fileMgr.dbp new file mode 100644 index 00000000..9272e53e --- /dev/null +++ b/src/apps/kpunch/fileMgr/fileMgr.dbp @@ -0,0 +1,29 @@ +[Project] +Name = File Manager +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Browse, copy, move, rename and delete files and folders +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +File1 = ../../../include/basic/fileops.bas +File2 = ../../../include/basic/help.bas +File3 = ../../../include/basic/shell.bas +File4 = fileMgr.bas +Count = 5 + +[Classes] +File0 = fmJob.cls +Count = 1 + +[Forms] +File0 = fileMgr.frm +File1 = fmProps.frm +Count = 2 + +[Settings] +StartupForm = FileMgrForm +MultiInstance = True diff --git a/src/apps/kpunch/fileMgr/fileMgr.frm b/src/apps/kpunch/fileMgr/fileMgr.frm new file mode 100644 index 00000000..7de0b838 --- /dev/null +++ b/src/apps/kpunch/fileMgr/fileMgr.frm @@ -0,0 +1,1507 @@ +VERSION DVX 1.00 +Begin Form FileMgrForm + Caption = "File Manager" + Layout = VBox + AutoSize = False + Resizable = True + Centered = False + Width = 600 + Height = 400 + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuOpen + Caption = "&Open" + ShortcutText = "Enter" + End + Begin Menu mnuNewFolder + Caption = "New &Folder..." + End + Begin Menu mnuFileSep1 + Caption = "-" + End + Begin Menu mnuCopyTo + Caption = "&Copy To..." + Shortcut = "F8" + End + Begin Menu mnuMoveTo + Caption = "&Move To..." + Shortcut = "F7" + End + Begin Menu mnuRename + Caption = "&Rename..." + Shortcut = "F2" + End + Begin Menu mnuDelete + Caption = "&Delete" + ShortcutText = "Del" + End + Begin Menu mnuProperties + Caption = "P&roperties" + Shortcut = "Alt+Enter" + End + Begin Menu mnuFileSep2 + Caption = "-" + End + Begin Menu mnuNewWindow + Caption = "New &Window" + End + Begin Menu mnuFileSep3 + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + End + End + Begin Menu mnuEdit + Caption = "&Edit" + Begin Menu mnuCut + Caption = "Cu&t" + ShortcutText = "Ctrl+X" + End + Begin Menu mnuCopy + Caption = "&Copy" + ShortcutText = "Ctrl+C" + End + Begin Menu mnuPaste + Caption = "&Paste" + ShortcutText = "Ctrl+V" + End + Begin Menu mnuEditSep1 + Caption = "-" + End + Begin Menu mnuSelectAll + Caption = "Select &All" + ShortcutText = "Ctrl+A" + End + Begin Menu mnuInvert + Caption = "&Invert Selection" + End + End + Begin Menu mnuView + Caption = "&View" + Begin Menu mnuRefresh + Caption = "&Refresh" + Shortcut = "F5" + End + Begin Menu mnuFilter + Caption = "&Filter..." + End + Begin Menu mnuUp + Caption = "&Up One Level" + ShortcutText = "Backspace" + End + Begin Menu mnuViewSep1 + Caption = "-" + End + Begin Menu mnuSortName + Caption = "Sort by &Name" + RadioCheck = True + Checked = True + End + Begin Menu mnuSortSize + Caption = "Sort by &Size" + RadioCheck = True + End + Begin Menu mnuSortType + Caption = "Sort by &Type" + RadioCheck = True + End + Begin Menu mnuSortDate + Caption = "Sort by &Date" + RadioCheck = True + End + Begin Menu mnuViewSep2 + Caption = "-" + End + Begin Menu mnuSortDesc + Caption = "D&escending" + End + End + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuHelpContents + Caption = "&Contents" + ShortcutText = "F1" + End + Begin Menu mnuHelpSep + Caption = "-" + End + Begin Menu mnuAbout + Caption = "&About File Manager..." + End + End + Begin Toolbar tbDrives + Begin CommandButton cmdDrive + Index = 0 + Visible = False + End + Begin CommandButton cmdDrive + Index = 1 + Visible = False + End + Begin CommandButton cmdDrive + Index = 2 + Visible = False + End + Begin CommandButton cmdDrive + Index = 3 + Visible = False + End + Begin CommandButton cmdDrive + Index = 4 + Visible = False + End + Begin CommandButton cmdDrive + Index = 5 + Visible = False + End + Begin CommandButton cmdDrive + Index = 6 + Visible = False + End + Begin CommandButton cmdDrive + Index = 7 + Visible = False + End + Begin CommandButton cmdDrive + Index = 8 + Visible = False + End + Begin CommandButton cmdDrive + Index = 9 + Visible = False + End + Begin CommandButton cmdDrive + Index = 10 + Visible = False + End + Begin CommandButton cmdDrive + Index = 11 + Visible = False + End + Begin CommandButton cmdDrive + Index = 12 + Visible = False + End + Begin CommandButton cmdDrive + Index = 13 + Visible = False + End + Begin CommandButton cmdDrive + Index = 14 + Visible = False + End + Begin CommandButton cmdDrive + Index = 15 + Visible = False + End + Begin CommandButton cmdDrive + Index = 16 + Visible = False + End + Begin CommandButton cmdDrive + Index = 17 + Visible = False + End + Begin CommandButton cmdDrive + Index = 18 + Visible = False + End + Begin CommandButton cmdDrive + Index = 19 + Visible = False + End + Begin CommandButton cmdDrive + Index = 20 + Visible = False + End + Begin CommandButton cmdDrive + Index = 21 + Visible = False + End + Begin CommandButton cmdDrive + Index = 22 + Visible = False + End + Begin CommandButton cmdDrive + Index = 23 + Visible = False + End + Begin CommandButton cmdDrive + Index = 24 + Visible = False + End + Begin CommandButton cmdDrive + Index = 25 + Visible = False + End + End + Begin HBox boxPath + Begin Label lblPath + Caption = "&Path:" + End + Begin TextBox fmPath + Weight = 1 + End + End + Begin Splitter splMain + Weight = 1 + Begin TreeView fmTree + End + Begin ListView fmList + End + End + Begin StatusBar sbStatus + Begin Label fmStatus + Caption = "" + Weight = 1 + End + Begin Label lblFree + Caption = "" + End + End +End + + +' 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. + + +' fileMgr.frm -- the File Manager window. +' +' A user interface over the file operations library (fileops.bas): drive +' buttons, the path box, the folder tree, the sortable file list and the +' status bar, with open / run, copy, move, rename, delete, new folder, +' properties, selection and filter commands. +' +' More than one File Manager may run (MultiInstance in fileMgr.dbp): File +' > New Window starts another. Cut / Copy / Paste of files between File +' Manager windows goes through the DVX clipboard as text: a header line +' (FM_CLIP_COPY or FM_CLIP_CUT) followed by one full path per line, so +' pasting the same text into a text editor gives the list of paths. +' +' The tree shows the path from the drive root to the current folder, the +' folders beside each step, and the folders inside the current one; each +' item keeps its folder's path as its tag. Choosing a folder in the tree +' lists it and adds its sub-folders under it; navigating any other way +' rebuilds the tree. +' +' Copy, move and delete run as jobs with their own windows (fileMgr.bas). + +OPTION EXPLICIT + +CONST HELP_FILE = "fileMgr.hlp" + +' Placement: FM_WIN_X, FM_WIN_Y, and FM_CASCADE further down and right for +' every File Manager already open (back to the start when the window would +' leave the screen) +CONST FM_WIN_X = 40 +CONST FM_WIN_Y = 30 +CONST FM_CASCADE = 20 + +' Width of the folder tree beside the file list +CONST FM_TREE_W = 170 + +' File list columns, in FILEOPS_SORT_ order (a header click sorts by its +' column): Name, Size (right-aligned), Type, Modified; widths in characters +CONST FM_COLUMNS = "Name,14|Size,12,Right|Type,10|Modified,16" + +CONST FM_DEFAULT_FOLDER = "NEWFOLDR" +CONST FM_FILTER_ALL = "*.*" +CONST FM_CUR_DIR = "." + +' A single-root file system's drive (FileOpsDriveList$ gives "/") +CONST FM_ROOT_DRIVE = "/" +CONST FM_DRIVE_SEP = ":" +CONST FM_ROOT_SEP = "\" + +' Clipboard text of copied or cut files +CONST FM_CLIP_COPY = "DVX-FILES COPY" +CONST FM_CLIP_CUT = "DVX-FILES CUT" +CONST FM_CLIP_EOL = CHR$(10) + +' KeyDown codes of Ctrl+letter (Ctrl+A is 1 ... Ctrl+Z is 26) +CONST KEY_CTRL_A = 1 +CONST KEY_CTRL_C = 3 +CONST KEY_CTRL_V = 22 +CONST KEY_CTRL_X = 24 + +' Separator of ListView.SelectedRows$ +CONST FM_ROW_SEP = " " + +' Deepest folder chain the tree shows +CONST FM_TREE_DEPTH_MAX = 130 + +DIM SHARED curFolder AS STRING ' the folder shown +DIM SHARED curFilter AS STRING ' files shown: a wildcard pattern +DIM SHARED sortCol AS LONG ' FILEOPS_SORT_ column +DIM SHARED sortDesc AS BOOLEAN +DIM SHARED listing AS LONG ' the folder's listing (fileops handle) +DIM SHARED driveLetters AS STRING ' FileOpsDriveList$: one per drive button +DIM SHARED treeActive AS BOOLEAN ' the tree (not the list) has the focus +DIM SHARED syncingTree AS BOOLEAN ' the program is setting the tree's item + + +' ============================================================ +' Window +' ============================================================ + +' Arguments: an optional starting folder (default: the current one). +SUB FileMgrForm_Load + DIM offset AS LONG + DIM winX AS LONG + DIM winY AS LONG + DIM start AS STRING + + fmMainOpen = True + curFilter = FM_FILTER_ALL + sortCol = FILEOPS_SORT_NAME + sortDesc = False + + offset = (ShellInstanceCount() - 1) * FM_CASCADE + winX = FM_WIN_X + offset + winY = FM_WIN_Y + offset + + IF winX + FileMgrForm.Width > Screen.Width OR winY + FileMgrForm.Height > Screen.Height THEN + winX = FM_WIN_X + winY = FM_WIN_Y + END IF + + FileMgrForm.Left = winX + FileMgrForm.Top = winY + + AddDriveButtons + fmList.SetColumns FM_COLUMNS + fmList.SetMultiSelect True + splMain.Position = FM_TREE_W + + start = COMMAND$ + + IF start = "" THEN + start = CURDIR$ + END IF + + IF start = "" THEN + start = FM_CUR_DIR + END IF + + ' A folder that cannot be shown: the first drive's root + IF NOT Navigate(start, False) AND driveLetters <> "" THEN + Navigate DriveRoot(0), False + END IF + + fmList.SetFocus + treeActive = False +END SUB + + +' Closing with operations still running asks first; the jobs then stop +' and close their own windows (the program ends with the last one). +SUB FileMgrForm_QueryUnload(Cancel AS INTEGER) + DIM job AS FmJob + DIM msg AS STRING + + IF fmJobs.Count = 0 THEN + EXIT SUB + END IF + + msg = "File operations are still running." & CHR$(10) & "Stop them and close?" + + IF MsgBox(msg, vbYesNo + vbQuestion, FM_TITLE) <> vbYes THEN + Cancel = 1 + EXIT SUB + END IF + + FOR EACH job IN fmJobs + FileOpsJobCancel job.Handle + NEXT job +END SUB + + +SUB FileMgrForm_Unload + fmMainOpen = False + FreeListing + + ' Every form of the program is loaded when it starts, and a loaded + ' form keeps it running: unload the hidden Properties dialog so the + ' program ends with its window (job windows end with their jobs). + Unload PropsForm +END SUB + + +' One button per drive that exists. The DOS probe never touches the +' media, so empty floppy and CD-ROM drives cost nothing here. +SUB AddDriveButtons + DIM i AS INTEGER + DIM letter AS STRING + + driveLetters = FileOpsDriveList$() + + FOR i = 0 TO Len(driveLetters) - 1 + letter = Mid$(driveLetters, i + 1, 1) + + ' "C:" for a DOS drive, "/" for a single-root file system + IF letter = FM_ROOT_DRIVE THEN + cmdDrive(i).Caption = letter + ELSE + cmdDrive(i).Caption = letter & FM_DRIVE_SEP + END IF + + cmdDrive(i).Visible = True + NEXT i +END SUB + + +' The root folder of drive button index ("C:\", or "/"). +FUNCTION DriveRoot(BYVAL index AS INTEGER) AS STRING + DIM letter AS STRING + + letter = Mid$(driveLetters, index + 1, 1) + + IF letter = FM_ROOT_DRIVE THEN + DriveRoot = letter + ELSE + DriveRoot = letter & FM_DRIVE_SEP & FM_ROOT_SEP + END IF +END FUNCTION + + +' Drive button: that drive's current folder ("C:" resolves to it). +SUB cmdDrive_Click(Index AS INTEGER) + DIM letter AS STRING + + letter = Mid$(driveLetters, Index + 1, 1) + + IF letter = FM_ROOT_DRIVE THEN + Navigate letter, False + ELSE + Navigate letter & FM_DRIVE_SEP, False + END IF +END SUB + + +SUB UpdateTitle + IF curFilter <> FM_FILTER_ALL THEN + FileMgrForm.Caption = FM_TITLE & " - " & FileOpsPathJoin$(curFolder, curFilter) + ELSE + FileMgrForm.Caption = FM_TITLE & " - " & curFolder + END IF +END SUB + + +' "N items" or "S of N selected (B bytes)", and the free space of the +' folder's drive. +SUB UpdateStatus + DIM rows AS STRING + DIM row AS VARIANT + DIM selected AS LONG + DIM bytes AS DOUBLE + DIM freeBytes AS DOUBLE + + rows = fmList.SelectedRows() + + IF rows <> "" THEN + FOR EACH row IN Split(rows, FM_ROW_SEP) + selected = selected + 1 + bytes = bytes + FileOpsListSize(listing, CLng(row)) + NEXT row + END IF + + IF selected > 0 THEN + fmStatus.Caption = CStr(selected) & " of " & CStr(FileOpsListCount(listing)) & " selected (" & FileOpsFormatSize$(bytes) & " bytes)" + ELSE + fmStatus.Caption = CStr(FileOpsListCount(listing)) & " items" + END IF + + freeBytes = FileOpsFreeBytes(curFolder) + + IF freeBytes >= 0 THEN + lblFree.Caption = FileOpsFormatSize$(freeBytes) & " bytes free" + ELSE + lblFree.Caption = "" + END IF +END SUB + + +' ============================================================ +' The listing +' ============================================================ + +SUB FreeListing + IF listing <> 0 THEN + FileOpsListFree listing + listing = 0 + END IF +END SUB + + +' Read folder into the list (sorted), replacing the listing. False (listing +' unchanged) when it cannot be read. +FUNCTION LoadListing(BYVAL folder AS STRING) AS BOOLEAN + DIM newListing AS LONG + + newListing = FileOpsList(folder, curFilter) + + IF newListing = 0 THEN + LoadListing = False + EXIT FUNCTION + END IF + + FreeListing + listing = newListing + FileOpsListSort listing, sortCol, sortDesc + ShowListing + LoadListing = True +END FUNCTION + + +' Put the listing in the file list (selection cleared). +SUB ShowListing + fmList.SetRows FileOpsListRows$(listing) + fmList.ClearSelection + UpdateStatus +END SUB + + +' Go to a folder. fromTree: the user chose it in the tree, which then +' only grows its sub-folders; otherwise the tree is rebuilt. +FUNCTION Navigate(BYVAL path AS STRING, BYVAL fromTree AS BOOLEAN) AS BOOLEAN + DIM norm AS STRING + DIM item AS LONG + + norm = FileOpsPathNormalize$(path) + + IF norm <> "" THEN + IF LoadListing(norm) THEN + curFolder = norm + ELSE + norm = "" + END IF + END IF + + IF norm = "" THEN + ShowError FM_TITLE, "Cannot open " & path & "." & CHR$(10) & "The folder does not exist or the drive is not ready." + Navigate = False + EXIT FUNCTION + END IF + + fmPath.Text = curFolder + UpdateTitle + UpdateStatus + + IF fromTree THEN + item = fmTree.ListIndex + + IF item >= 0 THEN + IF NOT fmTree.HasChildren(item) THEN + TreeAddChildren item, curFolder, "" + fmTree.SetExpanded item, True + END IF + END IF + ELSE + TreeRebuild + END IF + + Navigate = True +END FUNCTION + + +' Re-read the current folder, keeping the row under the cursor. Called by +' the jobs too, when one ends. +SUB FmRefresh + DIM keep AS STRING + DIM row AS LONG + DIM parent AS STRING + + row = fmList.ListIndex + + IF row >= 0 AND row < FileOpsListCount(listing) THEN + keep = FileOpsListName$(listing, row) + END IF + + IF NOT LoadListing(curFolder) THEN + ' The folder is gone (deleted, renamed, disk removed): go up + parent = FileOpsPathParent$(curFolder) + + IF parent <> "" THEN + IF Navigate(parent, False) THEN + EXIT SUB + END IF + END IF + + FreeListing + fmList.SetRows "" + END IF + + SelectByName keep + TreeRebuild + UpdateTitle + UpdateStatus +END SUB + + +' Put the list cursor (and selection) on the entry called entryName, if +' any. +SUB SelectByName(BYVAL entryName AS STRING) + DIM row AS LONG + + row = FileOpsListFind(listing, entryName) + + IF row >= 0 THEN + fmList.ListIndex = row + fmList.SetItemSelected row, True + UpdateStatus + END IF +END SUB + + +' The full paths the file commands act on: the tree's folder when the tree +' has the focus (none at a root), else the selected rows (or the row under +' the cursor). +FUNCTION GetSelection() AS Collection + DIM paths AS NEW Collection + DIM rows AS STRING + DIM row AS VARIANT + DIM cursor AS LONG + DIM path AS STRING + + IF treeActive THEN + IF NOT FileOpsPathIsRoot(curFolder) THEN + paths.Add curFolder + END IF + ELSE + rows = fmList.SelectedRows() + + IF rows <> "" THEN + FOR EACH row IN Split(rows, FM_ROW_SEP) + path = FileOpsPathJoin$(curFolder, FileOpsListName$(listing, CLng(row))) + + IF path <> "" THEN + paths.Add path + END IF + NEXT row + END IF + + cursor = fmList.ListIndex + + IF paths.Count = 0 AND cursor >= 0 AND cursor < FileOpsListCount(listing) THEN + path = FileOpsPathJoin$(curFolder, FileOpsListName$(listing, cursor)) + + IF path <> "" THEN + paths.Add path + END IF + END IF + END IF + + SET GetSelection = paths +END FUNCTION + + +' ============================================================ +' The folder tree +' ============================================================ + +' Add folder's sub-folders (sorted by name) under the tree item parentItem, +' which has none yet, so they follow it. Returns the item of the one at +' expandPath, -1 when none. +FUNCTION TreeAddChildren(BYVAL parentItem AS LONG, BYVAL folder AS STRING, BYVAL expandPath AS STRING) AS LONG + DIM subList AS LONG + DIM count AS LONG + DIM i AS LONG + DIM added AS LONG + DIM path AS STRING + DIM found AS LONG + + found = -1 + subList = FileOpsList(folder, "") + + IF subList = 0 THEN + TreeAddChildren = found + EXIT FUNCTION + END IF + + count = FileOpsListCount(subList) + + ' The folders come first + FOR i = 0 TO count - 1 + IF NOT FileOpsListIsFolder(subList, i) THEN + EXIT FOR + END IF + + path = FileOpsPathJoin$(folder, FileOpsListName$(subList, i)) + + IF path <> "" THEN + fmTree.AddChildItem parentItem, FileOpsListName$(subList, i) + added = added + 1 + fmTree.SetItemTag parentItem + added, path + + IF expandPath <> "" AND found < 0 THEN + IF FileOpsPathSame(path, expandPath) THEN + found = parentItem + added + END IF + END IF + END IF + NEXT i + + FileOpsListFree subList + TreeAddChildren = found +END FUNCTION + + +' The root, then each folder on the way to the current one with its +' sibling folders, then the current folder's own sub-folders. +SUB TreeRebuild + DIM chain(FM_TREE_DEPTH_MAX) AS STRING + DIM depth AS LONG + DIM parent AS STRING + DIM item AS LONG + DIM nextItem AS LONG + DIM level AS LONG + + syncingTree = True + fmTree.Clear + + ' Walk up from the current folder to its root + chain(0) = curFolder + depth = 1 + parent = FileOpsPathParent$(curFolder) + + DO WHILE parent <> "" AND depth < FM_TREE_DEPTH_MAX + chain(depth) = parent + depth = depth + 1 + parent = FileOpsPathParent$(parent) + LOOP + + fmTree.AddItem chain(depth - 1) + fmTree.SetItemTag 0, chain(depth - 1) + item = 0 + + FOR level = depth - 1 TO 1 STEP -1 + nextItem = TreeAddChildren(item, chain(level), chain(level - 1)) + fmTree.SetExpanded item, True + item = nextItem + + IF item < 0 THEN + EXIT FOR + END IF + NEXT level + + IF item >= 0 THEN + TreeAddChildren item, curFolder, "" + fmTree.SetExpanded item, True + fmTree.ListIndex = item + END IF + + syncingTree = False +END SUB + + +SUB fmTree_Change + DIM item AS LONG + DIM path AS STRING + + IF syncingTree THEN + EXIT SUB + END IF + + item = fmTree.ListIndex + + IF item >= 0 THEN + path = fmTree.GetItemTag(item) + + IF path <> "" AND NOT FileOpsPathSame(path, curFolder) THEN + Navigate path, True + END IF + END IF +END SUB + + +SUB fmTree_GotFocus + treeActive = True +END SUB + + +SUB fmTree_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER) + IF KeyCode = vbKeyDelete THEN + CmdDelete + ELSEIF KeyCode = vbKeyBack THEN + CmdUp + END IF +END SUB + + +' ============================================================ +' The file list and the path box +' ============================================================ + +' Header click: sort by that column; the same column again reverses. The +' list's own text sort is cleared (sizes and dates need real ordering). +SUB fmList_ColumnClick(Column AS INTEGER) + fmList.SetSort -1, 0 + + IF Column >= FILEOPS_SORT_NAME AND Column <= FILEOPS_SORT_DATE THEN + IF Column = sortCol THEN + CmdSort Column, NOT sortDesc + ELSE + CmdSort Column, False + END IF + END IF +END SUB + + +SUB fmList_Change + UpdateStatus +END SUB + + +' Enter and double-click open the row (folder, program or document) +SUB fmList_DblClick + CmdOpen +END SUB + + +SUB fmList_GotFocus + treeActive = False +END SUB + + +' Del, Backspace and Ctrl+A/C/V/X belong to the path box when it has the +' focus, so the list handles them here instead of as menu shortcuts. +SUB fmList_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER) + SELECT CASE KeyCode + CASE vbKeyDelete + CmdDelete + CASE vbKeyBack + CmdUp + CASE KEY_CTRL_A + CmdSelectAll + CASE KEY_CTRL_C + ClipboardPut False + CASE KEY_CTRL_X + ClipboardPut True + CASE KEY_CTRL_V + CmdPaste + END SELECT +END SUB + + +' Enter in the path box goes to the typed folder. +SUB fmPath_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER) + IF KeyCode = vbKeyReturn THEN + Navigate fmPath.Text, False + END IF +END SUB + + +' ============================================================ +' Job windows +' ============================================================ + +' A job's timer (Me is its window; the job engine is in fileMgr.bas): +' work on the job for a slice, ask when it needs an answer, finish when it +' is done. It is in this form's code so it runs with this form's +' variables: a finished job shows the folder again (FmRefresh). +SUB JobTick + DIM job AS FmJob + DIM stepResult AS LONG + + SET job = FindJob(Me.Name) + + IF job IS NOTHING THEN + EXIT SUB + END IF + + stepResult = FileOpsJobRunFor(job.Handle, FM_SLICE_MS) + + SELECT CASE stepResult + CASE FILEOPS_STEP_ASK + JobAsk job, FM_OVERWRITE_BUTTONS, CStr(FILEOPS_ANSWER_YES) & "|" & CStr(FILEOPS_ANSWER_NO) & "|" & CStr(FILEOPS_ANSWER_ALL) & "|" & CStr(FILEOPS_ANSWER_CANCEL) + CASE FILEOPS_STEP_ERROR + JobAsk job, FM_ERROR_BUTTONS, CStr(FILEOPS_ANSWER_RETRY) & "|" & CStr(FILEOPS_ANSWER_SKIP) & "|" & CStr(FILEOPS_ANSWER_CANCEL) + CASE FILEOPS_STEP_DONE, FILEOPS_STEP_CANCELLED + JobFinish job, stepResult + CASE ELSE + JobShowProgress job + END SELECT +END SUB + + +' ============================================================ +' Commands +' ============================================================ + +' Put the selection on the DVX clipboard as a file list. +SUB ClipboardPut(BYVAL cut AS BOOLEAN) + DIM paths AS Collection + DIM path AS VARIANT + DIM text AS STRING + + SET paths = GetSelection() + + IF paths.Count = 0 THEN + EXIT SUB + END IF + + IF cut THEN + text = FM_CLIP_CUT + ELSE + text = FM_CLIP_COPY + END IF + + FOR EACH path IN paths + text = text & FM_CLIP_EOL & path + NEXT path + + Clipboard.SetText text +END SUB + + +SUB CmdDelete + DIM paths AS Collection + DIM msg AS STRING + + SET paths = GetSelection() + + IF paths.Count = 0 THEN + EXIT SUB + END IF + + IF paths.Count = 1 THEN + msg = "Delete " & paths(1) & "?" + ELSE + msg = "Delete these " & CStr(paths.Count) & " items?" + END IF + + IF MsgBox(msg, vbYesNo + vbQuestion, "Confirm Delete") = vbYes THEN + JobStart FILEOPS_DELETE, paths, "" + END IF +END SUB + + +SUB CmdFilter + DIM pattern AS STRING + + pattern = DlgInputBox$("Filter", "Show only files matching:", curFilter) + + IF DlgInputCancelled() THEN + EXIT SUB + END IF + + IF pattern = "" THEN + curFilter = FM_FILTER_ALL + ELSE + curFilter = pattern + END IF + + FmRefresh +END SUB + + +SUB CmdInvert + DIM i AS LONG + + FOR i = 0 TO FileOpsListCount(listing) - 1 + fmList.SetItemSelected i, NOT fmList.IsItemSelected(i) + NEXT i + + UpdateStatus +END SUB + + +SUB CmdNewFolder + DIM folderName AS STRING + + folderName = DlgInputBox$("New Folder", "Name of the new folder:", FM_DEFAULT_FOLDER) + + IF DlgInputCancelled() THEN + EXIT SUB + END IF + + IF NOT FileOpsMakeDir(curFolder, folderName) THEN + ShowError "New Folder", FileOpsError$() + EXIT SUB + END IF + + ShellLog "FileMgr: created folder " & folderName + FmRefresh + SelectByName folderName +END SUB + + +' Enter / double-click / File > Open: a folder is opened in place, a +' program is started, a document opens in its associated program. +SUB CmdOpen + DIM row AS LONG + DIM entryName AS STRING + DIM path AS STRING + DIM ext AS STRING + DIM kind AS STRING + + IF treeActive THEN + EXIT SUB + END IF + + row = fmList.ListIndex + + IF row < 0 OR row >= FileOpsListCount(listing) THEN + EXIT SUB + END IF + + entryName = FileOpsListName$(listing, row) + path = FileOpsPathJoin$(curFolder, entryName) + + IF path = "" THEN + EXIT SUB + END IF + + IF FileOpsListIsFolder(listing, row) THEN + Navigate path, False + EXIT SUB + END IF + + ext = FileOpsExtension$(entryName) + + IF LCase$(ext) = "app" THEN + ShellRun path, "" + EXIT SUB + END IF + + IF ShellAssocProgram$(ext) = "" THEN + IF ext <> "" THEN + kind = ext + ELSE + kind = "these" + END IF + + ShowError FM_TITLE, "No program is set to open " & kind & " files." & CHR$(10) & "File types are set in the Control Panel." + EXIT SUB + END IF + + IF NOT ShellOpenDocument(path) THEN + ShowError FM_TITLE, entryName & " could not be opened." + END IF +END SUB + + +' Paste the clipboard's file list into the current folder. +SUB CmdPaste + DIM clip AS STRING + DIM cut AS BOOLEAN + DIM rest AS STRING + DIM lines AS VARIANT + DIM i AS LONG + DIM last AS LONG + DIM paths AS NEW Collection + + clip = Clipboard.GetText() + + IF Left$(clip, Len(FM_CLIP_CUT) + 1) = FM_CLIP_CUT & FM_CLIP_EOL THEN + cut = True + rest = Mid$(clip, Len(FM_CLIP_CUT) + 2) + ELSEIF Left$(clip, Len(FM_CLIP_COPY) + 1) = FM_CLIP_COPY & FM_CLIP_EOL THEN + rest = Mid$(clip, Len(FM_CLIP_COPY) + 2) + ELSE + ShowError "Paste", "The clipboard holds no files. Use Copy or Cut in a File Manager window first." + EXIT SUB + END IF + + ' One path per line (a line feed at the very end starts no path) + lines = Split(rest, FM_CLIP_EOL) + last = UBound(lines) + + IF Right$(rest, 1) = FM_CLIP_EOL THEN + last = last - 1 + END IF + + FOR i = 0 TO last + paths.Add lines(i) + NEXT i + + IF cut THEN + JobStart FILEOPS_MOVE, paths, curFolder + + ' A cut is used up by its paste + Clipboard.Clear + ELSE + JobStart FILEOPS_COPY, paths, curFolder + END IF +END SUB + + +' Size, dates and editable attributes of the selection. Attribute changes +' apply to every selected item. +SUB CmdProperties + DIM paths AS Collection + DIM path AS VARIANT + DIM first AS STRING + DIM bytes AS DOUBLE + DIM files AS LONG + DIM folders AS LONG + DIM allRead AS BOOLEAN + DIM attr AS LONG + DIM location AS STRING + DIM modified AS STRING + DIM failed AS BOOLEAN + DIM oldAttr AS LONG + + SET paths = GetSelection() + + IF paths.Count = 0 THEN + EXIT SUB + END IF + + allRead = True + + FOR EACH path IN paths + allRead = FileOpsTreeSize(path) AND allRead + bytes = bytes + FileOpsTreeBytes() + files = files + FileOpsTreeFiles() + folders = folders + FileOpsTreeFolders() + + IF FileOpsIsFolder(path) THEN + folders = folders + 1 + END IF + NEXT path + + first = paths(1) + attr = FileOpsGetAttr(first) + location = FileOpsPathParent$(first) + modified = FileOpsModified$(first) + + IF attr < 0 THEN + attr = 0 + END IF + + IF location = "" THEN + location = first + END IF + + Load PropsForm + + IF paths.Count = 1 THEN + PropsForm.lblName.Caption = "Name: " & FileOpsBaseName$(first) + ELSE + PropsForm.lblName.Caption = "Name: " & CStr(paths.Count) & " items" + END IF + + PropsForm.lblLocation.Caption = "Location: " & location + + IF allRead THEN + PropsForm.lblSize.Caption = "Size: " & FileOpsFormatSize$(bytes) & " bytes" + ELSE + PropsForm.lblSize.Caption = "Size: " & FileOpsFormatSize$(bytes) & " bytes (some items could not be read)" + END IF + + PropsForm.lblContains.Caption = "Contains: " & CStr(files) & " files, " & CStr(folders) & " folders" + + IF paths.Count = 1 AND modified <> "" THEN + PropsForm.lblModified.Caption = "Modified: " & modified + ELSE + PropsForm.lblModified.Visible = False + END IF + + PropsForm.chkReadOnly.Value = (attr AND FILEOPS_ATTR_READONLY) <> 0 + PropsForm.chkHidden.Value = (attr AND FILEOPS_ATTR_HIDDEN) <> 0 + PropsForm.chkSystem.Value = (attr AND FILEOPS_ATTR_SYSTEM) <> 0 + PropsForm.chkArchive.Value = (attr AND FILEOPS_ATTR_ARCHIVE) <> 0 + + ' Fit the dialog to what it now says and centre it, then wait for it + fmPropsAccepted = False + PropsForm.AutoSize = True + PropsForm.Centered = True + PropsForm.Show vbModal + + IF NOT fmPropsAccepted THEN + EXIT SUB + END IF + + FOR EACH path IN paths + oldAttr = FileOpsGetAttr(path) + + IF oldAttr < 0 THEN + failed = True + ELSEIF NOT FileOpsSetAttr(path, (oldAttr AND NOT FM_ATTR_EDITABLE) OR fmPropsAttr) THEN + failed = True + END IF + NEXT path + + IF failed THEN + ShowError "Properties", "The attributes of some items could not be changed." + END IF + + FmRefresh +END SUB + + +SUB CmdRename + DIM paths AS Collection + DIM oldName AS STRING + DIM newName AS STRING + DIM folder AS STRING + DIM newPath AS STRING + DIM renamedCurrent AS BOOLEAN + + SET paths = GetSelection() + + IF paths.Count <> 1 THEN + IF paths.Count > 1 THEN + ShowError "Rename", "Select one file or folder to rename." + END IF + + EXIT SUB + END IF + + oldName = FileOpsBaseName$(paths(1)) + folder = FileOpsPathParent$(paths(1)) + + IF folder = "" THEN + EXIT SUB + END IF + + newName = DlgInputBox$("Rename", "New name for " & oldName & ":", oldName) + + IF DlgInputCancelled() THEN + EXIT SUB + END IF + + renamedCurrent = FileOpsPathSame(paths(1), curFolder) + + IF NOT FileOpsRename(folder, oldName, newName) THEN + ShowError "Rename", FileOpsError$() + ELSEIF renamedCurrent THEN + ' The tree's current folder itself was renamed + newPath = FileOpsPathJoin$(folder, newName) + + IF newPath <> "" THEN + curFolder = newPath + END IF + END IF + + FmRefresh +END SUB + + +SUB CmdSelectAll + fmList.SelectAll + UpdateStatus +END SUB + + +' Sort the listing by col (FILEOPS_SORT_), folders first; the View menu +' follows. +SUB CmdSort(BYVAL col AS LONG, BYVAL descending AS BOOLEAN) + sortCol = col + sortDesc = descending + + mnuSortName.Checked = (col = FILEOPS_SORT_NAME) + mnuSortSize.Checked = (col = FILEOPS_SORT_SIZE) + mnuSortType.Checked = (col = FILEOPS_SORT_TYPE) + mnuSortDate.Checked = (col = FILEOPS_SORT_DATE) + mnuSortDesc.Checked = descending + + FileOpsListSort listing, col, descending + ShowListing +END SUB + + +' Copy To / Move To (FILEOPS_COPY / FILEOPS_MOVE): ask for the +' destination folder. +SUB CmdTransferTo(BYVAL op AS LONG) + DIM paths AS Collection + DIM verb AS STRING + DIM prompt AS STRING + DIM dest AS STRING + DIM norm AS STRING + + SET paths = GetSelection() + + IF paths.Count = 0 THEN + EXIT SUB + END IF + + IF op = FILEOPS_MOVE THEN + verb = "Move" + ELSE + verb = "Copy" + END IF + + IF paths.Count = 1 THEN + prompt = verb & " " & FileOpsBaseName$(paths(1)) & " to folder:" + ELSE + prompt = verb & " " & CStr(paths.Count) & " items to folder:" + END IF + + dest = DlgInputBox$(verb, prompt, curFolder) + + IF DlgInputCancelled() THEN + EXIT SUB + END IF + + norm = FileOpsPathNormalize$(dest) + + IF norm <> "" THEN + JobStart op, paths, norm + ELSE + ShowError verb, dest & " is not an existing folder, or its drive is not ready." + END IF +END SUB + + +SUB CmdUp + DIM parent AS STRING + + parent = FileOpsPathParent$(curFolder) + + IF parent <> "" THEN + Navigate parent, False + END IF +END SUB + + +' ============================================================ +' Menus +' ============================================================ + +SUB mnuOpen_Click + CmdOpen +END SUB + + +SUB mnuNewFolder_Click + CmdNewFolder +END SUB + + +SUB mnuCopyTo_Click + CmdTransferTo FILEOPS_COPY +END SUB + + +SUB mnuMoveTo_Click + CmdTransferTo FILEOPS_MOVE +END SUB + + +SUB mnuRename_Click + CmdRename +END SUB + + +SUB mnuDelete_Click + CmdDelete +END SUB + + +SUB mnuProperties_Click + CmdProperties +END SUB + + +' Another File Manager on this folder +SUB mnuNewWindow_Click + ShellRun ShellAppPath$(), curFolder +END SUB + + +SUB mnuExit_Click + Unload FileMgrForm +END SUB + + +SUB mnuCut_Click + ClipboardPut True +END SUB + + +SUB mnuCopy_Click + ClipboardPut False +END SUB + + +SUB mnuPaste_Click + CmdPaste +END SUB + + +SUB mnuSelectAll_Click + CmdSelectAll +END SUB + + +SUB mnuInvert_Click + CmdInvert +END SUB + + +SUB mnuRefresh_Click + FmRefresh +END SUB + + +SUB mnuFilter_Click + CmdFilter +END SUB + + +SUB mnuUp_Click + CmdUp +END SUB + + +SUB mnuSortName_Click + CmdSort FILEOPS_SORT_NAME, sortDesc +END SUB + + +SUB mnuSortSize_Click + CmdSort FILEOPS_SORT_SIZE, sortDesc +END SUB + + +SUB mnuSortType_Click + CmdSort FILEOPS_SORT_TYPE, sortDesc +END SUB + + +SUB mnuSortDate_Click + CmdSort FILEOPS_SORT_DATE, sortDesc +END SUB + + +SUB mnuSortDesc_Click + CmdSort sortCol, NOT sortDesc +END SUB + + +SUB mnuHelpContents_Click + HelpView HELP_FILE +END SUB + + +SUB mnuAbout_Click + DIM msg AS STRING + + msg = "File Manager" & CHR$(10) & CHR$(10) + msg = msg & "Browse, copy, move, rename and delete files and folders." & CHR$(10) & CHR$(10) + msg = msg & "Copyright 2026 Scott Duensing" + MsgBox msg, vbOKOnly + vbInformation, "About File Manager" +END SUB diff --git a/src/apps/kpunch/fileMgr/fileMgr.res b/src/apps/kpunch/fileMgr/fileMgr.res deleted file mode 100644 index 6799148a..00000000 --- a/src/apps/kpunch/fileMgr/fileMgr.res +++ /dev/null @@ -1,7 +0,0 @@ -# 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" diff --git a/src/apps/kpunch/fileMgr/fmJob.cls b/src/apps/kpunch/fileMgr/fmJob.cls new file mode 100644 index 00000000..84c7f118 --- /dev/null +++ b/src/apps/kpunch/fileMgr/fmJob.cls @@ -0,0 +1,39 @@ +VERSION DVX 1.00 CLASS +Attribute VB_Name = "FmJob" + + +' 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. + +' fmJob.cls -- one running copy, move or delete of the File Manager: the +' file operations job and the window that shows its progress. + +' The job (fileops.bas handle) +Public Handle As Long + +' "Copying", "Moving" or "Deleting": the window's caption and the start +' of its progress line +Public Verb As String + +' The job's progress window (a form made with CreateForm) and its name +Public Window As Form +Public WindowName As String diff --git a/src/apps/kpunch/fileMgr/fmProps.frm b/src/apps/kpunch/fileMgr/fmProps.frm new file mode 100644 index 00000000..23153ad7 --- /dev/null +++ b/src/apps/kpunch/fileMgr/fmProps.frm @@ -0,0 +1,116 @@ +VERSION DVX 1.00 +Begin Form PropsForm + Caption = "Properties" + Layout = VBox + AutoSize = True + Resizable = False + Centered = True + Begin Label lblName + Caption = "" + End + Begin Label lblLocation + Caption = "" + End + Begin Label lblSize + Caption = "" + End + Begin Label lblContains + Caption = "" + End + Begin Label lblModified + Caption = "" + End + Begin Frame fraAttributes + Caption = "Attributes" + Begin CheckBox chkReadOnly + Caption = "&Read-only" + End + Begin CheckBox chkHidden + Caption = "&Hidden" + End + Begin CheckBox chkSystem + Caption = "&System" + End + Begin CheckBox chkArchive + Caption = "&Archive" + End + End + Begin HBox boxButtons + Alignment = Right + Begin CommandButton cmdOk + Caption = "&OK" + Default = True + MinWidth = 80 + MinHeight = 24 + End + Begin CommandButton cmdCancel + Caption = "&Cancel" + Cancel = True + MinWidth = 80 + MinHeight = 24 + End + End +End + + +' 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. + + +' fmProps.frm -- the File Manager's Properties dialog: what the selection +' is (the File Manager fills in the labels and ticks the attributes before +' showing it with Show vbModal) and its Read-only, Hidden, System and +' Archive attributes. OK leaves the ticked attributes in fmPropsAttr and +' sets fmPropsAccepted (fileMgr.bas); Cancel, Escape and the close box +' leave fmPropsAccepted False. Its buttons are the size of the buttons of +' DVX's own dialog boxes. + +OPTION EXPLICIT + + +SUB cmdOk_Click + fmPropsAttr = 0 + + IF chkReadOnly.Value THEN + fmPropsAttr = fmPropsAttr OR FILEOPS_ATTR_READONLY + END IF + + IF chkHidden.Value THEN + fmPropsAttr = fmPropsAttr OR FILEOPS_ATTR_HIDDEN + END IF + + IF chkSystem.Value THEN + fmPropsAttr = fmPropsAttr OR FILEOPS_ATTR_SYSTEM + END IF + + IF chkArchive.Value THEN + fmPropsAttr = fmPropsAttr OR FILEOPS_ATTR_ARCHIVE + END IF + + fmPropsAccepted = True + Unload PropsForm +END SUB + + +SUB cmdCancel_Click + Unload PropsForm +END SUB diff --git a/src/apps/kpunch/progman/pmProps.c b/src/apps/kpunch/progman/pmProps.c index 13eaaca4..8a0c28cc 100644 --- a/src/apps/kpunch/progman/pmProps.c +++ b/src/apps/kpunch/progman/pmProps.c @@ -190,7 +190,7 @@ bool pmPropsDialog(AppContextT *ctx, const PmModelT *model, int32_t group, int32 pd.group = group; pd.index = index; - pmHotkeyFormat(item->hotkeyKey, item->hotkeyMods, hotkey, sizeof(hotkey)); + dvxAccelFormat(item->hotkeyKey, item->hotkeyMods, hotkey, sizeof(hotkey)); pd.name = dvxDialogAddField(dlg, "&Name:", PP_LABEL_W, PM_NAME_MAX - 1, item->name); pd.program = dvxDialogAddField(dlg, "&Program:", PP_LABEL_W, DVX_MAX_PATH - 1, item->program); @@ -227,13 +227,13 @@ bool pmPropsDialog(AppContextT *ctx, const PmModelT *model, int32_t group, int32 } -// The icon the item will show: the chosen picture, else the program's. -// A blank square when there is none. +// The icon the item will show (shellItemIcon); a blank square when there +// is none. static void showPreview(PropsDlgT *pd) { const char *iconPath = dvxSkipWs(wgtGetText(pd->icon)); - const char *source = iconPath[0] ? iconPath : dvxSkipWs(wgtGetText(pd->program)); + const char *program = dvxSkipWs(wgtGetText(pd->program)); int32_t pitch = 0; - uint8_t *pixels = pd->preview ? shellProgramIcon(pd->ctx, source, SHELL_PROG_ICON, &pitch) : NULL; + uint8_t *pixels = pd->preview ? shellItemIcon(pd->ctx, iconPath, program, SHELL_PROG_ICON, &pitch) : NULL; if (!pd->preview) { return; diff --git a/src/apps/kpunch/progman/pmView.c b/src/apps/kpunch/progman/pmView.c index 25b7f9dc..ca30c98e 100644 --- a/src/apps/kpunch/progman/pmView.c +++ b/src/apps/kpunch/progman/pmView.c @@ -26,8 +26,12 @@ // rows with its own vertical scrollbar, and routes Enter/Del/F2, double // click and drags back to Program Manager through PmViewCallbacksT. The // window's widget tree keeps it focusable with Tab like any other control. +// Its geometry and rules (cells, grid, label lines, drag, drop target, +// type-ahead) are the list helper library's icon view rules, which BASIC +// icon views use too (listhelp.bas). #include "pmView.h" +#include "listhelp/listHelp.h" #include "dvxApp.h" #include "dvxDraw.h" #include "dvxVideo.h" @@ -42,33 +46,15 @@ // Geometry // ============================================================ -#define PV_BORDER 2 -#define PV_PAD 2 -#define PV_ICON 32 // large icon edge -#define PV_ICON_SMALL 16 // small icon edge -#define PV_LABEL_GAP 2 // icon to label (large icons) -#define PV_LABEL_LINES 2 // label lines under a large icon -#define PV_TEXT_GAP 4 // icon to text (small icons, list) -#define PV_LARGE_CELL_W 80 -#define PV_SMALL_CHARS 14 // name width in a small-icon cell -#define PV_LIST_NAME_CHARS 24 // name column in the list -#define PV_DRAG_SLOP 4 // pixels before a press becomes a drag -#define PV_NO_CELL (-1) -#define PV_WHEEL_ROWS 1 // rows scrolled per wheel notch -#define PV_BUTTON_RIGHT 2 // onMouseDown button number of the right button +#define PV_BUTTON_RIGHT 2 // onMouseDown button number of the right button +#define PV_CELL_PARTS 2 // a cell's column and row (navigate's buffer) -typedef struct { - int32_t cellW; - int32_t cellH; - int32_t innerX; - int32_t innerY; - int32_t innerW; - int32_t innerH; - int32_t cols; - int32_t rowsVisible; - int32_t totalRows; - bool needSb; -} ViewGeomT; +// Program Manager's views as the icon view rules name them +static const ListIconViewE sIconView[PmViewCountE] = { + [PmViewLargeE] = ListIconLargeE, + [PmViewSmallE] = ListIconSmallE, + [PmViewListE] = ListIconListE +}; typedef struct { PmModelT *model; @@ -97,14 +83,15 @@ static int32_t sTypeId = -1; // Prototypes // ============================================================ -static void cellSize(PmViewE view, const BitmapFontT *font, int32_t innerW, int32_t *cellW, int32_t *cellH); -static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, const ViewGeomT *g, int32_t e); +static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, const ListIconGridT *g, int32_t e); static void drawPlaceholder(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, const char *name); -static void ensureVisible(WidgetT *w, const ViewGeomT *g, int32_t e); +static void ensureVisible(WidgetT *w, const ListIconGridT *g, int32_t e); static void entryCell(const PmViewDataT *d, int32_t e, int32_t cols, int32_t *col, int32_t *row); static PmItemT *entryItem(const PmViewDataT *d, int32_t e); -static int32_t hitEntry(const WidgetT *w, const ViewGeomT *g, int32_t vx, int32_t vy, int32_t *outCol, int32_t *outRow); -static int32_t navigate(const PmViewDataT *d, const ViewGeomT *g, int32_t key); +static int32_t hitEntry(const WidgetT *w, const ListIconGridT *g, int32_t vx, int32_t vy, int32_t *outCol, int32_t *outRow); +static void manualCell(const PmItemT *item, int32_t *col, int32_t *row); +static int32_t manualRows(const PmViewDataT *d); +static int32_t navigate(const PmViewDataT *d, const ListIconGridT *g, int32_t key); int32_t pmViewColumns(const WidgetT *w); WidgetT *pmViewCreate(WidgetT *parent, PmModelT *model, const PmViewCallbacksT *cb); void pmViewRegister(void); @@ -112,22 +99,21 @@ void pmViewSelect(WidgetT *w, int32_t entry); int32_t pmViewSelected(const WidgetT *w); void pmViewSetEntries(WidgetT *w, const PmViewEntryT *entries, int32_t count, bool manual, bool canDrag, const char *emptyText); void pmViewUnregister(void); -static void selectEntry(WidgetT *w, const ViewGeomT *g, int32_t e); -static void splitLabel(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2); +static void selectEntry(WidgetT *w, const ListIconGridT *g, int32_t e); static int32_t typeAhead(const PmViewDataT *d, char ch); static void viewCalcMinSize(WidgetT *w, const BitmapFontT *font); static void viewDblClick(WidgetT *w); static void viewDestroy(WidgetT *w); static void viewDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void viewDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); -static void viewGeom(const WidgetT *w, const BitmapFontT *font, ViewGeomT *g); +static void viewGeom(const WidgetT *w, const BitmapFontT *font, ListIconGridT *g); static const char *viewGetText(const WidgetT *w); static void viewMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y); static void viewOnKey(WidgetT *w, int32_t key, int32_t mod); static void viewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); static bool viewOnWheel(WidgetT *w, int32_t delta); static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -static void viewSync(WidgetT *w, const BitmapFontT *font, ViewGeomT *g); +static void viewSync(WidgetT *w, const BitmapFontT *font, ListIconGridT *g); static const WidgetClassT sClassView = { .version = WGT_CLASS_VERSION, @@ -146,44 +132,24 @@ static const WidgetClassT sClassView = { }; -// Cell size for a view; a list row spans the whole inner width. -static void cellSize(PmViewE view, const BitmapFontT *font, int32_t innerW, int32_t *cellW, int32_t *cellH) { - int32_t rowH = (font->charHeight > PV_ICON_SMALL ? font->charHeight : PV_ICON_SMALL) + PV_PAD * 2; - - switch (view) { - case PmViewSmallE: - *cellW = PV_PAD + PV_ICON_SMALL + PV_TEXT_GAP + PV_SMALL_CHARS * font->charWidth + PV_PAD; - *cellH = rowH; - break; - - case PmViewListE: - *cellW = innerW > 0 ? innerW : 1; - *cellH = rowH; - break; - - default: - *cellW = PV_LARGE_CELL_W; - *cellH = PV_PAD + PV_ICON + PV_LABEL_GAP + PV_LABEL_LINES * font->charHeight + PV_PAD; - break; - } -} - - // One entry: icon, then its name (two centered lines under a large icon, // one line beside a small one, plus the description in a list). The // selected name is highlighted; the keyboard focus box goes around it. -static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, const ViewGeomT *g, int32_t e) { - PmViewDataT *vd = (PmViewDataT *)w->data; - PmItemT *item = entryItem(vd, e); - int32_t col; - int32_t row; +static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, const ListIconGridT *g, int32_t e) { + PmViewDataT *vd = (PmViewDataT *)w->data; + PmItemT *item = entryItem(vd, e); + const ListIconCellT *cell = &g->cell; + int32_t col; + int32_t row; entryCell(vd, e, g->cols, &col, &row); - int32_t x = g->innerX + col * g->cellW; - int32_t y = g->innerY + (row - vd->scrollRow) * g->cellH; + int32_t x = w->x + g->innerX + col * cell->cellW; + int32_t y = w->y + g->innerY + (row - vd->scrollRow) * cell->cellH; + int32_t textY = y + cell->textY; bool selected = (e == vd->selected); bool focused = selected && (w == sFocusedWidget); + bool large = (vd->model->view == PmViewLargeE); uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg; uint32_t bg = selected ? colors->menuHighlightBg : colors->contentBg; @@ -191,36 +157,34 @@ static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapF vd->cb->loadIcon(item); } - if (vd->model->view == PmViewLargeE) { - int32_t maxChars = g->cellW / font->charWidth; - int32_t iconX = x + (g->cellW - PV_ICON) / 2; - int32_t textY = y + PV_PAD + PV_ICON + PV_LABEL_GAP; + const uint8_t *picture = large ? item->icon : item->iconSmall; + int32_t pitch = large ? item->iconPitch : item->iconSmallPitch; + + if (picture) { + rectCopy(d, ops, x + cell->pictureX, y + cell->pictureY, picture, pitch, 0, 0, cell->pictureSize, cell->pictureSize); + } else { + drawPlaceholder(d, ops, font, colors, x + cell->pictureX, y + cell->pictureY, cell->pictureSize, item->name); + } + + if (large) { int32_t len1; int32_t len2; - int32_t lines; - int32_t widest; const char *line2; - if (item->icon) { - rectCopy(d, ops, iconX, y + PV_PAD, item->icon, item->iconPitch, 0, 0, PV_ICON, PV_ICON); - } else { - drawPlaceholder(d, ops, font, colors, iconX, y + PV_PAD, PV_ICON, item->name); - } + listIconLabelSplit(item->name, cell->nameChars, &len1, &line2, &len2); - splitLabel(item->name, maxChars, &len1, &line2, &len2); - lines = len2 > 0 ? PV_LABEL_LINES : 1; - widest = (len1 > len2 ? len1 : len2) * font->charWidth; - - int32_t boxX = x + (g->cellW - widest) / 2; + int32_t lines = len2 > 0 ? LIST_ICON_LABEL_LINES : 1; + int32_t widest = DVX_MAX(len1, len2) * font->charWidth; + int32_t boxX = x + (cell->cellW - widest) / 2; if (selected) { rectFill(d, ops, boxX, textY, widest, lines * font->charHeight, bg); } - drawTextN(d, ops, font, x + (g->cellW - len1 * font->charWidth) / 2, textY, item->name, len1, fg, bg, selected); + drawTextN(d, ops, font, x + (cell->cellW - len1 * font->charWidth) / 2, textY, item->name, len1, fg, bg, selected); if (len2 > 0) { - drawTextN(d, ops, font, x + (g->cellW - len2 * font->charWidth) / 2, textY + font->charHeight, line2, len2, fg, bg, selected); + drawTextN(d, ops, font, x + (cell->cellW - len2 * font->charWidth) / 2, textY + font->charHeight, line2, len2, fg, bg, selected); } if (focused) { @@ -230,33 +194,23 @@ static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapF return; } - int32_t iconY = y + (g->cellH - PV_ICON_SMALL) / 2; - int32_t textX = x + PV_PAD + PV_ICON_SMALL + PV_TEXT_GAP; - int32_t textY = y + (g->cellH - font->charHeight) / 2; - int32_t nameMax = vd->model->view == PmViewListE ? PV_LIST_NAME_CHARS : PV_SMALL_CHARS; - int32_t nameLen = (int32_t)strnlen(item->name, (size_t)nameMax); - int32_t nameW = nameLen * font->charWidth; - - if (item->iconSmall) { - rectCopy(d, ops, x + PV_PAD, iconY, item->iconSmall, item->iconSmallPitch, 0, 0, PV_ICON_SMALL, PV_ICON_SMALL); - } else { - drawPlaceholder(d, ops, font, colors, x + PV_PAD, iconY, PV_ICON_SMALL, item->name); - } + int32_t textX = x + cell->textX; + int32_t nameLen = (int32_t)strnlen(item->name, (size_t)cell->nameChars); drawTextN(d, ops, font, textX, textY, item->name, nameLen, fg, bg, selected); - if (vd->model->view == PmViewListE) { - int32_t descX = textX + (PV_LIST_NAME_CHARS + 1) * font->charWidth; - int32_t room = (x + g->cellW - descX) / font->charWidth; - const char *desc = item->description[0] ? item->description : item->program; + // The list's detail column: the description, else the program + if (cell->detailX > 0) { + int32_t room = (cell->cellW - cell->detailX) / font->charWidth; + const char *desc = item->description[0] ? item->description : item->program; if (room > 0) { - drawTextN(d, ops, font, descX, textY, desc, (int32_t)strnlen(desc, (size_t)room), colors->contentFg, colors->contentBg, false); + drawTextN(d, ops, font, x + cell->detailX, textY, desc, (int32_t)strnlen(desc, (size_t)room), colors->contentFg, colors->contentBg, false); } } if (focused) { - drawFocusRect(d, ops, textX, textY, nameW, font->charHeight, colors->contentFg); + drawFocusRect(d, ops, textX, textY, nameLen * font->charWidth, font->charHeight, colors->contentFg); } } @@ -264,7 +218,7 @@ static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapF // Stand-in for an item without an icon: a raised square with the first // letter of its name. static void drawPlaceholder(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, const char *name) { - BevelStyleT bevel = BEVEL_RAISED(colors, PV_BORDER); + BevelStyleT bevel = BEVEL_RAISED(colors, LIST_ICON_FRAME); int32_t tx; int32_t ty; char letter[2]; @@ -277,7 +231,7 @@ static void drawPlaceholder(DisplayT *d, const BlitOpsT *ops, const BitmapFontT } -static void ensureVisible(WidgetT *w, const ViewGeomT *g, int32_t e) { +static void ensureVisible(WidgetT *w, const ListIconGridT *g, int32_t e) { PmViewDataT *d = (PmViewDataT *)w->data; int32_t col; int32_t row; @@ -301,10 +255,7 @@ static void entryCell(const PmViewDataT *d, int32_t e, int32_t cols, int32_t *co *col = 0; *row = e; } else if (d->manual) { - const PmItemT *item = entryItem(d, e); - - *col = item->col > 0 ? item->col : 0; - *row = item->row > 0 ? item->row : 0; + manualCell(entryItem(d, e), col, row); } else { *col = e % cols; *row = e / cols; @@ -318,37 +269,22 @@ static PmItemT *entryItem(const PmViewDataT *d, int32_t e) { // Entry under a content point (or -1); the cell is returned either way -// (PV_NO_CELL outside the grid). -static int32_t hitEntry(const WidgetT *w, const ViewGeomT *g, int32_t vx, int32_t vy, int32_t *outCol, int32_t *outRow) { +// (LIST_ICON_NO_CELL outside the grid). +static int32_t hitEntry(const WidgetT *w, const ListIconGridT *g, int32_t vx, int32_t vy, int32_t *outCol, int32_t *outRow) { const PmViewDataT *d = (const PmViewDataT *)w->data; - int32_t relX = vx - g->innerX; - int32_t relY = vy - g->innerY; int32_t count = (int32_t)arrlen(d->entries); - *outCol = PV_NO_CELL; - *outRow = PV_NO_CELL; - - if (relX < 0 || relY < 0 || relX >= g->innerW || relY >= g->innerH) { + if (listIconCellAt(g, vx - w->x, vy - w->y, d->scrollRow, outCol, outRow) == LIST_ICON_NO_CELL) { return -1; } - int32_t col = relX / g->cellW; - int32_t row = relY / g->cellH + d->scrollRow; - - if (col >= g->cols) { - return -1; - } - - *outCol = col; - *outRow = row; - for (int32_t e = 0; e < count; e++) { int32_t ec; int32_t er; entryCell(d, e, g->cols, &ec, &er); - if (ec == col && er == row) { + if (ec == *outCol && er == *outRow) { return e; } } @@ -357,81 +293,51 @@ static int32_t hitEntry(const WidgetT *w, const ViewGeomT *g, int32_t vx, int32_ } -// Arrow keys move through the grid by cell: Left/Right to the nearest -// entry in reading order, Up/Down to the entry in the nearest row above or -// below with the closest column. Home/End go to the first/last entry and -// PgUp/PgDn a screen of rows. Returns the new entry or -1. -static int32_t navigate(const PmViewDataT *d, const ViewGeomT *g, int32_t key) { - int32_t count = (int32_t)arrlen(d->entries); - int32_t cur = d->selected; - int32_t best = -1; - int32_t bestScore = 0; - int32_t curCol; - int32_t curRow; +// An item's own cell (manual layout; a negative col / row counts as 0). +static void manualCell(const PmItemT *item, int32_t *col, int32_t *row) { + *col = DVX_MAX(item->col, 0); + *row = DVX_MAX(item->row, 0); +} - if (count == 0) { - return -1; - } - // With nothing selected, any movement starts at the first entry. - if (cur < 0) { - cur = 0; - key = (key == KEY_LEFT || key == KEY_RIGHT || key == KEY_UP || key == KEY_DOWN || key == KEY_PGUP || key == KEY_PGDN) ? KEY_HOME : key; - } - - entryCell(d, cur, g->cols, &curCol, &curRow); - - int32_t curOrder = curRow * g->cols + curCol; - int32_t rowStep = (key == KEY_PGUP || key == KEY_PGDN) ? g->rowsVisible : 1; +// Rows the entries take in manual layout, whatever the columns. +static int32_t manualRows(const PmViewDataT *d) { + int32_t count = (int32_t)arrlen(d->entries); + int32_t rows = 0; for (int32_t e = 0; e < count; e++) { int32_t col; int32_t row; - int32_t score = -1; - entryCell(d, e, g->cols, &col, &row); - - int32_t order = row * g->cols + col; - int32_t dCol = abs(col - curCol); - - switch (key) { - case KEY_LEFT: - score = order < curOrder ? curOrder - order : -1; - break; - - case KEY_RIGHT: - score = order > curOrder ? order - curOrder : -1; - break; - - case KEY_UP: - case KEY_PGUP: - score = row < curRow ? abs(curRow - rowStep - row) * g->cols * g->cols + dCol : -1; - break; - - case KEY_DOWN: - case KEY_PGDN: - score = row > curRow ? abs(row - curRow - rowStep) * g->cols * g->cols + dCol : -1; - break; - - case KEY_HOME: - score = order; - break; - - case KEY_END: - score = INT32_MAX - order; - break; - - default: - return -1; - } - - if (score >= 0 && (best < 0 || score < bestScore)) { - best = e; - bestScore = score; - } + manualCell(entryItem(d, e), &col, &row); + rows = DVX_MAX(rows, row + 1); } - return best; + return rows; +} + + +// The entry an arrow key, Home, End, PgUp or PgDn moves to: the icon +// view rule over the entries' cells (listIconNavigate), or -1. +static int32_t navigate(const PmViewDataT *d, const ListIconGridT *g, int32_t key) { + int32_t count = (int32_t)arrlen(d->entries); + int32_t *cols = count > 0 ? (int32_t *)malloc((size_t)count * PV_CELL_PARTS * sizeof(*cols)) : NULL; + + // No entries (or no memory): nowhere to go + if (!cols) { + return -1; + } + + int32_t *rows = cols + count; + + for (int32_t e = 0; e < count; e++) { + entryCell(d, e, g->cols, &cols[e], &rows[e]); + } + + int32_t next = listIconNavigate(key, d->selected, cols, rows, count, g->cols, g->rowsVisible); + + free(cols); + return next; } @@ -460,8 +366,8 @@ WidgetT *pmViewCreate(WidgetT *parent, PmModelT *model, const PmViewCallbacksT * d->cb = cb; d->selected = -1; d->dragEntry = -1; - d->dropCol = PV_NO_CELL; - d->dropRow = PV_NO_CELL; + d->dropCol = LIST_ICON_NO_CELL; + d->dropRow = LIST_ICON_NO_CELL; w->data = d; w->weight = WGT_WEIGHT_FILL; w->onDblClick = viewDblClick; @@ -476,9 +382,9 @@ void pmViewRegister(void) { void pmViewSelect(WidgetT *w, int32_t entry) { - PmViewDataT *d = (PmViewDataT *)w->data; - AppContextT *ctx = wgtGetContext(w); - ViewGeomT g; + PmViewDataT *d = (PmViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + ListIconGridT g; viewGeom(w, &ctx->font, &g); selectEntry(w, &g, (entry >= 0 && entry < arrlen(d->entries)) ? entry : -1); @@ -520,7 +426,7 @@ void pmViewUnregister(void) { } -static void selectEntry(WidgetT *w, const ViewGeomT *g, int32_t e) { +static void selectEntry(WidgetT *w, const ListIconGridT *g, int32_t e) { PmViewDataT *d = (PmViewDataT *)w->data; if (e == d->selected) { @@ -537,54 +443,34 @@ static void selectEntry(WidgetT *w, const ViewGeomT *g, int32_t e) { } -// Two label lines for a large icon: break at the last blank that fits, -// else at maxChars; the second line is cut to maxChars. -static void splitLabel(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2) { - int32_t len = (int32_t)strlen(name); - - *line2 = name + len; - *len2 = 0; - - if (len <= maxChars) { - *len1 = len; - return; - } - - int32_t brk = maxChars; - - while (brk > 0 && name[brk] != ' ') { - brk--; - } - - *len1 = brk > 0 ? brk : maxChars; - *line2 = dvxSkipWs(name + *len1); - *len2 = (int32_t)strnlen(*line2, (size_t)maxChars); -} - - -// Next entry after the selection whose name starts with ch (any case). +// Next entry after the selection whose name starts with ch (any case): +// the list rule over the entries' names. static int32_t typeAhead(const PmViewDataT *d, char ch) { - int32_t count = (int32_t)arrlen(d->entries); + int32_t count = (int32_t)arrlen(d->entries); + const char **names = count > 0 ? (const char **)malloc((size_t)count * sizeof(*names)) : NULL; - for (int32_t step = 1; step <= count; step++) { - int32_t e = (d->selected + step + count) % count; - - if (strncasecmp(entryItem(d, e)->name, &ch, 1) == 0) { - return e; - } + // No entries (or no memory): nothing to find + if (!names) { + return -1; } - return -1; + for (int32_t e = 0; e < count; e++) { + names[e] = entryItem(d, e)->name; + } + + int32_t found = widgetTypeAheadSearch(ch, names, count, d->selected); + + free(names); + return found; } static void viewCalcMinSize(WidgetT *w, const BitmapFontT *font) { - int32_t cellW; - int32_t cellH; + ListIconCellT cell; - cellSize(PmViewLargeE, font, 0, &cellW, &cellH); - w->calcMinW = cellW + PV_BORDER * 2 + WGT_SB_W; - w->calcMinH = cellH + PV_BORDER * 2; + listIconCellLayout(ListIconLargeE, font->charWidth, font->charHeight, 0, &cell); + w->calcMinW = cell.cellW + LIST_ICON_FRAME * 2 + LIST_ICON_SB_W; + w->calcMinH = cell.cellH + LIST_ICON_FRAME * 2; } @@ -613,15 +499,15 @@ static void viewDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { (void)y; PmViewDataT *d = (PmViewDataT *)w->data; int32_t entry = d->dragEntry; - bool dropped = d->dragging && d->dropCol != PV_NO_CELL; + bool dropped = d->dragging && d->dropCol != LIST_ICON_NO_CELL; int32_t col = d->dropCol; int32_t row = d->dropRow; d->dragEntry = -1; d->dragging = false; d->sbDragging = false; - d->dropCol = PV_NO_CELL; - d->dropRow = PV_NO_CELL; + d->dropCol = LIST_ICON_NO_CELL; + d->dropRow = LIST_ICON_NO_CELL; if (dropped && d->cb->onDrop) { d->cb->onDrop(entry, col, row); @@ -629,23 +515,18 @@ static void viewDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { } -// Thumb drag, or an entry drag: once the pointer moves PV_DRAG_SLOP it -// tracks the cell under the pointer, scrolling at the top and bottom edges. +// Thumb drag, or an entry drag: once the pointer moves far enough +// (listIconDragStarted) it tracks the cell under the pointer, scrolling +// when it is above or below the grid. static void viewDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { - PmViewDataT *d = (PmViewDataT *)w->data; - AppContextT *ctx = (AppContextT *)root->userData; - ViewGeomT g; + PmViewDataT *d = (PmViewDataT *)w->data; + AppContextT *ctx = (AppContextT *)root->userData; + ListIconGridT g; viewGeom(w, &ctx->font, &g); if (d->sbDragging) { - int32_t maxScroll = g.totalRows - g.rowsVisible; - int32_t relMouse = y - g.innerY - WGT_SB_W - d->sbDragOff; - - if (maxScroll > 0) { - d->scrollRow = widgetScrollbarThumbDragScroll(g.innerH - WGT_SB_W * 2, g.totalRows, g.rowsVisible, relMouse, maxScroll); - } - + d->scrollRow = listIconScrollDrag(&g, y - w->y, d->sbDragOff); return; } @@ -653,61 +534,23 @@ static void viewDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { return; } - if (!d->dragging && abs(x - d->pressX) < PV_DRAG_SLOP && abs(y - d->pressY) < PV_DRAG_SLOP) { + if (!d->dragging && !listIconDragStarted(x - d->pressX, y - d->pressY)) { return; } - d->dragging = true; - - if (y < g.innerY && d->scrollRow > 0) { - d->scrollRow--; - } else if (y >= g.innerY + g.innerH && d->scrollRow < g.totalRows - g.rowsVisible) { - d->scrollRow++; - } - - int32_t clampedY = clampInt(y, g.innerY, g.innerY + g.innerH - 1); - - hitEntry(w, &g, x, clampedY, &d->dropCol, &d->dropRow); + d->dragging = true; + d->scrollRow = listIconDragScroll(&g, y - w->y, d->scrollRow); + listIconDropCell(&g, x - w->x, y - w->y, d->scrollRow, &d->dropCol, &d->dropRow); } -static void viewGeom(const WidgetT *w, const BitmapFontT *font, ViewGeomT *g) { - const PmViewDataT *d = (const PmViewDataT *)w->data; - PmViewE view = d->model->view; - int32_t count = (int32_t)arrlen(d->entries); - int32_t fullW = w->w - PV_BORDER * 2; +// The grid at the view's size; items placed by hand take their own rows. +static void viewGeom(const WidgetT *w, const BitmapFontT *font, ListIconGridT *g) { + const PmViewDataT *d = (const PmViewDataT *)w->data; + bool byHand = d->manual && d->model->view != PmViewListE; + int32_t rowsTaken = byHand ? manualRows(d) : LIST_ICON_BY_ORDER; - g->innerX = w->x + PV_BORDER; - g->innerY = w->y + PV_BORDER; - g->innerH = w->h - PV_BORDER * 2; - - // Lay out without a scrollbar first; add one (narrowing the grid) only - // when the rows do not fit. - for (int32_t pass = 0; pass < 2; pass++) { - g->needSb = (pass == 1); - g->innerW = fullW - (g->needSb ? WGT_SB_W : 0); - cellSize(view, font, g->innerW, &g->cellW, &g->cellH); - g->cols = view == PmViewListE ? 1 : g->innerW / g->cellW; - g->cols = g->cols < 1 ? 1 : g->cols; - g->rowsVisible = g->innerH / g->cellH; - g->rowsVisible = g->rowsVisible < 1 ? 1 : g->rowsVisible; - g->totalRows = 0; - - for (int32_t e = 0; e < count; e++) { - int32_t col; - int32_t row; - - entryCell(d, e, g->cols, &col, &row); - - if (row + 1 > g->totalRows) { - g->totalRows = row + 1; - } - } - - if (g->totalRows <= g->rowsVisible) { - break; - } - } + listIconGrid(sIconView[d->model->view], font->charWidth, font->charHeight, w->w, w->h, (int32_t)arrlen(d->entries), rowsTaken, g); } @@ -726,10 +569,10 @@ static const char *viewGetText(const WidgetT *w) { // Right button: select the entry under the pointer before the context // menu opens, so the menu acts on it. static void viewMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) { - AppContextT *ctx = wgtGetContext(w); - ViewGeomT g; - int32_t col; - int32_t row; + AppContextT *ctx = wgtGetContext(w); + ListIconGridT g; + int32_t col; + int32_t row; if (button != PV_BUTTON_RIGHT) { return; @@ -748,9 +591,9 @@ static void viewMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) { static void viewOnKey(WidgetT *w, int32_t key, int32_t mod) { - PmViewDataT *d = (PmViewDataT *)w->data; - AppContextT *ctx = wgtGetContext(w); - ViewGeomT g; + PmViewDataT *d = (PmViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + ListIconGridT g; if (mod & (KEY_MOD_CTRL | KEY_MOD_ALT)) { return; @@ -794,24 +637,23 @@ static void viewOnKey(WidgetT *w, int32_t key, int32_t mod) { // Left press: scrollbar parts, else select the entry under the pointer // (empty space clears the selection) and arm a drag. static void viewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { - PmViewDataT *d = (PmViewDataT *)w->data; - AppContextT *ctx = (AppContextT *)root->userData; - ViewGeomT g; - int32_t col; - int32_t row; + PmViewDataT *d = (PmViewDataT *)w->data; + AppContextT *ctx = (AppContextT *)root->userData; + ListIconGridT g; + int32_t col; + int32_t row; viewSync(w, &ctx->font, &g); sFocusedWidget = w; - if (g.needSb && vx >= g.innerX + g.innerW) { - int32_t dragOff = 0; + ListIconScrollE bar = listIconScrollPress(&g, vx - w->x, vy - w->y, &d->scrollRow, &d->sbDragOff); - if (widgetScrollbarApplyHit(g.innerH, vy - g.innerY, g.totalRows, g.rowsVisible, 1, g.rowsVisible, &d->scrollRow, &dragOff)) { - d->sbDragging = true; - d->sbDragOff = dragOff; - sDragWidget = w; - } + if (bar == ListIconScrollThumbE) { + d->sbDragging = true; + sDragWidget = w; + } + if (bar != ListIconScrollNoneE) { return; } @@ -830,15 +672,12 @@ static void viewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { static bool viewOnWheel(WidgetT *w, int32_t delta) { - PmViewDataT *d = (PmViewDataT *)w->data; - AppContextT *ctx = wgtGetContext(w); - ViewGeomT g; + PmViewDataT *d = (PmViewDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + ListIconGridT g; viewGeom(w, &ctx->font, &g); - - int32_t maxScroll = g.totalRows - g.rowsVisible; - - d->scrollRow = clampInt(d->scrollRow + delta * PV_WHEEL_ROWS, 0, maxScroll > 0 ? maxScroll : 0); + d->scrollRow = clampInt(d->scrollRow + delta * LIST_ICON_WHEEL_ROWS, 0, g.maxScroll); wgtInvalidatePaint(w); return true; } @@ -847,33 +686,32 @@ static bool viewOnWheel(WidgetT *w, int32_t delta) { // Sunken frame, the cells clipped to the inside, the drop target of a // drag, then the scrollbar. static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - PmViewDataT *vd = (PmViewDataT *)w->data; - BevelStyleT bevel = BEVEL_SUNKEN(colors, colors->contentBg, PV_BORDER); - int32_t count = (int32_t)arrlen(vd->entries); - int32_t clipX = d->clipX; - int32_t clipY = d->clipY; - int32_t clipW = d->clipW; - int32_t clipH = d->clipH; - ViewGeomT g; + PmViewDataT *vd = (PmViewDataT *)w->data; + BevelStyleT bevel = BEVEL_SUNKEN(colors, colors->contentBg, LIST_ICON_FRAME); + int32_t count = (int32_t)arrlen(vd->entries); + int32_t clipX = d->clipX; + int32_t clipY = d->clipY; + int32_t clipW = d->clipW; + int32_t clipH = d->clipH; + ListIconGridT g; viewSync(w, font, &g); - - int32_t maxScroll = g.totalRows - g.rowsVisible; - - vd->scrollRow = clampInt(vd->scrollRow, 0, maxScroll > 0 ? maxScroll : 0); + vd->scrollRow = clampInt(vd->scrollRow, 0, g.maxScroll); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Intersect the widget's inside with the clip already in force. - int32_t left = g.innerX > clipX ? g.innerX : clipX; - int32_t top = g.innerY > clipY ? g.innerY : clipY; - int32_t right = g.innerX + g.innerW < clipX + clipW ? g.innerX + g.innerW : clipX + clipW; - int32_t bottom = g.innerY + g.innerH < clipY + clipH ? g.innerY + g.innerH : clipY + clipH; + int32_t innerX = w->x + g.innerX; + int32_t innerY = w->y + g.innerY; + int32_t left = DVX_MAX(innerX, clipX); + int32_t top = DVX_MAX(innerY, clipY); + int32_t right = DVX_MIN(innerX + g.innerW, clipX + clipW); + int32_t bottom = DVX_MIN(innerY + g.innerH, clipY + clipH); if (right > left && bottom > top) { setClipRect(d, left, top, right - left, bottom - top); if (count == 0 && vd->emptyText) { - drawText(d, ops, font, g.innerX + PV_PAD, g.innerY + PV_PAD, vd->emptyText, colors->contentFg, colors->contentBg, false); + drawText(d, ops, font, w->x + g.emptyX, w->y + g.emptyY, vd->emptyText, colors->contentFg, colors->contentBg, false); } for (int32_t e = 0; e < count; e++) { @@ -887,15 +725,15 @@ static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap } } - if (vd->dragging && vd->dropCol != PV_NO_CELL) { - drawFocusRect(d, ops, g.innerX + vd->dropCol * g.cellW, g.innerY + (vd->dropRow - vd->scrollRow) * g.cellH, g.cellW, g.cellH, colors->contentFg); + if (vd->dragging && vd->dropCol != LIST_ICON_NO_CELL) { + drawFocusRect(d, ops, innerX + vd->dropCol * g.cell.cellW, innerY + (vd->dropRow - vd->scrollRow) * g.cell.cellH, g.cell.cellW, g.cell.cellH, colors->contentFg); } setClipRect(d, clipX, clipY, clipW, clipH); } if (g.needSb) { - widgetDrawScrollbarV(d, ops, colors, g.innerX + g.innerW, g.innerY, g.innerH, g.totalRows, g.rowsVisible, vd->scrollRow); + widgetDrawScrollbarV(d, ops, colors, innerX + g.innerW, innerY, g.innerH, g.totalRows, g.rowsVisible, vd->scrollRow); } if (w == sFocusedWidget && vd->selected < 0) { @@ -907,7 +745,7 @@ static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap // Geometry, first telling Program Manager the column count whenever it // changed or the entries were replaced (it places unplaced items then). // Nothing is reported before the first layout gives the view a width. -static void viewSync(WidgetT *w, const BitmapFontT *font, ViewGeomT *g) { +static void viewSync(WidgetT *w, const BitmapFontT *font, ListIconGridT *g) { PmViewDataT *d = (PmViewDataT *)w->data; viewGeom(w, font, g); diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c index 24306f41..cb4d4bf5 100644 --- a/src/apps/kpunch/progman/progman.c +++ b/src/apps/kpunch/progman/progman.c @@ -44,19 +44,20 @@ // A callback-only DXE app (hasMainLoop = false): appMain builds the // window, registers callbacks and returns; the shell's event loop drives // everything else. Program Manager is the desktop: closing it exits DVX -// through shellRequestExit, and shellRegisterDesktopUpdate keeps its +// through shellRequestExit (and the shell exits DVX by itself when the +// desktop ends any other way), and shellRegisterDesktopUpdate keeps its // running-program count current. #include "pmModel.h" #include "pmProps.h" #include "pmView.h" +#include "listhelp/listHelp.h" #include "dvxApp.h" #include "dvxDlg.h" #include "dvxPlat.h" #include "dvxWgt.h" #include "dvxWm.h" #include "shellApp.h" -#include "shellAssoc.h" #include "shellInf.h" #include "shellProg.h" #include "shellRun.h" @@ -66,6 +67,7 @@ #include "statusBar/statBar.h" #include "textInput/textInpt.h" +#include #include #include #include @@ -90,7 +92,6 @@ #define PM_STATUS_LEN 160 #define PM_MSG_LEN (PM_NAME_MAX * 2 + 128) #define PM_INFO_LEN (DVX_MAX_PATH + PM_NAME_MAX + SHELL_PROG_DESC_MAX + 64) -#define PM_MINIMIZE_FRAMES 60 // idle frames a Run Minimized program's new windows are minimized #define PM_KEY_ALT_ENTER (0xA6 | KEY_EXT_FLAG) // BIOS enhanced-keyboard Alt+Enter #define PM_INI_NAME "progman.ini" @@ -164,8 +165,6 @@ static PmHotkeyT *sHotkeys = NULL; // stb_ds: the item shortcut keys static PmModelT sModel; static char sIniPath[DVX_MAX_PATH]; static char sFilter[PM_SEARCH_MAX]; -static int32_t sMinimizeApp = 0; // Run Minimized program still opening windows -static int32_t sMinimizeFrames = 0; static bool sStartupPending = true; static bool sScanning = false; static void (*sPrevOnFocus)(WindowT *win) = NULL; @@ -194,11 +193,9 @@ static void doRename(void); static void doRenameGroup(void); static void doRun(void); static void doSetView(PmViewE view); -static bool installedChanged(void); static void launchItem(const PmItemT *item); static void loadItemIcon(PmItemT *item); static bool manualLayout(void); -static void minimizeAppWindows(int32_t appId); static void onGroupChange(WidgetT *w); static void onHotkey(void *userCtx); static void onPmClose(WindowT *win); @@ -269,7 +266,6 @@ void appShutdown(void) { sItemMenu = NULL; sGroupMenu = NULL; pmViewUnregister(); - dvxQuit(sAc); } @@ -562,7 +558,7 @@ static void doDeleteGroup(void) { return; } - snprintf(msg, sizeof(msg), "Delete the group \"%s\" and its %ld item(s)?\n\nThe programs themselves are not removed.", grp->name, (long)arrlen(grp->items)); + snprintf(msg, sizeof(msg), "Delete the group \"%s\" and its %" PRId32 " item(s)?\n\nThe programs themselves are not removed.", grp->name, (int32_t)arrlen(grp->items)); if (dvxMessageBox(sAc, "Delete Group", msg, MB_YESNO | MB_ICONQUESTION) == ID_YES && pmGroupDelete(&sModel, sModel.current)) { saveModel(); @@ -730,29 +726,6 @@ static void doSetView(PmViewE view) { } -// Programs added or removed since the last scan (paths only: no program -// is opened for this check). -static bool installedChanged(void) { - ShellProgramT *found = NULL; - const PmGroupT *installed = &sModel.groups[PM_INSTALLED_IDX]; - int32_t count = shellProgramScan(SHELL_PROG_DIR, sCtx->appPath, false, NULL, NULL, &found); - bool changed = count != arrlen(installed->items); - - for (int32_t i = 0; i < count && !changed; i++) { - bool known = false; - - for (int32_t j = 0; j < arrlen(installed->items) && !known; j++) { - known = shellPathsMatch(found[i].path, installed->items[j].program); - } - - changed = !known; - } - - shellProgramListFree(found); - return changed; -} - - // Start an item and, with Minimize on Run, put Program Manager away. static void launchItem(const PmItemT *item) { bool minimized = item->runMinimized; @@ -765,22 +738,11 @@ static void launchItem(const PmItemT *item) { } -// The chosen icon file, else the program's own icon, else (for a -// document) the icon of the program that opens it. +// The shell's item icon rule (shellItemIcon), at both sizes. static void loadItemIcon(PmItemT *item) { - const char *source = item->iconPath[0] ? item->iconPath : item->program; - char opener[DVX_MAX_PATH]; - char args[SHELL_ARGS_MAX]; - item->iconTried = true; - item->icon = shellProgramIcon(sAc, source, SHELL_PROG_ICON, &item->iconPitch); - - if (!item->icon && !shellIsProgram(source) && shellAssocCommand(source, opener, sizeof(opener), args, sizeof(args))) { - source = opener; - item->icon = shellProgramIcon(sAc, source, SHELL_PROG_ICON, &item->iconPitch); - } - - item->iconSmall = shellProgramIcon(sAc, source, SHELL_PROG_ICON_SMALL, &item->iconSmallPitch); + item->icon = shellItemIcon(sAc, item->iconPath, item->program, SHELL_PROG_ICON, &item->iconPitch); + item->iconSmall = shellItemIcon(sAc, item->iconPath, item->program, SHELL_PROG_ICON_SMALL, &item->iconSmallPitch); } @@ -791,17 +753,6 @@ static bool manualLayout(void) { } -static void minimizeAppWindows(int32_t appId) { - for (int32_t i = 0; i < sAc->stack.count; i++) { - WindowT *win = sAc->stack.windows[i]; - - if (win->appId == appId && win->visible && !win->minimized) { - dvxMinimizeWindow(sAc, win); - } - } -} - - // Choosing a group ends a search. static void onGroupChange(WidgetT *w) { int32_t sel = wgtListBoxGetSelected(w); @@ -846,7 +797,7 @@ static void onPmFocus(WindowT *win) { sPrevOnFocus(win); } - if (!sScanning && installedChanged()) { + if (!sScanning && pmInstalledChanged(&sModel, sCtx->appPath)) { scanInstalled(false); refreshView(); } @@ -968,9 +919,7 @@ static void onPmMenu(WindowT *win, int32_t menuId) { break; case CMD_TASK_MGR: - if (shellCtrlEscFn) { - shellCtrlEscFn(sAc); - } + shellOpenTaskManager(sAc); break; default: @@ -995,20 +944,22 @@ static void onViewColumns(int32_t cols) { // Auto Arrange (and the list) reorder the group: the item goes to the -// drop position. Without it the item takes the cell it was dropped on -// (swapping with an item already there). +// place of the cell it was dropped on (the icon view rule, +// listIconDropDest, which BASIC icon views share). Without it the item +// takes the cell (swapping with an item already there). static void onViewDrop(int32_t entry, int32_t col, int32_t row) { - int32_t g = sEntries[entry].group; - int32_t i = sEntries[entry].item; - PmGroupT *grp = &sModel.groups[g]; - int32_t last = (int32_t)arrlen(grp->items) - 1; - int32_t cols = pmViewColumns(sView); - int32_t index = sModel.view == PmViewListE ? row : row * cols + col; + int32_t g = sEntries[entry].group; + int32_t i = sEntries[entry].item; + PmGroupT *grp = &sModel.groups[g]; if (manualLayout()) { pmItemPlace(grp, i, col, row); } else { - i = pmItemReorder(grp, i, index < last ? index : last); + int32_t dest = listIconDropDest(i, row * pmViewColumns(sView) + col, (int32_t)arrlen(grp->items)); + + if (dest >= 0) { + i = pmItemReorder(grp, i, dest); + } } saveModel(); @@ -1033,8 +984,7 @@ static void onViewSelect(void) { } -// Idle work: the Startup group once, right after DVX has started, and -// minimizing the windows a Run Minimized program opens as it starts. +// Idle work: the Startup group once, right after DVX has started. static void pmIdle(void *ctx) { (void)ctx; @@ -1048,11 +998,6 @@ static void pmIdle(void *ctx) { g = pmGroupFind(&sModel, PM_STARTUP_NAME); } } - - if (sMinimizeFrames > 0) { - sMinimizeFrames--; - minimizeAppWindows(sMinimizeApp); - } } @@ -1098,7 +1043,7 @@ static void refreshGroups(void) { // Everything that follows the model: the view's entries (the selected // group, or every match of the search), shortcuts, menus, status. static void refreshView(void) { - const char *empty = EMPTY_GROUP; + const char *empty; arrsetlen(sEntries, 0); @@ -1152,32 +1097,15 @@ static void saveModel(void) { // itself). With pump the screen keeps updating during a long scan; the // guard stops a nested Refresh from freeing the list mid-scan. static void scanInstalled(bool pump) { - ShellProgramT *found = NULL; - PmItemT *items = NULL; - if (sScanning) { return; } sScanning = true; - int32_t count = shellProgramScan(SHELL_PROG_DIR, sCtx->appPath, true, pump ? scanProgress : NULL, NULL, &found); + int32_t count = pmInstalledScan(&sModel, sCtx->appPath, pump ? scanProgress : NULL, NULL); - for (int32_t i = 0; i < count; i++) { - PmItemT item; - - memset(&item, 0, sizeof(item)); - snprintf(item.name, sizeof(item.name), "%s", found[i].name); - snprintf(item.program, sizeof(item.program), "%s", found[i].path); - snprintf(item.description, sizeof(item.description), "%s", found[i].description); - item.col = PM_CELL_NONE; - item.row = PM_CELL_NONE; - arrput(items, item); - } - - shellProgramListFree(found); - pmInstalledReplace(&sModel, items); - dvxLog("Progman: found %ld program(s)", (long)count); + dvxLog("Progman: found %" PRId32 " program(s)", count); sScanning = false; } @@ -1222,7 +1150,7 @@ static void showAboutDialog(void) { static void showSystemInfo(void) { const char *info = shellGetSystemInfo(); - if (!info || !info[0]) { + if (!info[0]) { dvxInfoBox(sAc, "System Information", "No system information available."); return; } @@ -1253,16 +1181,13 @@ static void showSystemInfo(void) { // The item is copied first: starting a program runs its code, which may // pump events that change the model. A Run Minimized item's windows are -// minimized now and during the next idle frames (a program with a main -// loop opens them later). +// minimized by the shell (shellMinimizeApp). static void startItem(const PmItemT *item) { PmItemT run = *item; int32_t id = shellRunProgram(sAc, run.program, run.args, run.folder); if (id > 0 && run.runMinimized) { - sMinimizeApp = id; - sMinimizeFrames = PM_MINIMIZE_FRAMES; - minimizeAppWindows(id); + shellMinimizeApp(sAc, id); } updateStatusText(); @@ -1314,7 +1239,7 @@ static void updateStatusText(void) { snprintf(buf, sizeof(buf), "%s", item->description[0] ? item->description : item->program); } else { - snprintf(buf, sizeof(buf), "%ld item(s)", (long)arrlen(sEntries)); + snprintf(buf, sizeof(buf), "%" PRId32 " item(s)", (int32_t)arrlen(sEntries)); } wgtSetText(sStatusLabel, buf); @@ -1324,7 +1249,7 @@ static void updateStatusText(void) { } else if (running == 1) { snprintf(buf, sizeof(buf), "1 application running"); } else { - snprintf(buf, sizeof(buf), "%ld applications running", (long)running); + snprintf(buf, sizeof(buf), "%" PRId32 " applications running", running); } wgtSetText(sRunningLabel, buf); diff --git a/src/apps/kpunch/terminal/icon32.bmp b/src/apps/kpunch/terminal/ICON32.BMP similarity index 100% rename from src/apps/kpunch/terminal/icon32.bmp rename to src/apps/kpunch/terminal/ICON32.BMP diff --git a/src/apps/kpunch/terminal/termBook.frm b/src/apps/kpunch/terminal/termBook.frm new file mode 100644 index 00000000..51406c8b --- /dev/null +++ b/src/apps/kpunch/terminal/termBook.frm @@ -0,0 +1,181 @@ +VERSION DVX 1.00 +Begin Form BookForm + Caption = "Phone Book" + Layout = VBox + AutoSize = True + Resizable = False + Centered = True + Begin HBox boxBody + Begin ListBox lstBook + MinWidth = 288 + MinHeight = 160 + Weight = 1 + End + Begin VBox boxEntry + Weight = 0 + Begin CommandButton cmdBookNew + Caption = "&New..." + MinWidth = 80 + End + Begin CommandButton cmdBookEdit + Caption = "&Edit..." + MinWidth = 80 + End + Begin CommandButton cmdBookDelete + Caption = "&Delete" + MinWidth = 80 + End + End + End + Begin HBox boxButtons + Alignment = Right + Begin CommandButton cmdBookConnect + Caption = "&Connect" + Default = True + MinWidth = 80 + MinHeight = 24 + End + Begin CommandButton cmdBookCancel + Caption = "&Cancel" + Cancel = True + MinWidth = 80 + MinHeight = 24 + End + End +End + + +' 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. + + +' termBook.frm -- Terminal's phone book: pick a session to connect with, +' or add, change and delete sessions (each change is saved at once). +' Connect, Enter and a double click leave the chosen entry in bookChoice +' (terminal.bas); with none selected the dialog stays open. Cancel, +' Escape and the close box leave bookChoice -1. Its buttons are the size +' of the buttons of DVX's own dialog boxes. + +OPTION EXPLICIT + + +' Show the phone book (modal) with the current session selected. True +' when the user chose an entry to connect with (bookChoice). +FUNCTION BookDialog() AS BOOLEAN + Load BookForm + bookChoice = -1 + BookRefresh + lstBook.ListIndex = bookCurrent + lstBook.SetFocus + BookForm.Show vbModal + BookDialog = bookChoice >= 0 +END FUNCTION + + +' The list shows every session's name. +SUB BookRefresh + DIM i AS LONG + + lstBook.Clear + + FOR i = 0 TO bookCount - 1 + lstBook.AddItem book(i).sessionName + NEXT i +END SUB + + +SUB cmdBookCancel_Click + Unload BookForm +END SUB + + +' Connect with the selected session. +SUB cmdBookConnect_Click + IF lstBook.ListIndex >= 0 THEN + bookChoice = lstBook.ListIndex + Unload BookForm + END IF +END SUB + + +SUB cmdBookDelete_Click + DIM sel AS LONG + + sel = lstBook.ListIndex + + IF sel < 0 OR bookCount <= 1 THEN + EXIT SUB + END IF + + IF MsgBox("Delete " & QUOTE & book(sel).sessionName & QUOTE & " from the phone book?", vbYesNo + vbQuestion, "Phone Book") <> vbYes THEN + EXIT SUB + END IF + + BookRemove sel + SaveBook + BookRefresh +END SUB + + +SUB cmdBookEdit_Click + DIM sel AS LONG + + sel = lstBook.ListIndex + + IF sel < 0 THEN + EXIT SUB + END IF + + dlgSession = book(sel) + + IF SessionDialog("Edit Session") THEN + book(sel) = dlgSession + SaveBook + BookRefresh + END IF +END SUB + + +SUB cmdBookNew_Click + DIM index AS LONG + + IF bookCount >= TERM_BOOK_MAX THEN + MsgBox "The phone book is full. Delete an entry first.", vbOKOnly + vbCritical, "Phone Book" + EXIT SUB + END IF + + SessionDefaults dlgSession + dlgSession.sessionName = "New session" + + IF SessionDialog("New Session") THEN + index = BookAdd(dlgSession) + SaveBook + BookRefresh + lstBook.ListIndex = index + END IF +END SUB + + +' A double click connects, as the Connect button does. +SUB lstBook_DblClick + cmdBookConnect_Click +END SUB diff --git a/src/apps/kpunch/terminal/termMain.bas b/src/apps/kpunch/terminal/termMain.bas new file mode 100644 index 00000000..effafaa4 --- /dev/null +++ b/src/apps/kpunch/terminal/termMain.bas @@ -0,0 +1,29 @@ +' 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. + +' termMain.bas -- Terminal's start. A program's forms are loaded (and +' its startup form shown) before its module-level code runs, and the +' phone book's arrays (terminal.bas) exist only once that code has run: +' this module comes last, so the main window starts here, with every +' module ready. + +TermStart diff --git a/src/apps/kpunch/terminal/termPass.frm b/src/apps/kpunch/terminal/termPass.frm new file mode 100644 index 00000000..e6a4c718 --- /dev/null +++ b/src/apps/kpunch/terminal/termPass.frm @@ -0,0 +1,92 @@ +VERSION DVX 1.00 +Begin Form PassForm + Caption = "Secure Link Passphrase" + Layout = VBox + AutoSize = True + Resizable = False + Centered = True + Begin Label lblPrompt + Caption = "Passphrase for the secure link:" + End + Begin TextBox txtPassphrase + MinWidth = 224 + End + Begin HBox boxButtons + Alignment = Right + Begin CommandButton cmdPassConnect + Caption = "&Connect" + Default = True + MinWidth = 80 + MinHeight = 24 + End + Begin CommandButton cmdPassCancel + Caption = "&Cancel" + Cancel = True + MinWidth = 80 + MinHeight = 24 + End + End +End + + +' 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. + + +' termPass.frm -- Terminal's question for the secure link's passphrase, +' for a session that never stores it. Connect hands the text back and +' sets dlgAccepted (terminal.bas); the passphrase is not kept anywhere +' else. + +OPTION EXPLICIT + + +' Ask for the passphrase (modal). True, with it in pass, on Connect; +' False on Cancel. +FUNCTION PassphraseDialog(pass AS STRING) AS BOOLEAN + Load PassForm + dlgAccepted = False + txtPassphrase.MaxLength = TERM_PASS_MAX + txtPassphrase.PasswordChar = CHR$(PASSWORD_MASK) + txtPassphrase.Text = "" + txtPassphrase.SetFocus + PassForm.Show vbModal + + IF dlgAccepted THEN + pass = passText + passText = "" + END IF + + PassphraseDialog = dlgAccepted +END FUNCTION + + +SUB cmdPassCancel_Click + Unload PassForm +END SUB + + +SUB cmdPassConnect_Click + passText = txtPassphrase.Text + dlgAccepted = True + Unload PassForm +END SUB diff --git a/src/apps/kpunch/terminal/termProg.frm b/src/apps/kpunch/terminal/termProg.frm new file mode 100644 index 00000000..1fb7a07e --- /dev/null +++ b/src/apps/kpunch/terminal/termProg.frm @@ -0,0 +1,271 @@ +VERSION DVX 1.00 +Begin Form ProgressForm + Caption = "File Transfer" + Layout = VBox + AutoSize = True + Resizable = False + Centered = False + Width = 420 + Height = 300 + Begin Label lblTitle + Caption = "" + End + Begin Label lblFile + Caption = "" + MinWidth = 416 + End + Begin ProgressBar pbFile + End + Begin Label lblFileBytes + Caption = "" + End + Begin ProgressBar pbBatch + End + Begin Label lblBatchBytes + Caption = "" + End + Begin Label lblNote + Caption = "" + End + Begin VBox boxAsk + Visible = False + Begin Label lblAsk + Caption = "" + End + Begin HBox rowAsk + Begin CommandButton cmdAskResume + Caption = "Con&tinue it" + End + Begin CommandButton cmdAskRename + Caption = "&Keep both" + End + Begin CommandButton cmdAskReplace + Caption = "&Replace" + End + Begin CommandButton cmdAskSkip + Caption = "&Skip" + End + End + End + Begin HBox boxButtons + Alignment = Right + Begin CommandButton cmdProgressCancel + Caption = "Cancel" + MinWidth = 80 + End + End +End + + +' 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. + + +' termProg.frm -- Terminal's file transfer window: what is moving, how far +' (the current file and the whole batch), the retries or how it ended, and +' Cancel, which becomes Close once the transfer has ended. When a +' download would replace a file that is already there, the window asks +' what should happen to it (continue it, keep both, replace it, skip it). +' The heartbeat (terminal.frm) refreshes it at every tick; it also +' notices the end of the transfer, which it notes on the screen and in +' the log. + +OPTION EXPLICIT + +' Where the window opens: centered as a window of this size would be +CONST PROGRESS_W = 420 +CONST PROGRESS_H = 300 + +CONST PERCENT = 100 + + +' Open the window for the transfer xfer (terminal.bas), as new: the bars +' empty, no question. The window stays loaded (hidden) between transfers. +SUB ProgressOpen + ProgressForm.Left = (Screen.Width - PROGRESS_W) \ 2 + ProgressForm.Top = (Screen.Height - PROGRESS_H) \ 2 + pbFile.Value = 0 + pbBatch.Value = 0 + progressShown = True + ProgressUpdate + ProgressForm.Show +END SUB + + +SUB ProgressClose + progressShown = False + ProgressForm.Hide +END SUB + + +' Refresh the window from the transfer; notice its end. +SUB ProgressUpdate + DIM st AS LONG + DIM running AS BOOLEAN + DIM text AS STRING + DIM fileName AS STRING + DIM fileCount AS LONG + DIM fileIndex AS LONG + DIM size AS LONG + + st = XferStatus(xfer) + running = (st = XFER_RUNNING OR st = XFER_ASKING) + + IF transferring AND NOT running THEN + transferring = False + + IF XferSending(xfer) THEN + text = "Upload: " & XferMessage$(xfer) + ELSE + text = "Download: " & XferMessage$(xfer) + END IF + + TerminalNote text + ShellLog "Terminal: " & text + MenuUpdate + END IF + + IF NOT progressShown THEN + EXIT SUB + END IF + + IF XferSending(xfer) THEN + lblTitle.Caption = "Sending with " & ChoiceName$(PROTO_LABELS, XferProtocol(xfer)) + ELSE + lblTitle.Caption = "Receiving with " & ChoiceName$(PROTO_LABELS, XferProtocol(xfer)) + END IF + + fileName = XferFile$(xfer) + fileCount = XferFileCount(xfer) + fileIndex = XferFileIndex(xfer) + + IF fileCount > 0 THEN + IF fileIndex >= fileCount THEN + fileIndex = fileCount - 1 + END IF + + lblFile.Caption = "File " & CStr(fileIndex + 1) & " of " & CStr(fileCount) & ": " & fileName + ELSEIF fileName <> "" THEN + lblFile.Caption = "File " & CStr(fileIndex + 1) & ": " & fileName + ELSE + lblFile.Caption = "File " & CStr(fileIndex + 1) & ": (waiting for the other side)" + END IF + + ' The current file + size = XferSize(xfer) + + IF size > 0 THEN + text = ByteText$(XferBytes(xfer)) & " of " & ByteText$(size) & " bytes" + pbFile.Value = Int(CDbl(XferBytes(xfer)) * PERCENT / size) + ELSE + text = ByteText$(XferBytes(xfer)) & " bytes" + END IF + + IF XferResumedAt(xfer) > 0 THEN + text = text & " (continued from " & ByteText$(XferResumedAt(xfer)) & ")" + END IF + + lblFileBytes.Caption = text + + ' The whole batch + size = XferBatchSize(xfer) + + IF size > 0 THEN + lblBatchBytes.Caption = "All files: " & ByteText$(XferBatchBytes(xfer)) & " of " & ByteText$(size) & " bytes" + pbBatch.Value = Int(CDbl(XferBatchBytes(xfer)) * PERCENT / size) + ELSE + lblBatchBytes.Caption = "All files: " & ByteText$(XferBatchBytes(xfer)) & " bytes" + END IF + + IF running THEN + lblNote.Caption = "Retries: " & CStr(XferErrors(xfer)) + ELSE + lblNote.Caption = XferMessage$(xfer) + END IF + + ' A download that would replace a file: ask what should happen to it + IF st = XFER_ASKING THEN + lblAsk.Caption = fileName & " already exists (" & ByteText$(XferAskSize(xfer)) & " bytes). What should happen to it?" + cmdAskResume.Enabled = XferCanResume(xfer) + END IF + + boxAsk.Visible = (st = XFER_ASKING) + + IF running THEN + cmdProgressCancel.Caption = "Cancel" + ELSE + cmdProgressCancel.Caption = "Close" + END IF +END SUB + + +' An answer to the "file already exists" question. +SUB AskAnswer(BYVAL action AS LONG) + XferAnswer xfer, action + ProgressUpdate +END SUB + + +SUB cmdAskRename_Click + AskAnswer XFER_EXIST_RENAME +END SUB + + +SUB cmdAskReplace_Click + AskAnswer XFER_EXIST_OVERWRITE +END SUB + + +SUB cmdAskResume_Click + AskAnswer XFER_EXIST_RESUME +END SUB + + +SUB cmdAskSkip_Click + AskAnswer XFER_EXIST_SKIP +END SUB + + +' Cancel while running; Close once the transfer has ended. +SUB cmdProgressCancel_Click + IF transferring THEN + XferCancel xfer + ProgressUpdate + ELSE + ProgressClose + END IF +END SUB + + +' The close box: as Cancel, so the window stays until the transfer has +' ended, then it is hidden like Close does. Only the end of the program +' (TermForm_Unload) unloads it. +SUB ProgressForm_QueryUnload(Cancel AS INTEGER) + IF transferring THEN + XferCancel xfer + ProgressUpdate + Cancel = 1 + ELSEIF progressShown THEN + ProgressClose + Cancel = 1 + END IF +END SUB diff --git a/src/apps/kpunch/terminal/termSess.frm b/src/apps/kpunch/terminal/termSess.frm new file mode 100644 index 00000000..471bdfbf --- /dev/null +++ b/src/apps/kpunch/terminal/termSess.frm @@ -0,0 +1,402 @@ +VERSION DVX 1.00 +Begin Form SessionForm + Caption = "Session Settings" + Layout = VBox + AutoSize = True + Resizable = False + Centered = True + Begin TabStrip tabPages + Begin TabPage pgConn + Caption = "Connection" + Begin HBox rowName + Begin Label lblName + Caption = "Name:" + MinWidth = 128 + End + Begin TextBox txtName + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowType + Begin Label lblType + Caption = "Connect with:" + MinWidth = 128 + End + Begin DropDown ddType + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowPort + Begin Label lblPort + Caption = "Port:" + MinWidth = 128 + End + Begin DropDown ddPort + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowSpeed + Begin Label lblSpeed + Caption = "Speed (bps):" + MinWidth = 128 + End + Begin DropDown ddSpeed + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowData + Begin Label lblData + Caption = "Data bits:" + MinWidth = 128 + End + Begin DropDown ddData + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowParity + Begin Label lblParity + Caption = "Parity:" + MinWidth = 128 + End + Begin DropDown ddParity + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowStop + Begin Label lblStop + Caption = "Stop bits:" + MinWidth = 128 + End + Begin DropDown ddStop + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowFlow + Begin Label lblFlow + Caption = "Flow control:" + MinWidth = 128 + End + Begin DropDown ddFlow + MinWidth = 224 + Weight = 1 + End + End + End + Begin TabPage pgModem + Caption = "Modem" + Begin HBox rowInit + Begin Label lblInit + Caption = "Setup command:" + MinWidth = 128 + End + Begin TextBox txtInit + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowPrefix + Begin Label lblPrefix + Caption = "Dial command:" + MinWidth = 128 + End + Begin TextBox txtPrefix + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowPhone + Begin Label lblPhone + Caption = "Phone number:" + MinWidth = 128 + End + Begin TextBox txtPhone + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowRedial + Begin Label lblRedial + Caption = "Redial tries:" + MinWidth = 128 + End + Begin SpinButton spnRedial + MinWidth = 80 + End + End + Begin HBox rowDelay + Begin Label lblDelay + Caption = "Redial wait (s):" + MinWidth = 128 + End + Begin SpinButton spnDelay + MinWidth = 80 + End + End + Begin Label lblModemNote + Caption = "Leave the phone number empty to type modem commands yourself." + End + End + Begin TabPage pgSecure + Caption = "Secure Link" + Begin HBox rowPass + Begin Label lblPass + Caption = "Passphrase:" + MinWidth = 128 + End + Begin TextBox txtPass + MinWidth = 224 + Weight = 1 + End + End + Begin CheckBox chkAsk + Caption = "Ask for the passphrase when connecting (never save it)" + End + Begin CheckBox chkSecureNet + Caption = "Connect over the network (the host and port on the Network page)" + End + Begin Label lblSecureNote + Caption = "Use the same passphrase as the host, or none on both sides." + End + End + Begin TabPage pgNetwork + Caption = "Network" + Begin HBox rowHost + Begin Label lblHost + Caption = "Host name:" + MinWidth = 128 + End + Begin TextBox txtHost + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowTcpPort + Begin Label lblTcpPort + Caption = "Port:" + MinWidth = 128 + End + Begin SpinButton spnTcpPort + MinWidth = 80 + End + End + Begin Label lblNetNote + Caption = "For Telnet (usually port 23), and for a secure link over the network." + End + End + Begin TabPage pgScreen + Caption = "Screen" + Begin HBox rowCols + Begin Label lblCols + Caption = "Columns:" + MinWidth = 128 + End + Begin SpinButton spnCols + MinWidth = 80 + End + End + Begin HBox rowRows + Begin Label lblRows + Caption = "Rows:" + MinWidth = 128 + End + Begin SpinButton spnRows + MinWidth = 80 + End + End + Begin HBox rowScroll + Begin Label lblScroll + Caption = "Scrollback lines:" + MinWidth = 128 + End + Begin SpinButton spnScroll + MinWidth = 80 + End + End + Begin HBox rowScheme + Begin Label lblScheme + Caption = "Colors:" + MinWidth = 128 + End + Begin DropDown ddScheme + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowAnswer + Begin Label lblAnswer + Caption = "Answerback:" + MinWidth = 128 + End + Begin TextBox txtAnswer + MinWidth = 224 + Weight = 1 + End + End + Begin CheckBox chkEcho + Caption = "Local echo (show what you type)" + End + End + End + Begin HBox boxButtons + Alignment = Right + Begin CommandButton cmdSessionOk + Caption = "&OK" + Default = True + MinWidth = 80 + MinHeight = 24 + End + Begin CommandButton cmdSessionCancel + Caption = "&Cancel" + Cancel = True + MinWidth = 80 + MinHeight = 24 + End + End +End + +' 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. + + +' termSess.frm -- Terminal's Session Settings dialog: one session's +' connection (type, port and line settings), modem, secure link, network +' and screen settings on five pages. The phone book's New... and Edit... +' and Settings > Session Settings... show it with SessionDialog, which +' edits dlgSession (terminal.bas): OK copies every field back, forced into +' range, and sets dlgAccepted; Cancel, Escape and the close box leave +' dlgSession as it was. Every dropdown lists its choices in the order of +' terminal.bas's lists, so a choice's index is its place in the list. + +OPTION EXPLICIT + + +' Show the dialog (modal) with title, filled from dlgSession. True when +' the user pressed OK (dlgSession then holds the new settings). +FUNCTION SessionDialog(BYVAL title AS STRING) AS BOOLEAN + Load SessionForm + SessionForm.Caption = title + dlgAccepted = False + + txtName.MaxLength = TERM_NAME_MAX + txtInit.MaxLength = TERM_INIT_MAX + txtPrefix.MaxLength = TERM_PREFIX_MAX + txtPhone.MaxLength = TERM_PHONE_MAX + txtPass.MaxLength = TERM_PASS_MAX + txtPass.PasswordChar = CHR$(PASSWORD_MASK) + txtHost.MaxLength = TERM_HOST_MAX + txtAnswer.MaxLength = TERM_ANSWERBACK_MAX + + ' Connection + txtName.Text = dlgSession.sessionName + FillChoices ddType, TYPE_LABELS, dlgSession.connType + FillChoices ddPort, PORT_NAMES, dlgSession.comPort - 1 + FillChoices ddSpeed, SPEED_NAMES, ChoiceIndex(SPEED_NAMES, CStr(dlgSession.bps)) + FillChoices ddData, DATA_NAMES, ChoiceIndex(DATA_NAMES, CStr(dlgSession.dataBits)) + FillChoices ddParity, PARITY_LABELS, ChoiceIndex(PARITY_NAMES, dlgSession.parity) + FillChoices ddStop, STOP_NAMES, ChoiceIndex(STOP_NAMES, CStr(dlgSession.stopBits)) + FillChoices ddFlow, FLOW_LABELS, dlgSession.flow + + ' Modem + txtInit.Text = dlgSession.initString + txtPrefix.Text = dlgSession.dialPrefix + txtPhone.Text = dlgSession.phone + SetSpinner spnRedial, 0, TERM_REDIAL_MAX, dlgSession.redialCount + SetSpinner spnDelay, TERM_REDIAL_DELAY_MIN, TERM_REDIAL_DELAY_MAX, dlgSession.redialDelay + + ' Secure link: a passphrase that is asked for is never shown + IF dlgSession.askPassphrase THEN + txtPass.Text = "" + ELSE + txtPass.Text = dlgSession.passphrase + END IF + + chkAsk.Value = dlgSession.askPassphrase + chkSecureNet.Value = dlgSession.secureNet + + ' Network + txtHost.Text = dlgSession.host + SetSpinner spnTcpPort, TERM_TCP_PORT_MIN, TERM_TCP_PORT_MAX, dlgSession.tcpPort + + ' Screen + SetSpinner spnCols, TERM_COLS_MIN, TERM_COLS_MAX, dlgSession.cols + SetSpinner spnRows, TERM_ROWS_MIN, TERM_ROWS_MAX, dlgSession.rows + SetSpinner spnScroll, TERM_SCROLLBACK_MIN, TERM_SCROLLBACK_MAX, dlgSession.scrollback + FillChoices ddScheme, SCHEME_LABELS, dlgSession.scheme + txtAnswer.Text = dlgSession.answerback + chkEcho.Value = dlgSession.localEcho + + txtName.SetFocus + SessionForm.Show vbModal + SessionDialog = dlgAccepted +END FUNCTION + + +SUB cmdSessionCancel_Click + Unload SessionForm +END SUB + + +' OK: every field into dlgSession, forced into range. +SUB cmdSessionOk_Click + dlgSession.sessionName = txtName.Text + dlgSession.connType = ChosenIndex(ddType) + dlgSession.comPort = ChosenIndex(ddPort) + 1 + dlgSession.bps = Val(ChoiceName$(SPEED_NAMES, ChosenIndex(ddSpeed))) + dlgSession.dataBits = Val(ChoiceName$(DATA_NAMES, ChosenIndex(ddData))) + dlgSession.parity = ChoiceName$(PARITY_NAMES, ChosenIndex(ddParity)) + dlgSession.stopBits = Val(ChoiceName$(STOP_NAMES, ChosenIndex(ddStop))) + dlgSession.flow = ChosenIndex(ddFlow) + dlgSession.redialCount = spnRedial.Value + dlgSession.redialDelay = spnDelay.Value + dlgSession.tcpPort = spnTcpPort.Value + dlgSession.cols = spnCols.Value + dlgSession.rows = spnRows.Value + dlgSession.scrollback = spnScroll.Value + dlgSession.scheme = ChosenIndex(ddScheme) + + dlgSession.askPassphrase = chkAsk.Value + dlgSession.secureNet = chkSecureNet.Value + dlgSession.localEcho = chkEcho.Value + + dlgSession.initString = txtInit.Text + dlgSession.dialPrefix = txtPrefix.Text + dlgSession.phone = txtPhone.Text + dlgSession.host = txtHost.Text + dlgSession.answerback = txtAnswer.Text + + IF dlgSession.askPassphrase THEN + dlgSession.passphrase = "" + ELSE + dlgSession.passphrase = txtPass.Text + END IF + + SessionClamp dlgSession + dlgAccepted = True + Unload SessionForm +END SUB diff --git a/src/apps/kpunch/terminal/termSession.c b/src/apps/kpunch/terminal/termSession.c deleted file mode 100644 index aec8ea57..00000000 --- a/src/apps/kpunch/terminal/termSession.c +++ /dev/null @@ -1,455 +0,0 @@ -// 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. - -// termSession.c -- Terminal sessions (phone book) and TERMINAL.INI - -#include "termSession.h" -#include "serial/rs232/rs232.h" - -#include -#include -#include -#include - -#define SEC_BOOK "phonebook" -#define SEC_TRANSFER "transfer" -#define SEC_ENTRY_FMT "entry%d" -#define SEC_ENTRY_MAX 16 - -#define KEY_ENTRIES "entries" -#define KEY_CURRENT "current" -#define KEY_DIRECTORY "directory" -#define KEY_PROTOCOL "protocol" -#define KEY_EXISTS "exists" -#define KEY_AUTO_ZMODEM "autozmodem" -#define KEY_ESCAPE_CTL "escapectl" -#define KEY_RESUME "resume" -#define KEY_NAME "name" -#define KEY_TYPE "type" -#define KEY_PORT "port" -#define KEY_SPEED "speed" -#define KEY_DATA "databits" -#define KEY_PARITY "parity" -#define KEY_STOP "stopbits" -#define KEY_FLOW "flow" -#define KEY_INIT "init" -#define KEY_PREFIX "dialprefix" -#define KEY_PHONE "phone" -#define KEY_REDIAL "redial" -#define KEY_REDIAL_DELAY "redialdelay" -#define KEY_ASK_PASS "askpassphrase" -#define KEY_PASS "passphrase" -#define KEY_HOST "host" -#define KEY_SECURE_NET "securenet" -#define KEY_TCP_PORT "tcpport" -#define KEY_COLS "cols" -#define KEY_ROWS "rows" -#define KEY_SCROLLBACK "scrollback" -#define KEY_SCHEME "scheme" -#define KEY_ECHO "localecho" -#define KEY_ANSWERBACK "answerback" - -#define DECIMAL_BASE 10 - -// Entry sections are numbered from 1 in the file -#define ENTRY_FIRST 1 - -const PrefsChoiceT termTypeChoices[ConnTypeCountE] = { - { "Serial port", "serial", ConnSerialE }, - { "Modem", "modem", ConnModemE }, - { "Secure link", "secure", ConnSecureE }, - { "Telnet", "telnet", ConnTelnetE } -}; - -const PrefsChoiceT termPortChoices[TERM_PORT_CHOICE_COUNT] = { - { "COM1", "COM1", RS232_COM1 }, - { "COM2", "COM2", RS232_COM2 }, - { "COM3", "COM3", RS232_COM3 }, - { "COM4", "COM4", RS232_COM4 } -}; - -const PrefsChoiceT termSpeedChoices[TERM_SPEED_CHOICE_COUNT] = { - { "300", "300", 300 }, - { "1200", "1200", 1200 }, - { "2400", "2400", 2400 }, - { "4800", "4800", 4800 }, - { "9600", "9600", 9600 }, - { "19200", "19200", 19200 }, - { "38400", "38400", 38400 }, - { "57600", "57600", 57600 }, - { "115200", "115200", 115200 } -}; - -const PrefsChoiceT termDataChoices[TERM_DATA_CHOICE_COUNT] = { - { "5", "5", 5 }, - { "6", "6", 6 }, - { "7", "7", 7 }, - { "8", "8", 8 } -}; - -const PrefsChoiceT termParityChoices[TERM_PARITY_CHOICE_COUNT] = { - { "None", "N", 'N' }, - { "Even", "E", 'E' }, - { "Odd", "O", 'O' }, - { "Mark", "M", 'M' }, - { "Space", "S", 'S' } -}; - -const PrefsChoiceT termStopChoices[TERM_STOP_CHOICE_COUNT] = { - { "1", "1", 1 }, - { "2", "2", 2 } -}; - -const PrefsChoiceT termFlowChoices[TERM_FLOW_CHOICE_COUNT] = { - { "None", "none", RS232_HANDSHAKE_NONE }, - { "XON/XOFF", "xonxoff", RS232_HANDSHAKE_XONXOFF }, - { "RTS/CTS", "rtscts", RS232_HANDSHAKE_RTSCTS } -}; - -const PrefsChoiceT termSchemeChoices[SchemeCountE] = { - { "Standard", "standard", SchemeStandardE }, - { "Green screen", "green", SchemeGreenE }, - { "Amber screen", "amber", SchemeAmberE }, - { "Black on white", "light", SchemeLightE } -}; - -const PrefsChoiceT termProtoChoices[XferProtoCountE] = { - { "XMODEM", "xmodem", XferXmodemE }, - { "XMODEM-1K", "xmodem1k", XferXmodem1kE }, - { "YMODEM", "ymodem", XferYmodemE }, - { "ZMODEM", "zmodem", XferZmodemE } -}; - -const PrefsChoiceT termExistChoices[XferExistCountE] = { - { "Ask me", "ask", XferExistAskE }, - { "Keep both (rename the new)", "rename", XferExistRenameE }, - { "Continue a partial download", "resume", XferExistResumeE }, - { "Replace the old file", "overwrite", XferExistOverwriteE }, - { "Skip the file", "skip", XferExistSkipE } -}; - - -// ============================================================ -// Prototypes -// ============================================================ - -static int32_t clampInt32(int32_t value, int32_t lo, int32_t hi); -static int32_t getChoiceValue(PrefsHandleT *h, const char *sec, const char *key, const PrefsChoiceT *choices, int32_t count, int32_t defaultValue); -static void getText(PrefsHandleT *h, const char *sec, const char *key, char *out, int32_t outSize, const char *defaultVal); -static void loadEntry(PrefsHandleT *h, const char *sec, TermSessionT *s); -static void saveEntry(PrefsHandleT *h, const char *sec, const TermSessionT *s); -static void setChoice(PrefsHandleT *h, const char *sec, const char *key, const PrefsChoiceT *choices, int32_t count, int32_t value); -static int32_t validChoice(const PrefsChoiceT *choices, int32_t count, int32_t value, int32_t defaultValue); - - -static int32_t clampInt32(int32_t value, int32_t lo, int32_t hi) { - if (value < lo) { - return lo; - } - - if (value > hi) { - return hi; - } - - return value; -} - - -// The value named by the key, or defaultValue when the key is missing or -// names no choice. -static int32_t getChoiceValue(PrefsHandleT *h, const char *sec, const char *key, const PrefsChoiceT *choices, int32_t count, int32_t defaultValue) { - int32_t def = prefsChoiceIndexOfValue(choices, count, defaultValue); - - return choices[prefsGetChoice(h, sec, key, choices, count, choices[def < 0 ? 0 : def].iniName)].value; -} - - -static void getText(PrefsHandleT *h, const char *sec, const char *key, char *out, int32_t outSize, const char *defaultVal) { - snprintf(out, (size_t)outSize, "%s", prefsGetString(h, sec, key, defaultVal)); -} - - -static void loadEntry(PrefsHandleT *h, const char *sec, TermSessionT *s) { - TermSessionT d; - ConnConfigT *c = &s->conn; - - termSessionDefaults(&d); - getText(h, sec, KEY_NAME, s->name, sizeof(s->name), d.name); - c->type = getChoiceValue(h, sec, KEY_TYPE, termTypeChoices, ConnTypeCountE, d.conn.type); - c->comPort = getChoiceValue(h, sec, KEY_PORT, termPortChoices, TERM_PORT_CHOICE_COUNT, d.conn.comPort); - c->bps = getChoiceValue(h, sec, KEY_SPEED, termSpeedChoices, TERM_SPEED_CHOICE_COUNT, d.conn.bps); - c->dataBits = getChoiceValue(h, sec, KEY_DATA, termDataChoices, TERM_DATA_CHOICE_COUNT, d.conn.dataBits); - c->parity = getChoiceValue(h, sec, KEY_PARITY, termParityChoices, TERM_PARITY_CHOICE_COUNT, d.conn.parity); - c->stopBits = getChoiceValue(h, sec, KEY_STOP, termStopChoices, TERM_STOP_CHOICE_COUNT, d.conn.stopBits); - c->flow = getChoiceValue(h, sec, KEY_FLOW, termFlowChoices, TERM_FLOW_CHOICE_COUNT, d.conn.flow); - c->redialCount = prefsGetInt(h, sec, KEY_REDIAL, d.conn.redialCount); - c->redialDelay = prefsGetInt(h, sec, KEY_REDIAL_DELAY, d.conn.redialDelay); - c->tcpPort = prefsGetInt(h, sec, KEY_TCP_PORT, d.conn.tcpPort); - s->cols = prefsGetInt(h, sec, KEY_COLS, d.cols); - s->rows = prefsGetInt(h, sec, KEY_ROWS, d.rows); - s->scrollback = prefsGetInt(h, sec, KEY_SCROLLBACK, d.scrollback); - s->scheme = getChoiceValue(h, sec, KEY_SCHEME, termSchemeChoices, SchemeCountE, d.scheme); - - s->localEcho = prefsGetBool(h, sec, KEY_ECHO, d.localEcho); - s->askPassphrase = prefsGetBool(h, sec, KEY_ASK_PASS, d.askPassphrase); - s->secureNet = prefsGetBool(h, sec, KEY_SECURE_NET, d.secureNet); - - getText(h, sec, KEY_INIT, c->initString, sizeof(c->initString), d.conn.initString); - getText(h, sec, KEY_PREFIX, c->dialPrefix, sizeof(c->dialPrefix), d.conn.dialPrefix); - getText(h, sec, KEY_PHONE, c->phone, sizeof(c->phone), d.conn.phone); - getText(h, sec, KEY_PASS, c->passphrase, sizeof(c->passphrase), d.conn.passphrase); - getText(h, sec, KEY_HOST, c->host, sizeof(c->host), d.conn.host); - getText(h, sec, KEY_ANSWERBACK, s->answerback, sizeof(s->answerback), d.answerback); - termSessionClamp(s); -} - - -static void saveEntry(PrefsHandleT *h, const char *sec, const TermSessionT *s) { - const ConnConfigT *c = &s->conn; - - prefsSetString(h, sec, KEY_NAME, s->name); - setChoice(h, sec, KEY_TYPE, termTypeChoices, ConnTypeCountE, c->type); - setChoice(h, sec, KEY_PORT, termPortChoices, TERM_PORT_CHOICE_COUNT, c->comPort); - setChoice(h, sec, KEY_SPEED, termSpeedChoices, TERM_SPEED_CHOICE_COUNT, c->bps); - setChoice(h, sec, KEY_DATA, termDataChoices, TERM_DATA_CHOICE_COUNT, c->dataBits); - setChoice(h, sec, KEY_PARITY, termParityChoices, TERM_PARITY_CHOICE_COUNT, c->parity); - setChoice(h, sec, KEY_STOP, termStopChoices, TERM_STOP_CHOICE_COUNT, c->stopBits); - setChoice(h, sec, KEY_FLOW, termFlowChoices, TERM_FLOW_CHOICE_COUNT, c->flow); - prefsSetString(h, sec, KEY_INIT, c->initString); - prefsSetString(h, sec, KEY_PREFIX, c->dialPrefix); - prefsSetString(h, sec, KEY_PHONE, c->phone); - prefsSetInt(h, sec, KEY_REDIAL, c->redialCount); - prefsSetInt(h, sec, KEY_REDIAL_DELAY, c->redialDelay); - prefsSetBool(h, sec, KEY_ASK_PASS, s->askPassphrase); - prefsSetString(h, sec, KEY_PASS, s->askPassphrase ? "" : c->passphrase); - prefsSetString(h, sec, KEY_HOST, c->host); - prefsSetBool(h, sec, KEY_SECURE_NET, s->secureNet); - prefsSetInt(h, sec, KEY_TCP_PORT, c->tcpPort); - prefsSetInt(h, sec, KEY_COLS, s->cols); - prefsSetInt(h, sec, KEY_ROWS, s->rows); - prefsSetInt(h, sec, KEY_SCROLLBACK, s->scrollback); - setChoice(h, sec, KEY_SCHEME, termSchemeChoices, SchemeCountE, s->scheme); - prefsSetBool(h, sec, KEY_ECHO, s->localEcho); - prefsSetString(h, sec, KEY_ANSWERBACK, s->answerback); -} - - -static void setChoice(PrefsHandleT *h, const char *sec, const char *key, const PrefsChoiceT *choices, int32_t count, int32_t value) { - int32_t idx = prefsChoiceIndexOfValue(choices, count, value); - - prefsSetString(h, sec, key, choices[idx < 0 ? 0 : idx].iniName); -} - - -int32_t termBookAdd(TermBookT *book, const TermSessionT *s) { - if (book->count >= TERM_BOOK_MAX) { - return -1; - } - - book->entries[book->count] = *s; - termSessionClamp(&book->entries[book->count]); - return book->count++; -} - - -void termBookDefaults(TermBookT *book) { - memset(book, 0, sizeof(*book)); - termSessionDefaults(&book->entries[0]); - book->count = 1; - book->protocol = XferZmodemE; - book->exists = XferExistAskE; - book->autoZmodem = true; - snprintf(book->downloadDir, sizeof(book->downloadDir), "%s", TERM_DEFAULT_DIR); -} - - -int32_t termBookFindTelnet(const TermBookT *book, const TermSessionT *s) { - for (int32_t i = 0; i < book->count; i++) { - const ConnConfigT *c = &book->entries[i].conn; - - if (c->type == ConnTelnetE && c->tcpPort == s->conn.tcpPort && strcasecmp(c->host, s->conn.host) == 0) { - return i; - } - } - - return -1; -} - - -void termBookLoad(TermBookT *book, const char *path) { - PrefsHandleT *h = prefsLoad(path); - - termBookDefaults(book); - - if (!h) { - return; - } - - int32_t count = clampInt32(prefsGetInt(h, SEC_BOOK, KEY_ENTRIES, 0), 0, TERM_BOOK_MAX); - - for (int32_t i = 0; i < count; i++) { - char sec[SEC_ENTRY_MAX]; - - snprintf(sec, sizeof(sec), SEC_ENTRY_FMT, (int)(i + ENTRY_FIRST)); - loadEntry(h, sec, &book->entries[i]); - } - - if (count > 0) { - book->count = count; - } - - book->current = clampInt32(prefsGetInt(h, SEC_BOOK, KEY_CURRENT, 0), 0, book->count - 1); - book->protocol = getChoiceValue(h, SEC_TRANSFER, KEY_PROTOCOL, termProtoChoices, XferProtoCountE, book->protocol); - book->exists = getChoiceValue(h, SEC_TRANSFER, KEY_EXISTS, termExistChoices, XferExistCountE, book->exists); - book->autoZmodem = prefsGetBool(h, SEC_TRANSFER, KEY_AUTO_ZMODEM, book->autoZmodem); - book->escapeCtl = prefsGetBool(h, SEC_TRANSFER, KEY_ESCAPE_CTL, book->escapeCtl); - - book->resumeUploads = prefsGetBool(h, SEC_TRANSFER, KEY_RESUME, book->resumeUploads); - - getText(h, SEC_TRANSFER, KEY_DIRECTORY, book->downloadDir, sizeof(book->downloadDir), TERM_DEFAULT_DIR); - prefsClose(h); -} - - -void termBookRemove(TermBookT *book, int32_t index) { - if (index < 0 || index >= book->count || book->count <= 1) { - return; - } - - memmove(&book->entries[index], &book->entries[index + 1], (size_t)(book->count - index - 1) * sizeof(book->entries[0])); - book->count--; - book->current = clampInt32(book->current, 0, book->count - 1); -} - - -bool termBookSave(const TermBookT *book, const char *path) { - PrefsHandleT *h = prefsCreate(); - - if (!h) { - return false; - } - - prefsSetInt(h, SEC_BOOK, KEY_ENTRIES, book->count); - prefsSetInt(h, SEC_BOOK, KEY_CURRENT, book->current); - prefsSetString(h, SEC_TRANSFER, KEY_DIRECTORY, book->downloadDir); - setChoice(h, SEC_TRANSFER, KEY_PROTOCOL, termProtoChoices, XferProtoCountE, book->protocol); - setChoice(h, SEC_TRANSFER, KEY_EXISTS, termExistChoices, XferExistCountE, book->exists); - prefsSetBool(h, SEC_TRANSFER, KEY_AUTO_ZMODEM, book->autoZmodem); - prefsSetBool(h, SEC_TRANSFER, KEY_ESCAPE_CTL, book->escapeCtl); - prefsSetBool(h, SEC_TRANSFER, KEY_RESUME, book->resumeUploads); - - for (int32_t i = 0; i < book->count; i++) { - char sec[SEC_ENTRY_MAX]; - - snprintf(sec, sizeof(sec), SEC_ENTRY_FMT, (int)(i + ENTRY_FIRST)); - saveEntry(h, sec, &book->entries[i]); - } - - bool ok = prefsSaveAs(h, path); - - prefsClose(h); - return ok; -} - - -void termSessionClamp(TermSessionT *s) { - ConnConfigT *c = &s->conn; - TermSessionT d; - - termSessionDefaults(&d); - c->type = validChoice(termTypeChoices, ConnTypeCountE, c->type, d.conn.type); - c->comPort = validChoice(termPortChoices, TERM_PORT_CHOICE_COUNT, c->comPort, d.conn.comPort); - c->bps = validChoice(termSpeedChoices, TERM_SPEED_CHOICE_COUNT, c->bps, d.conn.bps); - c->dataBits = validChoice(termDataChoices, TERM_DATA_CHOICE_COUNT, c->dataBits, d.conn.dataBits); - c->parity = validChoice(termParityChoices, TERM_PARITY_CHOICE_COUNT, c->parity, d.conn.parity); - c->stopBits = validChoice(termStopChoices, TERM_STOP_CHOICE_COUNT, c->stopBits, d.conn.stopBits); - c->flow = validChoice(termFlowChoices, TERM_FLOW_CHOICE_COUNT, c->flow, d.conn.flow); - c->redialCount = clampInt32(c->redialCount, 0, TERM_REDIAL_MAX); - c->redialDelay = clampInt32(c->redialDelay, TERM_REDIAL_DELAY_MIN, TERM_REDIAL_DELAY_MAX); - c->tcpPort = clampInt32(c->tcpPort, TERM_TCP_PORT_MIN, TERM_TCP_PORT_MAX); - s->scheme = validChoice(termSchemeChoices, SchemeCountE, s->scheme, d.scheme); - s->cols = clampInt32(s->cols, TERM_COLS_MIN, TERM_COLS_MAX); - s->rows = clampInt32(s->rows, TERM_ROWS_MIN, TERM_ROWS_MAX); - s->scrollback = clampInt32(s->scrollback, TERM_SCROLLBACK_MIN, TERM_SCROLLBACK_MAX); - - if (s->name[0] == '\0') { - snprintf(s->name, sizeof(s->name), "%s", TERM_DEFAULT_NAME); - } -} - - -void termSessionDefaults(TermSessionT *s) { - memset(s, 0, sizeof(*s)); - connConfigDefaults(&s->conn); - snprintf(s->name, sizeof(s->name), "%s", TERM_DEFAULT_NAME); - s->cols = TERM_COLS_DEFAULT; - s->rows = TERM_ROWS_DEFAULT; - s->scrollback = TERM_SCROLLBACK_DEFAULT; - s->scheme = SchemeStandardE; -} - - -bool termSessionFromUrl(const char *url, TermSessionT *s) { - size_t schemeLen = strlen(TERM_TELNET_SCHEME); - TermSessionT t; - const char *host; - const char *colon; - size_t hostLen; - long port; - - if (!url || strncasecmp(url, TERM_TELNET_SCHEME, schemeLen) != 0) { - return false; - } - - termSessionDefaults(&t); - port = t.conn.tcpPort; - host = url + schemeLen; - colon = strchr(host, ':'); - hostLen = colon ? (size_t)(colon - host) : strcspn(host, "/"); - - if (hostLen == 0 || hostLen >= sizeof(t.conn.host)) { - return false; - } - - if (colon) { - char *end; - - port = strtol(colon + 1, &end, DECIMAL_BASE); - - if (end == colon + 1 || (*end != '\0' && *end != '/') || port < TERM_TCP_PORT_MIN || port > TERM_TCP_PORT_MAX) { - return false; - } - } - - t.conn.type = ConnTelnetE; - t.conn.tcpPort = (int32_t)port; - snprintf(t.conn.host, sizeof(t.conn.host), "%.*s", (int)hostLen, host); - snprintf(t.name, sizeof(t.name), "%s", t.conn.host); - *s = t; - return true; -} - - -// value when the table holds it, else defaultValue. -static int32_t validChoice(const PrefsChoiceT *choices, int32_t count, int32_t value, int32_t defaultValue) { - return prefsChoiceIndexOfValue(choices, count, value) >= 0 ? value : defaultValue; -} diff --git a/src/apps/kpunch/terminal/termSession.h b/src/apps/kpunch/terminal/termSession.h deleted file mode 100644 index 8d011766..00000000 --- a/src/apps/kpunch/terminal/termSession.h +++ /dev/null @@ -1,161 +0,0 @@ -// 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. - -// termSession.h -- Terminal sessions (the phone book) and TERMINAL.INI -// -// A session is a connection (ConnConfigT) plus how the screen looks while -// connected. The phone book is the list of saved sessions plus the file -// transfer settings. TERMINAL.INI: -// -// [phonebook] entries = N, current = index of the last used entry -// [transfer] directory, protocol, exists, autozmodem, escapectl, resume -// [entry1] .. one section per session (keys: see termSession.c) -// -// Every enumerated setting is stored by name through a PrefsChoiceT table -// that is also the source of the dialogs' dropdown labels. Loading -// clamps every value into range, so a hand-edited file cannot produce an -// invalid session. Saving writes a complete new file through prefsSaveAs, -// which replaces the old file only once the new one is fully written. - -#ifndef TERMSESSION_H -#define TERMSESSION_H - -#include "dvxPrefs.h" -#include "conn/conn.h" -#include "xfer/xfer.h" - -#include -#include - -#define TERM_INI_NAME "TERMINAL.INI" - -#define TERM_NAME_MAX 32 -#define TERM_ANSWERBACK_MAX 32 -#define TERM_DIR_MAX 260 -#define TERM_BOOK_MAX 32 - -// Screen geometry and scrollback limits -#define TERM_COLS_MIN 40 -#define TERM_COLS_MAX 132 -#define TERM_COLS_DEFAULT 80 -#define TERM_ROWS_MIN 10 -#define TERM_ROWS_MAX 60 -#define TERM_ROWS_DEFAULT 25 -#define TERM_SCROLLBACK_MIN 100 -#define TERM_SCROLLBACK_MAX 10000 -#define TERM_SCROLLBACK_DEFAULT 1000 - -// Modem dialing and Telnet limits -#define TERM_REDIAL_MAX 99 -#define TERM_REDIAL_DELAY_MIN 1 -#define TERM_REDIAL_DELAY_MAX 300 -#define TERM_TCP_PORT_MIN 1 -#define TERM_TCP_PORT_MAX 65535 - -#define TERM_DEFAULT_NAME "Direct COM1" -#define TERM_TELNET_SCHEME "telnet://" -#define TERM_DEFAULT_DIR "DOWNLOAD" - -typedef enum { - SchemeStandardE, - SchemeGreenE, - SchemeAmberE, - SchemeLightE, - SchemeCountE -} SchemeE; - -#define TERM_PORT_CHOICE_COUNT 4 -#define TERM_SPEED_CHOICE_COUNT 9 -#define TERM_DATA_CHOICE_COUNT 4 -#define TERM_PARITY_CHOICE_COUNT 5 -#define TERM_STOP_CHOICE_COUNT 2 -#define TERM_FLOW_CHOICE_COUNT 3 - -// Choice tables: dropdown label, INI spelling, value. Each is indexed in -// dropdown order. -extern const PrefsChoiceT termTypeChoices[ConnTypeCountE]; -extern const PrefsChoiceT termPortChoices[TERM_PORT_CHOICE_COUNT]; -extern const PrefsChoiceT termSpeedChoices[TERM_SPEED_CHOICE_COUNT]; -extern const PrefsChoiceT termDataChoices[TERM_DATA_CHOICE_COUNT]; -extern const PrefsChoiceT termParityChoices[TERM_PARITY_CHOICE_COUNT]; -extern const PrefsChoiceT termStopChoices[TERM_STOP_CHOICE_COUNT]; -extern const PrefsChoiceT termFlowChoices[TERM_FLOW_CHOICE_COUNT]; -extern const PrefsChoiceT termSchemeChoices[SchemeCountE]; -extern const PrefsChoiceT termProtoChoices[XferProtoCountE]; -extern const PrefsChoiceT termExistChoices[XferExistCountE]; - -typedef struct { - char name[TERM_NAME_MAX]; - ConnConfigT conn; - int32_t cols; - int32_t rows; - int32_t scrollback; - int32_t scheme; // SchemeE - bool askPassphrase; // secure link: ask on connect, never store it - bool secureNet; // secure link: over the network (conn.host, tcpPort) - bool localEcho; - char answerback[TERM_ANSWERBACK_MAX]; // reply to ENQ (empty = none) -} TermSessionT; - -typedef struct { - TermSessionT entries[TERM_BOOK_MAX]; - int32_t count; - int32_t current; // last used entry - int32_t protocol; // XferProtoE last used - int32_t exists; // XferExistE for downloads - bool autoZmodem; // start a ZMODEM download on its own - bool escapeCtl; // ZMODEM: escape every control character - bool resumeUploads; // ZMODEM: ask the receiver to continue partial files - char downloadDir[TERM_DIR_MAX]; -} TermBookT; - -// Append a copy of s. Returns its index, or -1 when the book is full. -int32_t termBookAdd(TermBookT *book, const TermSessionT *s); - -// A book with one default session and the default transfer settings. -void termBookDefaults(TermBookT *book); - -// Index of the Telnet entry for s's host and port, or -1. -int32_t termBookFindTelnet(const TermBookT *book, const TermSessionT *s); - -// Load the phone book from path. A missing or empty file gives the -// defaults. Every value is clamped into range. -void termBookLoad(TermBookT *book, const char *path); - -// Remove entry index (the book always keeps at least one entry). -void termBookRemove(TermBookT *book, int32_t index); - -// Write the phone book to path (safe save). A session that asks for its -// passphrase never has one written. -bool termBookSave(const TermBookT *book, const char *path); - -// Force every field of s into range (unknown choices become defaults). -void termSessionClamp(TermSessionT *s); - -// Default settings: direct serial on COM1, 9600 8N1, no flow control. -void termSessionDefaults(TermSessionT *s); - -// A Telnet session from "telnet://host[:port]" (port 23 when left out), -// named after the host. False when url is not such a text. -bool termSessionFromUrl(const char *url, TermSessionT *s); - -#endif diff --git a/src/apps/kpunch/terminal/termXfer.frm b/src/apps/kpunch/terminal/termXfer.frm new file mode 100644 index 00000000..ec7c4b34 --- /dev/null +++ b/src/apps/kpunch/terminal/termXfer.frm @@ -0,0 +1,123 @@ +VERSION DVX 1.00 +Begin Form XferOptsForm + Caption = "Transfer Options" + Layout = VBox + AutoSize = True + Resizable = False + Centered = True + Begin HBox rowDir + Begin Label lblDir + Caption = "Download folder:" + MinWidth = 128 + End + Begin TextBox txtDir + MinWidth = 224 + Weight = 1 + End + End + Begin HBox rowExists + Begin Label lblExists + Caption = "If a file exists:" + MinWidth = 128 + End + Begin DropDown ddExists + MinWidth = 224 + Weight = 1 + End + End + Begin CheckBox chkAutoZmodem + Caption = "Start ZMODEM downloads automatically" + End + Begin CheckBox chkResume + Caption = "Ask the other side to continue interrupted uploads (ZMODEM)" + End + Begin CheckBox chkEscape + Caption = "Escape all control characters (for links that lose them)" + End + Begin HBox boxButtons + Alignment = Right + Begin CommandButton cmdXferOk + Caption = "&OK" + Default = True + MinWidth = 80 + MinHeight = 24 + End + Begin CommandButton cmdXferCancel + Caption = "&Cancel" + Cancel = True + MinWidth = 80 + MinHeight = 24 + End + End +End + + +' 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. + + +' termXfer.frm -- Terminal's Transfer Options dialog: the download +' folder, what happens to a download that already exists, and the ZMODEM +' settings (start downloads automatically, ask to continue uploads, +' escape every control character). OK copies them into the phone book's +' transfer settings (terminal.bas) and sets dlgAccepted; the caller saves +' them. An empty folder means the default one. + +OPTION EXPLICIT + + +' Show the dialog (modal). True when the user pressed OK. +FUNCTION XferOptionsDialog() AS BOOLEAN + Load XferOptsForm + dlgAccepted = False + + txtDir.MaxLength = TERM_DIR_MAX + txtDir.Text = downloadDir + FillChoices ddExists, EXISTS_LABELS, xferExists + chkAutoZmodem.Value = autoZmodem + chkResume.Value = resumeUploads + chkEscape.Value = escapeCtl + + XferOptsForm.Show vbModal + XferOptionsDialog = dlgAccepted +END FUNCTION + + +SUB cmdXferCancel_Click + Unload XferOptsForm +END SUB + + +SUB cmdXferOk_Click + downloadDir = txtDir.Text + + IF downloadDir = "" THEN + downloadDir = TERM_DEFAULT_DIR + END IF + + xferExists = ChosenIndex(ddExists) + autoZmodem = chkAutoZmodem.Value + resumeUploads = chkResume.Value + escapeCtl = chkEscape.Value + dlgAccepted = True + Unload XferOptsForm +END SUB diff --git a/src/apps/kpunch/terminal/terminal.bas b/src/apps/kpunch/terminal/terminal.bas new file mode 100644 index 00000000..50a7af7f --- /dev/null +++ b/src/apps/kpunch/terminal/terminal.bas @@ -0,0 +1,716 @@ +' 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. + +' terminal.bas -- Terminal: the phone book, and what the windows share. +' +' Terminal is a thin user interface over shared libraries: +' conn.bas the connection -- serial port, modem dialer, secure link, +' Telnet -- which also feeds the Terminal control (capture, +' answerback, ZMODEM start) and runs the file transfers +' xfer.bas the progress of XMODEM, XMODEM-1K, YMODEM and ZMODEM +' transfers +' prefs.bas TERMINAL.INI +' This module keeps the phone book -- the saved sessions and the transfer +' settings -- and the state the windows share; the main window +' (terminal.frm) runs the connection. +' +' TERMINAL.INI, in Terminal's settings folder: +' [phonebook] entries = N, current = index of the last used entry +' [transfer] directory, protocol, exists, autozmodem, escapectl, resume +' [entry1] .. one section per session (keys: LoadEntry, SaveEntry) +' Every choice is stored by name: the *_NAMES lists below, in the order of +' their dropdowns (and of their CONN_ / XFER_ / SCHEME_ numbers). Loading +' forces every value into range, so a hand-edited file cannot make an +' invalid session; saving writes a whole new file, which replaces the old +' one only once it is completely written. + +OPTION EXPLICIT + +CONST TERM_TITLE = "Terminal" +CONST TERM_INI_NAME = "TERMINAL.INI" + +' The most characters each text holds +CONST TERM_NAME_MAX = 31 +CONST TERM_ANSWERBACK_MAX = 31 +CONST TERM_DIR_MAX = 259 +CONST TERM_INIT_MAX = 63 +CONST TERM_PREFIX_MAX = 15 +CONST TERM_PHONE_MAX = 47 +CONST TERM_PASS_MAX = 63 +CONST TERM_HOST_MAX = 63 + +' Phone book entries +CONST TERM_BOOK_MAX = 32 + +' Screen geometry and scrollback limits +CONST TERM_COLS_MIN = 40 +CONST TERM_COLS_MAX = 132 +CONST TERM_COLS_DEFAULT = 80 +CONST TERM_ROWS_MIN = 10 +CONST TERM_ROWS_MAX = 60 +CONST TERM_ROWS_DEFAULT = 25 +CONST TERM_SCROLLBACK_MIN = 100 +CONST TERM_SCROLLBACK_MAX = 10000 +CONST TERM_SCROLLBACK_DEFAULT = 1000 + +' Modem dialing and network limits +CONST TERM_REDIAL_MAX = 99 +CONST TERM_REDIAL_DELAY_MIN = 1 +CONST TERM_REDIAL_DELAY_MAX = 300 +CONST TERM_TCP_PORT_MIN = 1 +CONST TERM_TCP_PORT_MAX = 65535 + +' A new session: a direct serial connection on COM1, 9600 bps 8N1, no +' flow control, the usual modem commands +CONST TERM_DEFAULT_NAME = "Direct COM1" +CONST TERM_DEFAULT_PORT = 1 +CONST TERM_DEFAULT_BPS = 9600 +CONST TERM_DEFAULT_DATA = 8 +CONST TERM_DEFAULT_PARITY = "N" +CONST TERM_DEFAULT_STOP = 1 +CONST TERM_DEFAULT_REDIAL = 3 +CONST TERM_DEFAULT_REDIAL_DELAY = 30 +CONST TERM_DEFAULT_TCP_PORT = 23 +CONST TERM_DEFAULT_INIT = "ATZ" +CONST TERM_DEFAULT_PREFIX = "ATDT" +CONST TERM_DEFAULT_DIR = "DOWNLOAD" + +' Started with "telnet://host[:port]" +CONST TERM_TELNET_SCHEME = "telnet://" + +' Files to send: one path per line (ConnSendFiles) +CONST FILE_LIST_SEP = CHR$(10) + +' 1234567 -> "1,234,567" +CONST BYTES_FORMAT = "#,##0" + +CONST QUOTE = CHR$(34) + +' A password box shows this character (a small bullet) for each one typed +CONST PASSWORD_MASK = 249 + +' Color schemes (Settings > Colors) +CONST SCHEME_STANDARD = 0 +CONST SCHEME_GREEN = 1 +CONST SCHEME_AMBER = 2 +CONST SCHEME_LIGHT = 3 + +' Choice lists: dropdown labels and TERMINAL.INI names, one per choice, +' separated by CHOICE_SEP. The port, speed, data bits, parity and stop +' bits lists are their own labels; a port's COM number is its index + 1, +' a speed / data bits / stop bits value is the number itself and a parity +' the letter. The other values are the index (CONN_ types, CONN_FLOW_, +' SCHEME_, XFER_ protocols, XFER_EXIST_). +CONST CHOICE_SEP = "|" +CONST TYPE_LABELS = "Serial port|Modem|Secure link|Telnet" +CONST TYPE_NAMES = "serial|modem|secure|telnet" +CONST PORT_NAMES = "COM1|COM2|COM3|COM4" +CONST SPEED_NAMES = "300|1200|2400|4800|9600|19200|38400|57600|115200" +CONST DATA_NAMES = "5|6|7|8" +CONST PARITY_LABELS = "None|Even|Odd|Mark|Space" +CONST PARITY_NAMES = "N|E|O|M|S" +CONST STOP_NAMES = "1|2" +CONST FLOW_LABELS = "None|XON/XOFF|RTS/CTS" +CONST FLOW_NAMES = "none|xonxoff|rtscts" +CONST SCHEME_LABELS = "Standard|Green screen|Amber screen|Black on white" +CONST SCHEME_NAMES = "standard|green|amber|light" +CONST PROTO_LABELS = "XMODEM|XMODEM-1K|YMODEM|ZMODEM" +CONST PROTO_NAMES = "xmodem|xmodem1k|ymodem|zmodem" +CONST EXISTS_LABELS = "Ask me|Keep both (rename the new)|Continue a partial download|Replace the old file|Skip the file" +CONST EXISTS_NAMES = "ask|rename|resume|overwrite|skip" + +' TERMINAL.INI sections and keys +CONST SEC_BOOK = "phonebook" +CONST SEC_TRANSFER = "transfer" +CONST SEC_ENTRY = "entry" ' entry1, entry2, ... (numbered from 1) +CONST KEY_ENTRIES = "entries" +CONST KEY_CURRENT = "current" +CONST KEY_DIRECTORY = "directory" +CONST KEY_PROTOCOL = "protocol" +CONST KEY_EXISTS = "exists" +CONST KEY_AUTO_ZMODEM = "autozmodem" +CONST KEY_ESCAPE_CTL = "escapectl" +CONST KEY_RESUME = "resume" +CONST KEY_NAME = "name" +CONST KEY_TYPE = "type" +CONST KEY_PORT = "port" +CONST KEY_SPEED = "speed" +CONST KEY_DATA = "databits" +CONST KEY_PARITY = "parity" +CONST KEY_STOP = "stopbits" +CONST KEY_FLOW = "flow" +CONST KEY_INIT = "init" +CONST KEY_PREFIX = "dialprefix" +CONST KEY_PHONE = "phone" +CONST KEY_REDIAL = "redial" +CONST KEY_REDIAL_DELAY = "redialdelay" +CONST KEY_ASK_PASS = "askpassphrase" +CONST KEY_PASS = "passphrase" +CONST KEY_HOST = "host" +CONST KEY_SECURE_NET = "securenet" +CONST KEY_TCP_PORT = "tcpport" +CONST KEY_COLS = "cols" +CONST KEY_ROWS = "rows" +CONST KEY_SCROLLBACK = "scrollback" +CONST KEY_SCHEME = "scheme" +CONST KEY_ECHO = "localecho" +CONST KEY_ANSWERBACK = "answerback" + +' A session: a connection plus how the screen looks while connected +TYPE TermSession + sessionName AS STRING + connType AS LONG ' CONN_SERIAL ... CONN_TELNET + comPort AS LONG ' 1 - 4 + bps AS LONG + dataBits AS LONG + parity AS STRING ' "N", "E", "O", "M" or "S" + stopBits AS LONG + flow AS LONG ' CONN_FLOW_ + redialCount AS LONG ' modem: extra dial attempts + redialDelay AS LONG ' modem: seconds between attempts + tcpPort AS LONG ' Telnet, secure link over the network + initString AS STRING ' modem: sent first ("" = none) + dialPrefix AS STRING ' modem: e.g. ATDT + phone AS STRING ' modem: "" = no dialing + passphrase AS STRING ' secure link ("" = none) + host AS STRING ' Telnet; secure link over the network + cols AS LONG + rows AS LONG + scrollback AS LONG + scheme AS LONG ' SCHEME_ + askPassphrase AS BOOLEAN ' secure link: ask on connect, never store it + secureNet AS BOOLEAN ' secure link: over the network (host, tcpPort) + localEcho AS BOOLEAN + answerback AS STRING ' reply to ENQ ("" = none) +END TYPE + +' The phone book and the transfer settings +DIM SHARED book(TERM_BOOK_MAX - 1) AS TermSession +DIM SHARED bookCount AS LONG +DIM SHARED bookCurrent AS LONG ' the last used entry +DIM SHARED xferProto AS LONG ' XFER_ protocol last used +DIM SHARED xferExists AS LONG ' XFER_EXIST_ for downloads +DIM SHARED autoZmodem AS BOOLEAN ' start a ZMODEM download on its own +DIM SHARED escapeCtl AS BOOLEAN ' ZMODEM: escape every control character +DIM SHARED resumeUploads AS BOOLEAN ' ZMODEM: ask the receiver to continue partial files +DIM SHARED downloadDir AS STRING +DIM SHARED iniPath AS STRING ' "" when the settings folder cannot be made + +' The main window's session (a copy of its phone book entry) and its +' connection (conn.bas handle). They live here, not in the window's +' code, because the window starts from termMain.bas's module-level code. +DIM SHARED session AS TermSession +DIM SHARED conn AS LONG +DIM SHARED statusSerial AS LONG ' the connection status shown last +DIM SHARED lastState AS LONG ' the connection state the menus follow +DIM SHARED capturing AS BOOLEAN ' received bytes go to a capture file + +' The file transfer on the connection (xfer.bas handle, 0 = none) and +' whether it still runs; whether its window is open +DIM SHARED xfer AS LONG +DIM SHARED transferring AS BOOLEAN +DIM SHARED progressShown AS BOOLEAN + +' The dialogs' answers: the Session Settings dialog edits dlgSession and +' OK sets dlgAccepted (the other dialogs too); the phone book's Connect +' leaves the chosen entry in bookChoice, the passphrase dialog the text in +' passText (until PassphraseDialog takes it) +DIM SHARED dlgSession AS TermSession +DIM SHARED dlgAccepted AS BOOLEAN +DIM SHARED bookChoice AS LONG +DIM SHARED passText AS STRING + + +' ============================================================ +' Helpers +' ============================================================ + +' A byte count with thousands separators ("1,234,567"). +FUNCTION ByteText$(BYVAL n AS LONG) + ByteText$ = LTrim$(Format$(n, BYTES_FORMAT)) +END FUNCTION + + +' Index of name in a CHOICE_SEP list (any case), or -1. +FUNCTION ChoiceIndex(BYVAL names AS STRING, BYVAL choiceName AS STRING) AS LONG + DIM items AS VARIANT + DIM i AS LONG + + items = Split(names, CHOICE_SEP) + + FOR i = 0 TO UBound(items) + IF LCase$(items(i)) = LCase$(choiceName) THEN + ChoiceIndex = i + EXIT FUNCTION + END IF + NEXT i + + ChoiceIndex = -1 +END FUNCTION + + +' The index-th name of a CHOICE_SEP list. +FUNCTION ChoiceName$(BYVAL names AS STRING, BYVAL index AS LONG) + ChoiceName$ = Split(names, CHOICE_SEP)(index) +END FUNCTION + + +' The index chosen in a dropdown (the first when none is). +FUNCTION ChosenIndex(BYVAL dd AS Control) AS LONG + IF dd.ListIndex < 0 THEN + ChosenIndex = 0 + ELSE + ChosenIndex = dd.ListIndex + END IF +END FUNCTION + + +' value when it is in lo..hi, else the nearer end. +FUNCTION ClampLong(BYVAL value AS LONG, BYVAL lo AS LONG, BYVAL hi AS LONG) AS LONG + IF value < lo THEN + ClampLong = lo + ELSEIF value > hi THEN + ClampLong = hi + ELSE + ClampLong = value + END IF +END FUNCTION + + +' A dropdown with the CHOICE_SEP list labels, index chosen (the first +' when index is not in the list). +SUB FillChoices(BYVAL dd AS Control, BYVAL labels AS STRING, BYVAL index AS LONG) + DIM item AS VARIANT + + dd.Clear + + FOR EACH item IN Split(labels, CHOICE_SEP) + dd.AddItem item + NEXT item + + IF index < 0 THEN + index = 0 + END IF + + dd.ListIndex = index +END SUB + + +' The list's choice saved under key, or defaultIndex when the key is +' missing or names no choice. +FUNCTION GetChoice(BYVAL ini AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL names AS STRING, BYVAL defaultIndex AS LONG) AS LONG + DIM index AS LONG + + index = ChoiceIndex(names, PrefsGet$(ini, section, key, "")) + + IF index < 0 THEN + index = defaultIndex + END IF + + GetChoice = index +END FUNCTION + + +' A spinner from lo to hi in steps of one, at value. +SUB SetSpinner(BYVAL spin AS Control, BYVAL lo AS LONG, BYVAL hi AS LONG, BYVAL value AS LONG) + spin.SetRange lo, hi + spin.SetStep 1 + spin.Value = value +END SUB + + +' ============================================================ +' Sessions +' ============================================================ + +' Force every field of s into range: a value its list does not have +' becomes the default, numbers are clamped, texts cut to their length, +' and an empty name becomes the default name. +SUB SessionClamp(s AS TermSession) + DIM d AS TermSession + + SessionDefaults d + + IF s.connType < CONN_SERIAL OR s.connType > CONN_TELNET THEN + s.connType = d.connType + END IF + + IF s.comPort < 1 OR s.comPort > UBound(Split(PORT_NAMES, CHOICE_SEP)) + 1 THEN + s.comPort = d.comPort + END IF + + IF ChoiceIndex(SPEED_NAMES, CStr(s.bps)) < 0 THEN + s.bps = d.bps + END IF + + IF ChoiceIndex(DATA_NAMES, CStr(s.dataBits)) < 0 THEN + s.dataBits = d.dataBits + END IF + + IF s.parity = "" OR ChoiceIndex(PARITY_NAMES, s.parity) < 0 THEN + s.parity = d.parity + END IF + + IF ChoiceIndex(STOP_NAMES, CStr(s.stopBits)) < 0 THEN + s.stopBits = d.stopBits + END IF + + IF s.flow < CONN_FLOW_NONE OR s.flow > CONN_FLOW_RTSCTS THEN + s.flow = d.flow + END IF + + IF s.scheme < SCHEME_STANDARD OR s.scheme > SCHEME_LIGHT THEN + s.scheme = d.scheme + END IF + + s.redialCount = ClampLong(s.redialCount, 0, TERM_REDIAL_MAX) + s.redialDelay = ClampLong(s.redialDelay, TERM_REDIAL_DELAY_MIN, TERM_REDIAL_DELAY_MAX) + s.tcpPort = ClampLong(s.tcpPort, TERM_TCP_PORT_MIN, TERM_TCP_PORT_MAX) + s.cols = ClampLong(s.cols, TERM_COLS_MIN, TERM_COLS_MAX) + s.rows = ClampLong(s.rows, TERM_ROWS_MIN, TERM_ROWS_MAX) + s.scrollback = ClampLong(s.scrollback, TERM_SCROLLBACK_MIN, TERM_SCROLLBACK_MAX) + + s.sessionName = Left$(s.sessionName, TERM_NAME_MAX) + s.initString = Left$(s.initString, TERM_INIT_MAX) + s.dialPrefix = Left$(s.dialPrefix, TERM_PREFIX_MAX) + s.phone = Left$(s.phone, TERM_PHONE_MAX) + s.passphrase = Left$(s.passphrase, TERM_PASS_MAX) + s.host = Left$(s.host, TERM_HOST_MAX) + s.answerback = Left$(s.answerback, TERM_ANSWERBACK_MAX) + + IF s.sessionName = "" THEN + s.sessionName = TERM_DEFAULT_NAME + END IF +END SUB + + +' A new session (see TERM_DEFAULT_). +SUB SessionDefaults(s AS TermSession) + s.sessionName = TERM_DEFAULT_NAME + s.connType = CONN_SERIAL + s.comPort = TERM_DEFAULT_PORT + s.bps = TERM_DEFAULT_BPS + s.dataBits = TERM_DEFAULT_DATA + s.parity = TERM_DEFAULT_PARITY + s.stopBits = TERM_DEFAULT_STOP + s.flow = CONN_FLOW_NONE + s.redialCount = TERM_DEFAULT_REDIAL + s.redialDelay = TERM_DEFAULT_REDIAL_DELAY + s.tcpPort = TERM_DEFAULT_TCP_PORT + s.initString = TERM_DEFAULT_INIT + s.dialPrefix = TERM_DEFAULT_PREFIX + s.phone = "" + s.passphrase = "" + s.host = "" + s.cols = TERM_COLS_DEFAULT + s.rows = TERM_ROWS_DEFAULT + s.scrollback = TERM_SCROLLBACK_DEFAULT + s.scheme = SCHEME_STANDARD + s.askPassphrase = False + s.secureNet = False + s.localEcho = False + s.answerback = "" +END SUB + + +' A Telnet session from "telnet://host[:port]" (port 23 when left out), +' named after the host. False when url is not such a text. +FUNCTION SessionFromUrl(BYVAL url AS STRING, s AS TermSession) AS BOOLEAN + DIM t AS TermSession + DIM rest AS STRING + DIM colon AS LONG + DIM slash AS LONG + DIM hostLen AS LONG + DIM portText AS STRING + DIM first AS LONG + DIM i AS LONG + + SessionFromUrl = False + + IF LCase$(Left$(url, Len(TERM_TELNET_SCHEME))) <> TERM_TELNET_SCHEME THEN + EXIT FUNCTION + END IF + + SessionDefaults t + rest = Mid$(url, Len(TERM_TELNET_SCHEME) + 1) + colon = InStr(rest, ":") + + ' The host ends at the colon, else at the first slash + IF colon > 0 THEN + hostLen = colon - 1 + ELSE + slash = InStr(rest, "/") + + IF slash > 0 THEN + hostLen = slash - 1 + ELSE + hostLen = Len(rest) + END IF + END IF + + IF hostLen = 0 OR hostLen > TERM_HOST_MAX THEN + EXIT FUNCTION + END IF + + ' The port: blanks, an optional sign and decimal digits, then the end + ' or a slash + IF colon > 0 THEN + portText = LTrim$(Mid$(rest, colon + 1)) + slash = InStr(portText, "/") + + IF slash > 0 THEN + portText = Left$(portText, slash - 1) + END IF + + first = 1 + + IF Left$(portText, 1) = "+" OR Left$(portText, 1) = "-" THEN + first = 2 + END IF + + IF first > Len(portText) THEN + EXIT FUNCTION + END IF + + FOR i = first TO Len(portText) + IF Mid$(portText, i, 1) < "0" OR Mid$(portText, i, 1) > "9" THEN + EXIT FUNCTION + END IF + NEXT i + + IF Val(portText) < TERM_TCP_PORT_MIN OR Val(portText) > TERM_TCP_PORT_MAX THEN + EXIT FUNCTION + END IF + + t.tcpPort = Val(portText) + END IF + + t.connType = CONN_TELNET + t.host = Left$(rest, hostLen) + t.sessionName = Left$(t.host, TERM_NAME_MAX) + s = t + SessionFromUrl = True +END FUNCTION + + +' ============================================================ +' The phone book +' ============================================================ + +' Append a copy of s (clamped). Its index, or -1 when the book is full. +FUNCTION BookAdd(s AS TermSession) AS LONG + IF bookCount >= TERM_BOOK_MAX THEN + BookAdd = -1 + EXIT FUNCTION + END IF + + book(bookCount) = s + SessionClamp book(bookCount) + bookCount = bookCount + 1 + BookAdd = bookCount - 1 +END FUNCTION + + +' One default session and the default transfer settings. +SUB BookDefaults + SessionDefaults book(0) + bookCount = 1 + bookCurrent = 0 + xferProto = XFER_ZMODEM + xferExists = XFER_EXIST_ASK + autoZmodem = True + escapeCtl = False + resumeUploads = False + downloadDir = TERM_DEFAULT_DIR +END SUB + + +' Index of the Telnet entry for s's host and port, or -1. +FUNCTION BookFindTelnet(s AS TermSession) AS LONG + DIM i AS LONG + + FOR i = 0 TO bookCount - 1 + IF book(i).connType = CONN_TELNET AND book(i).tcpPort = s.tcpPort AND LCase$(book(i).host) = LCase$(s.host) THEN + BookFindTelnet = i + EXIT FUNCTION + END IF + NEXT i + + BookFindTelnet = -1 +END FUNCTION + + +' Load the phone book from TERMINAL.INI. A missing or empty file gives +' the defaults; every value is forced into range. +SUB BookLoad + DIM ini AS LONG + DIM count AS LONG + DIM i AS LONG + + BookDefaults + + IF iniPath = "" THEN + EXIT SUB + END IF + + ini = PrefsLoad(iniPath) + + IF ini = 0 THEN + EXIT SUB + END IF + + count = ClampLong(PrefsGetInt(ini, SEC_BOOK, KEY_ENTRIES, 0), 0, TERM_BOOK_MAX) + + FOR i = 0 TO count - 1 + LoadEntry ini, SEC_ENTRY & CStr(i + 1), book(i) + NEXT i + + IF count > 0 THEN + bookCount = count + END IF + + bookCurrent = ClampLong(PrefsGetInt(ini, SEC_BOOK, KEY_CURRENT, 0), 0, bookCount - 1) + xferProto = GetChoice(ini, SEC_TRANSFER, KEY_PROTOCOL, PROTO_NAMES, xferProto) + xferExists = GetChoice(ini, SEC_TRANSFER, KEY_EXISTS, EXISTS_NAMES, xferExists) + autoZmodem = PrefsGetBool(ini, SEC_TRANSFER, KEY_AUTO_ZMODEM, autoZmodem) + escapeCtl = PrefsGetBool(ini, SEC_TRANSFER, KEY_ESCAPE_CTL, escapeCtl) + resumeUploads = PrefsGetBool(ini, SEC_TRANSFER, KEY_RESUME, resumeUploads) + downloadDir = Left$(PrefsGet$(ini, SEC_TRANSFER, KEY_DIRECTORY, TERM_DEFAULT_DIR), TERM_DIR_MAX) + PrefsClose ini +END SUB + + +' Remove entry index (the book always keeps at least one entry). +SUB BookRemove(BYVAL index AS LONG) + DIM i AS LONG + + IF index < 0 OR index >= bookCount OR bookCount <= 1 THEN + EXIT SUB + END IF + + FOR i = index TO bookCount - 2 + book(i) = book(i + 1) + NEXT i + + bookCount = bookCount - 1 + bookCurrent = ClampLong(bookCurrent, 0, bookCount - 1) +END SUB + + +' Write the phone book to TERMINAL.INI (a whole new file). A session +' that asks for its passphrase never has one written. False when it could +' not be saved. +FUNCTION BookSave() AS BOOLEAN + DIM ini AS LONG + DIM i AS LONG + + ini = PrefsCreate() + + IF ini = 0 THEN + BookSave = False + EXIT FUNCTION + END IF + + PrefsSetInt ini, SEC_BOOK, KEY_ENTRIES, bookCount + PrefsSetInt ini, SEC_BOOK, KEY_CURRENT, bookCurrent + PrefsSet ini, SEC_TRANSFER, KEY_DIRECTORY, downloadDir + PrefsSet ini, SEC_TRANSFER, KEY_PROTOCOL, ChoiceName$(PROTO_NAMES, xferProto) + PrefsSet ini, SEC_TRANSFER, KEY_EXISTS, ChoiceName$(EXISTS_NAMES, xferExists) + PrefsSetBool ini, SEC_TRANSFER, KEY_AUTO_ZMODEM, autoZmodem + PrefsSetBool ini, SEC_TRANSFER, KEY_ESCAPE_CTL, escapeCtl + PrefsSetBool ini, SEC_TRANSFER, KEY_RESUME, resumeUploads + + FOR i = 0 TO bookCount - 1 + SaveEntry ini, SEC_ENTRY & CStr(i + 1), book(i) + NEXT i + + BookSave = PrefsSaveAs(ini, iniPath) + PrefsClose ini +END FUNCTION + + +' One session from its [entryN] section; a missing key keeps the default. +SUB LoadEntry(BYVAL ini AS LONG, BYVAL sec AS STRING, s AS TermSession) + DIM d AS TermSession + + SessionDefaults d + s.sessionName = PrefsGet$(ini, sec, KEY_NAME, d.sessionName) + s.connType = GetChoice(ini, sec, KEY_TYPE, TYPE_NAMES, d.connType) + s.comPort = GetChoice(ini, sec, KEY_PORT, PORT_NAMES, d.comPort - 1) + 1 + s.bps = Val(ChoiceName$(SPEED_NAMES, GetChoice(ini, sec, KEY_SPEED, SPEED_NAMES, ChoiceIndex(SPEED_NAMES, CStr(d.bps))))) + s.dataBits = Val(ChoiceName$(DATA_NAMES, GetChoice(ini, sec, KEY_DATA, DATA_NAMES, ChoiceIndex(DATA_NAMES, CStr(d.dataBits))))) + s.parity = ChoiceName$(PARITY_NAMES, GetChoice(ini, sec, KEY_PARITY, PARITY_NAMES, ChoiceIndex(PARITY_NAMES, d.parity))) + s.stopBits = Val(ChoiceName$(STOP_NAMES, GetChoice(ini, sec, KEY_STOP, STOP_NAMES, ChoiceIndex(STOP_NAMES, CStr(d.stopBits))))) + s.flow = GetChoice(ini, sec, KEY_FLOW, FLOW_NAMES, d.flow) + s.redialCount = PrefsGetInt(ini, sec, KEY_REDIAL, d.redialCount) + s.redialDelay = PrefsGetInt(ini, sec, KEY_REDIAL_DELAY, d.redialDelay) + s.tcpPort = PrefsGetInt(ini, sec, KEY_TCP_PORT, d.tcpPort) + s.cols = PrefsGetInt(ini, sec, KEY_COLS, d.cols) + s.rows = PrefsGetInt(ini, sec, KEY_ROWS, d.rows) + s.scrollback = PrefsGetInt(ini, sec, KEY_SCROLLBACK, d.scrollback) + s.scheme = GetChoice(ini, sec, KEY_SCHEME, SCHEME_NAMES, d.scheme) + s.localEcho = PrefsGetBool(ini, sec, KEY_ECHO, d.localEcho) + s.askPassphrase = PrefsGetBool(ini, sec, KEY_ASK_PASS, d.askPassphrase) + s.secureNet = PrefsGetBool(ini, sec, KEY_SECURE_NET, d.secureNet) + s.initString = PrefsGet$(ini, sec, KEY_INIT, d.initString) + s.dialPrefix = PrefsGet$(ini, sec, KEY_PREFIX, d.dialPrefix) + s.phone = PrefsGet$(ini, sec, KEY_PHONE, d.phone) + s.passphrase = PrefsGet$(ini, sec, KEY_PASS, d.passphrase) + s.host = PrefsGet$(ini, sec, KEY_HOST, d.host) + s.answerback = PrefsGet$(ini, sec, KEY_ANSWERBACK, d.answerback) + SessionClamp s +END SUB + + +' One session into its [entryN] section. +SUB SaveEntry(BYVAL ini AS LONG, BYVAL sec AS STRING, s AS TermSession) + PrefsSet ini, sec, KEY_NAME, s.sessionName + PrefsSet ini, sec, KEY_TYPE, ChoiceName$(TYPE_NAMES, s.connType) + PrefsSet ini, sec, KEY_PORT, ChoiceName$(PORT_NAMES, s.comPort - 1) + PrefsSet ini, sec, KEY_SPEED, CStr(s.bps) + PrefsSet ini, sec, KEY_DATA, CStr(s.dataBits) + PrefsSet ini, sec, KEY_PARITY, s.parity + PrefsSet ini, sec, KEY_STOP, CStr(s.stopBits) + PrefsSet ini, sec, KEY_FLOW, ChoiceName$(FLOW_NAMES, s.flow) + PrefsSet ini, sec, KEY_INIT, s.initString + PrefsSet ini, sec, KEY_PREFIX, s.dialPrefix + PrefsSet ini, sec, KEY_PHONE, s.phone + PrefsSetInt ini, sec, KEY_REDIAL, s.redialCount + PrefsSetInt ini, sec, KEY_REDIAL_DELAY, s.redialDelay + PrefsSetBool ini, sec, KEY_ASK_PASS, s.askPassphrase + + IF s.askPassphrase THEN + PrefsSet ini, sec, KEY_PASS, "" + ELSE + PrefsSet ini, sec, KEY_PASS, s.passphrase + END IF + + PrefsSet ini, sec, KEY_HOST, s.host + PrefsSetBool ini, sec, KEY_SECURE_NET, s.secureNet + PrefsSetInt ini, sec, KEY_TCP_PORT, s.tcpPort + PrefsSetInt ini, sec, KEY_COLS, s.cols + PrefsSetInt ini, sec, KEY_ROWS, s.rows + PrefsSetInt ini, sec, KEY_SCROLLBACK, s.scrollback + PrefsSet ini, sec, KEY_SCHEME, ChoiceName$(SCHEME_NAMES, s.scheme) + PrefsSetBool ini, sec, KEY_ECHO, s.localEcho + PrefsSet ini, sec, KEY_ANSWERBACK, s.answerback +END SUB diff --git a/src/apps/kpunch/terminal/terminal.c b/src/apps/kpunch/terminal/terminal.c deleted file mode 100644 index 7579ae92..00000000 --- a/src/apps/kpunch/terminal/terminal.c +++ /dev/null @@ -1,1662 +0,0 @@ -// 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. - -// terminal.c -- Terminal: a BBS / serial terminal program -// -// A thin user interface over three shared pieces: -// conn library serial port, modem dialer, secure link, Telnet -// xfer library XMODEM, XMODEM-1K, YMODEM, ZMODEM -// ANSI terminal the screen, scrollback, selection -// plus the phone book (termSession.c, TERMINAL.INI in the app's config -// directory). -// -// Everything runs from callbacks (no main loop of its own): a repeating -// timer widget is the heartbeat. Each tick polls the connection, then -// either feeds a running transfer or moves received bytes to the screen -// (capturing them to a file, answering ENQ and watching for a ZMODEM -// sender). The engines never block, so the screen, menus and the other -// programs stay responsive during dialing, key exchange and transfers. -// Transfers show a separate progress window; it also asks what to do -// when a download would replace an existing file. - -#include "dvxApp.h" -#include "dvxDlg.h" -#include "dvxWgt.h" -#include "dvxWgtP.h" -#include "dvxWm.h" -#include "dvxPlat.h" -#include "shellApp.h" -#include "taskSwch.h" - -#include "ansiTerm/ansiTerm.h" -#include "box/box.h" -#include "button/button.h" -#include "checkbox/checkbox.h" -#include "dropdown/dropdown.h" -#include "label/label.h" -#include "listBox/listBox.h" -#include "progressBar/progress.h" -#include "spinner/spinner.h" -#include "statusBar/statBar.h" -#include "tabControl/tabCtrl.h" -#include "textInput/textInpt.h" -#include "timer/timer.h" - -#include "termSession.h" -#include "serial/rs232/rs232.h" - -#include -#include -#include -#include - -#define TERM_TITLE "Terminal" -#define TERM_WIN_X 24 -#define TERM_WIN_Y 24 -#define TICK_MS 10 -#define READ_MAX 4096 -#define MS_PER_SEC 1000 -#define ENQ 0x05 -#define STATUS_MAX 160 -#define TEXT_MAX 256 -#define NUM_TEXT 16 -#define PERCENT 100 -#define DIGIT_GROUP 3 // digits between thousands separators -#define RGB_CHANNELS 3 -#define CHANNEL_MAX 255 -#define PASTE_CHUNK 256 - -// ZMODEM receive window on a serial line without flow control: no more -// than the UART buffer holds, so a slow frame never loses data. -#define RX_WINDOW_UNBUFFERED 2048 - -// Dialog layout -#define LABEL_CHARS 16 -#define FIELD_CHARS 28 -#define SHORT_FIELD_CHARS 10 -#define BUTTON_W 80 -#define LIST_CHARS 36 -#define LIST_H 160 -#define DLG_W 420 -#define DLG_H 300 -#define PROGRESS_CHARS 52 -#define DLG_SPACING 4 - -// Menu commands -#define CMD_PHONEBOOK 100 -#define CMD_CONNECT 101 -#define CMD_DISCONNECT 102 -#define CMD_CAPTURE 103 -#define CMD_EXIT 104 -#define CMD_COPY 110 -#define CMD_PASTE 111 -#define CMD_CLEAR 112 -#define CMD_BREAK 113 -#define CMD_SESSION 120 -#define CMD_ECHO 121 -#define CMD_XFER_OPTIONS 122 -#define CMD_SEND 130 -#define CMD_RECEIVE 131 -#define CMD_HELP_CONTENTS 140 -#define CMD_HELP_ABOUT 141 -#define CMD_SCHEME_BASE 200 // + SchemeE -#define CMD_PROTO_BASE 220 // + XferProtoE - -// Attribute brightness for the one-color schemes (scaled by the tint) -static const uint8_t sMonoLevel[ANSITERM_PALETTE_COLORS] = { 0, 110, 130, 150, 120, 140, 150, 190, 90, 200, 220, 240, 210, 230, 250, 255 }; - -static const uint8_t sGreenTint[RGB_CHANNELS] = { 51, 255, 51 }; -static const uint8_t sAmberTint[RGB_CHANNELS] = { 255, 176, 0 }; - -// Black text on white: background and grey swap, the colors darken -static const uint8_t sLightPalette[ANSITERM_PALETTE_BYTES] = { - 255, 255, 255, - 0, 0, 170, - 0, 128, 0, - 0, 128, 128, - 170, 0, 0, - 170, 0, 170, - 128, 64, 0, - 0, 0, 0, - 96, 96, 96, - 0, 0, 255, - 0, 160, 0, - 0, 140, 140, - 210, 0, 0, - 190, 0, 190, - 150, 110, 0, - 0, 0, 0 -}; - -typedef struct { - WidgetT *list; - int32_t choice; - const char *labels[TERM_BOOK_MAX]; -} BookDlgT; - -typedef struct { - TermSessionT *s; - WidgetT *name; - WidgetT *type; - WidgetT *port; - WidgetT *speed; - WidgetT *data; - WidgetT *parity; - WidgetT *stop; - WidgetT *flow; - WidgetT *init; - WidgetT *prefix; - WidgetT *phone; - WidgetT *redial; - WidgetT *delay; - WidgetT *pass; - WidgetT *ask; - WidgetT *secureNet; - WidgetT *host; - WidgetT *tcpPort; - WidgetT *cols; - WidgetT *rows; - WidgetT *scroll; - WidgetT *scheme; - WidgetT *echo; - WidgetT *answer; - const char *typeLabels[ConnTypeCountE]; - const char *portLabels[TERM_PORT_CHOICE_COUNT]; - const char *speedLabels[TERM_SPEED_CHOICE_COUNT]; - const char *dataLabels[TERM_DATA_CHOICE_COUNT]; - const char *parityLabels[TERM_PARITY_CHOICE_COUNT]; - const char *stopLabels[TERM_STOP_CHOICE_COUNT]; - const char *flowLabels[TERM_FLOW_CHOICE_COUNT]; - const char *schemeLabels[SchemeCountE]; -} SessionDlgT; - -typedef struct { - WidgetT *dir; - WidgetT *exists; - WidgetT *autoZ; - WidgetT *escape; - WidgetT *resume; - const char *existLabels[XferExistCountE]; -} XferDlgT; - -typedef struct { - WidgetT *input; - char *out; - int32_t outSize; -} PassDlgT; - -// The file transfer progress window -typedef struct { - WindowT *win; - WidgetT *title; - WidgetT *file; - WidgetT *fileBar; - WidgetT *fileBytes; - WidgetT *batchBar; - WidgetT *batchBytes; - WidgetT *note; - WidgetT *askBox; - WidgetT *askText; - WidgetT *askResume; - WidgetT *cancel; -} ProgressT; - -typedef struct { - DxeAppContextT *dxe; - AppContextT *ac; - WindowT *win; - MenuBarT *bar; - WidgetT *termBox; - WidgetT *term; - WidgetT *statusLbl; - WidgetT *infoLbl; - ConnT *conn; - XferT *xfer; - FILE *capture; - XferDetectT detect; - ProgressT prog; - TermBookT book; - TermSessionT session; - uint32_t statusSerial; - ConnStateE lastState; - bool transferring; - char iniPath[DVX_MAX_PATH]; -} TermAppT; - -static TermAppT sApp; - - -// ============================================================ -// Prototypes -// ============================================================ - -static WidgetT *addDropdown(WidgetT *parent, const char *label, const PrefsChoiceT *choices, int32_t count, int32_t value, const char **labels); -static WidgetT *addRow(WidgetT *parent, const char *label); -static WidgetT *addSpinner(WidgetT *parent, const char *label, int32_t lo, int32_t hi, int32_t value); -static WidgetT *addText(WidgetT *parent, const char *label, int32_t maxLen, const char *value, bool password); -static void applyLook(void); -void appShutdown(void); -static bool askPassphrase(char *out, int32_t outSize); -static bool bookOnOk(WindowT *dlg, void *userCtx); -static void buildMenus(void); -static void buildTerminal(void); -static void closeCapture(void); -static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len); -static void connectSession(void); -static WindowT *dlgCreate(const char *title); -static void echoLocal(const uint8_t *data, int32_t len); -static bool editSession(TermSessionT *s, const char *title); -static void fmtBytes(char *out, int32_t outSize, int32_t n); -static void infoUpdate(void); -static void menuUpdate(void); -static uint32_t nowMs(void); -static void onAskAnswer(WidgetT *w); -static void onBookConnect(WidgetT *w); -static void onBookDelete(WidgetT *w); -static void onBookEdit(WidgetT *w); -static void onBookNew(WidgetT *w); -static void onClose(WindowT *win); -static void onMenu(WindowT *win, int32_t menuId); -static void onProgressCancel(WidgetT *w); -static void onProgressClose(WindowT *win); -static void onTick(WidgetT *w); -static bool openUrlSession(const char *args); -static bool passOnOk(WindowT *dlg, void *userCtx); -static void pasteClipboard(void); -static void phoneBook(void); -static void phoneBookRefresh(BookDlgT *d); -static int32_t processIncoming(uint8_t *buf, int32_t n, bool *zmodemStart); -static void progressClose(void); -static void progressOpen(void); -static void progressUpdate(void); -static int32_t readDropdown(WidgetT *w, const PrefsChoiceT *choices); -static void receiveFiles(XferProtoE proto); -static void saveBook(void); -static bool schemePalette(int32_t scheme, uint8_t *out); -static void sendFiles(void); -static bool sessionOnOk(WindowT *dlg, void *userCtx); -static void startCapture(void); -static void startTransfer(bool sending, XferProtoE proto, const char *const *paths, int32_t count, const char *target); -static void statusUpdate(void); -static void switchSession(int32_t index); -static void terminalNote(const char *text); -static void transferOptions(void); -static bool xferOptsOnOk(WindowT *dlg, void *userCtx); -static int32_t xferPendingFn(void *ctx); -static int32_t xferReadFn(void *ctx, uint8_t *buf, int32_t maxLen); -static int32_t xferWriteFn(void *ctx, const uint8_t *data, int32_t len); -int32_t appMain(DxeAppContextT *ctx); - -// Phone book buttons beside the list (Connect and Cancel are below it) -static const struct { - const char *label; - void (*onClick)(WidgetT *w); -} sBookButtons[] = { - { "&New...", onBookNew }, - { "&Edit...", onBookEdit }, - { "&Delete", onBookDelete } -}; - - -// ============================================================ -// App descriptor -// ============================================================ - -// Callback-only: the timer widget drives the connection, so several -// copies (one per COM port) can run side by side. -SHELL_APP_DESCRIPTOR( - .name = TERM_TITLE, - .hasMainLoop = false, - .multiInstance = true, - .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_NORMAL -); - - -// A labelled dropdown filled from a choice table (labels must outlive -// the dropdown). -static WidgetT *addDropdown(WidgetT *parent, const char *label, const PrefsChoiceT *choices, int32_t count, int32_t value, const char **labels) { - WidgetT *row = addRow(parent, label); - WidgetT *dd = wgtDropdown(row); - int32_t idx = prefsChoiceIndexOfValue(choices, count, value); - - prefsChoiceLabels(choices, count, labels); - - dd->minW = wgtChars(FIELD_CHARS); - dd->weight = WGT_WEIGHT_FILL; - wgtDropdownSetItems(dd, labels, count); - wgtDropdownSetSelected(dd, idx < 0 ? 0 : idx); - return dd; -} - - -// "Label: [field]" row; the caller adds the field. -static WidgetT *addRow(WidgetT *parent, const char *label) { - WidgetT *row = wgtHBox(parent); - WidgetT *lbl = wgtLabel(row, label); - - row->spacing = wgtPixels(DLG_SPACING); - lbl->minW = wgtChars(LABEL_CHARS); - return row; -} - - -static WidgetT *addSpinner(WidgetT *parent, const char *label, int32_t lo, int32_t hi, int32_t value) { - WidgetT *row = addRow(parent, label); - WidgetT *sp = wgtSpinner(row, lo, hi, 1); - - sp->minW = wgtChars(SHORT_FIELD_CHARS); - wgtSpinnerSetValue(sp, value); - return sp; -} - - -static WidgetT *addText(WidgetT *parent, const char *label, int32_t maxLen, const char *value, bool password) { - WidgetT *row = addRow(parent, label); - WidgetT *in = password ? wgtPasswordInput(row, maxLen - 1) : wgtTextInput(row, maxLen - 1); - - in->minW = wgtChars(FIELD_CHARS); - in->weight = WGT_WEIGHT_FILL; - wgtSetText(in, value); - return in; -} - - -// Scrollback and colors of the current session on the terminal widget. -static void applyLook(void) { - uint8_t palette[ANSITERM_PALETTE_BYTES]; - - wgtAnsiTermSetScrollback(sApp.term, sApp.session.scrollback); - wgtAnsiTermSetPalette(sApp.term, schemePalette(sApp.session.scheme, palette) ? palette : NULL); - - for (int32_t i = 0; i < SchemeCountE; i++) { - wmMenuItemSetChecked(sApp.bar, CMD_SCHEME_BASE + i, i == sApp.session.scheme); - } - - wmMenuItemSetChecked(sApp.bar, CMD_ECHO, sApp.session.localEcho); -} - - -// The shell calls this when the program ends (window closed, Task -// Manager, shutdown): hang up, stop any transfer, save the phone book. -void appShutdown(void) { - if (sApp.xfer) { - xferCancel(sApp.xfer); - xferDestroy(sApp.xfer); - sApp.xfer = NULL; - } - - if (sApp.conn) { - connDestroy(sApp.conn); - sApp.conn = NULL; - } - - closeCapture(); -} - - -// Ask for the secure-link passphrase (never stored). False on Cancel. -static bool askPassphrase(char *out, int32_t outSize) { - PassDlgT d; - WindowT *win = dlgCreate("Secure Link Passphrase"); - - if (!win) { - return false; - } - - d.out = out; - d.outSize = outSize; - wgtLabel(win->widgetRoot, "Passphrase for the secure link:"); - d.input = wgtPasswordInput(win->widgetRoot, outSize - 1); - d.input->minW = wgtChars(FIELD_CHARS); - dvxDialogAddOkCancel(win, "&Connect", passOnOk, &d); - wgtSetFocused(d.input); - return dvxDialogRun(sApp.ac, win); -} - - -// Phone book Connect: the selected session (none selected keeps the -// dialog open). -static bool bookOnOk(WindowT *dlg, void *userCtx) { - BookDlgT *d = (BookDlgT *)userCtx; - - (void)dlg; - d->choice = wgtListBoxGetSelected(d->list); - return d->choice >= 0; -} - - -static void buildMenus(void) { - MenuT *file = wmAddMenu(sApp.bar, "&File"); - MenuT *edit = wmAddMenu(sApp.bar, "&Edit"); - MenuT *settings = wmAddMenu(sApp.bar, "&Settings"); - MenuT *xfer = wmAddMenu(sApp.bar, "&Transfer"); - MenuT *help = wmAddMenu(sApp.bar, "&Help"); - MenuT *colors = wmAddSubMenu(settings, "&Colors"); - - wmAddMenuItem(file, "&Phone Book...", CMD_PHONEBOOK); - wmAddMenuItem(file, "&Connect", CMD_CONNECT); - wmAddMenuItem(file, "&Disconnect", CMD_DISCONNECT); - wmAddMenuSeparator(file); - wmAddMenuCheckItem(file, "C&apture to File...", CMD_CAPTURE, false); - wmAddMenuSeparator(file); - wmAddMenuItem(file, "E&xit", CMD_EXIT); - - wmAddMenuItem(edit, "&Copy", CMD_COPY); - wmAddMenuItem(edit, "&Paste", CMD_PASTE); - wmAddMenuSeparator(edit); - wmAddMenuItem(edit, "C&lear Screen", CMD_CLEAR); - wmAddMenuItem(edit, "Send &Break", CMD_BREAK); - - wmAddMenuItem(settings, "&Session Settings...", CMD_SESSION); - wmAddMenuCheckItem(settings, "&Local Echo", CMD_ECHO, false); - - for (int32_t i = 0; i < SchemeCountE; i++) { - wmAddMenuRadioItem(colors, termSchemeChoices[i].label, CMD_SCHEME_BASE + i, false); - } - - wmAddMenuSeparator(settings); - wmAddMenuItem(settings, "&Transfer Options...", CMD_XFER_OPTIONS); - - wmAddMenuItem(xfer, "&Send Files...", CMD_SEND); - wmAddMenuItem(xfer, "&Receive Files...", CMD_RECEIVE); - wmAddMenuSeparator(xfer); - - for (int32_t i = 0; i < XferProtoCountE; i++) { - wmAddMenuRadioItem(xfer, termProtoChoices[i].label, CMD_PROTO_BASE + i, i == sApp.book.protocol); - } - - wmAddMenuItem(help, "&Contents\tF1", CMD_HELP_CONTENTS); - wmAddMenuSeparator(help); - wmAddMenuItem(help, "&About Terminal...", CMD_HELP_ABOUT); -} - - -// (Re)create the terminal widget at the session's size. -static void buildTerminal(void) { - if (sApp.term) { - wgtDestroy(sApp.term); - } - - sApp.term = wgtAnsiTerm(sApp.termBox, sApp.session.cols, sApp.session.rows); - sApp.term->weight = WGT_WEIGHT_FILL; - connSetWindowSize(sApp.conn, sApp.session.cols, sApp.session.rows); - wgtAnsiTermSetComm(sApp.term, NULL, NULL, commWrite); - applyLook(); - dvxFitWindow(sApp.ac, sApp.win); - wgtSetFocused(sApp.term); -} - - -static void closeCapture(void) { - if (sApp.capture) { - fclose(sApp.capture); - sApp.capture = NULL; - } -} - - -// Keys typed into the terminal. Nothing is sent while a transfer owns -// the connection. -static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len) { - (void)ctx; - - if (sApp.transferring || connState(sApp.conn) != ConnOpenE) { - return len; - } - - int32_t n = connWrite(sApp.conn, data, len); - - if (sApp.session.localEcho) { - echoLocal(data, n); - } - - return n; -} - - -static void connectSession(void) { - ConnConfigT cfg = sApp.session.conn; - - if (sApp.transferring) { - return; - } - - // The host and port are for Telnet, and for a secure link only when it - // is set to go over the network. - if (cfg.type == ConnSecureE && !sApp.session.secureNet) { - cfg.host[0] = '\0'; - } - - if ((cfg.type == ConnTelnetE || (cfg.type == ConnSecureE && sApp.session.secureNet)) && cfg.host[0] == '\0') { - dvxInfoBox(sApp.ac, TERM_TITLE, "Enter the host name on the Network page of Session Settings first."); - return; - } - - if (cfg.type == ConnSecureE && sApp.session.askPassphrase && !askPassphrase(cfg.passphrase, (int32_t)sizeof(cfg.passphrase))) { - return; - } - - connOpen(sApp.conn, &cfg, nowMs()); - memset(cfg.passphrase, 0, sizeof(cfg.passphrase)); - xferDetectReset(&sApp.detect); - statusUpdate(); -} - - -// An empty dialog of Terminal's width and spacing (see dvxDialogCreate). -static WindowT *dlgCreate(const char *title) { - WindowT *dlg = dvxDialogCreate(sApp.ac, title, DLG_W); - - if (dlg) { - dlg->widgetRoot->spacing = wgtPixels(DLG_SPACING); - } - - return dlg; -} - - -// Local echo: what was typed goes on the screen too (Enter as CR LF). -static void echoLocal(const uint8_t *data, int32_t len) { - static const uint8_t crlf[] = { '\r', '\n' }; - - for (int32_t i = 0; i < len; i++) { - if (data[i] == '\r') { - wgtAnsiTermWrite(sApp.term, crlf, (int32_t)sizeof(crlf)); - } else { - wgtAnsiTermWrite(sApp.term, &data[i], 1); - } - } -} - - -// Session settings dialog. True when the user pressed OK (s updated). -static bool editSession(TermSessionT *s, const char *title) { - SessionDlgT d; - WindowT *win; - - memset(&d, 0, sizeof(d)); - d.s = s; - win = dlgCreate(title); - - if (!win) { - return false; - } - - ConnConfigT *c = &s->conn; - WidgetT *tabs = wgtTabControl(win->widgetRoot); - WidgetT *pgConn = wgtTabPage(tabs, "Connection"); - WidgetT *pgMdm = wgtTabPage(tabs, "Modem"); - WidgetT *pgSec = wgtTabPage(tabs, "Secure Link"); - WidgetT *pgNet = wgtTabPage(tabs, "Network"); - WidgetT *pgScr = wgtTabPage(tabs, "Screen"); - - d.name = addText(pgConn, "Name:", TERM_NAME_MAX, s->name, false); - d.type = addDropdown(pgConn, "Connect with:", termTypeChoices, ConnTypeCountE, c->type, d.typeLabels); - d.port = addDropdown(pgConn, "Port:", termPortChoices, TERM_PORT_CHOICE_COUNT, c->comPort, d.portLabels); - d.speed = addDropdown(pgConn, "Speed (bps):", termSpeedChoices, TERM_SPEED_CHOICE_COUNT, c->bps, d.speedLabels); - d.data = addDropdown(pgConn, "Data bits:", termDataChoices, TERM_DATA_CHOICE_COUNT, c->dataBits, d.dataLabels); - d.parity = addDropdown(pgConn, "Parity:", termParityChoices, TERM_PARITY_CHOICE_COUNT, c->parity, d.parityLabels); - d.stop = addDropdown(pgConn, "Stop bits:", termStopChoices, TERM_STOP_CHOICE_COUNT, c->stopBits, d.stopLabels); - d.flow = addDropdown(pgConn, "Flow control:", termFlowChoices, TERM_FLOW_CHOICE_COUNT, c->flow, d.flowLabels); - - d.init = addText(pgMdm, "Setup command:", CONN_INIT_MAX, c->initString, false); - d.prefix = addText(pgMdm, "Dial command:", CONN_PREFIX_MAX, c->dialPrefix, false); - d.phone = addText(pgMdm, "Phone number:", CONN_PHONE_MAX, c->phone, false); - d.redial = addSpinner(pgMdm, "Redial tries:", 0, TERM_REDIAL_MAX, c->redialCount); - d.delay = addSpinner(pgMdm, "Redial wait (s):", TERM_REDIAL_DELAY_MIN, TERM_REDIAL_DELAY_MAX, c->redialDelay); - wgtLabel(pgMdm, "Leave the phone number empty to type modem commands yourself."); - - d.pass = addText(pgSec, "Passphrase:", CONN_PASS_MAX, s->askPassphrase ? "" : c->passphrase, true); - d.ask = wgtCheckbox(pgSec, "Ask for the passphrase when connecting (never save it)"); - wgtCheckboxSetChecked(d.ask, s->askPassphrase); - d.secureNet = wgtCheckbox(pgSec, "Connect over the network (the host and port on the Network page)"); - wgtCheckboxSetChecked(d.secureNet, s->secureNet); - wgtLabel(pgSec, "Use the same passphrase as the host, or none on both sides."); - - d.host = addText(pgNet, "Host name:", CONN_HOST_MAX, c->host, false); - d.tcpPort = addSpinner(pgNet, "Port:", TERM_TCP_PORT_MIN, TERM_TCP_PORT_MAX, c->tcpPort); - wgtLabel(pgNet, "For Telnet (usually port 23), and for a secure link over the network."); - - d.cols = addSpinner(pgScr, "Columns:", TERM_COLS_MIN, TERM_COLS_MAX, s->cols); - d.rows = addSpinner(pgScr, "Rows:", TERM_ROWS_MIN, TERM_ROWS_MAX, s->rows); - d.scroll = addSpinner(pgScr, "Scrollback lines:", TERM_SCROLLBACK_MIN, TERM_SCROLLBACK_MAX, s->scrollback); - d.scheme = addDropdown(pgScr, "Colors:", termSchemeChoices, SchemeCountE, s->scheme, d.schemeLabels); - d.answer = addText(pgScr, "Answerback:", TERM_ANSWERBACK_MAX, s->answerback, false); - d.echo = wgtCheckbox(pgScr, "Local echo (show what you type)"); - wgtCheckboxSetChecked(d.echo, s->localEcho); - - dvxDialogAddOkCancel(win, "&OK", sessionOnOk, &d); - wgtSetFocused(d.name); - return dvxDialogRun(sApp.ac, win); -} - - -// 1234567 -> "1,234,567" -static void fmtBytes(char *out, int32_t outSize, int32_t n) { - char digits[NUM_TEXT]; - int32_t len = snprintf(digits, sizeof(digits), "%ld", (long)n); - int32_t pos = 0; - - for (int32_t i = 0; i < len && pos < outSize - 1; i++) { - out[pos++] = digits[i]; - - int32_t left = len - i - 1; - - if (left > 0 && left % DIGIT_GROUP == 0 && digits[i] != '-' && pos < outSize - 1) { - out[pos++] = ','; - } - } - - out[pos] = '\0'; -} - - -// Right side of the status bar: the line settings, protocol, capture. -static void infoUpdate(void) { - const ConnConfigT *c = &sApp.session.conn; - int32_t port = prefsChoiceIndexOfValue(termPortChoices, TERM_PORT_CHOICE_COUNT, c->comPort); - int32_t par = prefsChoiceIndexOfValue(termParityChoices, TERM_PARITY_CHOICE_COUNT, c->parity); - const char *cap = sApp.capture ? " Capture" : ""; - char text[STATUS_MAX]; - - // Session values are clamped to the tables, so both indices are valid. - if (c->type == ConnTelnetE || (c->type == ConnSecureE && sApp.session.secureNet)) { - snprintf(text, sizeof(text), "%s:%ld %s%s", c->host, (long)c->tcpPort, termProtoChoices[sApp.book.protocol].label, cap); - } else { - snprintf(text, sizeof(text), "%s %ld %d%s%d %s%s", termPortChoices[port].label, (long)c->bps, (int)c->dataBits, termParityChoices[par].iniName, (int)c->stopBits, termProtoChoices[sApp.book.protocol].label, cap); - } - wgtSetText(sApp.infoLbl, text); -} - - -// Menu items that only make sense in some states. -static void menuUpdate(void) { - ConnStateE st = connState(sApp.conn); - bool idle = !sApp.transferring; - bool open = st == ConnOpenE; - - wmMenuItemSetEnabled(sApp.bar, CMD_CONNECT, idle && st == ConnClosedE); - wmMenuItemSetEnabled(sApp.bar, CMD_DISCONNECT, idle && st != ConnClosedE); - wmMenuItemSetEnabled(sApp.bar, CMD_PHONEBOOK, idle); - wmMenuItemSetEnabled(sApp.bar, CMD_SEND, idle && open); - wmMenuItemSetEnabled(sApp.bar, CMD_RECEIVE, idle && open); - wmMenuItemSetEnabled(sApp.bar, CMD_PASTE, idle && open); - wmMenuItemSetEnabled(sApp.bar, CMD_BREAK, idle && open && connCanBreak(sApp.session.conn.type)); - wmMenuItemSetChecked(sApp.bar, CMD_CAPTURE, sApp.capture != NULL); -} - - -static uint32_t nowMs(void) { - return (uint32_t)(((uint64_t)platformClock() * MS_PER_SEC) / platformClockHz()); -} - - -// A button of the "file already exists" question; its userData holds -// the XferExistE answer. -static void onAskAnswer(WidgetT *w) { - xferAnswer(sApp.xfer, (XferExistE)(intptr_t)w->userData, nowMs()); - progressUpdate(); -} - - -// Double-click in the phone book: Connect. -static void onBookConnect(WidgetT *w) { - dvxDialogEnd(w->window, true); -} - - -static void onBookDelete(WidgetT *w) { - BookDlgT *d = (BookDlgT *)w->userData; - int32_t sel = wgtListBoxGetSelected(d->list); - char msg[TEXT_MAX]; - - if (sel < 0 || sApp.book.count <= 1) { - return; - } - - snprintf(msg, sizeof(msg), "Delete \"%s\" from the phone book?", sApp.book.entries[sel].name); - - if (dvxMessageBox(sApp.ac, "Phone Book", msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) { - return; - } - - termBookRemove(&sApp.book, sel); - saveBook(); - phoneBookRefresh(d); -} - - -static void onBookEdit(WidgetT *w) { - BookDlgT *d = (BookDlgT *)w->userData; - int32_t sel = wgtListBoxGetSelected(d->list); - - if (sel < 0) { - return; - } - - TermSessionT s = sApp.book.entries[sel]; - - if (editSession(&s, "Edit Session")) { - sApp.book.entries[sel] = s; - saveBook(); - phoneBookRefresh(d); - } -} - - -static void onBookNew(WidgetT *w) { - BookDlgT *d = (BookDlgT *)w->userData; - TermSessionT s; - - if (sApp.book.count >= TERM_BOOK_MAX) { - dvxErrorBox(sApp.ac, "Phone Book", "The phone book is full. Delete an entry first."); - return; - } - - termSessionDefaults(&s); - snprintf(s.name, sizeof(s.name), "New session"); - - if (editSession(&s, "New Session")) { - int32_t idx = termBookAdd(&sApp.book, &s); - - saveBook(); - phoneBookRefresh(d); - wgtListBoxSetSelected(d->list, idx); - } -} - - -// Closing asks first while connected or transferring, then hangs up. -static void onClose(WindowT *win) { - if (sApp.transferring && dvxMessageBox(sApp.ac, TERM_TITLE, "A file transfer is running.\nCancel it and close?", MB_YESNO | MB_ICONQUESTION) != ID_YES) { - return; - } - - if (!sApp.transferring && connState(sApp.conn) != ConnClosedE && dvxMessageBox(sApp.ac, TERM_TITLE, "You are connected.\nDisconnect and close?", MB_YESNO | MB_ICONQUESTION) != ID_YES) { - return; - } - - progressClose(); - saveBook(); - appShutdown(); - dvxDestroyWindow(sApp.ac, win); - sApp.win = NULL; -} - - -static void onMenu(WindowT *win, int32_t menuId) { - (void)win; - - if (menuId >= CMD_SCHEME_BASE && menuId < CMD_SCHEME_BASE + SchemeCountE) { - sApp.session.scheme = menuId - CMD_SCHEME_BASE; - sApp.book.entries[sApp.book.current] = sApp.session; - applyLook(); - saveBook(); - return; - } - - if (menuId >= CMD_PROTO_BASE && menuId < CMD_PROTO_BASE + XferProtoCountE) { - sApp.book.protocol = menuId - CMD_PROTO_BASE; - wmMenuItemSetChecked(sApp.bar, menuId, true); - infoUpdate(); - saveBook(); - return; - } - - switch (menuId) { - case CMD_PHONEBOOK: - phoneBook(); - break; - - case CMD_CONNECT: - connectSession(); - break; - - case CMD_DISCONNECT: - connClose(sApp.conn); - statusUpdate(); - break; - - case CMD_CAPTURE: - if (sApp.capture) { - closeCapture(); - terminalNote("Capture stopped"); - } else { - startCapture(); - } - - menuUpdate(); - infoUpdate(); - break; - - case CMD_EXIT: - onClose(sApp.win); - break; - - case CMD_COPY: - wgtAnsiTermCopy(sApp.term); - break; - - case CMD_PASTE: - pasteClipboard(); - break; - - case CMD_CLEAR: - wgtAnsiTermClear(sApp.term); - break; - - case CMD_BREAK: - connSendBreak(sApp.conn, nowMs()); - break; - - case CMD_SESSION: { - TermSessionT s = sApp.session; - - if (editSession(&s, "Session Settings")) { - bool resize = s.cols != sApp.session.cols || s.rows != sApp.session.rows; - - sApp.session = s; - sApp.book.entries[sApp.book.current] = s; - saveBook(); - - if (resize) { - buildTerminal(); - } else { - applyLook(); - } - - dvxSetTitle(sApp.ac, sApp.win, s.name); - infoUpdate(); - - if (connState(sApp.conn) != ConnClosedE) { - terminalNote("New connection settings take effect the next time you connect"); - } - } - break; - } - - case CMD_ECHO: - sApp.session.localEcho = !sApp.session.localEcho; - sApp.book.entries[sApp.book.current] = sApp.session; - applyLook(); - saveBook(); - break; - - case CMD_XFER_OPTIONS: - transferOptions(); - break; - - case CMD_SEND: - sendFiles(); - break; - - case CMD_RECEIVE: - receiveFiles((XferProtoE)sApp.book.protocol); - break; - - case CMD_HELP_CONTENTS: - shellOpenHelp(sApp.ac, sApp.dxe->helpFile); - break; - - case CMD_HELP_ABOUT: - dvxInfoBox(sApp.ac, "About Terminal", "Terminal\n\nConnects to bulletin boards and other computers over a serial port, a modem or the secure link, and transfers files with XMODEM, YMODEM and ZMODEM.\n\nCopyright 2026 Scott Duensing"); - break; - - default: - break; - } - - if (sApp.win) { - menuUpdate(); - } -} - - -// Cancel while running; Close once the transfer has ended. -static void onProgressCancel(WidgetT *w) { - (void)w; - - if (sApp.transferring) { - xferCancel(sApp.xfer); - progressUpdate(); - return; - } - - progressClose(); -} - - -static void onProgressClose(WindowT *win) { - onProgressCancel(win->widgetRoot); -} - - -// The heartbeat: connection, transfer, screen, status. -static void onTick(WidgetT *w) { - uint32_t now = nowMs(); - bool zStart = false; - - (void)w; - - if (!sApp.win) { - return; - } - - connPoll(sApp.conn, now); - - if (sApp.transferring) { - xferPoll(sApp.xfer, now); - progressUpdate(); - } else { - uint8_t buf[READ_MAX]; - int32_t n = connRead(sApp.conn, buf, (int32_t)sizeof(buf)); - - if (n > 0) { - n = processIncoming(buf, n, &zStart); - wgtAnsiTermWrite(sApp.term, buf, n); - } - - if (zStart && sApp.book.autoZmodem) { - receiveFiles(XferZmodemE); - } - } - - statusUpdate(); -} - - -// Started with "telnet://host[:port]": that host's phone book entry (added -// when there is none) becomes the current session. True when it did. -static bool openUrlSession(const char *args) { - TermSessionT s; - int32_t index; - - if (!termSessionFromUrl(args, &s)) { - return false; - } - - index = termBookFindTelnet(&sApp.book, &s); - - if (index < 0) { - index = termBookAdd(&sApp.book, &s); - } - - if (index < 0) { - dvxLog("Terminal: the phone book is full; %s was not opened", args); - return false; - } - - sApp.book.current = index; - saveBook(); - return true; -} - - -static bool passOnOk(WindowT *dlg, void *userCtx) { - PassDlgT *d = (PassDlgT *)userCtx; - - (void)dlg; - snprintf(d->out, (size_t)d->outSize, "%s", wgtGetText(d->input)); - return true; -} - - -// Send the clipboard (newlines as CR) as if typed. -static void pasteClipboard(void) { - int32_t len = 0; - const char *clip = dvxClipboardGet(&len); - uint8_t chunk[PASTE_CHUNK]; - int32_t n = 0; - - for (int32_t i = 0; clip && i < len; i++) { - chunk[n++] = (clip[i] == '\n') ? '\r' : (uint8_t)clip[i]; - - if (n == PASTE_CHUNK || i == len - 1) { - commWrite(NULL, chunk, n); - n = 0; - } - } -} - - -// The phone book: pick a session to connect with, or add, change and -// delete sessions. -static void phoneBook(void) { - BookDlgT d; - WindowT *win; - - memset(&d, 0, sizeof(d)); - d.choice = -1; - win = dlgCreate("Phone Book"); - - if (!win) { - return; - } - - WidgetT *body = wgtHBox(win->widgetRoot); - - d.list = wgtListBox(body); - body->spacing = wgtPixels(DLG_SPACING); - d.list->minW = wgtChars(LIST_CHARS); - d.list->minH = wgtPixels(LIST_H); - d.list->weight = WGT_WEIGHT_FILL; - d.list->onDblClick = onBookConnect; - - WidgetT *btns = wgtVBox(body); - - btns->spacing = wgtPixels(DLG_SPACING); - btns->weight = 0; - - for (int32_t i = 0; i < DVX_ARRAY_LEN(sBookButtons); i++) { - WidgetT *b = wgtButton(btns, sBookButtons[i].label); - - b->prefW = wgtPixels(BUTTON_W); - b->userData = &d; - b->onClick = sBookButtons[i].onClick; - } - - dvxDialogAddOkCancel(win, "&Connect", bookOnOk, &d); - phoneBookRefresh(&d); - wgtListBoxSetSelected(d.list, sApp.book.current); - wgtSetFocused(d.list); - - if (dvxDialogRun(sApp.ac, win)) { - switchSession(d.choice); - connectSession(); - } -} - - -static void phoneBookRefresh(BookDlgT *d) { - for (int32_t i = 0; i < sApp.book.count; i++) { - d->labels[i] = sApp.book.entries[i].name; - } - - wgtListBoxSetItems(d->list, d->labels, sApp.book.count); -} - - -// Received bytes on their way to the screen: capture them, answer ENQ -// with the answerback and watch for a ZMODEM sender starting (the rest of -// its start frame is not shown). Returns the bytes left to show. -static int32_t processIncoming(uint8_t *buf, int32_t n, bool *zmodemStart) { - int32_t out = 0; - - if (sApp.capture) { - fwrite(buf, 1, (size_t)n, sApp.capture); - } - - for (int32_t i = 0; i < n; i++) { - uint8_t b = buf[i]; - - if (xferDetectFeed(&sApp.detect, b)) { - *zmodemStart = true; - break; - } - - if (b == ENQ) { - connWrite(sApp.conn, (const uint8_t *)sApp.session.answerback, (int32_t)strlen(sApp.session.answerback)); - continue; - } - - buf[out++] = b; - } - - return out; -} - - -static void progressClose(void) { - if (sApp.prog.win) { - dvxDestroyWindow(sApp.ac, sApp.prog.win); - } - - memset(&sApp.prog, 0, sizeof(sApp.prog)); -} - - -// The transfer window: what is moving, how far, and Cancel. It also -// holds the question asked when a download would replace a file. -static void progressOpen(void) { - static const struct { - const char *label; - XferExistE answer; - } asks[] = { - { "Con&tinue it", XferExistResumeE }, - { "&Keep both", XferExistRenameE }, - { "&Replace", XferExistOverwriteE }, - { "&Skip", XferExistSkipE } - }; - - progressClose(); - sApp.prog.win = dvxCreateWindowCentered(sApp.ac, "File Transfer", DLG_W, DLG_H, false); - - if (!sApp.prog.win || !wgtInitWindow(sApp.ac, sApp.prog.win)) { - return; - } - - WidgetT *root = sApp.prog.win->widgetRoot; - - root->spacing = wgtPixels(DLG_SPACING); - sApp.prog.win->onClose = onProgressClose; - sApp.prog.title = wgtLabel(root, ""); - sApp.prog.file = wgtLabel(root, ""); - sApp.prog.file->minW = wgtChars(PROGRESS_CHARS); - sApp.prog.fileBar = wgtProgressBar(root); - sApp.prog.fileBytes = wgtLabel(root, ""); - sApp.prog.batchBar = wgtProgressBar(root); - sApp.prog.batchBytes = wgtLabel(root, ""); - sApp.prog.note = wgtLabel(root, ""); - sApp.prog.askBox = wgtVBox(root); - sApp.prog.askText = wgtLabel(sApp.prog.askBox, ""); - - WidgetT *askRow = wgtHBox(sApp.prog.askBox); - - askRow->spacing = wgtPixels(DLG_SPACING); - - for (int32_t i = 0; i < DVX_ARRAY_LEN(asks); i++) { - WidgetT *b = wgtButton(askRow, asks[i].label); - - b->userData = (void *)(intptr_t)asks[i].answer; - b->onClick = onAskAnswer; - - if (asks[i].answer == XferExistResumeE) { - sApp.prog.askResume = b; - } - } - - WidgetT *row = wgtHBox(root); - - row->align = AlignEndE; - sApp.prog.cancel = wgtButton(row, "Cancel"); - sApp.prog.cancel->prefW = wgtPixels(BUTTON_W); - sApp.prog.cancel->onClick = onProgressCancel; - wgtSetVisible(sApp.prog.askBox, false); - progressUpdate(); - dvxFitWindow(sApp.ac, sApp.prog.win); -} - - -// Refresh the transfer window from the engine; notice the end. -static void progressUpdate(void) { - const XferProgressT *p = xferProgress(sApp.xfer); - char a[NUM_TEXT]; - char b[NUM_TEXT]; - char text[TEXT_MAX]; - - // A dropped connection ends the transfer at once. - if (sApp.transferring && p->status == XferRunningE && connState(sApp.conn) != ConnOpenE) { - xferCancel(sApp.xfer); - } - - bool running = p->status == XferRunningE || p->status == XferAskingE; - - if (sApp.transferring && !running) { - sApp.transferring = false; - snprintf(text, sizeof(text), "%s: %s", p->sending ? "Upload" : "Download", p->message); - terminalNote(text); - dvxLog("Terminal: %s", text); - menuUpdate(); - } - - if (!sApp.prog.win) { - return; - } - - snprintf(text, sizeof(text), "%s with %s", p->sending ? "Sending" : "Receiving", termProtoChoices[p->proto].label); - wgtSetText(sApp.prog.title, text); - - if (p->fileCount > 0) { - snprintf(text, sizeof(text), "File %d of %d: %s", (int)(p->fileIndex < p->fileCount ? p->fileIndex + 1 : p->fileCount), (int)p->fileCount, p->fileName); - } else { - snprintf(text, sizeof(text), "File %d: %s", (int)(p->fileIndex + 1), p->fileName[0] ? p->fileName : "(waiting for the other side)"); - } - - wgtSetText(sApp.prog.file, text); - fmtBytes(a, sizeof(a), p->fileBytes); - - if (p->fileSize > 0) { - fmtBytes(b, sizeof(b), p->fileSize); - snprintf(text, sizeof(text), "%s of %s bytes", a, b); - wgtProgressBarSetValue(sApp.prog.fileBar, (int32_t)(((int64_t)p->fileBytes * PERCENT) / p->fileSize)); - } else { - snprintf(text, sizeof(text), "%s bytes", a); - } - - if (p->resumedAt > 0) { - size_t used = strlen(text); - - fmtBytes(a, sizeof(a), p->resumedAt); - snprintf(text + used, sizeof(text) - used, " (continued from %s)", a); - } - - wgtSetText(sApp.prog.fileBytes, text); - - if (p->batchSize > 0) { - fmtBytes(a, sizeof(a), p->batchBytes); - fmtBytes(b, sizeof(b), p->batchSize); - snprintf(text, sizeof(text), "All files: %s of %s bytes", a, b); - wgtProgressBarSetValue(sApp.prog.batchBar, (int32_t)(((int64_t)p->batchBytes * PERCENT) / p->batchSize)); - } else { - fmtBytes(a, sizeof(a), p->batchBytes); - snprintf(text, sizeof(text), "All files: %s bytes", a); - } - - wgtSetText(sApp.prog.batchBytes, text); - - if (running) { - snprintf(text, sizeof(text), "Retries: %d", (int)p->errors); - } else { - snprintf(text, sizeof(text), "%s", p->message); - } - - wgtSetText(sApp.prog.note, text); - - if (p->status == XferAskingE) { - fmtBytes(a, sizeof(a), p->askLocalSize); - snprintf(text, sizeof(text), "%s already exists (%s bytes). What should happen to it?", p->fileName, a); - wgtSetText(sApp.prog.askText, text); - wgtSetEnabled(sApp.prog.askResume, p->askCanResume); - } - - wgtSetVisible(sApp.prog.askBox, p->status == XferAskingE); - wgtSetText(sApp.prog.cancel, running ? "Cancel" : "Close"); -} - - -// The value of the selected dropdown item (tables are in dropdown order). -static int32_t readDropdown(WidgetT *w, const PrefsChoiceT *choices) { - int32_t idx = wgtDropdownGetSelected(w); - - return choices[idx < 0 ? 0 : idx].value; -} - - -// Receive with proto: XMODEM asks where to save the file, the batch -// protocols store into the download folder (created if needed). -static void receiveFiles(XferProtoE proto) { - char path[DVX_MAX_PATH]; - - if (sApp.transferring || connState(sApp.conn) != ConnOpenE) { - return; - } - - if (proto == XferXmodemE || proto == XferXmodem1kE) { - if (!dvxFileDialog(sApp.ac, "Receive File As", FD_SAVE, sApp.book.downloadDir, NULL, 0, path, sizeof(path))) { - return; - } - - startTransfer(false, proto, NULL, 0, path); - return; - } - - if (platformMkdirRecursive(sApp.book.downloadDir) != 0) { - snprintf(path, sizeof(path), "The download folder %s could not be created.", sApp.book.downloadDir); - terminalNote(path); - return; - } - - startTransfer(false, proto, NULL, 0, sApp.book.downloadDir); -} - - -static void saveBook(void) { - if (sApp.iniPath[0] && !termBookSave(&sApp.book, sApp.iniPath)) { - terminalNote("The phone book could not be saved"); - } -} - - -// Palette of a color scheme; false for the standard colors. -static bool schemePalette(int32_t scheme, uint8_t *out) { - const uint8_t *tint; - - switch (scheme) { - case SchemeGreenE: - tint = sGreenTint; - break; - - case SchemeAmberE: - tint = sAmberTint; - break; - - case SchemeLightE: - memcpy(out, sLightPalette, ANSITERM_PALETTE_BYTES); - return true; - - default: - return false; - } - - for (int32_t i = 0; i < ANSITERM_PALETTE_COLORS; i++) { - for (int32_t c = 0; c < RGB_CHANNELS; c++) { - out[i * RGB_CHANNELS + c] = (uint8_t)((tint[c] * sMonoLevel[i]) / CHANNEL_MAX); - } - } - - return true; -} - - -// Pick files (one at a time; batch protocols may add more) and send. -static void sendFiles(void) { - static char store[XFER_MAX_FILES][DVX_MAX_PATH]; - const char *paths[XFER_MAX_FILES]; - int32_t count = 0; - XferProtoE proto = (XferProtoE)sApp.book.protocol; - bool batch = proto == XferYmodemE || proto == XferZmodemE; - - if (sApp.transferring || connState(sApp.conn) != ConnOpenE) { - return; - } - - while (count < XFER_MAX_FILES && dvxFileDialog(sApp.ac, count == 0 ? "Send File" : "Add Another File", FD_OPEN, NULL, NULL, 0, store[count], sizeof(store[count]))) { - paths[count] = store[count]; - count++; - - if (!batch || count == XFER_MAX_FILES || dvxMessageBox(sApp.ac, "Send Files", "Add another file to send in the same batch?", MB_YESNO | MB_ICONQUESTION) != ID_YES) { - break; - } - } - - if (count > 0) { - startTransfer(true, proto, paths, count, NULL); - } -} - - -// Session settings OK: copy every field into the session. -static bool sessionOnOk(WindowT *dlg, void *userCtx) { - SessionDlgT *d = (SessionDlgT *)userCtx; - TermSessionT *s = d->s; - ConnConfigT *c = &s->conn; - - (void)dlg; - snprintf(s->name, sizeof(s->name), "%s", wgtGetText(d->name)); - c->type = readDropdown(d->type, termTypeChoices); - c->comPort = readDropdown(d->port, termPortChoices); - c->bps = readDropdown(d->speed, termSpeedChoices); - c->dataBits = readDropdown(d->data, termDataChoices); - c->parity = readDropdown(d->parity, termParityChoices); - c->stopBits = readDropdown(d->stop, termStopChoices); - c->flow = readDropdown(d->flow, termFlowChoices); - c->redialCount = wgtSpinnerGetValue(d->redial); - c->redialDelay = wgtSpinnerGetValue(d->delay); - c->tcpPort = wgtSpinnerGetValue(d->tcpPort); - s->cols = wgtSpinnerGetValue(d->cols); - s->rows = wgtSpinnerGetValue(d->rows); - s->scrollback = wgtSpinnerGetValue(d->scroll); - s->scheme = readDropdown(d->scheme, termSchemeChoices); - - s->askPassphrase = wgtCheckboxIsChecked(d->ask); - s->secureNet = wgtCheckboxIsChecked(d->secureNet); - s->localEcho = wgtCheckboxIsChecked(d->echo); - - snprintf(c->initString, sizeof(c->initString), "%s", wgtGetText(d->init)); - snprintf(c->dialPrefix, sizeof(c->dialPrefix), "%s", wgtGetText(d->prefix)); - snprintf(c->phone, sizeof(c->phone), "%s", wgtGetText(d->phone)); - snprintf(c->passphrase, sizeof(c->passphrase), "%s", s->askPassphrase ? "" : wgtGetText(d->pass)); - snprintf(c->host, sizeof(c->host), "%s", wgtGetText(d->host)); - snprintf(s->answerback, sizeof(s->answerback), "%s", wgtGetText(d->answer)); - termSessionClamp(s); - return true; -} - - -static void startCapture(void) { - char path[DVX_MAX_PATH]; - char note[TEXT_MAX]; - - if (!dvxFileDialog(sApp.ac, "Capture to File", FD_SAVE, NULL, NULL, 0, path, sizeof(path))) { - return; - } - - // Appends, so an earlier capture of the same name is kept. - sApp.capture = fopen(path, "ab"); - - if (!sApp.capture) { - dvxErrorBox(sApp.ac, TERM_TITLE, "The capture file could not be opened."); - return; - } - - snprintf(note, sizeof(note), "Capturing to %s", path); - terminalNote(note); -} - - -// Start a transfer over the connection and show its window. The -// transfer options come from the phone book. -static void startTransfer(bool sending, XferProtoE proto, const char *const *paths, int32_t count, const char *target) { - XferIoT io; - XferOptionsT opt; - bool ok; - int32_t type = sApp.session.conn.type; - - io.ctx = sApp.conn; - io.read = xferReadFn; - io.write = xferWriteFn; - io.pending = xferPendingFn; - xferOptionsDefaults(&opt); - opt.exists = (XferExistE)sApp.book.exists; - opt.escapeCtl = sApp.book.escapeCtl; - opt.resume = sApp.book.resumeUploads; - opt.rxWindow = ((type == ConnSerialE || type == ConnModemE) && sApp.session.conn.flow == RS232_HANDSHAKE_NONE) ? RX_WINDOW_UNBUFFERED : 0; - - if (sending) { - ok = xferStartSend(sApp.xfer, proto, &io, &opt, paths, count, nowMs()); - } else { - ok = xferStartReceive(sApp.xfer, proto, &io, &opt, target, nowMs()); - } - - if (!ok) { - dvxErrorBox(sApp.ac, "File Transfer", xferProgress(sApp.xfer)->message); - return; - } - - sApp.transferring = true; - xferDetectReset(&sApp.detect); - progressOpen(); - menuUpdate(); -} - - -// The status bar follows the connection, and every change (dialing, -// connected, carrier lost ...) is also noted on the screen. -static void statusUpdate(void) { - ConnStateE st = connState(sApp.conn); - - if (connStatusSerial(sApp.conn) != sApp.statusSerial) { - sApp.statusSerial = connStatusSerial(sApp.conn); - wgtSetText(sApp.statusLbl, connStatus(sApp.conn)); - terminalNote(connStatus(sApp.conn)); - dvxLog("Terminal: %s", connStatus(sApp.conn)); - } - - if (st != sApp.lastState) { - sApp.lastState = st; - menuUpdate(); - } -} - - -// Make phone book entry index the current session. -static void switchSession(int32_t index) { - bool resize = sApp.book.entries[index].cols != sApp.session.cols || sApp.book.entries[index].rows != sApp.session.rows; - - connClose(sApp.conn); - sApp.book.current = index; - sApp.session = sApp.book.entries[index]; - saveBook(); - - if (resize) { - buildTerminal(); - } else { - applyLook(); - } - - dvxSetTitle(sApp.ac, sApp.win, sApp.session.name); - infoUpdate(); -} - - -// A line from Terminal itself (not from the other side) on the screen. -static void terminalNote(const char *text) { - if (sApp.term) { - char line[TEXT_MAX]; - - snprintf(line, sizeof(line), "\r\n[%s]\r\n", text); - wgtAnsiTermWrite(sApp.term, (const uint8_t *)line, (int32_t)strlen(line)); - } -} - - -// Download folder, what to do with existing files, ZMODEM behavior. -static void transferOptions(void) { - XferDlgT d; - WindowT *win; - - memset(&d, 0, sizeof(d)); - win = dlgCreate("Transfer Options"); - - if (!win) { - return; - } - - WidgetT *root = win->widgetRoot; - - d.dir = addText(root, "Download folder:", TERM_DIR_MAX, sApp.book.downloadDir, false); - d.exists = addDropdown(root, "If a file exists:", termExistChoices, XferExistCountE, sApp.book.exists, d.existLabels); - d.autoZ = wgtCheckbox(root, "Start ZMODEM downloads automatically"); - d.resume = wgtCheckbox(root, "Ask the other side to continue interrupted uploads (ZMODEM)"); - d.escape = wgtCheckbox(root, "Escape all control characters (for links that lose them)"); - wgtCheckboxSetChecked(d.autoZ, sApp.book.autoZmodem); - wgtCheckboxSetChecked(d.resume, sApp.book.resumeUploads); - wgtCheckboxSetChecked(d.escape, sApp.book.escapeCtl); - dvxDialogAddOkCancel(win, "&OK", xferOptsOnOk, &d); - - if (dvxDialogRun(sApp.ac, win)) { - saveBook(); - } -} - - -// Transfer options OK: copy the settings into the phone book. -static bool xferOptsOnOk(WindowT *dlg, void *userCtx) { - XferDlgT *d = (XferDlgT *)userCtx; - - (void)dlg; - snprintf(sApp.book.downloadDir, sizeof(sApp.book.downloadDir), "%s", wgtGetText(d->dir)); - - if (sApp.book.downloadDir[0] == '\0') { - snprintf(sApp.book.downloadDir, sizeof(sApp.book.downloadDir), "%s", TERM_DEFAULT_DIR); - } - - sApp.book.exists = readDropdown(d->exists, termExistChoices); - sApp.book.autoZmodem = wgtCheckboxIsChecked(d->autoZ); - sApp.book.resumeUploads = wgtCheckboxIsChecked(d->resume); - sApp.book.escapeCtl = wgtCheckboxIsChecked(d->escape); - return true; -} - - -static int32_t xferPendingFn(void *ctx) { - return connPending((ConnT *)ctx); -} - - -static int32_t xferReadFn(void *ctx, uint8_t *buf, int32_t maxLen) { - return connRead((ConnT *)ctx, buf, maxLen); -} - - -static int32_t xferWriteFn(void *ctx, const uint8_t *data, int32_t len) { - return connWrite((ConnT *)ctx, data, len); -} - - -int32_t appMain(DxeAppContextT *ctx) { - bool telnetUrl; - - memset(&sApp, 0, sizeof(sApp)); - sApp.dxe = ctx; - sApp.ac = ctx->shellCtx; - sApp.conn = connCreate(); - sApp.xfer = xferCreate(); - - if (!sApp.conn || !sApp.xfer) { - appShutdown(); - return -1; - } - - if (shellEnsureConfigDir(ctx) == 0) { - shellConfigPath(ctx, TERM_INI_NAME, sApp.iniPath, sizeof(sApp.iniPath)); - } - - termBookLoad(&sApp.book, sApp.iniPath); - telnetUrl = openUrlSession(ctx->args); - sApp.session = sApp.book.entries[sApp.book.current]; - sApp.lastState = ConnClosedE; - sApp.win = dvxCreateWindow(sApp.ac, sApp.session.name, TERM_WIN_X, TERM_WIN_Y, DLG_W, DLG_H, false); - - if (!sApp.win) { - appShutdown(); - return -1; - } - - sApp.win->onClose = onClose; - sApp.win->onMenu = onMenu; - sApp.bar = wmAddMenuBar(sApp.win); - buildMenus(); - - WidgetT *root = wgtInitWindow(sApp.ac, sApp.win); - - if (!root) { - dvxDestroyWindow(sApp.ac, sApp.win); - sApp.win = NULL; - appShutdown(); - return -1; - } - - // The terminal (in its own box, so it can be rebuilt at a new size), - // the status bar, and the invisible heartbeat timer. - sApp.termBox = wgtVBox(root); - sApp.termBox->weight = WGT_WEIGHT_FILL; - - WidgetT *status = wgtStatusBar(root); - WidgetT *timer = wgtTimer(root, TICK_MS, true); - - sApp.statusLbl = wgtLabel(status, "Not connected: choose File > Phone Book"); - sApp.statusLbl->weight = WGT_WEIGHT_FILL; - sApp.infoLbl = wgtLabel(status, ""); - timer->onChange = onTick; - buildTerminal(); - infoUpdate(); - menuUpdate(); - dvxLog("Terminal: started with session %s", sApp.session.name); - - if (telnetUrl) { - connectSession(); - } - - return 0; -} diff --git a/src/apps/kpunch/terminal/terminal.dbp b/src/apps/kpunch/terminal/terminal.dbp new file mode 100644 index 00000000..48631bce --- /dev/null +++ b/src/apps/kpunch/terminal/terminal.dbp @@ -0,0 +1,33 @@ +[Project] +Name = Terminal +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Serial, modem and secure-link terminal with XMODEM, YMODEM and ZMODEM file transfer +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/commdlg.bas +File1 = ../../../include/basic/conn.bas +File2 = ../../../include/basic/fileops.bas +File3 = ../../../include/basic/help.bas +File4 = ../../../include/basic/prefs.bas +File5 = ../../../include/basic/shell.bas +File6 = ../../../include/basic/xfer.bas +File7 = terminal.bas +File8 = termMain.bas +Count = 9 + +[Forms] +File0 = terminal.frm +File1 = termBook.frm +File2 = termSess.frm +File3 = termXfer.frm +File4 = termPass.frm +File5 = termProg.frm +Count = 6 + +[Settings] +StartupForm = TermForm +MultiInstance = True diff --git a/src/apps/kpunch/terminal/terminal.frm b/src/apps/kpunch/terminal/terminal.frm new file mode 100644 index 00000000..6a737166 --- /dev/null +++ b/src/apps/kpunch/terminal/terminal.frm @@ -0,0 +1,888 @@ +VERSION DVX 1.00 +Begin Form TermForm + Caption = "Terminal" + Layout = VBox + AutoSize = True + Resizable = False + Centered = False + Left = 24 + Top = 24 + Width = 420 + Height = 300 + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuPhoneBook + Caption = "&Phone Book..." + End + Begin Menu mnuConnect + Caption = "&Connect" + End + Begin Menu mnuDisconnect + Caption = "&Disconnect" + End + Begin Menu mnuFileSep1 + Caption = "-" + End + Begin Menu mnuCapture + Caption = "C&apture to File..." + End + Begin Menu mnuFileSep2 + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + End + End + Begin Menu mnuEdit + Caption = "&Edit" + Begin Menu mnuCopy + Caption = "&Copy" + End + Begin Menu mnuPaste + Caption = "&Paste" + End + Begin Menu mnuEditSep1 + Caption = "-" + End + Begin Menu mnuClear + Caption = "C&lear Screen" + End + Begin Menu mnuBreak + Caption = "Send &Break" + End + End + Begin Menu mnuSettings + Caption = "&Settings" + ' The scheme and protocol items are named in TermStart, from the + ' lists the dialogs and the status bar use (terminal.bas) + Begin Menu mnuColors + Caption = "&Colors" + Begin Menu mnuSchemeStandard + RadioCheck = True + End + Begin Menu mnuSchemeGreen + RadioCheck = True + End + Begin Menu mnuSchemeAmber + RadioCheck = True + End + Begin Menu mnuSchemeLight + RadioCheck = True + End + End + Begin Menu mnuSession + Caption = "&Session Settings..." + End + Begin Menu mnuEcho + Caption = "&Local Echo" + End + Begin Menu mnuSettingsSep1 + Caption = "-" + End + Begin Menu mnuXferOptions + Caption = "&Transfer Options..." + End + End + Begin Menu mnuTransfer + Caption = "&Transfer" + Begin Menu mnuSend + Caption = "&Send Files..." + End + Begin Menu mnuReceive + Caption = "&Receive Files..." + End + Begin Menu mnuTransferSep1 + Caption = "-" + End + Begin Menu mnuProtoXmodem + RadioCheck = True + End + Begin Menu mnuProtoXmodem1k + RadioCheck = True + End + Begin Menu mnuProtoYmodem + RadioCheck = True + End + Begin Menu mnuProtoZmodem + RadioCheck = True + End + End + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuHelpContents + Caption = "&Contents" + ShortcutText = "F1" + End + Begin Menu mnuHelpSep + Caption = "-" + End + Begin Menu mnuAbout + Caption = "&About Terminal..." + End + End + Begin VBox boxTerm + Weight = 1 + Begin Terminal trmMain + Weight = 1 + End + End + Begin StatusBar sbStatus + Begin Label lblStatus + Caption = "Not connected: choose File > Phone Book" + Weight = 1 + End + Begin Label lblInfo + Caption = "" + End + End + Begin Timer tmrTick + Interval = 10 + Enabled = True + End +End + + +' 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. + + +' terminal.frm -- Terminal: a BBS / serial terminal program. +' +' The window: the ANSI terminal, a status bar with what the connection is +' doing (left) and the line settings, transfer protocol and capture +' (right), and the menus. Everything runs from events: the timer is the +' heartbeat. Each tick moves the connection along (ConnPoll, which also +' runs a transfer or puts what arrived on the terminal, conn.bas), starts +' a ZMODEM download when a sender asks for one, follows a running +' transfer in its window (termProg.frm) and shows every change of the +' connection's status on the status bar and the screen. Nothing blocks, +' so the screen, the menus and the other programs stay usable while a +' modem dials, a secure link is set up or a file is transferred. +' +' The phone book (terminal.bas) is the list of saved sessions; the window +' shows one of them (session) and its title is the session's name. +' Started with "telnet://host[:port]", Terminal uses (or adds) that +' host's phone book entry and connects at once. +' +' More than one Terminal may run (MultiInstance in terminal.dbp), one per +' COM port for example. + +OPTION EXPLICIT + +CONST HELP_FILE = "terminal.hlp" + +' A line from Terminal itself (not from the other side) on the screen +CONST NOTE_START = "[" +CONST NOTE_END = "]" +CONST CRLF = CHR$(13) & CHR$(10) + +CONST ABOUT_TEXT = "Terminal" & CHR$(10) & CHR$(10) & "Connects to bulletin boards and other computers over a serial port, a modem or the secure link, and transfers files with XMODEM, YMODEM and ZMODEM." & CHR$(10) & CHR$(10) & "Copyright 2026 Scott Duensing" + +' Color schemes: the 16 text attribute colors. The one-color schemes +' scale a tint by each attribute's brightness; black on white swaps the +' background and the grays and darkens the colors. +CONST ANSI_COLORS = 16 +CONST CHANNEL_MAX = 255 +CONST MONO_LEVELS = "0|110|130|150|120|140|150|190|90|200|220|240|210|230|250|255" +CONST GREEN_TINT = "51|255|51" +CONST AMBER_TINT = "255|176|0" +CONST LIGHT_COLORS = "255,255,255|0,0,170|0,128,0|0,128,128|170,0,0|170,0,170|128,64,0|0,0,0|96,96,96|0,0,255|0,160,0|0,140,140|210,0,0|190,0,190|150,110,0|0,0,0" +CONST RGB_SEP = "," + + +' ============================================================ +' Window +' ============================================================ + +' Start (termMain.bas calls this once every form is loaded and the +' modules' variables exist): the phone book, the session, the menus and +' the terminal. Arguments: "telnet://host[:port]" connects to +' that host. +SUB TermStart + DIM telnetUrl AS BOOLEAN + + conn = ConnCreate() + + ' Out of memory or connection slots: nothing can work + IF conn = 0 THEN + ShellLog "Terminal: no connection available" + END + END IF + + iniPath = ShellConfigFile$(TERM_INI_NAME) + BookLoad + telnetUrl = OpenUrlSession(COMMAND$) + session = book(bookCurrent) + lastState = CONN_CLOSED + TermForm.Caption = session.sessionName + + ' The menus' scheme and protocol items, and the check items + mnuSchemeStandard.Caption = ChoiceName$(SCHEME_LABELS, SCHEME_STANDARD) + mnuSchemeGreen.Caption = ChoiceName$(SCHEME_LABELS, SCHEME_GREEN) + mnuSchemeAmber.Caption = ChoiceName$(SCHEME_LABELS, SCHEME_AMBER) + mnuSchemeLight.Caption = ChoiceName$(SCHEME_LABELS, SCHEME_LIGHT) + mnuProtoXmodem.Caption = ChoiceName$(PROTO_LABELS, XFER_XMODEM) + mnuProtoXmodem1k.Caption = ChoiceName$(PROTO_LABELS, XFER_XMODEM1K) + mnuProtoYmodem.Caption = ChoiceName$(PROTO_LABELS, XFER_YMODEM) + mnuProtoZmodem.Caption = ChoiceName$(PROTO_LABELS, XFER_ZMODEM) + ProtocolChecks + + ConnAttach conn, "trmMain" + BuildTerminal + InfoUpdate + MenuUpdate + ShellLog "Terminal: started with session " & session.sessionName + + IF telnetUrl THEN + ConnectSession + END IF +END SUB + + +' Closing asks first while connected or transferring. +SUB TermForm_QueryUnload(Cancel AS INTEGER) + IF transferring THEN + IF MsgBox("A file transfer is running." & CHR$(10) & "Cancel it and close?", vbYesNo + vbQuestion, TERM_TITLE) <> vbYes THEN + Cancel = 1 + END IF + ELSEIF ConnState(conn) <> CONN_CLOSED THEN + IF MsgBox("You are connected." & CHR$(10) & "Disconnect and close?", vbYesNo + vbQuestion, TERM_TITLE) <> vbYes THEN + Cancel = 1 + END IF + END IF +END SUB + + +' Stop any transfer, save the phone book, hang up. Every form of the +' program is loaded when it starts, and a loaded form keeps it running: +' the dialogs go too. +SUB TermForm_Unload + tmrTick.Enabled = False + + IF xfer <> 0 THEN + XferClose xfer + xfer = 0 + transferring = False + END IF + + ProgressClose + SaveBook + ConnFree conn + conn = 0 + Unload BookForm + Unload SessionForm + Unload XferOptsForm + Unload PassForm + Unload ProgressForm +END SUB + + +' The heartbeat: connection, transfer, screen, status. +SUB tmrTick_Timer + ConnPoll conn + + IF transferring THEN + ProgressUpdate + ELSEIF ConnZmodemRequest(conn) AND autoZmodem THEN + ReceiveFiles XFER_ZMODEM + END IF + + StatusUpdate +END SUB + + +' ============================================================ +' The session +' ============================================================ + +' Scrollback, colors and local echo of the session, on the terminal, the +' menus and the connection. +SUB ApplyLook + trmMain.Scrollback = session.scrollback + SchemeColors session.scheme + + mnuSchemeStandard.Checked = (session.scheme = SCHEME_STANDARD) + mnuSchemeGreen.Checked = (session.scheme = SCHEME_GREEN) + mnuSchemeAmber.Checked = (session.scheme = SCHEME_AMBER) + mnuSchemeLight.Checked = (session.scheme = SCHEME_LIGHT) + mnuEcho.Checked = session.localEcho + + ConnSetEcho conn, session.localEcho + ConnSetAnswerback conn, session.answerback +END SUB + + +' The terminal at the session's size (screen and scrollback start empty), +' the window fitted to it. +SUB BuildTerminal + trmMain.Cols = session.cols + trmMain.Rows = session.rows + ConnSetWindowSize conn, session.cols, session.rows + ApplyLook + TermForm.AutoSize = True + trmMain.SetFocus +END SUB + + +SUB ConnectSession + DIM host AS STRING + DIM pass AS STRING + + IF transferring THEN + EXIT SUB + END IF + + ' The host and port are for Telnet, and for a secure link only when it + ' is set to go over the network. + host = session.host + + IF session.connType = CONN_SECURE AND NOT session.secureNet THEN + host = "" + END IF + + IF (session.connType = CONN_TELNET OR (session.connType = CONN_SECURE AND session.secureNet)) AND host = "" THEN + MsgBox "Enter the host name on the Network page of Session Settings first.", vbOKOnly + vbInformation, TERM_TITLE + EXIT SUB + END IF + + pass = session.passphrase + + IF session.connType = CONN_SECURE AND session.askPassphrase THEN + IF NOT PassphraseDialog(pass) THEN + EXIT SUB + END IF + END IF + + ConnSetSerial conn, session.comPort, session.bps, session.dataBits, session.parity, session.stopBits, session.flow + ConnSetModem conn, session.initString, session.dialPrefix, session.phone, session.redialCount, session.redialDelay + ConnSetHost conn, host, session.tcpPort + ConnSetPassphrase conn, pass + pass = "" + ConnOpen conn, session.connType + StatusUpdate +END SUB + + +' Started with "telnet://host[:port]": that host's phone book entry (added +' when there is none) becomes the current session. True when it did. +FUNCTION OpenUrlSession(BYVAL args AS STRING) AS BOOLEAN + DIM s AS TermSession + DIM index AS LONG + + OpenUrlSession = False + + IF NOT SessionFromUrl(args, s) THEN + EXIT FUNCTION + END IF + + index = BookFindTelnet(s) + + IF index < 0 THEN + index = BookAdd(s) + END IF + + IF index < 0 THEN + ShellLog "Terminal: the phone book is full; " & args & " was not opened" + EXIT FUNCTION + END IF + + bookCurrent = index + SaveBook + OpenUrlSession = True +END FUNCTION + + +' The phone book on disk; a note when it cannot be saved. +SUB SaveBook + IF iniPath <> "" THEN + IF NOT BookSave() THEN + TerminalNote "The phone book could not be saved" + END IF + END IF +END SUB + + +' The session changed in the window: keep it in the phone book. +SUB SessionStore + book(bookCurrent) = session + SaveBook +END SUB + + +' Make phone book entry index the current session. +SUB SwitchSession(BYVAL index AS LONG) + DIM resize AS BOOLEAN + + resize = book(index).cols <> session.cols OR book(index).rows <> session.rows + ConnClose conn + bookCurrent = index + session = book(index) + SaveBook + + IF resize THEN + BuildTerminal + ELSE + ApplyLook + END IF + + TermForm.Caption = session.sessionName + InfoUpdate +END SUB + + +' ============================================================ +' Status bar, menus and screen +' ============================================================ + +' Right side of the status bar: the line settings, protocol, capture. +SUB InfoUpdate + DIM text AS STRING + + IF session.connType = CONN_TELNET OR (session.connType = CONN_SECURE AND session.secureNet) THEN + text = session.host & ":" & CStr(session.tcpPort) + ELSE + text = ChoiceName$(PORT_NAMES, session.comPort - 1) & " " & CStr(session.bps) & " " & CStr(session.dataBits) & session.parity & CStr(session.stopBits) + END IF + + text = text & " " & ChoiceName$(PROTO_LABELS, xferProto) + + IF capturing THEN + text = text & " Capture" + END IF + + lblInfo.Caption = text +END SUB + + +' Menu items that only make sense in some states. +SUB MenuUpdate + DIM st AS LONG + DIM idle AS BOOLEAN + DIM isOpen AS BOOLEAN + + st = ConnState(conn) + idle = NOT transferring + isOpen = (st = CONN_OPEN) + + mnuConnect.Enabled = idle AND st = CONN_CLOSED + mnuDisconnect.Enabled = idle AND st <> CONN_CLOSED + mnuPhoneBook.Enabled = idle + mnuSend.Enabled = idle AND isOpen + mnuReceive.Enabled = idle AND isOpen + mnuPaste.Enabled = idle AND isOpen + mnuBreak.Enabled = idle AND isOpen AND ConnCanBreak(session.connType) + mnuCapture.Checked = capturing +END SUB + + +' The Transfer menu's protocol items follow the phone book's protocol. +SUB ProtocolChecks + mnuProtoXmodem.Checked = (xferProto = XFER_XMODEM) + mnuProtoXmodem1k.Checked = (xferProto = XFER_XMODEM1K) + mnuProtoYmodem.Checked = (xferProto = XFER_YMODEM) + mnuProtoZmodem.Checked = (xferProto = XFER_ZMODEM) +END SUB + + +' The terminal's colors for a scheme (SCHEME_). +SUB SchemeColors(BYVAL scheme AS LONG) + DIM levels AS VARIANT + DIM tint AS VARIANT + DIM colors AS VARIANT + DIM rgbParts AS VARIANT + DIM i AS LONG + + SELECT CASE scheme + CASE SCHEME_GREEN + tint = Split(GREEN_TINT, CHOICE_SEP) + CASE SCHEME_AMBER + tint = Split(AMBER_TINT, CHOICE_SEP) + CASE SCHEME_LIGHT + colors = Split(LIGHT_COLORS, CHOICE_SEP) + + FOR i = 0 TO ANSI_COLORS - 1 + rgbParts = Split(colors(i), RGB_SEP) + trmMain.SetColor i, RGB(Val(rgbParts(0)), Val(rgbParts(1)), Val(rgbParts(2))) + NEXT i + + EXIT SUB + CASE ELSE + trmMain.ResetColors + EXIT SUB + END SELECT + + levels = Split(MONO_LEVELS, CHOICE_SEP) + + FOR i = 0 TO ANSI_COLORS - 1 + trmMain.SetColor i, RGB((Val(tint(0)) * Val(levels(i))) \ CHANNEL_MAX, (Val(tint(1)) * Val(levels(i))) \ CHANNEL_MAX, (Val(tint(2)) * Val(levels(i))) \ CHANNEL_MAX) + NEXT i +END SUB + + +' The status bar follows the connection, and every change (dialing, +' connected, carrier lost ...) is also noted on the screen and logged. +SUB StatusUpdate + DIM st AS LONG + DIM status AS STRING + + st = ConnState(conn) + + IF ConnStatusSerial(conn) <> statusSerial THEN + statusSerial = ConnStatusSerial(conn) + status = ConnStatus$(conn) + lblStatus.Caption = status + TerminalNote status + ShellLog "Terminal: " & status + END IF + + IF st <> lastState THEN + lastState = st + MenuUpdate + END IF +END SUB + + +' A line from Terminal itself (not from the other side) on the screen. +SUB TerminalNote(BYVAL text AS STRING) + trmMain.Write CRLF & NOTE_START & text & NOTE_END & CRLF +END SUB + + +' ============================================================ +' Capture and transfers +' ============================================================ + +SUB StartCapture + DIM path AS STRING + + path = DlgFileSave$("Capture to File", "") + + IF path = "" THEN + EXIT SUB + END IF + + ' Appends, so an earlier capture of the same name is kept. + capturing = ConnCapture(conn, path) + + IF NOT capturing THEN + MsgBox "The capture file could not be opened.", vbOKOnly + vbCritical, TERM_TITLE + EXIT SUB + END IF + + TerminalNote "Capturing to " & path +END SUB + + +' Receive with proto: XMODEM asks where to save the file, the batch +' protocols store into the download folder (made when it is missing). +SUB ReceiveFiles(BYVAL proto AS LONG) + DIM path AS STRING + + IF transferring OR ConnState(conn) <> CONN_OPEN THEN + EXIT SUB + END IF + + IF proto = XFER_XMODEM OR proto = XFER_XMODEM1K THEN + path = DlgFileSaveIn$("Receive File As", "", downloadDir) + + IF path <> "" THEN + StartTransfer ConnReceiveFiles(conn, proto, path, xferExists, escapeCtl) + END IF + + EXIT SUB + END IF + + IF NOT FileOpsMakePath(downloadDir) THEN + TerminalNote "The download folder " & downloadDir & " could not be created." + EXIT SUB + END IF + + StartTransfer ConnReceiveFiles(conn, proto, downloadDir, xferExists, escapeCtl) +END SUB + + +' Pick files (one at a time; the batch protocols may add more) and send +' them. +SUB SendFiles + DIM files AS STRING + DIM path AS STRING + DIM count AS LONG + DIM batch AS BOOLEAN + DIM title AS STRING + + IF transferring OR ConnState(conn) <> CONN_OPEN THEN + EXIT SUB + END IF + + batch = (xferProto = XFER_YMODEM OR xferProto = XFER_ZMODEM) + title = "Send File" + + DO WHILE count < XFER_MAX_FILES + path = DlgFileOpen$(title, "") + + IF path = "" THEN + EXIT DO + END IF + + IF count > 0 THEN + files = files & FILE_LIST_SEP + END IF + + files = files & path + count = count + 1 + title = "Add Another File" + + IF NOT batch OR count = XFER_MAX_FILES THEN + EXIT DO + END IF + + IF MsgBox("Add another file to send in the same batch?", vbYesNo + vbQuestion, "Send Files") <> vbYes THEN + EXIT DO + END IF + LOOP + + IF count > 0 THEN + StartTransfer ConnSendFiles(conn, xferProto, files, escapeCtl, resumeUploads) + END IF +END SUB + + +' A transfer was started on the connection (handle; 0 when none could +' be): show its window, or say why it could not start. +SUB StartTransfer(BYVAL handle AS LONG) + ' Every transfer handle in use (other programs' too), or no memory + IF handle = 0 THEN + MsgBox "No file transfer can be started now.", vbOKOnly + vbCritical, "File Transfer" + EXIT SUB + END IF + + IF XferStatus(handle) <> XFER_RUNNING THEN + MsgBox XferMessage$(handle), vbOKOnly + vbCritical, "File Transfer" + XferClose handle + EXIT SUB + END IF + + IF xfer <> 0 THEN + XferClose xfer + END IF + + xfer = handle + transferring = True + ProgressOpen + MenuUpdate +END SUB + + +' ============================================================ +' Menus +' ============================================================ + +' The phone book: pick a session to connect with. +SUB mnuPhoneBook_Click + IF BookDialog() THEN + SwitchSession bookChoice + ConnectSession + END IF + + MenuUpdate +END SUB + + +SUB mnuConnect_Click + ConnectSession + MenuUpdate +END SUB + + +SUB mnuDisconnect_Click + ConnClose conn + StatusUpdate + MenuUpdate +END SUB + + +SUB mnuCapture_Click + IF capturing THEN + capturing = ConnCapture(conn, "") + TerminalNote "Capture stopped" + ELSE + StartCapture + END IF + + MenuUpdate + InfoUpdate +END SUB + + +SUB mnuExit_Click + Unload TermForm +END SUB + + +SUB mnuCopy_Click + trmMain.Copy + MenuUpdate +END SUB + + +SUB mnuPaste_Click + trmMain.Paste + MenuUpdate +END SUB + + +SUB mnuClear_Click + trmMain.Clear + MenuUpdate +END SUB + + +SUB mnuBreak_Click + ConnSendBreak conn + MenuUpdate +END SUB + + +' A color scheme from the Colors menu. +SUB ChooseScheme(BYVAL scheme AS LONG) + session.scheme = scheme + ApplyLook + SessionStore + MenuUpdate +END SUB + + +SUB mnuSchemeStandard_Click + ChooseScheme SCHEME_STANDARD +END SUB + + +SUB mnuSchemeGreen_Click + ChooseScheme SCHEME_GREEN +END SUB + + +SUB mnuSchemeAmber_Click + ChooseScheme SCHEME_AMBER +END SUB + + +SUB mnuSchemeLight_Click + ChooseScheme SCHEME_LIGHT +END SUB + + +SUB mnuSession_Click + DIM resize AS BOOLEAN + + dlgSession = session + + IF SessionDialog("Session Settings") THEN + resize = dlgSession.cols <> session.cols OR dlgSession.rows <> session.rows + session = dlgSession + SessionStore + + IF resize THEN + BuildTerminal + ELSE + ApplyLook + END IF + + TermForm.Caption = session.sessionName + InfoUpdate + + IF ConnState(conn) <> CONN_CLOSED THEN + TerminalNote "New connection settings take effect the next time you connect" + END IF + END IF + + MenuUpdate +END SUB + + +SUB mnuEcho_Click + session.localEcho = NOT session.localEcho + ApplyLook + SessionStore + MenuUpdate +END SUB + + +SUB mnuXferOptions_Click + IF XferOptionsDialog() THEN + SaveBook + END IF + + MenuUpdate +END SUB + + +SUB mnuSend_Click + SendFiles + MenuUpdate +END SUB + + +SUB mnuReceive_Click + ReceiveFiles xferProto + MenuUpdate +END SUB + + +' A transfer protocol from the Transfer menu. +SUB ChooseProtocol(BYVAL proto AS LONG) + xferProto = proto + ProtocolChecks + InfoUpdate + SaveBook + MenuUpdate +END SUB + + +SUB mnuProtoXmodem_Click + ChooseProtocol XFER_XMODEM +END SUB + + +SUB mnuProtoXmodem1k_Click + ChooseProtocol XFER_XMODEM1K +END SUB + + +SUB mnuProtoYmodem_Click + ChooseProtocol XFER_YMODEM +END SUB + + +SUB mnuProtoZmodem_Click + ChooseProtocol XFER_ZMODEM +END SUB + + +SUB mnuHelpContents_Click + HelpView HELP_FILE +END SUB + + +SUB mnuAbout_Click + MsgBox ABOUT_TEXT, vbOKOnly + vbInformation, "About Terminal" +END SUB diff --git a/src/apps/kpunch/terminal/terminal.res b/src/apps/kpunch/terminal/terminal.res deleted file mode 100644 index 25df843d..00000000 --- a/src/apps/kpunch/terminal/terminal.res +++ /dev/null @@ -1,7 +0,0 @@ -# terminal.res -- Resource manifest for Terminal -icon32 icon icon32.bmp -name text "Terminal" -author text "Scott Duensing" -copyright text "Copyright 2026 Scott Duensing" -publisher text "Kangaroo Punch Studios" -description text "Serial, modem and secure-link terminal with XMODEM, YMODEM and ZMODEM file transfer" diff --git a/src/apps/kpunch/welcome/icon32.bmp b/src/apps/kpunch/welcome/ICON32.BMP similarity index 100% rename from src/apps/kpunch/welcome/icon32.bmp rename to src/apps/kpunch/welcome/ICON32.BMP diff --git a/src/apps/kpunch/welcome/welcome.c b/src/apps/kpunch/welcome/welcome.c deleted file mode 100644 index c5d763b2..00000000 --- a/src/apps/kpunch/welcome/welcome.c +++ /dev/null @@ -1,213 +0,0 @@ -// 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. - -// welcome.c -- Welcome window shown when DVX starts for the first time -// -// The shell starts the program named by DVX.INI [shell] welcome once the -// desktop is up; the shipped DVX.INI names this one. The window says what -// DVX is and that F1 opens help, and can open the user guide. "Don't show -// this again" starts ticked: closing the window then removes the key, so -// the window is seen once. Unticking it writes the key back with this -// program's path, and the window keeps coming back at every start. - -#include "dvxApp.h" -#include "dvxDlg.h" -#include "dvxPrefs.h" -#include "dvxWgt.h" -#include "dvxWm.h" -#include "dvxPlat.h" -#include "box/box.h" -#include "button/button.h" -#include "checkbox/checkbox.h" -#include "label/label.h" -#include "shellApp.h" - -#include -#include -#include "dvxMem.h" - -// ============================================================ -// Constants -// ============================================================ - -#define WELCOME_TITLE "Welcome to DVX" -#define WELCOME_WIN_W 480 -#define WELCOME_WIN_H 240 -#define WELCOME_SPACING 8 -#define WELCOME_GAP 4 - -// Menu command IDs -#define CMD_HELP_CONTENTS 100 -#define CMD_HELP_ABOUT 101 - -// ============================================================ -// Module state -// ============================================================ - -static DxeAppContextT *sCtx = NULL; -static WindowT *sWin = NULL; -static WidgetT *sDontShow = NULL; - -// ============================================================ -// Prototypes -// ============================================================ - -static void onClose(WindowT *win); -static void onCloseButton(WidgetT *w); -static void onGuide(WidgetT *w); -static void onMenu(WindowT *win, int32_t menuId); -static void saveChoice(void); -int32_t appMain(DxeAppContextT *ctx); - -// ============================================================ -// App descriptor -// ============================================================ - -SHELL_APP_DESCRIPTOR( - .name = "Welcome", - .hasMainLoop = false, - .multiInstance = false, - .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_NORMAL -); - - -// Every way of closing the window (Close, the close gadget, Alt+F4) records -// the choice first. -static void onClose(WindowT *win) { - saveChoice(); - dvxDestroyWindow(sCtx->shellCtx, win); - sWin = NULL; -} - - -static void onCloseButton(WidgetT *w) { - (void)w; - onClose(sWin); -} - - -// Empty arguments open the system help: the DVX user guide. -static void onGuide(WidgetT *w) { - (void)w; - shellOpenHelp(sCtx->shellCtx, ""); -} - - -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 Welcome", "Welcome\n\nIntroduces DVX when it starts for the first time.\n\nCopyright 2026 Scott Duensing"); - break; - - default: - break; - } -} - - -// "Don't show this again" removes [shell] welcome; otherwise the key names -// this program, so the shell starts it again next time. -static void saveChoice(void) { - PrefsHandleT *ini = prefsLoad(DVX_INI_PATH); - - if (!ini) { - return; - } - - bool again = !wgtCheckboxIsChecked(sDontShow); - - if (again) { - prefsSetString(ini, SHELL_INI_SECTION, SHELL_INI_WELCOME, sCtx->appPath); - } else { - prefsRemove(ini, SHELL_INI_SECTION, SHELL_INI_WELCOME); - } - - dvxLog("Welcome: %s at the next start", again ? "shown again" : "not shown"); - - if (!prefsSave(ini)) { - dvxLog("Welcome: could not save %s", DVX_INI_PATH); - } - - prefsClose(ini); -} - - -int32_t appMain(DxeAppContextT *ctx) { - sCtx = ctx; - - AppContextT *ac = ctx->shellCtx; - - sWin = dvxCreateWindowCentered(ac, WELCOME_TITLE, WELCOME_WIN_W, WELCOME_WIN_H, false); - - if (!sWin) { - return -1; - } - - sWin->onClose = onClose; - sWin->onMenu = onMenu; - - 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 Welcome...", CMD_HELP_ABOUT); - } - - WidgetT *root = wgtInitWindow(ac, sWin); - - root->spacing = wgtPixels(WELCOME_SPACING); - - WidgetT *text = wgtVBox(root); - - text->spacing = wgtPixels(WELCOME_GAP); - wgtLabel(text, "Welcome to DVX!"); - wgtLabel(text, "DVX is a desktop for your DOS computer."); - wgtLabel(text, "Start programs from Program Manager and work in windows."); - wgtLabel(text, "Press F1 at any time for help."); - - sDontShow = wgtCheckbox(root, "&Don't show this again"); - wgtCheckboxSetChecked(sDontShow, true); - - WidgetT *btnRow = wgtHBox(root); - - btnRow->align = AlignEndE; - btnRow->spacing = wgtPixels(WELCOME_SPACING); - - WidgetT *guideBtn = wgtButton(btnRow, "Open the &User Guide"); - WidgetT *closeBtn = wgtButton(btnRow, "&Close"); - - guideBtn->onClick = onGuide; - closeBtn->onClick = onCloseButton; - - dvxFitWindow(ac, sWin); - wgtSetFocused(closeBtn); - return 0; -} diff --git a/src/apps/kpunch/welcome/welcome.dbp b/src/apps/kpunch/welcome/welcome.dbp new file mode 100644 index 00000000..e966c03c --- /dev/null +++ b/src/apps/kpunch/welcome/welcome.dbp @@ -0,0 +1,20 @@ +[Project] +Name = Welcome +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Introduces DVX when it starts for the first time +Icon = ICON32.BMP + +[Modules] +File0 = ../../../include/basic/help.bas +File1 = ../../../include/basic/shell.bas +Count = 2 + +[Forms] +File0 = welcome.frm +Count = 1 + +[Settings] +StartupForm = WelcomeForm diff --git a/src/apps/kpunch/welcome/welcome.frm b/src/apps/kpunch/welcome/welcome.frm new file mode 100644 index 00000000..755d87d6 --- /dev/null +++ b/src/apps/kpunch/welcome/welcome.frm @@ -0,0 +1,146 @@ +VERSION DVX 1.00 +Begin Form WelcomeForm + Caption = "Welcome to DVX" + Layout = VBox + AutoSize = True + Resizable = False + Centered = False + Spacing = 8 + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuHelpContents + Caption = "&Contents" + ShortcutText = "F1" + End + Begin Menu mnuHelpSep + Caption = "-" + End + Begin Menu mnuAbout + Caption = "&About Welcome..." + End + End + Begin VBox boxText + Begin Label lblWelcome + Caption = "Welcome to DVX!" + End + Begin Label lblWhat + Caption = "DVX is a desktop for your DOS computer." + End + Begin Label lblStart + Caption = "Start programs from Program Manager and work in windows." + End + Begin Label lblHelp + Caption = "Press F1 at any time for help." + End + End + Begin CheckBox chkDontShow + Caption = "&Don't show this again" + Value = True + End + Begin HBox boxButtons + Alignment = Right + Spacing = 8 + Begin CommandButton cmdGuide + Caption = "Open the &User Guide" + End + Begin CommandButton cmdClose + Caption = "&Close" + End + End +End + + +' 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. + + +' welcome.frm -- the Welcome window DVX shows when it starts for the first +' time. +' +' The shell starts the program DVX.INI [shell] welcome names once the +' desktop is up; the DVX.INI DVX ships with names this one. The window +' says what DVX is and that F1 opens help, and can open the user guide. +' "Don't show this again" starts ticked: closing the window then removes +' the setting, so the window is seen once. Unticking it writes the setting +' back with this program's path, and the window comes back at every start. + +OPTION EXPLICIT + +CONST HELP_FILE = "welcome.hlp" + +' The window opens where a WELCOME_W x WELCOME_H window would be centred, +' then fits its contents (as the C Welcome did). +CONST WELCOME_W = 480 +CONST WELCOME_H = 240 + + +SUB WelcomeForm_Load + WelcomeForm.Left = (Screen.Width - WELCOME_W) \ 2 + WelcomeForm.Top = (Screen.Height - WELCOME_H) \ 2 + cmdClose.SetFocus +END SUB + + +' Every way of closing the window (Close, the close box, Alt+F4) records +' the choice first. +SUB WelcomeForm_Unload + SaveChoice +END SUB + + +' "Don't show this again" removes [shell] welcome; otherwise it names this +' program, so DVX starts it again next time. +SUB SaveChoice + IF chkDontShow.Value THEN + ShellLog "Welcome: not shown at the next start" + ShellSetWelcome "" + ELSE + ShellLog "Welcome: shown again at the next start" + ShellSetWelcome ShellAppPath$() + END IF +END SUB + + +SUB cmdGuide_Click + ' No file name: the DVX user guide + HelpView "" +END SUB + + +SUB cmdClose_Click + Unload WelcomeForm +END SUB + + +SUB mnuHelpContents_Click + HelpView HELP_FILE +END SUB + + +SUB mnuAbout_Click + DIM msg AS STRING + + msg = "Welcome" + CHR$(10) + CHR$(10) + msg = msg + "Introduces DVX when it starts for the first time." + CHR$(10) + CHR$(10) + msg = msg + "Copyright 2026 Scott Duensing" + MsgBox msg, vbOKOnly + vbInformation, "About Welcome" +END SUB diff --git a/src/apps/kpunch/welcome/welcome.res b/src/apps/kpunch/welcome/welcome.res deleted file mode 100644 index d70589d2..00000000 --- a/src/apps/kpunch/welcome/welcome.res +++ /dev/null @@ -1,7 +0,0 @@ -# welcome.res -- Resource manifest for Welcome -icon32 icon icon32.bmp -name text "Welcome" -author text "Scott Duensing" -copyright text "Copyright 2026 Scott Duensing" -publisher text "Kangaroo Punch Studios" -description text "Introduces DVX when it starts for the first time" diff --git a/src/include/basic/comm.bas b/src/include/basic/comm.bas index 1fbae76c..7529c2f5 100644 --- a/src/include/basic/comm.bas +++ b/src/include/basic/comm.bas @@ -39,6 +39,8 @@ ' passphrase to CommHandshake on both ends to also authenticate ' the peer; without one the link is private but unauthenticated. ' +' SerWrite, SerRead$, CommSend and CommRecv$ carry any bytes, CHR$(0) too. +' ' Usage: Add this file to your project. ' UART types (returned by SerGetUart) @@ -54,6 +56,47 @@ CONST SER_HANDSHAKE_XONXOFF = 1 CONST SER_HANDSHAKE_RTSCTS = 2 CONST SER_HANDSHAKE_DTRDSR = 3 +' Why a Ser call failed (SerLastError; the getters return them too) +CONST SER_OK = 0 +CONST SER_ERR_UNKNOWN = -1 +CONST SER_ERR_NOT_OPEN = -2 +CONST SER_ERR_ALREADY_OPEN = -3 +CONST SER_ERR_NO_UART = -4 +CONST SER_ERR_INVALID_PORT = -5 +CONST SER_ERR_INVALID_BASE = -6 +CONST SER_ERR_INVALID_IRQ = -7 +CONST SER_ERR_INVALID_BAUD = -8 +CONST SER_ERR_INVALID_DATA = -9 +CONST SER_ERR_INVALID_PARITY = -10 +CONST SER_ERR_INVALID_STOP = -11 +CONST SER_ERR_INVALID_HANDSHAKE = -12 +CONST SER_ERR_INVALID_FIFO = -13 +CONST SER_ERR_NULL_PTR = -14 +CONST SER_ERR_IRQ_NOT_FOUND = -15 +CONST SER_ERR_LOCK_MEM = -16 + +' Errors seen on received data (SerLineErrors; add them up) +CONST SER_LINE_OVERRUN = &H02 +CONST SER_LINE_PARITY = &H04 +CONST SER_LINE_FRAMING = &H08 +CONST SER_LINE_BREAK = &H10 + +' Results of the packet and encrypted calls (CommLastError, +' CommHandshakeStep) +CONST COMM_OK = 0 +CONST COMM_ERR_PARAM = -1 +CONST COMM_ERR_SERIAL = -2 +CONST COMM_ERR_ALLOC = -3 +CONST COMM_ERR_HANDSHAKE = -4 +CONST COMM_ERR_NOT_READY = -5 +CONST COMM_ERR_SEND = -6 +CONST COMM_ERR_TIMEOUT = -7 +CONST COMM_ERR_AUTH = -8 +CONST COMM_PENDING = 1 + +' The highest channel number (channels are 0 to COMM_MAX_CHANNEL) +CONST COMM_MAX_CHANNEL = 127 + DECLARE LIBRARY "basrt" ' ========================================================= @@ -81,17 +124,35 @@ DECLARE LIBRARY "basrt" ' Raw Serial I/O (no framing, no error correction) ' ========================================================= - ' Open a raw serial port. Returns True on success. + ' Open a raw serial port. Returns True on success; SerLastError says + ' why not. DECLARE FUNCTION SerOpen(BYVAL com AS LONG, BYVAL baud AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL handshake AS LONG) AS LONG + ' Change the speed, data bits, parity, stop bits and handshake of an + ' open port (the same arguments as SerOpen). Returns True on success. + DECLARE FUNCTION SerSet(BYVAL com AS LONG, BYVAL baud AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL handshake AS LONG) AS LONG + + ' Why the last SerOpen or SerSet on the port failed (an SER_ERR_ + ' constant), SER_OK when it worked. + DECLARE FUNCTION SerLastError(BYVAL com AS LONG) AS LONG + + ' The settings of an open port: speed, data bits, parity ("N", "E", + ' "O", "M" or "S"), stop bits and handshake (SER_HANDSHAKE_). When the + ' port is not open the numbers are an SER_ERR_ code and the parity "". + DECLARE FUNCTION SerGetBaud(BYVAL com AS LONG) AS LONG + DECLARE FUNCTION SerGetDataBits(BYVAL com AS LONG) AS LONG + DECLARE FUNCTION SerGetParity$(BYVAL com AS LONG) AS STRING + DECLARE FUNCTION SerGetStopBits(BYVAL com AS LONG) AS LONG + DECLARE FUNCTION SerGetHandshake(BYVAL com AS LONG) AS LONG + ' Close a raw serial port. DTR and RTS keep their state. DECLARE SUB SerClose(BYVAL com AS LONG) ' Write a string to the serial port. Returns True on success. - DECLARE FUNCTION SerWrite(BYVAL com AS LONG, BYVAL payload AS STRING) AS LONG + DECLARE FUNCTION SerWrite(BYVAL com AS LONG, BYVAL payload AS BINARY STRING) AS LONG ' Read pending data from the serial port. Returns "" if none available. - DECLARE FUNCTION SerRead$(BYVAL com AS LONG) AS STRING + DECLARE FUNCTION SerRead$(BYVAL com AS LONG) AS BINARY STRING ' Return the number of bytes waiting in the receive buffer. DECLARE FUNCTION SerAvailable(BYVAL com AS LONG) AS LONG @@ -99,6 +160,16 @@ DECLARE LIBRARY "basrt" ' Clear the receive buffer. DECLARE SUB SerFlush(BYVAL com AS LONG) + ' Bytes written but not sent yet, and throwing them away. + DECLARE FUNCTION SerTxPending(BYVAL com AS LONG) AS LONG + DECLARE SUB SerFlushTx(BYVAL com AS LONG) + + ' The errors the port saw on received data when it last looked: + ' SER_LINE_OVERRUN (data came faster than it was read), + ' SER_LINE_PARITY, SER_LINE_FRAMING, SER_LINE_BREAK (the other end + ' sent a break), added up; 0 for none. + DECLARE FUNCTION SerLineErrors(BYVAL com AS LONG) AS LONG + ' Attach a raw serial port to a Terminal widget. DECLARE SUB SerAttach(BYVAL com AS LONG, BYVAL termCtrlName AS STRING) @@ -124,6 +195,13 @@ DECLARE LIBRARY "basrt" DECLARE FUNCTION SerGetDsr(BYVAL com AS LONG) AS LONG DECLARE FUNCTION SerGetDcd(BYVAL com AS LONG) AS LONG + ' True while the other end rings (the ring indicator line). + DECLARE FUNCTION SerGetRi(BYVAL com AS LONG) AS LONG + + ' Send a break: hold the line in the break state (True), then release + ' it (False). + DECLARE SUB SerSetBreak(BYVAL com AS LONG, BYVAL state AS LONG) + ' ========================================================= ' Packet Serial (reliable delivery + channel multiplexing) ' ========================================================= @@ -134,12 +212,12 @@ DECLARE LIBRARY "basrt" ' Close a packet serial link. DECLARE SUB CommClose(BYVAL handle AS LONG) - ' Send data on a logical channel (0-127). + ' Send data on a logical channel (0 to COMM_MAX_CHANNEL). ' encrypt: True to encrypt (requires CommHandshake first). - DECLARE FUNCTION CommSend(BYVAL handle AS LONG, BYVAL payload AS STRING, BYVAL channel AS LONG, BYVAL encrypt AS LONG) AS LONG + DECLARE FUNCTION CommSend(BYVAL handle AS LONG, BYVAL payload AS BINARY STRING, BYVAL channel AS LONG, BYVAL encrypt AS LONG) AS LONG ' Receive pending data from a specific channel. Returns "" if none. - DECLARE FUNCTION CommRecv$(BYVAL handle AS LONG, BYVAL channel AS LONG) AS STRING + DECLARE FUNCTION CommRecv$(BYVAL handle AS LONG, BYVAL channel AS LONG) AS BINARY STRING ' True when every byte the last CommRecv$ on that channel returned came ' encrypted and authenticated; False when any came unencrypted (only @@ -153,6 +231,16 @@ DECLARE LIBRARY "basrt" ' Return the number of unacknowledged packets in the transmit window. DECLARE FUNCTION CommPending(BYVAL handle AS LONG) AS LONG + ' The COMM_ result of the last CommHandshake, CommHandshakeStep, + ' CommSend, CommSetPlain or CommPoll on the link: COMM_OK when it + ' worked, COMM_ERR_AUTH for a passphrase mismatch, ... + DECLARE FUNCTION CommLastError(BYVAL handle AS LONG) AS LONG + + ' How many arriving packets were thrown away because they failed the + ' tamper check or came unencrypted where that is refused (-1 for a + ' bad handle). + DECLARE FUNCTION CommDropped(BYVAL handle AS LONG) AS LONG + ' ========================================================= ' Encryption (authenticated key exchange) ' ========================================================= @@ -163,6 +251,13 @@ DECLARE LIBRARY "basrt" ' mismatch, when only one side uses a passphrase, or on a failed link. DECLARE FUNCTION CommHandshake(BYVAL handle AS LONG, OPTIONAL BYVAL passphrase AS STRING) AS LONG + ' The same key exchange without waiting: call it from a Timer until it + ' returns something other than COMM_PENDING -- COMM_OK when the link is + ' ready, else the COMM_ERR_ code that ended it (COMM_ERR_AUTH for a + ' passphrase mismatch). The passphrase is taken when an exchange + ' starts. + DECLARE FUNCTION CommHandshakeStep(BYVAL handle AS LONG, OPTIONAL BYVAL passphrase AS STRING) AS LONG + ' After the handshake, unencrypted data is refused on every channel ' unless that channel is opened for it here (plain = True). Returns ' True on success. diff --git a/src/include/basic/commdlg.bas b/src/include/basic/commdlg.bas index 9c621326..dc798417 100644 --- a/src/include/basic/commdlg.bas +++ b/src/include/basic/commdlg.bas @@ -44,20 +44,39 @@ DECLARE LIBRARY "basrt" ' filter$ uses the same format as DlgFileOpen$. DECLARE FUNCTION DlgFileSave$(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING + ' The same dialogs, starting in folder$ instead of the current folder + ' ("" = the current folder). + DECLARE FUNCTION DlgFileOpenIn$(BYVAL title AS STRING, BYVAL filter AS STRING, BYVAL folder AS STRING) AS STRING + DECLARE FUNCTION DlgFileSaveIn$(BYVAL title AS STRING, BYVAL filter AS STRING, BYVAL folder AS STRING) AS STRING + ' Show a modal text input box. Returns entered text, or "" if cancelled. DECLARE FUNCTION DlgInputBox$(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultText AS STRING) AS STRING - ' Returns True if the most recent DlgInputBox$ call was cancelled, - ' False if the user clicked OK (even with an empty field). + ' Returns True if the most recent DlgInputBox$, DlgIntInput or + ' DlgIntInputStep call was cancelled, False if the user clicked OK + ' (even with an empty field or the default number). DECLARE FUNCTION DlgInputCancelled() AS INTEGER + ' Show a message box with your own buttons. buttons$ is + ' pipe-delimited and & marks each button's access key, e.g. + ' "&Yes|&No|Yes to &All|&Cancel" (1 to 6 buttons). icon is vbQuestion, + ' vbInformation, vbExclamation, vbCritical or 0. Returns the chosen + ' button's index (0 = the first, which Enter presses; Esc and closing + ' the box choose the last), or -1 if buttons$ is empty or too long. + DECLARE FUNCTION DlgButtons(BYVAL title AS STRING, BYVAL message AS STRING, BYVAL buttons AS STRING, BYVAL icon AS LONG) AS INTEGER + ' Show a choice dialog with a listbox. items$ is pipe-delimited - ' (e.g. "Red|Green|Blue"). Returns chosen index (0-based), or -1. + ' (e.g. "Red|Green|Blue"), as many items as you like (an index of + ' hundreds of keywords fits). Returns chosen index (0-based), or -1. DECLARE FUNCTION DlgChoice(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL items AS STRING, BYVAL defaultIdx AS INTEGER) AS INTEGER - ' Show a modal integer input box with spinner. Returns the entered value. + ' Show a modal integer input box with spinner. Returns the entered + ' value, or defaultVal if cancelled (DlgInputCancelled tells which). DECLARE FUNCTION DlgIntInput(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultVal AS LONG, BYVAL minVal AS LONG, BYVAL maxVal AS LONG) AS LONG + ' DlgIntInput with the spinner arrows moving by stepVal. + DECLARE FUNCTION DlgIntInputStep(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultVal AS LONG, BYVAL minVal AS LONG, BYVAL maxVal AS LONG, BYVAL stepVal AS LONG) AS LONG + ' Show the color dialog: basic colors, Red/Green/Blue sliders and a ' preview, starting at defaultColor. Returns the chosen color as ' RGB() makes it (ForeColor/BackColor use the same form), or -1 if diff --git a/src/include/basic/compiler.bas b/src/include/basic/compiler.bas new file mode 100644 index 00000000..8fcd0ddc --- /dev/null +++ b/src/include/basic/compiler.bas @@ -0,0 +1,312 @@ +' 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. + +' compiler.bas -- the DVX BASIC compiler for DVX BASIC programs +' +' Compiles BASIC programs and projects, maps compiled line numbers back +' to files, lists procedures and keywords, reads source text the way +' the compiler does (tokens, strings and comments, renaming a control in +' code, WithEvents variables, class Events), knows the help topic of a +' word, edits project files (.dbp), builds .app files and control files +' (.wgt), and puts a project's UserControls on a form designer's +' toolbox: what a development tool written in BASIC needs. Run and +' debug a compiled program with debug.bas. +' +' A source set collects the files of a program in compile order -- the +' modules (.bas), then the class modules (.cls), then the UserControls +' (.ctl), then the forms (.frm), the order a project compiles in -- or +' is read from a project (.dbp). +' Compiling it gives a module. Errors and line numbers refer to the +' concatenated source of the set; CompMapFile$ / CompMapLine turn such a +' line into a file and line, CompConcatLine goes the other way. +' +' Lists are rows ready for ListView.SetRows: one row per line, the cells +' separated by TAB characters. +' +' Source sets, modules and projects are numbers (0 means the call +' failed); free each when done. Whatever a program leaves open is freed +' when it ends. A project (ProjOpen / ProjNew) is the project file +' (.dbp) as the DVX BASIC IDE edits it: its files and its settings. +' ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$ and ProjCtlKeys$ make a +' UserControl (.ctl) and read and change its header the way the IDE's +' File > New UserControl and Properties window do. +' +' Usage: Add this file to your project. + +' CompSourceAdd kinds +CONST COMP_KIND_AUTO = -1 ' by the file name: .frm a form, .cls a class module, .ctl a UserControl, else a module +CONST COMP_KIND_PROGRAM = 0 ' a whole program in one file (not part of a project) +CONST COMP_KIND_MODULE = 1 +CONST COMP_KIND_FORM = 2 +CONST COMP_KIND_CLASS = 3 +CONST COMP_KIND_CONTROL = 4 ' a UserControl (.ctl): its header, then its class code + +' CompKeywordClass (-1: no such keyword) +CONST COMP_KW_OTHER = 0 ' a statement, operator or other keyword +CONST COMP_KW_TYPE = 1 ' a data type (INTEGER, STRING, VARIANT ...) +CONST COMP_KW_LITERAL = 2 ' a value (TRUE, FALSE, NOTHING, EMPTY, NULL) + +DECLARE LIBRARY "basdev" + + ' ========================================================= + ' Source sets + ' ========================================================= + + ' A new, empty source set. + DECLARE FUNCTION CompSourceCreate() AS LONG + + ' Append one file: path names it (and gives its kind with + ' COMP_KIND_AUTO), text is its whole text -- for a form the whole + ' .frm, whose code after the form block is compiled. True when added; + ' CompError$ says why not. + DECLARE FUNCTION CompSourceAdd(BYVAL src AS LONG, BYVAL path AS STRING, BYVAL text AS STRING, BYVAL kind AS LONG) AS LONG + + ' Replace the set with the files of the project dbpPath, read from + ' disk. Its Option Explicit, startup form, name and help file come + ' along. True when every file could be read. + DECLARE FUNCTION CompSourceFromProject(BYVAL src AS LONG, BYVAL dbpPath AS STRING) AS LONG + + ' The files of the set in compile order, one row each: the path as + ' the line mapping below names it, the file line its code starts on + ' and the line of the concatenated source that line is. + DECLARE FUNCTION CompSourceFiles$(BYVAL src AS LONG) AS STRING + + ' The concatenated source the compiler reads. + DECLARE FUNCTION CompSourceText$(BYVAL src AS LONG) AS STRING + + DECLARE SUB CompSourceFree(BYVAL src AS LONG) + + ' The file line lineNum of the concatenated source comes from ("" for + ' a line the set adds itself, such as a form's BEGINFORM line). + DECLARE FUNCTION CompMapFile$(BYVAL src AS LONG, BYVAL lineNum AS LONG) AS STRING + + ' The line in that file (0 when there is none). + DECLARE FUNCTION CompMapLine(BYVAL src AS LONG, BYVAL lineNum AS LONG) AS LONG + + ' The line of the concatenated source holding line fileLine of file + ' path (0 when it is not compiled code, e.g. a form's layout). The + ' path is compared without regard to case, with / and \ alike. + DECLARE FUNCTION CompConcatLine(BYVAL src AS LONG, BYVAL path AS STRING, BYVAL fileLine AS LONG) AS LONG + + ' ========================================================= + ' Compiling + ' ========================================================= + + ' Compile the set into a module (0 on an error). optionExplicit + ' requires DIM before use everywhere (a project's own setting also + ' counts). validate checks Control.Property and Control.Method names + ' against the forms' controls, and control types against the loaded + ' controls, so typos fail here instead of when the line runs. + DECLARE FUNCTION CompCompile(BYVAL src AS LONG, BYVAL optionExplicit AS LONG, BYVAL validate AS LONG) AS LONG + + ' Why the last compiler call failed (without a "Line n" prefix). + DECLARE FUNCTION CompError$() AS STRING + + ' Where the last compile error is: line and column of the + ' concatenated source, and the file and line in that file. + DECLARE FUNCTION CompErrorLine() AS LONG + DECLARE FUNCTION CompErrorCol() AS LONG + DECLARE FUNCTION CompErrorFile$() AS STRING + DECLARE FUNCTION CompErrorFileLine() AS LONG + + ' The module's SUBs and FUNCTIONs, one row each: name, "Sub" or + ' "Function", and the form whose code declares it ("" for none). + DECLARE FUNCTION CompProcs$(BYVAL module AS LONG) AS STRING + + ' Free a module (a running program keeps it until it ends). + DECLARE SUB CompModuleFree(BYVAL module AS LONG) + + ' Build the application outPath (.app) from a module and its source + ' set: a project's name, icon, help file and the rest of its + ' [Project] settings, its forms and its UserControls. A control + ' project's set (Type = Control) builds a control file instead + ' (outPath NAME.wgt, the IDE's Make Control): its UserControls with + ' their toolbox pictures and names, for every program once it is + ' copied into DVX's WIDGETS folder (DVX loads it when it starts). + ' release leaves out the debugging information and shortens names. + ' True when built; CompError$ says why not. + DECLARE FUNCTION CompBuildApp(BYVAL module AS LONG, BYVAL src AS LONG, BYVAL outPath AS STRING, BYVAL release AS LONG) AS LONG + + ' Make the UserControls of a source set (its .ctl files), compiled in + ' module, control types of this program's form designer, as the DVX + ' BASIC IDE does with the project it edits: the designer.bas lists + ' (CtlTypeName$, CtlProp*, CtlEventList$ ...) and CreateControl and + ' LoadFormText then know them, for this program alone. Their code + ' runs on the forms -- not the module-level code -- with + ' UserControl.UserMode False on a form in DesignMode, App.Path appDir + ' ("": the project's folder, else this program's), which also holds + ' their ToolboxIcon files. The ones made before are let go first. + ' The module, one this program compiled, is kept until they go, even + ' past CompModuleFree. The number made; CompError$ says why when not + ' every one was (a control type of its name is installed already). + ' Compile the set with or without its forms: a designer usually + ' leaves them out. + DECLARE FUNCTION CompDesignControls(BYVAL module AS LONG, BYVAL src AS LONG, BYVAL appDir AS STRING) AS LONG + + ' Let them go, when the project closes (CompDesignControls lets the + ' ones before go itself, and so does the end of the program). + ' Controls of them still on forms keep their last picture. False, + ' with CompError$ saying why, while their code runs. + DECLARE FUNCTION CompDesignRelease() AS LONG + + ' ========================================================= + ' Source text + ' ========================================================= + + ' The SUB and FUNCTION blocks of source text, one row each: "Sub" or + ' "Function", the name, the first and the last line (1-based). Works + ' on code that does not compile. + DECLARE FUNCTION CompProcList$(BYVAL source AS STRING) AS STRING + + ' True when ident can name a variable, procedure or control. + DECLARE FUNCTION CompIsIdent(BYVAL ident AS STRING) AS LONG + + ' The language's keywords: CompKeyword$(0) to + ' CompKeyword$(CompKeywordCount() - 1). + DECLARE FUNCTION CompKeywordCount() AS LONG + DECLARE FUNCTION CompKeyword$(BYVAL index AS LONG) AS STRING + + ' What kind of word keyword index is (COMP_KW_*), and whether it is + ' reserved: False for the words a program may also use as its own + ' names (the type names, Empty, the graphics statements ...), which an + ' editor still colours as keywords. + DECLARE FUNCTION CompKeywordClass(BYVAL index AS LONG) AS LONG + DECLARE FUNCTION CompKeywordReserved(BYVAL index AS LONG) AS LONG + + ' The tokens of source text as the compiler reads them, one row each: + ' line, column, length, kind and the text. The kinds are Keyword, + ' Identifier, String (quotes included), Number, Date, Symbol, Comment + ' (' or REM to the end of the line) and Error (text the compiler + ' cannot read, such as a string without its closing quote). Line + ' ends are not listed. + DECLARE FUNCTION CompTokens$(BYVAL source AS STRING) AS STRING + + ' True when character position (1 for the first, as INSTR counts) of + ' source is code: not inside a string or a comment. + DECLARE FUNCTION CompInCode(BYVAL source AS STRING, BYVAL position AS LONG) AS LONG + + ' source with the form or control oldName renamed to newName where + ' code refers to it: oldName as a whole word followed by "." or "_" + ' (Command1.Caption, Command1_Click, Form1.Command1.Text). codeOnly + ' leaves strings and comments alone. The text unchanged when nothing + ' refers to oldName. Menu items are renamed the same way. + DECLARE FUNCTION CompRenameInCode$(BYVAL source AS STRING, BYVAL oldName AS STRING, BYVAL newName AS STRING, BYVAL codeOnly AS LONG) AS STRING + + ' The WithEvents variables source declares, one row each: name and + ' class. Their event procedures are name_EventName. + DECLARE FUNCTION CompWithEvents$(BYVAL source AS STRING) AS STRING + + ' The Events a class module declares, one row each: name and + ' parameter list (e.g. "Value As Integer"; "" for none). + DECLARE FUNCTION CompClassEvents$(BYVAL classText AS STRING) AS STRING + + ' The topic of the DVX BASIC help (dvxbasic.hlp beside the IDE) that + ' explains word, a keyword or built-in function ("" when there is + ' none): what F1 opens in the IDE's code window. Open it with + ' HelpViewTopic (help.bas). + DECLARE FUNCTION CompHelpTopic$(BYVAL word AS STRING) AS STRING + + ' The help topic of a control type, e.g. "ctrl.commandbutton". + DECLARE FUNCTION CompHelpCtrlTopic$(BYVAL typeName AS STRING) AS STRING + + ' ========================================================= + ' Projects (.dbp) + ' ========================================================= + + ' Open a project file; 0 when it does not exist or cannot be read. + DECLARE FUNCTION ProjOpen(BYVAL dbpPath AS STRING) AS LONG + + ' A new, empty project called name in folder; ProjSave writes it as + ' name.dbp in that folder. + DECLARE FUNCTION ProjNew(BYVAL projectName AS STRING, BYVAL folder AS STRING) AS LONG + + ' Write the project file (the old one is replaced only once the new + ' one is complete). True when written; CompError$ says why not. + DECLARE FUNCTION ProjSave(BYVAL project AS LONG) AS LONG + + DECLARE SUB ProjClose(BYVAL project AS LONG) + + ' The project file's path. + DECLARE FUNCTION ProjPath$(BYVAL project AS LONG) AS STRING + + ' Its files, numbered from 0: the name as the project lists it + ' (relative to the project's folder), the full path, and the kind + ' (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or + ' COMP_KIND_CONTROL). + DECLARE FUNCTION ProjFileCount(BYVAL project AS LONG) AS LONG + DECLARE FUNCTION ProjFile$(BYVAL project AS LONG, BYVAL index AS LONG) AS STRING + DECLARE FUNCTION ProjFilePath$(BYVAL project AS LONG, BYVAL index AS LONG) AS STRING + DECLARE FUNCTION ProjFileKind(BYVAL project AS LONG, BYVAL index AS LONG) AS LONG + + ' The index of a file by its name (any case), or -1. + DECLARE FUNCTION ProjFileFind(BYVAL project AS LONG, BYVAL fileName AS STRING) AS LONG + + ' Add a file of the project's folder (its kind from its extension: .bas, + ' .cls, .ctl or .frm). Its index, or -1 when it is already in the + ' project. + DECLARE FUNCTION ProjAddFile(BYVAL project AS LONG, BYVAL fileName AS STRING) AS LONG + + ' Take a file out of the project (the file itself stays). + DECLARE FUNCTION ProjRemoveFile(BYVAL project AS LONG, BYVAL index AS LONG) AS LONG + + ' Rename a file on disk and in the project together; the project file + ' is saved at once. False with CompError$ saying why (an existing + ' file is never overwritten). + DECLARE FUNCTION ProjRenameFile(BYVAL project AS LONG, BYVAL index AS LONG, BYVAL newName AS STRING) AS LONG + + ' A setting by its name in the project file: Name, Type ("Control" + ' for a control project, which builds a control file; empty for a + ' program), Author, Publisher, Version, Copyright, Description, Icon, + ' HelpFile, CommandArgs (the IDE's COMMAND$ for runs), StartupForm, + ' OptionExplicit and MultiInstance (these two read "true" or "false"; + ' set them with "true" / "false", True or False). ProjSet returns + ' False for a name it does not know. + DECLARE FUNCTION ProjGet$(BYVAL project AS LONG, BYVAL setting AS STRING) AS STRING + DECLARE FUNCTION ProjSet(BYVAL project AS LONG, BYVAL setting AS STRING, BYVAL value AS STRING) AS LONG + + ' The header of a UserControl's text (a .ctl file's): the value of + ' one of its keys -- Name (the control's name, on its Begin + ' UserControl line), Width, Height, CanGetFocus, BackColor, + ' ToolboxIcon, Description, DefaultEvent -- as the control takes it + ' (the default when the header leaves it out; CanGetFocus True or + ' False, BackColor &HBBGGRR& or empty for none). Empty for a text + ' that has no header or a name that is not a key. + DECLARE FUNCTION ProjCtlHeader$(BYVAL text AS STRING, BYVAL key AS STRING) AS STRING + + ' The header keys, one row each in the order the IDE's Properties + ' window lists them: the key and the kind of value it takes -- Name + ' (the control's name), Integer, Boolean, Color (&HBBGGRR&) or Text. + DECLARE FUNCTION ProjCtlKeys$() AS STRING + + ' The text of a new UserControl called name, as the IDE's File > New + ' UserControl makes it: its header with Width and Height, Option + ' Explicit and an empty UserControl_Paint. Empty, with CompError$ + ' saying why, when the name is not a valid name. + DECLARE FUNCTION ProjCtlNew$(BYVAL controlName AS STRING) AS STRING + + ' The text with a header key set to value (its line changed, or added + ' before the header's End; an empty value removes the line and the + ' default applies; Name renames the control and cannot be empty). + ' The rest of the text stays as it is. Empty, with CompError$ saying + ' why, when the key or the value does not suit. + DECLARE FUNCTION ProjCtlSetHeader$(BYVAL text AS STRING, BYVAL key AS STRING, BYVAL value AS STRING) AS STRING +END DECLARE diff --git a/src/include/basic/conn.bas b/src/include/basic/conn.bas new file mode 100644 index 00000000..3f83822c --- /dev/null +++ b/src/include/basic/conn.bas @@ -0,0 +1,224 @@ +' 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. + +' conn.bas -- Connections Library for DVX BASIC +' +' One way to talk to another computer, whatever is in between: a serial +' port, a modem (it dials, redials when the line is busy and notices when +' the carrier is lost), the secure link to the DVX host proxy (encrypted; +' authenticated too with a passphrase), or Telnet over the network. A +' connection is a handle (0 means the call failed): +' +' conn = ConnCreate() +' ConnSetSerial conn, 1, 9600, 8, "N", 1, CONN_FLOW_NONE +' IF ConnOpen(conn, CONN_SERIAL) <> CONN_OK THEN MsgBox ConnStatus$(conn) +' +' Opening only starts: dialing and the key exchange go on while the +' program runs. Call ConnPoll often -- from a Timer with a short Interval +' -- to move the connection along; ConnState tells where it is and +' ConnStatus$ what is happening, or why it ended. +' +' With ConnAttach a Terminal control shows what arrives (ConnPoll puts it +' there) and what is typed in it is sent. ConnPoll also answers ENQ with +' the answerback (ConnSetAnswerback), appends what arrives to a capture +' file (ConnCapture) and notices a ZMODEM sender starting a download +' (ConnZmodemRequest). Without a Terminal, read with ConnRead$. +' +' ConnSendFiles and ConnReceiveFiles start XMODEM, YMODEM or ZMODEM +' transfers on the connection (XFER_ constants and functions: xfer.bas). +' They return transfer handles: follow them with XferStatus, XferBytes, +' ... and free them with XferClose. While a transfer runs, ConnPoll moves +' it along, the Terminal shows nothing and typing is not sent; a +' connection that drops cancels it. +' +' ConnWrite and ConnRead$ carry any bytes, CHR$(0) too. Every connection +' a program leaves open is closed when it ends. +' +' Usage: Add this file (and xfer.bas for transfers) to your project. + +' Connection types (ConnOpen) +CONST CONN_SERIAL = 0 +CONST CONN_MODEM = 1 +CONST CONN_SECURE = 2 +CONST CONN_TELNET = 3 + +' ConnState +CONST CONN_CLOSED = 0 +CONST CONN_OPENING = 1 +CONST CONN_OPEN = 2 + +' ConnOpen results +CONST CONN_OK = 0 +CONST CONN_ERR_PARAM = -1 +CONST CONN_ERR_PORT = -3 +CONST CONN_ERR_HOST = -4 + +' Flow control (ConnSetSerial) +CONST CONN_FLOW_NONE = 0 +CONST CONN_FLOW_XONXOFF = 1 +CONST CONN_FLOW_RTSCTS = 2 + +DECLARE LIBRARY "conn" + + ' ========================================================= + ' Creating, setting up and opening + ' ========================================================= + + ' A closed connection with the defaults: COM1 at 9600 bps, 8 data + ' bits, no parity, 1 stop bit, no flow control; modem setup "ATZ", + ' dial command "ATDT", 3 redials 30 seconds apart; network port 23. + ' Returns a handle, or 0 when out of memory. + DECLARE FUNCTION ConnCreate() AS LONG + + ' The COM port (1-4), speed, data bits (5-8), parity ("N", "E", "O", + ' "M" or "S"), stop bits (1 or 2) and flow control (CONN_FLOW_) for + ' serial ports, modems and the secure link. + DECLARE SUB ConnSetSerial(BYVAL handle AS LONG, BYVAL com AS LONG, BYVAL bps AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL flow AS LONG) + + ' Modems: the setup command sent first ("" for none), the dial + ' command, the number ("" = no dialing: type AT commands yourself), + ' how many times BUSY or NO ANSWER is dialed again and the seconds + ' to wait before each. + DECLARE SUB ConnSetModem(BYVAL handle AS LONG, BYVAL initString AS STRING, BYVAL dialPrefix AS STRING, BYVAL phone AS STRING, BYVAL redialCount AS LONG, BYVAL redialDelay AS LONG) + + ' Telnet: the host (a name or an address) and port. The secure link + ' goes over the network to host and port when host is not "", else + ' over the COM port. + DECLARE SUB ConnSetHost(BYVAL handle AS LONG, BYVAL host AS STRING, BYVAL port AS LONG) + + ' The secure link's passphrase ("" = none; the host must use the same + ' one, or none). ConnOpen uses it once and forgets it. + DECLARE SUB ConnSetPassphrase(BYVAL handle AS LONG, BYVAL passphrase AS STRING) + + ' The Terminal's columns and rows, for a Telnet host that asks for + ' them (sent again whenever they change). Default 80 x 25. + DECLARE SUB ConnSetWindowSize(BYVAL handle AS LONG, BYVAL cols AS LONG, BYVAL rows AS LONG) + + ' Start connecting as type (CONN_SERIAL ... CONN_TELNET). Returns + ' CONN_OK (the connection is then opening or open) or a CONN_ERR_ + ' code; ConnStatus$ says more. + DECLARE FUNCTION ConnOpen(BYVAL handle AS LONG, BYVAL connType AS LONG) AS LONG + + ' Close the connection (a modem hangs up). It can be opened again. + DECLARE SUB ConnClose(BYVAL handle AS LONG) + + ' Close the connection and free the handle. + DECLARE SUB ConnFree(BYVAL handle AS LONG) + + ' ========================================================= + ' Running + ' ========================================================= + + ' Move the connection along: dialing, the key exchange, sending and + ' receiving, a transfer; what arrived goes to the attached Terminal. + ' Call it often (a Timer with a short Interval). + DECLARE SUB ConnPoll(BYVAL handle AS LONG) + + ' CONN_CLOSED, CONN_OPENING (dialing, setting up) or CONN_OPEN. + DECLARE FUNCTION ConnState(BYVAL handle AS LONG) AS LONG + + ' What is happening ("Dialing 555-1234", "Connected to COM1 at 9600 + ' bps"), or why the connection ended; "" before it was ever opened. + DECLARE FUNCTION ConnStatus$(BYVAL handle AS LONG) AS STRING + + ' A number that changes whenever ConnStatus$ does: compare it with + ' the last one to notice a new status. + DECLARE FUNCTION ConnStatusSerial(BYVAL handle AS LONG) AS LONG + + ' Queue bytes to send (text or any other bytes, CHR$(0) too). Returns + ' the bytes taken (0 while the connection is not open or the send + ' buffer is full). + DECLARE FUNCTION ConnWrite(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS LONG + + ' What arrived (up to 4096 bytes, any byte values), or "" when nothing + ' has; for a connection without a Terminal. + DECLARE FUNCTION ConnRead$(BYVAL handle AS LONG) AS BINARY STRING + + ' Bytes queued to send that have not left yet. + DECLARE FUNCTION ConnPending(BYVAL handle AS LONG) AS LONG + + ' True when ConnSendBreak works for a connection type (serial ports + ' and modems). + DECLARE FUNCTION ConnCanBreak(BYVAL connType AS LONG) AS LONG + + ' Hold the line in the break state for a moment. True when it did. + DECLARE FUNCTION ConnSendBreak(BYVAL handle AS LONG) AS LONG + + ' ========================================================= + ' Terminal controls + ' ========================================================= + + ' Show the connection on the Terminal control called termCtrlName + ' (on any of the program's forms) and send what is typed there. True + ' when it is attached. Unloading the form ends it. + DECLARE FUNCTION ConnAttach(BYVAL handle AS LONG, BYVAL termCtrlName AS STRING) AS LONG + + ' Stop showing the connection on its Terminal. + DECLARE SUB ConnDetach(BYVAL handle AS LONG) + + ' Local echo: what is typed is shown on the Terminal too (Enter as a + ' new line). + DECLARE SUB ConnSetEcho(BYVAL handle AS LONG, BYVAL echo AS LONG) + + ' The reply sent when the other side asks who is there (ENQ, Ctrl+E); + ' "" sends nothing. ENQ itself is not shown. + DECLARE SUB ConnSetAnswerback(BYVAL handle AS LONG, BYVAL text AS STRING) + + ' Append everything that arrives to the file path; "" stops. True + ' while a capture file is open. + DECLARE FUNCTION ConnCapture(BYVAL handle AS LONG, BYVAL path AS STRING) AS LONG + + ' True once after a ZMODEM sender started a download on the Terminal + ' (the start of its frame is not shown); answer with ConnReceiveFiles + ' and XFER_ZMODEM in the same Timer event. + DECLARE FUNCTION ConnZmodemRequest(BYVAL handle AS LONG) AS LONG + + ' ========================================================= + ' File transfers (xfer.bas) + ' ========================================================= + + ' Send files (one path per line; XMODEM sends the first) with proto + ' (XFER_XMODEM ... XFER_ZMODEM). escapeCtl: ZMODEM escapes every + ' control character (for links that lose them); resumeFiles: ZMODEM + ' asks the receiver to continue files it already has part of. Returns a + ' transfer handle -- XferStatus is XFER_FAILED and XferMessage$ says + ' why when it could not start -- or 0 when the connection is not open + ' or already has a transfer. + DECLARE FUNCTION ConnSendFiles(BYVAL handle AS LONG, BYVAL proto AS LONG, BYVAL files AS STRING, BYVAL escapeCtl AS LONG, BYVAL resumeFiles AS LONG) AS LONG + + ' Receive with proto: XMODEM into the file target, YMODEM and ZMODEM + ' into the folder target (the sender names the files). exists is what + ' happens to a file that is already there (XFER_EXIST_; with + ' XFER_EXIST_ASK the transfer waits in XFER_ASKING for XferAnswer); + ' escapeCtl asks a ZMODEM sender to escape every control character. + ' Returns a transfer handle as ConnSendFiles does. + DECLARE FUNCTION ConnReceiveFiles(BYVAL handle AS LONG, BYVAL proto AS LONG, BYVAL target AS STRING, BYVAL exists AS LONG, BYVAL escapeCtl AS LONG) AS LONG + + ' How many bytes a ZMODEM sender may send to this side before it waits + ' for an answer, for the transfers started after it: 0 = no limit, up + ' to XFER_WINDOW_MAX (xfer.bas); -1 (the default) picks by the line -- + ' small on a serial line without flow control, else no limit. + DECLARE SUB ConnSetXferWindow(BYVAL handle AS LONG, BYVAL bytes AS LONG) + + ' The words for a code ConnOpen returned (CONN_OK, CONN_ERR_...). + DECLARE FUNCTION ConnErrorText$(BYVAL errCode AS LONG) AS STRING +END DECLARE diff --git a/src/include/basic/debug.bas b/src/include/basic/debug.bas new file mode 100644 index 00000000..65891948 --- /dev/null +++ b/src/include/basic/debug.bas @@ -0,0 +1,148 @@ +' 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. + +' debug.bas -- run and debug a program inside your program +' +' Runs a module compiled with compiler.bas as a child program inside the +' calling program -- its forms are ordinary windows -- and debugs it: +' breakpoints, stepping, the variables where it is paused, expressions, +' Immediate-window statements, and changing variables. +' +' Nothing here waits. Start the program with RunStart, then call RunPoll +' from a Timer: each call runs the program for at most the time given +' and tells what it is doing (RUN_ constants). What the program PRINTs +' is collected for RunOutput$; INPUT asks with an input box. +' +' Line numbers are lines of the compiled source set: turn a file line +' into one with CompConcatLine and back with CompMapFile$ / CompMapLine. +' +' When the program stops in its module-level code, RunPoll reports +' RUN_PAUSED. When it stops inside one of its event handlers -- or in +' its forms' Load code, which runs inside RunStart -- it waits right +' there while your program's Timer and menus keep working, so they can +' step, continue or stop it; RunPoll called meanwhile returns at once +' (and RunStart returns only after the pause). The program's form +' captions show "[break]" while it is paused. +' +' Runs are numbers (0 means the call failed). Free each with RunFree; +' whatever a program leaves running is stopped and freed when it ends. +' +' Usage: Add this file and compiler.bas to your project. + +' RunState / RunPoll results +CONST RUN_READY = 0 ' made, RunStart not called yet +CONST RUN_RUNNING = 1 ' running its code +CONST RUN_EVENTS = 2 ' waiting for events: a form is open +CONST RUN_PAUSED = 3 ' stopped by a breakpoint or a step +CONST RUN_ENDED = 4 ' over (also after RunStop) +CONST RUN_ERROR = 5 ' ended by a runtime error: RunError$, RunErrorLine + +DECLARE LIBRARY "basdev" + + ' ========================================================= + ' Running + ' ========================================================= + + ' A child program running module (CompCompile) with the forms of + ' source set src (0: none). appDir is its App.Path ("" = the + ' project's folder, else your program's); args its COMMAND$. Returns + ' a run, or 0 (DbgMessage$ says why). + DECLARE FUNCTION RunCreate(BYVAL module AS LONG, BYVAL src AS LONG, BYVAL appDir AS STRING, BYVAL args AS STRING) AS LONG + + ' Load the program's forms, show the startup form ("" = the + ' project's, else the first) and let RunPoll run its code. + ' breakOnStart pauses at its first line. True when started. + DECLARE FUNCTION RunStart(BYVAL run AS LONG, BYVAL startupForm AS STRING, BYVAL breakOnStart AS LONG) AS LONG + + ' Run the program for about ms milliseconds (at least one short + ' slice) and return what it is doing now (RUN_ constants). + DECLARE FUNCTION RunPoll(BYVAL run AS LONG, BYVAL ms AS LONG) AS LONG + + ' What the program is doing (RUN_ constants), without running it. + DECLARE FUNCTION RunState(BYVAL run AS LONG) AS LONG + + ' What the program PRINTed since the last call. + DECLARE FUNCTION RunOutput$(BYVAL run AS LONG) AS STRING + + ' The runtime error that ended the program, and its line. + DECLARE FUNCTION RunError$(BYVAL run AS LONG) AS STRING + DECLARE FUNCTION RunErrorLine(BYVAL run AS LONG) AS LONG + + ' End the program: its windows close, RunState becomes RUN_ENDED. + DECLARE SUB RunStop(BYVAL run AS LONG) + + ' Stop the program and forget it. + DECLARE SUB RunFree(BYVAL run AS LONG) + + ' ========================================================= + ' Debugging + ' ========================================================= + + ' The breakpoints: line numbers separated by commas or spaces ("" for + ' none). They apply at once, also while the program runs. + DECLARE SUB DbgSetBreakpoints(BYVAL run AS LONG, BYVAL lineList AS STRING) + + ' Pause at the next line the program runs. + DECLARE SUB DbgPause(BYVAL run AS LONG) + + ' While paused: run to the next line (into a called procedure), to + ' the next line of this procedure, out of this procedure, to line + ' lineNum, or on until the next breakpoint. + DECLARE SUB DbgStepInto(BYVAL run AS LONG) + DECLARE SUB DbgStepOver(BYVAL run AS LONG) + DECLARE SUB DbgStepOut(BYVAL run AS LONG) + DECLARE SUB DbgRunToLine(BYVAL run AS LONG, BYVAL lineNum AS LONG) + DECLARE SUB DbgContinue(BYVAL run AS LONG) + + ' Where the program is paused: the line (0 when it is not paused) + ' and the procedure ("(module)" for module-level code). + DECLARE FUNCTION DbgLine(BYVAL run AS LONG) AS LONG + DECLARE FUNCTION DbgProc$(BYVAL run AS LONG) AS STRING + + ' The variables visible where it is paused, one row each: name, type + ' and value. + DECLARE FUNCTION DbgLocals$(BYVAL run AS LONG) AS STRING + + ' The calls that led there, innermost first, one row each: procedure + ' and line (the line only for the current one). + DECLARE FUNCTION DbgCallStack$(BYVAL run AS LONG) AS STRING + + ' The value of a variable (x, a(2, 3), p.Field, obj.Member) or of any + ' expression, as the Watch window shows it: "" when it cannot + ' be evaluated, "" when the program is not paused. + DECLARE FUNCTION DbgEval$(BYVAL run AS LONG, BYVAL expr AS STRING) AS STRING + + ' A line typed into an Immediate window: "x = expr" changes the + ' paused program's variable; a line starting with PRINT, DIM or LET + ' runs as it is; anything else is printed. Returns what it showed. + ' Without a paused program (run 0) only expressions and statements + ' that need none of its variables work. + DECLARE FUNCTION DbgExec$(BYVAL run AS LONG, BYVAL stmt AS STRING) AS STRING + + ' Set a variable of the paused program (x, a(2), p.Field) to the + ' value of expr. True when set; DbgMessage$ shows the new value or + ' why not. + DECLARE FUNCTION DbgSetVar(BYVAL run AS LONG, BYVAL varPath AS STRING, BYVAL expr AS STRING) AS LONG + + ' The message of the last RunCreate that failed or DbgSetVar. + DECLARE FUNCTION DbgMessage$() AS STRING +END DECLARE diff --git a/src/include/basic/designer.bas b/src/include/basic/designer.bas new file mode 100644 index 00000000..bad7af08 --- /dev/null +++ b/src/include/basic/designer.bas @@ -0,0 +1,239 @@ +' 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. + +' designer.bas -- Control types for DVX BASIC form designers +' +' Describes the control types a form can hold, as a toolbox and a +' properties window need them: their names, whether they hold other +' controls and can be placed by a designer, the name prefix of new +' controls, the default event, the toolbox tooltip and icon, and the +' properties, methods and events of each type. "Form" (the form +' itself) and "Menu" (a menu item) answer for properties, methods and +' events too, and "UserControl" (the object in a UserControl's own code) +' for events. The answers are the ones the DVX BASIC IDE uses. +' +' The rest of a designer is built in: CreateControl / RemoveControl, +' CallByName (properties and methods by name), Form.Controls, +' Control.Parent / Container / MoveBefore, LoadFormText, and the design +' surface (Form.DesignMode, DesignSelection, the DesignMouse, +' DesignKey and DesignPropertyChanged events). +' +' A project's own UserControls (.ctl files) become control types of the +' designer with CompDesignControls (compiler.bas): they are then listed +' here like any other type, with their Description as the tooltip and +' their ToolboxIcon as the icon, and run their code on the design forms +' with UserControl.UserMode False. When such a control's code calls +' UserControl.PropertyChanged, its design form raises +' DesignPropertyChanged (CtrlName, PropertyName): read the properties +' again. +' +' Form files as data (the .frm model, the one the DVX BASIC IDE loads, +' saves, copies and pastes with): FrmOpen / FrmOpenFile / FrmNew give a +' handle (0: not a form); FrmGet$ / FrmSet read and change the form's +' properties, FrmCtl* its controls (numbered from 0, each control +' followed by its children; -1 means the form as a parent or "the end" +' as a position) and FrmMenu* its menu items (numbered from 0 in menu +' order, Level 0 for a menu on the menu bar or a popup menu); FrmText$ / +' FrmSave write the .frm text; FrmFree frees the handle (what a program +' leaves open is freed when it ends). FrmCtlText$ / FrmCtlPaste copy and +' paste a control with its children as text. FormText$ writes a loaded +' form as it is now as .frm text. Only what was set is written: a +' property a control has by default is left out. +' +' Usage: Add this file to your project. + +' CtlTypeFlags bits (-1: no such type) +CONST CTL_FLAG_CONTAINER = 1 ' holds other controls (Frame, VBox, TabPage ...) +CONST CTL_FLAG_DESIGNABLE = 2 ' CreateControl can make one (so a designer can place it) + +' CtlPropType: the value type, plus the flags (-1: no such property) +CONST CTL_PROP_STRING = 0 +CONST CTL_PROP_INTEGER = 1 +CONST CTL_PROP_BOOLEAN = 2 +CONST CTL_PROP_SINGLE = 3 +CONST CTL_PROP_ENUM = 4 ' an Integer with named values (CtlPropEnum$) +CONST CTL_PROP_OBJECT = 16 ' an object (Controls, Parent, Container) +CONST CTL_PROP_TYPEMASK = 255 +CONST CTL_PROP_READONLY = 256 ' cannot be assigned +CONST CTL_PROP_WRITEONLY = 512 ' cannot be read + +' DesignMouse's Action +CONST DSGN_MOUSE_DOWN = 1 +CONST DSGN_MOUSE_MOVE = 2 +CONST DSGN_MOUSE_UP = 3 +CONST DSGN_MOUSE_DBLCLICK = 4 + +' DesignMouse's Handle: the selected control's resize handle under the +' mouse (the others are drawn but do not resize: the layout places +' controls) +CONST DSGN_HANDLE_NONE = -1 +CONST DSGN_HANDLE_E = 3 ' right edge: width +CONST DSGN_HANDLE_SE = 4 ' bottom right corner: width and height +CONST DSGN_HANDLE_S = 5 ' bottom edge: height + +DECLARE LIBRARY "basrt" + ' The number of control types, and the name of type idx (0 to + ' CtlTypeCount() - 1), e.g. "CommandButton", in alphabetical order. + DECLARE FUNCTION CtlTypeCount() AS LONG + DECLARE FUNCTION CtlTypeName$(BYVAL idx AS LONG) AS STRING + + ' CTL_FLAG_ bits of a type, -1 when there is no such type. + DECLARE FUNCTION CtlTypeFlags(BYVAL typeName AS STRING) AS LONG + + ' The name new controls of a type start with ("Command" for a + ' CommandButton: Command1, Command2 ...). + DECLARE FUNCTION CtlTypeNamePrefix$(BYVAL typeName AS STRING) AS STRING + + ' The event a designer opens when a control of the type is double + ' clicked (Click for a button, Change for a TextBox, Load for "Form"). + DECLARE FUNCTION CtlTypeDefaultEvent$(BYVAL typeName AS STRING) AS STRING + + ' The toolbox tooltip of a type (a project's UserControl: its + ' Description; its type name when it has none). + DECLARE FUNCTION CtlTypeTooltip$(BYVAL typeName AS STRING) AS STRING + + ' Writes the type's 24 x 24 toolbox icon to file outFile, for an + ' ImageButton's Picture (a project's UserControl: a copy of its + ' ToolboxIcon file). True when written. + DECLARE FUNCTION CtlTypeIconFile(BYVAL typeName AS STRING, BYVAL outFile AS STRING) AS LONG + + ' The properties of a type: how many, and the name, type (CTL_PROP_) + ' and value names (an enumerated property's, "|"-separated in value + ' order: Value 0 is the first) of property idx (0 to count - 1). + DECLARE FUNCTION CtlPropCount(BYVAL typeName AS STRING) AS LONG + DECLARE FUNCTION CtlPropName$(BYVAL typeName AS STRING, BYVAL idx AS LONG) AS STRING + DECLARE FUNCTION CtlPropType(BYVAL typeName AS STRING, BYVAL idx AS LONG) AS LONG + DECLARE FUNCTION CtlPropEnum$(BYVAL typeName AS STRING, BYVAL idx AS LONG) AS STRING + + ' The methods and the events of a type, "|"-separated. Forms and + ' PictureBoxes also have the drawing methods (Cls, Line, Print ...). + DECLARE FUNCTION CtlMethodList$(BYVAL typeName AS STRING) AS STRING + DECLARE FUNCTION CtlEventList$(BYVAL typeName AS STRING) AS STRING + + ' The parameter list of an event's handler, e.g. "Button As Integer, + ' X As Integer, Y As Integer" for MouseDown ("" when it has none). + DECLARE FUNCTION CtlEventParams$(BYVAL eventName AS STRING) AS STRING + + ' The same for an event of one type: a UserControl's own events have + ' parameters of their own, and so do the events of "UserControl" in + ' its code (UserControl_MouseDown has Button, Shift, X and Y). + DECLARE FUNCTION CtlTypeEventParams$(BYVAL typeName AS STRING, BYVAL eventName AS STRING) AS STRING + + ' The text of .frm files and class modules, as the project and the + ' Object list need them: the form's name, the names of the controls + ' and menu items it declares ("|"-separated, in file order) and its + ' code (what follows the form's End line); a class module's name + ' (its VB_Name, else the file name of path without the extension). + DECLARE FUNCTION FrmFormName$(BYVAL frmText AS STRING) AS STRING + DECLARE FUNCTION FrmControlNames$(BYVAL frmText AS STRING) AS STRING + DECLARE FUNCTION FrmCode$(BYVAL frmText AS STRING) AS STRING + DECLARE FUNCTION ClsClassName$(BYVAL clsText AS STRING, BYVAL path AS STRING) AS STRING + + ' A form model from .frm text, from a file, or a new blank form; + ' 0 when the text is not a form. FrmFree frees it. + DECLARE FUNCTION FrmOpen(BYVAL frmText AS STRING) AS LONG + DECLARE FUNCTION FrmOpenFile(BYVAL path AS STRING) AS LONG + DECLARE FUNCTION FrmNew(BYVAL formName AS STRING) AS LONG + DECLARE SUB FrmFree(BYVAL frm AS LONG) + + ' The model as .frm text ("" when a property value is too long for its + ' line), or written to a file (True when written). + DECLARE FUNCTION FrmText$(BYVAL frm AS LONG) AS STRING + DECLARE FUNCTION FrmSave(BYVAL frm AS LONG, BYVAL path AS STRING) AS LONG + + ' A form property as text (Name, Caption, Width, AutoSize, Layout, + ' Icon, Tag ...); setting one, True when the form has it. Name + ' renames the form. + DECLARE FUNCTION FrmGet$(BYVAL frm AS LONG, BYVAL prop AS STRING) AS STRING + DECLARE FUNCTION FrmSet(BYVAL frm AS LONG, BYVAL prop AS STRING, BYVAL value AS STRING) AS LONG + + ' The code that follows the form's End line, and replacing it. + DECLARE FUNCTION FrmGetCode$(BYVAL frm AS LONG) AS STRING + DECLARE FUNCTION FrmSetCode(BYVAL frm AS LONG, BYVAL code AS STRING) AS LONG + + ' The controls: how many, the number of control name (array member + ' index, -1 for the first of that name; -1 when there is none), and + ' control i's name, type, control array index (-1: not in an array) + ' and parent (-1: the form). + DECLARE FUNCTION FrmCtlCount(BYVAL frm AS LONG) AS LONG + DECLARE FUNCTION FrmCtlFind(BYVAL frm AS LONG, BYVAL ctlName AS STRING, BYVAL index AS LONG) AS LONG + DECLARE FUNCTION FrmCtlName$(BYVAL frm AS LONG, BYVAL i AS LONG) AS STRING + DECLARE FUNCTION FrmCtlType$(BYVAL frm AS LONG, BYVAL i AS LONG) AS STRING + DECLARE FUNCTION FrmCtlIndex(BYVAL frm AS LONG, BYVAL i AS LONG) AS LONG + DECLARE FUNCTION FrmCtlParent(BYVAL frm AS LONG, BYVAL i AS LONG) AS LONG + + ' A property of control i as text ("" when it has none set), setting + ' one (Name renames the control and its whole control array, Index + ' makes it an array member), and removing a set one. + DECLARE FUNCTION FrmCtlGet$(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL prop AS STRING) AS STRING + DECLARE FUNCTION FrmCtlSet(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL prop AS STRING, BYVAL value AS STRING) AS LONG + DECLARE FUNCTION FrmCtlRemoveProp(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL prop AS STRING) AS LONG + + ' The properties set on control i besides the built-in ones (MinWidth, + ' Visible, HelpTopic ...): how many, and the name of property j. + DECLARE FUNCTION FrmCtlPropCount(BYVAL frm AS LONG, BYVAL i AS LONG) AS LONG + DECLARE FUNCTION FrmCtlPropName$(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL j AS LONG) AS STRING + + ' Add a control of a type as the last child of parent (-1: the form) + ' named ctlName ("": the next free name, Command1, Label2 ...); its + ' number, -1 when the name is taken or parent cannot hold it. Remove + ' a control with its children. + DECLARE FUNCTION FrmCtlAdd(BYVAL frm AS LONG, BYVAL typeName AS STRING, BYVAL ctlName AS STRING, BYVAL parent AS LONG) AS LONG + DECLARE SUB FrmCtlRemove(BYVAL frm AS LONG, BYVAL i AS LONG) + + ' Move control i (with its children) before its sibling before (-1: + ' to the end), or into another container as its last child (-1: the + ' form). True when done. + DECLARE FUNCTION FrmCtlMove(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL before AS LONG) AS LONG + DECLARE FUNCTION FrmCtlReparent(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL parent AS LONG) AS LONG + + ' Control i and its children as .frm text (a copy for the Clipboard), + ' and pasting such text into parent (-1: the form): the number of the + ' pasted control, -1 when the text holds none. A name already taken + ' is replaced by a free one. + DECLARE FUNCTION FrmCtlText$(BYVAL frm AS LONG, BYVAL i AS LONG) AS STRING + DECLARE FUNCTION FrmCtlPaste(BYVAL frm AS LONG, BYVAL text AS STRING, BYVAL parent AS LONG) AS LONG + + ' The menu items: how many, a property of item i (Name, Caption, + ' Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, + ' Level) and setting one (True when done; a Name already taken is + ' refused). + DECLARE FUNCTION FrmMenuCount(BYVAL frm AS LONG) AS LONG + DECLARE FUNCTION FrmMenuGet$(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL prop AS STRING) AS STRING + DECLARE FUNCTION FrmMenuSet(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL prop AS STRING, BYVAL value AS STRING) AS LONG + + ' Insert a blank item at position at (-1: the end) at level (kept so + ' the menus stay a tree); its number. Remove an item with the items + ' under it. Move one past its previous (direction -1) or next + ' (direction 1) sibling: its new number, -1 when it has none. Indent + ' (delta 1) or outdent (delta -1) it with the items under it: True + ' when done. + DECLARE FUNCTION FrmMenuAdd(BYVAL frm AS LONG, BYVAL at AS LONG, BYVAL level AS LONG) AS LONG + DECLARE SUB FrmMenuRemove(BYVAL frm AS LONG, BYVAL i AS LONG) + DECLARE FUNCTION FrmMenuMove(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL direction AS LONG) AS LONG + DECLARE FUNCTION FrmMenuIndent(BYVAL frm AS LONG, BYVAL i AS LONG, BYVAL delta AS LONG) AS LONG + + ' A loaded form (its first copy, by name) as .frm text as it is now: + ' its properties, its controls in their containers with what differs + ' from a new control of the type, its menus; no code. + DECLARE FUNCTION FormText$(BYVAL formName AS STRING) AS STRING +END DECLARE diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas index 0c740f54..4d0ac0df 100644 --- a/src/include/basic/fileops.bas +++ b/src/include/basic/fileops.bas @@ -17,14 +17,64 @@ ' 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. + ' fileops.bas -- File Operations Library for DVX BASIC -' Copies, moves and deletes whole folders, lists drives and reports free -' disk space through DECLARE LIBRARY wrappers. The single-file statements +' Lists folders, works with paths, names and attributes, and copies, moves +' and deletes files and whole folders -- at once, or as a job a program +' runs a little at a time with progress, Cancel and questions. Lists +' drives and reports free disk space. The single-file statements ' (FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR, SETATTR) are built in. ' Usage: Add this file to your project. +' +' Listings and jobs are numbers ("handles", 0 = none). Free each with +' FileOpsListFree / FileOpsJobFree when done; whatever a program leaves +' open is freed when it ends. The numbers below are the library's own +' (fileOps.h) and must not change. + +' FileOpsListSort columns +CONST FILEOPS_SORT_NAME = 0 +CONST FILEOPS_SORT_SIZE = 1 +CONST FILEOPS_SORT_TYPE = 2 +CONST FILEOPS_SORT_DATE = 3 + +' FileOpsJobCreate operations +CONST FILEOPS_COPY = 0 +CONST FILEOPS_MOVE = 1 +CONST FILEOPS_DELETE = 2 + +' FileOpsJobRunFor results +CONST FILEOPS_STEP_BUSY = 0 ' more to do +CONST FILEOPS_STEP_ASK = 1 ' a question (FileOpsJobMessage$): answer Yes, No, All or Cancel +CONST FILEOPS_STEP_ERROR = 2 ' something failed (FileOpsJobMessage$): answer Retry, Skip or Cancel +CONST FILEOPS_STEP_DONE = 3 ' finished +CONST FILEOPS_STEP_CANCELLED = 4 ' stopped by FileOpsJobCancel or the Cancel answer + +' FileOpsJobAnswer answers +CONST FILEOPS_ANSWER_YES = 0 +CONST FILEOPS_ANSWER_NO = 1 +CONST FILEOPS_ANSWER_ALL = 2 +CONST FILEOPS_ANSWER_CANCEL = 3 +CONST FILEOPS_ANSWER_RETRY = 4 +CONST FILEOPS_ANSWER_SKIP = 5 + +' FileOpsGetAttr / FileOpsSetAttr bits (as the vb attribute constants) +CONST FILEOPS_ATTR_READONLY = 1 +CONST FILEOPS_ATTR_HIDDEN = 2 +CONST FILEOPS_ATTR_SYSTEM = 4 +CONST FILEOPS_ATTR_ARCHIVE = 32 +CONST FILEOPS_ATTR_EDITABLE = 39 ' all four: the bits FileOpsSetAttr and FileOpsJobClearAttr change + +' FileOpsDriveType kinds +CONST FILEOPS_DRIVE_FIXED = 0 ' a hard disk +CONST FILEOPS_DRIVE_REMOVABLE = 1 ' a floppy disk drive +CONST FILEOPS_DRIVE_REMOTE = 2 ' a network drive or a CD-ROM DECLARE LIBRARY "fileops" + ' ---- Whole trees, at once ---- + ' Copy a file or a folder with everything in it into folder destDir. ' overwrite True replaces files that already exist, False keeps them. ' Returns True when everything was copied. @@ -38,9 +88,212 @@ DECLARE LIBRARY "fileops" ' too). Returns True when everything was deleted. DECLARE FUNCTION FileOpsDeleteTree(BYVAL path AS STRING) AS LONG - ' The drive letters that exist, e.g. "ACD". + ' ---- Drives and disks ---- + + ' The drive letters that exist, e.g. "ACD" ("/" alone where there are + ' no drive letters). DECLARE FUNCTION FileOpsDriveList$() AS STRING ' Free space in KB on the drive holding path, or -1 when unknown. DECLARE FUNCTION FileOpsFreeKb(BYVAL path AS STRING) AS LONG + + ' Free space in bytes on the drive holding path, or -1 when unknown. + DECLARE FUNCTION FileOpsFreeBytes(BYVAL path AS STRING) AS DOUBLE + + ' The size in bytes of the disk holding path, or -1 when unknown. + DECLARE FUNCTION FileOpsTotalBytes(BYVAL path AS STRING) AS DOUBLE + + ' What kind of drive drive is (its letter, as "C", "c:" or "C:\"): a + ' FILEOPS_DRIVE_ value, or -1 when there is no such drive. Asking + ' never makes a floppy or CD-ROM drive spin up. + DECLARE FUNCTION FileOpsDriveType(BYVAL drive AS STRING) AS LONG + + ' The letter of the drive the computer started from ("C"), "" where + ' there is none (elsewhere than DOS). + DECLARE FUNCTION FileOpsBootDrive$() AS STRING + + ' True when files can be made in folder (False on a CD-ROM, a write- + ' protected floppy or a folder that does not exist). + DECLARE FUNCTION FileOpsIsWritable(BYVAL folder AS STRING) AS LONG + + ' True when DVX itself runs from a disk it cannot write (a CD-ROM): + ' settings cannot be saved in its folder then. + DECLARE FUNCTION FileOpsReadOnlySystem() AS LONG + + ' The folder for scratch files: the TEMP (or else TMP) setting; "" + ' when neither is set. + DECLARE FUNCTION FileOpsTempDir$() AS STRING + + ' ---- Folder listings ---- + + ' Read a folder: its folders, and its files that match filter + ' (wildcards * and ?, several patterns separated by ; or blanks; "" + ' for every file), sorted by name with the folders first. Returns a + ' listing handle, or 0 when the folder cannot be read. + DECLARE FUNCTION FileOpsList(BYVAL folder AS STRING, BYVAL filter AS STRING) AS LONG + + ' How many entries a listing has; they are numbered from 0. + DECLARE FUNCTION FileOpsListCount(BYVAL list AS LONG) AS LONG + + ' Re-sort a listing: folders first, then by FILEOPS_SORT_NAME, _SIZE, + ' _TYPE or _DATE (ties by name); descending True reverses each group. + DECLARE SUB FileOpsListSort(BYVAL list AS LONG, BYVAL column AS LONG, BYVAL descending AS LONG) + + ' The number of the entry called itemName (case does not matter), or + ' -1 when there is none. + DECLARE FUNCTION FileOpsListFind(BYVAL list AS LONG, BYVAL itemName AS STRING) AS LONG + + ' An entry's name, whether it is a folder, and its size in bytes. + DECLARE FUNCTION FileOpsListName$(BYVAL list AS LONG, BYVAL index AS LONG) AS STRING + DECLARE FUNCTION FileOpsListIsFolder(BYVAL list AS LONG, BYVAL index AS LONG) AS LONG + DECLARE FUNCTION FileOpsListSize(BYVAL list AS LONG, BYVAL index AS LONG) AS DOUBLE + + ' The whole listing as the rows of a file list, in its order: name, + ' size ("1,234,567", blank for a folder), type ("Folder", "TXT File") + ' and date ("YYYY-MM-DD HH:MM"), separated by tabs, one row per line + ' -- exactly what a ListView's SetRows takes. + DECLARE FUNCTION FileOpsListRows$(BYVAL list AS LONG) AS STRING + + ' Free a listing. + DECLARE SUB FileOpsListFree(BYVAL list AS LONG) + + ' ---- Paths and names ---- + + ' The canonical full path of an existing folder (DOS: upper case, the + ' drive's current folder for "C:"), or "" when it is not one. + DECLARE FUNCTION FileOpsPathNormalize$(BYVAL path AS STRING) AS STRING + + ' The folder holding path, or "" at a drive or file system root. + DECLARE FUNCTION FileOpsPathParent$(BYVAL path AS STRING) AS STRING + + ' folder + separator + itemName, or "" when the result would be too long. + DECLARE FUNCTION FileOpsPathJoin$(BYVAL folder AS STRING, BYVAL itemName AS STRING) AS STRING + + ' True when the two paths name the same place. + DECLARE FUNCTION FileOpsPathSame(BYVAL a AS STRING, BYVAL b AS STRING) AS LONG + + ' True for a root such as "C:\" or "/". + DECLARE FUNCTION FileOpsPathIsRoot(BYVAL path AS STRING) AS LONG + + ' True when path is complete by itself ("C:\DOCS", "\DOCS" or "/home"), + ' False when it counts from the current folder ("DOCS\A.TXT"). + DECLARE FUNCTION FileOpsPathIsAbsolute(BYVAL path AS STRING) AS LONG + + ' True when path is folder itself or anywhere inside it (a copy of a + ' folder into itself is refused this way). + DECLARE FUNCTION FileOpsPathIsInside(BYVAL path AS STRING, BYVAL folder AS STRING) AS LONG + + ' The character between folder names in a path ("\" on DOS). + DECLARE FUNCTION FileOpsPathSep$() AS STRING + + ' True when itemName matches filter the way the file dialogs and + ' FileOpsList match: wildcards * and ?, several patterns separated by + ' ; , or blanks, case does not matter; "" and "*.*" match everything. + DECLARE FUNCTION FileOpsFilterMatch(BYVAL itemName AS STRING, BYVAL filter AS STRING) AS LONG + + ' The last part of a path ("LETTER.TXT" of "C:\DOCS\LETTER.TXT"). + DECLARE FUNCTION FileOpsBaseName$(BYVAL path AS STRING) AS STRING + + ' The extension of a name without its dot, "" when it has none. + DECLARE FUNCTION FileOpsExtension$(BYVAL itemName AS STRING) AS STRING + + ' ---- Files and folders ---- + + ' Make folder itemName inside folder; rename oldName in folder to newName + ' (refusing a name in use). True when done; when not, + ' FileOpsError$ says why, in words for the user. + DECLARE FUNCTION FileOpsMakeDir(BYVAL folder AS STRING, BYVAL itemName AS STRING) AS LONG + DECLARE FUNCTION FileOpsRename(BYVAL folder AS STRING, BYVAL oldName AS STRING, BYVAL newName AS STRING) AS LONG + DECLARE FUNCTION FileOpsError$() AS STRING + + ' Save a file safely: write the new contents to FileOpsTempPath$(path) + ' first, then FileOpsReplaceFile swaps it in for path. The old file + ' is kept aside until the new one is in place and comes back if the + ' swap fails, so a failure never leaves path half written. True when + ' done; when not, FileOpsError$ says why. + DECLARE FUNCTION FileOpsTempPath$(BYVAL path AS STRING) AS STRING + DECLARE FUNCTION FileOpsReplaceFile(BYVAL tmpPath AS STRING, BYVAL path AS STRING) AS LONG + + ' True when itemName can be used as a file name on this system (on + ' DOS: 8.3 names, no device names such as CON, no characters a disk + ' does not allow). When not, FileOpsError$ says why. + DECLARE FUNCTION FileOpsValidName(BYVAL itemName AS STRING) AS LONG + + ' Make the folder path and every folder above it that is missing + ' (MKDIR makes one). True when the folder exists afterwards. + DECLARE FUNCTION FileOpsMakePath(BYVAL path AS STRING) AS LONG + + ' True when path is an existing folder. + DECLARE FUNCTION FileOpsIsFolder(BYVAL path AS STRING) AS LONG + + ' The FILEOPS_ATTR_ bits of a file or folder, or -1 when it cannot be + ' read; set the Read-only, Hidden, System and Archive bits (True when + ' done). Elsewhere than DOS, read-only means not writable and hidden + ' a name starting with "."; only read-only can be set. + DECLARE FUNCTION FileOpsGetAttr(BYVAL path AS STRING) AS LONG + DECLARE FUNCTION FileOpsSetAttr(BYVAL path AS STRING, BYVAL attr AS LONG) AS LONG + + ' When a file or folder was last changed, "YYYY-MM-DD HH:MM", or "" + ' when it cannot be read. + DECLARE FUNCTION FileOpsModified$(BYVAL path AS STRING) AS STRING + + ' Set when a file was last changed: stamp as FileOpsModified$ gives it + ' ("YYYY-MM-DD HH:MM", seconds may follow as ":SS"). True when done; + ' False for a stamp that is not a real date and time, or a file that + ' cannot be changed. + DECLARE FUNCTION FileOpsSetModified(BYVAL path AS STRING, BYVAL stamp AS STRING) AS LONG + + ' Add up a file, or a folder and everything in it: True when every + ' part could be read. Then FileOpsTreeBytes, FileOpsTreeFiles and + ' FileOpsTreeFolders give the totals (the folder itself not counted). + DECLARE FUNCTION FileOpsTreeSize(BYVAL path AS STRING) AS LONG + DECLARE FUNCTION FileOpsTreeBytes() AS DOUBLE + DECLARE FUNCTION FileOpsTreeFiles() AS LONG + DECLARE FUNCTION FileOpsTreeFolders() AS LONG + + ' A byte count with thousands separators, "1,234,567". + DECLARE FUNCTION FileOpsFormatSize$(BYVAL bytes AS DOUBLE) AS STRING + + ' ---- Jobs: copy, move or delete with progress and questions ---- + + ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder) or + ' FILEOPS_DELETE job. Returns a job handle, 0 when out of memory. + DECLARE FUNCTION FileOpsJobCreate(BYVAL op AS LONG, BYVAL destFolder AS STRING) AS LONG + + ' Add a file or folder (full path) before the job runs. False when it + ' cannot be found or its destination path would be too long. + DECLARE FUNCTION FileOpsJobAddSource(BYVAL job AS LONG, BYVAL path AS STRING) AS LONG + + ' Work on the job for up to milliseconds (a Timer calls this, so the + ' program and DVX stay usable). Returns a FILEOPS_STEP_ value: on + ' ASK or ERROR show FileOpsJobMessage$ and give the user's choice to + ' FileOpsJobAnswer; on DONE or CANCELLED free the job. + DECLARE FUNCTION FileOpsJobRunFor(BYVAL job AS LONG, BYVAL milliseconds AS LONG) AS LONG + + DECLARE FUNCTION FileOpsJobMessage$(BYVAL job AS LONG) AS STRING + DECLARE SUB FileOpsJobAnswer(BYVAL job AS LONG, BYVAL answer AS LONG) + + ' Before a copy or move runs: clear these FILEOPS_ATTR_ bits on every + ' file it copies (FILEOPS_ATTR_READONLY for files copied from a + ' CD-ROM, so they can be changed afterwards). + DECLARE SUB FileOpsJobClearAttr(BYVAL job AS LONG, BYVAL attr AS LONG) + + ' Stop the job: the next FileOpsJobRunFor cleans up (a partly copied + ' file is removed) and returns FILEOPS_STEP_CANCELLED. + DECLARE SUB FileOpsJobCancel(BYVAL job AS LONG) + + ' Progress: the path being worked on, files done and found so far, + ' and percent done (0-100). + DECLARE FUNCTION FileOpsJobCurrent$(BYVAL job AS LONG) AS STRING + DECLARE FUNCTION FileOpsJobFilesDone(BYVAL job AS LONG) AS LONG + DECLARE FUNCTION FileOpsJobFilesTotal(BYVAL job AS LONG) AS LONG + DECLARE FUNCTION FileOpsJobPercent(BYVAL job AS LONG) AS LONG + + ' Progress in bytes: the size of the files found so far, and how much + ' of it is done. + DECLARE FUNCTION FileOpsJobBytesTotal(BYVAL job AS LONG) AS DOUBLE + DECLARE FUNCTION FileOpsJobBytesDone(BYVAL job AS LONG) AS DOUBLE + + ' Free a job (an unfinished one stops, removing a partly copied file). + DECLARE SUB FileOpsJobFree(BYVAL job AS LONG) END DECLARE diff --git a/src/include/basic/game.bas b/src/include/basic/game.bas index eadcd370..b80b7e60 100644 --- a/src/include/basic/game.bas +++ b/src/include/basic/game.bas @@ -23,7 +23,8 @@ ' game.bas -- Game Helpers Library for DVX BASIC ' ' Tells whether a key is being held down right now, for games that move -' something for as long as a key is held. Keys are named by their raw key +' something for as long as a key is held, and helps a game loop with +' FRAMEWAIT keep time. Keys are named by their raw key ' code: one of the constants below, or for any other key its PC keyboard ' scan code (add &H80 for the grey keys between the typing keys and the ' number pad). Keys only read as held while one of your program's windows @@ -58,4 +59,18 @@ DECLARE LIBRARY "game" ' True while the key is held down and one of this program's windows ' is active, else False. DECLARE FUNCTION GameKeyDown(BYVAL keyCode AS LONG) AS LONG + + ' Forget the keys held now: each reads as up until it is pressed + ' again (for example after a pause screen). Only while one of this + ' program's windows is active. + DECLARE SUB GameKeyClear() + + ' How many frames FRAMEWAIT has skipped because the program was too + ' slow to draw them in time, since it began waiting at its present + ' rate (0 before the first FRAMEWAIT). + DECLARE FUNCTION FrameMissed() AS LONG + + ' Wait until the screen starts drawing its next picture from the top; + ' a full-screen game that draws right after it does not tear. + DECLARE SUB GameWaitRetrace() END DECLARE diff --git a/src/include/basic/help.bas b/src/include/basic/help.bas index d6975544..a5cd67a5 100644 --- a/src/include/basic/help.bas +++ b/src/include/basic/help.bas @@ -24,14 +24,30 @@ ' ' Provides access to the DVX help compiler and viewer. ' Compile .dhs source files to .hlp and open them in the viewer. +' To read help files yourself (a help viewer of your own), use +' hlpfile.bas and the HelpPane control. ' ' Usage: Add this file to your project. DECLARE LIBRARY "basrt" ' Compile a .dhs source file to a binary .hlp file. - ' Returns True on success. + ' Returns True on success; HelpCompileMessages$ says what was wrong. DECLARE FUNCTION HelpCompile(BYVAL inputFile AS STRING, BYVAL outputFile AS STRING) AS INTEGER - ' Open a .hlp file in the DVX Help Viewer. + ' The errors and warnings of the last HelpCompile, one per line + ' (separated by CHR$(10)), each naming the file and line, e.g. + ' "error: NOTES.DHS:1: directive .h1 outside of a topic"; "" when + ' there were none. A warning alone does not stop the compile. + DECLARE FUNCTION HelpCompileMessages$() AS STRING + + ' Open a .hlp file in the DVX Help Viewer. A name without a folder + ' is looked for beside the program; "" opens the DVX user guide. A + ' Help Viewer that is already running shows the file in its window. DECLARE SUB HelpView(BYVAL hlpFile AS STRING) + + ' The same, at the topic whose ID is topicId (for example + ' "help.overview"); "" or a topic the file does not have opens the + ' page the file starts on. "" for the file opens the DVX user guide + ' at that topic. + DECLARE SUB HelpViewTopic(BYVAL hlpFile AS STRING, BYVAL topicId AS STRING) END DECLARE diff --git a/src/include/basic/hlpfile.bas b/src/include/basic/hlpfile.bas new file mode 100644 index 00000000..6aec87d3 --- /dev/null +++ b/src/include/basic/hlpfile.bas @@ -0,0 +1,140 @@ +' 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. + +' hlpfile.bas -- Help Files for DVX BASIC +' +' Reads compiled DVX help files (.hlp, made by the help compiler or +' HelpCompile) the way the DVX Help Viewer does, so a program can be a +' help viewer of its own: list the topics, fill a TreeView with the +' table of contents, look up the keyword index, search the text, and +' show a topic in a HelpPane control (HelpPane1.ShowTopic handle, topic). +' +' A help file is a handle (0 means it could not be opened). Topics, +' table of contents entries, index entries, search results and records +' are numbered from 0. Close each handle with HlpClose; whatever a +' program leaves open is closed when it ends. +' +' Programs that draw topics themselves (instead of using HelpPane) read +' a topic's records with HlpLoadTopic and the HlpRecord functions: each +' record is one heading, paragraph, list item, note, code block, table, +' link, picture or rule (HLP_REC_ constants below), in order. +' +' Usage: Add this file to your project. + +DECLARE LIBRARY "hlpfile" + + ' Open a help file. Returns a handle, or 0 when the file cannot be + ' read or is not a help file. + DECLARE FUNCTION HlpOpen(BYVAL path AS STRING) AS LONG + + ' Close the file. + DECLARE SUB HlpClose(BYVAL handle AS LONG) + + ' How many topics the file has. + DECLARE FUNCTION HlpTopicCount(BYVAL handle AS LONG) AS LONG + + ' The ID (for example "help.overview", what links and HelpTopic name) + ' and the title of a topic; "" for a bad topic number. + DECLARE FUNCTION HlpTopicId$(BYVAL handle AS LONG, BYVAL topic AS LONG) AS STRING + DECLARE FUNCTION HlpTopicTitle$(BYVAL handle AS LONG, BYVAL topic AS LONG) AS STRING + + ' The topic whose ID is topicId, or -1 when there is none. + DECLARE FUNCTION HlpFindTopic(BYVAL handle AS LONG, BYVAL topicId AS STRING) AS LONG + + ' The topic to show first: topicId when the file has it, else the + ' file's default topic, else its first; -1 for a file with no topics. + ' Pass "" for the default. + DECLARE FUNCTION HlpStartTopic(BYVAL handle AS LONG, BYVAL topicId AS STRING) AS LONG + + ' The table of contents: how many entries, and each entry's title, + ' topic (-1 for a heading that groups others and opens nothing), + ' depth (0 at the top, 1 under that, ...) and whether it starts + ' expanded (True / False). + DECLARE FUNCTION HlpTocCount(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION HlpTocTitle$(BYVAL handle AS LONG, BYVAL entry AS LONG) AS STRING + DECLARE FUNCTION HlpTocTopic(BYVAL handle AS LONG, BYVAL entry AS LONG) AS LONG + DECLARE FUNCTION HlpTocDepth(BYVAL handle AS LONG, BYVAL entry AS LONG) AS LONG + DECLARE FUNCTION HlpTocExpanded(BYVAL handle AS LONG, BYVAL entry AS LONG) AS LONG + + ' The keyword index (sorted): how many keywords, and each keyword and + ' the topic it opens. + DECLARE FUNCTION HlpIndexCount(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION HlpIndexKeyword$(BYVAL handle AS LONG, BYVAL entry AS LONG) AS STRING + DECLARE FUNCTION HlpIndexTopic(BYVAL handle AS LONG, BYVAL entry AS LONG) AS LONG + + ' Search the file for words (any case, separated by blanks): a topic + ' is found when its title or text has every word, or when an index + ' keyword has the whole query. Returns how many topics were found + ' (-1 on a failure); HlpSearchTopic gives them in topic order. + DECLARE FUNCTION HlpSearch(BYVAL handle AS LONG, BYVAL query AS STRING) AS LONG + DECLARE FUNCTION HlpSearchTopic(BYVAL handle AS LONG, BYVAL result AS LONG) AS LONG + + ' Read a topic's records. Returns how many there are, or -1 for a bad + ' handle or topic. The HlpRecord functions read the last topic loaded. + DECLARE FUNCTION HlpLoadTopic(BYVAL handle AS LONG, BYVAL topic AS LONG) AS LONG + + ' A record's type (HLP_REC_ constants) and flags: the note kind + ' (HLP_NOTE_ constants) of a note, the alignment (HLP_IMG_ constants) + ' of a picture. + DECLARE FUNCTION HlpRecordType(BYVAL handle AS LONG, BYVAL record AS LONG) AS LONG + DECLARE FUNCTION HlpRecordFlags(BYVAL handle AS LONG, BYVAL record AS LONG) AS LONG + + ' A record's text (the words a link shows; "" for a picture or a + ' rule) and, for a link, the ID of the topic it opens. + DECLARE FUNCTION HlpRecordText$(BYVAL handle AS LONG, BYVAL record AS LONG) AS STRING + DECLARE FUNCTION HlpRecordLink$(BYVAL handle AS LONG, BYVAL record AS LONG) AS STRING + + ' Save a picture record to a file, for LOADPICTURE: the picture as it + ' was put in the help file (a BMP, PNG, JPEG or GIF file; LOADPICTURE + ' reads any of them whatever the file is called). Returns True when + ' the file was written. + DECLARE FUNCTION HlpRecordImageSave(BYVAL handle AS LONG, BYVAL record AS LONG, BYVAL path AS STRING) AS LONG + + ' Text word-wrapped to lines of at most cols characters (10 or more), + ' the lines separated by CHR$(10), as the Help Viewer wraps + ' paragraphs. + DECLARE FUNCTION HlpWrap$(BYVAL text AS STRING, BYVAL cols AS LONG) AS STRING + +END DECLARE + +' Record types (HlpRecordType) +CONST HLP_REC_TEXT = 1 +CONST HLP_REC_HEADING1 = 2 +CONST HLP_REC_HEADING2 = 3 +CONST HLP_REC_HEADING3 = 4 +CONST HLP_REC_IMAGE = 5 +CONST HLP_REC_LINK = 6 +CONST HLP_REC_LIST_ITEM = 7 +CONST HLP_REC_TABLE = 8 +CONST HLP_REC_HRULE = 9 +CONST HLP_REC_NOTE = 10 +CONST HLP_REC_CODE = 11 + +' Note kinds (HlpRecordFlags of a note) +CONST HLP_NOTE_INFO = 0 +CONST HLP_NOTE_TIP = 1 +CONST HLP_NOTE_WARNING = 2 + +' Picture alignment (HlpRecordFlags of a picture) +CONST HLP_IMG_LEFT = 0 +CONST HLP_IMG_CENTER = 1 +CONST HLP_IMG_RIGHT = 2 diff --git a/src/include/basic/joystick.bas b/src/include/basic/joystick.bas index 7c31943c..b61b566d 100644 --- a/src/include/basic/joystick.bas +++ b/src/include/basic/joystick.bas @@ -31,6 +31,15 @@ ' changes the keys); it only answers while one of your program's windows ' is active. Calibrate joysticks in the Control Panel. ' +' The second half is for a program that sets joysticks up, as the +' Control Panel's Game Controllers page does: raw readings and +' calibration, dead zones, the game port's mode, the keyboard joystick's +' keys and the other [joystick] settings of DVX.INI. Changes apply at +' once; JoySettingsStore puts them all into a settings handle from +' PrefsLoad (prefs.bas) for the program to save, and JoySettingsLoad +' takes them back from one (to undo). Lists (choices, keys, keyboard +' slots) are numbered from 0, so they fill a DropDown in order. +' ' Usage: Add this file to your project. ' A hat (point of view switch) that is not pushed @@ -40,6 +49,28 @@ CONST JOY_HAT_CENTERED = -1 CONST JOY_AXIS_MIN = -32768 CONST JOY_AXIS_MAX = 32767 +' The listed settings for JoyChoice and friends (the library's own +' numbers, which do not change): the game port's mode, and when the +' keyboard joystick stands in for a missing one. +CONST JOY_CHOICE_GAMEPORT = 0 +CONST JOY_CHOICE_FALLBACK = 1 + +' The parts of an axis's calibration for JoyCalibration +CONST JOY_CAL_MIN = 0 +CONST JOY_CAL_CENTER = 1 +CONST JOY_CAL_MAX = 2 + +' Dead zones (percent): the usual one and the largest +CONST JOY_DEADZONE_DEFAULT = 8 +CONST JOY_DEADZONE_MAX = 50 + +' How often joysticks are read (times a second): the JoySetting$ key, +' the usual rate and the slowest and fastest allowed +CONST JOY_KEY_POLL_HZ = "pollHz" +CONST JOY_POLL_HZ_DEFAULT = 60 +CONST JOY_POLL_HZ_MIN = 10 +CONST JOY_POLL_HZ_MAX = 200 + DECLARE LIBRARY "joystick" ' How many joysticks there are (0 when there are none). DECLARE FUNCTION JoyCount() AS LONG @@ -67,4 +98,90 @@ DECLARE LIBRARY "joystick" ' The direction a hat is pushed in degrees (0 = up, 90 = right), or ' JOY_HAT_CENTERED. DECLARE FUNCTION JoyHat(BYVAL joy AS LONG, BYVAL hat AS LONG) AS LONG + + ' How many hats the joystick has. + DECLARE FUNCTION JoyHats(BYVAL joy AS LONG) AS LONG + + ' True when the axis answers (an axis that does not always reads 0). + DECLARE FUNCTION JoyAxisPresent(BYVAL joy AS LONG, BYVAL axis AS LONG) AS LONG + + ' False when the joystick stopped answering (unplugged). + DECLARE FUNCTION JoyConnected(BYVAL joy AS LONG) AS LONG + + ' ---- Setting joysticks up ---- + + ' Look for joysticks again (one plugged in later, a new game port + ' mode). Your program's own joystick readings start over. Returns + ' the new count, or -1 while another program uses a joystick. + DECLARE FUNCTION JoyRescan() AS LONG + + ' True for a joystick with analog axes that need calibrating. + DECLARE FUNCTION JoyCanCalibrate(BYVAL joy AS LONG) AS LONG + + ' True for the keyboard standing in for a joystick (it only answers + ' while one of your program's windows is active). + DECLARE FUNCTION JoyIsKeyboard(BYVAL joy AS LONG) AS LONG + + ' An axis's calibration in raw units: part JOY_CAL_MIN, JOY_CAL_CENTER + ' or JOY_CAL_MAX of what was set, or of the range the axis found by + ' itself (0 when the joystick cannot be calibrated). + DECLARE FUNCTION JoyCalibration(BYVAL joy AS LONG, BYVAL axis AS LONG, BYVAL part AS LONG) AS LONG + + ' The axis's raw reading in the joystick's own units, and the buttons + ' held now (1 for button 1, 2 for button 2, 4 for button 3, ... + ' added up), for calibrating. + DECLARE FUNCTION JoyRawAxis(BYVAL joy AS LONG, BYVAL axis AS LONG) AS LONG + DECLARE FUNCTION JoyRawButtons(BYVAL joy AS LONG) AS LONG + + ' Calibrate an axis: the raw readings all the way one way, in the + ' middle and all the way the other way (minV < centerV < maxV). + ' True when it was taken. + DECLARE FUNCTION JoySetCalibration(BYVAL joy AS LONG, BYVAL axis AS LONG, BYVAL minV AS LONG, BYVAL centerV AS LONG, BYVAL maxV AS LONG) AS LONG + + ' Forget an axis's calibration: it finds its own range again. + DECLARE FUNCTION JoyClearCalibration(BYVAL joy AS LONG, BYVAL axis AS LONG) AS LONG + + ' The dead zone round the middle in percent (0 to 50; -1 when there + ' is no such joystick), and setting it (True when it was taken). + DECLARE FUNCTION JoyDeadZone(BYVAL joy AS LONG) AS LONG + DECLARE FUNCTION JoySetDeadZone(BYVAL joy AS LONG, BYVAL percent AS LONG) AS LONG + + ' A listed setting (JOY_CHOICE_GAMEPORT or JOY_CHOICE_FALLBACK): how + ' many choices, each one's name for people, the one chosen now, and + ' choosing one (True when it was taken). Call JoyRescan afterwards + ' for the joysticks to follow. + DECLARE FUNCTION JoyChoiceCount(BYVAL which AS LONG) AS LONG + DECLARE FUNCTION JoyChoiceLabel$(BYVAL which AS LONG, BYVAL index AS LONG) AS STRING + DECLARE FUNCTION JoyChoice(BYVAL which AS LONG) AS LONG + DECLARE FUNCTION JoySetChoice(BYVAL which AS LONG, BYVAL index AS LONG) AS LONG + + ' The keys the keyboard joystick can use: how many, and each one's + ' name ("Up", "LCtrl", "A", ...). + DECLARE FUNCTION JoyKeyCount() AS LONG + DECLARE FUNCTION JoyKeyName$(BYVAL index AS LONG) AS STRING + + ' A key's number by its name (-1 when unknown), its raw key code (for + ' GameKeyDown in game.bas; -1 for a bad number), and the name of a + ' raw key code ("" when it is none of these keys). + DECLARE FUNCTION JoyKeyIndex(BYVAL keyName AS STRING) AS LONG + DECLARE FUNCTION JoyKeyCode(BYVAL index AS LONG) AS LONG + DECLARE FUNCTION JoyKeyNameOf$(BYVAL rawCode AS LONG) AS STRING + + ' The keyboard joystick's keys: slots 0 to JoyKbdSlotCount() - 1 are + ' up, down, left, right and buttons 1 to 4. JoyKbdSlot gives the + ' JoyKeyName$ number of a slot's key; JoySetKbdSlot changes it (True + ' when it was taken). + DECLARE FUNCTION JoyKbdSlotCount() AS LONG + DECLARE FUNCTION JoyKbdSlot(BYVAL slot AS LONG) AS LONG + DECLARE FUNCTION JoySetKbdSlot(BYVAL slot AS LONG, BYVAL keyIndex AS LONG) AS LONG + + ' Any [joystick] setting of DVX.INI by its key (like "pollHz"), or + ' defaultValue; JoySetSetting with "" removes the key. + DECLARE FUNCTION JoySetting$(BYVAL key AS STRING, BYVAL defaultValue AS STRING) AS STRING + DECLARE SUB JoySetSetting(BYVAL key AS STRING, BYVAL value AS STRING) + + ' Every joystick setting into a settings handle (save it with + ' PrefsSave), or back from one (to undo changes). + DECLARE SUB JoySettingsStore(BYVAL prefs AS LONG) + DECLARE SUB JoySettingsLoad(BYVAL prefs AS LONG) END DECLARE diff --git a/src/include/basic/layout.bas b/src/include/basic/layout.bas new file mode 100644 index 00000000..19a7dcbc --- /dev/null +++ b/src/include/basic/layout.bas @@ -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 + + +' layout.bas -- Sizes in characters and percent for DVX BASIC +' +' A control's MinWidth, MinHeight, MaxWidth and MaxHeight (and Width and +' Height, which set the minimum) are pixels. To size a control by its +' text instead -- a TextBox wide enough for 16 characters, whatever the +' screen -- give them a size made here; the layout turns it into pixels +' each time it arranges the form. Reading the property back gives the +' pixels it stands for at that moment. +' +' txtAddress.MinWidth = SizeChars(16) +' lstFiles.MaxHeight = SizePercent(50) +' +' Usage: Add this file to your project. + +DECLARE LIBRARY "basrt" + ' n characters of the system font (0 to 32767). + DECLARE FUNCTION SizeChars(BYVAL n AS LONG) AS LONG + + ' n percent (0 to 100) of the size of the control's container. + DECLARE FUNCTION SizePercent(BYVAL n AS LONG) AS LONG +END DECLARE diff --git a/src/include/basic/listhelp.bas b/src/include/basic/listhelp.bas new file mode 100644 index 00000000..aa636b66 --- /dev/null +++ b/src/include/basic/listhelp.bas @@ -0,0 +1,252 @@ +' 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. + +' listhelp.bas -- List Rules for DVX BASIC +' +' The rules the ListBox and ListView controls follow, for lists and icon +' views you draw yourself (a UserControl, a PictureBox): which items a +' click, Shift+click or Ctrl+click selects, which item the arrow keys, +' Home, End, PgUp and PgDn move to (in a list or a grid of icons), Ctrl+A +' and Space, type-ahead (pressing a letter goes to the next item starting +' with it) and where a dragged item lands. Your list then works exactly +' like the built-in ones. +' +' For icon views it also has Program Manager's rules (ListIcon...): the +' cells of the large icon, small icon and list views and where an item's +' picture and name go in them, the grid inside a sunken frame with a +' scroll bar only when the rows do not fit, the two lines of a large +' icon's name, the arrow keys among items placed in cells of their own, +' the scroll bar's arrows, trough and thumb, when a press starts a drag, +' the scrolling and the cell under a dragged item, and where a dropped +' item lands. An icon view drawn with them looks and behaves like Program +' Manager's. +' +' A selection is a handle (0 means it could not be made) that remembers +' how many items there are, which are selected and which one is current +' (the one the keyboard moves). Items are numbered from 0. Tell it when +' items are added, removed or moved (ListSelInsert, ListSelRemove, +' ListSelReorder) or all replaced (ListSelReset). Free it with ListSelFree; +' whatever a program leaves open is freed when it ends. +' +' A shift argument is the Shift argument of the KeyDown or MouseDown event +' (vbShiftMask, vbCtrlMask). +' +' Usage: Add this file to your project. +' +' Sub UserControl_MouseDown (Button As Integer, Shift As Integer, X As Single, Y As Single) +' ListSelClick sel, ItemAt(X, Y), Shift +' UserControl.Refresh +' End Sub +' +' Sub UserControl_KeyDown (KeyCode As Integer, Shift As Integer) +' If ListSelKey(sel, KeyCode, Shift) <> LISTSEL_KEY_NONE Then UserControl.Refresh +' If ListSelNavigate(sel, KeyCode, Shift, mColumns, mRowsShown) >= 0 Then UserControl.Refresh +' End Sub + +DECLARE LIBRARY "listhelp" + + ' A selection of count items; several items can be selected at once + ' when multi is True. No item is selected. Returns a handle, or 0. + DECLARE FUNCTION ListSelCreate(BYVAL multi AS LONG, BYVAL count AS LONG) AS LONG + + ' Free a selection. + DECLARE SUB ListSelFree(BYVAL handle AS LONG) + + ' The list now holds count new items: only the current item stays + ' selected (none when it is past the end). + DECLARE SUB ListSelReset(BYVAL handle AS LONG, BYVAL count AS LONG) + + ' How many items the selection covers. + DECLARE FUNCTION ListSelCount(BYVAL handle AS LONG) AS LONG + + ' A click on item index: alone it selects only that item, with Ctrl it + ' turns that item on or off, with Shift it selects every item from the + ' last click without Shift up to this one. The item becomes current. + DECLARE SUB ListSelClick(BYVAL handle AS LONG, BYVAL index AS LONG, BYVAL shift AS LONG) + + ' The selection keys of a multi-selection list: Ctrl+A selects every + ' item (returns LISTSEL_KEY_ALL), Space turns the current item on or off + ' (LISTSEL_KEY_TOGGLE). Any other key: LISTSEL_KEY_NONE, nothing done. + DECLARE FUNCTION ListSelKey(BYVAL handle AS LONG, BYVAL keyCode AS LONG, BYVAL shift AS LONG) AS LONG + + ' A navigation key: the arrows, Home, End, PgUp or PgDn move the + ' current item, in a list (columns = 1) or in a grid with columns + ' items per row; pageRows rows are one page. With Shift the items from + ' the last click up to the new current item are selected. Returns the + ' new current item, or -1 when the key is not one of these or the + ' current item stays where it is. + DECLARE FUNCTION ListSelNavigate(BYVAL handle AS LONG, BYVAL keyCode AS LONG, BYVAL shift AS LONG, BYVAL columns AS LONG, BYVAL pageRows AS LONG) AS LONG + + ' Make item index current as the keyboard does (for type-ahead): with + ' Shift the items from the last click up to it are selected. + DECLARE SUB ListSelMove(BYVAL handle AS LONG, BYVAL index AS LONG, BYVAL shift AS LONG) + + ' The current item, -1 for none. + DECLARE FUNCTION ListSelCursor(BYVAL handle AS LONG) AS LONG + + ' Make index the current and only selected item (-1: none). + DECLARE SUB ListSelSetCursor(BYVAL handle AS LONG, BYVAL index AS LONG) + + ' True when item index is selected. In a single-selection list that is + ' the current item. + DECLARE FUNCTION ListSelIsSelected(BYVAL handle AS LONG, BYVAL index AS LONG) AS LONG + + ' How many items are selected. + DECLARE FUNCTION ListSelSelectedCount(BYVAL handle AS LONG) AS LONG + + ' Select (True) or clear (False) one item, or every item, of a + ' multi-selection list. + DECLARE SUB ListSelSetItem(BYVAL handle AS LONG, BYVAL index AS LONG, BYVAL selected AS LONG) + DECLARE SUB ListSelSetAll(BYVAL handle AS LONG, BYVAL selected AS LONG) + + ' An item was inserted at index (0 up to the count), or the item at + ' index was removed: the other items keep their selection. + DECLARE SUB ListSelInsert(BYVAL handle AS LONG, BYVAL index AS LONG) + DECLARE SUB ListSelRemove(BYVAL handle AS LONG, BYVAL index AS LONG) + + ' An item was dragged from fromIndex to dest (see ListReorderDest): its + ' selection moves with it and it becomes the current item. + DECLARE SUB ListSelReorder(BYVAL handle AS LONG, BYVAL fromIndex AS LONG, BYVAL dest AS LONG) + + ' Where an item dragged from fromIndex lands when it is dropped in front + ' of slot toSlot (0 up to count; count is after the last item): the + ' index it moves to, or -1 when the drop changes nothing. + DECLARE FUNCTION ListReorderDest(BYVAL fromIndex AS LONG, BYVAL toSlot AS LONG, BYVAL count AS LONG) AS LONG + + ' Without a selection handle: the item Up, Down, Home, End, PgUp or + ' PgDn moves to from current in a list of count items (pageSize items + ' a page), or in a grid with columns items per row (where Left and + ' Right work too); -1 for any other key. + DECLARE FUNCTION ListNavigateIndex(BYVAL keyCode AS LONG, BYVAL current AS LONG, BYVAL count AS LONG, BYVAL pageSize AS LONG) AS LONG + DECLARE FUNCTION ListGridNavigate(BYVAL keyCode AS LONG, BYVAL current AS LONG, BYVAL count AS LONG, BYVAL columns AS LONG, BYVAL pageRows AS LONG) AS LONG + + ' Type-ahead: the next item after current (going round to the first) + ' whose text starts with the first letter of ch, any case; items holds + ' the item texts separated by CHR$(10). -1 when none does. + DECLARE FUNCTION ListTypeAhead(BYVAL ch AS STRING, BYVAL items AS STRING, BYVAL current AS LONG) AS LONG + + ' True when text starts with the first letter of ch, any case. + DECLARE FUNCTION ListTypeAheadMatch(BYVAL ch AS STRING, BYVAL text AS STRING) AS LONG + + ' Icon views. The grid functions take the view mode (LISTICON_VIEW_LARGE, + ' LISTICON_VIEW_SMALL or LISTICON_VIEW_LIST), the size of a character + ' (TextWidth("W"), TextHeight("W")), the size of the whole view with + ' its frame (width, height), how many items it holds, and rowsTaken: + ' LISTICON_BY_ORDER when the items fill the grid in order, else how + ' many rows the items placed in cells of their own take. Positions are + ' pixels from the view's top left; the scroll bar is a control's, the + ' one DrawScrollBar draws when its control argument is True. + + ' One value of the grid: a LISTICON_ metric (see below). + DECLARE FUNCTION ListIconMetric(BYVAL viewMode AS LONG, BYVAL metric AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG) AS LONG + + ' The cell under x, y with the view scrolled to row topRow, numbered + ' row * columns + column; -1 outside the grid. + DECLARE FUNCTION ListIconCellAt(BYVAL viewMode AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG, BYVAL topRow AS LONG, BYVAL x AS LONG, BYVAL y AS LONG) AS LONG + + ' Line lineNo (1 or 2) of a large icon's name under it, maxChars + ' characters a line (LISTICON_NAME_CHARS): the first line ends at the + ' last blank that fits (else it is cut), the second is the rest, cut. + ' "" for a line the name does not need. + DECLARE FUNCTION ListIconLabel$(BYVAL itemName AS STRING, BYVAL maxChars AS LONG, BYVAL lineNo AS LONG) AS STRING + + ' The item an arrow key, Home, End, PgUp or PgDn moves to from current + ' when the items stand in cells of their own, gaps allowed: cells holds + ' each item's column and row, all separated by commas ("0,0,2,0,1,1"). + ' columns: the grid's (LISTICON_COLUMNS); pageRows: rows a page. -1 + ' for another key or when no item is that way. + DECLARE FUNCTION ListIconNavigate(BYVAL keyCode AS LONG, BYVAL current AS LONG, BYVAL cells AS STRING, BYVAL columns AS LONG, BYVAL pageRows AS LONG) AS LONG + + ' True when a press has moved dx, dy far enough to start a drag. + DECLARE FUNCTION ListIconDragStarted(BYVAL dx AS LONG, BYVAL dy AS LONG) AS LONG + + ' While an item is dragged with the mouse at y: the top row after the + ' view scrolls a row because the mouse is above or below the grid. + DECLARE FUNCTION ListIconDragScroll(BYVAL viewMode AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG, BYVAL topRow AS LONG, BYVAL y AS LONG) AS LONG + + ' The cell a dragged item would be dropped on with the mouse at x, y + ' (above or below the grid: the nearest row), numbered as in + ' ListIconCellAt; -1 for none. + DECLARE FUNCTION ListIconDropCell(BYVAL viewMode AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG, BYVAL topRow AS LONG, BYVAL x AS LONG, BYVAL y AS LONG) AS LONG + + ' A left press at x, y on the scroll bar: the top row after it (an + ' arrow moves a row, the trough a page, the thumb nothing); -1 when + ' x, y is not on the scroll bar. + DECLARE FUNCTION ListIconScrollPress(BYVAL viewMode AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG, BYVAL topRow AS LONG, BYVAL x AS LONG, BYVAL y AS LONG) AS LONG + + ' How far below the top of the scroll bar's thumb a press at x, y is + ' (to drag it with ListIconScrollDrag); -1 when it is not on the thumb. + DECLARE FUNCTION ListIconScrollGrab(BYVAL viewMode AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG, BYVAL topRow AS LONG, BYVAL x AS LONG, BYVAL y AS LONG) AS LONG + + ' The top row with the thumb, held grab pixels below its top, dragged + ' to y (the mouse may be outside the view). + DECLARE FUNCTION ListIconScrollDrag(BYVAL viewMode AS LONG, BYVAL charWidth AS LONG, BYVAL charHeight AS LONG, BYVAL viewWidth AS LONG, BYVAL viewHeight AS LONG, BYVAL count AS LONG, BYVAL rowsTaken AS LONG, BYVAL grab AS LONG, BYVAL y AS LONG) AS LONG + + ' Items in order: the place an item dragged from fromIndex takes when + ' dropped on cell number cell -- that cell's, the last place for a cell + ' past the end; -1 when the drop changes nothing. + DECLARE FUNCTION ListIconDropDest(BYVAL fromIndex AS LONG, BYVAL cell AS LONG, BYVAL count AS LONG) AS LONG +END DECLARE + +' What ListSelKey did +CONST LISTSEL_KEY_NONE = 0 +CONST LISTSEL_KEY_ALL = 1 +CONST LISTSEL_KEY_TOGGLE = 2 + +' Icon views +CONST LISTICON_VIEW_LARGE = 0 +CONST LISTICON_VIEW_SMALL = 1 +CONST LISTICON_VIEW_LIST = 2 + +' rowsTaken: the items fill the grid in order +CONST LISTICON_BY_ORDER = -1 + +' ListIconMetric's values. A cell: its size, its picture's place and size, +' its name's place (a large icon's two lines are each centred in the +' cell: LISTICON_TEXT_X is 0) and length in characters, and the list's +' detail column (0 in the other views). The grid: the inside of the frame +' less the scroll bar, the columns, the rows shown whole and in all, the +' largest top row, True when the scroll bar is needed (right of the +' inside, as tall), where the text of an empty view goes, the frame's +' width, and the rows a wheel notch scrolls. +CONST LISTICON_CELL_W = 0 +CONST LISTICON_CELL_H = 1 +CONST LISTICON_PICTURE_X = 2 +CONST LISTICON_PICTURE_Y = 3 +CONST LISTICON_PICTURE_SIZE = 4 +CONST LISTICON_TEXT_X = 5 +CONST LISTICON_TEXT_Y = 6 +CONST LISTICON_NAME_CHARS = 7 +CONST LISTICON_DETAIL_X = 8 +CONST LISTICON_INNER_X = 9 +CONST LISTICON_INNER_Y = 10 +CONST LISTICON_INNER_W = 11 +CONST LISTICON_INNER_H = 12 +CONST LISTICON_COLUMNS = 13 +CONST LISTICON_ROWS_SHOWN = 14 +CONST LISTICON_ROWS = 15 +CONST LISTICON_TOP_ROW_MAX = 16 +CONST LISTICON_SCROLLBAR = 17 +CONST LISTICON_EMPTY_X = 18 +CONST LISTICON_EMPTY_Y = 19 +CONST LISTICON_FRAME = 20 +CONST LISTICON_WHEEL_ROWS = 21 diff --git a/src/include/basic/music.bas b/src/include/basic/music.bas index bb18a974..5524e421 100644 --- a/src/include/basic/music.bas +++ b/src/include/basic/music.bas @@ -84,6 +84,17 @@ DECLARE LIBRARY "music" ' Why the last music function did not work. DECLARE FUNCTION MusicError$() AS STRING + ' The volume of this music (percent). + DECLARE FUNCTION MusicGetVolume(BYVAL music AS LONG) AS LONG + + ' About the music, by name (any case): "title" (a module's own + ' title), "output" (where it plays, for people, like "OPL3 FM + ' synthesizer at 388h"), "kind" ("module", "midi" or "live"), "where" + ' ("stream", "mpu401", "fm" or "software"; "" before it first plays), + ' "paused" and "looping" (True or False as text: VAL gives the + ' value). + DECLARE FUNCTION MusicInfo$(BYVAL music AS LONG, BYVAL field AS STRING) AS STRING + ' A live music for playing notes yourself (it is playing already). DECLARE FUNCTION MusicMidiOpen() AS LONG @@ -103,3 +114,24 @@ DECLARE LIBRARY "music" ' the full range is two semitones up or down). DECLARE SUB MusicPitchBend(BYVAL music AS LONG, BYVAL channel AS LONG, BYVAL bend AS LONG) END DECLARE + +' ---- The MIDI port itself: raw MIDI bytes (system exclusive messages, +' your own sequencer). One program at a time has the port; it is used +' only when the settings send MIDI music to the MIDI port. ---- +DECLARE LIBRARY "audio" + ' The MIDI port by name (any case): "description" (like "MPU-401 + ' MIDI port at 330h", "" when there is none), "present", "owned" (a + ' program has it) -- True or False as text. + DECLARE FUNCTION MidiPortInfo$(BYVAL field AS STRING) AS STRING + + ' Take the port for this program (False when another program has it + ' or there is none). + DECLARE FUNCTION MidiAcquire() AS LONG + + ' Send one byte (0 to 255); False when the port could not take it + ' yet: send it again. + DECLARE FUNCTION MidiPut(BYVAL byteValue AS LONG) AS LONG + + ' Give the port back. + DECLARE SUB MidiRelease() +END DECLARE diff --git a/src/include/basic/net.bas b/src/include/basic/net.bas index 8cccfc39..ad374b86 100644 --- a/src/include/basic/net.bas +++ b/src/include/basic/net.bas @@ -34,9 +34,13 @@ ' IF TcpPending(conn) > 0 THEN PRINT TcpRecv$(conn) ' End Sub ' -' Text only: a string stops at a zero byte. Every connection your program +' Strings sent and received may hold any bytes (CHR$(0) too), for binary +' protocols as well as text. Every connection your program ' opens is closed for it when the program ends. ' +' The network settings and status the Control Panel shows are here too +' (NetConfigLoad ... NetStatus$); the settings calls also need prefs.bas. +' ' Usage: Add this file to your project. DECLARE LIBRARY "dvxnet" @@ -45,6 +49,10 @@ DECLARE LIBRARY "dvxnet" ' "192.0.2.10", or "" when it cannot be found. DECLARE FUNCTION HostLookup$(BYVAL hostName AS STRING) AS STRING + ' The name of an address such as "192.0.2.10"; the address itself + ' when no name is known for it, "" when it is not an address. + DECLARE FUNCTION HostName$(BYVAL address AS STRING) AS STRING + ' This computer's address ("" while it has none). DECLARE FUNCTION NetAddress$() AS STRING @@ -62,14 +70,15 @@ DECLARE LIBRARY "dvxnet" ' waiting yet (check again later). DECLARE FUNCTION TcpAccept(BYVAL handle AS LONG) AS LONG - ' Send text. Returns the number of bytes sent, or -1. - DECLARE FUNCTION TcpSend(BYVAL handle AS LONG, BYVAL text AS STRING) AS LONG + ' Send bytes (text or any other bytes, CHR$(0) too). Returns the number + ' of bytes sent, or -1. + DECLARE FUNCTION TcpSend(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS LONG ' How many bytes have arrived and wait to be read (-1 for a bad handle). DECLARE FUNCTION TcpPending(BYVAL handle AS LONG) AS LONG ' What has arrived (up to 4096 bytes), or "" when nothing has. - DECLARE FUNCTION TcpRecv$(BYVAL handle AS LONG) AS STRING + DECLARE FUNCTION TcpRecv$(BYVAL handle AS LONG) AS BINARY STRING ' 1 while the connection is open, 0 once the other side closed it ' (text may still be waiting: read it with TcpRecv$ first). @@ -78,19 +87,96 @@ DECLARE LIBRARY "dvxnet" ' Close a connection or a listening handle. DECLARE SUB TcpClose(BYVAL handle AS LONG) + ' Tell the other side that nothing more will be sent; what it sends + ' can still be read. True when done. + DECLARE FUNCTION TcpShutdown(BYVAL handle AS LONG) AS LONG + + ' "address:port" of the other side of a connection, "" when it is not + ' connected. + DECLARE FUNCTION TcpPeer$(BYVAL handle AS LONG) AS STRING + + ' The port a connection or listening handle uses on this computer + ' (the one TcpListen(0) picked), -1 for a bad handle. + DECLARE FUNCTION TcpLocalPort(BYVAL handle AS LONG) AS LONG + + ' enable <> 0: send small pieces at once instead of gathering them into + ' fewer, larger ones (for typing and games). True when done. + DECLARE FUNCTION TcpSetNoDelay(BYVAL handle AS LONG, BYVAL enable AS LONG) AS LONG + + ' enable <> 0: check now and then that the other side of an idle + ' connection is still there. True when done. + DECLARE FUNCTION TcpSetKeepAlive(BYVAL handle AS LONG, BYVAL enable AS LONG) AS LONG + ' A UDP handle on port (0 = any free port). Returns the handle, or 0. DECLARE FUNCTION UdpOpen(BYVAL port AS LONG) AS LONG ' Send one message to host:port. Returns the bytes sent, or -1. ' The host may be "255.255.255.255" to reach every computer nearby. - DECLARE FUNCTION UdpSend(BYVAL handle AS LONG, BYVAL host AS STRING, BYVAL port AS LONG, BYVAL text AS STRING) AS LONG + DECLARE FUNCTION UdpSend(BYVAL handle AS LONG, BYVAL host AS STRING, BYVAL port AS LONG, BYVAL bytes AS BINARY STRING) AS LONG ' The next message that arrived, or "" when none has. - DECLARE FUNCTION UdpRecv$(BYVAL handle AS LONG) AS STRING + DECLARE FUNCTION UdpRecv$(BYVAL handle AS LONG) AS BINARY STRING ' "address:port" of whoever sent the message UdpRecv$ returned last. DECLARE FUNCTION UdpFrom$() AS STRING ' Close a UDP handle. DECLARE SUB UdpClose(BYVAL handle AS LONG) + + ' ---- The network settings (what the Control Panel's Network page + ' edits). prefs is a handle from prefs.bas: PrefsLoad(PrefsDvxIni$()) + ' for DVX's own settings. Fields are named by text (any case): + ' "dhcp" "-1" when the address comes from the network + ' automatically, "0" when it is fixed + ' "address", "netmask", "gateway" the fixed address + ' "dns1", "dns2" name servers ("" = none) + ' "hostname" this computer's name + ' "vector" the packet driver's interrupt (0 = find it) + + ' A settings handle holding the network settings of prefs (0 for the + ' usual settings), or 0 when none is free. Free it with NetConfigFree; + ' handles left open are freed when the program ends. + DECLARE FUNCTION NetConfigLoad(BYVAL prefs AS LONG) AS LONG + + ' One setting as text ("" for an unknown field). + DECLARE FUNCTION NetConfigGet$(BYVAL cfg AS LONG, BYVAL field AS STRING) AS STRING + + ' Change one setting ("dhcp" and "vector" take numbers). True when + ' the field exists. + DECLARE FUNCTION NetConfigSet(BYVAL cfg AS LONG, BYVAL field AS STRING, BYVAL value AS STRING) AS LONG + + ' "" when the settings can be used, else why not in plain words. + DECLARE FUNCTION NetConfigProblem$(BYVAL cfg AS LONG) AS STRING + + ' True when the settings differ from what NetConfigLoad read (then + ' NetRestart after saving makes them take effect). + DECLARE FUNCTION NetConfigChanged(BYVAL cfg AS LONG) AS LONG + + ' Put the settings into prefs; save prefs afterwards (PrefsSave). + ' True when done. + DECLARE FUNCTION NetConfigStore(BYVAL cfg AS LONG, BYVAL prefs AS LONG) AS LONG + + ' Free a settings handle. + DECLARE SUB NetConfigFree(BYVAL cfg AS LONG) + + ' The packet driver interrupts to choose from: NetVector(0) is 0 (find + ' it), then each interrupt the settings accept (-1 out of range). + DECLARE FUNCTION NetVectorCount() AS LONG + DECLARE FUNCTION NetVector(BYVAL index AS LONG) AS LONG + + ' Stop the network and start it again with the saved settings + ' (every connection is closed). True when it is running. + DECLARE FUNCTION NetRestart() AS LONG + + ' Stop the network: every program's connections are closed. NetRestart + ' starts it again. + DECLARE SUB NetStop() + + ' The network's state as text, by field: "message" (in plain words), + ' "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", + ' "gateway", "dns1", "dns2", "hostname", "driver" (the network card's + ' driver), "mac" (its hardware address), "vector", "sockets", + ' "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes" ("" for + ' an unknown field). + DECLARE FUNCTION NetStatus$(BYVAL field AS STRING) AS STRING END DECLARE diff --git a/src/include/basic/prefs.bas b/src/include/basic/prefs.bas new file mode 100644 index 00000000..34ce8345 --- /dev/null +++ b/src/include/basic/prefs.bas @@ -0,0 +1,156 @@ +' 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. + +' prefs.bas -- Settings Files for DVX BASIC +' +' Reads and writes INI files (the text format of DVX's own settings) the +' way DVX programs do: load a file once, read and change it in memory, +' save it when you are done. IniRead and IniWrite (built into BASIC) do +' one value at a time and read or write the whole file for each; use +' these when a program has many settings. +' +' [section] +' key = value +' +' Section and key names match without regard to case. A file that does +' not exist reads as empty. PrefsSave writes back to the file PrefsLoad +' read, keeping its comments and every line it did not change. To write +' a file from scratch -- dropping sections that are no longer wanted -- +' fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is +' replaced only once the new one is completely written, so a full disk +' never leaves half a file. +' +' Handles are numbers (0 means the call failed). Close each with +' PrefsClose; whatever a program leaves open is closed when it ends. +' +' Handles are also what other libraries take to read or store their +' settings: system.bas (colors, mouse, wallpaper), joystick.bas, net.bas, +' print.bas and sound.bas. PrefsDvxIni$ is the path of DVX's own settings +' file, DVX.INI, which is what Control Panel changes. +' +' The choice lists are the settings DVX.INI stores by name (mouse +' acceleration, wallpaper mode, scroll wheel direction): for each choice +' a label to show in a list, the name stored in the file and the value +' it stands for. The numbers below are the library's own and must not +' change. +' +' Usage: Add this file to your project. + +' Choice lists (the table argument of the PrefsChoice functions) +CONST PREFS_CHOICE_ACCEL = 0 ' [mouse] acceleration; value = the SysSetMouse accel argument +CONST PREFS_CHOICE_WALLPAPER = 1 ' [desktop] mode; index = SysSetWallpaperMode / SysWallpaperMode +CONST PREFS_CHOICE_WHEEL = 2 ' [mouse] wheel; value = the SysSetMouse wheelDir argument + +DECLARE LIBRARY "libdvx" + + ' The settings in the file path (empty when it does not exist yet). + ' Returns a handle, or 0 when out of memory. + DECLARE FUNCTION PrefsLoad(BYVAL path AS STRING) AS LONG + + ' Empty settings, for writing a whole new file with PrefsSaveAs. + DECLARE FUNCTION PrefsCreate() AS LONG + + ' The text of key in [section], or defaultValue when it is not there. + DECLARE FUNCTION PrefsGet$(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL defaultValue AS STRING) AS STRING + + ' The number in key of [section], or defaultValue. + DECLARE FUNCTION PrefsGetInt(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL defaultValue AS LONG) AS LONG + + ' True for "true", "yes" or "1", False for "false", "no" or "0" (in + ' any case); defaultValue for anything else or a missing key. + DECLARE FUNCTION PrefsGetBool(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL defaultValue AS LONG) AS LONG + + ' Set key in [section] (both are added when missing; a new key goes + ' after the last key of its section). + DECLARE SUB PrefsSet(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL value AS STRING) + DECLARE SUB PrefsSetInt(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL value AS LONG) + + ' Stored as "true" or "false". + DECLARE SUB PrefsSetBool(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL value AS LONG) + + ' Remove key from [section]. + DECLARE SUB PrefsRemove(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING) + + ' The name of key number index (from 0, in the order of the file) of + ' [section]; "" past the last key. A key written twice counts once. + ' Loop until it gives "" to see every key of a section. + DECLARE FUNCTION PrefsKeyName$(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL index AS LONG) AS STRING + + ' Save back to the file PrefsLoad read. True when it was saved. + DECLARE FUNCTION PrefsSave(BYVAL handle AS LONG) AS LONG + + ' Save to path (the old file is replaced only once the new one is + ' completely written). True when it was saved. + DECLARE FUNCTION PrefsSaveAs(BYVAL handle AS LONG, BYVAL path AS STRING) AS LONG + + ' Free the handle; changes not saved are lost. + DECLARE SUB PrefsClose(BYVAL handle AS LONG) + + ' The path of DVX's own settings file (DVX.INI). + DECLARE FUNCTION PrefsDvxIni$() AS STRING + + ' Store the screen mode DVX starts in ([video] width, height and bits + ' per pixel). + DECLARE SUB PrefsSetVideoMode(BYVAL handle AS LONG, BYVAL wide AS LONG, BYVAL high AS LONG, BYVAL bpp AS LONG) + + ' The screen mode stored in [video]: width, height and bits per pixel; + ' DVX's own start-up mode for a value that is not there. + DECLARE FUNCTION PrefsVideoWidth(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION PrefsVideoHeight(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION PrefsVideoBpp(BYVAL handle AS LONG) AS LONG + + ' How many choices list table (PREFS_CHOICE_*) has; 0 for an unknown + ' list. Choices are numbered from 0. + DECLARE FUNCTION PrefsChoiceCount(BYVAL table AS LONG) AS LONG + + ' The text to show for a choice ("" when there is no such choice). + DECLARE FUNCTION PrefsChoiceLabel$(BYVAL table AS LONG, BYVAL index AS LONG) AS STRING + + ' The name a choice is stored as in the settings file ("" when there + ' is no such choice). + DECLARE FUNCTION PrefsChoiceName$(BYVAL table AS LONG, BYVAL index AS LONG) AS STRING + + ' The value a choice stands for (0 when there is no such choice). + DECLARE FUNCTION PrefsChoiceValue(BYVAL table AS LONG, BYVAL index AS LONG) AS LONG + + ' The choice stored as choiceName (in any case), or -1. + DECLARE FUNCTION PrefsChoiceIndex(BYVAL table AS LONG, BYVAL choiceName AS STRING) AS LONG + + ' The choice whose value is value, or -1. + DECLARE FUNCTION PrefsChoiceIndexOfValue(BYVAL table AS LONG, BYVAL value AS LONG) AS LONG + + ' DVX's default choice of a list (what a Defaults button selects); -1 + ' for an unknown list. + DECLARE FUNCTION PrefsChoiceDefault(BYVAL table AS LONG) AS LONG + + ' The choice key of [section] names; DVX's default choice when the key + ' is missing or holds a name the list does not have. -1 for an + ' unknown list. + DECLARE FUNCTION PrefsGetChoice(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL table AS LONG) AS LONG + + ' The same for a list of your own: choiceNames is the names the + ' choices are stored as, separated by "|", in order ("large|small|list" + ' makes "small" choice 1). The choice key of [section] names (in any + ' case); defaultName's choice when the key is missing or holds a name + ' not in the list; -1 when defaultName is not in it either. + DECLARE FUNCTION PrefsGetChoiceFrom(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL choiceNames AS STRING, BYVAL defaultName AS STRING) AS LONG +END DECLARE diff --git a/src/include/basic/print.bas b/src/include/basic/print.bas index 42353405..b6a067d6 100644 --- a/src/include/basic/print.bas +++ b/src/include/basic/print.bas @@ -24,7 +24,9 @@ ' Prints text (with the page setup: margins, header, footer, page numbers) ' and pictures on the printer chosen in the Control Panel, with or without ' the common Print dialog. LPRINT, LPRINT USING and OPEN "LPT1:" are built -' in and need nothing from this file. +' in and need nothing from this file. It also reads and changes the +' printer settings (model, where the printer is connected) the way the +' Control Panel does; those calls also need prefs.bas. ' Usage: Add this file to your project. DECLARE LIBRARY "print" @@ -51,4 +53,129 @@ DECLARE LIBRARY "print" ' True when the printer model in the settings can print pictures. DECLARE FUNCTION PrintCanPrintPictures() AS LONG + + ' ---- The printer settings (what the Control Panel's Printer page + ' edits). prefs is a handle from prefs.bas: PrefsLoad(PrefsDvxIni$()) + ' for DVX's own settings. Models and destinations are numbered from 0. + + ' How many printer models there are, and the name of each (like + ' "Epson 9-pin (ESC/P)"); "" for a number out of range. + DECLARE FUNCTION PrintModelCount() AS LONG + DECLARE FUNCTION PrintModelLabel$(BYVAL model AS LONG) AS STRING + + ' How many places the printer can be connected to, and the name of + ' each (like "LPT1" or "File"). + DECLARE FUNCTION PrintDestCount() AS LONG + DECLARE FUNCTION PrintDestLabel$(BYVAL dest AS LONG) AS STRING + + ' True for the destination that prints into a file (it needs a file + ' name). + DECLARE FUNCTION PrintDestIsFile(BYVAL dest AS LONG) AS LONG + + ' The printer model, destination and file name in the settings of + ' prefs (the usual settings when they are missing). + DECLARE FUNCTION PrintSettingModel(BYVAL prefs AS LONG) AS LONG + DECLARE FUNCTION PrintSettingDest(BYVAL prefs AS LONG) AS LONG + DECLARE FUNCTION PrintSettingFile$(BYVAL prefs AS LONG) AS STRING + + ' "" when this destination and file name can be used, else why not + ' (printing to a file needs its name). + DECLARE FUNCTION PrintSettingsProblem$(BYVAL dest AS LONG, BYVAL filePath AS STRING) AS STRING + + ' Put the model, destination and file name into the settings of + ' prefs, keeping the page setup as it is saved now; save prefs + ' afterwards (PrefsSave). True when done, False for a bad handle or + ' a number out of range. + DECLARE FUNCTION PrintSettingsStore(BYVAL prefs AS LONG, BYVAL model AS LONG, BYVAL dest AS LONG, BYVAL filePath AS STRING) AS LONG + + ' ---- The page setup without the Page Setup dialog. Margins count + ' lines (top, bottom) and characters (left, right). In the header and + ' footer: &f document name, &p page, &n number of pages, &d date, + ' &t time, && an ampersand, &l &c &r left / centre / right. + + ' A number of the page setup in prefs: "Lines" (lines per page, 0 = + ' the printer's usual page length), "Columns", "MarginTop", + ' "MarginBottom", "MarginLeft", "MarginRight", "TabWidth" or "Wrap" + ' (True: long lines wrap at a space, False: they are cut off); -1 for + ' an unknown field. + DECLARE FUNCTION PrintPageSetting(BYVAL prefs AS LONG, BYVAL field AS STRING) AS LONG + + ' "Header" or "Footer" of the page setup in prefs. + DECLARE FUNCTION PrintPageText$(BYVAL prefs AS LONG, BYVAL field AS STRING) AS STRING + + ' "" when this page setup can be used, else why not. + DECLARE FUNCTION PrintPageSetupProblem$(BYVAL lines AS LONG, BYVAL columns AS LONG, BYVAL marginTop AS LONG, BYVAL marginBottom AS LONG, BYVAL marginLeft AS LONG, BYVAL marginRight AS LONG, BYVAL tabWidth AS LONG, BYVAL wrap AS LONG, BYVAL header AS STRING, BYVAL footer AS STRING) AS STRING + + ' Put a page setup into the settings of prefs, keeping the printer as + ' it is; save prefs afterwards (PrefsSave). True when done, False for a + ' bad handle or a setup PrintPageSetupProblem$ objects to. + DECLARE FUNCTION PrintPageSettingsStore(BYVAL prefs AS LONG, BYVAL lines AS LONG, BYVAL columns AS LONG, BYVAL marginTop AS LONG, BYVAL marginBottom AS LONG, BYVAL marginLeft AS LONG, BYVAL marginRight AS LONG, BYVAL tabWidth AS LONG, BYVAL wrap AS LONG, BYVAL header AS STRING, BYVAL footer AS STRING) AS LONG + + ' Lines per page that the page setup in prefs gives on printer model + ' (its usual page length when Lines is 0); -1 for a model out of range. + DECLARE FUNCTION PrintPageLines(BYVAL prefs AS LONG, BYVAL model AS LONG) AS LONG + + ' ---- Printing your own way + + ' How many pages text$ takes with the saved page setup and printer. + DECLARE FUNCTION PrintPageCount(BYVAL text AS STRING) AS LONG + + ' Print text$ without asking first: pages firstPage to lastPage (0 and + ' 0 for every page), copies times. True when it was printed. + DECLARE FUNCTION PrintTextPages(BYVAL text AS STRING, BYVAL title AS STRING, BYVAL firstPage AS LONG, BYVAL lastPage AS LONG, BYVAL copies AS LONG) AS LONG + + ' Show the Print dialog only (printer, copies and, when pageCount is + ' above 0, a page range), for a program that prints its own output. + ' True when the user chose Print; then read the choice: + DECLARE FUNCTION PrintChoose(BYVAL title AS STRING, BYVAL pageCount AS LONG) AS LONG + ' Where to print (a destination number), the file name when that is a + ' file, the copies, and the pages (0 and 0 for every page). + DECLARE FUNCTION PrintChoiceDest() AS LONG + DECLARE FUNCTION PrintChoiceFile$() AS STRING + DECLARE FUNCTION PrintChoiceCopies() AS LONG + DECLARE FUNCTION PrintChoiceFirst() AS LONG + DECLARE FUNCTION PrintChoiceLast() AS LONG + + ' Print what a form shows (it must be loaded), scaled to the page, on + ' a printer model that prints pictures; PrintFormDialog shows the + ' Print dialog first. True when it was printed. + DECLARE FUNCTION PrintForm(BYVAL formName AS STRING) AS LONG + DECLARE FUNCTION PrintFormDialog(BYVAL formName AS STRING) AS LONG + + ' ---- The printer ports themselves (LPT1 is 1, up to 3) + + ' True when the computer has printer port LPTn. + DECLARE FUNCTION PrintPortPresent(BYVAL port AS LONG) AS LONG + + ' The printer's state on LPTn: PRINT_STATUS_ values added up. + DECLARE FUNCTION PrintPortStatus(BYVAL port AS LONG) AS LONG + + ' What is wrong with a printer in that state, in words, for printer + ' where$ ("LPT1"); afterWrite True when the status came after sending + ' it something. "" when nothing is wrong. + DECLARE FUNCTION PrintStatusProblem$(BYVAL status AS LONG, BYVAL afterWrite AS LONG, BYVAL where AS STRING) AS STRING END DECLARE + +' The state of a printer (PrintPortStatus) +CONST PRINT_STATUS_TIMEOUT = &H01 +CONST PRINT_STATUS_IO_ERROR = &H08 +CONST PRINT_STATUS_SELECTED = &H10 +CONST PRINT_STATUS_PAPER_OUT = &H20 +CONST PRINT_STATUS_ACK = &H40 +CONST PRINT_STATUS_NOT_BUSY = &H80 + +' Page setup limits and defaults +CONST PRINT_LINES_STANDARD = 66 +CONST PRINT_LINES_LASER = 60 +CONST PRINT_LINES_MIN = 10 +CONST PRINT_LINES_MAX = 200 +CONST PRINT_COLUMNS_DEFAULT = 80 +CONST PRINT_COLUMNS_MIN = 20 +CONST PRINT_COLUMNS_MAX = 255 +CONST PRINT_MARGIN_TB_DEFAULT = 3 +CONST PRINT_MARGIN_LR_DEFAULT = 5 +CONST PRINT_TAB_DEFAULT = 8 +CONST PRINT_TAB_MAX = 16 +CONST PRINT_BODY_MIN_LINES = 1 +CONST PRINT_BODY_MIN_COLUMNS = 10 +CONST PRINT_COPIES_MAX = 99 diff --git a/src/include/basic/resource.bas b/src/include/basic/resource.bas index c0bc4c5b..c997605c 100644 --- a/src/include/basic/resource.bas +++ b/src/include/basic/resource.bas @@ -32,6 +32,11 @@ CONST RES_TYPE_ICON = 1 CONST RES_TYPE_TEXT = 2 CONST RES_TYPE_BINARY = 3 +' Names DVX gives the icon resources, and the longest name plus one +CONST RES_NAME_ICON = "icon32" +CONST RES_NAME_WIDGET_ICON = "icon24" +CONST RES_NAME_MAX = 32 + DECLARE LIBRARY "basrt" ' Open a resource file for reading. Returns a handle (> 0) ' or 0 on failure. Close with ResClose when done. @@ -57,6 +62,14 @@ DECLARE LIBRARY "basrt" ' internally. Returns "" if not found. DECLARE FUNCTION ResGetText$(BYVAL path AS STRING, BYVAL resName AS STRING) AS STRING + ' Read every byte of a resource by name (any type; CHR$(0) + ' included). Returns "" if not found. + DECLARE FUNCTION ResGetData$(BYVAL path AS STRING, BYVAL resName AS STRING) AS BINARY STRING + + ' Add or replace a resource holding any bytes. Type is usually + ' RES_TYPE_BINARY. Returns True on success. + DECLARE FUNCTION ResAddData(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL resType AS LONG, BYVAL bytes AS BINARY STRING) AS LONG + ' Add or replace a text resource. Returns True on success. DECLARE FUNCTION ResAddText(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL text AS STRING) AS LONG diff --git a/src/include/basic/security.bas b/src/include/basic/security.bas new file mode 100644 index 00000000..6ccb0a74 --- /dev/null +++ b/src/include/basic/security.bas @@ -0,0 +1,131 @@ +' 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. + +' security.bas -- Security for DVX BASIC +' +' The building blocks the encrypted serial link uses, for your own +' protocols and files: random bytes for keys, a key exchange (two +' programs agree on a secret over a line anyone can read), a cipher, +' SHA-256 hashes, HMAC signatures, key stretching (HKDF) and a compare +' that does not give away where two codes differ. +' +' Keys, data and results are strings that may hold any bytes, CHR$(0) +' too. A key exchange, cipher, hash or HMAC in progress is a handle (0 +' means it could not be made); free it with its Free call (SecSha256End$ +' and SecHmacEnd$ free theirs). Whatever a program leaves open is freed +' when it ends. +' +' A secret from SecDhSecret$ is not a key: make keys from it with +' SecHkdf$. The same key and counter must never encrypt two different +' messages: move the counter on (SecCipherSetCounter) or make a new key. +' +' Usage: Add this file to your project. + +' Sizes in bytes +CONST SEC_DH_KEY_SIZE = 128 ' a key exchange public key and secret +CONST SEC_XTEA_KEY_SIZE = 16 ' a cipher key +CONST SEC_SHA256_SIZE = 32 ' a SHA-256 hash or HMAC +CONST SEC_HKDF_MAX_OUT = 8160 ' the most SecHkdf$ makes at once + +' Results of SecDhComputeSecret +CONST SEC_OK = 0 +CONST SEC_ERR_PARAM = -1 +CONST SEC_ERR_NOT_READY = -2 +CONST SEC_ERR_IO = -3 + +DECLARE LIBRARY "serial" + + ' count random bytes, good for keys and salts (RND is not). + DECLARE FUNCTION SecRandom$(BYVAL count AS LONG) AS BINARY STRING + + ' Stir something unpredictable (the time between key presses, mouse + ' positions) into the random generator. + DECLARE SUB SecAddEntropy(BYVAL bytes AS BINARY STRING) + + ' ---- Key exchange (Diffie-Hellman, 1024 bits) + ' Each side makes one, sends SecDhPublicKey$ to the other, and gives + ' what it got to SecDhComputeSecret; both then have the same + ' SecDhSecret$, which nobody listening can work out. + + ' A key exchange with fresh keys (takes a few seconds on a slow + ' computer). Returns a handle, or 0. + DECLARE FUNCTION SecDhCreate() AS LONG + + ' This side's public key (SEC_DH_KEY_SIZE bytes), to send. + DECLARE FUNCTION SecDhPublicKey$(BYVAL handle AS LONG) AS BINARY STRING + + ' Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM + ' for a bad handle or a key that is not acceptable. + DECLARE FUNCTION SecDhComputeSecret(BYVAL handle AS LONG, BYVAL remotePublicKey AS BINARY STRING) AS LONG + + ' The shared secret (SEC_DH_KEY_SIZE bytes; "" before + ' SecDhComputeSecret). + DECLARE FUNCTION SecDhSecret$(BYVAL handle AS LONG) AS BINARY STRING + + DECLARE SUB SecDhFree(BYVAL handle AS LONG) + + ' ---- Cipher (XTEA in counter mode: encrypting and decrypting are the + ' same call) + + ' A cipher with a SEC_XTEA_KEY_SIZE-byte key, counter 0. Returns a + ' handle, or 0 for a key of another length. + DECLARE FUNCTION SecCipherCreate(BYVAL key AS BINARY STRING) AS LONG + + ' bytes encrypted (or decrypted); the counter moves on one per 8 + ' bytes. + DECLARE FUNCTION SecCipherCrypt$(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS BINARY STRING + + ' Set the counter (a whole number from 0; exact up to 2^53), for + ' example to the number of the message. + DECLARE SUB SecCipherSetCounter(BYVAL handle AS LONG, BYVAL counter AS DOUBLE) + + DECLARE SUB SecCipherFree(BYVAL handle AS LONG) + + ' ---- Hashes and signatures + + ' The SHA-256 hash of bytes (SEC_SHA256_SIZE bytes). + DECLARE FUNCTION SecSha256$(BYVAL bytes AS BINARY STRING) AS BINARY STRING + + ' The same for data that comes in pieces: open, add each piece, end + ' (which frees the handle). + DECLARE FUNCTION SecSha256Open() AS LONG + DECLARE SUB SecSha256Add(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) + DECLARE FUNCTION SecSha256End$(BYVAL handle AS LONG) AS BINARY STRING + + ' The HMAC-SHA256 signature of bytes with key (SEC_SHA256_SIZE bytes). + DECLARE FUNCTION SecHmac$(BYVAL key AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS BINARY STRING + + ' The same in pieces: open with the key, add, end (frees the handle). + DECLARE FUNCTION SecHmacOpen(BYVAL key AS BINARY STRING) AS LONG + DECLARE SUB SecHmacAdd(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) + DECLARE FUNCTION SecHmacEnd$(BYVAL handle AS LONG) AS BINARY STRING + + ' length bytes (1 to SEC_HKDF_MAX_OUT) of key material made from + ' secret (HKDF-SHA256), with a salt ("" for none) and info naming what + ' the key is for (different info, different keys); "" for a length + ' out of range. + DECLARE FUNCTION SecHkdf$(BYVAL salt AS BINARY STRING, BYVAL secret AS BINARY STRING, BYVAL info AS BINARY STRING, BYVAL length AS LONG) AS BINARY STRING + + ' True when a and b hold the same bytes. Takes the same time wherever + ' they differ, so a signature check gives nothing away. + DECLARE FUNCTION SecEqual(BYVAL a AS BINARY STRING, BYVAL b AS BINARY STRING) AS LONG +END DECLARE diff --git a/src/include/basic/shell.bas b/src/include/basic/shell.bas new file mode 100644 index 00000000..20b032a3 --- /dev/null +++ b/src/include/basic/shell.bas @@ -0,0 +1,397 @@ +' 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. + + +' shell.bas -- DVX Shell Library for DVX BASIC +' Everything a program needs to work with DVX the way Program Manager +' and Task Manager do, or to be the desktop itself: start programs and +' open documents, run command lines and the Run dialog, the installed +' programs with their names and descriptions, file types (which program +' opens which kind of document), the running programs (find one, bring +' it to the front, ask it to close or end it), command lines split into +' their parts, this program's priority, keys that work in every program, +' Task Manager, system information, the desktop and the program started +' after it, Exit DVX, this program's settings folder and the DVX log +' (DVX.LOG); and Program Manager's program groups (PROGMAN.INI) with +' their items, shortcut keys and icons. +' Usage: Add this file to your project. +' +' "This program" is the program the code belongs to; when it runs in the +' IDE, that is the IDE. Handles and hotkeys a program has not freed are +' freed when it ends. + +' ShellSetPriority / ShellPriority: how much of the computer's time this +' program gets while other programs run too +CONST SHELL_PRIORITY_LOW = 0 ' one turn for every 6 a normal program gets +CONST SHELL_PRIORITY_NORMAL = 5 ' what every program starts with +CONST SHELL_PRIORITY_HIGH = 10 ' about twice the turns of a normal program + +' ShellGroupsGet / ShellGroupsSet "view": how a group's items are shown +CONST SHELL_VIEW_LARGE = 0 ' large icons +CONST SHELL_VIEW_SMALL = 1 ' small icons +CONST SHELL_VIEW_LIST = 2 ' a list + +DECLARE LIBRARY "dvxshell" + ' ---- Starting programs and documents ---- + + ' Start a program (.app) with arguments ("" for none) -- what it gets + ' as COMMAND$ -- or open a document (anything else) in the program + ' set to open its type. Returns True when it started (if it cannot + ' start, DVX says why). The same as ShellRunProgram with no folder. + DECLARE FUNCTION ShellRun(BYVAL program AS STRING, BYVAL args AS STRING) AS LONG + + ' Start a program, or open a document in the program set to open its + ' type, the way Program Manager starts an item: file names in args + ' (and a document named without a folder) are looked for in folder + ' first ("" for none). Returns the new program's id, or 0 when it did + ' not start (DVX says why). + DECLARE FUNCTION ShellRunProgram(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING) AS LONG + + ' Run a command line: a program (in double quotes when its name has a + ' blank) followed by its arguments. The new program's id, or 0. + DECLARE FUNCTION ShellRunCommand(BYVAL commandLine AS STRING) AS LONG + + ' Show the Run dialog. history is the commands to offer, newest + ' first, one per line (separated by CHR$(10)). Returns the command + ' line the user chose, or "" when the dialog was cancelled; run it + ' with ShellRunCommand. + DECLARE FUNCTION ShellRunDialog$(BYVAL history AS STRING) AS STRING + + ' Run Minimized: minimize the windows of a program just started + ' (appId from ShellRunProgram or ShellRunCommand), including the ones + ' it opens over the next moments while it starts up. + DECLARE SUB ShellMinimizeApp(BYVAL appId AS LONG) + + ' Open a document in the program set to open its type (Control Panel, + ' File Types). Returns True when it opened. + DECLARE FUNCTION ShellOpenDocument(BYVAL path AS STRING) AS LONG + + ' ---- Installed programs ---- + + ' Every program (.app) in folder and its subfolders ("" for DVX's APPS + ' folder), except skipPath ("" for none; ShellAppPath$() leaves this + ' program out). Only the paths are read, so it is quick. Returns a + ' list handle, or 0 when no program was found. + DECLARE FUNCTION ShellProgramScan(BYVAL folder AS STRING, BYVAL skipPath AS STRING) AS LONG + + ' How many programs a list holds (0 for list 0). + DECLARE FUNCTION ShellProgramCount(BYVAL list AS LONG) AS LONG + + ' The path of program index (0 to ShellProgramCount - 1) in a list. + DECLARE FUNCTION ShellProgramPath$(BYVAL list AS LONG, BYVAL index AS LONG) AS STRING + + ' Free a list. + DECLARE SUB ShellProgramFree(BYVAL list AS LONG) + + ' The name a program shows (its file name when it has none). + DECLARE FUNCTION ShellProgramName$(BYVAL path AS STRING) AS STRING + + ' A program's description ("" when it has none). + DECLARE FUNCTION ShellProgramDescription$(BYVAL path AS STRING) AS STRING + + ' True when path names a program (a .app file). + DECLARE FUNCTION ShellIsProgram(BYVAL path AS STRING) AS LONG + + ' The folder the installed programs are in (relative to DVX's folder): + ' where ShellProgramScan("") looks, and where a Browse for a program + ' starts (DlgFileOpenIn$). + DECLARE FUNCTION ShellProgramsFolder$() AS STRING + + ' ---- File types ---- + + ' The program set to open files with this extension ("TXT", no dot), + ' as it is stored: the program's path, followed by "," and the + ' arguments it is given when there are any. "" when there is none. + DECLARE FUNCTION ShellAssocProgram$(BYVAL extension AS STRING) AS STRING + + ' The program that opens a document (just the program's path), or "" + ' when no program is set for its type. + DECLARE FUNCTION ShellAssocCommand$(BYVAL document AS STRING) AS STRING + + ' How many file types have a program set. + DECLARE FUNCTION ShellAssocCount() AS LONG + + ' The extension of file type index (0 to ShellAssocCount - 1), upper + ' case without the dot; "" when there is no such index. + DECLARE FUNCTION ShellAssocExt$(BYVAL index AS LONG) AS STRING + + ' What is stored for file type index: as ShellAssocProgram$. + DECLARE FUNCTION ShellAssocValue$(BYVAL index AS LONG) AS STRING + + ' Set the program that opens an extension (value as ShellAssocProgram$ + ' gives it; in the arguments %1 stands for the document). Saved at + ' once. Returns False for an extension that cannot be one or when it + ' could not be saved. + DECLARE FUNCTION ShellAssocSet(BYVAL extension AS STRING, BYVAL value AS STRING) AS LONG + + ' Remove a file type. Saved at once. False when it was not set or + ' could not be saved. + DECLARE FUNCTION ShellAssocRemove(BYVAL extension AS STRING) AS LONG + + ' extension as file types are stored (".txt" gives "TXT"), or "" when + ' it cannot be one (up to 8 letters and digits). + DECLARE FUNCTION ShellAssocNormalize$(BYVAL extension AS STRING) AS STRING + + ' ---- Running programs ---- + + ' How many programs are running. + DECLARE FUNCTION ShellRunningCount() AS LONG + + ' The id of running program index (0 to ShellRunningCount() - 1), 0 + ' when there is no such index. Ids are what ShellRunProgram returns; + ' this program's own is ShellAppId(). + DECLARE FUNCTION ShellRunningId(BYVAL index AS LONG) AS LONG + + ' The name a running program shows, and the path of its .app; "" when + ' appId is not a running program. + DECLARE FUNCTION ShellRunningName$(BYVAL appId AS LONG) AS STRING + DECLARE FUNCTION ShellRunningPath$(BYVAL appId AS LONG) AS STRING + + ' The id of a running copy of program (its .app path), 0 when it is + ' not running. + DECLARE FUNCTION ShellRunningFind(BYVAL program AS STRING) AS LONG + + ' Bring a running program to the front, as Task Manager's Switch To + ' does: its top window is restored if it was minimized. False when it + ' is not running or shows no window. + DECLARE FUNCTION ShellRunningActivate(BYVAL appId AS LONG) AS LONG + + ' Ask a running program to close, as if the user closed its windows: + ' it may ask to save its work, and the user may cancel. The program + ' is asked as soon as your code returns to DVX (the end of the event + ' procedure), so check afterwards, e.g. from a Timer, with + ' ShellRunningFind or ShellChangeCount. True when the request was + ' made, False when appId is not a running program. + DECLARE FUNCTION ShellRunningClose(BYVAL appId AS LONG) AS LONG + + ' End a running program at once, without asking it, as Task + ' Manager's End Task does: whatever it had not saved is lost. Use it + ' for a program that does not respond; ShellRunningClose for one that + ' does. A program cannot end itself this way (END does that). True + ' when it was ended. + DECLARE FUNCTION ShellRunningKill(BYVAL appId AS LONG) AS LONG + + ' True when a running program is not responding: it has been busy in + ' one of its event procedures for 5 seconds or longer. + DECLARE FUNCTION ShellAppIsHung(BYVAL appId AS LONG) AS LONG + + ' How many bytes of memory a running program is using; 0 when appId + ' is not a running program. + DECLARE FUNCTION ShellAppMemory(BYVAL appId AS LONG) AS LONG + + ' A number that changes whenever a program starts, ends or is ended: + ' check it from a Timer and refresh what you show when it differs + ' from the value you saw last. + DECLARE FUNCTION ShellChangeCount() AS LONG + + ' Open Task Manager (what Ctrl+Esc does). + DECLARE SUB ShellTaskManager() + + ' A description of the computer DVX runs on (processor, memory, video, + ' drives ...), several lines; "" when there is none. + DECLARE FUNCTION ShellSystemInfo$() AS STRING + + ' Exit DVX: every running program is asked to close (each asks about + ' unsaved work as when you close it), then DVX ends. A program that + ' stays open stops the exit. Ask the user first. + DECLARE SUB ShellExitDvx() + + ' ---- Keys that work in every program ---- + + ' Make keyText (for example "Ctrl+Alt+E"; it needs Ctrl or Alt, or a + ' key such as F2) work whichever program is in front. Returns the + ' hotkey's id, or 0 when keyText is not a key or another program + ' already uses it. + DECLARE FUNCTION HotkeyAdd(BYVAL keyText AS STRING) AS LONG + + ' Stop a hotkey of this program working. + DECLARE SUB HotkeyRemove(BYVAL id AS LONG) + + ' The id of the oldest press of one of this program's hotkeys not yet + ' asked for, 0 when none: call it from a Timer. + DECLARE FUNCTION HotkeyNext() AS LONG + + ' keyText the way DVX writes keys ("ctrl+alt+e" gives "Ctrl+Alt+E"), + ' or "" when it is not a key. + DECLARE FUNCTION HotkeyText$(BYVAL keyText AS STRING) AS STRING + + ' ---- Program groups (Program Manager's PROGMAN.INI) ---- + + ' Open a groups file (Program Manager keeps its own in + ' ShellConfigFile$("progman.ini") of Program Manager); a file never + ' saved gives the groups Main and Startup. Returns a handle. Groups + ' and items are numbered from 0; group 0 is Installed Programs, which + ' ShellGroupsScan fills and which is never saved or changed by hand. + DECLARE FUNCTION ShellGroupsLoad(BYVAL path AS STRING) AS LONG + + ' Save the groups, options and Run history (the old file is replaced + ' only once the new one is complete). True when saved. + DECLARE FUNCTION ShellGroupsSave(BYVAL groups AS LONG, BYVAL path AS STRING) AS LONG + + DECLARE SUB ShellGroupsFree(BYVAL groups AS LONG) + + ' Fill Installed Programs with every program in ShellProgramsFolder$ + ' except skipPath ("" for none), sorted by name. Returns how many. + DECLARE FUNCTION ShellGroupsScan(BYVAL groups AS LONG, BYVAL skipPath AS STRING) AS LONG + + ' True when a program was installed or removed since ShellGroupsScan + ' (quick: no program is opened). + DECLARE FUNCTION ShellGroupsChanged(BYVAL groups AS LONG, BYVAL skipPath AS STRING) AS LONG + + ' The options: "view" (SHELL_VIEW_*), "group" (the group shown), + ' "autoArrange", "minimizeOnRun", "restoreAlone" (True / False). + ' ShellGroupsGet gives -1 and ShellGroupsSet False for an unknown name + ' or a value out of range. + DECLARE FUNCTION ShellGroupsGet(BYVAL groups AS LONG, BYVAL optionName AS STRING) AS LONG + DECLARE FUNCTION ShellGroupsSet(BYVAL groups AS LONG, BYVAL optionName AS STRING, BYVAL value AS LONG) AS LONG + + ' The File > Run history, newest first, one command per line (what + ' ShellRunDialog$ takes), and adding a command to it (it moves to the + ' front; ten are kept). + DECLARE FUNCTION ShellGroupsHistory$(BYVAL groups AS LONG) AS STRING + DECLARE SUB ShellGroupsHistoryAdd(BYVAL groups AS LONG, BYVAL commandLine AS STRING) + + ' The groups: how many, a group's name, the group called groupName + ' (any case; -1 for none). + DECLARE FUNCTION ShellGroupCount(BYVAL groups AS LONG) AS LONG + DECLARE FUNCTION ShellGroupName$(BYVAL groups AS LONG, BYVAL group AS LONG) AS STRING + DECLARE FUNCTION ShellGroupFind(BYVAL groups AS LONG, BYVAL groupName AS STRING) AS LONG + + ' True when groupName (blanks trimmed) can name group except (-1 for a + ' new group): not empty and no other group's name. + DECLARE FUNCTION ShellGroupNameValid(BYVAL groups AS LONG, BYVAL groupName AS STRING, BYVAL except AS LONG) AS LONG + + ' Add a group (its index, or -1), delete one with its items, rename + ' one (True when done). + DECLARE FUNCTION ShellGroupAdd(BYVAL groups AS LONG, BYVAL groupName AS STRING) AS LONG + DECLARE FUNCTION ShellGroupDelete(BYVAL groups AS LONG, BYVAL group AS LONG) AS LONG + DECLARE FUNCTION ShellGroupRename(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL groupName AS STRING) AS LONG + + ' Grid cells of a group's items for an icon view cols cells wide: + ' ShellGroupArrange puts every item in order (Auto Arrange), + ' ShellGroupPlaceUnplaced gives each item without a cell the first + ' free one, ShellGroupCellItem is the item in a cell (-1 for none). + DECLARE SUB ShellGroupArrange(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL cols AS LONG) + DECLARE SUB ShellGroupPlaceUnplaced(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL cols AS LONG) + DECLARE FUNCTION ShellGroupCellItem(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL col AS LONG, BYVAL row AS LONG) AS LONG + + ' The items of a group. + DECLARE FUNCTION ShellItemCount(BYVAL groups AS LONG, BYVAL group AS LONG) AS LONG + + ' Add an item (a name and a program, or a document opened by its file + ' type) at the end of a group; its index, or -1. + DECLARE FUNCTION ShellItemAdd(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL itemName AS STRING, BYVAL program AS STRING) AS LONG + + ' An item's field as text: "name", "program", "args", "folder" (where + ' its file arguments are looked for), "icon" (a picture file, "" for + ' the program's own), "key" (its shortcut, "" for none), "minimized" + ' ("1" to start it minimized, else "0"), "col" and "row" (its grid + ' cell, -1 when not placed) and "description" (its tooltip). + DECLARE FUNCTION ShellItemGet$(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL field AS STRING) AS STRING + + ' Change a field. False for an unknown field, an empty name or + ' program, a shortcut ShellItemKeyValid refuses, or an item of + ' Installed Programs. + DECLARE FUNCTION ShellItemSet(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL field AS STRING, BYVAL value AS STRING) AS LONG + + ' Delete an item; copy or move it to group toGroup (the new index, or + ' -1); move it to position newIndex in its group's order; put it in a + ' grid cell (an item already there takes its old cell). + DECLARE FUNCTION ShellItemDelete(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG) AS LONG + DECLARE FUNCTION ShellItemCopy(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL toGroup AS LONG) AS LONG + DECLARE FUNCTION ShellItemMove(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL toGroup AS LONG) AS LONG + DECLARE FUNCTION ShellItemReorder(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL newIndex AS LONG) AS LONG + DECLARE SUB ShellItemPlace(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL col AS LONG, BYVAL row AS LONG) + + ' True when searchText (any case; "" matches everything) is in the item's + ' name, description or program file name: Program Manager's search. + DECLARE FUNCTION ShellItemMatches(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL searchText AS STRING) AS LONG + + ' True when keyText can be an item's shortcut key: Ctrl together with + ' Alt or Shift ("Ctrl+Alt+P"), so it never takes a key a program uses + ' itself; "" (no shortcut) is valid too. + DECLARE FUNCTION ShellItemKeyValid(BYVAL keyText AS STRING) AS LONG + + ' The file an item's icon comes from: iconFile when it is a picture, + ' else the item's program (its icon) or picture document, else the + ' program that opens the document; "" when none of them has one. + ' LOADPICTURE of it shows the icon. + DECLARE FUNCTION ShellItemIcon$(BYVAL iconFile AS STRING, BYVAL program AS STRING) AS STRING + + ' ---- Command lines ---- + + ' The program part and the arguments part of a command line, split as + ' ShellRunCommand splits it: the first word, or the text in double + ' quotes when it starts with one, and the rest without the blanks + ' around it. + DECLARE FUNCTION ShellCommandProgram$(BYVAL commandLine AS STRING) AS STRING + DECLARE FUNCTION ShellCommandArgs$(BYVAL commandLine AS STRING) AS STRING + + ' How many words a command line (or COMMAND$) has, and word index (0 + ' is the first) without its quotes; "" past the last. Words are + ' separated by blanks; one with blanks inside is written in double + ' quotes ("My Letter.txt"). + DECLARE FUNCTION ShellArgCount(BYVAL commandLine AS STRING) AS LONG + DECLARE FUNCTION ShellArg$(BYVAL commandLine AS STRING, BYVAL index AS LONG) AS STRING + + ' ---- This program and DVX ---- + + ' This program's id (see ShellRunningId). + DECLARE FUNCTION ShellAppId() AS LONG + + ' Give this program more or less of the computer's time: a + ' SHELL_PRIORITY_ value from SHELL_PRIORITY_LOW to SHELL_PRIORITY_HIGH + ' (any number between works). True when it was set. A busy program + ' at low priority leaves the others room; at high priority it works + ' faster while others slow down. + DECLARE FUNCTION ShellSetPriority(BYVAL priority AS LONG) AS LONG + + ' This program's priority (SHELL_PRIORITY_NORMAL unless changed). + DECLARE FUNCTION ShellPriority() AS LONG + + ' The path of this program's .app, as it was started (the path to give + ' ShellRun to start it again). + DECLARE FUNCTION ShellAppPath$() AS STRING + + ' The full path of fileName in this program's settings folder (in + ' DVX's CONFIG folder; the same place for every copy of the program, + ' where DVX's own programs keep their settings). The folder is made + ' when it does not exist yet; "" when it cannot be made. + DECLARE FUNCTION ShellConfigFile$(BYVAL fileName AS STRING) AS STRING + + ' How many copies of this program are running, this one included + ' (see Project Properties: Allow more than one copy to run at once). + DECLARE FUNCTION ShellInstanceCount() AS LONG + + ' Write one line to the DVX log, DVX.LOG in the DVX folder. + DECLARE SUB ShellLog(BYVAL text AS STRING) + + ' The desktop DVX starts the next time it starts (instead of Program + ' Manager): "" for the standard one. When the desktop ends, DVX + ' ends. Returns True when the DVX settings (DVX.INI) were saved. + DECLARE FUNCTION ShellSetDesktop(BYVAL program AS STRING) AS LONG + + ' The program DVX starts after the desktop the next time it starts + ' (the first-run Welcome window): "" for none. Returns True when the + ' DVX settings (DVX.INI) were saved. + DECLARE FUNCTION ShellSetWelcome(BYVAL program AS STRING) AS LONG +END DECLARE diff --git a/src/include/basic/sound.bas b/src/include/basic/sound.bas index 4381ec00..72b01a9d 100644 --- a/src/include/basic/sound.bas +++ b/src/include/basic/sound.bas @@ -32,11 +32,89 @@ ' when the sound could not be played (SoundError$ says why). Volumes are ' percent (0 to 100). ' +' The second half is for a program that changes DVX's own sound +' settings, as the Control Panel's Sounds page does: the sound DVX plays +' for each of its events, the master volume, the sound driver and where +' MIDI music plays. Those settings live in DVX.INI; the functions that +' read or write them take a handle from PrefsLoad (prefs.bas, with +' PrefsDvxIni$ for the file), and the program saves it with PrefsSave. +' Events, drivers and MIDI choices are numbered from 0, so they fill a +' ListBox or DropDown in order. +' ' Usage: Add this file to your project. ' Voice number returned when a sound could not be played CONST SOUND_FAILED = 0 +' DVX's event sounds, for SoundPlayEvent and the SoundEvent... functions +CONST SOUND_EVENT_DEFAULT = 0 +CONST SOUND_EVENT_START = 1 +CONST SOUND_EVENT_EXIT = 2 +CONST SOUND_EVENT_ERROR = 3 +CONST SOUND_EVENT_WARNING = 4 +CONST SOUND_EVENT_QUESTION = 5 +CONST SOUND_EVENT_INFO = 6 +CONST SOUND_EVENT_WINDOW_OPEN = 7 +CONST SOUND_EVENT_WINDOW_CLOSE = 8 + +' Pan of a voice (percent; the side away from the pan is turned down) +CONST SOUND_PAN_LEFT = -100 +CONST SOUND_PAN_CENTER = 0 +CONST SOUND_PAN_RIGHT = 100 + +' Pitch of a voice in percent of its recorded speed (200 is an octave +' higher, 50 an octave lower) +CONST SOUND_PITCH_NORMAL = 100 +CONST SOUND_PITCH_MIN = 25 +CONST SOUND_PITCH_MAX = 400 + +' Sample rates (hertz) of sounds made with SoundCreate and of streams +CONST SOUND_RATE_MIN = 4000 +CONST SOUND_RATE_MAX = 48000 +CONST SOUND_RATE_DEFAULT = 22050 + +' How much sound a stream holds (milliseconds) +CONST SOUND_STREAM_MS_MIN = 20 +CONST SOUND_STREAM_MS_DEFAULT = 250 +CONST SOUND_STREAM_MS_MAX = 2000 + +' The FM synthesizer chip: the most voices it can have, its own sample +' rate (frequency numbers are based on it: hertz = fnum * FM_CHIP_HZ * +' 2 ^ block / 2 ^ 20), the frequency number and block limits, and where +' the high frequency bits and the block go in FM_KEY_BLOCK +CONST FM_VOICES_MAX = 18 +CONST FM_CHIP_HZ = 49716 +CONST FM_FNUM_MAX = 1023 +CONST FM_BLOCK_MAX = 7 +CONST FM_FNUM_HI_SHIFT = 8 +CONST FM_BLOCK_SHIFT = 2 + +' Registers of a voice's own channel for FmWrite (modulator / carrier) +CONST FM_MOD_CHAR = &H20 +CONST FM_CAR_CHAR = &H23 +CONST FM_MOD_LEVEL = &H40 +CONST FM_CAR_LEVEL = &H43 +CONST FM_MOD_ATTACK = &H60 +CONST FM_CAR_ATTACK = &H63 +CONST FM_MOD_SUSTAIN = &H80 +CONST FM_CAR_SUSTAIN = &H83 +CONST FM_FNUM_LOW = &HA0 +CONST FM_KEY_BLOCK = &HB0 +CONST FM_FEEDBACK = &HC0 +CONST FM_MOD_WAVE = &HE0 +CONST FM_CAR_WAVE = &HE3 + +' Bits of those registers +CONST FM_KEY_ON = &H20 +CONST FM_OUT_LEFT = &H10 +CONST FM_OUT_RIGHT = &H20 +CONST FM_OUT_BOTH = &H30 +CONST FM_ADDITIVE = &H01 + +' Registers per bank of the whole chip (FmRawWrite: bank * FM_BANK_REGS +' + register; bank 1 only on an OPL3) +CONST FM_BANK_REGS = &H100 + DECLARE LIBRARY "audio" ' Play a WAV or VOC file once. DECLARE FUNCTION PlayWave(BYVAL fileName AS STRING) AS LONG @@ -70,4 +148,152 @@ DECLARE LIBRARY "audio" ' Why the last sound could not be played. DECLARE FUNCTION SoundError$() AS STRING + + ' Play a triangle tone (softer than PlayTone's square tone). + DECLARE FUNCTION PlayTriangle(BYVAL frequency AS LONG, BYVAL milliseconds AS LONG, BYVAL volume AS LONG) AS LONG + + ' The volume of everything this program plays (percent). + DECLARE FUNCTION SoundGetVolume() AS LONG + + ' What the sound device can do, by name (any case): "dac" (plays + ' recorded sound), "synth" (has a music synthesizer), "tone" (plays + ' tones on a tone generator), "running" -- True or False; "voices" + ' (sounds at once), "rate" (hertz), "bits", "channels" -- numbers. + DECLARE FUNCTION SoundCaps(BYVAL field AS STRING) AS LONG + + ' Play one of DVX's event sounds (SOUND_EVENT_...), the one the + ' settings choose; with wait True, return once it has played. + DECLARE SUB SoundPlayEvent(BYVAL eventIdx AS LONG, BYVAL wait AS LONG) + + ' ---- Loaded sounds ---- + + ' Load a WAV or VOC file once, to play it as often as you like. + ' Returns a sound number, or 0 (SoundError$ says why). + DECLARE FUNCTION SoundLoad(BYVAL fileName AS STRING) AS LONG + + ' Make a sound from raw sound data: 8-bit (0 to 255, 128 is silence) + ' or 16-bit (two bytes each, low byte first) samples, mono or stereo + ' (left, right, left, ...), at rate hertz. Returns a sound number, + ' or 0. + DECLARE FUNCTION SoundCreate(BYVAL pcm AS BINARY STRING, BYVAL rate AS LONG, BYVAL bits AS LONG, BYVAL channels AS LONG) AS LONG + + ' Stop a sound's voices and forget it. + DECLARE SUB SoundFree(BYVAL snd AS LONG) + + ' How long the sound is (milliseconds). + DECLARE FUNCTION SoundLength(BYVAL snd AS LONG) AS LONG + + ' Play a sound on a new voice at volume percent and pan + ' (SOUND_PAN_LEFT to SOUND_PAN_RIGHT), once or over and over. + ' Returns a voice number for StopWave and the SoundSet... calls. + DECLARE FUNCTION SoundPlay(BYVAL snd AS LONG, BYVAL volume AS LONG, BYVAL pan AS LONG, BYVAL loopIt AS LONG) AS LONG + + ' Change a voice while it plays: its volume (percent), its pan + ' (-100 to 100), its pitch (percent of the recorded speed, 25 to + ' 400) and a tone's frequency (hertz). + DECLARE SUB SoundSetVolume(BYVAL voice AS LONG, BYVAL volume AS LONG) + DECLARE SUB SoundSetPan(BYVAL voice AS LONG, BYVAL pan AS LONG) + DECLARE SUB SoundSetPitch(BYVAL voice AS LONG, BYVAL percent AS LONG) + DECLARE SUB SoundSetFrequency(BYVAL voice AS LONG, BYVAL frequency AS LONG) + + ' ---- Streams: sound your program makes as it goes ---- + + ' Open a stream of rate hertz, 8 or 16 bits, 1 or 2 channels, holding + ' bufferMs milliseconds (0 for the usual). It plays at once (silence + ' until you write). Returns a stream number, or 0. + DECLARE FUNCTION SoundStreamOpen(BYVAL rate AS LONG, BYVAL bits AS LONG, BYVAL channels AS LONG, BYVAL bufferMs AS LONG) AS LONG + + ' Add sound data in the stream's format; returns how many bytes were + ' taken (keep the rest and write it again later). + DECLARE FUNCTION SoundStreamWrite(BYVAL stream AS LONG, BYVAL pcm AS BINARY STRING) AS LONG + + ' Samples (frames) of room left, and samples waiting to play. + DECLARE FUNCTION SoundStreamSpace(BYVAL stream AS LONG) AS LONG + DECLARE FUNCTION SoundStreamQueued(BYVAL stream AS LONG) AS LONG + + ' The stream's voice, for SoundSetVolume, SoundSetPan and + ' SoundSetPitch. + DECLARE FUNCTION SoundStreamVoice(BYVAL stream AS LONG) AS LONG + + ' Stop the stream and close it. + DECLARE SUB SoundStreamClose(BYVAL stream AS LONG) + + ' ---- The FM synthesizer chip (AdLib-style instruments) ---- + + ' The chip by name (any case): "description" (like "OPL3 FM + ' synthesizer at 388h", "" when there is none), "voices", "free" + ' (voices no program has), "stereo", "owned" (one program has the + ' whole chip) -- numbers and True / False as text. + DECLARE FUNCTION FmInfo$(BYVAL field AS STRING) AS STRING + + ' Take a voice of the chip (0 when none is free), set its registers + ' (FM_... above; True when it worked) and give it back. + DECLARE FUNCTION FmAlloc() AS LONG + DECLARE FUNCTION FmWrite(BYVAL voice AS LONG, BYVAL reg AS LONG, BYVAL value AS LONG) AS LONG + DECLARE SUB FmFree(BYVAL voice AS LONG) + + ' The whole chip for this program alone (False while another program + ' has it), any register of it, and giving it back. + DECLARE FUNCTION FmRawAcquire() AS LONG + DECLARE FUNCTION FmRawWrite(BYVAL reg AS LONG, BYVAL value AS LONG) AS LONG + DECLARE SUB FmRawRelease() + + ' ---- DVX's own sounds and sound settings ---- + + ' How many events DVX plays a sound for (numbered 0 to + ' SoundEventCount() - 1). + DECLARE FUNCTION SoundEventCount() AS LONG + + ' The event's name for people, like "Start DVX" or "Error". + DECLARE FUNCTION SoundEventLabel$(BYVAL eventIdx AS LONG) AS STRING + + ' The event's key in DVX.INI's [sounds] section, like "startup". + DECLARE FUNCTION SoundEventKey$(BYVAL eventIdx AS LONG) AS STRING + + ' The sound file the settings name for the event ("" for none). + DECLARE FUNCTION SoundEventFile$(BYVAL prefs AS LONG, BYVAL eventIdx AS LONG) AS STRING + + ' Set the event's sound file in the settings ("" for none). + DECLARE SUB SoundSetEventFile(BYVAL prefs AS LONG, BYVAL eventIdx AS LONG, BYVAL fileName AS STRING) + + ' After DVX.INI was saved: DVX plays the new event sounds from now on. + DECLARE SUB SoundEventsReload() + + ' Play a file the way DVX plays its event sounds, to try it out. + ' Returns a voice number, or 0 (SoundError$ says why). + DECLARE FUNCTION SoundTestFile(BYVAL fileName AS STRING) AS LONG + + ' The folder of DVX's own sound files. + DECLARE FUNCTION SoundFolder$() AS STRING + + ' The volume of all sound from every program (percent); setting it + ' takes effect at once but does not change DVX.INI. + DECLARE FUNCTION SoundMasterVolume() AS LONG + DECLARE SUB SoundSetMasterVolume(BYVAL percent AS LONG) + + ' The sound drivers installed: SoundDriverCount reads the list, + ' SoundDriverName$ gives entry index (0 to SoundDriverCount() - 1), + ' like "sb" or "speaker". + DECLARE FUNCTION SoundDriverCount() AS LONG + DECLARE FUNCTION SoundDriverName$(BYVAL index AS LONG) AS STRING + + ' The driver setting that means "find the best one" ("auto"). + DECLARE FUNCTION SoundDriverAuto$() AS STRING + + ' Where MIDI music can play: how many choices, and each one's name + ' for people ("Automatic", "MIDI port (MPU-401)", ...). + DECLARE FUNCTION SoundMidiCount() AS LONG + DECLARE FUNCTION SoundMidiLabel$(BYVAL index AS LONG) AS STRING + + ' The settings' sound driver (SoundDriverAuto$ when none is set), + ' MIDI choice (an index for SoundMidiLabel$) and master volume + ' (percent). + DECLARE FUNCTION SoundSettingDriver$(BYVAL prefs AS LONG) AS STRING + DECLARE FUNCTION SoundSettingMidi(BYVAL prefs AS LONG) AS LONG + DECLARE FUNCTION SoundSettingVolume(BYVAL prefs AS LONG) AS LONG + + ' Store the sound driver, MIDI choice and master volume in the + ' settings. A new driver or MIDI choice takes effect when DVX next + ' starts. + DECLARE SUB SoundSettingsStore(BYVAL prefs AS LONG, BYVAL driver AS STRING, BYVAL midi AS LONG, BYVAL volume AS LONG) END DECLARE diff --git a/src/include/basic/system.bas b/src/include/basic/system.bas new file mode 100644 index 00000000..d5c2fbfd --- /dev/null +++ b/src/include/basic/system.bas @@ -0,0 +1,217 @@ +' 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. + + +' system.bas -- System Settings Library for DVX BASIC +' Changes what Control Panel changes, for the whole desktop and at once: +' the screen mode, the system colors and color themes, the wallpaper and +' the mouse. Together with prefs.bas (DVX.INI), it lets a program save +' these settings so DVX starts with them. Also reads the system font's +' character pictures (glyphs), the memory, the mouse pointer and the +' Shift / Ctrl / Alt keys anywhere on the screen, moves the pointer, +' types keys into the window in front and saves the screen as a picture. +' Usage: Add this file and prefs.bas to your project. +' +' Colors are RGB() values. Lists (screen modes, colors) are numbered +' from 0. The *Store and *Load routines write and read the settings in +' the layout of DVX.INI through a prefs.bas handle: PrefsLoad(PrefsDvxIni$()) +' to save them for the next start, or a PrefsCreate handle as a snapshot +' in memory to go back to (what Cancel in Control Panel does). +' +' The mouse's acceleration, scroll wheel direction and wallpaper mode +' choices are the prefs.bas choice lists PREFS_CHOICE_ACCEL, +' PREFS_CHOICE_WHEEL and PREFS_CHOICE_WALLPAPER. + +' Mouse limits and DVX's defaults (SysSetMouse) +CONST MOUSE_WHEEL_STEP_MIN = 1 ' lines per wheel notch, least +CONST MOUSE_WHEEL_STEP_MAX = 10 ' lines per wheel notch, most +CONST MOUSE_WHEEL_STEP_DEFAULT = 3 +CONST MOUSE_DBLCLICK_DEFAULT_MS = 500 ' double-click time in milliseconds +CONST MOUSE_SPEED_DEFAULT = 8 ' mickeys per 8 pixels: the normal speed + +' The screen mode DVX starts in when DVX.INI names none (or safe mode) +CONST SYS_VIDEO_DEFAULT_WIDTH = 640 +CONST SYS_VIDEO_DEFAULT_HEIGHT = 480 +CONST SYS_VIDEO_DEFAULT_BPP = 16 + +DECLARE LIBRARY "dvxshell" + ' ---- Screen mode ---- + + ' How many screen modes the display offers. + DECLARE FUNCTION SysVideoModeCount() AS LONG + + ' Width, height and bits per pixel (8, 15, 16, 24 or 32) of mode + ' index; 0 when there is no such mode. + DECLARE FUNCTION SysVideoModeWidth(BYVAL index AS LONG) AS LONG + DECLARE FUNCTION SysVideoModeHeight(BYVAL index AS LONG) AS LONG + DECLARE FUNCTION SysVideoModeBpp(BYVAL index AS LONG) AS LONG + + ' Change the screen mode. Every window is kept. Returns True when it + ' changed; False keeps the old mode (also while a program has the + ' whole screen to itself). A mode the monitor cannot show leaves the + ' user unable to answer, so ask "Keep this mode?" with a timeout that + ' goes back, as Control Panel does. + DECLARE FUNCTION SysSetVideoMode(BYVAL wide AS LONG, BYVAL high AS LONG, BYVAL bpp AS LONG) AS LONG + + ' Save the whole screen as a picture file: BMP when path ends in + ' .bmp, PNG otherwise. True when it was written. + DECLARE FUNCTION SysScreenshot(BYVAL path AS STRING) AS LONG + + ' ---- Colors ---- + + ' How many system colors there are (ids 0 to SysColorCount() - 1). + DECLARE FUNCTION SysColorCount() AS LONG + + ' What a list shows for color id ("Desktop", "Window Face" ...). + DECLARE FUNCTION SysColorLabel$(BYVAL id AS LONG) AS STRING + + ' The name color id is stored as in DVX.INI and theme files. + DECLARE FUNCTION SysColorName$(BYVAL id AS LONG) AS STRING + + ' The id of the color stored as colorName (in any case), or -1; for + ' drawing in the user's colors, e.g. SysGetColor(SysColorId("desktop")). + DECLARE FUNCTION SysColorId(BYVAL colorName AS STRING) AS LONG + + ' A system color as an RGB() value. + DECLARE FUNCTION SysGetColor(BYVAL id AS LONG) AS LONG + + ' Set a system color to an RGB() value. The whole screen is redrawn. + DECLARE SUB SysSetColor(BYVAL id AS LONG, BYVAL rgbValue AS LONG) + + ' Every color back to DVX's own scheme. + DECLARE SUB SysResetColors() + + ' Use the colors of a theme file (.THM, in CONFIG\THEMES). Returns + ' True when the file was read. + DECLARE FUNCTION SysLoadTheme(BYVAL path AS STRING) AS LONG + + ' Save the colors as a theme file. Returns True when it was saved. + DECLARE FUNCTION SysSaveTheme(BYVAL path AS STRING) AS LONG + + ' Store every color in [colors] of a prefs.bas handle. + DECLARE SUB SysColorsStore(BYVAL prefs AS LONG) + + ' Use the [colors] of a prefs.bas handle. Returns True when it had any. + DECLARE FUNCTION SysColorsLoad(BYVAL prefs AS LONG) AS LONG + + ' ---- Wallpaper ---- + + ' Show an image file (BMP, JPG, PNG or GIF) behind the desktop; "" + ' for none. Returns True when it was loaded (False keeps the old one). + DECLARE FUNCTION SysSetWallpaper(BYVAL path AS STRING) AS LONG + + ' The wallpaper's image file ("" for none). + DECLARE FUNCTION SysWallpaper$() AS STRING + + ' How the wallpaper fills the screen: a choice of PREFS_CHOICE_WALLPAPER + ' (Stretch, Tile, Center). Kept for the next wallpaper when none is shown. + DECLARE SUB SysSetWallpaperMode(BYVAL mode AS LONG) + DECLARE FUNCTION SysWallpaperMode() AS LONG + + ' Store the wallpaper and its mode in [desktop] of a prefs.bas handle. + DECLARE SUB SysWallpaperStore(BYVAL prefs AS LONG) + + ' Use the [desktop] wallpaper and mode of a prefs.bas handle (no + ' wallpaper when it names none). False when the image could not be + ' loaded. + DECLARE FUNCTION SysWallpaperLoad(BYVAL prefs AS LONG) AS LONG + + ' ---- Mouse ---- + + ' Apply a mouse setup: wheelDir 1 normal or -1 reversed (a value of + ' PREFS_CHOICE_WHEEL); dblClickMs the double-click time in + ' milliseconds; accel the acceleration (a value of PREFS_CHOICE_ACCEL); + ' mickeys the mouse movement per 8 pixels of pointer movement (8 is + ' the normal speed, higher is slower); wheelStep the lines scrolled per + ' wheel notch (MOUSE_WHEEL_STEP_MIN to MOUSE_WHEEL_STEP_MAX). An accel + ' or mickeys of 0 leaves that setting as it is. + DECLARE SUB SysSetMouse(BYVAL wheelDir AS LONG, BYVAL dblClickMs AS LONG, BYVAL accel AS LONG, BYVAL mickeys AS LONG, BYVAL wheelStep AS LONG) + + ' The mouse setup in effect (the values SysSetMouse takes). + DECLARE FUNCTION SysMouseWheelDir() AS LONG + DECLARE FUNCTION SysMouseDoubleClick() AS LONG + DECLARE FUNCTION SysMouseAccel() AS LONG + DECLARE FUNCTION SysMouseMickeys() AS LONG + DECLARE FUNCTION SysMouseWheelStep() AS LONG + + ' Store the mouse setup in [mouse] of a prefs.bas handle. + DECLARE SUB SysMouseStore(BYVAL prefs AS LONG) + + ' Apply the [mouse] setup of a prefs.bas handle (DVX's defaults for + ' settings it does not have). + DECLARE SUB SysMouseLoad(BYVAL prefs AS LONG) + + ' ---- The pointer and the keyboard, anywhere on the screen ---- + + ' Where the mouse pointer is, in screen pixels from the top left + ' corner, and the buttons held (vbLeftButton, vbRightButton and + ' vbMiddleButton bits) -- also over other programs' windows. + DECLARE FUNCTION SysMouseX() AS LONG + DECLARE FUNCTION SysMouseY() AS LONG + DECLARE FUNCTION SysMouseButtons() AS LONG + + ' Move the mouse pointer to screen position x, y (kept on the + ' screen). Windows under it see the move as if the user made it. + ' There is no way to press the mouse buttons from a program: to + ' "click" a control, call its event procedure or set its value. + DECLARE SUB SysSetMousePos(BYVAL x AS LONG, BYVAL y AS LONG) + + ' The Shift, Ctrl and Alt keys held now: vbShiftMask, vbCtrlMask and + ' vbAltMask bits. + DECLARE FUNCTION SysShiftState() AS LONG + + ' Type a key into the window that has the focus, as if the user + ' pressed it (like VB's SendKeys): keyCode as a KeyDown event gets it + ' -- a character code such as ASC("a") or 13 for Enter, or a vbKey + ' constant (vbKeyF1, vbKeyLeft ...) -- with shift the vbShiftMask, + ' vbCtrlMask and vbAltMask bits held with it (SysSendKey ASC("s"), + ' vbCtrlMask is Ctrl+S). The keys arrive in order once your code has + ' returned to DVX (the end of the event procedure), one at a time; + ' a key for your own program's form arrives as the user's would. + ' True when the key was queued; False when 64 keys are waiting + ' already. + DECLARE FUNCTION SysSendKey(BYVAL keyCode AS LONG, BYVAL shift AS LONG) AS LONG + + ' ---- Memory ---- + + ' The computer's memory in KB, and how much of it is free; -1 when + ' it cannot be told. + DECLARE FUNCTION SysMemoryTotalKb() AS LONG + DECLARE FUNCTION SysMemoryFreeKb() AS LONG + + ' ---- System font ---- + + ' The character cell of the system font in pixels: every character + ' is this wide and high. + DECLARE FUNCTION SysFontWidth() AS LONG + DECLARE FUNCTION SysFontHeight() AS LONG + + ' The characters the font has: codes SysFontFirstChar() to + ' SysFontFirstChar() + SysFontCharCount() - 1. + DECLARE FUNCTION SysFontFirstChar() AS LONG + DECLARE FUNCTION SysFontCharCount() AS LONG + + ' Row row (0 at the top) of the picture of character code: one bit + ' per pixel, &H80 the leftmost. 0 for a character or row the font + ' does not have. + DECLARE FUNCTION SysFontGlyphRow(BYVAL code AS LONG, BYVAL row AS LONG) AS LONG +END DECLARE diff --git a/src/include/basic/xfer.bas b/src/include/basic/xfer.bas index ca15534b..d891f718 100644 --- a/src/include/basic/xfer.bas +++ b/src/include/basic/xfer.bas @@ -22,16 +22,31 @@ ' xfer.bas -- File Transfer Library for DVX BASIC ' -' Sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM over -' a serial port opened with SerOpen (from comm.bas). Start a transfer, -' then call XferPoll often (from a Timer, for example) until it returns -' something other than XFER_RUNNING. While a transfer runs, do not read -' the port yourself and keep it detached from any Terminal control. +' Sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A +' transfer is a handle. XferSendFile and XferReceive start one over a +' serial port opened with SerOpen (from comm.bas); then call XferPoll +' often (from a Timer, for example) until it returns something other than +' XFER_RUNNING. While a transfer runs, do not read the port yourself and +' keep it detached from any Terminal control. On such a port a received +' file never replaces an existing one: the file that is already there is +' kept and the new one gets a new name (NAME~1.EXT). ' -' Received files never replace existing ones: a file that is already -' there is kept and the new one gets a new name (NAME~1.EXT). +' Transfers over a connection (ConnSendFiles and ConnReceiveFiles, from +' conn.bas) give the same handles: there ConnPoll moves the transfer +' along, and when an incoming file already exists the transfer can stop +' with XFER_ASKING until XferAnswer says what to do. ' -' Usage: Add this file (and comm.bas) to your project. +' The progress functions (XferBytes, XferBatchBytes, XferFileIndex, ...) +' work on every handle. Free a handle with XferClose when you are done; +' whatever a program leaves open is closed when it ends. +' +' Usage: Add this file (and comm.bas or conn.bas) to your project. + +' The most files one batch sends (ConnSendFiles in conn.bas) +CONST XFER_MAX_FILES = 16 + +' The largest ZMODEM receive window (ConnSetXferWindow in conn.bas) +CONST XFER_WINDOW_MAX = 65535 ' Protocols CONST XFER_XMODEM = 0 @@ -39,12 +54,22 @@ CONST XFER_XMODEM1K = 1 CONST XFER_YMODEM = 2 CONST XFER_ZMODEM = 3 -' XferPoll results +' XferPoll and XferStatus results +CONST XFER_IDLE = 0 CONST XFER_RUNNING = 1 +CONST XFER_ASKING = 2 CONST XFER_DONE = 3 CONST XFER_FAILED = 4 CONST XFER_CANCELLED = 5 +' What happens to a received file that already exists (XferAnswer, and +' ConnReceiveFiles in conn.bas) +CONST XFER_EXIST_ASK = 0 +CONST XFER_EXIST_RENAME = 1 +CONST XFER_EXIST_RESUME = 2 +CONST XFER_EXIST_OVERWRITE = 3 +CONST XFER_EXIST_SKIP = 4 + DECLARE LIBRARY "xfer" ' Send one file. com is the port number (1-4). Returns a handle, @@ -59,6 +84,16 @@ DECLARE LIBRARY "xfer" ' Move the transfer along. Returns XFER_RUNNING until it ends. DECLARE FUNCTION XferPoll(BYVAL handle AS LONG) AS LONG + ' Where the transfer is (XFER_RUNNING ... XFER_CANCELLED) without + ' moving it along; XFER_IDLE for a bad handle. + DECLARE FUNCTION XferStatus(BYVAL handle AS LONG) AS LONG + + ' True when the transfer sends, False when it receives. + DECLARE FUNCTION XferSending(BYVAL handle AS LONG) AS LONG + + ' The protocol (XFER_XMODEM ... XFER_ZMODEM). + DECLARE FUNCTION XferProtocol(BYVAL handle AS LONG) AS LONG + ' Bytes of the current file done so far, and its size (-1 = unknown). DECLARE FUNCTION XferBytes(BYVAL handle AS LONG) AS LONG DECLARE FUNCTION XferSize(BYVAL handle AS LONG) AS LONG @@ -66,6 +101,31 @@ DECLARE LIBRARY "xfer" ' The file being transferred, as stored on this computer. DECLARE FUNCTION XferFile$(BYVAL handle AS LONG) AS STRING + ' Which file of the batch is moving (counted from 0), and how many + ' files the batch has (-1 while the sender has not said). + DECLARE FUNCTION XferFileIndex(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION XferFileCount(BYVAL handle AS LONG) AS LONG + + ' Bytes of the whole batch done so far, and its size (-1 = unknown). + DECLARE FUNCTION XferBatchBytes(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION XferBatchSize(BYVAL handle AS LONG) AS LONG + + ' Where the current file continued from (0 when it started at the + ' beginning), and how many times a block had to be sent again. + DECLARE FUNCTION XferResumedAt(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION XferErrors(BYVAL handle AS LONG) AS LONG + + ' XFER_ASKING: a received file already exists. XferFile$ names it, + ' XferAskSize is the size of the file already there, XferCanResume is + ' True when the transfer can continue it (XFER_EXIST_RESUME). Answer + ' with XferAnswer and an XFER_EXIST_ action (RESUME falls back to + ' RENAME when the file cannot be continued). + DECLARE FUNCTION XferAskSize(BYVAL handle AS LONG) AS LONG + ' The size of the file the sender offers (-1 = it did not say). + DECLARE FUNCTION XferAskRemoteSize(BYVAL handle AS LONG) AS LONG + DECLARE FUNCTION XferCanResume(BYVAL handle AS LONG) AS LONG + DECLARE SUB XferAnswer(BYVAL handle AS LONG, BYVAL action AS LONG) + ' How the transfer ended, or why it failed. DECLARE FUNCTION XferMessage$(BYVAL handle AS LONG) AS STRING @@ -74,4 +134,15 @@ DECLARE LIBRARY "xfer" ' Free the handle when you are done with it. DECLARE SUB XferClose(BYVAL handle AS LONG) + + ' Check values: the CRC-16 the XMODEM protocol uses (0 to + ' 65535) and the CRC-32 of ZMODEM and ZIP files (a LONG, negative when + ' its top bit is set). + DECLARE FUNCTION XferCrc16(BYVAL bytes AS BINARY STRING) AS LONG + DECLARE FUNCTION XferCrc32(BYVAL bytes AS BINARY STRING) AS LONG + + ' A file name made safe to store: an 8.3 name with no folder or + ' device name and only characters every disk accepts (what a receive + ' does with the names a sender gives). + DECLARE FUNCTION XferSafeName$(BYVAL fileName AS STRING) AS STRING END DECLARE diff --git a/src/installer/pkgBuild.c b/src/installer/pkgBuild.c index b675771f..88f9a90d 100644 --- a/src/installer/pkgBuild.c +++ b/src/installer/pkgBuild.c @@ -270,7 +270,12 @@ bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t err return false; } - qsort(list.names, (size_t)list.count, sizeof(char *), byName); + // An empty folder leaves list.names NULL; qsort's base must not be + // NULL even for a zero count. + if (list.names) { + qsort(list.names, (size_t)list.count, sizeof(char *), byName); + } + ok = mz_zip_writer_init_file(&zip, zipPath, 0); if (!ok) { diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c index f06a8327..925ff2a6 100644 --- a/src/installer/setupCore.c +++ b/src/installer/setupCore.c @@ -462,7 +462,11 @@ bool setupUninstall(const char *destDir, bool removeSettings, char *err, int32_t removeFile(path); } - qsort(dirs, (size_t)arrlen(dirs), sizeof(char *), byLengthDesc); + // dirs is still NULL when no name was in a folder; qsort's base must + // not be NULL even for a zero count. + if (dirs) { + qsort(dirs, (size_t)arrlen(dirs), sizeof(char *), byLengthDesc); + } for (int32_t i = 0; i < (int32_t)arrlen(dirs); i++) { rmdir(dirs[i]); diff --git a/src/libs/kpunch/audio/audio.c b/src/libs/kpunch/audio/audio.c index 1c4604d3..0849a7a0 100644 --- a/src/libs/kpunch/audio/audio.c +++ b/src/libs/kpunch/audio/audio.c @@ -62,7 +62,6 @@ DVX_ABI_STAMP(audio); #define INI_CHANNELS "channels" #define INI_VOICES "voices" #define INI_FM_TONES "fmTones" -#define PERCENT_MAX 100 #define ERROR_TEXT_UNKNOWN "Unknown error" @@ -331,6 +330,10 @@ void audioClose(AudioCtxT *ctx) { return; } + if (ctx->basic) { + audioBasForget(ctx); + } + for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) { if (sVoices[i].owner == ctx) { audioMixer.voices[i].state = MIX_FREE; @@ -1275,10 +1278,10 @@ static bool devOpen(void) { devConfig(prefs, &cfg); sDev.voiceLimit = (uint32_t)clampInt(prefsGetInt(prefs, AUDIO_INI_SECTION, INI_VOICES, AUDIO_VOICES_DEFAULT), 1, AUDIO_VOICES_MAX); - sMaster = clampInt(prefsGetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, PERCENT_MAX), 0, PERCENT_MAX) * AUDIO_VOLUME_MAX / PERCENT_MAX; + sMaster = audioPercentToVolume(audioSettingVolume(prefs)); sDev.fmTones = prefsGetBool(prefs, AUDIO_INI_SECTION, INI_FM_TONES, true); - const char *want = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, AUDIO_DRIVER_AUTO); + const char *want = audioSettingDriver(prefs); DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass); int32_t count = (int32_t)arrlen(mods); int32_t chosen = -1; diff --git a/src/libs/kpunch/audio/audio.dhs b/src/libs/kpunch/audio/audio.dhs index 8a65422c..a1a6d038 100644 --- a/src/libs/kpunch/audio/audio.dhs +++ b/src/libs/kpunch/audio/audio.dhs @@ -59,6 +59,12 @@ .index audioDeskPlayFile .index audioDriverNames .index Desktop sounds +.index audioDeskFile +.index audioDeskSetFile +.index audioSettingsStore +.index audioMidiChoices +.index audioPercentToVolume +.index SoundSettingsStore .h1 Sound Library @@ -184,9 +190,11 @@ DVX.INI section [audio]: midiBank the OP2 instrument bank for FM MIDI (music.lib), a path from the DVX folder .endtable +A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, volume) writes all three (an empty driver stores auto, a midi index out of range stores auto). audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these. + .h2 Desktop Sounds -The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file (a path from the DVX folder), empty or missing for none; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). The default set, made by DVX's own sound generator at build time, is in AUDIO_SOUNDS_DIR (config/sounds). +The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file (a path from the DVX folder), empty or missing for none; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in AUDIO_SOUNDS_DIR (config/sounds). .table Key Event @@ -214,7 +222,7 @@ The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h) mpu401 (synthesizer) An MPU-401 MIDI port in UART mode at mpuPort, only with midi = mpu401: reset (FFh, answered FEh; tried twice, since a port left in UART mode takes the first FFh as data), then UART mode (3Fh). Bytes are sent when the port says it is ready (a short wait, else the caller sends later); input the port has (active sensing) is read and dropped. Closing sends All Notes Off on every channel and resets the port. .endtable -audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen). +audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for). Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Sound Blaster, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: of each kind (FM chip, MIDI port) the first that finds its hardware is kept, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer. @@ -227,3 +235,9 @@ A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, writ .h2 BASIC src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends. + +For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile: a voice number, or 0 with SoundError$); SoundFolder$ (AUDIO_SOUNDS_DIR); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs) and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing. + +The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped) and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*) and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*). + +Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference). diff --git a/src/libs/kpunch/audio/audio.h b/src/libs/kpunch/audio/audio.h index abf1f324..271ba63c 100644 --- a/src/libs/kpunch/audio/audio.h +++ b/src/libs/kpunch/audio/audio.h @@ -69,6 +69,7 @@ #define AUDIO_LIB_H #include "dvxApp.h" +#include "dvxPrefs.h" #include "dvxTypes.h" #include @@ -484,19 +485,108 @@ void audioDeskReload(void); // (at most max). int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max); +// The most drivers audioDriverNames is asked for by DVX's own callers +#define AUDIO_DRIVERS_MAX 16 + +// ============================================================ +// Settings (DVX.INI, the Control Panel's Sounds page) +// ============================================================ +// +// What the Sounds page and the BASIC functions read and write; prefs is +// a copy of DVX.INI the caller loads and saves. The device reads the +// same keys when it starts (driver, volume, midi: audioDrv.h). + +// [audio] midi: where MIDI music plays, in dropdown order (label for +// people, iniName stored; value unused). Default AUDIO_MIDI_AUTO. +#define AUDIO_MIDI_CHOICE_COUNT 4 +extern const PrefsChoiceT audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT]; + +// The sound file of an event in [sounds] ("" for none or an event out of +// range), and setting it ("" or NULL: none). +const char *audioDeskFile(PrefsHandleT *prefs, DvxSoundE event); +void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path); + +// A volume in percent (clamped to 0..100) as 0..AUDIO_VOLUME_MAX. +int32_t audioPercentToVolume(int32_t percent); + +// [audio] driver (AUDIO_DRIVER_AUTO when missing), midi (the index in +// audioMidiChoices; auto when missing or unknown) and volume (percent, +// 0..100, 100 when missing). +const char *audioSettingDriver(PrefsHandleT *prefs); +int32_t audioSettingMidi(PrefsHandleT *prefs); +int32_t audioSettingVolume(PrefsHandleT *prefs); + +// Store [audio] driver, midi (an audioMidiChoices index; out of range: +// auto) and volume (percent, clamped). +void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t volume); + // ============================================================ // BASIC (src/include/basic/sound.bas) // ============================================================ -// Each BASIC program gets its own context on first use. Volumes are in -// percent (0..100); a voice number is 0 on failure (SoundError$ says why). +// Each BASIC program gets its own context on first use. Volumes, pans +// and pitches are in percent (volume 0..100, pan -100..100, pitch +// 25..400); a voice, sound or stream number is 0 on failure (SoundError$ +// says why). Sounds and streams are numbered per program and freed with +// its context. SoundCreate and SoundStreamWrite take BINARY STRING PCM +// (bytes and byte count). The Midi... functions are declared in +// music.bas. +int32_t FmAlloc(void); +void FmFree(int32_t voice); +const char *FmInfo(const char *field); +int32_t FmRawAcquire(void); +void FmRawRelease(void); +int32_t FmRawWrite(int32_t reg, int32_t value); +int32_t FmWrite(int32_t voice, int32_t reg, int32_t value); +int32_t MidiAcquire(void); +const char *MidiPortInfo(const char *field); +int32_t MidiPut(int32_t byteValue); +void MidiRelease(void); int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume); int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume); int32_t PlayWave(const char *fileName); int32_t PlayWaveLoop(const char *fileName); +int32_t SoundCaps(const char *field); +int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels); +const char *SoundDriverAuto(void); +int32_t SoundDriverCount(void); +const char *SoundDriverName(int32_t index); const char *SoundError(void); +int32_t SoundEventCount(void); +const char *SoundEventFile(int32_t prefs, int32_t eventIdx); +const char *SoundEventKey(int32_t eventIdx); +const char *SoundEventLabel(int32_t eventIdx); +void SoundEventsReload(void); +const char *SoundFolder(void); +void SoundFree(int32_t sound); +int32_t SoundGetVolume(void); const char *SoundInfo(void); +int32_t SoundLength(int32_t sound); +int32_t SoundLoad(const char *fileName); +int32_t SoundMasterVolume(void); +int32_t SoundMidiCount(void); +const char *SoundMidiLabel(int32_t index); +int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt); +void SoundPlayEvent(int32_t eventIdx, int32_t wait); int32_t SoundPlaying(int32_t voice); +void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName); +void SoundSetFrequency(int32_t voice, int32_t frequency); +void SoundSetMasterVolume(int32_t percent); +void SoundSetPan(int32_t voice, int32_t pan); +void SoundSetPitch(int32_t voice, int32_t percent); +const char *SoundSettingDriver(int32_t prefs); +int32_t SoundSettingMidi(int32_t prefs); +void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t volume); +int32_t SoundSettingVolume(int32_t prefs); +void SoundSetVolume(int32_t voice, int32_t volume); +void SoundStreamClose(int32_t stream); +int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs); +int32_t SoundStreamQueued(int32_t stream); +int32_t SoundStreamSpace(int32_t stream); +int32_t SoundStreamVoice(int32_t stream); +int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len); +int32_t SoundTestFile(const char *fileName); void SoundVolume(int32_t volume); void StopAllSounds(void); void StopWave(int32_t voice); diff --git a/src/libs/kpunch/audio/audioBas.c b/src/libs/kpunch/audio/audioBas.c index 2a8a3c48..946ab9b2 100644 --- a/src/libs/kpunch/audio/audioBas.c +++ b/src/libs/kpunch/audio/audioBas.c @@ -22,52 +22,239 @@ // audioBas.c -- DVX BASIC entry points of the sound library // -// Called through DECLARE LIBRARY (src/include/basic/sound.bas), so the -// signatures use only BASIC-marshalable types. Each program plays -// through its own context (audioBasicContext), which the library closes -// when the program ends. Volumes are percent; a voice number is 0 when -// the sound could not be started, and SoundError$ then says why. +// Called through DECLARE LIBRARY (src/include/basic/sound.bas and the +// MIDI port part of music.bas), so the signatures use only +// BASIC-marshalable types. Each program plays through its own context +// (audioBasicContext), which the library closes when the program ends. +// Volumes, pans and pitches are percent; a voice number is 0 when the +// sound could not be started, and SoundError$ then says why. +// +// Loaded sounds (SoundLoad, SoundCreate) and streams (SoundStreamOpen) +// are numbered 1..BAS_SLOTS per kind (0 = failed); a number only works +// for the program that got it, and the numbers of a context are +// forgotten when it closes (audioBasForget, called by audioClose), which +// also frees what they named. PCM data comes and goes as BINARY STRING +// (pointer and byte count), in the sound's or stream's own format. #include "audioInt.h" #include +#include #define BAS_TRUE (-1) #define BAS_FALSE 0 #define PERCENT_MAX 100 #define BAS_INFO_MAX (AUDIO_DESC_MAX + 48) +#define BAS_SLOTS 64 +#define MS_PER_SEC 1000 +#define BITS_PER_BYTE 8 +#define BYTE_MAX_VALUE 0xFF -static char sInfo[BAS_INFO_MAX]; -static int32_t sLastError = AUDIO_OK; +// Percent ranges of the BASIC pan and pitch (sound.bas SOUND_PAN_* and +// SOUND_PITCH_*), from the library's own units. +#define BAS_PAN_MIN (-PERCENT_MAX) +#define BAS_PAN_MAX PERCENT_MAX +#define BAS_PITCH_MIN (AUDIO_PITCH_MIN * PERCENT_MAX / AUDIO_PITCH_NORMAL) +#define BAS_PITCH_MAX (AUDIO_PITCH_MAX * PERCENT_MAX / AUDIO_PITCH_NORMAL) + +// SoundCaps fields +typedef enum { + CapDacE, + CapSynthE, + CapToneE, + CapVoicesE, + CapRunningE, + CapRateE, + CapBitsE, + CapChannelsE, + CapCountE +} SoundCapE; + +// FmInfo$ fields +typedef enum { + FmDescriptionE, + FmVoicesE, + FmFreeE, + FmStereoE, + FmOwnedE, + FmFieldCountE +} FmFieldE; + +// MidiPortInfo$ fields +typedef enum { + MidiDescriptionE, + MidiPresentE, + MidiOwnedE, + MidiFieldCountE +} MidiFieldE; + +// A numbered sound or stream of a BASIC program. +typedef struct { + void *obj; // AudioSampleT / AudioStreamT; NULL: free + const AudioCtxT *ctx; // the program's context + int32_t appId; // the program + uint32_t frameBytes; // streams: bytes per frame of its format +} BasSlotT; + +static const char *const sCapNames[CapCountE] = { "dac", "synth", "tone", "voices", "running", "rate", "bits", "channels" }; +static const char *const sFmNames[FmFieldCountE] = { "description", "voices", "free", "stereo", "owned" }; +static const char *const sMidiNames[MidiFieldCountE] = { "description", "present", "owned" }; + +static char sInfo[BAS_INFO_MAX]; +static int32_t sLastError = AUDIO_OK; +static char sDrivers[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX]; +static int32_t sDriverCount = -1; // -1: not read yet +static BasSlotT sSamples[BAS_SLOTS]; +static BasSlotT sStreams[BAS_SLOTS]; // ============================================================ // Prototypes // ============================================================ -static int32_t basPercent(int32_t percent); -static int32_t basResult(int32_t rc); -static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseconds, int32_t volume); -static int32_t basWave(const char *fileName, bool loop); -int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume); -int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume); -int32_t PlayWave(const char *fileName); -int32_t PlayWaveLoop(const char *fileName); -const char *SoundError(void); -const char *SoundInfo(void); -int32_t SoundPlaying(int32_t voice); -void SoundVolume(int32_t volume); -void StopAllSounds(void); -void StopWave(int32_t voice); +void audioBasForget(const AudioCtxT *ctx); +static int32_t basAppId(void); +static int32_t basField(const char *field, const char *const *names, int32_t count); +static int32_t basNumber(BasSlotT *slots, void *obj, const AudioCtxT *ctx, uint32_t frameBytes); +static int32_t basOk(int32_t rc); +static int32_t basPercent(int32_t volume); +static int32_t basResult(int32_t rc); +static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit); +static BasSlotT *basSlot(BasSlotT *slots, int32_t handle); +static const char *basText(int32_t value); +static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseconds, int32_t volume); +static int32_t basTruth(bool value); +static void basVoiceResult(int32_t rc); +static int32_t basWave(const char *fileName, bool loop); +int32_t FmAlloc(void); +void FmFree(int32_t voice); +const char *FmInfo(const char *field); +int32_t FmRawAcquire(void); +void FmRawRelease(void); +int32_t FmRawWrite(int32_t reg, int32_t value); +int32_t FmWrite(int32_t voice, int32_t reg, int32_t value); +int32_t MidiAcquire(void); +const char *MidiPortInfo(const char *field); +int32_t MidiPut(int32_t byteValue); +void MidiRelease(void); +int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume); +int32_t PlayWave(const char *fileName); +int32_t PlayWaveLoop(const char *fileName); +int32_t SoundCaps(const char *field); +int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels); +const char *SoundDriverAuto(void); +int32_t SoundDriverCount(void); +const char *SoundDriverName(int32_t index); +const char *SoundError(void); +int32_t SoundEventCount(void); +const char *SoundEventFile(int32_t prefs, int32_t eventIdx); +const char *SoundEventKey(int32_t eventIdx); +const char *SoundEventLabel(int32_t eventIdx); +void SoundEventsReload(void); +const char *SoundFolder(void); +void SoundFree(int32_t sound); +int32_t SoundGetVolume(void); +const char *SoundInfo(void); +int32_t SoundLength(int32_t sound); +int32_t SoundLoad(const char *fileName); +int32_t SoundMasterVolume(void); +int32_t SoundMidiCount(void); +const char *SoundMidiLabel(int32_t index); +int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt); +void SoundPlayEvent(int32_t eventIdx, int32_t wait); +int32_t SoundPlaying(int32_t voice); +void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName); +void SoundSetFrequency(int32_t voice, int32_t frequency); +void SoundSetMasterVolume(int32_t percent); +void SoundSetPan(int32_t voice, int32_t pan); +void SoundSetPitch(int32_t voice, int32_t percent); +const char *SoundSettingDriver(int32_t prefs); +int32_t SoundSettingMidi(int32_t prefs); +void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t volume); +int32_t SoundSettingVolume(int32_t prefs); +void SoundSetVolume(int32_t voice, int32_t volume); +void SoundStreamClose(int32_t stream); +int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs); +int32_t SoundStreamQueued(int32_t stream); +int32_t SoundStreamSpace(int32_t stream); +int32_t SoundStreamVoice(int32_t stream); +int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len); +int32_t SoundTestFile(const char *fileName); +void SoundVolume(int32_t volume); +void StopAllSounds(void); +void StopWave(int32_t voice); -static int32_t basPercent(int32_t percent) { - if (percent < 0) { - percent = 0; - } else if (percent > PERCENT_MAX) { - percent = PERCENT_MAX; +// ctx is closing (it frees its samples and streams itself): forget their +// numbers. +void audioBasForget(const AudioCtxT *ctx) { + for (int32_t i = 0; i < BAS_SLOTS; i++) { + if (sSamples[i].ctx == ctx) { + sSamples[i].obj = NULL; + sSamples[i].ctx = NULL; + } + + if (sStreams[i].ctx == ctx) { + sStreams[i].obj = NULL; + sStreams[i].ctx = NULL; + } + } +} + + +// The program running now (0 without a shell: host tests). +static int32_t basAppId(void) { + return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; +} + + +// The index of field in names (any case), -1 when it is none of them. +static int32_t basField(const char *field, const char *const *names, int32_t count) { + for (int32_t i = 0; i < count; i++) { + if (field && strcasecmp(field, names[i]) == 0) { + return i; + } } - return percent * AUDIO_VOLUME_MAX / PERCENT_MAX; + return -1; +} + + +// A number for a new sound or stream, or 0 (it is freed again, the error +// kept) when every number is taken. +static int32_t basNumber(BasSlotT *slots, void *obj, const AudioCtxT *ctx, uint32_t frameBytes) { + for (int32_t i = 0; i < BAS_SLOTS; i++) { + if (!slots[i].obj) { + slots[i].obj = obj; + slots[i].ctx = ctx; + slots[i].appId = basAppId(); + slots[i].frameBytes = frameBytes; + sLastError = AUDIO_OK; + return i + 1; + } + } + + if (slots == sStreams) { + audioStreamClose((AudioStreamT *)obj); + } else { + audioSampleFree((AudioSampleT *)obj); + } + + return basResult(AUDIO_ERR_NO_MEMORY); +} + + +// True on AUDIO_OK, else False with the error kept for SoundError$. +static int32_t basOk(int32_t rc) { + sLastError = rc < 0 ? rc : AUDIO_OK; + return basTruth(rc >= 0); +} + + +// A volume (0..AUDIO_VOLUME_MAX) in percent, rounded. +static int32_t basPercent(int32_t volume) { + return (volume * PERCENT_MAX + AUDIO_VOLUME_MAX / 2) / AUDIO_VOLUME_MAX; } @@ -78,6 +265,34 @@ static int32_t basResult(int32_t rc) { } +// percent clamped to lo..hi, in the library's units (unit per +// PERCENT_MAX). +static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit) { + int32_t p = percent < lo ? lo : (percent > hi ? hi : percent); + + return p * unit / PERCENT_MAX; +} + + +// The calling program's sound or stream with this number, else NULL (and +// the error is noted). +static BasSlotT *basSlot(BasSlotT *slots, int32_t handle) { + if (handle < 1 || handle > BAS_SLOTS || !slots[handle - 1].obj || slots[handle - 1].appId != basAppId()) { + sLastError = AUDIO_ERR_ARG; + return NULL; + } + + return &slots[handle - 1]; +} + + +// A number or a truth value as text, in the buffer the next call reuses. +static const char *basText(int32_t value) { + snprintf(sInfo, sizeof(sInfo), "%ld", (long)value); + return sInfo; +} + + static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseconds, int32_t volume) { AudioCtxT *ctx = audioBasicContext(); @@ -89,7 +304,19 @@ static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseconds, return basResult(AUDIO_ERR_ARG); } - return basResult(audioTone(ctx, wave, (uint32_t)frequency, basPercent(volume), (uint32_t)milliseconds)); + return basResult(audioTone(ctx, wave, (uint32_t)frequency, audioPercentToVolume(volume), (uint32_t)milliseconds)); +} + + +static int32_t basTruth(bool value) { + return value ? BAS_TRUE : BAS_FALSE; +} + + +// Keeps the result of a change to a voice, a synthesizer or a port for +// SoundError$. +static void basVoiceResult(int32_t rc) { + sLastError = rc < 0 ? rc : AUDIO_OK; } @@ -104,6 +331,158 @@ static int32_t basWave(const char *fileName, bool loop) { } +// A shared two-operator FM voice (FmFree gives it back), or 0. +int32_t FmAlloc(void) { + AudioCtxT *ctx = audioBasicContext(); + + return basResult(ctx ? audioFmAlloc(ctx) : AUDIO_ERR_NO_DEVICE); +} + + +void FmFree(int32_t voice) { + AudioCtxT *ctx = audioBasicContext(); + + basVoiceResult(ctx ? audioFmFree(ctx, voice) : AUDIO_ERR_NO_DEVICE); +} + + +// The FM chip: "description" (text), "voices", "free" (counts), "stereo", +// "owned" (True when one program has the whole chip); "" for another +// field. +const char *FmInfo(const char *field) { + AudioFmInfoT info; + + audioBasicContext(); + audioFmGetInfo(&info); + + switch ((FmFieldE)basField(field, sFmNames, FmFieldCountE)) { + case FmDescriptionE: + snprintf(sInfo, sizeof(sInfo), "%s", info.description); + return sInfo; + + case FmVoicesE: + return basText((int32_t)info.voices); + + case FmFreeE: + return basText((int32_t)info.freeVoices); + + case FmStereoE: + return basText(basTruth(info.stereo)); + + case FmOwnedE: + return basText(basTruth(info.rawOwned)); + + default: + return ""; + } +} + + +// The whole FM chip for this program (True), until FmRawRelease. +int32_t FmRawAcquire(void) { + AudioCtxT *ctx = audioBasicContext(); + + return basOk(ctx ? audioFmRawAcquire(ctx) : AUDIO_ERR_NO_DEVICE); +} + + +void FmRawRelease(void) { + AudioCtxT *ctx = audioBasicContext(); + + basVoiceResult(ctx ? audioFmRawRelease(ctx) : AUDIO_ERR_NO_DEVICE); +} + + +// Any chip register (bank * FM_BANK_REGS + index) while the program has +// the whole chip. +int32_t FmRawWrite(int32_t reg, int32_t value) { + AudioCtxT *ctx = audioBasicContext(); + + if (!ctx) { + return basOk(AUDIO_ERR_NO_DEVICE); + } + + if (reg < 0 || value < 0 || value > BYTE_MAX_VALUE) { + return basOk(AUDIO_ERR_ARG); + } + + return basOk(audioFmRawWrite(ctx, (uint32_t)reg, (uint8_t)value)); +} + + +// A register of the voice's own channel (FM_* in sound.bas). +int32_t FmWrite(int32_t voice, int32_t reg, int32_t value) { + AudioCtxT *ctx = audioBasicContext(); + + if (!ctx) { + return basOk(AUDIO_ERR_NO_DEVICE); + } + + if (reg < 0 || reg > BYTE_MAX_VALUE || value < 0 || value > BYTE_MAX_VALUE) { + return basOk(AUDIO_ERR_ARG); + } + + return basOk(audioFmWrite(ctx, voice, (uint8_t)reg, (uint8_t)value)); +} + + +// The MIDI port for this program (True), until MidiRelease. +int32_t MidiAcquire(void) { + AudioCtxT *ctx = audioBasicContext(); + + return basOk(ctx ? audioMidiAcquire(ctx) : AUDIO_ERR_NO_DEVICE); +} + + +// The MIDI port: "description" (text, "" when there is none), "present", +// "owned" (True / False as text); "" for another field. +const char *MidiPortInfo(const char *field) { + AudioMidiInfoT info; + + audioBasicContext(); + audioMidiGetInfo(&info); + + switch ((MidiFieldE)basField(field, sMidiNames, MidiFieldCountE)) { + case MidiDescriptionE: + snprintf(sInfo, sizeof(sInfo), "%s", info.description); + return sInfo; + + case MidiPresentE: + return basText(basTruth(info.present)); + + case MidiOwnedE: + return basText(basTruth(info.owned)); + + default: + return ""; + } +} + + +// One raw byte to the MIDI port; False when it was not taken (busy: send +// it again). +int32_t MidiPut(int32_t byteValue) { + AudioCtxT *ctx = audioBasicContext(); + + if (!ctx) { + return basOk(AUDIO_ERR_NO_DEVICE); + } + + if (byteValue < 0 || byteValue > BYTE_MAX_VALUE) { + return basOk(AUDIO_ERR_ARG); + } + + return basOk(audioMidiPut(ctx, (uint8_t)byteValue)); +} + + +void MidiRelease(void) { + AudioCtxT *ctx = audioBasicContext(); + + basVoiceResult(ctx ? audioMidiRelease(ctx) : AUDIO_ERR_NO_DEVICE); +} + + int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume) { return basTone(AUDIO_WAVE_NOISE, frequency, milliseconds, volume); } @@ -114,6 +493,11 @@ int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume) { } +int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume) { + return basTone(AUDIO_WAVE_TRIANGLE, frequency, milliseconds, volume); +} + + int32_t PlayWave(const char *fileName) { return basWave(fileName, false); } @@ -124,11 +508,165 @@ int32_t PlayWaveLoop(const char *fileName) { } +// What the sound device can do (opens the program's context, so the +// device is started): "dac", "synth", "tone", "running" True / False; +// "voices", "rate", "bits", "channels" numbers; 0 for another field. +int32_t SoundCaps(const char *field) { + AudioInfoT info; + + audioBasicContext(); + audioGetInfo(&info); + + switch ((SoundCapE)basField(field, sCapNames, CapCountE)) { + case CapDacE: + return basTruth(info.hasDac); + + case CapSynthE: + return basTruth(info.hasSynth); + + case CapToneE: + return basTruth(info.hasTone); + + case CapVoicesE: + return (int32_t)info.voiceLimit; + + case CapRunningE: + return basTruth(info.running); + + case CapRateE: + return (int32_t)info.rate; + + case CapBitsE: + return (int32_t)info.bits; + + case CapChannelsE: + return (int32_t)info.channels; + + default: + return 0; + } +} + + +// A sound made from raw PCM bytes (8-bit unsigned or 16-bit signed +// little-endian, stereo interleaved left, right); a trailing partial +// frame is ignored. +int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels) { + AudioCtxT *ctx = audioBasicContext(); + AudioSampleT *sample = NULL; + AudioFormatT fmt; + + if (!ctx) { + return basResult(AUDIO_ERR_NO_DEVICE); + } + + if (rate <= 0 || bits <= 0 || channels <= 0 || len < 0) { + return basResult(AUDIO_ERR_ARG); + } + + fmt.rate = (uint32_t)rate; + fmt.bits = (uint32_t)bits; + fmt.channels = (uint32_t)channels; + + uint32_t frameBytes = fmt.bits / BITS_PER_BYTE * fmt.channels; + int32_t rc = audioSampleCreate(ctx, pcm, frameBytes ? (uint32_t)len / frameBytes : 0, &fmt, &sample); + + if (rc < 0) { + return basResult(rc); + } + + return basNumber(sSamples, sample, ctx, frameBytes); +} + + +// The driver name DVX.INI uses for "find the best one". +const char *SoundDriverAuto(void) { + return AUDIO_DRIVER_AUTO; +} + + +// Reads the installed drivers' names again (for SoundDriverName$). +int32_t SoundDriverCount(void) { + sDriverCount = audioDriverNames(sDrivers, AUDIO_DRIVERS_MAX); + return sDriverCount; +} + + +// Name index (0-based) of the list SoundDriverCount read ("" out of +// range; read now when it never was). +const char *SoundDriverName(int32_t index) { + if (sDriverCount < 0) { + SoundDriverCount(); + } + + return (index >= 0 && index < sDriverCount) ? sDrivers[index] : ""; +} + + const char *SoundError(void) { return audioErrorText(sLastError); } +int32_t SoundEventCount(void) { + return SoundCountE; +} + + +// The file DVX.INI (a prefs.bas handle) names for the event ("" none). +const char *SoundEventFile(int32_t prefs, int32_t eventIdx) { + return audioDeskFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx); +} + + +// The event's [sounds] key ("" out of range). +const char *SoundEventKey(int32_t eventIdx) { + const char *key = audioDeskKey((DvxSoundE)eventIdx); + + return key ? key : ""; +} + + +// The event's name for people ("" out of range). +const char *SoundEventLabel(int32_t eventIdx) { + const char *label = audioDeskLabel((DvxSoundE)eventIdx); + + return label ? label : ""; +} + + +// DVX.INI [sounds] was saved: the desktop plays the new choices. +void SoundEventsReload(void) { + audioDeskReload(); +} + + +// The folder of DVX's own sound files (from the DVX folder). +const char *SoundFolder(void) { + return AUDIO_SOUNDS_DIR; +} + + +// Stops the voices playing it. +void SoundFree(int32_t sound) { + BasSlotT *s = basSlot(sSamples, sound); + + if (s) { + audioSampleFree((AudioSampleT *)s->obj); + s->obj = NULL; + s->ctx = NULL; + } +} + + +// The volume of everything this program plays, percent. +int32_t SoundGetVolume(void) { + AudioCtxT *ctx = audioBasicContext(); + + return ctx ? basPercent(audioGetVolume(ctx)) : 0; +} + + // Opens the program's context, so the device is started and described. const char *SoundInfo(void) { AudioInfoT info; @@ -146,6 +684,81 @@ const char *SoundInfo(void) { } +// The sound's length in milliseconds (0 for no such sound). +int32_t SoundLength(int32_t sound) { + BasSlotT *s = basSlot(sSamples, sound); + uint32_t frames = 0; + AudioFormatT fmt; + + if (!s) { + return 0; + } + + audioSampleInfo((const AudioSampleT *)s->obj, &fmt, &frames); + return (int32_t)((uint64_t)frames * MS_PER_SEC / fmt.rate); +} + + +// Load a WAV or VOC file once, to play it many times (SoundPlay). +int32_t SoundLoad(const char *fileName) { + AudioCtxT *ctx = audioBasicContext(); + AudioSampleT *sample = NULL; + + if (!ctx) { + return basResult(AUDIO_ERR_NO_DEVICE); + } + + int32_t rc = audioSampleLoad(ctx, fileName, &sample); + + if (rc < 0) { + return basResult(rc); + } + + return basNumber(sSamples, sample, ctx, 0); +} + + +// The master volume of all sound, percent. +int32_t SoundMasterVolume(void) { + return basPercent(audioGetMasterVolume()); +} + + +int32_t SoundMidiCount(void) { + return AUDIO_MIDI_CHOICE_COUNT; +} + + +// Where MIDI music can play, for people ("" out of range). +const char *SoundMidiLabel(int32_t index) { + return (index >= 0 && index < AUDIO_MIDI_CHOICE_COUNT) ? audioMidiChoices[index].label : ""; +} + + +// Play a loaded sound on a new voice: volume percent, pan -100 (left) .. +// 100 (right). +int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt) { + BasSlotT *s = basSlot(sSamples, sound); + + if (!s) { + return 0; + } + + return basResult(audioPlay((AudioSampleT *)s->obj, audioPercentToVolume(volume), basScale(pan, BAS_PAN_MIN, BAS_PAN_MAX, AUDIO_PAN_RIGHT), loopIt != BAS_FALSE)); +} + + +// One of DVX's event sounds (DvxSoundE order), as C programs play it; +// with wait, once it has played. +void SoundPlayEvent(int32_t eventIdx, int32_t wait) { + if (wait != BAS_FALSE) { + dvxPlaySoundWait((DvxSoundE)eventIdx); + } else { + dvxPlaySound((DvxSoundE)eventIdx); + } +} + + int32_t SoundPlaying(int32_t voice) { AudioCtxT *ctx = audioBasicContext(); @@ -153,11 +766,169 @@ int32_t SoundPlaying(int32_t voice) { } +void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName) { + audioDeskSetFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx, fileName); +} + + +// A tone's frequency in hertz. +void SoundSetFrequency(int32_t voice, int32_t frequency) { + AudioCtxT *ctx = audioBasicContext(); + + if (!ctx) { + basVoiceResult(AUDIO_ERR_NO_DEVICE); + return; + } + + basVoiceResult(frequency < 0 ? AUDIO_ERR_ARG : audioVoiceSetFrequency(ctx, voice, (uint32_t)frequency)); +} + + +// Every program's sound, at once (DVX.INI keeps its own value). +void SoundSetMasterVolume(int32_t percent) { + audioSetMasterVolume(audioPercentToVolume(percent)); +} + + +// -100 (left) .. 0 (centre) .. 100 (right). +void SoundSetPan(int32_t voice, int32_t pan) { + AudioCtxT *ctx = audioBasicContext(); + + basVoiceResult(ctx ? audioVoiceSetPan(ctx, voice, basScale(pan, BAS_PAN_MIN, BAS_PAN_MAX, AUDIO_PAN_RIGHT)) : AUDIO_ERR_NO_DEVICE); +} + + +// Speed in percent of the recorded one (25 .. 400; 200 an octave higher). +void SoundSetPitch(int32_t voice, int32_t percent) { + AudioCtxT *ctx = audioBasicContext(); + + basVoiceResult(ctx ? audioVoiceSetPitch(ctx, voice, basScale(percent, BAS_PITCH_MIN, BAS_PITCH_MAX, AUDIO_PITCH_NORMAL)) : AUDIO_ERR_NO_DEVICE); +} + + +const char *SoundSettingDriver(int32_t prefs) { + return audioSettingDriver(prefsBasHandle(prefs)); +} + + +int32_t SoundSettingMidi(int32_t prefs) { + return audioSettingMidi(prefsBasHandle(prefs)); +} + + +void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t volume) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + audioSettingsStore(h, driver, midi, volume); + } +} + + +int32_t SoundSettingVolume(int32_t prefs) { + return audioSettingVolume(prefsBasHandle(prefs)); +} + + +// One voice's volume, percent. +void SoundSetVolume(int32_t voice, int32_t volume) { + AudioCtxT *ctx = audioBasicContext(); + + basVoiceResult(ctx ? audioVoiceSetVolume(ctx, voice, audioPercentToVolume(volume)) : AUDIO_ERR_NO_DEVICE); +} + + +void SoundStreamClose(int32_t stream) { + BasSlotT *s = basSlot(sStreams, stream); + + if (s) { + audioStreamClose((AudioStreamT *)s->obj); + s->obj = NULL; + s->ctx = NULL; + } +} + + +// A stream the program writes PCM into (SoundStreamWrite); it plays at +// once on its own voice (SoundStreamVoice). bufferMs 0: the default. +int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs) { + AudioCtxT *ctx = audioBasicContext(); + AudioStreamT *stream = NULL; + AudioFormatT fmt; + + if (!ctx) { + return basResult(AUDIO_ERR_NO_DEVICE); + } + + if (rate <= 0 || bits <= 0 || channels <= 0 || bufferMs < 0) { + return basResult(AUDIO_ERR_ARG); + } + + fmt.rate = (uint32_t)rate; + fmt.bits = (uint32_t)bits; + fmt.channels = (uint32_t)channels; + + int32_t rc = audioStreamOpen(ctx, &fmt, (uint32_t)bufferMs, NULL, NULL, &stream); + + if (rc < 0) { + return basResult(rc); + } + + return basNumber(sStreams, stream, ctx, fmt.bits / BITS_PER_BYTE * fmt.channels); +} + + +// Frames written and not yet played. +int32_t SoundStreamQueued(int32_t stream) { + BasSlotT *s = basSlot(sStreams, stream); + + return s ? (int32_t)audioStreamQueued((const AudioStreamT *)s->obj) : 0; +} + + +// Frames of room left. +int32_t SoundStreamSpace(int32_t stream) { + BasSlotT *s = basSlot(sStreams, stream); + + return s ? (int32_t)audioStreamSpace((const AudioStreamT *)s->obj) : 0; +} + + +// The stream's voice (for SoundSetVolume, SoundSetPan, SoundSetPitch). +int32_t SoundStreamVoice(int32_t stream) { + BasSlotT *s = basSlot(sStreams, stream); + + return s ? audioStreamVoice((const AudioStreamT *)s->obj) : 0; +} + + +// Append whole frames of pcm; returns the bytes taken (the rest did not +// fit: write it again later). +int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len) { + BasSlotT *s = basSlot(sStreams, stream); + + if (!s || len <= 0) { + return 0; + } + + uint32_t frames = audioStreamWrite((AudioStreamT *)s->obj, pcm, (uint32_t)len / s->frameBytes); + + return (int32_t)(frames * s->frameBytes); +} + + +// Play a file the way desktop sounds play: a voice number, or 0 with +// SoundError$ saying why. +int32_t SoundTestFile(const char *fileName) { + return basResult(audioDeskPlayFile(fileName)); +} + + void SoundVolume(int32_t volume) { AudioCtxT *ctx = audioBasicContext(); if (ctx) { - audioSetVolume(ctx, basPercent(volume)); + audioSetVolume(ctx, audioPercentToVolume(volume)); } } diff --git a/src/libs/kpunch/audio/audioDesk.c b/src/libs/kpunch/audio/audioDesk.c index a6ddb2c9..09af896d 100644 --- a/src/libs/kpunch/audio/audioDesk.c +++ b/src/libs/kpunch/audio/audioDesk.c @@ -64,17 +64,24 @@ static bool sNoDevice = false; // no sound device started: not tried aga // Prototypes // ============================================================ +const char *audioDeskFile(PrefsHandleT *prefs, DvxSoundE event); void audioDeskInit(void); const char *audioDeskKey(DvxSoundE event); const char *audioDeskLabel(DvxSoundE event); void audioDeskPlay(DvxSoundE event, bool wait); int32_t audioDeskPlayFile(const char *path); void audioDeskReload(void); +void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path); void audioDeskShutdown(void); static bool eventValid(DvxSoundE event); static void filesLoad(void); +const char *audioDeskFile(PrefsHandleT *prefs, DvxSoundE event) { + return eventValid(event) ? prefsGetString(prefs, AUDIO_SOUNDS_SECTION, sEvents[event].key, "") : ""; +} + + void audioDeskInit(void) { sEnding = false; dvxSetSoundHook(audioDeskPlay); @@ -150,6 +157,13 @@ void audioDeskReload(void) { } +void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path) { + if (eventValid(event)) { + prefsSetString(prefs, AUDIO_SOUNDS_SECTION, sEvents[event].key, path ? path : ""); + } +} + + // The library is closing every context (this one too): forget it and // take the hook back. void audioDeskShutdown(void) { @@ -173,7 +187,7 @@ static void filesLoad(void) { PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH); for (int32_t i = 0; i < SoundCountE; i++) { - snprintf(sFiles[i], sizeof(sFiles[i]), "%s", prefsGetString(prefs, AUDIO_SOUNDS_SECTION, sEvents[i].key, "")); + snprintf(sFiles[i], sizeof(sFiles[i]), "%s", audioDeskFile(prefs, (DvxSoundE)i)); } prefsClose(prefs); diff --git a/src/libs/kpunch/audio/audioInt.h b/src/libs/kpunch/audio/audioInt.h index 7f404dbc..9e5a08be 100644 --- a/src/libs/kpunch/audio/audioInt.h +++ b/src/libs/kpunch/audio/audioInt.h @@ -186,10 +186,12 @@ int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t // The library's end of a program's life, of the shell's idle time and of // its own unloading (audio.c; audioShell.c hooks them up to the shell), -// the calling BASIC program's context (audioBas.c), and making appId the -// current program (returns the one before). +// the calling BASIC program's context, and making appId the current +// program (returns the one before). audioBasForget (audioBas.c) drops the +// BASIC numbers of a closing BASIC context's sounds and streams. void audioAppEnded(int32_t appId); int32_t audioAppEnter(int32_t appId); +void audioBasForget(const AudioCtxT *ctx); AudioCtxT *audioBasicContext(void); void audioIdle(void *unused); void audioShutdown(void); diff --git a/src/libs/kpunch/audio/audioLoad.c b/src/libs/kpunch/audio/audioLoad.c index 9b00ed68..f8f651b9 100644 --- a/src/libs/kpunch/audio/audioLoad.c +++ b/src/libs/kpunch/audio/audioLoad.c @@ -404,6 +404,12 @@ static uint32_t rd32(const uint8_t *p) { static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence) { uint32_t add = src ? bytes / (out->fmt.bits / BITS_PER_BYTE) : silence * out->fmt.channels; + // An empty block adds nothing; before the first sound out->pcm is + // still NULL, and neither NULL + 0 nor memset(NULL, ..) is defined. + if (add == 0) { + return AUDIO_OK; + } + if (out->count + add > out->cap) { uint32_t cap = out->cap ? out->cap : add; diff --git a/src/libs/kpunch/audio/audioSet.c b/src/libs/kpunch/audio/audioSet.c new file mode 100644 index 00000000..7a869d16 --- /dev/null +++ b/src/libs/kpunch/audio/audioSet.c @@ -0,0 +1,87 @@ +// 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. + +// audioSet.c -- the sound settings in DVX.INI (see audio.h, Settings): +// one place for the [audio] keys the Control Panel's Sounds page, the +// BASIC functions and the device itself read and write. + +#include "audioInt.h" + +#define PERCENT_MAX 100 + +const PrefsChoiceT audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] = { + { "Automatic", AUDIO_MIDI_AUTO, 0 }, + { "MIDI port (MPU-401)", AUDIO_MIDI_MPU401, 1 }, + { "FM synthesizer chip", AUDIO_MIDI_FM, 2 }, + { "Built-in synthesizer", AUDIO_MIDI_SOFTWARE, 3 } +}; + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t audioPercentToVolume(int32_t percent); +const char *audioSettingDriver(PrefsHandleT *prefs); +int32_t audioSettingMidi(PrefsHandleT *prefs); +void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t volume); +int32_t audioSettingVolume(PrefsHandleT *prefs); +static int32_t clampPercent(int32_t percent); + + +int32_t audioPercentToVolume(int32_t percent) { + return clampPercent(percent) * AUDIO_VOLUME_MAX / PERCENT_MAX; +} + + +const char *audioSettingDriver(PrefsHandleT *prefs) { + return prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, AUDIO_DRIVER_AUTO); +} + + +int32_t audioSettingMidi(PrefsHandleT *prefs) { + return prefsGetChoice(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, audioMidiChoices, AUDIO_MIDI_CHOICE_COUNT, AUDIO_MIDI_AUTO); +} + + +void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t volume) { + if (midi < 0 || midi >= AUDIO_MIDI_CHOICE_COUNT) { + midi = prefsChoiceIndex(audioMidiChoices, AUDIO_MIDI_CHOICE_COUNT, AUDIO_MIDI_AUTO); + } + + prefsSetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, clampPercent(volume)); + prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, (driver && driver[0]) ? driver : AUDIO_DRIVER_AUTO); + prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, audioMidiChoices[midi].iniName); +} + + +int32_t audioSettingVolume(PrefsHandleT *prefs) { + return clampPercent(prefsGetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, PERCENT_MAX)); +} + + +static int32_t clampPercent(int32_t percent) { + if (percent < 0) { + return 0; + } + + return percent > PERCENT_MAX ? PERCENT_MAX : percent; +} diff --git a/src/libs/kpunch/conn/Makefile b/src/libs/kpunch/conn/Makefile index 29c2cf52..1822bbb9 100644 --- a/src/libs/kpunch/conn/Makefile +++ b/src/libs/kpunch/conn/Makefile @@ -23,18 +23,20 @@ # Connection Library Makefile for DJGPP cross-compilation # # Builds conn.lib -- serial, modem, secure-link and Telnet connections -# behind one interface (see conn.h). It sits on the serial library +# behind one interface (see conn.h) -- and their BASIC face (connBas.c, +# src/include/basic/conn.bas), which runs transfers with the transfer +# library and writes to Terminal controls. It sits on the serial library # (rs232, secLink) and the networking library (BSD sockets: its SDK # headers come first on the include path). include ../../../common.mk -CFLAGS = $(DOS_CFLAGS) -I. -I../dvxnet/include -I../libdvx -I../libdvx/platform +CFLAGS = $(DOS_CFLAGS) -I. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../../../widgets/kpunch OBJDIR = ../../../../obj/conn LIBSDIR = ../../../../bin/libs -SRCS = conn.c connTelnet.c +SRCS = conn.c connBas.c connTelnet.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGETDIR = $(LIBSDIR)/kpunch/conn TARGET = $(TARGETDIR)/conn.lib diff --git a/src/libs/kpunch/conn/conn.dep b/src/libs/kpunch/conn/conn.dep index 1b92dc89..215a451c 100644 --- a/src/libs/kpunch/conn/conn.dep +++ b/src/libs/kpunch/conn/conn.dep @@ -1,4 +1,5 @@ dvxnet serial +xfer libtasks libdvx diff --git a/src/libs/kpunch/conn/conn.dhs b/src/libs/kpunch/conn/conn.dhs index 007ca5b5..5dbe67e1 100644 --- a/src/libs/kpunch/conn/conn.dhs +++ b/src/libs/kpunch/conn/conn.dhs @@ -36,7 +36,7 @@ One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup. -Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial and networking libraries) +Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial, networking and transfer libraries; the transfer library for its BASIC functions) Header: conn/conn.h @@ -112,3 +112,46 @@ n = connRead(c, buf, sizeof(buf)); A Telnet session is the same with cfg.type = ConnTelnetE, cfg.host = "bbs.example.org" and cfg.tcpPort = 23. A file transfer runs over a connection by giving the transfer library connRead, connWrite and connPending as its XferIoT callbacks (ZMODEM over Telnet works: 0xFF bytes are escaped by the Telnet filter). + +.topic lib.conn.basic +.title Connections from BASIC +.toc 1 BASIC Functions +.index conn.bas +.index ConnCreate +.index ConnAttach +.index ConnPoll +.index basFormRtTermBind + +.h1 Connections from BASIC + +Add conn.bas to a BASIC project (and xfer.bas for transfers): connBas.c gives BASIC the connection library by handle, plus what a terminal program needs around it. The Terminal program is written this way. + +.table + Function Description + -------- ----------- + ConnCreate() A closed connection with connConfigDefaults; a handle, or 0. + ConnSetSerial h, com, bps, data, parity$, stop, flow COM port 1-4 and its framing (flow: CONN_FLOW_NONE, _XONXOFF, _RTSCTS). + ConnSetModem h, init$, prefix$, phone$, redials, delay The modem's commands and redialing. + ConnSetHost h, host$, port Telnet; the secure link over the network ("" = over the COM port). + ConnSetPassphrase h, pass$ The secure link's passphrase, forgotten once ConnOpen used it. + ConnSetWindowSize h, cols, rows connSetWindowSize. + ConnSetXferWindow h, bytes XferOptionsT.rxWindow of the transfers started later: 0 = no limit, up to XFER_WINDOW_MAX; -1 (the default) = by the line, see below. + ConnOpen(h, type) connOpen with type and the settings so far. + ConnErrorText$(code) connErrorText of a ConnOpen result. + ConnPoll h One step: connPoll, then the running transfer (cancelled when the line drops), else what arrived to the attached Terminal (see below). Call it from a Timer. + ConnState, ConnStatus$, ConnStatusSerial connState, connStatus, connStatusSerial. + ConnWrite(h, bytes$), ConnRead$(h), ConnPending(h) Bytes out and in without a Terminal: connWrite / connRead with BINARY STRING data, so every byte value passes. + ConnCanBreak(type), ConnSendBreak(h) connCanBreak, connSendBreak. + ConnClose h, ConnFree h Close (a modem hangs up); close and free the handle. + ConnAttach(h, ctrl$), ConnDetach h Show the connection on a Terminal control, and send what is typed there. + ConnSetEcho h, on; ConnSetAnswerback h, text$ Local echo (Enter as CR LF); the reply to ENQ. + ConnCapture(h, path$) Append what arrives to path ("" stops). True while capturing. + ConnZmodemRequest(h) True once after a ZMODEM sender started (xferDetectFeed). + ConnSendFiles(h, proto, files$, escapeCtl, resume) Send files (one path per line). An xfer.bas handle, with XFER_FAILED and the reason when it could not start; 0 when the line is not open or busy. + ConnReceiveFiles(h, proto, target$, exists, escapeCtl) Receive (exists: XFER_EXIST_). +.endtable + +ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow. + +The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended. + diff --git a/src/libs/kpunch/conn/connBas.c b/src/libs/kpunch/conn/connBas.c new file mode 100644 index 00000000..2a99fc1c --- /dev/null +++ b/src/libs/kpunch/conn/connBas.c @@ -0,0 +1,818 @@ +// 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. + +// connBas.c -- DVX BASIC entry points of the connection library +// (src/include/basic/conn.bas) +// +// Called through DECLARE LIBRARY, so the signatures use only BASIC- +// marshalable types: strings and LONG in, LONG or STRING out, True (-1) / +// False (0) for yes / no. A returned string stays valid until the next +// call that returns one (the BASIC runtime copies it at once). +// +// A connection is a handle (1, 2, ...; 0 means none) holding a ConnT and +// the settings ConnOpen uses, which the ConnSet functions fill in. It +// belongs to the program that made it: whatever a program leaves open is +// closed (a modem hangs up) when it ends or is ended. +// +// ConnAttach binds a connection to a Terminal control through the form +// runtime (basFormRtTermBind, looked up at run time like the shell's +// registry): what is typed there is sent (or shown too, with local echo), +// and ConnPoll -- which the program calls often, from a Timer -- moves the +// connection along and puts what arrived on the Terminal, the way the C +// Terminal's heartbeat did: received bytes are appended to a capture +// file, ENQ is answered with the answerback, and a ZMODEM sender starting +// up is noticed (ConnZmodemRequest). A transfer started on the +// connection (ConnSendFiles, ConnReceiveFiles) owns it until it ends: +// ConnPoll runs it instead, nothing reaches the Terminal and typing is +// dropped; a connection that drops cancels it. Transfers are xfer.bas +// handles, so the program follows and answers them with the Xfer +// functions. + +#include "conn.h" +#include "dvxMem.h" +#include "dvxPlat.h" +#include "dvxWgt.h" +#include "ansiTerm/ansiTerm.h" +#include "stb_ds_wrap.h" +#include "../xfer/xfer.h" +#include "../serial/rs232/rs232.h" + +#include +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define BAS_COM_FIRST 1 // BASIC numbers COM ports from 1 +#define READ_MAX 4096 // received bytes moved to the screen per ConnPoll +#define ANSWERBACK_MAX 64 +#define FILES_MAX (XFER_MAX_FILES * XFER_PATH_MAX) +#define FILE_LIST_SEP '\n' +#define FILE_LIST_CR '\r' +#define ENQ 0x05 +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" +#define TERM_BIND_SYM "basFormRtTermBind" +#define TERM_FORGET_SYM "basFormRtTermForget" +#define TERM_UNBIND_SYM "basFormRtTermUnbind" + +// ZMODEM receive window on a serial line without flow control: no more +// than the UART buffer holds, so a slow frame never loses data. +#define RX_WINDOW_UNBUFFERED 2048 + +// ConnSetXferWindow: the window above unless the program set one +#define RX_WINDOW_AUTO (-1) + +typedef struct { + ConnT *conn; + ConnConfigT cfg; // what ConnOpen opens with + int32_t openType; // type and flow control of the last ConnOpen + int32_t openFlow; + int32_t xferWindow; // ZMODEM receive window (RX_WINDOW_AUTO = by the line) + WidgetT *term; // attached Terminal (NULL = none) + FILE *capture; // received bytes are appended here + XferDetectT detect; + int32_t xfer; // xfer.bas handle of the transfer on the line (0 = none) + int32_t appId; // owner: closed when it ends + bool echo; + bool zmodemSeen; + char answerback[ANSWERBACK_MAX]; // reply to ENQ (empty = none) +} BasConnT; + +// The shell's app-cleanup registry and the form runtime's Terminal +// binding (shellApp.h, formrt.h), looked up at run time: the library +// loads before both, and a program without forms has no form runtime. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); +typedef int32_t (*BasTermWriteFnT)(void *ctx, const uint8_t *data, int32_t len); +typedef void (*BasTermUnbindFnT)(void *ctx); +typedef WidgetT *(*BasTermBindFnT)(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind); +typedef void (*BasTermCtxFnT)(void *ctx); + +// ============================================================ +// Module state +// ============================================================ + +static BasConnT **sConns = NULL; // stb_ds; handle = index + 1, NULL = free +static bool sCleanupHooked = false; +static uint8_t sReadBuf[READ_MAX]; // ConnPoll / ConnRead$ (never re-entered) + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +int32_t ConnAttach(int32_t handle, const char *termCtrlName); +int32_t ConnCanBreak(int32_t type); +int32_t ConnCapture(int32_t handle, const char *path); +void ConnClose(int32_t handle); +int32_t ConnCreate(void); +void ConnDetach(int32_t handle); +const char *ConnErrorText(int32_t err); +void ConnFree(int32_t handle); +static BasConnT *connOf(int32_t handle); +int32_t ConnOpen(int32_t handle, int32_t type); +int32_t ConnPending(int32_t handle); +void ConnPoll(int32_t handle); +const uint8_t *ConnRead(int32_t handle, int32_t *outLen); +int32_t ConnReceiveFiles(int32_t handle, int32_t proto, const char *target, int32_t exists, int32_t escapeCtl); +int32_t ConnSendBreak(int32_t handle); +int32_t ConnSendFiles(int32_t handle, int32_t proto, const char *files, int32_t escapeCtl, int32_t resumeFiles); +void ConnSetAnswerback(int32_t handle, const char *text); +void ConnSetEcho(int32_t handle, int32_t on); +void ConnSetHost(int32_t handle, const char *host, int32_t port); +void ConnSetModem(int32_t handle, const char *initString, const char *dialPrefix, const char *phone, int32_t redialCount, int32_t redialDelay); +void ConnSetPassphrase(int32_t handle, const char *passphrase); +void ConnSetSerial(int32_t handle, int32_t com, int32_t bps, int32_t dataBits, const char *parity, int32_t stopBits, int32_t flow); +void ConnSetWindowSize(int32_t handle, int32_t cols, int32_t rows); +void ConnSetXferWindow(int32_t handle, int32_t bytes); +int32_t ConnState(int32_t handle); +const char *ConnStatus(int32_t handle); +int32_t ConnStatusSerial(int32_t handle); +int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len); +int32_t ConnZmodemRequest(int32_t handle); +static void echoLocal(BasConnT *b, const uint8_t *data, int32_t len); +static void freeSlot(int32_t handle, bool termAlive); +static uint32_t nowMs(void); +static XferT *runningTransfer(BasConnT *b); +static void showIncoming(BasConnT *b); +static int32_t startTransfer(int32_t handle, bool sending, int32_t proto, const char *files, const char *target, int32_t exists, int32_t escapeCtl, int32_t resumeFiles); +static void termForget(BasConnT *b); +static void termUnbound(void *ctx); +static int32_t termWrite(void *ctx, const uint8_t *data, int32_t len); +static int32_t xferPendingFn(void *ctx); +static int32_t xferReadFn(void *ctx, uint8_t *buf, int32_t maxLen); +static int32_t xferWriteFn(void *ctx, const uint8_t *data, int32_t len); + + +// Shell app-cleanup hook: close what a program that ended left open. Its +// windows are already gone, so a Terminal binding is only forgotten. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sConns); i++) { + if (sConns[i] && sConns[i]->appId == appId) { + freeSlot(i + 1, false); + } + } +} + + +// Show the connection on the Terminal control named termCtrlName (on any +// of the program's forms) and send what is typed there. True when it is +// attached. +int32_t ConnAttach(int32_t handle, const char *termCtrlName) { + BasConnT *b = connOf(handle); + BasTermBindFnT bind = (BasTermBindFnT)dlsym(NULL, DVX_SYM(TERM_BIND_SYM)); + + if (!b || !bind) { + return BAS_FALSE; + } + + ConnDetach(handle); + b->term = bind(termCtrlName, b, termWrite, termUnbound); + return b->term ? BAS_TRUE : BAS_FALSE; +} + + +// True when ConnSendBreak works for a connection type (serial and modem). +int32_t ConnCanBreak(int32_t type) { + return connCanBreak(type) ? BAS_TRUE : BAS_FALSE; +} + + +// Append what arrives to the file path ("" stops). True while a capture +// file is open. +int32_t ConnCapture(int32_t handle, const char *path) { + BasConnT *b = connOf(handle); + + if (!b) { + return BAS_FALSE; + } + + if (b->capture) { + fclose(b->capture); + b->capture = NULL; + } + + if (path && path[0]) { + b->capture = fopen(path, "ab"); + } + + return b->capture ? BAS_TRUE : BAS_FALSE; +} + + +// Close the connection (a modem hangs up). +void ConnClose(int32_t handle) { + BasConnT *b = connOf(handle); + + if (b) { + connClose(b->conn); + } +} + + +// A closed connection with the defaults (serial COM1, 9600 8N1, no flow +// control, "ATZ", "ATDT", port 23). 0 when out of memory. +int32_t ConnCreate(void) { + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + BasConnT *b = (BasConnT *)calloc(1, sizeof(BasConnT)); + + if (!b) { + return BAS_NO_HANDLE; + } + + b->conn = connCreate(); + + if (!b->conn) { + free(b); + return BAS_NO_HANDLE; + } + + connConfigDefaults(&b->cfg); + b->appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + b->xferWindow = RX_WINDOW_AUTO; + + // Reuse the first free handle, else a new one + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sConns) && sConns[idx]) { + idx++; + } + + if (idx == (int32_t)arrlen(sConns)) { + arrput(sConns, NULL); + } + + sConns[idx] = b; + return idx + 1; +} + + +// Stop showing the connection on its Terminal. +void ConnDetach(int32_t handle) { + BasConnT *b = connOf(handle); + BasTermCtxFnT unbind; + + if (!b || !b->term) { + return; + } + + unbind = (BasTermCtxFnT)dlsym(NULL, DVX_SYM(TERM_UNBIND_SYM)); + + if (unbind) { + unbind(b); + } + + b->term = NULL; +} + + +// Close the connection and free the handle. +// The words for a CONN_ code ConnOpen returned. +const char *ConnErrorText(int32_t err) { + return connErrorText(err); +} + + +void ConnFree(int32_t handle) { + freeSlot(handle, true); +} + + +// The connection of a handle, or NULL. +static BasConnT *connOf(int32_t handle) { + if (handle <= BAS_NO_HANDLE || handle > (int32_t)arrlen(sConns)) { + return NULL; + } + + return sConns[handle - 1]; +} + + +// Start connecting with type (CONN_SERIAL ... CONN_TELNET) and the +// settings given so far. Returns CONN_OK (the connection is then opening +// or open) or an error code; ConnStatus$ says more. The passphrase is +// forgotten once it is used. +int32_t ConnOpen(int32_t handle, int32_t type) { + BasConnT *b = connOf(handle); + + if (!b) { + return CONN_ERR_PARAM; + } + + b->cfg.type = type; + b->openType = type; + b->openFlow = b->cfg.flow; + b->zmodemSeen = false; + xferDetectReset(&b->detect); + + int32_t rc = connOpen(b->conn, &b->cfg, nowMs()); + + memset(b->cfg.passphrase, 0, sizeof(b->cfg.passphrase)); + return rc; +} + + +// Bytes queued to send that have not left yet. +int32_t ConnPending(int32_t handle) { + BasConnT *b = connOf(handle); + + return b ? connPending(b->conn) : 0; +} + + +// Move the connection along: dialing, the key exchange, sending and +// receiving; a transfer on the line runs (and is cancelled when the line +// drops); otherwise what arrived goes to the attached Terminal. Call it +// often (a Timer with a short Interval). +void ConnPoll(int32_t handle) { + BasConnT *b = connOf(handle); + + if (!b) { + return; + } + + uint32_t now = nowMs(); + + connPoll(b->conn, now); + + XferT *x = runningTransfer(b); + + if (x) { + if (xferPoll(x, now) == XferRunningE && connState(b->conn) != ConnOpenE) { + xferCancel(x); + } + + return; + } + + if (b->term) { + showIncoming(b); + } +} + + +// What arrived (up to READ_MAX bytes, a BINARY STRING: every byte value), +// for a connection without a Terminal; "" when nothing has. +const uint8_t *ConnRead(int32_t handle, int32_t *outLen) { + BasConnT *b = connOf(handle); + + *outLen = b ? connRead(b->conn, sReadBuf, READ_MAX) : 0; + return sReadBuf; +} + + +// Receive files with proto: XMODEM into the file target, YMODEM and +// ZMODEM into the folder target. exists is what happens to a file that +// is already there (XFER_EXIST_); escapeCtl asks a ZMODEM sender to +// escape every control character. +int32_t ConnReceiveFiles(int32_t handle, int32_t proto, const char *target, int32_t exists, int32_t escapeCtl) { + return startTransfer(handle, false, proto, NULL, target, exists, escapeCtl, BAS_FALSE); +} + + +// Hold the line in the break state for a moment (serial and modem). +// True when the break started. +int32_t ConnSendBreak(int32_t handle) { + BasConnT *b = connOf(handle); + + return (b && connSendBreak(b->conn, nowMs())) ? BAS_TRUE : BAS_FALSE; +} + + +// Send files (one path per line; XMODEM sends the first) with proto. +// escapeCtl escapes every ZMODEM control character; resumeFiles asks a ZMODEM +// receiver to continue files it has a part of. +int32_t ConnSendFiles(int32_t handle, int32_t proto, const char *files, int32_t escapeCtl, int32_t resumeFiles) { + return startTransfer(handle, true, proto, files, NULL, XferExistAskE, escapeCtl, resumeFiles); +} + + +// The reply to ENQ ("" for none). +void ConnSetAnswerback(int32_t handle, const char *text) { + BasConnT *b = connOf(handle); + + if (b) { + snprintf(b->answerback, sizeof(b->answerback), "%s", text ? text : ""); + } +} + + +// Local echo: show what is typed on the Terminal too. +void ConnSetEcho(int32_t handle, int32_t on) { + BasConnT *b = connOf(handle); + + if (b) { + b->echo = on != BAS_FALSE; + } +} + + +// Telnet's host and port; for the secure link, a host makes it go over +// the network ("" = the COM port). +void ConnSetHost(int32_t handle, const char *host, int32_t port) { + BasConnT *b = connOf(handle); + + if (b) { + snprintf(b->cfg.host, sizeof(b->cfg.host), "%s", host ? host : ""); + b->cfg.tcpPort = port; + } +} + + +// The modem's setup command (sent first), dial command and number ("" = +// no dialing: type AT commands yourself), and how often and after how +// many seconds BUSY or NO ANSWER is dialed again. +void ConnSetModem(int32_t handle, const char *initString, const char *dialPrefix, const char *phone, int32_t redialCount, int32_t redialDelay) { + BasConnT *b = connOf(handle); + + if (!b) { + return; + } + + snprintf(b->cfg.initString, sizeof(b->cfg.initString), "%s", initString ? initString : ""); + snprintf(b->cfg.dialPrefix, sizeof(b->cfg.dialPrefix), "%s", dialPrefix ? dialPrefix : ""); + snprintf(b->cfg.phone, sizeof(b->cfg.phone), "%s", phone ? phone : ""); + b->cfg.redialCount = redialCount; + b->cfg.redialDelay = redialDelay; +} + + +// The secure link's passphrase ("" = none); used by the next ConnOpen. +void ConnSetPassphrase(int32_t handle, const char *passphrase) { + BasConnT *b = connOf(handle); + + if (b) { + snprintf(b->cfg.passphrase, sizeof(b->cfg.passphrase), "%s", passphrase ? passphrase : ""); + } +} + + +// The COM port (1-4) and its framing: speed, data bits, parity ("N", +// "E", "O", "M" or "S"), stop bits and flow control (CONN_FLOW_). +void ConnSetSerial(int32_t handle, int32_t com, int32_t bps, int32_t dataBits, const char *parity, int32_t stopBits, int32_t flow) { + BasConnT *b = connOf(handle); + + if (!b) { + return; + } + + b->cfg.comPort = com - BAS_COM_FIRST + RS232_COM1; + b->cfg.bps = bps; + b->cfg.dataBits = dataBits; + b->cfg.parity = (parity && parity[0]) ? toupper((unsigned char)parity[0]) : 0; + b->cfg.stopBits = stopBits; + b->cfg.flow = flow; +} + + +// The Terminal's size, for a Telnet host that asks (sent again whenever +// it changes). +void ConnSetWindowSize(int32_t handle, int32_t cols, int32_t rows) { + BasConnT *b = connOf(handle); + + if (b) { + connSetWindowSize(b->conn, cols, rows); + } +} + + +// The ZMODEM receive window of later transfers: bytes the sender may send +// before it waits for this side (0 = no limit, up to XFER_WINDOW_MAX); +// -1 (the default) picks by the line: small on a serial line without +// flow control, else no limit. +void ConnSetXferWindow(int32_t handle, int32_t bytes) { + BasConnT *b = connOf(handle); + + if (b && bytes >= RX_WINDOW_AUTO && bytes <= XFER_WINDOW_MAX) { + b->xferWindow = bytes; + } +} + + +// CONN_CLOSED, CONN_OPENING or CONN_OPEN +int32_t ConnState(int32_t handle) { + BasConnT *b = connOf(handle); + + return b ? (int32_t)connState(b->conn) : (int32_t)ConnClosedE; +} + + +// What is happening, or why the connection ended +const char *ConnStatus(int32_t handle) { + BasConnT *b = connOf(handle); + + return b ? connStatus(b->conn) : ""; +} + + +// A number that changes whenever ConnStatus$ does +int32_t ConnStatusSerial(int32_t handle) { + BasConnT *b = connOf(handle); + + return b ? (int32_t)connStatusSerial(b->conn) : 0; +} + + +// Queue bytes (a BINARY STRING) to send. Returns the bytes accepted (0 +// while not open). +int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len) { + BasConnT *b = connOf(handle); + + if (!b || !data) { + return 0; + } + + return connWrite(b->conn, data, len); +} + + +// True once after a ZMODEM sender started on the attached Terminal: the +// program can answer with ConnReceiveFiles(..., XFER_ZMODEM, ...). +int32_t ConnZmodemRequest(int32_t handle) { + BasConnT *b = connOf(handle); + bool seen = b && b->zmodemSeen; + + if (b) { + b->zmodemSeen = false; + } + + return seen ? BAS_TRUE : BAS_FALSE; +} + + +// Local echo: what was typed goes on the screen too (Enter as CR LF). +static void echoLocal(BasConnT *b, const uint8_t *data, int32_t len) { + static const uint8_t crlf[] = { '\r', '\n' }; + + for (int32_t i = 0; i < len; i++) { + if (data[i] == '\r') { + wgtAnsiTermWrite(b->term, crlf, (int32_t)sizeof(crlf)); + } else { + wgtAnsiTermWrite(b->term, &data[i], 1); + } + } +} + + +// Close and free a handle's connection. termAlive: its Terminal still +// exists (the program asked), so the binding is undone on the widget; +// otherwise it is only forgotten. +static void freeSlot(int32_t handle, bool termAlive) { + BasConnT *b = connOf(handle); + XferT *x; + + if (!b) { + return; + } + + if (termAlive) { + ConnDetach(handle); + } else { + termForget(b); + } + + x = runningTransfer(b); + + if (x) { + xferCancel(x); + } + + if (b->capture) { + fclose(b->capture); + } + + connDestroy(b->conn); + memset(b, 0, sizeof(*b)); + free(b); + sConns[handle - 1] = NULL; +} + + +static uint32_t nowMs(void) { + return PLATFORM_TICKS_TO_MS(platformClock()); +} + + +// The transfer that owns the line (running, or waiting for an answer), or +// NULL. A finished or closed one is let go. +static XferT *runningTransfer(BasConnT *b) { + XferT *x = b->xfer ? xferBasTransfer(b->xfer) : NULL; + + if (x) { + XferStatusE st = xferProgress(x)->status; + + if (st == XferRunningE || st == XferAskingE) { + return x; + } + } + + b->xfer = BAS_NO_HANDLE; + return NULL; +} + + +// Received bytes on their way to the Terminal: capture them, answer ENQ +// with the answerback and watch for a ZMODEM sender starting (the rest of +// what was read with its start frame is not shown). +static void showIncoming(BasConnT *b) { + int32_t n = connRead(b->conn, sReadBuf, (int32_t)sizeof(sReadBuf)); + int32_t out = 0; + + if (n <= 0) { + return; + } + + if (b->capture) { + fwrite(sReadBuf, 1, (size_t)n, b->capture); + } + + for (int32_t i = 0; i < n; i++) { + uint8_t ch = sReadBuf[i]; + + if (xferDetectFeed(&b->detect, ch)) { + b->zmodemSeen = true; + break; + } + + if (ch == ENQ) { + connWrite(b->conn, (const uint8_t *)b->answerback, (int32_t)strlen(b->answerback)); + continue; + } + + sReadBuf[out++] = ch; + } + + wgtAnsiTermWrite(b->term, sReadBuf, out); +} + + +// Start a transfer on an open, idle connection. The transfer options +// come from the caller; the receive window suits the line (see +// RX_WINDOW_UNBUFFERED). Returns the xfer.bas handle (its status is +// XFER_FAILED, with the reason, when the transfer could not start), or 0. +static int32_t startTransfer(int32_t handle, bool sending, int32_t proto, const char *files, const char *target, int32_t exists, int32_t escapeCtl, int32_t resumeFiles) { + BasConnT *b = connOf(handle); + XferT *x = NULL; + XferIoT io; + XferOptionsT opt; + bool ok; + + if (!b || connState(b->conn) != ConnOpenE || runningTransfer(b) || proto < XferXmodemE || proto >= XferProtoCountE) { + return BAS_NO_HANDLE; + } + + int32_t xh = xferBasNew(&x); + + if (xh == BAS_NO_HANDLE) { + return BAS_NO_HANDLE; + } + + io.ctx = b->conn; + io.read = xferReadFn; + io.write = xferWriteFn; + io.pending = xferPendingFn; + xferOptionsDefaults(&opt); + opt.exists = (exists >= XferExistAskE && exists < XferExistCountE) ? (XferExistE)exists : XferExistAskE; + opt.escapeCtl = escapeCtl != BAS_FALSE; + opt.resume = resumeFiles != BAS_FALSE; + opt.rxWindow = ((b->openType == ConnSerialE || b->openType == ConnModemE) && b->openFlow == RS232_HANDSHAKE_NONE) ? RX_WINDOW_UNBUFFERED : 0; + + if (b->xferWindow != RX_WINDOW_AUTO) { + opt.rxWindow = b->xferWindow; + } + + if (sending) { + static char list[FILES_MAX]; + const char *paths[XFER_MAX_FILES] = { NULL }; + int32_t count = 0; + + // One path per line (a line feed at the very end starts none) + snprintf(list, sizeof(list), "%s", files ? files : ""); + + for (char *p = list; *p && count < XFER_MAX_FILES; ) { + char *end = strchr(p, FILE_LIST_SEP); + + if (end) { + *end = '\0'; + } + + size_t len = strlen(p); + + if (len > 0 && p[len - 1] == FILE_LIST_CR) { + p[len - 1] = '\0'; + } + + if (*p) { + paths[count++] = p; + } + + if (!end) { + break; + } + + p = end + 1; + } + + ok = xferStartSend(x, (XferProtoE)proto, &io, &opt, paths, count, nowMs()); + } else { + ok = xferStartReceive(x, (XferProtoE)proto, &io, &opt, target ? target : "", nowMs()); + } + + if (ok) { + b->xfer = xh; + xferDetectReset(&b->detect); + } + + return xh; +} + + +// Forget the Terminal binding without touching the widget (it may be +// gone). +static void termForget(BasConnT *b) { + BasTermCtxFnT forget; + + if (!b->term) { + return; + } + + forget = (BasTermCtxFnT)dlsym(NULL, DVX_SYM(TERM_FORGET_SYM)); + + if (forget) { + forget(b); + } + + b->term = NULL; +} + + +// The form runtime undid the binding (the Terminal's form unloaded, or +// another binding took the control). +static void termUnbound(void *ctx) { + ((BasConnT *)ctx)->term = NULL; +} + + +// Keys typed into the Terminal. Nothing is sent while a transfer owns +// the line or the connection is not open. +static int32_t termWrite(void *ctx, const uint8_t *data, int32_t len) { + BasConnT *b = (BasConnT *)ctx; + + if (runningTransfer(b) || connState(b->conn) != ConnOpenE) { + return len; + } + + int32_t n = connWrite(b->conn, data, len); + + if (b->echo) { + echoLocal(b, data, n); + } + + return n; +} + + +static int32_t xferPendingFn(void *ctx) { + return connPending((ConnT *)ctx); +} + + +static int32_t xferReadFn(void *ctx, uint8_t *buf, int32_t maxLen) { + return connRead((ConnT *)ctx, buf, maxLen); +} + + +static int32_t xferWriteFn(void *ctx, const uint8_t *data, int32_t len) { + return connWrite((ConnT *)ctx, data, len); +} diff --git a/src/libs/kpunch/dvxnet/dvxNet.c b/src/libs/kpunch/dvxnet/dvxNet.c index 52e004ed..5c425501 100644 --- a/src/libs/kpunch/dvxnet/dvxNet.c +++ b/src/libs/kpunch/dvxnet/dvxNet.c @@ -110,6 +110,25 @@ void netConfigDefaults(NetConfigT *cfg) { } +bool netConfigEqual(const NetConfigT *a, const NetConfigT *b) { + if (a->dhcp != b->dhcp || a->vector != b->vector) { + return false; + } + + if (strcmp(a->address, b->address) != 0 || strcmp(a->netmask, b->netmask) != 0 || strcmp(a->gateway, b->gateway) != 0 || strcmp(a->hostName, b->hostName) != 0) { + return false; + } + + for (int32_t i = 0; i < NET_DNS_SERVERS; i++) { + if (strcmp(a->dns[i], b->dns[i]) != 0) { + return false; + } + } + + return true; +} + + void netConfigFromPrefs(PrefsHandleT *h, NetConfigT *cfg) { char key[KEY_NAME_MAX]; const char *vec; diff --git a/src/libs/kpunch/dvxnet/dvxNet.h b/src/libs/kpunch/dvxnet/dvxNet.h index 716648fe..558b67a7 100644 --- a/src/libs/kpunch/dvxnet/dvxNet.h +++ b/src/libs/kpunch/dvxnet/dvxNet.h @@ -102,13 +102,19 @@ typedef struct { char message[NET_MESSAGE_MAX]; // plain-words state, e.g. "Waiting for a DHCP server" } NetStatusT; -// Close every socket the app with this id still has open (the shell does -// this when an app ends or is ended; callable by anyone). +// Close every socket the app with this id still has open, and free the +// BASIC network settings handles it left (the shell does this when an +// app ends or is ended; callable by anyone). void netCloseAppSockets(int32_t appId); // Default settings: DHCP, search for the packet driver, host name "DVX". void netConfigDefaults(NetConfigT *cfg); +// True when a and b are the same settings (text compared up to its end, +// so bytes after a string's terminator do not count). The Control Panel +// restarts the network only when its settings changed. +bool netConfigEqual(const NetConfigT *a, const NetConfigT *b); + // Read [network] from an open preferences handle (defaults for anything // missing or not valid). The Control Panel uses this and netConfigToPrefs // on its own DVX.INI handle, so one save writes everything. diff --git a/src/libs/kpunch/dvxnet/dvxnet.dhs b/src/libs/kpunch/dvxnet/dvxnet.dhs index 07ca8af5..3af4dfd0 100644 --- a/src/libs/kpunch/dvxnet/dvxnet.dhs +++ b/src/libs/kpunch/dvxnet/dvxnet.dhs @@ -202,9 +202,10 @@ The settings live in DVX.INI, section [network]: dhcp (true/false), address, net void netConfigLoad(NetConfigT *cfg) Read DVX.INI [network]. void netConfigFromPrefs(h, cfg) Read [network] from an open preferences handle. void netConfigToPrefs(h, cfg) Store cfg as [network] in an open handle (the caller saves). + bool netConfigEqual(a, b) True when two configurations are the same settings (the Control Panel restarts the network only when they differ). const char *netConfigProblem(cfg) NULL when usable, else why not in plain words (bad address or mask, name, interrupt). bool netParseAddress(text, &addr) Strict dotted IPv4 (four decimal parts 0-255) to network byte order. - void netCloseAppSockets(int32_t appId) Close every socket of that app (the shell calls this when an app ends). + void netCloseAppSockets(int32_t appId) Close every socket of that app and free the net.bas settings handles it left (the shell calls this when an app ends). .endtable Name servers set in the settings are used even with DHCP; left empty, the DHCP server's are used. @@ -241,4 +242,34 @@ On the host the same sources are built into the "net" test suite: a fake packet .h1 Networking from BASIC -net.bas declares simple functions over the socket API (netBas.c): HostLookup$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. Strings end at a zero byte, so the functions carry text. See the BASIC language reference, Include Libraries. +net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries. + +.table + BASIC C it calls + ----- ---------- + HostName$(address) getnameinfo: the address itself, as this stack does not look names of addresses up; "" when it is not an address. + TcpShutdown(handle) shutdown(SHUT_WR); True when done. + TcpPeer$(handle) getpeername, as "address:port" ("" when not connected). + TcpLocalPort(handle) getsockname: the local port (-1 for a bad handle). + TcpSetNoDelay(handle, enable) setsockopt(IPPROTO_TCP, TCP_NODELAY); True when done. + TcpSetKeepAlive(handle, enable) setsockopt(SOL_SOCKET, SO_KEEPALIVE); True when done. + NetStop netStop (every program's sockets are closed). +.endtable + +The network settings and status (what the Control Panel's Network page shows) are reachable too, over the functions of dvxNet.h: + +.table + BASIC C it calls + ----- ---------- + NetConfigLoad(prefs) netConfigFromPrefs on a prefs.bas handle (prefsBasHandle; 0 = the defaults) into a new settings handle, which also keeps the settings as read. + NetConfigGet$ / NetConfigSet One field by name: dhcp, vector, address, netmask, gateway, dns1, dns2, hostname (text; yes / no as "-1" / "0"). + NetConfigProblem$(cfg) netConfigProblem ("" when usable). + NetConfigChanged(cfg) netConfigEqual against the settings as read. + NetConfigStore(cfg, prefs) netConfigToPrefs into a prefs.bas handle. + NetConfigFree(cfg) Free the handle. + NetVectorCount / NetVector(i) The packet driver interrupt choices: 0 (find it), then NET_VECTOR_FIRST..NET_VECTOR_LAST. + NetRestart() netRestart; True for NET_OK. + NetStatus$(field) netGetStatus, one field as text: message, up, link, dhcp, address, netmask, gateway, dns1, dns2, hostname, driver, mac, vector, sockets, rxpackets, rxbytes, rxdropped, txpackets, txbytes. +.endtable + +Settings handles (1 to 16, 0 = none) belong to the program that made them; netCloseAppSockets frees what a program left. diff --git a/src/libs/kpunch/dvxnet/netBas.c b/src/libs/kpunch/dvxnet/netBas.c index 94585dc7..7f2c9169 100644 --- a/src/libs/kpunch/dvxnet/netBas.c +++ b/src/libs/kpunch/dvxnet/netBas.c @@ -25,19 +25,33 @@ // // A BASIC handle is the socket's descriptor + 1, so 0 means "failed". // Everything but TcpConnect and TcpSend returns at once, so a program -// polls from a Timer (TcpPending, TcpRecv$, TcpAccept, UdpRecv$). Text -// only: BASIC strings end at a zero byte. Sockets belong to the program -// and are closed for it when it ends. +// polls from a Timer (TcpPending, TcpRecv$, TcpAccept, UdpRecv$). What +// is sent and received is a BINARY STRING (pointer + length both ways, +// claudeDocs/round2/binaryStrings.md), so every byte value passes. +// Sockets belong to the program and are closed for it when it ends. +// +// The network settings (DVX.INI [network]) are edited through handles +// (NetConfigLoad: 1, 2, ...; 0 = none) owned by the program that made +// them and freed for it when it ends (netCloseAppSockets). A handle +// holds the settings as read and the copy being edited, so +// NetConfigChanged can tell whether a restart is needed. Fields are +// named by text, the same names for settings and status where they mean +// the same thing. #include "netInt.h" +#include "dvxTypes.h" #include #include #include #include #include +#include +#include #include +#include #include +#include #include #include #include @@ -48,11 +62,58 @@ #define BAS_BACKLOG 4 #define BAS_ENDPOINT_MAX (INET_ADDRSTRLEN + 8) #define BAS_PORT_MAX 65535 +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_CONFIG_MAX 16 +#define BAS_FIELD_MAX NET_MESSAGE_MAX +#define BAS_DNS_PREFIX "dns" +#define BAS_DNS_FIRST 1 // "dns1" is the first name server +#define DECIMAL_BASE 10 +#define ANY_BASE 0 // strtol: 0x.. hex or decimal -static char sRecv[BAS_RECV_MAX + 1]; -static char sLookup[INET_ADDRSTRLEN]; +// Packet driver interrupts NetVector lists: "find it", then each one +#define BAS_VECTOR_COUNT (NET_VECTOR_LAST - NET_VECTOR_FIRST + 2) + +// One settings handle +typedef struct { + bool used; + int32_t appId; + NetConfigT cfg; // being edited + NetConfigT loaded; // as NetConfigLoad read it +} BasNetConfigT; + +// A text field of NetConfigT / NetStatusT by name +typedef struct { + const char *name; + size_t offset; + size_t size; +} BasTextFieldT; + +static const BasTextFieldT sConfigFields[] = { + { "address", offsetof(NetConfigT, address), NET_ADDR_MAX }, + { "netmask", offsetof(NetConfigT, netmask), NET_ADDR_MAX }, + { "gateway", offsetof(NetConfigT, gateway), NET_ADDR_MAX }, + { "hostname", offsetof(NetConfigT, hostName), NET_HOSTNAME_MAX } +}; + +static const BasTextFieldT sStatusFields[] = { + { "address", offsetof(NetStatusT, address), NET_ADDR_MAX }, + { "netmask", offsetof(NetStatusT, netmask), NET_ADDR_MAX }, + { "gateway", offsetof(NetStatusT, gateway), NET_ADDR_MAX }, + { "hostname", offsetof(NetStatusT, hostName), NET_HOSTNAME_MAX }, + { "driver", offsetof(NetStatusT, driverName), NET_DRIVER_NAME_MAX }, + { "mac", offsetof(NetStatusT, mac), NET_MAC_TEXT_MAX }, + { "message", offsetof(NetStatusT, message), NET_MESSAGE_MAX } +}; + +static BasNetConfigT sConfigs[BAS_CONFIG_MAX]; +static char sField[BAS_FIELD_MAX]; + +static uint8_t sRecv[BAS_RECV_MAX]; +static char sLookup[NI_MAXHOST]; static char sAddress[NET_ADDR_MAX]; static char sFrom[BAS_ENDPOINT_MAX]; +static char sEndpoint[BAS_ENDPOINT_MAX]; static int32_t sLastError = 0; @@ -60,26 +121,98 @@ static int32_t sLastError = 0; // Prototypes // ============================================================ -static int32_t handleFd(int32_t handle); -const char *HostLookup(const char *name); -const char *NetAddress(void); -const char *NetError(void); -static int32_t newSocket(int32_t type); -static int32_t noteError(void); -static bool resolve(const char *host, int32_t port, struct sockaddr_in *out); -int32_t TcpAccept(int32_t handle); -void TcpClose(int32_t handle); -int32_t TcpConnect(const char *host, int32_t port); -int32_t TcpConnected(int32_t handle); -int32_t TcpListen(int32_t port); -int32_t TcpPending(int32_t handle); -const char *TcpRecv(int32_t handle); -int32_t TcpSend(int32_t handle, const char *text); -void UdpClose(int32_t handle); -const char *UdpFrom(void); -int32_t UdpOpen(int32_t port); -const char *UdpRecv(int32_t handle); -int32_t UdpSend(int32_t handle, const char *host, int32_t port, const char *text); +static const char *basBoolText(bool value); +static BasNetConfigT *configOf(int32_t handle); +static int32_t dnsIndex(const char *field); +static const char *endpointText(const struct sockaddr_in *addr); +static int32_t handleFd(int32_t handle); +const char *HostLookup(const char *name); +const char *HostName(const char *address); +const char *NetAddress(void); +void netBasAppEnded(int32_t appId); +int32_t NetConfigChanged(int32_t config); +void NetConfigFree(int32_t config); +const char *NetConfigGet(int32_t config, const char *field); +int32_t NetConfigLoad(int32_t prefs); +const char *NetConfigProblem(int32_t config); +int32_t NetConfigSet(int32_t config, const char *field, const char *value); +int32_t NetConfigStore(int32_t config, int32_t prefs); +const char *NetError(void); +int32_t NetRestart(void); +const char *NetStatus(const char *field); +void NetStop(void); +int32_t NetVector(int32_t index); +int32_t NetVectorCount(void); +static int32_t newSocket(int32_t type); +static int32_t noteError(void); +static bool resolve(const char *host, int32_t port, struct sockaddr_in *out); +static int32_t setFlag(int32_t handle, int level, int name, int32_t on); +int32_t TcpAccept(int32_t handle); +void TcpClose(int32_t handle); +int32_t TcpConnect(const char *host, int32_t port); +int32_t TcpConnected(int32_t handle); +int32_t TcpListen(int32_t port); +int32_t TcpLocalPort(int32_t handle); +const char *TcpPeer(int32_t handle); +int32_t TcpPending(int32_t handle); +const uint8_t *TcpRecv(int32_t handle, int32_t *outLen); +int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len); +int32_t TcpSetKeepAlive(int32_t handle, int32_t on); +int32_t TcpSetNoDelay(int32_t handle, int32_t on); +int32_t TcpShutdown(int32_t handle); +static char *textField(void *base, const BasTextFieldT *fields, int32_t count, size_t dnsOffset, const char *field, size_t *size); +void UdpClose(int32_t handle); +const char *UdpFrom(void); +int32_t UdpOpen(int32_t port); +const uint8_t *UdpRecv(int32_t handle, int32_t *outLen); +int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *data, int32_t len); + + +// A yes / no field as text: "-1" (True) or "0" (False), so VAL gives +// BASIC's True / False. +static const char *basBoolText(bool value) { + return value ? "-1" : "0"; +} + + +// The settings handle's entry, or NULL when it is not open. +static BasNetConfigT *configOf(int32_t handle) { + if (handle < 1 || handle > BAS_CONFIG_MAX || !sConfigs[handle - 1].used) { + return NULL; + } + + return &sConfigs[handle - 1]; +} + + +// "dns1".."dnsN" (any case) as a name server index 0..N-1, else -1. +static int32_t dnsIndex(const char *field) { + size_t prefix = strlen(BAS_DNS_PREFIX); + char *end; + long n; + + if (strncasecmp(field, BAS_DNS_PREFIX, prefix) != 0 || field[prefix] < '0' || field[prefix] > '9') { + return -1; + } + + n = strtol(field + prefix, &end, DECIMAL_BASE); + + if (*end != '\0' || n < BAS_DNS_FIRST || n >= BAS_DNS_FIRST + NET_DNS_SERVERS) { + return -1; + } + + return (int32_t)(n - BAS_DNS_FIRST); +} + + +// "address:port" of addr, in sEndpoint (until the next call). +static const char *endpointText(const struct sockaddr_in *addr) { + char text[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &addr->sin_addr, text, sizeof(text)); + snprintf(sEndpoint, sizeof(sEndpoint), "%s:%u", text, (unsigned)ntohs(addr->sin_port)); + return sEndpoint; +} static int32_t handleFd(int32_t handle) { @@ -100,6 +233,33 @@ const char *HostLookup(const char *name) { } +// The name of an address ("192.0.2.10"), through getnameinfo: the +// address itself when no name is known for it, "" when address is not +// one (NetError$ says why). +const char *HostName(const char *address) { + struct sockaddr_in in; + int rc; + + memset(&in, 0, sizeof(in)); + in.sin_family = AF_INET; + sLookup[0] = '\0'; + + if (!address || inet_pton(AF_INET, address, &in.sin_addr) != 1) { + sLastError = EINVAL; + return sLookup; + } + + rc = getnameinfo((struct sockaddr *)&in, sizeof(in), sLookup, sizeof(sLookup), NULL, 0, 0); + + if (rc != 0) { + sLastError = EHOSTUNREACH; + sLookup[0] = '\0'; + } + + return sLookup; +} + + const char *NetAddress(void) { NetStatusT st; @@ -110,11 +270,231 @@ const char *NetAddress(void) { } +// Free what an app that ended left open (netCloseAppSockets). +void netBasAppEnded(int32_t appId) { + for (int32_t i = 0; i < BAS_CONFIG_MAX; i++) { + if (sConfigs[i].used && sConfigs[i].appId == appId) { + sConfigs[i].used = false; + } + } +} + + +// True when the settings differ from what NetConfigLoad read. +int32_t NetConfigChanged(int32_t config) { + BasNetConfigT *c = configOf(config); + + return (c && !netConfigEqual(&c->cfg, &c->loaded)) ? BAS_TRUE : BAS_FALSE; +} + + +void NetConfigFree(int32_t config) { + BasNetConfigT *c = configOf(config); + + if (c) { + c->used = false; + } +} + + +// A setting as text: "dhcp" ("-1" / "0"), "vector" (decimal, 0 = find +// it), "address", "netmask", "gateway", "dns1", "dns2", "hostname"; +// "" for an unknown field or handle. +const char *NetConfigGet(int32_t config, const char *field) { + BasNetConfigT *c = configOf(config); + char *text; + size_t size; + + if (!c || !field) { + return ""; + } + + if (strcasecmp(field, "dhcp") == 0) { + return basBoolText(c->cfg.dhcp); + } + + if (strcasecmp(field, "vector") == 0) { + snprintf(sField, sizeof(sField), "%ld", (long)c->cfg.vector); + return sField; + } + + text = textField(&c->cfg, sConfigFields, DVX_ARRAY_LEN(sConfigFields), offsetof(NetConfigT, dns), field, &size); + return text ? text : ""; +} + + +// A handle on the [network] settings of a prefs.bas handle (the +// defaults for 0); 0 when every handle is in use. +int32_t NetConfigLoad(int32_t prefs) { + for (int32_t i = 0; i < BAS_CONFIG_MAX; i++) { + if (!sConfigs[i].used) { + netConfigFromPrefs(prefsBasHandle(prefs), &sConfigs[i].cfg); + sConfigs[i].loaded = sConfigs[i].cfg; + sConfigs[i].appId = netSockAppId(); + sConfigs[i].used = true; + return i + 1; + } + } + + return 0; +} + + +// "" when the settings can be used, else why not. +const char *NetConfigProblem(int32_t config) { + BasNetConfigT *c = configOf(config); + const char *problem; + + if (!c) { + return ""; + } + + problem = netConfigProblem(&c->cfg); + return problem ? problem : ""; +} + + +// Change a setting (names as NetConfigGet; "dhcp": 0 = no, any other +// number = yes; "vector": a number, 0 = find it). Text too long is cut +// off. True when the field is known. +int32_t NetConfigSet(int32_t config, const char *field, const char *value) { + BasNetConfigT *c = configOf(config); + char *text; + size_t size; + + if (!c || !field) { + return BAS_FALSE; + } + + if (!value) { + value = ""; + } + + if (strcasecmp(field, "dhcp") == 0) { + c->cfg.dhcp = strtol(value, NULL, ANY_BASE) != 0; + return BAS_TRUE; + } + + if (strcasecmp(field, "vector") == 0) { + c->cfg.vector = (int32_t)strtol(value, NULL, ANY_BASE); + return BAS_TRUE; + } + + text = textField(&c->cfg, sConfigFields, DVX_ARRAY_LEN(sConfigFields), offsetof(NetConfigT, dns), field, &size); + + if (!text) { + return BAS_FALSE; + } + + snprintf(text, size, "%s", value); + return BAS_TRUE; +} + + +// Write the settings into the [network] section of a prefs.bas handle +// (the program saves it). True when done. +int32_t NetConfigStore(int32_t config, int32_t prefs) { + BasNetConfigT *c = configOf(config); + PrefsHandleT *h = prefsBasHandle(prefs); + + if (!c || !h) { + return BAS_FALSE; + } + + netConfigToPrefs(h, &c->cfg); + return BAS_TRUE; +} + + const char *NetError(void) { return netErrorText(sLastError); } +// Stop the network and start it again with the saved settings. True +// when it runs (it may still be waiting for a DHCP server). +int32_t NetRestart(void) { + return netRestart() == NET_OK ? BAS_TRUE : BAS_FALSE; +} + + +// The network's state as text: "message" (in plain words), "up", "link", +// "dhcp" ("-1" / "0"), "address", "netmask", "gateway", "dns1", "dns2", +// "hostname", "driver", "mac", "vector", "sockets", "rxpackets", +// "rxbytes", "rxdropped", "txpackets", "txbytes" (numbers in decimal); +// "" for an unknown field. +const char *NetStatus(const char *field) { + NetStatusT st; + char *text; + size_t size; + + if (!field) { + return ""; + } + + netGetStatus(&st); + + if (strcasecmp(field, "up") == 0) { + return basBoolText(st.state == NetUpE); + } + + if (strcasecmp(field, "link") == 0) { + return basBoolText(st.link); + } + + if (strcasecmp(field, "dhcp") == 0) { + return basBoolText(st.dhcp); + } + + text = textField(&st, sStatusFields, DVX_ARRAY_LEN(sStatusFields), offsetof(NetStatusT, dns), field, &size); + + if (text) { + snprintf(sField, sizeof(sField), "%s", text); + } else if (strcasecmp(field, "vector") == 0) { + snprintf(sField, sizeof(sField), "%ld", (long)st.vector); + } else if (strcasecmp(field, "sockets") == 0) { + snprintf(sField, sizeof(sField), "%ld", (long)st.openSockets); + } else if (strcasecmp(field, "rxpackets") == 0) { + snprintf(sField, sizeof(sField), "%lu", (unsigned long)st.rxPackets); + } else if (strcasecmp(field, "rxbytes") == 0) { + snprintf(sField, sizeof(sField), "%lu", (unsigned long)st.rxBytes); + } else if (strcasecmp(field, "rxdropped") == 0) { + snprintf(sField, sizeof(sField), "%lu", (unsigned long)st.rxDropped); + } else if (strcasecmp(field, "txpackets") == 0) { + snprintf(sField, sizeof(sField), "%lu", (unsigned long)st.txPackets); + } else if (strcasecmp(field, "txbytes") == 0) { + snprintf(sField, sizeof(sField), "%lu", (unsigned long)st.txBytes); + } else { + sField[0] = '\0'; + } + + return sField; +} + + +// Stop the network (every program's connections are closed); NetRestart +// starts it again. +void NetStop(void) { + netStop(); +} + + +// The packet driver interrupt choices: index 0 is 0 (find it), then +// each interrupt the settings accept, in order; -1 out of range. +int32_t NetVector(int32_t index) { + if (index < 0 || index >= BAS_VECTOR_COUNT) { + return -1; + } + + return (index == 0) ? NET_VECTOR_AUTO : NET_VECTOR_FIRST + index - 1; +} + + +int32_t NetVectorCount(void) { + return BAS_VECTOR_COUNT; +} + + // A socket; non-blocking when it is a UDP one. 0 (with the error noted) // on failure, else the BASIC handle. static int32_t newSocket(int32_t type) { @@ -159,6 +539,19 @@ static bool resolve(const char *host, int32_t port, struct sockaddr_in *out) { } +// A yes / no socket option: True when it was set. +static int32_t setFlag(int32_t handle, int level, int name, int32_t on) { + int value = (on != 0); + + if (setsockopt(handleFd(handle), level, name, &value, sizeof(value)) != 0) { + noteError(); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + // A waiting connection on a listening handle, or 0 when none is waiting. int32_t TcpAccept(int32_t handle) { int fd = handleFd(handle); @@ -264,6 +657,36 @@ int32_t TcpListen(int32_t port) { } +// The local port of a connection or listening handle (the one TcpListen(0) +// picked), -1 for a bad handle. +int32_t TcpLocalPort(int32_t handle) { + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + + if (getsockname(handleFd(handle), (struct sockaddr *)&addr, &len) != 0) { + noteError(); + return -1; + } + + return (int32_t)ntohs(addr.sin_port); +} + + +// "address:port" of the other side of a connection, "" when it is not +// connected. +const char *TcpPeer(int32_t handle) { + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + + if (getpeername(handleFd(handle), (struct sockaddr *)&addr, &len) != 0) { + noteError(); + return ""; + } + + return endpointText(&addr); +} + + // Bytes waiting to be read (-1 for a bad handle). int32_t TcpPending(int32_t handle) { int n = 0; @@ -278,7 +701,7 @@ int32_t TcpPending(int32_t handle) { // What has arrived (up to BAS_RECV_MAX bytes), "" when nothing has. -const char *TcpRecv(int32_t handle) { +const uint8_t *TcpRecv(int32_t handle, int32_t *outLen) { ssize_t n = recv(handleFd(handle), sRecv, BAS_RECV_MAX, MSG_DONTWAIT); if (n < 0) { @@ -289,15 +712,15 @@ const char *TcpRecv(int32_t handle) { n = 0; } - sRecv[n] = '\0'; + *outLen = (int32_t)n; return sRecv; } -// Send all of text (waiting while the connection's buffer is full). +// Send all of data (waiting while the connection's buffer is full). // Returns the bytes sent, -1 on failure. -int32_t TcpSend(int32_t handle, const char *text) { - ssize_t n = send(handleFd(handle), text, strlen(text), 0); +int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len) { + ssize_t n = send(handleFd(handle), data, (size_t)len, 0); if (n < 0) { noteError(); @@ -308,6 +731,53 @@ int32_t TcpSend(int32_t handle, const char *text) { } +// Keep-alive probes on an idle connection (SO_KEEPALIVE): True when set. +int32_t TcpSetKeepAlive(int32_t handle, int32_t on) { + return setFlag(handle, SOL_SOCKET, SO_KEEPALIVE, on); +} + + +// Send small pieces at once instead of gathering them (TCP_NODELAY): True +// when set. +int32_t TcpSetNoDelay(int32_t handle, int32_t on) { + return setFlag(handle, IPPROTO_TCP, TCP_NODELAY, on); +} + + +// Tell the other side nothing more will be sent (shutdown SHUT_WR); what +// it sends can still be read. True when done. +int32_t TcpShutdown(int32_t handle) { + if (shutdown(handleFd(handle), SHUT_WR) != 0) { + noteError(); + return BAS_FALSE; + } + + return BAS_TRUE; +} + + +// The text field named field (any case) of base -- one of fields, or a +// name server "dnsN" in the array at dnsOffset -- and its size; NULL +// when there is none. +static char *textField(void *base, const BasTextFieldT *fields, int32_t count, size_t dnsOffset, const char *field, size_t *size) { + int32_t dns = dnsIndex(field); + + if (dns >= 0) { + *size = NET_ADDR_MAX; + return (char *)base + dnsOffset + (size_t)dns * NET_ADDR_MAX; + } + + for (int32_t i = 0; i < count; i++) { + if (strcasecmp(field, fields[i].name) == 0) { + *size = fields[i].size; + return (char *)base + fields[i].offset; + } + } + + return NULL; +} + + void UdpClose(int32_t handle) { close(handleFd(handle)); } @@ -353,10 +823,9 @@ int32_t UdpOpen(int32_t port) { // The next datagram, "" when none is waiting. -const char *UdpRecv(int32_t handle) { +const uint8_t *UdpRecv(int32_t handle, int32_t *outLen) { struct sockaddr_in from; socklen_t len = sizeof(from); - char addr[INET_ADDRSTRLEN]; ssize_t n = recvfrom(handleFd(handle), sRecv, BAS_RECV_MAX, MSG_DONTWAIT, (struct sockaddr *)&from, &len); if (n < 0) { @@ -364,19 +833,18 @@ const char *UdpRecv(int32_t handle) { noteError(); } - sRecv[0] = '\0'; + *outLen = 0; return sRecv; } - sRecv[n] = '\0'; - inet_ntop(AF_INET, &from.sin_addr, addr, sizeof(addr)); - snprintf(sFrom, sizeof(sFrom), "%s:%u", addr, (unsigned)ntohs(from.sin_port)); + *outLen = (int32_t)n; + snprintf(sFrom, sizeof(sFrom), "%s", endpointText(&from)); return sRecv; } // One datagram to host:port. Returns the bytes sent, -1 on failure. -int32_t UdpSend(int32_t handle, const char *host, int32_t port, const char *text) { +int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *data, int32_t len) { struct sockaddr_in to; ssize_t n; @@ -384,7 +852,7 @@ int32_t UdpSend(int32_t handle, const char *host, int32_t port, const char *text return -1; } - n = sendto(handleFd(handle), text, strlen(text), 0, (struct sockaddr *)&to, sizeof(to)); + n = sendto(handleFd(handle), data, (size_t)len, 0, (struct sockaddr *)&to, sizeof(to)); if (n < 0) { noteError(); diff --git a/src/libs/kpunch/dvxnet/netInt.h b/src/libs/kpunch/dvxnet/netInt.h index 3ce5fea4..4ac73263 100644 --- a/src/libs/kpunch/dvxnet/netInt.h +++ b/src/libs/kpunch/dvxnet/netInt.h @@ -180,6 +180,11 @@ void protoResolveAbandon(int32_t slot); // Abandon every lookup started for owner (an app that ended mid-lookup). void protoResolveDropOwner(int32_t owner); +// ---- netBas.c ---- + +// Free the network settings handles (NetConfigLoad) an app left. +void netBasAppEnded(int32_t appId); + // ---- netSock.c ---- // Close every socket (before the stack stops). diff --git a/src/libs/kpunch/dvxnet/netSock.c b/src/libs/kpunch/dvxnet/netSock.c index 87df0e15..85b8f454 100644 --- a/src/libs/kpunch/dvxnet/netSock.c +++ b/src/libs/kpunch/dvxnet/netSock.c @@ -395,6 +395,7 @@ void netCloseAppSockets(int32_t appId) { } protoResolveDropOwner(appId); + netBasAppEnded(appId); } diff --git a/src/libs/kpunch/dvxshell/Makefile b/src/libs/kpunch/dvxshell/Makefile index 63a0fce3..1da37fa8 100644 --- a/src/libs/kpunch/dvxshell/Makefile +++ b/src/libs/kpunch/dvxshell/Makefile @@ -34,7 +34,7 @@ CONFIGDIR = ../../../../bin/config THEMEDIR = ../../../../bin/config/themes WPAPERDIR = ../../../../bin/config/wpaper -SRCS = shellMain.c shellApp.c shellInfo.c shellAssoc.c shellProg.c shellRun.c +SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellRun.c pmModel.c pmModelBas.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGETDIR = $(LIBSDIR)/kpunch/dvxshell TARGET = $(TARGETDIR)/dvxshell.lib diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs index e27932a7..0ed3d611 100644 --- a/src/libs/kpunch/dvxshell/dvxshell.dhs +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs @@ -106,6 +106,8 @@ LoadedE is transient (only during shellLoadApp before the entry point is called) .link shell.programs Installed Programs .link shell.run Running Programs and Documents .link shell.assoc File Associations +.link shell.basic BASIC Entry Points +.link shell.sysbasic BASIC System Settings .topic shell.types .title Shell Types and Constants @@ -187,7 +189,7 @@ Passed as the sole argument to appMain(). Gives the app access to the shell's GU helpTopic[128] char[] Current help topic ID (updated by the app at runtime). Size is SHELL_HELP_TOPIC_MAX. onHelpQuery void (*)(void *ctx) Optional callback the shell fires on F1 so the app can refresh helpTopic from context. helpQueryCtx void * Opaque context pointer passed to onHelpQuery. - onNewArgs void (*)(void *ctx, const char *args) Optional. Set by an app that can take new launch arguments while running. F1 hands its "helpfile [topic]" arguments to a running help viewer that sets this, and raises it, instead of loading another copy. + onNewArgs void (*)(void *ctx, const char *args) Optional. Set by an app that can take new launch arguments while running. Loading a single-instance app that is already running and sets this hands it the new arguments and raises it (the load returns its app id) instead of saying it is already running; F1 hands its "helpfile [topic]" arguments to a running help viewer that sets this, and raises it, instead of loading another copy. newArgsCtx void * Opaque context pointer passed to onNewArgs. .endtable @@ -240,6 +242,9 @@ Per-app slot in the shell's app table. Slot 0 is reserved for the shell itself; .index shellRequestExit .index shellRunExitRequest .index Exit DVX +.index shellCloseApp +.index shellRequestAppClose +.index shellRunCloseRequests .index shellOpenHelp .index shellAppCodeForgetTask .index shellTaskDepthForget @@ -265,7 +270,7 @@ Load and start an app from a DXE file. Returns the app ID (>= 1) on success, or For callback-only apps, appMain() runs synchronously and returns before shellLoadApp returns. For main-loop apps, a cooperative task is created and the app begins running on the next tsYield(). -If multiInstance is false in the app's descriptor and the same DXE is already loaded, the call fails with an error dialog. If multiInstance is true, the DXE is copied to a temp file so dlopen gets an independent code and data image. +If multiInstance is false in the app's descriptor and the same DXE is already loaded, the running app is handed args through its onNewArgs and raised, and its app id is returned, when it sets onNewArgs; otherwise the call fails with an error dialog. If multiInstance is true, the DXE is copied to a temp file so dlopen gets an independent code and data image. .h2 shellLoadAppWithArgs @@ -352,12 +357,28 @@ A window that is still open after a request is judged as follows: shellCloseAllApps also returns false, without asking any app, when a dialog box is already open (ctx->modalWindow): the dialog is raised so it can be answered first. It pumps the event loop, so call it only from the shell's top level, never from inside a callback. An app ended from the Task Manager while the sequence runs simply counts as closed. +.h2 Closing one app: shellRequestAppClose / shellRunCloseRequests / shellCloseApp + +.code +void shellRequestAppClose(int32_t appId); +void shellRunCloseRequests(AppContextT *ctx); +bool shellCloseApp(AppContextT *ctx, int32_t appId); +.endcode + +shellCloseApp asks one app to close exactly as shellCloseAllApps asks each (the same routine): its visible windows are closed the way the user would close them, so its save prompt or QueryUnload runs, and one that does not respond is offered to be ended ("'name' is not responding to the request to close. End it?", titled Close Program). True when the app is gone or was not running; false when it kept a window or a dialog box is open. It pumps the event loop, so it belongs to the shell's top level. shellRequestAppClose only records the request (a second request for the same app is not added; an app that ends, or is ended, before it runs has its requests dropped by shellPurgeAppCallbacks, since its slot may be reused); the main loop calls shellRunCloseRequests right after each frame, before shellRunExitRequest, and it asks the waiting apps oldest first. BASIC's ShellRunningClose is shellRequestAppClose. + +.h3 The desktop ending + +The shell itself exits DVX when the desktop app ends in any other way -- its last window closed, ended from the Task Manager, or crashed: an app-cleanup hook (shellRegisterAppCleanup) notices the desktop's id going away, logs "Shell: the desktop ended; exiting DVX" and calls shellRequestExit, so every other app is asked to close as above. If one of them refuses, DVX keeps running and ends as soon as the last app has ended. A desktop app (C or BASIC) therefore never calls dvxQuit itself. + .topic shell.query .title Query API .toc 1 Query API .index shellGetApp .index shellAppSlotCount .index shellRunningAppCount +.index shellRunningAppId +.index shellActivateApp .index shellAppFindByPath .index shellPathsMatch @@ -387,6 +408,22 @@ int32_t shellRunningAppCount(void); Count running apps, not counting the shell itself. Includes apps in both AppStateLoadedE and AppStateRunningE states. +.h2 shellRunningAppId + +.code +int32_t shellRunningAppId(int32_t index); +.endcode + +The id of running app index, 0 .. shellRunningAppCount() - 1 in slot order (the same apps shellRunningAppCount counts); 0 out of range. BASIC: ShellRunningId. + +.h2 shellActivateApp + +.code +bool shellActivateApp(AppContextT *ctx, int32_t appId); +.endcode + +Bring an app to the front, as the Task Manager's Switch To does (it calls this): its topmost visible window that is not being destroyed is restored when minimized (dvxRestoreWindow; a maximized window stays maximized), else raised (dvxRaiseWindow), and gets the focus. False when the app has no visible window. BASIC: ShellRunningActivate. + .h2 shellAppFindByPath .code @@ -482,19 +519,21 @@ When the serial library is loaded, the shell feeds keyboard and mouse event timi .index shellUnregisterAppCleanup .index ShellAppCleanupFnT .index shellDesktopUpdate +.index shellDesktopChangeCount .index shellCtrlEscFn +.index shellOpenTaskManager .h1 Desktop Callbacks -The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation). +The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it. .h2 Shell Event Hooks .table AppContextT field Handler Purpose ----------------- ------- ------- - idleCallback shellIdleDispatch Runs every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects. - onCtrlEsc ctrlEscHandler Invokes shellCtrlEscFn if the Task Manager DXE is loaded. + idleCallback shellIdleDispatch Runs one frame of Run Minimized (shellMinimizeIdle) and every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects. + onCtrlEsc ctrlEscHandler shellOpenTaskManager. onF1 f1HelpHandler Opens the focused app's helpFile/helpTopic (or the system help) in the help viewer, reusing a running viewer that sets onNewArgs. onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh. .endtable @@ -548,15 +587,24 @@ Register a function the shell calls with an app's id whenever that app ends -- n void shellDesktopUpdate(void); .endcode -Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. +Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. Each call also adds one to the change counter (shellDesktopChangeCount). -.h2 shellCtrlEscFn +.h2 shellDesktopChangeCount + +.code +int32_t shellDesktopChangeCount(void); +.endcode + +How many times shellDesktopUpdate has run (it wraps around). For a desktop that cannot take a callback -- a BASIC program polls it from a Timer (ShellChangeCount) and refreshes its running-program count when the value differs from the one it saw last. + +.h2 shellCtrlEscFn / shellOpenTaskManager .code extern void (*shellCtrlEscFn)(AppContextT *ctx); +void shellOpenTaskManager(AppContextT *ctx); .endcode -Function pointer set by the taskmgr DXE's constructor. The shell calls this when Ctrl+Esc is pressed. NULL if the task manager is not loaded. +shellCtrlEscFn is set by the taskmgr DXE's constructor; NULL if the task manager is not loaded. shellOpenTaskManager calls it when it is set and does nothing otherwise; Ctrl+Esc, Program Manager's Help > Task Manager and BASIC's ShellTaskManager all open Task Manager through it. .topic shell.info .title System Information @@ -592,6 +640,8 @@ Return the formatted system information text. The returned pointer is valid for .index shellProgramScan .index shellProgramInfo .index shellProgramIcon +.index shellItemIcon +.index shellItemIconFile .index shellProgramListFree .index shellIsProgram .index ShellProgramT @@ -644,6 +694,22 @@ uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int3 A picture edge x edge pixels in display format, for drawing with rectCopy: a program's icon32 resource, or any image file dvxLoadImage reads (BMP, PNG, JPEG, GIF). Other sizes are scaled (nearest pixel). Returns NULL when there is no picture. Free with dvxFreeImage. +.h2 shellItemIcon + +.code +uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch); +.endcode + +The icon a launcher shows for path (a program or a document), edge x edge pixels as shellProgramIcon gives them: the picture file iconPath when it is given (NULL or "" for none) and can be read; else path's own picture -- a program's icon32, or path itself when it is a picture file; else, for a document, the icon of the program its file type opens in (shellAssocCommand). NULL when there is none; free with dvxFreeImage. Program Manager's items (both sizes) and its Properties preview use it, so an item and its preview always show the same icon. (A BASIC program's LOADPICTURE of a .app reads the program's icon itself, with dvxResLoadIconData.) + +.h2 shellItemIconFile + +.code +bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize); +.endcode + +The same rule as shellItemIcon (one implementation), naming the file the icon comes from in out: iconPath, path, or the program path's file type opens in; false with "" when none gives a picture. dvxLoadImage of that file (BASIC LOADPICTURE) gives the icon. BASIC: ShellItemIcon$ in shell.bas. + .h2 shellProgramListFree / shellIsProgram .code @@ -661,7 +727,9 @@ shellProgramListFree frees a list from shellProgramScan (NULL is fine). shellIsP .index shellRunDialog .index shellSplitCommand .index shellResolveArgs +.index shellMinimizeApp .index Run dialog +.index Run Minimized .h1 Running Programs and Documents @@ -709,6 +777,15 @@ void shellResolveArgs(const char *folder, const char *args, char *out, int32_t o Copy args to out with blanks normalized, replacing every relative argument that names an existing file inside folder by folder\name, so a program finds the files an item's arguments name no matter where it runs. Quoted arguments keep their quotes. With an empty or NULL folder the arguments are only normalized. DVX programs share one current folder, so this is how a program item's working folder takes effect. +.h2 shellMinimizeApp + +.code +void shellMinimizeApp(AppContextT *ctx, int32_t appId); +void shellMinimizeIdle(AppContextT *ctx); +.endcode + +Run Minimized. shellMinimizeApp minimizes every visible window of appId (an id from shellRunProgram) at once and again on each of the next SHELL_MINIMIZE_FRAMES (60) idle frames, because a program with a main loop opens its windows from its own task after it has started. Asking again for the same program starts its frames over; nothing happens for an id that is not running, and a program that ends is forgotten (an app-cleanup hook). shellMinimizeIdle is the per-frame step, called by the shell's idle dispatch. Program Manager's Run Minimized items and BASIC's ShellMinimizeApp use it. + .topic shell.assoc .title File Associations .toc 1 File Associations @@ -766,3 +843,344 @@ bool shellAssocNormalize(const char *extension, char *out); shellAssocSet adds or replaces an association (programPath may carry ,arguments) and saves; false for a bad extension, an empty program, or a failed save. shellAssocRemove removes one and saves; false when it did not exist or the save failed. shellAssocNormalize writes the extension as the store keys it (SHELL_ASSOC_EXT_MAX bytes: no dot, upper case) and is false for an empty extension, one longer than eight characters, or anything but letters and digits. + +.topic shell.basic +.title BASIC Entry Points +.toc 1 BASIC Entry Points +.index ShellRun +.index ShellRunProgram +.index ShellRunCommand +.index ShellRunDialog +.index ShellMinimizeApp +.index ShellOpenDocument +.index ShellProgramScan +.index ShellProgramCount +.index ShellProgramPath +.index ShellProgramFree +.index ShellProgramName +.index ShellProgramDescription +.index ShellIsProgram +.index ShellAssocProgram +.index ShellAssocCommand +.index ShellAssocCount +.index ShellAssocExt +.index ShellAssocValue +.index ShellAssocSet +.index ShellAssocRemove +.index ShellAssocNormalize +.index ShellRunningCount +.index ShellRunningId +.index ShellRunningName +.index ShellRunningPath +.index ShellRunningFind +.index ShellRunningActivate +.index ShellRunningClose +.index ShellRunningKill +.index ShellAppIsHung +.index ShellAppMemory +.index ShellAppId +.index ShellPriority +.index ShellSetPriority +.index ShellCommandProgram +.index ShellCommandArgs +.index ShellArg +.index ShellArgCount +.index ShellChangeCount +.index ShellTaskManager +.index ShellSystemInfo +.index ShellExitDvx +.index HotkeyAdd +.index HotkeyRemove +.index HotkeyNext +.index HotkeyText +.index ShellAppPath +.index ShellConfigFile +.index ShellInstanceCount +.index ShellLog +.index ShellSetDesktop +.index ShellSetWelcome +.index shell.bas + +.h1 BASIC Entry Points + +Source: dvxshell/shellBas.c; BASIC declarations: src/include/basic/shell.bas (DECLARE LIBRARY "dvxshell"). + +The shell's services for DVX BASIC programs -- everything Program Manager does, so a desktop can be written in BASIC. Only BASIC-marshalable types: strings and LONG in, LONG or STRING out, True (-1) / False (0) for success. "This program" is the app whose code is running (ctx->currentAppId, which the shell keeps for every app's task and window callback). A returned string stays valid until the next call of the same function. Program lists (1, 2, ...; 0 means none) and hotkeys belong to the program that made them and are released by an app-cleanup hook (shellRegisterAppCleanup) when it ends. + +.h2 Starting programs and documents + +.code +int32_t ShellRun(const char *program, const char *args); +int32_t ShellRunProgram(const char *program, const char *args, const char *folder); +int32_t ShellRunCommand(const char *command); +const char *ShellRunDialog(const char *history); +void ShellMinimizeApp(int32_t appId); +int32_t ShellOpenDocument(const char *path); +.endcode + +ShellRunProgram is shellRunProgram (a document opens through its association; file names in args, and a relative document, are looked for in folder; "" for none), returning the new app id or 0. ShellRun is ShellRunProgram without a folder, True when it started; the two are one launch path. ShellRunCommand is shellRunCommand (id or 0). ShellRunDialog splits history at line ends (CHR$(10), a CR before it is dropped, empty lines skipped) into the list shellRunDialog shows, and returns the command chosen or "" when cancelled. ShellMinimizeApp is shellMinimizeApp. ShellOpenDocument is shellAssocOpenDocument (it shows nothing when there is no association; ShellRunProgram does). + +.h2 Installed programs + +.code +int32_t ShellProgramScan(const char *folder, const char *skipPath); +int32_t ShellProgramCount(int32_t list); +const char *ShellProgramPath(int32_t list, int32_t index); +void ShellProgramFree(int32_t list); +const char *ShellProgramName(const char *path); +const char *ShellProgramDescription(const char *path); +int32_t ShellIsProgram(const char *path); +.endcode + +ShellProgramScan is shellProgramScan of folder ("" for SHELL_PROG_DIR) without skipPath ("" for none), paths only (readInfo false), so it never opens a program; it returns a list handle, 0 when nothing was found. ShellProgramCount and ShellProgramPath read it (0 / "" for a bad list or index); ShellProgramFree frees it. ShellProgramName and ShellProgramDescription are shellProgramInfo's name and description, read one program at a time so a long list can be filled with DoEvents in between (the C progress callback). ShellIsProgram is shellIsProgram. + +.h2 File types + +.code +const char *ShellAssocProgram(const char *extension); +const char *ShellAssocCommand(const char *document); +int32_t ShellAssocCount(void); +const char *ShellAssocExt(int32_t index); +const char *ShellAssocValue(int32_t index); +int32_t ShellAssocSet(const char *extension, const char *value); +int32_t ShellAssocRemove(const char *extension); +const char *ShellAssocNormalize(const char *extension); +.endcode + +ShellAssocProgram is shellAssocLookup: the stored value, the program followed by ",args" when there is a template. ShellAssocCommand is the program part of shellAssocCommand for a document. ShellAssocCount, ShellAssocExt and ShellAssocValue enumerate shellAssocGet (0-based; "" past the end). ShellAssocSet, ShellAssocRemove and ShellAssocNormalize are shellAssocSet, shellAssocRemove and shellAssocNormalize ("" when the extension cannot be one). Every change is saved at once. + +.h2 Running programs and DVX + +.code +int32_t ShellRunningCount(void); +int32_t ShellChangeCount(void); +void ShellTaskManager(void); +const char *ShellSystemInfo(void); +void ShellExitDvx(void); +.endcode + +ShellRunningCount is shellRunningAppCount. ShellChangeCount is shellDesktopChangeCount: a BASIC desktop polls it from a Timer in place of shellRegisterDesktopUpdate's callback. ShellTaskManager is shellOpenTaskManager, ShellSystemInfo is shellGetSystemInfo ("" before shellInfoInit), and ShellExitDvx is shellRequestExit (every app is asked to close, then DVX quits; a desktop asks the user first). + +.code +int32_t ShellRunningId(int32_t index); +const char *ShellRunningName(int32_t appId); +const char *ShellRunningPath(int32_t appId); +int32_t ShellRunningFind(const char *program); +int32_t ShellRunningActivate(int32_t appId); +int32_t ShellRunningClose(int32_t appId); +int32_t ShellRunningKill(int32_t appId); +int32_t ShellAppIsHung(int32_t appId); +int32_t ShellAppMemory(int32_t appId); +.endcode + +The Task Manager's operations, one routine each with it. ShellRunningId is shellRunningAppId. ShellRunningName and ShellRunningPath are the ShellAppT name and path of an app in AppStateRunningE ("" otherwise; the same state every call below requires). ShellRunningFind is shellAppFindByPath (shellPathsMatch rules), 0 when that app is not running. ShellRunningActivate is shellActivateApp. ShellRunningClose is shellRequestAppClose: the app is asked at the main loop's top level, after the BASIC code has returned; True when the request was recorded. ShellRunningKill is shellForceKillApp followed by shellDesktopUpdate (End Task: no shutdownFn, callbacks purged, the unload deferred while the app's code is on a stack); the running program itself is refused (BASIC's END ends it). ShellAppIsHung is dvxAppIsHung and ShellAppMemory dvxMemGetAppUsage (clamped to INT32_MAX; 0 for an app that is not running). + +.h2 Command lines and priority + +.code +const char *ShellCommandProgram(const char *command); +const char *ShellCommandArgs(const char *command); +int32_t ShellArgCount(const char *command); +const char *ShellArg(const char *command, int32_t index); +int32_t ShellAppId(void); +int32_t ShellPriority(void); +int32_t ShellSetPriority(int32_t priority); +.endcode + +ShellCommandProgram and ShellCommandArgs are the two parts of shellSplitCommand (what shellRunCommand runs). ShellArgCount and ShellArg walk a command line with dvxNextArg -- the tokenizer shellSplitCommand and COMMAND$(n) use -- counting the program as argument 0; ShellArg drops the quotes and gives "" past the last. ShellAppId is ctx->currentAppId. ShellPriority and ShellSetPriority are tsGetPriority / tsSetPriority on the running program's mainTaskId (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH, shell.bas SHELL_PRIORITY_*); -1 / False for a program without a task of its own (every BASIC program has one) or a priority out of range. + +.h2 Hotkeys + +.code +int32_t HotkeyAdd(const char *keyText); +int32_t HotkeyNext(void); +void HotkeyRemove(int32_t id); +const char *HotkeyText(const char *keyText); +.endcode + +HotkeyAdd parses keyText with dvxAccelParse and registers it with dvxRegisterHotkey for the running program; it returns an id of its own (> 0), or 0 when keyText is not a key or dvxRegisterHotkey refuses it (it needs Ctrl or Alt unless it is a function or navigation key, and a key already registered is refused). A BASIC program cannot be called back, so the hotkey's callback queues the id for its program (up to 16 presses not yet collected); HotkeyNext returns and removes the oldest one, 0 when there is none. HotkeyRemove drops one of the running program's hotkeys (other ids are ignored). HotkeyText is dvxAccelParse followed by dvxAccelFormat ("" when keyText is not a key): the spelling menus show. dvxUnregisterAppHotkeys removes a program's hotkeys when it ends; the app-cleanup hook drops their records and queued presses. + +.h2 This program and DVX settings + +.code +const char *ShellAppPath(void); +const char *ShellConfigFile(const char *fileName); +int32_t ShellInstanceCount(void); +void ShellLog(const char *text); +int32_t ShellSetDesktop(const char *program); +int32_t ShellSetWelcome(const char *program); +.endcode + +ShellAppPath is the running program's .app path as started ("" when unknown). ShellConfigFile is shellConfigPath of fileName in the running program's config folder, which shellEnsureConfigDir makes first ("" when it cannot); every copy of a program gets the same folder, the one a C app's settings use. ShellInstanceCount counts the running apps with the same path, this one included (a second copy of a multiInstance program loads from a temporary copy but keeps the original path). ShellLog writes one line with dvxLog. ShellSetDesktop and ShellSetWelcome set, or with "" remove, DVX.INI [shell] desktop (the desktop app; SHELL_DESKTOP_APP when missing) and [shell] welcome (the program started after the desktop); both share one writer and return False (logged) when DVX.INI cannot be saved. + +.h2 Programs folder, item icons, program groups + +.code +const char *ShellProgramsFolder(void); +const char *ShellItemIcon(const char *iconPath, const char *path); +.endcode + +ShellProgramsFolder is SHELL_PROG_DIR (where ShellProgramScan("") looks and a Browse for a program starts). ShellItemIcon is shellItemIconFile ("" for none). The program-group bindings (ShellGroups*, ShellGroup*, ShellItem*) are in pmModelBas.c; see Program Groups. + +.h2 A desktop in BASIC + +A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell exits DVX when it ends, so it needs no code for that; its Exit command confirms and calls ShellExitDvx. Its groups use the shell's program-group model (Program Groups: the same PROGMAN.INI format, rules and limits as Program Manager) through shell.bas, in ShellConfigFile$'s folder. An icon grid is drawn on a PictureBox with PaintPicture and pictures from LOADPICTURE of ShellItemIcon$ (a .app gives its icon), with MouseDown/MouseUp for selecting and dragging and KeyDown for the keyboard. The test fixture src/test/dos/fixtures/parpm does all of this. + +.topic shell.groups +.title Program Groups +.toc 1 Program Groups +.index pmModel.h +.index PmModelT +.index pmModelLoad +.index pmModelSave +.index pmInstalledScan +.index pmHotkeyParse +.index pmItemGetField +.index ShellGroupsLoad +.index PROGMAN.INI + +.h1 Program Groups + +Header: dvxshell/pmModel.h; source pmModel.c (model) and pmModelBas.c (BASIC entry points, shell.bas). + +Program Manager's groups, items and PROGMAN.INI, as plain data with no windows, part of the shell so the C Program Manager and a desktop written in BASIC share one model (the pm prefix names it). groups[PM_INSTALLED_IDX] is the automatic Installed Programs group (pmInstalledScan: every program under SHELL_PROG_DIR, sorted by name, never saved; pmInstalledChanged tells cheaply whether a program was added or removed); the others are the user's, saved by pmModelSave with a crash-safe rewrite in this layout: + +.code +[options] minimizeOnRun, restoreAlone, autoArrange, view, group +[groups] count +[groupN] name, count, itemM.name, itemM.program, itemM.args, + itemM.folder, itemM.icon, itemM.key, itemM.minimized, + itemM.col, itemM.row (N and M count from 1) +[run] historyN (newest first) +.endcode + +.table + Function Description + -------- ----------- + pmModelInit / pmModelLoad / pmModelSave / pmModelFree An empty model; load a file (one never saved gives Main and Startup; damaged entries are skipped); save; free. + pmGroupAdd / pmGroupDelete / pmGroupFind / pmGroupRename / pmGroupNameValid Groups (names trimmed, unique in any case, PM_GROUP_MAX of them; the automatic group cannot change). + pmItemAdd / pmItemCopy / pmItemMove / pmItemDelete / pmItemReorder / pmItemPlace Items (a name and a program each, PM_ITEM_MAX per group). + pmItemGetField / pmItemSetField An item's field by its PROGMAN.INI name (PM_FIELD_*); a key must pass pmHotkeyParse, a name or program cannot be emptied. + pmOptionGet / pmOptionSet An [options] value by name (flags, view, group). + pmArrangeGroup / pmPlaceUnplaced / pmCellItem Grid cells for an icon view. + pmItemMatches / pmContainsNoCase Program Manager's search. + pmHistoryAdd The Run history (PM_HISTORY_MAX, newest first). + pmHotkeyParse An item shortcut: dvxAccelParse, and Ctrl plus Alt or Shift so it never takes a key a program uses itself; "" is no shortcut. + pmInstalledScan / pmInstalledChanged / pmInstalledReplace The automatic group. +.endtable + +BASIC (shell.bas, DECLARE LIBRARY "dvxshell"): ShellGroupsLoad / Save / Free / Scan / Changed / Get / Set / History$ / HistoryAdd, ShellGroupCount / Name$ / Find / Add / Delete / Rename / NameValid / Arrange / PlaceUnplaced / CellItem, ShellItemCount / Add / Get$ / Set / Delete / Copy / Move / Reorder / Place / Matches / KeyValid. A groups file is a handle of the program that opened it, freed when it ends (shellRegisterAppCleanup). + +.topic shell.sysbasic +.title BASIC System Settings +.toc 1 BASIC System Settings +.index system.bas +.index SysSetVideoMode +.index SysVideoModeCount +.index SysSetColor +.index SysGetColor +.index SysColorId +.index SysLoadTheme +.index SysSaveTheme +.index SysColorsStore +.index SysSetWallpaper +.index SysWallpaperStore +.index SysSetMouse +.index SysMouseStore +.index SysMouseX +.index SysMouseY +.index SysMouseButtons +.index SysSetMousePos +.index SysShiftState +.index SysSendKey +.index SysScreenshot +.index SysMemoryTotalKb +.index SysMemoryFreeKb +.index SYS_VIDEO_DEFAULT_WIDTH + +.h1 BASIC System Settings + +Source: dvxshell/shellSysBas.c; BASIC declarations: src/include/basic/system.bas (DECLARE LIBRARY "dvxshell"); prototypes in shellApp.h. + +What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False). + +.code +int32_t SysVideoModeCount(void); +int32_t SysVideoModeWidth(int32_t index); +int32_t SysVideoModeHeight(int32_t index); +int32_t SysVideoModeBpp(int32_t index); +int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp); +int32_t SysColorCount(void); +const char *SysColorLabel(int32_t id); +const char *SysColorName(int32_t id); +int32_t SysColorId(const char *name); +int32_t SysGetColor(int32_t id); +void SysSetColor(int32_t id, int32_t rgb); +void SysResetColors(void); +int32_t SysLoadTheme(const char *path); +int32_t SysSaveTheme(const char *path); +void SysColorsStore(int32_t prefs); +int32_t SysColorsLoad(int32_t prefs); +int32_t SysSetWallpaper(const char *path); +const char *SysWallpaper(void); +void SysSetWallpaperMode(int32_t mode); +int32_t SysWallpaperMode(void); +void SysWallpaperStore(int32_t prefs); +int32_t SysWallpaperLoad(int32_t prefs); +void SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep); +int32_t SysMouseWheelDir(void); +int32_t SysMouseDoubleClick(void); +int32_t SysMouseAccel(void); +int32_t SysMouseMickeys(void); +int32_t SysMouseWheelStep(void); +void SysMouseStore(int32_t prefs); +void SysMouseLoad(int32_t prefs); +int32_t SysFontWidth(void); +int32_t SysFontHeight(void); +int32_t SysFontFirstChar(void); +int32_t SysFontCharCount(void); +int32_t SysFontGlyphRow(int32_t code, int32_t row); +int32_t SysMouseX(void); +int32_t SysMouseY(void); +int32_t SysMouseButtons(void); +void SysSetMousePos(int32_t x, int32_t y); +int32_t SysShiftState(void); +int32_t SysSendKey(int32_t keyCode, int32_t shift); +int32_t SysScreenshot(const char *path); +int32_t SysMemoryTotalKb(void); +int32_t SysMemoryFreeKb(void); +.endcode + +.table + Function Calls + -------- ----- + SysVideoMode* dvxGetVideoModes (0 out of range) + SysSetVideoMode dvxChangeVideoMode; False on -1 (also while a program is in exclusive full-screen mode) + SysColorLabel / SysColorName dvxColorLabel / dvxColorName ("" for an id outside 0 .. ColorCountE - 1) + SysColorId the id whose dvxColorName matches, any case; -1 for none + SysGetColor / SysSetColor dvxGetColor / dvxSetColor (an unknown id: 0 / nothing) + SysResetColors dvxResetColorScheme + SysLoadTheme / SysSaveTheme dvxLoadTheme / dvxSaveTheme + SysColorsStore / Load dvxColorsToPrefs / dvxColorsFromPrefs (Load: True when the handle had a [colors] key) + SysSetWallpaper dvxSetWallpaper ("" clears) + SysSetWallpaperMode dvxSetWallpaperMode with the value of prefsWallpaperChoices[mode]; other modes are ignored + SysWallpaper / Mode ctx->wallpaperPath; the prefsWallpaperChoices index of ctx->wallpaperMode + SysWallpaperStore / Load dvxWallpaperToPrefs / dvxWallpaperFromPrefs (Load: False when the image could not be loaded) + SysSetMouse dvxSetMouseConfig + SysMouse* ctx->wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep + SysMouseStore / Load dvxMouseToPrefs / dvxMouseFromPrefs + SysFont* dvxGetFont: charWidth, charHeight, firstChar, numChars; SysFontGlyphRow is glyphData[(code - firstChar) * charHeight + row] (0 outside the font) + SysMouseX / Y / Buttons ctx->mouseX, mouseY, mouseButtons (MOUSE_LEFT / RIGHT / MIDDLE = vbLeftButton ...) + SysSetMousePos platformMouseWarp, clamped to the screen; the next poll reports the move like the user's + SysShiftState ctx->keyModifiers & KEY_MOD_ALL (KEY_MOD_SHIFT / CTRL / ALT = vbShiftMask / vbCtrlMask / vbAltMask) + SysSendKey queues (up to 64) keyCode + shift as the BIOS stores the keystroke (platformBiosScanCode for combinations with codes of their own, KEY_EXT_FLAG keys by scan code, Ctrl+letter as its control character); a shell idle handler (shellRegisterIdle) injects one per idle frame with platformInjectKey, so each is read with its own modifiers, and releases them (platformInjectModifiers(0)) after the last + SysScreenshot dvxScreenshot (BMP for .bmp, else PNG) + SysMemoryTotalKb / FreeKb platformGetMemoryInfo; -1 when unknown +.endtable + +There is no binding for platformInjectMouse: on DOS it replaces the real mouse until the next call and has no way back to it, so a synthesized click would leave the pointer stuck. A program moves the pointer with SysSetMousePos and "clicks" a control by calling its event procedure or setting its value. + +system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h. + +The Store / Load routines are the ones the shell uses at start (dvxMouseFromPrefs, dvxColorsFromPrefs, dvxWallpaperFromPrefs on DVX.INI) and Control Panel uses for OK and for its Cancel snapshot (an in-memory prefsCreate handle), so the INI keys are written in one place (dvxApp.c). + diff --git a/src/apps/kpunch/progman/pmModel.c b/src/libs/kpunch/dvxshell/pmModel.c similarity index 61% rename from src/apps/kpunch/progman/pmModel.c rename to src/libs/kpunch/dvxshell/pmModel.c index c6225ad8..5b4be5ee 100644 --- a/src/apps/kpunch/progman/pmModel.c +++ b/src/libs/kpunch/dvxshell/pmModel.c @@ -20,14 +20,16 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// pmModel.c -- Program Manager groups, items and PROGMAN.INI (see pmModel.h) +// pmModel.c -- program groups, items and PROGMAN.INI (see pmModel.h) #include "pmModel.h" +#include "shellApp.h" #include "dvxApp.h" #include "dvxPrefs.h" #include "dvxPlat.h" -#include +#include +#include #include #include #include @@ -51,27 +53,38 @@ #define INI_VIEW "view" #define INI_CURRENT "group" #define INI_HISTORY "history" -#define INI_ITEM_KEY_FMT "item%ld.%s" -#define INI_ITEM_NAME "name" -#define INI_ITEM_PROGRAM "program" -#define INI_ITEM_ARGS "args" -#define INI_ITEM_FOLDER "folder" -#define INI_ITEM_ICON "icon" -#define INI_ITEM_KEY "key" -#define INI_ITEM_MIN "minimized" -#define INI_ITEM_COL "col" -#define INI_ITEM_ROW "row" +#define INI_ITEM_KEY_FMT "item%" PRId32 ".%s" +#define INI_ITEM_NAME PM_FIELD_NAME +#define INI_ITEM_PROGRAM PM_FIELD_PROGRAM +#define INI_ITEM_ARGS PM_FIELD_ARGS +#define INI_ITEM_FOLDER PM_FIELD_FOLDER +#define INI_ITEM_ICON PM_FIELD_ICON +#define INI_ITEM_KEY PM_FIELD_KEY +#define INI_ITEM_MIN PM_FIELD_MINIMIZED +#define INI_ITEM_COL PM_FIELD_COL +#define INI_ITEM_ROW PM_FIELD_ROW #define INI_KEY_MAX 32 // "item256.minimized" plus room #define INI_SECTION_MAX 16 // "group64" plus room #define DUP_SUFFIX_MAX 8 // " (64)" plus NUL -#define FKEY_COUNT 12 -#define FKEY_NUMBER_BASE 10 +#define DECIMAL 10 -#define HOTKEY_SEP '+' -#define HOTKEY_CTRL "Ctrl" -#define HOTKEY_ALT "Alt" -#define HOTKEY_SHIFT "Shift" -#define HOTKEY_FKEY_FMT "F%ld" +// An item's text fields by name (PROGMAN.INI's item keys) +typedef struct { + const char *name; + size_t offset; + int32_t size; +} PmTextFieldT; + +#define PM_TEXT_FIELD(n, field) { n, offsetof(PmItemT, field), (int32_t)sizeof(((PmItemT *)0)->field) } + +static const PmTextFieldT sTextFields[] = { + PM_TEXT_FIELD(INI_ITEM_NAME, name), + PM_TEXT_FIELD(INI_ITEM_PROGRAM, program), + PM_TEXT_FIELD(INI_ITEM_ARGS, args), + PM_TEXT_FIELD(INI_ITEM_FOLDER, folder), + PM_TEXT_FIELD(INI_ITEM_ICON, iconPath), + PM_TEXT_FIELD(PM_FIELD_DESCRIPTION, description) +}; static const PrefsChoiceT sViewChoices[PmViewCountE] = { @@ -80,46 +93,47 @@ static const PrefsChoiceT sViewChoices[PmViewCountE] = { [PmViewListE] = { "List", "list", PmViewListE } }; -static const int32_t sFKeys[FKEY_COUNT] = { - KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12 -}; - // ============================================================ // Prototypes // ============================================================ -static void groupFree(PmGroupT *g); -static void itemKey(char *out, int32_t outSize, int32_t n, const char *field); -static int itemNameCmp(const void *a, const void *b); -static void loadGroup(PmModelT *m, PrefsHandleT *h, int32_t n); -static void loadItem(PmModelT *m, int32_t g, PrefsHandleT *h, const char *section, int32_t n); -void pmArrangeGroup(PmGroupT *g, int32_t cols); -int32_t pmCellItem(const PmGroupT *g, int32_t col, int32_t row); -bool pmContainsNoCase(const char *hay, const char *needle); -int32_t pmGroupAdd(PmModelT *m, const char *name); -bool pmGroupDelete(PmModelT *m, int32_t g); -int32_t pmGroupFind(const PmModelT *m, const char *name); -bool pmGroupNameValid(const PmModelT *m, const char *name, int32_t except); -bool pmGroupRename(PmModelT *m, int32_t g, const char *name); -void pmHistoryAdd(PmModelT *m, const char *command); -void pmHotkeyFormat(int32_t key, int32_t mods, char *out, int32_t outSize); -bool pmHotkeyParse(const char *text, int32_t *outKey, int32_t *outMods); -void pmInstalledReplace(PmModelT *m, PmItemT *items); -int32_t pmItemAdd(PmModelT *m, int32_t g, const PmItemT *item); -int32_t pmItemCopy(PmModelT *m, int32_t g, int32_t i, int32_t dst); -bool pmItemDelete(PmModelT *m, int32_t g, int32_t i); -void pmItemFreeIcons(PmItemT *item); -bool pmItemMatches(const PmItemT *item, const char *filter); -int32_t pmItemMove(PmModelT *m, int32_t g, int32_t i, int32_t dst); -void pmItemPlace(PmGroupT *g, int32_t i, int32_t col, int32_t row); -int32_t pmItemReorder(PmGroupT *g, int32_t from, int32_t to); -void pmModelFree(PmModelT *m); -void pmModelInit(PmModelT *m); -bool pmModelLoad(PmModelT *m, const char *path); -bool pmModelSave(const PmModelT *m, const char *path); -void pmPlaceUnplaced(PmGroupT *g, int32_t cols); -static void saveGroup(PrefsHandleT *h, const PmGroupT *g, int32_t n); -static void setIfAny(PrefsHandleT *h, const char *section, const char *key, const char *value); +static void groupFree(PmGroupT *g); +static void itemKey(char *out, int32_t outSize, int32_t n, const char *field); +static void loadGroup(PmModelT *m, PrefsHandleT *h, int32_t n); +static void loadItem(PmModelT *m, int32_t g, PrefsHandleT *h, const char *section, int32_t n); +void pmArrangeGroup(PmGroupT *g, int32_t cols); +int32_t pmCellItem(const PmGroupT *g, int32_t col, int32_t row); +bool pmContainsNoCase(const char *hay, const char *needle); +int32_t pmGroupAdd(PmModelT *m, const char *name); +bool pmGroupDelete(PmModelT *m, int32_t g); +int32_t pmGroupFind(const PmModelT *m, const char *name); +bool pmGroupNameValid(const PmModelT *m, const char *name, int32_t except); +bool pmGroupRename(PmModelT *m, int32_t g, const char *name); +void pmHistoryAdd(PmModelT *m, const char *command); +bool pmHotkeyParse(const char *text, int32_t *outKey, int32_t *outMods); +bool pmInstalledChanged(const PmModelT *m, const char *skipPath); +void pmInstalledReplace(PmModelT *m, PmItemT *items); +int32_t pmInstalledScan(PmModelT *m, const char *skipPath, ShellProgressFnT progress, void *progressCtx); +int32_t pmItemAdd(PmModelT *m, int32_t g, const PmItemT *item); +int32_t pmItemCopy(PmModelT *m, int32_t g, int32_t i, int32_t dst); +bool pmItemDelete(PmModelT *m, int32_t g, int32_t i); +void pmItemFreeIcons(PmItemT *item); +bool pmItemGetField(const PmItemT *item, const char *field, char *out, int32_t outSize); +bool pmItemMatches(const PmItemT *item, const char *filter); +int32_t pmItemMove(PmModelT *m, int32_t g, int32_t i, int32_t dst); +void pmItemPlace(PmGroupT *g, int32_t i, int32_t col, int32_t row); +int32_t pmItemReorder(PmGroupT *g, int32_t from, int32_t to); +bool pmItemSetField(PmItemT *item, const char *field, const char *value); +void pmModelFree(PmModelT *m); +void pmModelInit(PmModelT *m); +bool pmModelLoad(PmModelT *m, const char *path); +bool pmModelSave(const PmModelT *m, const char *path); +bool pmOptionGet(const PmModelT *m, const char *option, int32_t *outValue, bool *outIsFlag); +bool pmOptionSet(PmModelT *m, const char *option, int32_t value); +void pmPlaceUnplaced(PmGroupT *g, int32_t cols); +static void saveGroup(PrefsHandleT *h, const PmGroupT *g, int32_t n); +static void setIfAny(PrefsHandleT *h, const char *section, const char *key, const char *value); +static const PmTextFieldT *textFieldFind(const char *field); static void groupFree(PmGroupT *g) { @@ -133,12 +147,7 @@ static void groupFree(PmGroupT *g) { static void itemKey(char *out, int32_t outSize, int32_t n, const char *field) { - snprintf(out, outSize, INI_ITEM_KEY_FMT, (long)n, field); -} - - -static int itemNameCmp(const void *a, const void *b) { - return strcasecmp(((const PmItemT *)a)->name, ((const PmItemT *)b)->name); + snprintf(out, outSize, INI_ITEM_KEY_FMT, n, field); } @@ -148,17 +157,17 @@ static void loadGroup(PmModelT *m, PrefsHandleT *h, int32_t n) { char section[INI_SECTION_MAX]; char name[PM_NAME_MAX]; - snprintf(section, sizeof(section), INI_GROUP_PREFIX "%ld", (long)n); + snprintf(section, sizeof(section), INI_GROUP_PREFIX "%" PRId32, n); dvxCopyTrimmed(name, sizeof(name), prefsGetString(h, section, INI_NAME, "")); if (!name[0]) { - snprintf(name, sizeof(name), "Group %ld", (long)n); + snprintf(name, sizeof(name), "Group %" PRId32, n); } if (!pmGroupNameValid(m, name, -1)) { char suffix[DUP_SUFFIX_MAX]; - snprintf(suffix, sizeof(suffix), " (%ld)", (long)n); + snprintf(suffix, sizeof(suffix), " (%" PRId32 ")", n); name[sizeof(name) - sizeof(suffix)] = '\0'; strcat(name, suffix); } @@ -356,88 +365,18 @@ void pmHistoryAdd(PmModelT *m, const char *command) { } -void pmHotkeyFormat(int32_t key, int32_t mods, char *out, int32_t outSize) { - int32_t pos = 0; - - out[0] = '\0'; - - if (key == 0) { - return; - } - - if (mods & ACCEL_CTRL) { - pos = dvxStrAppendf(out, outSize, pos, HOTKEY_CTRL "%c", HOTKEY_SEP); - } - - if (mods & ACCEL_ALT) { - pos = dvxStrAppendf(out, outSize, pos, HOTKEY_ALT "%c", HOTKEY_SEP); - } - - if (mods & ACCEL_SHIFT) { - pos = dvxStrAppendf(out, outSize, pos, HOTKEY_SHIFT "%c", HOTKEY_SEP); - } - - for (int32_t i = 0; i < FKEY_COUNT; i++) { - if (sFKeys[i] == key) { - dvxStrAppendf(out, outSize, pos, HOTKEY_FKEY_FMT, (long)(i + 1)); - return; - } - } - - dvxStrAppendf(out, outSize, pos, "%c", (char)key); -} - - bool pmHotkeyParse(const char *text, int32_t *outKey, int32_t *outMods) { - char buf[PM_HOTKEY_TEXT]; - int32_t mods = 0; - int32_t key = 0; - - dvxCopyTrimmed(buf, sizeof(buf), text); *outKey = 0; *outMods = 0; - if (!buf[0]) { + if (!dvxSkipWs(text)[0]) { return true; } - char *part = buf; + int32_t key; + int32_t mods; - for (;;) { - char *sep = strchr(part, HOTKEY_SEP); - - if (!sep) { - break; - } - - *sep = '\0'; - dvxTrimRight(part); - - if (strcasecmp(part, HOTKEY_CTRL) == 0) { - mods |= ACCEL_CTRL; - } else if (strcasecmp(part, HOTKEY_ALT) == 0) { - mods |= ACCEL_ALT; - } else if (strcasecmp(part, HOTKEY_SHIFT) == 0) { - mods |= ACCEL_SHIFT; - } else { - return false; - } - - part = (char *)dvxSkipWs(sep + 1); - } - - if (part[0] && !part[1] && isalnum((unsigned char)part[0])) { - key = toupper((unsigned char)part[0]); - } else if (toupper((unsigned char)part[0]) == 'F') { - char *end = NULL; - long num = strtol(part + 1, &end, FKEY_NUMBER_BASE); - - if (end != part + 1 && *end == '\0' && num >= 1 && num <= FKEY_COUNT) { - key = sFKeys[num - 1]; - } - } - - if (key == 0 || !(mods & ACCEL_CTRL) || !(mods & (ACCEL_ALT | ACCEL_SHIFT))) { + if (!dvxAccelParse(text, &key, &mods) || !(mods & ACCEL_CTRL) || !(mods & (ACCEL_ALT | ACCEL_SHIFT))) { return false; } @@ -447,19 +386,72 @@ bool pmHotkeyParse(const char *text, int32_t *outKey, int32_t *outMods) { } +bool pmInstalledChanged(const PmModelT *m, const char *skipPath) { + ShellProgramT *found = NULL; + const PmGroupT *installed = &m->groups[PM_INSTALLED_IDX]; + int32_t count = shellProgramScan(SHELL_PROG_DIR, skipPath, false, NULL, NULL, &found); + bool changed = count != arrlen(installed->items); + + for (int32_t i = 0; i < count && !changed; i++) { + bool known = false; + + for (int32_t j = 0; j < arrlen(installed->items) && !known; j++) { + known = shellPathsMatch(found[i].path, installed->items[j].program); + } + + changed = !known; + } + + shellProgramListFree(found); + return changed; +} + + void pmInstalledReplace(PmModelT *m, PmItemT *items) { PmGroupT *g = &m->groups[PM_INSTALLED_IDX]; groupFree(g); - if (arrlen(items) > 1) { - qsort(items, arrlen(items), sizeof(PmItemT), itemNameCmp); + // By name, any case (an insertion sort: the list is short) + for (int32_t i = 1; i < (int32_t)arrlen(items); i++) { + PmItemT moved = items[i]; + int32_t j = i; + + while (j > 0 && strcasecmp(items[j - 1].name, moved.name) > 0) { + items[j] = items[j - 1]; + j--; + } + + items[j] = moved; } g->items = items; } +int32_t pmInstalledScan(PmModelT *m, const char *skipPath, ShellProgressFnT progress, void *progressCtx) { + ShellProgramT *found = NULL; + PmItemT *items = NULL; + int32_t count = shellProgramScan(SHELL_PROG_DIR, skipPath, true, progress, progressCtx, &found); + + for (int32_t i = 0; i < count; i++) { + PmItemT item; + + memset(&item, 0, sizeof(item)); + snprintf(item.name, sizeof(item.name), "%s", found[i].name); + snprintf(item.program, sizeof(item.program), "%s", found[i].path); + snprintf(item.description, sizeof(item.description), "%s", found[i].description); + item.col = PM_CELL_NONE; + item.row = PM_CELL_NONE; + arrput(items, item); + } + + shellProgramListFree(found); + pmInstalledReplace(m, items); + return count; +} + + int32_t pmItemAdd(PmModelT *m, int32_t g, const PmItemT *item) { if (g < 0 || g >= arrlen(m->groups) || m->groups[g].automatic || arrlen(m->groups[g].items) >= PM_ITEM_MAX || !item->name[0] || !item->program[0]) { return -1; @@ -508,6 +500,31 @@ void pmItemFreeIcons(PmItemT *item) { } +bool pmItemGetField(const PmItemT *item, const char *field, char *out, int32_t outSize) { + if (!field) { + return false; + } + + const PmTextFieldT *text = textFieldFind(field); + + if (text) { + snprintf(out, (size_t)outSize, "%s", (const char *)item + text->offset); + } else if (strcasecmp(field, INI_ITEM_KEY) == 0) { + dvxAccelFormat(item->hotkeyKey, item->hotkeyMods, out, outSize); + } else if (strcasecmp(field, INI_ITEM_MIN) == 0) { + snprintf(out, (size_t)outSize, "%d", item->runMinimized ? 1 : 0); + } else if (strcasecmp(field, INI_ITEM_COL) == 0) { + snprintf(out, (size_t)outSize, "%" PRId32, item->col); + } else if (strcasecmp(field, INI_ITEM_ROW) == 0) { + snprintf(out, (size_t)outSize, "%" PRId32, item->row); + } else { + return false; + } + + return true; +} + + bool pmItemMatches(const PmItemT *item, const char *filter) { return pmContainsNoCase(item->name, filter) || pmContainsNoCase(item->description, filter) || pmContainsNoCase(platformPathBaseName(item->program), filter); } @@ -561,6 +578,62 @@ int32_t pmItemReorder(PmGroupT *g, int32_t from, int32_t to) { } +// A name or program cannot be emptied (pmItemAdd's rule); a key must +// pass pmHotkeyParse. The item's icons are dropped when the program or +// icon changes, so they are looked up again. +bool pmItemSetField(PmItemT *item, const char *field, const char *value) { + if (!field) { + return false; + } + + const PmTextFieldT *text = textFieldFind(field); + const char *v = value ? value : ""; + + if (text) { + bool required = strcasecmp(field, INI_ITEM_NAME) == 0 || strcasecmp(field, INI_ITEM_PROGRAM) == 0; + + if (required && !dvxSkipWs(v)[0]) { + return false; + } + + dvxCopyTrimmed((char *)item + text->offset, text->size, v); + + if (strcasecmp(field, INI_ITEM_PROGRAM) == 0 || strcasecmp(field, INI_ITEM_ICON) == 0) { + pmItemFreeIcons(item); + } + + return true; + } + + if (strcasecmp(field, INI_ITEM_KEY) == 0) { + int32_t key = 0; + int32_t mods = 0; + + if (!pmHotkeyParse(v, &key, &mods)) { + return false; + } + + item->hotkeyKey = key; + item->hotkeyMods = mods; + return true; + } + + int32_t number = (int32_t)strtol(v, NULL, DECIMAL); + + if (strcasecmp(field, INI_ITEM_MIN) == 0) { + item->runMinimized = number != 0; + } else if (strcasecmp(field, INI_ITEM_COL) == 0) { + item->col = number < 0 ? PM_CELL_NONE : number; + } else if (strcasecmp(field, INI_ITEM_ROW) == 0) { + item->row = number < 0 ? PM_CELL_NONE : number; + } else { + return false; + } + + return true; +} + + void pmModelFree(PmModelT *m) { for (int32_t i = 0; i < arrlen(m->groups); i++) { groupFree(&m->groups[i]); @@ -625,7 +698,7 @@ bool pmModelLoad(PmModelT *m, const char *path) { for (int32_t n = PM_HISTORY_MAX; n >= 1; n--) { char key[INI_KEY_MAX]; - snprintf(key, sizeof(key), INI_HISTORY "%ld", (long)n); + snprintf(key, sizeof(key), INI_HISTORY "%" PRId32, n); pmHistoryAdd(m, prefsGetString(h, INI_RUN, key, "")); } @@ -663,7 +736,7 @@ bool pmModelSave(const PmModelT *m, const char *path) { for (int32_t i = 0; i < arrlen(m->history); i++) { char key[INI_KEY_MAX]; - snprintf(key, sizeof(key), INI_HISTORY "%ld", (long)(i + 1)); + snprintf(key, sizeof(key), INI_HISTORY "%" PRId32, i + 1); prefsSetString(h, INI_RUN, key, m->history[i]); } @@ -674,6 +747,48 @@ bool pmModelSave(const PmModelT *m, const char *path) { } +bool pmOptionGet(const PmModelT *m, const char *option, int32_t *outValue, bool *outIsFlag) { + *outIsFlag = true; + + if (strcasecmp(option, INI_MIN_ON_RUN) == 0) { + *outValue = m->minOnRun; + } else if (strcasecmp(option, INI_RESTORE_ALONE) == 0) { + *outValue = m->restoreAlone; + } else if (strcasecmp(option, INI_AUTO_ARRANGE) == 0) { + *outValue = m->autoArrange; + } else if (strcasecmp(option, INI_VIEW) == 0) { + *outValue = m->view; + *outIsFlag = false; + } else if (strcasecmp(option, INI_CURRENT) == 0) { + *outValue = m->current; + *outIsFlag = false; + } else { + return false; + } + + return true; +} + + +bool pmOptionSet(PmModelT *m, const char *option, int32_t value) { + if (strcasecmp(option, INI_MIN_ON_RUN) == 0) { + m->minOnRun = value != 0; + } else if (strcasecmp(option, INI_RESTORE_ALONE) == 0) { + m->restoreAlone = value != 0; + } else if (strcasecmp(option, INI_AUTO_ARRANGE) == 0) { + m->autoArrange = value != 0; + } else if (strcasecmp(option, INI_VIEW) == 0 && value >= 0 && value < PmViewCountE) { + m->view = (PmViewE)value; + } else if (strcasecmp(option, INI_CURRENT) == 0 && value >= 0 && value < arrlen(m->groups)) { + m->current = value; + } else { + return false; + } + + return true; +} + + // Cells already taken are kept; free cells are handed out row by row. void pmPlaceUnplaced(PmGroupT *g, int32_t cols) { int32_t next = 0; @@ -707,7 +822,7 @@ static void saveGroup(PrefsHandleT *h, const PmGroupT *g, int32_t n) { char key[INI_KEY_MAX]; char hotkey[PM_HOTKEY_TEXT]; - snprintf(section, sizeof(section), INI_GROUP_PREFIX "%ld", (long)n); + snprintf(section, sizeof(section), INI_GROUP_PREFIX "%" PRId32, n); prefsSetString(h, section, INI_NAME, g->name); prefsSetInt(h, section, INI_COUNT, (int32_t)arrlen(g->items)); @@ -715,7 +830,7 @@ static void saveGroup(PrefsHandleT *h, const PmGroupT *g, int32_t n) { const PmItemT *item = &g->items[i]; int32_t k = i + 1; - pmHotkeyFormat(item->hotkeyKey, item->hotkeyMods, hotkey, sizeof(hotkey)); + dvxAccelFormat(item->hotkeyKey, item->hotkeyMods, hotkey, sizeof(hotkey)); itemKey(key, sizeof(key), k, INI_ITEM_NAME); prefsSetString(h, section, key, item->name); itemKey(key, sizeof(key), k, INI_ITEM_PROGRAM); @@ -749,3 +864,14 @@ static void setIfAny(PrefsHandleT *h, const char *section, const char *key, cons prefsSetString(h, section, key, value); } } + + +static const PmTextFieldT *textFieldFind(const char *field) { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sTextFields); i++) { + if (strcasecmp(sTextFields[i].name, field) == 0) { + return &sTextFields[i]; + } + } + + return NULL; +} diff --git a/src/apps/kpunch/progman/pmModel.h b/src/libs/kpunch/dvxshell/pmModel.h similarity index 60% rename from src/apps/kpunch/progman/pmModel.h rename to src/libs/kpunch/dvxshell/pmModel.h index 1a2ed334..89e8f14e 100644 --- a/src/apps/kpunch/progman/pmModel.h +++ b/src/libs/kpunch/dvxshell/pmModel.h @@ -20,9 +20,13 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// pmModel.h -- Program Manager groups, items and PROGMAN.INI +// pmModel.h -- program groups: Program Manager's groups, items and +// PROGMAN.INI // -// The model is plain data with no windows or widgets, so the host tests +// Part of the shell so every desktop uses one model: the C Program +// Manager and BASIC programs (shell.bas ShellGroups*, ShellItem*, +// pmModelBas.c). The pm prefix names the model (program groups). The +// model is plain data with no windows or widgets, so the host tests // drive it directly. groups[0] is always the automatic Installed Programs // group (rebuilt from the scan of APPS\, never saved); every other group is // the user's, saved in PROGMAN.INI: @@ -57,6 +61,19 @@ #define PM_CELL_NONE (-1) // col/row of an item not placed yet #define PM_INSTALLED_IDX 0 // index of the automatic group +// An item's fields by name (pmItemGetField / pmItemSetField): the item +// keys of PROGMAN.INI, and the tooltip +#define PM_FIELD_NAME "name" +#define PM_FIELD_PROGRAM "program" +#define PM_FIELD_ARGS "args" +#define PM_FIELD_FOLDER "folder" +#define PM_FIELD_ICON "icon" +#define PM_FIELD_KEY "key" +#define PM_FIELD_MINIMIZED "minimized" +#define PM_FIELD_COL "col" +#define PM_FIELD_ROW "row" +#define PM_FIELD_DESCRIPTION "description" + #define PM_INSTALLED_NAME "Installed Programs" #define PM_STARTUP_NAME "Startup" #define PM_DEFAULT_NAME "Main" @@ -140,18 +157,27 @@ bool pmGroupRename(PmModelT *m, int32_t g, const char *name); // and keep at most PM_HISTORY_MAX entries. Blank commands are ignored. void pmHistoryAdd(PmModelT *m, const char *command); -// "Ctrl+Alt+P" text for a shortcut; empty when key is 0. -void pmHotkeyFormat(int32_t key, int32_t mods, char *out, int32_t outSize); - -// Parse "Ctrl+Alt+P" (any case, any modifier order). A shortcut needs -// Ctrl plus Alt or Shift and one letter, digit or F1-F12. Empty text is +// Parse an item's shortcut key "Ctrl+Alt+P" (dvxAccelParse; +// dvxAccelFormat writes it back). A shortcut needs Ctrl plus Alt or +// Shift, so it never takes a key a program uses itself. Empty text is // valid and means no shortcut (key 0). bool pmHotkeyParse(const char *text, int32_t *outKey, int32_t *outMods); +// True when the programs found under SHELL_PROG_DIR (skipPath left out) +// are not the automatic group's items: one was installed or removed +// since the last pmInstalledScan. Only the paths are read (quick). +bool pmInstalledChanged(const PmModelT *m, const char *skipPath); + // Replace the automatic group's items with items (an stb_ds array the // model takes over), sorted by name. void pmInstalledReplace(PmModelT *m, PmItemT *items); +// Rebuild the automatic group from every program under SHELL_PROG_DIR +// except skipPath (NULL for none), with their names and descriptions; +// progress (may be NULL) is called once per program found. Returns how +// many were found. +int32_t pmInstalledScan(PmModelT *m, const char *skipPath, ShellProgressFnT progress, void *progressCtx); + // Append a copy of item (icons not copied) to a user group; its index, or // -1 when the group is automatic, full, or the item has no name or program. int32_t pmItemAdd(PmModelT *m, int32_t g, const PmItemT *item); @@ -165,6 +191,13 @@ bool pmItemDelete(PmModelT *m, int32_t g, int32_t i); // Release the item's icons (they are loaded again when next drawn). void pmItemFreeIcons(PmItemT *item); +// An item's field by its PROGMAN.INI name as text in out: name, program, +// args, folder, icon (the picture file, "" for the program's own), key +// ("Ctrl+Alt+P", "" for none), minimized ("1" / "0"), col, row (-1: +// not placed) and description (the tooltip). False for an unknown (or +// NULL) name. +bool pmItemGetField(const PmItemT *item, const char *field, char *out, int32_t outSize); + // True when filter occurs in the item's name, description or program. bool pmItemMatches(const PmItemT *item, const char *filter); @@ -178,6 +211,11 @@ void pmItemPlace(PmGroupT *g, int32_t i, int32_t col, int32_t row); // Move item from to position to in the group's order; returns to. int32_t pmItemReorder(PmGroupT *g, int32_t from, int32_t to); +// Set an item's field by name (pmItemGetField's names, text as it +// gives them). False for an unknown name, an empty name or program, or +// a key pmHotkeyParse refuses (the item keeps its value). +bool pmItemSetField(PmItemT *item, const char *field, const char *value); + // Release everything the model holds (it is left empty, not reusable // until pmModelInit). void pmModelFree(PmModelT *m); @@ -194,7 +232,56 @@ bool pmModelLoad(PmModelT *m, const char *path); // Save the user groups, options and history with a crash-safe rewrite. bool pmModelSave(const PmModelT *m, const char *path); +// An option by its PROGMAN.INI [options] name: minimizeOnRun, +// restoreAlone, autoArrange (flags: *outIsFlag true, 1 / 0), view (a +// PmViewE) or group (the current group's index). False for an unknown +// name. +bool pmOptionGet(const PmModelT *m, const char *option, int32_t *outValue, bool *outIsFlag); + +// Set an option by name (any non-zero flag is on). False for an unknown +// name or a view / group out of range. +bool pmOptionSet(PmModelT *m, const char *option, int32_t value); + // Give every item without a cell (or with col >= cols) the first free cell. void pmPlaceUnplaced(PmGroupT *g, int32_t cols); +// ============================================================ +// DVX BASIC entry points (pmModelBas.c, src/include/basic/shell.bas) +// ============================================================ +// +// A groups file is a handle (0: none) of the program that opened it; +// flags are True (-1) / False (0); fields and options by the names of +// pmItemGetField / pmOptionGet. + +int32_t ShellGroupAdd(int32_t handle, const char *name); +void ShellGroupArrange(int32_t handle, int32_t g, int32_t cols); +int32_t ShellGroupCellItem(int32_t handle, int32_t g, int32_t col, int32_t row); +int32_t ShellGroupCount(int32_t handle); +int32_t ShellGroupDelete(int32_t handle, int32_t g); +int32_t ShellGroupFind(int32_t handle, const char *name); +const char *ShellGroupName(int32_t handle, int32_t g); +int32_t ShellGroupNameValid(int32_t handle, const char *name, int32_t except); +void ShellGroupPlaceUnplaced(int32_t handle, int32_t g, int32_t cols); +int32_t ShellGroupRename(int32_t handle, int32_t g, const char *name); +int32_t ShellGroupsChanged(int32_t handle, const char *skipPath); +void ShellGroupsFree(int32_t handle); +int32_t ShellGroupsGet(int32_t handle, const char *option); +const char *ShellGroupsHistory(int32_t handle); +void ShellGroupsHistoryAdd(int32_t handle, const char *command); +int32_t ShellGroupsLoad(const char *path); +int32_t ShellGroupsSave(int32_t handle, const char *path); +int32_t ShellGroupsScan(int32_t handle, const char *skipPath); +int32_t ShellGroupsSet(int32_t handle, const char *option, int32_t value); +int32_t ShellItemAdd(int32_t handle, int32_t g, const char *name, const char *program); +int32_t ShellItemCopy(int32_t handle, int32_t g, int32_t i, int32_t dst); +int32_t ShellItemCount(int32_t handle, int32_t g); +int32_t ShellItemDelete(int32_t handle, int32_t g, int32_t i); +const char *ShellItemGet(int32_t handle, int32_t g, int32_t i, const char *field); +int32_t ShellItemKeyValid(const char *keyText); +int32_t ShellItemMatches(int32_t handle, int32_t g, int32_t i, const char *filter); +int32_t ShellItemMove(int32_t handle, int32_t g, int32_t i, int32_t dst); +void ShellItemPlace(int32_t handle, int32_t g, int32_t i, int32_t col, int32_t row); +int32_t ShellItemReorder(int32_t handle, int32_t g, int32_t from, int32_t to); +int32_t ShellItemSet(int32_t handle, int32_t g, int32_t i, const char *field, const char *value); + #endif // PM_MODEL_H diff --git a/src/libs/kpunch/dvxshell/pmModelBas.c b/src/libs/kpunch/dvxshell/pmModelBas.c new file mode 100644 index 00000000..3a168726 --- /dev/null +++ b/src/libs/kpunch/dvxshell/pmModelBas.c @@ -0,0 +1,466 @@ +// 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. + +// pmModelBas.c -- DVX BASIC entry points of the program groups model +// (shell.bas ShellGroups*, ShellGroup*, ShellItem*; pmModel.h) +// +// Called through DECLARE LIBRARY "dvxshell", so the signatures use only +// BASIC-marshalable types: strings and LONG in, LONG or STRING out, True +// (-1) / False (0). A groups file is a handle (1, 2, ...; 0 means none) +// owned by the program that opened it and freed when that program ends. +// Groups and items are numbered from 0; group 0 is Installed Programs, +// rebuilt by ShellGroupsScan and never saved. A returned string stays +// valid until the next call returning one. + +#include "pmModel.h" +#include "shellApp.h" +#include "dvxApp.h" + +#include +#include +#include +#include +#include "dvxMem.h" +#include "stb_ds_wrap.h" + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define BAS_NO_INDEX (-1) +#define BAS_LINE_SEP '\n' // between the lines of ShellGroupsHistory$ +#define FIELD_TEXT_MAX DVX_MAX_PATH + +// One open groups file and the program it belongs to (m NULL: free slot) +typedef struct { + PmModelT *m; + int32_t appId; +} BasGroupsT; + +// ============================================================ +// Module state +// ============================================================ + +static BasGroupsT *sGroups = NULL; // stb_ds; handle = index + 1 +static char *sText = NULL; // stb_ds: the last string result + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static int32_t currentAppId(void); +static PmGroupT *groupOf(int32_t handle, int32_t g, PmModelT **outModel); +static PmItemT *itemOf(int32_t handle, int32_t g, int32_t i); +static PmModelT *modelOf(int32_t handle); +int32_t ShellGroupAdd(int32_t handle, const char *name); +void ShellGroupArrange(int32_t handle, int32_t g, int32_t cols); +int32_t ShellGroupCellItem(int32_t handle, int32_t g, int32_t col, int32_t row); +int32_t ShellGroupCount(int32_t handle); +int32_t ShellGroupDelete(int32_t handle, int32_t g); +int32_t ShellGroupFind(int32_t handle, const char *name); +const char *ShellGroupName(int32_t handle, int32_t g); +int32_t ShellGroupNameValid(int32_t handle, const char *name, int32_t except); +void ShellGroupPlaceUnplaced(int32_t handle, int32_t g, int32_t cols); +int32_t ShellGroupRename(int32_t handle, int32_t g, const char *name); +int32_t ShellGroupsChanged(int32_t handle, const char *skipPath); +void ShellGroupsFree(int32_t handle); +int32_t ShellGroupsGet(int32_t handle, const char *option); +const char *ShellGroupsHistory(int32_t handle); +void ShellGroupsHistoryAdd(int32_t handle, const char *command); +int32_t ShellGroupsLoad(const char *path); +int32_t ShellGroupsSave(int32_t handle, const char *path); +int32_t ShellGroupsScan(int32_t handle, const char *skipPath); +int32_t ShellGroupsSet(int32_t handle, const char *option, int32_t value); +int32_t ShellItemAdd(int32_t handle, int32_t g, const char *name, const char *program); +int32_t ShellItemCopy(int32_t handle, int32_t g, int32_t i, int32_t dst); +int32_t ShellItemCount(int32_t handle, int32_t g); +int32_t ShellItemDelete(int32_t handle, int32_t g, int32_t i); +const char *ShellItemGet(int32_t handle, int32_t g, int32_t i, const char *field); +int32_t ShellItemKeyValid(const char *keyText); +int32_t ShellItemMatches(int32_t handle, int32_t g, int32_t i, const char *filter); +int32_t ShellItemMove(int32_t handle, int32_t g, int32_t i, int32_t dst); +void ShellItemPlace(int32_t handle, int32_t g, int32_t i, int32_t col, int32_t row); +int32_t ShellItemReorder(int32_t handle, int32_t g, int32_t from, int32_t to); +int32_t ShellItemSet(int32_t handle, int32_t g, int32_t i, const char *field, const char *value); +static const char *textOf(const char *s); + + +// Shell app-cleanup hook: free the groups files a program that ended +// left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sGroups); i++) { + if (sGroups[i].m && sGroups[i].appId == appId) { + ShellGroupsFree(i + 1); + } + } +} + + +static int32_t currentAppId(void) { + AppContextT *ctx = shellContext(); + + return ctx ? ctx->currentAppId : 0; +} + + +// Group g of an open groups file (NULL for none); *outModel (may be NULL) +// gets the model. +static PmGroupT *groupOf(int32_t handle, int32_t g, PmModelT **outModel) { + PmModelT *m = modelOf(handle); + + if (outModel) { + *outModel = m; + } + + return (m && g >= 0 && g < (int32_t)arrlen(m->groups)) ? &m->groups[g] : NULL; +} + + +static PmItemT *itemOf(int32_t handle, int32_t g, int32_t i) { + PmGroupT *grp = groupOf(handle, g, NULL); + + return (grp && i >= 0 && i < (int32_t)arrlen(grp->items)) ? &grp->items[i] : NULL; +} + + +static PmModelT *modelOf(int32_t handle) { + return (handle >= 1 && handle <= (int32_t)arrlen(sGroups)) ? sGroups[handle - 1].m : NULL; +} + + +// A new group's index, or -1 (a bad or duplicate name, or too many groups). +int32_t ShellGroupAdd(int32_t handle, const char *name) { + PmModelT *m = modelOf(handle); + + return (m && name) ? pmGroupAdd(m, name) : BAS_NO_INDEX; +} + + +void ShellGroupArrange(int32_t handle, int32_t g, int32_t cols) { + PmGroupT *grp = groupOf(handle, g, NULL); + + if (grp) { + pmArrangeGroup(grp, cols); + } +} + + +int32_t ShellGroupCellItem(int32_t handle, int32_t g, int32_t col, int32_t row) { + PmGroupT *grp = groupOf(handle, g, NULL); + + return grp ? pmCellItem(grp, col, row) : BAS_NO_INDEX; +} + + +int32_t ShellGroupCount(int32_t handle) { + PmModelT *m = modelOf(handle); + + return m ? (int32_t)arrlen(m->groups) : 0; +} + + +int32_t ShellGroupDelete(int32_t handle, int32_t g) { + PmModelT *m = modelOf(handle); + + return (m && pmGroupDelete(m, g)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellGroupFind(int32_t handle, const char *name) { + PmModelT *m = modelOf(handle); + + return (m && name) ? pmGroupFind(m, name) : BAS_NO_INDEX; +} + + +const char *ShellGroupName(int32_t handle, int32_t g) { + PmGroupT *grp = groupOf(handle, g, NULL); + + return grp ? grp->name : ""; +} + + +int32_t ShellGroupNameValid(int32_t handle, const char *name, int32_t except) { + PmModelT *m = modelOf(handle); + + return (m && name && pmGroupNameValid(m, name, except)) ? BAS_TRUE : BAS_FALSE; +} + + +void ShellGroupPlaceUnplaced(int32_t handle, int32_t g, int32_t cols) { + PmGroupT *grp = groupOf(handle, g, NULL); + + if (grp) { + pmPlaceUnplaced(grp, cols); + } +} + + +int32_t ShellGroupRename(int32_t handle, int32_t g, const char *name) { + PmModelT *m = modelOf(handle); + + return (m && name && pmGroupRename(m, g, name)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellGroupsChanged(int32_t handle, const char *skipPath) { + PmModelT *m = modelOf(handle); + + return (m && pmInstalledChanged(m, (skipPath && skipPath[0]) ? skipPath : NULL)) ? BAS_TRUE : BAS_FALSE; +} + + +void ShellGroupsFree(int32_t handle) { + PmModelT *m = modelOf(handle); + + if (m) { + pmModelFree(m); + free(m); + sGroups[handle - 1].m = NULL; + } +} + + +// An option's value: a flag as True / False, view and group as numbers; +// -1 for an unknown option. +int32_t ShellGroupsGet(int32_t handle, const char *option) { + PmModelT *m = modelOf(handle); + int32_t value = 0; + bool isFlag = false; + + if (!m || !option || !pmOptionGet(m, option, &value, &isFlag)) { + return BAS_NO_INDEX; + } + + return isFlag ? (value ? BAS_TRUE : BAS_FALSE) : value; +} + + +// The Run history, newest first, one command per line: what +// ShellRunDialog$ takes. +const char *ShellGroupsHistory(int32_t handle) { + PmModelT *m = modelOf(handle); + + arrfree(sText); // a new result: the last one is gone + + for (int32_t i = 0; m && i < (int32_t)arrlen(m->history); i++) { + if (i > 0) { + arrput(sText, BAS_LINE_SEP); + } + + for (const char *c = m->history[i]; *c; c++) { + arrput(sText, *c); + } + } + + arrput(sText, '\0'); + return sText; +} + + +void ShellGroupsHistoryAdd(int32_t handle, const char *command) { + PmModelT *m = modelOf(handle); + + if (m && command) { + pmHistoryAdd(m, command); + } +} + + +// The groups file path (a file never saved gives Main and Startup); +// Installed Programs stays empty until ShellGroupsScan. +int32_t ShellGroupsLoad(const char *path) { + PmModelT *m = (PmModelT *)malloc(sizeof(PmModelT)); + + if (!m) { + return BAS_NO_HANDLE; + } + + pmModelInit(m); + + if (!pmModelLoad(m, path ? path : "")) { + pmModelFree(m); + free(m); + return BAS_NO_HANDLE; + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sGroups) && sGroups[idx].m) { + idx++; + } + + if (idx == (int32_t)arrlen(sGroups)) { + BasGroupsT blank = { NULL, 0 }; + + arrput(sGroups, blank); + } + + sGroups[idx].m = m; + sGroups[idx].appId = currentAppId(); + shellRegisterAppCleanup(appEnded); + return idx + 1; +} + + +int32_t ShellGroupsSave(int32_t handle, const char *path) { + PmModelT *m = modelOf(handle); + + return (m && path && pmModelSave(m, path)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellGroupsScan(int32_t handle, const char *skipPath) { + PmModelT *m = modelOf(handle); + + return m ? pmInstalledScan(m, (skipPath && skipPath[0]) ? skipPath : NULL, NULL, NULL) : 0; +} + + +int32_t ShellGroupsSet(int32_t handle, const char *option, int32_t value) { + PmModelT *m = modelOf(handle); + + return (m && option && pmOptionSet(m, option, value)) ? BAS_TRUE : BAS_FALSE; +} + + +// A new item at the end of a user group; its index, or -1. +int32_t ShellItemAdd(int32_t handle, int32_t g, const char *name, const char *program) { + PmModelT *m = modelOf(handle); + PmItemT item; + + if (!m) { + return BAS_NO_INDEX; + } + + memset(&item, 0, sizeof(item)); + item.col = PM_CELL_NONE; + item.row = PM_CELL_NONE; + + if (!pmItemSetField(&item, PM_FIELD_NAME, name) || !pmItemSetField(&item, PM_FIELD_PROGRAM, program)) { + return BAS_NO_INDEX; + } + + return pmItemAdd(m, g, &item); +} + + +int32_t ShellItemCopy(int32_t handle, int32_t g, int32_t i, int32_t dst) { + PmModelT *m = modelOf(handle); + + return m ? pmItemCopy(m, g, i, dst) : BAS_NO_INDEX; +} + + +int32_t ShellItemCount(int32_t handle, int32_t g) { + PmGroupT *grp = groupOf(handle, g, NULL); + + return grp ? (int32_t)arrlen(grp->items) : 0; +} + + +int32_t ShellItemDelete(int32_t handle, int32_t g, int32_t i) { + PmModelT *m = modelOf(handle); + + return (m && pmItemDelete(m, g, i)) ? BAS_TRUE : BAS_FALSE; +} + + +const char *ShellItemGet(int32_t handle, int32_t g, int32_t i, const char *field) { + PmItemT *item = itemOf(handle, g, i); + char text[FIELD_TEXT_MAX] = ""; + + if (item && field) { + pmItemGetField(item, field, text, (int32_t)sizeof(text)); + } + + return textOf(text); +} + + +// The shortcut rule of a group item: Ctrl plus Alt or Shift, or none (""). +int32_t ShellItemKeyValid(const char *keyText) { + int32_t key = 0; + int32_t mods = 0; + + return pmHotkeyParse(keyText ? keyText : "", &key, &mods) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellItemMatches(int32_t handle, int32_t g, int32_t i, const char *filter) { + PmItemT *item = itemOf(handle, g, i); + + return (item && pmItemMatches(item, filter ? filter : "")) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellItemMove(int32_t handle, int32_t g, int32_t i, int32_t dst) { + PmModelT *m = modelOf(handle); + + return m ? pmItemMove(m, g, i, dst) : BAS_NO_INDEX; +} + + +void ShellItemPlace(int32_t handle, int32_t g, int32_t i, int32_t col, int32_t row) { + PmGroupT *grp = groupOf(handle, g, NULL); + + if (grp && i >= 0 && i < (int32_t)arrlen(grp->items) && col >= 0 && row >= 0) { + pmItemPlace(grp, i, col, row); + } +} + + +// Move item from to position to in the group's order; the new index, or -1. +int32_t ShellItemReorder(int32_t handle, int32_t g, int32_t from, int32_t to) { + PmGroupT *grp = groupOf(handle, g, NULL); + int32_t count = grp ? (int32_t)arrlen(grp->items) : 0; + + if (!grp || grp->automatic || from < 0 || from >= count || to < 0 || to >= count) { + return BAS_NO_INDEX; + } + + return pmItemReorder(grp, from, to); +} + + +int32_t ShellItemSet(int32_t handle, int32_t g, int32_t i, const char *field, const char *value) { + PmGroupT *grp = groupOf(handle, g, NULL); + PmItemT *item = itemOf(handle, g, i); + + if (!grp || grp->automatic || !item || !field) { + return BAS_FALSE; + } + + return pmItemSetField(item, field, value) ? BAS_TRUE : BAS_FALSE; +} + + +static const char *textOf(const char *s) { + arrfree(sText); // a new result: the last one is gone + + for (const char *c = s; *c; c++) { + arrput(sText, *c); + } + + arrput(sText, '\0'); + return sText; +} diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c index 5f16662c..331c4694 100644 --- a/src/libs/kpunch/dvxshell/shellApp.c +++ b/src/libs/kpunch/dvxshell/shellApp.c @@ -85,13 +85,16 @@ static ShellAppCleanupFnT *sCleanupFns = NULL; static int32_t allocSlot(void); static int32_t appCodeDepth(const ShellAppT *app); static bool appCodeMayBeLive(const AppContextT *ctx, const ShellAppT *app); +static bool appCounted(const ShellAppT *app); static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId); static int32_t appTaskDispatchShare(const ShellAppT *app); static void appTaskWrapper(void *arg); +static WindowT *appTopWindow(const AppContextT *ctx, int32_t appId); static void cleanupTempFile(ShellAppT *app); static int32_t copyFile(const char *src, const char *dst); static void defaultHelpFile(DxeAppContextT *dxeCtx); static void deferForceKill(AppContextT *ctx, ShellAppT *app); +static void deliverNewArgs(AppContextT *ctx, ShellAppT *app, const char *args); static void deriveConfigDir(DxeAppContextT *dxeCtx, const char *path); static void destroyAppWindows(AppContextT *ctx, int32_t appId); static ShellAppT *findLoadedPath(const char *path); @@ -103,6 +106,7 @@ static bool pathCharsMatch(char a, char b); static const char *pathUnderDir(const char *path, const char *dir); static int32_t refuseSecondCopy(AppContextT *ctx, const char *path); static void releaseAppResources(ShellAppT *app); +bool shellActivateApp(AppContextT *ctx, int32_t appId); void shellAppCallEnter(ShellAppT *app); void shellAppCallLeave(ShellAppT *app); void shellAppCodeForgetTask(uint32_t taskId); @@ -121,11 +125,13 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, cons int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args); static int32_t shellLoadFail(void *handle, const char *tempPath); void shellOpenHelp(AppContextT *ctx, const char *args); +void shellOpenTaskManager(AppContextT *ctx); bool shellPathsMatch(const char *a, const char *b); void shellReapApp(AppContextT *ctx, ShellAppT *app); bool shellReapApps(AppContextT *ctx); void shellRegisterAppCleanup(ShellAppCleanupFnT fn); int32_t shellRunningAppCount(void); +int32_t shellRunningAppId(int32_t index); void shellTerminateAllApps(AppContextT *ctx); void shellUnregisterAppCleanup(ShellAppCleanupFnT fn); @@ -150,9 +156,10 @@ static int32_t allocSlot(void) { return -1; } - arrput(sApps, newSlot); + int32_t id = (int32_t)arrlen(sApps); - return arrlen(sApps) - 1; + arrput(sApps, newSlot); + return id; } @@ -192,6 +199,13 @@ static bool appCodeMayBeLive(const AppContextT *ctx, const ShellAppT *app) { } +// The single definition of "a running app" that shellRunningAppCount and +// shellRunningAppId count: started or starting, not on its way out. +static bool appCounted(const ShellAppT *app) { + return app->state == AppStateRunningE || app->state == AppStateLoadedE; +} + + // Grows the app's per-task call table on demand (zero-filled). static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId) { while (arrlen(app->taskCodeDepth) <= (ptrdiff_t)taskId) { @@ -243,6 +257,22 @@ static void appTaskWrapper(void *arg) { } +// The app's topmost visible window (highest Z first) that is not being +// destroyed, or NULL. Hidden windows (e.g. a hidden tool window) are +// skipped. +static WindowT *appTopWindow(const AppContextT *ctx, int32_t appId) { + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + WindowT *win = ctx->stack.windows[i]; + + if (win->appId == appId && win->visible && !win->destroyPending) { + return win; + } + } + + return NULL; +} + + // Delete the temp file for a multi-instance app copy, if one exists. static void cleanupTempFile(ShellAppT *app) { if (app->tempPath[0] != '\0') { @@ -330,6 +360,24 @@ static void deferForceKill(AppContextT *ctx, ShellAppT *app) { } +// Hands new launch arguments to a running app that takes them +// (onNewArgs) and raises its top window. The callback runs as that app +// (currentAppId) so anything it allocates is attributed to it. +static void deliverNewArgs(AppContextT *ctx, ShellAppT *app, const char *args) { + int32_t prevAppId = ctx->currentAppId; + + ctx->currentAppId = app->appId; + SHELL_APP_CALL(app, app->dxeCtx->onNewArgs(app->dxeCtx->newArgsCtx, args)); + ctx->currentAppId = prevAppId; + + WindowT *top = appTopWindow(ctx, app->appId); + + if (top) { + dvxRaiseWindow(ctx, top); + } +} + + // Derive the app's writable config directory by mirroring its directory // under CONFIG/: "APPS/KPUNCH/DVXBASIC" -> "CONFIG/APPS/KPUNCH/DVXBASIC". // An absolute path (e.g. from the Run... file dialog) is first made @@ -592,6 +640,26 @@ static void releaseAppResources(ShellAppT *app) { } +// The app's topmost visible window (appTopWindow) is restored if +// minimized, else raised and focused. False when it has none. +bool shellActivateApp(AppContextT *ctx, int32_t appId) { + WindowT *win = appTopWindow(ctx, appId); + + if (!win) { + return false; + } + + // dvxRestoreWindow would also un-maximize; only un-minimize. + if (win->minimized) { + dvxRestoreWindow(ctx, win); + } else { + dvxRaiseWindow(ctx, win); + } + + return true; +} + + void shellAppCallEnter(ShellAppT *app) { (*appTaskCodeSlot(app, tsCurrentId()))++; } @@ -732,6 +800,13 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch // Read multiInstance from the already-loaded descriptor AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, DVX_SYM("appDescriptor")); + // A running single-copy app that takes new arguments gets them + // instead of a second copy + if ((!existDesc || !existDesc->multiInstance) && existing->state == AppStateRunningE && existing->dxeCtx && existing->dxeCtx->onNewArgs) { + deliverNewArgs(ctx, existing, args ? args : ""); + return existing->appId; + } + if (!existDesc || !existDesc->multiInstance) { return refuseSecondCopy(ctx, path); } @@ -872,7 +947,11 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch } // Launch. Set currentAppId before any app code runs so that - // dvxCreateWindow stamps the correct owner on new windows. + // dvxCreateWindow stamps the correct owner on new windows; the program + // that asked for the start (0 for the shell) is "the running program" + // again afterwards. + int32_t callerAppId = ctx->currentAppId; + ctx->currentAppId = id; if (desc->hasMainLoop) { @@ -882,7 +961,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch int32_t taskId = tsCreate(app->name, appTaskWrapper, (void *)(intptr_t)id, stackSize, priority); if (taskId < 0) { - ctx->currentAppId = 0; + ctx->currentAppId = callerAppId; dvxSetBusy(ctx, false); dvxErrorBox(ctx, NULL, "Failed to create task for application."); free(app->dxeCtx); @@ -906,14 +985,14 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch // End Task from inside appMain (e.g. from a dialog it opened): // the kill was deferred and shellReapApps now owns the slot. if (app->killPending) { - ctx->currentAppId = 0; + ctx->currentAppId = callerAppId; dvxSetBusy(ctx, false); return -1; } if (entryResult != 0) { dvxLog("Shell: '%s' appMain returned %ld, unloading", app->name, (long)entryResult); - ctx->currentAppId = 0; + ctx->currentAppId = callerAppId; dvxSetBusy(ctx, false); forceKillNow(ctx, app); return -1; @@ -923,7 +1002,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch // The slot pointer is stable across the callback-only entry point above // (which can pump dvxUpdate and yield to other app loads), so no // re-resolve is needed. - ctx->currentAppId = 0; + ctx->currentAppId = callerAppId; app->state = AppStateRunningE; dvxLog("Shell: loaded '%s' (id=%ld, mainLoop=%s, entry=0x%08lx, desc=0x%08lx)", app->name, (long)id, app->hasMainLoop ? "yes" : "no", (unsigned long)entry, (unsigned long)desc); @@ -958,8 +1037,7 @@ static int32_t shellLoadFail(void *handle, const char *tempPath) { // F1 and application Help menus. Empty args (an app with no help file) -// open the system help. The viewer's onNewArgs runs as that app -// (currentAppId) so anything it allocates is attributed to it. +// open the system help, in the running viewer when there is one. void shellOpenHelp(AppContextT *ctx, const char *args) { ShellAppT *viewer = findRunningPath(DVX_HELP_VIEWER_APP_LITERAL); @@ -972,19 +1050,13 @@ void shellOpenHelp(AppContextT *ctx, const char *args) { return; } - int32_t prevAppId = ctx->currentAppId; + deliverNewArgs(ctx, viewer, args); +} - ctx->currentAppId = viewer->appId; - SHELL_APP_CALL(viewer, viewer->dxeCtx->onNewArgs(viewer->dxeCtx->newArgsCtx, args)); - ctx->currentAppId = prevAppId; - for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { - WindowT *win = ctx->stack.windows[i]; - - if (win->appId == viewer->appId && win->visible && !win->destroyPending) { - dvxRaiseWindow(ctx, win); - break; - } +void shellOpenTaskManager(AppContextT *ctx) { + if (shellCtrlEscFn) { + shellCtrlEscFn(ctx); } } @@ -1114,7 +1186,7 @@ int32_t shellRunningAppCount(void) { int32_t count = 0; for (int32_t i = 1; i < arrlen(sApps); i++) { - if (sApps[i]->state == AppStateRunningE || sApps[i]->state == AppStateLoadedE) { + if (appCounted(sApps[i])) { count++; } } @@ -1123,6 +1195,23 @@ int32_t shellRunningAppCount(void) { } +int32_t shellRunningAppId(int32_t index) { + int32_t seen = 0; + + for (int32_t i = 1; index >= 0 && i < arrlen(sApps); i++) { + if (appCounted(sApps[i])) { + if (seen == index) { + return i; + } + + seen++; + } + } + + return 0; +} + + // Shell shutdown: the main loop has exited, so every app is released now. void shellTerminateAllApps(AppContextT *ctx) { for (int32_t i = 1; i < arrlen(sApps); i++) { diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h index 78c60e79..1ab198a3 100644 --- a/src/libs/kpunch/dvxshell/shellApp.h +++ b/src/libs/kpunch/dvxshell/shellApp.h @@ -84,6 +84,10 @@ typedef struct { int32_t priority; // TS_PRIORITY_LOW..TS_PRIORITY_HIGH (anything else fails to launch) } AppDescriptorT; +// The descriptor an app defines (SHELL_APP_DESCRIPTOR) and the shell +// finds by name; declared here so every definition has a declaration. +extern AppDescriptorT appDescriptor; + // Defines the app's exported descriptor from designated initializers: // SHELL_APP_DESCRIPTOR( // .name = "Clock", @@ -204,7 +208,9 @@ bool shellPathsMatch(const char *a, const char *b); int32_t shellLoadApp(AppContextT *ctx, const char *path); // Load and run an app with arguments. The args string is copied into -// DxeAppContextT.args before appMain is called. +// DxeAppContextT.args before appMain is called. A single-instance app +// that is already running and sets onNewArgs is handed args and raised +// instead (its app ID is returned). int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args); // Reap finished apps (call each frame from main loop). Returns true if @@ -260,10 +266,24 @@ void shellTerminateAllApps(AppContextT *ctx); // Pumps the event loop, so call it only from the shell's top level. bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId); +// Ask one app to close the way shellCloseAllApps asks each (its save +// prompt runs; one that does not respond is offered to be ended). True +// when it is gone or was not running; false when it kept a window or a +// dialog box is open. Pumps the event loop: shell top level only. +bool shellCloseApp(AppContextT *ctx, int32_t appId); + +// Request that one app be asked to close (BASIC's ShellRunningClose). +// Only recorded; the main loop acts on it through shellRunCloseRequests. +void shellRequestAppClose(int32_t appId); + // Request Exit DVX (Program Manager's Exit). Only recorded; the shell's // main loop acts on it through shellRunExitRequest. void shellRequestExit(void); +// Main-loop step: ask every app shellRequestAppClose named to close +// (shellCloseApp), oldest request first. +void shellRunCloseRequests(AppContextT *ctx); + // Main-loop step: if an exit was requested, run shellCloseAllApps for // every app but keepAppId (the desktop) and dvxQuit when none refused. void shellRunExitRequest(AppContextT *ctx, int32_t keepAppId); @@ -271,6 +291,11 @@ void shellRunExitRequest(AppContextT *ctx, int32_t keepAppId); // Get app slot by ID (returns NULL if invalid/free) ShellAppT *shellGetApp(int32_t appId); +// Bring an app to the front, as Task Manager's Switch To does: its topmost +// visible window is restored when minimized, else raised, and focused. +// False when it has no visible window. +bool shellActivateApp(AppContextT *ctx, int32_t appId); + // Get an active app slot loaded from this path (shellPathsMatch), or NULL. ShellAppT *shellAppFindByPath(const char *path); @@ -293,6 +318,10 @@ AppContextT *shellContext(void); // Count running apps (not counting the shell itself) int32_t shellRunningAppCount(void); +// The id of running app index (0 .. shellRunningAppCount() - 1, slot +// order), 0 out of range. +int32_t shellRunningAppId(int32_t index); + // Ensure an app's config directory exists (creates all parent dirs). // Returns 0 on success, -1 on failure. int32_t shellEnsureConfigDir(const DxeAppContextT *ctx); @@ -327,8 +356,14 @@ void shellRegisterDesktopUpdate(void (*updateFn)(void)); void shellUnregisterDesktopUpdate(void (*updateFn)(void)); // Notify the desktop app that app state has changed (load, reap, crash). +// Also counts the change (shellDesktopChangeCount). void shellDesktopUpdate(void); +// How many times shellDesktopUpdate has run (wraps around): a program +// that cannot take a callback (BASIC) polls it and refreshes when it +// differs from the value it saw last. +int32_t shellDesktopChangeCount(void); + // Register an idle handler called every idle frame by the shell main loop, // regardless of which app is foreground. Serial/secLink pollers register // here so background connections keep pumping. Idempotent on the (fn, ctx) @@ -371,4 +406,235 @@ void shellUnregisterAppCleanup(ShellAppCleanupFnT fn); // calls this on Ctrl+Esc. NULL if taskmgr is not loaded. extern void (*shellCtrlEscFn)(AppContextT *ctx); +// Open Task Manager (what Ctrl+Esc does); nothing when it is not loaded. +void shellOpenTaskManager(AppContextT *ctx); + +// ============================================================ +// DVX BASIC entry points (shellBas.c, src/include/basic/shell.bas) +// ============================================================ +// +// "This program" is the app whose code is running. Strings stay valid +// until the next call returning one; success is True (-1), failure 0. + +// Register keyText ("Ctrl+Alt+E") as a key working in every program: +// its id, or 0 (not a key, or the key is taken). +int32_t HotkeyAdd(const char *keyText); + +// The id of the next of this program's hotkeys pressed, 0 when none. +int32_t HotkeyNext(void); + +// Remove one of this program's hotkeys. +void HotkeyRemove(int32_t id); + +// keyText in DVX's own spelling, "" when it is not a key. +const char *HotkeyText(const char *keyText); + +// This program's id. +int32_t ShellAppId(void); + +// True when program appId is "Not responding" (dvxAppIsHung). +int32_t ShellAppIsHung(int32_t appId); + +// Bytes program appId has allocated (dvxMemGetAppUsage), 0 when it is not +// running. +int32_t ShellAppMemory(int32_t appId); + +// This program's .app path as it was started. +const char *ShellAppPath(void); + +// Argument index (0 = the program) of a command line by dvxNextArg's +// rule, "" past the last; and how many there are. +const char *ShellArg(const char *command, int32_t index); +int32_t ShellArgCount(const char *command); + +// The program (no argument template) that opens document, "" when none. +const char *ShellAssocCommand(const char *document); + +// Number of file associations. +int32_t ShellAssocCount(void); + +// Extension of association index (0-based), "" when out of range. +const char *ShellAssocExt(int32_t index); + +// extension as associations key it ("" when it cannot be one). +const char *ShellAssocNormalize(const char *extension); + +// The program set to open files with this extension ("" when none). +const char *ShellAssocProgram(const char *extension); + +// Remove an association (saved at once). +int32_t ShellAssocRemove(const char *extension); + +// Add or replace an association (saved at once). +int32_t ShellAssocSet(const char *extension, const char *value); + +// Stored value (program[,args]) of association index, "" when out of +// range. +const char *ShellAssocValue(int32_t index); + +// shellDesktopChangeCount. +int32_t ShellChangeCount(void); + +// The arguments / program parts of a command line (shellSplitCommand). +const char *ShellCommandArgs(const char *command); +const char *ShellCommandProgram(const char *command); + +// fileName in this program's settings folder (made when missing; "" when +// it cannot be). +const char *ShellConfigFile(const char *fileName); + +// Exit DVX (every program is asked to close first). +void ShellExitDvx(void); + +// Copies of this program running, this one included. +int32_t ShellInstanceCount(void); + +// True when path names a program. +int32_t ShellIsProgram(const char *path); + +// The file a Program Manager item's icon comes from (shellItemIconFile; +// "" when none). +const char *ShellItemIcon(const char *iconPath, const char *path); + +// One line in DVX.LOG. +void ShellLog(const char *text); + +// Run Minimized for a program just started (shellMinimizeApp). +void ShellMinimizeApp(int32_t appId); + +// Open a document in the program set for its type. +int32_t ShellOpenDocument(const char *path); + +// This program's task priority (TS_PRIORITY_*), -1 without a task. +int32_t ShellPriority(void); + +// Programs in a list from ShellProgramScan (0 for list 0). +int32_t ShellProgramCount(int32_t list); + +// A program's description resource ("" when none). +const char *ShellProgramDescription(const char *path); + +// Free a list from ShellProgramScan. +void ShellProgramFree(int32_t list); + +// A program's display name (its file name when it has none). +const char *ShellProgramName(const char *path); + +// Path of entry index (0-based) of a list, "" when out of range. +const char *ShellProgramPath(int32_t list, int32_t index); + +// Every program under folder ("" = APPS) except skipPath ("" = none): a +// list handle owned by this program, 0 when none were found. +int32_t ShellProgramScan(const char *folder, const char *skipPath); + +// The installed programs' folder (SHELL_PROG_DIR). +const char *ShellProgramsFolder(void); + +// Start a program with arguments ("" for none). +int32_t ShellRun(const char *program, const char *args); + +// Run a command line: the new program's id, or 0. +int32_t ShellRunCommand(const char *command); + +// The Run dialog over history (newest first, one per line): the command +// chosen, "" when cancelled. +const char *ShellRunDialog(const char *history); + +// Bring a running program to the front (shellActivateApp). +int32_t ShellRunningActivate(int32_t appId); + +// Ask a running program to close (shellRequestAppClose). +int32_t ShellRunningClose(int32_t appId); + +// Programs running (shellRunningAppCount). +int32_t ShellRunningCount(void); + +// Id of a running copy of program, 0 when none. +int32_t ShellRunningFind(const char *program); + +// Id of running program index (shellRunningAppId), 0 out of range. +int32_t ShellRunningId(int32_t index); + +// End a running program at once (shellForceKillApp); not this program. +int32_t ShellRunningKill(int32_t appId); + +// Name / .app path of a running program, "" when not running. +const char *ShellRunningName(int32_t appId); +const char *ShellRunningPath(int32_t appId); + +// shellRunProgram: the new program's id, or 0. +int32_t ShellRunProgram(const char *program, const char *args, const char *folder); + +// DVX.INI [shell] desktop = program ("" removes it). +int32_t ShellSetDesktop(const char *program); + +// This program's task priority (tsSetPriority); True when set. +int32_t ShellSetPriority(int32_t priority); + +// DVX.INI [shell] welcome = program ("" removes it). +int32_t ShellSetWelcome(const char *program); + +// The system information text ("" when none). +const char *ShellSystemInfo(void); + +// Open Task Manager. +void ShellTaskManager(void); + +// ============================================================ +// DVX BASIC system settings (shellSysBas.c, src/include/basic/system.bas) +// ============================================================ +// +// Colors are BASIC RGB() values; lists are 0-based; prefs arguments are +// prefs.bas handles (prefsBasHandle); the SysFont* functions read the +// system font (dvxGetFont). SysMouseX / Y / Buttons and SysShiftState +// read the input state the context holds; SysSendKey queues keystrokes +// typed one per idle frame (platformInjectKey); SysSetMousePos warps the +// pointer (platformMouseWarp); SysScreenshot is dvxScreenshot; +// SysMemory*Kb are platformGetMemoryInfo (-1 when unknown). + +int32_t SysColorCount(void); +int32_t SysColorId(const char *name); +const char *SysColorLabel(int32_t id); +const char *SysColorName(int32_t id); +int32_t SysColorsLoad(int32_t prefs); +void SysColorsStore(int32_t prefs); +int32_t SysFontCharCount(void); +int32_t SysFontFirstChar(void); +int32_t SysFontGlyphRow(int32_t code, int32_t row); +int32_t SysFontHeight(void); +int32_t SysFontWidth(void); +int32_t SysGetColor(int32_t id); +int32_t SysLoadTheme(const char *path); +int32_t SysMemoryFreeKb(void); +int32_t SysMemoryTotalKb(void); +int32_t SysMouseAccel(void); +int32_t SysMouseButtons(void); +int32_t SysMouseDoubleClick(void); +void SysMouseLoad(int32_t prefs); +int32_t SysMouseMickeys(void); +void SysMouseStore(int32_t prefs); +int32_t SysMouseWheelDir(void); +int32_t SysMouseWheelStep(void); +int32_t SysMouseX(void); +int32_t SysMouseY(void); +void SysResetColors(void); +int32_t SysSaveTheme(const char *path); +int32_t SysScreenshot(const char *path); +int32_t SysSendKey(int32_t keyCode, int32_t shift); +void SysSetColor(int32_t id, int32_t rgb); +void SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep); +void SysSetMousePos(int32_t x, int32_t y); +int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp); +int32_t SysSetWallpaper(const char *path); +void SysSetWallpaperMode(int32_t mode); +int32_t SysShiftState(void); +int32_t SysVideoModeBpp(int32_t index); +int32_t SysVideoModeCount(void); +int32_t SysVideoModeHeight(int32_t index); +int32_t SysVideoModeWidth(int32_t index); +const char *SysWallpaper(void); +int32_t SysWallpaperLoad(int32_t prefs); +int32_t SysWallpaperMode(void); +void SysWallpaperStore(int32_t prefs); + #endif // SHELL_APP_H diff --git a/src/libs/kpunch/dvxshell/shellBas.c b/src/libs/kpunch/dvxshell/shellBas.c new file mode 100644 index 00000000..b7f3a12a --- /dev/null +++ b/src/libs/kpunch/dvxshell/shellBas.c @@ -0,0 +1,882 @@ +// 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. + +// shellBas.c -- DVX BASIC entry points of the shell (shell.bas) +// +// Called through DECLARE LIBRARY "dvxshell", so the signatures use only +// BASIC-marshalable types: strings and LONG in, LONG or STRING out, True +// (-1) / False (0) for success. "This program" is the app whose code is +// running (ctx->currentAppId, which the shell keeps for every app's task +// and every window callback). A returned string stays valid until the +// next call returning one (the BASIC runtime copies it at once). + +#include "shellApp.h" +#include "shellAssoc.h" +#include "shellInf.h" +#include "shellProg.h" +#include "shellRun.h" +#include "dvxMem.h" +#include "dvxPlat.h" +#include "dvxPrefs.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define BAS_HISTORY_SEP '\n' +#define BAS_LINE_END '\r' +#define HOTKEY_QUEUE_MAX 16 // presses kept per program that has not asked yet +#define ACCEL_TEXT_MAX 32 +#define ARG_NONE (-1) // argWalk: only count +#define PRIORITY_NONE (-1) // ShellPriority without a task of its own + +// A program list from ShellProgramScan and the program it belongs to. +typedef struct { + ShellProgramT *list; // stb_ds; NULL with count 0 when the slot is free + int32_t count; + int32_t appId; + bool used; +} BasProgListT; + +// A hotkey from HotkeyAdd: the id BASIC sees, the library's id, the owner. +typedef struct { + int32_t id; + int32_t dvxId; + int32_t appId; +} BasHotkeyT; + +// One press not yet collected by HotkeyNext. +typedef struct { + int32_t id; + int32_t appId; +} BasHotkeyPressT; + +// ============================================================ +// Module state +// ============================================================ + +static BasProgListT *sProgLists = NULL; // stb_ds; handle = index + 1 +static BasHotkeyT *sHotkeys = NULL; // stb_ds +static BasHotkeyPressT *sPresses = NULL; // stb_ds, oldest first +static int32_t sLastHotkeyId = 0; + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static int32_t argWalk(const char *command, int32_t index, char *out, int32_t outSize); +static const ShellAppT *currentApp(void); +static int32_t currentAppId(void); +int32_t HotkeyAdd(const char *keyText); +static void hotkeyDrop(int32_t index); +static int32_t hotkeyFind(int32_t id); +int32_t HotkeyNext(void); +static void hotkeyPressed(void *userCtx); +void HotkeyRemove(int32_t id); +const char *HotkeyText(const char *keyText); +static const ShellAppT *mainTaskApp(void); +static const char *orEmpty(const char *text); +static BasProgListT *progListOf(int32_t handle); +static ShellAppT *runningApp(int32_t appId); +static int32_t setShellKey(const char *key, const char *program); +int32_t ShellAppId(void); +int32_t ShellAppIsHung(int32_t appId); +int32_t ShellAppMemory(int32_t appId); +const char *ShellAppPath(void); +const char *ShellArg(const char *command, int32_t index); +int32_t ShellArgCount(const char *command); +const char *ShellAssocCommand(const char *document); +int32_t ShellAssocCount(void); +const char *ShellAssocExt(int32_t index); +const char *ShellAssocNormalize(const char *extension); +const char *ShellAssocProgram(const char *extension); +int32_t ShellAssocRemove(const char *extension); +int32_t ShellAssocSet(const char *extension, const char *value); +const char *ShellAssocValue(int32_t index); +int32_t ShellChangeCount(void); +const char *ShellCommandArgs(const char *command); +const char *ShellCommandProgram(const char *command); +const char *ShellConfigFile(const char *fileName); +void ShellExitDvx(void); +int32_t ShellInstanceCount(void); +int32_t ShellIsProgram(const char *path); +const char *ShellItemIcon(const char *iconPath, const char *path); +void ShellLog(const char *text); +void ShellMinimizeApp(int32_t appId); +int32_t ShellOpenDocument(const char *path); +int32_t ShellPriority(void); +int32_t ShellProgramCount(int32_t list); +const char *ShellProgramDescription(const char *path); +void ShellProgramFree(int32_t list); +const char *ShellProgramName(const char *path); +const char *ShellProgramPath(int32_t list, int32_t index); +int32_t ShellProgramScan(const char *folder, const char *skipPath); +const char *ShellProgramsFolder(void); +int32_t ShellRun(const char *program, const char *args); +int32_t ShellRunCommand(const char *command); +const char *ShellRunDialog(const char *history); +int32_t ShellRunningActivate(int32_t appId); +int32_t ShellRunningClose(int32_t appId); +int32_t ShellRunningCount(void); +int32_t ShellRunningFind(const char *program); +int32_t ShellRunningId(int32_t index); +int32_t ShellRunningKill(int32_t appId); +const char *ShellRunningName(int32_t appId); +const char *ShellRunningPath(int32_t appId); +int32_t ShellRunProgram(const char *program, const char *args, const char *folder); +int32_t ShellSetDesktop(const char *program); +int32_t ShellSetPriority(int32_t priority); +int32_t ShellSetWelcome(const char *program); +const char *ShellSystemInfo(void); +void ShellTaskManager(void); + + +// Shell app-cleanup hook: free what a program that ended left behind. Its +// hotkeys themselves are already gone (dvxUnregisterAppHotkeys). +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sProgLists); i++) { + if (sProgLists[i].used && sProgLists[i].appId == appId) { + ShellProgramFree(i + 1); + } + } + + for (int32_t i = (int32_t)arrlen(sHotkeys) - 1; i >= 0; i--) { + if (sHotkeys[i].appId == appId) { + hotkeyDrop(i); + } + } +} + + +// Walk command's arguments by DVX's rule (dvxNextArg: blanks between, +// double quotes around one with blanks): argument index (0 = the program) +// into out ("" when there is none), and the count as the result. +static int32_t argWalk(const char *command, int32_t index, char *out, int32_t outSize) { + char arg[SHELL_ARGS_MAX]; + bool quoted = false; + int32_t count = 0; + const char *p = dvxNextArg(orEmpty(command), arg, (int32_t)sizeof(arg), "ed); + + out[0] = '\0'; + + while (p) { + if (count == index) { + snprintf(out, (size_t)outSize, "%s", arg); + } + + count++; + p = dvxNextArg(p, arg, (int32_t)sizeof(arg), "ed); + } + + return count; +} + + +// The app whose code is running, or NULL. +static const ShellAppT *currentApp(void) { + return shellGetApp(currentAppId()); +} + + +static int32_t currentAppId(void) { + AppContextT *ctx = shellContext(); + + return ctx ? ctx->currentAppId : 0; +} + + +// A system-wide hotkey owned by the running program (dvxRegisterHotkey +// gives it to ctx->currentAppId as well). +int32_t HotkeyAdd(const char *keyText) { + int32_t key = 0; + int32_t mods = 0; + + if (!dvxAccelParse(orEmpty(keyText), &key, &mods)) { + return BAS_NO_HANDLE; + } + + int32_t id = sLastHotkeyId + 1; + int32_t dvxId = dvxRegisterHotkey(shellContext(), key, mods, hotkeyPressed, (void *)(intptr_t)id); + + if (dvxId == DVX_HOTKEY_NONE) { + return BAS_NO_HANDLE; + } + + BasHotkeyT hotkey = { id, dvxId, currentAppId() }; + + sLastHotkeyId = id; + shellRegisterAppCleanup(appEnded); + arrput(sHotkeys, hotkey); + return id; +} + + +// Forget hotkey record index and its presses not collected yet. +static void hotkeyDrop(int32_t index) { + int32_t id = sHotkeys[index].id; + + for (int32_t i = (int32_t)arrlen(sPresses) - 1; i >= 0; i--) { + if (sPresses[i].id == id) { + arrdel(sPresses, i); + } + } + + arrdel(sHotkeys, index); +} + + +// Index of the running program's hotkey id, or -1. +static int32_t hotkeyFind(int32_t id) { + int32_t appId = currentAppId(); + + for (int32_t i = 0; i < (int32_t)arrlen(sHotkeys); i++) { + if (sHotkeys[i].id == id && sHotkeys[i].appId == appId) { + return i; + } + } + + return -1; +} + + +// The oldest press of one of the running program's hotkeys, 0 when none. +int32_t HotkeyNext(void) { + int32_t appId = currentAppId(); + + for (int32_t i = 0; i < (int32_t)arrlen(sPresses); i++) { + if (sPresses[i].appId == appId) { + int32_t id = sPresses[i].id; + + arrdel(sPresses, i); + return id; + } + } + + return 0; +} + + +// The library's hotkey callback: queue the press for its program, which +// collects it with HotkeyNext (a BASIC program cannot be called back). +static void hotkeyPressed(void *userCtx) { + int32_t id = (int32_t)(intptr_t)userCtx; + int32_t appId = -1; + int32_t pending = 0; + + for (int32_t i = 0; i < (int32_t)arrlen(sHotkeys) && appId < 0; i++) { + if (sHotkeys[i].id == id) { + appId = sHotkeys[i].appId; + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(sPresses); i++) { + if (sPresses[i].appId == appId) { + pending++; + } + } + + if (appId >= 0 && pending < HOTKEY_QUEUE_MAX) { + BasHotkeyPressT press = { id, appId }; + + arrput(sPresses, press); + } +} + + +// Only the program's own hotkeys; anything else is ignored. +void HotkeyRemove(int32_t id) { + int32_t index = hotkeyFind(id); + + if (index >= 0) { + dvxUnregisterHotkey(shellContext(), sHotkeys[index].dvxId); + hotkeyDrop(index); + } +} + + +// keyText read and written back by the same code menus use. +const char *HotkeyText(const char *keyText) { + static char text[ACCEL_TEXT_MAX]; + int32_t key = 0; + int32_t mods = 0; + + text[0] = '\0'; + + if (dvxAccelParse(orEmpty(keyText), &key, &mods)) { + dvxAccelFormat(key, mods, text, sizeof(text)); + } + + return text; +} + + +// This program when it runs as a task of its own (every BASIC program +// does), else NULL. +static const ShellAppT *mainTaskApp(void) { + const ShellAppT *app = currentApp(); + + return (app && app->hasMainLoop && app->mainTaskId != TS_MAIN_TASK_ID) ? app : NULL; +} + + +static const char *orEmpty(const char *text) { + return text ? text : ""; +} + + +static BasProgListT *progListOf(int32_t handle) { + if (handle < 1 || handle > (int32_t)arrlen(sProgLists) || !sProgLists[handle - 1].used) { + return NULL; + } + + return &sProgLists[handle - 1]; +} + + +// The app appId when it is running (not starting or on its way out). +static ShellAppT *runningApp(int32_t appId) { + ShellAppT *app = shellGetApp(appId); + + return (app && app->state == AppStateRunningE) ? app : NULL; +} + + +// DVX.INI [shell] key = program; "" removes the key. True when DVX.INI +// was saved (a failure is logged). +static int32_t setShellKey(const char *key, const char *program) { + PrefsHandleT *ini = prefsLoad(DVX_INI_PATH); + + if (!ini) { + return BAS_FALSE; + } + + if (program && program[0]) { + prefsSetString(ini, SHELL_INI_SECTION, key, program); + } else { + prefsRemove(ini, SHELL_INI_SECTION, key); + } + + bool saved = prefsSave(ini); + + if (!saved) { + dvxLog("Shell: could not save %s", DVX_INI_PATH); + } + + prefsClose(ini); + return saved ? BAS_TRUE : BAS_FALSE; +} + + +// This program's id (as ShellRunProgram returns for a program it starts). +int32_t ShellAppId(void) { + return currentAppId(); +} + + +// True when the program has been inside one of its event handlers for +// DVX_HUNG_APP_MS or longer ("Not responding" in Task Manager). +int32_t ShellAppIsHung(int32_t appId) { + return (runningApp(appId) && dvxAppIsHung(shellContext(), appId)) ? BAS_TRUE : BAS_FALSE; +} + + +// Bytes the program has allocated (dvxMemGetAppUsage), 0 when it is not +// running. +int32_t ShellAppMemory(int32_t appId) { + if (!runningApp(appId)) { + return 0; + } + + uint32_t bytes = dvxMemGetAppUsage(appId); + + return bytes > INT32_MAX ? INT32_MAX : (int32_t)bytes; +} + + +// This program's .app path as it was started ("" when unknown). +const char *ShellAppPath(void) { + const ShellAppT *app = currentApp(); + + return app ? app->path : ""; +} + + +// Argument index of a command line (0 = the program), without its quotes; +// "" past the last. +const char *ShellArg(const char *command, int32_t index) { + static char arg[SHELL_ARGS_MAX]; + + argWalk(command, index, arg, (int32_t)sizeof(arg)); + return arg; +} + + +// How many arguments a command line has, the program included. +int32_t ShellArgCount(const char *command) { + char arg[SHELL_ARGS_MAX]; + + return argWalk(command, ARG_NONE, arg, (int32_t)sizeof(arg)); +} + + +// The program that opens document, without the association's argument +// template ("" when none). +const char *ShellAssocCommand(const char *document) { + static char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; + + if (!shellAssocCommand(document, program, sizeof(program), args, sizeof(args))) { + program[0] = '\0'; + } + + return program; +} + + +int32_t ShellAssocCount(void) { + return shellAssocCount(); +} + + +const char *ShellAssocExt(int32_t index) { + static char extension[SHELL_ASSOC_EXT_MAX]; + char value[DVX_MAX_PATH + SHELL_ARGS_MAX]; + + if (!shellAssocGet(index, extension, sizeof(extension), value, sizeof(value))) { + extension[0] = '\0'; + } + + return extension; +} + + +const char *ShellAssocNormalize(const char *extension) { + static char normal[SHELL_ASSOC_EXT_MAX]; + + if (!shellAssocNormalize(extension, normal)) { + normal[0] = '\0'; + } + + return normal; +} + + +// The program set to open files with this extension ("" when none). +const char *ShellAssocProgram(const char *extension) { + static char program[DVX_MAX_PATH]; + + if (!shellAssocLookup(extension, program, sizeof(program))) { + program[0] = '\0'; + } + + return program; +} + + +int32_t ShellAssocRemove(const char *extension) { + return shellAssocRemove(extension) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellAssocSet(const char *extension, const char *value) { + return shellAssocSet(extension, value) ? BAS_TRUE : BAS_FALSE; +} + + +const char *ShellAssocValue(int32_t index) { + static char value[DVX_MAX_PATH + SHELL_ARGS_MAX]; + char extension[SHELL_ASSOC_EXT_MAX]; + + if (!shellAssocGet(index, extension, sizeof(extension), value, sizeof(value))) { + value[0] = '\0'; + } + + return value; +} + + +int32_t ShellChangeCount(void) { + return shellDesktopChangeCount(); +} + + +// The arguments part of a command line, as ShellRunCommand passes them. +const char *ShellCommandArgs(const char *command) { + static char args[SHELL_ARGS_MAX]; + char program[DVX_MAX_PATH]; + + shellSplitCommand(orEmpty(command), program, (int32_t)sizeof(program), args, (int32_t)sizeof(args)); + return args; +} + + +// The program part of a command line, as ShellRunCommand starts it. +const char *ShellCommandProgram(const char *command) { + static char program[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; + + shellSplitCommand(orEmpty(command), program, (int32_t)sizeof(program), args, (int32_t)sizeof(args)); + return program; +} + + +// The full path of fileName in this program's settings folder (DVX's +// CONFIG folder, in a folder named after the program's path), which is +// made when it does not exist yet; "" when it cannot be made. +const char *ShellConfigFile(const char *fileName) { + static char path[DVX_MAX_PATH]; + const ShellAppT *app = currentApp(); + + if (!app || !app->dxeCtx || shellEnsureConfigDir(app->dxeCtx) != 0) { + return ""; + } + + shellConfigPath(app->dxeCtx, orEmpty(fileName), path, sizeof(path)); + return path; +} + + +void ShellExitDvx(void) { + shellRequestExit(); +} + + +// How many copies of this program are running, this one included. +int32_t ShellInstanceCount(void) { + const ShellAppT *self = currentApp(); + int32_t count = 0; + + for (int32_t i = 1; self && i < shellAppSlotCount(); i++) { + const ShellAppT *app = shellGetApp(i); + + if (app && app->state != AppStateTerminatingE && shellPathsMatch(app->path, self->path)) { + count++; + } + } + + return count; +} + + +int32_t ShellIsProgram(const char *path) { + return shellIsProgram(path) ? BAS_TRUE : BAS_FALSE; +} + + +// The file a Program Manager item's icon comes from (shellItemIconFile): +// iconPath when it is a picture, else the item's program or document, +// else the program that opens the document; "" when none gives one. +// LOADPICTURE of the file shows the icon. +const char *ShellItemIcon(const char *iconPath, const char *path) { + static char file[DVX_MAX_PATH]; + + shellItemIconFile(shellContext(), orEmpty(iconPath), orEmpty(path), file, (int32_t)sizeof(file)); + return file; +} + + +// One line in DVX.LOG. +void ShellLog(const char *text) { + dvxLog("%s", orEmpty(text)); +} + + +void ShellMinimizeApp(int32_t appId) { + shellMinimizeApp(shellContext(), appId); +} + + +// Open a document in the program set for its type. True when it opened. +int32_t ShellOpenDocument(const char *path) { + return shellAssocOpenDocument(shellContext(), path) ? BAS_TRUE : BAS_FALSE; +} + + +// This program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH), -1 when +// it has no task of its own. +int32_t ShellPriority(void) { + const ShellAppT *app = mainTaskApp(); + int32_t pri = app ? tsGetPriority(app->mainTaskId) : TS_ERR_PARAM; + + return pri >= TS_OK ? pri : PRIORITY_NONE; +} + + +int32_t ShellProgramCount(int32_t list) { + const BasProgListT *entry = progListOf(list); + + return entry ? entry->count : 0; +} + + +const char *ShellProgramDescription(const char *path) { + static ShellProgramT info; + + shellProgramInfo(orEmpty(path), &info); + return info.description; +} + + +void ShellProgramFree(int32_t list) { + BasProgListT *entry = progListOf(list); + + if (entry) { + shellProgramListFree(entry->list); + entry->list = NULL; + entry->count = 0; + entry->used = false; + } +} + + +const char *ShellProgramName(const char *path) { + static ShellProgramT info; + + shellProgramInfo(orEmpty(path), &info); + return info.name; +} + + +const char *ShellProgramPath(int32_t list, int32_t index) { + const BasProgListT *entry = progListOf(list); + + if (!entry || index < 0 || index >= entry->count) { + return ""; + } + + return entry->list[index].path; +} + + +// Paths only (no program is opened), so a long scan never holds the +// screen; ShellProgramName$ reads each name when the program wants it. +int32_t ShellProgramScan(const char *folder, const char *skipPath) { + ShellProgramT *list = NULL; + const char *dir = (folder && folder[0]) ? folder : SHELL_PROG_DIR; + const char *skip = (skipPath && skipPath[0]) ? skipPath : NULL; + int32_t count = shellProgramScan(dir, skip, false, NULL, NULL, &list); + + if (count == 0) { + return BAS_NO_HANDLE; + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sProgLists) && sProgLists[idx].used) { + idx++; + } + + if (idx == (int32_t)arrlen(sProgLists)) { + BasProgListT blank = { NULL, 0, 0, false }; + + arrput(sProgLists, blank); + } + + sProgLists[idx].list = list; + sProgLists[idx].count = count; + sProgLists[idx].appId = currentAppId(); + sProgLists[idx].used = true; + shellRegisterAppCleanup(appEnded); + return idx + 1; +} + + +// The folder holding the installed programs (relative to DVX's folder), +// where ShellProgramScan("") looks and a program Browse dialog starts. +const char *ShellProgramsFolder(void) { + return SHELL_PROG_DIR; +} + + +// Start a program with arguments ("" for none), the way ShellRunProgram +// does. True when it started. +int32_t ShellRun(const char *program, const char *args) { + return ShellRunProgram(program, args, "") > 0 ? BAS_TRUE : BAS_FALSE; +} + + +int32_t ShellRunCommand(const char *command) { + int32_t id = shellRunCommand(shellContext(), command); + + return id > 0 ? id : 0; +} + + +// history holds the remembered commands newest first, one per line. +const char *ShellRunDialog(const char *history) { + static char command[SHELL_RUN_CMD_MAX]; + char *copy = strdup(orEmpty(history)); + const char **lines = NULL; + + command[0] = '\0'; + + for (char *line = copy; line && *line;) { + char *end = strchr(line, BAS_HISTORY_SEP); + + if (end) { + *end = '\0'; + } + + char *cr = strchr(line, BAS_LINE_END); + + if (cr) { + *cr = '\0'; + } + + if (line[0]) { + arrput(lines, line); + } + + line = end ? end + 1 : NULL; + } + + if (!shellRunDialog(shellContext(), lines, (int32_t)arrlen(lines), command, sizeof(command))) { + command[0] = '\0'; + } + + arrfree(lines); + free(copy); + return command; +} + + +// Bring a program to the front (Task Manager's Switch To). True when it +// has a window to show. +int32_t ShellRunningActivate(int32_t appId) { + return (runningApp(appId) && shellActivateApp(shellContext(), appId)) ? BAS_TRUE : BAS_FALSE; +} + + +// Ask a program to close as if the user closed its windows (its save +// prompt runs). Only recorded: DVX asks it once this program's code has +// returned (shellRequestAppClose). True when the request was made. +int32_t ShellRunningClose(int32_t appId) { + if (!runningApp(appId)) { + return BAS_FALSE; + } + + shellRequestAppClose(appId); + return BAS_TRUE; +} + + +int32_t ShellRunningCount(void) { + return shellRunningAppCount(); +} + + +// The id of a running copy of program (its .app path, matched as the +// shell matches paths), 0 when none runs. +int32_t ShellRunningFind(const char *program) { + const ShellAppT *app = shellAppFindByPath(orEmpty(program)); + + return (app && runningApp(app->appId)) ? app->appId : 0; +} + + +// The id of running program index (0 .. ShellRunningCount - 1), 0 out of +// range. +int32_t ShellRunningId(int32_t index) { + return shellRunningAppId(index); +} + + +// End a program at once, without asking it (Task Manager's End Task): +// work it has not saved is lost. This program cannot end itself this way +// (END does that). True when it was ended. +int32_t ShellRunningKill(int32_t appId) { + ShellAppT *app = runningApp(appId); + + if (!app || appId == currentAppId()) { + return BAS_FALSE; + } + + shellForceKillApp(shellContext(), app); + shellDesktopUpdate(); + return BAS_TRUE; +} + + +// The name a running program shows, "" for no such program. +const char *ShellRunningName(int32_t appId) { + const ShellAppT *app = runningApp(appId); + + return app ? app->name : ""; +} + + +// The .app path a running program was started from, "" for no such +// program. +const char *ShellRunningPath(int32_t appId) { + const ShellAppT *app = runningApp(appId); + + return app ? app->path : ""; +} + + +// A program, or a document opened by its file type, with file names in +// args found in folder ("" for none). The new program's id, or 0 (DVX +// has said why). +int32_t ShellRunProgram(const char *program, const char *args, const char *folder) { + int32_t id = shellRunProgram(shellContext(), program, args, folder); + + return id > 0 ? id : 0; +} + + +// The desktop DVX starts the next time (DVX.INI [shell] desktop); "" +// removes the setting, so the standard desktop returns. +int32_t ShellSetDesktop(const char *program) { + return setShellKey(SHELL_INI_DESKTOP, program); +} + + +// Set this program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH). +// True when it was set. +int32_t ShellSetPriority(int32_t priority) { + const ShellAppT *app = mainTaskApp(); + + return (app && tsSetPriority(app->mainTaskId, priority) == TS_OK) ? BAS_TRUE : BAS_FALSE; +} + + +// The program DVX starts after the desktop at its next start (DVX.INI +// [shell] welcome); "" removes the setting. +int32_t ShellSetWelcome(const char *program) { + return setShellKey(SHELL_INI_WELCOME, program); +} + + +const char *ShellSystemInfo(void) { + return shellGetSystemInfo(); +} + + +void ShellTaskManager(void) { + shellOpenTaskManager(shellContext()); +} diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c index 7b2edb40..ffdd3f06 100644 --- a/src/libs/kpunch/dvxshell/shellMain.c +++ b/src/libs/kpunch/dvxshell/shellMain.c @@ -40,7 +40,9 @@ // Exit DVX (shellRequestExit) is carried out at the top of the main loop: // every running app is asked to close exactly as if the user had closed // its windows, so its own save prompt runs; one that keeps its window -// open cancels the exit (see shellCloseAllApps). +// open cancels the exit (see shellCloseAllApps). A request to close one +// program (shellRequestAppClose, BASIC's ShellRunningClose) is carried out +// there too, the same way (shellCloseApp). // // Crash recovery uses setjmp/longjmp: // The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL @@ -75,7 +77,6 @@ #include "../serial/security/security.h" // SEC_SUCCESS only; resolved at run time #include -#include #include #include #include @@ -95,6 +96,11 @@ DVX_ABI_STAMP(dvxshell); #define SHELL_EXIT_GRACE_MS 5000 #define SHELL_EXIT_SETTLE_FRAMES 4 #define SHELL_EXIT_TITLE "Exit DVX" +#define SHELL_CLOSE_TITLE "Close Program" + +// The not-responding question: while exiting DVX, and for one program +#define SHELL_STUCK_EXIT_ASK "'%s' is not responding to the request to close. End it and continue exiting DVX?" +#define SHELL_STUCK_CLOSE_ASK "'%s' is not responding to the request to close. End it?" // Command-line switches: start in safe video mode; start a program once // the desktop is up (DVX /RUN=APPS\KPUNCH\NOTEPAD\NOTEPAD.APP) @@ -191,6 +197,15 @@ static RngSeedFileFnT sRngSaveSeed = NULL; // Set by shellRequestExit, acted on at the top of the main loop. static bool sExitRequested = false; +// Programs asked to close (shellRequestAppClose), oldest first: stb_ds of +// app ids, carried out at the top of the main loop. +static int32_t *sCloseRequests = NULL; + +// The desktop app's id while it runs (0 once it has ended), and how many +// times shellDesktopUpdate has run. +static int32_t sDesktopId = 0; +static uint32_t sDesktopChanges = 0; + // ============================================================ // Prototypes // ============================================================ @@ -198,22 +213,26 @@ static bool sExitRequested = false; static void appExitHook(int status); static void appIdRestore(int32_t value); static int32_t appIdSave(void); +static bool closeRefusedForDialog(AppContextT *ctx); static void compactDesktopFns(void); static void compactIdleHandlers(void); static void ctrlEscHandler(void *ctx); +static void desktopEnded(int32_t appId); static void entropyInit(void); static void entropyShutdown(void); static ShellAppT *exitAppRunning(int32_t appId); -static bool exitAskApp(AppContextT *ctx, int32_t appId); +static bool exitAskApp(AppContextT *ctx, int32_t appId, bool exiting); static ExitCloseE exitAwaitClose(AppContextT *ctx, int32_t appId, int32_t winId, uint32_t created); -static bool exitEndStuckApp(AppContextT *ctx, int32_t appId); +static bool exitEndStuckApp(AppContextT *ctx, int32_t appId, bool exiting); static WindowT *exitOldestWindow(const AppContextT *ctx, int32_t appId); static bool exitWindowGone(const AppContextT *ctx, int32_t winId); static void f1HelpHandler(void *ctx); static const char *findSwitch(int argc, char *argv[], const char *name); static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData); bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId); +bool shellCloseApp(AppContextT *ctx, int32_t appId); AppContextT *shellContext(void); +int32_t shellDesktopChangeCount(void); void shellDesktopUpdate(void); static void shellFrame(AppContextT *ctx); static void shellIdleDispatch(void *ctx); @@ -221,7 +240,9 @@ int shellMain(int argc, char *argv[]); void shellPurgeAppCallbacks(int32_t appId); void shellRegisterDesktopUpdate(void (*updateFn)(void)); void shellRegisterIdle(void (*fn)(void *ctx), void *ctx); +void shellRequestAppClose(int32_t appId); void shellRequestExit(void); +void shellRunCloseRequests(AppContextT *ctx); void shellRunExitRequest(AppContextT *ctx, int32_t keepAppId); void shellTaskDepthForget(uint32_t taskId); int32_t shellTaskDepthShare(uint32_t taskId); @@ -271,6 +292,19 @@ static int32_t appIdSave(void) { } +// A dialog box still open (its nested event loop is on some stack) is +// raised and refuses closing programs, so it can be answered first. +static bool closeRefusedForDialog(AppContextT *ctx) { + if (!ctx->modalWindow) { + return false; + } + + dvxLog("Shell: closing programs refused while a dialog box is open"); + dvxRaiseWindow(ctx, ctx->modalWindow); + return true; +} + + // Drop desktop-update entries unregistered during a walk. static void compactDesktopFns(void) { for (int32_t i = arrlen(sDesktopUpdateFns) - 1; i >= 0; i--) { @@ -292,8 +326,19 @@ static void compactIdleHandlers(void) { static void ctrlEscHandler(void *ctx) { - if (shellCtrlEscFn) { - shellCtrlEscFn((AppContextT *)ctx); + shellOpenTaskManager((AppContextT *)ctx); +} + + +// App-cleanup hook: the desktop ending (closed, ended from Task Manager or +// crashed) exits DVX. The other programs are asked to close as for Exit +// DVX; one that refuses keeps running, and DVX ends with the last of them +// (see the main loop). +static void desktopEnded(int32_t appId) { + if (appId == sDesktopId) { + sDesktopId = 0; + dvxLog("Shell: the desktop ended; exiting DVX"); + shellRequestExit(); } } @@ -342,7 +387,8 @@ static ShellAppT *exitAppRunning(int32_t appId) { // gadget) and may prompt. Hidden windows are left alone, as the user // could not close them either. False when the app kept a window after // prompting, or the user chose not to end an app that did not respond. -static bool exitAskApp(AppContextT *ctx, int32_t appId) { +// exiting: part of Exit DVX (the not-responding question says so). +static bool exitAskApp(AppContextT *ctx, int32_t appId, bool exiting) { ShellAppT *app = exitAppRunning(appId); WindowT *win = exitOldestWindow(ctx, appId); @@ -362,7 +408,7 @@ static bool exitAskApp(AppContextT *ctx, int32_t appId) { return false; } - if (result == ExitCloseStuckE && !exitEndStuckApp(ctx, appId)) { + if (result == ExitCloseStuckE && !exitEndStuckApp(ctx, appId, exiting)) { return false; } @@ -412,15 +458,15 @@ static ExitCloseE exitAwaitClose(AppContextT *ctx, int32_t appId, int32_t winId, // Offer to end an app that did not react to the close request. True when // it is gone (ended here, or it closed while the user was deciding). -static bool exitEndStuckApp(AppContextT *ctx, int32_t appId) { +static bool exitEndStuckApp(AppContextT *ctx, int32_t appId, bool exiting) { ShellAppT *app = exitAppRunning(appId); char msg[SHELL_MSG_MAX]; - dvxLog("Shell: '%s' did not respond to the exit close request", app->name); - snprintf(msg, sizeof(msg), "'%s' is not responding to the request to close. End it and continue exiting DVX?", app->name); + dvxLog("Shell: '%s' did not respond to the close request", app->name); + snprintf(msg, sizeof(msg), exiting ? SHELL_STUCK_EXIT_ASK : SHELL_STUCK_CLOSE_ASK, app->name); - if (dvxMessageBox(ctx, SHELL_EXIT_TITLE, msg, MB_YESNO | MB_ICONWARNING) != ID_YES) { - dvxLog("Shell: exit cancelled at the not-responding prompt"); + if (dvxMessageBox(ctx, exiting ? SHELL_EXIT_TITLE : SHELL_CLOSE_TITLE, msg, MB_YESNO | MB_ICONWARNING) != ID_YES) { + dvxLog("Shell: close cancelled at the not-responding prompt"); return false; } @@ -531,14 +577,12 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) { // open (its nested event loop is on some stack) is raised and the exit // refused, so it can be answered first. bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId) { - if (ctx->modalWindow) { - dvxLog("Shell: exit refused while a dialog box is open"); - dvxRaiseWindow(ctx, ctx->modalWindow); + if (closeRefusedForDialog(ctx)) { return false; } for (int32_t id = 1; id < shellAppSlotCount(); id++) { - if (id != keepAppId && exitAppRunning(id) && !exitAskApp(ctx, id)) { + if (id != keepAppId && exitAppRunning(id) && !exitAskApp(ctx, id, true)) { return false; } } @@ -547,17 +591,39 @@ bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId) { } -// Callbacks may register or unregister (themselves or others) mid-walk; -// see sDesktopWalkDepth. -// The shell's GUI context. For code that runs before any program has -// its DxeAppContextT (the test-script module ticks from the first idle -// frame, which can be inside a start-up dialog). -AppContextT *shellContext(void) { - return &sCtx; +// Ask one app to close, as shellCloseAllApps asks each: its windows are +// closed the way the user would close them (its save prompt runs; one +// that does not respond is offered to be ended). True when it is gone or +// was not running; false when it kept a window or a dialog box is open. +// Pumps the event loop: call it from the main loop's top level only +// (shellRequestAppClose is the way from anywhere else). +bool shellCloseApp(AppContextT *ctx, int32_t appId) { + if (closeRefusedForDialog(ctx)) { + return false; + } + + return exitAskApp(ctx, appId, false); } +// The shell's GUI context. For code that runs before any program has +// its DxeAppContextT (the test-script module ticks from the first idle +// frame, which can be inside a start-up dialog). The harness's context +// when a test has bound one. +AppContextT *shellContext(void) { + return sCtxRef; +} + + +int32_t shellDesktopChangeCount(void) { + return (int32_t)sDesktopChanges; +} + + +// Callbacks may register or unregister (themselves or others) mid-walk; +// see sDesktopWalkDepth. void shellDesktopUpdate(void) { + sDesktopChanges++; sDesktopWalkDepth++; for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) { @@ -604,8 +670,7 @@ static void shellFrame(AppContextT *ctx) { // dead until the outermost walk ends (see sIdleWalkDepth), so no handler is // skipped and arrlen is re-read so none is indexed past the array. static void shellIdleDispatch(void *ctx) { - (void)ctx; - + shellMinimizeIdle((AppContextT *)ctx); sIdleWalkDepth++; for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) { @@ -711,59 +776,19 @@ int shellMain(int argc, char *argv[]) { // after dvxInit so the display fields have real values to report. shellInfoInit(&sCtx); - // Apply mouse preferences - // Unknown names fall back to the documented defaults (shared - // vocabulary with the Control Panel, dvxPrefs.h). - const PrefsChoiceT *wheel = &prefsWheelDirChoices[prefsGetChoice(sPrefs, "mouse", "wheel", prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, MOUSE_WHEEL_DIR_DEFAULT)]; - const PrefsChoiceT *accel = &prefsAccelChoices[prefsGetChoice(sPrefs, "mouse", "acceleration", prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT)]; - const char *wheelStr = wheel->iniName; - int32_t wheelDir = wheel->value; - int32_t dblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", MOUSE_DBLCLICK_DEFAULT_MS); - const char *accelStr = accel->iniName; - int32_t accelVal = accel->value; + // Apply the mouse, color and desktop preferences (the keys the + // Control Panel writes; dvxApp.h) + dvxMouseFromPrefs(&sCtx, sPrefs); + dvxLog("Preferences: mouse wheel=%ld doubleclick=%ldms accel=%ld speed=%ld wheelspeed=%ld", (long)sCtx.wheelDirection, (long)sCtx.dblClickMs, (long)sCtx.mouseAccel, (long)sCtx.mouseMickeys, (long)sCtx.wheelStep); - int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT); - int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT); - - dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed, wheelStep); - dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld wheelspeed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed, (long)wheelStep); - - // Apply saved color scheme - bool colorsLoaded = false; - - for (int32_t i = 0; i < ColorCountE; i++) { - const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL); - - if (val) { - int32_t r; - int32_t g; - int32_t b; - - if (sscanf(val, "%" SCNd32 ",%" SCNd32 ",%" SCNd32, &r, &g, &b) == 3) { - sCtx.colorRgb[i][0] = (uint8_t)r; - sCtx.colorRgb[i][1] = (uint8_t)g; - sCtx.colorRgb[i][2] = (uint8_t)b; - colorsLoaded = true; - } - } - } - - if (colorsLoaded) { - dvxApplyColorScheme(&sCtx); + if (dvxColorsFromPrefs(&sCtx, sPrefs)) { dvxLog("Preferences: loaded custom color scheme"); } - // Apply saved wallpaper - sCtx.wallpaperMode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(sPrefs, "desktop", "mode", prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value; - - const char *wpPath = prefsGetString(sPrefs, "desktop", "wallpaper", NULL); - - if (wpPath) { - if (dvxSetWallpaper(&sCtx, wpPath)) { - dvxLog("Preferences: loaded wallpaper %s (%s)", wpPath, prefsWallpaperChoices[sCtx.wallpaperMode].iniName); - } else { - dvxLog("Preferences: failed to load wallpaper %s", wpPath); - } + if (!dvxWallpaperFromPrefs(&sCtx, sPrefs)) { + dvxLog("Preferences: failed to load the wallpaper"); + } else if (sCtx.wallpaperPath[0]) { + dvxLog("Preferences: loaded wallpaper %s (%s)", sCtx.wallpaperPath, prefsWallpaperChoices[sCtx.wallpaperMode].iniName); } } @@ -772,14 +797,17 @@ int shellMain(int argc, char *argv[]) { // Load the desktop app (configurable via [shell] desktop= in dvx.ini) const char *desktopApp = prefsGetString(sPrefs, SHELL_INI_SECTION, SHELL_INI_DESKTOP, SHELL_DESKTOP_APP); - int32_t desktopId = shellLoadApp(&sCtx, desktopApp); + sDesktopId = shellLoadApp(&sCtx, desktopApp); - if (desktopId < 0) { + if (sDesktopId < 0) { dvxLog("Failed to load desktop app '%s'", desktopApp); + sDesktopId = 0; shellTeardown(); return 1; } + shellRegisterAppCleanup(desktopEnded); + dvxPlaySound(SoundStartE); // Read-only media (the loader found DVX's folder unwritable): said @@ -876,9 +904,18 @@ int shellMain(int argc, char *argv[]) { // with no dispatch of the shell's own on the stack. while (sCtx.running) { shellFrame(&sCtx); - shellRunExitRequest(&sCtx, desktopId); + shellRunCloseRequests(&sCtx); + shellRunExitRequest(&sCtx, sDesktopId); + + // Without a desktop DVX ends with the last program. + if (sDesktopId == 0 && shellRunningAppCount() == 0) { + dvxQuit(&sCtx); + } } + // The shutdown below ends the desktop too: that is no exit request. + shellUnregisterAppCleanup(desktopEnded); + sDesktopId = 0; dvxLog("DVX Shell shutting down..."); dvxPlaySoundWait(SoundExitE); @@ -919,6 +956,13 @@ void shellPurgeAppCallbacks(int32_t appId) { } } + // Its slot may be reused before the request runs. + for (int32_t i = arrlen(sCloseRequests) - 1; i >= 0; i--) { + if (sCloseRequests[i] == appId) { + arrdel(sCloseRequests, i); + } + } + if (sDesktopWalkDepth == 0) { compactDesktopFns(); } @@ -964,6 +1008,21 @@ void shellRegisterIdle(void (*fn)(void *ctx), void *ctx) { } +// Ask a program to close. Only records the request: the main loop carries +// it out at its top level (shellRunCloseRequests -> shellCloseApp), where +// it is safe to pump the event loop and reap apps. A request already +// waiting for the same program is not repeated. +void shellRequestAppClose(int32_t appId) { + for (int32_t i = 0; i < arrlen(sCloseRequests); i++) { + if (sCloseRequests[i] == appId) { + return; + } + } + + arrput(sCloseRequests, appId); +} + + // Exit DVX. Only records the request: the main loop carries it out at // its top level (shellRunExitRequest), where it is safe to pump the event // loop and reap apps. @@ -972,6 +1031,21 @@ void shellRequestExit(void) { } +// Each request is taken off the list before its program is asked, so one +// made while it is being asked (the program asks again) waits for the next +// pass. +void shellRunCloseRequests(AppContextT *ctx) { + int32_t pending = (int32_t)arrlen(sCloseRequests); + + for (int32_t i = 0; i < pending && arrlen(sCloseRequests) > 0; i++) { + int32_t appId = sCloseRequests[0]; + + arrdel(sCloseRequests, 0); + shellCloseApp(ctx, appId); + } +} + + // Cleared first: a request made while the apps are being asked (Exit // chosen again) starts a fresh pass rather than being lost. void shellRunExitRequest(AppContextT *ctx, int32_t keepAppId) { @@ -1025,6 +1099,8 @@ static void shellTeardown(void) { sPrefs = NULL; arrfree(sTaskDepth); sTaskDepth = NULL; + arrfree(sCloseRequests); + sCloseRequests = NULL; } @@ -1038,9 +1114,11 @@ void shellTestBind(AppContextT *ctx) { arrfree(sDesktopUpdateFns); arrfree(sIdleHandlers); arrfree(sTaskDepth); + arrfree(sCloseRequests); sDesktopUpdateFns = NULL; sIdleHandlers = NULL; sTaskDepth = NULL; + sCloseRequests = NULL; sRunningTask = TS_MAIN_TASK_ID; sDepthBase = 0; sDesktopWalkDepth = 0; diff --git a/src/libs/kpunch/dvxshell/shellProg.c b/src/libs/kpunch/dvxshell/shellProg.c index 6665657e..7d498e20 100644 --- a/src/libs/kpunch/dvxshell/shellProg.c +++ b/src/libs/kpunch/dvxshell/shellProg.c @@ -23,6 +23,7 @@ // shellProg.c -- Installed programs (see shellProg.h) #include "shellProg.h" +#include "shellAssoc.h" #include "dvxPlat.h" #include @@ -34,7 +35,6 @@ #include "stb_ds_wrap.h" // Resource names read from a program -#define PROG_RES_ICON "icon32" #define PROG_RES_NAME "name" #define PROG_RES_DESC "description" @@ -42,12 +42,43 @@ // Prototypes // ============================================================ -static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list); -bool shellIsProgram(const char *path); -uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch); -void shellProgramInfo(const char *path, ShellProgramT *out); -void shellProgramListFree(ShellProgramT *list); -int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **outList); +static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize); +static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list); +bool shellIsProgram(const char *path); +uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch); +bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize); +uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch); +void shellProgramInfo(const char *path, ShellProgramT *out); +void shellProgramListFree(ShellProgramT *list); +int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **outList); + + +// The item icon rule: the picture file iconPath (NULL or "" for none), +// else path's own picture, else for a document the icon of the program +// its file type opens in. chosen (chosenSize bytes, may be NULL) gets +// the file the icon came from ("" for none). +static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize) { + char opener[DVX_MAX_PATH]; + char args[SHELL_ARGS_MAX]; + const char *from = (iconPath && iconPath[0]) ? iconPath : NULL; + uint8_t *icon = from ? shellProgramIcon(ctx, from, edge, outPitch) : NULL; + + if (!icon) { + from = path; + icon = shellProgramIcon(ctx, from, edge, outPitch); + } + + if (!icon && path && !shellIsProgram(path) && shellAssocCommand(path, opener, sizeof(opener), args, sizeof(args))) { + from = opener; + icon = shellProgramIcon(ctx, from, edge, outPitch); + } + + if (chosen && chosenSize > 0) { + snprintf(chosen, (size_t)chosenSize, "%s", icon ? from : ""); + } + + return icon; +} // The names are read in full before anything is opened: DOS has few file @@ -99,6 +130,20 @@ bool shellIsProgram(const char *path) { } +uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch) { + return itemIconPick(ctx, iconPath, path, edge, outPitch, NULL, 0); +} + + +bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize) { + int32_t pitch = 0; + uint8_t *icon = itemIconPick(ctx, iconPath, path, SHELL_PROG_ICON_SMALL, &pitch, out, outSize); + + dvxFreeImage(icon); + return icon != NULL; +} + + // Nearest-pixel scaling: a whole number of source pixels per target pixel // is not needed, and icons are small. uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch) { @@ -111,11 +156,8 @@ uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int3 return NULL; } - if (shellIsProgram(path)) { - src = dvxResLoadIcon(ctx, path, PROG_RES_ICON, &srcW, &srcH, &srcPitch); - } else { - src = dvxLoadImage(ctx, path, &srcW, &srcH, &srcPitch); - } + // A picture file, or a program's own icon + src = dvxLoadImage(ctx, path, &srcW, &srcH, &srcPitch); if (!src || (srcW == edge && srcH == edge)) { *outPitch = srcPitch; @@ -149,7 +191,9 @@ void shellProgramInfo(const char *path, ShellProgramT *out) { memset(out, 0, sizeof(*out)); snprintf(out->path, sizeof(out->path), "%s", path); - snprintf(out->name, sizeof(out->name), "%.*s", (int)nameLen, base); + nameLen = DVX_MIN(nameLen, (int32_t)sizeof(out->name) - 1); + memcpy(out->name, base, (size_t)nameLen); + out->name[nameLen] = '\0'; out->name[0] = (char)toupper((unsigned char)out->name[0]); if (shellIsProgram(path)) { diff --git a/src/libs/kpunch/dvxshell/shellProg.h b/src/libs/kpunch/dvxshell/shellProg.h index 32d62963..1346c023 100644 --- a/src/libs/kpunch/dvxshell/shellProg.h +++ b/src/libs/kpunch/dvxshell/shellProg.h @@ -61,6 +61,21 @@ typedef void (*ShellProgressFnT)(void *ctx); // True when path names a program (.app extension, any case). bool shellIsProgram(const char *path); +// The icon an item for path shows, edge x edge pixels in display format: +// the picture file iconPath (NULL or "" for none), else path's own +// picture (a program's icon32, or path itself when it is a picture +// file), else for a document the icon of the program its file type opens +// in (shellAssocCommand). NULL when there is none. Free with +// dvxFreeImage. Program Manager's items and its Properties preview use +// it. +uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch); + +// The file shellItemIcon takes an item's icon from (iconPath, path or +// the program path's file type opens in) in out; false ("" in out) when +// none of them gives a picture. Its picture is what dvxLoadImage (and +// BASIC's LOADPICTURE) loads from that file. +bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize); + // A picture edge x edge pixels in display format: a program's icon32 // resource, or any image file DVX can load (BMP, PNG, JPEG, GIF), scaled // to fit. NULL when there is none. Free with dvxFreeImage. diff --git a/src/libs/kpunch/dvxshell/shellRun.c b/src/libs/kpunch/dvxshell/shellRun.c index 4f1ffef1..3ab8811f 100644 --- a/src/libs/kpunch/dvxshell/shellRun.c +++ b/src/libs/kpunch/dvxshell/shellRun.c @@ -37,6 +37,8 @@ #include #include #include +#include "dvxMem.h" +#include "stb_ds_wrap.h" #define RUN_TITLE "Run" #define RUN_DLG_W 320 @@ -48,13 +50,29 @@ typedef struct { int32_t outSize; } RunDlgT; +// A program whose new windows are still being minimized (Run Minimized). +typedef struct { + int32_t appId; + int32_t framesLeft; +} RunMinimizeT; + +// ============================================================ +// Module state +// ============================================================ + +static RunMinimizeT *sMinimize = NULL; // stb_ds + // ============================================================ // Prototypes // ============================================================ static bool fileInFolder(const char *folder, const char *name, char *out, int32_t outSize); +static void minimizeEnded(int32_t appId); +static void minimizeWindows(AppContextT *ctx, int32_t appId); static void runDlgBrowse(WidgetT *w); static bool runDlgOk(WindowT *dlg, void *userCtx); +void shellMinimizeApp(AppContextT *ctx, int32_t appId); +void shellMinimizeIdle(AppContextT *ctx); void shellResolveArgs(const char *folder, const char *args, char *out, int32_t outSize); int32_t shellRunCommand(AppContextT *ctx, const char *command); bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t historyCount, char *outCommand, int32_t outSize); @@ -74,6 +92,28 @@ static bool fileInFolder(const char *folder, const char *name, char *out, int32_ } +// App-cleanup hook: an ended program's windows need no more minimizing +// (and its id may be given to the next program started). +static void minimizeEnded(int32_t appId) { + for (int32_t i = (int32_t)arrlen(sMinimize) - 1; i >= 0; i--) { + if (sMinimize[i].appId == appId) { + arrdel(sMinimize, i); + } + } +} + + +static void minimizeWindows(AppContextT *ctx, int32_t appId) { + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->appId == appId && win->visible && !win->minimized) { + dvxMinimizeWindow(ctx, win); + } + } +} + + // Browse puts the chosen file in the command box, quoted when it has a // blank. static void runDlgBrowse(WidgetT *w) { @@ -103,6 +143,42 @@ static bool runDlgOk(WindowT *dlg, void *userCtx) { } +// A second call for the same program starts its frames again. +void shellMinimizeApp(AppContextT *ctx, int32_t appId) { + RunMinimizeT entry = { appId, SHELL_MINIMIZE_FRAMES }; + + if (!ctx || !shellGetApp(appId)) { + return; + } + + shellRegisterAppCleanup(minimizeEnded); + minimizeEnded(appId); + arrput(sMinimize, entry); + minimizeWindows(ctx, appId); +} + + +// Walked backwards with the length re-read: minimizing runs window +// callbacks, which may end or start another Run Minimized program. +void shellMinimizeIdle(AppContextT *ctx) { + for (int32_t i = (int32_t)arrlen(sMinimize) - 1; i >= 0; i--) { + if (i >= (int32_t)arrlen(sMinimize)) { + continue; + } + + sMinimize[i].framesLeft--; + + int32_t appId = sMinimize[i].appId; + + if (sMinimize[i].framesLeft <= 0) { + arrdel(sMinimize, i); + } + + minimizeWindows(ctx, appId); + } +} + + void shellResolveArgs(const char *folder, const char *args, char *out, int32_t outSize) { char token[DVX_MAX_PATH]; char joined[DVX_MAX_PATH]; diff --git a/src/libs/kpunch/dvxshell/shellRun.h b/src/libs/kpunch/dvxshell/shellRun.h index 9a193786..5fb900b8 100644 --- a/src/libs/kpunch/dvxshell/shellRun.h +++ b/src/libs/kpunch/dvxshell/shellRun.h @@ -38,6 +38,20 @@ // Command line buffer size (program plus arguments). #define SHELL_RUN_CMD_MAX 512 +// Idle frames during which shellMinimizeApp keeps minimizing the windows +// a program opens (a program with a main loop opens them after it starts). +#define SHELL_MINIMIZE_FRAMES 60 + +// Run Minimized: minimize every visible window of appId now and on each of +// the next SHELL_MINIMIZE_FRAMES idle frames (shellMinimizeIdle), so the +// windows it opens while starting are minimized too. Nothing for an id +// that is not running. +void shellMinimizeApp(AppContextT *ctx, int32_t appId); + +// One idle frame of shellMinimizeApp's work (the shell's idle dispatch +// calls this). +void shellMinimizeIdle(AppContextT *ctx); + // args with every relative file name that exists inside folder replaced by // folder\name, so a program started from anywhere finds the files its // command line names. Quoted arguments keep their quotes. An empty or diff --git a/src/libs/kpunch/dvxshell/shellSysBas.c b/src/libs/kpunch/dvxshell/shellSysBas.c new file mode 100644 index 00000000..33dd49f3 --- /dev/null +++ b/src/libs/kpunch/dvxshell/shellSysBas.c @@ -0,0 +1,543 @@ +// 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. + +// shellSysBas.c -- DVX BASIC entry points of the system settings +// (src/include/basic/system.bas) +// +// What Control Panel changes, for BASIC programs: the video mode, the +// system colors and themes, the wallpaper and the mouse; and the system +// font's glyphs (what DVX Demo's own text drawing reads). Each change +// takes effect at once for the whole desktop. The *Store / *Load pairs +// write and read the DVX.INI keys DVX starts with (dvxApp.h), through +// a prefs.bas handle; a PrefsCreate handle filled with *Store is an +// in-memory snapshot to go back to with *Load. +// +// Called through DECLARE LIBRARY "dvxshell", so the signatures use only +// BASIC-marshalable types: strings and LONG in, LONG or STRING out, True +// (-1) / False (0). Colors are BASIC RGB() values (&HBBGGRR). Lists +// are 0-based. A returned string stays valid until the next call +// returning one. + +#include "shellApp.h" +#include "dvxApp.h" +#include "dvxDlg.h" +#include "dvxPrefs.h" +#include "dvxWgt.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define MEMORY_UNKNOWN (-1) +#define KEY_BYTE_MASK 0xFF // the character / scan code byte of a key code +#define SYS_SEND_KEY_MAX 64 // keys SysSendKey keeps waiting + +// A keystroke SysSendKey is waiting to type, as the BIOS stores it +typedef struct { + int32_t ascii; + int32_t scan; + int32_t shift; +} SysKeyT; + +static SysKeyT *sSendKeys = NULL; // stb_ds, oldest first + +// ============================================================ +// Prototypes +// ============================================================ + +static void biosKeyOf(int32_t keyCode, int32_t shift, int32_t *ascii, int32_t *scan); +static bool colorOk(int32_t id); +static const BitmapFontT *fontOf(void); +static int32_t memoryKb(bool total); +static void sendKeyIdle(void *ctx); +int32_t SysColorCount(void); +int32_t SysColorId(const char *name); +const char *SysColorLabel(int32_t id); +const char *SysColorName(int32_t id); +int32_t SysColorsLoad(int32_t prefs); +void SysColorsStore(int32_t prefs); +int32_t SysFontCharCount(void); +int32_t SysFontFirstChar(void); +int32_t SysFontGlyphRow(int32_t code, int32_t row); +int32_t SysFontHeight(void); +int32_t SysFontWidth(void); +int32_t SysGetColor(int32_t id); +int32_t SysLoadTheme(const char *path); +int32_t SysMemoryFreeKb(void); +int32_t SysMemoryTotalKb(void); +int32_t SysMouseAccel(void); +int32_t SysMouseButtons(void); +int32_t SysMouseDoubleClick(void); +void SysMouseLoad(int32_t prefs); +int32_t SysMouseMickeys(void); +void SysMouseStore(int32_t prefs); +int32_t SysMouseWheelDir(void); +int32_t SysMouseWheelStep(void); +int32_t SysMouseX(void); +int32_t SysMouseY(void); +void SysResetColors(void); +int32_t SysSaveTheme(const char *path); +int32_t SysScreenshot(const char *path); +int32_t SysSendKey(int32_t keyCode, int32_t shift); +void SysSetColor(int32_t id, int32_t rgb); +void SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep); +void SysSetMousePos(int32_t x, int32_t y); +int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp); +int32_t SysSetWallpaper(const char *path); +void SysSetWallpaperMode(int32_t mode); +int32_t SysShiftState(void); +int32_t SysVideoModeBpp(int32_t index); +int32_t SysVideoModeCount(void); +int32_t SysVideoModeHeight(int32_t index); +int32_t SysVideoModeWidth(int32_t index); +const char *SysWallpaper(void); +int32_t SysWallpaperLoad(int32_t prefs); +int32_t SysWallpaperMode(void); +void SysWallpaperStore(int32_t prefs); +static const VideoModeInfoT *videoMode(int32_t index); + + +// keyCode (a character, or a DVX key code with KEY_EXT_FLAG: KeyDown's +// KeyCode) with modifiers shift as the BIOS stores the keystroke: the +// codes platformInjectKey takes. Alt / Ctrl / Shift combinations that +// have codes of their own get them (platformBiosScanCode), Ctrl with a +// letter its control character. +static void biosKeyOf(int32_t keyCode, int32_t shift, int32_t *ascii, int32_t *scan) { + int32_t special = platformBiosScanCode(keyCode, shift); + + *ascii = 0; + *scan = 0; + + if (special >= 0) { + *scan = special; + } else if (keyCode & KEY_EXT_FLAG) { + *scan = keyCode & KEY_BYTE_MASK; + } else if ((shift & KEY_MOD_CTRL) && isalpha(keyCode & KEY_BYTE_MASK)) { + *ascii = toupper(keyCode & KEY_BYTE_MASK) - 'A' + KEY_CTRL_A; + } else { + *ascii = keyCode & KEY_BYTE_MASK; + } +} + + +static bool colorOk(int32_t id) { + return id >= 0 && id < ColorCountE; +} + + +static const BitmapFontT *fontOf(void) { + return dvxGetFont(shellContext()); +} + + +// Total or free memory in KB, -1 when unknown. +static int32_t memoryKb(bool total) { + uint32_t totalKb = 0; + uint32_t freeKb = 0; + + if (!platformGetMemoryInfo(&totalKb, &freeKb)) { + return MEMORY_UNKNOWN; + } + + uint32_t kb = total ? totalKb : freeKb; + + return kb > INT32_MAX ? INT32_MAX : (int32_t)kb; +} + + +// Shell idle handler while SysSendKey keys wait: one key per idle frame, +// so each is read with its own modifiers before the next one changes +// them (the keyboard reports the modifiers of the moment it is read); +// after the last, the injected modifiers are released. +static void sendKeyIdle(void *ctx) { + (void)ctx; + + if (arrlen(sSendKeys) == 0) { + arrfree(sSendKeys); + platformInjectModifiers(0); + shellUnregisterIdle(sendKeyIdle, NULL); + return; + } + + if (platformInjectKey(sSendKeys[0].ascii, sSendKeys[0].scan, sSendKeys[0].shift)) { + arrdel(sSendKeys, 0); + } +} + + +// How many system colors there are (ids 0 to SysColorCount - 1). +int32_t SysColorCount(void) { + return ColorCountE; +} + + +// The id of the color whose settings name is name (any case), or -1. +int32_t SysColorId(const char *name) { + for (int32_t i = 0; name && i < ColorCountE; i++) { + if (strcasecmp(name, dvxColorName((ColorIdE)i)) == 0) { + return i; + } + } + + return -1; +} + + +// What a list shows for the color ("" for an unknown id). +const char *SysColorLabel(int32_t id) { + return colorOk(id) ? dvxColorLabel((ColorIdE)id) : ""; +} + + +// The color's key in [colors] of DVX.INI and themes ("" for an unknown +// id). +const char *SysColorName(int32_t id) { + return colorOk(id) ? dvxColorName((ColorIdE)id) : ""; +} + + +// Apply the [colors] of a prefs handle. True when it had any. +int32_t SysColorsLoad(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + return (h && dvxColorsFromPrefs(shellContext(), h)) ? BAS_TRUE : BAS_FALSE; +} + + +// Store every color in [colors] of a prefs handle. +void SysColorsStore(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + dvxColorsToPrefs(shellContext(), h); + } +} + + +// How many glyphs the system font has (characters SysFontFirstChar to +// SysFontFirstChar + SysFontCharCount - 1). +int32_t SysFontCharCount(void) { + return fontOf()->numChars; +} + + +int32_t SysFontFirstChar(void) { + return fontOf()->firstChar; +} + + +// Row row (0 at the top) of character code's glyph: one bit per pixel, +// &H80 the leftmost (SysFontWidth pixels). 0 for a character or row the +// font does not have. +int32_t SysFontGlyphRow(int32_t code, int32_t row) { + const BitmapFontT *font = fontOf(); + int32_t idx = code - font->firstChar; + + if (idx < 0 || idx >= font->numChars || row < 0 || row >= font->charHeight) { + return 0; + } + + return font->glyphData[idx * font->charHeight + row]; +} + + +int32_t SysFontHeight(void) { + return fontOf()->charHeight; +} + + +// The system font's character cell: every glyph is this wide and high. +int32_t SysFontWidth(void) { + return fontOf()->charWidth; +} + + +// The color as a BASIC RGB() value (0 for an unknown id). +int32_t SysGetColor(int32_t id) { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + + if (colorOk(id)) { + dvxGetColor(shellContext(), (ColorIdE)id, &r, &g, &b); + } + + return (int32_t)wgtBasicColor(DVX_RGB(r, g, b)); +} + + +// Apply a theme file. True when it was read. +int32_t SysLoadTheme(const char *path) { + return dvxLoadTheme(shellContext(), path) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t SysMemoryFreeKb(void) { + return memoryKb(false); +} + + +int32_t SysMemoryTotalKb(void) { + return memoryKb(true); +} + + +// Acceleration threshold in effect (PrefsChoiceValue of PREFS_CHOICE_ACCEL). +int32_t SysMouseAccel(void) { + return shellContext()->mouseAccel; +} + + +// The mouse buttons held now: vbLeftButton, vbRightButton and +// vbMiddleButton bits. +int32_t SysMouseButtons(void) { + return shellContext()->mouseButtons; +} + + +// Double-click speed in milliseconds. +int32_t SysMouseDoubleClick(void) { + return shellContext()->dblClickMs; +} + + +// Apply the [mouse] settings of a prefs handle (defaults for missing keys). +void SysMouseLoad(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + dvxMouseFromPrefs(shellContext(), h); + } +} + + +// Mouse movement per 8 pixels of pointer movement (higher is slower). +int32_t SysMouseMickeys(void) { + return shellContext()->mouseMickeys; +} + + +// Store the mouse settings in effect in [mouse] of a prefs handle. +void SysMouseStore(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + dvxMouseToPrefs(shellContext(), h); + } +} + + +// Scroll wheel direction: 1 normal, -1 reversed. +int32_t SysMouseWheelDir(void) { + return shellContext()->wheelDirection; +} + + +// Lines scrolled per wheel notch. +int32_t SysMouseWheelStep(void) { + return shellContext()->wheelStep; +} + + +// Where the pointer is on the screen (pixels from the left edge). +int32_t SysMouseX(void) { + return shellContext()->mouseX; +} + + +// Where the pointer is on the screen (pixels from the top edge). +int32_t SysMouseY(void) { + return shellContext()->mouseY; +} + + +// Every color back to DVX's own scheme. +void SysResetColors(void) { + dvxResetColorScheme(shellContext()); +} + + +// Save the colors as a theme file. True when it was saved. +int32_t SysSaveTheme(const char *path) { + return dvxSaveTheme(shellContext(), path) ? BAS_TRUE : BAS_FALSE; +} + + +// Save the whole screen as a picture file (BMP when path ends in .bmp, +// else PNG). True when it was written. +int32_t SysScreenshot(const char *path) { + return (path && path[0] && dvxScreenshot(shellContext(), path) == 0) ? BAS_TRUE : BAS_FALSE; +} + + +// Type a key into the window that has the focus, as if the user pressed +// it: keyCode as KeyDown gets it (a character, or a vbKey constant), +// shift the vbShiftMask / vbCtrlMask / vbAltMask bits held with it. The +// keys arrive in order once the program's code has returned to DVX, one +// per frame (sendKeyIdle). False when SYS_SEND_KEY_MAX keys are waiting +// already. +int32_t SysSendKey(int32_t keyCode, int32_t shift) { + if (arrlen(sSendKeys) >= SYS_SEND_KEY_MAX) { + return BAS_FALSE; + } + + SysKeyT key; + + biosKeyOf(keyCode, shift & KEY_MOD_ALL, &key.ascii, &key.scan); + key.shift = shift & KEY_MOD_ALL; + arrput(sSendKeys, key); + shellRegisterIdle(sendKeyIdle, NULL); + return BAS_TRUE; +} + + +// Set one color (a BASIC RGB() value); the whole screen is redrawn. +void SysSetColor(int32_t id, int32_t rgb) { + uint32_t c = wgtBasicColor((uint32_t)rgb); + + if (colorOk(id)) { + dvxSetColor(shellContext(), (ColorIdE)id, DVX_RGB_RED(c), DVX_RGB_GREEN(c), DVX_RGB_BLUE(c)); + } +} + + +// Apply a mouse configuration (dvxSetMouseConfig: accel and mickeys of 0 +// or less leave those as they are). +void SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep) { + dvxSetMouseConfig(shellContext(), wheelDir, dblClickMs, accel, mickeys, wheelStep); +} + + +// Move the pointer to screen position x, y (kept on the screen). The +// program sees the move as the user's (MouseMove events). +void SysSetMousePos(int32_t x, int32_t y) { + const AppContextT *ctx = shellContext(); + + platformMouseWarp(DVX_MAX(0, DVX_MIN(x, ctx->display.width - 1)), DVX_MAX(0, DVX_MIN(y, ctx->display.height - 1))); +} + + +// Change the screen mode. True when it changed (False keeps the old +// mode, also while a program has the screen to itself). +int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp) { + return dvxChangeVideoMode(shellContext(), width, height, bpp) == 0 ? BAS_TRUE : BAS_FALSE; +} + + +// Show an image file as the wallpaper ("" for none). True when it was +// loaded (False keeps the old one). +int32_t SysSetWallpaper(const char *path) { + return dvxSetWallpaper(shellContext(), (path && path[0]) ? path : NULL) ? BAS_TRUE : BAS_FALSE; +} + + +// How the wallpaper fills the screen (index of PREFS_CHOICE_WALLPAPER); +// other values are ignored. +void SysSetWallpaperMode(int32_t mode) { + if (mode >= 0 && mode < PREFS_WALLPAPER_CHOICE_COUNT) { + dvxSetWallpaperMode(shellContext(), (WallpaperModeE)prefsWallpaperChoices[mode].value); + } +} + + +// The vbShiftMask / vbCtrlMask / vbAltMask bits of the keys held now. +int32_t SysShiftState(void) { + return shellContext()->keyModifiers & KEY_MOD_ALL; +} + + +// Bits per pixel of video mode index (0 out of range). +int32_t SysVideoModeBpp(int32_t index) { + const VideoModeInfoT *m = videoMode(index); + + return m ? m->bpp : 0; +} + + +// How many screen modes the display offers. +int32_t SysVideoModeCount(void) { + int32_t count = 0; + + dvxGetVideoModes(shellContext(), &count); + return count; +} + + +// Height of video mode index (0 out of range). +int32_t SysVideoModeHeight(int32_t index) { + const VideoModeInfoT *m = videoMode(index); + + return m ? m->h : 0; +} + + +// Width of video mode index (0 out of range). +int32_t SysVideoModeWidth(int32_t index) { + const VideoModeInfoT *m = videoMode(index); + + return m ? m->w : 0; +} + + +// The wallpaper's image file ("" for none). +const char *SysWallpaper(void) { + return shellContext()->wallpaperPath; +} + + +// Apply the [desktop] wallpaper and mode of a prefs handle (none when the +// wallpaper key is missing). False when the image could not be loaded. +int32_t SysWallpaperLoad(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + return (h && dvxWallpaperFromPrefs(shellContext(), h)) ? BAS_TRUE : BAS_FALSE; +} + + +// How the wallpaper fills the screen (index of PREFS_CHOICE_WALLPAPER). +int32_t SysWallpaperMode(void) { + return prefsChoiceIndexOfValue(prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, shellContext()->wallpaperMode); +} + + +// Store the wallpaper and its mode in [desktop] of a prefs handle. +void SysWallpaperStore(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + dvxWallpaperToPrefs(shellContext(), h); + } +} + + +// Entry index of the video mode list, or NULL. +static const VideoModeInfoT *videoMode(int32_t index) { + int32_t count = 0; + const VideoModeInfoT *modes = dvxGetVideoModes(shellContext(), &count); + + return (index >= 0 && index < count) ? &modes[index] : NULL; +} diff --git a/src/libs/kpunch/fileops/fileOps.c b/src/libs/kpunch/fileops/fileOps.c index 0f915adb..1603ae1e 100644 --- a/src/libs/kpunch/fileops/fileOps.c +++ b/src/libs/kpunch/fileops/fileOps.c @@ -36,13 +36,14 @@ // on the item (approved, copied). #include "fileOps.h" +#include "fileOpsSys.h" #include "dvxPlat.h" #include "dvxTypes.h" #include "stb_ds_wrap.h" #include -#include #include +#include #include #include #include @@ -56,6 +57,17 @@ #define SIZE_SEPARATOR ',' #define SIZE_BASE 10 #define DATE_FORMAT "%Y-%m-%d %H:%M" +#define DATE_SCAN "%" SCNd32 "-%" SCNd32 "-%" SCNd32 " %" SCNd32 ":%" SCNd32 "%n" +#define DATE_SCAN_SECONDS ":%" SCNd32 "%n" +#define DATE_FIELDS 5 // year, month, day, hour, minute +#define DATE_TIME_SEP ':' +#define DATE_YEAR_BASE 1900 // struct tm counts years from 1900 +#define DATE_MONTH_MAX 12 +#define DATE_DAY_MAX 31 +#define DATE_HOUR_MAX 23 +#define DATE_MINUTE_MAX 59 +#define DATE_SECOND_MAX 59 +#define DATE_DST_UNKNOWN (-1) // mktime decides whether summer time applies #define NEW_DIR_MODE 0777 #define NO_PARENT (-1) @@ -114,21 +126,47 @@ struct FileOpsJobS { const char *current; // path being worked on (points into items) }; +// fileOpsListDir's scan: the entries so far and the filter for files. +typedef struct { + FileOpsEntryT *entries; // stb_ds array + const char *filter; + bool ok; // false: out of memory +} ListScanT; + +// scanFolder's scan: the job and the folder item being read. +typedef struct { + FileOpsJobT *job; + int32_t idx; + bool ok; +} FolderScanT; + +// treeSizeDir's scan: the folder and the totals. +typedef struct { + const char *dir; + uint64_t *bytes; + int32_t *files; + int32_t *folders; + bool ok; // false: something could not be read +} SizeScanT; + // ============================================================ // Prototypes // ============================================================ static bool addItem(FileOpsJobT *job, const char *src, const char *dst, int32_t parent); +static bool appendItem(FileOpsJobT *job, const char *src, const char *dst, int32_t parent, bool isDir, uint64_t size); static FileOpsStepE askUser(FileOpsJobT *job, FileOpsQuestionE question, FileOpsStepE kind, const char *fmt, ...) __attribute__((format(printf, 4, 5))); static FileOpsStepE cancelJob(FileOpsJobT *job); static void closeCopy(FileOpsJobT *job, bool discard); static int compareEntries(const void *a, const void *b); static const char *errorText(int32_t err); static void keepAncestors(FileOpsJobT *job, int32_t idx); +static bool listScanEntry(const FileOpsSysEntryT *e, void *ctx); static bool pathCharEq(char a, char b); static int32_t pathTrimmedLen(const char *path); static int32_t percentOf(const FileOpsJobT *job); static bool scanFolder(FileOpsJobT *job, int32_t idx); +static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx); static void skipItem(FileOpsJobT *job, int32_t idx); static int32_t statItem(const char *path, struct stat *st); static FileOpsStepE stepRemove(FileOpsJobT *job); @@ -136,6 +174,8 @@ static FileOpsStepE stepScan(FileOpsJobT *job); static FileOpsStepE stepTransfer(FileOpsJobT *job); static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it); static bool takeAnswer(FileOpsJobT *job, FileOpsQuestionE question, FileOpsAnswerE *answer); +static bool treeSizeDir(const char *dir, uint64_t *bytes, int32_t *files, int32_t *folders); +static bool treeSizeEntry(const FileOpsSysEntryT *e, void *ctx); static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders); // Sort parameters for compareEntries (qsort has no context argument) @@ -143,7 +183,7 @@ static FileOpsSortE sSortCol = FileOpsSortNameE; static bool sSortDesc = false; -// Append an item (breadth-first order). src and dst are copied. +// Append an item found by its path (a source the caller named). static bool addItem(FileOpsJobT *job, const char *src, const char *dst, int32_t parent) { struct stat st; @@ -151,13 +191,19 @@ static bool addItem(FileOpsJobT *job, const char *src, const char *dst, int32_t return false; } + return appendItem(job, src, dst, parent, S_ISDIR(st.st_mode), (uint64_t)st.st_size); +} + + +// Append an item (breadth-first order). src and dst are copied. +static bool appendItem(FileOpsJobT *job, const char *src, const char *dst, int32_t parent, bool isDir, uint64_t size) { FileOpsItemT it; memset(&it, 0, sizeof(it)); it.src = strdup(src); it.dst = dst ? strdup(dst) : NULL; - it.isDir = S_ISDIR(st.st_mode); - it.size = it.isDir ? 0 : (uint64_t)st.st_size; + it.isDir = isDir; + it.size = isDir ? 0 : size; it.parent = parent; if (!it.src || (dst && !it.dst)) { @@ -482,52 +528,21 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) { } +// One pass over the folder (fileOpsSysScan); entries that do not fit in +// memory are left out. bool fileOpsListDir(const char *dir, const char *filter, FileOpsEntryT **outEntries, int32_t *outCount) { - FileOpsEntryT *entries = NULL; - DIR *d = opendir(dir); + ListScanT scan; - if (!d) { + scan.entries = NULL; + scan.filter = filter; + scan.ok = true; + + if (!fileOpsSysScan(dir, true, listScanEntry, &scan)) { return false; } - struct dirent *ent; - - while ((ent = readdir(d)) != NULL) { - char path[DVX_MAX_PATH]; - struct stat st; - - if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { - continue; - } - - if (!fileOpsPathJoin(path, sizeof(path), dir, ent->d_name) || stat(path, &st) != 0) { - continue; - } - - bool isDir = S_ISDIR(st.st_mode); - - if (!isDir && !platformFilterMatch(ent->d_name, filter)) { - continue; - } - - FileOpsEntryT e; - - e.name = strdup(ent->d_name); - e.size = isDir ? 0 : (uint64_t)st.st_size; - e.mtime = st.st_mtime; - e.isDir = isDir; - - if (!e.name) { - break; - } - - arrput(entries, e); - } - - closedir(d); - - *outEntries = entries; - *outCount = (int32_t)arrlen(entries); + *outEntries = scan.entries; + *outCount = (int32_t)arrlen(scan.entries); return true; } @@ -574,6 +589,64 @@ bool fileOpsMakeDir(const char *dir, const char *name, char *err, int32_t errSiz } +// "YYYY-MM-DD HH:MM" (fileOpsFormatDate's form) or "YYYY-MM-DD HH:MM:SS", +// local time. A date that does not exist (February 30) is refused rather +// than moved to another day. +bool fileOpsParseDate(const char *text, time_t *out) { + int32_t year = 0; + int32_t month = 0; + int32_t day = 0; + int32_t hour = 0; + int32_t minute = 0; + int32_t second = 0; + int used = 0; + + if (!text || !out || sscanf(text, DATE_SCAN, &year, &month, &day, &hour, &minute, &used) != DATE_FIELDS) { + return false; + } + + const char *rest = text + used; + + if (*rest == DATE_TIME_SEP) { + used = 0; + + if (sscanf(rest, DATE_SCAN_SECONDS, &second, &used) != 1) { + return false; + } + + rest += used; + } + + if (*rest) { + return false; + } + + if (month < 1 || month > DATE_MONTH_MAX || day < 1 || day > DATE_DAY_MAX || hour < 0 || hour > DATE_HOUR_MAX || minute < 0 || minute > DATE_MINUTE_MAX || second < 0 || second > DATE_SECOND_MAX) { + return false; + } + + struct tm tm; + + memset(&tm, 0, sizeof(tm)); + tm.tm_year = year - DATE_YEAR_BASE; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_isdst = DATE_DST_UNKNOWN; + + time_t t = mktime(&tm); + + if (t == (time_t)-1 || tm.tm_mday != day || tm.tm_mon != month - 1) { + return false; + } + + *out = t; + return true; +} + + bool fileOpsPathIsInside(const char *path, const char *dir) { int32_t len = pathTrimmedLen(dir); @@ -748,6 +821,32 @@ static void keepAncestors(FileOpsJobT *job, int32_t idx) { } +// A folder entry for fileOpsListDir: folders always, files that match the +// filter. Stops when out of memory. +static bool listScanEntry(const FileOpsSysEntryT *e, void *ctx) { + ListScanT *scan = (ListScanT *)ctx; + + if (!e->isDir && !platformFilterMatch(e->name, scan->filter)) { + return true; + } + + FileOpsEntryT entry; + + entry.name = strdup(e->name); + entry.size = e->size; + entry.mtime = e->mtime; + entry.isDir = e->isDir; + + if (!entry.name) { + scan->ok = false; + return false; + } + + arrput(scan->entries, entry); + return true; +} + + // Separators are interchangeable; DOS names compare without case. static bool pathCharEq(char a, char b) { bool sepA = a == '/' || a == '\\'; @@ -795,47 +894,40 @@ static int32_t percentOf(const FileOpsJobT *job) { } -// Append the contents of folder item idx. False when it cannot be read. +// Append the contents of folder item idx, in one pass over the folder. +// False when it cannot be read (or a path would be too long). static bool scanFolder(FileOpsJobT *job, int32_t idx) { - DIR *d = opendir(job->items[idx].src); + FolderScanT scan; - if (!d) { + scan.job = job; + scan.idx = idx; + scan.ok = true; + + // The item array may move while appending, but the path string stays. + return fileOpsSysScan(job->items[idx].src, false, scanFolderEntry, &scan) && scan.ok; +} + + +// One entry of the folder scanFolder reads: an item under it. +static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx) { + FolderScanT *scan = (FolderScanT *)ctx; + const FileOpsItemT *dirItem = &scan->job->items[scan->idx]; + char src[DVX_MAX_PATH]; + char dst[DVX_MAX_PATH]; + bool fits = fileOpsPathJoin(src, sizeof(src), dirItem->src, e->name); + + if (fits && dirItem->dst) { + fits = fileOpsPathJoin(dst, sizeof(dst), dirItem->dst, e->name); + } + + if (!fits) { + errno = ENAMETOOLONG; + scan->ok = false; return false; } - struct dirent *ent; - bool ok = true; - - while ((ent = readdir(d)) != NULL) { - char src[DVX_MAX_PATH]; - char dst[DVX_MAX_PATH]; - - if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { - continue; - } - - // The item array may move while appending: re-read the paths. - const FileOpsItemT *dirItem = &job->items[idx]; - bool fits = fileOpsPathJoin(src, sizeof(src), dirItem->src, ent->d_name); - - if (fits && dirItem->dst) { - fits = fileOpsPathJoin(dst, sizeof(dst), dirItem->dst, ent->d_name); - } - - if (!fits) { - errno = ENAMETOOLONG; - ok = false; - break; - } - - if (!addItem(job, src, dirItem->dst ? dst : NULL, idx)) { - ok = false; - break; - } - } - - closedir(d); - return ok; + scan->ok = appendItem(scan->job, src, dirItem->dst ? dst : NULL, scan->idx, e->isDir, e->size); + return scan->ok; } @@ -1242,6 +1334,42 @@ static bool takeAnswer(FileOpsJobT *job, FileOpsQuestionE question, FileOpsAnswe } +// Add everything in folder dir to the totals, one pass per folder. +static bool treeSizeDir(const char *dir, uint64_t *bytes, int32_t *files, int32_t *folders) { + SizeScanT scan; + + scan.dir = dir; + scan.bytes = bytes; + scan.files = files; + scan.folders = folders; + scan.ok = true; + + return fileOpsSysScan(dir, false, treeSizeEntry, &scan) && scan.ok; +} + + +// One entry of a folder treeSizeDir reads: a file's size, or a folder +// counted and walked. +static bool treeSizeEntry(const FileOpsSysEntryT *e, void *ctx) { + SizeScanT *scan = (SizeScanT *)ctx; + char child[DVX_MAX_PATH]; + + if (!e->isDir) { + *scan->bytes += e->size; + (*scan->files)++; + return true; + } + + (*scan->folders)++; + + if (!fileOpsPathJoin(child, sizeof(child), scan->dir, e->name) || !treeSizeDir(child, scan->bytes, scan->files, scan->folders)) { + scan->ok = false; + } + + return true; +} + + // Add path (and, for a folder, everything below it) to the totals. static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders) { struct stat st; @@ -1256,34 +1384,5 @@ static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int3 return true; } - DIR *d = opendir(path); - - if (!d) { - return false; - } - - struct dirent *ent; - bool ok = true; - - while ((ent = readdir(d)) != NULL) { - char child[DVX_MAX_PATH]; - - if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { - continue; - } - - if (!fileOpsPathJoin(child, sizeof(child), path, ent->d_name) || statItem(child, &st) != 0) { - ok = false; - continue; - } - - if (S_ISDIR(st.st_mode)) { - (*folders)++; - } - - ok = treeSizeWalk(child, bytes, files, folders) && ok; - } - - closedir(d); - return ok; + return treeSizeDir(path, bytes, files, folders); } diff --git a/src/libs/kpunch/fileops/fileOps.h b/src/libs/kpunch/fileops/fileOps.h index a699ad21..7e1ca9e0 100644 --- a/src/libs/kpunch/fileops/fileOps.h +++ b/src/libs/kpunch/fileops/fileOps.h @@ -176,6 +176,11 @@ const char *fileOpsExtension(const char *name); // "YYYY-MM-DD HH:MM" in local time. void fileOpsFormatDate(time_t t, char *out, int32_t outSize); +// The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", or +// with ":SS" added), local time. False for anything else, a date that +// does not exist included. +bool fileOpsParseDate(const char *text, time_t *out); + // "1,234,567" void fileOpsFormatSize(uint64_t bytes, char *out, int32_t outSize); @@ -266,10 +271,18 @@ FileOpsStepE fileOpsJobRun(FileOpsJobT *job, const FileOpsCallbacksT *cb); FileOpsStepE fileOpsJobStep(FileOpsJobT *job); // ------------------------------------------------------------ -// DVX BASIC entry points (see fileops.bas). Paths are strings; success -// is True (-1), failure False (0). +// DVX BASIC entry points (see fileops.bas and fileOpsBas.c). Paths are +// strings; success is True (-1), failure False (0). Listings and jobs +// are handles (0 = none), freed with the program that made them if it +// does not free them itself. A returned string stays valid until the +// next call returning one. // ------------------------------------------------------------ +const char *FileOpsBaseName(const char *path); + +// The letter of the drive the system started from, "" where none. +const char *FileOpsBootDrive(void); + // Copy src (a file, or a folder with everything in it) into folder // destDir. overwrite nonzero replaces existing files, zero keeps them. // Errors skip the item (and make the result False). @@ -281,10 +294,114 @@ int32_t FileOpsDeleteTree(const char *path); // Drive letters that exist, e.g. "ACD". const char *FileOpsDriveList(void); -// Free space in KB on the drive holding path, -1 when unknown. +// PlatformDriveTypeE of the drive drive names (by its first letter), -1 +// when there is no such drive. +int32_t FileOpsDriveType(const char *drive); + +// Why the last FileOpsMakeDir / FileOpsRename / FileOpsReplaceFile / +// FileOpsValidName failed. +const char *FileOpsError(void); + +const char *FileOpsExtension(const char *name); + +// platformFilterMatch. +int32_t FileOpsFilterMatch(const char *name, const char *filter); +const char *FileOpsFormatSize(double bytes); + +// Free bytes / KB on the drive holding path, -1 when unknown. +double FileOpsFreeBytes(const char *path); int32_t FileOpsFreeKb(const char *path); +// Attribute bits (FILEOPS_ATTR_*), -1 when path cannot be read. +int32_t FileOpsGetAttr(const char *path); +int32_t FileOpsIsFolder(const char *path); + +// platformDirIsWritable. +int32_t FileOpsIsWritable(const char *dir); + +// Jobs: create (op 0 copy, 1 move, 2 delete), add sources, then run in +// time slices (FileOpsJobRunFor returns a FileOpsStepE), answering Ask / +// Error steps; progress as files done / total, percent and current path. +int32_t FileOpsJobAddSource(int32_t job, const char *path); +void FileOpsJobAnswer(int32_t job, int32_t answer); +double FileOpsJobBytesDone(int32_t job); +double FileOpsJobBytesTotal(int32_t job); +void FileOpsJobCancel(int32_t job); +void FileOpsJobClearAttr(int32_t job, int32_t attr); +int32_t FileOpsJobCreate(int32_t op, const char *destDir); +const char *FileOpsJobCurrent(int32_t job); +int32_t FileOpsJobFilesDone(int32_t job); +int32_t FileOpsJobFilesTotal(int32_t job); +void FileOpsJobFree(int32_t job); +const char *FileOpsJobMessage(int32_t job); +int32_t FileOpsJobPercent(int32_t job); +int32_t FileOpsJobRunFor(int32_t job, int32_t milliseconds); + +// Listings: read a folder (sorted by name, folders first), sort it, read +// its entries, or all of them as ListView SetRows text (name, size, type, +// date per row). +int32_t FileOpsList(const char *dir, const char *filter); +int32_t FileOpsListCount(int32_t list); +int32_t FileOpsListFind(int32_t list, const char *name); +void FileOpsListFree(int32_t list); +int32_t FileOpsListIsFolder(int32_t list, int32_t index); +const char *FileOpsListName(int32_t list, int32_t index); +const char *FileOpsListRows(int32_t list); +double FileOpsListSize(int32_t list, int32_t index); +void FileOpsListSort(int32_t list, int32_t column, int32_t descending); + +int32_t FileOpsMakeDir(const char *dir, const char *name); + +// Make path and every missing folder above it (True when it exists then). +int32_t FileOpsMakePath(const char *path); + +// "YYYY-MM-DD HH:MM", "" when path cannot be read. +const char *FileOpsModified(const char *path); + // Move src into folder destDir (see FileOpsCopyTree). int32_t FileOpsMoveTree(const char *src, const char *destDir, int32_t overwrite); +// Paths; the string ones return "" where the C function returns false. +int32_t FileOpsPathIsAbsolute(const char *path); +int32_t FileOpsPathIsInside(const char *path, const char *dir); +int32_t FileOpsPathIsRoot(const char *path); +const char *FileOpsPathJoin(const char *dir, const char *name); +const char *FileOpsPathNormalize(const char *path); +const char *FileOpsPathParent(const char *path); +int32_t FileOpsPathSame(const char *a, const char *b); + +// DVX_PATH_SEP. +const char *FileOpsPathSep(void); + +// platformIsReadOnly. +int32_t FileOpsReadOnlySystem(void); + +int32_t FileOpsRename(const char *dir, const char *oldName, const char *newName); + +// Atomic save: write FileOpsTempPath(path) first, then swap it in for path +// (platformReplaceFile; the original is restored if the swap fails). +int32_t FileOpsReplaceFile(const char *tmpPath, const char *path); +int32_t FileOpsSetAttr(const char *path, int32_t attr); + +// Modification time from FileOpsModified's text (fileOpsParseDate). +int32_t FileOpsSetModified(const char *path, const char *stamp); + +// platformTempDir. +const char *FileOpsTempDir(void); +const char *FileOpsTempPath(const char *path); + +// Size in bytes of the disk holding path, -1 when unknown. +double FileOpsTotalBytes(const char *path); + +// FileOpsTreeSize totals the bytes, files and folders of path for the +// three readers after it. +double FileOpsTreeBytes(void); +int32_t FileOpsTreeFiles(void); +int32_t FileOpsTreeFolders(void); +int32_t FileOpsTreeSize(const char *path); + +// True when name is a valid file name here (fileOpsValidateName); +// FileOpsError$ gives the reason when it is not. +int32_t FileOpsValidName(const char *name); + #endif // FILE_OPS_H diff --git a/src/libs/kpunch/fileops/fileOpsBas.c b/src/libs/kpunch/fileops/fileOpsBas.c index 71e3fa11..c9444e60 100644 --- a/src/libs/kpunch/fileops/fileOpsBas.c +++ b/src/libs/kpunch/fileops/fileOpsBas.c @@ -23,31 +23,102 @@ // fileOpsBas.c -- DVX BASIC entry points of the file operations library // // Called through DECLARE LIBRARY (src/include/basic/fileops.bas), so the -// signatures use only BASIC-marshalable types: strings in, LONG or STRING -// out, True (-1) / False (0) for success. Each tree operation runs a whole -// job without prompting. +// signatures use only BASIC-marshalable types: strings and LONG / DOUBLE +// in, LONG / DOUBLE / STRING out, True (-1) / False (0) for success. A +// returned string stays valid until the next call that returns one (the +// BASIC runtime copies it at once). +// +// The tree operations run a whole job without prompting. Listings and +// jobs are handed to BASIC as handles (1, 2, ...; 0 means none): each +// belongs to the program that made it, and whatever that program leaves +// open when it ends (or is ended) is freed then -- a job closes its files +// and removes a partly copied one. #include "fileOps.h" +#include "dvxMem.h" #include "dvxPlat.h" +#include "stb_ds_wrap.h" +#include +#include #include +#include +#include +#include +#include +#include -#define BAS_TRUE (-1) -#define BAS_FALSE 0 -#define FREE_UNKNOWN (-1) -#define BYTES_PER_KB 1024 +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define FREE_UNKNOWN (-1) +#define DRIVE_UNKNOWN (-1) +#define ATTR_UNKNOWN (-1) +#define LIST_NOT_FOUND (-1) +#define BYTES_PER_KB 1024 +#define DRIVE_TEXT_MAX 2 // a drive letter and its NUL +#define ROW_CELL_SEP '\t' // between the cells of a FileOpsListRows$ row +#define ROW_END '\n' // after each row +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" typedef struct { bool yesToAll; // replace existing / delete read-only files int32_t problems; // errors that skipped an item } BasRunT; +// One handle: a listing or a job, and the program it belongs to. +typedef struct { + FileOpsEntryT *entries; // listing (NULL for a job) + FileOpsJobT *job; // job (NULL for a listing) + int32_t count; // entries in the listing + int32_t appId; // owner: freed when it ends + bool used; +} BasHandleT; + +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// the library loads before the shell, and the host tests have none. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); + +// ============================================================ +// Module state +// ============================================================ + +static BasHandleT *sHandles = NULL; // stb_ds; handle = index + 1 +static bool sCleanupHooked = false; +static char sError[FILEOPS_MSG_MAX]; // why the last MakeDir / Rename / ReplaceFile / ValidName failed +static char *sText = NULL; // stb_ds: the last string result +static uint64_t sTreeBytes = 0; // the last FileOpsTreeSize +static int32_t sTreeFiles = 0; +static int32_t sTreeFolders = 0; + // ============================================================ // Prototypes // ============================================================ -static FileOpsAnswerE basAnswer(FileOpsJobT *job, FileOpsStepE kind, const char *message, void *ctx); -static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll); +static void appEnded(int32_t appId); +static FileOpsAnswerE basAnswer(FileOpsJobT *job, FileOpsStepE kind, const char *message, void *ctx); +static double diskBytes(const char *path, bool total); +static int32_t handleNew(FileOpsEntryT *entries, int32_t count, FileOpsJobT *job); +static BasHandleT *handleOf(int32_t handle); +static void handleRelease(BasHandleT *hd); +static FileOpsJobT *jobOf(int32_t job); +static bool jobProgress(int32_t job, FileOpsProgressT *p); +static const FileOpsEntryT *listEntry(int32_t list, int32_t index); +static BasHandleT *listOf(int32_t list); +static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll); +static void textAppend(const char *s); +static const char *textOf(const char *s); + + +// Shell app-cleanup hook: free what a program that ended left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + if (sHandles[i].used && sHandles[i].appId == appId) { + handleRelease(&sHandles[i]); + } + } +} // Questions get the caller's overwrite choice; errors are counted and the @@ -67,6 +138,36 @@ static FileOpsAnswerE basAnswer(FileOpsJobT *job, FileOpsStepE kind, const char } +// Bytes on the disk holding path: its size (total) or its free space; +// -1 when unknown. A DOUBLE holds every disk size exactly. +static double diskBytes(const char *path, bool total) { + uint64_t totalBytes; + uint64_t freeBytes; + + if (!platformDiskSpace(path, &totalBytes, &freeBytes)) { + return FREE_UNKNOWN; + } + + return (double)(total ? totalBytes : freeBytes); +} + + +const char *FileOpsBaseName(const char *path) { + return textOf(platformPathBaseName(path)); +} + + +// The letter of the drive the system started from ("C"), "" where there +// is none. +const char *FileOpsBootDrive(void) { + static char letter[DRIVE_TEXT_MAX]; + + letter[0] = platformBootDrive(); + letter[1] = '\0'; + return letter; +} + + int32_t FileOpsCopyTree(const char *src, const char *destDir, int32_t overwrite) { return runTree(FileOpsCopyE, src, destDir, overwrite != 0); } @@ -91,25 +192,633 @@ const char *FileOpsDriveList(void) { } -int32_t FileOpsFreeKb(const char *path) { - uint64_t totalBytes; - uint64_t freeBytes; +// The kind of drive (PlatformDriveTypeE: fixed, removable, remote) whose +// letter starts drive ("C", "c:", "C:\\..."; "/" elsewhere than DOS), -1 +// for a drive that does not exist. +int32_t FileOpsDriveType(const char *drive) { + PlatformDriveT drives[PLATFORM_DRIVE_MAX]; + int32_t count = platformDriveList(drives, PLATFORM_DRIVE_MAX); - if (!platformDiskSpace(path, &totalBytes, &freeBytes)) { + for (int32_t i = 0; drive && drive[0] && i < count; i++) { + if (toupper((unsigned char)drives[i].root[0]) == toupper((unsigned char)drive[0])) { + return (int32_t)drives[i].type; + } + } + + return DRIVE_UNKNOWN; +} + + +const char *FileOpsError(void) { + return sError; +} + + +const char *FileOpsExtension(const char *name) { + return textOf(fileOpsExtension(name)); +} + + +// True when itemName matches filter by the file dialogs' rule +// (platformFilterMatch: "*.txt", "*.bmp;*.png", "*.*"). +int32_t FileOpsFilterMatch(const char *name, const char *filter) { + return platformFilterMatch(name ? name : "", filter ? filter : "") ? BAS_TRUE : BAS_FALSE; +} + + +// "1,234,567" (a negative count is shown as 0) +const char *FileOpsFormatSize(double bytes) { + char text[FILEOPS_SIZE_TEXT_MAX]; + + fileOpsFormatSize(bytes > 0 ? (uint64_t)bytes : 0, text, sizeof(text)); + return textOf(text); +} + + +// Free bytes on the drive holding path, or -1 when unknown. +double FileOpsFreeBytes(const char *path) { + return diskBytes(path, false); +} + + +// Free KB on the drive holding path (diskBytes), -1 when unknown. +int32_t FileOpsFreeKb(const char *path) { + double bytes = diskBytes(path, false); + + if (bytes < 0) { return FREE_UNKNOWN; } - uint64_t kb = freeBytes / BYTES_PER_KB; + double kb = bytes / BYTES_PER_KB; return kb > INT32_MAX ? INT32_MAX : (int32_t)kb; } +int32_t FileOpsGetAttr(const char *path) { + uint8_t attr = 0; + + return fileOpsGetAttr(path, &attr) ? (int32_t)attr : ATTR_UNKNOWN; +} + + +int32_t FileOpsIsFolder(const char *path) { + struct stat st; + + return (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) ? BAS_TRUE : BAS_FALSE; +} + + +// True when files can be made in folder. +int32_t FileOpsIsWritable(const char *dir) { + return (dir && dir[0] && platformDirIsWritable(dir)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FileOpsJobAddSource(int32_t job, const char *path) { + FileOpsJobT *j = jobOf(job); + + return (j && fileOpsJobAddSource(j, path)) ? BAS_TRUE : BAS_FALSE; +} + + +void FileOpsJobAnswer(int32_t job, int32_t answer) { + FileOpsJobT *j = jobOf(job); + + if (j && answer >= FileOpsAnswerYesE && answer <= FileOpsAnswerSkipE) { + fileOpsJobAnswer(j, (FileOpsAnswerE)answer); + } +} + + +double FileOpsJobBytesDone(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return (double)p.bytesDone; +} + + +double FileOpsJobBytesTotal(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return (double)p.bytesTotal; +} + + +void FileOpsJobCancel(int32_t job) { + FileOpsJobT *j = jobOf(job); + + if (j) { + fileOpsJobCancel(j); + } +} + + +// Copy / move: clear these attribute bits (FILEOPS_ATTR_EDITABLE) on every +// file the job copies. +void FileOpsJobClearAttr(int32_t job, int32_t attr) { + FileOpsJobT *j = jobOf(job); + + if (j) { + fileOpsJobClearAttr(j, (uint8_t)(attr & FILEOPS_ATTR_EDITABLE)); + } +} + + +// A copy (0), move (1) or delete (2) into destDir; 0 for another op or +// when out of memory. +int32_t FileOpsJobCreate(int32_t op, const char *destDir) { + if (op < FileOpsCopyE || op > FileOpsDeleteE) { + return BAS_NO_HANDLE; + } + + FileOpsJobT *j = fileOpsJobCreate((FileOpsOpE)op, (destDir && destDir[0]) ? destDir : NULL); + + if (!j) { + return BAS_NO_HANDLE; + } + + int32_t handle = handleNew(NULL, 0, j); + + if (handle == BAS_NO_HANDLE) { + fileOpsJobFree(j); + } + + return handle; +} + + +const char *FileOpsJobCurrent(int32_t job) { + FileOpsProgressT p; + + return jobProgress(job, &p) ? textOf(p.current) : ""; +} + + +int32_t FileOpsJobFilesDone(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return p.filesDone; +} + + +int32_t FileOpsJobFilesTotal(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return p.filesTotal; +} + + +void FileOpsJobFree(int32_t job) { + BasHandleT *hd = handleOf(job); + + if (hd && hd->job) { + handleRelease(hd); + } +} + + +const char *FileOpsJobMessage(int32_t job) { + FileOpsJobT *j = jobOf(job); + + return j ? textOf(fileOpsJobMessage(j)) : ""; +} + + +int32_t FileOpsJobPercent(int32_t job) { + FileOpsProgressT p; + + jobProgress(job, &p); + return p.percent; +} + + +// Step the job for up to milliseconds (at least one step): stops early when +// it needs an answer (Ask / Error), finishes or is cancelled. The step +// result (FileOpsStepE) of the last step; Cancelled for a bad handle. +int32_t FileOpsJobRunFor(int32_t job, int32_t milliseconds) { + FileOpsJobT *j = jobOf(job); + + if (!j) { + return FileOpsStepCancelledE; + } + + PlatformTicksT end = platformClock() + PLATFORM_MS_TO_TICKS(milliseconds > 0 ? milliseconds : 0); + FileOpsStepE step = fileOpsJobStep(j); + + while (step == FileOpsStepBusyE && platformClock() < end) { + step = fileOpsJobStep(j); + } + + return (int32_t)step; +} + + +// Read dir (files matching filter, "" for all; folders always) sorted by +// name, folders first. A handle, or 0 when the folder cannot be read. +int32_t FileOpsList(const char *dir, const char *filter) { + FileOpsEntryT *entries = NULL; + int32_t count = 0; + + if (!fileOpsListDir(dir, (filter && filter[0]) ? filter : NULL, &entries, &count)) { + return BAS_NO_HANDLE; + } + + fileOpsListSort(entries, count, FileOpsSortNameE, false); + + int32_t handle = handleNew(entries, count, NULL); + + if (handle == BAS_NO_HANDLE) { + fileOpsListFree(entries, count); + } + + return handle; +} + + +int32_t FileOpsListCount(int32_t list) { + BasHandleT *hd = listOf(list); + + return hd ? hd->count : 0; +} + + +// The index of the entry called name (case does not matter), -1 when none. +int32_t FileOpsListFind(int32_t list, const char *name) { + BasHandleT *hd = listOf(list); + + for (int32_t i = 0; hd && name && i < hd->count; i++) { + if (strcasecmp(hd->entries[i].name, name) == 0) { + return i; + } + } + + return LIST_NOT_FOUND; +} + + +void FileOpsListFree(int32_t list) { + BasHandleT *hd = listOf(list); + + if (hd) { + handleRelease(hd); + } +} + + +int32_t FileOpsListIsFolder(int32_t list, int32_t index) { + const FileOpsEntryT *e = listEntry(list, index); + + return (e && e->isDir) ? BAS_TRUE : BAS_FALSE; +} + + +const char *FileOpsListName(int32_t list, int32_t index) { + const FileOpsEntryT *e = listEntry(list, index); + + return e ? textOf(e->name) : ""; +} + + +// The listing as the rows of a file list (ListView SetRows): per entry, +// name, size ("1,234"; blank for a folder), type and modification date, +// separated by tabs, each row ending in a line feed. One call for the +// whole folder. +const char *FileOpsListRows(int32_t list) { + BasHandleT *hd = listOf(list); + + arrsetlen(sText, 0); + + for (int32_t i = 0; hd && i < hd->count; i++) { + const FileOpsEntryT *e = &hd->entries[i]; + char size[FILEOPS_SIZE_TEXT_MAX]; + char type[FILEOPS_TYPE_TEXT_MAX]; + char date[FILEOPS_DATE_TEXT_MAX]; + const char sep[] = { ROW_CELL_SEP, '\0' }; + const char end[] = { ROW_END, '\0' }; + + size[0] = '\0'; + + if (!e->isDir) { + fileOpsFormatSize(e->size, size, sizeof(size)); + } + + fileOpsTypeName(e, type, sizeof(type)); + fileOpsFormatDate(e->mtime, date, sizeof(date)); + + textAppend(e->name); + textAppend(sep); + textAppend(size); + textAppend(sep); + textAppend(type); + textAppend(sep); + textAppend(date); + textAppend(end); + } + + arrput(sText, '\0'); + return sText; +} + + +double FileOpsListSize(int32_t list, int32_t index) { + const FileOpsEntryT *e = listEntry(list, index); + + return e ? (double)e->size : 0; +} + + +// Folders first, then by column (0 name, 1 size, 2 type, 3 date), ties by +// name; descending reverses the order within each group. +void FileOpsListSort(int32_t list, int32_t column, int32_t descending) { + BasHandleT *hd = listOf(list); + + if (hd && column >= FileOpsSortNameE && column <= FileOpsSortDateE) { + fileOpsListSort(hd->entries, hd->count, (FileOpsSortE)column, descending != 0); + } +} + + +int32_t FileOpsMakeDir(const char *dir, const char *name) { + sError[0] = '\0'; + + return fileOpsMakeDir(dir, name, sError, sizeof(sError)) ? BAS_TRUE : BAS_FALSE; +} + + +// Make the folder path and every folder above it that is missing. True +// when it exists afterwards. +int32_t FileOpsMakePath(const char *path) { + return (path && path[0] && platformMkdirRecursive(path) == 0) ? BAS_TRUE : BAS_FALSE; +} + + +// "YYYY-MM-DD HH:MM", or "" when path cannot be read. +const char *FileOpsModified(const char *path) { + struct stat st; + char date[FILEOPS_DATE_TEXT_MAX]; + + if (stat(path, &st) != 0) { + return ""; + } + + fileOpsFormatDate(st.st_mtime, date, sizeof(date)); + return textOf(date); +} + + int32_t FileOpsMoveTree(const char *src, const char *destDir, int32_t overwrite) { return runTree(FileOpsMoveE, src, destDir, overwrite != 0); } +int32_t FileOpsPathIsAbsolute(const char *path) { + return (path && platformPathIsAbsolute(path)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FileOpsPathIsInside(const char *path, const char *dir) { + return (path && dir && fileOpsPathIsInside(path, dir)) ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FileOpsPathIsRoot(const char *path) { + return fileOpsPathIsRoot(path) ? BAS_TRUE : BAS_FALSE; +} + + +const char *FileOpsPathJoin(const char *dir, const char *name) { + char path[DVX_MAX_PATH]; + + return fileOpsPathJoin(path, sizeof(path), dir, name) ? textOf(path) : ""; +} + + +const char *FileOpsPathNormalize(const char *path) { + char norm[DVX_MAX_PATH]; + + return fileOpsPathNormalize(path, norm, sizeof(norm)) ? textOf(norm) : ""; +} + + +const char *FileOpsPathParent(const char *path) { + char parent[DVX_MAX_PATH]; + + return fileOpsPathParent(path, parent, sizeof(parent)) ? textOf(parent) : ""; +} + + +int32_t FileOpsPathSame(const char *a, const char *b) { + return fileOpsPathSame(a, b) ? BAS_TRUE : BAS_FALSE; +} + + +// The character between folder names ("\\" on DOS). +const char *FileOpsPathSep(void) { + return DVX_PATH_SEP; +} + + +// True when DVX runs from media it cannot write (a CD-ROM). +int32_t FileOpsReadOnlySystem(void) { + return platformIsReadOnly() ? BAS_TRUE : BAS_FALSE; +} + + +int32_t FileOpsRename(const char *dir, const char *oldName, const char *newName) { + sError[0] = '\0'; + + return fileOpsRename(dir, oldName, newName, sError, sizeof(sError)) ? BAS_TRUE : BAS_FALSE; +} + + +// Swap the fully written file tmpPath in for path (an atomic save: the +// original is kept aside until the new one is in place, and comes back if +// the swap fails). True when done; when not, FileOpsError$ says why. +int32_t FileOpsReplaceFile(const char *tmpPath, const char *path) { + char bakPath[DVX_MAX_PATH]; + + sError[0] = '\0'; + + if (platformPathSibling(bakPath, sizeof(bakPath), path, PLATFORM_BAK_EXT) && platformReplaceFile(tmpPath, path, bakPath)) { + return BAS_TRUE; + } + + snprintf(sError, sizeof(sError), "%s could not be replaced.", path); + return BAS_FALSE; +} + + +int32_t FileOpsSetAttr(const char *path, int32_t attr) { + return fileOpsSetAttr(path, (uint8_t)attr) ? BAS_TRUE : BAS_FALSE; +} + + +// Set a file's modification time from FileOpsModified$'s form +// ("YYYY-MM-DD HH:MM", or with ":SS"). False for a stamp that is not a +// date or a file that cannot be changed. +int32_t FileOpsSetModified(const char *path, const char *stamp) { + time_t t; + + return (path && fileOpsParseDate(stamp, &t) && fileOpsSetTime(path, t)) ? BAS_TRUE : BAS_FALSE; +} + + +// The folder for scratch files (the TEMP or TMP setting), "" when +// neither is set. +const char *FileOpsTempDir(void) { + const char *dir = platformTempDir(); + + return dir ? dir : ""; +} + + +// The scratch file to write before FileOpsReplaceFile: path with its +// extension changed (FOO.BAS -> FOO.$BA), "" when there is none. +const char *FileOpsTempPath(const char *path) { + char tmpPath[DVX_MAX_PATH]; + + return platformPathSibling(tmpPath, sizeof(tmpPath), path, PLATFORM_TMP_EXT) ? textOf(tmpPath) : ""; +} + + +// Size in bytes of the disk holding path, or -1 when unknown. +double FileOpsTotalBytes(const char *path) { + return diskBytes(path, true); +} + + +double FileOpsTreeBytes(void) { + return (double)sTreeBytes; +} + + +int32_t FileOpsTreeFiles(void) { + return sTreeFiles; +} + + +int32_t FileOpsTreeFolders(void) { + return sTreeFolders; +} + + +// Totals of a file, or of a folder and everything in it (not counting the +// folder itself), for FileOpsTreeBytes / Files / Folders. True when every +// part could be read. +int32_t FileOpsTreeSize(const char *path) { + return fileOpsTreeSize(path, &sTreeBytes, &sTreeFiles, &sTreeFolders) ? BAS_TRUE : BAS_FALSE; +} + + +// True when name can be used as a file name on this system; when not, +// FileOpsError$ says why (on DOS: 8.3 names, no device names such as CON, +// no characters a disk does not allow). +int32_t FileOpsValidName(const char *name) { + sError[0] = '\0'; + + return fileOpsValidateName(name, sError, sizeof(sError)) ? BAS_TRUE : BAS_FALSE; +} + + +// A free slot (or a new one) for a listing or a job of the current +// program. The shell's cleanup hook is registered with the first one. +static int32_t handleNew(FileOpsEntryT *entries, int32_t count, FileOpsJobT *job) { + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sHandles) && sHandles[idx].used) { + idx++; + } + + if (idx == (int32_t)arrlen(sHandles)) { + BasHandleT blank; + + memset(&blank, 0, sizeof(blank)); + arrput(sHandles, blank); + + if (idx == (int32_t)arrlen(sHandles)) { + return BAS_NO_HANDLE; + } + } + + BasHandleT *hd = &sHandles[idx]; + + hd->entries = entries; + hd->count = count; + hd->job = job; + hd->appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + hd->used = true; + return idx + 1; +} + + +static BasHandleT *handleOf(int32_t handle) { + if (handle <= BAS_NO_HANDLE || handle > (int32_t)arrlen(sHandles) || !sHandles[handle - 1].used) { + return NULL; + } + + return &sHandles[handle - 1]; +} + + +static void handleRelease(BasHandleT *hd) { + fileOpsListFree(hd->entries, hd->count); + fileOpsJobFree(hd->job); + memset(hd, 0, sizeof(*hd)); +} + + +static FileOpsJobT *jobOf(int32_t job) { + BasHandleT *hd = handleOf(job); + + return hd ? hd->job : NULL; +} + + +// The progress of a job (all zero, current "", for a bad handle). False +// for a bad handle. +static bool jobProgress(int32_t job, FileOpsProgressT *p) { + FileOpsJobT *j = jobOf(job); + + memset(p, 0, sizeof(*p)); + p->current = ""; + + if (j) { + fileOpsJobProgress(j, p); + } + + return j != NULL; +} + + +static const FileOpsEntryT *listEntry(int32_t list, int32_t index) { + BasHandleT *hd = listOf(list); + + return (hd && index >= 0 && index < hd->count) ? &hd->entries[index] : NULL; +} + + +static BasHandleT *listOf(int32_t list) { + BasHandleT *hd = handleOf(list); + + return (hd && !hd->job) ? hd : NULL; +} + + // Run one job over one source; True only when nothing failed. static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll) { FileOpsJobT *job = fileOpsJobCreate(op, destDir); @@ -125,3 +834,20 @@ static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool fileOpsJobFree(job); return ok ? BAS_TRUE : BAS_FALSE; } + + +// Append s to the string result being built (without its NUL). +static void textAppend(const char *s) { + for (const char *p = s; *p; p++) { + arrput(sText, *p); + } +} + + +// s as the string result (a copy that outlives s). +static const char *textOf(const char *s) { + arrsetlen(sText, 0); + textAppend(s ? s : ""); + arrput(sText, '\0'); + return sText; +} diff --git a/src/libs/kpunch/fileops/fileOpsSys.c b/src/libs/kpunch/fileops/fileOpsSys.c index 45a6b6ee..aa11dbac 100644 --- a/src/libs/kpunch/fileops/fileOpsSys.c +++ b/src/libs/kpunch/fileops/fileOpsSys.c @@ -34,6 +34,7 @@ // Host: POSIX. #include "fileOps.h" +#include "fileOpsSys.h" #include "dvxPlat.h" #include @@ -41,6 +42,7 @@ #ifdef __DJGPP__ +#include #include #include #include @@ -68,9 +70,21 @@ #define DOS_TIME_HOUR_SHIFT 11 #define DOS_TIME_MIN_SHIFT 5 #define DOS_TIME_SEC_DIVISOR 2 +#define DOS_DATE_DAY_MASK 0x1F +#define DOS_DATE_MONTH_MASK 0x0F +#define DOS_TIME_HOUR_MASK 0x1F +#define DOS_TIME_MIN_MASK 0x3F +#define DOS_TIME_SEC_MASK 0x1F + +// Find first / find next: every file and folder (not the volume label), +// hidden and system ones too, as readdir lists them +#define DOS_FIND_ALL "*.*" +#define DOS_FIND_ATTR (FA_RDONLY | FA_HIDDEN | FA_SYSTEM | FA_DIREC | FA_ARCH) +#define DOS_SHORT_NAME_MAX 13 // "FILENAME.EXT" and its NUL #else +#include #include #define HOST_HIDDEN_PREFIX '.' @@ -83,8 +97,9 @@ // ============================================================ #ifdef __DJGPP__ -static bool dosInt(uint32_t intNo, __dpmi_regs *r); -static bool putPath(const char *path, __dpmi_regs *r); +static bool dosInt(uint32_t intNo, __dpmi_regs *r); +static time_t dosTime(uint16_t date, uint16_t time); +static bool putPath(const char *path, __dpmi_regs *r); #endif @@ -108,6 +123,25 @@ static bool dosInt(uint32_t intNo, __dpmi_regs *r) { #endif +#ifdef __DJGPP__ +// A DOS packed date and time as local time, exactly as the C library's +// stat converts them (mktime decides daylight saving time). +static time_t dosTime(uint16_t date, uint16_t time) { + struct tm tm; + + memset(&tm, 0, sizeof(tm)); + tm.tm_isdst = -1; + tm.tm_sec = (time & DOS_TIME_SEC_MASK) * DOS_TIME_SEC_DIVISOR; + tm.tm_min = (time >> DOS_TIME_MIN_SHIFT) & DOS_TIME_MIN_MASK; + tm.tm_hour = (time >> DOS_TIME_HOUR_SHIFT) & DOS_TIME_HOUR_MASK; + tm.tm_mday = date & DOS_DATE_DAY_MASK; + tm.tm_mon = ((date >> DOS_DATE_MONTH_SHIFT) & DOS_DATE_MONTH_MASK) - 1; + tm.tm_year = (date >> DOS_DATE_YEAR_SHIFT) + DOS_YEAR_BASE - TM_YEAR_BASE; + return mktime(&tm); +} +#endif + + // Host: read-only = no owner write permission, hidden = a dot-file; no // system or archive bits. bool fileOpsGetAttr(const char *path, uint8_t *attr) { @@ -223,6 +257,95 @@ bool fileOpsSetTime(const char *path, time_t mtime) { } +bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx) { + bool more = true; + +#ifdef __DJGPP__ + char pattern[DVX_MAX_PATH]; + struct ffblk ff; + bool lower = !_preserve_fncase(); + + (void)followLinks; + + if (!fileOpsPathJoin(pattern, sizeof(pattern), dir, DOS_FIND_ALL)) { + return false; + } + + int rc = findfirst(pattern, &ff, DOS_FIND_ATTR); + + // Nothing found: an empty root folder, or no folder at all + if (rc != 0) { + struct stat st; + + return stat(dir, &st) == 0 && S_ISDIR(st.st_mode); + } + + // The search runs to its end even after the callback said stop, so a + // long-name search handle is always closed by the library. + for (; rc == 0; rc = findnext(&ff)) { + char *name = ff.ff_name; + char shortName[DOS_SHORT_NAME_MAX]; + + if (!more || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + continue; + } + + // readdir's rule: a name that is its own short (8.3) name is shown + // in lower case + if (lower && strcmp(_lfn_gen_short_fname(name, shortName), name) == 0) { + for (char *c = name; *c; c++) { + if (*c >= 'A' && *c <= 'Z') { + *c = (char)(*c - 'A' + 'a'); + } + } + } + + FileOpsSysEntryT e; + + e.name = name; + e.isDir = (ff.ff_attrib & FA_DIREC) != 0; + e.size = e.isDir ? 0 : (uint64_t)ff.ff_fsize; + e.mtime = dosTime(ff.ff_fdate, ff.ff_ftime); + more = fn(&e, ctx); + } + + return true; +#else + DIR *d = opendir(dir); + + if (!d) { + return false; + } + + struct dirent *ent; + + while (more && (ent = readdir(d)) != NULL) { + char path[DVX_MAX_PATH]; + struct stat st; + + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + + if (!fileOpsPathJoin(path, sizeof(path), dir, ent->d_name) || (followLinks ? stat(path, &st) : lstat(path, &st)) != 0) { + continue; + } + + FileOpsSysEntryT e; + + e.name = ent->d_name; + e.isDir = S_ISDIR(st.st_mode); + e.size = e.isDir ? 0 : (uint64_t)st.st_size; + e.mtime = st.st_mtime; + more = fn(&e, ctx); + } + + closedir(d); + return true; +#endif +} + + #ifdef __DJGPP__ // Copy a NUL-terminated path into the transfer buffer and point DS:DX // at it. False when it does not fit. diff --git a/src/libs/kpunch/fileops/fileOpsSys.h b/src/libs/kpunch/fileops/fileOpsSys.h new file mode 100644 index 00000000..5d89b619 --- /dev/null +++ b/src/libs/kpunch/fileops/fileOpsSys.h @@ -0,0 +1,55 @@ +// 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. + +// fileOpsSys.h -- system layer of the file operations library (private) +// +// Shared by fileOps.c and fileOpsSys.c only; not part of the SDK. + +#ifndef FILE_OPS_SYS_H +#define FILE_OPS_SYS_H + +#include +#include +#include + +// One folder entry as fileOpsSysScan reports it. +typedef struct { + const char *name; // leaf name, valid during the callback + uint64_t size; // bytes (0 for a folder) + time_t mtime; // last modification + bool isDir; +} FileOpsSysEntryT; + +// Called for each entry; false stops the scan (no more calls). +typedef bool (*FileOpsScanFnT)(const FileOpsSysEntryT *e, void *ctx); + +// Every entry of dir except "." and ".." (hidden ones too) with its size, +// date and kind. DOS reads them in one pass of the folder (find first / +// find next, which report all of that), not one stat per entry -- a stat +// searches the folder again, so a listing would take quadratic time; +// names come back as readdir gives them (8.3 names in lower case unless +// FNCASE is set). Elsewhere readdir and stat (lstat unless followLinks: +// a link is then an entry of its own); an entry that cannot be read +// there is left out. False when dir cannot be read. +bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx); + +#endif // FILE_OPS_SYS_H diff --git a/src/libs/kpunch/fileops/fileops.dhs b/src/libs/kpunch/fileops/fileops.dhs index 25381c1b..0f65f23b 100644 --- a/src/libs/kpunch/fileops/fileops.dhs +++ b/src/libs/kpunch/fileops/fileops.dhs @@ -30,7 +30,9 @@ .index fileOpsJobRun .index fileOpsListDir .index fileOpsJobClearAttr +.index fileOpsParseDate .index FileOpsJobT +.index fileops.bas .h1 File Operations Library @@ -65,6 +67,7 @@ On DOS, touching a drive with no disk (or a write-protected or removed one) rais fileOpsTypeName(entry, out, size) "Folder", "Program" (.APP), "TXT File", "File". fileOpsFormatSize(bytes, out, size) "1,234,567". fileOpsFormatDate(t, out, size) "YYYY-MM-DD HH:MM" in local time. + fileOpsParseDate(text, &t) The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", optionally ":SS"), local time; false for anything else, a date that does not exist included. fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it. fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure. fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use. @@ -136,4 +139,6 @@ fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) .h2 BASIC -src/include/basic/fileops.bas declares BASIC wrappers: FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree (a whole folder, without prompting), FileOpsDriveList$ and FileOpsFreeKb. Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements. +src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value. + +Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements. diff --git a/src/libs/kpunch/game/game.dhs b/src/libs/kpunch/game/game.dhs index b8859fc5..415709d6 100644 --- a/src/libs/kpunch/game/game.dhs +++ b/src/libs/kpunch/game/game.dhs @@ -91,4 +91,4 @@ On an 8-bit desktop every window shares the desktop palette (dvxGetDisplay(ctx)- .h2 BASIC -src/include/basic/game.bas declares GameKeyDown(keyCode), True while the key is held and one of the calling program's windows has the focus, with GAMEKEY_* constants for common keys. The BASIC runtime gives the library the GUI context (gameBasSetContext) when a program starts. Frame pacing from BASIC comes with the BASIC game statements later. +src/include/basic/game.bas declares GameKeyDown(keyCode), True while the key is held and one of the calling program's windows has the focus, with GAMEKEY_* constants for common keys; GameKeyClear (platformKeyStateClear, only while the calling program's window has the focus); GameWaitRetrace (platformVideoWaitRetrace); and FrameMissed(), the missed count of the BASIC runtime's own FRAMEWAIT pacer (GameFramePacerT.missed since FRAMEWAIT last started at a new rate; 0 before the first FRAMEWAIT). The BASIC runtime gives the library the GUI context (gameBasSetContext) when a program starts, and runs FRAMEWAIT on gameFrameInit / gameFrameNext. diff --git a/src/libs/kpunch/game/game.h b/src/libs/kpunch/game/game.h index d9251e05..d651c5b9 100644 --- a/src/libs/kpunch/game/game.h +++ b/src/libs/kpunch/game/game.h @@ -117,8 +117,13 @@ uint8_t gamePalNearest(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b); int32_t gameTimerAdd(const AppContextT *ctx, uint32_t hz, PlatformTimerFnT fn, void *userData); // BASIC (src/include/basic/game.bas). The runtime hands over its GUI -// context when it starts a program. +// context when it starts a program. GameKeyClear (platformKeyStateClear) +// and GameKeyDown act only while the calling program's window has the +// focus; GameWaitRetrace is platformVideoWaitRetrace. FrameMissed, also +// declared in game.bas, is the BASIC runtime's (FRAMEWAIT's pacer). void gameBasSetContext(AppContextT *ctx); +void GameKeyClear(void); int32_t GameKeyDown(int32_t rawCode); +void GameWaitRetrace(void); #endif // GAME_LIB_H diff --git a/src/libs/kpunch/game/gameBas.c b/src/libs/kpunch/game/gameBas.c index 88781eb6..a8a631fb 100644 --- a/src/libs/kpunch/game/gameBas.c +++ b/src/libs/kpunch/game/gameBas.c @@ -39,8 +39,11 @@ static AppContextT *sBasCtx = NULL; // Prototypes // ============================================================ -void gameBasSetContext(AppContextT *ctx); -int32_t GameKeyDown(int32_t rawCode); +void gameBasSetContext(AppContextT *ctx); +void GameKeyClear(void); +int32_t GameKeyDown(int32_t rawCode); +void GameWaitRetrace(void); +static bool ownsFocus(void); void gameBasSetContext(AppContextT *ctx) { @@ -48,14 +51,37 @@ void gameBasSetContext(AppContextT *ctx) { } -// The focused window must belong to the program asking (currentAppId is -// the calling program while its BASIC code runs). +// Every key reads as up until it is pressed again (or repeats); only +// while the program's own window has the focus, as GameKeyDown. +void GameKeyClear(void) { + if (ownsFocus()) { + platformKeyStateClear(); + } +} + + int32_t GameKeyDown(int32_t rawCode) { - if (!sBasCtx || sBasCtx->stack.focusedIdx < 0) { + if (!ownsFocus()) { return BAS_FALSE; } - const WindowT *win = sBasCtx->stack.windows[sBasCtx->stack.focusedIdx]; - - return (win->appId == sBasCtx->currentAppId && gameKeyIsDown(win, rawCode)) ? BAS_TRUE : BAS_FALSE; + return gameKeyIsDown(sBasCtx->stack.windows[sBasCtx->stack.focusedIdx], rawCode) ? BAS_TRUE : BAS_FALSE; +} + + +// Wait for the start of the next vertical retrace (a full-screen game +// draws right after it so the picture does not tear). +void GameWaitRetrace(void) { + platformVideoWaitRetrace(); +} + + +// The focused window belongs to the program asking (currentAppId is the +// calling program while its BASIC code runs). +static bool ownsFocus(void) { + if (!sBasCtx || sBasCtx->stack.focusedIdx < 0) { + return false; + } + + return sBasCtx->stack.windows[sBasCtx->stack.focusedIdx]->appId == sBasCtx->currentAppId; } diff --git a/src/libs/kpunch/hlpfile/Makefile b/src/libs/kpunch/hlpfile/Makefile new file mode 100644 index 00000000..487e0022 --- /dev/null +++ b/src/libs/kpunch/hlpfile/Makefile @@ -0,0 +1,63 @@ +# 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 File Library Makefile for DJGPP cross-compilation +# +# Help file library (hlpfile.lib): reads compiled DVX help files (.hlp) +# for the Help Viewer, the HelpPane widget and BASIC. See hlpFile.h. + +include ../../../common.mk + +CFLAGS = $(DOS_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty + +OBJDIR = ../../../../obj/hlpfile +LIBSDIR = ../../../../bin/libs + +SRCS = $(wildcard *.c) +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGETDIR = $(LIBSDIR)/kpunch/hlpfile +TARGET = $(TARGETDIR)/hlpfile.lib + +.PHONY: all clean + +all: $(TARGET) $(TARGETDIR)/hlpfile.dep + +$(TARGETDIR)/hlpfile.dep: hlpfile.dep | $(TARGETDIR) + sed 's/$$/\r/' $< > $@ + +$(TARGET): $(OBJS) | $(TARGETDIR) + $(DXE3GEN) -o $(TARGETDIR)/hlpfile.dxe -U $(OBJS) + mv $(TARGETDIR)/hlpfile.dxe $@ + +$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(TARGETDIR): + mkdir -p $(TARGETDIR) + +-include $(OBJS:.o=.d) + +clean: + rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/hlpfile.dep $(TARGETDIR) diff --git a/src/libs/kpunch/hlpfile/hlpFile.c b/src/libs/kpunch/hlpfile/hlpFile.c new file mode 100644 index 00000000..c580af22 --- /dev/null +++ b/src/libs/kpunch/hlpfile/hlpFile.c @@ -0,0 +1,692 @@ +// 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. + +// hlpFile.c -- DVX help file (.hlp) reader (see hlpFile.h) +// +// The header is at the end of the file. Open reads the topic directory, +// string table, table of contents, keyword index and trigram search +// section into memory; topic content and images are read from the file +// when asked for. +// +// Search is full text: the trigram index the help compiler writes narrows +// the candidate topics, then each candidate's text is checked for every +// search word. + +#include "hlpFile.h" +#include "dvxPlat.h" +#include "dvxTypes.h" + +#include +#include +#include +#include +#include + +// ABI stamp checked by the loader (see DVX_ABI_VERSION). +DVX_ABI_STAMP(hlpfile); + +// Characters that separate search words. +#define HLP_SEARCH_SEPS " \t" + +struct HlpFileT { + FILE *fp; + HlpHeaderT header; + HlpTopicDirT *topicDir; + char *stringTable; + HlpTocEntryT *tocEntries; + HlpIndexEntryT *indexEntries; + uint8_t *searchData; // search section, NULL if absent + uint32_t trigramCount; +}; + +// Search words for topicMatchRecord. +typedef struct { + char **terms; + bool *found; + int32_t termCount; + int32_t foundCount; +} HlpSearchT; + +// ============================================================ +// Prototypes +// ============================================================ + +static bool containsNoCase(const char *hay, const char *needle); +static const HlpTrigramEntryT *findTrigram(const HlpFileT *f, const uint8_t *tri); +void hlpFileClose(HlpFileT *f); +int32_t hlpFileFindTopic(const HlpFileT *f, const char *topicId); +bool hlpFileForEachRecord(HlpFileT *f, int32_t topic, HlpRecordFnT fn, void *userData); +const HlpIndexEntryT *hlpFileIndex(const HlpFileT *f, int32_t i); +int32_t hlpFileIndexCount(const HlpFileT *f); +HlpFileT *hlpFileOpen(const char *path); +uint8_t *hlpFileReadImage(HlpFileT *f, const HlpImageRefT *ref, uint32_t *outSize); +int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches); +int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId); +const char *hlpFileString(const HlpFileT *f, uint32_t offset); +const HlpTocEntryT *hlpFileToc(const HlpFileT *f, int32_t i); +int32_t hlpFileTocCount(const HlpFileT *f); +int32_t hlpFileTopicCount(const HlpFileT *f); +const char *hlpFileTopicId(const HlpFileT *f, int32_t topic); +const char *hlpFileTopicTitle(const HlpFileT *f, int32_t topic); +bool hlpRecordImage(const HlpRecordHdrT *hdr, const char *payload, HlpImageRefT *ref); +bool hlpRecordLink(const HlpRecordHdrT *hdr, const char *payload, const char **target, const char **display); +char *hlpWrapText(const char *text, int32_t cols); +static bool readSearchIndex(HlpFileT *f); +static bool readTable(HlpFileT *f, void **out, uint32_t offset, uint32_t count, uint32_t entrySize); +static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery, uint8_t *matches, uint8_t *inPosting); +static bool topicMatchRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); +static void trigramFilter(const HlpFileT *f, const HlpSearchT *search, uint8_t *matches, uint8_t *inPosting, bool *titlesOnly); + + +// Case-insensitive substring test. +static bool containsNoCase(const char *hay, const char *needle) { + size_t needleLen = strlen(needle); + + for (const char *p = hay; *p; p++) { + if (strncasecmp(p, needle, needleLen) == 0) { + return true; + } + } + + return false; +} + + +// Binary search of the (sorted) trigram table. NULL when no topic +// contains the trigram. +static const HlpTrigramEntryT *findTrigram(const HlpFileT *f, const uint8_t *tri) { + const HlpTrigramEntryT *entries = (const HlpTrigramEntryT *)(f->searchData + sizeof(HlpSearchHeaderT)); + int32_t lo = 0; + int32_t hi = (int32_t)f->trigramCount - 1; + + while (lo <= hi) { + int32_t mid = lo + (hi - lo) / 2; + int32_t cmp = memcmp(entries[mid].trigram, tri, HLP_TRIGRAM_LEN); + + if (cmp == 0) { + return &entries[mid]; + } + + if (cmp < 0) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + return NULL; +} + + +void hlpFileClose(HlpFileT *f) { + if (!f) { + return; + } + + if (f->fp) { + fclose(f->fp); + } + + free(f->topicDir); + free(f->stringTable); + free(f->tocEntries); + free(f->indexEntries); + free(f->searchData); + free(f); +} + + +// Linear search -- the topic directory is small. +int32_t hlpFileFindTopic(const HlpFileT *f, const char *topicId) { + if (!f || !topicId) { + return -1; + } + + for (uint32_t i = 0; i < f->header.topicCount; i++) { + if (strcmp(hlpFileString(f, f->topicDir[i].topicIdStr), topicId) == 0) { + return (int32_t)i; + } + } + + return -1; +} + + +bool hlpFileForEachRecord(HlpFileT *f, int32_t topic, HlpRecordFnT fn, void *userData) { + if (!f || topic < 0 || (uint32_t)topic >= f->header.topicCount) { + return false; + } + + const HlpTopicDirT *dir = &f->topicDir[topic]; + uint32_t bytesRead = 0; + char *payload = NULL; + uint32_t payloadSz = 0; + bool ok = true; + + if (fseek(f->fp, dir->contentOffset, SEEK_SET) != 0) { + return false; + } + + while (bytesRead < dir->contentSize) { + HlpRecordHdrT recHdr; + + if (fread(&recHdr, sizeof(recHdr), 1, f->fp) != 1) { + ok = false; + break; + } + + bytesRead += sizeof(recHdr); + + if (recHdr.type == HLP_REC_END) { + break; + } + + // One buffer serves every record of the topic, grown as needed. + if (recHdr.length + 1u > payloadSz) { + char *grown = (char *)realloc(payload, recHdr.length + 1u); + + if (!grown) { + ok = false; + break; + } + + payload = grown; + payloadSz = recHdr.length + 1u; + } + + if (fread(payload, 1, recHdr.length, f->fp) != recHdr.length) { + ok = false; + break; + } + + payload[recHdr.length] = '\0'; + bytesRead += recHdr.length; + + if (!fn(&recHdr, payload, userData)) { + break; + } + } + + free(payload); + return ok; +} + + +const HlpIndexEntryT *hlpFileIndex(const HlpFileT *f, int32_t i) { + if (!f || i < 0 || (uint32_t)i >= f->header.indexCount) { + return NULL; + } + + return &f->indexEntries[i]; +} + + +int32_t hlpFileIndexCount(const HlpFileT *f) { + return f ? (int32_t)f->header.indexCount : 0; +} + + +HlpFileT *hlpFileOpen(const char *path) { + HlpFileT *f = (HlpFileT *)calloc(1, sizeof(HlpFileT)); + + if (!f || !path) { + free(f); + return NULL; + } + + f->fp = fopen(path, "rb"); + + // The header is at the end of the file + bool ok = f->fp && + fseek(f->fp, -(long)sizeof(HlpHeaderT), SEEK_END) == 0 && + fread(&f->header, sizeof(HlpHeaderT), 1, f->fp) == 1 && + f->header.magic == HLP_MAGIC && + f->header.version == HLP_VERSION && + readTable(f, (void **)&f->topicDir, f->header.topicDirOffset, f->header.topicCount, sizeof(HlpTopicDirT)) && + readTable(f, (void **)&f->stringTable, f->header.stringTableOffset, f->header.stringTableSize, 1) && + readTable(f, (void **)&f->tocEntries, f->header.tocOffset, f->header.tocCount, sizeof(HlpTocEntryT)) && + readTable(f, (void **)&f->indexEntries, f->header.indexOffset, f->header.indexCount, sizeof(HlpIndexEntryT)); + + if (!ok) { + hlpFileClose(f); + return NULL; + } + + // Force-terminate the table so a truncated or corrupt file cannot make + // a string walk past the allocation. + if (f->header.stringTableSize > 0) { + f->stringTable[f->header.stringTableSize - 1] = '\0'; + } + + // A missing or damaged search index only makes searching slower + // (every topic is scanned), so it does not fail the open. + if (!readSearchIndex(f)) { + free(f->searchData); + f->searchData = NULL; + f->trigramCount = 0; + } + + return f; +} + + +// The record walk reads sequentially, so its file position is kept. +uint8_t *hlpFileReadImage(HlpFileT *f, const HlpImageRefT *ref, uint32_t *outSize) { + if (!f || !ref || ref->imageSize == 0) { + return NULL; + } + + // Both offsets come from the file: a sum that wraps past 4 GB (a + // damaged file) fails instead of reading some other part of the file. + uint32_t imagePos = f->header.imagePoolOffset + ref->imageOffset; + long savedPos = ftell(f->fp); + uint8_t *data = imagePos >= ref->imageOffset ? (uint8_t *)malloc(ref->imageSize) : NULL; + + if (data && (fseek(f->fp, (long)imagePos, SEEK_SET) != 0 || fread(data, 1, ref->imageSize, f->fp) != ref->imageSize)) { + free(data); + data = NULL; + } + + fseek(f->fp, savedPos, SEEK_SET); + + if (data && outSize) { + *outSize = ref->imageSize; + } + + return data; +} + + +int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches) { + if (!f || !query || !matches) { + return -1; + } + + int32_t maxTerms = (int32_t)(strlen(query) / 2 + 1); + char *words = strdup(query); + char *wholeQuery = strdup(dvxSkipWs(query)); + uint8_t *inPosting = (uint8_t *)malloc(f->header.topicCount + 1); + int32_t matchCount = -1; + HlpSearchT search; + + search.terms = (char **)malloc(sizeof(char *) * maxTerms); + search.found = (bool *)malloc(sizeof(bool) * maxTerms); + search.termCount = 0; + search.foundCount = 0; + + if (words && wholeQuery && inPosting && search.terms && search.found) { + // Lowercase for the trigram lookup (text matching ignores case) + for (char *p = words; *p; p++) { + if (*p >= 'A' && *p <= 'Z') { + *p = (char)(*p - 'A' + 'a'); + } + } + + dvxTrimRight(wholeQuery); + + for (char *tok = strtok(words, HLP_SEARCH_SEPS); tok; tok = strtok(NULL, HLP_SEARCH_SEPS)) { + search.terms[search.termCount++] = tok; + } + + matchCount = searchRun(f, &search, wholeQuery, matches, inPosting); + } + + free(words); + free(wholeQuery); + free(inPosting); + free(search.terms); + free(search.found); + return matchCount; +} + + +int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId) { + if (!f) { + return -1; + } + + int32_t topic = (topicId && topicId[0]) ? hlpFileFindTopic(f, topicId) : -1; + + if (topic < 0) { + topic = hlpFileFindTopic(f, hlpFileString(f, f->header.defaultTopicStr)); + } + + if (topic < 0 && f->header.topicCount > 0) { + topic = 0; + } + + return topic; +} + + +const char *hlpFileString(const HlpFileT *f, uint32_t offset) { + if (!f || !f->stringTable || offset >= f->header.stringTableSize) { + return ""; + } + + return f->stringTable + offset; +} + + +const HlpTocEntryT *hlpFileToc(const HlpFileT *f, int32_t i) { + if (!f || i < 0 || (uint32_t)i >= f->header.tocCount) { + return NULL; + } + + return &f->tocEntries[i]; +} + + +int32_t hlpFileTocCount(const HlpFileT *f) { + return f ? (int32_t)f->header.tocCount : 0; +} + + +int32_t hlpFileTopicCount(const HlpFileT *f) { + return f ? (int32_t)f->header.topicCount : 0; +} + + +const char *hlpFileTopicId(const HlpFileT *f, int32_t topic) { + if (!f || topic < 0 || (uint32_t)topic >= f->header.topicCount) { + return ""; + } + + return hlpFileString(f, f->topicDir[topic].topicIdStr); +} + + +const char *hlpFileTopicTitle(const HlpFileT *f, int32_t topic) { + if (!f || topic < 0 || (uint32_t)topic >= f->header.topicCount) { + return ""; + } + + return hlpFileString(f, f->topicDir[topic].titleStr); +} + + +bool hlpRecordImage(const HlpRecordHdrT *hdr, const char *payload, HlpImageRefT *ref) { + if (hdr->type != HLP_REC_IMAGE || hdr->length < sizeof(HlpImageRefT)) { + return false; + } + + memcpy(ref, payload, sizeof(HlpImageRefT)); + return true; +} + + +// Payload: target topic ID \0 display text \0. Only a payload with no +// terminator within bounds is refused: exactly strlen(target) + 1 bytes +// is the writer's encoding of an empty display text (display then points +// at the NUL the reader appends at payload[length]), which shows the +// target, as the compiler's own HTML output does. +bool hlpRecordLink(const HlpRecordHdrT *hdr, const char *payload, const char **target, const char **display) { + size_t targetLen = strlen(payload); + + if (hdr->type != HLP_REC_LINK || targetLen + 1 > hdr->length) { + return false; + } + + *target = payload; + *display = payload + targetLen + 1; + + if ((*display)[0] == '\0') { + *display = payload; + } + + return true; +} + + +char *hlpWrapText(const char *text, int32_t cols) { + if (!text) { + text = ""; + } + + if (cols < HLP_MIN_WRAP_COLS) { + cols = HLP_MIN_WRAP_COLS; + } + + // Each wrap point replaces a blank or adds one '\n' per cols + // characters at most (a word longer than a line is not split). + int32_t textLen = (int32_t)strlen(text); + char *result = (char *)malloc(textLen + textLen / cols + 2); + + if (!result) { + return NULL; + } + + int32_t outPos = 0; + int32_t col = 0; + const char *p = text; + + while (*p) { + // Skip leading blanks at the start of a line + if (col == 0) { + while (*p == ' ') { + p++; + } + } + + if (!*p) { + break; + } + + const char *wordStart = p; + + while (*p && *p != ' ' && *p != '\n') { + p++; + } + + int32_t wordLen = (int32_t)(p - wordStart); + + if (col > 0 && col + 1 + wordLen > cols) { + result[outPos++] = '\n'; + col = 0; + } else if (col > 0) { + result[outPos++] = ' '; + col++; + } + + memcpy(result + outPos, wordStart, wordLen); + outPos += wordLen; + col += wordLen; + + while (*p == ' ') { + p++; + } + + // An explicit newline in the source is a paragraph break + if (*p == '\n') { + result[outPos++] = '\n'; + col = 0; + p++; + } + } + + result[outPos] = '\0'; + return result; +} + + +// Loads the trigram search section. False when it is missing or does not +// hold together (the caller then drops it). Posting lists are range- +// checked when used. +static bool readSearchIndex(HlpFileT *f) { + if (f->header.searchSize < sizeof(HlpSearchHeaderT) || !readTable(f, (void **)&f->searchData, f->header.searchOffset, f->header.searchSize, 1)) { + return false; + } + + HlpSearchHeaderT searchHdr; + uint32_t maxEntries = (f->header.searchSize - (uint32_t)sizeof(HlpSearchHeaderT)) / (uint32_t)sizeof(HlpTrigramEntryT); + + memcpy(&searchHdr, f->searchData, sizeof(searchHdr)); + + if (searchHdr.trigramCount > maxEntries) { + return false; + } + + f->trigramCount = searchHdr.trigramCount; + return true; +} + + +// Reads count entries of entrySize bytes at offset into a new allocation +// (NULL and true for none). +static bool readTable(HlpFileT *f, void **out, uint32_t offset, uint32_t count, uint32_t entrySize) { + if (count == 0) { + return true; + } + + *out = malloc((size_t)count * entrySize); + + return *out && fseek(f->fp, (long)offset, SEEK_SET) == 0 && fread(*out, entrySize, count, f->fp) == count; +} + + +// hlpFileSearch with the words split: the trigram filter, then each +// candidate checked word by word, then the keyword index. +static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery, uint8_t *matches, uint8_t *inPosting) { + uint32_t topicCount = f->header.topicCount; + int32_t matchCount = 0; + bool titlesOnly = false; + + memset(matches, 0, topicCount); + + if (search->termCount == 0) { + return 0; + } + + memset(matches, 1, topicCount); + trigramFilter(f, search, matches, inPosting, &titlesOnly); + + // The trigram index only proves the letters occur somewhere in the + // topic; confirm each candidate really contains every word. + for (uint32_t k = 0; k < topicCount; k++) { + if (!matches[k]) { + continue; + } + + const char *title = hlpFileString(f, f->topicDir[k].titleStr); + + memset(search->found, 0, sizeof(bool) * search->termCount); + search->foundCount = 0; + + for (int32_t t = 0; t < search->termCount; t++) { + if (containsNoCase(title, search->terms[t])) { + search->found[t] = true; + search->foundCount++; + } + } + + if (search->foundCount < search->termCount && !titlesOnly && !hlpFileForEachRecord(f, (int32_t)k, topicMatchRecord, search)) { + return -1; + } + + matches[k] = (search->foundCount == search->termCount); + matchCount += matches[k]; + } + + // Keyword index entries that contain the whole query also match. + for (uint32_t i = 0; i < f->header.indexCount; i++) { + uint16_t topic = f->indexEntries[i].topicIdx; + + if (topic < topicCount && !matches[topic] && containsNoCase(hlpFileString(f, f->indexEntries[i].keywordStr), wholeQuery)) { + matches[topic] = 1; + matchCount++; + } + } + + return matchCount; +} + + +// hlpFileForEachRecord callback for hlpFileSearch: marks the search words +// found in one record. Only the record types the help compiler indexes +// are searched. Stops the walk once every word has been found. +static bool topicMatchRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { + HlpSearchT *search = (HlpSearchT *)userData; + + if (!hlpRecordSearchable(hdr->type)) { + return true; + } + + for (int32_t t = 0; t < search->termCount; t++) { + if (!search->found[t] && containsNoCase(payload, search->terms[t])) { + search->found[t] = true; + search->foundCount++; + } + } + + return search->foundCount < search->termCount; +} + + +// Every topic starts as a candidate in matches; each trigram of each word +// that is not in a topic's posting list rules that topic out. titlesOnly +// is set when the file has an index but no word has a whole trigram. +static void trigramFilter(const HlpFileT *f, const HlpSearchT *search, uint8_t *matches, uint8_t *inPosting, bool *titlesOnly) { + uint32_t topicCount = f->header.topicCount; + + *titlesOnly = f->searchData != NULL; + + for (int32_t t = 0; t < search->termCount && f->searchData; t++) { + const uint8_t *term = (const uint8_t *)search->terms[t]; + + for (int32_t i = 0; term[i] && term[i + 1] && term[i + 2]; i++) { + if (hlpTrigramSymbol(term[i]) < 0 || hlpTrigramSymbol(term[i + 1]) < 0 || hlpTrigramSymbol(term[i + 2]) < 0) { + continue; + } + + *titlesOnly = false; + + const HlpTrigramEntryT *entry = findTrigram(f, &term[i]); + + if (!entry) { + memset(matches, 0, topicCount); + return; + } + + uint32_t postBytes = (uint32_t)entry->postingCount * (uint32_t)sizeof(uint16_t); + + if (entry->postingCount == HLP_POSTING_SATURATED || entry->postingOffset > f->header.searchSize || postBytes > f->header.searchSize - entry->postingOffset) { + continue; + } + + memset(inPosting, 0, topicCount); + + for (uint32_t k = 0; k < entry->postingCount; k++) { + uint16_t topic; + + memcpy(&topic, f->searchData + entry->postingOffset + k * sizeof(uint16_t), sizeof(topic)); + + if (topic < topicCount) { + inPosting[topic] = 1; + } + } + + for (uint32_t k = 0; k < topicCount; k++) { + matches[k] &= inPosting[k]; + } + } + } +} diff --git a/src/libs/kpunch/hlpfile/hlpFile.h b/src/libs/kpunch/hlpfile/hlpFile.h new file mode 100644 index 00000000..1fc692a6 --- /dev/null +++ b/src/libs/kpunch/hlpfile/hlpFile.h @@ -0,0 +1,115 @@ +// 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. + +// hlpFile.h -- DVX help file (.hlp) reader library (hlpfile.lib) +// +// Opens a compiled help file (see hlpFormat.h; written by the help +// compiler) and answers everything a help viewer needs: topics, the table +// of contents, the keyword index, a topic's content records in order, +// images and full-text search. The Help Viewer, the HelpPane widget and +// BASIC's hlpfile.bas all read help files through it. +// +// A file is an opaque HlpFileT handle. Strings returned point into the +// handle's string table and stay valid until hlpFileClose. + +#ifndef HLPFILE_H +#define HLPFILE_H + +#include "hlpFormat.h" + +#include +#include + +// Fewest columns text is wrapped at (hlpWrapText); narrower requests use it. +#define HLP_MIN_WRAP_COLS 10 + +typedef struct HlpFileT HlpFileT; + +// hlpFileForEachRecord callback: one record of a topic with its payload +// (NUL-terminated after hdr->length bytes). Return false to stop. +typedef bool (*HlpRecordFnT)(const HlpRecordHdrT *hdr, const char *payload, void *userData); + +// The help file behind a BASIC handle (hlpfile.bas HlpOpen), or NULL. +HlpFileT *hlpFileBasHandle(int32_t handle); + +// Close the file and free everything read from it (NULL is ignored). +void hlpFileClose(HlpFileT *f); + +// Index of the topic whose ID is topicId, or -1. +int32_t hlpFileFindTopic(const HlpFileT *f, const char *topicId); + +// Passes topic's content records in order to fn until fn returns false or +// the topic ends. False on a read or allocation failure or a bad topic. +bool hlpFileForEachRecord(HlpFileT *f, int32_t topic, HlpRecordFnT fn, void *userData); + +// Keyword index entry i (0 .. hlpFileIndexCount - 1), or NULL. +const HlpIndexEntryT *hlpFileIndex(const HlpFileT *f, int32_t i); +int32_t hlpFileIndexCount(const HlpFileT *f); + +// Open a help file; NULL when it cannot be read or is not a help file. +HlpFileT *hlpFileOpen(const char *path); + +// The picture of an image record: the image file as the help compiler +// took it in (BMP, PNG, JPEG, GIF ...), malloc'd, *outSize bytes (the +// caller frees it), or NULL. Safe inside hlpFileForEachRecord. +uint8_t *hlpFileReadImage(HlpFileT *f, const HlpImageRefT *ref, uint32_t *outSize); + +// Full-text search. query holds words separated by blanks (any case; it +// is not changed). A topic matches when its title or text contains every +// word, or when an index keyword contains the whole query. Words too +// short for the trigram index cannot narrow the candidates, so when no +// word has HLP_TRIGRAM_LEN letters or digits in a row only titles and +// keywords are searched. Sets matches[topic] (hlpFileTopicCount bytes) +// to 1 for each match, 0 otherwise, and returns the match count, or -1 on +// an allocation or read failure. +int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches); + +// The topic to open for "file [topicId]": topicId (NULL or "" for none), +// else the file's default topic, else the first (-1 for no topics). +int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId); + +// String table entry at offset ("" when out of range). +const char *hlpFileString(const HlpFileT *f, uint32_t offset); + +// Table of contents entry i (0 .. hlpFileTocCount - 1), or NULL. +const HlpTocEntryT *hlpFileToc(const HlpFileT *f, int32_t i); +int32_t hlpFileTocCount(const HlpFileT *f); + +// Topics: count, and the ID and title of topic ("" when out of range). +int32_t hlpFileTopicCount(const HlpFileT *f); +const char *hlpFileTopicId(const HlpFileT *f, int32_t topic); +const char *hlpFileTopicTitle(const HlpFileT *f, int32_t topic); + +// The image reference of an HLP_REC_IMAGE record, false when the record +// is not a whole image record. +bool hlpRecordImage(const HlpRecordHdrT *hdr, const char *payload, HlpImageRefT *ref); + +// Target topic ID and display text of an HLP_REC_LINK record (the +// target when the display text is empty), false when the record is not +// a whole link record. +bool hlpRecordLink(const HlpRecordHdrT *hdr, const char *payload, const char **target, const char **display); + +// Flowing text word-wrapped at cols columns (at least HLP_MIN_WRAP_COLS): +// a malloc'd copy with '\n' at the wrap points (NULL when out of memory). +char *hlpWrapText(const char *text, int32_t cols); + +#endif // HLPFILE_H diff --git a/src/libs/kpunch/hlpfile/hlpFileBas.c b/src/libs/kpunch/hlpfile/hlpFileBas.c new file mode 100644 index 00000000..8897f3ea --- /dev/null +++ b/src/libs/kpunch/hlpfile/hlpFileBas.c @@ -0,0 +1,450 @@ +// 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. + +// hlpFileBas.c -- DVX BASIC entry points of the help file library +// (src/include/basic/hlpfile.bas) +// +// Called through DECLARE LIBRARY, so the signatures use only BASIC- +// marshalable types: strings and LONG in, LONG or STRING out, True (-1) / +// False (0) for yes / no. A returned string stays valid until the next +// call that returns one (the BASIC runtime copies it at once). +// +// A handle (1, 2, ...; 0 means none) is one open help file plus what the +// program last asked of it: the topics HlpSearch found and the records +// HlpLoadTopic read (BASIC cannot take a callback or a byte array, so the +// results are kept here and read one by one). Handles belong to the +// program that opened them; whatever a program leaves open is closed when +// it ends. + +#include "hlpFile.h" +#include "dvxMem.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define BAS_NONE (-1) +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" + +// One content record of the loaded topic, copied out of the file. +typedef struct { + char *text; // text, or a link's display text ("" for none) + char *link; // a link's target topic ID ("" for none) + HlpImageRefT image; // an image's place in the file + uint8_t type; // HLP_REC_* + uint8_t flags; // note type / image alignment +} BasHlpRecordT; + +// One open handle and the program it belongs to +typedef struct { + HlpFileT *file; // NULL: the handle is free + int32_t *found; // stb_ds: the last HlpSearch's topics + BasHlpRecordT *records; // stb_ds: the last HlpLoadTopic's records + int32_t appId; +} BasHlpT; + +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// this library loads before the shell, and the host tests may have none. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); + +static BasHlpT *sHandles = NULL; // stb_ds; handle = index + 1 +static bool sCleanupHooked = false; +static char *sWrapped = NULL; // the last HlpWrap result + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static BasHlpT *handleOf(int32_t handle); +void HlpClose(int32_t handle); +HlpFileT *hlpFileBasHandle(int32_t handle); +int32_t HlpFindTopic(int32_t handle, const char *topicId); +int32_t HlpIndexCount(int32_t handle); +const char *HlpIndexKeyword(int32_t handle, int32_t i); +int32_t HlpIndexTopic(int32_t handle, int32_t i); +int32_t HlpLoadTopic(int32_t handle, int32_t topic); +int32_t HlpOpen(const char *path); +int32_t HlpRecordFlags(int32_t handle, int32_t n); +int32_t HlpRecordImageSave(int32_t handle, int32_t n, const char *path); +const char *HlpRecordLink(int32_t handle, int32_t n); +const char *HlpRecordText(int32_t handle, int32_t n); +int32_t HlpRecordType(int32_t handle, int32_t n); +int32_t HlpSearch(int32_t handle, const char *query); +int32_t HlpSearchTopic(int32_t handle, int32_t n); +int32_t HlpStartTopic(int32_t handle, const char *topicId); +int32_t HlpTocCount(int32_t handle); +int32_t HlpTocDepth(int32_t handle, int32_t i); +int32_t HlpTocExpanded(int32_t handle, int32_t i); +const char *HlpTocTitle(int32_t handle, int32_t i); +int32_t HlpTocTopic(int32_t handle, int32_t i); +int32_t HlpTopicCount(int32_t handle); +const char *HlpTopicId(int32_t handle, int32_t topic); +const char *HlpTopicTitle(int32_t handle, int32_t topic); +const char *HlpWrap(const char *text, int32_t cols); +static const BasHlpRecordT *recordOf(int32_t handle, int32_t n); +static void recordsFree(BasHlpT *hd); +static bool recordStore(const HlpRecordHdrT *hdr, const char *payload, void *userData); + + +// Shell app-cleanup hook: close what a program that ended left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + if (sHandles[i].file && sHandles[i].appId == appId) { + HlpClose(i + 1); + } + } +} + + +static BasHlpT *handleOf(int32_t handle) { + if (handle <= BAS_NO_HANDLE || handle > (int32_t)arrlen(sHandles) || !sHandles[handle - 1].file) { + return NULL; + } + + return &sHandles[handle - 1]; +} + + +void HlpClose(int32_t handle) { + BasHlpT *hd = handleOf(handle); + + if (!hd) { + return; + } + + recordsFree(hd); + arrfree(hd->found); + hlpFileClose(hd->file); + hd->file = NULL; +} + + +HlpFileT *hlpFileBasHandle(int32_t handle) { + BasHlpT *hd = handleOf(handle); + + return hd ? hd->file : NULL; +} + + +// The topic whose ID is topicId, or -1. +int32_t HlpFindTopic(int32_t handle, const char *topicId) { + return hlpFileFindTopic(hlpFileBasHandle(handle), topicId); +} + + +int32_t HlpIndexCount(int32_t handle) { + return hlpFileIndexCount(hlpFileBasHandle(handle)); +} + + +const char *HlpIndexKeyword(int32_t handle, int32_t i) { + HlpFileT *f = hlpFileBasHandle(handle); + const HlpIndexEntryT *e = hlpFileIndex(f, i); + + return e ? hlpFileString(f, e->keywordStr) : ""; +} + + +int32_t HlpIndexTopic(int32_t handle, int32_t i) { + const HlpIndexEntryT *e = hlpFileIndex(hlpFileBasHandle(handle), i); + + return e ? (int32_t)e->topicIdx : BAS_NONE; +} + + +// Read topic's records into the handle: their count, or -1 for a bad +// handle or topic or a read failure. +int32_t HlpLoadTopic(int32_t handle, int32_t topic) { + BasHlpT *hd = handleOf(handle); + + if (!hd) { + return BAS_NONE; + } + + recordsFree(hd); + + if (!hlpFileForEachRecord(hd->file, topic, recordStore, hd)) { + recordsFree(hd); + return BAS_NONE; + } + + return (int32_t)arrlen(hd->records); +} + + +// Open a help file: a handle, or 0 when it cannot be read. +int32_t HlpOpen(const char *path) { + HlpFileT *f = hlpFileOpen(path); + + if (!f) { + return BAS_NO_HANDLE; + } + + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sHandles) && sHandles[idx].file) { + idx++; + } + + if (idx == (int32_t)arrlen(sHandles)) { + BasHlpT blank; + + memset(&blank, 0, sizeof(blank)); + arrput(sHandles, blank); + } + + memset(&sHandles[idx], 0, sizeof(sHandles[idx])); + sHandles[idx].file = f; + sHandles[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + return idx + 1; +} + + +int32_t HlpRecordFlags(int32_t handle, int32_t n) { + const BasHlpRecordT *r = recordOf(handle, n); + + return r ? r->flags : BAS_NONE; +} + + +// Write image record n's picture to path (the image file as it was put in +// the help file: BMP, PNG, ...): True when written. +int32_t HlpRecordImageSave(int32_t handle, int32_t n, const char *path) { + const BasHlpRecordT *r = recordOf(handle, n); + + if (!r || r->type != HLP_REC_IMAGE || !path) { + return BAS_FALSE; + } + + uint32_t size = 0; + uint8_t *data = hlpFileReadImage(hlpFileBasHandle(handle), &r->image, &size); + FILE *out = data ? fopen(path, "wb") : NULL; + bool ok = out && fwrite(data, 1, size, out) == size; + + if (out && fclose(out) != 0) { + ok = false; + } + + free(data); + return ok ? BAS_TRUE : BAS_FALSE; +} + + +const char *HlpRecordLink(int32_t handle, int32_t n) { + const BasHlpRecordT *r = recordOf(handle, n); + + return r ? r->link : ""; +} + + +const char *HlpRecordText(int32_t handle, int32_t n) { + const BasHlpRecordT *r = recordOf(handle, n); + + return r ? r->text : ""; +} + + +int32_t HlpRecordType(int32_t handle, int32_t n) { + const BasHlpRecordT *r = recordOf(handle, n); + + return r ? r->type : BAS_NONE; +} + + +// Search the file (hlpFileSearch): the number of topics found, kept in +// the handle for HlpSearchTopic, or -1 on a failure. +int32_t HlpSearch(int32_t handle, const char *query) { + BasHlpT *hd = handleOf(handle); + + if (!hd) { + return BAS_NONE; + } + + int32_t topicCount = hlpFileTopicCount(hd->file); + uint8_t *matches = (uint8_t *)malloc(topicCount + 1); + int32_t count = matches ? hlpFileSearch(hd->file, query, matches) : BAS_NONE; + + arrsetlen(hd->found, 0); + + for (int32_t t = 0; count > 0 && t < topicCount; t++) { + if (matches[t]) { + arrput(hd->found, t); + } + } + + free(matches); + return count; +} + + +// The topic of search result n (0-based, in topic order), or -1. +int32_t HlpSearchTopic(int32_t handle, int32_t n) { + BasHlpT *hd = handleOf(handle); + + if (!hd || n < 0 || n >= (int32_t)arrlen(hd->found)) { + return BAS_NONE; + } + + return hd->found[n]; +} + + +int32_t HlpStartTopic(int32_t handle, const char *topicId) { + return hlpFileStartTopic(hlpFileBasHandle(handle), topicId); +} + + +int32_t HlpTocCount(int32_t handle) { + return hlpFileTocCount(hlpFileBasHandle(handle)); +} + + +int32_t HlpTocDepth(int32_t handle, int32_t i) { + const HlpTocEntryT *e = hlpFileToc(hlpFileBasHandle(handle), i); + + return e ? e->depth : BAS_NONE; +} + + +int32_t HlpTocExpanded(int32_t handle, int32_t i) { + const HlpTocEntryT *e = hlpFileToc(hlpFileBasHandle(handle), i); + + return (e && (e->flags & HLP_TOC_EXPANDED)) ? BAS_TRUE : BAS_FALSE; +} + + +const char *HlpTocTitle(int32_t handle, int32_t i) { + HlpFileT *f = hlpFileBasHandle(handle); + const HlpTocEntryT *e = hlpFileToc(f, i); + + return e ? hlpFileString(f, e->titleStr) : ""; +} + + +// The topic entry i opens, or -1 for a section heading (or a bad entry). +int32_t HlpTocTopic(int32_t handle, int32_t i) { + const HlpTocEntryT *e = hlpFileToc(hlpFileBasHandle(handle), i); + + return (e && e->topicIdx != HLP_NO_TOPIC) ? (int32_t)e->topicIdx : BAS_NONE; +} + + +int32_t HlpTopicCount(int32_t handle) { + return hlpFileTopicCount(hlpFileBasHandle(handle)); +} + + +const char *HlpTopicId(int32_t handle, int32_t topic) { + return hlpFileTopicId(hlpFileBasHandle(handle), topic); +} + + +const char *HlpTopicTitle(int32_t handle, int32_t topic) { + return hlpFileTopicTitle(hlpFileBasHandle(handle), topic); +} + + +// text word-wrapped at cols columns, lines separated by CHR$(10). +const char *HlpWrap(const char *text, int32_t cols) { + free(sWrapped); + sWrapped = hlpWrapText(text, cols); + return sWrapped ? sWrapped : ""; +} + + +static const BasHlpRecordT *recordOf(int32_t handle, int32_t n) { + BasHlpT *hd = handleOf(handle); + + if (!hd || n < 0 || n >= (int32_t)arrlen(hd->records)) { + return NULL; + } + + return &hd->records[n]; +} + + +static void recordsFree(BasHlpT *hd) { + for (int32_t i = 0; i < (int32_t)arrlen(hd->records); i++) { + free(hd->records[i].text); + free(hd->records[i].link); + } + + arrfree(hd->records); +} + + +// hlpFileForEachRecord callback of HlpLoadTopic: copies one record. +static bool recordStore(const HlpRecordHdrT *hdr, const char *payload, void *userData) { + BasHlpT *hd = (BasHlpT *)userData; + const char *text = payload; + const char *target = ""; + BasHlpRecordT rec; + + memset(&rec, 0, sizeof(rec)); + rec.type = hdr->type; + rec.flags = hdr->flags; + + if (hdr->type == HLP_REC_LINK) { + if (!hlpRecordLink(hdr, payload, &target, &text)) { + return true; + } + } else if (hdr->type == HLP_REC_IMAGE) { + if (!hlpRecordImage(hdr, payload, &rec.image)) { + return true; + } + + text = ""; + } else if (hdr->type == HLP_REC_HRULE) { + text = ""; + } + + rec.text = strdup(text); + rec.link = strdup(target); + + if (!rec.text || !rec.link) { + free(rec.text); + free(rec.link); + return false; + } + + arrput(hd->records, rec); + return true; +} diff --git a/src/apps/kpunch/dvxhelp/hlpformat.h b/src/libs/kpunch/hlpfile/hlpFormat.h similarity index 77% rename from src/apps/kpunch/dvxhelp/hlpformat.h rename to src/libs/kpunch/hlpfile/hlpFormat.h index 1ae04c69..8a73a5fa 100644 --- a/src/apps/kpunch/dvxhelp/hlpformat.h +++ b/src/libs/kpunch/hlpfile/hlpFormat.h @@ -20,9 +20,11 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// hlpformat.h -- DVX Help binary file format structures +// hlpFormat.h -- DVX Help binary file format structures // -// Shared between the host-side help compiler and the DVX help viewer app. +// Shared by the help compiler (src/tools/dvxhlpc.c, the writer) and the +// help file library (hlpFile.c, the reader), including the one rule for +// which characters make up search trigrams. #ifndef HLPFORMAT_H #define HLPFORMAT_H @@ -132,7 +134,7 @@ typedef struct { typedef struct { uint32_t imageOffset; // offset within image pool - uint32_t imageSize; // BMP data size + uint32_t imageSize; // size of the image file (BMP, PNG, ...) } HlpImageRefT; @@ -184,4 +186,54 @@ typedef struct { } HlpSearchHeaderT; +// --------------------------------------------------------------------------- +// Search trigrams +// --------------------------------------------------------------------------- + +// A trigram is HLP_TRIGRAM_LEN consecutive lowercase ASCII letters or +// digits: HLP_TRIGRAM_ALPHABET symbols. Posting counts are stored in a +// byte, so a trigram in more topics than that is written with +// HLP_POSTING_SATURATED and an incomplete posting list (it cannot rule +// topics out). +#define HLP_TRIGRAM_LEN 3 +#define HLP_TRIGRAM_DIGITS 10 +#define HLP_TRIGRAM_LETTERS 26 +#define HLP_TRIGRAM_ALPHABET (HLP_TRIGRAM_DIGITS + HLP_TRIGRAM_LETTERS) +#define HLP_POSTING_SATURATED UINT8_MAX + + +// True for the record types whose text is searched (and indexed by the +// compiler): text, headings, list items, notes and code. +static inline bool hlpRecordSearchable(uint8_t type) { + switch (type) { + case HLP_REC_TEXT: + case HLP_REC_HEADING1: + case HLP_REC_HEADING2: + case HLP_REC_HEADING3: + case HLP_REC_LIST_ITEM: + case HLP_REC_NOTE: + case HLP_REC_CODE: + return true; + + default: + return false; + } +} + + +// The trigram symbol (0 .. HLP_TRIGRAM_ALPHABET - 1) of an already +// lowercased character, or -1 when it cannot be part of a trigram. +static inline int32_t hlpTrigramSymbol(uint8_t c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + + if (c >= 'a' && c <= 'z') { + return HLP_TRIGRAM_DIGITS + (c - 'a'); + } + + return -1; +} + + #endif // HLPFORMAT_H diff --git a/src/libs/kpunch/hlpfile/hlpfile.dep b/src/libs/kpunch/hlpfile/hlpfile.dep new file mode 100644 index 00000000..d613d36a --- /dev/null +++ b/src/libs/kpunch/hlpfile/hlpfile.dep @@ -0,0 +1 @@ +libdvx diff --git a/src/libs/kpunch/hlpfile/hlpfile.dhs b/src/libs/kpunch/hlpfile/hlpfile.dhs new file mode 100644 index 00000000..c224576e --- /dev/null +++ b/src/libs/kpunch/hlpfile/hlpfile.dhs @@ -0,0 +1,97 @@ +# 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. + +.section Technical Reference (for Developers)|Libraries +.topic lib.hlpfile +.title Help File Library +.toc 0 Help File Library +.index hlpfile +.index hlpFileOpen +.index hlpFileForEachRecord +.index hlpFileSearch +.index hlpFileStartTopic +.index hlpFileReadImage +.index hlpWrapText +.index HlpFileT +.index hlpFormat.h +.index .hlp file format + +.h1 Help File Library + +Reads compiled DVX help files (.hlp, written by the help compiler dvxhlpc or HelpCompile): topics, the table of contents, the keyword index, each topic's content records in order, pictures and full-text search. The Help Viewer, the HelpView widget (BASIC HelpPane) and BASIC's hlpfile.bas all read help files through it, so they find, search and show topics the same way. + +Header: hlpfile/hlpFile.h (the file format: hlpfile/hlpFormat.h, shared with the help compiler) + +Loaded as: bin/libs/kpunch/hlpfile/hlpfile.lib + +.h2 Opening and Topics + +A file is an opaque HlpFileT. Strings returned point into the file's string table and stay valid until hlpFileClose. + +.table + Function Description + -------- ----------- + hlpFileOpen(path) Open a help file; NULL when it cannot be read or is not a help file. The header, topic directory, strings, table of contents, index and search index are read at once; topic content and pictures are read when asked for. + hlpFileClose(f) Close it (NULL is ignored). + hlpFileTopicCount(f) Number of topics. + hlpFileTopicId(f, topic) / Title(f, topic) A topic's ID (what links and HelpTopic name) and title; "" out of range. + hlpFileFindTopic(f, topicId) The topic with that ID, or -1. + hlpFileStartTopic(f, topicId) The topic to open for "file [topic]": topicId (NULL or "" for none), else the file's .default topic, else the first; -1 for a file with no topics. + hlpFileString(f, offset) A string table entry ("" out of range). + hlpFileTocCount(f) / hlpFileToc(f, i) Table of contents entries (HlpTocEntryT: title string, topic or HLP_NO_TOPIC for a section heading, depth, HLP_TOC_EXPANDED flag). + hlpFileIndexCount(f) / hlpFileIndex(f, i) Keyword index entries (HlpIndexEntryT: keyword string, topic), sorted. +.endtable + +.h2 Records and Pictures + +hlpFileForEachRecord(f, topic, fn, userData) calls fn(hdr, payload, userData) for each record of a topic in order (HLP_REC_TEXT, HEADING1 .. 3, LIST_ITEM, NOTE with its HLP_NOTE_ kind in hdr->flags, CODE, TABLE, IMAGE with its HLP_IMG_ alignment, LINK, HRULE); the payload is NUL-terminated. fn returns false to stop. hlpRecordLink gives a link record's target topic ID and display text (the target when the text is empty); hlpRecordImage its picture's place in the file, and hlpFileReadImage the picture itself: the image file as the compiler took it in (BMP, PNG, JPEG or GIF), malloc'd, for dvxLoadImageAlpha. Reading a picture inside the record walk is safe. + +hlpWrapText(text, cols) wraps flowing text at cols columns (at least HLP_MIN_WRAP_COLS) as the viewer does, a malloc'd copy with '\n' at the wrap points. + +.h2 Search + +hlpFileSearch(f, query, matches) finds the topics whose title or text contains every word of query (any case, separated by blanks), plus those with an index keyword containing the whole query, setting matches[topic] (hlpFileTopicCount bytes) and returning the count (-1 on a failure). The query is not changed. The compiler's trigram index narrows the candidates first; the trigram characters (hlpTrigramSymbol) and the record types searched (hlpRecordSearchable) are defined once in hlpFormat.h for both. Words with no three letters or digits in a row cannot use the index, so then only titles and keywords are searched. + +.code +uint8_t *matches = malloc(hlpFileTopicCount(f)); +int32_t n = hlpFileSearch(f, "serial port", matches); + +for (int32_t t = 0; n > 0 && t < hlpFileTopicCount(f); t++) { + if (matches[t]) { + printf("%s\n", hlpFileTopicTitle(f, t)); + } +} + +free(matches); +.endcode + +.topic lib.hlpfile.basic +.title Help File Library: BASIC +.toc 1 Help Files from BASIC +.index hlpfile.bas +.index HlpOpen + +.h2 BASIC + +src/include/basic/hlpfile.bas declares the BASIC functions (hlpFileBas.c): HlpOpen, HlpClose, HlpTopicCount, HlpTopicId$, HlpTopicTitle$, HlpFindTopic, HlpStartTopic, HlpTocCount, HlpTocTitle$, HlpTocTopic, HlpTocDepth, HlpTocExpanded, HlpIndexCount, HlpIndexKeyword$, HlpIndexTopic, HlpSearch, HlpSearchTopic, HlpLoadTopic, HlpRecordType, HlpRecordFlags, HlpRecordText$, HlpRecordLink$, HlpRecordImageSave and HlpWrap$, with the HLP_REC_, HLP_NOTE_ and HLP_IMG_ constants of hlpFormat.h. + +A handle (a small number, 0 = none) is an open file plus what the program last asked of it -- HlpSearch keeps the topics found for HlpSearchTopic, HlpLoadTopic copies a topic's records for the HlpRecord functions -- since BASIC cannot take a callback or a byte array. Each handle records the program that opened it; the shell's app-cleanup hook closes what a program leaves open when it ends. hlpFileBasHandle(handle) gives the HlpFileT behind a handle to C code (the HelpPane control's ShowTopic uses it). A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). diff --git a/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c b/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c index 67b2c1eb..48690603 100644 --- a/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c +++ b/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c @@ -38,9 +38,7 @@ #define KEYS_AXIS_MASK 0x03 #define KEYS_BUTTONS 4 -#define KEYS_KEY_MAX 32 - -// The slots, in JOY_KBD_SLOT_NAMES order +// The slots, in JOY_KBD_SLOT_NAMES order (joyKbdSlotGet) typedef enum { KeyUpE, KeyDownE, @@ -52,13 +50,6 @@ typedef enum { _Static_assert(KeyCountE == JOY_KBD_SLOT_COUNT, "keyboard joystick slots must match JOY_KBD_SLOT_NAMES"); -// ============================================================ -// Tables -// ============================================================ - -static const char *const sSlotNames[KeyCountE] = { JOY_KBD_SLOT_NAMES }; -static const char *const sDefaultKeys[KeyCountE] = { JOY_KBD_SLOT_DEFAULTS }; - // ============================================================ // Module state // ============================================================ @@ -128,15 +119,10 @@ static int32_t keysProbe(void) { } -// A key name that is not known falls back to the default key. +// A key name that is not known falls back to the default key +// (joyKbdSlotGet). static void keysSettings(void) { - char key[KEYS_KEY_MAX]; - for (int32_t i = 0; i < KeyCountE; i++) { - snprintf(key, sizeof(key), "%s%s", JOY_KBD_KEY_PREFIX, sSlotNames[i]); - - int32_t code = joyKeyFind(joySettingGet(key, sDefaultKeys[i])); - - sCodes[i] = code >= 0 ? code : joyKeyFind(sDefaultKeys[i]); + sCodes[i] = joyKeyCode(joyKbdSlotGet(i)); } } diff --git a/src/libs/kpunch/joystick/joyBas.c b/src/libs/kpunch/joystick/joyBas.c index 55271c6d..60cad6e0 100644 --- a/src/libs/kpunch/joystick/joyBas.c +++ b/src/libs/kpunch/joystick/joyBas.c @@ -61,17 +61,48 @@ static StickLatchT sLatches[QB_LATCH_SLOTS]; static int32_t basHandle(int32_t joy); static bool basInfo(int32_t joy, JoyInfoT *info); +static bool basRaw(int32_t joy, JoyRawT *raw); static bool basState(int32_t joy, JoyStateT *st); +static int32_t basTruth(bool value); int32_t JoyAxes(int32_t joy); int32_t JoyAxis(int32_t joy, int32_t axis); +int32_t JoyAxisPresent(int32_t joy, int32_t axis); void joyBasForget(int32_t appId); int32_t joyBasStick(int32_t n); int32_t joyBasStrig(int32_t n); int32_t JoyButton(int32_t joy, int32_t button); int32_t JoyButtons(int32_t joy); +int32_t JoyCalibration(int32_t joy, int32_t axis, int32_t part); +int32_t JoyCanCalibrate(int32_t joy); +int32_t JoyChoice(int32_t which); +int32_t JoyChoiceCount(int32_t which); +const char *JoyChoiceLabel(int32_t which, int32_t index); +int32_t JoyClearCalibration(int32_t joy, int32_t axis); +int32_t JoyConnected(int32_t joy); int32_t JoyCount(void); +int32_t JoyDeadZone(int32_t joy); int32_t JoyHat(int32_t joy, int32_t hat); +int32_t JoyHats(int32_t joy); +int32_t JoyIsKeyboard(int32_t joy); +int32_t JoyKbdSlot(int32_t slot); +int32_t JoyKbdSlotCount(void); +int32_t JoyKeyCode(int32_t index); +int32_t JoyKeyCount(void); +int32_t JoyKeyIndex(const char *keyName); +const char *JoyKeyName(int32_t index); +const char *JoyKeyNameOf(int32_t rawCode); const char *JoyName(int32_t joy); +int32_t JoyRawAxis(int32_t joy, int32_t axis); +int32_t JoyRawButtons(int32_t joy); +int32_t JoyRescan(void); +int32_t JoySetCalibration(int32_t joy, int32_t axis, int32_t minV, int32_t centerV, int32_t maxV); +int32_t JoySetChoice(int32_t which, int32_t index); +int32_t JoySetDeadZone(int32_t joy, int32_t percent); +int32_t JoySetKbdSlot(int32_t slot, int32_t keyIndex); +void JoySetSetting(const char *key, const char *value); +const char *JoySetting(const char *key, const char *defaultValue); +void JoySettingsLoad(int32_t prefs); +void JoySettingsStore(int32_t prefs); int32_t JoyX(int32_t joy); int32_t JoyY(int32_t joy); static StickLatchT *latchOf(int32_t appId, bool create); @@ -90,11 +121,22 @@ static bool basInfo(int32_t joy, JoyInfoT *info) { } +// The joystick's last raw poll (calibration reads these). +static bool basRaw(int32_t joy, JoyRawT *raw) { + return joyGetRaw(joy - 1, raw); +} + + static bool basState(int32_t joy, JoyStateT *st) { return joyRead(basHandle(joy), st); } +static int32_t basTruth(bool value) { + return value ? BAS_TRUE : BAS_FALSE; +} + + int32_t JoyAxes(int32_t joy) { JoyInfoT info; @@ -113,6 +155,14 @@ int32_t JoyAxis(int32_t joy, int32_t axis) { } +// True when axis answers (a missing axis always reads 0). +int32_t JoyAxisPresent(int32_t joy, int32_t axis) { + JoyInfoT info; + + return basTruth(axis >= 1 && axis <= JOY_MAX_AXES && basInfo(joy, &info) && (info.axisPresent & (1U << (axis - 1)))); +} + + void joyBasForget(int32_t appId) { StickLatchT *l = latchOf(appId, false); @@ -164,15 +214,15 @@ int32_t joyBasStrig(int32_t n) { int32_t button = first + pair / 2; if (n % 2 == 0) { - return joyButtonWentDown(handle, button) ? BAS_TRUE : BAS_FALSE; + return basTruth(joyButtonWentDown(handle, button)); } - return joyRead(handle, &st) && (st.buttons & (1U << button)) ? BAS_TRUE : BAS_FALSE; + return basTruth(joyRead(handle, &st) && (st.buttons & (1U << button))); } int32_t JoyButton(int32_t joy, int32_t button) { - return joyButtonPressed(basHandle(joy), button - 1) ? BAS_TRUE : BAS_FALSE; + return basTruth(joyButtonPressed(basHandle(joy), button - 1)); } @@ -183,11 +233,88 @@ int32_t JoyButtons(int32_t joy) { } +// One part (JOY_CAL_*) of an axis's calibration in raw units: what was +// set, or the range found automatically; 0 for a joystick or axis that is +// not calibrated (JoyCanCalibrate False) or another part. +int32_t JoyCalibration(int32_t joy, int32_t axis, int32_t part) { + JoyAxisCalT cal; + + if (!joyGetCalibration(joy - 1, axis - 1, &cal)) { + return 0; + } + + switch (part) { + case JOY_CAL_MIN: + return cal.min; + + case JOY_CAL_CENTER: + return cal.center; + + case JOY_CAL_MAX: + return cal.max; + + default: + return 0; + } +} + + +// True for analog axes that need calibrating (a game port joystick). +int32_t JoyCanCalibrate(int32_t joy) { + JoyInfoT info; + + return basTruth(basInfo(joy, &info) && (info.caps & JOY_CAP_CALIBRATE)); +} + + +// The index of a listed setting's current choice (JOY_CHOICE_*), -1 for +// another which. +int32_t JoyChoice(int32_t which) { + return joyChoiceGet((JoyChoiceE)which); +} + + +int32_t JoyChoiceCount(int32_t which) { + int32_t count = 0; + + joyChoices((JoyChoiceE)which, &count); + return count; +} + + +const char *JoyChoiceLabel(int32_t which, int32_t index) { + int32_t count = 0; + const PrefsChoiceT *choices = joyChoices((JoyChoiceE)which, &count); + + return (index >= 0 && index < count) ? choices[index].label : ""; +} + + +// Back to automatic ranging. +int32_t JoyClearCalibration(int32_t joy, int32_t axis) { + return basTruth(joySetCalibration(joy - 1, axis - 1, NULL)); +} + + +// False when the joystick stopped answering (or does not exist). +int32_t JoyConnected(int32_t joy) { + JoyStateT st; + + return basTruth(basState(joy, &st) && st.connected); +} + + int32_t JoyCount(void) { return joyCount(); } +// Percent, -1 when there is no such joystick. +int32_t JoyDeadZone(int32_t joy) { + return joyGetDeadZone(joy - 1); +} + + int32_t JoyHat(int32_t joy, int32_t hat) { JoyStateT st; @@ -199,6 +326,66 @@ int32_t JoyHat(int32_t joy, int32_t hat) { } +int32_t JoyHats(int32_t joy) { + JoyInfoT info; + + return basInfo(joy, &info) ? info.hatCount : 0; +} + + +// True for the keyboard joystick (it reads neutral unless one of the +// program's windows has the focus). +int32_t JoyIsKeyboard(int32_t joy) { + JoyInfoT info; + + return basTruth(basInfo(joy, &info) && (info.caps & JOY_CAP_KEYBOARD)); +} + + +// The JoyKeyName$ index of a keyboard joystick slot's key, -1 for a bad +// slot. +int32_t JoyKbdSlot(int32_t slot) { + return joyKbdSlotGet(slot); +} + + +int32_t JoyKbdSlotCount(void) { + return JOY_KBD_SLOT_COUNT; +} + + +// The raw key code of key index (for GameKeyDown), -1 for a bad index. +int32_t JoyKeyCode(int32_t index) { + return joyKeyCode(index); +} + + +int32_t JoyKeyCount(void) { + return joyKeyCount(); +} + + +// The index of a key by its name ("Up", "LCtrl", "A"), -1 when unknown. +int32_t JoyKeyIndex(const char *keyName) { + return joyKeyIndex(keyName); +} + + +const char *JoyKeyName(int32_t index) { + const char *name = joyKeyName(index); + + return name ? name : ""; +} + + +// The name of a raw key code ("" when it is none of the keys). +const char *JoyKeyNameOf(int32_t rawCode) { + const char *name = joyKeyNameOf(rawCode); + + return name ? name : ""; +} + + // The name is copied into a buffer that the next call reuses (the BASIC // runtime copies the string at once). const char *JoyName(int32_t joy) { @@ -208,6 +395,87 @@ const char *JoyName(int32_t joy) { } +// The axis's raw value in the driver's own units, 0 when there is no such +// joystick or axis. +int32_t JoyRawAxis(int32_t joy, int32_t axis) { + JoyRawT raw; + + if (axis < 1 || axis > JOY_MAX_AXES || !basRaw(joy, &raw)) { + return 0; + } + + return raw.axes[axis - 1]; +} + + +// The buttons held now, bit n - 1 for button n. +int32_t JoyRawButtons(int32_t joy) { + JoyRawT raw; + + return basRaw(joy, &raw) ? (int32_t)raw.buttons : 0; +} + + +// The program's own joystick handles (and STICK readings) go first; the +// new count, or -1 while another program has a joystick open. +int32_t JoyRescan(void) { + return joyRescanApp(shellContext()->currentAppId); +} + + +int32_t JoySetCalibration(int32_t joy, int32_t axis, int32_t minV, int32_t centerV, int32_t maxV) { + JoyAxisCalT cal = { minV, centerV, maxV }; + + return basTruth(joySetCalibration(joy - 1, axis - 1, &cal)); +} + + +int32_t JoySetChoice(int32_t which, int32_t index) { + return basTruth(joyChoiceSet((JoyChoiceE)which, index)); +} + + +int32_t JoySetDeadZone(int32_t joy, int32_t percent) { + return basTruth(joySetDeadZone(joy - 1, percent)); +} + + +int32_t JoySetKbdSlot(int32_t slot, int32_t keyIndex) { + return basTruth(joyKbdSlotSet(slot, keyIndex)); +} + + +// "" removes the setting. +void JoySetSetting(const char *key, const char *value) { + joySettingSet(key, (value && value[0]) ? value : NULL); +} + + +const char *JoySetting(const char *key, const char *defaultValue) { + return joySettingGet(key, defaultValue ? defaultValue : ""); +} + + +// Every [joystick] setting from a prefs.bas handle (to undo changes). +void JoySettingsLoad(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + joySettingsLoad(h); + } +} + + +// Every [joystick] setting into a prefs.bas handle (save it yourself). +void JoySettingsStore(int32_t prefs) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (h) { + joySettingsStore(h); + } +} + + int32_t JoyX(int32_t joy) { return JoyAxis(joy, 1); } diff --git a/src/libs/kpunch/joystick/joystick.c b/src/libs/kpunch/joystick/joystick.c index 8f6c91e8..d6b04df4 100644 --- a/src/libs/kpunch/joystick/joystick.c +++ b/src/libs/kpunch/joystick/joystick.c @@ -128,6 +128,14 @@ typedef struct { int32_t code; } JoyKeyT; +// A [joystick] setting offered as a list (joyChoices) +typedef struct { + const char *key; + const char *defaultName; + const PrefsChoiceT *choices; + int32_t count; +} JoyChoiceTableT; + // ============================================================ // Tables // ============================================================ @@ -172,6 +180,27 @@ static const JoyKeyT sKeys[] = { { "6", SC_6 }, { "7", SC_7 }, { "8", SC_8 }, { "9", SC_9 }, { "0", SC_0 } }; +// [joystick] gameport.mode and fallback: what a list shows and stores +static const PrefsChoiceT sGamePortChoices[JOY_GAMEPORT_CHOICE_COUNT] = { + { "Two joysticks", JOY_GAMEPORT_TWO, 0 }, + { "One joystick with 4 axes", JOY_GAMEPORT_FOUR, 1 } +}; + +static const PrefsChoiceT sFallbackChoices[JOY_FALLBACK_CHOICE_COUNT] = { + { "When no joystick is found", JOY_FALLBACK_AUTO, 0 }, + { "Always", JOY_FALLBACK_ALWAYS, 1 }, + { "Never", JOY_FALLBACK_NEVER, 2 } +}; + +static const JoyChoiceTableT sChoiceTables[JoyChoiceCountE] = { + [JoyChoiceGamePortE] = { JOY_KEY_GAMEPORT_MODE, JOY_GAMEPORT_TWO, sGamePortChoices, JOY_GAMEPORT_CHOICE_COUNT }, + [JoyChoiceFallbackE] = { JOY_KEY_FALLBACK, JOY_FALLBACK_AUTO, sFallbackChoices, JOY_FALLBACK_CHOICE_COUNT } +}; + +// The keyboard joystick's slots and their default keys +static const char *const sSlotNames[JOY_KBD_SLOT_COUNT] = { JOY_KBD_SLOT_NAMES }; +static const char *const sSlotDefaults[JOY_KBD_SLOT_COUNT] = { JOY_KBD_SLOT_DEFAULTS }; + // ============================================================ // Module state // ============================================================ @@ -200,6 +229,7 @@ static void addUnits(const JoyDriverT *drv); static void applyState(JoyDeviceT *dev); static void autoRange(JoyDeviceT *dev, int32_t axis, int32_t raw); static bool axisEventDue(int32_t last, int32_t now); +static bool choiceValid(JoyChoiceE which); static int32_t clampAxis(int64_t v); static void copySection(PrefsHandleT *from, PrefsHandleT *to); static void deliverEvents(int32_t handle); @@ -215,6 +245,9 @@ void joyAppCleanup(int32_t appId); int32_t joyAxisScale(const JoyAxisCalT *cal, int32_t raw); bool joyButtonPressed(int32_t handle, int32_t button); bool joyButtonWentDown(int32_t handle, int32_t button); +int32_t joyChoiceGet(JoyChoiceE which); +const PrefsChoiceT *joyChoices(JoyChoiceE which, int32_t *count); +bool joyChoiceSet(JoyChoiceE which, int32_t index); void joyClose(int32_t handle); int32_t joyCount(void); int32_t joyDeadZoneApply(int32_t value, int32_t percent); @@ -222,9 +255,12 @@ bool joyGetCalibration(int32_t device, int32_t axis, JoyAxis int32_t joyGetDeadZone(int32_t device); bool joyGetInfo(int32_t device, JoyInfoT *out); bool joyGetRaw(int32_t device, JoyRawT *out); +int32_t joyKbdSlotGet(int32_t slot); +bool joyKbdSlotSet(int32_t slot, int32_t keyIndex); int32_t joyKeyCode(int32_t index); int32_t joyKeyCount(void); int32_t joyKeyFind(const char *name); +int32_t joyKeyIndex(const char *name); const char *joyKeyName(int32_t index); const char *joyKeyNameOf(int32_t rawCode); int32_t joyOpen(AppContextT *ctx, int32_t device); @@ -232,6 +268,7 @@ int32_t joyOpenShared(AppContextT *ctx, int32_t device); void joyPump(void); bool joyRead(int32_t handle, JoyStateT *out); int32_t joyRescan(void); +int32_t joyRescanApp(int32_t appId); bool joySetCalibration(int32_t device, int32_t axis, const JoyAxisCalT *cal); bool joySetDeadZone(int32_t device, int32_t percent); bool joySetEventFn(int32_t handle, JoyEventFnT fn, void *userData); @@ -249,6 +286,7 @@ static void pollTick(void *userData); static void probeAll(void); static void probeDrivers(bool fallback); static void settingsApplied(void); +static void slotKey(int32_t slot, char *out, int32_t outSize); static void startPolling(void); static void stopPolling(void); static bool validCalAxis(int32_t device, int32_t axis); @@ -350,6 +388,12 @@ static bool axisEventDue(int32_t last, int32_t now) { } +// which names a listed setting (sChoiceTables). +static bool choiceValid(JoyChoiceE which) { + return (int32_t)which >= 0 && which < JoyChoiceCountE; +} + + static int32_t clampAxis(int64_t v) { if (v < JOY_AXIS_MIN) { return JOY_AXIS_MIN; @@ -609,6 +653,36 @@ bool joyButtonWentDown(int32_t handle, int32_t button) { } +int32_t joyChoiceGet(JoyChoiceE which) { + if (!choiceValid(which)) { + return -1; + } + + const JoyChoiceTableT *t = &sChoiceTables[which]; + int32_t idx = prefsChoiceIndex(t->choices, t->count, joySettingGet(t->key, NULL)); + + return idx >= 0 ? idx : prefsChoiceIndex(t->choices, t->count, t->defaultName); +} + + +const PrefsChoiceT *joyChoices(JoyChoiceE which, int32_t *count) { + bool valid = choiceValid(which); + + *count = valid ? sChoiceTables[which].count : 0; + return valid ? sChoiceTables[which].choices : NULL; +} + + +bool joyChoiceSet(JoyChoiceE which, int32_t index) { + if (!choiceValid(which) || index < 0 || index >= sChoiceTables[which].count) { + return false; + } + + joySettingSet(sChoiceTables[which].key, sChoiceTables[which].choices[index].iniName); + return true; +} + + void joyClose(int32_t handle) { JoyHandleT *h = handleAt(handle); @@ -693,6 +767,35 @@ bool joyGetRaw(int32_t device, JoyRawT *out) { } +// A setting naming no known key reads as the slot's default. +int32_t joyKbdSlotGet(int32_t slot) { + char key[SETTING_BUF]; + + if (slot < 0 || slot >= JOY_KBD_SLOT_COUNT) { + return -1; + } + + slotKey(slot, key, sizeof(key)); + + int32_t idx = joyKeyIndex(joySettingGet(key, sSlotDefaults[slot])); + + return idx >= 0 ? idx : joyKeyIndex(sSlotDefaults[slot]); +} + + +bool joyKbdSlotSet(int32_t slot, int32_t keyIndex) { + char key[SETTING_BUF]; + + if (slot < 0 || slot >= JOY_KBD_SLOT_COUNT || keyIndex < 0 || keyIndex >= DVX_ARRAY_LEN(sKeys)) { + return false; + } + + slotKey(slot, key, sizeof(key)); + joySettingSet(key, sKeys[keyIndex].name); + return true; +} + + int32_t joyKeyCode(int32_t index) { if (index < 0 || index >= DVX_ARRAY_LEN(sKeys)) { return -1; @@ -708,9 +811,14 @@ int32_t joyKeyCount(void) { int32_t joyKeyFind(const char *name) { - for (int32_t i = 0; i < DVX_ARRAY_LEN(sKeys); i++) { + return joyKeyCode(joyKeyIndex(name)); +} + + +int32_t joyKeyIndex(const char *name) { + for (int32_t i = 0; name && i < DVX_ARRAY_LEN(sKeys); i++) { if (strcasecmp(sKeys[i].name, name) == 0) { - return sKeys[i].code; + return i; } } @@ -838,6 +946,12 @@ int32_t joyRescan(void) { } +int32_t joyRescanApp(int32_t appId) { + joyAppCleanup(appId); + return joyRescan(); +} + + bool joySetCalibration(int32_t device, int32_t axis, const JoyAxisCalT *cal) { char key[SETTING_BUF]; char val[SETTING_BUF]; @@ -1140,6 +1254,12 @@ static void settingsApplied(void) { // The poll timer (first handle opened). What the interrupt touches is // pinned once. +// The [joystick] key of a keyboard joystick slot. +static void slotKey(int32_t slot, char *out, int32_t outSize) { + snprintf(out, (size_t)outSize, "%s%s", JOY_KBD_KEY_PREFIX, sSlotNames[slot]); +} + + static void startPolling(void) { if (!sTickLocked) { platformLockRegion((const void *)pollTick, TICK_LOCK_BYTES, true); diff --git a/src/libs/kpunch/joystick/joystick.dhs b/src/libs/kpunch/joystick/joystick.dhs index 12d8c863..98567e9b 100644 --- a/src/libs/kpunch/joystick/joystick.dhs +++ b/src/libs/kpunch/joystick/joystick.dhs @@ -40,6 +40,14 @@ .index joySetEventFn .index joyPump .index joyRescan +.index joyRescanApp +.index joyChoices +.index joyChoiceGet +.index joyChoiceSet +.index JoyChoiceE +.index joyKbdSlotGet +.index joyKbdSlotSet +.index joyKeyIndex .index joyGetRaw .index joyGetCalibration .index joySetCalibration @@ -118,7 +126,9 @@ Settings changed through the library take effect at once and reach DVX.INI only .deadZone The device's dead zone in percent. .endtable -joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise. +Two settings are offered as lists: joyChoices(which, &count) returns the choices of JoyChoiceGamePortE (gameport.mode: "Two joysticks", "One joystick with 4 axes") or JoyChoiceFallbackE (fallback: "When no joystick is found", "Always", "Never") in list order, each a PrefsChoiceT (dvxPrefs.h) with its label for people and the name stored; joyChoiceGet(which) is the index of the current one (the default for a missing or unknown value) and joyChoiceSet(which, index) stores one. The keyboard joystick's keys go by slot (0 to JOY_KBD_SLOT_COUNT - 1 in JOY_KBD_SLOT_NAMES order: up, down, left, right, button1 to button4): joyKbdSlotGet(slot) is the joyKeyName index of the slot's key (its default key when the setting names no known key), joyKbdSlotSet(slot, keyIndex) stores one; the keyboard driver reads its keys through joyKbdSlotGet. joyKeyIndex(name) finds a key name's index (joyKeyFind its raw code). The Control Panel's Game Controllers page uses these, so the stored names and the defaults live only in the library. + +joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise. joyRescanApp(appId) closes every handle of that program first (joyAppCleanup) and then rescans, for a program that holds handles itself (the BASIC JoyRescan; the Control Panel closes its one handle and calls joyRescan). .h2 The Game Port Driver @@ -153,4 +163,6 @@ Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded a src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle. +For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1. + The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1. diff --git a/src/libs/kpunch/joystick/joystick.h b/src/libs/kpunch/joystick/joystick.h index 37a795af..949f35c9 100644 --- a/src/libs/kpunch/joystick/joystick.h +++ b/src/libs/kpunch/joystick/joystick.h @@ -103,6 +103,18 @@ #define JOY_KBD_SLOT_NAMES "up", "down", "left", "right", "button1", "button2", "button3", "button4" #define JOY_KBD_SLOT_DEFAULTS "Up", "Down", "Left", "Right", "LCtrl", "Space", "LShift", "Z" +// The [joystick] settings offered as a list (joyChoices): the game port's +// mode (gameport.mode) and when the keyboard joystick stands in +// (fallback) +typedef enum { + JoyChoiceGamePortE, + JoyChoiceFallbackE, + JoyChoiceCountE +} JoyChoiceE; + +#define JOY_GAMEPORT_CHOICE_COUNT 2 +#define JOY_FALLBACK_CHOICE_COUNT 3 + // JoyInfoT.caps #define JOY_CAP_CALIBRATE 0x01 // analog axes: raw values need calibrating #define JOY_CAP_KEYBOARD 0x02 // made from keys: reads neutral unless the program's window has the focus @@ -157,6 +169,12 @@ typedef struct { int32_t max; } JoyAxisCalT; +// The parts of a calibration as BASIC numbers them (JoyCalibration, +// joystick.bas JOY_CAL_*) +#define JOY_CAL_MIN 0 +#define JOY_CAL_CENTER 1 +#define JOY_CAL_MAX 2 + typedef enum { JoyEventAxisE, // index = axis, value = new position JoyEventButtonDownE, // index = button @@ -190,6 +208,11 @@ bool joyGetInfo(int32_t device, JoyInfoT *out); // handles are open (the list stays as it is). int32_t joyRescan(void); +// joyRescan for a program that may hold handles itself: every handle of +// appId is closed first (as joyAppCleanup), so only other programs' +// handles stop the search. +int32_t joyRescanApp(int32_t appId); + // ============================================================ // Reading // ============================================================ @@ -271,6 +294,25 @@ void joySettingSet(const char *key, const char *value); void joySettingsLoad(PrefsHandleT *prefs); void joySettingsStore(PrefsHandleT *prefs); +// The choices of a listed setting in list order (label for people, +// iniName stored; value unused) and their count; NULL for another which. +const PrefsChoiceT *joyChoices(JoyChoiceE which, int32_t *count); + +// The index of the setting's current choice (its default when missing or +// unknown; -1 for another which), and choosing one (applied at once like +// joySettingSet; a new game port mode or fallback shows in the device +// list after joyRescan). Set returns false for a bad which or index. +int32_t joyChoiceGet(JoyChoiceE which); +bool joyChoiceSet(JoyChoiceE which, int32_t index); + +// The keyboard joystick's keys: slot 0..JOY_KBD_SLOT_COUNT-1 in +// JOY_KBD_SLOT_NAMES order. Get returns the joyKeyName index of the +// slot's key (its default key when the setting is missing or names no +// known key; -1 for a bad slot); Set stores key index keyIndex for the +// slot (false for a bad slot or index). +int32_t joyKbdSlotGet(int32_t slot); +bool joyKbdSlotSet(int32_t slot, int32_t keyIndex); + // ============================================================ // Helpers // ============================================================ @@ -285,10 +327,12 @@ int32_t joyDeadZoneApply(int32_t value, int32_t percent); // Keys a keyboard joystick can use: count, the name ("Up", "LCtrl", "A") // and raw key code (KEY_RAW_CODE) of entry index, and lookups both ways -// (-1 / NULL when unknown). +// (-1 / NULL when unknown): joyKeyFind gives a name's raw code, +// joyKeyIndex its entry index. int32_t joyKeyCode(int32_t index); int32_t joyKeyCount(void); int32_t joyKeyFind(const char *name); +int32_t joyKeyIndex(const char *name); const char *joyKeyName(int32_t index); const char *joyKeyNameOf(int32_t rawCode); @@ -354,11 +398,40 @@ int32_t joyBasStrig(int32_t n); int32_t JoyAxes(int32_t joy); int32_t JoyAxis(int32_t joy, int32_t axis); +int32_t JoyAxisPresent(int32_t joy, int32_t axis); int32_t JoyButton(int32_t joy, int32_t button); int32_t JoyButtons(int32_t joy); +int32_t JoyCalibration(int32_t joy, int32_t axis, int32_t part); +int32_t JoyCanCalibrate(int32_t joy); +int32_t JoyChoice(int32_t which); +int32_t JoyChoiceCount(int32_t which); +const char *JoyChoiceLabel(int32_t which, int32_t index); +int32_t JoyClearCalibration(int32_t joy, int32_t axis); +int32_t JoyConnected(int32_t joy); int32_t JoyCount(void); +int32_t JoyDeadZone(int32_t joy); int32_t JoyHat(int32_t joy, int32_t hat); +int32_t JoyHats(int32_t joy); +int32_t JoyIsKeyboard(int32_t joy); +int32_t JoyKbdSlot(int32_t slot); +int32_t JoyKbdSlotCount(void); +int32_t JoyKeyCode(int32_t index); +int32_t JoyKeyCount(void); +int32_t JoyKeyIndex(const char *keyName); +const char *JoyKeyName(int32_t index); +const char *JoyKeyNameOf(int32_t rawCode); const char *JoyName(int32_t joy); +int32_t JoyRawAxis(int32_t joy, int32_t axis); +int32_t JoyRawButtons(int32_t joy); +int32_t JoyRescan(void); +int32_t JoySetCalibration(int32_t joy, int32_t axis, int32_t minV, int32_t centerV, int32_t maxV); +int32_t JoySetChoice(int32_t which, int32_t index); +int32_t JoySetDeadZone(int32_t joy, int32_t percent); +int32_t JoySetKbdSlot(int32_t slot, int32_t keyIndex); +void JoySetSetting(const char *key, const char *value); +const char *JoySetting(const char *key, const char *defaultValue); +void JoySettingsLoad(int32_t prefs); +void JoySettingsStore(int32_t prefs); int32_t JoyX(int32_t joy); int32_t JoyY(int32_t joy); diff --git a/src/libs/kpunch/libdvx/Makefile b/src/libs/kpunch/libdvx/Makefile index 941575b5..0bc3171d 100644 --- a/src/libs/kpunch/libdvx/Makefile +++ b/src/libs/kpunch/libdvx/Makefile @@ -36,9 +36,9 @@ LIBSDIR = ../../../../bin/libs # Core sources. dvxPrefs.c is not among them on DOS: the loader needs it # before libdvx loads, so dvx.exe links it and exports prefs* to every DXE. SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c dvxDriver.c \ - dvxApp.c dvxDialog.c dvxResource.c \ + dvxApp.c dvxDialog.c dvxPrefsBas.c dvxResource.c \ widgetClass.c widgetCore.c widgetScrollbar.c \ - widgetLayout.c widgetEvent.c widgetOps.c + widgetLayout.c widgetEvent.c widgetOps.c widgetIface.c # Resource writing is shared with the dvxres tool and lives beside it. TOOLSDIR = ../../../tools diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs index b4685f21..9ae0ea1e 100644 --- a/src/libs/kpunch/libdvx/apiref.dhs +++ b/src/libs/kpunch/libdvx/apiref.dhs @@ -805,7 +805,7 @@ Returns: Native pixel value suitable for direct framebuffer write. void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b); .endcode -Reverse of packColor -- decode a native pixel value to 8-bit RGB components. For direct-color modes, reverses the shift/mask arithmetic. For 8-bit mode, looks up the palette entry at the given index. +Reverse of packColor -- decode a native pixel value to 8-bit RGB components. For direct-color modes, reverses the shift/mask arithmetic and widens a field narrower than 8 bits by repeating its bits, so black and full channels come back exact (white in a 16-bit mode reads as 255, 255, 255). For 8-bit mode, looks up the palette entry at the given index. .table Parameter Description @@ -863,8 +863,11 @@ Reset the clip rectangle to the full display dimensions. .index drawMaskedBitmap .index drawTermRow .index drawArrowGlyph +.index drawScrollbar .index drawScrollbarArrow +.index drawScrollbarArrowButton .index drawFocusRect +.index drawFocusRectDots .index drawHLine .index drawVLine @@ -1183,6 +1186,28 @@ Draw a solid triangle of rows rows (base 2 * rows - 1 pixels) pointing in direct color Fill color .endtable +.h2 drawScrollbar + +.code +void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize); +.endcode + +Paint a complete scrollbar from a ready-made thumb geometry: the sunken trough, an arrow button (drawScrollbarArrowButton) at each end, and a raised thumb thumbPos pixels past the first arrow button. The window scrollbars, the widget scrollbars and wmDrawScrollbarAt (which computes the thumb from a scroll state) all paint through it. + +.table + Parameter Description + --------- ----------- + d Display context + ops Blit operations vtable + colors Color scheme + orient ScrollbarVerticalE (arrows up and down) or ScrollbarHorizontalE (arrows left and right) + x, y Top-left corner of the bar + length Bar length in pixels along its axis, both arrow buttons included + barW Bar thickness, which is also the side of the square arrow buttons (SCROLLBAR_WIDTH for the standard look) + thumbPos Thumb offset in pixels from the end of the first arrow button + thumbSize Thumb length in pixels; 0 or less draws no thumb +.endtable + .h2 drawScrollbarArrow .code @@ -1191,6 +1216,25 @@ void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *co Draw the arrow glyph of a scrollbar button whose top-left is (x, y) and side is size, using drawArrowGlyph centered in the button with colors->scrollbarFg. +.h2 drawScrollbarArrowButton + +.code +void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir); +.endcode + +Draw one complete arrow button of a scrollbar, as the scrollbar painter does at each end of a bar: a size x size raised button (1px bevel on colors->scrollbarBg) whose top-left is (x, y), with the drawScrollbarArrow glyph pointing dir centered on it. The BASIC DrawScrollArrow method draws with it. + +.table + Parameter Description + --------- ----------- + d Display context + ops Blit operations vtable + colors Color scheme + x, y Top-left corner of the button + size Side of the square button in pixels + dir Direction the arrow points (ScrollArrowUpE/DownE/LeftE/RightE) +.endtable + .h2 drawFocusRect .code @@ -1208,6 +1252,15 @@ Draw a 1px dotted rectangle (alternating pixels). Used for keyboard focus indica color Dot color (packed) .endtable +.h2 drawFocusRectDots + +.code +typedef void (*DrawDotFnT)(void *ctx, int32_t x, int32_t y); +void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn, void *ctx); +.endcode + +The dots drawFocusRect draws, one fn call each and without clipping: the one definition of the pattern, for drawing it somewhere other than the back buffer. drawFocusRect plots them in the back buffer inside the clip rect; the BASIC DrawFocusRect method of forms and PictureBoxes sets them on the canvas. + .h2 drawHLine .code @@ -1366,7 +1419,9 @@ Returns: true if the rectangles overlap, false if disjoint. .index wmAddMenuItem .index wmAddSubMenu .index wmCreateMenu +.index wmRemoveMenu .index wmDrawChrome +.index wmDrawScrollbarAt .index wmHitTest .index wmDragBegin .index wmResizeBegin @@ -1645,6 +1700,24 @@ Insert a horizontal separator line. Separators are not interactive. menu Menu to append separator to .endtable +.h3 wmRemoveMenu + +.code +bool wmRemoveMenu(WindowT *win, MenuT *menu, const DisplayT *d); +.endcode + +Take one menu (a wmAddMenu result) off the window's menu bar and free it with its items and submenus. Removing the last menu removes the bar, and the content buffer is reallocated for d as by wmDestroyMenuBar. The content area changes size when the bar goes, so the caller lays the window out again (dvxResizeWindow with the same size does it and fires onResize). The BASIC RemoveMenu method uses it. + +.table + Parameter Description + --------- ----------- + win Window whose menu bar holds the menu + menu The menu to remove + d Display, for reallocating the content buffer when the bar goes +.endtable + +Returns: true if the menu was on the bar. + .h3 wmRemoveMenuItem .code @@ -1725,6 +1798,14 @@ Enable or disable a menu item by command ID. enabled true = enabled, false = grayed out .endtable +.h3 wmMenuItemSetLabel + +.code +void wmMenuItemSetLabel(MenuItemT *item, const char *label); +.endcode + +Give an item a new label, written as wmAddMenuItem takes it: & before the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). The accelerator follows the new label; the item's ID, type and state stay. An open popup shows the new label the next time it is drawn. A NULL item or label does nothing. + .h3 wmAddSubMenu .code @@ -1878,13 +1959,35 @@ Draw icons for all minimized windows along the bottom of the screen. Each icon s clipTo Dirty rectangle .endtable +.h3 wmDrawScrollbarAt + +.code +void wmDrawScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); +.endcode + +Draw a standalone scrollbar of either orientation at the given screen coordinates, without a ScrollbarT struct. A vertical bar (ScrollbarVerticalE) is SCROLLBAR_WIDTH wide and length tall with up and down arrow buttons; a horizontal one (ScrollbarHorizontalE) is length wide and SCROLLBAR_WIDTH tall with left and right arrow buttons. Thumb size and position are computed from scrollPos / visibleItems / totalItems with the window scrollbars' rule; there is no thumb when everything is visible. wmVScrollbarAtClick maps a click on the bar to a new scrollPos. The BASIC DrawScrollBar method draws with it. + +.table + Parameter Description + --------- ----------- + d Display context + ops Blit operations vtable + colors Color scheme + orient ScrollbarVerticalE or ScrollbarHorizontalE + x, y Screen position (top-left corner of the bar) + length Bar length in pixels along its axis, both arrow buttons included + scrollPos Current first visible item index + visibleItems Visible item count (for thumb size) + totalItems Total item count +.endtable + .h3 wmDrawVScrollbarAt .code void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); .endcode -Draw a standalone vertical scrollbar at the given screen coordinates, without a ScrollbarT struct. Used by popup lists (dropdown, combobox) that need a scrollbar inline within a popup overlay. Thumb size and position are computed from scrollPos / visibleItems / totalItems. +wmDrawScrollbarAt for a vertical bar h tall. Used by popup lists (dropdown, combobox) that need a scrollbar inline within a popup overlay. .table Parameter Description @@ -1893,7 +1996,7 @@ Draw a standalone vertical scrollbar at the given screen coordinates, without a ops Blit operations vtable colors Color scheme x, y Screen position (top of scrollbar) - h Track height in pixels + h Bar height in pixels, both arrow buttons included scrollPos Current top visible item index visibleItems Visible item count (for thumb size) totalItems Total item count @@ -1902,10 +2005,10 @@ Draw a standalone vertical scrollbar at the given screen coordinates, without a .h3 wmVScrollbarAtClick .code -int32_t wmVScrollbarAtClick(int32_t h, int32_t relY, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); +int32_t wmVScrollbarAtClick(int32_t length, int32_t offset, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); .endcode -Apply a click on a scrollbar drawn by wmDrawVScrollbarAt with the same h and scroll state; relY is the click's offset from the top of the bar. Returns the new scrollPos: the arrow buttons step one item, the trough steps a page (visibleItems) toward the click, and the thumb leaves the position unchanged. The result is clamped to 0 .. totalItems - visibleItems. The thumb comes from the same geometry the paint uses, so a click always matches what is drawn. +Apply a click on a scrollbar drawn by wmDrawScrollbarAt (either orientation) or wmDrawVScrollbarAt with the same length and scroll state; offset is the click's offset along the bar from its start: down from the top of a vertical bar, right from the left end of a horizontal one. Only that offset matters, so the function serves both orientations. Returns the new scrollPos: the arrow buttons step one item, the trough steps a page (visibleItems) toward the click, and the thumb leaves the position unchanged. The result is clamped to 0 .. totalItems - visibleItems. The thumb comes from the same geometry the paint uses, so a click always matches what is drawn. .h2 Hit Testing @@ -2299,9 +2402,18 @@ Install an icon that is already in display pixel format (for example from dvxLoa .index dvxSetWallpaper .index dvxSetWallpaperMode .index dvxSetMouseConfig +.index dvxColorsFromPrefs +.index dvxColorsToPrefs +.index dvxMouseFromPrefs +.index dvxMouseToPrefs +.index dvxWallpaperFromPrefs +.index dvxWallpaperToPrefs .index dvxCreateAccelTable .index dvxFreeAccelTable .index dvxAddAccel +.index dvxRemoveAccel +.index dvxAccelFormat +.index dvxAccelParse .index dvxRegisterHotkey .index dvxUnregisterHotkey .index dvxUnregisterAppHotkeys @@ -2333,6 +2445,7 @@ Install an icon that is already in display pixel format (for example from dvxLoa .index dvxDragOutlineShow .index Drag Feedback .index dvxResLoadIcon +.index dvxResLoadIconData .index dvxResLoadText .index dvxResLoadData .index dvxTextHash @@ -2370,6 +2483,9 @@ Fields applications and the shell commonly read (all others are internal state): int32_t wheelDirection 1 = normal, -1 = reversed int32_t wheelStep Lines per wheel notch (1-10, default 3) PlatformTicksT dblClickTicks Double-click interval in platformClock ticks + int32_t dblClickMs Double-click interval in milliseconds (as given to dvxSetMouseConfig) + int32_t mouseAccel Acceleration threshold last applied (MOUSE_ACCEL_*) + int32_t mouseMickeys Mickeys per 8 pixels last applied (MOUSE_SPEED_DEFAULT at start) uint32_t charHeightRecip 16.16 reciprocal of font.charHeight (popup row mapping) WindowT *modalWindow If non-NULL, only this window receives input int32_t dispatchDepth Callback nesting depth; destroys are deferred while > 0 @@ -2383,7 +2499,7 @@ Fields applications and the shell commonly read (all others are internal state): WallpaperModeE wallpaperMode Stretch, tile or center .endtable -The mouse speed and acceleration settings are not stored in AppContextT: dvxSetMouseConfig programs them into the platform layer, which re-applies them after every mouse driver reset. +dvxSetMouseConfig programs the mouse speed and acceleration into the platform layer, which re-applies them after every mouse driver reset; mouseAccel and mouseMickeys only record the last values applied (for dvxMouseToPrefs and settings pages). .h2 Initialization and Shutdown @@ -2967,7 +3083,7 @@ Reset all colors to the built-in defaults and repaint. bool dvxLoadTheme(AppContextT *ctx, const char *filename); .endcode -Load a theme file (INI format with [colors] section) and apply it. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names and malformed values are ignored, and channels are clamped to 0..255. +Load a theme file (INI format with [colors] section) and apply it through dvxColorsFromPrefs. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names and malformed values are ignored, and channels are clamped to 0..255. A missing file returns false. .table Parameter Description @@ -2984,7 +3100,7 @@ Returns: true on success. bool dvxSaveTheme(const AppContextT *ctx, const char *filename); .endcode -Save the current color scheme to a theme file. When the file already exists its comments and other lines are kept and only the [colors] keys are updated. The new copy is written to a temporary file first and swapped in only when every write succeeded, so a failed save leaves the old theme intact. +Save the current color scheme to a theme file (the keys dvxColorsToPrefs writes). When the file already exists its comments and other lines are kept and only the [colors] keys are updated. The new copy is written to a temporary file first and swapped in only when every write succeeded, so a failed save leaves the old theme intact. .table Parameter Description @@ -2995,6 +3111,15 @@ Save the current color scheme to a theme file. When the file already exists its Returns: true on success. +.h3 dvxColorsFromPrefs / dvxColorsToPrefs + +.code +bool dvxColorsFromPrefs(AppContextT *ctx, PrefsHandleT *h); +void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h); +.endcode + +The [colors] section of an INI handle, the one place its keys are read and written: dvxLoadTheme / dvxSaveTheme, the shell at start (DVX.INI) and Control Panel (OK, and its Cancel snapshot in a prefsCreate handle) all use these. FromPrefs reads every "name = r,g,b" key (malformed ones ignored, channels clamped to 0..255), repaints only when a color changed and returns true when any color key was read. ToPrefs writes every color. + .h3 dvxColorName .code @@ -3052,7 +3177,7 @@ Returns: true on success. void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode); .endcode -Change the wallpaper display mode and re-render. No effect if no wallpaper is loaded. +Change the wallpaper display mode; a loaded wallpaper is rendered again. With none loaded the mode is kept for the next one. .table Parameter Description @@ -3061,6 +3186,15 @@ Change the wallpaper display mode and re-render. No effect if no wallpaper is lo mode WallpaperStretchE, WallpaperTileE, or WallpaperCenterE .endtable +.h3 dvxWallpaperFromPrefs / dvxWallpaperToPrefs + +.code +bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h); +void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h); +.endcode + +The [desktop] wallpaper and mode keys of an INI handle (mode through prefsWallpaperChoices). FromPrefs applies them: a missing mode is WALLPAPER_MODE_DEFAULT and a missing wallpaper means none; the image is built again only when the path or mode differs from the current one. It returns false when the named image could not be loaded. ToPrefs writes the path (removes the key when there is none) and the mode. + .h2 Mouse Configuration .h3 dvxSetMouseConfig @@ -3082,7 +3216,16 @@ Configure mouse behavior. Updates the in-memory AppContextT fields and programs wheelStep Lines to scroll per wheel notch (1-10, default 3) .endtable -The speed, acceleration and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. +The speed, acceleration and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. The values are kept in AppContextT (wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep). + +.h3 dvxMouseFromPrefs / dvxMouseToPrefs + +.code +void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h); +void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h); +.endcode + +The [mouse] keys of an INI handle: wheel and acceleration (by name, through prefsWheelDirChoices and prefsAccelChoices), doubleclick (milliseconds), speed (mickeys per 8 pixels) and wheelspeed. FromPrefs applies them with dvxSetMouseConfig, defaults for missing keys (MOUSE_*_DEFAULT); ToPrefs writes the configuration in effect (an acceleration no choice has leaves that key alone). The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot. .h2 Input Entropy @@ -3261,6 +3404,7 @@ The key and modifiers are normalized at registration with the same rule applied .item Letters are uppercased ('s' and 'S' are the same key). .item Ctrl and Alt are always significant. Shift is significant for extended keys (F-keys, arrows, ...) and for any Ctrl/Alt combination, so Ctrl+S and Ctrl+Shift+S are different accelerators, as are F5 and Shift+F5. .item A printable character typed without Ctrl/Alt matches regardless of Shift, because the character already encodes Shift ('+', 'A'). +.item KEY_ENTER and KEY_BACKSPACE with ACCEL_CTRL or ACCEL_ALT match those keys however the BIOS reports them (with Alt it gives no ASCII byte; the keypad Enter has scancodes of its own), so (KEY_ENTER, ACCEL_ALT) is Alt with either Enter key. .endlist .table @@ -3272,6 +3416,41 @@ The key and modifiers are normalized at registration with the same rule applied cmdId Command ID passed to onMenu .endtable +.h3 dvxRemoveAccel + +.code +void dvxRemoveAccel(AccelTableT *table, int32_t cmdId); +.endcode + +Remove every shortcut of command cmdId from the table: a menu item that lost its key or went away. The BASIC menu Shortcut property and RemoveMenuItem use it. + +.table + Parameter Description + --------- ----------- + table Accelerator table (NULL is a no-op) + cmdId Command whose shortcuts go +.endtable + +.h3 dvxAccelFormat + +.code +void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize); +.endcode + +Write a key and its modifiers (given as for dvxAddAccel) as text, the way menus show it after a tab: the modifiers Ctrl, Alt and Shift in that order, each followed by +, then the key -- a letter or digit, F1 to F12, or one of the names Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up. Key 0 gives "". + +.code +dvxAccelFormat(KEY_ENTER, ACCEL_ALT, text, sizeof(text)); // "Alt+Enter" +.endcode + +.h3 dvxAccelParse + +.code +bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods); +.endcode + +Read key text as dvxAccelFormat writes it ("F2", "Ctrl+X", "Alt+Enter", "Shift+Del") into a key and ACCEL_* modifiers for dvxAddAccel or dvxRegisterHotkey. Case does not matter, blanks around the parts are ignored, and Delete and Insert are accepted for Del and Ins. Returns false, with *outKey 0, for empty text, an unknown modifier or an unknown key. + .h2 System-Wide Hotkeys A hotkey is a keystroke that works whichever window has the focus (or when none has): its callback runs instead of anything the focused window would do with the keys, including its menu keys and accelerators. Program Manager's item shortcut keys are hotkeys. Hotkeys are not checked while a menu or a modal dialog is open, and keys the system handles first (Alt+Tab, Alt+F4, Ctrl+F12, Ctrl+Esc, F1, F10, Alt+Space) never reach them. @@ -3402,7 +3581,7 @@ Tile windows vertically (stacked, full width, equal height). uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch); .endcode -Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. Caller must free with dvxFreeImage(). +Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage(). .table Parameter Description @@ -3413,7 +3592,7 @@ Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pix outPitch Output: row pitch in bytes .endtable -Returns: Pixel buffer, or NULL on failure. +Returns: Pixel buffer, or NULL on failure. An image without pixels (a width or height of 0, which some BMP headers declare) is a failure for every image loader. .h3 dvxLoadImageFromMemory @@ -3486,7 +3665,7 @@ Query image dimensions without decoding the full file. outW, outH Output: image dimensions .endtable -Returns: true on success. +Returns: true on success; false for an unreadable image or one without pixels. .h3 dvxSaveImage @@ -3644,6 +3823,23 @@ Load an icon/image resource from a DXE file and decode to native pixel format. C Returns: Pixel buffer, or NULL if not found. +.h3 dvxResLoadIconData + +.code +void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize); +.endcode + +The icon of a DVX program, widget or library file as the bytes of its image file: its DVX_RES_NAME_ICON resource ("icon32", a program's icon), else DVX_RES_NAME_WIDGET_ICON ("icon24", a widget's toolbox icon). Decode it with dvxLoadImageFromMemory or dvxLoadImageAlpha. Caller must free() the buffer. + +.table + Parameter Description + --------- ----------- + dxePath Path to the program, widget or library file + outSize Output: size of the data in bytes +.endtable + +Returns: The data, or NULL when the file has neither resource (or is no DVX file). + .h3 dvxResLoadText .code @@ -3791,12 +3987,19 @@ A library that has drivers puts them in drivers// in its source folder; it .index wgtDestroy .index wgtSetTooltip .index wgtResolveSize +.index wgtGetPaddingPx +.index wgtSetPaddingPx +.index wgtGetSpacingPx +.index wgtSetSpacingPx .index wgtLayout .index wgtPaint .index wgtSetDebugLayout +.index wgtGetDebugLayout .index wgtRegisterClass .index wgtUnregisterClass .index wgtUnregisterAppClasses +.index wgtUnregisterIface +.index WCLASS_RAISES_EVENTS .index wgtRegisterApi .index wgtGetApi .index WgtApiHeaderT @@ -3809,6 +4012,44 @@ A library that has drivers puts them in drivers// in its source folder; it .index wgtPixels .index wgtChars .index wgtPercent +.index WgtValueT +.index wgtValueInt +.index wgtValueBool +.index wgtValueFloat +.index wgtValueDouble +.index WGT_IFACE_DOUBLE +.index wgtValueStr +.index wgtValueToInt +.index wgtValueToBool +.index wgtValueToFloat +.index wgtValueToText +.index wgtParseBool +.index WgtDynIfaceT +.index WGT_DYN_ACCESS +.index WGT_SIG_DYNAMIC +.index WgtSigInfoT +.index wgtSigInfo +.index wgtIfaceSetType +.index wgtGetIfaceOf +.index wgtResolveTypeName +.index wgtIfaceFindProp +.index wgtIfaceFindMethod +.index wgtIfaceFindEvent +.index wgtCreateByName +.index wgtGetProp +.index wgtSetProp +.index wgtCallMethod +.index wgtIfaceGetProp +.index wgtIfaceSetProp +.index wgtIfaceGetPropText +.index wgtIfaceSetPropText +.index wgtIfaceCallMethod +.index wgtFireEvent +.index onEvent +.index WgtMouseActionE +.index WGT_METHOD_ON_MOUSE_EVENT +.index WGT_METHOD_ON_KEY_UP +.index WGT_METHOD_ON_FOCUS .h1 dvxWgt.h -- Widget System @@ -3845,6 +4086,7 @@ Core widget structure. Generic across all widget types; type-specific data lives MenuT *contextMenu Right-click menu (NULL = none) uint32_t serial Unique per allocation, never reused while the process runs (tells "the same widget" from a new widget at a recycled address) RectT paintClip Display clip when last painted (content-buffer coords, w = 0 until first paint); used by mouse dispatch inside clipping containers + onEvent(w, name, args, argc) Custom (named) events: every event wgtFireEvent fires that has no universal callback below (see Custom Events) .endtable Universal Callbacks: @@ -3867,6 +4109,8 @@ Universal Callbacks: onValidate(WidgetT *w) Return false to cancel a write .endtable +The universal callbacks belong to the widget's host (the application or the form runtime). A widget class sees the same input earlier through its own class methods: WGT_METHOD_ON_MOUSE_EVENT, WGT_METHOD_ON_KEY, WGT_METHOD_ON_KEY_UP, WGT_METHOD_ON_FOCUS, WGT_METHOD_ON_BLUR and WGT_METHOD_ON_WHEEL run before the matching callback (see Method IDs). + .h2 Size Specification Macros .table @@ -3895,6 +4139,8 @@ Universal Callbacks: WCLASS_RELAYOUT_ON_SCROLL Full relayout on scrollbar drag WCLASS_PRESS_RELEASE Click = press + release (Button, ImageButton) WCLASS_ACCEL_WHEN_HIDDEN Accelerator matching works even when invisible + WCLASS_HAS_TEXT The first field of w->data is the widget's own text (strdup / free) + WCLASS_RAISES_EVENTS The class fires the universal mouse, click, key and scroll callbacks itself (wgtFireEvent); the dispatcher does not call them for its widgets. Its class methods still get every event. A BASIC UserControl has it: its host sees only what the control's code raises. GotFocus / LostFocus still reach onFocus / onBlur. .endtable .h2 Window Integration @@ -4158,6 +4404,17 @@ Decode a tagged size value (WGT_SIZE_PIXELS/CHARS/PERCENT) into a concrete pixel Returns: Size in pixels. +.h3 wgtGetPaddingPx / wgtSetPaddingPx / wgtGetSpacingPx / wgtSetSpacingPx + +.code +int32_t wgtGetPaddingPx(const WidgetT *w); +void wgtSetPaddingPx(WidgetT *w, int32_t pixels); +int32_t wgtGetSpacingPx(const WidgetT *w); +void wgtSetSpacingPx(WidgetT *w, int32_t pixels); +.endcode + +A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return 0 when the size is unset (the container's default) or given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 restores the default) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these. + .h3 wgtLayout .code @@ -4210,6 +4467,14 @@ Draw colored borders around layout containers for debugging. enabled true = draw debug borders .endtable +.h3 wgtGetDebugLayout + +.code +bool wgtGetDebugLayout(void); +.endcode + +Returns true while the debug borders are drawn (the last value given to wgtSetDebugLayout; false at start). + .h2 Dynamic Widget Registration .h3 wgtRegisterClass @@ -4242,6 +4507,14 @@ Release a class registered with wgtRegisterClass, typically from an app's shutdo classId Type ID returned by wgtRegisterClass .endtable +.h3 wgtUnregisterIface + +.code +void wgtUnregisterIface(const char *name); +.endcode + +Remove the interface registered under name (wgtRegisterIface). A widget class made at run time -- a BASIC program's own UserControl -- leaves the registry this way when its owner is done with it, so the name can be registered again. Nothing happens when no interface has the name. + .h3 wgtUnregisterAppClasses .code @@ -4294,7 +4567,7 @@ static const ButtonApiT sApi = { .endtable .note warning -The create function must be the first field after the header. BASIC's form runtime and the designer instantiate widgets through WgtApiPrefixT.create (see createWidgetByIface), cast to the signature WgtIfaceT.createSig names, so whichever field comes first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it. +The create function must be the first field after the header. wgtCreateByName (which the form runtime and the designer use) instantiates widgets through WgtApiPrefixT.create, cast to the signature WgtIfaceT.createSig names, so whichever field comes first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it. .endnote .h3 wgtGetApi @@ -4484,8 +4757,13 @@ The following inline functions provide type-safe dispatch through the WidgetClas wclsScrollChildIntoView(parent, child) WGT_METHOD_SCROLL_CHILD_INTO_VIEW Scroll child visible wclsOnBlur(w) WGT_METHOD_ON_BLUR Widget lost keyboard focus wclsOnWheel(w, delta) WGT_METHOD_ON_WHEEL Mouse wheel; true = consumed + wclsOnMouseEvent(w, action, button, x, y, shift) WGT_METHOD_ON_MOUSE_EVENT Every press, release and move (hover too) + wclsOnKeyUp(w, keyCode, shift) WGT_METHOD_ON_KEY_UP Key released on the focused widget + wclsOnFocus(w) WGT_METHOD_ON_FOCUS Widget gained keyboard focus .endtable +The central dispatcher calls WGT_METHOD_ON_MOUSE_EVENT for every widget whose class has it, at exactly the points where it calls the universal mouse callbacks and on the same widget (the one under the cursor; inside a non-focusable WCLASS_NO_HIT_RECURSE container, a child with the method gets them itself, and a container with the method keeps them), and always before the callback. action is a WgtMouseActionE: WgtMouseDownE and WgtMouseUpE carry the 1-based button ordinal (1 left, 2 right, 3 middle), WgtMouseMoveE the MOUSE_* mask of the held buttons (0 is a hover move). x and y are content-relative (contentOffX / contentOffY subtracted), shift is the KEY_MOD_* Shift / Ctrl / Alt state. A class that captures the mouse on a press (sets sDragWidget in its WGT_METHOD_ON_MOUSE handler) also gets the moves while the button is held and the left release, wherever the pointer is. WGT_METHOD_ON_KEY_UP runs before onKeyUp with the same arguments, WGT_METHOD_ON_FOCUS before onFocus (the counterpart of ON_BLUR). If a class method destroys widgets, the callback after it is skipped. Classes without these methods see no change. + The mouse wheel is offered first to an ON_WHEEL handler on the focused widget, then on each ancestor, with delta in notches (positive = down, the user's reverse-wheel setting already applied; the lines-per-notch preference is AppContextT.wheelStep). Returning true consumes the event. Otherwise a WCLASS_SCROLLABLE focused widget (or a scroll-container ancestor) receives Up/Down arrow keys, and failing that the window scrollbar moves. The widget's onScroll callback fires in every case. .h2 Method IDs @@ -4518,6 +4796,9 @@ Fixed constants used to index into WidgetClassT.handlers[]. Method IDs are stabl 20 WGT_METHOD_SCROLL_CHILD_INTO_VIEW void (parent, child) 21 WGT_METHOD_ON_BLUR void (w) 22 WGT_METHOD_ON_WHEEL bool (w, delta) + 23 WGT_METHOD_ON_MOUSE_EVENT void (w, action, button, x, y, shift) + 24 WGT_METHOD_ON_KEY_UP void (w, keyCode, shift) + 25 WGT_METHOD_ON_FOCUS void (w) -- WGT_METHOD_MAX = 32 (room for future methods) .endtable @@ -4572,6 +4853,8 @@ Describes a BASIC-exposed property on a widget type. Used by the form runtime an enumNames For WGT_IFACE_ENUM: NULL-terminated array of value names .endtable +A property is readable when getFn is not NULL and writable when setFn is not NULL -- the one rule for every interface (the form runtime, the designer and the property sheet all apply it). A dynamic interface (WgtIfaceT.dyn) puts WGT_DYN_ACCESS in getFn / setFn for "present"; they are never called. A FLOAT getter returns float and its setter takes float. + .h3 WgtMethodDescT Describes an invokable method on a widget type. @@ -4613,18 +4896,26 @@ Calling convention IDs (method sig field). IDs are stable ABI; new ones are appe WGT_SIG_STR_STR 21 void fn(WidgetT *, const char *, const char *) WGT_SIG_STR_BOOL 22 void fn(WidgetT *, const char *, bool) WGT_SIG_RET_INT_STR 23 int32_t fn(WidgetT *, const char *) -- status-returning string method (e.g. Load/Save) + WGT_SIG_STR_STR_BOOL_BOOL 24 bool fn(WidgetT *, const char *, const char *, bool, bool) + WGT_SIG_RET_INT_INT 25 int32_t fn(const WidgetT *, int32_t) + WGT_SIG_DYNAMIC 26 bool fn(WidgetT *, const WgtValueT *args, int32_t argc, WgtValueT *ret) (WgtDynMethodFnT): any arguments, as they come .endtable +wgtSigInfo describes each signature (WgtSigInfoT: argc, retType and argTypes[], WGT_SIG_MAX_ARGS = 5; NULL for WGT_SIG_DYNAMIC). wgtIfaceCallMethod is the one place that calls through these signatures. + .h3 WgtEventDescT -Describes an event beyond the standard common set (Click, DblClick, Change, GotFocus, LostFocus). +Describes an event beyond the standard common set (Click, DblClick, Change, GotFocus, LostFocus, the key and mouse events ...). The widget fires it with wgtFireEvent. .table Field Description ----- ----------- - name Event name (e.g. "Change", "Timer") + name Event name (e.g. "ColumnClick", "Timer") + params The handler's parameter list as BASIC writes it ("Topic As String", "" for none); NULL when the event is one of the BASIC master list's (Timer, Validate, Reposition) and takes its parameters from there .endtable +params is the only place a widget's own event parameters are written down: the IDE's handler stubs and designer.bas CtlEventParams$ read it. The shipped widgets' own events: ListView ColumnClick (Column As Integer, 0-based), TextArea GutterClick (LineNumber As Integer, 1-based), HelpPane LinkClick (Topic As String). + .h3 WgtIfaceT Top-level interface descriptor registered by each widget DXE. @@ -4644,8 +4935,156 @@ Top-level interface descriptor registered by each widget DXE. isContainer true = widget can hold child widgets defaultEvent Default event name for form designer namePrefix Auto-name prefix (NULL = derive from basName) + dyn Dynamic interface hooks (WgtDynIfaceT), NULL for a widget written in C .endtable +.h3 WgtDynIfaceT + +.code +typedef struct { + WidgetT *(*create)(WidgetT *parent, const WgtIfaceT *iface); + bool (*getProp)(WidgetT *w, int32_t propIdx, WgtValueT *out); + bool (*setProp)(WidgetT *w, int32_t propIdx, const WgtValueT *value); + bool (*callMethod)(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret); + void *ctx; +} WgtDynIfaceT; +.endcode + +A widget class whose properties and methods are not C functions with fixed signatures (a BASIC UserControl, a scripted widget) sets WgtIfaceT.dyn. The generic access functions then call these hooks instead of the descriptors' getFn / setFn / fn: create makes a widget for wgtCreateByName, propIdx and methodIdx index the interface's props[] and methods[]. Each hook returns false when it could not do the job. A value read is converted to the property's type; a method whose sig is a fixed WGT_SIG_* gets its arguments converted to that signature and its result converted to the signature's type, a WGT_SIG_DYNAMIC method gets them as they come. Strings returned follow the WgtValueT lifetime rule. ctx is the provider's own pointer. + +.h2 Generic Values + +.h3 WgtValueT + +.code +typedef struct { + uint8_t type; // WGT_IFACE_* + union { + int32_t i; // INT, BOOL (0 / 1), ENUM + double f; // FLOAT, DOUBLE + const char *s; // STRING + }; +} WgtValueT; +.endcode + +One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits. + +.h3 wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText + +.code +int32_t wgtValueToInt(const WgtValueT *v); +bool wgtValueToBool(const WgtValueT *v); +double wgtValueToFloat(const WgtValueT *v); +const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize); +.endcode + +Convert any value: numbers convert to each other (a FLOAT truncates toward zero and is clamped to the int32_t range), a BOOL is 0 / 1, a STRING is parsed (decimal, strtod, wgtParseBool), NONE is 0 / false / "". wgtValueToText returns the value's own text for a STRING, otherwise formats into buf (INT / ENUM decimal, BOOL "True" / "False", FLOAT %g); WGT_VALUE_TEXT_MAX (32) bytes hold any non-STRING value. + +.h3 wgtParseBool + +.code +bool wgtParseBool(const char *text, bool defaultValue); +.endcode + +The .frm and property-sheet boolean rule: "True" or "-1" is true, "False" or "0" false (case-insensitive), anything else (and NULL) defaultValue. + +.h2 Generic Widget Access + +One implementation of "create, read, write and call by name" for every widget, static or dynamic. The form runtime, the IDE designer and C programs all use it; nothing else switches on WGT_SIG_* or WgtCreateSigE. + +.h3 wgtIfaceSetType / wgtGetIfaceOf + +.code +void wgtIfaceSetType(const char *name, int32_t typeId); +const WgtIfaceT *wgtGetIfaceOf(const WidgetT *w, const char **outName); +.endcode + +wgtIfaceSetType binds the interface registered under name to the widget class typeId (from wgtRegisterClass); every shipped widget calls it in wgtRegister after wgtRegisterIface. wgtGetIfaceOf then finds the interface of any widget of that class (NULL when none is bound); outName (may be NULL) receives the registered type name. wgtUnregisterClass drops the binding. + +.h3 wgtResolveTypeName + +.code +const char *wgtResolveTypeName(const char *name); +.endcode + +The registered type name for a BASIC name ("CommandButton" -> "button", tried first) or for a type name that has an API or an interface; NULL when neither is registered. + +.h3 wgtIfaceFindProp / wgtIfaceFindMethod / wgtIfaceFindEvent + +.code +const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propName); +const WgtMethodDescT *wgtIfaceFindMethod(const WgtIfaceT *iface, const char *methodName); +const WgtEventDescT *wgtIfaceFindEvent(const WgtIfaceT *iface, const char *eventName); +.endcode + +Case-insensitive lookups of a descriptor by name (the first match); NULL when iface is NULL or nothing matches. + +.h3 wgtCreateByName + +.code +WidgetT *wgtCreateByName(const char *name, WidgetT *parent); +.endcode + +Create a widget of type name (a BASIC or type name, wgtResolveTypeName) under parent: a dynamic interface's create hook, otherwise the API's create function called by the interface's createSig with its createArgs ("" for a text, an empty picture for WGT_CREATE_PARENT_DATA; a type without an interface is created as fn(parent)). Returns NULL when the type is unknown or creation failed. + +.h3 wgtGetProp / wgtSetProp / wgtCallMethod + +.code +bool wgtGetProp(WidgetT *w, const char *propName, WgtValueT *out); +bool wgtSetProp(WidgetT *w, const char *propName, const WgtValueT *value); +bool wgtCallMethod(WidgetT *w, const char *methodName, const WgtValueT *args, int32_t argc, WgtValueT *ret); +.endcode + +By name on any widget, through its interface (wgtGetIfaceOf). False when the widget has no interface or no such property or method, otherwise as wgtIfaceGetProp / wgtIfaceSetProp / wgtIfaceCallMethod. + +.code +WgtValueT v = wgtValueStr("Right"); +WgtValueT item = wgtValueStr("one"); +WgtValueT count; + +wgtSetProp(label, "Alignment", &v); +wgtCallMethod(list, "AddItem", &item, 1, NULL); +wgtCallMethod(list, "ListCount", NULL, 0, &count); +.endcode + +.h3 wgtIfaceGetProp / wgtIfaceSetProp + +.code +bool wgtIfaceGetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, WgtValueT *out); +bool wgtIfaceSetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const WgtValueT *value); +.endcode + +Read or write property p (from iface's props[]) of w. A read is false when p is write-only or the dynamic hook failed (out is then the zero value of p's type); a STRING read is never NULL. A write converts value to p's type (wgtValueTo*; a STRING for an ENUM property is looked up in enumNames, case-insensitive) and is false when p is read-only, an enum name is unknown, or the dynamic hook refused. + +.h3 wgtIfaceGetPropText / wgtIfaceSetPropText + +.code +bool wgtIfaceGetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); +bool wgtIfaceSetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const char *text); +.endcode + +The same through the .frm / property-sheet text form: ENUM by value name, INT decimal, BOOL wgtParseBool (written "True" / "False"), FLOAT strtod / %g, STRING as it is. A read fails for an enum value that has no name. + +.h3 wgtIfaceCallMethod + +.code +bool wgtIfaceCallMethod(const WgtIfaceT *iface, WidgetT *w, const WgtMethodDescT *m, const WgtValueT *args, int32_t argc, WgtValueT *ret); +.endcode + +Call method m with argc arguments, converted to the signature's types (wgtSigInfo); extra arguments are ignored. ret (may be NULL) receives the result, type WGT_IFACE_NONE for a void method. Returns false without calling when fewer arguments than the signature needs were given (ret is then the zero value of the result type), and when a dynamic method failed. + +.h2 Custom Events + +.h3 wgtFireEvent + +.code +bool wgtFireEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); +.endcode + +Fire event name on w. The universal events go to their WidgetT callback with their arguments taken from args (a missing one is 0): Click -> onClick, DblClick -> onDblClick, Change -> onChange, GotFocus -> onFocus, LostFocus -> onBlur, KeyPress (KeyAscii) -> onKeyPress, KeyDown / KeyUp (KeyCode, Shift) -> onKeyDown / onKeyUp, MouseDown / MouseUp / MouseMove (Button, X, Y, or the VB form Button, Shift, X, Y) -> onMouseDown / onMouseUp / onMouseMove (which take no Shift), Scroll (Delta) -> onScroll (names case-insensitive). Every other name goes to w->onEvent; the universal names never do, so a host that sets both sees each event once. Validate is not mapped (onValidate answers a question; fire it directly). onEvent may write back into args (ByRef parameters; the form runtime writes every ByRef parameter back, a String as text that stays valid until the next event). Returns false when a handler destroyed widgets -- do not touch w then. + +ListView (ColumnClick), TextArea (GutterClick) and HelpView (LinkClick) fire their own events this way after their C callbacks (wgtListViewSetHeaderClickCallback, wgtTextAreaSetGutterClick, wgtHelpViewSetLinkCallback), which stay for C programs; the event is skipped when the C callback destroyed widgets. A TextArea's gutter takes clicks when either is set. + .h2 Property Data Type Constants .table @@ -4654,8 +5093,10 @@ Top-level interface descriptor registered by each widget DXE. WGT_IFACE_STRING 0 const char * WGT_IFACE_INT 1 int32_t WGT_IFACE_BOOL 2 bool - WGT_IFACE_FLOAT 3 float + WGT_IFACE_FLOAT 3 float (WgtValueT: double) WGT_IFACE_ENUM 4 int32_t with named values + WGT_IFACE_NONE 5 WgtValueT only: no value + WGT_IFACE_DOUBLE 6 double (BASIC Double) .endtable .h2 Create Function Signature Constants @@ -5073,8 +5514,11 @@ Reading starts from the end: seek to EOF - sizeof(footer), verify the magic, the DVX_RES_BINARY 3 Arbitrary binary data (app-specific) DVX_RES_MAGIC 0x52585644 Footer signature ("DVXR" little-endian) DVX_RES_NAME_MAX 32 Max resource name length including null + DVX_RES_ONLY_MAGIC "DVXRESONLY" First bytes (NUL included, DVX_RES_ONLY_MAGIC_LEN) of a file that holds only resources .endtable +A file that starts with DVX_RES_ONLY_MAGIC has no code before its resources -- a control written in DVX BASIC, installed as a .wgt (see Using BASIC Controls from C). The loader reads such a widget file through the BASIC run-time library instead of loading it as a module. + .h2 Structures .h3 DvxResDirEntryT @@ -5250,11 +5694,22 @@ Returns: 0 on success, -1 if not found, the name is too long, or I/O failed. .index prefsAccelChoices .index prefsWallpaperChoices .index prefsWheelDirChoices +.index prefs.bas +.index dvxPrefsBas.c +.index prefsBasHandle .h1 dvxPrefs.h -- INI Preferences System Handle-based API over classic INI files. Each prefsLoad/prefsCreate returns a PrefsHandleT that must be passed to all subsequent calls and freed with prefsClose when done. Multiple INI files can be open simultaneously. +BASIC programs reach the same functions through prefs.bas (dvxPrefsBas.c in libdvx): PrefsLoad, PrefsCreate, PrefsGet$, PrefsGetInt, PrefsGetBool, PrefsSet, PrefsSetInt, PrefsSetBool, PrefsRemove, PrefsKeyName$ (prefsKeyName, "" past the last key), PrefsSave, PrefsSaveAs and PrefsClose, on handles (1, 2, ...; 0 = none) that belong to the program that made them and are closed, unsaved, when it ends. True is -1, False 0. PrefsDvxIni$ is DVX_INI_PATH, PrefsSetVideoMode is prefsSetVideoMode, and PrefsVideoWidth / PrefsVideoHeight / PrefsVideoBpp are the three values of prefsGetVideoMode (DVX_DEFAULT_VIDEO_* for a missing key or a bad handle). The choice tables below are PREFS_CHOICE_ACCEL (0), PREFS_CHOICE_WALLPAPER (1) and PREFS_CHOICE_WHEEL (2) there: PrefsChoiceCount, PrefsChoiceLabel$, PrefsChoiceName$ (iniName), PrefsChoiceValue, PrefsChoiceIndex, PrefsChoiceIndexOfValue, PrefsChoiceDefault (the index of the choice MOUSE_ACCEL_DEFAULT / WALLPAPER_MODE_DEFAULT / MOUSE_WHEEL_DIR_DEFAULT names; -1 for an unknown table), and PrefsGetChoice(handle, section, key, table) with the table's default. PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) is prefsGetChoice on a table the program gives as "|"-separated names (each choice's iniName and label is its name, its value its index). + +.code +PrefsHandleT *prefsBasHandle(int32_t handle); +.endcode + +The settings behind a prefs.bas handle, NULL for 0 or a handle that is not open. Other libraries' BASIC entry points that take a prefs.bas handle (system.bas, joystick.bas, net.bas, print.bas, sound.bas) resolve it with this; libdvx is loaded first, so the symbol is always there. + .h2 Constants .table @@ -5497,7 +5952,7 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT]; The entry counts are PREFS_ACCEL_CHOICE_COUNT (4), PREFS_WALLPAPER_CHOICE_COUNT (3) and PREFS_WHEEL_DIR_CHOICE_COUNT (2). MOUSE_WHEEL_DIR_NORMAL (1) and MOUSE_WHEEL_DIR_REVERSED (-1) in dvxTypes.h are also the wheelDir values for dvxSetMouseConfig. -Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. The shell reads [mouse] wheel, [mouse] acceleration and [desktop] mode through prefsGetChoice at startup, and the Control Panel builds its dropdowns and writes the INI from the same tables. +Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. dvxMouseFromPrefs and dvxWallpaperFromPrefs (dvxApp.h) read [mouse] wheel, [mouse] acceleration and [desktop] mode through prefsGetChoice, and the Control Panel builds its dropdowns from the same tables. .h3 prefsChoiceIndex @@ -6185,9 +6640,10 @@ typedef uint32_t PlatformTicksT; PlatformTicksT platformClock(void); PlatformTicksT platformClockHz(void); #define PLATFORM_MS_TO_TICKS(ms) +#define PLATFORM_TICKS_TO_MS(ticks) .endcode -Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, timers), advancing platformClockHz() ticks per second. PlatformTicksT is unsigned so "now - then" is wrap-safe: always compare intervals by subtraction, never raw tick values. PLATFORM_MS_TO_TICKS converts milliseconds to ticks at the platform rate. +Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, timers), advancing platformClockHz() ticks per second. PlatformTicksT is unsigned so "now - then" is wrap-safe: always compare intervals by subtraction, never raw tick values. PLATFORM_MS_TO_TICKS converts milliseconds to ticks at the platform rate, PLATFORM_TICKS_TO_MS ticks to milliseconds (the millisecond clock the conn and xfer engines take is PLATFORM_TICKS_TO_MS(platformClock())). .h3 platformEntropyStamp diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c index a8f0902a..6c1be679 100644 --- a/src/libs/kpunch/libdvx/dvxApp.c +++ b/src/libs/kpunch/libdvx/dvxApp.c @@ -62,6 +62,8 @@ #include #include +#include +#include #include #include #include @@ -94,6 +96,17 @@ DVX_ABI_STAMP(libdvx); #define ACCEL_INITIAL_CAP 8 #define POPUP_STACK_INITIAL_CAP 4 +// Key text (dvxAccelParse / dvxAccelFormat): "Ctrl+Alt+Shift+Key" +#define ACCEL_TEXT_SEP '+' +#define ACCEL_TEXT_CTRL "Ctrl" +#define ACCEL_TEXT_ALT "Alt" +#define ACCEL_TEXT_SHIFT "Shift" +#define ACCEL_TEXT_MAX 32 // longest key text taken apart +#define ACCEL_FKEY_PREFIX 'F' +#define ACCEL_FKEY_FMT "F%ld" +#define ACCEL_FKEY_COUNT 12 +#define ACCEL_FKEY_BASE 10 + // Cursor dirty region: covers all cursor shapes (16x16 with hotspot at 0,0 or 7,7) #define CURSOR_HOTSPOT_X 7 #define CURSOR_HOTSPOT_Y 7 @@ -143,6 +156,18 @@ DVX_ABI_STAMP(libdvx); #define THEME_SECTION "colors" #define THEME_VALUE_MAX 16 // "255,255,255" plus terminator +// DVX.INI [mouse] and [desktop] keys (dvxMouseFromPrefs / ToPrefs, +// dvxWallpaperFromPrefs / ToPrefs) +#define MOUSE_SECTION "mouse" +#define MOUSE_KEY_WHEEL "wheel" +#define MOUSE_KEY_DBLCLICK "doubleclick" +#define MOUSE_KEY_ACCEL "acceleration" +#define MOUSE_KEY_SPEED "speed" +#define MOUSE_KEY_WHEEL_STEP "wheelspeed" +#define DESKTOP_SECTION "desktop" +#define DESKTOP_KEY_WALLPAPER "wallpaper" +#define DESKTOP_KEY_MODE "mode" + // djb2 text hash parameters #define DJB2_SEED 5381 #define DJB2_SHIFT 5 @@ -229,6 +254,7 @@ static void buildChannelLut(ChannelLutT *lut, const PixelFormatT *fmt); static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode, int32_t *outPitch); static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph, bool *hasCheck); static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers); +static void choiceToPrefs(PrefsHandleT *h, const char *section, const char *key, const PrefsChoiceT *choices, int32_t count, int32_t value); static void clampRectToScreen(const AppContextT *ctx, int32_t *x, int32_t *y, int32_t w, int32_t h); static void clearMenuBarActive(AppContextT *ctx); static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx); @@ -311,6 +337,7 @@ static void popupClickItem(AppContextT *ctx, int32_t idx); static void popupHandleOutsideCurrent(AppContextT *ctx, int32_t mx, int32_t my, bool leftPress); static void raiseAndFocus(AppContextT *ctx, int32_t idx); static void refreshMinimizedIcons(AppContextT *ctx); +static uint8_t *rejectEmptyImage(uint8_t *pixels, const int *imgW, const int *imgH); static void releaseKeyPressedButton(AppContextT *ctx); static int32_t remapIndexForLower(int32_t index, int32_t lowered); static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); @@ -362,6 +389,37 @@ typedef struct { int32_t normMods; } HotkeyT; +// Key text: the function keys in order, and the other keys that have a +// name. A key listed twice is written with its first name. +typedef struct { + const char *name; + int32_t key; +} AccelKeyNameT; + +static const int32_t sAccelFKeys[ACCEL_FKEY_COUNT] = { + KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12 +}; + +static const AccelKeyNameT sAccelKeyNames[] = { + { "Backspace", KEY_BACKSPACE }, + { "Del", KEY_DELETE }, + { "Delete", KEY_DELETE }, + { "Down", KEY_DOWN }, + { "End", KEY_END }, + { "Enter", KEY_ENTER }, + { "Esc", KEY_ESCAPE }, + { "Home", KEY_HOME }, + { "Ins", KEY_INSERT }, + { "Insert", KEY_INSERT }, + { "Left", KEY_LEFT }, + { "PgDn", KEY_PGDN }, + { "PgUp", KEY_PGUP }, + { "Right", KEY_RIGHT }, + { "Space", KEY_SPACE }, + { "Tab", KEY_TAB }, + { "Up", KEY_UP } +}; + static HotkeyT *sHotkeys = NULL; // stb_ds dynamic array static int32_t sLastHotkeyId = DVX_HOTKEY_NONE; @@ -478,16 +536,19 @@ _Static_assert(offsetof(ColorSchemeT, cursorFg) == (size_t)ColorCursorFgE _Static_assert(offsetof(ColorSchemeT, cursorBg) == (size_t)ColorCursorBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: cursorBg"); -// The accelerator key for a keystroke with Ctrl held. The ASCII byte -// alone is ambiguous: Ctrl+Enter and Ctrl+J are both 0x0A, Ctrl+M and +// The accelerator key for a keystroke with Ctrl or Alt held. The ASCII +// byte alone is ambiguous: Ctrl+Enter and Ctrl+J are both 0x0A, Ctrl+M and // Enter are both 0x0D, and Ctrl+Tab has no ASCII byte at all, so the -// physical scancode decides between the named key and the letter. +// physical scancode decides between the named key and the letter. With +// Alt the BIOS gives Enter (either one) and Backspace no ASCII byte, so +// their scancodes become KEY_ENTER / KEY_BACKSPACE too: an accelerator +// registered as (KEY_ENTER, ACCEL_ALT) matches Alt with either Enter. static int32_t accelKeyFromKeystroke(int32_t scancode, int32_t ascii, int32_t key, int32_t mods) { - if (!(mods & KEY_MOD_CTRL)) { + if (!(mods & (KEY_MOD_CTRL | KEY_MOD_ALT))) { return key; } - if (scancode == KEY_SCAN_ENTER || scancode == KEY_SCAN_KP_ENTER) { + if (scancode == KEY_SCAN_ENTER || scancode == KEY_SCAN_KP_ENTER || scancode == KEY_SCAN_ALT_KP_ENTER) { return KEY_ENTER; } @@ -947,6 +1008,17 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t } +// Stores the iniName of the choice whose value is value; a value no +// choice has leaves the key as it is. +static void choiceToPrefs(PrefsHandleT *h, const char *section, const char *key, const PrefsChoiceT *choices, int32_t count, int32_t value) { + int32_t idx = prefsChoiceIndexOfValue(choices, count, value); + + if (idx >= 0) { + prefsSetString(h, section, key, choices[idx].iniName); + } +} + + // Keeps a popup rect on-screen. Shared by the context-menu, menu-bar // dropdown, submenu and system-menu open paths so off-screen items (and // thus unreachable clicks, since the mouse can't leave the screen) can't @@ -2009,7 +2081,7 @@ static void dispatchEvents(AppContextT *ctx) { // form), leaving the earlier pointer dangling. focus = wgtGetFocused(); - if (focus && focus->onScroll) { + if (focus && focus->onScroll && !wclsRaisesEvents(focus)) { WIN_CALLBACK(ctx, wheelWin, focus->onScroll(focus, delta)); } } @@ -2439,6 +2511,114 @@ static void drawSysMenu(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, cons } +void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize) { + int32_t pos = 0; + + if (!out || outSize <= 0) { + return; + } + + out[0] = '\0'; + + if (key == 0) { + return; + } + + if (modifiers & ACCEL_CTRL) { + pos = dvxStrAppendf(out, outSize, pos, ACCEL_TEXT_CTRL "%c", ACCEL_TEXT_SEP); + } + + if (modifiers & ACCEL_ALT) { + pos = dvxStrAppendf(out, outSize, pos, ACCEL_TEXT_ALT "%c", ACCEL_TEXT_SEP); + } + + if (modifiers & ACCEL_SHIFT) { + pos = dvxStrAppendf(out, outSize, pos, ACCEL_TEXT_SHIFT "%c", ACCEL_TEXT_SEP); + } + + for (int32_t i = 0; i < ACCEL_FKEY_COUNT; i++) { + if (sAccelFKeys[i] == key) { + dvxStrAppendf(out, outSize, pos, ACCEL_FKEY_FMT, (long)i + 1); + return; + } + } + + for (int32_t i = 0; i < DVX_ARRAY_LEN(sAccelKeyNames); i++) { + if (sAccelKeyNames[i].key == key) { + dvxStrAppendf(out, outSize, pos, "%s", sAccelKeyNames[i].name); + return; + } + } + + dvxStrAppendf(out, outSize, pos, "%c", toupper((unsigned char)key)); +} + + +// Modifier names, each followed by the separator, then one key: a letter +// or digit, F1 to F12, or a name from sAccelKeyNames (case does not +// matter, blanks around the parts are ignored). +bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods) { + char buf[ACCEL_TEXT_MAX]; + int32_t mods = 0; + int32_t key = 0; + + if (!text || !outKey || !outMods) { + return false; + } + + *outKey = 0; + *outMods = 0; + dvxCopyTrimmed(buf, sizeof(buf), text); + + char *part = buf; + char *sep = strchr(part, ACCEL_TEXT_SEP); + + while (sep) { + *sep = '\0'; + dvxTrimRight(part); + + if (strcasecmp(part, ACCEL_TEXT_CTRL) == 0) { + mods |= ACCEL_CTRL; + } else if (strcasecmp(part, ACCEL_TEXT_ALT) == 0) { + mods |= ACCEL_ALT; + } else if (strcasecmp(part, ACCEL_TEXT_SHIFT) == 0) { + mods |= ACCEL_SHIFT; + } else { + return false; + } + + part = (char *)dvxSkipWs(sep + 1); + sep = strchr(part, ACCEL_TEXT_SEP); + } + + if (part[0] && !part[1] && isalnum((unsigned char)part[0])) { + key = toupper((unsigned char)part[0]); + } else if (toupper((unsigned char)part[0]) == ACCEL_FKEY_PREFIX && isdigit((unsigned char)part[1])) { + char *end = NULL; + long num = strtol(part + 1, &end, ACCEL_FKEY_BASE); + + if (*end == '\0' && num >= 1 && num <= ACCEL_FKEY_COUNT) { + key = sAccelFKeys[num - 1]; + } + } else { + for (int32_t i = 0; i < DVX_ARRAY_LEN(sAccelKeyNames); i++) { + if (strcasecmp(part, sAccelKeyNames[i].name) == 0) { + key = sAccelKeyNames[i].key; + break; + } + } + } + + if (key == 0) { + return false; + } + + *outKey = key; + *outMods = mods; + return true; +} + + // ============================================================ // dvxAddAccel // ============================================================ @@ -2755,6 +2935,58 @@ const char *dvxColorName(ColorIdE id) { } +// ============================================================ +// dvxColorsFromPrefs +// ============================================================ +// +// Reads the [colors] section ("name = r,g,b"); unknown or malformed keys +// are ignored and channel values are clamped to 0..255. Repaints only +// when a color changed. + +bool dvxColorsFromPrefs(AppContextT *ctx, PrefsHandleT *h) { + bool found = false; + bool changed = false; + + for (int32_t i = 0; i < ColorCountE; i++) { + const char *val = prefsGetString(h, THEME_SECTION, sColorNames[i], NULL); + int32_t r; + int32_t g; + int32_t b; + + if (val && sscanf(val, "%" SCNd32 ",%" SCNd32 ",%" SCNd32, &r, &g, &b) == RGB_CHANNELS) { + uint8_t rgb[RGB_CHANNELS] = { (uint8_t)clampInt(r, 0, UINT8_MAX), (uint8_t)clampInt(g, 0, UINT8_MAX), (uint8_t)clampInt(b, 0, UINT8_MAX) }; + + found = true; + + if (memcmp(ctx->colorRgb[i], rgb, sizeof(rgb)) != 0) { + memcpy(ctx->colorRgb[i], rgb, sizeof(rgb)); + changed = true; + } + } + } + + if (changed) { + dvxApplyColorScheme(ctx); + } + + return found; +} + + +// ============================================================ +// dvxColorsToPrefs +// ============================================================ + +void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h) { + for (int32_t i = 0; i < ColorCountE; i++) { + char val[THEME_VALUE_MAX]; + + snprintf(val, sizeof(val), "%d,%d,%d", ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); + prefsSetString(h, THEME_SECTION, sColorNames[i], val); + } +} + + // ============================================================ // dvxContentDisplay // ============================================================ @@ -3303,6 +3535,19 @@ bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) { return false; } + // stb_image gives a top-down BMP's height as stored, negative, and + // loads that image at its magnitude. INT_MIN has none (the load + // refuses that height as well). + if (h < 0 && h != INT_MIN) { + h = -h; + } + + // An image without pixels does not load (rejectEmptyImage), so it has + // no size either. + if (w <= 0 || h <= 0) { + return false; + } + if (outW) { *outW = w; } @@ -3381,9 +3626,12 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ ctx->lastCloseClickTime = 0; ctx->lastTitleClickId = -1; ctx->lastTitleClickTime = 0; - ctx->wheelDirection = 1; + ctx->wheelDirection = MOUSE_WHEEL_DIR_NORMAL; ctx->wheelStep = MOUSE_WHEEL_STEP_DEFAULT; + ctx->dblClickMs = MOUSE_DBLCLICK_DEFAULT_MS; ctx->dblClickTicks = PLATFORM_MS_TO_TICKS(MOUSE_DBLCLICK_DEFAULT_MS); + ctx->mouseAccel = prefsAccelChoices[prefsChoiceIndex(prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT)].value; + ctx->mouseMickeys = MOUSE_SPEED_DEFAULT; sDblClickTicks = ctx->dblClickTicks; // Pre-compute fixed-point 16.16 reciprocal of character height so @@ -3499,9 +3747,19 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i int imgW; int imgH; int channels; - uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, RGB_CHANNELS); + uint8_t *rgb = rejectEmptyImage(stbi_load(path, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH); - return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch); + if (rgb) { + return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch); + } + + // A DVX program, widget or library file: its icon + uint32_t size = 0; + uint8_t *icon = (uint8_t *)dvxResLoadIconData(path, &size); + uint8_t *pixels = icon ? dvxLoadImageFromMemory(ctx, icon, (int32_t)size, outW, outH, outPitch) : NULL; + + free(icon); + return pixels; } @@ -3515,7 +3773,7 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t int imgW; int imgH; int channels; - uint8_t *rgba = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGBA_CHANNELS); + uint8_t *rgba = rejectEmptyImage(stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGBA_CHANNELS), &imgW, &imgH); if (!rgba) { return NULL; @@ -3559,7 +3817,7 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int int imgW; int imgH; int channels; - uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGB_CHANNELS); + uint8_t *rgb = rejectEmptyImage(stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH); return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch); } @@ -3569,8 +3827,7 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int // dvxLoadTheme // ============================================================ // -// Reads the [colors] section ("name = r,g,b") through dvxPrefs; unknown -// or malformed keys are ignored and channel values are clamped to 0..255. +// Reads the [colors] section through dvxColorsFromPrefs. bool dvxLoadTheme(AppContextT *ctx, const char *filename) { // prefsLoad treats a missing file as empty; a theme must exist. @@ -3584,21 +3841,8 @@ bool dvxLoadTheme(AppContextT *ctx, const char *filename) { return false; } - for (int32_t i = 0; i < ColorCountE; i++) { - const char *val = prefsGetString(h, THEME_SECTION, sColorNames[i], NULL); - int32_t r; - int32_t g; - int32_t b; - - if (val && sscanf(val, "%" SCNd32 ",%" SCNd32 ",%" SCNd32, &r, &g, &b) == RGB_CHANNELS) { - ctx->colorRgb[i][0] = (uint8_t)clampInt(r, 0, UINT8_MAX); - ctx->colorRgb[i][1] = (uint8_t)clampInt(g, 0, UINT8_MAX); - ctx->colorRgb[i][2] = (uint8_t)clampInt(b, 0, UINT8_MAX); - } - } - + dvxColorsFromPrefs(ctx, h); prefsClose(h); - dvxApplyColorScheme(ctx); return true; } @@ -3651,6 +3895,37 @@ void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) { } +// ============================================================ +// dvxMouseFromPrefs +// ============================================================ +// +// Unknown names fall back to the documented defaults (the shared +// vocabularies in dvxPrefs.h). + +void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h) { + int32_t wheel = prefsGetChoice(h, MOUSE_SECTION, MOUSE_KEY_WHEEL, prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, MOUSE_WHEEL_DIR_DEFAULT); + int32_t accel = prefsGetChoice(h, MOUSE_SECTION, MOUSE_KEY_ACCEL, prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT); + int32_t dbl = prefsGetInt(h, MOUSE_SECTION, MOUSE_KEY_DBLCLICK, MOUSE_DBLCLICK_DEFAULT_MS); + int32_t speed = prefsGetInt(h, MOUSE_SECTION, MOUSE_KEY_SPEED, MOUSE_SPEED_DEFAULT); + int32_t step = prefsGetInt(h, MOUSE_SECTION, MOUSE_KEY_WHEEL_STEP, MOUSE_WHEEL_STEP_DEFAULT); + + dvxSetMouseConfig(ctx, prefsWheelDirChoices[wheel].value, dbl, prefsAccelChoices[accel].value, speed, step); +} + + +// ============================================================ +// dvxMouseToPrefs +// ============================================================ + +void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h) { + choiceToPrefs(h, MOUSE_SECTION, MOUSE_KEY_WHEEL, prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, ctx->wheelDirection); + choiceToPrefs(h, MOUSE_SECTION, MOUSE_KEY_ACCEL, prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, ctx->mouseAccel); + prefsSetInt(h, MOUSE_SECTION, MOUSE_KEY_DBLCLICK, ctx->dblClickMs); + prefsSetInt(h, MOUSE_SECTION, MOUSE_KEY_SPEED, ctx->mouseMickeys); + prefsSetInt(h, MOUSE_SECTION, MOUSE_KEY_WHEEL_STEP, ctx->wheelStep); +} + + void dvxPlaySound(DvxSoundE event) { if (sSoundHook) { sSoundHook(event, false); @@ -3725,6 +4000,23 @@ int32_t dvxRegisterHotkey(AppContextT *ctx, int32_t key, int32_t modifiers, DvxH // before onClose opens a modal dialog over it. Shared by Alt+F4, the // system menu Close command, the close-gadget double-click and the // shell's Exit DVX sequence. +void dvxRemoveAccel(AccelTableT *table, int32_t cmdId) { + if (!table) { + return; + } + + int32_t kept = 0; + + for (int32_t i = 0; i < table->count; i++) { + if (table->entries[i].cmdId != cmdId) { + table->entries[kept++] = table->entries[i]; + } + } + + table->count = kept; +} + + void dvxRequestWindowClose(AppContextT *ctx, WindowT *win) { compositeAndFlush(ctx); @@ -3821,19 +4113,8 @@ void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resName, int32_t *outW, int32_t *outH, int32_t *outPitch) { - if (!ctx || !dxePath || !resName) { - return NULL; - } - - DvxResHandleT *res = dvxResOpen(dxePath); - - if (!res) { - return NULL; - } - uint32_t size = 0; - void *buf = dvxResRead(res, resName, &size); - dvxResClose(res); + void *buf = ctx ? dvxResLoadData(dxePath, resName, &size) : NULL; if (!buf) { return NULL; @@ -3845,6 +4126,24 @@ uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resNa } +void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize) { + DvxResHandleT *res = dxePath ? dvxResOpen(dxePath) : NULL; + + if (!res) { + return NULL; + } + + void *data = dvxResRead(res, DVX_RES_NAME_ICON, outSize); + + if (!data) { + data = dvxResRead(res, DVX_RES_NAME_WIDGET_ICON, outSize); + } + + dvxResClose(res); + return data; +} + + bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize) { if (!dxePath || !resName || !buf || bufSize <= 0) { return false; @@ -3935,12 +4234,7 @@ bool dvxSaveTheme(const AppContextT *ctx, const char *filename) { return false; } - for (int32_t i = 0; i < ColorCountE; i++) { - char val[THEME_VALUE_MAX]; - - snprintf(val, sizeof(val), "%d,%d,%d", ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); - prefsSetString(h, THEME_SECTION, sColorNames[i], val); - } + dvxColorsToPrefs(ctx, h); bool ok = prefsSaveAs(h, filename); @@ -4049,7 +4343,8 @@ void dvxSetInputEntropyHook(DvxEntropyHookT fn) { // ============================================================ void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep) { - ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; + ctx->wheelDirection = (wheelDir < 0) ? MOUSE_WHEEL_DIR_REVERSED : MOUSE_WHEEL_DIR_NORMAL; + ctx->dblClickMs = dblClickMs; ctx->dblClickTicks = PLATFORM_MS_TO_TICKS(dblClickMs); sDblClickTicks = ctx->dblClickTicks; ctx->wheelStep = clampInt(wheelStep, MOUSE_WHEEL_STEP_MIN, MOUSE_WHEEL_STEP_MAX); @@ -4057,10 +4352,12 @@ void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, i // A non-positive value means "leave the driver setting unchanged". if (accelThreshold > 0) { platformMouseSetAccel(accelThreshold); + ctx->mouseAccel = accelThreshold; } if (mickeyRatio > 0) { platformMouseSetMickeys(mickeyRatio, mickeyRatio); + ctx->mouseMickeys = mickeyRatio; } } @@ -4131,7 +4428,7 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) { int imgW; int imgH; int channels; - uint8_t *rgb = stbi_load(newPath, &imgW, &imgH, &channels, RGB_CHANNELS); + uint8_t *rgb = rejectEmptyImage(stbi_load(newPath, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH); uint8_t *buf = NULL; int32_t pitch = 0; @@ -4332,7 +4629,10 @@ void dvxTileWindows(AppContextT *ctx) { int32_t tileH = DVX_MAX(screenH / rows, MIN_WINDOW_H); int32_t slot = 0; - for (int32_t i = 0; i < ctx->stack.count; i++) { + // Only the count windows planned for: a resize callback may open a + // window while this runs, and a slot past count is in an empty row + // (rowCols 0). + for (int32_t i = 0; i < ctx->stack.count && slot < count; i++) { WindowT *win = ctx->stack.windows[i]; if (win->minimized || !win->visible) { @@ -4534,6 +4834,45 @@ bool dvxUpdate(AppContextT *ctx) { } +// ============================================================ +// dvxWallpaperFromPrefs +// ============================================================ +// +// A missing wallpaper key means none. The image is only built again +// when the path or the mode differs from the current one. + +bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h) { + WallpaperModeE mode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(h, DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value; + const char *path = prefsGetString(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, ""); + + if (strcmp(path, ctx->wallpaperPath) != 0) { + ctx->wallpaperMode = mode; + return dvxSetWallpaper(ctx, path[0] ? path : NULL); + } + + if (mode != ctx->wallpaperMode) { + dvxSetWallpaperMode(ctx, mode); + } + + return true; +} + + +// ============================================================ +// dvxWallpaperToPrefs +// ============================================================ + +void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h) { + if (ctx->wallpaperPath[0]) { + prefsSetString(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, ctx->wallpaperPath); + } else { + prefsRemove(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER); + } + + prefsSetString(h, DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices[ctx->wallpaperMode].iniName); +} + + // ============================================================ // dvxWindowScreenshot // ============================================================ @@ -6486,6 +6825,26 @@ static void refreshMinimizedIcons(AppContextT *ctx) { } +// ============================================================ +// rejectEmptyImage -- an stb_image decode with no pixels is a failure +// ============================================================ +// +// stb_image accepts some images without pixels (a BMP may declare a +// height of 0); every DVX consumer expects at least one pixel (the +// wallpaper scaler reads row h - 1, callers divide by the size). Takes +// the decoded buffer (NULL when decoding failed) and the size stb_image +// stored, frees an empty image and returns NULL for it. + +static uint8_t *rejectEmptyImage(uint8_t *pixels, const int *imgW, const int *imgH) { + if (pixels && (*imgW <= 0 || *imgH <= 0)) { + stbi_image_free(pixels); + return NULL; + } + + return pixels; +} + + // Releases the button pressed via keyboard one frame earlier (see // sKeyPressedBtn) and fires its click. The one-frame delay lets the // pressed look render before the callback runs (which may open a dialog). diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h index f3521eac..47bf122f 100644 --- a/src/libs/kpunch/libdvx/dvxApp.h +++ b/src/libs/kpunch/libdvx/dvxApp.h @@ -46,6 +46,7 @@ #include "dvxPal.h" #include "dvxComp.h" #include "dvxWm.h" +#include "dvxPrefs.h" // ============================================================ @@ -172,10 +173,13 @@ typedef struct AppContextT { // multiply+shift instead of an integer divide, which is very slow on // a 486 (40+ cycles per DIV). uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight - // Mouse configuration (loaded from preferences) + // Mouse configuration (dvxSetMouseConfig, loaded from preferences) int32_t wheelDirection; // 1 = normal, -1 = reversed int32_t wheelStep; // lines per wheel notch (1-10, default 3) PlatformTicksT dblClickTicks; // double-click speed in platformClock() ticks + int32_t dblClickMs; // double-click speed in milliseconds + int32_t mouseAccel; // acceleration threshold applied (MOUSE_ACCEL_*) + int32_t mouseMickeys; // mickeys per 8 pixels applied // Color scheme source RGB values (unpacked, for theme save/get) uint8_t colorRgb[ColorCountE][3]; // Available video modes (enumerated once at init) @@ -208,6 +212,15 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request // wheelStep: lines per wheel notch (1-10, default 3). void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep); +// Apply the DVX.INI [mouse] settings in h (wheel, doubleclick, +// acceleration, speed, wheelspeed; defaults for missing keys) through +// dvxSetMouseConfig. +void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h); + +// Store the mouse configuration in effect as [mouse] keys of h (the +// keys dvxMouseFromPrefs reads). +void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h); + // Input-timing entropy sink. When set, every keyboard event and every // mouse change (move, button, wheel) is timestamped with a fine-grained // platform counter and passed, in batches of a few events, to fn (e.g. a @@ -341,6 +354,15 @@ bool dvxLoadTheme(AppContextT *ctx, const char *filename); // was fully written. bool dvxSaveTheme(const AppContextT *ctx, const char *filename); +// Read the [colors] section of h ("name = r,g,b" per color; unknown or +// malformed keys are ignored, channels clamped to 0..255) and repaint +// when a color changed. Returns true when any color key was read. +bool dvxColorsFromPrefs(AppContextT *ctx, PrefsHandleT *h); + +// Store every color as a [colors] key of h (what dvxColorsFromPrefs +// reads; dvxSaveTheme writes the same). +void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h); + // Return the INI key name for a color ID (e.g. "desktop", "windowFace"). const char *dvxColorName(ColorIdE id); @@ -355,10 +377,20 @@ const char *dvxColorLabel(ColorIdE id); // (stretch/tile/center). Pass NULL to clear the wallpaper. bool dvxSetWallpaper(AppContextT *ctx, const char *path); -// Change the wallpaper display mode and re-render the buffer. -// Has no effect if no wallpaper is loaded. +// Change the wallpaper display mode; a loaded wallpaper is rendered +// again. The mode is kept for the next wallpaper when none is loaded. void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode); +// Apply the DVX.INI [desktop] settings in h: mode (WALLPAPER_MODE_DEFAULT +// when missing) and wallpaper (none when missing). The image is only +// rebuilt when the path or mode differs from the current one. Returns +// false when the named image could not be loaded. +bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h); + +// Store the wallpaper path (removed when none) and mode as [desktop] +// keys of h. +void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h); + // Return the list of available video modes (enumerated at init). // count receives the number of entries. const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count); @@ -506,6 +538,17 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path); // so the image shows the current state. Returns 0 on success, -1 on failure. int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path); +// Key text such as "F2", "Ctrl+X" or "Alt+Enter" (the text menus show +// after a tab). dvxAccelFormat writes key + modifiers (as for +// dvxAddAccel) that way: modifiers Ctrl, Alt, Shift in that order, then +// the key -- a letter or digit, F1 to F12, or a named key (Backspace, Del, +// Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, +// Up); "" for key 0. dvxAccelParse reads such text back (case does not +// matter; Delete and Insert are also accepted); false, with *outKey 0, +// for anything else. +void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize); +bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods); + // Allocate a new accelerator table. Attach it to a window via // win->accelTable. The event loop checks the focused window's accel // table on every keystroke and fires onMenu(cmdId) on match. @@ -523,6 +566,10 @@ void dvxFreeAccelTable(AccelTableT *table); // menus and hotkeys. void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId); +// Remove every shortcut of command cmdId from the table (a menu item that +// lost its key or went away). +void dvxRemoveAccel(AccelTableT *table, int32_t cmdId); + // System-wide hotkeys. fn(userCtx) runs when key+modifiers is pressed, // whichever window has the focus (or none), before that window's menu // keys and accelerators; not while a menu or a modal dialog is open, and @@ -575,9 +622,10 @@ void dvxTileWindowsH(AppContextT *ctx); void dvxTileWindowsV(AppContextT *ctx); // Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's -// native pixel format. Returns the pixel buffer on success (caller must -// free with dvxFreeImage), or NULL on failure. Output params receive the -// image dimensions and row pitch in bytes. +// native pixel format; a DVX program, widget or library file gives its +// icon (dvxResLoadIconData). Returns the pixel buffer on success (caller +// must free with dvxFreeImage), or NULL on failure. Output params receive +// the image dimensions and row pitch in bytes. uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch); // Load an image from a memory buffer (e.g. resource data). Same output @@ -657,6 +705,12 @@ void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, in // or NULL if not found. Sets outW/outH/outPitch on success. uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resName, int32_t *outW, int32_t *outH, int32_t *outPitch); +// The icon of a DVX program, widget or library file as the bytes of its +// image file (its DVX_RES_NAME_ICON resource, else DVX_RES_NAME_WIDGET_ICON), +// for dvxLoadImageFromMemory / dvxLoadImageAlpha. NULL when the file has +// neither (or is no DVX file). Free with free(); *outSize gets the size. +void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize); + // Load a text resource into a caller-provided buffer. // Returns true on success. The text is null-terminated and truncated // to fit bufSize. Returns false if the resource is not found. diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c index c4b6f2ba..bbf2ccc0 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.c +++ b/src/libs/kpunch/libdvx/dvxDraw.c @@ -97,12 +97,20 @@ typedef enum { AccelTokenEndE, AccelTokenLiteralE, AccelTokenAccelE, AccelTokenN // cell, which the unrolled glyph-row helpers below write in one go. #define GLYPH_COLS 8 +// What drawFocusRect plots its dots with +typedef struct { + DisplayT *d; + int32_t bpp; + uint32_t color; +} FocusDotCtxT; + // ============================================================ // Prototypes // ============================================================ static AccelTokenE accelNextToken(const char **text, char *outCh); static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h); +static void focusDot(void *ctx, int32_t x, int32_t y); static inline void glyphRowFg16(uint16_t *dst, uint8_t bits, uint16_t fg); static inline void glyphRowFg32(uint32_t *dst, uint8_t bits, uint32_t fg); static inline void glyphRowFg8(uint8_t *dst, uint8_t bits, uint8_t fg); @@ -585,54 +593,51 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // once per focused widget per repaint. void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { - int32_t bpp = ops->bytesPerPixel; - int32_t pitch = d->pitch; + FocusDotCtxT dc; - int32_t clipX1 = d->clipX; - int32_t clipX2 = d->clipX + d->clipW; - int32_t clipY1 = d->clipY; - int32_t clipY2 = d->clipY + d->clipH; + dc.d = d; + dc.bpp = ops->bytesPerPixel; + dc.color = color; + drawFocusRectDots(x, y, w, h, focusDot, &dc); +} + +// ============================================================ +// drawFocusRectDots +// ============================================================ +// +// The one focus-rectangle pattern: drawFocusRect plots it in the back +// buffer, the BASIC DrawFocusRect method on a drawing surface (canvas). + +void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn, void *ctx) { int32_t x2 = x + w - 1; int32_t y2 = y + h - 1; // Top edge - if (y >= clipY1 && y < clipY2) { - for (int32_t px = x; px <= x2; px += 2) { - if (px >= clipX1 && px < clipX2) { - putPixel(d->backBuf + y * pitch + px * bpp, color, bpp); - } - } + for (int32_t px = x; px <= x2; px += 2) { + fn(ctx, px, y); } // Bottom edge - if (y2 >= clipY1 && y2 < clipY2 && y2 != y) { + if (y2 != y) { int32_t parity = (y2 - y) & 1; for (int32_t px = x + parity; px <= x2; px += 2) { - if (px >= clipX1 && px < clipX2) { - putPixel(d->backBuf + y2 * pitch + px * bpp, color, bpp); - } + fn(ctx, px, y2); } } // Left edge (skip corners already drawn) - if (x >= clipX1 && x < clipX2) { - for (int32_t py = y + 2; py < y2; py += 2) { - if (py >= clipY1 && py < clipY2) { - putPixel(d->backBuf + py * pitch + x * bpp, color, bpp); - } - } + for (int32_t py = y + 2; py < y2; py += 2) { + fn(ctx, x, py); } // Right edge (skip corners already drawn) - if (x2 >= clipX1 && x2 < clipX2 && x2 != x) { + if (x2 != x) { int32_t parity = (x2 - x) & 1; for (int32_t py = y + 2 - parity; py < y2; py += 2) { - if (py >= clipY1 && py < clipY2) { - putPixel(d->backBuf + py * pitch + x2 * bpp, color, bpp); - } + fn(ctx, x2, py); } } } @@ -846,34 +851,20 @@ void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors); if (orient == ScrollbarVerticalE) { - // Sunken trough + // Sunken trough, up and down arrow buttons drawBevel(d, ops, x, y, barW, length, &troughBevel); - - // Up arrow button - drawBevel(d, ops, x, y, barW, barW, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, y, barW, ScrollArrowUpE); - - // Down arrow button - int32_t downY = y + length - barW; - drawBevel(d, ops, x, downY, barW, barW, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, downY, barW, ScrollArrowDownE); + drawScrollbarArrowButton(d, ops, colors, x, y, barW, ScrollArrowUpE); + drawScrollbarArrowButton(d, ops, colors, x, y + length - barW, barW, ScrollArrowDownE); // Thumb if (thumbSize > 0) { drawBevel(d, ops, x, y + barW + thumbPos, barW, thumbSize, &btnBevel); } } else { - // Sunken trough + // Sunken trough, left and right arrow buttons drawBevel(d, ops, x, y, length, barW, &troughBevel); - - // Left arrow button - drawBevel(d, ops, x, y, barW, barW, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, y, barW, ScrollArrowLeftE); - - // Right arrow button - int32_t rightX = x + length - barW; - drawBevel(d, ops, rightX, y, barW, barW, &btnBevel); - drawScrollbarArrow(d, ops, colors, rightX, y, barW, ScrollArrowRightE); + drawScrollbarArrowButton(d, ops, colors, x, y, barW, ScrollArrowLeftE); + drawScrollbarArrowButton(d, ops, colors, x + length - barW, y, barW, ScrollArrowRightE); // Thumb if (thumbSize > 0) { @@ -892,6 +883,18 @@ void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *co } +// One arrow button of a scrollbar, as drawScrollbar paints at each end: +// the raised 1px button bevel on the scrollbar face, then the arrow glyph +// centred on it. + +void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir) { + BevelStyleT bevel = BEVEL_SB_BUTTON(colors); + + drawBevel(d, ops, x, y, size, size, &bevel); + drawScrollbarArrow(d, ops, colors, x, y, size, dir); +} + + // ============================================================ // drawTermRow // ============================================================ @@ -1001,7 +1004,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 cEnd = clipX2 - cx; } - int32_t idx = (uint8_t)gch - font->firstChar; + int32_t idx = gch - font->firstChar; // A character outside the font renders as a blank cell: one bg // span per scanline, decided once per cell rather than per row. @@ -1405,6 +1408,18 @@ void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo // only the set pixels (transparent text); the Opaque variants write every // pixel as fg or bg. Shared by drawChar, drawTextN and drawTermRow. +// One dot of a focus rectangle in the back buffer, inside the clip rect. +// cppcheck-suppress constParameterCallback ; a DrawDotFnT: the context stays a plain void * for callers that keep state +static void focusDot(void *ctx, int32_t x, int32_t y) { + const FocusDotCtxT *dc = (const FocusDotCtxT *)ctx; + const DisplayT *d = dc->d; + + if (x >= d->clipX && x < d->clipX + d->clipW && y >= d->clipY && y < d->clipY + d->clipH) { + putPixel(d->backBuf + y * d->pitch + x * dc->bpp, dc->color, dc->bpp); + } +} + + static inline void glyphRowFg16(uint16_t *dst, uint8_t bits, uint16_t fg) { if (bits & 0x80) { dst[0] = fg; diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h index 334f7186..4563bec7 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.h +++ b/src/libs/kpunch/libdvx/dvxDraw.h @@ -124,6 +124,14 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // Windows 3.x focus rectangle convention. void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); +// One dot of a focus rectangle, for drawFocusRectDots. +typedef void (*DrawDotFnT)(void *ctx, int32_t x, int32_t y); + +// The dots drawFocusRect draws, one fn call each (no clipping): the one +// definition of the pattern, for drawing it somewhere other than the +// back buffer (a canvas: BASIC's DrawFocusRect method). +void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn, void *ctx); + // Compute the (x, y) origin for rendering `text` centered both // horizontally and vertically inside the rect (rectX, rectY, rectW, // rectH). Measures `text` with textWidthAccel (ignores & markers) and @@ -166,6 +174,11 @@ void drawArrowGlyph(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, in // dir selects the direction (up/down/left/right). void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir); +// Draw one complete arrow button of a scrollbar (what drawScrollbar paints +// at each end): a size x size raised button on the scrollbar face with the +// drawScrollbarArrow glyph pointing dir centred on it. +void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir); + // Horizontal line (1px tall rectangle fill, but with simpler clipping). void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); diff --git a/src/libs/kpunch/libdvx/dvxPrefs.h b/src/libs/kpunch/libdvx/dvxPrefs.h index f37bec06..f7ac687f 100644 --- a/src/libs/kpunch/libdvx/dvxPrefs.h +++ b/src/libs/kpunch/libdvx/dvxPrefs.h @@ -160,4 +160,13 @@ void prefsChoiceLabels(const PrefsChoiceT *choices, int32_t count, const char ** // when the key is missing or holds an unknown name. int32_t prefsGetChoice(PrefsHandleT *h, const char *section, const char *key, const PrefsChoiceT *choices, int32_t count, const char *defaultName); +// ============================================================ +// DVX BASIC bridge (dvxPrefsBas.c) +// ============================================================ + +// The settings behind a prefs.bas handle (PrefsLoad / PrefsCreate), or +// NULL for 0 or a handle that is not open. Other libraries' BASIC +// entry points take prefs.bas handles through this. +PrefsHandleT *prefsBasHandle(int32_t handle); + #endif diff --git a/src/libs/kpunch/libdvx/dvxPrefsBas.c b/src/libs/kpunch/libdvx/dvxPrefsBas.c new file mode 100644 index 00000000..7a6c3a1e --- /dev/null +++ b/src/libs/kpunch/libdvx/dvxPrefsBas.c @@ -0,0 +1,465 @@ +// 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. + +// dvxPrefsBas.c -- DVX BASIC entry points of the preferences (INI file) +// functions (src/include/basic/prefs.bas) +// +// Called through DECLARE LIBRARY, so the signatures use only BASIC- +// marshalable types: strings and LONG in, LONG or STRING out, True (-1) / +// False (0) for yes / no. A returned string stays valid until the next +// call that returns one (the BASIC runtime copies it at once). +// +// IniRead and IniWrite (built into BASIC) load and save the whole file +// for every value; a handle here holds a file loaded once, read and +// changed in memory and saved when the program says so -- with +// PrefsSaveAs from a PrefsCreate handle, a whole new file that replaces +// the old one only once it is completely written (dvxPrefs.h). Handles +// (1, 2, ...; 0 means none) belong to the program that made them; +// whatever a program leaves open is closed (unsaved) when it ends. + +#include "dvxPrefs.h" +#include "dvxMem.h" +#include "dvxTypes.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" +#define CHOICE_NAME_SEP '|' // between the names of PrefsGetChoiceFrom's list + +// The choice tables BASIC can read (prefs.bas PREFS_CHOICE_*: the +// binding's own numbers, which must not change) +typedef enum { + BasChoiceAccelE, + BasChoiceWallpaperE, + BasChoiceWheelE, + BasChoiceCountE +} BasChoiceE; + +typedef struct { + const PrefsChoiceT *choices; + int32_t count; + const char *defaultName; +} BasChoiceTableT; + +// One open handle and the program it belongs to +typedef struct { + PrefsHandleT *prefs; // NULL: the handle is free + int32_t appId; +} BasPrefsT; + +// The [video] mode of a handle (prefsGetVideoMode) +typedef struct { + int32_t width; + int32_t height; + int32_t bpp; +} BasVideoModeT; + +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// this library loads before the shell, and the host tests may have none. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); + +static BasPrefsT *sPrefs = NULL; // stb_ds; handle = index + 1 +static bool sCleanupHooked = false; + +static const BasChoiceTableT sChoiceTables[BasChoiceCountE] = { + [BasChoiceAccelE] = { prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT }, + [BasChoiceWallpaperE] = { prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT }, + [BasChoiceWheelE] = { prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, MOUSE_WHEEL_DIR_DEFAULT } +}; + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static const PrefsChoiceT *choiceOf(int32_t table, int32_t index); +static const BasChoiceTableT *choiceTable(int32_t table); +static int32_t handleNew(PrefsHandleT *prefs); +PrefsHandleT *prefsBasHandle(int32_t handle); +int32_t PrefsChoiceCount(int32_t table); +int32_t PrefsChoiceDefault(int32_t table); +int32_t PrefsChoiceIndex(int32_t table, const char *name); +int32_t PrefsChoiceIndexOfValue(int32_t table, int32_t value); +const char *PrefsChoiceLabel(int32_t table, int32_t index); +const char *PrefsChoiceName(int32_t table, int32_t index); +int32_t PrefsChoiceValue(int32_t table, int32_t index); +void PrefsClose(int32_t handle); +int32_t PrefsCreate(void); +const char *PrefsDvxIni(void); +const char *PrefsGet(int32_t handle, const char *section, const char *key, const char *defaultValue); +int32_t PrefsGetBool(int32_t handle, const char *section, const char *key, int32_t defaultValue); +int32_t PrefsGetChoice(int32_t handle, const char *section, const char *key, int32_t table); +int32_t PrefsGetChoiceFrom(int32_t handle, const char *section, const char *key, const char *names, const char *defaultName); +int32_t PrefsGetInt(int32_t handle, const char *section, const char *key, int32_t defaultValue); +const char *PrefsKeyName(int32_t handle, const char *section, int32_t index); +int32_t PrefsLoad(const char *path); +void PrefsRemove(int32_t handle, const char *section, const char *key); +int32_t PrefsSave(int32_t handle); +int32_t PrefsSaveAs(int32_t handle, const char *path); +void PrefsSet(int32_t handle, const char *section, const char *key, const char *value); +void PrefsSetBool(int32_t handle, const char *section, const char *key, int32_t value); +void PrefsSetInt(int32_t handle, const char *section, const char *key, int32_t value); +void PrefsSetVideoMode(int32_t handle, int32_t width, int32_t height, int32_t bpp); +int32_t PrefsVideoBpp(int32_t handle); +int32_t PrefsVideoHeight(int32_t handle); +int32_t PrefsVideoWidth(int32_t handle); +static BasVideoModeT videoModeOf(int32_t handle); + + +// Shell app-cleanup hook: close what a program that ended left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sPrefs); i++) { + if (sPrefs[i].prefs && sPrefs[i].appId == appId) { + PrefsClose(i + 1); + } + } +} + + +// Entry index of choice table table, or NULL when either is out of range. +static const PrefsChoiceT *choiceOf(int32_t table, int32_t index) { + const BasChoiceTableT *t = choiceTable(table); + + return (t && index >= 0 && index < t->count) ? &t->choices[index] : NULL; +} + + +// Choice table number table (PREFS_CHOICE_*), or NULL. +static const BasChoiceTableT *choiceTable(int32_t table) { + return (table >= 0 && table < BasChoiceCountE) ? &sChoiceTables[table] : NULL; +} + + +// A handle for prefs, owned by the running program (prefs is closed when +// no handle can be had). +static int32_t handleNew(PrefsHandleT *prefs) { + if (!prefs) { + return BAS_NO_HANDLE; + } + + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + int32_t idx = 0; + + while (idx < (int32_t)arrlen(sPrefs) && sPrefs[idx].prefs) { + idx++; + } + + if (idx == (int32_t)arrlen(sPrefs)) { + BasPrefsT blank = { NULL, 0 }; + + arrput(sPrefs, blank); + } + + sPrefs[idx].prefs = prefs; + sPrefs[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + return idx + 1; +} + + +// The settings behind a handle, or NULL (dvxPrefs.h). +PrefsHandleT *prefsBasHandle(int32_t handle) { + if (handle <= BAS_NO_HANDLE || handle > (int32_t)arrlen(sPrefs)) { + return NULL; + } + + return sPrefs[handle - 1].prefs; +} + + +// How many choices table has (0 for an unknown table). +int32_t PrefsChoiceCount(int32_t table) { + const BasChoiceTableT *t = choiceTable(table); + + return t ? t->count : 0; +} + + +// Index of the table's default choice (what a settings dialog's +// Defaults button selects), or -1 for an unknown table. +int32_t PrefsChoiceDefault(int32_t table) { + const BasChoiceTableT *t = choiceTable(table); + + return t ? PrefsChoiceIndex(table, t->defaultName) : -1; +} + + +// Index of the choice stored as name (any case), or -1. +int32_t PrefsChoiceIndex(int32_t table, const char *name) { + const BasChoiceTableT *t = choiceTable(table); + + return t ? prefsChoiceIndex(t->choices, t->count, name) : -1; +} + + +// Index of the choice whose value is value, or -1. +int32_t PrefsChoiceIndexOfValue(int32_t table, int32_t value) { + const BasChoiceTableT *t = choiceTable(table); + + return t ? prefsChoiceIndexOfValue(t->choices, t->count, value) : -1; +} + + +// The text a list shows for the choice ("" out of range). +const char *PrefsChoiceLabel(int32_t table, int32_t index) { + const PrefsChoiceT *c = choiceOf(table, index); + + return c ? c->label : ""; +} + + +// The spelling stored in the settings file ("" out of range). +const char *PrefsChoiceName(int32_t table, int32_t index) { + const PrefsChoiceT *c = choiceOf(table, index); + + return c ? c->iniName : ""; +} + + +// The value the choice applies (0 out of range). +int32_t PrefsChoiceValue(int32_t table, int32_t index) { + const PrefsChoiceT *c = choiceOf(table, index); + + return c ? c->value : 0; +} + + +// Free the handle; changes not saved are lost. +void PrefsClose(int32_t handle) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (prefs) { + prefsClose(prefs); + sPrefs[handle - 1].prefs = NULL; + } +} + + +// Empty settings, for writing a whole new file with PrefsSaveAs. +int32_t PrefsCreate(void) { + return handleNew(prefsCreate()); +} + + +// The path of DVX's own settings file (DVX.INI). +const char *PrefsDvxIni(void) { + return DVX_INI_PATH; +} + + +// The text of key in [section], or defaultValue. +const char *PrefsGet(int32_t handle, const char *section, const char *key, const char *defaultValue) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (!defaultValue) { + defaultValue = ""; + } + + return prefs ? prefsGetString(prefs, section, key, defaultValue) : defaultValue; +} + + +// True or False for key in [section] ("true", "yes" or "1" / "false", +// "no" or "0" in any case), or defaultValue. +int32_t PrefsGetBool(int32_t handle, const char *section, const char *key, int32_t defaultValue) { + PrefsHandleT *prefs = prefsBasHandle(handle); + bool dflt = defaultValue != BAS_FALSE; + + return (prefs ? prefsGetBool(prefs, section, key, dflt) : dflt) ? BAS_TRUE : BAS_FALSE; +} + + +// Index of the choice named by key in [section]; the table's default +// choice when the key is missing or holds an unknown name; -1 for an +// unknown table. +int32_t PrefsGetChoice(int32_t handle, const char *section, const char *key, int32_t table) { + const BasChoiceTableT *t = choiceTable(table); + + return t ? prefsGetChoice(prefsBasHandle(handle), section, key, t->choices, t->count, t->defaultName) : -1; +} + + +// prefsGetChoice with a program's own list: names is the choices as +// stored, "|"-separated in index order ("large|small|list"). The index +// of the name key holds; defaultName's index when the key is missing or +// unknown; -1 when defaultName is not in the list either. +int32_t PrefsGetChoiceFrom(int32_t handle, const char *section, const char *key, const char *names, const char *defaultName) { + char *buf = NULL; // stb_ds: names, each NUL-terminated + PrefsChoiceT *choices = NULL; // stb_ds + const char *list = names ? names : ""; + int32_t result = -1; + + for (const char *c = list; ; c++) { + arrput(buf, (*c == CHOICE_NAME_SEP) ? '\0' : *c); + + if (!*c) { + break; + } + } + + for (int32_t at = 0; list[0] && at < (int32_t)arrlen(buf); at += (int32_t)strlen(buf + at) + 1) { + PrefsChoiceT choice = { buf + at, buf + at, (int32_t)arrlen(choices) }; + + arrput(choices, choice); + } + + if (arrlen(choices) > 0) { + result = prefsGetChoice(prefsBasHandle(handle), section, key, choices, (int32_t)arrlen(choices), defaultName); + } + + arrfree(choices); + arrfree(buf); + return result; +} + + +// The number in key of [section], or defaultValue. +int32_t PrefsGetInt(int32_t handle, const char *section, const char *key, int32_t defaultValue) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + return prefs ? prefsGetInt(prefs, section, key, defaultValue) : defaultValue; +} + + +// Name of the index-th key (0-based, file order) of [section], "" past +// the last one. +const char *PrefsKeyName(int32_t handle, const char *section, int32_t index) { + const char *name = prefsKeyName(prefsBasHandle(handle), section, index); + + return name ? name : ""; +} + + +// The settings in the file path (none when it does not exist yet). 0 +// when out of memory. +int32_t PrefsLoad(const char *path) { + return handleNew(prefsLoad(path)); +} + + +void PrefsRemove(int32_t handle, const char *section, const char *key) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (prefs) { + prefsRemove(prefs, section, key); + } +} + + +// Save back to the file PrefsLoad read. True when it was saved. +int32_t PrefsSave(int32_t handle) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + return (prefs && prefsSave(prefs)) ? BAS_TRUE : BAS_FALSE; +} + + +// Save to path; the old file is replaced only once the new one is +// completely written. True when it was saved. +int32_t PrefsSaveAs(int32_t handle, const char *path) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + return (prefs && prefsSaveAs(prefs, path)) ? BAS_TRUE : BAS_FALSE; +} + + +void PrefsSet(int32_t handle, const char *section, const char *key, const char *value) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (prefs) { + prefsSetString(prefs, section, key, value ? value : ""); + } +} + + +// Stored as "true" or "false" +void PrefsSetBool(int32_t handle, const char *section, const char *key, int32_t value) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (prefs) { + prefsSetBool(prefs, section, key, value != BAS_FALSE); + } +} + + +void PrefsSetInt(int32_t handle, const char *section, const char *key, int32_t value) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (prefs) { + prefsSetInt(prefs, section, key, value); + } +} + + +// The [video] mode DVX starts in. +void PrefsSetVideoMode(int32_t handle, int32_t width, int32_t height, int32_t bpp) { + PrefsHandleT *prefs = prefsBasHandle(handle); + + if (prefs) { + prefsSetVideoMode(prefs, width, height, bpp); + } +} + + +// The [video] bits per pixel DVX starts in (videoModeOf). +int32_t PrefsVideoBpp(int32_t handle) { + return videoModeOf(handle).bpp; +} + + +// The [video] height DVX starts in (videoModeOf). +int32_t PrefsVideoHeight(int32_t handle) { + return videoModeOf(handle).height; +} + + +// The [video] width DVX starts in (videoModeOf). +int32_t PrefsVideoWidth(int32_t handle) { + return videoModeOf(handle).width; +} + + +// The [video] mode DVX starts in, DVX's default for each value that is +// not set and for a bad handle. +static BasVideoModeT videoModeOf(int32_t handle) { + BasVideoModeT mode; + + prefsGetVideoMode(prefsBasHandle(handle), &mode.width, &mode.height, &mode.bpp); + return mode; +} diff --git a/src/libs/kpunch/libdvx/dvxRes.h b/src/libs/kpunch/libdvx/dvxRes.h index cf381cbc..43df9800 100644 --- a/src/libs/kpunch/libdvx/dvxRes.h +++ b/src/libs/kpunch/libdvx/dvxRes.h @@ -42,6 +42,13 @@ #include "dvxPlat.h" +// A file that holds only resources -- no code, e.g. a control written in +// BASIC installed as a .wgt -- starts with these bytes (the NUL +// included); its resources follow. The DVX loader hands such a file to +// the reader that knows it instead of loading it as a module. +#define DVX_RES_ONLY_MAGIC "DVXRESONLY" +#define DVX_RES_ONLY_MAGIC_LEN ((int32_t)sizeof(DVX_RES_ONLY_MAGIC)) + // Resource type IDs #define DVX_RES_ICON 1 // image data (BMP icon: 16x16, 32x32, etc.) #define DVX_RES_TEXT 2 // null-terminated string (author, copyright, etc.) @@ -53,6 +60,11 @@ // Maximum resource name length (including null terminator) #define DVX_RES_NAME_MAX 32 +// The pictures a DVX file carries: a program's (32 x 32) icon, and a +// widget's toolbox icon (24 x 24) +#define DVX_RES_NAME_ICON "icon32" +#define DVX_RES_NAME_WIDGET_ICON "icon24" + // Directory entry (48 bytes each) typedef struct { char name[DVX_RES_NAME_MAX]; // resource name (e.g. "icon16", "author") diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h index 7dc75ee4..78444c63 100644 --- a/src/libs/kpunch/libdvx/dvxTypes.h +++ b/src/libs/kpunch/libdvx/dvxTypes.h @@ -65,7 +65,7 @@ // src/test/abi/abiSizes.txt (make -C src/test/abi update) in the same // change; the abi test fails when a public struct changes size without a // bump, and when the table was not regenerated after one. -#define DVX_ABI_VERSION 5 +#define DVX_ABI_VERSION 7 // A module's stamp is named , e.g. the // stamp of LIBS/KPUNCH/LIBDVX/LIBDVX.LIB is libdvxAbiVersion. @@ -539,6 +539,7 @@ typedef struct { #define KEY_SCAN_CTRL_TAB 0x94 #define KEY_SCAN_ALT_TAB 0xA5 #define KEY_SCAN_KP_ENTER 0xE0 // keypad Enter (grey-key prefix as scancode) +#define KEY_SCAN_ALT_KP_ENTER 0xA6 // keypad Enter with Alt held #define KEY_F1 (KEY_SCAN_F1 | KEY_EXT_FLAG) #define KEY_F2 (KEY_SCAN_F2 | KEY_EXT_FLAG) diff --git a/src/libs/kpunch/libdvx/dvxVideo.c b/src/libs/kpunch/libdvx/dvxVideo.c index 4108a129..6e0f93e3 100644 --- a/src/libs/kpunch/libdvx/dvxVideo.c +++ b/src/libs/kpunch/libdvx/dvxVideo.c @@ -67,6 +67,26 @@ #include "dvxPlat.h" #include "dvxPal.h" +#define VIDEO_CHANNEL_BITS 8 // bits per channel of the RGB triples packColor / unpackColor take and give + +static uint8_t expandChannel(uint32_t v, int32_t bits); + + +// A bits-wide channel value widened to 8 bits by repeating its bits +// below themselves (5-bit 31 -> 255, 5-bit 16 -> 132): black and a full +// channel read back exactly, which a plain left shift (31 -> 248) does +// not. A width of 0 (a BIOS that reports none) reads as 0. + +static uint8_t expandChannel(uint32_t v, int32_t bits) { + uint32_t out = v << (VIDEO_CHANNEL_BITS - bits); + + for (int32_t filled = bits; filled > 0 && filled < VIDEO_CHANNEL_BITS; filled *= 2) { + out |= out >> filled; + } + + return (uint8_t)out; +} + // Converts an 8-bit-per-channel RGB triple into whatever pixel // representation the current display mode uses. The result is @@ -91,9 +111,9 @@ uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) { // Shift each channel: first discard the low bits that won't fit in // the mode's field width (e.g. 8-bit red -> 5-bit red for 565), // then shift left to the field's position within the pixel word. - uint32_t rv = ((uint32_t)r >> (8 - d->format.redBits)) << d->format.redShift; - uint32_t gv = ((uint32_t)g >> (8 - d->format.greenBits)) << d->format.greenShift; - uint32_t bv = ((uint32_t)b >> (8 - d->format.blueBits)) << d->format.blueShift; + uint32_t rv = ((uint32_t)r >> (VIDEO_CHANNEL_BITS - d->format.redBits)) << d->format.redShift; + uint32_t gv = ((uint32_t)g >> (VIDEO_CHANNEL_BITS - d->format.greenBits)) << d->format.greenShift; + uint32_t bv = ((uint32_t)b >> (VIDEO_CHANNEL_BITS - d->format.blueBits)) << d->format.blueShift; return rv | gv | bv; } @@ -157,13 +177,9 @@ void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint return; } - uint32_t rv = (color & d->format.redMask) >> d->format.redShift; - uint32_t gv = (color & d->format.greenMask) >> d->format.greenShift; - uint32_t bv = (color & d->format.blueMask) >> d->format.blueShift; - - *r = (uint8_t)(rv << (8 - d->format.redBits)); - *g = (uint8_t)(gv << (8 - d->format.greenBits)); - *b = (uint8_t)(bv << (8 - d->format.blueBits)); + *r = expandChannel((color & d->format.redMask) >> d->format.redShift, d->format.redBits); + *g = expandChannel((color & d->format.greenMask) >> d->format.greenShift, d->format.greenBits); + *b = expandChannel((color & d->format.blueMask) >> d->format.blueShift, d->format.blueBits); } diff --git a/src/libs/kpunch/libdvx/dvxVideo.h b/src/libs/kpunch/libdvx/dvxVideo.h index 6a9a081e..c8ea8f3f 100644 --- a/src/libs/kpunch/libdvx/dvxVideo.h +++ b/src/libs/kpunch/libdvx/dvxVideo.h @@ -59,7 +59,9 @@ void videoShutdown(DisplayT *d); // linear scan of the grey ramp and chrome entries). uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b); -// Unpack a native pixel value back to 8-bit R, G, B components. +// Unpack a native pixel value back to 8-bit R, G, B components. A field +// narrower than 8 bits is widened by repeating its bits, so black and full +// channels come back exact (16 bpp white reads as 255, 255, 255). void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b); // Set the clip rectangle on the display. All subsequent draw operations diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h index e46dbb57..eb152f96 100644 --- a/src/libs/kpunch/libdvx/dvxWgt.h +++ b/src/libs/kpunch/libdvx/dvxWgt.h @@ -169,6 +169,7 @@ typedef enum { #define WCLASS_PRESS_RELEASE 0x00001000 // click = press+release (Button, ImageButton) #define WCLASS_ACCEL_WHEN_HIDDEN 0x00002000 // accel matching works even when invisible #define WCLASS_HAS_TEXT 0x00004000 // first field of w->data is (const char *), owned by widget (strdup/free) +#define WCLASS_RAISES_EVENTS 0x00008000 // the class fires the universal mouse, click, key and scroll callbacks itself (wgtFireEvent), the dispatcher does not (a BASIC UserControl: its host gets what its code raises) // Method IDs -- stable ABI, never reorder, never reuse. // New methods are appended at the end with the next sequential ID. @@ -195,8 +196,23 @@ typedef enum { #define WGT_METHOD_SCROLL_CHILD_INTO_VIEW 20 // void (parent, child) #define WGT_METHOD_ON_BLUR 21 // void (w) -- widget lost keyboard focus #define WGT_METHOD_ON_WHEEL 22 // bool (w, delta) -- wheel notches (+down); true = consumed +#define WGT_METHOD_ON_MOUSE_EVENT 23 // void (w, action, button, x, y, shift) -- every press / release / move, hover included +#define WGT_METHOD_ON_KEY_UP 24 // void (w, keyCode, shift) -- a key went up on the focused widget +#define WGT_METHOD_ON_FOCUS 25 // void (w) -- widget gained keyboard focus #define WGT_METHOD_MAX 32 // room for future methods +// WGT_METHOD_ON_MOUSE_EVENT's action. button and x / y are exactly what +// the universal onMouseDown / onMouseUp / onMouseMove callbacks receive: +// down / up carry the 1-based button ordinal (1 left, 2 right, 3 middle), +// move the MOUSE_* mask of the held buttons (0 = hover); x / y are +// content-relative (contentOffX / contentOffY already subtracted). shift +// is the KEY_MOD_* state of Shift / Ctrl / Alt. +typedef enum { + WgtMouseDownE, + WgtMouseUpE, + WgtMouseMoveE +} WgtMouseActionE; + // A class records the ABI it was built with (see DVX_ABI_VERSION). #define WGT_CLASS_VERSION DVX_ABI_VERSION @@ -206,6 +222,62 @@ typedef struct WidgetClassT { void *handlers[WGT_METHOD_MAX]; } WidgetClassT; +// ============================================================ +// Generic values +// ============================================================ +// +// One property value, method argument / result or event argument, used +// by the generic widget access (wgtGetProp / wgtSetProp / wgtCallMethod), +// dynamic interfaces (WgtDynIfaceT) and custom events (onEvent, +// wgtFireEvent). type is a WGT_IFACE_* value: INT, BOOL (0 / 1) and ENUM +// use i, FLOAT and DOUBLE use f, STRING uses s. A STRING value never owns its +// text: an argument's text belongs to the caller for the duration of the +// call; a returned one (property get, method result) belongs to the +// widget and stays valid until the next call on that widget or until +// the widget changes or is destroyed -- copy it to keep it. + +#define WGT_IFACE_STRING 0 +#define WGT_IFACE_INT 1 +#define WGT_IFACE_BOOL 2 +#define WGT_IFACE_FLOAT 3 +#define WGT_IFACE_ENUM 4 // int32_t with named values +#define WGT_IFACE_NONE 5 // WgtValueT only: no value (a void method's result) +#define WGT_IFACE_DOUBLE 6 // a double-precision number (BASIC Double; FLOAT is Single) + +typedef struct { + uint8_t type; // WGT_IFACE_* + union { + int32_t i; + double f; + const char *s; + }; +} WgtValueT; + +// Value constructors (wgtValueStr turns NULL into ""). +WgtValueT wgtValueBool(bool b); +WgtValueT wgtValueDouble(double f); +WgtValueT wgtValueFloat(double f); +WgtValueT wgtValueInt(int32_t i); +WgtValueT wgtValueStr(const char *s); + +// Coercions for any WgtValueT: numbers convert to each other (a FLOAT or +// DOUBLE truncates toward zero), a BOOL is 0 / 1, a STRING is parsed +// (decimal, strtod, wgtParseBool), NONE is 0 / false. wgtValueToText +// formats into buf (INT / ENUM decimal, BOOL "True" / "False", FLOAT %g, +// DOUBLE with 15 significant digits) and returns +// buf, or the value's own text for a STRING; WGT_VALUE_TEXT_MAX bytes +// hold any non-STRING value. +#define WGT_VALUE_TEXT_MAX 32 + +bool wgtValueToBool(const WgtValueT *v); +double wgtValueToFloat(const WgtValueT *v); +int32_t wgtValueToInt(const WgtValueT *v); +const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize); + +// The .frm / property-sheet boolean rule: "True" or "-1" is true, "False" +// or "0" false (case-insensitive), anything else defaultValue. +bool wgtParseBool(const char *text, bool defaultValue); + // ============================================================ // Widget structure // ============================================================ @@ -316,6 +388,12 @@ typedef struct WidgetT { // part of the child actually visible, which mouse dispatch uses to find // the child under the cursor without the container's help. RectT paintClip; + + // Custom (named) events: wgtFireEvent hands every event that has no + // universal callback above to this one (e.g. a ListView's + // "ColumnClick", an event a BASIC UserControl raises). args may be + // written back (ByRef parameters); see wgtFireEvent. + void (*onEvent)(struct WidgetT *w, const char *name, WgtValueT *args, int32_t argc); } WidgetT; static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) { @@ -486,6 +564,18 @@ static inline void wclsOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_ } +// Dispatched on the widget GAINING keyboard focus, before the app-level +// onFocus callback (the counterpart of wclsOnBlur). +static inline void wclsOnFocus(WidgetT *w) { + typedef void (*FnT)(WidgetT *); + FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_FOCUS] : NULL; + + if (fn) { + fn(w); + } +} + + static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) { typedef void (*FnT)(WidgetT *, int32_t, int32_t); FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_KEY] : NULL; @@ -496,6 +586,18 @@ static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) { } +// Key release on the focused widget, before the app-level onKeyUp with +// the same arguments. +static inline void wclsOnKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) { + typedef void (*FnT)(WidgetT *, int32_t, int32_t); + FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_KEY_UP] : NULL; + + if (fn) { + fn(w, keyCode, shift); + } +} + + static inline void wclsOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t); FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_MOUSE] : NULL; @@ -506,6 +608,20 @@ static inline void wclsOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy } +// Every mouse press, release and move (hover included) on the widget the +// universal mouse callbacks go to, before those callbacks (see +// WgtMouseActionE for the arguments). A widget that captured the mouse +// (sDragWidget) also gets the moves and the left release of the capture. +static inline void wclsOnMouseEvent(WidgetT *w, WgtMouseActionE action, int32_t button, int32_t x, int32_t y, int32_t shift) { + typedef void (*FnT)(WidgetT *, WgtMouseActionE, int32_t, int32_t, int32_t, int32_t); + FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_MOUSE_EVENT] : NULL; + + if (fn) { + fn(w, action, button, x, y, shift); + } +} + + // Offered the mouse wheel before the default WCLASS_SCROLLABLE handling. // delta is in notches, positive = down, with the user's reverse setting // already applied. Returns true when the widget consumed the event. @@ -555,6 +671,13 @@ static inline int32_t wclsQuickRepaint(WidgetT *w, int32_t *outY, int32_t *outH) } +// True when w's class fires the universal mouse, click, key and scroll +// callbacks itself (WCLASS_RAISES_EVENTS). +static inline bool wclsRaisesEvents(const WidgetT *w) { + return w->wclass && (w->wclass->flags & WCLASS_RAISES_EVENTS); +} + + static inline void wclsScrollChildIntoView(WidgetT *parent, const WidgetT *child) { typedef void (*FnT)(WidgetT *, const WidgetT *); FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_SCROLL_CHILD_INTO_VIEW] : NULL; @@ -682,6 +805,9 @@ void wgtSetTooltip(WidgetT *w, const char *text); // Draw borders around layout containers in ugly colors void wgtSetDebugLayout(struct AppContextT *ctx, bool enabled); +// True while the layout borders are drawn (wgtSetDebugLayout) +bool wgtGetDebugLayout(void); + // ============================================================ // Dynamic widget registration // ============================================================ @@ -693,6 +819,12 @@ void wgtSetDebugLayout(struct AppContextT *ctx, bool enabled); // static const in the registering module). int32_t wgtRegisterClass(const WidgetClassT *wclass); +// Remove the interface registered under name (wgtRegisterIface): a +// widget class made at run time (a BASIC UserControl a program registers +// for its own use) leaves the registry when its owner is done with it. +// Nothing happens when no interface has the name. +void wgtUnregisterIface(const char *name); + // Release a class registered by wgtRegisterClass (e.g. from an app's // shutdown before its module is unloaded). Every widget of the class must // already be destroyed. Creating a widget with the freed ID fails, and a @@ -715,6 +847,18 @@ void wgtUnregisterAppClasses(int32_t appId); // (meaning "auto"). int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth); +// Padding / Spacing of a container in pixels, as the BASIC Padding and +// Spacing properties of the boxes and the ScrollPane read and write them. +// The getters return 0 for an unset size (the container's default) or one +// in another unit; the setters clamp to 0..WGT_GAP_PIXELS_MAX (0 = the +// default) and re-lay out the window. +#define WGT_GAP_PIXELS_MAX 4096 + +int32_t wgtGetPaddingPx(const WidgetT *w); +int32_t wgtGetSpacingPx(const WidgetT *w); +void wgtSetPaddingPx(WidgetT *w, int32_t pixels); +void wgtSetSpacingPx(WidgetT *w, int32_t pixels); + // Execute the full two-pass layout algorithm on the widget tree: // Pass 1 (bottom-up): calcMinSize on every widget to compute minimum sizes. // Pass 2 (top-down): allocate space within availW/availH, distributing @@ -791,14 +935,12 @@ static inline uint32_t wgtBasicColor(uint32_t color) { return (color & WGT_COLOR_KEEP_MASK) | ((color & WGT_COLOR_CHAN_MASK) << WGT_COLOR_SWAP_SHIFT) | ((color >> WGT_COLOR_SWAP_SHIFT) & WGT_COLOR_CHAN_MASK); } -// Property data types -#define WGT_IFACE_STRING 0 -#define WGT_IFACE_INT 1 -#define WGT_IFACE_BOOL 2 -#define WGT_IFACE_FLOAT 3 -#define WGT_IFACE_ENUM 4 // int32_t with named values +// Property data types: WGT_IFACE_* (see "Generic values" above). A +// FLOAT property's getter returns float and its setter takes float, a +// DOUBLE property's double. -// Method calling conventions (how the form runtime marshals args) +// Method calling conventions (how wgtIfaceCallMethod marshals args; +// wgtSigInfo describes each one) #define WGT_SIG_VOID 0 // void fn(WidgetT *) #define WGT_SIG_INT 1 // void fn(WidgetT *, int32_t) #define WGT_SIG_BOOL 2 // void fn(WidgetT *, bool) @@ -823,8 +965,32 @@ static inline uint32_t wgtBasicColor(uint32_t color) { #define WGT_SIG_STR_STR 21 // void fn(WidgetT *, const char *, const char *) #define WGT_SIG_STR_BOOL 22 // void fn(WidgetT *, const char *, bool) #define WGT_SIG_RET_INT_STR 23 // int32_t fn(WidgetT *, const char *) -- status-returning string method (e.g. Load/Save) +#define WGT_SIG_STR_STR_BOOL_BOOL 24 // bool fn(WidgetT *, const char *, const char *, bool, bool) +#define WGT_SIG_RET_INT_INT 25 // int32_t fn(const WidgetT *, int32_t) +#define WGT_SIG_DYNAMIC 26 // bool fn(WidgetT *, const WgtValueT *args, int32_t argc, WgtValueT *ret) (WgtDynMethodFnT): any arguments, as they come -// Property descriptor +typedef bool (*WgtDynMethodFnT)(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); + +// What a WGT_SIG_* passes and returns: argc arguments of argTypes[] and a +// result of retType (WGT_IFACE_NONE for none). Callers that convert their +// own values (the form runtime's BASIC rules) convert to these types +// first; wgtIfaceCallMethod coerces anything else (wgtValueTo*). +#define WGT_SIG_MAX_ARGS 5 + +typedef struct { + uint8_t argc; + uint8_t retType; // WGT_IFACE_* + uint8_t argTypes[WGT_SIG_MAX_ARGS]; // WGT_IFACE_* of each argument +} WgtSigInfoT; + +// The description of sig, or NULL for WGT_SIG_DYNAMIC (any arguments) +// and unknown values. +const WgtSigInfoT *wgtSigInfo(uint8_t sig); + +// Property descriptor. Readable means getFn != NULL and writable means +// setFn != NULL, for every interface (the one rule all property sheets +// and runtimes apply); a dynamic interface (WgtIfaceT.dyn) puts +// WGT_DYN_ACCESS there, and its getFn / setFn are never called. typedef struct { const char *name; // BASIC property name (e.g. "Caption", "Value") uint8_t type; // WGT_IFACE_* @@ -833,23 +999,33 @@ typedef struct { const char **enumNames; // WGT_IFACE_ENUM only: NULL-terminated array of value names } WgtPropDescT; -// Method descriptor +// Marker for getFn / setFn / fn of a dynamic interface: present, never called. +#define WGT_DYN_ACCESS ((void *)(uintptr_t)1) + +// Method descriptor. On a dynamic interface fn is WGT_DYN_ACCESS and sig +// is WGT_SIG_DYNAMIC (or the signature the arguments are converted to). typedef struct { const char *name; // BASIC method name (e.g. "Clear", "SetFocus") uint8_t sig; // WGT_SIG_* void *fn; // function pointer } WgtMethodDescT; -// Event descriptor +// Event descriptor. params is the handler's parameter list as BASIC +// writes it ("Topic As String", "" for none) -- the one place a widget's +// own event parameters are written down (IDE handler stubs, designer.bas +// CtlEventParams$). NULL means the event is one of the BASIC master +// list's (Timer, Validate ...) and takes its parameters from there. typedef struct { const char *name; // event name (e.g. "Click", "Change") + const char *params; // handler parameter list, NULL = the master list's } WgtEventDescT; // Common events implicitly available on all widgets: the form runtime // wires them on every control. The list (Click, DblClick, Change, focus, // key and mouse events, ...) is BAS_EVENT_LIST in the BASIC compiler's // basEvents.h (entries with BAS_EVT_SCOPE_CTRL) -- the single source of -// truth. Widget descriptors only need to list EXTRA events beyond these. +// truth. Widget descriptors only need to list EXTRA events beyond these; +// a widget fires them with wgtFireEvent. // Create function signature types for design-time / runtime instantiation. // The create function is always the first slot after the header in the @@ -867,8 +1043,26 @@ typedef enum { #define WGT_MAX_CREATE_ARGS 3 -// Widget interface descriptor (registered by each .wgt) +struct WgtIfaceT; + +// Dynamic interface: a widget class whose properties and methods are not +// C functions with fixed signatures (a BASIC UserControl, a scripted +// widget) implements these hooks; the generic access below calls them +// instead of the descriptors' getFn / setFn / fn. propIdx / methodIdx +// index the interface's props[] / methods[]. Each returns false when it +// could not do it (a set refused, a method failed). Strings returned in +// out / ret follow the WgtValueT lifetime rule (owned by the widget until +// the next call on it). ctx is the provider's own pointer. typedef struct { + struct WidgetT *(*create)(struct WidgetT *parent, const struct WgtIfaceT *iface); + bool (*getProp)(struct WidgetT *w, int32_t propIdx, WgtValueT *out); + bool (*setProp)(struct WidgetT *w, int32_t propIdx, const WgtValueT *value); + bool (*callMethod)(struct WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret); + void *ctx; +} WgtDynIfaceT; + +// Widget interface descriptor (registered by each .wgt) +typedef struct WgtIfaceT { // Fields are ordered by alignment to minimize struct padding. const char *basName; // VB-style name (e.g. "CommandButton"), or NULL const WgtPropDescT *props; // type-specific properties @@ -882,21 +1076,105 @@ typedef struct { const char *namePrefix; // auto-name prefix (NULL = use basName) uint8_t createSig; // WgtCreateSigE: how to call create fn bool isContainer; // can hold child widgets + const WgtDynIfaceT *dyn; // dynamic interface hooks, NULL for a C widget } WgtIfaceT; // Register/retrieve interface descriptors by widget type name. const WgtIfaceT *wgtGetIface(const char *name); void wgtRegisterIface(const char *name, const WgtIfaceT *iface); +// Bind the interface registered under name to the widget class typeId +// (from wgtRegisterClass), so wgtGetIfaceOf finds it from any widget of +// that class. Call after wgtRegisterIface; wgtUnregisterClass drops the +// binding. +void wgtIfaceSetType(const char *name, int32_t typeId); + +// The interface of w's class (wgtIfaceSetType), or NULL; outName (may be +// NULL) receives its registered type name. +const WgtIfaceT *wgtGetIfaceOf(const WidgetT *w, const char **outName); + // Case-insensitive lookup of a property descriptor on iface by name. // Single source of truth for the runtime and IDE property-override rule. // Returns NULL if iface is NULL or no property matches. const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propName); +// Case-insensitive lookups of a method / an extra event on iface by name +// (the first match); NULL if iface is NULL or nothing matches. +const WgtEventDescT *wgtIfaceFindEvent(const WgtIfaceT *iface, const char *eventName); +const WgtMethodDescT *wgtIfaceFindMethod(const WgtIfaceT *iface, const char *methodName); + // Find a widget type name by its VB-style name (e.g. "CommandButton" -> "button"). // Returns NULL if no widget has that basName. Case-insensitive. const char *wgtFindByBasName(const char *basName); +// The registered type name for name, which is either a BASIC name +// ("CommandButton" -> "button", tried first) or a type name with an API or +// an interface ("button" -> "button"); NULL when neither is registered. +const char *wgtResolveTypeName(const char *name); + +// ============================================================ +// Generic widget access +// ============================================================ +// +// One implementation of "create / read / write / call by name" for every +// widget, static (C functions in the descriptors) or dynamic +// (WgtIfaceT.dyn). The form runtime, the IDE designer and C programs use +// these; nothing else switches on WGT_SIG_* or WgtCreateSigE. + +// Create a widget of type name (BASIC or type name, wgtResolveTypeName) +// under parent: a dynamic interface's create hook, else the API's create +// function called by the interface's createSig with its createArgs ("" +// for a text, an empty picture for WGT_CREATE_PARENT_DATA; a type with no +// interface is created as fn(parent)). NULL if the type is unknown or +// creation failed. +WidgetT *wgtCreateByName(const char *name, WidgetT *parent); + +// Read / write property p of iface on w. Read: false when p is +// write-only or the dynamic hook failed (out is then a zero value of +// p's type); a STRING read is never NULL. Write: value is converted to +// p's type (wgtValueTo*; a STRING for an ENUM property is looked up in +// enumNames, case-insensitive); false when p is read-only, an enum name +// is unknown, or the dynamic hook refused. +bool wgtIfaceGetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, WgtValueT *out); +bool wgtIfaceSetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const WgtValueT *value); + +// The same through text in the .frm / property-sheet form: ENUM by name, +// INT decimal, BOOL wgtParseBool ("True" / "False" when read), FLOAT +// strtod / %g, STRING as it is. Get fails for an enum value with no name. +bool wgtIfaceGetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); +bool wgtIfaceSetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const char *text); + +// Call method m of iface on w with argc arguments (converted to the +// signature's types, wgtSigInfo). ret (may be NULL) receives the result +// (type WGT_IFACE_NONE for a void method). False, without calling, when +// fewer arguments than the signature needs are given (ret is then the +// zero value of the result type), and when a dynamic method failed. +bool wgtIfaceCallMethod(const WgtIfaceT *iface, WidgetT *w, const WgtMethodDescT *m, const WgtValueT *args, int32_t argc, WgtValueT *ret); + +// By name on any widget (its interface from wgtGetIfaceOf): false when +// the widget has no interface or no such property / method, else as +// above. +bool wgtCallMethod(WidgetT *w, const char *methodName, const WgtValueT *args, int32_t argc, WgtValueT *ret); +bool wgtGetProp(WidgetT *w, const char *propName, WgtValueT *out); +bool wgtSetProp(WidgetT *w, const char *propName, const WgtValueT *value); + +// ============================================================ +// Custom events +// ============================================================ + +// Fire event name on w. The universal events go to their callback, with +// their arguments taken from args (a missing one is 0): Click -> onClick, +// DblClick -> onDblClick, Change -> onChange, GotFocus -> onFocus, +// LostFocus -> onBlur, KeyPress (KeyAscii) -> onKeyPress, KeyDown / KeyUp +// (KeyCode, Shift) -> onKeyDown / onKeyUp, MouseDown / MouseUp / MouseMove +// (Button, X, Y, or VB's Button, Shift, X, Y) -> onMouseDown / onMouseUp / +// onMouseMove (which take no Shift), Scroll (Delta) +// -> onScroll (names case-insensitive). Every other name goes to +// w->onEvent; the universal ones never do, so a host that sets both sees +// each event once. onEvent may write back into args (ByRef parameters). +// Returns false when a handler destroyed widgets (do not touch w then). +bool wgtFireEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); + // Enumerate all registered widget interfaces. const WgtIfaceT *wgtIfaceAt(int32_t idx, const char **outName); int32_t wgtIfaceCount(void); diff --git a/src/libs/kpunch/libdvx/dvxWm.c b/src/libs/kpunch/libdvx/dvxWm.c index a01ed369..8c5f2a79 100644 --- a/src/libs/kpunch/libdvx/dvxWm.c +++ b/src/libs/kpunch/libdvx/dvxWm.c @@ -136,6 +136,7 @@ static bool menuGrowItems(MenuT *menu); static int32_t menuLabelWidth(const char *label); static MenuItemT *menuNewItem(MenuT *menu, const char *label); static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize); +static void scrollbarAtThumb(int32_t length, int32_t scrollPos, int32_t visibleItems, int32_t totalItems, int32_t *thumbPos, int32_t *thumbSize); static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue); static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize); static void thumbGeometry(int32_t trackLen, int32_t min, int32_t max, int32_t value, int32_t pageSize, int32_t *thumbPos, int32_t *thumbSize); @@ -818,6 +819,23 @@ static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrient } +// The thumb of a standalone scrollbar (wmDrawScrollbarAt and +// wmVScrollbarAtClick, so a click always matches the paint): length long +// including both arrow buttons, showing visibleItems of totalItems from +// scrollPos. No thumb (size 0) when everything is visible or the track +// has no room, unlike a window scrollbar, whose thumb then fills the track. +static void scrollbarAtThumb(int32_t length, int32_t scrollPos, int32_t visibleItems, int32_t totalItems, int32_t *thumbPos, int32_t *thumbSize) { + int32_t trackLen = length - SCROLLBAR_WIDTH * 2; + + *thumbPos = 0; + *thumbSize = 0; + + if (trackLen > 0 && totalItems > visibleItems) { + thumbGeometry(trackLen, 0, totalItems - visibleItems, scrollPos, visibleItems, thumbPos, thumbSize); + } +} + + // Clamps a scrollbar value to its range and, if it changed, dirties the // scrollbar area and fires the window's onScroll callback. Shared tail of // wmScrollbarClick and wmScrollbarDrag. @@ -855,8 +873,8 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32 } -// The WM scrollbar thumb rule, shared by the window scrollbars and -// wmDrawVScrollbarAt. Thumb size is proportional to the visible page +// The WM scrollbar thumb rule, shared by the window scrollbars and the +// standalone ones (scrollbarAtThumb). Thumb size is proportional to the visible page // relative to the total scrollable range: trackLen * pageSize / // (range + pageSize). This gives a thumb that shrinks as content grows, // matching Motif/Windows scrollbars. The size is clamped up to @@ -1590,6 +1608,15 @@ static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT } +void wmDrawScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) { + int32_t thumbPos; + int32_t thumbSize; + + scrollbarAtThumb(length, scrollPos, visibleItems, totalItems, &thumbPos, &thumbSize); + drawScrollbar(d, ops, colors, orient, x, y, length, SCROLLBAR_WIDTH, thumbPos, thumbSize); +} + + void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) { RectT savedClip = clipSave(d); @@ -1608,17 +1635,7 @@ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colo void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) { - int32_t trackLen = h - SCROLLBAR_WIDTH * 2; - int32_t thumbPos = 0; - int32_t thumbSize = 0; - - // No thumb when everything is visible (unlike a window scrollbar, whose - // thumb then fills the track). - if (trackLen > 0 && totalItems > visibleItems) { - thumbGeometry(trackLen, 0, totalItems - visibleItems, scrollPos, visibleItems, &thumbPos, &thumbSize); - } - - drawScrollbar(d, ops, colors, ScrollbarVerticalE, x, y, h, SCROLLBAR_WIDTH, thumbPos, thumbSize); + wmDrawScrollbarAt(d, ops, colors, ScrollbarVerticalE, x, y, h, scrollPos, visibleItems, totalItems); } @@ -1944,6 +1961,18 @@ void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled) { } +// The label and its accelerator, as menuNewItem sets them. An open popup +// shows the new label the next time it is drawn. +void wmMenuItemSetLabel(MenuItemT *item, const char *label) { + if (!item || !label) { + return; + } + + copyLabel(item->label, label, MAX_MENU_LABEL); + item->accelKey = accelParse(item->label); +} + + // Minimizes a window: marks it minimized so it's skipped during compositing // (except for icon drawing) and moves focus to the next available window. // The window's geometry and content buffer are preserved -- no reallocation @@ -2268,6 +2297,35 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { } +// wmRemoveMenu -- take one menu off a window's menu bar and free it; the +// bar goes away with its last menu + +bool wmRemoveMenu(WindowT *win, MenuT *menu, const DisplayT *d) { + MenuBarT *bar = win ? win->menuBar : NULL; + + for (int32_t i = 0; bar && i < bar->menuCount; i++) { + if (bar->menus[i] != menu) { + continue; + } + + freeMenuRecursive(menu); + free(menu); + memmove(&bar->menus[i], &bar->menus[i + 1], (bar->menuCount - i - 1) * sizeof(MenuT *)); + bar->menuCount--; + bar->activeIdx = -1; + bar->positionsDirty = true; + + if (bar->menuCount == 0) { + wmDestroyMenuBar(win, d); + } + + return true; + } + + return false; +} + + // wmRemoveMenuItem -- remove a menu item by command ID bool wmRemoveMenuItem(MenuT *menu, int32_t id) { @@ -2913,21 +2971,18 @@ void wmUpdateContentRect(WindowT *win) { } -int32_t wmVScrollbarAtClick(int32_t h, int32_t relY, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) { +// Only the offset along the bar matters, so this serves both orientations. +int32_t wmVScrollbarAtClick(int32_t length, int32_t offset, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) { int32_t maxScroll = DVX_MAX(totalItems - visibleItems, 0); - int32_t trackLen = h - SCROLLBAR_WIDTH * 2; - int32_t thumbPos = 0; - int32_t thumbSize = 0; + int32_t trackRel = offset - SCROLLBAR_WIDTH; + int32_t thumbPos; + int32_t thumbSize; - if (trackLen > 0 && maxScroll > 0) { - thumbGeometry(trackLen, 0, maxScroll, scrollPos, visibleItems, &thumbPos, &thumbSize); - } + scrollbarAtThumb(length, scrollPos, visibleItems, totalItems, &thumbPos, &thumbSize); - int32_t trackRel = relY - SCROLLBAR_WIDTH; - - if (relY < SCROLLBAR_WIDTH) { + if (offset < SCROLLBAR_WIDTH) { scrollPos--; - } else if (relY >= h - SCROLLBAR_WIDTH) { + } else if (offset >= length - SCROLLBAR_WIDTH) { scrollPos++; } else if (trackRel < thumbPos) { scrollPos -= visibleItems; diff --git a/src/libs/kpunch/libdvx/dvxWm.h b/src/libs/kpunch/libdvx/dvxWm.h index c62fb096..b89ca939 100644 --- a/src/libs/kpunch/libdvx/dvxWm.h +++ b/src/libs/kpunch/libdvx/dvxWm.h @@ -118,6 +118,13 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked // Insert a horizontal separator line. Separators are not interactive. void wmAddMenuSeparator(MenuT *menu); +// Remove one menu (a wmAddMenu result) from the window's menu bar and free +// it with its items and submenus; removing the last one removes the bar +// (its content buffer is reallocated for d, as by wmDestroyMenuBar). +// Returns true if the menu was on the bar. The caller re-lays out the +// window (the content area changes size when the bar goes). +bool wmRemoveMenu(WindowT *win, MenuT *menu, const DisplayT *d); + // Remove a menu item by command ID. Returns true if found and removed. bool wmRemoveMenuItem(MenuT *menu, int32_t id); @@ -153,6 +160,10 @@ void menuItemApplyChecked(MenuT *menu, int32_t idx, bool checked); // Enable or disable a menu item by command ID. void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled); +// Give an item a new label, written as wmAddMenuItem takes it: & before +// the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). +void wmMenuItemSetLabel(MenuItemT *item, const char *label); + // Create a cascading submenu attached to the parent menu. Returns the // child MenuT to populate, or NULL on allocation failure. // The child MenuT is heap-allocated and freed when the parent window @@ -290,18 +301,30 @@ MenuT *wmCreateMenu(void); // heap-allocated submenu children recursively. void wmFreeMenu(MenuT *menu); -// Draw a standalone vertical scrollbar at the given screen coordinates. -// Used by popup lists (dropdown, combobox) that need a scrollbar without -// a full ScrollbarT struct. scrollPos/visibleItems/totalItems define the -// scroll state; the function computes thumb position/size internally. +// Draw a standalone scrollbar of either orientation at the given screen +// coordinates, for code that needs a scrollbar without a full ScrollbarT +// struct. A vertical bar (ScrollbarVerticalE) is SCROLLBAR_WIDTH wide and +// length tall with up / down arrows; a horizontal one (ScrollbarHorizontalE) +// is length wide and SCROLLBAR_WIDTH tall with left / right arrows. +// scrollPos/visibleItems/totalItems define the scroll state (visibleItems +// of totalItems shown from scrollPos); the function computes thumb +// position/size internally, with no thumb when everything is visible. +// wmVScrollbarAtClick maps a click on the bar to a new scrollPos. +void wmDrawScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); + +// wmDrawScrollbarAt for a vertical bar h tall. Used by popup lists +// (dropdown, combobox). void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); -// Apply a click on a scrollbar drawn by wmDrawVScrollbarAt with the same h -// and scroll state; relY is the click's offset from the bar's top. Returns -// the new scrollPos: the arrows step one item, the trough a page -// (visibleItems), the thumb leaves it unchanged; the result is clamped to +// Apply a click on a scrollbar drawn by wmDrawScrollbarAt (either +// orientation) or wmDrawVScrollbarAt with the same length and scroll +// state; offset is the click's offset along the bar from its start (down +// from the top of a vertical bar, right from the left end of a horizontal +// one), so the function is orientation-independent. Returns the new +// scrollPos: the arrows step one item, the trough a page (visibleItems), +// the thumb leaves it unchanged; the result is clamped to // [0, totalItems - visibleItems]. The thumb comes from the same geometry // the paint uses, so clicks always match what is drawn. -int32_t wmVScrollbarAtClick(int32_t h, int32_t relY, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); +int32_t wmVScrollbarAtClick(int32_t length, int32_t offset, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); #endif // DVX_WM_H diff --git a/src/libs/kpunch/libdvx/games.dhs b/src/libs/kpunch/libdvx/games.dhs index 69a58083..d066c1cb 100644 --- a/src/libs/kpunch/libdvx/games.dhs +++ b/src/libs/kpunch/libdvx/games.dhs @@ -336,6 +336,7 @@ Close the game's window and return from appMain. Every sound, music, joystick ha .index STICK .index STRIG .index GameKeyDown +.index FrameMissed .h1 Games in DVX BASIC @@ -352,7 +353,7 @@ Sub tmrStart_Timer () End Sub .endcode -GameKeyDown (game.bas) tells whether a key is held; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference. +GameKeyDown (game.bas) tells whether a key is held, GameKeyClear forgets the held keys, FrameMissed() counts the frames FRAMEWAIT skipped and GameWaitRetrace waits for the screen's retrace; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, sounds loaded once and played with volume, pan and pitch, sound the game makes as it goes (streams) and FM instruments; music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference. .topic games.porting .title Porting a Game Library to DVX diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h index d3bac930..11d3df19 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlat.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h @@ -147,6 +147,10 @@ uint64_t platformEntropyStamp(void); // Convert a millisecond interval to ticks at the platform rate. #define PLATFORM_MS_TO_TICKS(ms) ((PlatformTicksT)(((uint64_t)(ms) * platformClockHz()) / 1000)) +// Convert ticks to milliseconds: the "now" the protocol engines (conn, +// xfer) take is PLATFORM_TICKS_TO_MS(platformClock()). +#define PLATFORM_TICKS_TO_MS(ticks) ((uint32_t)(((uint64_t)(ticks) * 1000) / platformClockHz())) + // ============================================================ // Timing -- high-resolution clock and the timer service // ============================================================ @@ -473,8 +477,8 @@ void platformKeyStateClear(void); // Input -- Test injection // ============================================================ // -// Used only by the test script runner (DVX /SCRIPT=...); nothing in the -// shipping code path calls these. Injected input enters at the lowest +// Used by the test script runner (DVX /SCRIPT=...), the installer and +// BASIC's SysSendKey (keys only). Injected input enters at the lowest // level each backend has, so it travels the same code as real input. // Queue one keystroke exactly as the BIOS would store it: ascii is the diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c index 1f716da0..eb09ea02 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c @@ -3325,6 +3325,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) // --- help compiler (dvxhlpc.c linked into dvx.exe; BASIC HelpCompile) --- DXE_EXPORT(hlpcCompile) + DXE_EXPORT(hlpcSetMessageFn) // --- preferences (dvxPrefs.c linked into dvx.exe; the only resident copy) --- DXE_EXPORT(prefsAccelChoices) @@ -3557,7 +3558,11 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(tmpnam) // --- directory --- + DXE_EXPORT(_lfn_gen_short_fname) + DXE_EXPORT(_preserve_fncase) DXE_EXPORT(closedir) + DXE_EXPORT(findfirst) + DXE_EXPORT(findnext) DXE_EXPORT(mkdir) DXE_EXPORT(opendir) DXE_EXPORT(readdir) diff --git a/src/libs/kpunch/libdvx/sdkGuide.dhs b/src/libs/kpunch/libdvx/sdkGuide.dhs index a13e1d7e..9784ea49 100644 --- a/src/libs/kpunch/libdvx/sdkGuide.dhs +++ b/src/libs/kpunch/libdvx/sdkGuide.dhs @@ -73,6 +73,7 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source basic A DVX BASIC program (NAME.dbp, built to NAME.app) game A C game (NAME.app): a 320x200 screen in a window, frame pacing, keyboard and joystick, sound effects and music basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY + control A control written in DVX BASIC (NAME.dbp with NAME.ctl, built to NAME.wgt) usable from C programs and DVX BASIC .endtable The two game templates are explained in Writing Games for DVX. @@ -131,6 +132,8 @@ Nothing else is needed: DVX loads dvxnet.lib when it starts and connects the cal .link sdk.firstwidget Your First Widget .link sdk.firstlib Your First Library .link sdk.firstbasic Your First BASIC Program +.link sdk.firstcontrol Your First BASIC Control +.link sdk.basiccontrols Using BASIC Controls from C .link sdk.debug Debugging: the Log and Crash Reports .link sdk.abi Binary Interface Rules .link sdk.packaging Packaging and Installing @@ -229,7 +232,7 @@ DVX loads every widget when it starts and calls its wgtRegister once. The templa .list .item A widget class (WidgetClassT): the paint and minimum-size handlers, and the shared text handlers (the widget's data starts with its text, so widgetAllocWithText creates it and WCLASS_HAS_TEXT frees it). .item An API table (BadgeApiT) under the name "badge". It starts with WgtApiHeaderT, filled by WGT_API_HEADER. Programs reach it through the macros in badge.h, which call wgtGetApi("badge", sizeof(BadgeApiT)). -.item A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. +.item A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. wgtIfaceSetType binds the interface to the class, so wgtGetIfaceOf and the generic calls (wgtGetProp, wgtSetProp, wgtCallMethod) find it from any badge widget. .endlist DVX_ABI_STAMP(badge) at file scope records the binary interface version. Its argument must be the module's file name in lower case. @@ -297,6 +300,13 @@ DVX loads every library when it starts, before any widget or program, in the ord BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *. +A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length or a length never set give an empty string). The template's tplNameXor shows both: + +.code + // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING + const uint8_t *tplNameXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen); +.endcode + .h2 Run Install it (LIBS/LOCAL/MATHX/ gets mathx.lib, mathx.dep and MATHX.DHS) and restart DVX. Build programs that use it with CFLAGS=-I../mathx. @@ -349,6 +359,102 @@ make install puts greeter.app and greeter.hlp in APPS/LOCAL/GREETER/. You can al .link sdk.packaging Packaging and Installing +.topic sdk.firstcontrol +.title Your First BASIC Control +.toc 1 Your First BASIC Control +.index Your First BASIC Control +.index Control project +.index UserControl (SDK) + +.h1 Your First BASIC Control + +.h2 Create and Build + +.code + bin/sdk/newProject.sh control dial + cd dial + make +.endcode + +The build compiles the control project with bascomp into dial.wgt, a control file, and the help into dial.hlp. The project holds: + +.table + File Purpose + ---- ------- + dial.dbp The project: [Project] Type = Control, the [Controls] list (and any [Modules] the controls need) + dial.ctl The control: its Begin UserControl header (natural size, focus, toolbox picture and tooltip, default event) and its code, a class + icon24.bmp The toolbox picture (the header's ToolboxIcon) + dial.bhs The control's BASIC help, installed beside it and added to the DVX BASIC help + Makefile KIND = control +.endtable + +The control's Public properties, methods and events are what programs use; UserControl is its drawing surface. The DVX BASIC help (UserControls: Your Own Controls) describes the language side. + +.h2 Install + +make install puts dial.wgt and DIAL.BHS in WIDGETS/LOCAL/DIAL/. When DVX starts it reads the control file through the BASIC run-time library and registers its controls; BASIC programs put a Dial on a form (Begin Dial Dial1 in a .frm, or from the designer's toolbox) and C programs create one by name (see Using BASIC Controls from C). + +.h2 Next + +.link sdk.basiccontrols Using BASIC Controls from C +.link sdk.packaging Packaging and Installing + +.topic sdk.basiccontrols +.title Using BASIC Controls from C +.toc 1 Using BASIC Controls from C +.index Using BASIC Controls from C +.index BASIC control file +.index Resource-only widget +.index basCtlLoadFile + +.h1 Using BASIC Controls from C + +A control written in DVX BASIC is an ordinary widget type once it is registered: it has a widget class, an interface (WgtIfaceT) with its properties, methods and events, and a type name equal to its BASIC name. A C program uses it through the generic widget access, without a header of its own: + +.code + WidgetT *dial = wgtCreateByName("Dial", parent); + WgtValueT v = wgtValueInt(40); + WgtValueT ret; + + wgtSetProp(dial, "Level", &v); // Public Property Let Level + wgtGetProp(dial, "Level", &v); // Property Get: v.i == 40 + wgtCallMethod(dial, "Reset", NULL, 0, &ret); // Public Sub Reset (ret.type WGT_IFACE_NONE) + dial->onEvent = onDialEvent; // RaiseEvent in the control's code + + static void onDialEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + if (strcmp(name, "Changed") == 0 && argc > 0) { + ... // args[0]: the event's first argument + } + } +.endcode + +Property values follow the BASIC types: Integer, Long and Byte are WGT_IFACE_INT, Boolean WGT_IFACE_BOOL, Single WGT_IFACE_FLOAT, Double and Currency WGT_IFACE_DOUBLE, String, Date and Variant WGT_IFACE_STRING. A property declared As an Enum whose members are numbered 0, 1, 2 ... (each number once) is WGT_IFACE_ENUM with the member names in value order (WgtPropDescT.enumNames; wgtSetProp takes the number or a member name, wgtIfaceGetPropText gives the name); an Enum with other values is WGT_IFACE_INT. A method takes its arguments as they come (WGT_SIG_DYNAMIC) and returns its Function's value, or no value (WGT_IFACE_NONE) for a Sub. An event's numeric and Boolean arguments that onEvent changes go back to the control (ByRef parameters). Events the control names Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll go to the matching universal callback (onClick ...) instead; the dispatcher calls those for a BASIC control only when its code raises them (WCLASS_RAISES_EVENTS). The interface's events list each event with its parameters as BASIC writes them (WgtEventDescT.params). + +wgtCreateByName goes through the interface's dynamic create hook (WgtIfaceT.dyn); the class has no C API structure (wgtGetApi finds none), because one create function cannot tell which BASIC class to make. + +.h2 The Control File + +A control project builds NAME.wgt, a file that holds only resources: + +.table + Part Contents + ---- -------- + Marker DVX_RES_ONLY_MAGIC ("DVXRESONLY" and its NUL) at offset 0: no code + MODULE The compiled BASIC module (with DEBUG unless built for release) + CTL0, CTL1 ... Each control's Begin UserControl header text, in [Controls] order + icon24, name The first control's toolbox picture and tooltip; the second and later have "-2", "-3" ... suffixes, as for any widget file holding several widgets + author, version ... The project's metadata; helpfile its help file's name +.endtable + +The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name. + +.h2 The Bridge + +Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM. + +.link sdk.firstcontrol Your First BASIC Control +.link sdk.firstwidget Your First Widget + .topic sdk.debug .title Debugging: the Log and Crash Reports .toc 1 Debugging: the Log and Crash Reports diff --git a/src/libs/kpunch/libdvx/widgetClass.c b/src/libs/kpunch/libdvx/widgetClass.c index c3aff2a1..8cff78b8 100644 --- a/src/libs/kpunch/libdvx/widgetClass.c +++ b/src/libs/kpunch/libdvx/widgetClass.c @@ -71,8 +71,11 @@ typedef struct { char path[DVX_MAX_PATH]; int32_t pathIndex; // 1-based: 1 = first widget from this file, 2 = second, etc. int32_t owner; // registering app id (see sClassOwner) + int32_t typeId; // widget class bound by wgtIfaceSetType, IFACE_NO_TYPE if none } IfaceEntryT; +#define IFACE_NO_TYPE (-1) + typedef struct { char *key; IfaceEntryT value; @@ -86,6 +89,7 @@ static IfaceMapEntryT *sIfaceMap = NULL; // ============================================================ static int32_t currentOwner(void); +static int32_t ifaceIndex(const char *name); // The app id current at registration (0 = the shell / boot-time modules). @@ -94,6 +98,18 @@ static int32_t currentOwner(void) { } +// Index of name in the interface map, or -1. A lookup on a NULL map must +// not create it: stb_ds would allocate a header in borrowed-key mode and +// wgtRegisterIface's sh_new_strdup would then be skipped. +static int32_t ifaceIndex(const char *name) { + if (!name || !sIfaceMap) { + return -1; + } + + return (int32_t)shgeti(sIfaceMap, name); +} + + // Scan all registered interfaces for one whose basName matches // (case-insensitive). Returns the widget type name, or NULL. @@ -121,9 +137,7 @@ const char *wgtFindByBasName(const char *basName) { // the hash. const void *wgtGetApi(const char *name, uint32_t minSize) { - // A lookup on a NULL map must not create it: stb_ds would allocate a - // header in borrowed-key mode and wgtRegisterApi's sh_new_strdup - // would then be skipped. + // See ifaceIndex: never let a lookup create the map. if (!name || !sApiMap) { return NULL; } @@ -148,18 +162,28 @@ const void *wgtGetApi(const char *name, uint32_t minSize) { // Look up a widget interface descriptor by type name. const WgtIfaceT *wgtGetIface(const char *name) { - // See wgtGetApi: never let a lookup create the map. - if (!name || !sIfaceMap) { - return NULL; + int32_t idx = ifaceIndex(name); + + return (idx < 0) ? NULL : sIfaceMap[idx].value.iface; +} + + +// The interface bound to w's class by wgtIfaceSetType. A linear scan: +// there are a few dozen interfaces, and generic access by name is not a +// per-pixel path. + +const WgtIfaceT *wgtGetIfaceOf(const WidgetT *w, const char **outName) { + for (size_t i = 0; w && i < shlenu(sIfaceMap); i++) { + if (sIfaceMap[i].value.typeId == w->type) { + if (outName) { + *outName = sIfaceMap[i].key; + } + + return sIfaceMap[i].value.iface; + } } - int32_t idx = (int32_t)shgeti(sIfaceMap, name); - - if (idx < 0) { - return NULL; - } - - return sIfaceMap[idx].value.iface; + return NULL; } @@ -185,17 +209,37 @@ int32_t wgtIfaceCount(void) { } +// Case-insensitive lookup of an extra event descriptor by name. +const WgtEventDescT *wgtIfaceFindEvent(const WgtIfaceT *iface, const char *eventName) { + for (int32_t i = 0; iface && eventName && i < iface->eventCount; i++) { + if (strcasecmp(iface->events[i].name, eventName) == 0) { + return &iface->events[i]; + } + } + + return NULL; +} + + +// Case-insensitive lookup of a method descriptor by name (the first one). +const WgtMethodDescT *wgtIfaceFindMethod(const WgtIfaceT *iface, const char *methodName) { + for (int32_t i = 0; iface && methodName && i < iface->methodCount; i++) { + if (strcasecmp(iface->methods[i].name, methodName) == 0) { + return &iface->methods[i]; + } + } + + return NULL; +} + + // Case-insensitive lookup of a property descriptor by name. The single // source of truth for "does this widget interface define property X" -- // used by the form runtime (get/set) and the IDE (designer/properties) // so both layers resolve the Visible/Enabled override identically. // Returns NULL if iface is NULL or the property is not found. const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propName) { - if (!iface) { - return NULL; - } - - for (int32_t i = 0; i < iface->propCount; i++) { + for (int32_t i = 0; iface && propName && i < iface->propCount; i++) { if (strcasecmp(iface->props[i].name, propName) == 0) { return &iface->props[i]; } @@ -206,11 +250,7 @@ const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propNam const char *wgtIfaceGetPath(const char *name) { - if (!name || !sIfaceMap) { - return NULL; - } - - int32_t idx = (int32_t)shgeti(sIfaceMap, name); + int32_t idx = ifaceIndex(name); if (idx < 0) { return NULL; @@ -221,11 +261,7 @@ const char *wgtIfaceGetPath(const char *name) { int32_t wgtIfaceGetPathIndex(const char *name) { - if (!name || !sIfaceMap) { - return 1; - } - - int32_t idx = (int32_t)shgeti(sIfaceMap, name); + int32_t idx = ifaceIndex(name); if (idx < 0) { return 1; @@ -236,25 +272,34 @@ int32_t wgtIfaceGetPathIndex(const char *name) { void wgtIfaceSetPath(const char *name, const char *path) { - if (!name || !path || !sIfaceMap) { + int32_t idx = ifaceIndex(name); + + if (idx < 0 || !path) { return; } - int32_t idx = (int32_t)shgeti(sIfaceMap, name); + snprintf(sIfaceMap[idx].value.path, sizeof(sIfaceMap[idx].value.path), "%s", path); + + // Count how many ifaces already have this path to assign a 1-based index + int32_t count = 0; + + for (size_t i = 0; i < shlenu(sIfaceMap); i++) { + if (sIfaceMap[i].value.path[0] && strcmp(sIfaceMap[i].value.path, path) == 0) { + count++; + } + } + + sIfaceMap[idx].value.pathIndex = count; +} + + +// Bind a registered interface to the widget class its widgets carry, for +// wgtGetIfaceOf. Unknown names are ignored. +void wgtIfaceSetType(const char *name, int32_t typeId) { + int32_t idx = ifaceIndex(name); if (idx >= 0) { - snprintf(sIfaceMap[idx].value.path, sizeof(sIfaceMap[idx].value.path), "%s", path); - - // Count how many ifaces already have this path to assign a 1-based index - int32_t count = 0; - - for (size_t i = 0; i < shlenu(sIfaceMap); i++) { - if (sIfaceMap[i].value.path[0] && strcmp(sIfaceMap[i].value.path, path) == 0) { - count++; - } - } - - sIfaceMap[idx].value.pathIndex = count; + sIfaceMap[idx].value.typeId = typeId; } } @@ -326,8 +371,9 @@ void wgtRegisterIface(const char *name, const WgtIfaceT *iface) { IfaceEntryT entry; memset(&entry, 0, sizeof(entry)); - entry.iface = iface; - entry.owner = currentOwner(); + entry.iface = iface; + entry.owner = currentOwner(); + entry.typeId = IFACE_NO_TYPE; if (!sIfaceMap) { sh_new_strdup(sIfaceMap); @@ -337,6 +383,23 @@ void wgtRegisterIface(const char *name, const WgtIfaceT *iface) { } +// BASIC name first (the form runtime and designer hand these in), then a +// type name that has an API or an interface of its own. +const char *wgtResolveTypeName(const char *name) { + const char *wgtName = wgtFindByBasName(name); + + if (wgtName) { + return wgtName; + } + + if (wgtGetApi(name, sizeof(WgtApiPrefixT)) || wgtGetIface(name)) { + return name; + } + + return NULL; +} + + // Release every class, API and interface registered while appId was the // current app: all of them point into the app's module. The shell calls // this whenever it unloads an app, after the app's windows and widgets @@ -351,7 +414,7 @@ void wgtUnregisterAppClasses(int32_t appId) { for (int32_t i = 0; i < (int32_t)arrlen(widgetClassTable); i++) { if (sClassOwner[i] == appId) { - widgetClassTable[i] = NULL; + wgtUnregisterClass(i); } } @@ -382,7 +445,9 @@ void wgtUnregisterAppClasses(int32_t appId) { // Release a class slot taken by wgtRegisterClass. The caller has already // destroyed every widget of this class; the slot is cleared so creation // with the stale ID fails instead of dispatching into unloaded code, and -// the next wgtRegisterClass may reuse it. +// the next wgtRegisterClass may reuse it. An interface bound to the class +// (wgtIfaceSetType) is unbound, so a class that later takes the ID does +// not inherit it. void wgtUnregisterClass(int32_t classId) { if (classId < 0 || classId >= (int32_t)arrlen(widgetClassTable)) { @@ -390,4 +455,17 @@ void wgtUnregisterClass(int32_t classId) { } widgetClassTable[classId] = NULL; + + for (size_t i = 0; i < shlenu(sIfaceMap); i++) { + if (sIfaceMap[i].value.typeId == classId) { + sIfaceMap[i].value.typeId = IFACE_NO_TYPE; + } + } +} + + +void wgtUnregisterIface(const char *name) { + if (ifaceIndex(name) >= 0) { + (void)shdel(sIfaceMap, name); + } } diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c index 3d5b7953..f641cabc 100644 --- a/src/libs/kpunch/libdvx/widgetCore.c +++ b/src/libs/kpunch/libdvx/widgetCore.c @@ -379,12 +379,14 @@ WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, con // container's own chrome (divider, scrollbars, dead corner) lie outside // every child's clip and stay with the container. Focusable // NO_HIT_RECURSE widgets (ListView, TreeView, grids) are single controls -// and are returned as they are. +// and are returned as they are, and so is a class with its own +// WGT_METHOD_ON_MOUSE_EVENT (it receives its own mouse events even when it +// is not focusable). WidgetT *widgetCallbackTarget(WidgetT *root, int32_t x, int32_t y) { WidgetT *hit = widgetHitTest(root, x, y); - while (hit && hit->wclass && (hit->wclass->flags & (WCLASS_NO_HIT_RECURSE | WCLASS_FOCUSABLE)) == WCLASS_NO_HIT_RECURSE) { + while (hit && hit->wclass && (hit->wclass->flags & (WCLASS_NO_HIT_RECURSE | WCLASS_FOCUSABLE)) == WCLASS_NO_HIT_RECURSE && !wclsHas(hit, WGT_METHOD_ON_MOUSE_EVENT)) { WidgetT *inner = NULL; for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { @@ -673,9 +675,10 @@ WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) { // Fires the focus-transition callbacks for a change that has ALREADY been // recorded in sFocusedWidget: the class-level blur (commit/clamp edits) -// and app onBlur on prev, then the app onFocus on next. This is the one -// place the blur/focus sequence lives; every focus path (mouse, Tab, -// accelerator, wgtSetFocused, window blur, paint-time restore) ends here. +// and app onBlur on prev, then the class-level focus and app onFocus on +// next. This is the one place the blur/focus sequence lives; every focus +// path (mouse, Tab, accelerator, wgtSetFocused, window blur, paint-time +// restore) ends here. // // Returns false if a callback destroyed widgets (sWidgetGen changed) -- // prev and next may be dangling then and the caller must not touch them. @@ -697,8 +700,14 @@ bool widgetFireFocusChange(WidgetT *prev, WidgetT *next) { } } - if (next && next != prev && next->onFocus) { - next->onFocus(next); + // The class method first (the counterpart of wclsOnBlur), then the + // app callback unless the method destroyed widgets. + if (next && next != prev) { + wclsOnFocus(next); + + if (sWidgetGen == gen && next->onFocus) { + next->onFocus(next); + } } return sWidgetGen == gen; diff --git a/src/libs/kpunch/libdvx/widgetEvent.c b/src/libs/kpunch/libdvx/widgetEvent.c index c8dafa69..99b13635 100644 --- a/src/libs/kpunch/libdvx/widgetEvent.c +++ b/src/libs/kpunch/libdvx/widgetEvent.c @@ -48,6 +48,11 @@ // button again and re-open the popup in the same event. WidgetT *sClosedPopup = NULL; +// 1-based button ordinals the mouse callbacks receive for press / release +#define MOUSE_ORD_LEFT 1 +#define MOUSE_ORD_RIGHT 2 +#define MOUSE_ORD_MIDDLE 3 + // Mouse state for tracking button transitions and movement static int32_t sPrevMouseButtons = 0; static int32_t sPrevMouseX = -1; @@ -58,19 +63,22 @@ static int32_t sPrevMouseY = -1; // Prototypes // ============================================================ -static void dispatchButtonEdges(const WindowT *win, WidgetT *hit, uint32_t hitSerial, uint32_t *gen, int32_t buttons, int32_t prevButtons, int32_t relX, int32_t relY); -static bool widgetAliveSince(const WindowT *win, const WidgetT *w, uint32_t serial, uint32_t *gen); -static bool widgetInTree(const WidgetT *root, const WidgetT *w); -static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons); -static void widgetSetRootGeometry(const WindowT *win, WidgetT *root); +static void dispatchButtonEdges(const WindowT *win, WidgetT *hit, uint32_t hitSerial, uint32_t *gen, int32_t buttons, int32_t prevButtons, int32_t relX, int32_t relY); +static void fireMouse(const WindowT *win, WidgetT *w, uint32_t serial, uint32_t *gen, WgtMouseActionE action, int32_t button, int32_t x, int32_t y); +static int32_t mouseShift(void); +static bool widgetAliveSince(const WindowT *win, const WidgetT *w, uint32_t serial, uint32_t *gen); +static bool widgetInTree(const WidgetT *root, const WidgetT *w); +static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons); +static void widgetSetRootGeometry(const WindowT *win, WidgetT *root); // Dispatch mouse button press/release edges to the widget under the cursor. -// Each transition between prevButtons and buttons fires the widget's -// onMouseDown (press edge) or onMouseUp (release edge) callback with the -// 1-based button ordinal (1 left, 2 right, 3 middle). Any callback may -// destroy widgets, so every dereference is gated on hit still being alive -// (widgetAliveSince), not merely on nothing having been destroyed. +// Each transition between prevButtons and buttons is one fireMouse: the +// press edge as WgtMouseDownE / onMouseDown, the release edge as +// WgtMouseUpE / onMouseUp, with the 1-based button ordinal (1 left, 2 +// right, 3 middle). Any callback may destroy widgets, so every +// dereference is gated on hit still being alive (widgetAliveSince), not +// merely on nothing having been destroyed. // // The left release edge is delivered first so both callers keep their // original per-path ordering: on the left-release path (buttons has @@ -83,34 +91,75 @@ static void widgetSetRootGeometry(const WindowT *win, WidgetT *root); static void dispatchButtonEdges(const WindowT *win, WidgetT *hit, uint32_t hitSerial, uint32_t *gen, int32_t buttons, int32_t prevButtons, int32_t relX, int32_t relY) { // Left release edge first (leftUp-first order on the release path; // unreachable on the press path where MOUSE_LEFT is set) - if (!(buttons & MOUSE_LEFT) && (prevButtons & MOUSE_LEFT) && widgetAliveSince(win, hit, hitSerial, gen) && hit->onMouseUp) { - hit->onMouseUp(hit, 1, relX, relY); + if (!(buttons & MOUSE_LEFT) && (prevButtons & MOUSE_LEFT)) { + fireMouse(win, hit, hitSerial, gen, WgtMouseUpE, MOUSE_ORD_LEFT, relX, relY); } // Press edges (left, right, middle) - if ((buttons & MOUSE_LEFT) && !(prevButtons & MOUSE_LEFT) && widgetAliveSince(win, hit, hitSerial, gen) && hit->onMouseDown) { - hit->onMouseDown(hit, 1, relX, relY); + if ((buttons & MOUSE_LEFT) && !(prevButtons & MOUSE_LEFT)) { + fireMouse(win, hit, hitSerial, gen, WgtMouseDownE, MOUSE_ORD_LEFT, relX, relY); } - if ((buttons & MOUSE_RIGHT) && !(prevButtons & MOUSE_RIGHT) && widgetAliveSince(win, hit, hitSerial, gen) && hit->onMouseDown) { - hit->onMouseDown(hit, 2, relX, relY); + if ((buttons & MOUSE_RIGHT) && !(prevButtons & MOUSE_RIGHT)) { + fireMouse(win, hit, hitSerial, gen, WgtMouseDownE, MOUSE_ORD_RIGHT, relX, relY); } - if ((buttons & MOUSE_MIDDLE) && !(prevButtons & MOUSE_MIDDLE) && widgetAliveSince(win, hit, hitSerial, gen) && hit->onMouseDown) { - hit->onMouseDown(hit, 3, relX, relY); + if ((buttons & MOUSE_MIDDLE) && !(prevButtons & MOUSE_MIDDLE)) { + fireMouse(win, hit, hitSerial, gen, WgtMouseDownE, MOUSE_ORD_MIDDLE, relX, relY); } // Remaining release edges (right, middle) - if (!(buttons & MOUSE_RIGHT) && (prevButtons & MOUSE_RIGHT) && widgetAliveSince(win, hit, hitSerial, gen) && hit->onMouseUp) { - hit->onMouseUp(hit, 2, relX, relY); + if (!(buttons & MOUSE_RIGHT) && (prevButtons & MOUSE_RIGHT)) { + fireMouse(win, hit, hitSerial, gen, WgtMouseUpE, MOUSE_ORD_RIGHT, relX, relY); } - if (!(buttons & MOUSE_MIDDLE) && (prevButtons & MOUSE_MIDDLE) && widgetAliveSince(win, hit, hitSerial, gen) && hit->onMouseUp) { - hit->onMouseUp(hit, 3, relX, relY); + if (!(buttons & MOUSE_MIDDLE) && (prevButtons & MOUSE_MIDDLE)) { + fireMouse(win, hit, hitSerial, gen, WgtMouseUpE, MOUSE_ORD_MIDDLE, relX, relY); } } +// One mouse event for w: its class's WGT_METHOD_ON_MOUSE_EVENT first, +// then the matching universal callback (onMouseDown / onMouseUp / +// onMouseMove), each only while w is still alive. Classes without the +// method see exactly the universal callbacks as before. +static void fireMouse(const WindowT *win, WidgetT *w, uint32_t serial, uint32_t *gen, WgtMouseActionE action, int32_t button, int32_t x, int32_t y) { + if (widgetAliveSince(win, w, serial, gen) && wclsHas(w, WGT_METHOD_ON_MOUSE_EVENT)) { + wclsOnMouseEvent(w, action, button, x, y, mouseShift()); + } + + if (!widgetAliveSince(win, w, serial, gen) || wclsRaisesEvents(w)) { + return; + } + + switch (action) { + case WgtMouseDownE: + if (w->onMouseDown) { + w->onMouseDown(w, button, x, y); + } + break; + + case WgtMouseUpE: + if (w->onMouseUp) { + w->onMouseUp(w, button, x, y); + } + break; + + case WgtMouseMoveE: + if (w->onMouseMove) { + w->onMouseMove(w, button, x, y); + } + break; + } +} + + +// The Shift / Ctrl / Alt state handed to WGT_METHOD_ON_MOUSE_EVENT. +static int32_t mouseShift(void) { + return platformKeyboardGetModifiers() & KEY_MOD_ALL; +} + + // True when w is still safe to dereference after user callbacks ran. The // fast path is an unchanged destroy generation. When something WAS // destroyed it may have been an unrelated widget (a MouseUp handler @@ -389,12 +438,15 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { wclsOnKey(focus, key, mod); - // Fire user callbacks after the widget's internal handler - if (sWidgetGen == gen && key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && focus->onKeyPress) { + // Fire user callbacks after the widget's internal handler (a class + // that raises its own events fires them itself) + bool own = (sWidgetGen == gen) && wclsRaisesEvents(focus); + + if (sWidgetGen == gen && !own && key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && focus->onKeyPress) { focus->onKeyPress(focus, key); } - if (sWidgetGen == gen && focus->onKeyDown) { + if (sWidgetGen == gen && !own && focus->onKeyDown) { focus->onKeyDown(focus, key, mod); } @@ -419,20 +471,26 @@ void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) { return; } - if (focus->onKeyUp) { - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = (AppContextT *)root->userData; - if (!ctx) { - return; - } - - int32_t prevAppId = ctx->currentAppId; - ctx->currentAppId = win->appId; - - focus->onKeyUp(focus, scancode, mod); - - ctx->currentAppId = prevAppId; + if (!ctx || (!focus->onKeyUp && !wclsHas(focus, WGT_METHOD_ON_KEY_UP))) { + return; } + + int32_t prevAppId = ctx->currentAppId; + ctx->currentAppId = win->appId; + + // The class method first, then the app callback unless the method + // destroyed widgets. + uint32_t gen = sWidgetGen; + + wclsOnKeyUp(focus, scancode, mod); + + if (sWidgetGen == gen && focus->onKeyUp && !wclsRaisesEvents(focus)) { + focus->onKeyUp(focus, scancode, mod); + } + + ctx->currentAppId = prevAppId; } @@ -502,8 +560,16 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y // the drag widget itself dies, which makes sDragWidget == drag the // exact liveness test (a MouseUp that removes some OTHER control // must not skip the drag end and leave a button stuck down). - if (widgetIsEnabledChain(drag) && drag->onMouseUp) { - drag->onMouseUp(drag, 1, x - drag->x - drag->contentOffX, y - drag->y - drag->contentOffY); + // A class with WGT_METHOD_ON_MOUSE_EVENT sees the release first. + if (widgetIsEnabledChain(drag)) { + int32_t relX = x - drag->x - drag->contentOffX; + int32_t relY = y - drag->y - drag->contentOffY; + + wclsOnMouseEvent(drag, WgtMouseUpE, MOUSE_ORD_LEFT, relX, relY, mouseShift()); + + if (sDragWidget == drag && drag->onMouseUp && !wclsRaisesEvents(drag)) { + drag->onMouseUp(drag, MOUSE_ORD_LEFT, relX, relY); + } } // The handler may also have destroyed the root; re-read it. @@ -518,8 +584,19 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y return; } - // Handle drag move + // Handle drag move. A capturing class with WGT_METHOD_ON_MOUSE_EVENT + // gets the move (with the held buttons) before its drag update. if (sDragWidget && (buttons & MOUSE_LEFT)) { + WidgetT *drag = sDragWidget; + + if ((x != sPrevMouseX || y != sPrevMouseY) && wclsHas(drag, WGT_METHOD_ON_MOUSE_EVENT)) { + wclsOnMouseEvent(drag, WgtMouseMoveE, buttons, x - drag->x - drag->contentOffX, y - drag->y - drag->contentOffY, mouseShift()); + + if (sDragWidget != drag) { + return; + } + } + wclsOnDragUpdate(sDragWidget, root, x, y); // quickRepaint fast path for text drag (dirty rect instead of full repaint) @@ -630,8 +707,8 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y dispatchButtonEdges(win, upHit, upSerial, &upGen, buttons, sPrevMouseButtons, relX, relY); // MouseMove: plain hover motion with no left button held - if ((x != sPrevMouseX || y != sPrevMouseY) && widgetAliveSince(win, upHit, upSerial, &upGen) && upHit->onMouseMove) { - upHit->onMouseMove(upHit, buttons, relX, relY); + if (x != sPrevMouseX || y != sPrevMouseY) { + fireMouse(win, upHit, upSerial, &upGen, WgtMouseMoveE, buttons, relX, relY); } } @@ -712,7 +789,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y // are excluded from onClick because they use press-release semantics // (onClick fires on button-up, not button-down) and already handle it // in the release handler above. They still get onDblClick here. - if (targetEnabled && widgetAliveSince(win, target, targetSerial, &targetGen)) { + if (targetEnabled && widgetAliveSince(win, target, targetSerial, &targetGen) && !wclsRaisesEvents(target)) { int32_t clicks = multiClickDetect(vx, vy); bool isBtn = (target->wclass && (target->wclass->flags & WCLASS_PRESS_RELEASE)); @@ -736,8 +813,8 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y dispatchButtonEdges(win, target, targetSerial, &targetGen, buttons, sPrevMouseButtons, relX, relY); // MouseMove: position changed - if ((vx != sPrevMouseX || vy != sPrevMouseY) && widgetAliveSince(win, target, targetSerial, &targetGen) && target->onMouseMove) { - target->onMouseMove(target, buttons, relX, relY); + if (vx != sPrevMouseX || vy != sPrevMouseY) { + fireMouse(win, target, targetSerial, &targetGen, WgtMouseMoveE, buttons, relX, relY); } } diff --git a/src/libs/kpunch/libdvx/widgetIface.c b/src/libs/kpunch/libdvx/widgetIface.c new file mode 100644 index 00000000..c695f586 --- /dev/null +++ b/src/libs/kpunch/libdvx/widgetIface.c @@ -0,0 +1,911 @@ +// 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. + + +// widgetIface.c -- Generic widget access and custom events +// +// The one implementation of "create / read / write / call by name" over +// the interface descriptors (WgtIfaceT) every widget registers: the +// WgtCreateSigE switch, the WGT_SIG_* marshalling switch, the property +// type switch, their .frm text forms, the WgtValueT coercions, and the +// dynamic-interface (WgtDynIfaceT) routing. The form runtime, the IDE +// designer and C programs all come through here. Also wgtFireEvent, the +// custom-event entry that maps the universal event names onto WidgetT's +// callbacks and hands every other name to onEvent. + +#include "dvxWgtP.h" + +#include +#include + +// Short names for the signature table below. +#define SIG_NONE WGT_IFACE_NONE +#define SIG_INT WGT_IFACE_INT +#define SIG_BOOL WGT_IFACE_BOOL +#define SIG_STR WGT_IFACE_STRING + +// Text of the BOOL values as wgtValueToText and the .frm write them. +#define WGT_TEXT_TRUE "True" +#define WGT_TEXT_FALSE "False" +#define WGT_TEXT_TRUE_NUM "-1" +#define WGT_TEXT_FALSE_NUM "0" + +// Decimal parse base for STRING -> INT coercion. +#define WGT_DECIMAL 10 + +// A DOUBLE value as text: as many digits as BASIC prints of a Double. +#define WGT_DOUBLE_TEXT_FMT "%.15g" + +// A mouse event's arguments: (Button, X, Y), or VB's (Button, Shift, X, Y) +// whose Shift the mouse callbacks do not take. +#define WGT_MOUSE_ARGC_SHIFT 4 +#define WGT_MOUSE_X_ARG 1 +#define WGT_MOUSE_X_ARG_SHIFT 2 + +// One argument converted to its signature type; buf holds the text of a +// number converted to a STRING argument. +typedef struct { + WgtValueT v; + char buf[WGT_VALUE_TEXT_MAX]; +} ArgT; + +// The universal events wgtFireEvent maps onto WidgetT callbacks. +typedef enum { + UniClickE, + UniDblClickE, + UniChangeE, + UniGotFocusE, + UniLostFocusE, + UniKeyPressE, + UniKeyDownE, + UniKeyUpE, + UniMouseDownE, + UniMouseUpE, + UniMouseMoveE, + UniScrollE, + UniCountE +} UniEventE; + +static const char *sUniNames[UniCountE] = { + [UniClickE] = "Click", + [UniDblClickE] = "DblClick", + [UniChangeE] = "Change", + [UniGotFocusE] = "GotFocus", + [UniLostFocusE] = "LostFocus", + [UniKeyPressE] = "KeyPress", + [UniKeyDownE] = "KeyDown", + [UniKeyUpE] = "KeyUp", + [UniMouseDownE] = "MouseDown", + [UniMouseUpE] = "MouseUp", + [UniMouseMoveE] = "MouseMove", + [UniScrollE] = "Scroll" +}; + +// What each WGT_SIG_* passes and returns (wgtSigInfo). WGT_SIG_DYNAMIC +// has no entry: its arguments are passed as they come. +static const WgtSigInfoT sSigInfo[] = { + [WGT_SIG_VOID] = { 0, SIG_NONE, { 0 } }, + [WGT_SIG_INT] = { 1, SIG_NONE, { SIG_INT } }, + [WGT_SIG_BOOL] = { 1, SIG_NONE, { SIG_BOOL } }, + [WGT_SIG_STR] = { 1, SIG_NONE, { SIG_STR } }, + [WGT_SIG_INT_INT] = { 2, SIG_NONE, { SIG_INT, SIG_INT } }, + [WGT_SIG_INT_BOOL] = { 2, SIG_NONE, { SIG_INT, SIG_BOOL } }, + [WGT_SIG_RET_INT] = { 0, SIG_INT, { 0 } }, + [WGT_SIG_RET_BOOL] = { 0, SIG_BOOL, { 0 } }, + [WGT_SIG_RET_BOOL_INT] = { 1, SIG_BOOL, { SIG_INT } }, + [WGT_SIG_INT3] = { 3, SIG_NONE, { SIG_INT, SIG_INT, SIG_INT } }, + [WGT_SIG_INT4] = { 4, SIG_NONE, { SIG_INT, SIG_INT, SIG_INT, SIG_INT } }, + [WGT_SIG_RET_INT_INT_INT] = { 2, SIG_INT, { SIG_INT, SIG_INT } }, + [WGT_SIG_INT_INT_STR] = { 3, SIG_NONE, { SIG_INT, SIG_INT, SIG_STR } }, + [WGT_SIG_INT5] = { 5, SIG_NONE, { SIG_INT, SIG_INT, SIG_INT, SIG_INT, SIG_INT } }, + [WGT_SIG_RET_STR_INT] = { 1, SIG_STR, { SIG_INT } }, + [WGT_SIG_RET_STR_INT_INT] = { 2, SIG_STR, { SIG_INT, SIG_INT } }, + [WGT_SIG_STR_BOOL_BOOL] = { 3, SIG_BOOL, { SIG_STR, SIG_BOOL, SIG_BOOL } }, + [WGT_SIG_STR_STR_BOOL] = { 3, SIG_INT, { SIG_STR, SIG_STR, SIG_BOOL } }, + [WGT_SIG_RET_STR] = { 0, SIG_STR, { 0 } }, + [WGT_SIG_STR_INT] = { 2, SIG_NONE, { SIG_STR, SIG_INT } }, + [WGT_SIG_INT_STR] = { 2, SIG_NONE, { SIG_INT, SIG_STR } }, + [WGT_SIG_STR_STR] = { 2, SIG_NONE, { SIG_STR, SIG_STR } }, + [WGT_SIG_STR_BOOL] = { 2, SIG_NONE, { SIG_STR, SIG_BOOL } }, + [WGT_SIG_RET_INT_STR] = { 1, SIG_INT, { SIG_STR } }, + [WGT_SIG_STR_STR_BOOL_BOOL] = { 4, SIG_BOOL, { SIG_STR, SIG_STR, SIG_BOOL, SIG_BOOL } }, + [WGT_SIG_RET_INT_INT] = { 1, SIG_INT, { SIG_INT } } +}; + +// Text of a non-STRING value coerced to STRING for a caller (a dynamic +// hook's result of another type): valid until the next generic call, the +// WgtValueT lifetime rule. +static char sValueText[WGT_VALUE_TEXT_MAX]; + + +// ============================================================ +// Prototypes +// ============================================================ + +static int32_t argInt(const WgtValueT *args, int32_t argc, int32_t idx); +static void callStatic(WidgetT *w, const WgtMethodDescT *m, const ArgT *a, WgtValueT *ret); +static bool coerceArgs(const WgtSigInfoT *info, const WgtValueT *args, int32_t argc, ArgT *out); +static void coerceValue(WgtValueT *v, uint8_t type, char *buf, int32_t bufSize); +static int32_t enumIndex(const WgtPropDescT *p, const char *name); +static int32_t universalIndex(const char *name); +static WgtValueT zeroOf(uint8_t type); + + +// args[idx] as an integer, 0 when it was not given. +static int32_t argInt(const WgtValueT *args, int32_t argc, int32_t idx) { + return (args && idx < argc) ? wgtValueToInt(&args[idx]) : 0; +} + + +// The static call: a[] already holds the signature's argument types and +// ret the zero value of its result type. +static void callStatic(WidgetT *w, const WgtMethodDescT *m, const ArgT *a, WgtValueT *ret) { + switch (m->sig) { + case WGT_SIG_VOID: + ((void (*)(WidgetT *))m->fn)(w); + break; + + case WGT_SIG_INT: + ((void (*)(WidgetT *, int32_t))m->fn)(w, a[0].v.i); + break; + + case WGT_SIG_BOOL: + ((void (*)(WidgetT *, bool))m->fn)(w, a[0].v.i != 0); + break; + + case WGT_SIG_STR: + ((void (*)(WidgetT *, const char *))m->fn)(w, a[0].v.s); + break; + + case WGT_SIG_INT_INT: + ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, a[0].v.i, a[1].v.i); + break; + + case WGT_SIG_INT_BOOL: + ((void (*)(WidgetT *, int32_t, bool))m->fn)(w, a[0].v.i, a[1].v.i != 0); + break; + + case WGT_SIG_RET_INT: + ret->i = ((int32_t (*)(const WidgetT *))m->fn)(w); + break; + + case WGT_SIG_RET_BOOL: + ret->i = ((bool (*)(const WidgetT *))m->fn)(w) ? 1 : 0; + break; + + case WGT_SIG_RET_BOOL_INT: + ret->i = ((bool (*)(const WidgetT *, int32_t))m->fn)(w, a[0].v.i) ? 1 : 0; + break; + + case WGT_SIG_INT3: + ((void (*)(WidgetT *, int32_t, int32_t, int32_t))m->fn)(w, a[0].v.i, a[1].v.i, a[2].v.i); + break; + + case WGT_SIG_INT4: + ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t))m->fn)(w, a[0].v.i, a[1].v.i, a[2].v.i, a[3].v.i); + break; + + case WGT_SIG_RET_INT_INT_INT: + ret->i = ((int32_t (*)(const WidgetT *, int32_t, int32_t))m->fn)(w, a[0].v.i, a[1].v.i); + break; + + case WGT_SIG_INT_INT_STR: + ((void (*)(WidgetT *, int32_t, int32_t, const char *))m->fn)(w, a[0].v.i, a[1].v.i, a[2].v.s); + break; + + case WGT_SIG_INT5: + ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t))m->fn)(w, a[0].v.i, a[1].v.i, a[2].v.i, a[3].v.i, a[4].v.i); + break; + + case WGT_SIG_RET_STR_INT: + ret->s = ((const char *(*)(const WidgetT *, int32_t))m->fn)(w, a[0].v.i); + break; + + case WGT_SIG_RET_STR_INT_INT: + ret->s = ((const char *(*)(const WidgetT *, int32_t, int32_t))m->fn)(w, a[0].v.i, a[1].v.i); + break; + + case WGT_SIG_STR_BOOL_BOOL: + ret->i = ((bool (*)(WidgetT *, const char *, bool, bool))m->fn)(w, a[0].v.s, a[1].v.i != 0, a[2].v.i != 0) ? 1 : 0; + break; + + case WGT_SIG_STR_STR_BOOL: + ret->i = ((int32_t (*)(WidgetT *, const char *, const char *, bool))m->fn)(w, a[0].v.s, a[1].v.s, a[2].v.i != 0); + break; + + case WGT_SIG_RET_STR: + ret->s = ((const char *(*)(const WidgetT *))m->fn)(w); + break; + + case WGT_SIG_STR_INT: + ((void (*)(WidgetT *, const char *, int32_t))m->fn)(w, a[0].v.s, a[1].v.i); + break; + + case WGT_SIG_INT_STR: + ((void (*)(WidgetT *, int32_t, const char *))m->fn)(w, a[0].v.i, a[1].v.s); + break; + + case WGT_SIG_STR_STR: + ((void (*)(WidgetT *, const char *, const char *))m->fn)(w, a[0].v.s, a[1].v.s); + break; + + case WGT_SIG_STR_BOOL: + ((void (*)(WidgetT *, const char *, bool))m->fn)(w, a[0].v.s, a[1].v.i != 0); + break; + + case WGT_SIG_RET_INT_STR: + ret->i = ((int32_t (*)(WidgetT *, const char *))m->fn)(w, a[0].v.s); + break; + + case WGT_SIG_STR_STR_BOOL_BOOL: + ret->i = ((bool (*)(WidgetT *, const char *, const char *, bool, bool))m->fn)(w, a[0].v.s, a[1].v.s, a[2].v.i != 0, a[3].v.i != 0) ? 1 : 0; + break; + + case WGT_SIG_RET_INT_INT: + ret->i = ((int32_t (*)(const WidgetT *, int32_t))m->fn)(w, a[0].v.i); + break; + } + + // A string getter may hand back NULL; a STRING value never is. + if (ret->type == WGT_IFACE_STRING && !ret->s) { + ret->s = ""; + } +} + + +// Convert the first info->argc arguments to the signature's types; false +// when fewer were given. +static bool coerceArgs(const WgtSigInfoT *info, const WgtValueT *args, int32_t argc, ArgT *out) { + if (argc < info->argc || (info->argc > 0 && !args)) { + return false; + } + + for (int32_t k = 0; k < info->argc; k++) { + out[k].v = args[k]; + coerceValue(&out[k].v, info->argTypes[k], out[k].buf, (int32_t)sizeof(out[k].buf)); + } + + return true; +} + + +// Convert *v to type in place; a number turned into text is written to buf. +static void coerceValue(WgtValueT *v, uint8_t type, char *buf, int32_t bufSize) { + WgtValueT out = zeroOf(type); + + switch (type) { + case WGT_IFACE_STRING: + out.s = wgtValueToText(v, buf, bufSize); + break; + + case WGT_IFACE_INT: + case WGT_IFACE_ENUM: + out.i = wgtValueToInt(v); + break; + + case WGT_IFACE_BOOL: + out.i = wgtValueToBool(v) ? 1 : 0; + break; + + case WGT_IFACE_FLOAT: + case WGT_IFACE_DOUBLE: + out.f = wgtValueToFloat(v); + break; + + default: + break; + } + + *v = out; +} + + +// Index of an ENUM property's value name (case-insensitive), or -1. +static int32_t enumIndex(const WgtPropDescT *p, const char *name) { + for (int32_t en = 0; p->enumNames && name && p->enumNames[en]; en++) { + if (strcasecmp(p->enumNames[en], name) == 0) { + return en; + } + } + + return -1; +} + + +// UniEventE of a universal event name, UniCountE for any other name. +static int32_t universalIndex(const char *name) { + for (int32_t i = 0; i < UniCountE; i++) { + if (strcasecmp(sUniNames[i], name) == 0) { + return i; + } + } + + return UniCountE; +} + + +bool wgtCallMethod(WidgetT *w, const char *methodName, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + const WgtIfaceT *iface = wgtGetIfaceOf(w, NULL); + const WgtMethodDescT *m = wgtIfaceFindMethod(iface, methodName); + + if (!m) { + if (ret) { + *ret = zeroOf(WGT_IFACE_NONE); + } + + return false; + } + + return wgtIfaceCallMethod(iface, w, m, args, argc, ret); +} + + +WidgetT *wgtCreateByName(const char *name, WidgetT *parent) { + const char *typeName = wgtResolveTypeName(name); + + if (!typeName) { + return NULL; + } + + const WgtIfaceT *iface = wgtGetIface(typeName); + + if (iface && iface->dyn) { + return iface->dyn->create ? iface->dyn->create(parent, iface) : NULL; + } + + const WgtApiPrefixT *api = (const WgtApiPrefixT *)wgtGetApi(typeName, sizeof(WgtApiPrefixT)); + + if (!api) { + return NULL; + } + + const int32_t *args = iface ? iface->createArgs : NULL; + WgtApiFnT create = api->create; + + switch (iface ? iface->createSig : WGT_CREATE_PARENT) { + case WGT_CREATE_PARENT_TEXT: + return ((WidgetT *(*)(WidgetT *, const char *))create)(parent, ""); + + case WGT_CREATE_PARENT_INT: + return ((WidgetT *(*)(WidgetT *, int32_t))create)(parent, args[0]); + + case WGT_CREATE_PARENT_INT_INT: + return ((WidgetT *(*)(WidgetT *, int32_t, int32_t))create)(parent, args[0], args[1]); + + case WGT_CREATE_PARENT_INT_INT_INT: + return ((WidgetT *(*)(WidgetT *, int32_t, int32_t, int32_t))create)(parent, args[0], args[1], args[2]); + + case WGT_CREATE_PARENT_INT_BOOL: + return ((WidgetT *(*)(WidgetT *, int32_t, bool))create)(parent, args[0], args[1] != 0); + + case WGT_CREATE_PARENT_BOOL: + return ((WidgetT *(*)(WidgetT *, bool))create)(parent, args[0] != 0); + + case WGT_CREATE_PARENT_DATA: + // An empty picture; the content comes later through properties + return ((WidgetT *(*)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t))create)(parent, NULL, 0, 0, 0); + + default: + return ((WidgetT *(*)(WidgetT *))create)(parent); + } +} + + +// The universal names go to their callback only, every other name to +// onEvent only, so a host that sets both sees each event once. +bool wgtFireEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + if (!w || !name) { + return true; + } + + uint32_t gen = sWidgetGen; + int32_t a0 = argInt(args, argc, 0); + int32_t a1 = argInt(args, argc, 1); + int32_t xArg = (argc >= WGT_MOUSE_ARGC_SHIFT) ? WGT_MOUSE_X_ARG_SHIFT : WGT_MOUSE_X_ARG; + int32_t mouseX = argInt(args, argc, xArg); + int32_t mouseY = argInt(args, argc, xArg + 1); + + switch (universalIndex(name)) { + case UniClickE: + if (w->onClick) { + w->onClick(w); + } + break; + + case UniDblClickE: + if (w->onDblClick) { + w->onDblClick(w); + } + break; + + case UniChangeE: + if (w->onChange) { + w->onChange(w); + } + break; + + case UniGotFocusE: + if (w->onFocus) { + w->onFocus(w); + } + break; + + case UniLostFocusE: + if (w->onBlur) { + w->onBlur(w); + } + break; + + case UniKeyPressE: + if (w->onKeyPress) { + w->onKeyPress(w, a0); + } + break; + + case UniKeyDownE: + if (w->onKeyDown) { + w->onKeyDown(w, a0, a1); + } + break; + + case UniKeyUpE: + if (w->onKeyUp) { + w->onKeyUp(w, a0, a1); + } + break; + + case UniMouseDownE: + if (w->onMouseDown) { + w->onMouseDown(w, a0, mouseX, mouseY); + } + break; + + case UniMouseUpE: + if (w->onMouseUp) { + w->onMouseUp(w, a0, mouseX, mouseY); + } + break; + + case UniMouseMoveE: + if (w->onMouseMove) { + w->onMouseMove(w, a0, mouseX, mouseY); + } + break; + + case UniScrollE: + if (w->onScroll) { + w->onScroll(w, a0); + } + break; + + default: + if (w->onEvent) { + w->onEvent(w, name, args, argc); + } + break; + } + + return sWidgetGen == gen; +} + + +bool wgtGetProp(WidgetT *w, const char *propName, WgtValueT *out) { + const WgtIfaceT *iface = wgtGetIfaceOf(w, NULL); + const WgtPropDescT *p = wgtIfaceFindProp(iface, propName); + + if (!out) { + return false; + } + + if (!p) { + *out = zeroOf(WGT_IFACE_NONE); + return false; + } + + return wgtIfaceGetProp(iface, w, p, out); +} + + +bool wgtIfaceCallMethod(const WgtIfaceT *iface, WidgetT *w, const WgtMethodDescT *m, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + WgtValueT scratch; + WgtValueT *r = ret ? ret : &scratch; + const WgtSigInfoT *info = m ? wgtSigInfo(m->sig) : NULL; + ArgT a[WGT_SIG_MAX_ARGS]; + + // coerceArgs fills the signature's arguments; the rest stay defined + memset(a, 0, sizeof(a)); + *r = zeroOf(info ? info->retType : WGT_IFACE_NONE); + + if (!w || !m || (info && !coerceArgs(info, args, argc, a))) { + return false; + } + + if (iface && iface->dyn) { + if (!iface->dyn->callMethod) { + return false; + } + + // A fixed signature on a dynamic interface: its converted arguments + // in, its result type out. + WgtValueT conv[WGT_SIG_MAX_ARGS]; + const WgtValueT *pass = args; + int32_t passN = argc; + + if (info) { + for (int32_t k = 0; k < info->argc; k++) { + conv[k] = a[k].v; + } + + pass = conv; + passN = info->argc; + } + + bool ok = iface->dyn->callMethod(w, (int32_t)(m - iface->methods), pass, passN, r); + + if (info) { + coerceValue(r, info->retType, sValueText, (int32_t)sizeof(sValueText)); + } else if (r->type == WGT_IFACE_STRING && !r->s) { + r->s = ""; + } + + return ok; + } + + if (m->sig == WGT_SIG_DYNAMIC) { + return ((WgtDynMethodFnT)m->fn)(w, args, argc, r); + } + + if (!info) { + return false; + } + + callStatic(w, m, a, r); + return true; +} + + +bool wgtIfaceGetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, WgtValueT *out) { + if (!out) { + return false; + } + + *out = zeroOf(p ? p->type : WGT_IFACE_NONE); + + if (!w || !p || !p->getFn) { + return false; + } + + if (iface && iface->dyn) { + bool ok = iface->dyn->getProp && iface->dyn->getProp(w, (int32_t)(p - iface->props), out); + + coerceValue(out, p->type, sValueText, (int32_t)sizeof(sValueText)); + return ok; + } + + switch (p->type) { + case WGT_IFACE_STRING: { + const char *s = ((const char *(*)(const WidgetT *))p->getFn)(w); + + out->s = s ? s : ""; + break; + } + + case WGT_IFACE_INT: + case WGT_IFACE_ENUM: + out->i = ((int32_t (*)(const WidgetT *))p->getFn)(w); + break; + + case WGT_IFACE_BOOL: + out->i = ((bool (*)(const WidgetT *))p->getFn)(w) ? 1 : 0; + break; + + case WGT_IFACE_FLOAT: + out->f = ((float (*)(const WidgetT *))p->getFn)(w); + break; + + case WGT_IFACE_DOUBLE: + out->f = ((double (*)(const WidgetT *))p->getFn)(w); + break; + + default: + return false; + } + + return true; +} + + +bool wgtIfaceGetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize) { + WgtValueT v; + char buf[WGT_VALUE_TEXT_MAX]; + + if (!out || outSize <= 0 || !wgtIfaceGetProp(iface, w, p, &v)) { + return false; + } + + if (p->type == WGT_IFACE_ENUM) { + const char *name = NULL; + + for (int32_t en = 0; p->enumNames && p->enumNames[en] && !name; en++) { + if (en == v.i) { + name = p->enumNames[en]; + } + } + + if (!name) { + return false; + } + + snprintf(out, outSize, "%s", name); + return true; + } + + snprintf(out, outSize, "%s", wgtValueToText(&v, buf, (int32_t)sizeof(buf))); + return true; +} + + +bool wgtIfaceSetProp(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const WgtValueT *value) { + if (!w || !p || !value || !p->setFn) { + return false; + } + + WgtValueT v = *value; + char buf[WGT_VALUE_TEXT_MAX]; + + // An enumerated property takes a value name as well as its index + if (p->type == WGT_IFACE_ENUM && v.type == WGT_IFACE_STRING) { + int32_t idx = enumIndex(p, v.s); + + if (idx < 0) { + return false; + } + + v = wgtValueInt(idx); + } + + coerceValue(&v, p->type, buf, (int32_t)sizeof(buf)); + + if (iface && iface->dyn) { + return iface->dyn->setProp && iface->dyn->setProp(w, (int32_t)(p - iface->props), &v); + } + + switch (p->type) { + case WGT_IFACE_STRING: + ((void (*)(WidgetT *, const char *))p->setFn)(w, v.s); + break; + + case WGT_IFACE_INT: + case WGT_IFACE_ENUM: + ((void (*)(WidgetT *, int32_t))p->setFn)(w, v.i); + break; + + case WGT_IFACE_BOOL: + ((void (*)(WidgetT *, bool))p->setFn)(w, v.i != 0); + break; + + case WGT_IFACE_FLOAT: + ((void (*)(WidgetT *, float))p->setFn)(w, (float)v.f); + break; + + case WGT_IFACE_DOUBLE: + ((void (*)(WidgetT *, double))p->setFn)(w, v.f); + break; + + default: + return false; + } + + return true; +} + + +bool wgtIfaceSetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT *p, const char *text) { + if (!text) { + return false; + } + + // The STRING coercions are the .frm text rules (enum names, decimal, + // wgtParseBool, strtod). + WgtValueT v = wgtValueStr(text); + + return wgtIfaceSetProp(iface, w, p, &v); +} + + +bool wgtParseBool(const char *text, bool defaultValue) { + if (!text) { + return defaultValue; + } + + if (strcasecmp(text, WGT_TEXT_TRUE) == 0 || strcmp(text, WGT_TEXT_TRUE_NUM) == 0) { + return true; + } + + if (strcasecmp(text, WGT_TEXT_FALSE) == 0 || strcmp(text, WGT_TEXT_FALSE_NUM) == 0) { + return false; + } + + return defaultValue; +} + + +bool wgtSetProp(WidgetT *w, const char *propName, const WgtValueT *value) { + const WgtIfaceT *iface = wgtGetIfaceOf(w, NULL); + + return wgtIfaceSetProp(iface, w, wgtIfaceFindProp(iface, propName), value); +} + + +const WgtSigInfoT *wgtSigInfo(uint8_t sig) { + return (sig < DVX_ARRAY_LEN(sSigInfo)) ? &sSigInfo[sig] : NULL; +} + + +WgtValueT wgtValueBool(bool b) { + WgtValueT v = zeroOf(WGT_IFACE_BOOL); + + v.i = b ? 1 : 0; + return v; +} + + +WgtValueT wgtValueDouble(double f) { + WgtValueT v = zeroOf(WGT_IFACE_DOUBLE); + + v.f = f; + return v; +} + + +WgtValueT wgtValueFloat(double f) { + WgtValueT v = zeroOf(WGT_IFACE_FLOAT); + + v.f = f; + return v; +} + + +WgtValueT wgtValueInt(int32_t i) { + WgtValueT v = zeroOf(WGT_IFACE_INT); + + v.i = i; + return v; +} + + +WgtValueT wgtValueStr(const char *s) { + WgtValueT v = zeroOf(WGT_IFACE_STRING); + + v.s = s ? s : ""; + return v; +} + + +bool wgtValueToBool(const WgtValueT *v) { + switch (v->type) { + case WGT_IFACE_STRING: + return wgtParseBool(v->s, false); + + case WGT_IFACE_FLOAT: + case WGT_IFACE_DOUBLE: + return v->f != 0.0; + + case WGT_IFACE_NONE: + return false; + + default: + return v->i != 0; + } +} + + +double wgtValueToFloat(const WgtValueT *v) { + switch (v->type) { + case WGT_IFACE_STRING: + return v->s ? strtod(v->s, NULL) : 0.0; + + case WGT_IFACE_FLOAT: + case WGT_IFACE_DOUBLE: + return v->f; + + case WGT_IFACE_NONE: + return 0.0; + + default: + return (double)v->i; + } +} + + +int32_t wgtValueToInt(const WgtValueT *v) { + double d; + + switch (v->type) { + case WGT_IFACE_STRING: + d = v->s ? (double)strtol(v->s, NULL, WGT_DECIMAL) : 0.0; + break; + + case WGT_IFACE_FLOAT: + case WGT_IFACE_DOUBLE: + d = v->f; + break; + + case WGT_IFACE_NONE: + return 0; + + default: + return v->i; + } + + // Out-of-range doubles do not convert to int32_t; clamp them. + if (d >= (double)INT32_MAX) { + return INT32_MAX; + } + + if (d <= (double)INT32_MIN) { + return INT32_MIN; + } + + return (int32_t)d; +} + + +const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize) { + switch (v->type) { + case WGT_IFACE_STRING: + return v->s ? v->s : ""; + + case WGT_IFACE_BOOL: + return v->i ? WGT_TEXT_TRUE : WGT_TEXT_FALSE; + + case WGT_IFACE_FLOAT: + snprintf(buf, bufSize, "%g", v->f); + return buf; + + case WGT_IFACE_DOUBLE: + snprintf(buf, bufSize, WGT_DOUBLE_TEXT_FMT, v->f); + return buf; + + case WGT_IFACE_NONE: + return ""; + + default: + snprintf(buf, bufSize, "%ld", (long)v->i); + return buf; + } +} + + +// The zero value of type ("" for STRING). +static WgtValueT zeroOf(uint8_t type) { + WgtValueT v; + + memset(&v, 0, sizeof(v)); + v.type = type; + + if (type == WGT_IFACE_STRING) { + v.s = ""; + } + + return v; +} diff --git a/src/libs/kpunch/libdvx/widgetLayout.c b/src/libs/kpunch/libdvx/widgetLayout.c index 1fb8d170..5ce38350 100644 --- a/src/libs/kpunch/libdvx/widgetLayout.c +++ b/src/libs/kpunch/libdvx/widgetLayout.c @@ -57,6 +57,8 @@ static int32_t boxCapRoom(const WidgetT *c, bool horiz, int32_t innerW, int32_t innerH, const BitmapFontT *font); static bool boxChildFrozen(const WidgetT *c, int32_t capRoom, int32_t freeExtra, int32_t freeWeight); +static int32_t gapPixelsOf(int32_t size); +static int32_t gapSizeOf(int32_t pixels); // Extra main-axis space a weighted box child can absorb before its @@ -84,6 +86,30 @@ static bool boxChildFrozen(const WidgetT *c, int32_t capRoom, int32_t freeExtra, } +// Pixels of a Padding / Spacing size given in pixels (0 for one that is +// unset or in another unit). +static int32_t gapPixelsOf(int32_t size) { + return ((uint32_t)size & WGT_SIZE_TYPE_MASK) == WGT_SIZE_PIXELS ? size : 0; +} + + +// A Padding / Spacing size from pixels (negative counts as 0, the +// container's default; at most WGT_GAP_PIXELS_MAX). +static int32_t gapSizeOf(int32_t pixels) { + return wgtPixels(DVX_MIN(DVX_MAX(pixels, 0), WGT_GAP_PIXELS_MAX)); +} + + +int32_t wgtGetPaddingPx(const WidgetT *w) { + return gapPixelsOf(w->padding); +} + + +int32_t wgtGetSpacingPx(const WidgetT *w) { + return gapPixelsOf(w->spacing); +} + + // Public entry point: runs both passes on the entire widget tree. // The root widget is positioned at (0,0) and given the full available // area, then the arrange pass distributes space to its children. @@ -150,6 +176,18 @@ int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth } +void wgtSetPaddingPx(WidgetT *w, int32_t pixels) { + w->padding = gapSizeOf(pixels); + wgtInvalidate(w); +} + + +void wgtSetSpacingPx(WidgetT *w, int32_t pixels) { + w->spacing = gapSizeOf(pixels); + wgtInvalidate(w); +} + + // Resolves the layout metrics of a box container: padding and gap come // from the widget's tagged sizes (falling back to DEFAULT_PADDING / // DEFAULT_SPACING when unset), then a class-supplied getLayoutMetrics diff --git a/src/libs/kpunch/libdvx/widgetOps.c b/src/libs/kpunch/libdvx/widgetOps.c index 15c1331d..eb81b80d 100644 --- a/src/libs/kpunch/libdvx/widgetOps.c +++ b/src/libs/kpunch/libdvx/widgetOps.c @@ -205,6 +205,11 @@ AppContextT *wgtGetContext(const WidgetT *w) { } +bool wgtGetDebugLayout(void) { + return sDebugLayout; +} + + WidgetT *wgtGetFocused(void) { return sFocusedWidget; } diff --git a/src/libs/kpunch/libtasks/taskswitch.c b/src/libs/kpunch/libtasks/taskswitch.c index 69bf2c17..05c76b63 100644 --- a/src/libs/kpunch/libtasks/taskswitch.c +++ b/src/libs/kpunch/libtasks/taskswitch.c @@ -62,7 +62,11 @@ DVX_ABI_STAMP(libtasks); // ABI-required stack alignment at function entry (bytes) -#define STACK_ALIGN 16 +#define STACK_ALIGN 16 + +// Task stack multiplier under AddressSanitizer (its redzones make frames +// several times their native size) +#define ASAN_STACK_SCALE 4u // ============================================================================ // Internal types @@ -479,6 +483,13 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi stackSize = TS_MIN_STACK_SIZE; } +#if defined(__SANITIZE_ADDRESS__) + // A stack sized for the target overflows under the sanitizer, and + // silently: task stacks are heap blocks it does not guard. A size too + // large to scale saturates and fails in malloc as it would unscaled. + stackSize = (stackSize > UINT32_MAX / ASAN_STACK_SCALE) ? UINT32_MAX : stackSize * ASAN_STACK_SCALE; +#endif + // Reuse a terminated/free slot, or append a new one. // Recycling avoids unbounded array growth when apps are repeatedly // launched and terminated over the lifetime of the shell. diff --git a/src/libs/kpunch/listhelp/Makefile b/src/libs/kpunch/listhelp/Makefile index 57fb2d9a..4995b5fa 100644 --- a/src/libs/kpunch/listhelp/Makefile +++ b/src/libs/kpunch/listhelp/Makefile @@ -33,7 +33,7 @@ CFLAGS = $(DOS_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libtasks OBJDIR = ../../../../obj/listhelp LIBSDIR = ../../../../bin/libs -SRCS = listHelp.c +SRCS = listHelp.c listHelpBas.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGETDIR = $(LIBSDIR)/kpunch/listhelp TARGET = $(TARGETDIR)/listhelp.lib diff --git a/src/libs/kpunch/listhelp/README.md b/src/libs/kpunch/listhelp/README.md index 5f79246f..68d7b17d 100644 --- a/src/libs/kpunch/listhelp/README.md +++ b/src/libs/kpunch/listhelp/README.md @@ -5,7 +5,9 @@ Dropdown widgets: the owned-string item list (ListItemsT), the single/multi-select model (ListSelT), drag-reorder helpers, the dropdown popup (ListPopupT: state, geometry, painting, hit testing, keyboard), dropdown-arrow glyphs, and key-navigation helpers, so the widgets stay -consistent and small. +consistent and small. It also holds the icon view rules of Program +Manager's view (cells, grid, two-line labels, keys, drag and drop), which +BASIC icon views share through src/include/basic/listhelp.bas. ## Documentation @@ -15,12 +17,14 @@ DVX System Reference. Open via the Help Viewer app, or read Topics covered: item list, selection model, drag-reorder, dropdown popup, dropdown arrow drawing, popup-list hit test and paint, navigation -keys (Up/Down/Home/End/PgUp/PgDn), type-ahead. +keys (Up/Down/Home/End/PgUp/PgDn), type-ahead, icon views, the BASIC +entry points. ## Source Files -- `listHelp.h` -- public API -- `listHelp.c` -- implementation +- `listHelp.h` -- public API +- `listHelp.c` -- implementation +- `listHelpBas.c` -- DVX BASIC entry points (listhelp.bas) ## Build diff --git a/src/libs/kpunch/listhelp/listHelp.c b/src/libs/kpunch/listhelp/listHelp.c index a369ecd2..7fbab5a7 100644 --- a/src/libs/kpunch/listhelp/listHelp.c +++ b/src/libs/kpunch/listhelp/listHelp.c @@ -42,66 +42,80 @@ DVX_ABI_STAMP(listhelp); // Prototypes // ============================================================ -int32_t listDisplayRowOf(const int32_t *order, int32_t count, int32_t dataIdx); -int32_t listIndexAfterMove(int32_t idx, int32_t from, int32_t dest); -int32_t listIndexAfterRemove(int32_t idx, int32_t removed); -bool listItemsAdd(ListItemsT *li, const char *text); -void listItemsAdopt(ListItemsT *li); -bool listItemsClear(ListItemsT *li); -void listItemsFree(ListItemsT *li); -const char *listItemsGet(const ListItemsT *li, int32_t idx); -bool listItemsRemove(ListItemsT *li, int32_t idx); -bool listItemsSetExternal(ListItemsT *li, const char **items, int32_t count); -static void listItemsSync(ListItemsT *li); -void listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text); -void listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, const ListPopupT *p); -void listPopupClearItems(WidgetT *w, ListPopupT *p); -void listPopupClose(WidgetT *w, ListPopupT *p); -bool listPopupCommit(ListPopupT *p); -void listPopupDrawFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const WidgetT *w, uint32_t bg, bool pressed); -void listPopupEnsureVisible(ListPopupT *p); -void listPopupInit(ListPopupT *p); -bool listPopupIsToggleKey(int32_t key, int32_t mod); -int32_t listPopupItemAt(const ListPopupT *p, const BitmapFontT *font, int32_t popY, int32_t vy); -static void listPopupItemsChanged(WidgetT *w, ListPopupT *p, bool widthChanged); -bool listPopupNavKey(ListPopupT *p, int32_t key); -ListPopupHitE listPopupOnMouse(WidgetT *w, ListPopupT *p, int32_t vx, int32_t vy); -void listPopupOpen(WidgetT *w, ListPopupT *p); -void listPopupPaint(const WidgetT *w, const ListPopupT *p, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx); -void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count); -int32_t listPopupVisibleCount(int32_t itemCount); -void listReorderCancel(WidgetT *w, int32_t *pDragIdx, int32_t *pDropIdx); -int32_t listReorderDest(int32_t from, int32_t to, int32_t count); -void listReorderMove(void *base, size_t elemSize, int32_t from, int32_t dest); -void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t top, int32_t w, int32_t h, int32_t rowH, int32_t scrollPos, int32_t dropIdx, uint32_t color); -void listReorderTrack(int32_t relY, int32_t rowH, int32_t visibleRows, int32_t count, int32_t *pScrollPos, int32_t *pDropIdx); -static void listReverseBytes(uint8_t *lo, uint8_t *hi); -static void listRotateLeft(uint8_t *p, size_t len, size_t k); -void listSelClick(ListSelT *s, int32_t count, const int32_t *order, int32_t displayIdx, bool shift, bool ctrl); -void listSelFree(ListSelT *s); -void listSelInit(ListSelT *s); -void listSelInsert(ListSelT *s, int32_t newCount, int32_t idx); -bool listSelIsSelected(const ListSelT *s, int32_t count, int32_t idx); -ListSelKeyE listSelKey(ListSelT *s, int32_t count, int32_t key, int32_t mod); -void listSelMove(ListSelT *s, int32_t count, const int32_t *order, int32_t displayIdx, bool shift); -static void listSelRange(ListSelT *s, int32_t count, const int32_t *order, int32_t fromDisplay, int32_t toDisplay); -void listSelRemove(ListSelT *s, int32_t oldCount, int32_t idx); -void listSelReorder(ListSelT *s, int32_t from, int32_t dest); -void listSelReset(ListSelT *s, int32_t count); -bool listSelSetAll(ListSelT *s, int32_t count, bool selected); -void listSelSetCursor(ListSelT *s, int32_t count, int32_t idx); -bool listSelSetItem(ListSelT *s, int32_t count, int32_t idx, bool selected); -char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count); -void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color); -void widgetDropdownPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); -void widgetFreeOwnedStrings(char **owned); -int32_t widgetMaxItemLen(const char **items, int32_t count); -int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); -void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); -bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos); -bool widgetTypeAheadMatch(char ch, const char *text); -int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx); +int32_t listDisplayRowOf(const int32_t *order, int32_t count, int32_t dataIdx); +int32_t listIconCellAt(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow); +void listIconCellLayout(ListIconViewE view, int32_t charW, int32_t charH, int32_t innerW, ListIconCellT *out); +int32_t listIconDragScroll(const ListIconGridT *g, int32_t y, int32_t scrollRow); +bool listIconDragStarted(int32_t dx, int32_t dy); +int32_t listIconDropCell(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow); +int32_t listIconDropDest(int32_t from, int32_t cell, int32_t count); +void listIconGrid(ListIconViewE view, int32_t charW, int32_t charH, int32_t viewW, int32_t viewH, int32_t count, int32_t rowsTaken, ListIconGridT *out); +static void listIconGridFit(ListIconGridT *g, ListIconViewE view, int32_t charW, int32_t charH, int32_t count, int32_t rowsTaken); +int32_t listIconGridMetric(const ListIconGridT *g, ListIconMetricE metric); +void listIconLabelSplit(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2); +int32_t listIconNavigate(int32_t key, int32_t current, const int32_t *cols, const int32_t *rows, int32_t count, int32_t gridCols, int32_t pageRows); +int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab); +ListIconScrollE listIconScrollPress(const ListIconGridT *g, int32_t x, int32_t y, int32_t *pScrollRow, int32_t *outGrab); +int32_t listIndexAfterMove(int32_t idx, int32_t from, int32_t dest); +int32_t listIndexAfterRemove(int32_t idx, int32_t removed); +bool listItemsAdd(ListItemsT *li, const char *text); +void listItemsAdopt(ListItemsT *li); +bool listItemsClear(ListItemsT *li); +void listItemsFree(ListItemsT *li); +const char *listItemsGet(const ListItemsT *li, int32_t idx); +bool listItemsRemove(ListItemsT *li, int32_t idx); +bool listItemsSetExternal(ListItemsT *li, const char **items, int32_t count); +static void listItemsSync(ListItemsT *li); +void listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text); +void listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, const ListPopupT *p); +void listPopupClearItems(WidgetT *w, ListPopupT *p); +void listPopupClose(WidgetT *w, ListPopupT *p); +bool listPopupCommit(ListPopupT *p); +void listPopupDrawFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const WidgetT *w, uint32_t bg, bool pressed); +void listPopupEnsureVisible(ListPopupT *p); +void listPopupInit(ListPopupT *p); +bool listPopupIsToggleKey(int32_t key, int32_t mod); +int32_t listPopupItemAt(const ListPopupT *p, const BitmapFontT *font, int32_t popY, int32_t vy); +static void listPopupItemsChanged(WidgetT *w, ListPopupT *p, bool widthChanged); +bool listPopupNavKey(ListPopupT *p, int32_t key); +ListPopupHitE listPopupOnMouse(WidgetT *w, ListPopupT *p, int32_t vx, int32_t vy); +void listPopupOpen(WidgetT *w, ListPopupT *p); +void listPopupPaint(const WidgetT *w, const ListPopupT *p, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx); +void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count); +int32_t listPopupVisibleCount(int32_t itemCount); +void listReorderCancel(WidgetT *w, int32_t *pDragIdx, int32_t *pDropIdx); +int32_t listReorderDest(int32_t from, int32_t to, int32_t count); +void listReorderMove(void *base, size_t elemSize, int32_t from, int32_t dest); +void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t top, int32_t w, int32_t h, int32_t rowH, int32_t scrollPos, int32_t dropIdx, uint32_t color); +void listReorderTrack(int32_t relY, int32_t rowH, int32_t visibleRows, int32_t count, int32_t *pScrollPos, int32_t *pDropIdx); +static void listReverseBytes(uint8_t *lo, uint8_t *hi); +static void listRotateLeft(uint8_t *p, size_t len, size_t k); +void listSelClick(ListSelT *s, int32_t count, const int32_t *order, int32_t displayIdx, bool shift, bool ctrl); +void listSelFree(ListSelT *s); +void listSelInit(ListSelT *s); +void listSelInsert(ListSelT *s, int32_t newCount, int32_t idx); +bool listSelIsSelected(const ListSelT *s, int32_t count, int32_t idx); +ListSelKeyE listSelKey(ListSelT *s, int32_t count, int32_t key, int32_t mod); +void listSelMove(ListSelT *s, int32_t count, const int32_t *order, int32_t displayIdx, bool shift); +static void listSelRange(ListSelT *s, int32_t count, const int32_t *order, int32_t fromDisplay, int32_t toDisplay); +void listSelRemove(ListSelT *s, int32_t oldCount, int32_t idx); +void listSelReorder(ListSelT *s, int32_t from, int32_t dest); +void listSelReset(ListSelT *s, int32_t count); +bool listSelSetAll(ListSelT *s, int32_t count, bool selected); +void listSelSetCursor(ListSelT *s, int32_t count, int32_t idx); +bool listSelSetItem(ListSelT *s, int32_t count, int32_t idx, bool selected); +char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count); +void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color); +void widgetDropdownPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); +void widgetFreeOwnedStrings(char **owned); +int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows); +int32_t widgetMaxItemLen(const char **items, int32_t count); +int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); +void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); +bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos); +bool widgetTypeAheadMatch(char ch, const char *text); +int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx); // Display row of a data index through an optional display->data order @@ -121,6 +135,313 @@ int32_t listDisplayRowOf(const int32_t *order, int32_t count, int32_t dataIdx) { } +int32_t listIconCellAt(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow) { + int32_t relX = x - g->innerX; + int32_t relY = y - g->innerY; + + *outCol = LIST_ICON_NO_CELL; + *outRow = LIST_ICON_NO_CELL; + + if (relX < 0 || relY < 0 || relX >= g->innerW || relY >= g->innerH || relX / g->cell.cellW >= g->cols) { + return LIST_ICON_NO_CELL; + } + + *outCol = relX / g->cell.cellW; + *outRow = relY / g->cell.cellH + scrollRow; + return *outRow * g->cols + *outCol; +} + + +// Large icons: the picture centred at the top, two label lines under it. +// Small icons and the list: one row as tall as the picture or the font, +// the picture then the name; the list's row spans the grid and adds a +// detail column after the name column. +void listIconCellLayout(ListIconViewE view, int32_t charW, int32_t charH, int32_t innerW, ListIconCellT *out) { + int32_t cw = DVX_MAX(charW, 1); + int32_t ch = DVX_MAX(charH, 1); + + if (view != ListIconSmallE && view != ListIconListE) { + out->cellW = LIST_ICON_LARGE_CELL_W; + out->cellH = LIST_ICON_PAD + LIST_ICON_LARGE + LIST_ICON_LABEL_GAP + LIST_ICON_LABEL_LINES * ch + LIST_ICON_PAD; + out->pictureX = (LIST_ICON_LARGE_CELL_W - LIST_ICON_LARGE) / 2; + out->pictureY = LIST_ICON_PAD; + out->pictureSize = LIST_ICON_LARGE; + out->textX = 0; + out->textY = LIST_ICON_PAD + LIST_ICON_LARGE + LIST_ICON_LABEL_GAP; + out->nameChars = LIST_ICON_LARGE_CELL_W / cw; + out->detailX = 0; + return; + } + + out->cellH = DVX_MAX(ch, LIST_ICON_SMALL) + LIST_ICON_PAD * 2; + out->pictureX = LIST_ICON_PAD; + out->pictureY = (out->cellH - LIST_ICON_SMALL) / 2; + out->pictureSize = LIST_ICON_SMALL; + out->textX = LIST_ICON_PAD + LIST_ICON_SMALL + LIST_ICON_TEXT_GAP; + out->textY = (out->cellH - ch) / 2; + + if (view == ListIconListE) { + out->cellW = DVX_MAX(innerW, 1); + out->nameChars = LIST_ICON_LIST_CHARS; + out->detailX = out->textX + (LIST_ICON_LIST_CHARS + LIST_ICON_COLUMN_GAP) * cw; + } else { + out->cellW = out->textX + LIST_ICON_SMALL_CHARS * cw + LIST_ICON_PAD; + out->nameChars = LIST_ICON_SMALL_CHARS; + out->detailX = 0; + } +} + + +int32_t listIconDragScroll(const ListIconGridT *g, int32_t y, int32_t scrollRow) { + if (y < g->innerY && scrollRow > 0) { + return scrollRow - 1; + } + + if (y >= g->innerY + g->innerH && scrollRow < g->maxScroll) { + return scrollRow + 1; + } + + return scrollRow; +} + + +bool listIconDragStarted(int32_t dx, int32_t dy) { + return abs(dx) >= LIST_ICON_DRAG_SLOP || abs(dy) >= LIST_ICON_DRAG_SLOP; +} + + +int32_t listIconDropCell(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow) { + return listIconCellAt(g, x, clampInt(y, g->innerY, g->innerY + g->innerH - 1), scrollRow, outCol, outRow); +} + + +int32_t listIconDropDest(int32_t from, int32_t cell, int32_t count) { + if (from < 0 || from >= count || cell < 0) { + return -1; + } + + int32_t dest = DVX_MIN(cell, count - 1); + + return dest == from ? -1 : dest; +} + + +// Laid out without a scroll bar first; with one (narrowing the grid) only +// when the rows do not fit. +void listIconGrid(ListIconViewE view, int32_t charW, int32_t charH, int32_t viewW, int32_t viewH, int32_t count, int32_t rowsTaken, ListIconGridT *out) { + int32_t fullW = viewW - LIST_ICON_FRAME * 2; + + out->innerX = LIST_ICON_FRAME; + out->innerY = LIST_ICON_FRAME; + out->innerW = fullW; + out->innerH = viewH - LIST_ICON_FRAME * 2; + out->emptyX = LIST_ICON_FRAME + LIST_ICON_PAD; + out->emptyY = LIST_ICON_FRAME + LIST_ICON_PAD; + out->needSb = false; + listIconGridFit(out, view, charW, charH, count, rowsTaken); + + if (out->totalRows > out->rowsVisible) { + out->needSb = true; + out->innerW = fullW - LIST_ICON_SB_W; + listIconGridFit(out, view, charW, charH, count, rowsTaken); + } + + out->maxScroll = DVX_MAX(out->totalRows - out->rowsVisible, 0); +} + + +// The cell, columns and rows for the grid's current innerW. +static void listIconGridFit(ListIconGridT *g, ListIconViewE view, int32_t charW, int32_t charH, int32_t count, int32_t rowsTaken) { + listIconCellLayout(view, charW, charH, g->innerW, &g->cell); + g->cols = (view == ListIconListE) ? 1 : DVX_MAX(g->innerW / g->cell.cellW, 1); + g->rowsVisible = DVX_MAX(g->innerH / g->cell.cellH, 1); + g->totalRows = (rowsTaken == LIST_ICON_BY_ORDER) ? (DVX_MAX(count, 0) + g->cols - 1) / g->cols : rowsTaken; +} + + +int32_t listIconGridMetric(const ListIconGridT *g, ListIconMetricE metric) { + switch (metric) { + case ListIconCellWE: + return g->cell.cellW; + + case ListIconCellHE: + return g->cell.cellH; + + case ListIconPictureXE: + return g->cell.pictureX; + + case ListIconPictureYE: + return g->cell.pictureY; + + case ListIconPictureSizeE: + return g->cell.pictureSize; + + case ListIconTextXE: + return g->cell.textX; + + case ListIconTextYE: + return g->cell.textY; + + case ListIconNameCharsE: + return g->cell.nameChars; + + case ListIconDetailXE: + return g->cell.detailX; + + case ListIconInnerXE: + return g->innerX; + + case ListIconInnerYE: + return g->innerY; + + case ListIconInnerWE: + return g->innerW; + + case ListIconInnerHE: + return g->innerH; + + case ListIconColumnsE: + return g->cols; + + case ListIconRowsShownE: + return g->rowsVisible; + + case ListIconRowsE: + return g->totalRows; + + case ListIconTopRowMaxE: + return g->maxScroll; + + case ListIconScrollBarE: + return g->needSb ? 1 : 0; + + case ListIconEmptyXE: + return g->emptyX; + + case ListIconEmptyYE: + return g->emptyY; + + case ListIconFrameE: + return LIST_ICON_FRAME; + + case ListIconWheelRowsE: + return LIST_ICON_WHEEL_ROWS; + + default: + return 0; + } +} + + +void listIconLabelSplit(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2) { + const char *text = name ? name : ""; + int32_t len = (int32_t)strlen(text); + + *line2 = text + len; + *len2 = 0; + + if (len <= maxChars || maxChars <= 0) { + *len1 = DVX_MIN(len, DVX_MAX(maxChars, 0)); + return; + } + + int32_t brk = maxChars; + + while (brk > 0 && text[brk] != ' ') { + brk--; + } + + *len1 = brk > 0 ? brk : maxChars; + *line2 = dvxSkipWs(text + *len1); + *len2 = (int32_t)strnlen(*line2, (size_t)maxChars); +} + + +// Each item is scored by how far it is the key's way; the closest wins +// (the first of equals). Up / Down score the row distance from the +// target row first and the column distance second. +int32_t listIconNavigate(int32_t key, int32_t current, const int32_t *cols, const int32_t *rows, int32_t count, int32_t gridCols, int32_t pageRows) { + int32_t gc = DVX_MAX(gridCols, 1); + int32_t cur = current; + int32_t best = -1; + int32_t bestScore = 0; + + if (count <= 0) { + return -1; + } + + // With nothing current, any movement starts at the first item. + if (cur < 0 || cur >= count) { + cur = 0; + key = (key == KEY_LEFT || key == KEY_RIGHT || key == KEY_UP || key == KEY_DOWN || key == KEY_PGUP || key == KEY_PGDN) ? KEY_HOME : key; + } + + int32_t curOrder = rows[cur] * gc + cols[cur]; + int32_t rowStep = (key == KEY_PGUP || key == KEY_PGDN) ? DVX_MAX(pageRows, 1) : LIST_ICON_LINE_ROWS; + + for (int32_t i = 0; i < count; i++) { + int32_t order = rows[i] * gc + cols[i]; + int32_t dCol = abs(cols[i] - cols[cur]); + int32_t score = -1; + + switch (key) { + case KEY_LEFT: + score = order < curOrder ? curOrder - order : -1; + break; + + case KEY_RIGHT: + score = order > curOrder ? order - curOrder : -1; + break; + + case KEY_UP: + case KEY_PGUP: + score = rows[i] < rows[cur] ? abs(rows[cur] - rowStep - rows[i]) * gc * gc + dCol : -1; + break; + + case KEY_DOWN: + case KEY_PGDN: + score = rows[i] > rows[cur] ? abs(rows[i] - rows[cur] - rowStep) * gc * gc + dCol : -1; + break; + + case KEY_HOME: + score = order; + break; + + case KEY_END: + score = INT32_MAX - order; + break; + + default: + return -1; + } + + if (score >= 0 && (best < 0 || score < bestScore)) { + best = i; + bestScore = score; + } + } + + return best; +} + + +// The shared control scroll bar's thumb drag (widgetScrollbarThumbDragScroll). +int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab) { + return widgetScrollbarThumbDragScroll(g->innerH - LIST_ICON_SB_W * 2, g->totalRows, g->rowsVisible, y - g->innerY - LIST_ICON_SB_W - grab, g->maxScroll); +} + + +// The shared control scroll bar's press rule (widgetScrollbarApplyHit): +// arrows a row, the trough a page of rows. +ListIconScrollE listIconScrollPress(const ListIconGridT *g, int32_t x, int32_t y, int32_t *pScrollRow, int32_t *outGrab) { + if (!g->needSb || x < g->innerX + g->innerW) { + return ListIconScrollNoneE; + } + + return widgetScrollbarApplyHit(g->innerH, y - g->innerY, g->totalRows, g->rowsVisible, LIST_ICON_LINE_ROWS, g->rowsVisible, pScrollRow, outGrab) ? ListIconScrollThumbE : ListIconScrollMovedE; +} + + // Where idx ends up after the element at `from` moved to `dest`. int32_t listIndexAfterMove(int32_t idx, int32_t from, int32_t dest) { if (idx == from) { @@ -886,6 +1207,45 @@ void widgetFreeOwnedStrings(char **owned) { } +// Keyboard navigation in a grid of count items laid out row by row, +// columns per row (icon views): Left / Right step through the items in +// order, Up / Down / PgUp / PgDn move by rows (pageRows per page) the way +// widgetNavigateIndex moves through a list, keeping the column (the last +// item when the row below is shorter), Home / End go to the first / last +// item. With nothing selected (current < 0) every one of these keys goes +// to the first item. Returns -1 for any other key; with one column it is +// widgetNavigateIndex. +int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows) { + int32_t cols = DVX_MAX(columns, 1); + int32_t row; + int32_t newRow; + + if (count <= 0) { + return current; + } + + if (key == KEY_LEFT || key == KEY_RIGHT) { + if (current < 0) { + return 0; + } + + return widgetNavigateIndex(key == KEY_LEFT ? KEY_UP : KEY_DOWN, current, count, 1); + } + + if (key != KEY_UP && key != KEY_DOWN && key != KEY_PGUP && key != KEY_PGDN) { + return widgetNavigateIndex(key, current, count, pageRows); + } + + if (current < 0) { + return 0; + } + + row = current / cols; + newRow = widgetNavigateIndex(key, row, (count + cols - 1) / cols, DVX_MAX(pageRows, 1)); + return DVX_MIN(newRow * cols + current % cols, count - 1); +} + + // Scans an array of string items and returns the maximum strlen. NULL // entries count as empty. int32_t widgetMaxItemLen(const char **items, int32_t count) { diff --git a/src/libs/kpunch/listhelp/listHelp.h b/src/libs/kpunch/listhelp/listHelp.h index d31d81bf..13ab354a 100644 --- a/src/libs/kpunch/listhelp/listHelp.h +++ b/src/libs/kpunch/listhelp/listHelp.h @@ -197,6 +197,11 @@ int32_t widgetMaxItemLen(const char **items, int32_t count); int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); +// The same keys in a grid laid out row by row, columns items per row (icon +// views): Left / Right step through the items, Up / Down / PgUp / PgDn +// move by rows keeping the column, Home / End. -1 for other keys. +int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows); + // ============================================================ // Popup rect calculation // ============================================================ @@ -229,4 +234,159 @@ int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, in // Updates *scrollPos in place (clamped to valid range). bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos); +// ============================================================ +// Icon views (large icons, small icons, a list) +// ============================================================ + +// The rules of an icon view -- Program Manager's, and icon views drawn in +// BASIC (listhelp.bas): each view's cell and where an item's picture and +// name go in it, the grid inside a sunken frame with a scroll bar only +// when the rows do not fit, the two label lines under a large icon, the +// arrow keys among items in order or in cells of their own, the scroll +// bar (a control's, as ListBox's: its arrows, trough and thumb), when a +// press becomes a drag, the scrolling and the target cell while dragging, +// and where a dropped item lands. Positions are pixels from the view's +// top left (grid) or the cell's (cell). +#define LIST_ICON_FRAME 2 // sunken frame around the view +#define LIST_ICON_PAD 2 // cell edge to its picture; inside of the frame to the empty text +#define LIST_ICON_LARGE 32 // large icon edge +#define LIST_ICON_SMALL 16 // small icon edge +#define LIST_ICON_LABEL_GAP 2 // large icon to its label +#define LIST_ICON_LABEL_LINES 2 // label lines under a large icon +#define LIST_ICON_TEXT_GAP 4 // small icon to its name +#define LIST_ICON_LARGE_CELL_W 80 // large icon cell width +#define LIST_ICON_SMALL_CHARS 14 // name beside a small icon, in characters +#define LIST_ICON_LIST_CHARS 24 // the list's name column, in characters +#define LIST_ICON_COLUMN_GAP 1 // the list's name column to its detail column, in characters +#define LIST_ICON_DRAG_SLOP 4 // pixels a press moves (either way) before it drags +#define LIST_ICON_WHEEL_ROWS 1 // rows a wheel notch scrolls +#define LIST_ICON_LINE_ROWS 1 // rows an arrow key or a scroll bar arrow moves +#define LIST_ICON_SB_W WGT_SB_W // the scroll bar's width (a control's) +#define LIST_ICON_NO_CELL (-1) // no cell (outside the grid) +#define LIST_ICON_BY_ORDER (-1) // listIconGrid rowsTaken: items laid out in order + +typedef enum { + ListIconLargeE, + ListIconSmallE, + ListIconListE, + ListIconViewCountE +} ListIconViewE; + +// What a press did to the scroll bar (listIconScrollPress). +typedef enum { + ListIconScrollNoneE, // not on the scroll bar + ListIconScrollMovedE, // an arrow or the trough: the top row moved + ListIconScrollThumbE // the thumb, grabbed for listIconScrollDrag +} ListIconScrollE; + +// One item's cell (listIconCellLayout): its size, the picture's place +// and edge, the name's place (a large icon's label lines are each centred +// in the cell, textX 0) and length (a large icon's: per line), and the +// list's detail column (0 in the other views). +typedef struct { + int32_t cellW; + int32_t cellH; + int32_t pictureX; + int32_t pictureY; + int32_t pictureSize; + int32_t textX; + int32_t textY; + int32_t nameChars; + int32_t detailX; +} ListIconCellT; + +// A view's grid (listIconGrid): the cell, the inside of the frame less +// the scroll bar, the columns, the rows shown whole and in all, the +// largest top row, the empty text's place and whether the scroll bar is +// needed (it stands right of innerW, innerH tall). +typedef struct { + ListIconCellT cell; + int32_t innerX; + int32_t innerY; + int32_t innerW; + int32_t innerH; + int32_t cols; + int32_t rowsVisible; + int32_t totalRows; + int32_t maxScroll; + int32_t emptyX; + int32_t emptyY; + bool needSb; +} ListIconGridT; + +// listIconGridMetric's values (BASIC LISTICON_* of ListIconMetric). +typedef enum { + ListIconCellWE, + ListIconCellHE, + ListIconPictureXE, + ListIconPictureYE, + ListIconPictureSizeE, + ListIconTextXE, + ListIconTextYE, + ListIconNameCharsE, + ListIconDetailXE, + ListIconInnerXE, + ListIconInnerYE, + ListIconInnerWE, + ListIconInnerHE, + ListIconColumnsE, + ListIconRowsShownE, + ListIconRowsE, + ListIconTopRowMaxE, + ListIconScrollBarE, + ListIconEmptyXE, + ListIconEmptyYE, + ListIconFrameE, + ListIconWheelRowsE, + ListIconMetricCountE +} ListIconMetricE; + +// The cell number (reading order, row * cols + col) under x, y of a view +// scrolled to scrollRow, and its column and row; LIST_ICON_NO_CELL for all +// three outside the grid or right of the last column. +int32_t listIconCellAt(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow); +// The cell of a view whose grid is innerW wide (a list row spans it). +void listIconCellLayout(ListIconViewE view, int32_t charW, int32_t charH, int32_t innerW, ListIconCellT *out); +// The top row after a drag move to y: one row up above the grid, one down +// below it, else unchanged. +int32_t listIconDragScroll(const ListIconGridT *g, int32_t y, int32_t scrollRow); +// True when a press has moved dx, dy far enough to drag. +bool listIconDragStarted(int32_t dx, int32_t dy); +// The cell a dragged item would drop on with the pointer at x, y (held +// inside the grid's height): listIconCellAt. +int32_t listIconDropCell(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow); +// The index an item dragged from `from` takes when dropped on cell number +// cell: that cell's, the last for a cell past the end; -1 when that +// changes nothing or from / cell is out of range. +int32_t listIconDropDest(int32_t from, int32_t cell, int32_t count); +// The grid of a view viewW x viewH holding count items: the frame, a +// scroll bar (LIST_ICON_SB_W) only when the rows do not fit. rowsTaken: +// the rows the items take whatever the columns (items placed in cells of +// their own), or LIST_ICON_BY_ORDER for items laid out in order. +void listIconGrid(ListIconViewE view, int32_t charW, int32_t charH, int32_t viewW, int32_t viewH, int32_t count, int32_t rowsTaken, ListIconGridT *out); +// One value of a grid by ListIconMetricE (the scroll bar: 1 / 0; 0 for an +// unknown metric). +int32_t listIconGridMetric(const ListIconGridT *g, ListIconMetricE metric); +// The two label lines under a large icon, maxChars wide: the first ends +// at the last blank that fits (else it is cut at maxChars), the second +// starts after the blanks there and is cut to maxChars. *line2 points +// into name; *len2 is 0 for a name that fits one line. +void listIconLabelSplit(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2); +// The item an arrow key, Home, End, PgUp or PgDn moves to from current +// when item i stands in cell (cols[i], rows[i]) of a grid gridCols wide +// (gaps allowed): Left / Right the nearest item before / after it in +// reading order, Up / Down the item in the nearest row above / below with +// the closest column, PgUp / PgDn the same pageRows rows away, Home / End +// the first / last in reading order. With no current item (current < 0) +// every one of these keys goes to the first. -1 for another key or when +// no item is that way. +int32_t listIconNavigate(int32_t key, int32_t current, const int32_t *cols, const int32_t *rows, int32_t count, int32_t gridCols, int32_t pageRows); +// The top row while the thumb grabbed grab pixels below its top +// (listIconScrollPress) is dragged to y. +int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab); +// A left press at x, y on the scroll bar (right of the grid, when there is +// one): an arrow moves the top row *pScrollRow a row, the trough a page; +// the thumb is grabbed (*outGrab: how far below its top). +ListIconScrollE listIconScrollPress(const ListIconGridT *g, int32_t x, int32_t y, int32_t *pScrollRow, int32_t *outGrab); + #endif // LIST_HELP_H diff --git a/src/libs/kpunch/listhelp/listHelpBas.c b/src/libs/kpunch/listhelp/listHelpBas.c new file mode 100644 index 00000000..8bd6266e --- /dev/null +++ b/src/libs/kpunch/listhelp/listHelpBas.c @@ -0,0 +1,600 @@ +// 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. + +// listHelpBas.c -- DVX BASIC entry points of the list helper library +// (src/include/basic/listhelp.bas) +// +// The selection model, keyboard navigation, type-ahead and drag-reorder +// rules of the ListBox and ListView widgets, and the icon view rules of +// Program Manager's view (cells, grid, label lines, drag and drop), for +// list-like controls written in BASIC (a UserControl such as an icon +// view): one set of rules for every list. Called through DECLARE +// LIBRARY, so the signatures use only LONG and STRING; True is -1, False +// 0. An icon view's grid is figured from the view's size each call (no +// handle); its scroll bar is a control's (DrawScrollBar's control style). +// +// A selection handle (1, 2, ...; 0 means none) is one ListSelT plus the +// item count it covers, so the calls need no count. Items are numbered +// from 0. A Shift argument is the Shift value of the KeyDown / MouseDown +// event (vbShiftMask and vbCtrlMask bits; True counts as both). Handles +// belong to the program that made them; whatever a program leaves open is +// freed when it ends. + +#include "listHelp.h" +#include "dvxMem.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define BAS_NONE (-1) +#define ITEM_SEPARATOR '\n' +#define LABEL_LINE_FIRST 1 // ListIconLabel's line numbers +#define LABEL_LINE_SECOND 2 +#define CELL_BASE 10 // ListIconNavigate's cells: decimal numbers +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" + +// One selection handle and the program it belongs to +typedef struct { + ListSelT sel; + int32_t count; + int32_t appId; + bool used; +} BasListSelT; + +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// this library loads before the shell, and the host tests may have none. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); + +static BasListSelT *sHandles = NULL; // stb_ds; handle = index + 1 +static bool sCleanupHooked = false; +static char *sLabel = NULL; // stb_ds: the last ListIconLabel result + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static int32_t basBool(bool value); +static BasListSelT *handleOf(int32_t handle); +static void iconGrid(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, ListIconGridT *g); +static bool inRange(const BasListSelT *h, int32_t index); +int32_t ListGridNavigate(int32_t keyCode, int32_t current, int32_t count, int32_t columns, int32_t pageRows); +int32_t ListIconCellAt(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y); +int32_t ListIconDragScroll(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t y); +int32_t ListIconDragStarted(int32_t dx, int32_t dy); +int32_t ListIconDropCell(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y); +int32_t ListIconDropDest(int32_t fromIndex, int32_t cell, int32_t count); +const char *ListIconLabel(const char *name, int32_t maxChars, int32_t lineNo); +int32_t ListIconMetric(int32_t view, int32_t metric, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken); +int32_t ListIconNavigate(int32_t keyCode, int32_t current, const char *cells, int32_t columns, int32_t pageRows); +int32_t ListIconScrollDrag(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t grab, int32_t y); +int32_t ListIconScrollGrab(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y); +int32_t ListIconScrollPress(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y); +int32_t ListNavigateIndex(int32_t keyCode, int32_t current, int32_t count, int32_t pageSize); +int32_t ListReorderDest(int32_t fromIndex, int32_t toSlot, int32_t count); +void ListSelClick(int32_t handle, int32_t index, int32_t shift); +int32_t ListSelCount(int32_t handle); +int32_t ListSelCreate(int32_t multi, int32_t count); +int32_t ListSelCursor(int32_t handle); +void ListSelFree(int32_t handle); +void ListSelInsert(int32_t handle, int32_t index); +int32_t ListSelIsSelected(int32_t handle, int32_t index); +int32_t ListSelKey(int32_t handle, int32_t keyCode, int32_t shift); +void ListSelMove(int32_t handle, int32_t index, int32_t shift); +int32_t ListSelNavigate(int32_t handle, int32_t keyCode, int32_t shift, int32_t columns, int32_t pageRows); +void ListSelRemove(int32_t handle, int32_t index); +void ListSelReorder(int32_t handle, int32_t fromIndex, int32_t dest); +void ListSelReset(int32_t handle, int32_t count); +int32_t ListSelSelectedCount(int32_t handle); +void ListSelSetAll(int32_t handle, int32_t selected); +void ListSelSetCursor(int32_t handle, int32_t index); +void ListSelSetItem(int32_t handle, int32_t index, int32_t selected); +int32_t ListTypeAhead(const char *ch, const char *items, int32_t current); +int32_t ListTypeAheadMatch(const char *ch, const char *text); + + +// Shell app-cleanup hook: free what a program that ended left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < (int32_t)arrlen(sHandles); i++) { + if (sHandles[i].used && sHandles[i].appId == appId) { + ListSelFree(i + 1); + } + } +} + + +static int32_t basBool(bool value) { + return value ? BAS_TRUE : BAS_FALSE; +} + + +static BasListSelT *handleOf(int32_t handle) { + if (handle <= BAS_NO_HANDLE || handle > (int32_t)arrlen(sHandles) || !sHandles[handle - 1].used) { + return NULL; + } + + return &sHandles[handle - 1]; +} + + +// The grid of an icon view width x height (frame included) holding count +// items (rowsTaken: LIST_ICON_BY_ORDER, or the rows items in cells of +// their own take). +static void iconGrid(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, ListIconGridT *g) { + listIconGrid((ListIconViewE)view, charWidth, charHeight, width, height, count, rowsTaken, g); +} + + +static bool inRange(const BasListSelT *h, int32_t index) { + return index >= 0 && index < h->count; +} + + +// widgetGridNavigateIndex: the item a key moves to in a grid of count +// items, columns per row; -1 for a key that does not move. +int32_t ListGridNavigate(int32_t keyCode, int32_t current, int32_t count, int32_t columns, int32_t pageRows) { + return widgetGridNavigateIndex(keyCode, current, count, columns, pageRows); +} + + +// listIconCellAt: the cell number (row * columns + column) under x, y of +// an icon view scrolled to topRow; -1 outside the grid. +int32_t ListIconCellAt(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y) { + ListIconGridT g; + int32_t col; + int32_t row; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + return listIconCellAt(&g, x, y, topRow, &col, &row); +} + + +// listIconDragScroll: the top row after a drag move to y. +int32_t ListIconDragScroll(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t y) { + ListIconGridT g; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + return listIconDragScroll(&g, y, topRow); +} + + +int32_t ListIconDragStarted(int32_t dx, int32_t dy) { + return basBool(listIconDragStarted(dx, dy)); +} + + +// listIconDropCell: the cell a dragged item would drop on, -1 for none. +int32_t ListIconDropCell(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y) { + ListIconGridT g; + int32_t col; + int32_t row; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + return listIconDropCell(&g, x, y, topRow, &col, &row); +} + + +int32_t ListIconDropDest(int32_t fromIndex, int32_t cell, int32_t count) { + return listIconDropDest(fromIndex, cell, count); +} + + +// listIconLabelSplit: line lineNo (1 or 2) of a large icon's label +// maxChars wide; "" for a line it does not have. +const char *ListIconLabel(const char *name, int32_t maxChars, int32_t lineNo) { + const char *text = name ? name : ""; + const char *line2; + int32_t len1; + int32_t len2; + + listIconLabelSplit(text, maxChars, &len1, &line2, &len2); + + const char *from = (lineNo == LABEL_LINE_SECOND) ? line2 : text; + int32_t len = (lineNo == LABEL_LINE_FIRST) ? len1 : ((lineNo == LABEL_LINE_SECOND) ? len2 : 0); + + arrsetlen(sLabel, len + 1); + memcpy(sLabel, from, (size_t)len); + sLabel[len] = '\0'; + return sLabel; +} + + +// listIconGridMetric of an icon view width x height (frame included) +// holding count items; the scroll bar as True / False. +int32_t ListIconMetric(int32_t view, int32_t metric, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken) { + ListIconGridT g; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + + if (metric == ListIconScrollBarE) { + return basBool(g.needSb); + } + + return listIconGridMetric(&g, (ListIconMetricE)metric); +} + + +// listIconNavigate: cells holds each item's column and row, every number +// separated from the next by a comma (or blanks): "0,0,1,0,0,1" is three +// items in cells (0, 0), (1, 0) and (0, 1). +int32_t ListIconNavigate(int32_t keyCode, int32_t current, const char *cells, int32_t columns, int32_t pageRows) { + int32_t *cols = NULL; + int32_t *rows = NULL; + const char *p = cells ? cells : ""; + char *end = NULL; + + for (;;) { + int32_t col = (int32_t)strtol(p, &end, CELL_BASE); + + if (end == p) { + break; + } + + p = end + strspn(end, ", "); + + int32_t row = (int32_t)strtol(p, &end, CELL_BASE); + + if (end == p) { + break; + } + + p = end + strspn(end, ", "); + arrput(cols, col); + arrput(rows, row); + } + + int32_t next = listIconNavigate(keyCode, current, cols, rows, (int32_t)arrlen(cols), columns, pageRows); + + arrfree(cols); + arrfree(rows); + return next; +} + + +// listIconScrollDrag: the top row with the thumb, grabbed grab pixels +// below its top (ListIconScrollGrab), dragged to y. +int32_t ListIconScrollDrag(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t grab, int32_t y) { + ListIconGridT g; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + return listIconScrollDrag(&g, y, grab); +} + + +// listIconScrollPress: how far below the thumb's top a press at x, y is; +// -1 when it is not on the thumb. +int32_t ListIconScrollGrab(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y) { + ListIconGridT g; + int32_t row = topRow; + int32_t grab = BAS_NONE; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + return listIconScrollPress(&g, x, y, &row, &grab) == ListIconScrollThumbE ? grab : BAS_NONE; +} + + +// listIconScrollPress: the top row after a press at x, y on the scroll bar +// (the thumb leaves it as it is); -1 when x, y is not on the scroll bar. +int32_t ListIconScrollPress(int32_t view, int32_t charWidth, int32_t charHeight, int32_t width, int32_t height, int32_t count, int32_t rowsTaken, int32_t topRow, int32_t x, int32_t y) { + ListIconGridT g; + int32_t row = topRow; + int32_t grab = 0; + + iconGrid(view, charWidth, charHeight, width, height, count, rowsTaken, &g); + return listIconScrollPress(&g, x, y, &row, &grab) == ListIconScrollNoneE ? BAS_NONE : row; +} + + +// widgetNavigateIndex: the item Up / Down / Home / End / PgUp / PgDn move +// to in a list; -1 for a key that does not move. +int32_t ListNavigateIndex(int32_t keyCode, int32_t current, int32_t count, int32_t pageSize) { + return widgetNavigateIndex(keyCode, current, count, pageSize); +} + + +// listReorderDest: where an item dragged from fromIndex lands when dropped +// in front of slot toSlot (0..count; count = after the last item), or -1 +// when the drop changes nothing. +int32_t ListReorderDest(int32_t fromIndex, int32_t toSlot, int32_t count) { + return listReorderDest(fromIndex, toSlot, count); +} + + +// A click on an item: alone it selects only that item, with Ctrl it +// toggles it, with Shift it selects the range from the last plain or Ctrl +// click. +void ListSelClick(int32_t handle, int32_t index, int32_t shift) { + BasListSelT *h = handleOf(handle); + + if (h && inRange(h, index)) { + listSelClick(&h->sel, h->count, NULL, index, (shift & KEY_MOD_SHIFT) != 0, (shift & KEY_MOD_CTRL) != 0); + } +} + + +// The number of items the handle covers. +int32_t ListSelCount(int32_t handle) { + BasListSelT *h = handleOf(handle); + + return h ? h->count : 0; +} + + +// A selection of count items, several at once when multi is True; no item +// is selected. 0 when it cannot be made. +int32_t ListSelCreate(int32_t multi, int32_t count) { + int32_t idx = 0; + + if (count < 0) { + return BAS_NO_HANDLE; + } + + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + while (idx < (int32_t)arrlen(sHandles) && sHandles[idx].used) { + idx++; + } + + if (idx == (int32_t)arrlen(sHandles)) { + BasListSelT blank; + + memset(&blank, 0, sizeof(blank)); + arrput(sHandles, blank); + } + + BasListSelT *h = &sHandles[idx]; + + listSelInit(&h->sel); + h->sel.multi = multi != 0; + listSelReset(&h->sel, count); + + if (h->sel.multi && count > 0 && !h->sel.bits) { + return BAS_NO_HANDLE; + } + + h->count = count; + h->appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + h->used = true; + return idx + 1; +} + + +// The current (keyboard) item, -1 for none. +int32_t ListSelCursor(int32_t handle) { + BasListSelT *h = handleOf(handle); + + return h ? h->sel.cursor : BAS_NONE; +} + + +void ListSelFree(int32_t handle) { + BasListSelT *h = handleOf(handle); + + if (h) { + listSelFree(&h->sel); + h->used = false; + } +} + + +// An item was inserted at index (0..count): it is unselected, the others +// keep their selection. +void ListSelInsert(int32_t handle, int32_t index) { + BasListSelT *h = handleOf(handle); + + if (h && index >= 0 && index <= h->count) { + h->count++; + listSelInsert(&h->sel, h->count, index); + } +} + + +int32_t ListSelIsSelected(int32_t handle, int32_t index) { + BasListSelT *h = handleOf(handle); + + return h ? basBool(listSelIsSelected(&h->sel, h->count, index)) : BAS_FALSE; +} + + +// The selection keys of a multi-selection: Ctrl+A selects every item +// (LISTSEL_KEY_ALL), Space toggles the current one (LISTSEL_KEY_TOGGLE); +// LISTSEL_KEY_NONE for any other key. +int32_t ListSelKey(int32_t handle, int32_t keyCode, int32_t shift) { + BasListSelT *h = handleOf(handle); + + return h ? (int32_t)listSelKey(&h->sel, h->count, keyCode, shift) : (int32_t)ListSelKeyNoneE; +} + + +// The keyboard moved the current item to index: with Shift the range from +// the anchor is selected, else the selection stays as it is. +void ListSelMove(int32_t handle, int32_t index, int32_t shift) { + BasListSelT *h = handleOf(handle); + + if (h && inRange(h, index)) { + listSelMove(&h->sel, h->count, NULL, index, (shift & KEY_MOD_SHIFT) != 0); + } +} + + +// A navigation key (arrows, Home, End, PgUp, PgDn) on a list (columns 1) +// or a grid of columns per row, pageRows rows a page: the current item +// moves (ListSelMove). The new current item, or -1 when the key is not +// one of these or the current item did not change. +int32_t ListSelNavigate(int32_t handle, int32_t keyCode, int32_t shift, int32_t columns, int32_t pageRows) { + BasListSelT *h = handleOf(handle); + int32_t next; + + if (!h) { + return BAS_NONE; + } + + next = widgetGridNavigateIndex(keyCode, h->sel.cursor, h->count, columns, pageRows); + + if (!inRange(h, next) || next == h->sel.cursor) { + return BAS_NONE; + } + + listSelMove(&h->sel, h->count, NULL, next, (shift & KEY_MOD_SHIFT) != 0); + return next; +} + + +// The item at index was removed; the others keep their selection. +void ListSelRemove(int32_t handle, int32_t index) { + BasListSelT *h = handleOf(handle); + + if (h && inRange(h, index)) { + listSelRemove(&h->sel, h->count, index); + h->count--; + } +} + + +// An item moved from fromIndex to dest (ListReorderDest): its selection +// moves with it and it becomes the current item. +void ListSelReorder(int32_t handle, int32_t fromIndex, int32_t dest) { + BasListSelT *h = handleOf(handle); + + if (h && inRange(h, fromIndex) && inRange(h, dest)) { + listSelReorder(&h->sel, fromIndex, dest); + } +} + + +// The list now has count items (all new): only the current item stays +// selected, none when it is past the end. +void ListSelReset(int32_t handle, int32_t count) { + BasListSelT *h = handleOf(handle); + + if (h && count >= 0) { + h->count = count; + listSelReset(&h->sel, count); + listSelSetCursor(&h->sel, count, h->sel.cursor); + } +} + + +// How many items are selected. +int32_t ListSelSelectedCount(int32_t handle) { + BasListSelT *h = handleOf(handle); + int32_t n = 0; + + if (h) { + for (int32_t i = 0; i < h->count; i++) { + n += listSelIsSelected(&h->sel, h->count, i) ? 1 : 0; + } + } + + return n; +} + + +// Select (True) or clear (False) every item of a multi-selection. +void ListSelSetAll(int32_t handle, int32_t selected) { + BasListSelT *h = handleOf(handle); + + if (h) { + listSelSetAll(&h->sel, h->count, selected != 0); + } +} + + +// index becomes the current and only selected item (-1: none). +void ListSelSetCursor(int32_t handle, int32_t index) { + BasListSelT *h = handleOf(handle); + + if (h) { + listSelSetCursor(&h->sel, h->count, index); + } +} + + +// Select (True) or clear (False) one item of a multi-selection. +void ListSelSetItem(int32_t handle, int32_t index, int32_t selected) { + BasListSelT *h = handleOf(handle); + + if (h) { + listSelSetItem(&h->sel, h->count, index, selected != 0); + } +} + + +// widgetTypeAheadSearch: the next item after current (wrapping around) +// whose text starts with the first character of ch, any case; items is +// the texts separated by CHR$(10). -1 when none does. +int32_t ListTypeAhead(const char *ch, const char *items, int32_t current) { + const char **texts = NULL; + char *copy; + char *p; + int32_t found; + + if (!ch || !ch[0] || !items) { + return BAS_NONE; + } + + copy = strdup(items); + + if (!copy) { + return BAS_NONE; + } + + p = copy; + arrput(texts, p); + + while ((p = strchr(p, ITEM_SEPARATOR)) != NULL) { + *p++ = '\0'; + arrput(texts, p); + } + + found = widgetTypeAheadSearch(ch[0], texts, (int32_t)arrlen(texts), current); + arrfree(texts); + free(copy); + return found; +} + + +// widgetTypeAheadMatch: True when text starts with the first character of +// ch, any case. +int32_t ListTypeAheadMatch(const char *ch, const char *text) { + if (!ch || !ch[0]) { + return BAS_FALSE; + } + + return basBool(widgetTypeAheadMatch(ch[0], text)); +} diff --git a/src/libs/kpunch/listhelp/listhelp.dhs b/src/libs/kpunch/listhelp/listhelp.dhs index 2dd1cbe2..1a8a67ea 100644 --- a/src/libs/kpunch/listhelp/listhelp.dhs +++ b/src/libs/kpunch/listhelp/listhelp.dhs @@ -28,16 +28,20 @@ .index widgetDrawDropdownArrow .index widgetMaxItemLen .index widgetNavigateIndex +.index widgetGridNavigateIndex .index widgetDropdownPopupRect .index widgetPaintPopupList .index ListItemsT .index ListSelT .index ListPopupT .index listReorderMove +.index listIconGrid +.index listIconLabelSplit +.index ListIconGridT .h1 List Helper Library -Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing and keyboard handling), dropdown arrow rendering, item measurement, keyboard navigation, type-ahead search, and popup scrollbar hit testing (with the same thumb geometry the window manager paints). +Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing and keyboard handling), dropdown arrow rendering, item measurement, keyboard navigation, type-ahead search, popup scrollbar hit testing (with the same thumb geometry the window manager paints), and the icon view rules of Program Manager's view, which BASIC icon views share through listhelp.bas. Header: listhelp/listHelp.h @@ -215,6 +219,16 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t Returns the new index, clamped to [0, count-1]. Returns -1 for unrecognized keys so callers can detect whether the key was consumed. +.h2 widgetGridNavigateIndex + +The same keys in a grid of count items laid out row by row, columns items per row (icon views): Left and Right step through the items in order, Up, Down, Page Up and Page Down move by rows (pageRows rows a page) the way widgetNavigateIndex moves through a list -- it is called on the row numbers -- keeping the column, or landing on the last item when the row below is shorter; Home and End go to the first and last item. With nothing selected (current below 0) every one of these keys goes to the first item. With one column it gives what widgetNavigateIndex gives. + +.code +int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows); +.endcode + +Returns the new index, or -1 for any other key. + .h2 widgetDropdownPopupRect Calculate the screen rectangle for a dropdown popup list, positioning it below the owning widget and clamping to screen bounds. @@ -313,3 +327,136 @@ bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, .endtable Returns true if the click landed on the scrollbar (even if scrollPos was not changed, for example clicking the thumb), false if the click was in the item list area. When itemCount is less than or equal to DROPDOWN_MAX_VISIBLE, always returns false (there is no scrollbar). + +.h2 Icon Views + +The rules of an icon view with large icons, small icons or a list: Program Manager's view (progman/pmView.c) is built on them, and so are icon views written in BASIC (listhelp.bas ListIcon...), so both look and behave alike. They cover each view's cell and where an item's picture and name go in it, the grid inside a sunken frame with a scroll bar only when the rows do not fit (which narrows the grid), the two label lines under a large icon, the arrow keys among items in order or in cells of their own (gaps allowed), the scroll bar (a control's, as ListBox's: arrows, trough, thumb drag), when a press becomes a drag, the scrolling and the target cell while dragging, and where a dropped item lands. Positions are pixels from the view's top left (ListIconGridT) or the cell's (ListIconCellT). + +.table + Constant Value Description + -------- ----- ----------- + LIST_ICON_FRAME 2 Sunken frame around the view. + LIST_ICON_PAD 2 Cell edge to its picture; inside of the frame to the empty text. + LIST_ICON_LARGE 32 Large icon edge. + LIST_ICON_SMALL 16 Small icon edge. + LIST_ICON_LABEL_GAP 2 Large icon to its label. + LIST_ICON_LABEL_LINES 2 Label lines under a large icon. + LIST_ICON_TEXT_GAP 4 Small icon to its name. + LIST_ICON_LARGE_CELL_W 80 Large icon cell width. + LIST_ICON_SMALL_CHARS 14 Name beside a small icon, in characters. + LIST_ICON_LIST_CHARS 24 The list's name column, in characters. + LIST_ICON_COLUMN_GAP 1 The list's name column to its detail column, in characters. + LIST_ICON_DRAG_SLOP 4 Pixels a press moves (either way) before it drags. + LIST_ICON_WHEEL_ROWS 1 Rows a wheel notch scrolls. + LIST_ICON_LINE_ROWS 1 Rows an arrow key or a scroll bar arrow moves. + LIST_ICON_SB_W WGT_SB_W The scroll bar's width: a control's (ListBox's). + LIST_ICON_NO_CELL -1 No cell (outside the grid). + LIST_ICON_BY_ORDER -1 listIconGrid rowsTaken: the items fill the grid in order. +.endtable + +.code +typedef enum { ListIconLargeE, ListIconSmallE, ListIconListE, ListIconViewCountE } ListIconViewE; +typedef enum { ListIconScrollNoneE, ListIconScrollMovedE, ListIconScrollThumbE } ListIconScrollE; + +typedef struct { + int32_t cellW; // a list row spans the grid + int32_t cellH; + int32_t pictureX; // the picture, pictureSize square + int32_t pictureY; + int32_t pictureSize; + int32_t textX; // the name (large icons: each line centred, textX 0) + int32_t textY; + int32_t nameChars; // its length (a large icon's: per line) + int32_t detailX; // the list's detail column, 0 in the other views +} ListIconCellT; + +typedef struct { + ListIconCellT cell; + int32_t innerX; // inside the frame, less the scroll bar + int32_t innerY; + int32_t innerW; + int32_t innerH; + int32_t cols; + int32_t rowsVisible; // rows shown whole + int32_t totalRows; + int32_t maxScroll; // the largest top row + int32_t emptyX; // the empty view's text + int32_t emptyY; + bool needSb; // the scroll bar stands right of innerW, innerH tall +} ListIconGridT; + +void listIconCellLayout(ListIconViewE view, int32_t charW, int32_t charH, int32_t innerW, ListIconCellT *out); +void listIconGrid(ListIconViewE view, int32_t charW, int32_t charH, int32_t viewW, int32_t viewH, int32_t count, int32_t rowsTaken, ListIconGridT *out); +int32_t listIconGridMetric(const ListIconGridT *g, ListIconMetricE metric); +int32_t listIconCellAt(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow); +void listIconLabelSplit(const char *name, int32_t maxChars, int32_t *len1, const char **line2, int32_t *len2); +int32_t listIconNavigate(int32_t key, int32_t current, const int32_t *cols, const int32_t *rows, int32_t count, int32_t gridCols, int32_t pageRows); +bool listIconDragStarted(int32_t dx, int32_t dy); +int32_t listIconDragScroll(const ListIconGridT *g, int32_t y, int32_t scrollRow); +int32_t listIconDropCell(const ListIconGridT *g, int32_t x, int32_t y, int32_t scrollRow, int32_t *outCol, int32_t *outRow); +int32_t listIconDropDest(int32_t from, int32_t cell, int32_t count); +ListIconScrollE listIconScrollPress(const ListIconGridT *g, int32_t x, int32_t y, int32_t *pScrollRow, int32_t *outGrab); +int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab); +.endcode + +.table + Function Description + -------- ----------- + listIconCellLayout The cell of a view whose grid is innerW wide. Large icons: LIST_ICON_LARGE_CELL_W wide, the picture centred at the top, two label lines under it. Small icons and the list: a row as tall as the font or the small picture (plus padding), the picture then the name; the list's row spans the grid and adds a detail column. + listIconGrid The grid of a view viewW x viewH (frame included) holding count items: laid out without a scroll bar first, with one LIST_ICON_SB_W wide (narrowing the grid) only when the rows do not fit. rowsTaken is LIST_ICON_BY_ORDER for items in order (count / columns rows, rounded up) or the rows items placed in cells of their own take. + listIconGridMetric One value of a grid by ListIconMetricE (ListIconCellWE ... ListIconWheelRowsE; the scroll bar as 1 / 0; 0 for an unknown metric). The BASIC binding reads the grid through it. + listIconCellAt The cell number (row * cols + col) under x, y of a view scrolled to scrollRow, with its column and row; LIST_ICON_NO_CELL outside the grid or right of the last column. + listIconLabelSplit A large icon's two label lines maxChars wide: the first ends at the last blank that fits (else it is cut at maxChars), the second starts after the blanks there and is cut to maxChars. *line2 points into name; *len2 is 0 when the name fits one line. + listIconNavigate The item an arrow key, Home, End, PgUp or PgDn moves to when item i stands in cell (cols[i], rows[i]): Left / Right the nearest before / after in reading order, Up / Down the nearest row above / below with the closest column, PgUp / PgDn the same pageRows away, Home / End the first / last in reading order; with no current item the first. For items in order it gives what widgetGridNavigateIndex gives. -1 for another key or when no item is that way. + listIconDragStarted True when a press has moved LIST_ICON_DRAG_SLOP pixels either way. + listIconDragScroll The top row after a drag move to y: one row up above the grid, one down below it. + listIconDropCell The cell a dragged item would drop on with the pointer at x, y, held inside the grid's height (listIconCellAt). + listIconScrollPress A left press at x, y: ListIconScrollNoneE left of the scroll bar or without one; else widgetScrollbarApplyHit -- an arrow moves *pScrollRow LIST_ICON_LINE_ROWS, the trough a page (ListIconScrollMovedE), the thumb is grabbed (ListIconScrollThumbE, *outGrab how far below its top). + listIconScrollDrag The top row with the grabbed thumb dragged to y (widgetScrollbarThumbDragScroll). + listIconDropDest Items in order: the index an item dragged from `from` takes when dropped on cell number cell (that cell's, the last for a cell past the end); -1 when that changes nothing or from / cell is out of range. +.endtable + +Program Manager's pmView.c draws the scroll bar with widgetDrawScrollbarV; a BASIC icon view draws the same bar with DrawScrollBar's control argument True. + +.topic lib.listhelp.basic +.title List Helper Library: BASIC +.toc 1 Lists from BASIC +.index listhelp.bas +.index ListSelCreate + +.h2 BASIC + +src/include/basic/listhelp.bas gives BASIC programs -- above all UserControls that draw a list or an icon view themselves -- the rules of this library (listHelpBas.c), so their lists behave like ListBox and ListView. A selection handle (1, 2, ...; 0 = none) holds a ListSelT and the item count it covers; display order is data order (no order map). Each handle records the program that made it; the shell's app-cleanup hook frees what a program leaves open. + +.table + BASIC C it calls + ----- ---------- + ListSelCreate(multi, count), ListSelFree, ListSelReset(h, count), ListSelCount(h) listSelInit + listSelReset; listSelFree; listSelReset then listSelSetCursor (a cursor past the end becomes -1). + ListSelClick h, index, shift listSelClick; shift is the event's Shift (vbShiftMask = range, vbCtrlMask = toggle). + ListSelKey(h, keyCode, shift) listSelKey: LISTSEL_KEY_NONE, LISTSEL_KEY_ALL (Ctrl+A), LISTSEL_KEY_TOGGLE (Space). + ListSelNavigate(h, keyCode, shift, columns, pageRows) widgetGridNavigateIndex from the cursor, then listSelMove; the new cursor, or -1 when the key does not move it. + ListSelMove h, index, shift listSelMove. + ListSelCursor(h), ListSelSetCursor h, index ListSelT.cursor; listSelSetCursor. + ListSelIsSelected(h, index), ListSelSelectedCount(h) listSelIsSelected; how many are. + ListSelSetItem h, index, selected; ListSelSetAll h, selected listSelSetItem; listSelSetAll. + ListSelInsert h, index; ListSelRemove h, index listSelInsert / listSelRemove (the handle's count follows). + ListSelReorder h, fromIndex, dest listSelReorder. + ListReorderDest(fromIndex, toSlot, count) listReorderDest. + ListNavigateIndex(keyCode, current, count, pageSize) widgetNavigateIndex. + ListGridNavigate(keyCode, current, count, columns, pageRows) widgetGridNavigateIndex. + ListTypeAhead(ch, items, current) widgetTypeAheadSearch over items separated by CHR$(10). + ListTypeAheadMatch(ch, text) widgetTypeAheadMatch. + ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken) listIconGrid, then listIconGridMetric; LISTICON_SCROLLBAR as True / False. + ListIconCellAt(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) listIconGrid, then listIconCellAt. + ListIconDragScroll(mode, cw, ch, w, h, count, rowsTaken, topRow, y) listIconGrid, then listIconDragScroll. + ListIconDropCell(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) listIconGrid, then listIconDropCell. + ListIconDropDest(fromIndex, cell, count) listIconDropDest. + ListIconDragStarted(dx, dy) listIconDragStarted. + ListIconLabel$(name, maxChars, lineNo) listIconLabelSplit: line 1 or 2 ("" for any other line number or a line the name does not need). + ListIconNavigate(keyCode, current, cells, columns, pageRows) listIconNavigate over cells given as text: each item's column and row, every number separated by a comma. + ListIconScrollPress(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) listIconScrollPress: the new top row, -1 when not on the scroll bar. + ListIconScrollGrab(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y) listIconScrollPress: the grab offset when the thumb is hit, else -1. + ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y) listIconScrollDrag. +.endtable + +An icon view's grid is figured from the view's size on each call (no handle). The LISTSEL_KEY_ and LISTICON_ CONSTs of listhelp.bas are compared with ListSelKeyE, ListIconViewE, LIST_ICON_BY_ORDER and ListIconMetricE by a formrt test. diff --git a/src/libs/kpunch/music/music.c b/src/libs/kpunch/music/music.c index 176bbd5e..d7704b83 100644 --- a/src/libs/kpunch/music/music.c +++ b/src/libs/kpunch/music/music.c @@ -185,7 +185,7 @@ static int32_t ctxEnsure(void) { // instrument bank only the MPU-401 port can play it. static int32_t decideMidi(MusicT *m) { PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH); - const char *want = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO); + const char *want = audioMidiChoices[audioSettingMidi(prefs)].iniName; int32_t mode = mpuModeChoices[prefsGetChoice(prefs, AUDIO_INI_SECTION, MUSIC_INI_MPU_MODE, mpuModeChoices, MPU_MODE_COUNT, mpuModeChoices[MPU_MODE_GM].iniName)].value; bool mpu = strcasecmp(want, AUDIO_MIDI_MPU401) == 0; bool soft = strcasecmp(want, AUDIO_MIDI_SOFTWARE) == 0; diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs index ea0f0366..620fc4db 100644 --- a/src/libs/kpunch/music/music.dhs +++ b/src/libs/kpunch/music/music.dhs @@ -152,7 +152,7 @@ Every music plays through one audio context of the library's own; audio.lib mixe .h2 BASIC -src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. +src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease; see the sound library). The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount and musicBasBusy for the statements BEEP, SOUND, PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second. diff --git a/src/libs/kpunch/music/music.h b/src/libs/kpunch/music/music.h index 65dd2ee7..470603cf 100644 --- a/src/libs/kpunch/music/music.h +++ b/src/libs/kpunch/music/music.h @@ -282,6 +282,8 @@ int32_t musicBasSound(double hz, double ticks); void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value); const char *MusicError(void); void MusicFree(int32_t handle); +int32_t MusicGetVolume(int32_t handle); +const char *MusicInfo(int32_t handle, const char *field); int32_t MusicLength(int32_t handle); int32_t MusicLoad(const char *fileName); int32_t MusicMidiOpen(void); diff --git a/src/libs/kpunch/music/musicBas.c b/src/libs/kpunch/music/musicBas.c index e0e5c3c6..c85dcca0 100644 --- a/src/libs/kpunch/music/musicBas.c +++ b/src/libs/kpunch/music/musicBas.c @@ -34,13 +34,16 @@ #include "musicInt.h" +#include #include +#include #define BAS_TRUE (-1) #define BAS_FALSE 0 #define BAS_SLOTS 64 #define BAS_FIRST_CHANNEL 1 #define PERCENT_MAX 100 +#define BAS_INFO_MAX (MUSIC_TITLE_MAX + MUSIC_DESC_MAX) // QBasic's clock tick (SOUND durations): 65536 PIT clocks of 1,193,182 Hz #define QB_TICK_US (65536.0 * 1000000.0 / 1193182.0) @@ -53,40 +56,61 @@ typedef struct { int32_t appId; } BasSlotT; +// MusicInfo$ fields +typedef enum { + InfoTitleE, + InfoOutputE, + InfoKindE, + InfoWhereE, + InfoPausedE, + InfoLoopingE, + InfoCountE +} MusicInfoFieldE; + +// MusicInfo$ field names, and the words for MusicKindE and MusicOutputE +// in their enum order +static const char *const sInfoNames[InfoCountE] = { "title", "output", "kind", "where", "paused", "looping" }; +static const char *const sKindWords[] = { "module", "midi", "live" }; +static const char *const sWhereWords[] = { "", "stream", "mpu401", "fm", "software" }; + static BasSlotT sSlots[BAS_SLOTS]; static int32_t sLastError = MUSIC_OK; +static char sInfo[BAS_INFO_MAX]; // ============================================================ // Prototypes // ============================================================ -static int32_t basAdd(MusicT *music, int32_t rc); -static MusicT *basGet(int32_t handle); -static int32_t basResult(int32_t rc); -int32_t musicBasBeep(void); -bool musicBasBusy(void); -void musicBasForget(int32_t appId); -int32_t musicBasPlay(const char *macro); -int32_t musicBasPlayCount(void); -int32_t musicBasSound(double hz, double ticks); -void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value); -const char *MusicError(void); -void MusicFree(int32_t handle); -int32_t MusicLength(int32_t handle); -int32_t MusicLoad(const char *fileName); -int32_t MusicMidiOpen(void); -void MusicNoteOff(int32_t handle, int32_t channel, int32_t note); -void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity); -void MusicPause(int32_t handle); -void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend); -void MusicPlay(int32_t handle, int32_t loopIt); -int32_t MusicPlaying(int32_t handle); -int32_t MusicPosition(int32_t handle); -void MusicProgram(int32_t handle, int32_t channel, int32_t program); -void MusicResume(int32_t handle); -void MusicSeek(int32_t handle, int32_t milliseconds); -void MusicStop(int32_t handle); -void MusicVolume(int32_t handle, int32_t volume); +static int32_t basAdd(MusicT *music, int32_t rc); +static MusicT *basGet(int32_t handle); +static int32_t basResult(int32_t rc); +static const char *basWord(const char *const *words, int32_t count, int32_t value); +int32_t musicBasBeep(void); +bool musicBasBusy(void); +void musicBasForget(int32_t appId); +int32_t musicBasPlay(const char *macro); +int32_t musicBasPlayCount(void); +int32_t musicBasSound(double hz, double ticks); +void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value); +const char *MusicError(void); +void MusicFree(int32_t handle); +int32_t MusicGetVolume(int32_t handle); +const char *MusicInfo(int32_t handle, const char *field); +int32_t MusicLength(int32_t handle); +int32_t MusicLoad(const char *fileName); +int32_t MusicMidiOpen(void); +void MusicNoteOff(int32_t handle, int32_t channel, int32_t note); +void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity); +void MusicPause(int32_t handle); +void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend); +void MusicPlay(int32_t handle, int32_t loopIt); +int32_t MusicPlaying(int32_t handle); +int32_t MusicPosition(int32_t handle); +void MusicProgram(int32_t handle, int32_t channel, int32_t program); +void MusicResume(int32_t handle); +void MusicSeek(int32_t handle, int32_t milliseconds); +void MusicStop(int32_t handle); +void MusicVolume(int32_t handle, int32_t volume); // A number for a newly loaded music (rc its load result), or 0. @@ -132,6 +156,12 @@ static int32_t basResult(int32_t rc) { } +// The word for an enum value ("" out of range). +static const char *basWord(const char *const *words, int32_t count, int32_t value) { + return (value >= 0 && value < count) ? words[value] : ""; +} + + int32_t musicBasBeep(void) { return musicMmlTone(musicMmlBasic(true), QB_BEEP_MILLIHZ, QB_BEEP_US); } @@ -197,6 +227,65 @@ void MusicFree(int32_t handle) { } +// The music's own volume, percent (0 for no such music). +int32_t MusicGetVolume(int32_t handle) { + MusicT *m = basGet(handle); + + return m ? (musicGetVolume(m) * PERCENT_MAX + MUSIC_VOLUME_MAX / 2) / MUSIC_VOLUME_MAX : 0; +} + + +// A fact about the music by name (any case): "title" (a module's own +// title), "output" (where it plays, for people), "kind" ("module", +// "midi", "live"), "where" ("stream", "mpu401", "fm", "software"; "" +// before it first plays), "paused" and "looping" (True / False as text); +// "" for another field or no such music. +const char *MusicInfo(int32_t handle, const char *field) { + MusicT *m = basGet(handle); + int32_t idx = -1; + MusicInfoT info; + + if (!m) { + return ""; + } + + for (int32_t i = 0; i < InfoCountE; i++) { + if (field && strcasecmp(field, sInfoNames[i]) == 0) { + idx = i; + } + } + + musicGetInfo(m, &info); + + switch ((MusicInfoFieldE)idx) { + case InfoTitleE: + snprintf(sInfo, sizeof(sInfo), "%s", info.title); + return sInfo; + + case InfoOutputE: + snprintf(sInfo, sizeof(sInfo), "%s", info.output); + return sInfo; + + case InfoKindE: + return basWord(sKindWords, DVX_ARRAY_LEN(sKindWords), info.kind); + + case InfoWhereE: + return basWord(sWhereWords, DVX_ARRAY_LEN(sWhereWords), info.where); + + case InfoPausedE: + snprintf(sInfo, sizeof(sInfo), "%d", info.paused ? BAS_TRUE : BAS_FALSE); + return sInfo; + + case InfoLoopingE: + snprintf(sInfo, sizeof(sInfo), "%d", info.looping ? BAS_TRUE : BAS_FALSE); + return sInfo; + + default: + return ""; + } +} + + int32_t MusicLength(int32_t handle) { MusicInfoT info; diff --git a/src/libs/kpunch/print/print.dhs b/src/libs/kpunch/print/print.dhs index d4aaea0c..c61892f6 100644 --- a/src/libs/kpunch/print/print.dhs +++ b/src/libs/kpunch/print/print.dhs @@ -58,6 +58,9 @@ The printer is chosen once for everything, in DVX.INI [printer] (the Control Pan printPageSetupProblem(&ps) NULL when the page setup can be used, else the reason (a static buffer). printPageLines(&ps, model) Lines per page used: the set value, or the model's usual length (66; 60 for a laser printer). printDestChoices[] / printModelChoices[] The labels and INI spellings (PrefsChoiceT), indexed by PrintDestE / PrintModelE. + printDestLabel(d) / printModelLabel(m) The label of one choice (for programs that look the library up by name). + printDeviceProblem(dest, file) NULL when the destination can be used, else why not (printing to a file needs its name). + printDeviceStore(h, model, dest, file) Put model, destination and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it. .endtable .h2 Jobs and Printer Problems @@ -117,6 +120,7 @@ A PrintDriverT holds the resolution, the printable size in dots, the band height printDialog(ctx, title, pageCount, &choice) The common Print dialog: print to a port or a file, copies and, when pageCount > 0, all pages or a range. Starts from the settings; true with the PrintChoiceT filled when the user chose Print. printPageSetupDialog(ctx) The Page Setup dialog; saves the page setup to DVX.INI. printUiText(ctx, text, title, askFirst) Print text with the page setup on the printer in the settings (the Print dialog first when askFirst). + printUiTextChoice(ctx, text, title, &choice) Print text on the destination, copies and page range of a PrintChoiceT (a Print dialog answer, or one the program filled) without asking; printUiText ends here. printUiImageFile(ctx, path, title, askFirst) Print a picture file (BMP, PNG, JPEG, GIF). printUiWindow(ctx, win, askFirst) Print a window's contents as a picture. printUiOpenPort(ctx, port) A job with the user interface hooks on LPTn (0-based) or PRINT_PORT_DEFAULT. @@ -126,4 +130,21 @@ Jobs opened by these ask about printer problems with a Retry / Cancel message bo .h2 BASIC -src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup and PrintCanPrintPictures. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries). +src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem) and PrintSettingsStore (printDeviceStore). + +.table + BASIC C it calls + ----- ---------- + PrintPageSetting(prefs, field), PrintPageText$(prefs, field) printSettingsRead on a prefs.bas handle: one PrintPageSetupT field by name (Lines, Columns, MarginTop, MarginBottom, MarginLeft, MarginRight, TabWidth, Wrap; Header, Footer). + PrintPageSetupProblem$(lines, ..., footer) printPageSetupProblem ("" when usable). + PrintPageSettingsStore(prefs, lines, ..., footer) printSettingsWrite of the page part (the printer part as read); False when printPageSetupProblem objects. + PrintPageLines(prefs, model) printPageLines. + PrintPageCount(text) printTextPageCount with the settings in DVX.INI. + PrintTextPages(text, title, first, last, copies) printUiTextChoice on the destination in the settings. + PrintChoose(title, pageCount), PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst, PrintChoiceLast printDialog; the readers return the PrintChoiceT of the last PrintChoose. + PrintForm(form), PrintFormDialog(form) printUiWindow on a loaded form's window (these two live in the BASIC runtime, which knows the forms). + PrintPortPresent(port), PrintPortStatus(port) printPortPresent / printPortStatus; port 1-3 is LPT1-LPT3. + PrintStatusProblem$(status, afterWrite, where) printStatusProblem ("" when the status shows none). +.endtable + +print.bas also has CONSTs for the PRINT_STATUS_ bits and the page setup limits and defaults (PRINT_LINES_STANDARD ... PRINT_COPIES_MAX); a formrt test compares them with print.h. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries). diff --git a/src/libs/kpunch/print/print.h b/src/libs/kpunch/print/print.h index 7582a38b..3baf4b92 100644 --- a/src/libs/kpunch/print/print.h +++ b/src/libs/kpunch/print/print.h @@ -247,6 +247,16 @@ void printSettingsWrite(PrefsHandleT *h, const PrintSettingsT *s); // Write s into DVX.INI. False when the file could not be saved. bool printSettingsSave(const PrintSettingsT *s); +// NULL when the printer model / destination / file chosen (e.g. in the +// Control Panel) can be used, else why not: printing to a file needs +// its name. +const char *printDeviceProblem(PrintDestE dest, const char *filePath); + +// Store a printer model, destination and file name into the [printer] +// section of h with the page setup as it is in DVX.INI now (the caller +// saves h). The Control Panel's OK and print.bas PrintSettingsStore. +void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath); + // NULL when the page setup can be used, else why not. const char *printPageSetupProblem(const PrintPageSetupT *ps); @@ -349,6 +359,10 @@ bool printUiImageFile(AppContextT *ctx, const char *path, const char *tit bool printUiText(AppContextT *ctx, const char *text, const char *title, bool askFirst); bool printUiWindow(AppContextT *ctx, WindowT *win, bool askFirst); +// Print text on the destination, with the copies and page range, of a +// Print dialog answer (or one filled by the program), without asking. +bool printUiTextChoice(AppContextT *ctx, const char *text, const char *title, const PrintChoiceT *choice); + // The context the BASIC functions (print.bas) use; the BASIC runtime // sets it. void printBasSetContext(AppContextT *ctx); diff --git a/src/libs/kpunch/print/printBas.c b/src/libs/kpunch/print/printBas.c index 8972e153..10157830 100644 --- a/src/libs/kpunch/print/printBas.c +++ b/src/libs/kpunch/print/printBas.c @@ -30,28 +30,90 @@ // // LPRINT and OPEN "LPT1:" are handled by the BASIC runtime itself, through // printUiOpenPort / printJobWrite / printJobClose. - +// +// The printer settings functions (PrintModel*, PrintDest*, PrintSetting*, +// PrintSettingsProblem$, PrintSettingsStore) take a prefs.bas handle, so a +// program can edit DVX.INI [printer] the way the Control Panel does. #include "printInt.h" +#include "dvxPrefs.h" +#include #include +#include +#include +#include #define BAS_TRUE (-1) #define BAS_FALSE 0 +#define BAS_NONE (-1) +#define BAS_LPT_FIRST 1 // BASIC numbers the printer ports LPT1 = 1 + +// A number field of PrintPageSetupT by name (PrintPageSetting) +typedef struct { + const char *name; + size_t offset; +} BasPageFieldT; + +static const BasPageFieldT sPageFields[] = { + { "Lines", offsetof(PrintPageSetupT, linesPerPage) }, + { "Columns", offsetof(PrintPageSetupT, columns) }, + { "MarginTop", offsetof(PrintPageSetupT, marginTop) }, + { "MarginBottom", offsetof(PrintPageSetupT, marginBottom) }, + { "MarginLeft", offsetof(PrintPageSetupT, marginLeft) }, + { "MarginRight", offsetof(PrintPageSetupT, marginRight) }, + { "TabWidth", offsetof(PrintPageSetupT, tabWidth) } +}; + +#define BAS_PAGE_FIELD_COUNT ((int32_t)(sizeof(sPageFields) / sizeof(sPageFields[0]))) +#define BAS_PAGE_WRAP "Wrap" +#define BAS_PAGE_HEADER "Header" +#define BAS_PAGE_FOOTER "Footer" static AppContextT *sBasCtx = NULL; +static PrintChoiceT sChoice; // the last PrintChoose answer +static char sText[PRINT_MSG_MAX]; // ============================================================ // Prototypes // ============================================================ static int32_t basBool(bool value); +static bool destValid(int32_t dest); +static bool modelValid(int32_t model); +static void pageSetupFrom(PrintPageSetupT *ps, int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer); void printBasSetContext(AppContextT *ctx); int32_t PrintCanPrintPictures(void); +int32_t PrintChoiceCopies(void); +int32_t PrintChoiceDest(void); +const char *PrintChoiceFile(void); +int32_t PrintChoiceFirst(void); +int32_t PrintChoiceLast(void); +int32_t PrintChoose(const char *title, int32_t pageCount); +int32_t PrintDestCount(void); +int32_t PrintDestIsFile(int32_t dest); +const char *PrintDestLabel(int32_t dest); +int32_t PrintModelCount(void); +const char *PrintModelLabel(int32_t model); +int32_t PrintPageCount(const char *text); +int32_t PrintPageLines(int32_t prefs, int32_t model); +int32_t PrintPageSetting(int32_t prefs, const char *field); +int32_t PrintPageSettingsStore(int32_t prefs, int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer); int32_t PrintPageSetup(void); +const char *PrintPageSetupProblem(int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer); +const char *PrintPageText(int32_t prefs, const char *field); int32_t PrintPicture(const char *path, const char *title); int32_t PrintPictureDialog(const char *path, const char *title); +int32_t PrintPortPresent(int32_t port); +int32_t PrintPortStatus(int32_t port); +int32_t PrintSettingDest(int32_t prefs); +const char *PrintSettingFile(int32_t prefs); +int32_t PrintSettingModel(int32_t prefs); +const char *PrintSettingsProblem(int32_t dest, const char *filePath); +int32_t PrintSettingsStore(int32_t prefs, int32_t model, int32_t dest, const char *filePath); +const char *PrintStatusProblem(int32_t status, int32_t afterWrite, const char *where); int32_t PrintText(const char *text, const char *title); int32_t PrintTextDialog(const char *text, const char *title); +int32_t PrintTextPages(const char *text, const char *title, int32_t firstPage, int32_t lastPage, int32_t copies); static int32_t basBool(bool value) { @@ -59,6 +121,33 @@ static int32_t basBool(bool value) { } +static bool destValid(int32_t dest) { + return dest >= 0 && dest < PrintDestCountE; +} + + +static bool modelValid(int32_t model) { + return model >= 0 && model < PrintModelCountE; +} + + +// A page setup from PrintPageSettingsStore's arguments (texts too long +// are cut off). +static void pageSetupFrom(PrintPageSetupT *ps, int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer) { + memset(ps, 0, sizeof(*ps)); + ps->linesPerPage = lines; + ps->columns = columns; + ps->marginTop = marginTop; + ps->marginBottom = marginBottom; + ps->marginLeft = marginLeft; + ps->marginRight = marginRight; + ps->tabWidth = tabWidth; + ps->wrap = wrap != 0; + snprintf(ps->header, sizeof(ps->header), "%s", header ? header : ""); + snprintf(ps->footer, sizeof(ps->footer), "%s", footer ? footer : ""); +} + + void printBasSetContext(AppContextT *ctx) { sBasCtx = ctx; } @@ -73,11 +162,188 @@ int32_t PrintCanPrintPictures(void) { } +// The copies the user chose in the last PrintChoose. +int32_t PrintChoiceCopies(void) { + return sChoice.copies; +} + + +// Where the user chose to print in the last PrintChoose (a destination +// number). +int32_t PrintChoiceDest(void) { + return sChoice.dest; +} + + +// The file name chosen in the last PrintChoose when printing to a file. +const char *PrintChoiceFile(void) { + return sChoice.filePath; +} + + +// The page range chosen in the last PrintChoose (0 and 0: every page). +int32_t PrintChoiceFirst(void) { + return sChoice.firstPage; +} + + +int32_t PrintChoiceLast(void) { + return sChoice.lastPage; +} + + +// The Print dialog alone (printer, copies and, when pageCount is above +// 0, a page range), for a program that prints its own output; read the +// answer with the PrintChoice functions. True when the user chose Print. +int32_t PrintChoose(const char *title, int32_t pageCount) { + if (!sBasCtx) { + return BAS_FALSE; + } + + return basBool(printDialog(sBasCtx, title, pageCount, &sChoice)); +} + + +int32_t PrintDestCount(void) { + return PrintDestCountE; +} + + +// True for the destination that prints into a file (it needs a name). +int32_t PrintDestIsFile(int32_t dest) { + return basBool(dest == PrintDestFileE); +} + + +// "" for a number out of range. +const char *PrintDestLabel(int32_t dest) { + return destValid(dest) ? printDestLabel((PrintDestE)dest) : ""; +} + + +int32_t PrintModelCount(void) { + return PrintModelCountE; +} + + +// "" for a number out of range. +const char *PrintModelLabel(int32_t model) { + return modelValid(model) ? printModelLabel((PrintModelE)model) : ""; +} + + +// Pages text takes with the page setup and printer in the settings. +int32_t PrintPageCount(const char *text) { + PrintSettingsT s; + + printSettingsLoad(&s); + return printTextPageCount(text ? text : "", text ? (int32_t)strlen(text) : 0, &s.page, s.model); +} + + +// Lines per page of the page setup in prefs on printer model (the model's +// usual page length when the setup says 0); -1 for a model out of range. +int32_t PrintPageLines(int32_t prefs, int32_t model) { + PrintSettingsT s; + + if (!modelValid(model)) { + return BAS_NONE; + } + + printSettingsRead(prefsBasHandle(prefs), &s); + return printPageLines(&s.page, (PrintModelE)model); +} + + +// A number of the page setup in prefs: "Lines" (0 = the printer's usual +// page length), "Columns", "MarginTop", "MarginBottom", "MarginLeft", +// "MarginRight", "TabWidth", "Wrap" (True / False); -1 for an unknown +// field. +int32_t PrintPageSetting(int32_t prefs, const char *field) { + PrintSettingsT s; + + if (!field) { + return BAS_NONE; + } + + printSettingsRead(prefsBasHandle(prefs), &s); + + if (strcasecmp(field, BAS_PAGE_WRAP) == 0) { + return basBool(s.page.wrap); + } + + for (int32_t i = 0; i < BAS_PAGE_FIELD_COUNT; i++) { + if (strcasecmp(field, sPageFields[i].name) == 0) { + return *(const int32_t *)((const char *)&s.page + sPageFields[i].offset); + } + } + + return BAS_NONE; +} + + +// Put a page setup into the [printer] settings of prefs (the printer +// stays as it is there); save prefs afterwards. False for a bad handle +// or a setup PrintPageSetupProblem$ finds a problem with. +int32_t PrintPageSettingsStore(int32_t prefs, int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer) { + PrefsHandleT *h = prefsBasHandle(prefs); + PrintSettingsT s; + + if (!h) { + return BAS_FALSE; + } + + printSettingsRead(h, &s); + pageSetupFrom(&s.page, lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer); + + if (printPageSetupProblem(&s.page)) { + return BAS_FALSE; + } + + printSettingsWrite(h, &s); + return BAS_TRUE; +} + + int32_t PrintPageSetup(void) { return basBool(sBasCtx && printPageSetupDialog(sBasCtx)); } +// "" when this page setup can be used, else why not. +const char *PrintPageSetupProblem(int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer) { + PrintPageSetupT ps; + const char *problem; + + pageSetupFrom(&ps, lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer); + problem = printPageSetupProblem(&ps); + return problem ? problem : ""; +} + + +// A text of the page setup in prefs: "Header" or "Footer"; "" for an +// unknown field. +const char *PrintPageText(int32_t prefs, const char *field) { + PrintSettingsT s; + + sText[0] = '\0'; + + if (!field) { + return sText; + } + + printSettingsRead(prefsBasHandle(prefs), &s); + + if (strcasecmp(field, BAS_PAGE_HEADER) == 0) { + snprintf(sText, sizeof(sText), "%s", s.page.header); + } else if (strcasecmp(field, BAS_PAGE_FOOTER) == 0) { + snprintf(sText, sizeof(sText), "%s", s.page.footer); + } + + return sText; +} + + int32_t PrintPicture(const char *path, const char *title) { return basBool(sBasCtx && printUiImageFile(sBasCtx, path, title, false)); } @@ -88,6 +354,94 @@ int32_t PrintPictureDialog(const char *path, const char *title) { } +// True when printer port LPTn (1-3) exists. +int32_t PrintPortPresent(int32_t port) { + return basBool(port >= BAS_LPT_FIRST && port < BAS_LPT_FIRST + PRINT_PORT_COUNT && printPortPresent(port - BAS_LPT_FIRST)); +} + + +// The PRINT_STATUS_ bits of printer port LPTn (1-3); 0 for a port out of +// range. +int32_t PrintPortStatus(int32_t port) { + if (port < BAS_LPT_FIRST || port >= BAS_LPT_FIRST + PRINT_PORT_COUNT) { + return 0; + } + + return printPortStatus(port - BAS_LPT_FIRST); +} + + +// The destination in the [printer] settings of a prefs.bas handle +// (the defaults for 0). +int32_t PrintSettingDest(int32_t prefs) { + PrintSettingsT s; + + printSettingsRead(prefsBasHandle(prefs), &s); + return s.dest; +} + + +// The file printed into when the destination is a file. +const char *PrintSettingFile(int32_t prefs) { + static char path[PRINT_PATH_MAX]; + PrintSettingsT s; + + printSettingsRead(prefsBasHandle(prefs), &s); + snprintf(path, sizeof(path), "%s", s.filePath); + return path; +} + + +int32_t PrintSettingModel(int32_t prefs) { + PrintSettingsT s; + + printSettingsRead(prefsBasHandle(prefs), &s); + return s.model; +} + + +// "" when the choice can be used, else why not. +const char *PrintSettingsProblem(int32_t dest, const char *filePath) { + const char *problem; + + if (!destValid(dest)) { + return "Choose where the printer is connected."; + } + + problem = printDeviceProblem((PrintDestE)dest, filePath); + return problem ? problem : ""; +} + + +// Store model, destination and file into the [printer] settings of a +// prefs.bas handle (the page setup as it is in DVX.INI now). False for +// a bad handle or a number out of range. +int32_t PrintSettingsStore(int32_t prefs, int32_t model, int32_t dest, const char *filePath) { + PrefsHandleT *h = prefsBasHandle(prefs); + + if (!h || !modelValid(model) || !destValid(dest)) { + return BAS_FALSE; + } + + printDeviceStore(h, (PrintModelE)model, (PrintDestE)dest, filePath); + return BAS_TRUE; +} + + +// What is wrong with a printer whose status (PrintPortStatus) is status +// -- after writing a byte when afterWrite is True -- for printer where +// ("LPT1"); "" when nothing is. +const char *PrintStatusProblem(int32_t status, int32_t afterWrite, const char *where) { + sText[0] = '\0'; + + if (!printStatusProblem((uint8_t)status, afterWrite != 0, where ? where : "", sText, sizeof(sText))) { + sText[0] = '\0'; + } + + return sText; +} + + int32_t PrintText(const char *text, const char *title) { return basBool(sBasCtx && printUiText(sBasCtx, text, title, false)); } @@ -96,3 +450,24 @@ int32_t PrintText(const char *text, const char *title) { int32_t PrintTextDialog(const char *text, const char *title) { return basBool(sBasCtx && printUiText(sBasCtx, text, title, true)); } + + +// Print text without asking first on the printer in the settings: pages +// firstPage to lastPage (0 and 0: every page), copies times. +int32_t PrintTextPages(const char *text, const char *title, int32_t firstPage, int32_t lastPage, int32_t copies) { + PrintSettingsT s; + PrintChoiceT ch; + + if (!sBasCtx) { + return BAS_FALSE; + } + + printSettingsLoad(&s); + memset(&ch, 0, sizeof(ch)); + ch.dest = s.dest; + ch.firstPage = firstPage; + ch.lastPage = lastPage; + ch.copies = copies; + snprintf(ch.filePath, sizeof(ch.filePath), "%s", s.filePath); + return basBool(printUiTextChoice(sBasCtx, text, title, &ch)); +} diff --git a/src/libs/kpunch/print/printJob.c b/src/libs/kpunch/print/printJob.c index c7c9dfec..0678c9da 100644 --- a/src/libs/kpunch/print/printJob.c +++ b/src/libs/kpunch/print/printJob.c @@ -96,6 +96,8 @@ static bool askRetry(PrintJobT *job, const char *message); static bool idle(PrintJobT *job); const char *printDestLabel(PrintDestE dest); void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t outSize); +const char *printDeviceProblem(PrintDestE dest, const char *filePath); +void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath); uint32_t printJobBytes(const PrintJobT *job); bool printJobCancelled(const PrintJobT *job); bool printJobClose(PrintJobT *job); @@ -165,6 +167,30 @@ void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t out } +// NULL when the printer can be used as chosen, else why not. +const char *printDeviceProblem(PrintDestE dest, const char *filePath) { + if (dest == PrintDestFileE && (!filePath || !filePath[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 (the Page Setup dialog +// saves it there at once), so a handle read earlier does not put back an +// older one. +void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath) { + PrintSettingsT s; + + printSettingsLoad(&s); + s.model = model; + s.dest = dest; + snprintf(s.filePath, sizeof(s.filePath), "%s", filePath ? filePath : ""); + printSettingsWrite(h, &s); +} + + uint32_t printJobBytes(const PrintJobT *job) { return job ? job->bytes : 0; } diff --git a/src/libs/kpunch/print/printRaster.c b/src/libs/kpunch/print/printRaster.c index 9c9ef027..d4b44a01 100644 --- a/src/libs/kpunch/print/printRaster.c +++ b/src/libs/kpunch/print/printRaster.c @@ -333,8 +333,9 @@ bool printRasterJob(PrintJobT *job, const PrintDriverT *drv, const uint8_t *rgb, outH = 1; } - int32_t rowBytes = (int32_t)((outW + BITS_PER_BYTE - 1) / BITS_PER_BYTE); - uint8_t *band = (uint8_t *)malloc((size_t)(rowBytes * drv->bandRows)); + int32_t rowBytes = (int32_t)((outW + BITS_PER_BYTE - 1) / BITS_PER_BYTE); + size_t bandBytes = (size_t)rowBytes * (size_t)drv->bandRows; + uint8_t *band = (uint8_t *)malloc(bandBytes); if (!band) { return false; @@ -345,7 +346,7 @@ bool printRasterJob(PrintJobT *job, const PrintDriverT *drv, const uint8_t *rgb, for (int32_t y0 = 0; ok && y0 < outH; y0 += drv->bandRows) { int32_t rows = (int32_t)(outH - y0) < drv->bandRows ? (int32_t)(outH - y0) : drv->bandRows; - memset(band, 0, (size_t)(rowBytes * drv->bandRows)); + memset(band, 0, bandBytes); for (int32_t r = 0; r < rows; r++) { int32_t y = y0 + r; diff --git a/src/libs/kpunch/print/printUi.c b/src/libs/kpunch/print/printUi.c index b66e9f7c..d8bf46e0 100644 --- a/src/libs/kpunch/print/printUi.c +++ b/src/libs/kpunch/print/printUi.c @@ -120,6 +120,7 @@ static bool printRgb(AppContextT *ctx, const uint8_t *rgb, int32_t w, in bool printUiImageFile(AppContextT *ctx, const char *path, const char *title, bool askFirst); PrintJobT *printUiOpenPort(AppContextT *ctx, int32_t port); bool printUiText(AppContextT *ctx, const char *text, const char *title, bool askFirst); +bool printUiTextChoice(AppContextT *ctx, const char *text, const char *title, const PrintChoiceT *choice); bool printUiWindow(AppContextT *ctx, WindowT *win, bool askFirst); static void progressCancel(WidgetT *w); static void progressClose(WindowT *win); @@ -440,9 +441,6 @@ PrintJobT *printUiOpenPort(AppContextT *ctx, int32_t port) { bool printUiText(AppContextT *ctx, const char *text, const char *title, bool askFirst) { PrintSettingsT s; PrintChoiceT ch; - PrintTextInfoT info; - char err[PRINT_MSG_MAX]; - int32_t len = (int32_t)strlen(text); printSettingsLoad(&s); memset(&ch, 0, sizeof(ch)); @@ -450,11 +448,23 @@ bool printUiText(AppContextT *ctx, const char *text, const char *title, bool ask ch.copies = 1; snprintf(ch.filePath, sizeof(ch.filePath), "%s", s.filePath); - if (askFirst && !printDialog(ctx, UI_TITLE, printTextPageCount(text, len, &s.page, s.model), &ch)) { + if (askFirst && !printDialog(ctx, UI_TITLE, printTextPageCount(text, (int32_t)strlen(text), &s.page, s.model), &ch)) { return false; } - PrintJobT *job = openJob(ctx, ch.dest, ch.filePath, title, true, err, sizeof(err)); + return printUiTextChoice(ctx, text, title, &ch); +} + + +bool printUiTextChoice(AppContextT *ctx, const char *text, const char *title, const PrintChoiceT *choice) { + PrintSettingsT s; + PrintTextInfoT info; + char err[PRINT_MSG_MAX]; + int32_t len = (int32_t)strlen(text); + + printSettingsLoad(&s); + + PrintJobT *job = openJob(ctx, choice->dest, choice->filePath, title, true, err, sizeof(err)); if (!job) { dvxErrorBox(ctx, UI_TITLE, err); @@ -463,9 +473,9 @@ bool printUiText(AppContextT *ctx, const char *text, const char *title, bool ask memset(&info, 0, sizeof(info)); info.title = title; - info.firstPage = ch.firstPage; - info.lastPage = ch.lastPage; - info.copies = ch.copies; + info.firstPage = choice->firstPage; + info.lastPage = choice->lastPage; + info.copies = choice->copies; bool ok = printTextJob(job, s.model, text, len, &s.page, &info); bool cancelled = printJobCancelled(job); diff --git a/src/libs/kpunch/serial/Makefile b/src/libs/kpunch/serial/Makefile index e3535935..bb96528a 100644 --- a/src/libs/kpunch/serial/Makefile +++ b/src/libs/kpunch/serial/Makefile @@ -34,7 +34,7 @@ LIBSDIR = ../../../../bin/libs # One object per source; the source's subdirectory is dropped (the names # are unique across rs232/, packet/, security/ and seclink/). -SRCS = rs232/rs232.c packet/packet.c security/secHash.c security/security.c seclink/secLink.c +SRCS = rs232/rs232.c packet/packet.c security/secHash.c security/security.c security/secBas.c seclink/secLink.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS))) TARGETDIR = $(LIBSDIR)/kpunch/serial TARGET = $(TARGETDIR)/serial.lib diff --git a/src/libs/kpunch/serial/packet/packet.h b/src/libs/kpunch/serial/packet/packet.h index e3c3e169..4fdc1913 100644 --- a/src/libs/kpunch/serial/packet/packet.h +++ b/src/libs/kpunch/serial/packet/packet.h @@ -66,10 +66,10 @@ // Error codes #define PKT_SUCCESS 0 -#define PKT_ERR_INVALID_PARAM -1 -#define PKT_ERR_TX_FULL -2 -#define PKT_ERR_DISCONNECTED -3 -#define PKT_ERR_TIMEOUT -4 +#define PKT_ERR_INVALID_PARAM (-1) +#define PKT_ERR_TX_FULL (-2) +#define PKT_ERR_DISCONNECTED (-3) +#define PKT_ERR_TIMEOUT (-4) // A blocking send gives up when the peer acknowledges nothing for this // long (a silent or vanished peer must not hang the caller forever). diff --git a/src/libs/kpunch/serial/rs232/rs232.c b/src/libs/kpunch/serial/rs232/rs232.c index 45e1944b..0d5f44e7 100644 --- a/src/libs/kpunch/serial/rs232/rs232.c +++ b/src/libs/kpunch/serial/rs232/rs232.c @@ -265,10 +265,10 @@ DVX_ABI_STAMP(serial); // LSR bits #define LSR_DATA_READY 0x01 -#define LSR_OVERRUN 0x02 -#define LSR_PARITY_ERR 0x04 -#define LSR_FRAME_ERR 0x08 -#define LSR_BREAK 0x10 +#define LSR_OVERRUN RS232_LSR_OVERRUN +#define LSR_PARITY_ERR RS232_LSR_PARITY_ERR +#define LSR_FRAME_ERR RS232_LSR_FRAME_ERR +#define LSR_BREAK RS232_LSR_BREAK #define LSR_TX_HOLD_EMPTY 0x20 #define LSR_TX_EMPTY 0x40 #define LSR_FIFO_ERR 0x80 @@ -280,7 +280,7 @@ DVX_ABI_STAMP(serial); #define MSR_DLT_DCD 0x08 #define MSR_CTS 0x10 #define MSR_DSR 0x20 -#define MSR_RI 0x40 +#define MSR_RI RS232_MSR_RI #define MSR_DCD 0x80 // Transmit FIFO depth of a 16550A; every other UART holds one byte diff --git a/src/libs/kpunch/serial/rs232/rs232.h b/src/libs/kpunch/serial/rs232/rs232.h index 6665213e..963cf481 100644 --- a/src/libs/kpunch/serial/rs232/rs232.h +++ b/src/libs/kpunch/serial/rs232/rs232.h @@ -77,22 +77,31 @@ // Error Codes #define RS232_SUCCESS 0 -#define RS232_ERR_UNKNOWN -1 -#define RS232_ERR_NOT_OPEN -2 -#define RS232_ERR_ALREADY_OPEN -3 -#define RS232_ERR_NO_UART -4 -#define RS232_ERR_INVALID_PORT -5 -#define RS232_ERR_INVALID_BASE -6 -#define RS232_ERR_INVALID_IRQ -7 -#define RS232_ERR_INVALID_BPS -8 -#define RS232_ERR_INVALID_DATA -9 -#define RS232_ERR_INVALID_PARITY -10 -#define RS232_ERR_INVALID_STOP -11 -#define RS232_ERR_INVALID_HANDSHAKE -12 -#define RS232_ERR_INVALID_FIFO -13 -#define RS232_ERR_NULL_PTR -14 -#define RS232_ERR_IRQ_NOT_FOUND -15 -#define RS232_ERR_LOCK_MEM -16 +#define RS232_ERR_UNKNOWN (-1) +#define RS232_ERR_NOT_OPEN (-2) +#define RS232_ERR_ALREADY_OPEN (-3) +#define RS232_ERR_NO_UART (-4) +#define RS232_ERR_INVALID_PORT (-5) +#define RS232_ERR_INVALID_BASE (-6) +#define RS232_ERR_INVALID_IRQ (-7) +#define RS232_ERR_INVALID_BPS (-8) +#define RS232_ERR_INVALID_DATA (-9) +#define RS232_ERR_INVALID_PARITY (-10) +#define RS232_ERR_INVALID_STOP (-11) +#define RS232_ERR_INVALID_HANDSHAKE (-12) +#define RS232_ERR_INVALID_FIFO (-13) +#define RS232_ERR_NULL_PTR (-14) +#define RS232_ERR_IRQ_NOT_FOUND (-15) +#define RS232_ERR_LOCK_MEM (-16) + +// Line status bits (rs232GetLsr): errors seen on received data +#define RS232_LSR_OVERRUN 0x02 +#define RS232_LSR_PARITY_ERR 0x04 +#define RS232_LSR_FRAME_ERR 0x08 +#define RS232_LSR_BREAK 0x10 + +// Modem status bit (rs232GetMsr): the ring indicator line is up +#define RS232_MSR_RI 0x40 // Buffer management diff --git a/src/libs/kpunch/serial/seclink/secLink.h b/src/libs/kpunch/serial/seclink/secLink.h index cd62abbd..26e9c2dd 100644 --- a/src/libs/kpunch/serial/seclink/secLink.h +++ b/src/libs/kpunch/serial/seclink/secLink.h @@ -81,14 +81,14 @@ // Error codes #define SECLINK_SUCCESS 0 -#define SECLINK_ERR_PARAM -1 -#define SECLINK_ERR_SERIAL -2 -#define SECLINK_ERR_ALLOC -3 -#define SECLINK_ERR_HANDSHAKE -4 -#define SECLINK_ERR_NOT_READY -5 -#define SECLINK_ERR_SEND -6 -#define SECLINK_ERR_TIMEOUT -7 -#define SECLINK_ERR_AUTH -8 // passphrase mismatch or tampering +#define SECLINK_ERR_PARAM (-1) +#define SECLINK_ERR_SERIAL (-2) +#define SECLINK_ERR_ALLOC (-3) +#define SECLINK_ERR_HANDSHAKE (-4) +#define SECLINK_ERR_NOT_READY (-5) +#define SECLINK_ERR_SEND (-6) +#define SECLINK_ERR_TIMEOUT (-7) +#define SECLINK_ERR_AUTH (-8) // passphrase mismatch or tampering #define SECLINK_PENDING 1 // secLinkHandshakeStep: exchange still running // Wire protocol version carried in HELLO diff --git a/src/libs/kpunch/serial/security/secBas.c b/src/libs/kpunch/serial/security/secBas.c new file mode 100644 index 00000000..9c6a9656 --- /dev/null +++ b/src/libs/kpunch/serial/security/secBas.c @@ -0,0 +1,538 @@ +// 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. + +// secBas.c -- DVX BASIC entry points of the security library +// (src/include/basic/security.bas) +// +// Random bytes, the Diffie-Hellman key exchange, the XTEA-CTR cipher, +// SHA-256, HMAC-SHA256, HKDF and a constant-time compare, over the same +// functions secLink and C programs use. Called through DECLARE LIBRARY: +// keys, data and results are BINARY STRING (pointer + length in, a result +// buffer + length out; claudeDocs/round2/binaryStrings.md), so every byte +// value passes. A result stays valid until the next call that returns +// one (the BASIC runtime copies it at once); the result buffer is wiped +// before it is reused or grown, as it may hold key material. +// +// A handle (1, 2, ...; 0 means none) is one key exchange, cipher, SHA-256 +// or HMAC in progress. Each handle records the program that made it; the +// shell's app-cleanup hook frees (and wipes) what a program leaves open. + +#include "security.h" +#include "secHash.h" +#include "dvxMem.h" +#include "dvxPlat.h" + +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" + +typedef enum { + BasSecFreeE, + BasSecDhE, + BasSecCipherE, + BasSecSha256E, + BasSecHmacE +} BasSecKindE; + +// One handle: what it holds and the program it belongs to +typedef struct { + void *obj; // SecDhT, SecCipherT, SecSha256T or SecHmacT + BasSecKindE kind; // BasSecFreeE: the handle is free + int32_t appId; +} BasSecT; + +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// this library loads before the shell, and the host tests may have none. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); + +static BasSecT *sHandles = NULL; // handle = index + 1 +static int32_t sHandleCount = 0; +static bool sCleanupHooked = false; +static uint8_t *sOut = NULL; // the last result +static int32_t sOutCap = 0; + +// ============================================================ +// Prototypes +// ============================================================ + +static void appEnded(int32_t appId); +static int32_t handleNew(BasSecKindE kind, void *obj); +static void *handleObj(int32_t handle, BasSecKindE kind); +static void objFree(BasSecKindE kind, void *obj); +static const uint8_t *result(int32_t len, int32_t *outLen); +void SecAddEntropy(const uint8_t *data, int32_t len); +int32_t SecCipherCreate(const uint8_t *key, int32_t len); +const uint8_t *SecCipherCrypt(int32_t handle, const uint8_t *data, int32_t len, int32_t *outLen); +void SecCipherFree(int32_t handle); +void SecCipherSetCounter(int32_t handle, double counter); +int32_t SecDhComputeSecret(int32_t handle, const uint8_t *remotePub, int32_t len); +int32_t SecDhCreate(void); +void SecDhFree(int32_t handle); +const uint8_t *SecDhPublicKey(int32_t handle, int32_t *outLen); +const uint8_t *SecDhSecret(int32_t handle, int32_t *outLen); +int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen); +static void secFree(int32_t handle); +const uint8_t *SecHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, int32_t length, int32_t *outLen); +const uint8_t *SecHmac(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, int32_t *outLen); +void SecHmacAdd(int32_t handle, const uint8_t *data, int32_t len); +const uint8_t *SecHmacEnd(int32_t handle, int32_t *outLen); +int32_t SecHmacOpen(const uint8_t *key, int32_t len); +const uint8_t *SecRandom(int32_t count, int32_t *outLen); +const uint8_t *SecSha256(const uint8_t *data, int32_t len, int32_t *outLen); +void SecSha256Add(int32_t handle, const uint8_t *data, int32_t len); +const uint8_t *SecSha256End(int32_t handle, int32_t *outLen); +int32_t SecSha256Open(void); + + +// Shell app-cleanup hook: free what a program that ended left open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < sHandleCount; i++) { + if (sHandles[i].kind != BasSecFreeE && sHandles[i].appId == appId) { + secFree(i + 1); + } + } +} + + +// A handle for obj (owned by it from now on), or 0 (obj freed) when out of +// memory. +static int32_t handleNew(BasSecKindE kind, void *obj) { + int32_t idx = 0; + + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } + } + + while (idx < sHandleCount && sHandles[idx].kind != BasSecFreeE) { + idx++; + } + + if (idx == sHandleCount) { + BasSecT *grown = (BasSecT *)realloc(sHandles, (size_t)(sHandleCount + 1) * sizeof(BasSecT)); + + if (!grown) { + objFree(kind, obj); + return BAS_NO_HANDLE; + } + + sHandles = grown; + sHandleCount++; + } + + sHandles[idx].obj = obj; + sHandles[idx].kind = kind; + sHandles[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; + return idx + 1; +} + + +// What handle holds when it is a kind handle, else NULL. +static void *handleObj(int32_t handle, BasSecKindE kind) { + if (handle <= BAS_NO_HANDLE || handle > sHandleCount || sHandles[handle - 1].kind != kind) { + return NULL; + } + + return sHandles[handle - 1].obj; +} + + +// Free (and wipe) an object of kind. +static void objFree(BasSecKindE kind, void *obj) { + switch (kind) { + case BasSecDhE: + secDhDestroy((SecDhT *)obj); + break; + + case BasSecCipherE: + secCipherDestroy((SecCipherT *)obj); + break; + + case BasSecSha256E: + secSecureZero(obj, sizeof(SecSha256T)); + free(obj); + break; + + case BasSecHmacE: + secSecureZero(obj, sizeof(SecHmacT)); + free(obj); + break; + + case BasSecFreeE: + break; + } +} + + +// The result buffer, wiped and at least len bytes; NULL (and *outLen 0) +// when out of memory. *outLen is set to len. +static const uint8_t *result(int32_t len, int32_t *outLen) { + *outLen = 0; + + if (sOut) { + secSecureZero(sOut, sOutCap); + } + + if (len > sOutCap) { + uint8_t *grown = (uint8_t *)malloc((size_t)len); + + if (!grown) { + return NULL; + } + + free(sOut); + sOut = grown; + sOutCap = len; + } + + *outLen = len; + return sOut; +} + + +// Mix bytes (key presses' timing, mouse positions ...) into the random +// pool. +void SecAddEntropy(const uint8_t *data, int32_t len) { + if (data && len > 0) { + secRngAddEntropy(data, len); + } +} + + +// An XTEA-CTR cipher with key (SEC_XTEA_KEY_SIZE bytes), counter 0. 0 +// for a key of another length. +int32_t SecCipherCreate(const uint8_t *key, int32_t len) { + SecCipherT *c; + + if (!key || len != SEC_XTEA_KEY_SIZE) { + return BAS_NO_HANDLE; + } + + c = secCipherCreate(key); + return c ? handleNew(BasSecCipherE, c) : BAS_NO_HANDLE; +} + + +// data encrypted -- or decrypted, the same in CTR mode -- with the cipher; +// its counter moves on one per 8 bytes. "" for a bad handle. +const uint8_t *SecCipherCrypt(int32_t handle, const uint8_t *data, int32_t len, int32_t *outLen) { + SecCipherT *c = (SecCipherT *)handleObj(handle, BasSecCipherE); + const uint8_t *out; + + if (!c || !data || len <= 0) { + *outLen = 0; + return NULL; + } + + out = result(len, outLen); + + if (out) { + memcpy(sOut, data, (size_t)len); + secCipherCrypt(c, sOut, len); + } + + return out; +} + + +void SecCipherFree(int32_t handle) { + if (handleObj(handle, BasSecCipherE)) { + secFree(handle); + } +} + + +// Position the counter (a whole number; exact up to 2^53). The same key +// and counter must never encrypt two different messages. +void SecCipherSetCounter(int32_t handle, double counter) { + SecCipherT *c = (SecCipherT *)handleObj(handle, BasSecCipherE); + + if (c && counter >= 0.0) { + secCipherSetCounter(c, (uint64_t)counter); + } +} + + +// Combine our keys with the other side's public key: SEC_OK, or a SEC_ERR_ +// code for a bad handle or an unacceptable key. +int32_t SecDhComputeSecret(int32_t handle, const uint8_t *remotePub, int32_t len) { + SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE); + + if (!dh || !remotePub) { + return SEC_ERR_PARAM; + } + + return secDhComputeSecret(dh, remotePub, len); +} + + +// A key exchange with fresh keys (secDhCreate + secDhGenerateKeys; can take +// a few seconds on a slow computer). 0 when it could not be made. +int32_t SecDhCreate(void) { + SecDhT *dh = secDhCreate(); + + if (!dh) { + return BAS_NO_HANDLE; + } + + if (secDhGenerateKeys(dh) != SEC_SUCCESS) { + secDhDestroy(dh); + return BAS_NO_HANDLE; + } + + return handleNew(BasSecDhE, dh); +} + + +void SecDhFree(int32_t handle) { + if (handleObj(handle, BasSecDhE)) { + secFree(handle); + } +} + + +// Our public key (SEC_DH_KEY_SIZE bytes) to send to the other side; "" +// for a bad handle. +const uint8_t *SecDhPublicKey(int32_t handle, int32_t *outLen) { + SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE); + int len = SEC_DH_KEY_SIZE; + const uint8_t *out; + + *outLen = 0; + + if (!dh || !(out = result(SEC_DH_KEY_SIZE, outLen))) { + return NULL; + } + + if (secDhGetPublicKey(dh, sOut, &len) != SEC_SUCCESS) { + *outLen = 0; + return NULL; + } + + *outLen = len; + return out; +} + + +// The shared secret (SEC_DH_KEY_SIZE bytes) after SecDhComputeSecret; "" +// before it. Make keys from it with SecHkdf$, never use it as a key. +const uint8_t *SecDhSecret(int32_t handle, int32_t *outLen) { + SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE); + int len = SEC_DH_KEY_SIZE; + const uint8_t *out; + + *outLen = 0; + + if (!dh || !(out = result(SEC_DH_KEY_SIZE, outLen))) { + return NULL; + } + + if (secDhGetSecret(dh, sOut, &len) != SEC_SUCCESS) { + *outLen = 0; + return NULL; + } + + *outLen = len; + return out; +} + + +// True when a and b hold the same bytes, compared in a time that does not +// depend on where they differ (for MACs and passwords). +int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen) { + if (aLen != bLen) { + return BAS_FALSE; + } + + return (aLen == 0 || secConstTimeEqual(a, b, aLen)) ? BAS_TRUE : BAS_FALSE; +} + + +// Free (and wipe) what handle holds. +static void secFree(int32_t handle) { + BasSecT *h = &sHandles[handle - 1]; + + objFree(h->kind, h->obj); + h->obj = NULL; + h->kind = BasSecFreeE; +} + + +// HKDF-SHA256: length bytes (1 to SEC_HKDF_MAX_OUT) of key material from +// ikm, with salt ("" = none) and info; "" for a length out of range. +const uint8_t *SecHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, int32_t length, int32_t *outLen) { + const uint8_t *out; + + *outLen = 0; + + if (length < 1 || length > SEC_HKDF_MAX_OUT || !(out = result(length, outLen))) { + return NULL; + } + + if (secHkdf(salt, saltLen, ikm, ikmLen, info, infoLen, sOut, length) != SEC_SUCCESS) { + *outLen = 0; + return NULL; + } + + return out; +} + + +// HMAC-SHA256 of data with key (SEC_SHA256_SIZE bytes). +const uint8_t *SecHmac(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, int32_t *outLen) { + const uint8_t *out = result(SEC_SHA256_SIZE, outLen); + + if (out) { + secHmacSha256(key, keyLen, data, len, sOut); + } + + return out; +} + + +void SecHmacAdd(int32_t handle, const uint8_t *data, int32_t len) { + SecHmacT *h = (SecHmacT *)handleObj(handle, BasSecHmacE); + + if (h && data && len > 0) { + secHmacUpdate(h, data, len); + } +} + + +// The HMAC of everything added; frees the handle. "" for a bad handle. +const uint8_t *SecHmacEnd(int32_t handle, int32_t *outLen) { + SecHmacT *h = (SecHmacT *)handleObj(handle, BasSecHmacE); + const uint8_t *out; + + *outLen = 0; + + if (!h) { + return NULL; + } + + out = result(SEC_SHA256_SIZE, outLen); + + if (out) { + secHmacFinal(h, sOut); + } + + secFree(handle); + return out; +} + + +// An HMAC-SHA256 to add data to piece by piece (SecHmacAdd, SecHmacEnd$). +int32_t SecHmacOpen(const uint8_t *key, int32_t len) { + SecHmacT *h = (SecHmacT *)malloc(sizeof(SecHmacT)); + + if (!h) { + return BAS_NO_HANDLE; + } + + secHmacInit(h, key, len); + return handleNew(BasSecHmacE, h); +} + + +// count random bytes from the security library's generator (for keys, +// salts and counters; RND is not for those). "" for count below 1. +const uint8_t *SecRandom(int32_t count, int32_t *outLen) { + const uint8_t *out; + + *outLen = 0; + + if (count < 1 || !(out = result(count, outLen))) { + return NULL; + } + + secRngBytes(sOut, count); + return out; +} + + +// SHA-256 of data (SEC_SHA256_SIZE bytes). +const uint8_t *SecSha256(const uint8_t *data, int32_t len, int32_t *outLen) { + const uint8_t *out = result(SEC_SHA256_SIZE, outLen); + + if (out) { + secSha256(data, len, sOut); + } + + return out; +} + + +void SecSha256Add(int32_t handle, const uint8_t *data, int32_t len) { + SecSha256T *ctx = (SecSha256T *)handleObj(handle, BasSecSha256E); + + if (ctx && data && len > 0) { + secSha256Update(ctx, data, len); + } +} + + +// The SHA-256 of everything added; frees the handle. "" for a bad +// handle. +const uint8_t *SecSha256End(int32_t handle, int32_t *outLen) { + SecSha256T *ctx = (SecSha256T *)handleObj(handle, BasSecSha256E); + const uint8_t *out; + + *outLen = 0; + + if (!ctx) { + return NULL; + } + + out = result(SEC_SHA256_SIZE, outLen); + + if (out) { + secSha256Final(ctx, sOut); + } + + secFree(handle); + return out; +} + + +// A SHA-256 to add data to piece by piece (SecSha256Add, SecSha256End$). +int32_t SecSha256Open(void) { + SecSha256T *ctx = (SecSha256T *)malloc(sizeof(SecSha256T)); + + if (!ctx) { + return BAS_NO_HANDLE; + } + + secSha256Init(ctx); + return handleNew(BasSecSha256E, ctx); +} diff --git a/src/libs/kpunch/serial/security/security.h b/src/libs/kpunch/serial/security/security.h index 801949cb..1b7cb17a 100644 --- a/src/libs/kpunch/serial/security/security.h +++ b/src/libs/kpunch/serial/security/security.h @@ -63,9 +63,9 @@ // Error codes #define SEC_SUCCESS 0 -#define SEC_ERR_PARAM -1 -#define SEC_ERR_NOT_READY -2 -#define SEC_ERR_IO -3 +#define SEC_ERR_PARAM (-1) +#define SEC_ERR_NOT_READY (-2) +#define SEC_ERR_IO (-3) // Opaque types typedef struct SecDhS SecDhT; diff --git a/src/libs/kpunch/serial/serial.dhs b/src/libs/kpunch/serial/serial.dhs index 4d204f13..04559f5f 100644 --- a/src/libs/kpunch/serial/serial.dhs +++ b/src/libs/kpunch/serial/serial.dhs @@ -258,9 +258,9 @@ rs232GetParity returns a lowercase character on success. Callers comparing again rs232GetDsr(com) DSR line state (0 or 1). rs232GetDtr(com) DTR line state (0 or 1). rs232GetRts(com) RTS line state (0 or 1). - rs232GetLsr(com) Line status register value. + rs232GetLsr(com) Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR and RS232_LSR_BREAK are its error bits. rs232GetMcr(com) Modem control register value. - rs232GetMsr(com) Modem status register value. + rs232GetMsr(com) Modem status register value; RS232_MSR_RI is the ring indicator. .endtable .h2 Configuration Setters @@ -495,6 +495,7 @@ Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIM .index secRngBytes .index secRngAddEntropy .index secRngLoadSeed +.index security.bas .h1 Security (DH, XTEA, SHA-256) @@ -621,6 +622,30 @@ void secCipherDestroy(SecCipherT *c); secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0. +.h2 BASIC + +src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open. + +.table + BASIC C it calls + ----- ---------- + SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy. + SecDhCreate() secDhCreate + secDhGenerateKeys. + SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret). + SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code. + SecDhFree h secDhDestroy. + SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes. + SecCipherCrypt$(h, bytes) secCipherCrypt on a copy. + SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53). + SecCipherFree h secCipherDestroy. + SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle). + SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final. + SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT). + SecEqual(a, b) secConstTimeEqual (False for different lengths). +.endtable + +The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a formrt test, which also calls every function for real. + .topic lib.serial.seclink .title Secure Link .toc 1 Secure Link diff --git a/src/libs/kpunch/sql/Makefile b/src/libs/kpunch/sql/Makefile index 830d8a1e..072b1e51 100644 --- a/src/libs/kpunch/sql/Makefile +++ b/src/libs/kpunch/sql/Makefile @@ -56,11 +56,13 @@ OBJS = $(WRAP_SRCS:%.c=$(OBJDIR)/%.o) $(OBJDIR)/sqlite3.o # Builds the same amalgamation, VFS and wrapper with the host gcc into a # static archive the test executables link with -rdynamic so the # datactrl widget .so resolves dvxSql* at load time. SAN/COV per -# common.mk; the amalgamation keeps the sanitizers but not gcov. +# common.mk; the amalgamation keeps the build's instrumentation (the +# sanitizers, or MemorySanitizer for src/test/lint msan, whose reports +# are false without it) but not gcov. HOSTOBJDIR = $(HOST_OBJROOT)/sql HOSTTARGET = $(HOST_OBJROOT)/dvxsql.a HOSTCFLAGS = $(HOST_CFLAGS) -I../libdvx -I../libdvx/platform $(SQLITE_OPTS) -HOSTSQLITEFLAGS = -O1 -g -fPIC -w -D_GNU_SOURCE $(SQLITE_OPTS) $(if $(filter host,$(HOST_SUB)),$(SAN_FLAGS)) +HOSTSQLITEFLAGS = -O1 -g -fPIC -w -D_GNU_SOURCE $(SQLITE_OPTS) $(if $(filter hostcov,$(HOST_SUB)),,$(HOST_INSTFLAGS)) HOST_OBJS = $(WRAP_SRCS:%.c=$(HOSTOBJDIR)/%.o) $(HOSTOBJDIR)/sqlite3.o diff --git a/src/libs/kpunch/taskmgr/shellTaskMgr.c b/src/libs/kpunch/taskmgr/shellTaskMgr.c index cb7f3813..7d36613a 100644 --- a/src/libs/kpunch/taskmgr/shellTaskMgr.c +++ b/src/libs/kpunch/taskmgr/shellTaskMgr.c @@ -188,26 +188,8 @@ static void onTmSwitchTo(WidgetT *w) { ShellAppT *app = selectedApp(); - if (!app) { - return; - } - - // Scan the window stack top-down (highest Z first) to find the app's - // topmost visible window, restore it if minimized, then raise and focus - // it. Hidden windows (e.g. a hidden tool window) are skipped. - for (int32_t j = sCtx->stack.count - 1; j >= 0; j--) { - WindowT *win = sCtx->stack.windows[j]; - - if (win->appId == app->appId && win->visible && !win->destroyPending) { - // dvxRestoreWindow would also un-maximize; only un-minimize. - if (win->minimized) { - dvxRestoreWindow(sCtx, win); - } else { - dvxRaiseWindow(sCtx, win); - } - - return; - } + if (app) { + shellActivateApp(sCtx, app->appId); } } diff --git a/src/libs/kpunch/taskmgr/taskmgr.dhs b/src/libs/kpunch/taskmgr/taskmgr.dhs index 1ff8ca2b..71392ca7 100644 --- a/src/libs/kpunch/taskmgr/taskmgr.dhs +++ b/src/libs/kpunch/taskmgr/taskmgr.dhs @@ -50,8 +50,8 @@ The Task Manager reads from the shell's app slot table (shellGetApp, shellAppSlo .list .item Six-column ListView showing Name, Title, File, Type (Task or Callback), Memory (from dvxMemGetAppUsage), and Status for every running app. Status is Running, or Not responding when dvxAppIsHung reports the app has been inside one of its window callbacks for DVX_HUNG_APP_MS (5 seconds) without a modal dialog of its own open; the open window re-checks every second (a shell idle handler) and refreshes when a status changes. -.item Switch To button (Alt+S): raises and focuses the selected app's topmost visible window, restoring it if minimized. Also triggered by double-clicking a row. -.item End Task button (Alt+E): force-kills the selected app via shellForceKillApp. Every refresh keeps the selection on the same app; when that app is gone the selection moves to the row that took its place (or the new last row). +.item Switch To button (Alt+S): raises and focuses the selected app's topmost visible window, restoring it if minimized (shellActivateApp, which BASIC's ShellRunningActivate calls too). Also triggered by double-clicking a row. +.item End Task button (Alt+E): force-kills the selected app via shellForceKillApp (BASIC: ShellRunningKill). Every refresh keeps the selection on the same app; when that app is gone the selection moves to the row that took its place (or the new last row). .item Run... button (Alt+R): the shell's Run dialog (shellRunDialog, no history): a program with arguments, or a document, which opens in its associated program (shellRunCommand). .item Status bar showing running app count plus total and used system memory. .item Single-instance: reopening the Task Manager while it is already visible raises and focuses the existing window rather than creating a new one. diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c index b163e264..508b1002 100644 --- a/src/libs/kpunch/texthelp/textHelp.c +++ b/src/libs/kpunch/texthelp/textHelp.c @@ -99,10 +99,16 @@ int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_ static bool textEditMatchAt(const char *buf, int32_t pos, const char *needle, int32_t needleLen, bool caseSensitive); int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len); void textEditMoveCursor(int32_t newPos, bool shift, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); +bool textEditMultiCopy(const TextEditMultiT *te); +bool textEditMultiCut(TextEditMultiT *te); static void textEditMultiDeleteSelection(TextEditLineCacheT *lc, char *buf, int32_t *pLen, int32_t *pRow, int32_t *pCol, int32_t *pSelAnchor, int32_t *pSelCursor); void textEditMultiFree(TextEditMultiT *te); bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth); static bool textEditMultiMoveCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t key, int32_t visRows, int32_t *pRow, int32_t *pCol, int32_t *pDesiredCol); +bool textEditMultiPaste(TextEditMultiT *te); +bool textEditMultiReplaceSel(TextEditMultiT *te, const char *text, int32_t textLen); +void textEditMultiSelectAll(TextEditMultiT *te); +bool textEditMultiUndo(TextEditMultiT *te); void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col); int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); @@ -112,6 +118,9 @@ void textEditScrollToCursor(int32_t cursor, int32_t visibleChars, int3 bool textEditSingleClearSelection(TextEditSingleT *te); void textEditSingleFree(TextEditSingleT *te); bool textEditSingleInit(TextEditSingleT *te, int32_t maxLen); +bool textEditSingleReplaceSel(TextEditSingleT *te, const char *text, int32_t textLen); +bool textEditSingleResize(TextEditSingleT *te, int32_t maxLen); +void textEditSingleSelRange(const TextEditSingleT *te, int32_t *lo, int32_t *hi); void textEditSingleSetText(TextEditSingleT *te, const char *text); uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom); bool textEditUndoSwap(char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); @@ -127,7 +136,7 @@ void widgetTextEditDragUpdateLine(TextEditSingleT *te, int32_t vx, int void widgetTextEditMouseClick(WidgetT *w, TextEditSingleT *te, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, bool wordSelect, bool dragSelect); void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor); void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor); -void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts); +void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMultiT *te, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts); void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks); void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT *te, int32_t fieldWidth); void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX); @@ -757,6 +766,27 @@ void textEditMoveCursor(int32_t newPos, bool shift, int32_t *pCursor, int32_t *p } +// Copies the selection to the clipboard (Ctrl+C, the Copy method). +// False when nothing is selected. +bool textEditMultiCopy(const TextEditMultiT *te) { + if (te->selAnchor < 0 || te->selCursor < 0 || te->selAnchor == te->selCursor) { + return false; + } + + int32_t lo = DVX_MIN(te->selAnchor, te->selCursor); + + clipboardCopy(te->buf + lo, DVX_MAX(te->selAnchor, te->selCursor) - lo); + return true; +} + + +// Copies the selection to the clipboard and removes it (Ctrl+X, the Cut +// method). True when the text changed. +bool textEditMultiCut(TextEditMultiT *te) { + return textEditMultiCopy(te) && textEditMultiReplaceSel(te, "", 0); +} + + // Removes the selected range, leaves the cursor at its low end and the // line cache dirty (the range may have spanned newlines). static void textEditMultiDeleteSelection(TextEditLineCacheT *lc, char *buf, int32_t *pLen, int32_t *pRow, int32_t *pCol, int32_t *pSelAnchor, int32_t *pSelCursor) { @@ -780,12 +810,19 @@ void textEditMultiFree(TextEditMultiT *te) { // Allocates the buf/undoBuf pair sized for maxLen characters plus -// NUL, and initializes cursor/scroll/selection/undo state. Returns +// NUL (TEXT_EDIT_DEFAULT_MAX_LEN when maxLen <= 0, at most +// TEXT_EDIT_MULTI_MAX_LEN), and initializes cursor/scroll/selection/undo state. Returns // false (leaving te->buf == NULL) if allocation fails. bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) { memset(te, 0, sizeof(*te)); - int32_t bufSize = (maxLen > 0 ? maxLen : TEXT_EDIT_DEFAULT_MAX_LEN) + 1; + if (maxLen <= 0) { + maxLen = TEXT_EDIT_DEFAULT_MAX_LEN; + } else if (maxLen > TEXT_EDIT_MULTI_MAX_LEN) { + maxLen = TEXT_EDIT_MULTI_MAX_LEN; + } + + int32_t bufSize = maxLen + 1; te->buf = (char *)malloc(bufSize); te->undoBuf = (char *)malloc(bufSize); @@ -870,6 +907,81 @@ static bool textEditMultiMoveCursor(TextEditLineCacheT *lc, const char *buf, int } +// Inserts the clipboard at the cursor, replacing the selection (Ctrl+V, +// the Paste method). True when the text changed. +bool textEditMultiPaste(TextEditMultiT *te) { + int32_t clipLen = 0; + const char *clip = clipboardGet(&clipLen); + + return clipLen > 0 && textEditMultiReplaceSel(te, clip, clipLen); +} + + +// Replaces the selection (or inserts at the cursor when there is none) +// with textLen bytes of text, as much as the buffer holds, leaving the +// cursor after it and no selection. Saves the undo snapshot first. True +// when the text changed. The paste, cut and SelText paths. +bool textEditMultiReplaceSel(TextEditMultiT *te, const char *text, int32_t textLen) { + TextEditLineCacheT *lc = &te->lines; + bool hasSel = te->selAnchor >= 0 && te->selCursor >= 0 && te->selAnchor != te->selCursor; + int32_t off = textEditRowColToOff(lc, te->buf, te->len, te->cursorRow, te->cursorCol); + int32_t room = te->bufSize - 1 - te->len + (hasSel ? DVX_MAX(te->selAnchor, te->selCursor) - DVX_MIN(te->selAnchor, te->selCursor) : 0); + int32_t insert = DVX_MIN(textLen, room); + + if (!hasSel && insert <= 0) { + return false; + } + + textEditSaveUndo(te->buf, te->len, off, te->undoBuf, &te->undoLen, &te->undoCursor, te->bufSize); + + if (hasSel) { + textEditMultiDeleteSelection(lc, te->buf, &te->len, &te->cursorRow, &te->cursorCol, &te->selAnchor, &te->selCursor); + off = textEditRowColToOff(lc, te->buf, te->len, te->cursorRow, te->cursorCol); + } + + if (insert > 0) { + memmove(te->buf + off + insert, te->buf + off, te->len - off + 1); + memcpy(te->buf + off, text, insert); + te->len += insert; + textEditLineCacheDirty(lc); + textEditOffToRowCol(lc, te->buf, te->len, off + insert, &te->cursorRow, &te->cursorCol); + } + + te->desiredCol = te->cursorCol; + te->selAnchor = -1; + te->selCursor = -1; + return true; +} + + +// Selects the whole text, cursor at the end (Ctrl+A, the SelectAll +// method). +void textEditMultiSelectAll(TextEditMultiT *te) { + te->selAnchor = 0; + te->selCursor = te->len; + textEditOffToRowCol(&te->lines, te->buf, te->len, te->len, &te->cursorRow, &te->cursorCol); + te->desiredCol = te->cursorCol; +} + + +// Swaps the text with the undo snapshot (Ctrl+Z, the Undo method; a +// second undo is redo). True when there was a snapshot. +bool textEditMultiUndo(TextEditMultiT *te) { + int32_t off = textEditRowColToOff(&te->lines, te->buf, te->len, te->cursorRow, te->cursorCol); + + if (!textEditUndoSwap(te->buf, te->bufSize, &te->len, &off, te->undoBuf, &te->undoLen, &te->undoCursor)) { + return false; + } + + textEditLineCacheDirty(&te->lines); + textEditOffToRowCol(&te->lines, te->buf, te->len, off, &te->cursorRow, &te->cursorCol); + te->desiredCol = te->cursorCol; + te->selAnchor = -1; + te->selCursor = -1; + return true; +} + + void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col) { textEditLineCacheEnsure(lc, buf, len); @@ -1086,13 +1198,19 @@ void textEditSingleFree(TextEditSingleT *te) { // Allocates the buf/undoBuf pair for maxLen characters plus NUL -// (TEXT_EDIT_DEFAULT_MAX_LEN when maxLen <= 0): empty text, no -// selection, no undo snapshot. Returns false, with both pointers NULL, -// when allocation fails. +// (TEXT_EDIT_DEFAULT_MAX_LEN when maxLen <= 0, at most +// TEXT_EDIT_SINGLE_MAX_LEN): empty text, no selection, no undo snapshot. +// Returns false, with both pointers NULL, when allocation fails. bool textEditSingleInit(TextEditSingleT *te, int32_t maxLen) { memset(te, 0, sizeof(*te)); - int32_t bufSize = (maxLen > 0 ? maxLen : TEXT_EDIT_DEFAULT_MAX_LEN) + 1; + if (maxLen <= 0) { + maxLen = TEXT_EDIT_DEFAULT_MAX_LEN; + } else if (maxLen > TEXT_EDIT_SINGLE_MAX_LEN) { + maxLen = TEXT_EDIT_SINGLE_MAX_LEN; + } + + int32_t bufSize = maxLen + 1; te->buf = (char *)malloc(bufSize); te->undoBuf = (char *)malloc(bufSize); @@ -1112,6 +1230,84 @@ bool textEditSingleInit(TextEditSingleT *te, int32_t maxLen) { } +// The selection (or nothing, at the cursor) replaced by text, its +// newlines dropped and cut to what fits; the cursor after it, nothing +// selected, the old text in the undo slot. False, with nothing changed +// (undo included), when there is no selection and nothing fits. +bool textEditSingleReplaceSel(TextEditSingleT *te, const char *text, int32_t textLen) { + int32_t lo; + int32_t hi; + int32_t take = 0; + + textEditSingleSelRange(te, &lo, &hi); + + int32_t canFit = te->bufSize - 1 - (te->len - (hi - lo)); + + for (int32_t i = 0; i < textLen && take < canFit; i++) { + take += (text[i] != '\n' && text[i] != '\r') ? 1 : 0; + } + + if (take == 0 && lo == hi) { + return false; + } + + textEditSaveUndo(te->buf, te->len, te->cursorPos, te->undoBuf, &te->undoLen, &te->undoCursor, te->bufSize); + + if (lo < hi) { + textEditDeleteSelection(te->buf, &te->len, &te->cursorPos, &te->selStart, &te->selEnd); + } + + int32_t pos = te->cursorPos; + int32_t j = 0; + + memmove(te->buf + pos + take, te->buf + pos, te->len - pos + 1); + + for (int32_t i = 0; i < textLen && j < take; i++) { + if (text[i] != '\n' && text[i] != '\r') { + te->buf[pos + j++] = text[i]; + } + } + + te->len += take; + te->cursorPos += take; + te->selStart = -1; + te->selEnd = -1; + return true; +} + + +// New buffers for maxLen characters (textEditSingleInit's rule) holding +// the old text, cut to fit; the old ones are freed. On an allocation +// failure the field is left as it was and false is returned. +bool textEditSingleResize(TextEditSingleT *te, int32_t maxLen) { + TextEditSingleT fresh; + + if (!textEditSingleInit(&fresh, maxLen)) { + return false; + } + + textEditSingleSetText(&fresh, te->buf); + textEditSingleFree(te); + *te = fresh; + return true; +} + + +// The selection as [lo, hi) clamped to the text; lo == hi == the cursor +// when nothing is selected. +void textEditSingleSelRange(const TextEditSingleT *te, int32_t *lo, int32_t *hi) { + bool has = te->selStart >= 0 && te->selEnd >= 0; + + *lo = has ? DVX_MAX(DVX_MIN(te->selStart, te->selEnd), 0) : te->cursorPos; + *hi = has ? DVX_MIN(DVX_MAX(te->selStart, te->selEnd), te->len) : te->cursorPos; + + if (*lo >= *hi) { + *lo = te->cursorPos; + *hi = te->cursorPos; + } +} + + // Replaces the whole text (NULL = empty, truncated to the buffer): cursor // at the end, view scrolled home, no selection, and no undo snapshot so // Ctrl+Z cannot bring back the replaced text. @@ -1393,7 +1589,7 @@ void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const cha return; } - if (clicks == 2 && buf) { + if (clicks == 2) { int32_t off = textEditRowColToOff(lc, buf, len, clickRow, clickCol); int32_t ws = wordStart(buf, off); int32_t we = wordEnd(buf, len, off); @@ -1426,14 +1622,22 @@ void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const cha // never dereferences TextAreaDataT or similar. Every edit brings the // line cache up to date before the Change event fires, so a handler // that queries line count or cursor row sees the post-edit text. -void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts) { - int32_t *pRow = pCursorRow; - int32_t *pCol = pCursorCol; - int32_t *pSA = pSelAnchor; - int32_t *pSC = pSelCursor; - bool shift = (mod & KEY_MOD_SHIFT) != 0; - int32_t tabW = opts->tabWidth > 0 ? opts->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; - bool changed = false; +void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMultiT *te, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts) { + TextEditLineCacheT *lc = &te->lines; + char *buf = te->buf; + int32_t bufSize = te->bufSize; + int32_t *pLen = &te->len; + int32_t *pRow = &te->cursorRow; + int32_t *pCol = &te->cursorCol; + int32_t *pDesiredCol = &te->desiredCol; + int32_t *pSA = &te->selAnchor; + int32_t *pSC = &te->selCursor; + char *undoBuf = te->undoBuf; + int32_t *pUndoLen = &te->undoLen; + int32_t *pUndoCursor = &te->undoCursor; + bool shift = (mod & KEY_MOD_SHIFT) != 0; + int32_t tabW = opts->tabWidth > 0 ? opts->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; + bool changed = false; #define CUR_OFF() textEditRowColToOff(lc, buf, *pLen, *pRow, *pCol) #define FROM_OFF(o) textEditOffToRowCol(lc, buf, *pLen, (o), pRow, pCol) @@ -1451,76 +1655,29 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine } if (key == KEY_CTRL_A) { - *pSA = 0; - *pSC = *pLen; - FROM_OFF(*pLen); - *pDesiredCol = *pCol; + textEditMultiSelectAll(te); goto done; } if (key == KEY_CTRL_C) { - if (HAS_SEL()) { - clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); - } - + textEditMultiCopy(te); return; } if (!opts->readOnly) { if (key == KEY_CTRL_V) { - int32_t clipLen = 0; - const char *clip = clipboardGet(&clipLen); - - if (clipLen > 0) { - textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); - - if (HAS_SEL()) { - DELETE_SEL(); - changed = true; - } - - int32_t off = CUR_OFF(); - int32_t paste = DVX_MIN(clipLen, bufSize - 1 - *pLen); - - if (paste > 0) { - memmove(buf + off + paste, buf + off, *pLen - off + 1); - memcpy(buf + off, clip, paste); - *pLen += paste; - textEditLineCacheDirty(lc); - FROM_OFF(off + paste); - *pDesiredCol = *pCol; - changed = true; - } - } - + changed = textEditMultiPaste(te); goto done; } if (key == KEY_CTRL_X) { - if (HAS_SEL()) { - clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); - textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); - DELETE_SEL(); - *pDesiredCol = *pCol; - changed = true; - } - + changed = textEditMultiCut(te); goto done; } // Single-slot swap; a second Ctrl+Z is redo. if (key == KEY_CTRL_Z) { - int32_t off = CUR_OFF(); - - if (textEditUndoSwap(buf, bufSize, pLen, &off, undoBuf, pUndoLen, pUndoCursor)) { - textEditLineCacheDirty(lc); - FROM_OFF(off); - *pDesiredCol = *pCol; - *pSA = -1; - *pSC = -1; - changed = true; - } - + changed = textEditMultiUndo(te); goto done; } @@ -1668,7 +1825,7 @@ done: return; } - textEditEnsureVisible(lc, buf, *pLen, *pRow, *pCol, tabW, visRows, visCols, pScrollRow, pScrollCol); + textEditEnsureVisible(lc, buf, *pLen, *pRow, *pCol, tabW, visRows, visCols, &te->scrollRow, &te->scrollCol); wgtInvalidatePaint(w); #undef CUR_OFF @@ -1892,13 +2049,13 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT * } bool shift = (mod & KEY_MOD_SHIFT) != 0; - bool hasSel = (te->selStart >= 0 && te->selEnd >= 0); - int32_t selLo = hasSel ? DVX_MAX(DVX_MIN(te->selStart, te->selEnd), 0) : -1; - int32_t selHi = hasSel ? DVX_MIN(DVX_MAX(te->selStart, te->selEnd), te->len) : -1; bool changed = false; + int32_t selLo; + int32_t selHi; - // A selection clamped to the buffer may be empty. - hasSel = hasSel && selLo < selHi; + textEditSingleSelRange(te, &selLo, &selHi); + + bool hasSel = selLo < selHi; if (key == KEY_CTRL_A) { te->selStart = 0; @@ -1914,44 +2071,11 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT * int32_t cbLen = 0; const char *cb = clipboardGet(&cbLen); - if (cbLen <= 0) { + if (cbLen <= 0 || !textEditSingleReplaceSel(te, cb, cbLen)) { return; } - textEditSaveUndo(buf, te->len, te->cursorPos, te->undoBuf, &te->undoLen, &te->undoCursor, bufSize); - - if (hasSel) { - textEditDeleteSelection(buf, &te->len, &te->cursorPos, &te->selStart, &te->selEnd); - changed = true; - } - - // Single-line: newlines in the clipboard are dropped. - int32_t canFit = bufSize - 1 - te->len; - int32_t paste = 0; - - for (int32_t i = 0; i < cbLen && paste < canFit; i++) { - if (cb[i] != '\n' && cb[i] != '\r') { - paste++; - } - } - - if (paste > 0) { - int32_t pos = te->cursorPos; - int32_t j = 0; - - memmove(buf + pos + paste, buf + pos, te->len - pos + 1); - - for (int32_t i = 0; i < cbLen && j < paste; i++) { - if (cb[i] != '\n' && cb[i] != '\r') { - buf[pos + j] = cb[i]; - j++; - } - } - - te->len += paste; - te->cursorPos += paste; - changed = true; - } + changed = true; } else if (key == KEY_CTRL_X) { if (!hasSel) { return; @@ -1970,25 +2094,12 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT * textEditMoveCursor(te->cursorPos, false, &te->cursorPos, &te->selStart, &te->selEnd); changed = true; } else if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) { - // Only snapshot undo when the edit will actually happen: a full - // buffer with no selection is a no-op, and saving undo then would - // discard the real prior state. - if (!hasSel && te->len >= bufSize - 1) { + char ch = (char)key; + + if (!textEditSingleReplaceSel(te, &ch, 1)) { return; } - textEditSaveUndo(buf, te->len, te->cursorPos, te->undoBuf, &te->undoLen, &te->undoCursor, bufSize); - - if (hasSel) { - textEditDeleteSelection(buf, &te->len, &te->cursorPos, &te->selStart, &te->selEnd); - } - - int32_t pos = te->cursorPos; - - memmove(buf + pos + 1, buf + pos, te->len - pos + 1); - buf[pos] = (char)key; - te->len++; - te->cursorPos++; changed = true; } else if (key == KEY_BACKSPACE || key == KEY_DELETE) { if (hasSel) { diff --git a/src/libs/kpunch/texthelp/textHelp.h b/src/libs/kpunch/texthelp/textHelp.h index 1110993c..84bf55c5 100644 --- a/src/libs/kpunch/texthelp/textHelp.h +++ b/src/libs/kpunch/texthelp/textHelp.h @@ -38,6 +38,14 @@ // passes maxLen <= 0; also the BASIC default for TextBox and ComboBox. #define TEXT_EDIT_DEFAULT_MAX_LEN 256 +// Largest capacity (characters) of a single-line editor; a larger maxLen +// is reduced to it. +#define TEXT_EDIT_SINGLE_MAX_LEN 32767 + +// Largest capacity (characters) of a multi-line editor; a larger maxLen +// is reduced to it. +#define TEXT_EDIT_MULTI_MAX_LEN 262144 + // Caret blink half-period. #define TEXT_CURSOR_BLINK_MS 250 @@ -238,21 +246,6 @@ typedef struct TextEditMultiOptionsT { bool readOnly; } TextEditMultiOptionsT; -// Runs a single keystroke through the multi-line editing engine. -// Handles clipboard (Ctrl+A/C/V/X), undo (Ctrl+Z), navigation -// (arrows, Ctrl-arrows, Home/End, Ctrl-Home/Ctrl-End, PgUp/PgDn), -// editing (Enter with auto-indent, Backspace, Delete, Tab, -// printable characters), and shift-based selection extension. -// -// The caller owns buf/undoBuf storage; buf must have room for -// bufSize - 1 characters plus terminator. The library invalidates -// the cache when it knows edits span newlines; incremental -// single-byte updates go through the cache's notify paths. -// -// Fires w->onChange for edits that mutate the buffer. Calls -// wgtInvalidatePaint(w) before returning for any keystroke that -// visibly changes state. -void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts); // ============================================================ // Text-grid scrollbars @@ -308,6 +301,37 @@ typedef struct TextEditMultiT { void textEditMultiFree(TextEditMultiT *te); bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth); +// Clipboard and undo commands: the Ctrl+A/C/X/V/Z keys of +// widgetTextEditMultiOnKey and a TextArea's SelectAll / Copy / Cut / +// Paste / Undo methods run the same functions. Copy returns false when +// nothing is selected; the others return true when the text changed (the +// caller then fires Change, scrolls to the cursor and repaints). +// textEditMultiReplaceSel replaces the selection (or inserts at the +// cursor) with textLen bytes of text, as much as fits, saving the undo +// snapshot first; it leaves the cursor after the new text. +bool textEditMultiCopy(const TextEditMultiT *te); +bool textEditMultiCut(TextEditMultiT *te); +bool textEditMultiPaste(TextEditMultiT *te); +bool textEditMultiReplaceSel(TextEditMultiT *te, const char *text, int32_t textLen); +void textEditMultiSelectAll(TextEditMultiT *te); +bool textEditMultiUndo(TextEditMultiT *te); + +// Runs a single keystroke through the multi-line editing engine. +// Handles clipboard (Ctrl+A/C/V/X), undo (Ctrl+Z), navigation +// (arrows, Ctrl-arrows, Home/End, Ctrl-Home/Ctrl-End, PgUp/PgDn), +// editing (Enter with auto-indent, Backspace, Delete, Tab, +// printable characters), and shift-based selection extension, on +// the editor state te. visRows / visCols are the visible text grid. +// +// The library invalidates the cache when it knows edits span +// newlines; incremental single-byte updates go through the cache's +// notify paths. +// +// Fires w->onChange for edits that mutate the buffer. Calls +// wgtInvalidatePaint(w) before returning for any keystroke that +// visibly changes state. +void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditMultiT *te, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts); + // ============================================================ // Multi-line text operations // ============================================================ @@ -355,11 +379,19 @@ bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t // undoBuf for maxLen characters plus NUL (TEXT_EDIT_DEFAULT_MAX_LEN when // maxLen <= 0) and returns false if that fails; textEditSingleFree // releases them. selStart/selEnd are -1 for "no selection". +// textEditSingleResize gives the field room for maxLen characters (same +// rule as Init) and keeps its text, cut to the new length; false (field +// unchanged) when the allocation fails. // // textEditSingleSetText replaces the text wholesale (NULL = ""): cursor // at the end, scroll home, no selection, undo slot emptied. // textEditSingleClearSelection drops a non-empty selection and returns -// true when there was one. +// true when there was one. textEditSingleSelRange gives the selection as +// [lo, hi) (lo == hi == the cursor when none). textEditSingleReplaceSel +// replaces the selection (or inserts at the cursor) with text, newlines +// dropped and cut to what fits, saving undo; false and nothing changed +// when nothing is selected and nothing fits. The key handler's typing +// and Ctrl+V use it, and the widgets' SelText. // // textEditVisibleChars is the number of columns a field fieldWidth pixels // wide shows inside the text pad; textEditScrollToCursor adjusts a @@ -392,6 +424,9 @@ void textEditScrollToCursor(int32_t cursor, int32_t visibleChars, int32_t *pS bool textEditSingleClearSelection(TextEditSingleT *te); void textEditSingleFree(TextEditSingleT *te); bool textEditSingleInit(TextEditSingleT *te, int32_t maxLen); +bool textEditSingleReplaceSel(TextEditSingleT *te, const char *text, int32_t textLen); +bool textEditSingleResize(TextEditSingleT *te, int32_t maxLen); +void textEditSingleSelRange(const TextEditSingleT *te, int32_t *lo, int32_t *hi); void textEditSingleSetText(TextEditSingleT *te, const char *text); int32_t textEditVisibleChars(int32_t fieldWidth, const BitmapFontT *font); void widgetTextEditDragUpdateLine(TextEditSingleT *te, int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font); diff --git a/src/libs/kpunch/texthelp/texthelp.dhs b/src/libs/kpunch/texthelp/texthelp.dhs index d05d1bc0..993fad50 100644 --- a/src/libs/kpunch/texthelp/texthelp.dhs +++ b/src/libs/kpunch/texthelp/texthelp.dhs @@ -48,7 +48,7 @@ Widget DXEs that implement text editing delegate to textHelp for the four high-c .item Single-line editing engine. A single-line widget (TextInput, Spinner, ComboBox) embeds a TextEditSingleT (buffer, cursor, scroll offset, selection, undo slot) and hands it to widgetTextEditOnKey, widgetTextEditMouseClick and widgetTextEditDragUpdateLine. widgetTextEditPaintLine draws a line from an explicit buffer so a widget can paint substitute text (password bullets). .endlist -The multi-line engine (TextArea) works the same way with a TextEditMultiT. +The multi-line engine (TextArea) works the same way with a TextEditMultiT: widgetTextEditMultiOnKey(w, key, mod, te, visRows, visCols, opts) runs a key on it. Its clipboard and undo commands are functions of their own, which the keys and a TextArea's Copy / Cut / Paste / SelectAll / Undo / SelText run alike: textEditMultiCopy (false when nothing is selected), textEditMultiCut, textEditMultiPaste, textEditMultiReplaceSel(te, text, textLen) (replaces the selection or inserts at the cursor, as much as fits, undo saved first), textEditMultiSelectAll and textEditMultiUndo (a second one is redo). The editing ones return true when the text changed; the caller then fires Change, scrolls to the cursor and repaints. .h2 Constants @@ -58,6 +58,8 @@ The multi-line engine (TextArea) works the same way with a TextEditMultiT. TEXT_INPUT_PAD 3 Pixel padding inside text editing fields. TEXT_EDIT_UNDO_NONE -1 Undo length meaning "no snapshot". Set it at creation and whenever the text is replaced wholesale. TEXT_EDIT_DEFAULT_MAX_LEN 256 Capacity in characters of a single-line editor created with maxLen <= 0; also the BASIC default for TextBox and ComboBox. + TEXT_EDIT_SINGLE_MAX_LEN 32767 Largest capacity of a single-line editor; a larger maxLen is reduced to it. + TEXT_EDIT_MULTI_MAX_LEN 262144 Largest capacity of a multi-line editor (textEditMultiInit). TEXT_CURSOR_BLINK_MS 250 Caret blink half-period in milliseconds. .endtable @@ -272,8 +274,11 @@ Two related helpers: .index TextEditSingleT .index textEditSingleInit .index textEditSingleFree +.index textEditSingleResize .index textEditSingleSetText .index textEditSingleClearSelection +.index textEditSingleSelRange +.index textEditSingleReplaceSel .index textEditKeyEdits .index textEditVisibleChars .index textEditScrollToCursor @@ -301,10 +306,13 @@ typedef struct TextEditSingleT { .table Function Description -------- ----------- - bool textEditSingleInit(TextEditSingleT *te, int32_t maxLen) Allocate buf and undoBuf for maxLen characters plus the terminator (TEXT_EDIT_DEFAULT_MAX_LEN when maxLen <= 0): empty text, no selection, no undo snapshot. Returns false (both pointers NULL) when allocation fails. + bool textEditSingleInit(TextEditSingleT *te, int32_t maxLen) Allocate buf and undoBuf for maxLen characters plus the terminator (TEXT_EDIT_DEFAULT_MAX_LEN when maxLen <= 0, at most TEXT_EDIT_SINGLE_MAX_LEN): empty text, no selection, no undo snapshot. Returns false (both pointers NULL) when allocation fails. void textEditSingleFree(TextEditSingleT *te) Free both buffers. + bool textEditSingleResize(TextEditSingleT *te, int32_t maxLen) New buffers for maxLen characters (same rule as Init) holding the old text, cut to fit. Returns false, leaving the field as it was, when allocation fails. void textEditSingleSetText(TextEditSingleT *te, const char *text) Replace the text (NULL = empty, truncated to the buffer): cursor at the end, view scrolled home, no selection, undo slot emptied. bool textEditSingleClearSelection(TextEditSingleT *te) Drop the selection. Returns true when a non-empty selection was cleared. + void textEditSingleSelRange(const TextEditSingleT *te, int32_t *lo, int32_t *hi) The selection as [lo, hi), clamped to the text; lo == hi == the cursor when nothing is selected. + bool textEditSingleReplaceSel(TextEditSingleT *te, const char *text, int32_t textLen) Replace the selection (or insert at the cursor) with text, its newlines dropped and cut to what fits; the cursor after it, nothing selected, the old text in the undo slot. Returns false, with nothing changed, when nothing is selected and nothing fits. The key handler's typing and Ctrl+V use it, and a text box's SelText. bool textEditKeyEdits(int32_t key) True for keys that modify the text (printable characters, Backspace, Delete, Ctrl+V, Ctrl+X, Ctrl+Z). Read-only editors drop these before calling the engine. int32_t textEditVisibleChars(int32_t fieldWidth, const BitmapFontT *font) Whole columns visible in a field fieldWidth pixels wide, inside the text padding. void textEditScrollToCursor(int32_t cursor, int32_t visibleChars, int32_t *pScrollOff) Adjust a horizontal scroll offset so the cursor is inside the visible columns. diff --git a/src/libs/kpunch/xfer/xfer.dhs b/src/libs/kpunch/xfer/xfer.dhs index 8e0fb376..610fed2d 100644 --- a/src/libs/kpunch/xfer/xfer.dhs +++ b/src/libs/kpunch/xfer/xfer.dhs @@ -119,6 +119,8 @@ if (xferPoll(x, nowMs()) != XferRunningE) { void xferSafeName(name, out) Reduce a sender's file name to a safe 8.3 name: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _), only portable characters. out holds XFER_NAME_MAX bytes. uint16_t xferCrc16(data, len) CRC-16/XMODEM (polynomial 0x1021). uint32_t xferCrc32(data, len) CRC-32 (IEEE 802.3), as ZMODEM uses it. + int32_t xferBasNew(&x) For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way). + XferT *xferBasTransfer(handle) The transfer of an xfer.bas handle, or NULL once the program closed it or ended. .endtable .h2 Files @@ -162,20 +164,34 @@ Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in Add xfer.bas (and comm.bas, for SerOpen) to a BASIC project to send and receive files over a serial port. Start a transfer, then call XferPoll often, for example from a Timer, until it no longer returns XFER_RUNNING. While it runs, do not read the port yourself and keep it detached from any Terminal control. Received files never replace existing ones: the new file gets a new name. +Transfers over a connection (conn.bas: ConnSendFiles, ConnReceiveFiles) are the same handles, with the options a terminal needs (what to do with an existing file -- XFER_EXIST_ASK stops in XFER_ASKING for XferAnswer --, escaping every control character, continuing interrupted files); ConnPoll moves them along. Every handle belongs to the program that made it: whatever a program leaves open is cancelled and freed when it ends. + .table Function Description -------- ----------- XferSendFile(com, proto, path) Send one file over COM port com (1-4). Returns a handle, or 0. XferReceive(com, proto, target) Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0. - XferPoll(handle) Move the transfer along. Returns XFER_RUNNING, XFER_DONE, XFER_FAILED or XFER_CANCELLED. + XferPoll(handle) Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED or XFER_CANCELLED. + XferStatus(handle) The same, without moving it along (XFER_IDLE for a bad handle). + XferSending(handle), XferProtocol(handle) True for a send; the protocol. XferBytes(handle), XferSize(handle) Bytes of the current file done, and its size (-1 when unknown). XferFile$(handle) The file being transferred, as stored on this computer. + XferFileIndex(handle), XferFileCount(handle) Which file of the batch (from 0), and how many (-1 while unknown). + XferBatchBytes(handle), XferBatchSize(handle) Bytes of the whole batch done, and its size (-1 when unknown). + XferResumedAt(handle), XferErrors(handle) Where the current file continued from; retries so far. + XferAskSize(handle), XferCanResume(handle) XFER_ASKING: the size of the file already there; True when XFER_EXIST_RESUME can continue it. + XferAskRemoteSize(handle) XFER_ASKING: the size the sender offers (XferProgressT.askRemoteSize; -1 when unknown). + XferAnswer handle, action Answer XFER_ASKING with XFER_EXIST_RENAME, _RESUME, _OVERWRITE or _SKIP. XferMessage$(handle) How the transfer ended, or why it failed. XferCancel handle Stop a running transfer (the other side is told). XferClose handle Free the handle. + XferCrc16(bytes), XferCrc32(bytes) xferCrc16 / xferCrc32 of a BINARY STRING (every byte value counts); the CRC-32 as a LONG (negative when its top bit is set). + XferSafeName$(fileName) xferSafeName: the 8.3 name a receive stores a sender's file name as. .endtable -Protocols: XFER_XMODEM, XFER_XMODEM1K, XFER_YMODEM, XFER_ZMODEM. At most four transfers run at once. +XFER_WINDOW_MAX (xfer.bas) is the largest ZMODEM receive window conn.bas's ConnSetXferWindow takes. + +Protocols: XFER_XMODEM, XFER_XMODEM1K, XFER_YMODEM, XFER_ZMODEM. At most eight transfers run at once (XFER_MAX_FILES files in one batch). .code Dim h As Long diff --git a/src/libs/kpunch/xfer/xfer.h b/src/libs/kpunch/xfer/xfer.h index 464001f3..9826ee64 100644 --- a/src/libs/kpunch/xfer/xfer.h +++ b/src/libs/kpunch/xfer/xfer.h @@ -176,6 +176,19 @@ const XferProgressT *xferProgress(const XferT *x); // device name, only portable characters). out holds XFER_NAME_MAX bytes. void xferSafeName(const char *name, char *out); +// BASIC handles (xferBas.c, src/include/basic/xfer.bas) for the BASIC +// functions of other libraries, which start transfers over their own +// transports: a new handle owning a new idle transfer (in *outX) that +// belongs to the running program and is freed when it ends. Returns the +// handle, or 0 (*outX NULL) when every handle is in use or memory is +// short. The program reads the progress, answers and closes it with the +// Xfer functions of xfer.bas. +int32_t xferBasNew(XferT **outX); + +// The transfer of a BASIC handle, or NULL when the handle is not open +// (the program closed it, or it ended). +XferT *xferBasTransfer(int32_t handle); + // Start receiving. XMODEM: target is the file to create. YMODEM and // ZMODEM: target is the directory for the files. opt may be NULL for // the defaults. Returns false (status XferFailedE, message set) when the diff --git a/src/libs/kpunch/xfer/xferBas.c b/src/libs/kpunch/xfer/xferBas.c index 985bb48a..b4d4b724 100644 --- a/src/libs/kpunch/xfer/xferBas.c +++ b/src/libs/kpunch/xfer/xferBas.c @@ -21,49 +21,101 @@ // IN THE SOFTWARE. // xferBas.c -- BASIC face of the transfer library (src/include/basic/ -// xfer.bas): transfers over a serial port a BASIC program opened with -// SerOpen, by handle. COM ports are numbered 1-4 as in the rest of the -// BASIC serial functions. An incoming file that already exists is never -// replaced: it is stored under a new name (BASIC cannot answer the -// question the C API asks). +// xfer.bas): transfers by handle (1, 2, ...; 0 means none). +// +// XferSendFile and XferReceive run over a serial port a BASIC program +// opened with SerOpen (COM ports are numbered 1-4 as in the rest of the +// BASIC serial functions); there an incoming file that already exists is +// never replaced but stored under a new name. The BASIC functions of +// other libraries start transfers over their own transports and hand out +// the same handles (xferBasNew; the connection library's ConnSendFiles and +// ConnReceiveFiles), so the progress, question and end functions below +// work on every handle. Each handle belongs to the program that made it: +// whatever a program leaves open is cancelled and freed when it ends (or +// is ended). #include "xferInt.h" +#include "dvxMem.h" #include "dvxPlat.h" #include "../serial/rs232/rs232.h" +#include #include #include -#define BAS_MAX_XFERS 4 +#define BAS_MAX_XFERS 8 #define BAS_COM_FIRST 1 -#define MS_PER_SEC 1000 +#define BAS_NO_COM (-1) // a handle made by xferBasNew (its own transport) +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_NO_HANDLE 0 +#define SIZE_UNKNOWN (-1) +#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup" typedef struct { - XferT *x; - int32_t com; // rs232 port index (0-based) + XferT *x; // NULL: the slot is free + int32_t com; // rs232 port index (0-based), or BAS_NO_COM + int32_t appId; // owner: freed when it ends } BasXferT; +// The shell's app-cleanup registry (shellApp.h), looked up at run time: +// the library loads before the shell, and the host tests have none. +typedef void (*BasCleanupFnT)(int32_t appId); +typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn); + static BasXferT sBas[BAS_MAX_XFERS]; +static bool sCleanupHooked = false; // ============================================================ // Prototypes // ============================================================ -static int32_t basRead(void *ctx, uint8_t *buf, int32_t maxLen); -static int32_t basWrite(void *ctx, const uint8_t *data, int32_t len); -static BasXferT *byHandle(int32_t handle); -static int32_t newHandle(int32_t com, XferIoT *io, XferOptionsT *opt); -static uint32_t nowMs(void); -int32_t XferBytes(int32_t handle); -void XferCancel(int32_t handle); -void XferClose(int32_t handle); -const char *XferFile(int32_t handle); -const char *XferMessage(int32_t handle); -int32_t XferPoll(int32_t handle); -int32_t XferReceive(int32_t com, int32_t proto, const char *target); -int32_t XferSendFile(int32_t com, int32_t proto, const char *path); -int32_t XferSize(int32_t handle); +static void appEnded(int32_t appId); +static int32_t basRead(void *ctx, uint8_t *buf, int32_t maxLen); +static int32_t basWrite(void *ctx, const uint8_t *data, int32_t len); +static BasXferT *byHandle(int32_t handle); +static const XferProgressT *progressOf(int32_t handle); +static int32_t slotNew(int32_t com); +static int32_t startOnPort(int32_t com, XferIoT *io, XferOptionsT *opt); +void XferAnswer(int32_t handle, int32_t action); +int32_t XferAskRemoteSize(int32_t handle); +int32_t XferAskSize(int32_t handle); +int32_t xferBasNew(XferT **outX); +XferT *xferBasTransfer(int32_t handle); +int32_t XferBatchBytes(int32_t handle); +int32_t XferBatchSize(int32_t handle); +int32_t XferBytes(int32_t handle); +void XferCancel(int32_t handle); +int32_t XferCanResume(int32_t handle); +void XferClose(int32_t handle); +int32_t XferCrc16(const uint8_t *data, int32_t len); +int32_t XferCrc32(const uint8_t *data, int32_t len); +int32_t XferErrors(int32_t handle); +const char *XferFile(int32_t handle); +int32_t XferFileCount(int32_t handle); +int32_t XferFileIndex(int32_t handle); +const char *XferMessage(int32_t handle); +int32_t XferPoll(int32_t handle); +int32_t XferProtocol(int32_t handle); +int32_t XferReceive(int32_t com, int32_t proto, const char *target); +int32_t XferResumedAt(int32_t handle); +const char *XferSafeName(const char *fileName); +int32_t XferSendFile(int32_t com, int32_t proto, const char *path); +int32_t XferSending(int32_t handle); +int32_t XferSize(int32_t handle); +int32_t XferStatus(int32_t handle); + + +// Shell app-cleanup hook: cancel and free what a program that ended left +// open. +static void appEnded(int32_t appId) { + for (int32_t i = 0; i < BAS_MAX_XFERS; i++) { + if (sBas[i].x && sBas[i].appId == appId) { + XferClose(i + 1); + } + } +} static int32_t basRead(void *ctx, uint8_t *buf, int32_t maxLen) { @@ -90,11 +142,26 @@ static BasXferT *byHandle(int32_t handle) { } -// A free slot for COM port com (1-4), with the transport set up; 0 when -// the port number is bad or every slot is in use. -static int32_t newHandle(int32_t com, XferIoT *io, XferOptionsT *opt) { - if (com < BAS_COM_FIRST || com >= BAS_COM_FIRST + RS232_NUM_PORTS) { - return 0; +// The progress of a handle's transfer, or NULL for a bad handle. +static const XferProgressT *progressOf(int32_t handle) { + BasXferT *b = byHandle(handle); + + return b ? xferProgress(b->x) : NULL; +} + + +// A free slot with a new idle transfer, owned by the running program: +// its handle, or BAS_NO_HANDLE when every slot is in use or memory is +// short. +static int32_t slotNew(int32_t com) { + if (!sCleanupHooked) { + BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM)); + + sCleanupHooked = true; + + if (reg) { + reg(appEnded); + } } for (int32_t i = 0; i < BAS_MAX_XFERS; i++) { @@ -102,34 +169,108 @@ static int32_t newHandle(int32_t com, XferIoT *io, XferOptionsT *opt) { sBas[i].x = xferCreate(); if (!sBas[i].x) { - return 0; + return BAS_NO_HANDLE; } - sBas[i].com = com - BAS_COM_FIRST; - io->ctx = &sBas[i]; - io->read = basRead; - io->write = basWrite; - io->pending = NULL; - xferOptionsDefaults(opt); - opt->exists = XferExistRenameE; + sBas[i].com = com; + sBas[i].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0; return i + 1; } } - return 0; + return BAS_NO_HANDLE; } -static uint32_t nowMs(void) { - return (uint32_t)(((uint64_t)platformClock() * MS_PER_SEC) / platformClockHz()); +// A handle for a transfer over COM port com (1-4), with the port's +// transport and the BASIC options (an existing file is renamed); +// BAS_NO_HANDLE when the port number is bad or no handle is free. +static int32_t startOnPort(int32_t com, XferIoT *io, XferOptionsT *opt) { + if (com < BAS_COM_FIRST || com >= BAS_COM_FIRST + RS232_NUM_PORTS) { + return BAS_NO_HANDLE; + } + + int32_t handle = slotNew(com - BAS_COM_FIRST); + + if (handle != BAS_NO_HANDLE) { + io->ctx = &sBas[handle - 1]; + io->read = basRead; + io->write = basWrite; + io->pending = NULL; + xferOptionsDefaults(opt); + opt->exists = XferExistRenameE; + } + + return handle; +} + + +// Answer the question of an XFER_ASKING transfer with an XFER_EXIST_ +// action. +void XferAnswer(int32_t handle, int32_t action) { + BasXferT *b = byHandle(handle); + + if (b) { + xferAnswer(b->x, (XferExistE)action, PLATFORM_TICKS_TO_MS(platformClock())); + } +} + + +// XFER_ASKING: the size of the file that already exists +// XFER_ASKING: the size of the file the sender offers (-1 = unknown). +int32_t XferAskRemoteSize(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->askRemoteSize : SIZE_UNKNOWN; +} + + +int32_t XferAskSize(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->askLocalSize : 0; +} + + +// A handle owning a new idle transfer for another library's BASIC +// function to start over its own transport (see xfer.h). +int32_t xferBasNew(XferT **outX) { + int32_t handle = slotNew(BAS_NO_COM); + + *outX = (handle != BAS_NO_HANDLE) ? sBas[handle - 1].x : NULL; + return handle; +} + + +// The transfer of a handle, or NULL once it is closed. +XferT *xferBasTransfer(int32_t handle) { + BasXferT *b = byHandle(handle); + + return b ? b->x : NULL; +} + + +// Bytes of the whole batch done +int32_t XferBatchBytes(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->batchBytes : 0; +} + + +// Bytes in the whole batch (-1 when the sender did not say) +int32_t XferBatchSize(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->batchSize : SIZE_UNKNOWN; } // Bytes of the current file done int32_t XferBytes(int32_t handle) { - BasXferT *b = byHandle(handle); + const XferProgressT *p = progressOf(handle); - return b ? xferProgress(b->x)->fileBytes : 0; + return p ? p->fileBytes : 0; } @@ -142,6 +283,14 @@ void XferCancel(int32_t handle) { } +// XFER_ASKING: True when XFER_EXIST_RESUME can continue the file +int32_t XferCanResume(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return (p && p->askCanResume) ? BAS_TRUE : BAS_FALSE; +} + + // Free the handle (cancelling a transfer that is still running). void XferClose(int32_t handle) { BasXferT *b = byHandle(handle); @@ -156,28 +305,75 @@ void XferClose(int32_t handle) { } +// The CRC-16 (XMODEM) of data (a BINARY STRING: every byte counts), 0 to +// 65535. +int32_t XferCrc16(const uint8_t *data, int32_t len) { + return (int32_t)xferCrc16(data, len); +} + + +// The CRC-32 (ZMODEM, ZIP) of data as a LONG (negative when its top bit is +// set). +int32_t XferCrc32(const uint8_t *data, int32_t len) { + return (int32_t)xferCrc32(data, len); +} + + +// Retries so far +int32_t XferErrors(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->errors : 0; +} + + // The file being transferred, as stored locally ("" for a bad handle) const char *XferFile(int32_t handle) { - BasXferT *b = byHandle(handle); + const XferProgressT *p = progressOf(handle); - return b ? xferProgress(b->x)->fileName : ""; + return p ? p->fileName : ""; +} + + +// Files in the batch (-1 while the sender has not said) +int32_t XferFileCount(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->fileCount : SIZE_UNKNOWN; +} + + +// The file being transferred, counted from 0 +int32_t XferFileIndex(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->fileIndex : 0; } // How the transfer ended, or why it failed const char *XferMessage(int32_t handle) { - BasXferT *b = byHandle(handle); + const XferProgressT *p = progressOf(handle); - return b ? xferProgress(b->x)->message : ""; + return p ? p->message : ""; } // Advance the transfer; call it often. Returns the XferStatusE value -// (1 running, 3 done, 4 failed, 5 cancelled; 0 for a bad handle). +// (1 running, 2 asking, 3 done, 4 failed, 5 cancelled; 0 for a bad +// handle). int32_t XferPoll(int32_t handle) { BasXferT *b = byHandle(handle); - return b ? (int32_t)xferPoll(b->x, nowMs()) : XferIdleE; + return b ? (int32_t)xferPoll(b->x, PLATFORM_TICKS_TO_MS(platformClock())) : XferIdleE; +} + + +// The protocol (XFER_XMODEM ... XFER_ZMODEM; 0 for a bad handle) +int32_t XferProtocol(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? (int32_t)p->proto : XferXmodemE; } @@ -186,35 +382,71 @@ int32_t XferPoll(int32_t handle) { int32_t XferReceive(int32_t com, int32_t proto, const char *target) { XferIoT io; XferOptionsT opt; - int32_t handle = newHandle(com, &io, &opt); + int32_t handle = startOnPort(com, &io, &opt); - if (handle && !xferStartReceive(sBas[handle - 1].x, (XferProtoE)proto, &io, &opt, target, nowMs())) { + if (handle && !xferStartReceive(sBas[handle - 1].x, (XferProtoE)proto, &io, &opt, target, PLATFORM_TICKS_TO_MS(platformClock()))) { XferClose(handle); - return 0; + return BAS_NO_HANDLE; } return handle; } +// Where the current file continued from (0 when it started at the +// beginning) +int32_t XferResumedAt(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? p->resumedAt : 0; +} + + +// A file name a sender offered, made safe to store here: an 8.3 name +// without a folder or a device name, only characters every disk takes. +const char *XferSafeName(const char *fileName) { + static char name[XFER_NAME_MAX]; + + xferSafeName(fileName ? fileName : "", name); + return name; +} + + // Send one file. Returns a handle, or 0 when the transfer cannot start. int32_t XferSendFile(int32_t com, int32_t proto, const char *path) { XferIoT io; XferOptionsT opt; - int32_t handle = newHandle(com, &io, &opt); + int32_t handle = startOnPort(com, &io, &opt); - if (handle && !xferStartSend(sBas[handle - 1].x, (XferProtoE)proto, &io, &opt, &path, 1, nowMs())) { + if (handle && !xferStartSend(sBas[handle - 1].x, (XferProtoE)proto, &io, &opt, &path, 1, PLATFORM_TICKS_TO_MS(platformClock()))) { XferClose(handle); - return 0; + return BAS_NO_HANDLE; } return handle; } +// True for a transfer that sends, False for one that receives +int32_t XferSending(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return (p && p->sending) ? BAS_TRUE : BAS_FALSE; +} + + // Size of the current file (-1 when the sender did not say) int32_t XferSize(int32_t handle) { - BasXferT *b = byHandle(handle); + const XferProgressT *p = progressOf(handle); - return b ? xferProgress(b->x)->fileSize : -1; + return p ? p->fileSize : SIZE_UNKNOWN; +} + + +// The XferStatusE value without moving the transfer along (0 for a bad +// handle) +int32_t XferStatus(int32_t handle) { + const XferProgressT *p = progressOf(handle); + + return p ? (int32_t)p->status : XferIdleE; } diff --git a/src/loader/loaderMain.c b/src/loader/loaderMain.c index c344a792..72fdb9a3 100644 --- a/src/loader/loaderMain.c +++ b/src/loader/loaderMain.c @@ -37,6 +37,7 @@ #include "stddclmr.h" #include "dvxPlat.h" #include "dvxPrefs.h" +#include "dvxRes.h" #include "../tools/hlpcCompile.h" #include @@ -140,6 +141,11 @@ typedef const void *(*IfaceAtFnT)(int32_t, const char **); typedef const char *(*IfaceGetPathFnT)(const char *); typedef void (*IfaceSetPathFnT)(const char *, const char *); +// The BASIC run-time library's reader of resource-only control files +// (basCtlLoadFile), resolved after every module is loaded. +typedef bool (*CtlLoadFnT)(const char *path); +#define CTL_LOAD_SYMBOL "basCtlLoadFile" + // scriptStart / scriptStop in the test-script module typedef bool (*ScriptStartFnT)(const char *path, const char *logPath); typedef void (*ScriptStopFnT)(int32_t exitCode); @@ -162,6 +168,11 @@ static IfaceAtFnT sIfaceAt = NULL; static IfaceGetPathFnT sIfaceGetPath = NULL; static IfaceSetPathFnT sIfaceSetPath = NULL; +// Widget files that hold only resources (controls written in BASIC): +// handed to the BASIC run-time library once every module is loaded +// (stb_ds array of strdup'd paths). +static char **sResOnlyWidgets = NULL; + // Where the log is written ("" = kept in memory; see logStart) static char sLogPath[DVX_MAX_PATH] = LOG_PATH; @@ -183,7 +194,9 @@ static void helpJobCallback(const char *key, const char *val, const char *ex static void helpRecompileIfNeeded(void); static void helpSaveCounts(PrefsHandleT *ini); static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total); +static bool isResourceOnly(const char *path); static void **loadAllModules(void); +static void loadBasicControls(void); static void loadInOrder(ModuleT *mods); static void logAndReadDeps(ModuleT *mods); static void logStart(void); @@ -608,6 +621,23 @@ static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total) { } +// True when the file starts with DVX_RES_ONLY_MAGIC: resources and no +// code (a control written in BASIC), never dlopen'd. +static bool isResourceOnly(const char *path) { + FILE *f = fopen(path, "rb"); + bool is = false; + + if (f) { + char head[DVX_RES_ONLY_MAGIC_LEN]; + + is = fread(head, 1, sizeof(head), f) == sizeof(head) && memcmp(head, DVX_RES_ONLY_MAGIC, sizeof(head)) == 0; + fclose(f); + } + + return is; +} + + static void **loadAllModules(void) { void **handles = NULL; @@ -649,10 +679,34 @@ static void **loadAllModules(void) { freeMods(widgets); + // Controls written in BASIC, now that every native widget is there + loadBasicControls(); + return handles; } +// Hands every resource-only widget file to the BASIC run-time library +// (it registers the file's controls) and records the file as theirs for +// the toolbox. Without the library the files are skipped, not fatal. +static void loadBasicControls(void) { + CtlLoadFnT loadFn = (CtlLoadFnT)dlsym(NULL, DVX_SYM(CTL_LOAD_SYMBOL)); + + for (int32_t i = 0; i < arrlen(sResOnlyWidgets); i++) { + if (!loadFn) { + dvxLog("Skipping %s: the BASIC run-time library is not loaded", sResOnlyWidgets[i]); + } else if (loadFn(sResOnlyWidgets[i])) { + recordIfacePaths(sResOnlyWidgets[i]); + } else { + dvxLog("Skipping %s: no usable BASIC control in it", sResOnlyWidgets[i]); + } + } + + freeStrings(sResOnlyWidgets); + sResOnlyWidgets = NULL; +} + + // Repeatedly scans the module list for entries whose dependencies // are all satisfied, loads them, and marks them done. Stops when // all modules are loaded or no progress can be made (circular dep). @@ -675,6 +729,24 @@ static void loadInOrder(ModuleT *mods) { } dvxLog("Loading: %s", mods[i].path); + + // A control written in BASIC: no code to load, its resources + // are read once every module is there (loadBasicControls) + if (dvxHasExt(mods[i].path, ".wgt") && isResourceOnly(mods[i].path)) { + char *path = strdup(mods[i].path); + + if (path) { + arrput(sResOnlyWidgets, path); + } + + mods[i].loaded = true; + loaded++; + progress = true; + sSplashLoaded++; + splashUpdateProgress(); + continue; + } + mods[i].handle = dlopen(mods[i].path, RTLD_NOW | RTLD_GLOBAL); if (!mods[i].handle) { diff --git a/src/sdk/newProject.sh b/src/sdk/newProject.sh index c4261db4..b6b6bbb5 100755 --- a/src/sdk/newProject.sh +++ b/src/sdk/newProject.sh @@ -33,6 +33,8 @@ # keyboard and joystick, sound effects and music # basicgame a DVX BASIC game: FRAMEWAIT loop, GameKeyDown, # STICK/STRIG and PLAY +# control a control written in DVX BASIC (.wgt), usable from +# BASIC and C programs # NAME the module name: a letter, then letters and digits, 8 at most # (DOS file names). Files are named NAME with its first letter in # lower case (camelCase, e.g. myApp); DVX sees it in upper case. @@ -102,7 +104,7 @@ nextSteps() { echo "Then start DVX and run $module from Program Manager (File > Run), or open" echo "$camel.dbp in DVX BASIC to design the form and edit the code." ;; - widget) + widget|control) echo "Then restart DVX: it loads the widget at start-up, adds its help to the" echo "system help, and shows it in the DVX BASIC toolbox." ;; @@ -120,7 +122,7 @@ nextSteps() { usage() { - die "usage: newProject.sh app|widget|library|basic|game|basicgame NAME [DIR]" + die "usage: newProject.sh app|widget|library|basic|game|basicgame|control NAME [DIR]" } @@ -163,6 +165,11 @@ case $kind in shared="" what="a widget" ;; + control) + ext=wgt + shared="widget/icon24.bmp" + what="a control written in DVX BASIC" + ;; *) usage ;; @@ -181,7 +188,7 @@ if isDeviceName "$lower"; then die "'$name' is a DOS device name and cannot be a file name" fi -if [ "$kind" = widget ] && isBuiltinWidget "$lower"; then +if { [ "$kind" = widget ] || [ "$kind" = control ]; } && isBuiltinWidget "$lower"; then die "DVX already has a widget named '$lower'" fi diff --git a/src/sdk/sdk.mk b/src/sdk/sdk.mk index b5f56333..5b327361 100644 --- a/src/sdk/sdk.mk +++ b/src/sdk/sdk.mk @@ -24,8 +24,8 @@ # # A project Makefile sets three variables and includes this file: # SDK_DIR the DVX SDK directory (the one holding this file) -# KIND app, widget, library or basic (the game templates are app -# and basic projects) +# KIND app, widget, library, basic or control (a control written in +# DVX BASIC; the game templates are app and basic projects) # NAME the module's base name (at most 8 characters) # and, for a module that uses sockets (the dvxnet library), # NET = 1 puts include/net (sys/socket.h, netdb.h, ...) on the include @@ -57,8 +57,8 @@ .DEFAULT_GOAL := all -ifeq ($(filter $(KIND),app widget library basic),) -$(error sdk.mk: KIND must be app, widget, library or basic, not '$(KIND)') +ifeq ($(filter $(KIND),app widget library basic control),) +$(error sdk.mk: KIND must be app, widget, library, basic or control, not '$(KIND)') endif # ---- Toolchain ---- @@ -104,6 +104,9 @@ PKG_TOP = apps else ifeq ($(KIND),widget) MODULE = $(NAME).wgt PKG_TOP = widgets +else ifeq ($(KIND),control) +MODULE = $(NAME).wgt +PKG_TOP = widgets else MODULE = $(NAME).lib PKG_TOP = libs @@ -124,16 +127,18 @@ sdkToolchain: # C modules: dxe3gen links the objects (-U leaves DVX symbols to be resolved # when DVX loads the module), then dvxres appends the resources. -ifneq ($(KIND),basic) +ifeq ($(filter $(KIND),basic control),) $(MODULE): $(OBJS) $(LIBS) $(RES) $(ASSETS) $(DXE3GEN) -o $(NAME).dxe -U $(OBJS) $(LIBS) mv $(NAME).dxe $@ $(if $(RES),$(DVXRES) build $@ $(RES)) endif -# BASIC: the compiler reads the project file and everything it lists. -ifeq ($(KIND),basic) -$(MODULE): $(NAME).dbp $(wildcard *.frm *.bas) $(ASSETS) +# BASIC: the compiler reads the project file and everything it lists (a +# control project, [Project] Type = Control, becomes a control file: a +# .wgt holding only resources, its controls written in BASIC). +ifneq ($(filter $(KIND),basic control),) +$(MODULE): $(NAME).dbp $(wildcard *.frm *.bas *.ctl) $(ASSETS) $(BASCOMP) $(NAME).dbp -o $@ -release endif diff --git a/src/sdk/templates/control/Makefile b/src/sdk/templates/control/Makefile new file mode 100644 index 00000000..58a5cdee --- /dev/null +++ b/src/sdk/templates/control/Makefile @@ -0,0 +1,14 @@ +# Makefile for tplName, built with the DVX SDK. +# +# make build the control file (tplName.wgt) and its help +# make package lay out pkg/ exactly as it goes into DVX +# make install DVX_DIR=/path/to/dvx +# make clean +# +# DJGPP_PREFIX and VENDOR can be set on the command line; see sdk.mk. + +SDK_DIR ?= TPLSDKDIR +KIND = control +NAME = tplName + +include $(SDK_DIR)/sdk.mk diff --git a/src/sdk/templates/control/tplName.bhs b/src/sdk/templates/control/tplName.bhs new file mode 100644 index 00000000..7c9f532c --- /dev/null +++ b/src/sdk/templates/control/tplName.bhs @@ -0,0 +1,41 @@ +# tplName.bhs -- BASIC help for the TplName control. Installed beside +# tplName.wgt as TPLNAME.BHS; DVX adds it to the DVX BASIC help. + +.topic ctrl.tplname +.title TplName +.toc 1 TplName +.index TplName + +.h1 TplName + +A box that shows its Title and counts the clicks it gets. Space clicks it too when it has the focus. + +.h2 Properties + +.table + Property Type Description + -------- ---- ----------- + Title String What it shows. + Count Integer How many times it was clicked (read-only). +.endtable + +.h2 Methods + +.table + Method Description + ------ ----------- + Reset Start counting again. +.endtable + +.h2 Events + +.table + Event Description + ----- ----------- + Click It was clicked. + Changed(ByVal Count As Integer) The count changed. +.endtable + +.h2 Default Event + +Click. diff --git a/src/sdk/templates/control/tplName.ctl b/src/sdk/templates/control/tplName.ctl new file mode 100644 index 00000000..6a8b20c1 --- /dev/null +++ b/src/sdk/templates/control/tplName.ctl @@ -0,0 +1,98 @@ +VERSION DVX 1.00 +Begin UserControl TplName + Width = 120 + Height = 40 + CanGetFocus = True + BackColor = &H00FFFFFF& + ToolboxIcon = "icon24.bmp" + Description = "TplName" + DefaultEvent = "Click" +End +' tplName.ctl -- the TplName control, written in DVX BASIC: a button-like +' box that shows its Caption and counts the clicks it gets. Every +' program can use it once tplName.wgt is in DVX's WIDGETS folder: +' BASIC programs put it on a form like any control, C programs create it +' with wgtCreateByName("TplName", parent). +' +' A control is a class: its Public variables and Property Get / Let are +' its properties, its Public Subs and Functions its methods, its Events +' the events its host gets. UserControl is the control itself: draw on +' it (Cls, Line, Print ...) and read its size (Width, Height). + +Option Explicit + +Public Event Click() +Public Event Changed(ByVal Count As Integer) + +Private mCaption As String +Private mCount As Integer + + +' What it shows (a property: read and written by name) +Public Property Get Title() As String + Title = mCaption +End Property + + +Public Property Let Title(ByVal Value As String) + mCaption = Value + UserControl.Refresh +End Property + + +' How many times it was clicked (read-only) +Public Property Get Count() As Integer + Count = mCount +End Property + + +' Start counting again (a method) +Public Sub Reset() + mCount = 0 + RaiseEvent Changed(mCount) + UserControl.Refresh +End Sub + + +Private Sub UserControl_Initialize() + mCaption = "TplName" +End Sub + + +Private Sub UserControl_Click() + mCount = mCount + 1 + RaiseEvent Changed(mCount) + RaiseEvent Click + UserControl.Refresh +End Sub + + +Private Sub UserControl_KeyPress(KeyAscii As Integer) + If KeyAscii = 32 Then UserControl_Click +End Sub + + +Private Sub UserControl_Paint() + Dim text As String + + text = mCaption & " (" & mCount & ")" + UserControl.Cls + UserControl.Line (0, 0)-(UserControl.Width - 1, UserControl.Height - 1), , B + UserControl.CurrentX = (UserControl.Width - UserControl.TextWidth(text)) \ 2 + UserControl.CurrentY = (UserControl.Height - UserControl.TextHeight(text)) \ 2 + UserControl.Print text; + + If UserControl.HasFocus Then + UserControl.DrawFocusRect 2, 2, UserControl.Width - 4, UserControl.Height - 4 + End If +End Sub + + +Private Sub UserControl_GotFocus() + UserControl.Refresh +End Sub + + +Private Sub UserControl_LostFocus() + UserControl.Refresh +End Sub diff --git a/src/sdk/templates/control/tplName.dbp b/src/sdk/templates/control/tplName.dbp new file mode 100644 index 00000000..bf5b9b38 --- /dev/null +++ b/src/sdk/templates/control/tplName.dbp @@ -0,0 +1,15 @@ +[Project] +Name = TplName +Type = Control +Author = Your Name +Publisher = Your Name +Version = 1.00 +Copyright = Copyright Your Name +Description = A control written in DVX BASIC + +[Controls] +File0 = tplName.ctl +Count = 1 + +[Settings] +OptionExplicit = True diff --git a/src/sdk/templates/library/tplName.bas b/src/sdk/templates/library/tplName.bas index a1b9931c..9dd6229a 100644 --- a/src/sdk/templates/library/tplName.bas +++ b/src/sdk/templates/library/tplName.bas @@ -8,4 +8,8 @@ DECLARE LIBRARY "tplname" ' A greeting naming who ("world" when who is empty). DECLARE FUNCTION tplNameGreet$(BYVAL who AS STRING) AS STRING + + ' Every byte of data XORed with key. BINARY STRING: any bytes, + ' CHR$(0) included, in and out (AS STRING would stop at a zero byte). + DECLARE FUNCTION tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING END DECLARE diff --git a/src/sdk/templates/library/tplName.c b/src/sdk/templates/library/tplName.c index afc77d67..b5853103 100644 --- a/src/sdk/templates/library/tplName.c +++ b/src/sdk/templates/library/tplName.c @@ -4,7 +4,10 @@ // libraries. DVX loads every library when it starts, before any widget or // program, in the order the .dep files require. DECLARE LIBRARY in // tplName.bas looks functions up by exactly the name it declares (less any -// $ type suffix), so BASIC programs can call them too. +// $ type suffix), so BASIC programs can call them too. tplNameXor shows +// the BINARY STRING convention: a byte string comes in as pointer and +// length, and goes back as a pointer with its length stored through the +// extra last parameter. #include "tplName.h" #include "dvxTypes.h" @@ -17,17 +20,20 @@ DVX_ABI_STAMP(tplname); #define TPLNAME_GREET_MAX 80 +#define TPLNAME_XOR_MAX 256 #define TPLNAME_DEFAULT "world" -static char sGreeting[TPLNAME_GREET_MAX]; +static char sGreeting[TPLNAME_GREET_MAX]; +static uint8_t sXorBuf[TPLNAME_XOR_MAX]; // ============================================================ // Prototypes // ============================================================ -int32_t tplNameAdd(int32_t a, int32_t b); -const char *tplNameGreet(const char *who); -int32_t tplNameVersion(void); +int32_t tplNameAdd(int32_t a, int32_t b); +const char *tplNameGreet(const char *who); +int32_t tplNameVersion(void); +const uint8_t *tplNameXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen); int32_t tplNameAdd(int32_t a, int32_t b) { @@ -44,3 +50,15 @@ const char *tplNameGreet(const char *who) { int32_t tplNameVersion(void) { return TPLNAME_VERSION; } + + +const uint8_t *tplNameXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen) { + int32_t n = (len < TPLNAME_XOR_MAX) ? len : TPLNAME_XOR_MAX; + + for (int32_t i = 0; i < n; i++) { + sXorBuf[i] = (uint8_t)(data[i] ^ key); + } + + *outLen = n; + return sXorBuf; +} diff --git a/src/sdk/templates/library/tplName.dhs b/src/sdk/templates/library/tplName.dhs index 8f707686..74c2db3a 100644 --- a/src/sdk/templates/library/tplName.dhs +++ b/src/sdk/templates/library/tplName.dhs @@ -9,6 +9,7 @@ .index tplNameAdd .index tplNameGreet .index tplNameVersion +.index tplNameXor .h1 TplName Library @@ -24,8 +25,9 @@ Header: tplName.h tplNameAdd(a, b) Sum of a and b. tplNameGreet(who) "Hello, who!" ("world" when who is empty); valid until the next call. tplNameVersion() TPLNAME_VERSION of the loaded library. + tplNameXor(data, len, key, outLen) The first 256 bytes of data XORed with key; the length goes to *outLen, the bytes stay valid until the next call. .endtable .h2 BASIC -Add tplName.bas to a project, then call tplNameAdd and tplNameGreet$ like any other function. +Add tplName.bas to a project, then call tplNameAdd, tplNameGreet$ and tplNameXor$ like any other function. tplNameXor$ is declared with BINARY STRING, so its argument and result may hold any bytes, CHR$(0) included. diff --git a/src/sdk/templates/library/tplName.h b/src/sdk/templates/library/tplName.h index e59be0ea..9b52c1c7 100644 --- a/src/sdk/templates/library/tplName.h +++ b/src/sdk/templates/library/tplName.h @@ -14,13 +14,19 @@ #define TPLNAME_VERSION 1 // Sum of a and b. Also callable from BASIC (tplName.bas). -int32_t tplNameAdd(int32_t a, int32_t b); +int32_t tplNameAdd(int32_t a, int32_t b); // A greeting naming who ("world" when who is NULL or empty). The text is // valid until the next call. Also callable from BASIC as tplNameGreet$. -const char *tplNameGreet(const char *who); +const char *tplNameGreet(const char *who); // TPLNAME_VERSION of the loaded library. -int32_t tplNameVersion(void); +int32_t tplNameVersion(void); + +// The first 256 bytes of data (len of them, zero bytes included), each +// XORed with key; the result length is stored in *outLen and the bytes +// are valid until the next call. Also callable from BASIC as tplNameXor$ +// (BINARY STRING in and out, see tplName.bas). +const uint8_t *tplNameXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen); #endif // TPLNAME_H diff --git a/src/sdk/templates/widget/tplName.c b/src/sdk/templates/widget/tplName.c index 215df571..50fc3f4e 100644 --- a/src/sdk/templates/widget/tplName.c +++ b/src/sdk/templates/widget/tplName.c @@ -169,4 +169,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClass); wgtRegisterApi("tplname", &sApi); wgtRegisterIface("tplname", &sIface); + wgtIfaceSetType("tplname", sTypeId); } diff --git a/src/sdkHeaders.mk b/src/sdkHeaders.mk index a52251f2..7ccd782b 100644 --- a/src/sdkHeaders.mk +++ b/src/sdkHeaders.mk @@ -31,12 +31,13 @@ # with a trailing slash (empty at the top level). SDK_CORE_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/libdvx/,dvxApp.h dvxTypes.h dvxWgt.h dvxWgtP.h dvxWm.h dvxDraw.h dvxVideo.h dvxComp.h dvxPrefs.h dvxDlg.h dvxRes.h dvxFont.h dvxCur.h dvxPal.h dvxMem.h dvxDrv.h platform/dvxPlat.h) -SDK_SHELL_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/dvxshell/,shellApp.h shellAssoc.h shellInf.h shellProg.h shellRun.h) $(SDK_ROOT)src/libs/kpunch/taskmgr/shellTaskMgr.h +SDK_SHELL_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/dvxshell/,shellApp.h shellAssoc.h shellInf.h shellProg.h shellRun.h pmModel.h) $(SDK_ROOT)src/libs/kpunch/taskmgr/shellTaskMgr.h SDK_TASKS_H = $(SDK_ROOT)src/libs/kpunch/libtasks/taskSwch.h SDK_SQL_H = $(SDK_ROOT)src/libs/kpunch/sql/dvxSql.h SDK_TEXTHELP_H = $(SDK_ROOT)src/libs/kpunch/texthelp/textHelp.h SDK_LISTHELP_H = $(SDK_ROOT)src/libs/kpunch/listhelp/listHelp.h SDK_FILEOPS_H = $(SDK_ROOT)src/libs/kpunch/fileops/fileOps.h +SDK_HLPFILE_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/hlpfile/,hlpFile.h hlpFormat.h) SDK_XFER_H = $(SDK_ROOT)src/libs/kpunch/xfer/xfer.h SDK_CONN_H = $(SDK_ROOT)src/libs/kpunch/conn/conn.h SDK_PRINT_H = $(SDK_ROOT)src/libs/kpunch/print/print.h diff --git a/src/test/abi/Makefile b/src/test/abi/Makefile index 4a637d8a..aaca509f 100644 --- a/src/test/abi/Makefile +++ b/src/test/abi/Makefile @@ -40,7 +40,7 @@ include ../../sdkHeaders.mk OBJDIR = $(HOST_OBJROOT)/test/abi TABLE = abiSizes.txt -HEADERS = $(SDK_CORE_H) $(SDK_SHELL_H) $(SDK_TASKS_H) $(SDK_SQL_H) $(SDK_TEXTHELP_H) $(SDK_LISTHELP_H) $(SDK_FILEOPS_H) $(SDK_XFER_H) $(SDK_CONN_H) $(SDK_PRINT_H) $(SDK_GAME_H) $(SDK_JOYSTICK_H) $(SDK_AUDIO_H) $(SDK_MUSIC_H) $(addprefix $(SDK_SERIAL_DIR)/,$(SDK_SERIAL_H)) $(SDK_WIDGET_H) $(SDK_NET_H) +HEADERS = $(SDK_CORE_H) $(SDK_SHELL_H) $(SDK_TASKS_H) $(SDK_SQL_H) $(SDK_TEXTHELP_H) $(SDK_LISTHELP_H) $(SDK_FILEOPS_H) $(SDK_HLPFILE_H) $(SDK_XFER_H) $(SDK_CONN_H) $(SDK_PRINT_H) $(SDK_GAME_H) $(SDK_JOYSTICK_H) $(SDK_AUDIO_H) $(SDK_MUSIC_H) $(addprefix $(SDK_SERIAL_DIR)/,$(SDK_SERIAL_H)) $(SDK_WIDGET_H) $(SDK_NET_H) ABI_FLAGS = $(WARNFLAGS) -march=i486 -I$(DVX_ROOT)/src/libs/kpunch/libdvx/thirdparty .PHONY: all check update clean diff --git a/src/test/abi/abiSizes.txt b/src/test/abi/abiSizes.txt index 6cf458b3..115ec7e8 100644 --- a/src/test/abi/abiSizes.txt +++ b/src/test/abi/abiSizes.txt @@ -2,12 +2,12 @@ # (i386 DJGPP), recorded at the DVX_ABI_VERSION below. Generated by # `make -C src/test/abi update`; read abiCheck.py and DVX_ABI_VERSION in # src/libs/kpunch/libdvx/dvxTypes.h before changing it. -version 5 +version 7 AccelEntryT 12 AccelTableT 12 AnsiTermApiT 36 AnsiTermExApiT 16 -AppContextT 3424 +AppContextT 3436 AppDescriptorT 80 AudioDriverT 28 AudioDrvConfigT 16 @@ -25,10 +25,10 @@ BitmapFontT 20 BlitOpsT 12 BoxApiT 20 ButtonApiT 12 -CanvasApiT 68 +CanvasApiT 80 CheckboxApiT 20 ColorSchemeT 80 -ComboBoxApiT 44 +ComboBoxApiT 52 ConnConfigT 296 CursorT 24 DataCtrlApiT 88 @@ -50,6 +50,15 @@ FileOpsEntryT 20 FileOpsProgressT 32 GameFramePacerT 24 GamePalCacheT 37632 +HelpViewApiT 32 +HlpHeaderT 60 +HlpImageRefT 8 +HlpIndexEntryT 8 +HlpRecordHdrT 4 +HlpSearchHeaderT 4 +HlpTocEntryT 8 +HlpTopicDirT 20 +HlpTrigramEntryT 8 ImageApiT 28 ImageButtonApiT 24 JoyAxisCalT 12 @@ -61,6 +70,8 @@ JoyStateT 56 KbMoveResizeT 24 LabelApiT 16 ListBoxApiT 68 +ListIconCellT 36 +ListIconGridT 80 ListItemsT 20 ListPopupT 36 ListSelT 16 @@ -77,6 +88,9 @@ PlatformDriveT 8 PlatformKeyEventT 12 PlatformSymOverrideT 8 PlatformTimerStatsT 16 +PmGroupT 72 +PmItemT 1264 +PmModelT 20 PopupLevelT 32 PopupStateT 52 PrefsChoiceT 12 @@ -86,11 +100,11 @@ PrintPageSetupT 192 PrintPortOpsT 16 PrintSettingsT 460 PrintTextInfoT 24 -ProgressBarApiT 24 +ProgressBarApiT 32 RadioApiT 36 RawKeyEventT 8 RectT 16 -ScrollPaneApiT 24 +ScrollPaneApiT 40 ScrollbarT 32 SecHmacT 216 SecSha256T 108 @@ -100,7 +114,7 @@ ShellProgramT 452 SliderApiT 20 SpacerApiT 12 SpinnerApiT 52 -SplitterApiT 20 +SplitterApiT 28 StatusBarApiT 12 SysMenuItemT 40 SysMenuStateT 432 @@ -110,19 +124,22 @@ TextEditMultiOptionsT 8 TextEditMultiT 80 TextEditPaintHooksT 36 TextEditSingleT 40 -TextInputApiT 92 +TextInputApiT 144 TimerApiT 32 ToolbarApiT 16 TreeViewApiT 48 VideoModeInfoT 12 WgtApiHeaderT 8 WgtApiPrefixT 12 -WgtEventDescT 4 -WgtIfaceT 52 +WgtDynIfaceT 20 +WgtEventDescT 8 +WgtIfaceT 56 WgtMethodDescT 12 WgtPropDescT 20 +WgtSigInfoT 7 +WgtValueT 12 WidgetClassT 136 -WidgetT 236 +WidgetT 240 WindowStackT 60 WindowT 308 WrapBoxApiT 12 diff --git a/src/test/audio/Makefile b/src/test/audio/Makefile index f2848e0a..81fdab77 100644 --- a/src/test/audio/Makefile +++ b/src/test/audio/Makefile @@ -27,7 +27,9 @@ # rewrites it), the WAV / VOC decoder against good and damaged files, and # driver discovery against fake drivers (fakeDrv.c, and fakeSynth.c for # the FM chip and the MIDI port) built as .drv shared objects into the -# work tree the test runs in. +# work tree the test runs in. The BASIC compiler and VM (basrt.a) run +# sound.bas / music.bas programs whose DECLARE LIBRARY calls (BINARY +# STRING data included) reach the library for real. include ../dvxtest/dvxtest.mk @@ -42,9 +44,9 @@ GOLDEN = $(abspath golden/mixer.txt) AUDIO_SRCS = $(filter-out $(AUDIO_DIR)/audioShell.c,$(wildcard $(AUDIO_DIR)/*.c)) $(AUDIO_DIR)/drivers/sb/sbCalc.c AUDIO_OBJS = $(patsubst $(AUDIO_DIR)/%.c,$(BINDIR)/lib/%.o,$(AUDIO_SRCS)) -LIBS_A = $(DVXTEST_ARCHIVES) +LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) -CFLAGS = $(DVXTEST_CFLAGS) -I$(AUDIO_DIR) -I$(AUDIO_DIR)/drivers/sb -I$(DVXTEST_ROOT)/src/libs/kpunch -DAUDIO_GOLDEN='"$(GOLDEN)"' -DAUDIO_WORKDIR='"$(WORKDIR)"' +CFLAGS = $(DVXTEST_CFLAGS) -I$(AUDIO_DIR) -I$(AUDIO_DIR)/drivers/sb -I$(DVXTEST_ROOT)/src/libs/kpunch -DAUDIO_GOLDEN='"$(GOLDEN)"' -DAUDIO_WORKDIR='"$(WORKDIR)"' -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' # The fake drivers: name, priority, open / play succeed, rate, bits, # channels, DAC, tone generator, clocked @@ -82,7 +84,7 @@ $(DRVDIR)/midifake.drv: fakeSynth.c $(AUDIO_DIR)/audioDrv.h $(DVX_MAKEFILES) $(AUDIO_TEST): audioTest.c $(AUDIO_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -o $@ $< $(AUDIO_OBJS) $(DVXTEST_LINK) + $(HOSTCC) $(CFLAGS) -o $@ $< $(AUDIO_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm -include $(AUDIO_OBJS:.o=.d) $(AUDIO_TEST).d $(FAKE_DRVS:.drv=.d) diff --git a/src/test/audio/audioTest.c b/src/test/audio/audioTest.c index d0e2eb81..613f2a9b 100644 --- a/src/test/audio/audioTest.c +++ b/src/test/audio/audioTest.c @@ -45,7 +45,11 @@ // files WAV and VOC decoding, good and damaged, and truncations // sb BLASTER parsing, playback plans per DSP version, DMA // buffer placement -// basic the sound.bas functions +// basic the sound.bas functions: tones, loaded and created sounds +// (numbered per program, forgotten when it ends), voice +// volume / pan / pitch / frequency, device facts, streams +// written with binary data, FM voices and the whole chip, +// the raw MIDI port (music.bas), DVX's event sounds // fm the FM chip (fake synth driver): shared voices handed out // least recently used first, running out, stale ids, the // channel register map (OPL2 and the OPL3's second bank), @@ -70,6 +74,9 @@ #include "audioInt.h" #include "sbCalc.h" #include "stb_ds.h" +#include "compiler/compiler.h" +#include "runtime/vm.h" +#include "formrt/formrt.h" #include #include @@ -124,6 +131,29 @@ #define DESK_WAIT_MAX_US 1000000 #define YIELD_STEP_US 1000 #define DRIVER_NAMES_MAX 8 +#define DRIVERS_FAKE 5 // the fake drivers that load (alpha .. mono8) +#define MIDI_FM_INDEX 2 +#define MIDI_MPU_INDEX 1 +#define PERCENT_FULL 100 +#define PERCENT_HALF 50 +#define PERCENT_INI 40 +#define PERCENT_STORED 70 +#define BAS_BAD_HANDLE 99 +#define BAS_TRUE (-1) +#define BAS_SOUND_SLOTS 64 // audioBas.c BAS_SLOTS +#define BAS_PCM_FRAMES 2205 // 100 ms at 22,050 Hz +#define BAS_PCM_MS 100 +#define BAS_PCM_LEVEL 200 // an 8-bit sample above the middle (128) +#define BAS_STREAM_FRAMES 100 +#define BAS_RENDER_FRAMES 32 +#define BAS_PITCH_DOUBLE 200 +#define BAS_FM_REG 0xB0 // FM_KEY_BLOCK +#define BAS_FM_RAW_REG 0xB3 +#define BAS_MIDI_NOTE_ON 0x90 +#define BAS_MIDI_NOTE 0x3C +#define BAS_BYTE_TOO_BIG 256 +#define BAS_CAPTURE_MAX 256 +#define BAS_SRC_MAX 65536 // A recorded end notification typedef struct { @@ -158,12 +188,18 @@ static uint32_t sFmWrites = 0; static uint8_t sMidiBytes[MIDI_BYTES_MAX]; static int32_t sMidiCount = 0; static bool sMidiReady = true; +static char sCapture[BAS_CAPTURE_MAX]; // ============================================================ // Prototypes // ============================================================ +static void basicLiveTests(void); +static void basicSampleTests(void); +static void basicStreamTests(void); +static void basicSynthTests(void); static void basicTests(void); +static void captureOut(void *ctx, const char *text, bool newline); static void checkGolden(const char *name, uint64_t hash); static void countCall(void *userData); static void deskTests(void); @@ -199,9 +235,386 @@ static void render(uint32_t frames); static void saveGoldens(void); static void sbTests(void); static void setIni(const char *extra); +static void settingsBasicTests(void); +static void settingsTests(void); static void toneTests(void); static void voiceTests(void); +// prefs.bas (libdvx dvxPrefsBas.c) +void PrefsClose(int32_t handle); +int32_t PrefsLoad(const char *path); + + +// sound.bas and music.bas compiled with a program whose DECLARE LIBRARY +// calls reach the library for real: BINARY STRING data with zero bytes +// arrives whole (its length shows in SoundLength and the bytes a stream +// takes), text fields come back. +static void basicLiveTests(void) { + static char src[BAS_SRC_MAX]; + int32_t soundLen = 0; + int32_t musicLen = 0; + char *soundBas = platformReadFile(BASIC_INCLUDE_DIR "/sound.bas", &soundLen); + char *musicBas = platformReadFile(BASIC_INCLUDE_DIR "/music.bas", &musicLen); + + tCase("basic: sound.bas from a program (binary data, text fields)"); + setIni("driver = gamma\nmidi = mpu401\n"); + sAppId = APP_A; + TEST_TRUE(soundBas && musicBas); + + if (soundBas && musicBas) { + snprintf(src, sizeof(src), "%s\n%s\n" + "DIM d AS STRING\n" + "DIM i AS LONG\n" + "DIM s AS LONG\n" + "DIM st AS LONG\n" + "FOR i = 1 TO 1102\n" + " d = d + CHR$(0) + CHR$(255)\n" + "NEXT i\n" + "s = SoundCreate(d + CHR$(0), SOUND_RATE_DEFAULT, 8, 1)\n" + "PRINT SoundLength(s); SoundPlay(s, 100, SOUND_PAN_LEFT, 0) > 0\n" + "st = SoundStreamOpen(SOUND_RATE_DEFAULT, 16, 1, 0)\n" + "PRINT SoundStreamWrite(st, CHR$(0) + CHR$(0) + CHR$(0) + CHR$(7) + CHR$(0)); SoundStreamQueued(st)\n" + "PRINT SoundCaps(\"rate\"); FmInfo$(\"voices\"); \"/\"; MidiPortInfo$(\"present\")\n", soundBas, musicBas); + + BasCompilerT *compiler = basCompilerCreate(src, (int32_t)strlen(src)); + BasModuleT *mod = NULL; + + if (basCompile(compiler)) { + mod = basCompilerBuildModule(compiler); + } else { + printf(" compile error: %s\n", basCompilerError(compiler)); + } + + basCompilerDestroy(compiler); + + BasVmT *vm = basVmCreate(); + + TEST_TRUE(mod && vm); + + if (mod && vm) { + BasExternCallbacksT ext = { basExternResolve, basExternCall, NULL }; + + sCapture[0] = '\0'; + basVmLoadModule(vm, mod); + basVmSetPrintCallback(vm, captureOut, NULL); + basVmSetExternCallbacks(vm, &ext); + basNativeHostSetLive(true); + + BasVmResultE rr = basVmRun(vm); + + basNativeHostSetLive(false); + TEST_TRUE(rr == BAS_VM_HALTED || rr == BAS_VM_OK); + TEST_STR_EQ(sCapture, "100 True \n4 2 \n22050 9/-1\n"); + } + + basVmDestroy(vm); + basModuleFree(mod); + } + + free(soundBas); + free(musicBas); + audioAppEnded(APP_A); + sAppId = 0; +} + + +// sound.bas: loaded and created sounds, voices changed while playing, +// device facts. +static void basicSampleTests(void) { + static uint8_t pcm8[BAS_PCM_FRAMES]; + static uint8_t wav[WAV_MAX]; + int16_t stereo[PCM_FRAMES * 2]; + AudioInfoT info; + + tCase("basic: device facts, program volume, triangle tones"); + setIni("driver = gamma\n"); + sAppId = APP_A; + TEST_EQ(SoundCaps("RATE"), RATE_DEV); + TEST_EQ(SoundCaps("bits"), 16); + TEST_EQ(SoundCaps("channels"), 2); + TEST_EQ(SoundCaps("running"), BAS_TRUE); + TEST_EQ(SoundCaps("dac"), BAS_TRUE); + TEST_EQ(SoundCaps("tone"), 0); + audioGetInfo(&info); + TEST_EQ(SoundCaps("voices"), (int32_t)info.voiceLimit); + TEST_EQ(SoundCaps("synth"), info.hasSynth ? BAS_TRUE : 0); + TEST_EQ(SoundCaps("nonsense"), 0); + TEST_EQ(SoundCaps(NULL), 0); + TEST_EQ(SoundGetVolume(), PERCENT_FULL); + SoundVolume(PERCENT_HALF); + TEST_EQ(SoundGetVolume(), PERCENT_HALF); + SoundVolume(PERCENT_FULL); + + int32_t tri = PlayTriangle(TONE_HZ, 0, PERCENT_FULL); + + TEST_TRUE(tri > 0); + TEST_EQ(audioMixer.voices[(tri - 1) % 256].wave, AUDIO_WAVE_TRIANGLE); + SoundSetFrequency(tri, TONE_HZ * 2); + TEST_STR_EQ(SoundError(), "No error"); + TEST_EQ(audioMixer.voices[(tri - 1) % 256].toneHz, TONE_HZ * 2); + SoundSetFrequency(tri, 5); + TEST_STR_EQ(SoundError(), "Invalid argument"); + SoundSetFrequency(tri, -1); + TEST_STR_EQ(SoundError(), "Invalid argument"); + StopWave(tri); + SoundSetVolume(tri, PERCENT_HALF); + TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_VOICE)); + + tCase("basic: sounds made from raw data, panned, louder, faster"); + memset(pcm8, BAS_PCM_LEVEL, sizeof(pcm8)); + + int32_t snd = SoundCreate(pcm8, BAS_PCM_FRAMES, RATE_DEV, 8, 1); + + TEST_TRUE(snd > 0); + TEST_EQ(SoundLength(snd), BAS_PCM_MS); + TEST_EQ(SoundCreate(pcm8, BAS_PCM_FRAMES, 100, 8, 1), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + TEST_EQ(SoundCreate(pcm8, BAS_PCM_FRAMES, RATE_DEV, 12, 1), 0); + TEST_EQ(SoundCreate(pcm8, 0, RATE_DEV, 8, 1), 0); + TEST_EQ(SoundCreate(pcm8, -1, RATE_DEV, 8, 1), 0); + + // A trailing partial frame is ignored: 16-bit stereo, 3 bytes over. + memset(stereo, 0, sizeof(stereo)); + + int32_t st = SoundCreate((const uint8_t *)stereo, (int32_t)sizeof(stereo) + 3 - 4, RATE_DEV, 16, 2); + + TEST_TRUE(st > 0 && st != snd); + TEST_EQ(SoundLength(st), (PCM_FRAMES - 1) * MS_PER_SEC / RATE_DEV); + SoundFree(st); + TEST_EQ(SoundLength(st), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + + int32_t v = SoundPlay(snd, PERCENT_FULL, -PERCENT_FULL, 0); + + TEST_TRUE(v > 0); + render(BAS_RENDER_FRAMES); + TEST_TRUE(sOut[0] != 0 && sOut[1] == 0); + SoundSetPan(v, PERCENT_FULL * 2); + TEST_STR_EQ(SoundError(), "No error"); + render(BAS_RENDER_FRAMES); + TEST_TRUE(sOut[0] == 0 && sOut[1] != 0); + TEST_EQ(audioMixer.voices[(v - 1) % 256].step, MIX_FRAC_ONE); + SoundSetPitch(v, BAS_PITCH_DOUBLE); + TEST_EQ(audioMixer.voices[(v - 1) % 256].step, MIX_FRAC_ONE * 2); + SoundSetPitch(v, PERCENT_FULL * PERCENT_FULL); + TEST_EQ(audioMixer.voices[(v - 1) % 256].step, MIX_FRAC_ONE * AUDIO_PITCH_MAX / AUDIO_PITCH_NORMAL); + SoundSetVolume(v, 0); + render(BAS_RENDER_FRAMES); + TEST_EQ(outPeak(BAS_RENDER_FRAMES), 0); + TEST_EQ(SoundPlay(snd + 1, PERCENT_FULL, 0, 0), 0); + TEST_EQ(SoundPlay(0, PERCENT_FULL, 0, 0), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + + // Freeing a sound stops its voices. + int32_t loopV = SoundPlay(snd, PERCENT_FULL, 0, BAS_TRUE); + + render(BAS_PCM_FRAMES + BAS_RENDER_FRAMES); + TEST_EQ(SoundPlaying(loopV), BAS_TRUE); + SoundFree(snd); + TEST_EQ(SoundPlaying(loopV), 0); + TEST_EQ(SoundPlaying(v), 0); + + tCase("basic: sounds loaded from files"); + uint32_t n = makeWav(wav, 1, 1, RATE_HALF, 8, 1, pcm8, PCM_FRAMES, false); + FILE *fp = fopen("basic.wav", "wb"); + + TEST_TRUE(fp && fwrite(wav, 1, n, fp) == n); + + if (fp) { + fclose(fp); + } + + int32_t ld = SoundLoad("basic.wav"); + + TEST_TRUE(ld > 0); + TEST_EQ(SoundLength(ld), PCM_FRAMES * MS_PER_SEC / RATE_HALF); + TEST_TRUE(SoundPlay(ld, PERCENT_HALF, 0, 0) > 0); + TEST_EQ(SoundLoad("missing.wav"), 0); + TEST_STR_EQ(SoundError(), "The file could not be read"); + + tCase("basic: sound numbers belong to their program and end with it"); + sAppId = APP_B; + TEST_EQ(SoundLength(ld), 0); + TEST_EQ(SoundPlay(ld, PERCENT_FULL, 0, 0), 0); + SoundFree(ld); + sAppId = APP_A; + TEST_EQ(SoundLength(ld), PCM_FRAMES * MS_PER_SEC / RATE_HALF); + audioAppEnded(APP_A); + TEST_EQ(SoundLength(ld), 0); + TEST_EQ(SoundLoad("basic.wav"), 1); + audioAppEnded(APP_A); + + tCase("basic: every sound number taken"); + int32_t last = 0; + + for (int32_t i = 0; i < BAS_SOUND_SLOTS; i++) { + last = SoundCreate(pcm8, 1, RATE_DEV, 8, 1); + } + + TEST_EQ(last, BAS_SOUND_SLOTS); + TEST_EQ(SoundCreate(pcm8, 1, RATE_DEV, 8, 1), 0); + TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_NO_MEMORY)); + SoundFree(last); + TEST_EQ(SoundCreate(pcm8, 1, RATE_DEV, 8, 1), last); + audioAppEnded(APP_A); + audioAppEnded(APP_B); + sAppId = 0; +} + + +// sound.bas streams: binary PCM written as it is played. +static void basicStreamTests(void) { + int16_t pcm[BAS_STREAM_FRAMES]; + + tCase("basic: a stream written with binary data"); + setIni("driver = gamma\n"); + sAppId = APP_A; + + for (int32_t i = 0; i < BAS_STREAM_FRAMES; i++) { + pcm[i] = TONE_AMP; + } + + TEST_EQ(SoundStreamOpen(100, 16, 1, 0), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + TEST_EQ(SoundStreamOpen(RATE_DEV, 16, 1, -1), 0); + + int32_t h = SoundStreamOpen(RATE_DEV, 16, 1, 0); + + TEST_TRUE(h > 0); + + int32_t room = SoundStreamSpace(h); + + TEST_TRUE(room >= RATE_DEV * AUDIO_STREAM_MS_DEFAULT / MS_PER_SEC); + TEST_EQ(SoundStreamQueued(h), 0); + + // Whole frames only: the odd byte is left for the next write. + TEST_EQ(SoundStreamWrite(h, (const uint8_t *)pcm, (int32_t)sizeof(pcm) + 1 - 2), (int32_t)sizeof(pcm) - 2); + TEST_EQ(SoundStreamQueued(h), BAS_STREAM_FRAMES - 1); + TEST_EQ(SoundStreamSpace(h), room - (BAS_STREAM_FRAMES - 1)); + TEST_EQ(SoundStreamWrite(h, (const uint8_t *)pcm, 0), 0); + + int32_t voice = SoundStreamVoice(h); + + TEST_TRUE(voice > 0); + render(BAS_RENDER_FRAMES); + TEST_TRUE(outPeak(BAS_RENDER_FRAMES) > 0); + TEST_TRUE(SoundStreamQueued(h) < BAS_STREAM_FRAMES - 1); + SoundSetVolume(voice, 0); + TEST_STR_EQ(SoundError(), "No error"); + render(BAS_RENDER_FRAMES); + TEST_EQ(outPeak(BAS_RENDER_FRAMES), 0); + + // More than fits: the bytes taken say how much. + int32_t taken = 0; + int32_t space = SoundStreamSpace(h); + + for (int32_t i = 0; i * BAS_STREAM_FRAMES <= space; i++) { + taken += SoundStreamWrite(h, (const uint8_t *)pcm, (int32_t)sizeof(pcm)); + } + + TEST_EQ(taken, space * (int32_t)sizeof(int16_t)); + TEST_EQ(SoundStreamSpace(h), 0); + + // Another program cannot use it; closing forgets the number. + sAppId = APP_B; + TEST_EQ(SoundStreamQueued(h), 0); + TEST_EQ(SoundStreamVoice(h), 0); + SoundStreamClose(h); + sAppId = APP_A; + TEST_TRUE(SoundStreamQueued(h) > 0); + SoundStreamClose(h); + TEST_EQ(SoundStreamQueued(h), 0); + TEST_EQ(SoundStreamWrite(h, (const uint8_t *)pcm, (int32_t)sizeof(pcm)), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + + // A program's streams close when it ends. + h = SoundStreamOpen(RATE_HALF, 8, 2, AUDIO_STREAM_MS_MIN); + TEST_TRUE(h > 0); + audioAppEnded(APP_A); + TEST_EQ(SoundStreamSpace(h), 0); + audioAppEnded(APP_B); + sAppId = 0; +} + + +// sound.bas FM and music.bas MIDI port functions (the fake synthesizer). +static void basicSynthTests(void) { + char text[EVENT_TEXT_MAX]; + + tCase("basic: FM voices and the whole chip"); + setIni("driver = gamma\n"); + sAppId = APP_A; + TEST_STR_EQ(FmInfo("description"), "Fake FM (9 voices)"); + snprintf(text, sizeof(text), "%d", FM_OPL2_VOICES); + TEST_STR_EQ(FmInfo("VOICES"), text); + TEST_STR_EQ(FmInfo("free"), text); + TEST_STR_EQ(FmInfo("stereo"), "0"); + TEST_STR_EQ(FmInfo("owned"), "0"); + TEST_STR_EQ(FmInfo("nonsense"), ""); + + int32_t v = FmAlloc(); + + TEST_TRUE(v > 0); + snprintf(text, sizeof(text), "%d", FM_OPL2_VOICES - 1); + TEST_STR_EQ(FmInfo("free"), text); + TEST_EQ(FmWrite(v, BAS_FM_REG, AUDIO_FM_KEY_ON), BAS_TRUE); + TEST_EQ(FmWrite(v, BAS_BYTE_TOO_BIG, 0), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + TEST_EQ(FmWrite(v, BAS_FM_REG, BAS_BYTE_TOO_BIG), 0); + TEST_EQ(FmWrite(v + 1, BAS_FM_REG, 0), 0); + FmFree(v); + TEST_STR_EQ(SoundError(), "No error"); + snprintf(text, sizeof(text), "%d", FM_OPL2_VOICES); + TEST_STR_EQ(FmInfo("free"), text); + FmFree(v); + TEST_TRUE(strcmp(SoundError(), "No error") != 0); + + TEST_EQ(FmRawAcquire(), BAS_TRUE); + TEST_STR_EQ(FmInfo("owned"), "-1"); + TEST_EQ(FmAlloc(), 0); + TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_BUSY)); + TEST_EQ(FmRawWrite(BAS_FM_RAW_REG, AUDIO_FM_KEY_ON), BAS_TRUE); + TEST_EQ(sFmRegs[BAS_FM_RAW_REG], AUDIO_FM_KEY_ON); + TEST_EQ(FmRawWrite(-1, 0), 0); + TEST_EQ(FmRawWrite(BAS_FM_RAW_REG, BAS_BYTE_TOO_BIG), 0); + sAppId = APP_B; + TEST_EQ(FmRawAcquire(), 0); + TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_BUSY)); + sAppId = APP_A; + FmRawRelease(); + TEST_STR_EQ(FmInfo("owned"), "0"); + TEST_EQ(sFmRegs[BAS_FM_RAW_REG], 0); + audioAppEnded(APP_A); + audioAppEnded(APP_B); + + tCase("basic: the raw MIDI port (music.bas)"); + setIni("driver = gamma\nmidi = mpu401\n"); + TEST_STR_EQ(MidiPortInfo("description"), "Fake MIDI port"); + TEST_STR_EQ(MidiPortInfo("Present"), "-1"); + TEST_STR_EQ(MidiPortInfo("owned"), "0"); + TEST_STR_EQ(MidiPortInfo("nonsense"), ""); + TEST_EQ(MidiPut(BAS_MIDI_NOTE_ON), 0); + TEST_EQ(MidiAcquire(), BAS_TRUE); + TEST_STR_EQ(MidiPortInfo("owned"), "-1"); + sMidiCount = 0; + TEST_EQ(MidiPut(BAS_MIDI_NOTE_ON), BAS_TRUE); + TEST_EQ(MidiPut(BAS_BYTE_TOO_BIG), 0); + TEST_STR_EQ(SoundError(), "Invalid argument"); + sMidiReady = false; + TEST_EQ(MidiPut(BAS_MIDI_NOTE), 0); + TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_BUSY)); + sMidiReady = true; + TEST_EQ(MidiPut(BAS_MIDI_NOTE), BAS_TRUE); + TEST_EQ(sMidiCount, 2); + TEST_EQ(sMidiBytes[1], BAS_MIDI_NOTE); + MidiRelease(); + TEST_STR_EQ(MidiPortInfo("owned"), "0"); + MidiRelease(); + TEST_TRUE(strcmp(SoundError(), "No error") != 0); + audioAppEnded(APP_A); + sAppId = 0; +} + static void basicTests(void) { tCase("basic: sound.bas functions"); @@ -246,6 +659,14 @@ static void basicTests(void) { } +static void captureOut(void *ctx, const char *text, bool newline) { + size_t used = strlen(sCapture); + + (void)ctx; + snprintf(sCapture + used, sizeof(sCapture) - used, "%s%s", text, newline ? "\n" : ""); +} + + static void checkGolden(const char *name, uint64_t hash) { for (int32_t i = 0; i < sGoldenCount; i++) { if (strcmp(sGolden[i].name, name) == 0) { @@ -261,7 +682,7 @@ static void checkGolden(const char *name, uint64_t hash) { } if (!sUpdate) { - TEST_TRUE(!"golden missing (run with UPDATE_GOLDENS=1)"); + TEST_FAIL("golden missing (run with UPDATE_GOLDENS=1)"); return; } @@ -628,6 +1049,17 @@ static void fileTests(void) { TEST_EQ(frames, 2); free(out); } + { + // An empty type 1 block (no sample bytes) before any sound adds + // nothing to the still unallocated buffer, then 2 bytes follow. + const uint8_t blocks[] = { 1, 2, 0, 0, 165, 0, 1, 4, 0, 0, 165, 0, 0x80, 0x90, 0 }; + + n = makeVoc(wav, 0, blocks, sizeof(blocks)); + TEST_EQ(audioDecode(wav, n, &out, &frames, &fmt), AUDIO_OK); + TEST_EQ(frames, 2); + TEST_EQ(out[1], 16 << 8); + free(out); + } tCase("files: VOC damaged or unsupported"); { @@ -1629,7 +2061,7 @@ static void saveGoldens(void) { f = fopen(AUDIO_GOLDEN, "w"); if (!f) { - TEST_TRUE(!"cannot write the golden file"); + TEST_FAIL("cannot write the golden file"); return; } @@ -1723,6 +2155,135 @@ static void setIni(const char *extra) { } +// sound.bas: DVX's own sounds and sound settings (after deskTests made +// ding.wav) +static void settingsBasicTests(void) { + tCase("basic: desktop events"); + setIni("driver = gamma\nvolume = 40\nmidi = fm\n[sounds]\nerror = ding.wav\n"); + + int32_t h = PrefsLoad(DVX_INI_PATH); + + TEST_TRUE(h > 0); + TEST_EQ(SoundEventCount(), SoundCountE); + TEST_STR_EQ(SoundEventLabel(SoundDefaultE), "Default beep"); + TEST_STR_EQ(SoundEventLabel(-1), ""); + TEST_STR_EQ(SoundEventKey(SoundInfoE), "information"); + TEST_STR_EQ(SoundEventKey(SoundCountE), ""); + TEST_STR_EQ(SoundEventFile(h, SoundErrorE), "ding.wav"); + TEST_STR_EQ(SoundEventFile(h, SoundExitE), ""); + SoundSetEventFile(h, SoundExitE, "bye.wav"); + TEST_STR_EQ(SoundEventFile(h, SoundExitE), "bye.wav"); + TEST_STR_EQ(SoundEventFile(BAS_BAD_HANDLE, SoundErrorE), ""); + SoundSetEventFile(BAS_BAD_HANDLE, SoundExitE, "x.wav"); + TEST_STR_EQ(SoundFolder(), AUDIO_SOUNDS_DIR); + SoundEventsReload(); + + tCase("basic: test a sound file, master volume"); + sAppId = APP_A; + TEST_TRUE(SoundTestFile("ding.wav") > 0); + TEST_EQ(SoundTestFile("missing.wav"), 0); + TEST_STR_EQ(SoundError(), "The file could not be read"); + SoundSetMasterVolume(PERCENT_HALF); + TEST_EQ(audioGetMasterVolume(), HALF); + TEST_EQ(SoundMasterVolume(), PERCENT_HALF); + SoundSetMasterVolume(PERCENT_FULL * 2); + TEST_EQ(SoundMasterVolume(), PERCENT_FULL); + + tCase("basic: DVX's event sounds, as C programs play them"); + AudioStatsT st; + + audioDeskInit(); + audioGetStats(&st); + + uint32_t before = st.playing; + + SoundPlayEvent(SoundErrorE, 0); + audioGetStats(&st); + TEST_EQ(st.playing, before + 1); + SoundPlayEvent(SoundWarningE, BAS_TRUE); + SoundPlayEvent(SoundCountE, 0); + audioGetStats(&st); + TEST_EQ(st.playing, before + 1); + audioShutdown(); + sAppId = 0; + + tCase("basic: drivers and MIDI choices"); + TEST_STR_EQ(SoundDriverAuto(), AUDIO_DRIVER_AUTO); + TEST_EQ(SoundDriverCount(), DRIVERS_FAKE); + TEST_STR_EQ(SoundDriverName(0), "alpha"); + TEST_STR_EQ(SoundDriverName(DRIVERS_FAKE - 1), "mono8"); + TEST_STR_EQ(SoundDriverName(DRIVERS_FAKE), ""); + TEST_STR_EQ(SoundDriverName(-1), ""); + TEST_EQ(SoundMidiCount(), AUDIO_MIDI_CHOICE_COUNT); + TEST_STR_EQ(SoundMidiLabel(0), "Automatic"); + TEST_STR_EQ(SoundMidiLabel(AUDIO_MIDI_CHOICE_COUNT), ""); + + tCase("basic: [audio] settings through a settings handle"); + TEST_STR_EQ(SoundSettingDriver(h), "gamma"); + TEST_EQ(SoundSettingMidi(h), MIDI_FM_INDEX); + TEST_EQ(SoundSettingVolume(h), PERCENT_INI); + SoundSettingsStore(h, "beta", MIDI_MPU_INDEX, PERCENT_STORED); + TEST_STR_EQ(SoundSettingDriver(h), "beta"); + TEST_EQ(SoundSettingMidi(h), MIDI_MPU_INDEX); + TEST_EQ(SoundSettingVolume(h), PERCENT_STORED); + SoundSettingsStore(BAS_BAD_HANDLE, "beta", 0, 0); + TEST_STR_EQ(SoundSettingDriver(BAS_BAD_HANDLE), AUDIO_DRIVER_AUTO); + TEST_EQ(SoundSettingMidi(BAS_BAD_HANDLE), 0); + TEST_EQ(SoundSettingVolume(BAS_BAD_HANDLE), PERCENT_FULL); + PrefsClose(h); +} + + +// The settings the Sounds page and the device share (audioSet.c, +// audioDeskFile). +static void settingsTests(void) { + tCase("settings: [audio] and [sounds] keys"); + setIni("driver = gamma\nvolume = 150\nmidi = FM\n[sounds]\nerror = ding.wav\n"); + + PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH); + + TEST_STR_EQ(audioSettingDriver(prefs), "gamma"); + TEST_EQ(audioSettingVolume(prefs), PERCENT_FULL); + TEST_EQ(audioSettingMidi(prefs), MIDI_FM_INDEX); + TEST_STR_EQ(audioMidiChoices[MIDI_FM_INDEX].iniName, AUDIO_MIDI_FM); + TEST_STR_EQ(audioDeskFile(prefs, SoundErrorE), "ding.wav"); + TEST_STR_EQ(audioDeskFile(prefs, SoundWarningE), ""); + TEST_STR_EQ(audioDeskFile(prefs, SoundCountE), ""); + audioDeskSetFile(prefs, SoundWarningE, "w.wav"); + audioDeskSetFile(prefs, SoundQuestionE, NULL); + audioDeskSetFile(prefs, SoundCountE, "x.wav"); + TEST_STR_EQ(audioDeskFile(prefs, SoundWarningE), "w.wav"); + TEST_STR_EQ(prefsGetString(prefs, AUDIO_SOUNDS_SECTION, "question", "?"), ""); + audioSettingsStore(prefs, "", AUDIO_MIDI_CHOICE_COUNT, -1); + TEST_STR_EQ(audioSettingDriver(prefs), AUDIO_DRIVER_AUTO); + TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, ""), AUDIO_MIDI_AUTO); + TEST_EQ(audioSettingVolume(prefs), 0); + audioSettingsStore(prefs, "delta", MIDI_MPU_INDEX, PERCENT_HALF); + TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, ""), "delta"); + TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, ""), AUDIO_MIDI_MPU401); + TEST_EQ(prefsGetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, 0), PERCENT_HALF); + prefsClose(prefs); + + tCase("settings: defaults and percent conversion"); + prefs = prefsCreate(); + TEST_STR_EQ(audioSettingDriver(prefs), AUDIO_DRIVER_AUTO); + TEST_EQ(audioSettingMidi(prefs), 0); + TEST_EQ(audioSettingVolume(prefs), PERCENT_FULL); + prefsClose(prefs); + TEST_EQ(audioPercentToVolume(PERCENT_HALF), HALF); + TEST_EQ(audioPercentToVolume(PERCENT_FULL * 2), FULL); + TEST_EQ(audioPercentToVolume(-1), 0); + + tCase("settings: the device starts at DVX.INI's master volume"); + setIni("driver = gamma\nvolume = 50\n"); + + AudioCtxT *ctx = audioOpen(); + + TEST_EQ(audioGetMasterVolume(), HALF); + audioClose(ctx); +} + + static void toneTests(void) { AudioCtxT *ctx; @@ -1867,7 +2428,7 @@ int main(void) { mkdir(AUDIO_WORKDIR, DIR_MODE); if (chdir(AUDIO_WORKDIR) != 0 || (mkdir(DVX_CONFIG_DIR, DIR_MODE) != 0 && access(DVX_CONFIG_DIR, F_OK) != 0)) { - TEST_TRUE(!"cannot prepare the work folder"); + TEST_FAIL("cannot prepare the work folder"); return tSuiteEnd(); } @@ -1880,10 +2441,16 @@ int main(void) { fileTests(); sbTests(); basicTests(); + basicSampleTests(); + basicStreamTests(); fmTests(); midiTests(); + basicSynthTests(); + basicLiveTests(); notesTests(); deskTests(); + settingsTests(); + settingsBasicTests(); platformTests(); audioShutdown(); platformTimerShutdown(); diff --git a/src/test/basdev/Makefile b/src/test/basdev/Makefile new file mode 100644 index 00000000..e298aeee --- /dev/null +++ b/src/test/basdev/Makefile @@ -0,0 +1,69 @@ +# 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. + +# basdev suite: the basdev library (compiler.bas / debug.bas entry +# points), the shared debugger, validator and proc scanner, and the run +# host -- from C and from a BASIC program making DECLARE LIBRARY calls for +# real. The BASIC program is the DOS fixture src/test/dos/fixtures/paride +# (it compiles, debugs and builds the project in its kid folder). +# +# make -C src/test/basdev # build + run (SAN=1 default) + +.DEFAULT_GOAL := all + +include ../dvxtest/dvxtest.mk + +DVXBASIC = $(DVXTEST_BASICDIR) +BINDIR = $(DVXTEST_BINDIR)/basdev +TARGET = $(BINDIR)/testBasDev +DEV_SRCS = basValidate.c basDebug.c basProcs.c basSource.c basHelpMap.c basProject.c basDevBas.c +OBJS = $(BINDIR)/testBasDev.o $(BINDIR)/basBuild.o $(patsubst %.c,$(BINDIR)/%.o,$(DEV_SRCS)) +CFLAGS = $(DVXTEST_CFLAGS) -I$(DVXBASIC) -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -I$(DVXTEST_ROOT)/src/libs/kpunch/audio -DBDT_FIXTURE_DIR='"$(DVXTEST_ROOT)/src/test/dos/fixtures/paride"' -DBDT_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' -DBDT_BASCOMP='"$(DVXTEST_BASCOMP)"' + +.PHONY: all run clean + +all: run + +$(BINDIR)/%.o: %.c $(DVX_MAKEFILES) + @mkdir -p $(BINDIR) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + +$(BINDIR)/basBuild.o: $(DVXBASIC)/basBuild.c $(DVX_MAKEFILES) + @mkdir -p $(BINDIR) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + +$(BINDIR)/%.o: $(DVXBASIC)/basdev/%.c $(DVX_MAKEFILES) + @mkdir -p $(BINDIR) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + +$(TARGET): $(OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_ARCHIVES) | dvxtest-libs + $(HOSTCC) -o $@ $(OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LINK) + +-include $(OBJS:.o=.d) + +# Runs from its object directory so the files it writes never land in the +# source tree. bascomp carries the STUB the build case needs. +run: $(TARGET) | dvxtest-bascomp + cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./testBasDev + +clean: + rm -rf $(BINDIR) diff --git a/src/test/basdev/testBasDev.c b/src/test/basdev/testBasDev.c new file mode 100644 index 00000000..d42e86a1 --- /dev/null +++ b/src/test/basdev/testBasDev.c @@ -0,0 +1,1378 @@ +// 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. + +// testBasDev.c -- the basdev library suite +// +// caseBasicIde runs the DOS fixture PARIDE (src/test/dos/fixtures/paride) +// on the host: a BASIC program that compiles, debugs and builds the +// project in its kid folder through compiler.bas and debug.bas, with its +// DECLARE LIBRARY calls made for real. The other cases drive the shared +// C pieces directly: the build pipeline's compile and source map, the run +// host, the debugger's view of a paused program and the entry points of +// compiler.bas. + +#include "dvxtest.h" +#include "../../apps/kpunch/dvxbasic/basBuild.h" +#include "../../apps/kpunch/dvxbasic/basdev/basDebug.h" +#include "../../apps/kpunch/dvxbasic/basdev/basProject.h" +#include "../../apps/kpunch/dvxbasic/basdev/basValidate.h" +#include "../../apps/kpunch/dvxbasic/compiler/lexer.h" +#include "../../apps/kpunch/dvxbasic/formrt/basRunHost.h" +#include "../../apps/kpunch/dvxbasic/formrt/formrt.h" +#include "dvxPlat.h" +#include "dvxRes.h" +#include "dvxTypes.h" +#include "pmModel.h" +#include "../../libs/kpunch/audio/audioDrv.h" +#include "../../libs/kpunch/joystick/joystick.h" +#include "../../libs/kpunch/fileops/fileOps.h" +#include "taskSwch.h" +#include "dvxWm.h" + +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include +#include + +#define SCREEN_W 800 +#define SCREEN_H 600 +#define SCREEN_BPP 32 +#define BDT_TEXT_MAX 4096 // captured PRINT text / collected rows +#define BDT_TICK_MS 60 // the fake clock per Timer round +#define BDT_MAX_TICKS 200 // rounds before the fixture counts as hung +#define BDT_BP_FILE_LINE 38 // "total = total + i" in kid/kid.bas +#define BDT_PASS_TITLE "PARIDE PASS 109 built" +#define BDT_PARENT_FORM "ParIde" +#define BDT_KID_FORM "KidForm" +#define BDT_KID_TITLE "Kid" +#define BDT_PREFIX "PARIDE " +#define BDT_RENAME_REFS 70000 // references to rename ... +#define BDT_RENAME_NEWLEN 31000 // ... to a name this long: a result past INT32_MAX bytes +#define BDT_RENAME_REF "a.x\n" // one reference +#define BAS_TRUE (-1) +#define KIND_PROGRAM 0 // compiler.bas COMP_KIND_PROGRAM +#define KIND_MODULE 1 // compiler.bas COMP_KIND_MODULE +#define KIND_FORM 2 // compiler.bas COMP_KIND_FORM +#define KIND_CONTROL 4 // compiler.bas COMP_KIND_CONTROL +#define BDT_CTL_KEYS 8 // UserControl header keys (Name, Width ... DefaultEvent) +#define BDT_RUN_ENDED 4 // debug.bas RUN_ENDED +#define BDT_RUN_ERROR 5 // debug.bas RUN_ERROR +#define BDT_DIAL_ICON "BM a dial" +#define KW_CLASS_TYPE 1 // compiler.bas COMP_KW_TYPE +#define KW_CLASS_LITERAL 2 // compiler.bas COMP_KW_LITERAL +#define NO_INDEX (-1) +#define CONST_MISSING (-999999) // includeConst: no such CONST +#define DECIMAL 10 +#define HEXADECIMAL 16 +#define PERCENT 100 // sound.bas pitches are percent +#define BDT_KW_CONST "CONST " + +// A CONST of an include file and the C value it must equal +typedef struct { + const char *file; + const char *name; + int32_t value; +} BdtConstT; + +// The compiler.bas / debug.bas entry points the C cases call directly +int32_t CompBuildApp(int32_t mdl, int32_t src, const char *outPath, int32_t release); +const char *CompClassEvents(const char *classText); +int32_t CompCompile(int32_t src, int32_t optionExplicit, int32_t validate); +int32_t CompDesignControls(int32_t mdl, int32_t src, const char *appDir); +int32_t CompDesignRelease(void); +const char *CompError(void); +const char *CompErrorFile(void); +int32_t CompErrorFileLine(void); +const char *CompHelpCtrlTopic(const char *typeName); +const char *CompHelpTopic(const char *word); +int32_t CompInCode(const char *source, int32_t position); +int32_t CompIsIdent(const char *ident); +const char *CompKeyword(int32_t idx); +int32_t CompKeywordClass(int32_t idx); +int32_t CompKeywordCount(void); +int32_t CompKeywordReserved(int32_t idx); +void CompModuleFree(int32_t mdl); +const char *CompProcList(const char *source); +const char *CompProcs(int32_t mdl); +const char *CompRenameInCode(const char *source, const char *oldName, const char *newName, int32_t codeOnly); +int32_t CompSourceAdd(int32_t src, const char *path, const char *text, int32_t kind); +int32_t CompSourceCreate(void); +const char *CompSourceFiles(int32_t src); +void CompSourceFree(int32_t src); +int32_t CompSourceFromProject(int32_t src, const char *dbpPath); +const char *CompTokens(const char *source); +const char *CompWithEvents(const char *source); +const char *DbgExec(int32_t run, const char *line); +int32_t ProjAddFile(int32_t prj, const char *fileName); +void ProjClose(int32_t prj); +const char *ProjCtlHeader(const char *text, const char *key); +const char *ProjCtlKeys(void); +const char *ProjCtlNew(const char *name); +const char *ProjCtlSetHeader(const char *text, const char *key, const char *value); +const char *ProjFile(int32_t prj, int32_t idx); +int32_t ProjFileCount(int32_t prj); +int32_t ProjFileFind(int32_t prj, const char *fileName); +int32_t ProjFileKind(int32_t prj, int32_t idx); +const char *ProjFilePath(int32_t prj, int32_t idx); +const char *ProjGet(int32_t prj, const char *key); +int32_t ProjNew(const char *name, const char *folder); +int32_t ProjOpen(const char *dbpPath); +const char *ProjPath(int32_t prj); +int32_t ProjRemoveFile(int32_t prj, int32_t idx); +int32_t ProjRenameFile(int32_t prj, int32_t idx, const char *newName); +int32_t ProjSave(int32_t prj); +int32_t ProjSet(int32_t prj, const char *key, const char *value); +int32_t RunCreate(int32_t mdl, int32_t src, const char *appDir, const char *args); +void RunFree(int32_t run); +const char *RunOutput(int32_t run); +int32_t RunPoll(int32_t run, int32_t ms); +int32_t RunStart(int32_t run, const char *startupForm, int32_t breakOnStart); + +// Text gathered from print callbacks and row callbacks +typedef struct { + char text[BDT_TEXT_MAX]; + int32_t len; +} BdtTextT; + +// The UserControl of caseDesignControls (a source set's and a project's): +// its header (a built control file's CTL0), then its code +#define BDT_DIAL_HEADER \ + "VERSION DVX 1.00\n" \ + "Begin UserControl Dial\n" \ + " ToolboxIcon = \"dial.bmp\"\n" \ + "End\n" + +static const char sDialCtl[] = + BDT_DIAL_HEADER + "Private Sub UserControl_Paint()\n" + "End Sub\n"; + +// The child program of designProject: compiler.bas (the first %s), then +// its own design controls from the project (the second), its module freed +// while they are in use; then it ends. +#define BDT_DESIGN_CHILD_FMT \ + "%s\n" \ + "Dim s As Long\n" \ + "Dim m As Long\n" \ + "s = CompSourceCreate()\n" \ + "If CompSourceFromProject(s, \"%s\") Then m = CompCompile(s, 0, 0)\n" \ + "Print m\n" \ + "Print CompDesignControls(m, s, \"\")\n" \ + "CompSourceFree s\n" \ + "CompModuleFree m\n" + +// Prototypes (alphabetical; main last) +static void caseBasicIde(void); +static void caseCDebugger(void); +static void caseCompileApi(void); +static void caseDesignControls(void); +static void caseIncludeConsts(void); +static void caseProjectApi(void); +static void caseProjectControls(void); +static void caseSourceApi(void); +static bool copyKid(const char *kidDir); +static void designProject(const BasFormRtT *rt); +static BasFormT *formOf(const BasFormRtT *rt, const char *formName); +static const char *formTitle(const BasFormRtT *rt, const char *formName); +static int32_t includeConst(const char *text, const char *name); +static bool includeConstIs(const char *text, const char *name, const char *value); +static const char *includeConstValue(const char *text, const char *name); +static int32_t keywordIndex(const char *word); +static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasValidatorT *validator); +static bool resHas(DvxResHandleT *res, const char *name, const char *want); +static void textPrint(void *ctx, const char *text, bool newline); +static void textRow(void *ctx, const char *const *cells, int32_t cellCount); +int main(void); + + +// The DOS fixture on the host: the parent runs as a compiled program would +// (its forms loaded, then its Timer events), its library calls are real. +static void caseBasicIde(void) { + tCase("a BASIC program compiles, debugs and builds another (compiler.bas, debug.bas)"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + char kidDir[DVX_MAX_PATH]; + char carrierDir[DVX_MAX_PATH]; + char kidApp[DVX_MAX_PATH]; + + // The kid project where runDos stages it (TEST\PARIDE\KID beside the + // app), and the STUB carrier CompBuildApp reads (bascomp has one) + snprintf(kidDir, sizeof(kidDir), "%s/paride/kid", tTempDir()); + snprintf(kidApp, sizeof(kidApp), "%s/kid.app", tTempDir()); + basBuildDirOf(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir)); + platformMkdirRecursive(carrierDir); + remove(kidApp); + TEST_TRUE(copyKid(kidDir)); + TEST_TRUE(platformCopyFile(BDT_BASCOMP, BAS_BUILD_STUB_CARRIER)); + + BasProjectSourceT ps; + BasModuleT *mod = loadProject(BDT_FIXTURE_DIR "/paride.dbp", &ps, NULL); + + TEST_TRUE(mod != NULL); + + if (!mod) { + basBuildFreeProject(&ps); + tShutdown(); + return; + } + + const char *hostError = NULL; + BasRunSpecT spec; + + memset(&spec, 0, sizeof(spec)); + spec.appDir = tTempDir(); + spec.title = BDT_PREFIX; + spec.suppressErrorDialog = true; + + BasFormRtT *rt = basRunHostCreate(tCtx(), mod, &spec, &hostError); + + TEST_TRUE(rt != NULL); + + if (rt) { + for (int32_t i = 0; i < (int32_t)arrlen(ps.frmTexts); i++) { + TEST_TRUE(basRunHostRegisterFrm(rt, ps.frmTexts[i], (int32_t)strlen(ps.frmTexts[i]))); + } + + // ParIde_Load compiles the kid, makes the run and starts it + basNativeHostSetLive(true); + basRunHostLoadForms(rt, BDT_PARENT_FORM); + basVmSetStepLimit(rt->vm, BAS_VM_DEFAULT_STEP_SLICE); + + while (basRunHostSlice(rt) == BAS_VM_STEP_LIMIT) { + } + + // The Timer polls the kid through its stops to the end + for (int32_t i = 0; i < BDT_MAX_TICKS && strncmp(formTitle(rt, BDT_PARENT_FORM), BDT_PREFIX, strlen(BDT_PREFIX)) != 0; i++) { + tAdvanceClock(BDT_TICK_MS); + } + + TEST_STR_EQ(formTitle(rt, BDT_PARENT_FORM), BDT_PASS_TITLE); + struct stat st; + + TEST_TRUE(stat(kidApp, &st) == 0 && st.st_size > 0); + + // Unloading frees the kid's run, module and source set + BasFormT *form = formOf(rt, BDT_PARENT_FORM); + + if (form) { + basFormRtUnloadForm(rt, form); + } + + basNativeHostSetLive(false); + basRunHostDestroy(rt); + } + + basModuleFree(mod); + basBuildFreeProject(&ps); + tShutdown(); +} + + +// The shared C pieces the IDE and the library use: a run with a +// breakpoint, the paused program's variables, evaluation, assignment, +// stepping and the [break] captions. +static void caseCDebugger(void) { + tCase("C: run host + debugger view of a paused program"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + BasProjectSourceT ps; + BasValidatorT validator; + BasModuleT *mod = loadProject(BDT_FIXTURE_DIR "/kid/kid.dbp", &ps, &validator); + + TEST_TRUE(mod != NULL); + TEST_EQ(arrlen(validator.badTypes), 0); + TEST_TRUE(arrlen(validator.entries) > 0); + basValidateFree(&validator); + + if (!mod) { + basBuildFreeProject(&ps); + tShutdown(); + return; + } + + // The breakpoint: kid.bas (the first file) line 38, both ways + int32_t bp = basBuildMapFileLine(ps.map, ps.map[0].path, BDT_BP_FILE_LINE); + const char *mapPath = NULL; + int32_t fileLine = 0; + + TEST_TRUE(bp > 0); + TEST_TRUE(basBuildMapLine(ps.map, bp, &mapPath, &fileLine)); + TEST_EQ(fileLine, BDT_BP_FILE_LINE); + + BdtTextT out; + BdtTextT rows; + const char *hostError = NULL; + BasRunSpecT spec; + + memset(&out, 0, sizeof(out)); + memset(&spec, 0, sizeof(spec)); + spec.appDir = tTempDir(); + spec.printFn = textPrint; + spec.printCtx = &out; + spec.suppressErrorDialog = true; + + BasFormRtT *rt = basRunHostCreate(tCtx(), mod, &spec, &hostError); + BasVmT *vm = rt ? rt->vm : NULL; + BasBreakTitleT *titles = NULL; + + TEST_TRUE(rt != NULL); + + if (!rt) { + basModuleFree(mod); + basBuildFreeProject(&ps); + tShutdown(); + return; + } + + for (int32_t i = 0; i < (int32_t)arrlen(ps.frmTexts); i++) { + basRunHostRegisterFrm(rt, ps.frmTexts[i], (int32_t)strlen(ps.frmTexts[i])); + } + + basVmSetBreakpoints(vm, &bp, 1); + basRunHostLoadForms(rt, BDT_KID_FORM); + basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE); + + BasVmResultE result; + + do { + result = basRunHostSlice(rt); + } while (result == BAS_VM_STEP_LIMIT); + + TEST_EQ(result, BAS_VM_BREAKPOINT); + TEST_EQ(basVmGetCurrentLine(vm), bp); + TEST_STR_EQ(basDbgProcNameForPc(vm, vm->pc), "AddUp"); + TEST_TRUE(basDbgCurProcIdx(vm) >= 0); + + // Locals: the parameter, the local and the shared module variables + memset(&rows, 0, sizeof(rows)); + TEST_TRUE(basDbgLocals(vm, textRow, &rows) >= 4); + TEST_TRUE(strstr(rows.text, "n|Long|4\n") != NULL); + TEST_TRUE(strstr(rows.text, "i|Long|1\n") != NULL); + TEST_TRUE(strstr(rows.text, "total|Long|0\n") != NULL); + TEST_TRUE(strstr(rows.text, "label|String|\"sum\"\n") != NULL); + + // Call stack: AddUp at the line, then the module frame + char expect[BDT_TEXT_MAX]; + + memset(&rows, 0, sizeof(rows)); + TEST_EQ(basDbgCallStack(vm, bp, textRow, &rows), 2); + snprintf(expect, sizeof(expect), "AddUp|%d\n" BAS_DBG_MODULE_FRAME "|\n", (int)bp); + TEST_STR_EQ(rows.text, expect); + + // Watch: a variable path is read directly, an expression is evaluated + char value[BAS_DBG_VALUE_LEN]; + + TEST_TRUE(basDbgEval(vm, "n", value, sizeof(value))); + TEST_STR_EQ(value, "4"); + TEST_TRUE(basDbgEval(vm, "total + n * 10", value, sizeof(value))); + TEST_EQ(atoi(value), 40); + TEST_TRUE(!basDbgEval(vm, "nosuch(", value, sizeof(value))); + TEST_TRUE(!basDbgEval(NULL, "n", value, sizeof(value))); + + // [break] on the program's form captions while paused + basRunHostTagBreak(rt, &titles, true); + TEST_STR_EQ(formTitle(rt, BDT_KID_FORM), BDT_KID_TITLE BAS_RUN_BREAK_TAG); + basRunHostTagBreak(rt, &titles, false); + TEST_STR_EQ(formTitle(rt, BDT_KID_FORM), BDT_KID_TITLE); + arrfree(titles); + + // Immediate: assignment, statement, expression, error; and SetVar + memset(&out, 0, sizeof(out)); + TEST_TRUE(basDbgExec(vm, "total = 3", textPrint, &out)); + TEST_STR_EQ(out.text, "total = 3\n"); + memset(&out, 0, sizeof(out)); + TEST_TRUE(!basDbgExec(vm, "PRINT total * 2", textPrint, &out)); + TEST_EQ(atoi(out.text), 6); + memset(&out, 0, sizeof(out)); + TEST_TRUE(!basDbgExec(NULL, "6 * 7", textPrint, &out)); + TEST_EQ(atoi(out.text), 42); + memset(&out, 0, sizeof(out)); + basDbgExec(vm, "label = ", textPrint, &out); + TEST_TRUE(strncmp(out.text, "Error: ", strlen("Error: ")) == 0); + memset(&out, 0, sizeof(out)); + TEST_TRUE(basDbgSetVar(vm, "total", "total + 4", textPrint, &out)); + TEST_STR_EQ(out.text, "total = 7\n"); + memset(&out, 0, sizeof(out)); + TEST_TRUE(!basDbgSetVar(vm, "nosuch", "1", textPrint, &out)); + TEST_TRUE(!basDbgSetVar(vm, "label", "1 +", textPrint, &out)); + + // Step over: the breakpoint line ran once (7 + 1) + basVmStepOver(vm); + basVmResume(vm); + + do { + result = basRunHostSlice(rt); + } while (result == BAS_VM_STEP_LIMIT); + + TEST_EQ(result, BAS_VM_BREAKPOINT); + TEST_TRUE(basDbgEval(vm, "total", value, sizeof(value))); + TEST_STR_EQ(value, "8"); + + // Run free to the end: 8 + 2 + 3 + 4 + basVmSetBreakpoints(vm, NULL, 0); + basVmClearStepState(vm); + basVmResume(vm); + memset(&out, 0, sizeof(out)); + + do { + result = basRunHostSlice(rt); + } while (result == BAS_VM_STEP_LIMIT); + + TEST_EQ(result, BAS_VM_HALTED); + TEST_TRUE(strstr(out.text, "17") != NULL); + TEST_TRUE(!basRunHostAlive(rt)); + + basRunHostDestroy(rt); + basModuleFree(mod); + basBuildFreeProject(&ps); + tShutdown(); +} + + +// compiler.bas from C: source sets, error mapping, procedure lists, +// identifiers, keywords, the validator's unknown types and member typos. +static void caseCompileApi(void) { + tCase("compiler.bas entry points: errors, maps, procs, validation"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + // A compile error maps back to its file and line + int32_t src = CompSourceCreate(); + + TEST_TRUE(src > 0); + TEST_EQ(CompSourceAdd(src, "first.bas", "DIM a AS LONG\na = 1\n", KIND_MODULE), BAS_TRUE); + TEST_EQ(CompSourceAdd(src, "second.bas", "DIM b AS LONG\nb = \n", KIND_MODULE), BAS_TRUE); + TEST_EQ(CompCompile(src, 0, 0), 0); + TEST_TRUE(CompError()[0] != '\0'); + TEST_TRUE(strncmp(CompError(), "Line", strlen("Line")) != 0); + TEST_STR_EQ(CompErrorFile(), "second.bas"); + TEST_EQ(CompErrorFileLine(), 2); + TEST_TRUE(strstr(CompSourceFiles(src), "first.bas\t1\t") != NULL); + CompSourceFree(src); + + // A form whose control type no widget library registers + src = CompSourceCreate(); + TEST_EQ(CompSourceAdd(src, "f.frm", "Begin Form F\n Begin Bogus b1\n End\nEnd\n", -1), BAS_TRUE); + TEST_EQ(CompCompile(src, 0, BAS_TRUE), 0); + TEST_TRUE(strstr(CompError(), "type 'Bogus' is not registered") != NULL); + CompSourceFree(src); + + // A member typo on a known control fails at compile time when + // validating, and compiles without validation + const char *frm = "Begin Form F\n Begin Label lbl\n End\nEnd\n\nSUB F_Load\n lbl.Captoin = \"x\"\nEND SUB\n"; + + src = CompSourceCreate(); + TEST_EQ(CompSourceAdd(src, "f.frm", frm, KIND_FORM), BAS_TRUE); + TEST_EQ(CompCompile(src, 0, BAS_TRUE), 0); + TEST_STR_EQ(CompErrorFile(), "f.frm"); + TEST_EQ(CompErrorFileLine(), 7); + + int32_t mdl = CompCompile(src, 0, 0); + + TEST_TRUE(mdl > 0); + TEST_STR_EQ(CompProcs(mdl), "F_Load\tSub\tF\n"); + CompModuleFree(mdl); + CompSourceFree(src); + + // Procedure blocks of source text, identifiers, keywords + TEST_STR_EQ(CompProcList("x = 1\nSUB A\nEND SUB\nPrivate Function B(n)\n B = n\nEnd Function\n"), "Sub\tA\t2\t3\nFunction\tB\t4\t6\n"); + TEST_EQ(CompIsIdent("abc_1"), BAS_TRUE); + TEST_EQ(CompIsIdent("1abc"), 0); + TEST_TRUE(CompKeywordCount() > 0); + TEST_TRUE(CompKeyword(0)[0] != '\0'); + TEST_STR_EQ(CompKeyword(-1), ""); + + // The Immediate window without a program + TEST_EQ(atoi(DbgExec(0, "6 * 7")), 42); + tShutdown(); +} + + +// compiler.bas for a form designer: a source set's UserControls made +// control types of the calling program alone (CompDesignControls), their +// module kept past CompModuleFree until they go (CompDesignRelease), a +// second call replacing the first, another program's module refused; a +// control project's (designProject); the header keys (ProjCtlKeys$). +static void caseDesignControls(void) { + tCase("compiler.bas designer: CompDesignControls, CompDesignRelease, CompBuildApp of a control project, ProjCtlKeys$"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + static const char host[] = "Dim x As Integer\n"; + + TEST_STR_EQ(ProjCtlKeys(), "Name\tName\nWidth\tInteger\nHeight\tInteger\nCanGetFocus\tBoolean\nBackColor\tColor\nToolboxIcon\tText\nDescription\tText\nDefaultEvent\tText\n"); + + char dir[DVX_MAX_PATH]; + char want[DVX_MAX_PATH]; + char path[DVX_MAX_PATH]; + + snprintf(dir, sizeof(dir), "%s/dsgnctl", tTempDir()); + snprintf(want, sizeof(want), "%s/dial.bmp", dir); + + int32_t src = CompSourceCreate(); + + TEST_EQ(CompSourceAdd(src, "dial.ctl", sDialCtl, KIND_CONTROL), BAS_TRUE); + + int32_t mdl = CompCompile(src, 0, 0); + + TEST_TRUE(mdl > 0); + + // Outside a program there is no designer to register for + TEST_EQ(CompDesignControls(mdl, src, dir), 0); + TEST_TRUE(CompError()[0] != '\0'); + TEST_EQ(CompDesignRelease(), BAS_TRUE); + + // The calling program: a runtime made current + BasCompileErrorT err; + BasModuleT *hostMod = basBuildCompile(host, (int32_t)strlen(host), false, NULL, NULL, &err); + const char *hostError = NULL; + BasRunSpecT spec; + + memset(&spec, 0, sizeof(spec)); + spec.appDir = tTempDir(); + spec.title = BDT_PARENT_FORM; + spec.suppressErrorDialog = true; + + BasFormRtT *rt = hostMod ? basRunHostCreate(tCtx(), hostMod, &spec, &hostError) : NULL; + BasFormRtT *prev = basFormRtSetCurrent(rt); + + TEST_TRUE(rt != NULL); + TEST_EQ(CompDesignControls(mdl, src, dir), 1); + TEST_STR_EQ(CompError(), ""); + + const WgtIfaceT *iface = basUcOwnIface(rt, "Dial"); + + TEST_TRUE(iface != NULL); + TEST_PTR_EQ(basUcOwnIface(NULL, "Dial"), NULL); + TEST_TRUE(basUcIconPath(iface, path, sizeof(path))); + TEST_STR_EQ(path, want); + + // Freed while its controls are in use: kept, and no new use of it + CompModuleFree(mdl); + TEST_TRUE(basUcOwnIface(rt, "Dial") != NULL); + TEST_EQ(CompDesignControls(mdl, src, dir), 0); + TEST_TRUE(basUcOwnIface(rt, "Dial") != NULL); + TEST_EQ(CompDesignRelease(), BAS_TRUE); + TEST_PTR_EQ(basUcOwnIface(rt, "Dial"), NULL); + TEST_EQ(CompDesignControls(mdl, src, dir), 0); + + // Another module's controls, App.Path the caller's, then replaced + int32_t next = CompCompile(src, 0, 0); + + snprintf(want, sizeof(want), "%s/dial.bmp", tTempDir()); + TEST_EQ(CompDesignControls(next, src, ""), 1); + TEST_TRUE(basUcIconPath(basUcOwnIface(rt, "Dial"), path, sizeof(path))); + TEST_STR_EQ(path, want); + TEST_EQ(CompDesignControls(next, src, dir), 1); + TEST_TRUE(basUcOwnIface(rt, "Dial") != NULL); + TEST_EQ(CompDesignRelease(), BAS_TRUE); + CompModuleFree(next); + + // Another program's module is refused: it would go when that one ends + int32_t *ownApp = dvxMemAppIdPtr; + int32_t otherApp = (ownApp ? *ownApp : 0) + 1; + + dvxMemAppIdPtr = &otherApp; + + int32_t foreign = CompCompile(src, 0, 0); + + dvxMemAppIdPtr = ownApp; + TEST_TRUE(foreign > 0); + TEST_EQ(CompDesignControls(foreign, src, dir), 0); + TEST_PTR_EQ(basUcOwnIface(rt, "Dial"), NULL); + CompModuleFree(foreign); + CompSourceFree(src); + + if (rt) { + designProject(rt); + } + + basFormRtSetCurrent(prev); + + if (rt) { + basRunHostDestroy(rt); + } + + basModuleFree(hostMod); + tShutdown(); +} + + +// The CONSTs of the include files that mirror C values: one source of +// truth, checked here (a C value changed without its CONST fails). +static void caseIncludeConsts(void) { + static const BdtConstT consts[] = { + { "resource.bas", "RES_NAME_MAX", DVX_RES_NAME_MAX }, + { "system.bas", "MOUSE_WHEEL_STEP_MIN", MOUSE_WHEEL_STEP_MIN }, + { "system.bas", "MOUSE_WHEEL_STEP_MAX", MOUSE_WHEEL_STEP_MAX }, + { "system.bas", "MOUSE_WHEEL_STEP_DEFAULT", MOUSE_WHEEL_STEP_DEFAULT }, + { "system.bas", "MOUSE_DBLCLICK_DEFAULT_MS", MOUSE_DBLCLICK_DEFAULT_MS }, + { "system.bas", "MOUSE_SPEED_DEFAULT", MOUSE_SPEED_DEFAULT }, + { "system.bas", "SYS_VIDEO_DEFAULT_WIDTH", DVX_DEFAULT_VIDEO_W }, + { "system.bas", "SYS_VIDEO_DEFAULT_HEIGHT", DVX_DEFAULT_VIDEO_H }, + { "system.bas", "SYS_VIDEO_DEFAULT_BPP", DVX_DEFAULT_VIDEO_BPP }, + { "shell.bas", "SHELL_VIEW_LARGE", PmViewLargeE }, + { "shell.bas", "SHELL_VIEW_SMALL", PmViewSmallE }, + { "shell.bas", "SHELL_VIEW_LIST", PmViewListE }, + { "shell.bas", "SHELL_PRIORITY_LOW", TS_PRIORITY_LOW }, + { "shell.bas", "SHELL_PRIORITY_NORMAL", TS_PRIORITY_NORMAL }, + { "shell.bas", "SHELL_PRIORITY_HIGH", TS_PRIORITY_HIGH }, + { "fileops.bas", "FILEOPS_ATTR_READONLY", FILEOPS_ATTR_READONLY }, + { "fileops.bas", "FILEOPS_ATTR_HIDDEN", FILEOPS_ATTR_HIDDEN }, + { "fileops.bas", "FILEOPS_ATTR_SYSTEM", FILEOPS_ATTR_SYSTEM }, + { "fileops.bas", "FILEOPS_ATTR_ARCHIVE", FILEOPS_ATTR_ARCHIVE }, + { "fileops.bas", "FILEOPS_ATTR_EDITABLE", FILEOPS_ATTR_EDITABLE }, + { "fileops.bas", "FILEOPS_DRIVE_FIXED", PlatformDriveFixedE }, + { "fileops.bas", "FILEOPS_DRIVE_REMOVABLE", PlatformDriveRemovableE }, + { "fileops.bas", "FILEOPS_DRIVE_REMOTE", PlatformDriveRemoteE }, + { "fileops.bas", "FILEOPS_SORT_NAME", FileOpsSortNameE }, + { "fileops.bas", "FILEOPS_SORT_SIZE", FileOpsSortSizeE }, + { "fileops.bas", "FILEOPS_SORT_TYPE", FileOpsSortTypeE }, + { "fileops.bas", "FILEOPS_SORT_DATE", FileOpsSortDateE }, + { "fileops.bas", "FILEOPS_COPY", FileOpsCopyE }, + { "fileops.bas", "FILEOPS_MOVE", FileOpsMoveE }, + { "fileops.bas", "FILEOPS_DELETE", FileOpsDeleteE }, + { "fileops.bas", "FILEOPS_STEP_BUSY", FileOpsStepBusyE }, + { "fileops.bas", "FILEOPS_STEP_ASK", FileOpsStepAskE }, + { "fileops.bas", "FILEOPS_STEP_ERROR", FileOpsStepErrorE }, + { "fileops.bas", "FILEOPS_STEP_DONE", FileOpsStepDoneE }, + { "fileops.bas", "FILEOPS_STEP_CANCELLED", FileOpsStepCancelledE }, + { "fileops.bas", "FILEOPS_ANSWER_YES", FileOpsAnswerYesE }, + { "fileops.bas", "FILEOPS_ANSWER_NO", FileOpsAnswerNoE }, + { "fileops.bas", "FILEOPS_ANSWER_ALL", FileOpsAnswerAllE }, + { "fileops.bas", "FILEOPS_ANSWER_CANCEL", FileOpsAnswerCancelE }, + { "fileops.bas", "FILEOPS_ANSWER_RETRY", FileOpsAnswerRetryE }, + { "fileops.bas", "FILEOPS_ANSWER_SKIP", FileOpsAnswerSkipE }, + { "compiler.bas", "COMP_KW_OTHER", BAS_KW_CLASS_OTHER }, + { "compiler.bas", "COMP_KW_TYPE", BAS_KW_CLASS_TYPE }, + { "compiler.bas", "COMP_KW_LITERAL", BAS_KW_CLASS_LITERAL }, + { "compiler.bas", "COMP_KIND_MODULE", BAS_BUILD_WRAP_MODULE }, + { "compiler.bas", "COMP_KIND_FORM", BAS_BUILD_WRAP_FORM }, + { "compiler.bas", "COMP_KIND_CLASS", BAS_BUILD_WRAP_CLASS }, + { "compiler.bas", "COMP_KIND_CONTROL", BAS_BUILD_WRAP_CONTROL }, + { "sound.bas", "SOUND_EVENT_DEFAULT", SoundDefaultE }, + { "sound.bas", "SOUND_EVENT_START", SoundStartE }, + { "sound.bas", "SOUND_EVENT_EXIT", SoundExitE }, + { "sound.bas", "SOUND_EVENT_ERROR", SoundErrorE }, + { "sound.bas", "SOUND_EVENT_WARNING", SoundWarningE }, + { "sound.bas", "SOUND_EVENT_QUESTION", SoundQuestionE }, + { "sound.bas", "SOUND_EVENT_INFO", SoundInfoE }, + { "sound.bas", "SOUND_EVENT_WINDOW_OPEN", SoundWindowOpenE }, + { "sound.bas", "SOUND_EVENT_WINDOW_CLOSE", SoundWindowCloseE }, + { "sound.bas", "SOUND_PITCH_NORMAL", AUDIO_PITCH_NORMAL * PERCENT / AUDIO_PITCH_NORMAL }, + { "sound.bas", "SOUND_PITCH_MIN", AUDIO_PITCH_MIN * PERCENT / AUDIO_PITCH_NORMAL }, + { "sound.bas", "SOUND_PITCH_MAX", AUDIO_PITCH_MAX * PERCENT / AUDIO_PITCH_NORMAL }, + { "sound.bas", "SOUND_RATE_MIN", AUDIO_RATE_MIN }, + { "sound.bas", "SOUND_RATE_MAX", AUDIO_RATE_MAX }, + { "sound.bas", "SOUND_RATE_DEFAULT", AUDIO_RATE_DEFAULT }, + { "sound.bas", "SOUND_STREAM_MS_MIN", AUDIO_STREAM_MS_MIN }, + { "sound.bas", "SOUND_STREAM_MS_DEFAULT", AUDIO_STREAM_MS_DEFAULT }, + { "sound.bas", "SOUND_STREAM_MS_MAX", AUDIO_STREAM_MS_MAX }, + { "sound.bas", "FM_VOICES_MAX", AUDIO_FM_VOICES_MAX }, + { "sound.bas", "FM_CHIP_HZ", AUDIO_FM_CHIP_HZ }, + { "sound.bas", "FM_FNUM_MAX", AUDIO_FM_FNUM_MAX }, + { "sound.bas", "FM_BLOCK_MAX", AUDIO_FM_BLOCK_MAX }, + { "sound.bas", "FM_FNUM_HI_SHIFT", AUDIO_FM_FNUM_HI_SHIFT }, + { "sound.bas", "FM_BLOCK_SHIFT", AUDIO_FM_BLOCK_SHIFT }, + { "sound.bas", "FM_MOD_CHAR", AUDIO_FM_MOD_CHAR }, + { "sound.bas", "FM_CAR_CHAR", AUDIO_FM_CAR_CHAR }, + { "sound.bas", "FM_MOD_LEVEL", AUDIO_FM_MOD_LEVEL }, + { "sound.bas", "FM_CAR_LEVEL", AUDIO_FM_CAR_LEVEL }, + { "sound.bas", "FM_MOD_ATTACK", AUDIO_FM_MOD_ATTACK }, + { "sound.bas", "FM_CAR_ATTACK", AUDIO_FM_CAR_ATTACK }, + { "sound.bas", "FM_MOD_SUSTAIN", AUDIO_FM_MOD_SUSTAIN }, + { "sound.bas", "FM_CAR_SUSTAIN", AUDIO_FM_CAR_SUSTAIN }, + { "sound.bas", "FM_FNUM_LOW", AUDIO_FM_FNUM_LOW }, + { "sound.bas", "FM_KEY_BLOCK", AUDIO_FM_KEY_BLOCK }, + { "sound.bas", "FM_FEEDBACK", AUDIO_FM_FEEDBACK }, + { "sound.bas", "FM_MOD_WAVE", AUDIO_FM_MOD_WAVE }, + { "sound.bas", "FM_CAR_WAVE", AUDIO_FM_CAR_WAVE }, + { "sound.bas", "FM_KEY_ON", AUDIO_FM_KEY_ON }, + { "sound.bas", "FM_OUT_LEFT", AUDIO_FM_OUT_LEFT }, + { "sound.bas", "FM_OUT_RIGHT", AUDIO_FM_OUT_RIGHT }, + { "sound.bas", "FM_OUT_BOTH", AUDIO_FM_OUT_BOTH }, + { "sound.bas", "FM_ADDITIVE", AUDIO_FM_ADDITIVE }, + { "sound.bas", "FM_BANK_REGS", AUDIO_FM_BANK_REGS }, + { "joystick.bas", "JOY_CAL_MIN", JOY_CAL_MIN }, + { "joystick.bas", "JOY_CAL_CENTER", JOY_CAL_CENTER }, + { "joystick.bas", "JOY_CAL_MAX", JOY_CAL_MAX }, + { "joystick.bas", "JOY_DEADZONE_DEFAULT", JOY_DEADZONE_DEFAULT }, + { "joystick.bas", "JOY_DEADZONE_MAX", JOY_DEADZONE_MAX }, + { "joystick.bas", "JOY_POLL_HZ_DEFAULT", JOY_POLL_HZ_DEFAULT }, + { "joystick.bas", "JOY_POLL_HZ_MIN", JOY_POLL_HZ_MIN }, + { "joystick.bas", "JOY_POLL_HZ_MAX", JOY_POLL_HZ_MAX }, + { "joystick.bas", "JOY_HAT_CENTERED", JOY_HAT_CENTERED }, + { "joystick.bas", "JOY_AXIS_MIN", JOY_AXIS_MIN }, + { "joystick.bas", "JOY_AXIS_MAX", JOY_AXIS_MAX }, + { "joystick.bas", "JOY_CHOICE_GAMEPORT", JoyChoiceGamePortE }, + { "joystick.bas", "JOY_CHOICE_FALLBACK", JoyChoiceFallbackE } + }; + + tCase("include-file CONSTs equal their C values"); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(consts); i++) { + char path[DVX_MAX_PATH]; + int32_t len = 0; + + snprintf(path, sizeof(path), BDT_INCLUDE_DIR "/%s", consts[i].file); + + char *text = platformReadFile(path, &len); + + TEST_TRUE(text != NULL); + + if (text) { + TEST_EQ(includeConst(text, consts[i].name), consts[i].value); + free(text); + } + } + + // String CONSTs + int32_t resLen = 0; + char *resBas = platformReadFile(BDT_INCLUDE_DIR "/resource.bas", &resLen); + + TEST_TRUE(resBas != NULL); + + if (resBas) { + TEST_TRUE(includeConstIs(resBas, "RES_NAME_ICON", DVX_RES_NAME_ICON)); + TEST_TRUE(includeConstIs(resBas, "RES_NAME_WIDGET_ICON", DVX_RES_NAME_WIDGET_ICON)); + free(resBas); + } +} + + +// The .dbp model from BASIC: a new project with files and settings, +// saved, opened again, a file renamed and removed. +static void caseProjectApi(void) { + tCase("compiler.bas projects: ProjNew, ProjSave, ProjOpen, files, settings"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + char dir[DVX_MAX_PATH]; + char dbp[DVX_MAX_PATH]; + + snprintf(dir, sizeof(dir), "%s/projapi", tTempDir()); + snprintf(dbp, sizeof(dbp), "%s/demo.dbp", dir); + platformMkdirRecursive(dir); + remove(dbp); + + int32_t prj = ProjNew("demo", dir); + + TEST_TRUE(prj > 0); + TEST_EQ(ProjAddFile(prj, "main.bas"), 0); + TEST_EQ(ProjAddFile(prj, "form1.frm"), 1); + TEST_EQ(ProjAddFile(prj, "MAIN.BAS"), NO_INDEX); + TEST_EQ(ProjFileKind(prj, 1), KIND_FORM); + TEST_EQ(ProjFileKind(prj, 0), KIND_MODULE); + TEST_EQ(ProjFileKind(prj, 2), NO_INDEX); + TEST_EQ(ProjSet(prj, "Author", "Somebody"), BAS_TRUE); + TEST_EQ(ProjSet(prj, "optionexplicit", " -1"), BAS_TRUE); + TEST_EQ(ProjSet(prj, "StartupForm", "Form1"), BAS_TRUE); + TEST_EQ(ProjSet(prj, "Bogus", "x"), 0); + TEST_EQ(ProjSave(prj), BAS_TRUE); + ProjClose(prj); + + TEST_EQ(ProjOpen("no-such-project.dbp"), 0); + prj = ProjOpen(dbp); + TEST_TRUE(prj > 0); + TEST_STR_EQ(ProjGet(prj, "author"), "Somebody"); + TEST_STR_EQ(ProjGet(prj, "Name"), "demo"); + TEST_STR_EQ(ProjGet(prj, "OptionExplicit"), "true"); + TEST_STR_EQ(ProjGet(prj, "MultiInstance"), "false"); + TEST_STR_EQ(ProjGet(prj, "StartupForm"), "Form1"); + TEST_STR_EQ(ProjGet(prj, "Bogus"), ""); + TEST_STR_EQ(ProjPath(prj), dbp); + TEST_EQ(ProjFileCount(prj), 2); + TEST_STR_EQ(ProjFile(prj, 0), "main.bas"); + TEST_EQ(ProjFileFind(prj, "FORM1.FRM"), 1); + TEST_TRUE(strstr(ProjFilePath(prj, 1), "projapi") != NULL); + + // A rename keeps the kind's extension and saves the project at once + TEST_EQ(ProjRenameFile(prj, 0, "x.frm"), 0); + TEST_TRUE(CompError()[0] != '\0'); + TEST_EQ(ProjRenameFile(prj, 0, "app"), BAS_TRUE); + TEST_STR_EQ(ProjFile(prj, 0), "app.bas"); + TEST_EQ(ProjRemoveFile(prj, 1), BAS_TRUE); + TEST_EQ(ProjRemoveFile(prj, 1), 0); + TEST_EQ(ProjFileCount(prj), 1); + ProjClose(prj); + + prj = ProjOpen(dbp); + TEST_EQ(ProjFileFind(prj, "app.bas"), 0); + ProjClose(prj); + TEST_EQ(ProjFileCount(prj), 0); + tShutdown(); +} + + +// A control project and a UserControl's header: [Project] Type = Control +// and the [Controls] section written, read and kept by renames; the +// header keys read with their defaults and set key by key, the code after +// the header untouched (compiler.bas and the C model). +static void caseProjectControls(void) { + tCase("compiler.bas projects: control projects, [Controls], UserControl headers"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + static const char ctl[] = + "VERSION DVX 1.00\n" + "Begin UserControl Gauge\n" + " Width = 120\n" + " CanGetFocus = False\n" + "End\n" + "Option Explicit\n" + "Private Sub UserControl_Paint()\n" + "End Sub\n"; + static const char ctlCode[] = "Option Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"; + + // The header keys and their defaults + TEST_STR_EQ(ProjCtlHeader(ctl, "name"), "Gauge"); + TEST_STR_EQ(ProjCtlHeader(ctl, "width"), "120"); + TEST_STR_EQ(ProjCtlHeader(ctl, "Height"), "64"); + TEST_STR_EQ(ProjCtlHeader(ctl, "CanGetFocus"), "False"); + TEST_STR_EQ(ProjCtlHeader(ctl, "BackColor"), ""); + TEST_STR_EQ(ProjCtlHeader(ctl, "Description"), ""); + TEST_STR_EQ(ProjCtlHeader(ctl, "Bogus"), ""); + TEST_STR_EQ(ProjCtlHeader("x = 1\n", "Width"), ""); + TEST_TRUE(prjCtlKeyAt(0) != NULL); + TEST_STR_EQ(prjCtlKeyAt(0)->key, BAS_PROP_NAME); + TEST_STR_EQ(prjCtlKeyAt(1)->key, BAS_UC_KEY_WIDTH); + TEST_STR_EQ(prjCtlKeyAt(BDT_CTL_KEYS - 1)->key, BAS_UC_KEY_DEFAULTEVENT); + TEST_TRUE(prjCtlKeyAt(BDT_CTL_KEYS) == NULL); + TEST_TRUE(prjCtlKeyAt(-1) == NULL); + TEST_EQ(prjCtlKeyFind("backcolor")->kind, PrjCtlColorE); + TEST_EQ(prjCtlKeyFind("CANGETFOCUS")->kind, PrjCtlBoolE); + TEST_EQ(prjCtlKeyFind("ToolboxIcon")->kind, PrjCtlTextE); + TEST_EQ(prjCtlKeyFind("NAME")->kind, PrjCtlNameE); + TEST_TRUE(prjCtlKeyFind("Caption") == NULL); + + // A key's line changed in place, added before End, removed + TEST_STR_EQ(ProjCtlSetHeader(ctl, "WIDTH", " 200 "), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 200\n CanGetFocus = False\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"); + TEST_STR_EQ(CompError(), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "CanGetFocus", "-1"), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\n CanGetFocus = True\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "BackColor", "&hff0000"), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\n CanGetFocus = False\n BackColor = &H00FF0000&\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Description", "A gauge"), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\n CanGetFocus = False\n Description = \"A gauge\"\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "CanGetFocus", ""), "VERSION DVX 1.00\nBegin UserControl Gauge\n Width = 120\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Height", ""), ctl); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", "Meter"), "VERSION DVX 1.00\nBegin UserControl Meter\n Width = 120\n CanGetFocus = False\nEnd\nOption Explicit\nPrivate Sub UserControl_Paint()\nEnd Sub\n"); + + // Read back as set (a decimal color too), the code kept + char colored[sizeof(ctl) * 2]; + + snprintf(colored, sizeof(colored), "%s", ProjCtlSetHeader(ctl, "BackColor", "255")); + TEST_STR_EQ(ProjCtlHeader(colored, "BackColor"), "&H000000FF&"); + TEST_TRUE(strstr(colored, ctlCode) != NULL); + TEST_STR_EQ(ProjCtlHeader(ProjCtlSetHeader(ctl, "DefaultEvent", "Changed"), "DefaultEvent"), "Changed"); + + // Values a key does not take + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Width", "wide"), ""); + TEST_TRUE(CompError()[0] != '\0'); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Width", "-5"), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "BackColor", "&HXYZ"), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Description", "say \"hi\""), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Description", "two\nlines"), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Bogus", "1"), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", ""), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", "2Bad"), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctl, "Name", "Two Words"), ""); + TEST_STR_EQ(ProjCtlSetHeader(ctlCode, "Width", "1"), ""); + + // A new control's text, and it compiles + static const char fresh[] = "VERSION DVX 1.00\nBegin UserControl Dial\n Width = 64\n Height = 64\nEnd\n\nOption Explicit\n\nPrivate Sub UserControl_Paint ()\n\nEnd Sub\n"; + + TEST_STR_EQ(ProjCtlNew("Dial"), fresh); + TEST_STR_EQ(CompError(), ""); + TEST_STR_EQ(ProjCtlNew("9Dial"), ""); + TEST_TRUE(CompError()[0] != '\0'); + TEST_STR_EQ(ProjCtlNew(""), ""); + + int32_t src = CompSourceCreate(); + + TEST_EQ(CompSourceAdd(src, "dial.ctl", fresh, KIND_CONTROL), BAS_TRUE); + + int32_t mdl = CompCompile(src, BAS_TRUE, 0); + + TEST_TRUE(mdl > 0); + CompModuleFree(mdl); + CompSourceFree(src); + + // A control project: Type and [Controls] saved and read back + char dir[DVX_MAX_PATH]; + char dbp[DVX_MAX_PATH]; + + snprintf(dir, sizeof(dir), "%s/projctl", tTempDir()); + snprintf(dbp, sizeof(dbp), "%s/gauge.dbp", dir); + platformMkdirRecursive(dir); + remove(dbp); + + int32_t prj = ProjNew("gauge", dir); + + TEST_STR_EQ(ProjGet(prj, "Type"), ""); + TEST_EQ(ProjSet(prj, "type", BAS_PROJECT_TYPE_CONTROL), BAS_TRUE); + TEST_EQ(ProjAddFile(prj, "gauge.ctl"), 0); + TEST_EQ(ProjAddFile(prj, "util.bas"), 1); + TEST_EQ(ProjFileKind(prj, 0), KIND_CONTROL); + TEST_EQ(ProjSave(prj), BAS_TRUE); + ProjClose(prj); + + int32_t len = 0; + char *text = platformReadFile(dbp, &len); + + TEST_TRUE(text != NULL); + TEST_TRUE(text && strstr(text, "[" BAS_INI_SECTION_CONTROLS "]") != NULL); + TEST_TRUE(text && strstr(text, "gauge.ctl") != NULL); + TEST_TRUE(text && strstr(text, BAS_INI_KEY_TYPE) != NULL); + free(text); + + PrjStateT model; + + prjInit(&model); + TEST_TRUE(prjLoad(&model, dbp)); + TEST_TRUE(prjIsControlProject(&model)); + TEST_EQ(prjFileCount(&model), 2); + TEST_EQ(model.files[prjFileIndexByPath(&model, "gauge.ctl")].kind, PrjKindControlE); + TEST_EQ(model.files[0].kind, PrjKindModuleE); + TEST_EQ(prjKindForPath("X.CTL"), PrjKindControlE); + TEST_EQ(prjKindWrap(PrjKindControlE), BAS_BUILD_WRAP_CONTROL); + TEST_EQ(prjFieldSet(&model, BAS_INI_KEY_TYPE, ""), true); + TEST_TRUE(!prjIsControlProject(&model)); + prjClose(&model); + + // Modules load before controls (the compile order) + prj = ProjOpen(dbp); + TEST_STR_EQ(ProjGet(prj, "Type"), BAS_PROJECT_TYPE_CONTROL); + TEST_EQ(ProjFileFind(prj, "GAUGE.CTL"), 1); + TEST_EQ(ProjFileKind(prj, 1), KIND_CONTROL); + TEST_EQ(ProjRenameFile(prj, 1, "meter.bas"), 0); + TEST_EQ(ProjRenameFile(prj, 1, "meter"), BAS_TRUE); + TEST_STR_EQ(ProjFile(prj, 1), "meter.ctl"); + ProjClose(prj); + + prj = ProjOpen(dbp); + TEST_EQ(ProjFileKind(prj, ProjFileFind(prj, "meter.ctl")), KIND_CONTROL); + TEST_EQ(ProjRemoveFile(prj, 1), BAS_TRUE); + TEST_EQ(ProjSave(prj), BAS_TRUE); + ProjClose(prj); + + prj = ProjOpen(dbp); + TEST_EQ(ProjFileCount(prj), 1); + TEST_EQ(ProjFileKind(prj, 0), KIND_MODULE); + ProjClose(prj); + + // The validator: a project's own control type is known, its members + // are left to the run time (a registered one may be an older build) + BasValidatorT v; + + memset(&v, 0, sizeof(v)); + basValidateAddType(&v, "Gauge"); + TEST_TRUE(basValidateIsPropValid(&v, "gauge", "Needle")); + TEST_TRUE(basValidateIsMethodValid(&v, "Gauge", "Spin")); + TEST_TRUE(!basValidateIsPropValid(&v, "CommandButton", "Needle")); + TEST_TRUE(!basValidateIsMethodValid(NULL, "CommandButton", "Spin")); + basValidateFree(&v); + tShutdown(); +} + + +// Source text as the compiler reads it: tokens, code positions, renaming +// in code, WithEvents variables, class Events, help topics, keyword +// classes. +static void caseSourceApi(void) { + tCase("compiler.bas source text: tokens, in-code, rename, events, help topics, keyword flags"); + + TEST_STR_EQ(CompTokens("x = \"a\tb\" ' note\nIF y THEN\n"), "1\t1\t1\tIdentifier\tx\n1\t3\t1\tSymbol\t=\n1\t5\t5\tString\t\"a b\"\n1\t11\t6\tComment\t' note\n2\t1\t2\tKeyword\tIF\n2\t4\t1\tIdentifier\ty\n2\t6\t4\tKeyword\tTHEN\n"); + TEST_STR_EQ(CompTokens("REM all of it\n"), "1\t1\t13\tComment\tREM all of it\n"); + TEST_STR_EQ(CompTokens("s = \"open\n"), "1\t1\t1\tIdentifier\ts\n1\t3\t1\tSymbol\t=\n1\t5\t5\tError\t\"open\n"); + + const char *code = "a = \"Cmd1.x\" ' Cmd1.y\nCmd1.z = 1\ns = \"abc\n"; + + TEST_EQ(CompInCode(code, 1), BAS_TRUE); + TEST_EQ(CompInCode(code, 4), BAS_TRUE); + TEST_EQ(CompInCode(code, 6), 0); + TEST_EQ(CompInCode(code, 16), 0); + TEST_EQ(CompInCode(code, 23), BAS_TRUE); + TEST_EQ(CompInCode(code, 39), 0); + TEST_EQ(CompInCode(code, 0), 0); + + const char *frm = "Command1.Caption = \"Command1.x\"\nSub Command1_Click\n' Command1.y\nMyCommand1.z = 1\n"; + + TEST_STR_EQ(CompRenameInCode(frm, "command1", "Go", BAS_TRUE), "Go.Caption = \"Command1.x\"\nSub Go_Click\n' Command1.y\nMyCommand1.z = 1\n"); + TEST_STR_EQ(CompRenameInCode(frm, "Command1", "Go", 0), "Go.Caption = \"Go.x\"\nSub Go_Click\n' Go.y\nMyCommand1.z = 1\n"); + TEST_STR_EQ(CompRenameInCode(frm, "Label1", "Go", BAS_TRUE), frm); + + // A result too long for an int32_t length leaves the text unchanged + // (its size overflowed int32_t before) + size_t refLen = strlen(BDT_RENAME_REF); + char *many = (char *)malloc(BDT_RENAME_REFS * refLen + 1); + char *newName = (char *)malloc(BDT_RENAME_NEWLEN + 1); + + if (many && newName) { + for (int32_t i = 0; i < BDT_RENAME_REFS; i++) { + memcpy(many + (size_t)i * refLen, BDT_RENAME_REF, refLen); + } + + many[BDT_RENAME_REFS * refLen] = '\0'; + memset(newName, 'b', BDT_RENAME_NEWLEN); + newName[BDT_RENAME_NEWLEN] = '\0'; + TEST_STR_EQ(CompRenameInCode(many, "a", newName, 0), many); + } + + free(many); + free(newName); + + TEST_STR_EQ(CompWithEvents("Dim WithEvents a As Foo, b As Long\nPrivate WithEvents c As Bar\nx = 1 ' Dim WithEvents d As Baz\n"), "a\tFoo\nc\tBar\n"); + TEST_STR_EQ(CompClassEvents("Public Event Changed(Value As Integer)\nEvent Done\nx = \"Event Nope\"\n"), "Changed\tValue As Integer\nDone\t\n"); + + TEST_STR_EQ(CompHelpTopic("withevents"), "lang.events"); + TEST_STR_EQ(CompHelpTopic("NoSuchWord"), ""); + TEST_STR_EQ(CompHelpCtrlTopic("CommandButton"), "ctrl.commandbutton"); + + TEST_EQ(CompKeywordClass(keywordIndex("INTEGER")), KW_CLASS_TYPE); + TEST_EQ(CompKeywordReserved(keywordIndex("INTEGER")), BAS_TRUE); + TEST_EQ(CompKeywordClass(keywordIndex("BYTE")), KW_CLASS_TYPE); + TEST_EQ(CompKeywordReserved(keywordIndex("BYTE")), 0); + TEST_EQ(CompKeywordClass(keywordIndex("TRUE")), KW_CLASS_LITERAL); + TEST_EQ(CompKeywordClass(NO_INDEX), NO_INDEX); + TEST_EQ(CompKeywordReserved(CompKeywordCount()), 0); +} + + +// Copy the fixture's kid project into kidDir. +static bool copyKid(const char *kidDir) { + static const char *const files[] = { "kid.dbp", "kid.bas", "kidform.frm" }; + bool ok = platformMkdirRecursive(kidDir) == 0; + + for (int32_t i = 0; ok && i < DVX_ARRAY_LEN(files); i++) { + char from[DVX_MAX_PATH]; + char to[DVX_MAX_PATH]; + + snprintf(from, sizeof(from), BDT_FIXTURE_DIR "/kid/%s", files[i]); + snprintf(to, sizeof(to), "%s/%s", kidDir, files[i]); + ok = platformCopyFile(from, to); + } + + return ok; +} + + +// caseDesignControls with program rt current: a control project's set +// (App.Path its folder; built into a control file by CompBuildApp) and a +// child program's design controls, which go when it ends along with its +// hold on their module, so that handle is free again. +static void designProject(const BasFormRtT *rt) { + char dir[DVX_MAX_PATH]; + char dbp[DVX_MAX_PATH]; + char ctlPath[DVX_MAX_PATH]; + char iconPath[DVX_MAX_PATH]; + char wgt[DVX_MAX_PATH]; + char path[DVX_MAX_PATH]; + char ctl0[BAS_RES_FORM_NAME_LEN]; + + snprintf(dir, sizeof(dir), "%s/dsgnprj", tTempDir()); + snprintf(dbp, sizeof(dbp), "%s/dial.dbp", dir); + snprintf(ctlPath, sizeof(ctlPath), "%s/dial.ctl", dir); + snprintf(iconPath, sizeof(iconPath), "%s/dial.bmp", dir); + snprintf(wgt, sizeof(wgt), "%s/dial.wgt", tTempDir()); + snprintf(ctl0, sizeof(ctl0), BAS_RES_CTL_FMT, (long)0); + platformMkdirRecursive(dir); + remove(dbp); + remove(wgt); + TEST_TRUE(tWriteFile(ctlPath, sDialCtl, (int32_t)strlen(sDialCtl))); + TEST_TRUE(tWriteFile(iconPath, BDT_DIAL_ICON, (int32_t)sizeof(BDT_DIAL_ICON))); + + int32_t prj = ProjNew("dial", dir); + + TEST_EQ(ProjSet(prj, "Type", BAS_PROJECT_TYPE_CONTROL), BAS_TRUE); + TEST_EQ(ProjAddFile(prj, "dial.ctl"), 0); + TEST_EQ(ProjSave(prj), BAS_TRUE); + ProjClose(prj); + + // A project's set: App.Path (and the ToolboxIcon) its folder + int32_t src = CompSourceCreate(); + + TEST_EQ(CompSourceFromProject(src, dbp), BAS_TRUE); + + int32_t mdl = CompCompile(src, 0, 0); + + TEST_EQ(CompDesignControls(mdl, src, ""), 1); + TEST_TRUE(basUcIconPath(basUcOwnIface(rt, "Dial"), path, sizeof(path))); + TEST_STR_EQ(path, iconPath); + TEST_EQ(CompDesignRelease(), BAS_TRUE); + + // A control project builds a control file: the module, the header + // text, the toolbox picture and name (no Description: the name) + TEST_EQ(CompBuildApp(mdl, src, wgt, 0), BAS_TRUE); + CompModuleFree(mdl); + CompSourceFree(src); + + DvxResHandleT *res = dvxResOpen(wgt); + + TEST_TRUE(res != NULL); + TEST_TRUE(resHas(res, BAS_RES_MODULE, NULL)); + TEST_TRUE(resHas(res, ctl0, BDT_DIAL_HEADER)); + TEST_TRUE(resHas(res, BAS_CTL_RES_ICON, BDT_DIAL_ICON)); + TEST_TRUE(resHas(res, BAS_CTL_RES_NAME, "Dial")); + + if (res) { + dvxResClose(res); + } + + // A child program's own design controls + int32_t len = 0; + char *inc = platformReadFile(BDT_INCLUDE_DIR "/compiler.bas", &len); + size_t size = (size_t)len + sizeof(BDT_DESIGN_CHILD_FMT) + strlen(dbp); + char *text = inc ? (char *)malloc(size) : NULL; + int32_t kidSrc = CompSourceCreate(); + + TEST_TRUE(text != NULL); + + if (text) { + snprintf(text, size, BDT_DESIGN_CHILD_FMT, inc, dbp); + TEST_EQ(CompSourceAdd(kidSrc, "child.bas", text, KIND_PROGRAM), BAS_TRUE); + } + + int32_t kidMdl = CompCompile(kidSrc, 0, 0); + int32_t run = RunCreate(kidMdl, kidSrc, "", ""); + int32_t state = BDT_RUN_ERROR; + + basNativeHostSetLive(true); + TEST_EQ(RunStart(run, "", 0), BAS_TRUE); + + for (int32_t i = 0; i < BDT_MAX_TICKS && (state = RunPoll(run, BDT_TICK_MS)) != BDT_RUN_ENDED && state != BDT_RUN_ERROR; i++) { + } + + basNativeHostSetLive(false); + TEST_EQ(state, BDT_RUN_ENDED); + + char *end = NULL; + int32_t freed = (int32_t)strtol(RunOutput(run), &end, DECIMAL); + int32_t designed = (int32_t)strtol(end, NULL, DECIMAL); + + TEST_EQ(designed, 1); + + // Its module went with it: handles are given out lowest free first, + // so at most "freed" new ones reach its number + int32_t *probes = NULL; + bool reused = false; + + for (int32_t i = 0; i < freed && !reused; i++) { + int32_t h = CompSourceCreate(); + + arrput(probes, h); + reused = (h == freed); + } + + TEST_TRUE(reused); + + for (int32_t i = 0; i < (int32_t)arrlen(probes); i++) { + CompSourceFree(probes[i]); + } + + arrfree(probes); + RunFree(run); + CompModuleFree(kidMdl); + CompSourceFree(kidSrc); + free(text); + free(inc); +} + + +static BasFormT *formOf(const BasFormRtT *rt, const char *formName) { + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + if (strcasecmp(rt->forms[i]->name, formName) == 0) { + return rt->forms[i]; + } + } + + return NULL; +} + + +// The caption of a program's form window ("" when it has none). +static const char *formTitle(const BasFormRtT *rt, const char *formName) { + const BasFormT *form = formOf(rt, formName); + + return (form && form->window) ? form->window->title : ""; +} + + +// The value of "CONST name = value" in BASIC source text, CONST_MISSING +// when it has none. +static int32_t includeConst(const char *text, const char *name) { + const char *p = includeConstValue(text, name); + + if (!p) { + return CONST_MISSING; + } + + // &H20 is hexadecimal, as in BASIC + if (p[0] == '&' && (p[1] == 'H' || p[1] == 'h')) { + return (int32_t)strtol(p + 2, NULL, HEXADECIMAL); + } + + return (int32_t)strtol(p, NULL, DECIMAL); +} + + +// Whether "CONST name = "value"" in BASIC source text is that string. +static bool includeConstIs(const char *text, const char *name, const char *value) { + const char *p = includeConstValue(text, name); + size_t len = strlen(value); + + return p && p[0] == '"' && strncmp(p + 1, value, len) == 0 && p[len + 1] == '"'; +} + + +// Where the value of "CONST name = value" starts in BASIC source text, +// NULL when it has none. +static const char *includeConstValue(const char *text, const char *name) { + size_t nameLen = strlen(name); + + for (const char *line = text; *line; ) { + const char *p = dvxSkipWs(line); + const char *end = strchr(line, '\n'); + + if (strncasecmp(p, BDT_KW_CONST, strlen(BDT_KW_CONST)) == 0) { + p = dvxSkipWs(p + strlen(BDT_KW_CONST)); + + if (strncasecmp(p, name, nameLen) == 0 && !basIsIdentChar(p[nameLen])) { + p = dvxSkipWs(p + nameLen); + + if (*p == '=') { + return dvxSkipWs(p + 1); + } + } + } + + line = end ? end + 1 : line + strlen(line); + } + + return NULL; +} + + +// The index of keyword word in CompKeyword$'s list, -1 when it is not one. +static int32_t keywordIndex(const char *word) { + for (int32_t i = 0; i < CompKeywordCount(); i++) { + if (strcmp(CompKeyword(i), word) == 0) { + return i; + } + } + + return NO_INDEX; +} + + +// Read and compile a project as bascomp does (validator: also scan its +// forms and check Control.Member names). NULL when it does not compile. +static BasModuleT *loadProject(const char *dbpPath, BasProjectSourceT *ps, BasValidatorT *validator) { + PrefsHandleT *prefs = NULL; + char dir[DVX_MAX_PATH]; + BasCompileErrorT err; + BasCtrlValidatorT cb; + + memset(ps, 0, sizeof(*ps)); + + if (validator) { + memset(validator, 0, sizeof(*validator)); + basValidateCallbacks(validator, &cb); + } + + if (!basBuildOpenProject(dbpPath, &prefs, dir, sizeof(dir))) { + return NULL; + } + + bool ok = basBuildConcatProject(prefs, dir, ps); + BasModuleT *mod = NULL; + + prefsClose(prefs); + + if (!ok) { + return NULL; + } + + for (int32_t i = 0; validator && i < (int32_t)arrlen(ps->frmTexts); i++) { + basValidateScanFrm(validator, ps->frmTexts[i], (int32_t)strlen(ps->frmTexts[i]), NULL, NULL); + } + + mod = basBuildCompile(ps->source, ps->sourceLen, ps->optionExplicit, validator ? &cb : NULL, validator ? &validator->compiler : NULL, &err); + + if (!mod) { + printf(" compile: %s\n", err.message); + } + + return mod; +} + + +// True when res has resource name and, unless want is NULL, it is the +// text want (with its NUL, as the build writes text). +static bool resHas(DvxResHandleT *res, const char *name, const char *want) { + uint32_t size = 0; + char *data = res ? (char *)dvxResRead(res, name, &size) : NULL; + bool same = data && (!want || (size == strlen(want) + 1 && memcmp(data, want, size) == 0)); + + free(data); + return same; +} + + +static void textPrint(void *ctx, const char *text, bool newline) { + BdtTextT *t = (BdtTextT *)ctx; + + if (text) { + t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, text, (int32_t)strlen(text)); + } + + if (newline) { + t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, "\n", 1); + } +} + + +// A debugger row as "cell|cell|cell\n". +static void textRow(void *ctx, const char *const *cells, int32_t cellCount) { + BdtTextT *t = (BdtTextT *)ctx; + + for (int32_t i = 0; i < cellCount; i++) { + t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, i > 0 ? "|" : "", i > 0 ? 1 : 0); + t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, cells[i], (int32_t)strlen(cells[i])); + } + + t->len = basDbgAppendDropping(t->text, BDT_TEXT_MAX, t->len, "\n", 1); +} + + +int main(void) { + setvbuf(stdout, NULL, _IOLBF, 0); + tSuiteBegin("basdev"); + tProcessInit(DVXTEST_WIDGET_DIR); + + caseCompileApi(); + caseDesignControls(); + caseIncludeConsts(); + caseSourceApi(); + caseProjectApi(); + caseProjectControls(); + caseCDebugger(); + caseBasicIde(); + + return tSuiteEnd(); +} diff --git a/src/test/basic/Makefile b/src/test/basic/Makefile index 8914c15a..9bbe8f79 100644 --- a/src/test/basic/Makefile +++ b/src/test/basic/Makefile @@ -44,7 +44,7 @@ include ../../common.mk DVXBASIC = ../../apps/kpunch/dvxbasic LIBDVX = ../../libs/kpunch/libdvx -INCLUDES = -I$(DVXBASIC) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty +INCLUDES = -I$(DVXBASIC) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I$(LIBDVX)/../texthelp HOSTCFLAGS = -O2 -g $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DEPFLAGS) $(INCLUDES) HOSTLDFLAGS = ifeq ($(TESTSAN),1) diff --git a/src/test/basic/fuzzCommon.c b/src/test/basic/fuzzCommon.c index 233217d3..965b6db5 100644 --- a/src/test/basic/fuzzCommon.c +++ b/src/test/basic/fuzzCommon.c @@ -162,7 +162,7 @@ FuzzRunResultE fuzzRunModule(BasModuleT *mod, int32_t stepCap, FuzzRunStatsT *st if (!vm) { fprintf(stderr, "fuzz: out of memory creating VM\n"); - exit(2); + exit(FUZZ_EXIT_USAGE); } basVmLoadModule(vm, mod); diff --git a/src/test/basic/fuzzCommon.h b/src/test/basic/fuzzCommon.h index a4df0454..794ac157 100644 --- a/src/test/basic/fuzzCommon.h +++ b/src/test/basic/fuzzCommon.h @@ -40,6 +40,8 @@ #define FUZZ_DEFAULT_MUTATIONS 10 #define FUZZ_DEFAULT_STEP_CAP 200000 #define FUZZ_STEP_SLICE BAS_VM_DEFAULT_STEP_SLICE +#define FUZZ_EXIT_FAIL 1 // a check failed (a rejected pristine input, a leak) +#define FUZZ_EXIT_USAGE 2 // bad arguments, nothing to fuzz, out of memory typedef struct { uint64_t state; diff --git a/src/test/basic/fuzzCompile.c b/src/test/basic/fuzzCompile.c index 967b9cf1..8accba78 100644 --- a/src/test/basic/fuzzCompile.c +++ b/src/test/basic/fuzzCompile.c @@ -296,13 +296,13 @@ int main(int argc, char **argv) { corpusDir = argv[i]; } else { usage(); - return 2; + return FUZZ_EXIT_USAGE; } } if (!corpusDir) { usage(); - return 2; + return FUZZ_EXIT_USAGE; } int32_t seedCount = 0; @@ -310,7 +310,7 @@ int main(int argc, char **argv) { if (!seeds || seedCount == 0) { fprintf(stderr, "fuzzCompile: empty corpus\n"); - return 2; + return FUZZ_EXIT_USAGE; } FuzzRngT rng; @@ -343,7 +343,7 @@ int main(int argc, char **argv) { if (!out) { fprintf(stderr, "fuzzCompile: out of memory\n"); - return 2; + return FUZZ_EXIT_USAGE; } } diff --git a/src/test/basic/fuzzModule.c b/src/test/basic/fuzzModule.c index dd740fa8..82d4814c 100644 --- a/src/test/basic/fuzzModule.c +++ b/src/test/basic/fuzzModule.c @@ -56,6 +56,8 @@ #define FUZZ_LEAK_HISTORY 4 // LeakSanitizer can lag a few mutants behind (stale stack pointers) #define FUZZ_CHUNK_MAX 64 #define FUZZ_FIELD_VALUES 8 +#define FUZZ_DBG_ODDS 4 // one mutant in this many mutates the debug blob +#define FUZZ_NAME_MAX 256 // a blob's name (its file) in messages typedef enum { MUT_BYTE, @@ -71,7 +73,7 @@ typedef struct { int32_t modLen; uint8_t *dbg; // may be NULL int32_t dbgLen; - char name[256]; + char name[FUZZ_NAME_MAX]; } BlobT; typedef struct { @@ -90,7 +92,7 @@ static FILE *sLastDbg = NULL; typedef struct { uint8_t *mod; int32_t modLen; - char name[256]; + char name[FUZZ_NAME_MAX]; int32_t mutation; } LeakHistT; @@ -105,6 +107,7 @@ extern int __lsan_do_recoverable_leak_check(void) __attribute__((weak)); static void addAppBlob(BlobT **blobs, int32_t *count, const char *path); static void addCorpusBlobs(BlobT **blobs, int32_t *count, const char *dir); static void blobAppend(BlobT **blobs, int32_t *count, const BlobT *b); +static void blobsFree(BlobT *blobs, int32_t count); static bool leaked(void); static void leakHistAdd(const uint8_t *mod, int32_t modLen, const char *name, int32_t mutation); static void leakHistDump(void); @@ -196,7 +199,7 @@ static void blobAppend(BlobT **blobs, int32_t *count, const BlobT *b) { if (!grown) { fprintf(stderr, "fuzzModule: out of memory\n"); - exit(2); + exit(FUZZ_EXIT_USAGE); } grown[*count] = *b; @@ -205,6 +208,17 @@ static void blobAppend(BlobT **blobs, int32_t *count, const BlobT *b) { } +// Frees every blob's buffers, then the array. +static void blobsFree(BlobT *blobs, int32_t count) { + for (int32_t b = 0; b < count; b++) { + free(blobs[b].mod); + free(blobs[b].dbg); + } + + free(blobs); +} + + // True when LeakSanitizer finds unreachable allocations right now. static bool leaked(void) { return __lsan_do_recoverable_leak_check && __lsan_do_recoverable_leak_check() != 0; @@ -344,13 +358,13 @@ static void usage(void) { int main(int argc, char **argv) { - int32_t mutations = FUZZ_DEFAULT_MUTATIONS; - uint64_t seed = FUZZ_DEFAULT_SEED; - int32_t stepCap = FUZZ_DEFAULT_STEP_CAP; - BlobT *blobs = NULL; - int32_t blobCount = 0; - bool leakCheck = false; - const char *dumpDir = NULL; + int32_t mutations = FUZZ_DEFAULT_MUTATIONS; + uint64_t seed = FUZZ_DEFAULT_SEED; + int32_t stepCap = FUZZ_DEFAULT_STEP_CAP; + BlobT *blobs = NULL; + int32_t blobCount = 0; + bool leakCheck = false; + const char *dumpDir = NULL; for (int32_t i = 1; i < argc; i++) { if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) { @@ -369,14 +383,15 @@ int main(int argc, char **argv) { addAppBlob(&blobs, &blobCount, argv[i]); } else { usage(); - return 2; + blobsFree(blobs, blobCount); + return FUZZ_EXIT_USAGE; } } if (blobCount == 0) { fprintf(stderr, "fuzzModule: nothing to fuzz\n"); usage(); - return 2; + return FUZZ_EXIT_USAGE; } FuzzRngT rng; @@ -388,36 +403,39 @@ int main(int argc, char **argv) { DeserStatsT deser = { 0, 0 }; FuzzRunStatsT run = { 0, 0, 0 }; int32_t mutants = 0; + int32_t rc = 0; printf("DVX BASIC Bytecode Fuzz\n"); printf("=======================\n"); printf("blobs: %d, %d mutations each, seed %llu, step cap %d\n", (int)blobCount, (int)mutations, (unsigned long long)seed, (int)stepCap); - for (int32_t b = 0; b < blobCount; b++) { - BlobT *blob = &blobs[b]; - uint8_t *modOut = (uint8_t *)malloc((size_t)blob->modLen + FUZZ_CHUNK_MAX); - uint8_t *dbgOut = blob->dbg ? (uint8_t *)malloc((size_t)blob->dbgLen + FUZZ_CHUNK_MAX) : NULL; + // A failure stops the run but still frees everything (LeakSanitizer + // reports would bury the FAIL line) and keeps the last-input files. + for (int32_t b = 0; b < blobCount && rc == 0; b++) { + BlobT *blob = &blobs[b]; + uint8_t *modOut = (uint8_t *)malloc((size_t)blob->modLen + FUZZ_CHUNK_MAX); + uint8_t *dbgOut = blob->dbg ? (uint8_t *)malloc((size_t)blob->dbgLen + FUZZ_CHUNK_MAX) : NULL; + int32_t acceptedBefore = deser.accepted; if (!modOut || (blob->dbg && !dbgOut)) { fprintf(stderr, "fuzzModule: out of memory\n"); - return 2; + rc = FUZZ_EXIT_USAGE; + } else { + // Unmutated first: the pristine blob must deserialize and run. + testBlob(blob->mod, blob->modLen, blob->dbg, blob->dbgLen, stepCap, &deser, &run); + + if (deser.accepted == acceptedBefore) { + printf("FAIL %s: pristine module rejected by basModuleDeserialize\n", blob->name); + rc = FUZZ_EXIT_FAIL; + } } - // Unmutated first: the pristine blob must deserialize and run. - int32_t acceptedBefore = deser.accepted; - testBlob(blob->mod, blob->modLen, blob->dbg, blob->dbgLen, stepCap, &deser, &run); - - if (deser.accepted == acceptedBefore) { - printf("FAIL %s: pristine module rejected by basModuleDeserialize\n", blob->name); - return 1; - } - - for (int32_t m = 0; m < mutations; m++) { + for (int32_t m = 0; m < mutations && rc == 0; m++) { // Mutate the module most of the time, the debug blob otherwise. - bool mutateDbg = blob->dbg && fuzzRngNext(&rng, 4) == 0; - MutationE kind = (MutationE)fuzzRngNext(&rng, FUZZ_MUTATION_KINDS); - int32_t modLen = blob->modLen; - int32_t dbgLen = blob->dbgLen; + bool mutateDbg = blob->dbg && fuzzRngNext(&rng, FUZZ_DBG_ODDS) == 0; + MutationE kind = (MutationE)fuzzRngNext(&rng, FUZZ_MUTATION_KINDS); + int32_t modLen = blob->modLen; + int32_t dbgLen = blob->dbgLen; if (mutateDbg) { memcpy(modOut, blob->mod, (size_t)blob->modLen); @@ -453,18 +471,16 @@ int main(int argc, char **argv) { printf("FAIL leak detected after %s mutation %d; recent mutants:\n", blob->name, (int)m); leakHistDump(); fflush(stdout); - return 1; + rc = FUZZ_EXIT_FAIL; } } } free(modOut); free(dbgOut); - free(blob->mod); - free(blob->dbg); } - free(blobs); + blobsFree(blobs, blobCount); if (sLastMod) { fclose(sLastMod); @@ -474,6 +490,10 @@ int main(int argc, char **argv) { fclose(sLastDbg); } + if (rc != 0) { + return rc; + } + remove(FUZZ_LAST_FILE); remove(FUZZ_LAST_DBG_FILE); diff --git a/src/test/basic/runBasic.sh b/src/test/basic/runBasic.sh index ee2af71c..9aeb1e48 100755 --- a/src/test/basic/runBasic.sh +++ b/src/test/basic/runBasic.sh @@ -27,7 +27,10 @@ # # samples every src/test/samples/golden/*-debug.app is loaded and # run to completion (module-level code only; no forms) and -# must exit 0 with no sanitizer report. +# must exit 0 with no sanitizer report. basrun has no +# libraries, so a program whose module code calls one (the +# Terminal starts from module code) may instead stop there +# with error 453 "External function not found". # # fixtures every fixtures/*.bas is compiled and run in a fresh # scratch directory (App.Data points at it); stdout must @@ -66,6 +69,12 @@ update="${UPDATE_GOLDENS:-0}" # making a legitimately long fixture fail. stepCap=2000000 +# basrun's exit code for runtime errors above it (BASRUN_EXIT_ERR_MAX in +# basrun.c) and the error a library call raises there +# (BAS_ERR_EXTERNAL_NOT_FOUND in runtime/basErrors.h). +basrunExitErrMax=250 +basErrExternNotFound=453 + passCount=0 failCount=0 @@ -106,12 +115,19 @@ runOne() { # runSample runSample() { local app="$1" - local name="sample-$(basename "$app" .app)" + local name + name="sample-$(basename "$app" .app)" local work="$outDir/$name" local rc rc=$(runOne "$work" "$work.out" /dev/null "$app") + # Stopped at its first DECLARE LIBRARY call: all the code before it ran + if [ "$rc" = "$basrunExitErrMax" ] && [ "$(wc -l < "$work.out.err")" -eq 1 ] && grep -q "^basrun: runtime error $basErrExternNotFound at .*External function not found" "$work.out.err"; then + pass "$name" + return + fi + if [ "$rc" != "0" ]; then fail "$name" "exit $rc" "$(head -5 "$work.out.err")" return @@ -129,7 +145,8 @@ runSample() { # runFixture runFixture() { local bas="$1" - local name="$(basename "$bas" .bas)" + local name + name="$(basename "$bas" .bas)" local expectOut="$fixtureDir/$name.out" local expectExit=0 local in=/dev/null @@ -202,7 +219,8 @@ runFixture() { # bascomp debug vs release (strip + obfuscate + compact) must run alike. runProjectDiff() { local dbp="$1" - local name="$(basename "$dbp" .dbp)" + local name + name="$(basename "$dbp" .dbp)" local work="$outDir/project-$name" local rcDebug local rcRelease diff --git a/src/test/coverage/runCoverage.sh b/src/test/coverage/runCoverage.sh index cea52d9a..30611a35 100755 --- a/src/test/coverage/runCoverage.sh +++ b/src/test/coverage/runCoverage.sh @@ -52,7 +52,9 @@ covOut="$root/obj/coverage" basicHost="$covObj/basic" basicSrc="$root/src/apps/kpunch/dvxbasic" # The suite list lives in src/test/suites.mk (shared with make test-host). -hostSuites=$(make -s --no-print-directory -f "$root/src/test/suites.mk" --eval 'printSuites: ; @echo $(HOST_SUITES)' printSuites) +# The query drops the calling make's flags: under make -j they bring its +# jobserver, and the query then prints directory lines into the list. +hostSuites=$(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$root/src/test/suites.mk" --eval 'printSuites: ; @echo $(HOST_SUITES)' printSuites) failed="" # Same command-line variables to every make so the sub-makes agree. diff --git a/src/test/dialogs/dialogsTest.c b/src/test/dialogs/dialogsTest.c index 74e5cce0..44e6693c 100644 --- a/src/test/dialogs/dialogsTest.c +++ b/src/test/dialogs/dialogsTest.c @@ -1389,7 +1389,7 @@ static void clickCloseGadget(WindowT *win) { } } - TEST_TRUE(!"close gadget not found"); + TEST_FAIL("close gadget not found"); } diff --git a/src/test/dos/emuEnv.sh b/src/test/dos/emuEnv.sh index b05ceb92..bb069ba9 100644 --- a/src/test/dos/emuEnv.sh +++ b/src/test/dos/emuEnv.sh @@ -32,6 +32,8 @@ # 86Box nor DOSBox Staging (nor Qt/SDL underneath them) can read or write # the user's own configuration, data or cache folders. +# shellcheck shell=bash + EMU_TOOLCHAINS="${DVX_ROOT}/toolchains" BOX86_DIR="${EMU_TOOLCHAINS}/86box" @@ -80,11 +82,10 @@ emuIsolatedEnv() { # setsid, so xvfb-run, Xvfb and the emulator all go), TERM first, then KILL. emuStopGroup() { local pid="$1" - local i kill -TERM -- "-${pid}" 2>/dev/null || true - for i in 1 2 3 4 5; do + for _ in 1 2 3 4 5; do if ! kill -0 -- "-${pid}" 2>/dev/null; then break fi diff --git a/src/test/dos/fixtures/bigdir/bigdir.bas b/src/test/dos/fixtures/bigdir/bigdir.bas new file mode 100644 index 00000000..8bc2f4a5 --- /dev/null +++ b/src/test/dos/fixtures/bigdir/bigdir.bas @@ -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. + +' bigdir.bas -- DOS harness fixture: makes the folder BIG beside this +' program (TEST\BIG) with BIG_FILES files of different sizes and BIG_DIRS +' folders, for timing the File Manager on a large folder (fmperf.scr), and +' logs "BIGDIR: done" when it is ready. An existing BIG is used as it is. + +OPTION EXPLICIT + +CONST BIG_FILES = 400 +CONST BIG_DIRS = 20 +CONST LINE_TEXT = "The quick brown fox jumps over the lazy dog." + +DIM folder AS STRING +DIM i AS INTEGER +DIM j AS INTEGER +DIM f AS INTEGER + +folder = App.Path & "\BIG" + +IF DIR$(folder & "\F0001.TXT") = "" THEN + MKDIR folder + + FOR i = 1 TO BIG_DIRS + MKDIR folder & "\D" & Right$("0" & LTrim$(Str$(i)), 2) + NEXT i + + FOR i = 1 TO BIG_FILES + f = FreeFile + OPEN folder & "\F" & Right$("000" & LTrim$(Str$(i)), 4) & ".TXT" FOR OUTPUT AS #f + + FOR j = 1 TO i MOD 7 + PRINT #f, LINE_TEXT + NEXT j + + CLOSE #f + NEXT i +END IF + +ShellLog "BIGDIR: done" diff --git a/src/test/dos/fixtures/bigdir/bigdir.dbp b/src/test/dos/fixtures/bigdir/bigdir.dbp new file mode 100644 index 00000000..ff8178a0 --- /dev/null +++ b/src/test/dos/fixtures/bigdir/bigdir.dbp @@ -0,0 +1,12 @@ +[Project] +Name = Big Folder +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: makes TEST\BIG, a folder of 400 files and 20 folders, for timing the File Manager + +[Modules] +File0 = ../../../../include/basic/shell.bas +File1 = bigdir.bas +Count = 2 diff --git a/src/test/dos/fixtures/iconview/iconview.bmp b/src/test/dos/fixtures/iconview/iconview.bmp new file mode 100644 index 00000000..1aefa3e6 --- /dev/null +++ b/src/test/dos/fixtures/iconview/iconview.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be4cc8640923cb0214b912845fabb3c7dd03d6c3f1bf53cc4a0a67176da413be +size 1782 diff --git a/src/test/dos/fixtures/iconview/iconview.ctl b/src/test/dos/fixtures/iconview/iconview.ctl new file mode 100644 index 00000000..c11da436 --- /dev/null +++ b/src/test/dos/fixtures/iconview/iconview.ctl @@ -0,0 +1,885 @@ +VERSION DVX 1.00 +Begin UserControl IconView + Width = 240 + Height = 160 + CanGetFocus = True + BackColor = &H00FFFFFF& + ToolboxIcon = "iconview.bmp" + Description = "Icons in a grid" + DefaultEvent = "ItemOpen" +End +' 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. + + +' iconview.ctl -- IconView, Program Manager's icon view written in BASIC +' (a UserControl): items with a name, a picture (an image or a program, +' whose icon it shows), a tag and a detail text, in large icons, small +' icons or a list. Its geometry and rules are Program Manager's, from the +' list helper library (listhelp.bas ListIcon...): the cells, the grid in +' a sunken frame with a scroll bar when the rows do not fit, a large +' icon's name on two lines, the arrow keys, Home, End, PgUp, PgDn and +' type-ahead, when a press drags, the scrolling and the target cell of a +' drag, where a dropped item lands; its colors are the theme's +' (system.bas). A click selects (empty space clears the selection); +' Enter and a double click open an item (ItemOpen), Del and F2 ask to +' delete or rename it (ItemDelete, ItemRename), a right click selects the +' item under the mouse and asks for a menu (ItemContext, -1 over empty +' space). With AutoArrange the items fill the grid in order and an item +' dragged onto another cell moves to that place (ItemMoved); without it +' each item stands in a cell of its own (SetItemCell), and a dragged item +' dropped on a cell is reported (ItemDropped) for the program to place +' -- with the groups model's rule, ShellItemPlace, for example. +' ColumnsChange tells the program the columns whenever they change or the +' items were replaced, so it can give items without a cell one. The +' wheel and the scroll bar (a control's, as ListBox's: its arrows, its +' trough and its thumb, which can be dragged) scroll by rows; the row +' under the last whole one shows as far as it fits; with no items it +' shows EmptyText. Changes paint once, at the next chance +' (UserControl.Invalidate), however many items are added at a time. +' +' The project needs listhelp.bas and system.bas among its modules. + +Option Explicit + +Public Event ItemOpen(ByVal Index As Integer) +Public Event ItemContext(ByVal Index As Integer, ByVal X As Integer, ByVal Y As Integer) +Public Event ItemMoved(ByVal FromIndex As Integer, ByVal ToIndex As Integer) +Public Event ItemDropped(ByVal Index As Integer, ByVal Col As Integer, ByVal Row As Integer) +Public Event ItemDelete(ByVal Index As Integer) +Public Event ItemRename(ByVal Index As Integer) +Public Event ColumnsChange(ByVal Columns As Integer) +Public Event SelChange(ByVal Index As Integer) + +Const NONE = -1 +Const CHAR_SAMPLE = "W" ' a character of the (fixed-width) font +Const LINE_FIRST = 1 ' ListIconLabel$ lines +Const LINE_SECOND = 2 +Const NO_FILL = -1 ' DrawBevel's face that leaves the inside as it is +Const BAR_ACROSS = False ' DrawScrollBar: upright, +Const BAR_CONTROL = True ' a control's bar (Program Manager's) + +' The theme colors Program Manager's view uses (system.bas SysColorId) +Const COLOR_BACK = "contentBg" +Const COLOR_TEXT = "contentFg" +Const COLOR_SEL = "menuHighlightBg" +Const COLOR_SEL_TXT = "menuHighlightFg" + +Private mText() As String +Private mPicFile() As String +Private mTag() As String +Private mDetail() As String +Private mCol() As Integer +Private mRow() As Integer +Private mPic() As Object +Private mCount As Integer +Private mPictures As Integer +Private mView As Integer +Private mEmpty As String +Private mAutoArrange As Boolean +Private mAllowDrag As Boolean +Private mSel As Long +Private mTopRow As Long +Private mLastCols As Long +Private mPlaceDirty As Boolean +Private mDragFrom As Integer +Private mDragging As Boolean +Private mDropCell As Long +Private mDownX As Single +Private mDownY As Single +Private mKeyShift As Integer +Private mBarGrab As Long + +' The grid at the control's size (Layout) +Private mCharW As Long +Private mCharH As Long +Private mRowsTaken As Long +Private mCellW As Long +Private mCellH As Long +Private mPicX As Long +Private mPicY As Long +Private mPicSize As Long +Private mTextX As Long +Private mTextY As Long +Private mNameChars As Long +Private mDetailX As Long +Private mInnerX As Long +Private mInnerY As Long +Private mInnerW As Long +Private mInnerH As Long +Private mCols As Long +Private mRowsShown As Long +Private mRows As Long +Private mTopMax As Long +Private mHasBar As Boolean +Private mEmptyX As Long +Private mEmptyY As Long +Private mFrame As Long + +' The theme's colors (Paint) +Private mBack As Long +Private mFore As Long +Private mSelBack As Long +Private mSelFore As Long + + +' ---- Properties ---- + +Public Property Get AllowDrag() As Boolean + AllowDrag = mAllowDrag +End Property + + +Public Property Let AllowDrag(ByVal Value As Boolean) + mAllowDrag = Value +End Property + + +' True: the items fill the grid in order; False: each stands in its own +' cell (SetItemCell) +Public Property Get AutoArrange() As Boolean + AutoArrange = mAutoArrange +End Property + + +Public Property Let AutoArrange(ByVal Value As Boolean) + mAutoArrange = Value + mPlaceDirty = True + UserControl.Invalidate +End Property + + +Public Property Get Columns() As Integer + Layout + Columns = mCols +End Property + + +Public Property Get EmptyText() As String + EmptyText = mEmpty +End Property + + +Public Property Let EmptyText(ByVal Text As String) + mEmpty = Text + UserControl.Invalidate +End Property + + +Public Property Get ItemCount() As Integer + ItemCount = mCount +End Property + + +' Items whose picture could be loaded +Public Property Get PictureCount() As Integer + PictureCount = mPictures +End Property + + +Public Property Get SelectedIndex() As Integer + SelectedIndex = ListSelCursor(mSel) +End Property + + +Public Property Let SelectedIndex(ByVal Index As Integer) + If Index < NONE Or Index >= mCount Then Exit Property + SelectItem Index +End Property + + +' Scroll position in pixels (the top row times the cell height) +Public Property Get TopOffset() As Long + Layout + TopOffset = mTopRow * mCellH +End Property + + +Public Property Get View() As Integer + View = mView +End Property + + +Public Property Let View(ByVal Value As Integer) + If Value < LISTICON_VIEW_LARGE Or Value > LISTICON_VIEW_LIST Then Exit Property + mView = Value + mPlaceDirty = True + UserControl.Invalidate +End Property + + +' ---- Methods ---- + +' A new item at the end (without a cell of its own yet); its index. +Public Function AddItem(ByVal Text As String, ByVal Picture As String, ByVal Tag As String) As Integer + ItemSlots mCount + mText(mCount) = Text + mPicFile(mCount) = Picture + mTag(mCount) = Tag + mDetail(mCount) = "" + mCol(mCount) = NONE + mRow(mCount) = NONE + Set mPic(mCount) = Nothing + + If Picture <> "" Then + On Error Resume Next + Set mPic(mCount) = LoadPicture(Picture) + If Err.Number = 0 Then mPictures = mPictures + 1 Else Set mPic(mCount) = Nothing + On Error GoTo 0 + End If + + ListSelInsert mSel, mCount + AddItem = mCount + mCount = mCount + 1 + mPlaceDirty = True + UserControl.Invalidate +End Function + + +Public Sub Clear() + mCount = 0 + mPictures = 0 + mPlaceDirty = True + ListSelReset mSel, 0 + UserControl.Invalidate +End Sub + + +' Scroll so item Index is in view. +Public Sub EnsureVisible(ByVal Index As Integer) + Dim col As Long + Dim row As Long + + If Index < 0 Or Index >= mCount Then Exit Sub + Layout + ItemCell Index, col, row + + If row < mTopRow Then + mTopRow = row + ElseIf row >= mTopRow + mRowsShown Then + mTopRow = row - mRowsShown + 1 + End If +End Sub + + +' The item at a point of the control, -1 for none. +Public Function ItemAtPoint(ByVal X As Integer, ByVal Y As Integer) As Integer + Layout + ItemAtPoint = ItemAt(X, Y) +End Function + + +' An item's own cell (-1 when it has none) +Public Function ItemCol(ByVal Index As Integer) As Integer + ItemCol = NONE + If Index >= 0 And Index < mCount Then ItemCol = mCol(Index) +End Function + + +' The text the list shows after an item's name +Public Function ItemDetail(ByVal Index As Integer) As String + If Index >= 0 And Index < mCount Then ItemDetail = mDetail(Index) +End Function + + +Public Function ItemName(ByVal Index As Integer) As String + If Index >= 0 And Index < mCount Then ItemName = mText(Index) +End Function + + +Public Function ItemPicture(ByVal Index As Integer) As String + If Index >= 0 And Index < mCount Then ItemPicture = mPicFile(Index) +End Function + + +Public Function ItemRow(ByVal Index As Integer) As Integer + ItemRow = NONE + If Index >= 0 And Index < mCount Then ItemRow = mRow(Index) +End Function + + +Public Function ItemTag(ByVal Index As Integer) As String + If Index >= 0 And Index < mCount Then ItemTag = mTag(Index) +End Function + + +Public Sub RemoveItem(ByVal Index As Integer) + Dim i As Integer + + If Index < 0 Or Index >= mCount Then Exit Sub + If Not (mPic(Index) Is Nothing) Then mPictures = mPictures - 1 + + For i = Index To mCount - 2 + CopyItem i, i + 1 + Next i + + mCount = mCount - 1 + mPlaceDirty = True + ListSelRemove mSel, Index + UserControl.Invalidate +End Sub + + +' The cell an item stands in when AutoArrange is False +Public Sub SetItemCell(ByVal Index As Integer, ByVal Col As Integer, ByVal Row As Integer) + If Index < 0 Or Index >= mCount Then Exit Sub + mCol(Index) = Col + mRow(Index) = Row + UserControl.Invalidate +End Sub + + +Public Sub SetItemDetail(ByVal Index As Integer, ByVal Text As String) + If Index < 0 Or Index >= mCount Then Exit Sub + mDetail(Index) = Text + UserControl.Invalidate +End Sub + + +' ---- Layout ---- + +' True when the items stand in cells of their own (not in the list) +Private Function ByHand() As Boolean + ByHand = Not mAutoArrange And mView <> LISTICON_VIEW_LIST +End Function + + +' A cell number (ListIconCellAt) into its column and row +Private Sub CellPlace(ByVal cell As Long, col As Long, row As Long) + col = cell Mod mCols + row = cell \ mCols +End Sub + + +' Every item's cell for ListIconNavigate: "col,row,col,row..." +Private Function CellList() As String + Dim i As Integer + Dim col As Long + Dim row As Long + Dim cells As String + + For i = 0 To mCount - 1 + ItemCell i, col, row + If i > 0 Then cells = cells & "," + cells = cells & col & "," & row + Next i + + CellList = cells +End Function + + +' One value of the grid (listhelp.bas ListIconMetric) +Private Function GridValue(ByVal metric As Long) As Long + GridValue = ListIconMetric(mView, metric, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken) +End Function + + +' The item under X, Y; -1 for none. +Private Function ItemAt(ByVal X As Single, ByVal Y As Single) As Integer + Dim cell As Long + Dim col As Long + Dim row As Long + Dim i As Integer + + ItemAt = NONE + cell = ListIconCellAt(mView, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken, mTopRow, X, Y) + If cell = NONE Then Exit Function + + If Not ByHand() Then + If cell < mCount Then ItemAt = cell + Exit Function + End If + + CellPlace cell, col, row + + For i = 0 To mCount - 1 + If mCol(i) = col And mRow(i) = row Then + ItemAt = i + Exit Function + End If + Next i +End Function + + +' Item i's cell: its own (a missing one counts as 0), or its place in +' order +Private Sub ItemCell(ByVal i As Integer, col As Long, row As Long) + If ByHand() Then + col = mCol(i) + row = mRow(i) + If col < 0 Then col = 0 + If row < 0 Then row = 0 + Else + CellPlace i, col, row + End If +End Sub + + +' The grid at the control's size, the top row kept in range +Private Sub Layout() + Dim i As Integer + Dim col As Long + Dim row As Long + + mCharW = UserControl.TextWidth(CHAR_SAMPLE) + mCharH = UserControl.TextHeight(CHAR_SAMPLE) + mRowsTaken = LISTICON_BY_ORDER + + ' Items in cells of their own take rows whatever the columns + If ByHand() Then + mRowsTaken = 0 + + For i = 0 To mCount - 1 + ItemCell i, col, row + If row + 1 > mRowsTaken Then mRowsTaken = row + 1 + Next i + End If + + mCellW = GridValue(LISTICON_CELL_W) + mCellH = GridValue(LISTICON_CELL_H) + mPicX = GridValue(LISTICON_PICTURE_X) + mPicY = GridValue(LISTICON_PICTURE_Y) + mPicSize = GridValue(LISTICON_PICTURE_SIZE) + mTextX = GridValue(LISTICON_TEXT_X) + mTextY = GridValue(LISTICON_TEXT_Y) + mNameChars = GridValue(LISTICON_NAME_CHARS) + mDetailX = GridValue(LISTICON_DETAIL_X) + mInnerX = GridValue(LISTICON_INNER_X) + mInnerY = GridValue(LISTICON_INNER_Y) + mInnerW = GridValue(LISTICON_INNER_W) + mInnerH = GridValue(LISTICON_INNER_H) + mCols = GridValue(LISTICON_COLUMNS) + mRowsShown = GridValue(LISTICON_ROWS_SHOWN) + mRows = GridValue(LISTICON_ROWS) + mTopMax = GridValue(LISTICON_TOP_ROW_MAX) + mHasBar = GridValue(LISTICON_SCROLLBAR) + mEmptyX = GridValue(LISTICON_EMPTY_X) + mEmptyY = GridValue(LISTICON_EMPTY_Y) + mFrame = GridValue(LISTICON_FRAME) + If mTopRow > mTopMax Then mTopRow = mTopMax + If mTopRow < 0 Then mTopRow = 0 +End Sub + + +' The grid, first telling the program the columns whenever they changed +' or the items were replaced (it may give items without a cell one) -- +' the order Program Manager's view keeps. +Private Sub Sync() + Layout + If mCols = mLastCols And Not mPlaceDirty Then Exit Sub + mLastCols = mCols + mPlaceDirty = False + RaiseEvent ColumnsChange(mCols) + Layout +End Sub + + +' ---- Drawing ---- + +' One item: its picture, then its name -- two centred lines under a large +' icon, one beside a small one, and in the list its detail after the +' name column. The selected name is highlighted; the focus rectangle +' goes around it. +Private Sub DrawItem(ByVal i As Integer, ByVal col As Long, ByVal row As Long) + Dim x As Long + Dim y As Long + Dim ty As Long + Dim boxX As Long + Dim boxW As Long + Dim boxH As Long + Dim room As Long + Dim line1 As String + Dim line2 As String + Dim selected As Boolean + + x = mInnerX + col * mCellW + y = mInnerY + (row - mTopRow) * mCellH + ty = y + mTextY + selected = ListSelIsSelected(mSel, i) <> 0 + + If mPic(i) Is Nothing Then + DrawPlaceholder x + mPicX, y + mPicY, mText(i) + Else + UserControl.PaintPicture mPic(i), x + mPicX, y + mPicY, mPicSize, mPicSize + End If + + boxH = mCharH + + If mView = LISTICON_VIEW_LARGE Then + line1 = ListIconLabel$(mText(i), mNameChars, LINE_FIRST) + line2 = ListIconLabel$(mText(i), mNameChars, LINE_SECOND) + boxW = UserControl.TextWidth(line1) + If UserControl.TextWidth(line2) > boxW Then boxW = UserControl.TextWidth(line2) + If line2 <> "" Then boxH = boxH + mCharH + boxX = x + (mCellW - boxW) \ 2 + Else + line1 = Left$(mText(i), mNameChars) + boxW = UserControl.TextWidth(line1) + boxX = x + mTextX + End If + + If selected And boxW > 0 Then + UserControl.Line (boxX, ty)-(boxX + boxW - 1, ty + boxH - 1), mSelBack, BF + UserControl.ForeColor = mSelFore + End If + + If mView = LISTICON_VIEW_LARGE Then + UserControl.CurrentX = x + (mCellW - UserControl.TextWidth(line1)) \ 2 + UserControl.CurrentY = ty + UserControl.Print line1; + UserControl.CurrentX = x + (mCellW - UserControl.TextWidth(line2)) \ 2 + UserControl.CurrentY = ty + mCharH + UserControl.Print line2; + Else + UserControl.CurrentX = boxX + UserControl.CurrentY = ty + UserControl.Print line1; + End If + + UserControl.ForeColor = mFore + + ' The list's detail column + If mDetailX > 0 Then + room = (mCellW - mDetailX) \ mCharW + + If room > 0 Then + UserControl.CurrentX = x + mDetailX + UserControl.CurrentY = ty + UserControl.Print Left$(mDetail(i), room); + End If + End If + + If selected And boxW > 0 And UserControl.HasFocus Then + UserControl.DrawFocusRect boxX, ty, boxW, boxH + End If +End Sub + + +' Stand-in for an item without a picture: a raised square with the first +' letter of its name. +Private Sub DrawPlaceholder(ByVal x As Long, ByVal y As Long, ByVal itemName As String) + Dim letter As String + + letter = Left$(itemName, 1) + UserControl.DrawBevel x, y, mPicSize, mPicSize + UserControl.CurrentX = x + (mPicSize - UserControl.TextWidth(letter)) \ 2 + UserControl.CurrentY = y + (mPicSize - mCharH) \ 2 + UserControl.Print letter; +End Sub + + +' The background, the rows shown (the one under the last whole row as +' far as it fits), the target of a drag, then the sunken frame over the +' edges (it leaves the inside alone, so a part row stops at it), the +' scroll bar, and with no selection the focus around the whole view. +Private Sub UserControl_Paint() + Dim i As Integer + Dim col As Long + Dim row As Long + + mBack = SysGetColor(SysColorId(COLOR_BACK)) + mFore = SysGetColor(SysColorId(COLOR_TEXT)) + mSelBack = SysGetColor(SysColorId(COLOR_SEL)) + mSelFore = SysGetColor(SysColorId(COLOR_SEL_TXT)) + Sync + UserControl.ForeColor = mFore + UserControl.Line (0, 0)-(UserControl.Width - 1, UserControl.Height - 1), mBack, BF + + If mCount = 0 Then + UserControl.CurrentX = mEmptyX + UserControl.CurrentY = mEmptyY + UserControl.Print mEmpty; + End If + + For i = 0 To mCount - 1 + ItemCell i, col, row + If row >= mTopRow And row <= mTopRow + mRowsShown Then DrawItem i, col, row + Next i + + If mDragging And mDropCell <> NONE Then + CellPlace mDropCell, col, row + UserControl.DrawFocusRect mInnerX + col * mCellW, mInnerY + (row - mTopRow) * mCellH, mCellW, mCellH + End If + + UserControl.DrawBevel 0, 0, UserControl.Width, UserControl.Height, True, NO_FILL, mFrame + If mHasBar Then UserControl.DrawScrollBar mInnerX + mInnerW, mInnerY, mInnerH, mTopRow, mRowsShown, mRows, BAR_ACROSS, BAR_CONTROL + + If UserControl.HasFocus And ListSelCursor(mSel) = NONE Then + UserControl.DrawFocusRect 1, 1, UserControl.Width - 2, UserControl.Height - 2 + End If +End Sub + + +' ---- Events of the control ---- + +Private Sub UserControl_DblClick() + Dim cur As Integer + + cur = ListSelCursor(mSel) + If cur <> NONE Then RaiseEvent ItemOpen(cur) +End Sub + + +Private Sub UserControl_GotFocus() + UserControl.Invalidate +End Sub + + +Private Sub UserControl_Initialize() + mSel = ListSelCreate(False, 0) + mDragFrom = NONE + mDropCell = NONE + mBarGrab = NONE + mLastCols = NONE + mEmpty = "(no items)" + mAutoArrange = True + mAllowDrag = True + mPlaceDirty = True +End Sub + + +' Enter opens, Del and F2 ask to delete or rename, the arrow keys, Home, +' End, PgUp and PgDn move (Program Manager's rule over the items' cells); +' keys with Ctrl or Alt are left alone. +Private Sub UserControl_KeyDown(KeyCode As Integer, Shift As Integer) + Dim cur As Integer + Dim target As Long + + mKeyShift = Shift + If Shift And (vbCtrlMask Or vbAltMask) Then Exit Sub + Sync + cur = ListSelCursor(mSel) + + Select Case KeyCode + Case vbKeyReturn + If cur <> NONE Then RaiseEvent ItemOpen(cur) + Case vbKeyDelete + If cur <> NONE Then RaiseEvent ItemDelete(cur) + Case vbKeyF2 + If cur <> NONE Then RaiseEvent ItemRename(cur) + Case Else + target = ListIconNavigate(KeyCode, cur, CellList(), mCols, mRowsShown) + If target <> NONE Then SelectItem target + End Select +End Sub + + +' Type-ahead: the next item starting with the letter +Private Sub UserControl_KeyPress(KeyAscii As Integer) + Dim items As String + Dim i As Integer + Dim found As Long + + If mKeyShift And (vbCtrlMask Or vbAltMask) Then Exit Sub + + For i = 0 To mCount - 1 + If i > 0 Then items = items & Chr$(10) + items = items & mText(i) + Next i + + found = ListTypeAhead(Chr$(KeyAscii), items, ListSelCursor(mSel)) + If found <> NONE Then SelectItem found +End Sub + + +Private Sub UserControl_LostFocus() + UserControl.Invalidate +End Sub + + +' Left: the scroll bar (its arrows a row, its trough a page, its thumb +' grabbed to drag), else the item under the mouse is selected (empty +' space clears the selection) and may be dragged. Right: the item under +' the mouse is selected, then the menu is asked for. +Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) + Dim i As Integer + Dim top As Long + + Sync + + If Button = vbLeftButton Then + top = ListIconScrollPress(mView, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken, mTopRow, X, Y) + + If top <> NONE Then + mBarGrab = ListIconScrollGrab(mView, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken, mTopRow, X, Y) + mTopRow = top + UserControl.Invalidate + Exit Sub + End If + End If + + i = ItemAt(X, Y) + + If Button = vbRightButton Then + If i <> NONE Then SelectItem i + UserControl.SetFocus + RaiseEvent ItemContext(i, X, Y) + Exit Sub + End If + + If Button <> vbLeftButton Then Exit Sub + SelectItem i + + If i <> NONE And mAllowDrag Then + mDragFrom = i + mDownX = X + mDownY = Y + mDragging = False + End If +End Sub + + +' The grabbed thumb follows the mouse (outside the control too, while the +' button is down). A press on an item that moves far enough drags it: +' the cell under the mouse is the target, the view scrolling a row while +' the mouse is above or below the grid. +Private Sub UserControl_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) + If (Button And vbLeftButton) = 0 Then Exit Sub + + If mBarGrab <> NONE Then + Layout + mTopRow = ListIconScrollDrag(mView, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken, mBarGrab, Y) + UserControl.Invalidate + Exit Sub + End If + + If mDragFrom = NONE Then Exit Sub + + If Not mDragging Then + If Not ListIconDragStarted(X - mDownX, Y - mDownY) Then Exit Sub + mDragging = True + End If + + Layout + mTopRow = ListIconDragScroll(mView, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken, mTopRow, Y) + mDropCell = ListIconDropCell(mView, mCharW, mCharH, UserControl.Width, UserControl.Height, mCount, mRowsTaken, mTopRow, X, Y) + UserControl.Invalidate +End Sub + + +' The end of a drag: in order the item moves to the target cell's place +' (ItemMoved, then SelChange as it is selected there); in cells of their +' own the program is told the target cell (ItemDropped). +Private Sub UserControl_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single) + Dim fromIdx As Integer + Dim dest As Long + Dim col As Long + Dim row As Long + + If Button <> vbLeftButton Then Exit Sub + mBarGrab = NONE + fromIdx = mDragFrom + + If mDragging And mDropCell <> NONE Then + If ByHand() Then + CellPlace mDropCell, col, row + RaiseEvent ItemDropped(fromIdx, col, row) + Else + dest = ListIconDropDest(fromIdx, mDropCell, mCount) + + If dest <> NONE Then + MoveItem fromIdx, dest + ListSelReorder mSel, fromIdx, dest + RaiseEvent ItemMoved(fromIdx, dest) + RaiseEvent SelChange(dest) + End If + End If + End If + + mDragFrom = NONE + mDragging = False + mDropCell = NONE + UserControl.Invalidate +End Sub + + +Private Sub UserControl_MouseWheel(Delta As Integer) + Layout + mTopRow = mTopRow + Delta * GridValue(LISTICON_WHEEL_ROWS) + Layout + UserControl.Invalidate +End Sub + + +Private Sub UserControl_Terminate() + ListSelFree mSel +End Sub + + +' ---- Helpers ---- + +' Item src's data into place dst +Private Sub CopyItem(ByVal dst As Integer, ByVal src As Integer) + mText(dst) = mText(src) + mPicFile(dst) = mPicFile(src) + mTag(dst) = mTag(src) + mDetail(dst) = mDetail(src) + mCol(dst) = mCol(src) + mRow(dst) = mRow(src) + Set mPic(dst) = mPic(src) +End Sub + + +' Room for items 0 to last +Private Sub ItemSlots(ByVal last As Integer) + ReDim Preserve mText(last) + ReDim Preserve mPicFile(last) + ReDim Preserve mTag(last) + ReDim Preserve mDetail(last) + ReDim Preserve mCol(last) + ReDim Preserve mRow(last) + ReDim Preserve mPic(last) +End Sub + + +' Item fromIdx moves to dest, the items between shift over (the spare +' slot after the last item holds it meanwhile). +Private Sub MoveItem(ByVal fromIdx As Integer, ByVal dest As Integer) + Dim i As Integer + + ItemSlots mCount + CopyItem mCount, fromIdx + + If dest > fromIdx Then + For i = fromIdx To dest - 1 + CopyItem i, i + 1 + Next i + Else + For i = fromIdx To dest + 1 Step -1 + CopyItem i, i - 1 + Next i + End If + + CopyItem dest, mCount + Set mPic(mCount) = Nothing +End Sub + + +' Item i (-1: none) becomes the selected one, scrolled into view; +' SelChange when the selection changed. +Private Sub SelectItem(ByVal i As Integer) + If i = ListSelCursor(mSel) Then Exit Sub + ListSelSetCursor mSel, i + EnsureVisible i + UserControl.Invalidate + RaiseEvent SelChange(i) +End Sub diff --git a/src/test/dos/fixtures/iconview/iconview.dbp b/src/test/dos/fixtures/iconview/iconview.dbp new file mode 100644 index 00000000..cb6dd94a --- /dev/null +++ b/src/test/dos/fixtures/iconview/iconview.dbp @@ -0,0 +1,20 @@ +[Project] +Name = ICONVIEW +Type = Control +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = IconView, an icon view written in BASIC (DOS fixture and Program Manager parity) + +[Modules] +File0 = ../../../../include/basic/listhelp.bas +File1 = ../../../../include/basic/system.bas +Count = 2 + +[Controls] +File0 = iconview.ctl +Count = 1 + +[Settings] +OptionExplicit = True diff --git a/src/test/dos/fixtures/parcp/parcp.dbp b/src/test/dos/fixtures/parcp/parcp.dbp new file mode 100644 index 00000000..fa54dffa --- /dev/null +++ b/src/test/dos/fixtures/parcp/parcp.dbp @@ -0,0 +1,26 @@ +[Project] +Name = Control Panel Parity +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: what Control Panel does, from BASIC + +[Modules] +File0 = ../../../../include/basic/prefs.bas +File1 = ../../../../include/basic/system.bas +File2 = ../../../../include/basic/shell.bas +File3 = ../../../../include/basic/fileops.bas +File4 = ../../../../include/basic/sound.bas +File5 = ../../../../include/basic/joystick.bas +File6 = ../../../../include/basic/net.bas +File7 = ../../../../include/basic/print.bas +File8 = ../../../../include/basic/layout.bas +Count = 9 + +[Forms] +File0 = parcp.frm +Count = 1 + +[Settings] +StartupForm = ParCp diff --git a/src/test/dos/fixtures/parcp/parcp.frm b/src/test/dos/fixtures/parcp/parcp.frm new file mode 100644 index 00000000..da60403f --- /dev/null +++ b/src/test/dos/fixtures/parcp/parcp.frm @@ -0,0 +1,627 @@ +VERSION DVX 1.00 + +Begin Form ParCp + Caption = "Control Panel Parity" + Layout = VBox + AutoSize = False + Resizable = False + Width = 360 + Height = 90 + + Begin Label lblResult + Caption = "Running..." + Weight = 1 + End +End + +' The MIT License (MIT) +' +' Copyright (C) 2026 Scott Duensing +' +' Permission is hereby granted, free of charge, to any person obtaining a copy + +' parcp -- DOS harness fixture: the hardest parts of Control Panel, from +' BASIC. Changes the screen mode and back, reads and sets a color and +' puts it back, saves and loads a theme, sets and clears a wallpaper, +' applies a mouse setup and restores it from a snapshot, lists the sound +' events and drivers, reads the joysticks and their settings, the +' network settings and status, the printer models and the file types. +' The window title reports the outcome for the script: "PARCP PASS" or +' "PARCP FAIL ". A library that is not installed (error 453, as +' on a host test with fewer libraries) is listed after PASS as +' "(no )", the way Control Panel shows "... is not installed.". + +OPTION EXPLICIT + +CONST ERR_NOT_INSTALLED = 453 +CONST TEST_COLOR = 2 +CONST TEST_DBL_MS = 650 +CONST TEST_MICKEYS = 12 +CONST TEST_WHEEL_STEP = 5 +CONST TEST_VOLUME = 50 +CONST TEST_SND_VOLUME = 70 +CONST JOY_ZONE_LIMIT = 50 +CONST TEST_CHARS = 20 + +DIM SHARED missing AS STRING + + +' The settings snapshot every step goes back to +DIM SHARED snap AS LONG + + +FUNCTION CheckColors() AS STRING + DIM oldColor AS LONG + DIM theme AS STRING + + CheckColors = "" + + IF SysColorCount() < TEST_COLOR + 1 THEN + CheckColors = "color count" + EXIT FUNCTION + END IF + + IF SysColorLabel$(TEST_COLOR) = "" OR SysColorId(UCASE$(SysColorName$(TEST_COLOR))) <> TEST_COLOR THEN + CheckColors = "color names" + EXIT FUNCTION + END IF + + oldColor = SysGetColor(TEST_COLOR) + SysSetColor TEST_COLOR, RGB(11, 22, 33) + + IF SysGetColor(TEST_COLOR) <> RGB(11, 22, 33) THEN + CheckColors = "set color" + EXIT FUNCTION + END IF + + SysSetColor TEST_COLOR, oldColor + + IF SysGetColor(TEST_COLOR) <> oldColor THEN + CheckColors = "restore color" + EXIT FUNCTION + END IF + + ' A theme saved, the color changed, the theme loaded back + theme = ShellConfigFile$("PARCP.THM") + + IF NOT SysSaveTheme(theme) THEN + CheckColors = "save theme" + EXIT FUNCTION + END IF + + SysSetColor TEST_COLOR, RGB(1, 2, 3) + + IF NOT SysLoadTheme(theme) OR SysGetColor(TEST_COLOR) <> oldColor THEN + CheckColors = "load theme" + EXIT FUNCTION + END IF + + KILL theme + + ' Reset, then the snapshot puts the colors back + SysResetColors + + IF NOT SysColorsLoad(snap) OR SysGetColor(TEST_COLOR) <> oldColor THEN + CheckColors = "colors snapshot" + END IF +END FUNCTION + + +FUNCTION CheckFileTypes() AS STRING + DIM i AS LONG + + CheckFileTypes = "" + + FOR i = 0 TO ShellAssocCount() - 1 + IF ShellAssocExt$(i) = "" OR ShellAssocValue$(i) = "" THEN + CheckFileTypes = "file type " + LTRIM$(STR$(i)) + EXIT FUNCTION + END IF + + IF ShellAssocNormalize$(ShellAssocExt$(i)) <> ShellAssocExt$(i) THEN + CheckFileTypes = "file type name " + ShellAssocExt$(i) + EXIT FUNCTION + END IF + NEXT i + + IF ShellProgramName$(ShellAppPath$()) = "" THEN + CheckFileTypes = "program name" + EXIT FUNCTION + END IF + + ' The folder a program Browse starts in (where ShellProgramScan looks) + IF ShellProgramsFolder$() = "" THEN + CheckFileTypes = "programs folder" + END IF +END FUNCTION + + +FUNCTION CheckJoystick() AS STRING + DIM what AS STRING + DIM i AS LONG + DIM saved AS LONG + DIM oldChoice AS LONG + DIM oldZone AS LONG + DIM newZone AS LONG + DIM count AS LONG + + ON ERROR GOTO NoJoy + what = "" + + FOR i = 1 TO JoyCount() + IF JoyName$(i) = "" OR JoyHats(i) < 0 OR JoyDeadZone(i) < 0 THEN + what = "joystick " + LTRIM$(STR$(i)) + GOTO JoyOut + END IF + + IF JoyConnected(i) AND JoyAxisPresent(i, 1) AND JoyCanCalibrate(i) THEN + IF JoyRawAxis(i, 1) < 0 THEN + what = "raw axis" + GOTO JoyOut + END IF + END IF + NEXT i + + IF JoyCount() > 0 THEN + oldZone = JoyDeadZone(1) + newZone = (oldZone + 1) MOD JOY_ZONE_LIMIT + + IF NOT JoySetDeadZone(1, newZone) OR JoyDeadZone(1) <> newZone THEN + what = "dead zone" + GOTO JoyOut + END IF + + JoySetDeadZone 1, oldZone + END IF + + ' The keyboard joystick's keys come from the library's own list + IF JoyKbdSlotCount() < 1 OR JoyKeyCount() < 1 THEN + what = "keyboard joystick" + GOTO JoyOut + END IF + + IF JoyKeyName$(JoyKbdSlot(0)) = "" THEN + what = "keyboard key" + GOTO JoyOut + END IF + + ' A setting changed, then the snapshot puts it back + saved = PrefsCreate() + JoySettingsStore saved + count = JoyChoiceCount(JOY_CHOICE_FALLBACK) + oldChoice = JoyChoice(JOY_CHOICE_FALLBACK) + + IF count < 2 OR JoyChoiceLabel$(JOY_CHOICE_FALLBACK, 0) = "" THEN + what = "joystick choices" + ELSEIF NOT JoySetChoice(JOY_CHOICE_FALLBACK, (oldChoice + 1) MOD count) OR JoyChoice(JOY_CHOICE_FALLBACK) = oldChoice THEN + what = "joystick setting" + END IF + + JoySettingsLoad saved + PrefsClose saved + + IF what = "" AND JoyChoice(JOY_CHOICE_FALLBACK) <> oldChoice THEN + what = "joystick settings back" + END IF + + IF what = "" THEN + count = JoyRescan() + + IF count <> -1 AND count <> JoyCount() THEN + what = "joystick rescan" + END IF + END IF + + GOTO JoyOut + +NoJoy: + what = NotInstalled("joystick") + RESUME JoyOut + +JoyOut: + CheckJoystick = what +END FUNCTION + + +FUNCTION CheckMouse() AS STRING + DIM accel AS LONG + DIM wheelDir AS LONG + + CheckMouse = "" + accel = PrefsChoiceValue(PREFS_CHOICE_ACCEL, 0) + wheelDir = PrefsChoiceValue(PREFS_CHOICE_WHEEL, PrefsChoiceIndex(PREFS_CHOICE_WHEEL, "reversed")) + + IF PrefsChoiceCount(PREFS_CHOICE_ACCEL) < 2 OR PrefsChoiceLabel$(PREFS_CHOICE_WHEEL, 0) = "" OR wheelDir <> -1 THEN + CheckMouse = "mouse choices" + EXIT FUNCTION + END IF + + SysSetMouse wheelDir, TEST_DBL_MS, accel, TEST_MICKEYS, TEST_WHEEL_STEP + + IF SysMouseWheelDir() <> wheelDir OR SysMouseDoubleClick() <> TEST_DBL_MS OR SysMouseAccel() <> accel OR SysMouseMickeys() <> TEST_MICKEYS OR SysMouseWheelStep() <> TEST_WHEEL_STEP THEN + CheckMouse = "set mouse" + EXIT FUNCTION + END IF + + ' The wheel step is kept within DVX's limits + SysSetMouse wheelDir, MOUSE_DBLCLICK_DEFAULT_MS, 0, 0, MOUSE_WHEEL_STEP_MAX + 1 + + IF SysMouseWheelStep() <> MOUSE_WHEEL_STEP_MAX OR SysMouseDoubleClick() <> MOUSE_DBLCLICK_DEFAULT_MS OR MOUSE_WHEEL_STEP_DEFAULT < MOUSE_WHEEL_STEP_MIN OR MOUSE_SPEED_DEFAULT <= 0 THEN + CheckMouse = "mouse limits" + EXIT FUNCTION + END IF + + SysMouseLoad snap + + IF SysMouseDoubleClick() <> PrefsGetInt(snap, "mouse", "doubleclick", 0) OR PrefsChoiceIndexOfValue(PREFS_CHOICE_WHEEL, SysMouseWheelDir()) <> PrefsGetChoice(snap, "mouse", "wheel", PREFS_CHOICE_WHEEL) THEN + CheckMouse = "restore mouse" + END IF +END FUNCTION + + +FUNCTION CheckNet() AS STRING + DIM what AS STRING + DIM cfg AS LONG + DIM saved AS LONG + + ON ERROR GOTO NoNet + what = "" + + ' The defaults (no DVX.INI is touched): edit, check, store, read back + cfg = NetConfigLoad(0) + + IF cfg = 0 OR NetConfigGet$(cfg, "dhcp") = "" OR NetConfigChanged(cfg) THEN + what = "net config" + NetConfigFree cfg + GOTO NetOut + END IF + + IF NOT NetConfigSet(cfg, "hostname", "PARCP") OR NOT NetConfigChanged(cfg) OR NetConfigProblem$(cfg) <> "" THEN + what = "net edit" + NetConfigFree cfg + GOTO NetOut + END IF + + saved = PrefsCreate() + + IF NOT NetConfigStore(cfg, saved) THEN + what = "net store" + END IF + + NetConfigFree cfg + cfg = NetConfigLoad(saved) + PrefsClose saved + + IF what = "" AND NetConfigGet$(cfg, "hostname") <> "PARCP" THEN + what = "net read back" + END IF + + NetConfigFree cfg + + IF what = "" AND (NetVectorCount() < 1 OR NetStatus$("message") = "") THEN + what = "net status" + END IF + + GOTO NetOut + +NoNet: + what = NotInstalled("net") + RESUME NetOut + +NetOut: + CheckNet = what +END FUNCTION + + +FUNCTION CheckPrint() AS STRING + DIM what AS STRING + DIM i AS LONG + DIM fileDest AS LONG + DIM saved AS LONG + + ON ERROR GOTO NoPrint + what = "" + fileDest = -1 + + FOR i = 0 TO PrintModelCount() - 1 + IF PrintModelLabel$(i) = "" THEN + what = "printer model " + LTRIM$(STR$(i)) + GOTO PrintOut + END IF + NEXT i + + FOR i = 0 TO PrintDestCount() - 1 + IF PrintDestIsFile(i) THEN + fileDest = i + END IF + NEXT i + + IF PrintModelCount() < 1 OR fileDest < 0 OR PrintDestLabel$(fileDest) = "" OR PrintSettingsProblem$(fileDest, "") = "" THEN + what = "printer lists" + GOTO PrintOut + END IF + + saved = PrefsCreate() + + IF NOT PrintSettingsStore(saved, 0, fileDest, "PARCP.PRN") THEN + what = "printer store" + ELSEIF PrintSettingModel(saved) <> 0 OR PrintSettingDest(saved) <> fileDest OR PrintSettingFile$(saved) <> "PARCP.PRN" THEN + what = "printer read back" + END IF + + PrefsClose saved + GOTO PrintOut + +NoPrint: + what = NotInstalled("print") + RESUME PrintOut + +PrintOut: + CheckPrint = what +END FUNCTION + + +' Sizes in characters (Control Panel's wgtChars): the label takes the +' width of TEST_CHARS characters and reads it back in pixels +FUNCTION CheckSizes() AS STRING + CheckSizes = "" + lblResult.MinWidth = SizeChars(TEST_CHARS) + + IF lblResult.MinWidth <> TEST_CHARS * ParCp.TextWidth("0") THEN + CheckSizes = "size in characters" + END IF + + lblResult.MinWidth = 0 +END FUNCTION + + +FUNCTION CheckSound() AS STRING + DIM what AS STRING + DIM i AS LONG + DIM saved AS LONG + DIM oldVolume AS LONG + + ON ERROR GOTO NoSound + what = "" + + IF SoundEventCount() < 1 OR SoundFolder$() = "" OR SoundDriverAuto$() = "" OR SoundMidiCount() < 1 THEN + what = "sound lists" + GOTO SoundOut + END IF + + FOR i = 0 TO SoundEventCount() - 1 + IF SoundEventLabel$(i) = "" OR SoundEventKey$(i) = "" THEN + what = "sound event " + LTRIM$(STR$(i)) + GOTO SoundOut + END IF + NEXT i + + FOR i = 0 TO SoundDriverCount() - 1 + IF SoundDriverName$(i) = "" THEN + what = "sound driver " + LTRIM$(STR$(i)) + GOTO SoundOut + END IF + NEXT i + + FOR i = 0 TO SoundMidiCount() - 1 + IF SoundMidiLabel$(i) = "" THEN + what = "midi choice " + LTRIM$(STR$(i)) + GOTO SoundOut + END IF + NEXT i + + ' Settings into a handle of their own (DVX.INI is not touched) + saved = PrefsCreate() + SoundSetEventFile saved, 0, "PARCP.WAV" + SoundSettingsStore saved, SoundDriverAuto$(), 0, TEST_SND_VOLUME + + IF SoundEventFile$(saved, 0) <> "PARCP.WAV" OR SoundSettingVolume(saved) <> TEST_SND_VOLUME OR SoundSettingDriver$(saved) <> SoundDriverAuto$() OR SoundSettingMidi(saved) <> 0 THEN + what = "sound settings" + END IF + + PrefsClose saved + + IF what <> "" THEN + GOTO SoundOut + END IF + + oldVolume = SoundMasterVolume() + SoundSetMasterVolume TEST_VOLUME + + IF ABS(SoundMasterVolume() - TEST_VOLUME) > 1 THEN + what = "master volume" + END IF + + SoundSetMasterVolume oldVolume + + IF what = "" AND (SoundTestFile("NOSUCH.WAV") <> 0 OR SoundError$() = "") THEN + what = "test sound" + END IF + + GOTO SoundOut + +NoSound: + what = NotInstalled("sound") + RESUME SoundOut + +SoundOut: + CheckSound = what +END FUNCTION + + +FUNCTION CheckVideo() AS STRING + DIM oldW AS LONG + DIM oldH AS LONG + DIM oldBpp AS LONG + DIM i AS LONG + DIM other AS LONG + + CheckVideo = "" + oldW = Screen.Width + oldH = Screen.Height + oldBpp = Screen.BitsPerPixel + other = -1 + + ' Another size at the same depth, the quickest change + FOR i = 0 TO SysVideoModeCount() - 1 + IF other < 0 AND SysVideoModeBpp(i) = oldBpp AND SysVideoModeWidth(i) <> oldW THEN + other = i + END IF + NEXT i + + IF other < 0 THEN + CheckVideo = "no other mode" + EXIT FUNCTION + END IF + + IF NOT SysSetVideoMode(SysVideoModeWidth(other), SysVideoModeHeight(other), oldBpp) THEN + CheckVideo = "change mode" + EXIT FUNCTION + END IF + + IF Screen.Width <> SysVideoModeWidth(other) THEN + CheckVideo = "mode not changed" + EXIT FUNCTION + END IF + + IF NOT SysSetVideoMode(oldW, oldH, oldBpp) OR Screen.Width <> oldW OR Screen.Height <> oldH THEN + CheckVideo = "mode back" + END IF +END FUNCTION + + +FUNCTION CheckWallpaper() AS STRING + DIM folder AS STRING + DIM list AS LONG + DIM image AS STRING + DIM oldPath AS STRING + DIM oldMode AS LONG + DIM newMode AS LONG + + CheckWallpaper = "" + oldPath = SysWallpaper$() + oldMode = SysWallpaperMode() + folder = FileOpsPathJoin$("CONFIG", "WPAPER") + list = FileOpsList(folder, "*.bmp;*.jpg;*.png;*.gif") + + IF FileOpsListCount(list) = 0 THEN + CheckWallpaper = "no wallpaper image" + FileOpsListFree list + EXIT FUNCTION + END IF + + image = FileOpsPathJoin$(folder, FileOpsListName$(list, 0)) + FileOpsListFree list + + newMode = (oldMode + 1) MOD PrefsChoiceCount(PREFS_CHOICE_WALLPAPER) + SysSetWallpaperMode newMode + + IF SysWallpaperMode() <> newMode THEN + CheckWallpaper = "wallpaper mode" + EXIT FUNCTION + END IF + + IF NOT SysSetWallpaper(image) OR SysWallpaper$() <> image THEN + CheckWallpaper = "set wallpaper" + EXIT FUNCTION + END IF + + IF NOT SysSetWallpaper("") OR SysWallpaper$() <> "" THEN + CheckWallpaper = "clear wallpaper" + EXIT FUNCTION + END IF + + IF NOT SysWallpaperLoad(snap) OR SysWallpaper$() <> oldPath OR SysWallpaperMode() <> oldMode THEN + CheckWallpaper = "restore wallpaper" + END IF +END FUNCTION + + +' In an error handler: "" when the error was a library that is not +' installed (noted in the title), else what failed. +FUNCTION NotInstalled(BYVAL part AS STRING) AS STRING + IF ERR = ERR_NOT_INSTALLED THEN + missing = missing + " (no " + part + ")" + NotInstalled = "" + ELSE + NotInstalled = part + " error " + LTRIM$(STR$(ERR)) + END IF +END FUNCTION + + +SUB Report(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) + IF ok THEN + ParCp.Caption = "PARCP PASS" + what + ELSE + ParCp.Caption = "PARCP FAIL " + what + END IF + + lblResult.Caption = what +END SUB + + +SUB ParCp_Load + DIM failed AS STRING + + missing = "" + snap = PrefsCreate() + SysColorsStore snap + SysMouseStore snap + SysWallpaperStore snap + + IF PrefsDvxIni$() = "" THEN + failed = "settings file" + END IF + + IF failed = "" THEN + failed = CheckVideo() + END IF + + IF failed = "" THEN + failed = CheckColors() + END IF + + IF failed = "" THEN + failed = CheckWallpaper() + END IF + + IF failed = "" THEN + failed = CheckMouse() + END IF + + IF failed = "" THEN + failed = CheckFileTypes() + END IF + + IF failed = "" THEN + failed = CheckSizes() + END IF + + IF failed = "" THEN + failed = CheckSound() + END IF + + IF failed = "" THEN + failed = CheckJoystick() + END IF + + IF failed = "" THEN + failed = CheckNet() + END IF + + IF failed = "" THEN + failed = CheckPrint() + END IF + + ' Put everything back even when a step failed + SysColorsLoad snap + SysMouseLoad snap + SysWallpaperLoad snap + PrefsClose snap + + IF failed = "" THEN + Report True, missing + ELSE + Report False, failed + END IF +END SUB diff --git a/src/test/dos/fixtures/pardemo/ICON32.BMP b/src/test/dos/fixtures/pardemo/ICON32.BMP new file mode 100644 index 00000000..9d09bda9 --- /dev/null +++ b/src/test/dos/fixtures/pardemo/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2e69cba0284673922b99674345bf9534ad2b2d31655be7631a6db8b596d43f1 +size 3126 diff --git a/src/test/dos/fixtures/pardemo/logo.bmp b/src/test/dos/fixtures/pardemo/logo.bmp new file mode 100644 index 00000000..4633ea9d --- /dev/null +++ b/src/test/dos/fixtures/pardemo/logo.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d98dd5f0f63db53421f60bc35813df103572f7e9622671934f4a7b4ab3c9154 +size 3126 diff --git a/src/test/dos/fixtures/pardemo/new.bmp b/src/test/dos/fixtures/pardemo/new.bmp new file mode 100644 index 00000000..50e8fef0 --- /dev/null +++ b/src/test/dos/fixtures/pardemo/new.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c602b729d75f14f36bccd84b27810bc7e04f4b8566d5311eee029ed76ed971e +size 822 diff --git a/src/test/dos/fixtures/pardemo/open.bmp b/src/test/dos/fixtures/pardemo/open.bmp new file mode 100644 index 00000000..6d2f732a --- /dev/null +++ b/src/test/dos/fixtures/pardemo/open.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2791595909626da80d820eadc1334e0dfb356194f58f2cce42919d7d5c9e1018 +size 822 diff --git a/src/test/dos/fixtures/pardemo/parcopy.frm b/src/test/dos/fixtures/pardemo/parcopy.frm new file mode 100644 index 00000000..3d81018a --- /dev/null +++ b/src/test/dos/fixtures/pardemo/parcopy.frm @@ -0,0 +1,70 @@ +VERSION DVX 1.00 + +Begin Form ParCopy + Caption = "ParCopy" + Layout = VBox + AutoSize = False + Resizable = True + Centered = False + Width = 200 + Height = 100 + + Begin Label lblN + Caption = "Copy" + End + Begin CommandButton cmdClicks + Caption = "&Click me" + End +End + +' 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. + + +' parcopy -- a window of the pardemo fixture that is open twice: each copy +' (the form itself and one made with New) has its own controls and its own +' form-level variable, as DVX Demo opens a window again from its menu. + +OPTION EXPLICIT + +CONST COPY_LEFT = 300 +CONST COPY_TOP = 40 +CONST COPY_STEP = 30 + +DIM SHARED copyNo AS INTEGER +DIM SHARED clicks AS INTEGER + + +SUB ParCopy_Load + copiesMade = copiesMade + 1 + copyNo = copiesMade + lblN.Caption = "Copy " & copyNo + Me.Caption = "ParCopy " & copyNo + Me.Left = COPY_LEFT + copyNo * COPY_STEP + Me.Top = COPY_TOP + copyNo * COPY_STEP +END SUB + + +SUB cmdClicks_Click + clicks = clicks + 1 + lblN.Caption = "Copy " & copyNo & ": " & clicks & " clicks" +END SUB diff --git a/src/test/dos/fixtures/pardemo/pardemo.bas b/src/test/dos/fixtures/pardemo/pardemo.bas new file mode 100644 index 00000000..e1f37366 --- /dev/null +++ b/src/test/dos/fixtures/pardemo/pardemo.bas @@ -0,0 +1,29 @@ +' 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. + + +' pardemo.bas -- what the pardemo fixture's forms share: the number of +' ParCopy windows made so far (each copy numbers itself from it). + +OPTION EXPLICIT + +DIM SHARED copiesMade AS INTEGER diff --git a/src/test/dos/fixtures/pardemo/pardemo.dbp b/src/test/dos/fixtures/pardemo/pardemo.dbp new file mode 100644 index 00000000..99a8b99c --- /dev/null +++ b/src/test/dos/fixtures/pardemo/pardemo.dbp @@ -0,0 +1,22 @@ +[Project] +Name = PARDEMO +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: what DVX Demo shows, written in BASIC +Icon = ICON32.BMP + +[Modules] +File0 = pardemo.bas +File1 = ../../../../include/basic/system.bas +Count = 2 + +[Forms] +File0 = pardemo.frm +File1 = parterm.frm +File2 = parcopy.frm +Count = 3 + +[Settings] +StartupForm = ParDemo diff --git a/src/test/dos/fixtures/pardemo/pardemo.frm b/src/test/dos/fixtures/pardemo/pardemo.frm new file mode 100644 index 00000000..db8adf0d --- /dev/null +++ b/src/test/dos/fixtures/pardemo/pardemo.frm @@ -0,0 +1,698 @@ +VERSION DVX 1.00 + +Begin Form ParDemo + Caption = "PARDEMO RUN" + Layout = VBox + AutoSize = False + Resizable = True + Centered = False + Left = 10 + Top = 10 + Width = 620 + Height = 440 + ContextMenu = "mnuCtx" + + Begin Menu mnuFile + Caption = "&File" + Begin Menu mnuNew + Caption = "&New" + Shortcut = "Ctrl+N" + End + Begin Menu mnuOpen + Caption = "&Open..." + Shortcut = "Ctrl+O" + End + Begin Menu mnuSave + Caption = "&Save" + Shortcut = "Ctrl+S" + End + Begin Menu mnuFileSep + Caption = "-" + End + Begin Menu mnuExit + Caption = "E&xit" + Shortcut = "Ctrl+Q" + End + End + Begin Menu mnuView + Caption = "&View" + Begin Menu mnuToolbar + Caption = "&Toolbar" + Checked = True + End + Begin Menu mnuStatus + Caption = "&Status Bar" + Checked = True + End + Begin Menu mnuViewSep1 + Caption = "-" + End + Begin Menu mnuSmall + Caption = "S&mall" + RadioCheck = True + End + Begin Menu mnuMedium + Caption = "Me&dium" + RadioCheck = True + Checked = True + End + Begin Menu mnuLarge + Caption = "&Large" + RadioCheck = True + End + Begin Menu mnuViewSep2 + Caption = "-" + End + Begin Menu mnuZoom + Caption = "&Zoom" + Begin Menu mnuZoomIn + Caption = "Zoom &In" + End + Begin Menu mnuZoomOut + Caption = "Zoom &Out" + End + End + Begin Menu mnuViewSep3 + Caption = "-" + End + Begin Menu mnuDebug + Caption = "&Debug Layout" + Checked = False + End + End + Begin Menu mnuWindow + Caption = "&Window" + Begin Menu mnuCascade + Caption = "&Cascade" + End + Begin Menu mnuTileH + Caption = "Tile &Horizontal" + End + Begin Menu mnuTileV + Caption = "Tile &Vertical" + End + Begin Menu mnuTileGrid + Caption = "Tile &Grid" + End + Begin Menu mnuWindowSep + Caption = "-" + End + Begin Menu mnuNewCopy + Caption = "New &Copy" + End + End + Begin Menu mnuHelp + Caption = "&Help" + Begin Menu mnuAbout + Caption = "&About PARDEMO..." + End + End + Begin Menu mnuCtx + Caption = "Context" + Visible = False + Begin Menu mnuCtxCut + Caption = "Cu&t" + End + Begin Menu mnuCtxCopy + Caption = "&Copy" + End + Begin Menu mnuCtxPaste + Caption = "&Paste" + End + End + Begin Menu mnuLbCtx + Caption = "List" + Visible = False + Begin Menu mnuLbUp + Caption = "Move &Up" + End + Begin Menu mnuLbDown + Caption = "Move &Down" + End + End + + Begin Toolbar tbMain + Weight = 0 + End + + Begin TabStrip tabs + Weight = 1 + + Begin TabPage pgInput + Caption = "&Input" + + Begin Frame fraInput + Caption = "&User Input" + Begin HBox rowName + Begin Label lblName + Caption = "&Name:" + End + Begin TextBox txtName + MaxLength = 64 + Weight = 1 + End + End + Begin HBox rowPass + Begin Label lblPass + Caption = "&Password:" + End + Begin TextBox txtPass + MaxLength = 32 + PasswordChar = "*" + Weight = 1 + End + End + Begin HBox rowPhone + Begin Label lblPhone + Caption = "P&hone:" + End + Begin TextBox txtPhone + Mask = "(###) ###-####" + Weight = 1 + End + End + End + Begin HBox rowPicks + Begin CheckBox chkA + Caption = "&Check A" + Value = True + End + Begin CheckBox chkB + Caption = "Check &B" + End + Begin VBox grpRadio + Begin OptionButton optOne + Caption = "Option &1" + End + Begin OptionButton optTwo + Caption = "Option &2" + End + Begin OptionButton optThree + Caption = "Option &3" + End + End + Begin DropDown ddColor + ToolTipText = "A drop-down list" + End + Begin ComboBox cboSize + MaxLength = 32 + End + Begin SpinButton spnQty + Weight = 0 + End + End + Begin HBox rowButtons + Alignment = Right + Begin CommandButton cmdOK + Caption = "&OK" + End + Begin CommandButton cmdCancel + Caption = "&Cancel" + End + End + End + + Begin TabPage pgRange + Caption = "&Range" + + Begin HBox rowRange + Begin ProgressBar prgH + Weight = 1 + End + Begin ProgressBar prgV + Vertical = True + MinHeight = 80 + End + End + Begin HScrollBar sldVal + Min = 0 + Max = 100 + End + Begin Line linRange + End + End + + Begin TabPage pgLists + Caption = "&Lists" + + Begin HBox rowTrees + Weight = 1 + Begin TreeView tvTree + Weight = 1 + End + Begin ListView lvFiles + Weight = 2 + End + End + Begin HBox rowLists + Weight = 1 + Begin ListBox lstFruit + Weight = 1 + ContextMenu = "mnuLbCtx" + End + Begin ListBox lstMulti + Weight = 1 + End + End + End + + Begin TabPage pgScroll + Caption = "&Scroll" + + Begin ScrollPane spMain + Weight = 1 + Padding = 4 + Spacing = 4 + Begin CheckBox chkS1 + Caption = "Scroll item 1" + End + Begin CheckBox chkS2 + Caption = "Scroll item 2" + End + Begin CheckBox chkS3 + Caption = "Scroll item 3" + End + Begin CheckBox chkS4 + Caption = "Scroll item 4" + End + Begin CheckBox chkS5 + Caption = "Scroll item 5" + End + Begin CheckBox chkS6 + Caption = "Scroll item 6" + End + Begin CheckBox chkS7 + Caption = "Scroll item 7" + End + Begin CheckBox chkS8 + Caption = "Scroll item 8" + End + End + End + + Begin TabPage pgMedia + Caption = "&Media" + + Begin HBox rowMedia + Weight = 1 + Begin Image imgLogo + End + Begin TextArea taNotes + Weight = 1 + End + End + End + + Begin TabPage pgSplit + Caption = "S&plit" + + Begin Splitter splTop + Weight = 1 + Vertical = False + Position = 80 + Begin Splitter splLeft + Position = 120 + Begin TreeView tvLeft + End + Begin ListBox lstRight + End + End + Begin Frame fraPreview + Caption = "Preview" + Begin Label lblPreview + Caption = "Top and bottom, left and right." + End + End + End + End + End + + Begin PictureBox picDraw + Width = 600 + Height = 50 + Weight = 0 + End + + Begin StatusBar sbMain + Weight = 0 + Begin Label lblStatus + Caption = "Ready." + Weight = 1 + End + Begin Label lblPos + Caption = "Line 1, Col 1" + End + End + + Begin Timer tmrCheck + Interval = 200 + Enabled = True + End +End + +' 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. + + +' pardemo -- DOS harness fixture: what DVX Demo (dvxdemo.c) shows, written +' in BASIC. It +' - has menus with check, radio and unchecked check items, a submenu, +' shortcut keys and context menus (form and list), and adds a recent +' file item to the File menu at run time (Visible, RemoveMenuItem), +' - fills every control the demo uses: text boxes (MaxLength, password, +' mask), check boxes, option buttons, drop-down, combo box (MaxLength), +' spin button, list boxes, a horizontal and a vertical progress bar, a +' slider, tree view, list view, a scroll pane with padding, an image, a +' text area, a top/bottom splitter holding a left/right one, a toolbar +' of picture buttons (one shows this program's own icon), a status bar +' with panes and, in ParTerm, a terminal written with ANSI text, +' - draws on a PictureBox, and the mouse draws on it too, +' - minimizes the terminal window with WindowState, opens a second copy +' of the ParCopy window (New), arranges the windows (Screen.Arrange) +' and switches the debug layout borders on and off. +' Its pictures are in the pardemo folder beside the program (App.Path). +' The window title reports the outcome for the script: "PARDEMO WAIT +' input" once the checks ran, until a mouse stroke on the PictureBox and +' Ctrl+N (the New item's shortcut) arrive; then "PARDEMO PASS checks" +' when everything worked, else "PARDEMO FAIL ". + +OPTION EXPLICIT + +CONST TAG = "PARDEMO " +CONST WAIT_TICKS = 150 +CONST FORM_LEFT = 10 +CONST FORM_TOP = 10 +CONST FORM_W = 620 +CONST FORM_H = 440 +CONST BTN_LEFT = 1 +CONST APP_FILE = "/pardemo.app" +CONST DATA_DIR = "/pardemo/" +CONST MIN_BUTTONS = 5 +CONST GLYPH_ROW = 6 + +DIM SHARED failWhat AS STRING +DIM SHARED checks AS INTEGER +DIM SHARED phase AS INTEGER +DIM SHARED ticks AS INTEGER +DIM SHARED strokes AS INTEGER +DIM SHARED newPressed AS INTEGER +DIM SHARED lastX AS INTEGER +DIM SHARED lastY AS INTEGER +DIM SHARED copyB AS ParCopy + + +SUB Check(ok AS INTEGER, what AS STRING) + checks = checks + 1 + + IF NOT ok AND failWhat = "" THEN + failWhat = what + END IF +END SUB + + +SUB Finish + tmrCheck.Enabled = False + + IF failWhat = "" THEN + ParDemo.Caption = TAG & "PASS " & checks & " checks" + ELSE + ParDemo.Caption = TAG & "FAIL " & failWhat + END IF +END SUB + + +SUB FillControls + ddColor.AddItem "Red" + ddColor.AddItem "Green" + ddColor.AddItem "Blue" + ddColor.ListIndex = 0 + + cboSize.AddItem "Small" + cboSize.AddItem "Medium" + cboSize.AddItem "Large" + cboSize.ListIndex = 1 + + spnQty.SetRange 0, 999 + spnQty.SetStep 1 + spnQty.Value = 42 + + lstFruit.SetItems "Apple|Banana|Cherry|Date" + lstFruit.SetReorderable True + lstMulti.SetItems "One|Two|Three|Four" + lstMulti.SetMultiSelect True + + prgH.Value = 65 + prgV.Value = 75 + sldVal.Value = 30 + + tvTree.AddItem "Animals" + tvTree.AddChildItem 0, "Cat" + tvTree.AddChildItem 0, "Dog" + tvTree.AddItem "Plants" + tvTree.AddChildItem 3, "Oak" + tvTree.AddChildItem 3, "Pine" + tvTree.SetExpanded 0, True + tvTree.SetExpanded 3, True + tvTree.SetReorderable True + + lvFiles.SetColumns "Name,16|Size,8,Right|Type,12|Modified,12" + lvFiles.SetRows "readme.txt" & CHR$(9) & "1024" & CHR$(9) & "Text" & CHR$(9) & "2026-01-15" & CHR$(10) & "program.exe" & CHR$(9) & "65536" & CHR$(9) & "Program" & CHR$(9) & "2026-02-01" & CHR$(10) & "data.dat" & CHR$(9) & "8192" & CHR$(9) & "Binary" & CHR$(9) & "2026-03-10" & CHR$(10) & "logo.bmp" & CHR$(9) & "3126" & CHR$(9) & "Picture" & CHR$(9) & "2026-04-22" + lvFiles.ListIndex = 0 + lvFiles.SetMultiSelect True + lvFiles.SetReorderable True + + tvLeft.AddItem "Desktop" + tvLeft.AddChildItem 0, "Documents" + tvLeft.AddChildItem 0, "Pictures" + tvLeft.SetExpanded 0, True + tvLeft.SetMultiSelect True + lstRight.SetItems "file1.txt|file2.txt|file3.txt" + + taNotes.Text = "A TextArea holds several lines." & CHR$(10) & "Type here." + + tbMain.AddButton "tbNew", App.Path & DATA_DIR & "new.bmp" + tbMain.AddButton "tbOpen", App.Path & DATA_DIR & "open.bmp" + tbMain.AddButton "tbSave", App.Path & DATA_DIR & "save.bmp" + tbMain.AddSeparator + tbMain.AddButton "tbApp", App.Path & APP_FILE + tbMain.AddTextButton "tbHelp", "&Help" + tbNew.ToolTipText = "New" + tbOpen.ToolTipText = "Open" + tbSave.ToolTipText = "Save" + tbApp.ToolTipText = "This program's own icon" + + imgLogo.Picture = App.Path & DATA_DIR & "logo.bmp" + + picDraw.Line (5, 5)-(54, 39), vbRed, BF + picDraw.FillStyle = 0 + picDraw.FillColor = vbGreen + picDraw.Circle (150, 35), 25, vbGreen + picDraw.Line (200, 60)-(400, 10), vbBlue +END SUB + + +SUB RunChecks + DIM p AS OBJECT + + Check prgV.Vertical AND NOT prgH.Vertical, "progress vertical" + Check NOT splTop.Vertical AND splLeft.Vertical, "splitter orientation" + Check spMain.Padding = 4 AND spMain.Spacing = 4, "scroll pane padding" + Check cboSize.MaxLength = 32 AND txtName.MaxLength = 64, "max length" + Check txtPhone.Mask = "(###) ###-####", "mask" + Check ParTerm.WindowState = vbMinimized, "terminal minimized" + Check mnuToolbar.Checked AND mnuMedium.Checked AND NOT mnuDebug.Checked, "menu checks" + Check ParDemo.ContextMenu = "mnuCtx", "context menu" + + Screen.DebugLayout = True + Check Screen.DebugLayout, "debug layout" + Screen.DebugLayout = False + Check Screen.BitsPerPixel >= 8, "bits per pixel" + + ' A recent file at the end of the File menu; a second one comes and + ' goes again + ParDemo.AddMenuSeparator "mnuFile", "mnuRecentSep" + ParDemo.AddMenuItem "mnuFile", "mnuRecent1", "&1 PARDEMO.TXT" + ParDemo.AddMenuItem "mnuFile", "mnuRecent2", "&2 OTHER.TXT" + mnuRecent1.Visible = False + Check NOT mnuRecent1.Visible, "menu visible" + mnuRecent1.Visible = True + ParDemo.RemoveMenuItem "mnuRecent2" + Check mnuRecent1.Visible AND mnuRecent1.Caption = "&1 PARDEMO.TXT", "recent files" + + Check copyB.lblN.Caption <> ParCopy.lblN.Caption, "form copies" + + SET p = LOADPICTURE(App.Path & APP_FILE) + Check p.Width > 0, "program icon" + Check imgLogo.ImageWidth > 0, "image file" + Check tbMain.ButtonCount() >= MIN_BUTTONS, "toolbar" + Check picDraw.Point(10, 10) <> picDraw.Point(590, 65), "picture drawing" + Check lvFiles.RowCount() = 4, "list view" + + ' The system font's glyphs, as the C demo draws text by hand: "A" has + ' pixels in its middle rows, a blank has none + Check SysFontWidth() > 0 AND SysFontHeight() > GLYPH_ROW AND SysFontCharCount() > ASC("A") - SysFontFirstChar(), "font metrics" + Check SysFontGlyphRow(ASC("A"), GLYPH_ROW) <> 0 AND SysFontGlyphRow(ASC(" "), GLYPH_ROW) = 0, "font glyphs" + + Screen.Arrange vbTileVertical + Check ParDemo.Top = 0, "tile vertical" + Screen.Arrange vbTileHorizontal + Check ParDemo.Left = 0, "tile horizontal" + Screen.Arrange vbTileGrid + Screen.Arrange vbCascade + + ' Back in place for the mouse + ParDemo.Left = FORM_LEFT + ParDemo.Top = FORM_TOP + ParDemo.Width = FORM_W + ParDemo.Height = FORM_H + ParDemo.Show +END SUB + + +SUB ParDemo_Load + FillControls + + ParTerm.Show + ParTerm.WindowState = vbMinimized + + ParCopy.Show + SET copyB = NEW ParCopy + copyB.Show +END SUB + + +SUB ParDemo_Unload + Unload ParTerm + Unload copyB + Unload ParCopy +END SUB + + +SUB tmrCheck_Timer + IF phase = 0 THEN + RunChecks + phase = 1 + ParDemo.Caption = TAG & "WAIT input" + EXIT SUB + END IF + + ticks = ticks + 1 + + IF strokes > 0 AND newPressed THEN + Finish + ELSEIF ticks > WAIT_TICKS THEN + IF strokes = 0 THEN + Check False, "no mouse drawing" + ELSE + Check False, "no Ctrl+N" + END IF + + Finish + END IF +END SUB + + +SUB picDraw_MouseDown(Button AS INTEGER, X AS INTEGER, Y AS INTEGER) + lastX = X + lastY = Y +END SUB + + +SUB picDraw_MouseMove(Button AS INTEGER, X AS INTEGER, Y AS INTEGER) + IF Button = BTN_LEFT THEN + picDraw.Line (lastX, lastY)-(X, Y), vbBlack + lastX = X + lastY = Y + strokes = strokes + 1 + END IF + + lblPos.Caption = "X " & X & ", Y " & Y +END SUB + + +SUB mnuNew_Click + newPressed = True + lblStatus.Caption = "New" +END SUB + + +SUB mnuOpen_Click + lblStatus.Caption = "Open" +END SUB + + +SUB mnuSave_Click + lblStatus.Caption = "Save" +END SUB + + +SUB mnuExit_Click + Unload ParDemo +END SUB + + +SUB mnuDebug_Click + Screen.DebugLayout = mnuDebug.Checked +END SUB + + +SUB mnuCascade_Click + Screen.Arrange vbCascade +END SUB + + +SUB mnuTileH_Click + Screen.Arrange vbTileHorizontal +END SUB + + +SUB mnuTileV_Click + Screen.Arrange vbTileVertical +END SUB + + +SUB mnuTileGrid_Click + Screen.Arrange vbTileGrid +END SUB + + +SUB mnuNewCopy_Click + DIM c AS ParCopy + + SET c = NEW ParCopy + c.Show +END SUB + + +SUB mnuAbout_Click + MsgBox "PARDEMO" & CHR$(10) & CHR$(10) & "What DVX Demo shows, in BASIC.", vbInformation, "About PARDEMO" +END SUB + + +SUB cmdOK_Click + lblStatus.Caption = "Button clicked!" +END SUB + + +SUB cmdCancel_Click + lblStatus.Caption = "Cancelled." +END SUB diff --git a/src/test/dos/fixtures/pardemo/parterm.frm b/src/test/dos/fixtures/pardemo/parterm.frm new file mode 100644 index 00000000..f0a3b518 --- /dev/null +++ b/src/test/dos/fixtures/pardemo/parterm.frm @@ -0,0 +1,88 @@ +VERSION DVX 1.00 + +Begin Form ParTerm + Caption = "ANSI Terminal" + Layout = VBox + AutoSize = True + Resizable = True + Centered = False + Left = 60 + Top = 60 + + Begin Terminal term1 + Cols = 80 + Rows = 25 + Scrollback = 500 + End + + Begin StatusBar sbTerm + Begin Label lblTerm + Caption = "80x25 [Local]" + Weight = 1 + End + End +End + +' 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. + + +' parterm -- the ANSI terminal window of the pardemo fixture: colours, +' bold, reverse, line-drawing and block characters written with escape +' sequences, as DVX Demo's terminal window shows them. The main form +' minimizes it with WindowState. + +OPTION EXPLICIT + + +FUNCTION Csi(code AS STRING) AS STRING + Csi = CHR$(27) & "[" & code +END FUNCTION + + +SUB ParTerm_Load + DIM crlf AS STRING + DIM i AS INTEGER + DIM shades AS STRING + + crlf = CHR$(13) & CHR$(10) + term1.Write Csi("2J") & Csi("1;1H") & Csi("1;37;44m") & " DVX BASIC ANSI Terminal " & Csi("0m") & crlf & crlf + + FOR i = 30 TO 37 + term1.Write Csi(i & "m") & "Colour " & i & " " + NEXT + + term1.Write Csi("0m") & crlf + term1.Write Csi("1m") & "Bold " & Csi("0m") & Csi("7m") & " Reverse " & Csi("0m") & Csi("4m") & " Underline" & Csi("0m") & crlf & crlf + term1.Write CHR$(&HDA) & STRING$(20, CHR$(&HC4)) & CHR$(&HBF) & crlf + term1.Write CHR$(&HB3) & " Box drawing " & CHR$(&HB3) & crlf + term1.Write CHR$(&HC0) & STRING$(20, CHR$(&HC4)) & CHR$(&HD9) & crlf & crlf + + shades = "" + + FOR i = &HB0 TO &HB2 + shades = shades & STRING$(4, CHR$(i)) + NEXT + + term1.Write shades & STRING$(4, CHR$(&HDB)) & " Shades" & crlf + term1.Write CHR$(1) & " " & CHR$(2) & " " & CHR$(3) & " " & CHR$(4) & " " & CHR$(5) & " " & CHR$(6) & " Symbols" & crlf +END SUB diff --git a/src/test/dos/fixtures/pardemo/save.bmp b/src/test/dos/fixtures/pardemo/save.bmp new file mode 100644 index 00000000..d033c2c2 --- /dev/null +++ b/src/test/dos/fixtures/pardemo/save.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd8eb75b0e7dea67eafa87fde8dd2106b603986c5cc6ae6be511ae8f27958fc8 +size 822 diff --git a/src/test/dos/fixtures/pardsgn/pardsgn.dbp b/src/test/dos/fixtures/pardsgn/pardsgn.dbp new file mode 100644 index 00000000..b9d23560 --- /dev/null +++ b/src/test/dos/fixtures/pardsgn/pardsgn.dbp @@ -0,0 +1,19 @@ +[Project] +Name = PARDSGN +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: what a form designer and code editor written in BASIC need (designer.bas, the design surface, TextArea editing) + +[Modules] +File0 = ../../../../include/basic/designer.bas +Count = 1 + +[Forms] +File0 = pardsgn.frm +Count = 1 + +[Settings] +StartupForm = ParDsgn +OptionExplicit = True diff --git a/src/test/dos/fixtures/pardsgn/pardsgn.frm b/src/test/dos/fixtures/pardsgn/pardsgn.frm new file mode 100644 index 00000000..a58a3165 --- /dev/null +++ b/src/test/dos/fixtures/pardsgn/pardsgn.frm @@ -0,0 +1,386 @@ +VERSION DVX 1.00 + +Begin Form ParDsgn + Caption = "PARDSGN RUN" + Layout = VBox + AutoSize = False + Resizable = False + Centered = True + Width = 400 + Height = 300 + + Begin TextArea Code + Height = 60 + MaxHeight = 80 + End + + Begin Frame Surface + Caption = "Surface" + Weight = 1 + End + + Begin Timer tmrStart + Interval = 200 + Enabled = True + End +End + +' 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. + +' pardsgn -- DOS harness fixture: what a DVX BASIC form designer and code +' editor written in BASIC do. It lists the control types and their +' properties (designer.bas), creates a control of every designable type +' at run time and reads and sets properties by name (CallByName), moves +' controls between containers (Container, MoveBefore), builds a form +' with the .frm model (designer.bas Frm*: add, copy and paste a control, +' write the text), loads it (LoadFormText), writes the live form back +' (FormText$), uses the editor members of a TextArea (line +' markers, selection, clipboard commands, syntax colours, lines) and puts +' the form in design mode. The script then clicks the middle of the form +' (DesignMouse names the Surface frame there, which the program selects +' so its handles are drawn) and presses a key (DesignKey). The window +' title reports the outcome: "PARDSGN PASS types" or "PARDSGN +' FAIL ". + +OPTION EXPLICIT + +CONST TAB_CHAR = 9 +CONST LF_CHAR = 10 +CONST QUOTE_CHAR = 34 +CONST KEY_SEEN = 97 ' the script presses "a" + +DIM SHARED made AS INTEGER +DIM SHARED failed AS BOOLEAN +DIM SHARED clicked AS BOOLEAN + + +SUB Report(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) + IF failed THEN + EXIT SUB + END IF + + failed = NOT ok + + IF ok THEN + ParDsgn.Caption = "PARDSGN PASS " + what + ELSE + ParDsgn.Caption = "PARDSGN FAIL " + what + END IF +END SUB + + +' The type registry: the TextArea is there with its editor members and +' the GutterClick event, Frame is a container, the form has Controls. +FUNCTION CheckTypes() AS BOOLEAN + DIM i AS INTEGER + DIM found AS BOOLEAN + + CheckTypes = False + + FOR i = 0 TO CtlTypeCount() - 1 + IF CtlTypeName$(i) = "TextArea" THEN + found = True + END IF + NEXT + + IF NOT found THEN + Report False, "no TextArea type" + ELSEIF (CtlTypeFlags("Frame") AND CTL_FLAG_CONTAINER) = 0 THEN + Report False, "Frame flags" + ELSEIF PropIndex("TextArea", "SelText") < 0 OR PropIndex("Form", "DesignMode") < 0 THEN + Report False, "property list" + ELSEIF INSTR(CtlEventList$("TextArea"), "GutterClick") = 0 THEN + Report False, "event list" + ELSEIF CtlEventParams$("MouseDown") <> "Button As Integer, X As Integer, Y As Integer" THEN + Report False, "event params " + CtlEventParams$("MouseDown") + ELSEIF CtlTypeDefaultEvent$("CommandButton") <> "Click" THEN + Report False, "default event" + ELSE + CheckTypes = True + END IF +END FUNCTION + + +' The index of property p of type t, -1 when it has none. +FUNCTION PropIndex(BYVAL t AS STRING, BYVAL p AS STRING) AS INTEGER + DIM i AS INTEGER + + PropIndex = -1 + + FOR i = 0 TO CtlPropCount(t) - 1 + IF CtlPropName$(t, i) = p THEN + PropIndex = i + END IF + NEXT +END FUNCTION + + +' A control of every designable type in the Surface frame, every readable +' property read by name, Caption / Text set by name where the type has a +' writable one; all removed again. +FUNCTION CheckCreate() AS BOOLEAN + DIM i AS INTEGER + DIM j AS INTEGER + DIM t AS STRING + DIM nm AS STRING + DIM c AS Object + DIM v AS Variant + DIM pt AS LONG + + CheckCreate = False + ON ERROR GOTO Failed + + FOR i = 0 TO CtlTypeCount() - 1 + t = CtlTypeName$(i) + + IF (CtlTypeFlags(t) AND CTL_FLAG_DESIGNABLE) <> 0 THEN + nm = CtlTypeNamePrefix$(t) + LTRIM$(STR$(i)) + SET c = CreateControl(Me, t, nm, Surface) + + FOR j = 0 TO CtlPropCount(t) - 1 + pt = CtlPropType(t, j) + + IF (pt AND CTL_PROP_WRITEONLY) = 0 THEN + v = Empty + + IF (pt AND CTL_PROP_TYPEMASK) <> CTL_PROP_OBJECT THEN + v = CallByName(c, CtlPropName$(t, j), vbGet) + END IF + END IF + NEXT + + IF PropIndex(t, "Caption") >= 0 AND (CtlPropType(t, PropIndex(t, "Caption")) AND CTL_PROP_READONLY) = 0 THEN + CallByName c, "Caption", vbLet, nm + END IF + + IF CallByName(c, "Name", vbGet) <> nm THEN + Report False, "name of " + nm + EXIT FUNCTION + END IF + + IF c.Container.Name <> "Surface" OR c.Parent.Name <> "ParDsgn" THEN + Report False, "container of " + nm + EXIT FUNCTION + END IF + + made = made + 1 + RemoveControl Me, nm + END IF + NEXT + + CheckCreate = made > 0 + EXIT FUNCTION + +Failed: + Report False, "create " + nm + " " + t + "." + CtlPropName$(t, j) + ": " + Err.Description + EXIT FUNCTION +END FUNCTION + + +' Controls, Container and MoveBefore: two buttons made in the Surface, +' the second moved before the first, then both out to the form. +FUNCTION CheckTree() AS BOOLEAN + DIM a AS Object + DIM b AS Object + DIM c AS Object + DIM order AS STRING + + CheckTree = False + SET a = CreateControl(Me, "CommandButton", "BtnA", Surface) + SET b = CreateControl(Me, "CommandButton", "BtnB", Surface) + b.MoveBefore a + + FOR EACH c IN ParDsgn.Controls + IF c.Container.Name = "Surface" THEN + order = order + c.Name + " " + END IF + NEXT + + IF order <> "BtnB BtnA " THEN + Report False, "order " + order + EXIT FUNCTION + END IF + + SET b.Container = Me + + IF b.Container.Name <> "ParDsgn" OR ParDsgn.Controls("BtnA").Container.Name <> "Surface" THEN + Report False, "container move" + EXIT FUNCTION + END IF + + RemoveControl Me, "BtnA" + RemoveControl Me, "BtnB" + CheckTree = True +END FUNCTION + + +' A form built with the .frm model (designer.bas): two controls, the +' label copied and pasted as text (it gets a free name) and removed +' again, the text loaded as a form; then that live form written back +' as .frm text (FormText$) and read with the model. +FUNCTION CheckFormText() AS BOOLEAN + DIM f AS Object + DIM h AS LONG + DIM lbl AS LONG + DIM txt AS LONG + DIM pasted AS LONG + DIM t AS STRING + + CheckFormText = False + h = FrmNew("Gen1") + lbl = FrmCtlAdd(h, "Label", "LblA", -1) + txt = FrmCtlAdd(h, "TextBox", "TxtB", -1) + + IF h = 0 OR lbl < 0 OR txt < 0 OR NOT FrmSet(h, "Caption", "Generated") OR NOT FrmCtlSet(h, lbl, "Caption", "A") OR NOT FrmCtlSet(h, txt, "Text", "B") THEN + Report False, "form model" + EXIT FUNCTION + END IF + + pasted = FrmCtlPaste(h, FrmCtlText$(h, lbl), -1) + + IF pasted < 0 OR FrmCtlCount(h) <> 3 OR FrmCtlName$(h, pasted) = "LblA" OR FrmCtlGet$(h, pasted, "Caption") <> "A" THEN + Report False, "model copy / paste" + EXIT FUNCTION + END IF + + FrmCtlRemove h, pasted + t = FrmText$(h) + FrmFree h + + SET f = LoadFormText(t) + + IF f.Caption <> "Generated" OR f.Controls.Count <> 2 THEN + Report False, "form text" + ELSEIF f.Controls(1).Caption <> "A" OR f.Controls("TxtB").Text <> "B" THEN + Report False, "form text controls" + ELSE + h = FrmOpen(FormText$("Gen1")) + + IF h = 0 OR FrmCtlCount(h) <> 2 OR FrmCtlGet$(h, FrmCtlFind(h, "TxtB", -1), "Text") <> "B" OR FrmGet$(h, "Caption") <> "Generated" THEN + Report False, "FormText$" + ELSE + CheckFormText = True + END IF + + FrmFree h + END IF + + UNLOAD f +END FUNCTION + + +' The code editor: BASIC colouring, lines, selection, clipboard commands, +' undo, replace, a breakpoint marker and the current-line marker. +FUNCTION CheckEditor() AS BOOLEAN + DIM nl AS STRING + + nl = CHR$(LF_CHAR) + CheckEditor = False + Code.SetSyntaxMode "bas" + Code.SetShowLineNumbers True + Code.Text = "SUB Main" + nl + " PRINT " + CHR$(QUOTE_CHAR) + "hi" + CHR$(QUOTE_CHAR) + nl + "END SUB" + Code.SetSyntaxColor 1, RGB(0, 0, 192) + Code.SetLineMarker 2, RGB(200, 0, 0), -1 + Code.SetLineMarker 3, -1, RGB(255, 255, 128) + + IF Code.LineCount <> 3 OR Code.GetLine(1) <> "SUB Main" THEN + Report False, "lines" + EXIT FUNCTION + END IF + + Code.SetCursor 2, 5 + Code.SelLength = 5 + + IF Code.SelText <> "PRINT" OR Code.CursorCol <> 10 THEN + Report False, "selection " + Code.SelText + EXIT FUNCTION + END IF + + Code.Cut + Code.Undo + + IF Code.GetLine(2) <> " PRINT " + CHR$(QUOTE_CHAR) + "hi" + CHR$(QUOTE_CHAR) THEN + Report False, "cut / undo " + Code.GetLine(2) + EXIT FUNCTION + END IF + + Code.SetCursor 1, 1 + + IF NOT Code.FindNext("Main", True, True) OR NOT Code.ReplaceSelection("Main", "Start", True, True) OR Code.GetLine(1) <> "SUB Start" THEN + Report False, "replace " + Code.GetLine(1) + EXIT FUNCTION + END IF + + IF Code.GetSyntaxColor(1) <> RGB(0, 0, 192) THEN + Report False, "syntax colour" + EXIT FUNCTION + END IF + + CheckEditor = True +END FUNCTION + + +SUB tmrStart_Timer + tmrStart.Enabled = False + + IF CheckTypes() AND CheckCreate() AND CheckTree() AND CheckFormText() AND CheckEditor() THEN + ParDsgn.DesignMode = True + ParDsgn.DesignSelection = "Code" + + IF NOT ParDsgn.DesignMode OR ParDsgn.DesignSelection <> "Code" THEN + Report False, "design mode" + ELSE + ParDsgn.Caption = "PARDSGN READY" + END IF + END IF +END SUB + + +' The script's click in the middle of the form lands on the Surface. +SUB ParDsgn_DesignMouse(Action AS INTEGER, Button AS INTEGER, X AS INTEGER, Y AS INTEGER, CtrlName AS STRING, Handle AS INTEGER) + IF Action <> DSGN_MOUSE_DOWN OR failed THEN + EXIT SUB + END IF + + IF CtrlName <> "Surface" OR Handle <> DSGN_HANDLE_NONE OR Button <> 1 THEN + Report False, "design click " + CtrlName + STR$(Handle) + ELSE + ParDsgn.DesignSelection = CtrlName + clicked = True + ParDsgn.Caption = "PARDSGN CLICKED" + END IF +END SUB + + +SUB ParDsgn_DesignKey(KeyCode AS INTEGER, Shift AS INTEGER) + IF failed THEN + EXIT SUB + END IF + + IF NOT clicked OR KeyCode <> KEY_SEEN THEN + Report False, "design key" + STR$(KeyCode) + ELSE + ParDsgn.DesignMode = False + Report True, LTRIM$(STR$(made)) + " types" + END IF +END SUB diff --git a/src/test/dos/fixtures/parhlp/parhlp.dbp b/src/test/dos/fixtures/parhlp/parhlp.dbp new file mode 100644 index 00000000..c23a92fa --- /dev/null +++ b/src/test/dos/fixtures/parhlp/parhlp.dbp @@ -0,0 +1,19 @@ +[Project] +Name = PARHLP +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: what a Help Viewer written in BASIC needs + +[Modules] +File0 = ../../../../include/basic/hlpfile.bas +File1 = ../../../../include/basic/help.bas +Count = 2 + +[Forms] +File0 = parhlp.frm +Count = 1 + +[Settings] +StartupForm = ParHlp diff --git a/src/test/dos/fixtures/parhlp/parhlp.dhs b/src/test/dos/fixtures/parhlp/parhlp.dhs new file mode 100644 index 00000000..eb91a606 --- /dev/null +++ b/src/test/dos/fixtures/parhlp/parhlp.dhs @@ -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. + +# parhlp.dhs -- the help file of the PARHLP fixture (parhlp.frm compiles +# it with HelpCompile and reads it with hlpfile.bas). + +.topic ph.home +.title Home +.toc 0 Home +.default +.index home + +.h1 Parity Help + +The start page of the PARHLP fixture. + +.link ph.second Go to the second page + +.topic ph.second +.title Second Page +.toc 1 Second Page +.index second +.index walrus + +The second page mentions an aardvark for the search test. + +.link ph.third Go to the third page + +.section Extras + +.topic ph.third +.title Third Page +.toc 0 Third Page +.index third + +.h2 The Third Page + +.note tip +A tip on the third page. +.endnote + +.link ph.home Back to the start diff --git a/src/test/dos/fixtures/parhlp/parhlp.frm b/src/test/dos/fixtures/parhlp/parhlp.frm new file mode 100644 index 00000000..c3f5a01c --- /dev/null +++ b/src/test/dos/fixtures/parhlp/parhlp.frm @@ -0,0 +1,345 @@ +VERSION DVX 1.00 + +Begin Form ParHlp + Caption = "PARHLP WAIT" + Layout = HBox + AutoSize = False + Resizable = True + Width = 480 + Height = 300 + + Begin TreeView Toc + Weight = 1 + End + + Begin HelpPane Pane + Weight = 3 + End + + Begin PictureBox Pic + Visible = False + End +End + +' 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. + +' parhlp -- DOS harness fixture: what a Help Viewer written in BASIC needs. +' Compiles PARHLP.DHS (HelpCompile), opens it (hlpfile.bas), fills the +' TreeView with the table of contents, shows topics in a HelpPane, follows +' a link (programmatically here; a click on one fires Pane_LinkClick), +' searches, looks up the index, keeps a back / forward history in BASIC +' and takes new launch arguments while it runs (ParHlp_NewArgs). A +' broken help source must fail with the compiler's message +' (HelpCompileMessages$), and DrawFocusRect draws the dotted focus +' rectangle a self-drawn topic puts around its focused link. The +' window title reports the outcome: "PARHLP PASS" or "PARHLP FAIL ". +' +' COMMAND$(1) names the help source (default: PARHLP\PARHLP.DHS beside +' the program); COMMAND$(2) a topic to open at start or on new arguments. + +OPTION EXPLICIT + +CONST HIST_MAX = 64 +CONST TOC_DEPTH_MAX = 16 +CONST SEARCH_WORD = "aardvark" +CONST INDEX_WORD = "walrus" +CONST FOCUS_W = 20 +CONST FOCUS_H = 8 + +DIM SHARED hlp AS LONG +DIM SHARED hist(HIST_MAX - 1) AS LONG +DIM SHARED histPos AS LONG ' position in hist (-1: empty) +DIM SHARED histCount AS LONG +DIM SHARED syncing AS BOOLEAN +DIM SHARED failure AS STRING + + +' Remember the first check that failed. +SUB Check(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) + IF NOT ok AND failure = "" THEN + failure = what + END IF +END SUB + + +' The table of contents as a tree: each node's tag is its topic (-1 for +' a heading that opens nothing). +SUB BuildToc + DIM parents(TOC_DEPTH_MAX - 1) AS LONG + DIM i AS LONG + DIM depth AS LONG + DIM node AS LONG + + Toc.Clear + + FOR i = 0 TO HlpTocCount(hlp) - 1 + depth = HlpTocDepth(hlp, i) + + IF depth >= TOC_DEPTH_MAX THEN + depth = TOC_DEPTH_MAX - 1 + END IF + + IF depth = 0 THEN + Toc.AddItem HlpTocTitle$(hlp, i) + ELSE + Toc.AddChildItem parents(depth - 1), HlpTocTitle$(hlp, i) + END IF + + node = Toc.ItemCount() - 1 + parents(depth) = node + Toc.SetItemTag node, LTRIM$(STR$(HlpTocTopic(hlp, i))) + + IF HlpTocExpanded(hlp, i) THEN + Toc.SetExpanded node, True + END IF + NEXT i +END SUB + + +' A help source with a directive before its first topic: the compile +' fails and says where. +SUB CheckBrokenSource + DIM bad AS STRING + DIM f AS INTEGER + + bad = App.Data + "\BROKEN.DHS" + f = FREEFILE + OPEN bad FOR OUTPUT AS #f + PRINT #f, ".h1 No topic yet" + CLOSE #f + Check NOT HelpCompile(bad, App.Data + "\BROKEN.HLP"), "broken compiles" + Check INSTR(HelpCompileMessages$(), "error:") > 0 AND INSTR(HelpCompileMessages$(), "BROKEN.DHS") > 0, "broken message " + HelpCompileMessages$() + KILL bad +END SUB + + +' The dotted rectangle around a focused link of a self-drawn topic: every +' other pixel of the edge in the ForeColor. +SUB CheckFocusRect + Pic.Resize FOCUS_W, FOCUS_H + Pic.BackColor = RGB(255, 255, 255) + Pic.ForeColor = RGB(0, 0, 0) + Pic.Cls + Pic.DrawFocusRect 0, 0, FOCUS_W, FOCUS_H + Check Pic.Point(0, 0) = Pic.ForeColor AND Pic.Point(1, 0) = Pic.BackColor AND Pic.Point(2, 0) = Pic.ForeColor, "focus rect" +END SUB + + +' Back one topic in the history. +SUB GoBack + IF histPos > 0 THEN + histPos = histPos - 1 + ShowTopic hist(histPos) + END IF +END SUB + + +' Forward one topic in the history. +SUB GoForward + IF histPos < histCount - 1 THEN + histPos = histPos + 1 + ShowTopic hist(histPos) + END IF +END SUB + + +' Show topic t and add it to the history (the forward part is dropped; +' the oldest entry goes when the history is full). +SUB GoTopic(BYVAL t AS LONG) + DIM i AS LONG + + IF t < 0 OR t = Pane.Topic THEN + EXIT SUB + END IF + + histPos = histPos + 1 + + IF histPos >= HIST_MAX THEN + FOR i = 1 TO HIST_MAX - 1 + hist(i - 1) = hist(i) + NEXT i + + histPos = HIST_MAX - 1 + END IF + + hist(histPos) = t + histCount = histPos + 1 + ShowTopic t +END SUB + + +' The topic of the first link on topic t, or -1. +FUNCTION FirstLinkTarget(BYVAL t AS LONG) AS LONG + DIM i AS LONG + + FirstLinkTarget = -1 + + FOR i = 0 TO HlpLoadTopic(hlp, t) - 1 + IF HlpRecordType(hlp, i) = HLP_REC_LINK THEN + FirstLinkTarget = HlpFindTopic(hlp, HlpRecordLink$(hlp, i)) + EXIT FUNCTION + END IF + NEXT i +END FUNCTION + + +' The topic of index keyword word, or -1. +FUNCTION IndexTopic(BYVAL word AS STRING) AS LONG + DIM i AS LONG + + IndexTopic = -1 + + FOR i = 0 TO HlpIndexCount(hlp) - 1 + IF HlpIndexKeyword$(hlp, i) = word THEN + IndexTopic = HlpIndexTopic(hlp, i) + EXIT FUNCTION + END IF + NEXT i +END FUNCTION + + +' Show topic t without touching the history: the pane, the title and the +' tree's selection (without its Change navigating again). +SUB ShowTopic(BYVAL t AS LONG) + DIM i AS LONG + + Pane.ShowTopic hlp, t + syncing = True + + FOR i = 0 TO Toc.ItemCount() - 1 + IF Toc.GetItemTag(i) = LTRIM$(STR$(t)) THEN + Toc.ListIndex = i + END IF + NEXT i + + syncing = False +END SUB + + +SUB Pane_LinkClick(Topic AS STRING) + GoTopic HlpFindTopic(hlp, Topic) + ParHlp.Caption = "PARHLP LINK " + HlpTopicTitle$(hlp, Pane.Topic) +END SUB + + +SUB Toc_Change + DIM tag AS STRING + + IF syncing OR Toc.ListIndex < 0 THEN + EXIT SUB + END IF + + tag = Toc.GetItemTag(Toc.ListIndex) + + IF tag <> "" AND tag <> "-1" THEN + GoTopic VAL(tag) + END IF +END SUB + + +' F1 (or the shell) hands the running program "file [topic]". +SUB ParHlp_NewArgs + GoTopic HlpStartTopic(hlp, COMMAND$(2)) + ParHlp.Caption = "PARHLP ARGS " + HlpTopicTitle$(hlp, Pane.Topic) +END SUB + + +SUB ParHlp_Load + DIM source AS STRING + DIM hlpPath AS STRING + DIM home AS LONG + DIM second AS LONG + DIM third AS LONG + + histPos = -1 + source = COMMAND$(1) + + IF source = "" THEN + source = App.Path + "\PARHLP\PARHLP.DHS" + END IF + + hlpPath = App.Data + "\PARHLP.HLP" + Check HelpCompile(source, hlpPath), "compile " + source + Check HelpCompileMessages$() = "", "compile messages" + CheckBrokenSource + CheckFocusRect + hlp = HlpOpen(hlpPath) + Check hlp <> 0, "open" + + IF hlp = 0 THEN + ParHlp.Caption = "PARHLP FAIL " + failure + EXIT SUB + END IF + + home = HlpFindTopic(hlp, "ph.home") + second = HlpFindTopic(hlp, "ph.second") + third = HlpFindTopic(hlp, "ph.third") + Check HlpTopicCount(hlp) = 3 AND home >= 0 AND second >= 0 AND third >= 0, "topics" + + ' The table of contents: Home, Second Page under it, the Extras + ' heading (no topic) and Third Page under that + BuildToc + Check Toc.ItemCount() = HlpTocCount(hlp) AND HlpTocCount(hlp) = 4, "toc" + Check Toc.GetItemTag(2) = "-1" AND Toc.GetItemText(2) = "Extras", "toc heading" + + ' The start topic, then the links followed + GoTopic HlpStartTopic(hlp, COMMAND$(2)) + Check Pane.Topic = home, "start" + GoTopic FirstLinkTarget(Pane.Topic) + Check Pane.Topic = second, "link 1" + GoTopic FirstLinkTarget(Pane.Topic) + Check Pane.Topic = third, "link 2" + Check Toc.GetItemTag(Toc.ListIndex) = LTRIM$(STR$(third)), "tree sync" + + ' Back, back, forward + GoBack + Check Pane.Topic = second, "back 1" + GoBack + Check Pane.Topic = home, "back 2" + GoBack + Check Pane.Topic = home, "back at start" + GoForward + Check Pane.Topic = second, "forward" + + ' Search and index + Check HlpSearch(hlp, SEARCH_WORD) = 1, "search count" + Check HlpSearchTopic(hlp, 0) = second, "search topic" + Check HlpSearch(hlp, "qqqzzz") = 0, "search none" + Check IndexTopic(INDEX_WORD) = second, "index" + + ' The tree picks a topic + Toc.ListIndex = 0 + Check Pane.Topic = home, "tree click" + Check Pane.LinkTopic = "", "link topic" + + IF failure = "" THEN + ParHlp.Caption = "PARHLP PASS" + ELSE + ParHlp.Caption = "PARHLP FAIL " + failure + END IF +END SUB + + +SUB ParHlp_Unload + HlpClose hlp +END SUB diff --git a/src/test/dos/fixtures/paride/kid/kid.bas b/src/test/dos/fixtures/paride/kid/kid.bas new file mode 100644 index 00000000..ba16539c --- /dev/null +++ b/src/test/dos/fixtures/paride/kid/kid.bas @@ -0,0 +1,46 @@ +' 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. + +' kid.bas -- the child program of the PARIDE fixture: PARIDE compiles it, +' stops at the breakpoint in AddUp, steps, reads and changes total, lets +' it run to the end and builds KID.APP from it. It prints "sum= 109" +' when total was set to 100 at the first stop. + +OPTION EXPLICIT + +DIM SHARED total AS LONG +DIM SHARED label AS STRING + + +SUB AddUp(BYVAL n AS LONG) + DIM i AS LONG + + FOR i = 1 TO n + total = total + i + NEXT i +END SUB + + +label = "sum" +AddUp 4 +PRINT label; "="; total +UNLOAD KidForm diff --git a/src/test/dos/fixtures/paride/kid/kid.dbp b/src/test/dos/fixtures/paride/kid/kid.dbp new file mode 100644 index 00000000..d02a7830 --- /dev/null +++ b/src/test/dos/fixtures/paride/kid/kid.dbp @@ -0,0 +1,19 @@ +[Project] +Name = Kid +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = The program the PARIDE fixture compiles, debugs and builds + +[Modules] +File0 = kid.bas +Count = 1 + +[Forms] +File0 = kidform.frm +Count = 1 + +[Settings] +StartupForm = KidForm +OptionExplicit = True diff --git a/src/test/dos/fixtures/paride/kid/kidform.frm b/src/test/dos/fixtures/paride/kid/kidform.frm new file mode 100644 index 00000000..55baa673 --- /dev/null +++ b/src/test/dos/fixtures/paride/kid/kidform.frm @@ -0,0 +1,44 @@ +VERSION DVX 1.00 + +Begin Form KidForm + Caption = "Kid" + Layout = VBox + AutoSize = False + Resizable = False + Width = 160 + Height = 60 + + Begin Label lblKid + Caption = "..." + Weight = 1 + End +End + +' 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. + +' kidform.frm code -- the child's one form (checked by the compiler's +' Control.Property validation). + +SUB KidForm_Load + lblKid.Caption = "Kid running" +END SUB diff --git a/src/test/dos/fixtures/paride/paride.dbp b/src/test/dos/fixtures/paride/paride.dbp new file mode 100644 index 00000000..2399428c --- /dev/null +++ b/src/test/dos/fixtures/paride/paride.dbp @@ -0,0 +1,20 @@ +[Project] +Name = PARIDE +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: a BASIC program compiles, debugs and builds another (compiler.bas, debug.bas) + +[Modules] +File0 = ../../../../include/basic/compiler.bas +File1 = ../../../../include/basic/debug.bas +Count = 2 + +[Forms] +File0 = paride.frm +Count = 1 + +[Settings] +StartupForm = ParIde +OptionExplicit = True diff --git a/src/test/dos/fixtures/paride/paride.frm b/src/test/dos/fixtures/paride/paride.frm new file mode 100644 index 00000000..76fc4111 --- /dev/null +++ b/src/test/dos/fixtures/paride/paride.frm @@ -0,0 +1,296 @@ +VERSION DVX 1.00 + +Begin Form ParIde + Caption = "Debugger test" + Layout = VBox + AutoSize = False + Resizable = False + Width = 360 + Height = 90 + + Begin Label lblStep + Caption = "Starting..." + Weight = 1 + End + + Begin Timer tmrPoll + Interval = 50 + Enabled = False + End +End + +' 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. + +' paride -- DOS harness fixture: what a DVX BASIC IDE written in BASIC +' does with compiler.bas and debug.bas. It compiles the project in +' kid\ (kid.dbp), runs it as a child program with a breakpoint in AddUp, +' checks the line, procedure, locals, call stack and an expression +' there, runs an Immediate statement, steps, changes a variable, lets the +' child run to its end, reads what it printed and builds KID.APP from it. +' First it reads the project file and source text the way an editor +' does: the project's files and settings (Proj*), tokens, code positions, +' renaming in code, WithEvents and class Events, F1 topics and keyword +' flags. +' The window title reports the outcome for the script: "PARIDE PASS 109 +' built" or "PARIDE FAIL ". + +OPTION EXPLICIT + +CONST KID_DIR = "paride/kid" ' beside the fixture, under App.Path +CONST BP_FILE_LINE = 38 ' "total = total + i" in kid.bas +CONST POLL_MS = 40 +CONST TAB_CHAR = 9 +CONST EXPECT_TOTAL = 109 ' 100 set at the first stop, + 2 + 3 + 4 +CONST STAGE_BREAK = 1 ' waiting for the breakpoint +CONST STAGE_STEP = 2 ' waiting for the step to finish +CONST STAGE_END = 3 ' waiting for the end + +DIM SHARED src AS LONG +DIM SHARED mdl AS LONG +DIM SHARED kid AS LONG +DIM SHARED stage AS INTEGER +DIM SHARED kidDir AS STRING +DIM SHARED bpLine AS LONG + + +SUB Report(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) + tmrPoll.Enabled = False + + IF ok THEN + ParIde.Caption = "PARIDE PASS " + what + ELSE + ParIde.Caption = "PARIDE FAIL " + what + END IF + + lblStep.Caption = what +END SUB + + +' What an editor reads without compiling: "" when all is as expected, +' else what is not. +FUNCTION CheckEditor() AS STRING + DIM prj AS LONG + DIM i AS LONG + DIM kwInteger AS LONG + DIM cell AS STRING + DIM nl AS STRING + + cell = CHR$(TAB_CHAR) + nl = CHR$(10) + CheckEditor = "" + prj = ProjOpen(kidDir + "/kid.dbp") + + IF prj = 0 THEN + CheckEditor = "project open " + CompError$() + EXIT FUNCTION + END IF + + IF ProjFileCount(prj) <> 2 OR ProjGet$(prj, "Name") <> "Kid" OR ProjGet$(prj, "OptionExplicit") <> "true" OR ProjGet$(prj, "StartupForm") <> "KidForm" THEN + CheckEditor = "project settings" + ELSEIF ProjFileKind(prj, ProjFileFind(prj, "KIDFORM.FRM")) <> COMP_KIND_FORM OR ProjFile$(prj, 0) <> "kid.bas" THEN + CheckEditor = "project files" + END IF + + ProjClose prj + + IF CheckEditor <> "" THEN + EXIT FUNCTION + END IF + + FOR i = 0 TO CompKeywordCount() - 1 + IF CompKeyword$(i) = "INTEGER" THEN + kwInteger = i + END IF + NEXT i + + IF CompTokens$("x = 1 ' hi") <> "1" + cell + "1" + cell + "1" + cell + "Identifier" + cell + "x" + nl + "1" + cell + "3" + cell + "1" + cell + "Symbol" + cell + "=" + nl + "1" + cell + "5" + cell + "1" + cell + "Number" + cell + "1" + nl + "1" + cell + "7" + cell + "4" + cell + "Comment" + cell + "' hi" + nl THEN + CheckEditor = "tokens " + CompTokens$("x = 1 ' hi") + ELSEIF CompInCode("a = " + CHR$(34) + "b" + CHR$(34), 6) OR NOT CompInCode("a = 1", 1) THEN + CheckEditor = "in code" + ELSEIF CompRenameInCode$("Kid.x = 1 ' Kid.y", "kid", "Child", True) <> "Child.x = 1 ' Kid.y" THEN + CheckEditor = "rename " + CompRenameInCode$("Kid.x = 1 ' Kid.y", "kid", "Child", True) + ELSEIF CompWithEvents$("Dim WithEvents w As Widget") <> "w" + cell + "Widget" + nl THEN + CheckEditor = "with events" + ELSEIF CompClassEvents$("Event Done(Code As Integer)") <> "Done" + cell + "Code As Integer" + nl THEN + CheckEditor = "class events" + ELSEIF CompHelpTopic$("dim") <> "lang.declarations" OR CompHelpCtrlTopic$("Timer") <> "ctrl.timer" THEN + CheckEditor = "help topics" + ELSEIF CompKeywordClass(kwInteger) <> COMP_KW_TYPE OR NOT CompKeywordReserved(kwInteger) THEN + CheckEditor = "keyword flags" + END IF +END FUNCTION + + +' The first stop: in AddUp at the breakpoint, i = 1, nothing added yet. +FUNCTION CheckBreak() AS BOOLEAN + DIM cell AS STRING + + cell = CHR$(TAB_CHAR) + CheckBreak = False + + IF DbgLine(kid) <> bpLine THEN + Report False, "line" + STR$(DbgLine(kid)) + ELSEIF DbgProc$(kid) <> "AddUp" THEN + Report False, "proc " + DbgProc$(kid) + ELSEIF CompMapLine(src, DbgLine(kid)) <> BP_FILE_LINE THEN + Report False, "map" + STR$(CompMapLine(src, DbgLine(kid))) + ELSEIF INSTR(DbgLocals$(kid), "i" + cell + "Long" + cell + "1") = 0 THEN + Report False, "locals " + DbgLocals$(kid) + ELSEIF LEFT$(DbgCallStack$(kid), 6) <> "AddUp" + cell THEN + Report False, "stack " + DbgCallStack$(kid) + ELSEIF VAL(DbgEval$(kid, "total + n * 10")) <> 40 THEN + Report False, "eval " + DbgEval$(kid, "total + n * 10") + ELSEIF VAL(DbgExec$(kid, "PRINT n * 2")) <> 8 THEN + Report False, "exec " + DbgExec$(kid, "PRINT n * 2") + ELSE + CheckBreak = True + END IF +END FUNCTION + + +' After one step: the breakpoint line ran once. total becomes 100. +FUNCTION CheckStep() AS BOOLEAN + CheckStep = False + + IF DbgEval$(kid, "total") <> "1" THEN + Report False, "step total " + DbgEval$(kid, "total") + ELSEIF NOT DbgSetVar(kid, "total", "total + 99") THEN + Report False, "set " + DbgMessage$() + ELSEIF DbgEval$(kid, "total") <> "100" THEN + Report False, "set total " + DbgEval$(kid, "total") + ELSE + CheckStep = True + END IF +END FUNCTION + + +' The end: the child printed the total; build its .app. +SUB CheckEnd + DIM printed AS STRING + DIM appPath AS STRING + + printed = RunOutput$(kid) + appPath = App.Path + "/kid.app" + + IF INSTR(printed, LTRIM$(STR$(EXPECT_TOTAL))) = 0 THEN + Report False, "output " + printed + ELSEIF NOT CompBuildApp(mdl, src, appPath, False) THEN + Report False, "build " + CompError$() + ELSEIF DIR$(appPath) = "" THEN + Report False, "no kid.app" + ELSE + Report True, LTRIM$(STR$(EXPECT_TOTAL)) + " built" + END IF +END SUB + + +SUB ParIde_Load + DIM what AS STRING + + kidDir = App.Path + "/" + KID_DIR + what = CheckEditor() + + IF what <> "" THEN + Report False, what + EXIT SUB + END IF + + src = CompSourceCreate() + + IF NOT CompSourceFromProject(src, kidDir + "/kid.dbp") THEN + Report False, "project " + CompError$() + EXIT SUB + END IF + + mdl = CompCompile(src, True, True) + + IF mdl = 0 THEN + Report False, "compile " + CompError$() + EXIT SUB + END IF + + bpLine = CompConcatLine(src, kidDir + "/kid.bas", BP_FILE_LINE) + kid = RunCreate(mdl, src, "", "") + + IF bpLine = 0 OR kid = 0 THEN + Report False, "create " + DbgMessage$() + EXIT SUB + END IF + + DbgSetBreakpoints kid, STR$(bpLine) + + IF NOT RunStart(kid, "", False) THEN + Report False, "start" + EXIT SUB + END IF + + stage = STAGE_BREAK + tmrPoll.Enabled = True +END SUB + + +SUB ParIde_Unload + RunFree kid + CompModuleFree mdl + CompSourceFree src +END SUB + + +SUB tmrPoll_Timer + DIM st AS LONG + + st = RunPoll(kid, POLL_MS) + + IF st = RUN_ERROR THEN + Report False, "runtime " + RunError$(kid) + EXIT SUB + END IF + + SELECT CASE stage + CASE STAGE_BREAK + IF st = RUN_PAUSED THEN + IF CheckBreak() THEN + stage = STAGE_STEP + DbgStepOver kid + END IF + ELSEIF st = RUN_ENDED THEN + Report False, "ended before the breakpoint" + END IF + + CASE STAGE_STEP + IF st = RUN_PAUSED THEN + IF CheckStep() THEN + stage = STAGE_END + DbgSetBreakpoints kid, "" + DbgContinue kid + END IF + ELSEIF st = RUN_ENDED THEN + Report False, "ended during the step" + END IF + + CASE STAGE_END + IF st = RUN_ENDED THEN + CheckEnd + END IF + END SELECT +END SUB diff --git a/src/test/dos/fixtures/parpm/parpm.dbp b/src/test/dos/fixtures/parpm/parpm.dbp new file mode 100644 index 00000000..3c25d79d --- /dev/null +++ b/src/test/dos/fixtures/parpm/parpm.dbp @@ -0,0 +1,26 @@ +[Project] +Name = PARPM +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: what a Program Manager written in BASIC needs + +[Modules] +File0 = ../../../../include/basic/shell.bas +File1 = ../../../../include/basic/prefs.bas +File2 = ../../../../include/basic/listhelp.bas +File3 = ../../../../include/basic/system.bas +Count = 4 + +[Controls] +File0 = ../iconview/iconview.ctl +Count = 1 + +[Forms] +File0 = parpm.frm +Count = 1 + +[Settings] +StartupForm = ParPm +MultiInstance = True diff --git a/src/test/dos/fixtures/parpm/parpm.frm b/src/test/dos/fixtures/parpm/parpm.frm new file mode 100644 index 00000000..0f096caf --- /dev/null +++ b/src/test/dos/fixtures/parpm/parpm.frm @@ -0,0 +1,454 @@ +VERSION DVX 1.00 + +Begin Form ParPm + Caption = "Parity Program Manager" + Layout = VBox + AutoSize = True + Resizable = False + + Begin IconView pmIcons + MinWidth = 404 + MinHeight = 150 + Weight = 1 + EmptyText = "No programs" + End + + Begin Label lblResult + Caption = "Running..." + End + + Begin Timer tmrStep + Interval = 100 + Enabled = True + End +End + +' 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. + + +' parpm -- DOS harness fixture (and host test, appsTest): the hardest parts +' of Program Manager written in BASIC. It +' - scans the installed programs and reads their names (ShellProgramScan, +' ShellProgramName$), +' - keeps the programs as a group in a groups file of PROGMAN.INI's +' format with the shell's groups model (ShellGroups*, ShellItem*), +' reads the view back with Program Manager's own choice names +' (PrefsGetChoiceFrom) and finds each item's icon file +' (ShellItemIcon$), +' - checks the item shortcut rule (ShellItemKeyValid), registers a key +' that works in every program (HotkeyAdd) and waits for it to be +' pressed (HotkeyNext from a Timer), +' - reads the system information and the file types list, and sets and +' removes a file type, +' - shows the programs in an IconView, a control written in BASIC +' (../iconview/iconview.ctl, a UserControl of this project, Program +' Manager's view rules): their icons (the .app's), names on two lines, +' descriptions in the list view, selection, the arrow keys, Enter and +' a double click starting the selected program, and -- Auto Arrange +' off -- each program in its own cell from the groups model, a program +' dragged to a free cell or onto another placed there with the model's +' rule (ShellItemPlace, ShellGroupPlaceUnplaced), +' - starts a second copy of itself (ShellRunProgram, with its id), runs +' it minimized (ShellMinimizeApp) and sees the running count and the +' change counter move as it starts and ends. +' The window sizes itself to the IconView (five large icon cells wide, +' two rows tall) and the result line. The window title reports the +' outcome for the script: "PARPM PASS programs" once every step +' worked and the key was pressed, "PARPM FAIL " otherwise. Started +' with the argument "child" it is the second copy: it closes itself after +' a moment. + +OPTION EXPLICIT + +CONST TAG = "PARPM " +CONST CHILD_ARG = "child" +CONST CHILD_TITLE = "Parity Child" +CONST CHILD_TICKS = 5 +CONST WAIT_TICKS = 300 +CONST HOT_KEY = "Ctrl+Alt+P" +CONST GROUPS_FILE = "groups.ini" +CONST MAIN_GROUP = "Main" +CONST VIEW_NAMES = "large|small|list" ' Program Manager's view names, in SHELL_VIEW_* order +CONST TEST_EXT = "PZQ" +CONST ICON_MAX = 8 + +DIM SHARED progPath(ICON_MAX) AS STRING +DIM SHARED progName(ICON_MAX) AS STRING +DIM SHARED progCount AS LONG +DIM SHARED shown AS LONG +DIM SHARED failText AS STRING +DIM SHARED isChild AS BOOLEAN +DIM SHARED ticks AS LONG +DIM SHARED hotId AS LONG +DIM SHARED gotKey AS BOOLEAN +DIM SHARED childId AS LONG +DIM SHARED childDone AS BOOLEAN +DIM SHARED runningStart AS LONG +DIM SHARED changeStarted AS LONG +DIM SHARED iconsDrawn AS LONG +DIM SHARED groups AS LONG +DIM SHARED mainGrp AS LONG + + +' The first failure is the one reported. +SUB Fail(BYVAL what AS STRING) + IF failText = "" THEN + failText = what + END IF +END SUB + + +' The icons are judged last, so every other step is known even while +' LOADPICTURE cannot read a program's icon. +SUB ShowState + IF failText <> "" THEN + ParPm.Caption = TAG + "FAIL " + failText + ELSEIF childDone AND gotKey AND iconsDrawn = 0 THEN + ParPm.Caption = TAG + "FAIL icons: no program's icon in the IconView" + ELSEIF childDone AND gotKey THEN + ParPm.Caption = TAG + "PASS " + LTRIM$(STR$(progCount)) + " programs" + ELSE + ParPm.Caption = TAG + "WAIT" + END IF + + lblResult.Caption = ParPm.Caption +END SUB + + +' Every installed program but this one, with its name. +SUB StepScan + DIM list AS LONG + DIM i AS LONG + DIM path AS STRING + + list = ShellProgramScan("", ShellAppPath$()) + progCount = ShellProgramCount(list) + + IF progCount = 0 THEN + Fail "scan: no programs" + EXIT SUB + END IF + + FOR i = 0 TO progCount - 1 + path = ShellProgramPath$(list, i) + + IF NOT ShellIsProgram(path) OR ShellProgramName$(path) = "" THEN + Fail "scan: " + path + END IF + + IF i < ICON_MAX THEN + progPath(i) = path + progName(i) = ShellProgramName$(path) + shown = i + 1 + END IF + NEXT i + + ShellLog "Parpm: " + progName(0) + " - " + ShellProgramDescription$(progPath(0)) + ShellProgramFree list + + IF ShellProgramCount(list) <> 0 OR ShellProgramPath$(list, 0) <> "" THEN + Fail "scan: list not freed" + END IF +END SUB + + +' The programs found as the group Main of a groups file (PROGMAN.INI's +' format, the shell's groups model), saved and read back (and kept for +' the IconView's cells); the view read again with Program Manager's own +' choice names; each item's icon file. +SUB StepGroups + DIM ini AS STRING + DIM g AS LONG + DIM grp AS LONG + DIM h AS LONG + DIM i AS LONG + DIM icon AS STRING + DIM icons AS LONG + + ini = ShellConfigFile$(GROUPS_FILE) + + IF DIR$(ini) <> "" THEN + KILL ini + END IF + + g = ShellGroupsLoad(ini) + grp = ShellGroupFind(g, MAIN_GROUP) + + FOR i = 0 TO shown - 1 + IF ShellItemAdd(g, grp, progName(i), progPath(i)) <> i THEN + Fail "groups: add" + END IF + NEXT i + + IF NOT ShellItemSet(g, grp, 0, "minimized", "1") OR NOT ShellGroupsSet(g, "view", SHELL_VIEW_SMALL) THEN + Fail "groups: set" + END IF + + IF NOT ShellGroupsSave(g, ini) THEN + Fail "groups: not saved" + END IF + + ShellGroupsFree g + g = ShellGroupsLoad(ini) + grp = ShellGroupFind(g, MAIN_GROUP) + + IF ShellItemCount(g, grp) <> shown OR ShellItemGet$(g, grp, 0, "program") <> progPath(0) OR ShellItemGet$(g, grp, 0, "minimized") <> "1" OR ShellGroupsGet(g, "view") <> SHELL_VIEW_SMALL THEN + Fail "groups: read back" + END IF + + ' An item's icon comes from its icon file, else its program + FOR i = 0 TO shown - 1 + icon = ShellItemIcon$(ShellItemGet$(g, grp, i, "icon"), ShellItemGet$(g, grp, i, "program")) + + IF icon <> "" AND icon <> progPath(i) THEN + Fail "groups: icon " + icon + ELSEIF icon <> "" THEN + icons = icons + 1 + END IF + NEXT i + + IF icons = 0 THEN + Fail "groups: no icon" + END IF + + groups = g + mainGrp = grp + + ' Any program reads the file with prefs.bas too + h = PrefsLoad(ini) + + IF PrefsGetChoiceFrom(h, "options", "view", VIEW_NAMES, "large") <> SHELL_VIEW_SMALL OR PrefsGetInt(h, "group1", "count", 0) <> shown THEN + Fail "groups: view choice" + END IF + + PrefsClose h +END SUB + + +' A key for every program, in DVX's own spelling; pressing it is checked +' by the Timer. +SUB StepHotkey + IF HotkeyText$("ctrl+alt+p") <> HOT_KEY OR HotkeyText$("no key") <> "" THEN + Fail "hotkey: text" + END IF + + ' An item's shortcut needs Ctrl with Alt or Shift + IF NOT ShellItemKeyValid(HOT_KEY) OR ShellItemKeyValid("Ctrl+P") THEN + Fail "hotkey: item rule" + END IF + + hotId = HotkeyAdd(HOT_KEY) + + IF hotId = 0 THEN + Fail "hotkey: not added" + END IF +END SUB + + +SUB StepInfo + IF LEN(ShellSystemInfo$()) = 0 THEN + Fail "system info" + END IF +END SUB + + +' The File Types page: the list, normalizing, and a type set and removed. +SUB StepFileTypes + DIM n AS LONG + DIM i AS LONG + + n = ShellAssocCount() + + FOR i = 0 TO n - 1 + IF ShellAssocExt$(i) = "" OR ShellAssocValue$(i) = "" THEN + Fail "file types: entry " + LTRIM$(STR$(i)) + END IF + NEXT i + + IF ShellAssocNormalize$(".txt") <> "TXT" OR ShellAssocNormalize$("no.good") <> "" THEN + Fail "file types: normalize" + END IF + + IF NOT ShellAssocSet(TEST_EXT, progPath(0)) OR ShellAssocCount() <> n + 1 THEN + Fail "file types: set" + END IF + + IF ShellAssocCommand$("letter." + LCASE$(TEST_EXT)) <> progPath(0) OR ShellAssocProgram$(TEST_EXT) <> progPath(0) THEN + Fail "file types: lookup" + END IF + + IF NOT ShellAssocRemove(TEST_EXT) OR ShellAssocCount() <> n OR ShellAssocExt$(n) <> "" THEN + Fail "file types: remove" + END IF +END SUB + + +' A second copy of this program, run minimized: its id, and the running +' count and change counter moving. The Timer sees it end. +SUB StepLaunch + DIM changeBefore AS LONG + + runningStart = ShellRunningCount() + changeBefore = ShellChangeCount() + childId = ShellRunProgram(ShellAppPath$(), CHILD_ARG, "") + + IF childId = 0 THEN + Fail "launch: no id" + EXIT SUB + END IF + + ShellMinimizeApp childId + + IF ShellRunningCount() <> runningStart + 1 OR ShellChangeCount() = changeBefore THEN + Fail "launch: count" + END IF + + changeStarted = ShellChangeCount() +END SUB + + +' The programs in the IconView: each one's name, its icon (the .app's) +' and its description, each in its own cell (Auto Arrange off) -- the +' groups model gives the cells (ShowCells, from ColumnsChange). +SUB FillIcons + DIM i AS LONG + + pmIcons.Clear + pmIcons.AutoArrange = False + + FOR i = 0 TO shown - 1 + pmIcons.AddItem progName(i), progPath(i), "" + pmIcons.SetItemDetail i, ShellProgramDescription$(progPath(i)) + NEXT i + + pmIcons.SelectedIndex = 0 + iconsDrawn = pmIcons.PictureCount +END SUB + + +' Every program's cell from the groups model. +SUB ShowCells + DIM i AS LONG + + FOR i = 0 TO pmIcons.ItemCount - 1 + pmIcons.SetItemCell i, VAL(ShellItemGet$(groups, mainGrp, i, "col")), VAL(ShellItemGet$(groups, mainGrp, i, "row")) + NEXT i +END SUB + + +SUB ParPm_Load + IF COMMAND$(1) = CHILD_ARG THEN + isChild = True + ParPm.Caption = CHILD_TITLE + EXIT SUB + END IF + + StepScan + StepGroups + StepHotkey + StepInfo + StepFileTypes + FillIcons + StepLaunch + ShowState +END SUB + + +SUB ParPm_Unload + HotkeyRemove hotId + ShellGroupsFree groups +END SUB + + +' Enter or a double click on a program starts it. +SUB pmIcons_ItemOpen(BYVAL Index AS INTEGER) + DIM started AS LONG + + started = ShellRunProgram(progPath(Index), "", "") +END SUB + + +' The view's columns changed or its programs were replaced: programs +' without a cell get the first free one (the groups model's rule). +SUB pmIcons_ColumnsChange(BYVAL Columns AS INTEGER) + IF groups = 0 THEN + EXIT SUB + END IF + + ShellGroupPlaceUnplaced groups, mainGrp, Columns + ShowCells +END SUB + + +' A program dropped on a cell takes it; one already there takes its old +' cell (the groups model's rule, Program Manager's with Auto Arrange off). +SUB pmIcons_ItemDropped(BYVAL Index AS INTEGER, BYVAL Col AS INTEGER, BYVAL Row AS INTEGER) + ShellItemPlace groups, mainGrp, Index, Col, Row + ShowCells +END SUB + + +' The parent waits for the second copy to end and for the key; the second +' copy closes itself. +SUB tmrStep_Timer + DIM id AS LONG + + ticks = ticks + 1 + + IF isChild THEN + IF ticks >= CHILD_TICKS THEN + tmrStep.Enabled = False + Unload ParPm + END IF + + EXIT SUB + END IF + + id = HotkeyNext() + + DO WHILE id <> 0 + IF id = hotId THEN + gotKey = True + END IF + + id = HotkeyNext() + LOOP + + IF childId <> 0 AND NOT childDone THEN + childDone = ShellRunningCount() = runningStart AND ShellChangeCount() <> changeStarted + END IF + + IF ticks >= WAIT_TICKS AND NOT (childDone AND gotKey) THEN + IF NOT childDone THEN + Fail "launch: the second copy did not end" + ELSE + Fail "hotkey: not pressed" + END IF + END IF + + IF failText <> "" OR (childDone AND gotKey) THEN + tmrStep.Enabled = False + END IF + + ShowState +END SUB diff --git a/src/test/dos/fixtures/sndmore/sndmore.dbp b/src/test/dos/fixtures/sndmore/sndmore.dbp new file mode 100644 index 00000000..ad1926e5 --- /dev/null +++ b/src/test/dos/fixtures/sndmore/sndmore.dbp @@ -0,0 +1,18 @@ +[Project] +Name = Sound BASIC More +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: sounds made from binary data, voices changed while playing, streams, triangle tones, the FM chip and event sounds from sound.bas + +[Modules] +File0 = ../../../../include/basic/sound.bas +Count = 1 + +[Forms] +File0 = sndmore.frm +Count = 1 + +[Settings] +StartupForm = SndMore diff --git a/src/test/dos/fixtures/sndmore/sndmore.frm b/src/test/dos/fixtures/sndmore/sndmore.frm new file mode 100644 index 00000000..86198dd1 --- /dev/null +++ b/src/test/dos/fixtures/sndmore/sndmore.frm @@ -0,0 +1,158 @@ +VERSION DVX 1.00 + +Begin Form SndMore + Caption = "Sound more starting" + Layout = VBox + AutoSize = False + Resizable = False + Width = 480 + Height = 80 + + Begin Label lblInfo + Caption = "Playing made sounds" + Weight = 1 + End + + Begin Timer tmrPoll + Interval = 50 + Enabled = True + End +End + +' 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. + +'' sndmore -- DOS harness fixture for the sound.bas calls added in round 2. +' Makes a sound from 8-bit square-wave data (bytes 255 and 0, so the data +' holds zero bytes and must arrive whole: its length proves it), plays it +' panned and faster, plays a triangle tone, feeds a stream from a Timer +' until it has played, takes and frees an FM voice when there is an FM +' chip and plays DVX's Information sound. The caption ends as +' "Sound more ok ..." or "Sound more failed ", which sndmore.scr +' waits for. + +Const HALF_WAVE = 25 ' bytes per half cycle (441 Hz at 22050 Hz) +Const SOUND_CYCLES = 220 ' 11000 bytes: 498 ms at 22050 Hz +Const SOUND_MS = 498 +Const STREAM_CYCLES = 100 ' 5000 bytes: more than one write fits +Const POLL_LIMIT = 400 ' 20 s of Timer ticks + +Dim Shared stage As Long +Dim Shared snd As Long +Dim Shared voice As Long +Dim Shared stream As Long +Dim Shared pending As String +Dim Shared fails As String +Dim Shared polls As Long + +Sub Check (ok As Long, what As String) + If Not ok Then fails = fails & " " & what +End Sub + +Function SquareWave (cycles As Long) As String + Dim i As Long + Dim hi As String + Dim lo As String + Dim s As String + + For i = 1 To HALF_WAVE + hi = hi & Chr$(255) + lo = lo & Chr$(0) + Next i + + For i = 1 To cycles + s = s & hi & lo + Next i + + SquareWave = s +End Function + +Sub Finish () + Dim fm As Long + + If FmInfo$("voices") <> "0" Then + fm = FmAlloc() + Check fm <> 0, "fmalloc" + + If fm <> 0 Then + Check FmWrite(fm, FM_CAR_LEVEL, 0), "fmwrite" + FmFree fm + End If + End If + + SoundPlayEvent SOUND_EVENT_INFO, False + SoundFree snd + + If fails = "" Then + SndMore.Caption = "Sound more ok rate " & SoundCaps("rate") & ", FM voices " & FmInfo$("voices") + Else + SndMore.Caption = "Sound more failed" & fails + End If + + stage = 3 +End Sub + +Sub tmrPoll_Timer () + Dim taken As Long + Dim tri As Long + + polls = polls + 1 + + If stage = 0 Then + snd = SoundCreate(SquareWave(SOUND_CYCLES), 22050, 8, 1) + Check snd <> 0, "create " & SoundError$() + Check SoundLength(snd) = SOUND_MS, "length " & SoundLength(snd) + voice = SoundPlay(snd, 80, SOUND_PAN_LEFT, False) + Check voice <> 0, "play " & SoundError$() + SoundSetPitch voice, 150 + SoundSetPan voice, SOUND_PAN_RIGHT + Check SoundError$() = "No error", "voice " & SoundError$() + tri = PlayTriangle(880, 200, 60) + Check tri <> 0, "triangle " & SoundError$() + stream = SoundStreamOpen(11025, 8, 1, 0) + Check stream <> 0, "stream " & SoundError$() + pending = SquareWave(STREAM_CYCLES) + SndMore.Caption = "Sound more playing" + stage = 1 + + If stream = 0 Then + Finish + End If + ElseIf stage = 1 Then + taken = SoundStreamWrite(stream, pending) + pending = Mid$(pending, taken + 1) + + If Len(pending) = 0 Then + stage = 2 + End If + ElseIf stage = 2 Then + If SoundStreamQueued(stream) = 0 And Not SoundPlaying(voice) Then + SoundStreamClose stream + Finish + End If + End If + + If stage < 3 And polls > POLL_LIMIT Then + Check False, "timeout stage " & stage + Finish + End If +End Sub diff --git a/src/test/dos/fixtures/terminal/xferperf.ini b/src/test/dos/fixtures/terminal/xferperf.ini new file mode 100644 index 00000000..7aeccbdf --- /dev/null +++ b/src/test/dos/fixtures/terminal/xferperf.ini @@ -0,0 +1,23 @@ +; TERMINAL.INI for xferperf.scr: one secure-link session on COM2, ZMODEM +[phonebook] +entries = 1 +current = 0 + +[transfer] +directory = DOWNLOAD +protocol = zmodem +exists = overwrite +autozmodem = true + +[entry1] +name = Transfer Test +type = secure +port = COM2 +speed = 115200 +databits = 8 +parity = N +stopbits = 1 +flow = none +askpassphrase = false +passphrase = +answerback = DVXACK diff --git a/src/test/dos/fixtures/ucdemo/ucdemo.dbp b/src/test/dos/fixtures/ucdemo/ucdemo.dbp new file mode 100644 index 00000000..1d6b176f --- /dev/null +++ b/src/test/dos/fixtures/ucdemo/ucdemo.dbp @@ -0,0 +1,19 @@ +[Project] +Name = UCDEMO +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: a program using the installed BASIC control IconView + +[Modules] +File0 = ../../../../include/basic/designer.bas +Count = 1 + +[Forms] +File0 = ucdemo.frm +Count = 1 + +[Settings] +StartupForm = UcDemo +OptionExplicit = True diff --git a/src/test/dos/fixtures/ucdemo/ucdemo.frm b/src/test/dos/fixtures/ucdemo/ucdemo.frm new file mode 100644 index 00000000..1edbd97e --- /dev/null +++ b/src/test/dos/fixtures/ucdemo/ucdemo.frm @@ -0,0 +1,135 @@ +VERSION DVX 1.00 + +Begin Form UcDemo + Caption = "UCDEMO RUN" + Layout = VBox + AutoSize = True + Resizable = False + + Begin IconView ivDemo + MinWidth = 300 + MinHeight = 150 + Weight = 1 + End + + Begin Label lblState + Caption = "Running..." + End +End + +' 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. + + +' ucdemo -- DOS harness fixture: a program using IconView, a control +' written in BASIC installed in WIDGETS (fixtures/iconview, a control +' project, staged as WIDGETS\ICONVIEW\ICONVIEW.WGT by the script's +' "# WIDGET: iconview"). Its .frm holds one; the control types list +' (what the IDE toolbox shows, designer.bas) has it with its tooltip and +' icon; items are added; the script presses Right, End, Home and Enter, +' and ItemOpen must come for the first item. The window sizes itself to +' the IconView and the status line. The title reports "UCDEMO PASS" only +' when every step worked, "UCDEMO FAIL " otherwise. + +OPTION EXPLICIT + +CONST TAG = "UCDEMO " +CONST TYPE_NAME = "IconView" +CONST TOOLTIP = "Icons in a grid" +CONST ICON_FILE = "ucicon.bmp" +CONST ITEMS = 5 + +DIM SHARED failText AS STRING +DIM SHARED selMoves AS LONG + + +SUB Fail(BYVAL what AS STRING) + IF failText = "" THEN + failText = what + END IF +END SUB + + +' The control types list has IconView, with the toolbox's tooltip and icon +SUB CheckToolbox + DIM i AS LONG + DIM found AS BOOLEAN + + FOR i = 0 TO CtlTypeCount() - 1 + IF CtlTypeName$(i) = TYPE_NAME THEN + found = True + END IF + NEXT i + + IF NOT found THEN + Fail "toolbox: no " + TYPE_NAME + ELSEIF CtlTypeTooltip$(TYPE_NAME) <> TOOLTIP THEN + Fail "toolbox: tooltip " + CtlTypeTooltip$(TYPE_NAME) + ELSEIF CtlTypeIconFile(TYPE_NAME, App.Data + "\" + ICON_FILE) = 0 THEN + Fail "toolbox: no icon" + ELSEIF CtlTypeDefaultEvent$(TYPE_NAME) <> "ItemOpen" THEN + Fail "toolbox: default event" + END IF +END SUB + + +SUB UcDemo_Load + DIM i AS LONG + + CheckToolbox + + FOR i = 1 TO ITEMS + ivDemo.AddItem "Item " + LTRIM$(STR$(i)), "", "tag" + LTRIM$(STR$(i)) + NEXT i + + IF ivDemo.ItemCount <> ITEMS OR ivDemo.ItemTag(2) <> "tag3" THEN + Fail "items" + END IF + + ivDemo.SelectedIndex = 0 + ivDemo.SetFocus + + IF failText <> "" THEN + UcDemo.Caption = TAG + "FAIL " + failText + ELSE + UcDemo.Caption = TAG + "READY" + END IF + + lblState.Caption = UcDemo.Caption +END SUB + + +SUB ivDemo_SelChange(BYVAL Index AS INTEGER) + selMoves = selMoves + 1 +END SUB + + +' Right, End, Home, then Enter: the first item opens +SUB ivDemo_ItemOpen(BYVAL Index AS INTEGER) + IF failText = "" AND Index = 0 AND selMoves >= 3 THEN + UcDemo.Caption = TAG + "PASS " + ivDemo.ItemName(Index) + ELSE + UcDemo.Caption = TAG + "FAIL open " + LTRIM$(STR$(Index)) + " after " + LTRIM$(STR$(selMoves)) + " moves " + failText + END IF + + lblState.Caption = UcDemo.Caption +END SUB diff --git a/src/test/dos/fixtures/ucide/ucide.dbp b/src/test/dos/fixtures/ucide/ucide.dbp new file mode 100644 index 00000000..6c3b1ce3 --- /dev/null +++ b/src/test/dos/fixtures/ucide/ucide.dbp @@ -0,0 +1,23 @@ +[Project] +Name = UCIDE +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = DOS harness fixture: a program with a UserControl of its own, edited in the IDE + +[Modules] +File0 = uclog.bas +Count = 1 + +[Controls] +File0 = ucmeter.ctl +Count = 1 + +[Forms] +File0 = ucide.frm +Count = 1 + +[Settings] +StartupForm = UcIde +OptionExplicit = True diff --git a/src/test/dos/fixtures/ucide/ucide.frm b/src/test/dos/fixtures/ucide/ucide.frm new file mode 100644 index 00000000..9bc1607a --- /dev/null +++ b/src/test/dos/fixtures/ucide/ucide.frm @@ -0,0 +1,42 @@ +VERSION DVX 1.00 + +Begin Form UcIde + Caption = "UCIDE" + Layout = VBox + AutoSize = True + Resizable = False + + Begin UcMeter UcMeter1 + Value = 9 + End +End + +' 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. + +' ucide.frm -- the UCIDE fixture's form (scripts/ucide.scr): the +' project's own UcMeter with Value set in the designer; the caption +' shows the value the running control has. + +Sub UcIde_Load () + Me.Caption = "UCIDE PASS" + STR$(UcMeter1.Value) +End Sub diff --git a/src/test/dos/fixtures/ucide/uclog.bas b/src/test/dos/fixtures/ucide/uclog.bas new file mode 100644 index 00000000..89c56952 --- /dev/null +++ b/src/test/dos/fixtures/ucide/uclog.bas @@ -0,0 +1,31 @@ +' 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. + +' uclog.bas -- the UCIDE fixture's module (scripts/ucide.scr): DVX.LOG +' for the script, and the count of UcMeter controls made in this VM (the +' designer's or the running program's). + +DECLARE LIBRARY "basrt" + DECLARE SUB dvxLog(BYVAL msg AS STRING) +END DECLARE + +DIM SHARED meterCount AS INTEGER diff --git a/src/test/dos/fixtures/ucide/ucmeter.bmp b/src/test/dos/fixtures/ucide/ucmeter.bmp new file mode 100644 index 00000000..1aefa3e6 --- /dev/null +++ b/src/test/dos/fixtures/ucide/ucmeter.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be4cc8640923cb0214b912845fabb3c7dd03d6c3f1bf53cc4a0a67176da413be +size 1782 diff --git a/src/test/dos/fixtures/ucide/ucmeter.ctl b/src/test/dos/fixtures/ucide/ucmeter.ctl new file mode 100644 index 00000000..efdc58e4 --- /dev/null +++ b/src/test/dos/fixtures/ucide/ucmeter.ctl @@ -0,0 +1,69 @@ +VERSION DVX 1.00 +Begin UserControl UcMeter + Width = 96 + Height = 32 + BackColor = &H00C0FFC0& + ToolboxIcon = "ucmeter.bmp" + Description = "UCIDE meter" + DefaultEvent = "Changed" +End +' 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. + +' ucmeter.ctl -- UcMeter, the UCIDE fixture's own control +' (scripts/ucide.scr). Each one made logs "UCIDE INIT n DESIGN" in the +' IDE's designer (UserMode False) or "UCIDE INIT n RUN" in the running +' program, n counting the controls made in that VM; it draws a bar as +' long as Value. + +Option Explicit + +Public Event Changed(NewValue As Integer) + +Private mValue As Integer + +Public Property Get Value() As Integer + Value = mValue +End Property + +Public Property Let Value(n As Integer) + mValue = n + UserControl.PropertyChanged "Value" + UserControl.Refresh + RaiseEvent Changed(n) +End Property + +Private Sub UserControl_Initialize() + meterCount = meterCount + 1 + mValue = 7 + + If UserControl.UserMode Then + dvxLog "UCIDE INIT" + STR$(meterCount) + " RUN" + Else + dvxLog "UCIDE INIT" + STR$(meterCount) + " DESIGN" + End If +End Sub + +Private Sub UserControl_Paint() + UserControl.Cls + UserControl.Line (0, 0)-(mValue * 8, UserControl.Height - 1), RGB(0, 128, 0), BF +End Sub diff --git a/src/test/dos/runDos.sh b/src/test/dos/runDos.sh index 914a8fa7..f678469a 100755 --- a/src/test/dos/runDos.sh +++ b/src/test/dos/runDos.sh @@ -57,7 +57,7 @@ # that also holds the DVX tree. DOSBox Staging mounts the # staged folder as C: and uses its own mouse driver. Both emulators run # under Xvfb with HOME and the XDG folders inside toolchains/ (emuEnv.sh). -# Needs ./installToolchain.sh, xvfb-run, mtools and python3. +# Needs ./installToolchain.sh, xvfb-run, mtools, flock and python3. # The whole script is one { } block so bash reads all of it before running # any of it: editing this file while a long run is in progress cannot @@ -84,6 +84,9 @@ BIN_DIR="${DVX_ROOT}/bin" # Every script of a run is staged from this private copy of bin/, taken # once the build is done (see takeSnapshot), never from bin/ itself. SNAPSHOT_DIR="${DVX_ROOT}/obj/test/dos/bin.$$" +# sdkTest.sh runs make in the source tree: runs on both emulators at once +# take turns at it +SDK_LOCK="${DVX_ROOT}/obj/test/dos/sdkTest.lock" BASCOMP="${SNAPSHOT_DIR}/host/bascomp" # The module DVX /SCRIPT= loads (the loader names it TEST\SCRIPT.DXE) SCRIPT_MODULE="test/script.dxe" @@ -255,6 +258,9 @@ SETUPLOG_EXPECTS=() # (in a file, never on its command line); "# PROXYLOG: text" lines must be # in the proxy's output after the run. "# STAGE: fixture path" copies a # text file from fixtures/ into the staged DVX tree (DOS line ends). +# "# WIDGET: fixture" installs a control project fixture (a control file +# written in BASIC, built like every fixture) as WIDGETS\\ +# .WGT, so DVX loads it at boot. SECPROXY="${SNAPSHOT_DIR}/host/secproxy" SECLINK_HOST="${HERE}/secLinkHost.py" @@ -270,6 +276,7 @@ SECLINK_PORT="" SECLINK_PIDS=() PROXY_EXPECTS=() STAGES=() +WIDGET_FIXTURES=() BROKEN_FIXTURES=() BOX86_SERIAL2=0 DOSBOX_SERIAL2="disabled" @@ -502,15 +509,17 @@ stageTree() { # sdktest (for sdk.scr); the game templates only for a script with a # "# SDK-GAMES" line (sdkgame.scr), so no other script's Program # Manager changes. A failure there only affects those scripts, so it - # is reported, not fatal. + # is reported, not fatal. Its log goes beside DIR, which is removed + # after the run. local games=0 + local sdkLog="${dir%/*}/sdkTest.log" if grep -q '^#[[:space:]]*SDK-GAMES' "${script}"; then games=1 fi - if ! SDK_PKG_OUT="${dir}/DVX" SDK_PKG_GAMES="${games}" "${DVX_ROOT}/src/test/sdk/sdkTest.sh" "${DVX_ROOT}" > "${dir}/sdkTest.log" 2>&1; then - echo "runDos: warning: sdkTest.sh failed, SDK templates not staged (see ${dir}/sdkTest.log)" + if ! SDK_PKG_OUT="${dir}/DVX" SDK_PKG_GAMES="${games}" flock "${SDK_LOCK}" "${DVX_ROOT}/src/test/sdk/sdkTest.sh" "${DVX_ROOT}" > "${sdkLog}" 2>&1; then + echo "runDos: warning: sdkTest.sh failed, SDK templates not staged (see ${sdkLog})" fi mkdir -p "${dir}/DVX/test" @@ -597,6 +606,14 @@ PY mkdir -p "$(dirname "${dir}/DVX/${stage##* }")" crlf < "${FIXTURES_DIR}/${stage%% *}" > "${dir}/DVX/${stage##* }" done + + # "# WIDGET: fixture": the fixture's control file installed in WIDGETS + local widget + + for widget in "${WIDGET_FIXTURES[@]}"; do + mkdir -p "${dir}/DVX/widgets/${widget}" + cp "${dir}/DVX/test/${widget}.app" "${dir}/DVX/widgets/${widget}/${widget}.wgt" + done } @@ -1348,7 +1365,8 @@ makeNvr() { # NVR_POST_SECONDS; the setup keys then need a moment each. sleep "${NVR_POST_SECONDS}" ( - export DISPLAY=":$(xDisplayOf "${emuPid}")" + DISPLAY=":$(xDisplayOf "${emuPid}")" + export DISPLAY export XAUTHORITY="${dir}/xauth" xdotool search --name "86Box 6" windowfocus sleep 1 @@ -1607,6 +1625,12 @@ for script in "${SCRIPTS[@]}"; do STAGES+=("${expect}") done < <(sed -n 's/^#[[:space:]]*STAGE:[[:space:]]*\([^[:space:]]*\)[[:space:]]\{1,\}\([^[:space:]]*\).*$/\1 \2/p' "${script}") + WIDGET_FIXTURES=() + + while IFS= read -r expect; do + WIDGET_FIXTURES+=("${expect}") + done < <(sed -n 's/^#[[:space:]]*WIDGET:[[:space:]]*\([^[:space:]]*\).*$/\1/p' "${script}") + # "# VIDEO: W H BPP" stages that display mode instead of the test mode; # "# KEEP: welcome" keeps the first-run Welcome window, "# KEEP: sounds" # the desktop sounds. diff --git a/src/test/dos/script/scriptLang.c b/src/test/dos/script/scriptLang.c index ef5fba9a..b83ccd99 100644 --- a/src/test/dos/script/scriptLang.c +++ b/src/test/dos/script/scriptLang.c @@ -249,6 +249,7 @@ static const CommandT sCommands[] = { { "KEYUP", ScriptOpKeyUpE, cmdRawKey }, { "LAUNCH", ScriptOpLaunchE, cmdLaunch }, { "LOG", ScriptOpLogE, cmdLog }, + { "LOGTIME", ScriptOpLogTimeE, cmdLog }, { "LOGWINDOWS", ScriptOpLogWindowsE, cmdNoArgs }, { "MOUSEDOWN", ScriptOpMouseDownE, cmdButton }, { "MOUSEUP", ScriptOpMouseUpE, cmdButton }, diff --git a/src/test/dos/script/scriptLang.h b/src/test/dos/script/scriptLang.h index 8ba21aba..66603c3a 100644 --- a/src/test/dos/script/scriptLang.h +++ b/src/test/dos/script/scriptLang.h @@ -66,6 +66,7 @@ typedef enum { ScriptOpKeyUpE, // KEYUP key: release it at the keyboard (one step per key) ScriptOpLaunchE, // LAUNCH path [args] ScriptOpLogE, // LOG "text" + ScriptOpLogTimeE, // LOGTIME "text": LOG with the time in milliseconds after it (for timing) ScriptOpLogWindowsE, // LOGWINDOWS: log every window and its frame ScriptOpMouseDownE, // MOUSEDOWN [button] ScriptOpMouseUpE, // MOUSEUP [button] diff --git a/src/test/dos/script/scriptRun.c b/src/test/dos/script/scriptRun.c index c157363f..e7f75145 100644 --- a/src/test/dos/script/scriptRun.c +++ b/src/test/dos/script/scriptRun.c @@ -321,6 +321,12 @@ static StepResultE execStep(ScriptRunT *run, const ScriptStepT *step) { dvxLog(SCRIPT_LOG_PREFIX " log: %s", step->text); return StepNextE; + case ScriptOpLogTimeE: + // Milliseconds of the DVX clock: the difference between two + // LOGTIMEs is how long the steps between them took. + dvxLog(SCRIPT_LOG_PREFIX " log: %s %lu ms", step->text, (unsigned long)elapsedMs(0)); + return StepNextE; + case ScriptOpLogWindowsE: logWindows(ctx); return StepNextE; diff --git a/src/test/dos/scripts/fmperf.scr b/src/test/dos/scripts/fmperf.scr new file mode 100644 index 00000000..e326de7d --- /dev/null +++ b/src/test/dos/scripts/fmperf.scr @@ -0,0 +1,44 @@ +# fmperf.scr -- File Manager timing on a large folder. +# +# TEST\BIGDIR.APP (fixtures/bigdir) makes TEST\BIG, 400 files and 20 +# folders. The File Manager opens it, then refreshes it (F5), selects +# every file (Ctrl+A), moves the cursor (Down: the status line is redone) +# and sorts it by size (View > Sort by Size). LOGTIME writes the DVX clock +# before and after each step to DVX.LOG ("FMPERF: "); the +# differences are recorded in claudeDocs/basicPorts.md. Nothing here +# fails on a slow run. A step ends after the frame that handled its key. +# ONLY: 86box +TIMEOUT 300 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/BIGDIR.APP +WAITLOG "BIGDIR: done" 280 +WAIT 10 +LOGTIME "FMPERF: launch" +LAUNCH apps/kpunch/fileMgr/fileMgr.app TEST\BIG +WAITWIN "File Manager - *BIG" +LOGTIME "FMPERF: open" +WAIT 10 +LOGTIME "FMPERF: refresh start" +KEY F5 +WAIT 1 +LOGTIME "FMPERF: refresh end" +WAIT 10 +LOGTIME "FMPERF: select all start" +KEY Ctrl+A +WAIT 1 +LOGTIME "FMPERF: select all end" +WAIT 10 +LOGTIME "FMPERF: down start" +KEY Down +WAIT 1 +LOGTIME "FMPERF: down end" +WAIT 10 +LOGTIME "FMPERF: sort start" +KEY Alt+V S +WAIT 1 +LOGTIME "FMPERF: sort end" +WAIT 10 +KEY Alt+F4 +WAITNOWIN "File Manager - *" +QUIT diff --git a/src/test/dos/scripts/parcp.scr b/src/test/dos/scripts/parcp.scr new file mode 100644 index 00000000..bee329b1 --- /dev/null +++ b/src/test/dos/scripts/parcp.scr @@ -0,0 +1,20 @@ +# parcp.scr -- what Control Panel does, from a BASIC program. +# +# TEST\PARCP.APP (fixtures/parcp) changes the screen mode and back, sets +# a color and a theme and puts them back, sets and clears a wallpaper, +# applies a mouse setup and restores it, and reads the sound events and +# drivers, the joysticks and their settings, the network settings and +# status, the printer models and the file types through system.bas, +# prefs.bas, sound.bas, joystick.bas, net.bas, print.bas and shell.bas. +# Its window title is "PARCP PASS" only when every step worked and every +# library was installed. +TIMEOUT 240 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PARCP.APP +WAITWIN "PARCP *" +WAIT 10 +ASSERTWIN "PARCP PASS" +KEY Alt+F4 +WAITNOWIN "PARCP *" +QUIT diff --git a/src/test/dos/scripts/pardemo.scr b/src/test/dos/scripts/pardemo.scr new file mode 100644 index 00000000..06e7412a --- /dev/null +++ b/src/test/dos/scripts/pardemo.scr @@ -0,0 +1,30 @@ +# pardemo.scr -- what DVX Demo shows, written in BASIC. +# +# TEST\PARDEMO.APP (fixtures/pardemo; its pictures in TEST\PARDEMO) has +# menus with check, radio and unchecked check items, a submenu, shortcut +# keys and context menus, and adds a recent-file item to the File menu at +# run time; it fills every control DVX Demo uses (text boxes with a length +# limit, a password and a mask, check and option buttons, lists, a +# horizontal and a vertical progress bar, tree and list views, a scroll +# pane with padding, a top/bottom splitter holding a left/right one, a +# toolbar of picture buttons, one showing the program's own icon, a status +# bar with panes, an ANSI terminal); it minimizes the terminal window with +# WindowState, opens a second copy of a window, arranges the windows and +# switches the debug layout borders on and off, checking each. The title +# then shows "PARDEMO WAIT input" until a mouse stroke on its PictureBox +# (near the bottom of the window, above the status bar) and Ctrl+N (the +# New item's shortcut) arrive, and reports "PARDEMO PASS checks" only +# when everything worked. +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PARDEMO.APP +WAITWIN "PARDEMO WAIT input" +DRAG "PARDEMO WAIT input" 40 B-55 240 B-55 +KEY Ctrl+N +WAITNOWIN "PARDEMO WAIT input" +SHOT pardemo +ASSERTWIN "PARDEMO PASS *" +KEY Alt+F4 +WAITNOWIN "PARDEMO *" +QUIT diff --git a/src/test/dos/scripts/pardsgn.scr b/src/test/dos/scripts/pardsgn.scr new file mode 100644 index 00000000..38e1cdf5 --- /dev/null +++ b/src/test/dos/scripts/pardsgn.scr @@ -0,0 +1,30 @@ +# pardsgn.scr -- what a form designer and code editor written in BASIC do. +# +# TEST\PARDSGN.APP (fixtures/pardsgn) lists the control types and their +# properties (designer.bas), creates a control of every designable type +# and reads and sets properties by name, moves controls between +# containers, builds a form with the .frm model (copy and paste of a +# control included), loads it, writes the live form back as .frm text +# (FormText$) and uses the TextArea's editor members; then it puts its +# form in design mode ("PARDSGN READY"). The +# click in the middle of the form must reach it as DesignMouse on the +# Surface frame (which is then drawn with its selection handles) and the +# key as DesignKey. The title reports "PARDSGN PASS types" only when +# every step worked. +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PARDSGN.APP +WAITWIN "PARDSGN *" +WAITNOWIN "PARDSGN RUN" +ASSERTWIN "PARDSGN READY" +CLICK 320 290 +WAIT 5 +ASSERTWIN "PARDSGN CLICKED" +SHOT pardsgn +KEY a +WAIT 5 +ASSERTWIN "PARDSGN PASS *" +KEY Alt+F4 +WAITNOWIN "PARDSGN *" +QUIT diff --git a/src/test/dos/scripts/parhlp.scr b/src/test/dos/scripts/parhlp.scr new file mode 100644 index 00000000..66fa58d6 --- /dev/null +++ b/src/test/dos/scripts/parhlp.scr @@ -0,0 +1,26 @@ +# parhlp.scr -- what a Help Viewer written in BASIC needs. +# +# TEST\PARHLP.APP (fixtures/parhlp) compiles TEST\PARHLP\PARHLP.DHS with +# HelpCompile, opens the result with hlpfile.bas, fills a TreeView with +# the table of contents, shows topics in a HelpPane, follows links, +# searches, looks up the index, walks back and forward through its own +# history and picks a topic in the tree; the title reports "PARHLP PASS" +# only when every step worked. Then Tab goes from the tree to the start +# page's link in the pane and Enter follows it: LinkClick shows the +# second page ("PARHLP LINK Second Page"). +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PARHLP.APP +WAITWIN "PARHLP *" +WAIT 10 +SHOT parhlp +ASSERTWIN "PARHLP PASS" +KEY Tab +KEY Enter +WAITWIN "PARHLP LINK *" +SHOT parhlp-link +ASSERTWIN "PARHLP LINK Second Page" +KEY Alt+F4 +WAITNOWIN "PARHLP *" +QUIT diff --git a/src/test/dos/scripts/paride.scr b/src/test/dos/scripts/paride.scr new file mode 100644 index 00000000..70dd973d --- /dev/null +++ b/src/test/dos/scripts/paride.scr @@ -0,0 +1,19 @@ +# paride.scr -- a BASIC program that compiles, debugs and builds another. +# +# TEST\PARIDE.APP (fixtures/paride) uses compiler.bas and debug.bas (the +# basdev library): it compiles the project in TEST\PARIDE\KID, runs it as +# a child program stopped at a breakpoint, checks the line, procedure, +# locals, call stack, an expression and an Immediate statement, steps, +# sets a variable, runs it to its end, reads its output and builds +# TEST\KID.APP. The window title reports "PARIDE PASS 109 built" only +# when every step worked. +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PARIDE.APP +WAITWIN "PARIDE *" +WAIT 10 +ASSERTWIN "PARIDE PASS 109 built" +KEY Alt+F4 +WAITNOWIN "PARIDE *" +QUIT diff --git a/src/test/dos/scripts/parpm.scr b/src/test/dos/scripts/parpm.scr new file mode 100644 index 00000000..d5c197f4 --- /dev/null +++ b/src/test/dos/scripts/parpm.scr @@ -0,0 +1,26 @@ +# parpm.scr -- what a Program Manager written in BASIC needs. +# +# TEST\PARPM.APP (fixtures/parpm) scans the installed programs and reads +# their names, saves and reads back a groups file, reads the system +# information and the file types (setting and removing one), shows the +# programs with their icons in an IconView (a control written in BASIC, +# fixtures/iconview/iconview.ctl, a UserControl of the project; the shot +# shows it), starts a second copy of itself +# minimized and sees the running count and change counter move as it +# starts and ends, and registers Ctrl+Alt+P as a key for every program. +# The key is pressed here once the title shows the steps have run; the +# title leaves "PARPM WAIT" when the second copy has ended and the key was +# seen (or a step failed, or 30 seconds passed) and reports +# "PARPM PASS programs" only when every step worked. +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/PARPM.APP +WAITWIN "PARPM *" +KEY Ctrl+Alt+P +WAITNOWIN "PARPM WAIT" +SHOT parpm +ASSERTWIN "PARPM PASS *" +KEY Alt+F4 +WAITNOWIN "PARPM *" +QUIT diff --git a/src/test/dos/scripts/sndmore.scr b/src/test/dos/scripts/sndmore.scr new file mode 100644 index 00000000..3f2b38c9 --- /dev/null +++ b/src/test/dos/scripts/sndmore.scr @@ -0,0 +1,17 @@ +# sndmore.scr -- the round-2 sound.bas calls on DOS. +# +# TEST\SNDMORE.APP (fixtures/sndmore) makes a sound from binary data with +# zero bytes in it (SoundCreate; its length proves the data arrived whole +# through the 32-bit DECLARE LIBRARY path), plays it panned and faster +# (SoundPlay, SoundSetPan, SoundSetPitch), plays a triangle tone, feeds a +# stream from a Timer (SoundStreamOpen / Write / Queued / Close), takes an +# FM voice when there is an FM chip (FmInfo$, FmAlloc, FmWrite, FmFree) and +# plays DVX's Information sound (SoundPlayEvent). The caption ends +# "Sound more ok ..." or "Sound more failed ". Works with the PC +# speaker (86Box) and with a Sound Blaster (DOSBox). +TIMEOUT 90 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/SNDMORE.APP +WAITWIN "Sound more ok *" 60 +QUIT diff --git a/src/test/dos/scripts/ucdemo.scr b/src/test/dos/scripts/ucdemo.scr new file mode 100644 index 00000000..3f8579b8 --- /dev/null +++ b/src/test/dos/scripts/ucdemo.scr @@ -0,0 +1,29 @@ +# ucdemo.scr -- a program using a control written in BASIC that is +# installed in WIDGETS. +# +# The control project fixtures/iconview (IconView, a UserControl) is built +# into a control file and staged as WIDGETS\ICONVIEW\ICONVIEW.WGT; DVX +# loads it at boot through the BASIC run-time library. TEST\UCDEMO.APP +# (fixtures/ucdemo) has one in its form, finds it in the control types +# list with its toolbox tooltip and icon, adds items and focuses it +# ("UCDEMO READY"). Right, End, Home and Enter must reach the control's +# code and its ItemOpen event the program ("UCDEMO PASS Item 1"). +# WIDGET: iconview +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH TEST/UCDEMO.APP +WAITWIN "UCDEMO *" +WAITNOWIN "UCDEMO RUN" +ASSERTWIN "UCDEMO READY" +KEY Right +KEY End +KEY Home +WAIT 5 +SHOT ucdemo +KEY Enter +WAIT 5 +ASSERTWIN "UCDEMO PASS Item 1" +KEY Alt+F4 +WAITNOWIN "UCDEMO *" +QUIT diff --git a/src/test/dos/scripts/ucide.scr b/src/test/dos/scripts/ucide.scr new file mode 100644 index 00000000..b3440cdc --- /dev/null +++ b/src/test/dos/scripts/ucide.scr @@ -0,0 +1,51 @@ +# ucide.scr -- UserControls in the DVX BASIC IDE. +# +# TEST\UCIDE\UCIDE.DBP (fixtures/ucide) is a program with a control of +# its own, UcMeter (ucmeter.ctl), on its form. Opening the project +# registers UcMeter for the designer; opening the form's designer makes +# the form's UcMeter1 from it, and its UserControl_Initialize logs +# "UCIDE INIT 1 DESIGN" (UserMode False). The toolbox is in alphabetical +# order, UcMeter (its ucmeter.bmp picture, tooltip "UCIDE meter") between +# TreeView and VBox; clicking it and then the form places a second one +# ("UCIDE INIT 2 DESIGN"). F5 runs the program: its own UcMeter +# (UserMode True, "UCIDE INIT 1 RUN"), Value 9 from the .frm in the +# caption ("UCIDE PASS 9"). +# +# The click positions are frame-relative (the IDE's Project window and +# Toolbox at 800 x 600). The Toolbox position was read off the ucide-tbx +# shot of the DOS build, which has more tools than the host IDE suite's +# caseUcDosFixture: UcMeter is the first tool of the last row. +# VIDEO: 800 600 16 +TIMEOUT 120 +WAITWIN "Program Manager" +MOVE 799 599 +LAUNCH apps/kpunch/dvxbasic/dvxbasic.app TEST/UCIDE/UCIDE.DBP +WAITWIN "DVX BASIC*" +WAIT 30 +# The project tree's form row (UCIDE, Forms, ucide.frm), then View > Designer +CLICK "Project" 60 72 +WAIT 10 +KEY Shift+F7 +WAITWIN "UCIDE [Design]" +WAITLOG "UCIDE INIT 1 DESIGN" +# The toolbox fills in its tools after the designer opens. The pointer +# leaves the toolbox first: a tool's tooltip would be in the shot. +WAITMS 5000 +MOVE 799 599 +WAIT 20 +SHOT ucide-tbx +CLICK "Toolbox" 28 371 +WAIT 5 +CLICK "UCIDE [Design]" C C +WAITLOG "UCIDE INIT 2 DESIGN" +MOVE 799 599 +WAIT 10 +SHOT ucide-dsgn +KEY F5 +WAITWIN "UCIDE PASS 9" +WAITLOG "UCIDE INIT 1 RUN" +WAIT 10 +SHOT ucide-run +KEY Alt+F4 +WAITNOWIN "UCIDE PASS 9" +QUIT diff --git a/src/test/dos/scripts/xferperf.scr b/src/test/dos/scripts/xferperf.scr new file mode 100644 index 00000000..6d436c98 --- /dev/null +++ b/src/test/dos/scripts/xferperf.scr @@ -0,0 +1,66 @@ +# xferperf.scr -- ZMODEM transfers in Terminal over the secure link, timed. +# +# COM2 is joined to bin/host/secproxy (REQUIRES: SECLINK), which talks to +# the fake BBS (secLinkHost.py). Terminal connects with a secure-link +# session (STAGE: its TERMINAL.INI, ZMODEM, automatic downloads), the first +# Enter makes the proxy dial the BBS, then: +# FLOOD the BBS sends 1000 lines of colored text (68 KB) and ENQ; the +# answerback tells it (and through COM1 the script) when all of +# it is on the screen +# ZSEND the BBS sends XFERTEST.BIN (128 KB) with lsz; Terminal starts +# the download by itself (DOWNLOAD\XFERTEST.BIN) +# ZRECV the BBS receives with lrz; Terminal sends the downloaded file +# back (Transfer > Send Files...) and the BBS checks every byte +# LOGTIME writes the DVX clock before and after each step to DVX.LOG +# ("XFERPERF: "); the times are recorded in +# claudeDocs/basicPorts.md. Nothing here fails on a slow run. +# REQUIRES: SECLINK +# STAGE: terminal/xferperf.ini config/apps/kpunch/terminal/TERMINAL.INI +# LOG: Terminal: Download: Transfer complete +# LOG: Terminal: Upload: Transfer complete +# PROXYLOG: Handshake complete. +# PROXYLOG: BBS connected. +TIMEOUT 900 +WAITWIN "Program Manager" +MOVE 639 479 +LAUNCH apps/kpunch/terminal/terminal.app +WAITWIN "Transfer Test" +KEY Alt+F C +WAITLOG "Terminal: Secure link established" 60 +KEY Enter +WAITHOST "SECLINK SHOWN" 60 +LOGTIME "XFERPERF: flood start" +TYPE "FLOOD" +KEY Enter +WAITHOST "SECLINK SHOWN" 300 +LOGTIME "XFERPERF: flood end" +LOGTIME "XFERPERF: download start" +TYPE "ZSEND" +KEY Enter +WAITLOG "Terminal: Download: Transfer complete" 600 +LOGTIME "XFERPERF: download end" +WAITHOST "XFER SENT" 60 +WAITWIN "File Transfer" +KEY Alt+F4 +WAITNOWIN "File Transfer" +TYPE "ZRECV" +KEY Enter +KEY Alt+T S +WAITWIN "Send File" +KEY Alt+N +TYPE "DOWNLOAD\XFERTEST.BIN" +KEY Enter +WAITWIN "Send Files" +KEY Alt+N +LOGTIME "XFERPERF: upload start" +WAITLOG "Terminal: Upload: Transfer complete" 600 +LOGTIME "XFERPERF: upload end" +WAITHOST "XFER OK" 60 +WAITWIN "File Transfer" +KEY Alt+F4 +WAITNOWIN "File Transfer" +KEY Alt+F D +WAIT 20 +KEY Alt+F4 +WAITNOWIN "Transfer Test" +QUIT diff --git a/src/test/dos/secLinkHost.py b/src/test/dos/secLinkHost.py index 1978f04b..56000f31 100644 --- a/src/test/dos/secLinkHost.py +++ b/src/test/dos/secLinkHost.py @@ -37,7 +37,18 @@ # comLink.py) and the script's WAITHOST knows the text is on the # screen (and, after the banner, that typing reaches the BBS). # Telnet commands from the proxy are dropped. Everything is logged -# to LOG. +# to LOG. Two lines start ZMODEM transfers with lrzsz +# (toolchains/lrzsz, installed by src/test/terminal/installLrzsz.sh): +# ZSEND sends XFER_NAME (XFER_SIZE bytes of fixed pseudo-random +# data) with lsz, ZRECV receives one file with lrz and checks it is +# that data again. The end of each is logged with its time and +# reported on FEED (XFER_SENT_LINE, XFER_OK_LINE / XFER_BAD_LINE). +# lrzsz runs without its timeouts (-O): the proxy and the sockets +# hold far more than a slow emulated serial line drains in lsz's +# ten seconds. +# FLOOD sends FLOOD_LINES lines of colored text, then ENQ, so the +# answerback (SHOWN_LINE on FEED) times how fast the terminal shows +# text. # secLinkHost.py bridge FIFOBASE PORT # 86Box's COM2 is a named-pipe device (FIFOBASE.out is what the # guest sends, FIFOBASE.in what it receives); this joins it to a TCP @@ -45,8 +56,12 @@ # secproxy itself.) import os +import select +import shutil import socket +import subprocess import sys +import tempfile import threading import time @@ -66,6 +81,28 @@ CHUNK = 4096 CONNECT_TRIES = 100 CONNECT_WAIT = 0.1 +# ZMODEM transfers (bbs): the commands, the test file and what is reported +# (each FEED line fits the 16-byte FIFO of the guest's COM1, which the +# script polls only between frames) +LRZSZ_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "toolchains", "lrzsz", "bin") +ZSEND_CMD = b"ZSEND" +ZRECV_CMD = b"ZRECV" +FLOOD_CMD = b"FLOOD" +FLOOD_LINES = 1000 +FLOOD_LINE = b"\x1b[1;3%dmLine %04d: The quick brown fox jumps over the lazy dog.\x1b[0m\r\n" +FLOOD_COLORS = 7 +XFER_NAME = "XFERTEST.BIN" +XFER_SIZE = 131072 +XFER_SEED = 0x7E57A99 +XFER_MASK = 0xFFFFFFFF +XS_A = 13 +XS_B = 17 +XS_C = 5 +XFER_PROMPT = b"\r\nType a line: " +XFER_SENT_LINE = "XFER SENT\r\n" +XFER_OK_LINE = "XFER OK\r\n" +XFER_BAD_LINE = "XFER BAD\r\n" + def bbs(portFile, logPath, feed): srv = socket.socket() @@ -105,7 +142,16 @@ def bbs(portFile, logPath, feed): state = 1 elif b == CR: log.write("BBS: line '%s'\n" % line.decode("ascii", "replace")) - conn.sendall(REPLY % bytes(line) + ENQ) + + if bytes(line) == ZSEND_CMD: + zmodemSend(conn, log, feed) + elif bytes(line) == ZRECV_CMD: + zmodemReceive(conn, log, feed) + elif bytes(line) == FLOOD_CMD: + flood(conn, log) + else: + conn.sendall(REPLY % bytes(line) + ENQ) + line = bytearray() elif b != LF: line.append(b) @@ -114,8 +160,7 @@ def bbs(portFile, logPath, feed): log.write("BBS: answerback\n") line = bytearray() - with open(feed, "a") as f: - f.write(SHOWN_LINE) + feedLine(feed, SHOWN_LINE) def bridge(fifoBase, port): @@ -154,6 +199,19 @@ def bridge(fifoBase, port): toGuest.write(data) +def feedLine(feed, text): + with open(feed, "a") as f: + f.write(text) + + +# FLOOD: lines of colored text, then ENQ (the answerback comes back once +# they are all on the screen). +def flood(conn, log): + text = b"".join(FLOOD_LINE % (i % FLOOD_COLORS + 1, i) for i in range(FLOOD_LINES)) + conn.sendall(text + XFER_PROMPT + ENQ) + log.write("BBS: flood %d bytes\n" % len(text)) + + def freePort(): s = socket.socket() s.bind((LOCALHOST, 0)) @@ -161,6 +219,122 @@ def freePort(): s.close() +# Run an lrzsz program in cwd with its stdin and stdout as the line: Telnet +# from the proxy is decoded on the way in (commands dropped, IAC IAC is a +# 0xFF byte) and 0xFF is doubled on the way out. Returns the exit code and +# the seconds it ran. +def runLrzsz(conn, argv, cwd): + start = time.monotonic() + child = subprocess.Popen([os.path.join(LRZSZ_DIR, argv[0])] + argv[1:], cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + outFd = child.stdout.fileno() + state = 0 # 0 data, 1 after IAC, 2 option byte, 3 in SB, 4 IAC in SB + running = True + + while running: + ready, _, _ = select.select([conn, outFd], [], []) + + if outFd in ready: + data = os.read(outFd, CHUNK) + + if not data: + running = False + else: + conn.sendall(data.replace(bytes([IAC]), bytes([IAC, IAC]))) + + if conn in ready: + data = conn.recv(CHUNK) + + if not data: + break + + out = bytearray() + + for b in data: + if state == 1: + if b == IAC: + out.append(IAC) + state = 0 + else: + state = 3 if b == SB else (2 if b >= CMD_WITH_OPTION_FIRST else 0) + elif state == 2: + state = 0 + elif state == 3: + state = 4 if b == IAC else 3 + elif state == 4: + state = 0 if b == SE else 3 + elif b == IAC: + state = 1 + else: + out.append(b) + + try: + child.stdin.write(bytes(out)) + child.stdin.flush() + except BrokenPipeError: + running = False + + try: + child.stdin.close() + except BrokenPipeError: + pass + + rc = child.wait() + return rc, time.monotonic() - start + + +# The test file's bytes (xorshift, as the transfer tests make them) +def xferData(): + x = XFER_SEED + data = bytearray(XFER_SIZE) + + for i in range(XFER_SIZE): + x ^= (x << XS_A) & XFER_MASK + x ^= x >> XS_B + x ^= (x << XS_C) & XFER_MASK + data[i] = x & 0xFF + + return bytes(data) + + +# ZRECV: receive one file with lrz and check it is the test file. +def zmodemReceive(conn, log, feed): + work = tempfile.mkdtemp(prefix="bbsrecv") + + try: + rc, secs = runLrzsz(conn, ["lrz", "-q", "-y", "-O"], work) + names = os.listdir(work) + got = b"" + + if len(names) == 1: + with open(os.path.join(work, names[0]), "rb") as f: + got = f.read() + + ok = rc == 0 and got == xferData() + log.write("BBS: zrecv rc=%d %d bytes %.1f s %s\n" % (rc, len(got), secs, "ok" if ok else "BAD")) + feedLine(feed, XFER_OK_LINE if ok else XFER_BAD_LINE) + finally: + shutil.rmtree(work, ignore_errors=True) + + conn.sendall(XFER_PROMPT) + + +# ZSEND: send the test file with lsz. +def zmodemSend(conn, log, feed): + work = tempfile.mkdtemp(prefix="bbssend") + + try: + with open(os.path.join(work, XFER_NAME), "wb") as f: + f.write(xferData()) + + rc, secs = runLrzsz(conn, ["lsz", "-q", "-O", XFER_NAME], work) + log.write("BBS: zsend rc=%d %d bytes %.1f s\n" % (rc, XFER_SIZE, secs)) + feedLine(feed, XFER_SENT_LINE) + finally: + shutil.rmtree(work, ignore_errors=True) + + conn.sendall(XFER_PROMPT) + + def main(): if len(sys.argv) == 2 and sys.argv[1] == "port": freePort() diff --git a/src/test/dvxtest/README.md b/src/test/dvxtest/README.md index 19c553ba..356ccf20 100644 --- a/src/test/dvxtest/README.md +++ b/src/test/dvxtest/README.md @@ -40,7 +40,9 @@ list widgets resolve from the executable at load time. ## API summary Assertions (record pass/fail, print `file:line` plus expected/actual, -never abort): `TEST_TRUE`, `TEST_EQ`, `TEST_STR_EQ`, `TEST_PTR_EQ`. +never abort): `TEST_TRUE`, `TEST_EQ`, `TEST_STR_EQ`, `TEST_PTR_EQ`, and +`TEST_FAIL(msg)` for a path that must not be reached (records a failure +with the message). Lifecycle: `tSuiteBegin(name)`, `tCase(name)`, `tSuiteEnd()` (returns the exit code), `tProcessInit(widgetDir)` (dlopen + wgtRegister every `.so` diff --git a/src/test/dvxtest/dvxtest.c b/src/test/dvxtest/dvxtest.c index 62c82760..5c8f3bd7 100644 --- a/src/test/dvxtest/dvxtest.c +++ b/src/test/dvxtest/dvxtest.c @@ -197,18 +197,20 @@ void textHelpTestReset(void) __attribute__((weak)); // Module state // ============================================================ -static AppContextT *sCtx = NULL; -static const char *sSuiteName = ""; -static const char *sCaseName = ""; -static int32_t sPassCount = 0; -static int32_t sFailCount = 0; -static bool sProcessReady = false; -static SpyHookT *sHooks = NULL; // stb_ds dynamic array -static TDialogScriptFnT sDialogFn = NULL; -static void *sDialogCtx = NULL; +static AppContextT *sCtx = NULL; +static const char *sSuiteName = ""; +static const char *sCaseName = ""; +static int32_t sPassCount = 0; +static int32_t sFailCount = 0; +static bool sProcessReady = false; +static SpyHookT *sHooks = NULL; // stb_ds dynamic array +static TDialogScriptFnT sDialogFn = NULL; +static void *sDialogCtx = NULL; +static void (*sDialogChain)(void *ctx) = NULL; // the idle callback the script's hook replaced +static void *sDialogChainCtx = NULL; static char sTempDir[DVX_MAX_PATH]; static char sSpyLogBuf[SPY_LOG_BUF_SIZE]; -static int32_t sCaseTimeout = -1; // seconds; -1 = not read yet +static int32_t sCaseTimeout = -1; // seconds; -1 = not read yet static const char *const sWidgetSlotNames[WSlotCountE] = { "onClick", "onDblClick", "onChange", "onFocus", "onBlur", "onKeyPress", "onKeyDown", "onKeyUp", "onMouseDown", "onMouseUp", "onMouseMove", "onScroll", "onValidate" @@ -300,11 +302,20 @@ static void caseTimeout(int sig) { } +// The dialog script's idle hook: answer the modal dialog; with none up, +// run the idle callback it replaced (a shell's yields to the app tasks, +// which a program's own event loop relies on while a script is set). +// Not both: another task's idle would see the answered dialog before its +// own loop closed it and answer it again. static void dialogIdle(void *userCtx) { AppContextT *ctx = (AppContextT *)userCtx; - if (sDialogFn && ctx->modalWindow) { - sDialogFn(ctx, ctx->modalWindow, sDialogCtx); + if (ctx->modalWindow) { + if (sDialogFn) { + sDialogFn(ctx, ctx->modalWindow, sDialogCtx); + } + } else if (sDialogChain) { + sDialogChain(sDialogChainCtx); } } @@ -713,11 +724,18 @@ void tDialogScript(TDialogScriptFnT fn, void *userCtx) { sDialogCtx = userCtx; if (fn) { + if (sCtx->idleCallback != dialogIdle) { + sDialogChain = sCtx->idleCallback; + sDialogChainCtx = sCtx->idleCtx; + } + sCtx->idleCallback = dialogIdle; sCtx->idleCtx = sCtx; } else if (sCtx->idleCallback == dialogIdle) { - sCtx->idleCallback = NULL; - sCtx->idleCtx = NULL; + sCtx->idleCallback = sDialogChain; + sCtx->idleCtx = sDialogChainCtx; + sDialogChain = NULL; + sDialogChainCtx = NULL; } } diff --git a/src/test/dvxtest/dvxtest.h b/src/test/dvxtest/dvxtest.h index c8e2243d..9140d75e 100644 --- a/src/test/dvxtest/dvxtest.h +++ b/src/test/dvxtest/dvxtest.h @@ -47,6 +47,7 @@ // file:line with expected/actual in a diff-style layout. They never // abort the case; check tSuiteEnd's return value for the verdict. #define TEST_TRUE(cond) tCheckTrue(__FILE__, __LINE__, #cond, (cond)) +#define TEST_FAIL(msg) tCheckTrue(__FILE__, __LINE__, (msg), false) #define TEST_EQ(a, b) tCheckEq(__FILE__, __LINE__, #a " == " #b, (int64_t)(a), (int64_t)(b)) #define TEST_STR_EQ(a, b) tCheckStrEq(__FILE__, __LINE__, #a " == " #b, (a), (b)) #define TEST_PTR_EQ(a, b) tCheckPtrEq(__FILE__, __LINE__, #a " == " #b, (const void *)(a), (const void *)(b)) diff --git a/src/test/dvxtest/dvxtest.mk b/src/test/dvxtest/dvxtest.mk index cbf3842f..12089d1e 100644 --- a/src/test/dvxtest/dvxtest.mk +++ b/src/test/dvxtest/dvxtest.mk @@ -35,7 +35,11 @@ # dvxtest-libs (order-only: also builds every widget .so), and rules for # the other host archives a suite may link: DVXTEST_TASKS_A (libtasks.a), # DVXTEST_SQL_A (dvxsql.a), DVXTEST_BASRT_A (basrt.a) plus the phony -# dvxtest-apps (the C app .so files). Each archive rule always runs its +# dvxtest-apps (the C app .so files, and clock.so: the shell suites' +# stand-in for the Clock program, which is BASIC), and for a suite that +# runs a BASIC program: DVXTEST_BASSTUB_SO (the BASIC stub as a host .so; +# the program's resources are appended to a copy) and the phony +# dvxtest-bascomp (the host compiler, DVXTEST_BASCOMP). Each archive rule always runs its # library's sub-make, and make re-reads the archive's time stamp # afterwards, so dependents relink exactly when the archive changed. # @@ -54,6 +58,7 @@ DVXTEST_LIBDVX := $(DVXTEST_ROOT)/src/libs/kpunch/libdvx DVXTEST_LIBTASKS := $(DVXTEST_ROOT)/src/libs/kpunch/libtasks DVXTEST_TEXTHELP := $(DVXTEST_ROOT)/src/libs/kpunch/texthelp DVXTEST_LISTHELP := $(DVXTEST_ROOT)/src/libs/kpunch/listhelp +DVXTEST_HLPFILE := $(DVXTEST_ROOT)/src/libs/kpunch/hlpfile DVXTEST_SQLDIR := $(DVXTEST_ROOT)/src/libs/kpunch/sql DVXTEST_BASICDIR := $(DVXTEST_ROOT)/src/apps/kpunch/dvxbasic DVXTEST_APPSDIR := $(DVXTEST_ROOT)/src/apps/kpunch @@ -66,18 +71,29 @@ DVXTEST_TASKS_A := $(DVXTEST_OBJDIR)/libtasks.a DVXTEST_SQL_A := $(DVXTEST_OBJDIR)/dvxsql.a DVXTEST_BASRT_A := $(DVXTEST_OBJDIR)/basrt.a DVXTEST_ARCHIVES := $(DVXTEST_LIBDVX_A) $(DVXTEST_LIB) +DVXTEST_BASSTUB_SO:= $(DVXTEST_BINDIR)/basstub.so +DVXTEST_BASCOMP := $(DVXTEST_ROOT)/bin/host/bascomp +DVXTEST_BASCOMPREL:= ../../../../bin/host/bascomp +DVXTEST_CLOCK_SRC := $(DVXTEST_ROOT)/src/test/shell/fixtures/fixClock.c +DVXTEST_CLOCK_SO := $(DVXTEST_OBJDIR)/apps/clock.so DVXTEST_INCLUDES := -I$(DVXTEST_DIR) -I$(DVXTEST_LIBDVX) -I$(DVXTEST_LIBDVX)/platform -I$(DVXTEST_LIBDVX)/thirdparty -I$(DVXTEST_LIBTASKS) -I$(DVXTEST_WIDGETS) -I$(DVXTEST_TEXTHELP) -I$(DVXTEST_LISTHELP) -DVXTEST_CFLAGS := $(HOST_CFLAGS) $(DVXTEST_INCLUDES) -DDVXTEST_WIDGET_DIR='"$(DVXTEST_WIDGET_DIR)"' -DDVXTEST_GOLDEN_DIR='"$(DVXTEST_GOLDEN_DIR)"' +# The help compiler's every-directive fixture (src/test/tools), compiled +# once for the suites that read help files (hlpfile, widgets, formrt). +DVXTEST_HLPC := $(DVXTEST_ROOT)/bin/host/dvxhlpc +DVXTEST_HELP_SRC := $(DVXTEST_ROOT)/src/test/tools/fixtures/help +DVXTEST_HELP_HLP := $(DVXTEST_BINDIR)/help/fixture.hlp + +DVXTEST_CFLAGS := $(HOST_CFLAGS) $(DVXTEST_INCLUDES) -DDVXTEST_WIDGET_DIR='"$(DVXTEST_WIDGET_DIR)"' -DDVXTEST_GOLDEN_DIR='"$(DVXTEST_GOLDEN_DIR)"' -DDVXTEST_HELP_HLP='"$(DVXTEST_HELP_HLP)"' DVXTEST_LDFLAGS := -rdynamic $(HOST_LDFLAGS) DVXTEST_LINK := $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm # Sources folded into libdvxtest.a: the runner plus the helper libraries # widget .so files resolve from the executable at load time. -DVXTEST_LIB_SRCS := $(DVXTEST_DIR)/dvxtest.c $(DVXTEST_TEXTHELP)/textHelp.c $(DVXTEST_LISTHELP)/listHelp.c +DVXTEST_LIB_SRCS := $(DVXTEST_DIR)/dvxtest.c $(DVXTEST_TEXTHELP)/textHelp.c $(wildcard $(DVXTEST_LISTHELP)/*.c) $(wildcard $(DVXTEST_HLPFILE)/*.c) DVXTEST_LIB_OBJS := $(patsubst %.c,$(DVXTEST_BINDIR)/lib/%.o,$(notdir $(DVXTEST_LIB_SRCS))) -.PHONY: dvxtest-libs dvxtest-widgets dvxtest-apps dvxtest-clean dvxtest-force +.PHONY: dvxtest-libs dvxtest-widgets dvxtest-apps dvxtest-bascomp dvxtest-clean dvxtest-force dvxtest-hlpc # Host core, widget shared objects and the runner archive. dvxtest-libs: $(DVXTEST_ARCHIVES) dvxtest-widgets @@ -85,9 +101,31 @@ dvxtest-libs: $(DVXTEST_ARCHIVES) dvxtest-widgets dvxtest-widgets: $(MAKE) -C $(DVXTEST_WIDGETS) host SAN=$(SAN) -dvxtest-apps: +dvxtest-apps: $(DVXTEST_CLOCK_SO) $(MAKE) -C $(DVXTEST_APPSDIR) host SAN=$(SAN) +$(DVXTEST_CLOCK_SO): $(DVXTEST_CLOCK_SRC) $(DVX_COMMON_MK) $(DVXTEST_DIR)/dvxtest.mk + @mkdir -p $(dir $@) + $(HOSTCC) $(DVXTEST_CFLAGS) -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -fPIC -shared $(HOST_APP_LDFLAGS) -o $@ $< + +# The runtime it calls is linked into the suite (DVXTEST_BASRT_A, whole +# archive, exported). +$(DVXTEST_BASSTUB_SO): $(DVXTEST_BASICDIR)/stub/basstub.c $(DVXTEST_BASICDIR)/stub/basModuleRes.c $(DVX_COMMON_MK) $(DVXTEST_DIR)/dvxtest.mk + @mkdir -p $(dir $@) + $(HOSTCC) $(DVXTEST_CFLAGS) -I$(DVXTEST_BASICDIR) -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -fPIC -shared $(HOST_APP_LDFLAGS) -o $@ $(DVXTEST_BASICDIR)/stub/basstub.c $(DVXTEST_BASICDIR)/stub/basModuleRes.c + +# The help fixture is rebuilt with the host help compiler each run (it +# takes a moment), so a compiler change is always tested. +$(DVXTEST_HELP_HLP): dvxtest-hlpc + @mkdir -p $(dir $@) + cd $(DVXTEST_HELP_SRC) && $(DVXTEST_HLPC) -o $@ --quiet -i . main.dhs @inputs.txt + +dvxtest-hlpc: + $(MAKE) -C $(DVXTEST_ROOT)/src/tools host COV=0 + +dvxtest-bascomp: + $(MAKE) -C $(DVXTEST_BASICDIR) $(DVXTEST_BASCOMPREL) + $(DVXTEST_LIBDVX_A): dvxtest-force $(MAKE) -C $(DVXTEST_LIBDVX) host SAN=$(SAN) @@ -112,7 +150,7 @@ endef $(foreach src,$(DVXTEST_LIB_SRCS),$(eval $(call DVXTEST_LIB_RULE,$(src)))) --include $(DVXTEST_LIB_OBJS:.o=.d) +-include $(DVXTEST_LIB_OBJS:.o=.d) $(DVXTEST_CLOCK_SO:.so=.d) $(DVXTEST_BASSTUB_SO:.so=.d) dvxtest-clean: rm -rf $(DVXTEST_BINDIR)/lib $(DVXTEST_LIB) diff --git a/src/test/fileops/Makefile b/src/test/fileops/Makefile index a358e810..f140920d 100644 --- a/src/test/fileops/Makefile +++ b/src/test/fileops/Makefile @@ -22,7 +22,8 @@ # File operations library and File Manager tests. # fileOpsTest -- the library against a temporary directory -# fileMgrTest -- the File Manager app driven through the shell fixture +# fileMgrTest -- the File Manager program (compiled from its project, +# run by the BASIC stub) driven through the shell fixture include ../dvxtest/dvxtest.mk @@ -42,7 +43,7 @@ OPS_TEST = $(BINDIR)/fileOpsTest MGR_TEST = $(BINDIR)/fileMgrTest LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) -CFLAGS = $(DVXTEST_CFLAGS) -fno-omit-frame-pointer -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(SHELLDIR) -I$(SHELLTESTDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(BINDIR)"' +CFLAGS = $(DVXTEST_CFLAGS) -fno-omit-frame-pointer -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(SHELLDIR) -I$(SHELLTESTDIR) -I$(DVXTEST_BASICDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(BINDIR)"' -DSHELLTEST_BASSTUB_SO='"$(DVXTEST_BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(DVXTEST_BASCOMP)"' .PHONY: all run clean host-tools @@ -64,9 +65,9 @@ $(OPS_TEST): fileOpsTest.c $(FILEOPS_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest- @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -o $@ $< $(FILEOPS_OBJS) $(DVXTEST_LINK) -$(MGR_TEST): fileMgrTest.c $(FILEOPS_OBJS) $(SHELL_OBJS) $(SHFIX_OBJ) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs dvxtest-apps +$(MGR_TEST): fileMgrTest.c $(FILEOPS_OBJS) $(SHELL_OBJS) $(SHFIX_OBJ) $(DVX_MAKEFILES) $(LIBS_A) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) | dvxtest-libs dvxtest-apps dvxtest-bascomp @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -o $@ $< $(FILEOPS_OBJS) $(SHELL_OBJS) $(SHFIX_OBJ) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(CFLAGS) -o $@ $< $(FILEOPS_OBJS) $(SHELL_OBJS) $(SHFIX_OBJ) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_BASRT_A) $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm host-tools: $(MAKE) -C $(DVXTEST_ROOT)/src/tools host COV=0 diff --git a/src/test/fileops/fileMgrTest.c b/src/test/fileops/fileMgrTest.c index f0c0b2fe..22e59f53 100644 --- a/src/test/fileops/fileMgrTest.c +++ b/src/test/fileops/fileMgrTest.c @@ -22,15 +22,19 @@ // fileMgrTest.c -- File Manager driven through the shell fixture // -// The app runs as it does under DVX (loaded by the shell, its copy jobs on -// their own tasks) against a data folder in the fixture's temporary -// working directory. Dialogs are answered by a script that can type into -// an input box and press a named button. +// The program is compiled from its project and run by the BASIC stub as +// it is under DVX (loaded by the shell, its copy jobs run by their +// windows' timers), with its library calls (fileops, the shell) made for +// real, against a data folder in the fixture's temporary working +// directory. Dialogs are answered by a script that can type into an +// input box, tick a check box and press a named button. #include "shellFixture.h" +#include "formrt/formrt.h" #include "shellAssoc.h" #include "dvxDlg.h" #include "dvxPlat.h" +#include "dvxPlatHost.h" #include "taskSwch.h" #include "checkbox/checkbox.h" @@ -43,24 +47,27 @@ #include #define APP_FILEMGR "apps/kpunch/fileMgr/fileMgr.app" +#define FM_DBP "src/apps/kpunch/fileMgr/fileMgr.dbp" // under fixRepoRoot #define APP_CLOCK_DIR "apps/kpunch/clock" #define FM_TITLE_PREFIX "File Manager - " #define JOB_FRAMES_MAX 2000 +#define JOB_FRAME_MS 10 // clock advance per frame: the job windows' timers fire +#define START_FRAMES_MAX 400 #define TEXT_MAX DVX_MAX_PATH #define MODE_DIR 0755 -// File Manager command IDs (fileMgr.c) -#define CMD_FILE_NEW_FOLDER 101 -#define CMD_FILE_RENAME 104 -#define CMD_FILE_DELETE 105 -#define CMD_FILE_PROPERTIES 106 -#define CMD_FILE_NEW_WINDOW 107 -#define CMD_EDIT_PASTE 202 -#define CMD_EDIT_SELECT_ALL 203 -#define CMD_EDIT_INVERT 204 -#define CMD_VIEW_FILTER 301 -#define CMD_VIEW_SORT_SIZE 311 -#define CMD_VIEW_SORT_DESC 314 +// File Manager menu captions (fileMgr.frm) +#define MENU_NEW_FOLDER "New &Folder..." +#define MENU_RENAME "&Rename..." +#define MENU_DELETE "&Delete" +#define MENU_PROPERTIES "P&roperties" +#define MENU_NEW_WINDOW "New &Window" +#define MENU_PASTE "&Paste" +#define MENU_SELECT_ALL "Select &All" +#define MENU_INVERT "&Invert Selection" +#define MENU_FILTER "&Filter..." +#define MENU_SORT_SIZE "Sort by &Size" +#define MENU_SORT_DESC "D&escending" // Dialog script: optionally type text into the input after a prompt, @@ -85,10 +92,12 @@ static void caseErrors(void); static void caseFolderOps(void); static void caseKeysAndProperties(void); static void caseOpenAndTree(void); +static void closeManagers(void); static bool exists(const char *path); static int32_t launch(const char *dir); static WidgetT *listOf(int32_t appId); static void makeData(void); +static void menu(WindowT *win, const char *caption); static void pumpUntilNoWindow(const char *title); static int32_t rowOf(WidgetT *list, const char *name); static void scriptOn(ScriptT *s, const char *prompt, const char *text, const char *check, const char *button); @@ -139,23 +148,23 @@ static void caseBrowse(void) { TEST_EQ(wgtListViewGetRowCount(list), 3); // Sort by size, then descending (folders stay first). - win->onMenu(win, CMD_VIEW_SORT_SIZE); + menu(win, MENU_SORT_SIZE); TEST_STR_EQ(wgtListViewGetCell(list, 1, 0), "alpha.txt"); - win->onMenu(win, CMD_VIEW_SORT_DESC); + menu(win, MENU_SORT_DESC); TEST_STR_EQ(wgtListViewGetCell(list, 0, 0), "sub"); TEST_STR_EQ(wgtListViewGetCell(list, 1, 0), "beta.bat"); // Selection: all, then invert. - win->onMenu(win, CMD_EDIT_SELECT_ALL); + menu(win, MENU_SELECT_ALL); TEST_STR_EQ(statusOf(id), "3 of 3 selected (55 bytes)"); - win->onMenu(win, CMD_EDIT_INVERT); + menu(win, MENU_INVERT); TEST_STR_EQ(statusOf(id), "3 items"); // Filter. ScriptT s; scriptOn(&s, "Show only files matching:", "*.txt", NULL, "OK"); - win->onMenu(win, CMD_VIEW_FILTER); + menu(win, MENU_FILTER); fixDialogsOff(); TEST_EQ(s.answered, 1); TEST_EQ(wgtListViewGetRowCount(list), 2); @@ -167,6 +176,7 @@ static void caseBrowse(void) { fixPumpShell(SETTLE_FRAMES); TEST_EQ(wgtListViewGetRowCount(list), 3); + closeManagers(); fixShellDown(); } @@ -204,7 +214,7 @@ static void caseCopyPaste(void) { TEST_TRUE(clip && strncmp(clip, "DVX-FILES COPY\n", strlen("DVX-FILES COPY\n")) == 0); - winB->onMenu(winB, CMD_EDIT_PASTE); + menu(winB, MENU_PASTE); pumpUntilNoWindow("Copying"); TEST_TRUE(exists("dest/alpha.txt")); TEST_TRUE(exists("dest/sub/inner.txt")); @@ -214,7 +224,7 @@ static void caseCopyPaste(void) { // Again: the file exists. No keeps it (answered from the job's task). TEST_TRUE(tWriteFile("dest/alpha.txt", "old", 3)); scriptOn(&s, NULL, NULL, NULL, "No"); - winB->onMenu(winB, CMD_EDIT_PASTE); + menu(winB, MENU_PASTE); pumpUntilNoWindow("Copying"); fixDialogsOff(); TEST_TRUE(s.answered >= 1); @@ -228,7 +238,7 @@ static void caseCopyPaste(void) { wgtSetFocused(listA); selectOnly(listA, "beta.bat"); tWidgetKey(winA, KEY_CTRL_X, KEY_MOD_CTRL); - winB->onMenu(winB, CMD_EDIT_PASTE); + menu(winB, MENU_PASTE); pumpUntilNoWindow("Moving"); TEST_TRUE(exists("dest/beta.bat")); TEST_TRUE(!exists("data/beta.bat")); @@ -239,10 +249,11 @@ static void caseCopyPaste(void) { // New Window opens a second window on the same folder. int32_t before = shellRunningAppCount(); - winA->onMenu(winA, CMD_FILE_NEW_WINDOW); + menu(winA, MENU_NEW_WINDOW); fixPumpShell(SETTLE_FRAMES); TEST_EQ(shellRunningAppCount(), before + 1); + closeManagers(); fixShellDown(); } @@ -272,7 +283,7 @@ static void caseErrors(void) { TEST_TRUE(strstr(win->title, "data") != NULL); dvxClipboardCopy("plain text", (int32_t)strlen("plain text")); - win->onMenu(win, CMD_EDIT_PASTE); + menu(win, MENU_PASTE); TEST_EQ(s.answered, 2); // A folder that cannot be listed at start is reported, then the first @@ -282,6 +293,7 @@ static void caseErrors(void) { TEST_TRUE(other > 0); TEST_EQ(s.answered, 3); fixDialogsOff(); + closeManagers(); fixShellDown(); } @@ -304,7 +316,7 @@ static void caseFolderOps(void) { } scriptOn(&s, "Name of the new folder:", "made", NULL, "OK"); - win->onMenu(win, CMD_FILE_NEW_FOLDER); + menu(win, MENU_NEW_FOLDER); fixDialogsOff(); TEST_TRUE(exists("data/made")); TEST_TRUE(rowOf(list, "made") >= 0); @@ -312,7 +324,7 @@ static void caseFolderOps(void) { wgtSetFocused(list); selectOnly(list, "made"); scriptOn(&s, "New name for made:", "renamed", NULL, "OK"); - win->onMenu(win, CMD_FILE_RENAME); + menu(win, MENU_RENAME); fixDialogsOff(); TEST_TRUE(exists("data/renamed")); TEST_TRUE(!exists("data/made")); @@ -320,14 +332,14 @@ static void caseFolderOps(void) { // A bad name is refused with a message. selectOnly(list, "renamed"); scriptOn(&s, "New name for renamed:", "a/b", NULL, "OK"); - win->onMenu(win, CMD_FILE_RENAME); + menu(win, MENU_RENAME); fixDialogsOff(); TEST_EQ(s.answered, 2); TEST_TRUE(exists("data/renamed")); selectOnly(list, "renamed"); scriptOn(&s, NULL, NULL, NULL, "Yes"); - win->onMenu(win, CMD_FILE_DELETE); + menu(win, MENU_DELETE); fixDialogsOff(); pumpUntilNoWindow("Deleting"); TEST_TRUE(!exists("data/renamed")); @@ -340,6 +352,7 @@ static void caseFolderOps(void) { fixDialogsOff(); TEST_TRUE(exists("data/alpha.txt")); + closeManagers(); fixShellDown(); } @@ -365,7 +378,7 @@ static void caseKeysAndProperties(void) { selectOnly(list, "alpha.txt"); scriptOn(&s, NULL, NULL, "Read-only", "OK"); s.expectLabel = "Size: 5 bytes"; - win->onMenu(win, CMD_FILE_PROPERTIES); + menu(win, MENU_PROPERTIES); fixDialogsOff(); TEST_EQ(s.answered, 1); TEST_EQ(s.sawLabel, 1); @@ -384,6 +397,7 @@ static void caseKeysAndProperties(void) { TEST_EQ(stat("data/alpha.txt", &st), 0); TEST_EQ(st.st_mode & S_IWUSR, 0); + closeManagers(); fixShellDown(); } @@ -466,10 +480,27 @@ static void caseOpenAndTree(void) { TEST_TRUE(rowOf(list, "kpunch") >= 0); } + closeManagers(); fixShellDown(); } +// Close every File Manager window like a user would, so the programs end +// and free their runtimes before the shell goes down. +static void closeManagers(void) { + AppContextT *ctx = tCtx(); + + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + WindowT *win = ctx->stack.windows[i]; + + if (strncmp(win->title, FM_TITLE_PREFIX, strlen(FM_TITLE_PREFIX)) == 0 && !win->destroyPending) { + TEST_TRUE(fixCloseApp(win)); + i = ctx->stack.count; + } + } +} + + static bool exists(const char *path) { struct stat st; @@ -477,10 +508,21 @@ static bool exists(const char *path) { } -// Start a File Manager on dir; returns its app id. +// Start a File Manager on dir and run until its window shows a folder; +// returns its app id. static int32_t launch(const char *dir) { int32_t id = shellLoadAppWithArgs(tCtx(), APP_FILEMGR, dir); + for (int32_t i = 0; i < START_FRAMES_MAX; i++) { + WindowT *win = fixWindowForApp(id); + + if (win && win->visible && strncmp(win->title, FM_TITLE_PREFIX, strlen(FM_TITLE_PREFIX)) == 0) { + break; + } + + fixPumpShell(1); + } + fixPumpShell(SETTLE_FRAMES); return id; } @@ -494,11 +536,11 @@ static WidgetT *listOf(int32_t appId) { // data/{alpha.txt (5), beta.bat (50), sub/inner.txt}, dest/ (empty); the -// File Manager app linked into the fixture tree. +// File Manager program built into the fixture tree. static void makeData(void) { char cwd[TEXT_MAX]; - fixLinkApp("fileMgr"); + TEST_TRUE(fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, FM_DBP, APP_FILEMGR)); TEST_EQ(mkdir("data", MODE_DIR), 0); TEST_EQ(mkdir("data/sub", MODE_DIR), 0); TEST_EQ(mkdir("dest", MODE_DIR), 0); @@ -511,9 +553,25 @@ static void makeData(void) { } -// Run the shell (and so the job tasks) until the window has gone. +// Choose a menu command by its caption, as the window's program (the +// shell attributes a window's callbacks to its app). +static void menu(WindowT *win, const char *caption) { + AppContextT *ctx = tCtx(); + int32_t id = fixMenuId(win, caption); + int32_t saved = ctx->currentAppId; + + TEST_TRUE(id >= 0); + ctx->currentAppId = win->appId; + win->onMenu(win, id); + ctx->currentAppId = saved; +} + + +// Run the shell, the test clock moving (so the job windows' timers run +// the jobs), until the window has gone. static void pumpUntilNoWindow(const char *title) { for (int32_t i = 0; i < JOB_FRAMES_MAX; i++) { + hostClockAdvance(JOB_FRAME_MS); fixPumpShell(1); if (!tFindWindow(title)) { @@ -603,6 +661,8 @@ int main(void) { return 1; } + basNativeHostSetLive(true); + caseBrowse(); caseFolderOps(); caseOpenAndTree(); diff --git a/src/test/fileops/fileOpsTest.c b/src/test/fileops/fileOpsTest.c index bc55c245..14659bdc 100644 --- a/src/test/fileops/fileOpsTest.c +++ b/src/test/fileops/fileOpsTest.c @@ -51,6 +51,12 @@ #define MODE_READONLY 0444 #define MODE_FILE 0644 #define LONG_NAME_LEN (DVX_MAX_PATH - 8) +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define NO_DRIVE (-1) // FileOpsDriveType of a drive that is not there +#define JOB_SLICE_MS 1000 +#define STAMP_MINUTES "2001-02-03 04:05" +#define STAMP_SECONDS "2001-02-03 04:05:06" // Scripted answers for runScripted typedef struct { @@ -68,6 +74,7 @@ typedef struct { static void caseAttributes(void); static void caseBasic(void); +static void caseBasicPlatform(void); static void caseCancel(void); static void caseClearAttr(void); static void caseConflicts(void); @@ -169,6 +176,131 @@ static void caseBasic(void) { } +// The fileops.bas entry points added on the platform layer (drives, +// disks, paths, filters, the temp folder), a job's byte progress and its +// attribute clearing, and setting a file's time from FileOpsModified$'s +// text. +static void caseBasicPlatform(void) { + tCase("BASIC entry points: drives, paths, filters, job bytes, times"); + + makeDir(tp("bp")); + makeDir(tp("bp/src")); + makeDir(tp("bp/dst")); + makeFile(tp("bp/src/a.txt"), MID_FILE_BYTES, 'a'); + TEST_EQ(chmod(tp("bp/src/a.txt"), MODE_READONLY), 0); + + // Drives and disks: the host has one drive, "/", and no boot drive + TEST_EQ(FileOpsDriveType("/"), PlatformDriveFixedE); + TEST_EQ(FileOpsDriveType("Q:"), NO_DRIVE); + TEST_EQ(FileOpsDriveType(""), NO_DRIVE); + TEST_EQ(FileOpsDriveType(NULL), NO_DRIVE); + TEST_STR_EQ(FileOpsBootDrive(), ""); + TEST_TRUE(FileOpsTotalBytes(tTempDir()) > 0); + TEST_TRUE(FileOpsTotalBytes(tTempDir()) >= FileOpsFreeBytes(tTempDir())); + TEST_EQ(FileOpsIsWritable(tp("bp")), BAS_TRUE); + TEST_EQ(FileOpsIsWritable(tp("bp/none")), BAS_FALSE); + TEST_EQ(FileOpsIsWritable(""), BAS_FALSE); + TEST_EQ(FileOpsIsWritable(NULL), BAS_FALSE); + platformSetReadOnly(true); + TEST_EQ(FileOpsReadOnlySystem(), BAS_TRUE); + platformSetReadOnly(false); + TEST_EQ(FileOpsReadOnlySystem(), BAS_FALSE); + + // The temp folder follows TEMP, then TMP; "" with neither + char savedTemp[DVX_MAX_PATH]; + char savedTmp[DVX_MAX_PATH]; + const char *envTemp = getenv("TEMP"); + const char *envTmp = getenv("TMP"); + + snprintf(savedTemp, sizeof(savedTemp), "%s", envTemp ? envTemp : ""); + snprintf(savedTmp, sizeof(savedTmp), "%s", envTmp ? envTmp : ""); + unsetenv("TEMP"); + unsetenv("TMP"); + TEST_STR_EQ(FileOpsTempDir(), ""); + setenv("TMP", tp("bp"), 1); + TEST_STR_EQ(FileOpsTempDir(), tp("bp")); + setenv("TEMP", tp("bp/src"), 1); + TEST_STR_EQ(FileOpsTempDir(), tp("bp/src")); + unsetenv("TEMP"); + unsetenv("TMP"); + + if (savedTemp[0]) { + setenv("TEMP", savedTemp, 1); + } + + if (savedTmp[0]) { + setenv("TMP", savedTmp, 1); + } + + // Paths and filters + TEST_EQ(FileOpsPathIsAbsolute("/x/y"), BAS_TRUE); + TEST_EQ(FileOpsPathIsAbsolute("C:\\DOCS"), BAS_TRUE); + TEST_EQ(FileOpsPathIsAbsolute("docs/a.txt"), BAS_FALSE); + TEST_EQ(FileOpsPathIsAbsolute(NULL), BAS_FALSE); + TEST_EQ(FileOpsPathIsInside(tp("bp/src/a.txt"), tp("bp")), BAS_TRUE); + TEST_EQ(FileOpsPathIsInside(tp("bp"), tp("bp")), BAS_TRUE); + TEST_EQ(FileOpsPathIsInside(tp("bp"), tp("bp/src")), BAS_FALSE); + TEST_EQ(FileOpsPathIsInside(NULL, tp("bp")), BAS_FALSE); + TEST_STR_EQ(FileOpsPathSep(), DVX_PATH_SEP); + TEST_EQ(FileOpsFilterMatch("A.TXT", "*.txt"), BAS_TRUE); + TEST_EQ(FileOpsFilterMatch("a.bmp", "*.txt;*.bmp"), BAS_TRUE); + TEST_EQ(FileOpsFilterMatch("a.png", "*.txt;*.bmp"), BAS_FALSE); + TEST_EQ(FileOpsFilterMatch("a.png", ""), BAS_TRUE); + TEST_EQ(FileOpsFilterMatch("a.png", PLATFORM_FILTER_ALL), BAS_TRUE); + TEST_EQ(FileOpsFilterMatch(NULL, "*.txt"), BAS_FALSE); + + // A copy job: byte progress, and the read-only bit cleared on the copy + int32_t job = FileOpsJobCreate(FileOpsCopyE, tp("bp/dst")); + + TEST_TRUE(job != 0); + TEST_EQ(FileOpsJobAddSource(job, tp("bp/src")), BAS_TRUE); + FileOpsJobClearAttr(job, FILEOPS_ATTR_READONLY); + TEST_TRUE(FileOpsJobBytesDone(job) == 0); + + int32_t step = FileOpsStepBusyE; + + for (int32_t i = 0; i < STEP_LIMIT && step == FileOpsStepBusyE; i++) { + step = FileOpsJobRunFor(job, JOB_SLICE_MS); + } + + TEST_EQ(step, FileOpsStepDoneE); + TEST_TRUE(FileOpsJobBytesTotal(job) == MID_FILE_BYTES); + TEST_TRUE(FileOpsJobBytesDone(job) == MID_FILE_BYTES); + TEST_EQ(FileOpsGetAttr(tp("bp/dst/src/a.txt")) & FILEOPS_ATTR_READONLY, 0); + TEST_EQ(FileOpsGetAttr(tp("bp/src/a.txt")) & FILEOPS_ATTR_READONLY, FILEOPS_ATTR_READONLY); + FileOpsJobFree(job); + TEST_TRUE(FileOpsJobBytesTotal(job) == 0); + TEST_TRUE(FileOpsJobBytesDone(0) == 0); + FileOpsJobClearAttr(0, FILEOPS_ATTR_READONLY); + + // Modification times in FileOpsModified$'s form + const char *f = tp("bp/dst/src/a.txt"); + time_t t = 0; + char text[FILEOPS_DATE_TEXT_MAX]; + + TEST_EQ(FileOpsSetModified(f, STAMP_MINUTES), BAS_TRUE); + TEST_STR_EQ(FileOpsModified(f), STAMP_MINUTES); + TEST_EQ(FileOpsSetModified(f, STAMP_SECONDS), BAS_TRUE); + TEST_STR_EQ(FileOpsModified(f), STAMP_MINUTES); + TEST_EQ(FileOpsSetModified(f, "2001-02-30 04:05"), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, "2001-13-01 04:05"), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, "2001-02-03 24:05"), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, "2001-02-03"), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, "2001-02-03 04:05 pm"), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, "2001-02-03 04:05:"), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, ""), BAS_FALSE); + TEST_EQ(FileOpsSetModified(f, NULL), BAS_FALSE); + TEST_EQ(FileOpsSetModified(tp("bp/missing.txt"), STAMP_MINUTES), BAS_FALSE); + TEST_EQ(FileOpsSetModified(NULL, STAMP_MINUTES), BAS_FALSE); + + // fileOpsParseDate reads back what fileOpsFormatDate writes + fileOpsFormatDate(OLD_TIME, text, sizeof(text)); + TEST_TRUE(fileOpsParseDate(text, &t)); + TEST_EQ(t, OLD_TIME); + TEST_TRUE(!fileOpsParseDate(text, NULL)); +} + + static void caseCancel(void) { tCase("cancel mid-file removes the partial copy"); @@ -710,6 +842,36 @@ static void caseMakeRename(void) { TEST_TRUE(!fileOpsRename(tp("mk"), "g.txt", longName, err, sizeof(err))); TEST_TRUE(fileOpsRename(tp("mk"), "newdir", "renamed", err, sizeof(err))); TEST_TRUE(exists(tp("mk/renamed"))); + + // BASIC: a whole chain of new folders, one that is there already, a + // file in the way, nothing + TEST_EQ(FileOpsMakePath(tp("mk/deep/er/still")), -1); + TEST_TRUE(exists(tp("mk/deep/er/still"))); + TEST_EQ(FileOpsMakePath(tp("mk/deep")), -1); + TEST_EQ(FileOpsMakePath(tp("mk/g.txt/below")), 0); + TEST_EQ(FileOpsMakePath(""), 0); + TEST_EQ(FileOpsMakePath(NULL), 0); + + // BASIC: file names, and an atomic save through the scratch sibling + TEST_EQ(FileOpsValidName("ok.txt"), -1); + TEST_STR_EQ(FileOpsError(), ""); + TEST_EQ(FileOpsValidName(".."), 0); + TEST_TRUE(FileOpsError()[0] != '\0'); + + char target[DVX_MAX_PATH]; + char scratch[DVX_MAX_PATH]; + + snprintf(target, sizeof(target), "%s", tp("mk/save.bas")); + makeFile(target, 1, 'o'); + snprintf(scratch, sizeof(scratch), "%s", FileOpsTempPath(target)); + TEST_TRUE(scratch[0] != '\0' && strcmp(scratch, target) != 0); + makeFile(scratch, 1, 'n'); + TEST_EQ(FileOpsReplaceFile(scratch, target), -1); + TEST_EQ(readFirst(target), 'n'); + TEST_TRUE(!exists(scratch)); + TEST_EQ(FileOpsReplaceFile(tp("mk/none.$ba"), target), 0); + TEST_TRUE(strstr(FileOpsError(), "could not be replaced") != NULL); + TEST_EQ(readFirst(target), 'n'); } @@ -1065,6 +1227,7 @@ int main(void) { caseRunCallbacks(); caseSymlinks(); caseBasic(); + caseBasicPlatform(); return tSuiteEnd(); } diff --git a/src/test/formrt/Makefile b/src/test/formrt/Makefile index 5b3f5b95..fcc38db4 100644 --- a/src/test/formrt/Makefile +++ b/src/test/formrt/Makefile @@ -40,7 +40,10 @@ TARGET = $(BINDIR)/testFormRt STUBS_SO = $(BINDIR)/frtStubs.so # Every .c here except the stub library is part of the test binary. SRCS = $(filter-out frtStubs.c,$(wildcard *.c)) -OBJS = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS)) $(BINDIR)/basBuild.o +SEC_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/serial/security +SEC_OBJS = $(patsubst $(SEC_DIR)/%.c,$(BINDIR)/security/%.o,$(wildcard $(SEC_DIR)/*.c)) +OBJS = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS)) $(BINDIR)/basBuild.o $(BINDIR)/game.o $(SEC_OBJS) +GAME_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/game CFLAGS = $(DVXTEST_CFLAGS) -I$(DVXBASIC) -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -DFRT_FIXTURE_DIR='"$(abspath .)/fixtures"' -DFRT_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' -DFRT_STUBS_SO='"$(abspath $(STUBS_SO))"' .PHONY: all run clean @@ -56,18 +59,28 @@ $(BINDIR)/basBuild.o: $(DVXBASIC)/basBuild.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< +# The game library's frame pacer, so FRAMEWAIT runs (FrameMissed test). +$(BINDIR)/game.o: $(GAME_DIR)/game.c $(DVX_MAKEFILES) + @mkdir -p $(BINDIR) + $(HOSTCC) $(CFLAGS) -I$(GAME_DIR) -c -o $@ $< + +# security.bas calls the security library for real +$(BINDIR)/security/%.o: $(SEC_DIR)/%.c $(DVX_MAKEFILES) + @mkdir -p $(dir $@) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + $(STUBS_SO): frtStubs.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -fPIC -shared -o $@ frtStubs.c -$(TARGET): $(OBJS) $(STUBS_SO) $(DVXTEST_BASRT_A) $(DVXTEST_ARCHIVES) | dvxtest-libs - $(HOSTCC) -o $@ $(OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LINK) +$(TARGET): $(OBJS) $(STUBS_SO) $(DVXTEST_BASRT_A) $(DVXTEST_TASKS_A) $(DVXTEST_ARCHIVES) | dvxtest-libs + $(HOSTCC) -o $@ $(OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_TASKS_A) $(DVXTEST_LINK) -include $(OBJS:.o=.d) $(STUBS_SO:.so=.d) # Runs from its object directory so the log and scratch files it writes # never land in the source tree. -run: $(TARGET) +run: $(TARGET) $(DVXTEST_HELP_HLP) cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./testFormRt clean: diff --git a/src/test/formrt/frtHarness.c b/src/test/formrt/frtHarness.c index 86f1f7dd..ede65c22 100644 --- a/src/test/formrt/frtHarness.c +++ b/src/test/formrt/frtHarness.c @@ -24,10 +24,12 @@ // frtHarness.c -- form-runtime test harness (suite T5) #include "frtHarness.h" +#include "frtStubs.h" #include "../../apps/kpunch/dvxbasic/basRes.h" #include "../../apps/kpunch/dvxbasic/compiler/compiler.h" #include "thirdparty/stb_ds_wrap.h" +#include #include #include #include @@ -40,6 +42,8 @@ #define FRT_DIRECTIVE_MAX 128 #define FRT_PROBE_SIZE 200 #define FRT_PROBE_NAME_MAX 32 +#define FRT_SRC_SLACK 256 // frtBuild: the BEGINFORM / ENDFORM lines and newlines around a part +#define FRT_FRM_COPIES 2 // frtBuild: a form contributes its control list and its code typedef struct { @@ -80,6 +84,7 @@ static const char *const sRoundTripCommon[] = { }; static FrtTextProbeT *sTextProbes = NULL; // stb_ds array, lives for the process +static void *sStubHandle = NULL; // frtStubs.so while a case has it open #define FRT_ROUND_TRIP_COMMON_COUNT (DVX_ARRAY_LEN(sRoundTripCommon)) @@ -121,7 +126,7 @@ static int32_t emitCtrl(const BasFormT *form, const BasControlT *ctrl, char *buf continue; } - if (!wgtPropValueToString(w, p, val, sizeof(val))) { + if (!wgtIfaceGetPropText(ctrl->iface, w, p, val, sizeof(val))) { continue; } @@ -167,7 +172,8 @@ static int32_t emitFormHeader(const BasFormT *form, char *buf, int32_t bufSize, for (int32_t i = 0; i < count; i++) { const BasPropDescT *pd = &props[i]; - if (strcasecmp(pd->name, "Name") == 0 || strcasecmp(pd->name, "Visible") == 0) { + // Run-time state, never in a .frm file + if (strcasecmp(pd->name, "Name") == 0 || strcasecmp(pd->name, "Visible") == 0 || strcasecmp(pd->name, BAS_PROP_DESIGNMODE) == 0 || strcasecmp(pd->name, BAS_PROP_DESIGNSELECTION) == 0 || pd->type == BAS_PROP_OBJECT) { continue; } @@ -179,9 +185,26 @@ static int32_t emitFormHeader(const BasFormT *form, char *buf, int32_t bufSize, continue; } + // Icon reads as a Picture; a .frm holds its file name + if (strcasecmp(pd->name, BAS_PROP_ICON) == 0) { + if (form->iconPath[0]) { + pos = emitLine(buf, bufSize, pos, 1, "%s = \"%s\"\n", pd->name, form->iconPath); + } + + continue; + } + BasValueT v = basFormRtGetProp(rt, (void *)&form->formCtrl, pd->name); - if (pd->type == WGT_IFACE_STRING && (strcasecmp(pd->name, "HelpTopic") == 0 || strcasecmp(pd->name, "ContextMenu") == 0) && (!v.strVal || v.strVal->len == 0)) { + if (pd->type == WGT_IFACE_STRING && (strcasecmp(pd->name, "HelpTopic") == 0 || strcasecmp(pd->name, "ContextMenu") == 0 || strcasecmp(pd->name, BAS_PROP_TAG) == 0) && (!v.strVal || v.strVal->len == 0)) { + basValRelease(&v); + continue; + } + + // Padding / Spacing 0 (the layout's default), WindowState 0 (a + // normal window) and MousePointer 0 (vbDefault) are not written, + // as the IDE designer does + if ((strcasecmp(pd->name, BAS_PROP_PADDING) == 0 || strcasecmp(pd->name, BAS_PROP_SPACING) == 0 || strcasecmp(pd->name, BAS_PROP_WINDOWSTATE) == 0 || strcasecmp(pd->name, BAS_PROP_MOUSEPOINTER) == 0) && basValToNumber(v) == 0) { basValRelease(&v); continue; } @@ -277,7 +300,15 @@ static int32_t emitSubtree(const BasFormT *form, const WidgetT *parent, char *bu bool frtBuild(FrtAppT *app, const char *basSource, const char *const *frms, int32_t frmCount) { memset(app, 0, sizeof(*app)); - char *src = (char *)calloc(1, FRT_SRC_MAX); + // Each form's code and control list (both shorter than the .frm + // text) inside its BEGINFORM / ENDFORM lines, then the module source + size_t cap = (basSource ? strlen(basSource) : 0) + FRT_SRC_SLACK; + + for (int32_t i = 0; i < frmCount; i++) { + cap += FRT_FRM_COPIES * strlen(frms[i]) + FRT_SRC_SLACK; + } + + char *src = (char *)calloc(1, cap); int32_t pos = 0; if (!src) { @@ -302,11 +333,11 @@ bool frtBuild(FrtAppT *app, const char *basSource, const char *const *frms, int3 } // The BEGINFORM line lists the form's controls, as the project builder writes it - pos += snprintf(src + pos, FRT_SRC_MAX - pos, "BEGINFORM \"%s\"%s\n%s\nENDFORM\n", formName, ctrlList, code); + pos += snprintf(src + pos, cap - pos, "BEGINFORM \"%s\"%s\n%s\nENDFORM\n", formName, ctrlList, code); free(ctrlList); } - pos += snprintf(src + pos, FRT_SRC_MAX - pos, "%s\n", basSource ? basSource : ""); + pos += snprintf(src + pos, cap - pos, "%s\n", basSource ? basSource : ""); BasCompilerT *compiler = basCompilerCreate(src, pos); @@ -349,7 +380,7 @@ bool frtBuild(FrtAppT *app, const char *basSource, const char *const *frms, int3 // Unexpected modal dialogs (runtime errors) are dismissed instead of // blocking the suite; a case that expects one installs its own // script after frtBuild. - tDialogScript(harnessDismissDialog, app); + frtDialogsDefault(app); for (int32_t i = 0; i < frmCount; i++) { char formName[BAS_MAX_IDENT] = ""; @@ -413,6 +444,12 @@ void frtDestroy(FrtAppT *app) { } +// Put the harness's own dialog script back after a case installed one. +void frtDialogsDefault(FrtAppT *app) { + tDialogScript(harnessDismissDialog, app); +} + + bool frtFire(FrtAppT *app, const char *formName, const char *eventName) { BasFormT *form = frtForm(app, formName); @@ -498,6 +535,37 @@ void frtStart(FrtAppT *app, const char *startupForm) { } +void frtStubsClose(void) { + if (sStubHandle) { + dlclose(sStubHandle); + sStubHandle = NULL; + } +} + + +const FrtStubApiT *frtStubsOpen(void) { + sStubHandle = dlopen(FRT_STUBS_SO, RTLD_NOW | RTLD_GLOBAL); + + if (!sStubHandle) { + printf(" dlopen(%s): %s\n", FRT_STUBS_SO, dlerror()); + return NULL; + } + + const FrtStubApiT *(*getApi)(void) = (const FrtStubApiT *(*)(void))dlsym(sStubHandle, "frtStubApi"); + + if (!getApi) { + dlclose(sStubHandle); + sStubHandle = NULL; + return NULL; + } + + const FrtStubApiT *api = getApi(); + + api->reset(); + return api; +} + + bool frtWidgetKeepsText(const char *wgtName) { for (int32_t i = 0; i < (int32_t)arrlen(sTextProbes); i++) { if (strcasecmp(sTextProbes[i].name, wgtName) == 0) { @@ -507,7 +575,7 @@ bool frtWidgetKeepsText(const char *wgtName) { WindowT *win = dvxCreateWindow(tCtx(), "Probe", 0, 0, FRT_PROBE_SIZE, FRT_PROBE_SIZE, false); WidgetT *root = wgtInitWindow(tCtx(), win); - WidgetT *w = createWidget(wgtName, root); + WidgetT *w = wgtCreateByName(wgtName, root); bool keeps = false; if (w) { @@ -535,8 +603,10 @@ static void harnessDismissDialog(AppContextT *ctx, WindowT *modal, void *userCtx app->dialogsDismissed++; printf(" harness: dismissed dialog [%s]\n", modal->title); - if (!tDialogClickButton(modal, "OK")) { - tDialogClickButton(modal, "Cancel"); + // A modal BASIC form without OK or Cancel is closed as by its close + // box (Show vbModal then returns). + if (!tDialogClickButton(modal, "OK") && !tDialogClickButton(modal, "Cancel") && modal->onClose) { + modal->onClose(modal); } } diff --git a/src/test/formrt/frtHarness.h b/src/test/formrt/frtHarness.h index 19ad8b3f..e0c357fa 100644 --- a/src/test/formrt/frtHarness.h +++ b/src/test/formrt/frtHarness.h @@ -32,6 +32,7 @@ #define FRT_HARNESS_H #include "dvxtest.h" +#include "frtStubs.h" #include "../../apps/kpunch/dvxbasic/formrt/formrt.h" #include @@ -69,6 +70,11 @@ BasVmResultE frtRunMain(FrtAppT *app); // Tear everything down (runtime, VM, module). void frtDestroy(FrtAppT *app); +// Put the harness's dialog script back (dismiss unexpected modal dialogs +// with OK / Cancel, close a modal form without them) after a case +// installed its own with tDialogScript. +void frtDialogsDefault(FrtAppT *app); + // Lookups. BasControlT *frtCtrl(const BasFormT *form, const char *name); BasControlT *frtCtrlIdx(const BasFormT *form, const char *name, int32_t index); @@ -93,6 +99,13 @@ int32_t frtSerializeForm(const BasFormT *form, char *buf, int32_t bufSize); // on the live context and cached per type name. bool frtWidgetKeepsText(const char *wgtName); +// Load frtStubs.so (RTLD_GLOBAL, so formrt's dlsym(NULL, ...) finds its +// rs232 / secLink / shell fakes) and reset its fakes; NULL (reason +// printed) when it cannot be loaded. frtStubsClose unloads it again so +// later cases see the unresolved paths. +void frtStubsClose(void); +const FrtStubApiT *frtStubsOpen(void); + // Clear a runtime error left by a negative test so the next statement // can run: rt->terminated, the VM error text and the halted flags. void frtClearError(FrtAppT *app); diff --git a/src/test/formrt/frtStubs.c b/src/test/formrt/frtStubs.c index bdd11852..f88470d1 100644 --- a/src/test/formrt/frtStubs.c +++ b/src/test/formrt/frtStubs.c @@ -32,6 +32,7 @@ #include "../../tools/hlpcCompile.h" #include "dvxTypes.h" +#include #include #include #include @@ -39,6 +40,9 @@ #define STUB_UART_16550A 4 #define STUB_DEFAULT_BASE 0x3F8 #define STUB_DEFAULT_IRQ 4 +#define FRT_STUB_DATA_MIN 5 +#define FRT_STUB_DATA_MAX 8 +#define FRT_STUB_ERR_DATA (-9) // mirrors RS232_ERR_INVALID_DATA typedef struct { bool open; @@ -50,6 +54,12 @@ typedef struct { int32_t irq; bool dtr; bool rts; + bool brk; + int32_t bps; + int32_t dataBits; + char parity; + int32_t stopBits; + int32_t handshake; } StubPortT; typedef struct { @@ -58,6 +68,7 @@ typedef struct { void *recvCtx; char pass[FRT_STUB_PASS_MAX]; bool plain[FRT_STUB_CHANNELS]; + bool stepping; // secLinkHandshakeStep answered PENDING once } StubLinkT; typedef struct { @@ -68,28 +79,43 @@ typedef struct { // Prototypes (alphabetical; exported symbols formrt.c resolves by name) const FrtStubApiT *frtStubApi(void); +int32_t hlpcCompile(const char **inputs, int32_t inputCount, const char *outputFile, const char *a, const char *b, uint32_t options, HlpcProgressFnT x, void *y); +void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx); int32_t rs232ClearRxBuffer(int32_t com); +int32_t rs232ClearTxBuffer(int32_t com); int32_t rs232Close(int32_t com); int32_t rs232GetBase(int32_t com); +int32_t rs232GetBps(int32_t com); int32_t rs232GetCts(int32_t com); +int32_t rs232GetData(int32_t com); int32_t rs232GetDcd(int32_t com); int32_t rs232GetDsr(int32_t com); int32_t rs232GetDtr(int32_t com); +int32_t rs232GetHandshake(int32_t com); int32_t rs232GetIrq(int32_t com); +int32_t rs232GetLsr(int32_t com); +int32_t rs232GetMsr(int32_t com); +char rs232GetParity(int32_t com); int32_t rs232GetRts(int32_t com); int32_t rs232GetRxBuffered(int32_t com); +int32_t rs232GetStop(int32_t com); +int32_t rs232GetTxBuffered(int32_t com); int32_t rs232GetUartType(int32_t com); int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake); int32_t rs232Read(int32_t com, char *data, int32_t len); +int32_t rs232Set(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake); int32_t rs232SetBase(int32_t com, int32_t base); +int32_t rs232SetBreak(int32_t com, bool on); int32_t rs232SetDtr(int32_t com, bool dtr); int32_t rs232SetIrq(int32_t com, int32_t irq); int32_t rs232SetRts(int32_t com, bool rts); int32_t rs232Write(int32_t com, const char *data, int32_t len); int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len); void secLinkClose(void *link); +int32_t secLinkGetDropped(void *link); int32_t secLinkGetPending(void *link); int32_t secLinkHandshake(void *link); +int32_t secLinkHandshakeStep(void *link); bool secLinkIsReady(void *link); void *secLinkOpen(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake, void (*recvCb)(void *, const uint8_t *, int32_t, uint8_t, bool), void *recvCtx); int32_t secLinkPoll(void *link); @@ -114,10 +140,12 @@ static int32_t stubIdleCount(void); static void stubIdleRun(void); static int32_t stubLine(int32_t com, bool up); static void stubReset(void); +static bool stubSerBreak(int32_t com); static void stubSerFail(bool openFails, bool writeFails); static bool stubSerIsOpen(int32_t com); static void stubSerPush(int32_t com, const void *data, int32_t len); static const uint8_t *stubSerTx(int32_t com, int32_t *len); +static int32_t stubSetting(int32_t com, size_t offset); static const char *stubShellLastArgs(void); @@ -140,6 +168,8 @@ static int32_t sStubIdleCount = 0; static int32_t sStubHlpcRc = 0; static char sStubHlpcOutput[FRT_STUB_PATH_MAX]; +static HlpcMessageFnT sStubHlpcMsgFn = NULL; +static void *sStubHlpcMsgCtx = NULL; static char sStubShellArgs[FRT_STUB_PATH_MAX]; static const FrtStubApiT sStubApi = { @@ -148,6 +178,7 @@ static const FrtStubApiT sStubApi = { .serTx = stubSerTx, .serFail = stubSerFail, .serIsOpen = stubSerIsOpen, + .serBreak = stubSerBreak, .commFail = stubCommFail, .commInject = stubCommInject, .commLinks = stubCommLinks, @@ -180,10 +211,22 @@ int32_t hlpcCompile(const char **inputs, int32_t inputCount, const char *outputF } snprintf(sStubHlpcOutput, sizeof(sStubHlpcOutput), "%s", outputFile); + + // A failing compile reports why, as the real compiler does + if (sStubHlpcRc != 0 && sStubHlpcMsgFn) { + sStubHlpcMsgFn(sStubHlpcMsgCtx, FRT_STUB_HLPC_ERROR); + } + return sStubHlpcRc; } +void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx) { + sStubHlpcMsgFn = fn; + sStubHlpcMsgCtx = ctx; +} + + int32_t rs232ClearRxBuffer(int32_t com) { if (com < 0 || com >= FRT_STUB_PORTS) { return -1; @@ -194,6 +237,16 @@ int32_t rs232ClearRxBuffer(int32_t com) { } +int32_t rs232ClearTxBuffer(int32_t com) { + if (com < 0 || com >= FRT_STUB_PORTS) { + return -1; + } + + sStubPorts[com].txLen = 0; + return 0; +} + + int32_t rs232Close(int32_t com) { if (com < 0 || com >= FRT_STUB_PORTS) { return -1; @@ -213,6 +266,11 @@ int32_t rs232GetBase(int32_t com) { } +int32_t rs232GetBps(int32_t com) { + return stubSetting(com, offsetof(StubPortT, bps)); +} + + // The stub modem: CTS and DSR up, no carrier. Lines of a closed port // read as the rs232 "not open" error. int32_t rs232GetCts(int32_t com) { @@ -220,6 +278,11 @@ int32_t rs232GetCts(int32_t com) { } +int32_t rs232GetData(int32_t com) { + return stubSetting(com, offsetof(StubPortT, dataBits)); +} + + int32_t rs232GetDcd(int32_t com) { return stubLine(com, false); } @@ -235,6 +298,11 @@ int32_t rs232GetDtr(int32_t com) { } +int32_t rs232GetHandshake(int32_t com) { + return stubSetting(com, offsetof(StubPortT, handshake)); +} + + int32_t rs232GetIrq(int32_t com) { if (com < 0 || com >= FRT_STUB_PORTS) { return 0; @@ -244,6 +312,21 @@ int32_t rs232GetIrq(int32_t com) { } +int32_t rs232GetLsr(int32_t com) { + return stubLine(com, true) > 0 ? FRT_STUB_LSR : FRT_STUB_ERR_NOT_OPEN; +} + + +int32_t rs232GetMsr(int32_t com) { + return stubLine(com, true) > 0 ? FRT_STUB_MSR : FRT_STUB_ERR_NOT_OPEN; +} + + +char rs232GetParity(int32_t com) { + return stubLine(com, true) > 0 ? sStubPorts[com].parity : (char)FRT_STUB_ERR_NOT_OPEN; +} + + int32_t rs232GetRts(int32_t com) { return stubLine(com, com >= 0 && com < FRT_STUB_PORTS && sStubPorts[com].rts); } @@ -258,6 +341,16 @@ int32_t rs232GetRxBuffered(int32_t com) { } +int32_t rs232GetStop(int32_t com) { + return stubSetting(com, offsetof(StubPortT, stopBits)); +} + + +int32_t rs232GetTxBuffered(int32_t com) { + return stubLine(com, true) > 0 ? sStubPorts[com].txLen : FRT_STUB_ERR_NOT_OPEN; +} + + int32_t rs232GetUartType(int32_t com) { (void)com; return STUB_UART_16550A; @@ -265,12 +358,6 @@ int32_t rs232GetUartType(int32_t com) { int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake) { - (void)bps; - (void)dataBits; - (void)parity; - (void)stopBits; - (void)handshake; - if (com < 0 || com >= FRT_STUB_PORTS || sStubSerOpenFails) { return -1; } @@ -278,7 +365,11 @@ int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32 sStubPorts[com].open = true; sStubPorts[com].dtr = true; sStubPorts[com].rts = true; - return 0; + + int32_t rc = rs232Set(com, bps, dataBits, parity, stopBits, handshake); + + sStubPorts[com].open = (rc == 0); + return rc; } @@ -297,6 +388,25 @@ int32_t rs232Read(int32_t com, char *data, int32_t len) { } +// Any speed above 0; data bits 5-8 (else the rs232 "invalid data" code). +int32_t rs232Set(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake) { + if (stubLine(com, true) < 0) { + return FRT_STUB_ERR_NOT_OPEN; + } + + if (dataBits < FRT_STUB_DATA_MIN || dataBits > FRT_STUB_DATA_MAX) { + return FRT_STUB_ERR_DATA; + } + + sStubPorts[com].bps = bps; + sStubPorts[com].dataBits = dataBits; + sStubPorts[com].parity = parity; + sStubPorts[com].stopBits = stopBits; + sStubPorts[com].handshake = handshake; + return 0; +} + + int32_t rs232SetBase(int32_t com, int32_t base) { if (com >= 0 && com < FRT_STUB_PORTS) { sStubPorts[com].base = base; @@ -306,6 +416,16 @@ int32_t rs232SetBase(int32_t com, int32_t base) { } +int32_t rs232SetBreak(int32_t com, bool on) { + if (stubLine(com, true) < 0) { + return FRT_STUB_ERR_NOT_OPEN; + } + + sStubPorts[com].brk = on; + return 0; +} + + int32_t rs232SetDtr(int32_t com, bool dtr) { if (com < 0 || com >= FRT_STUB_PORTS || !sStubPorts[com].open) { return -1; @@ -367,6 +487,11 @@ void secLinkClose(void *link) { } +int32_t secLinkGetDropped(void *link) { + return link ? FRT_STUB_DROPPED : -1; +} + + int32_t secLinkGetPending(void *link) { (void)link; return 0; @@ -384,6 +509,24 @@ int32_t secLinkHandshake(void *link) { } +// PENDING on the first step of an exchange, then what secLinkHandshake +// would return. +int32_t secLinkHandshakeStep(void *link) { + StubLinkT *l = (StubLinkT *)link; + + if (l && !l->stepping) { + l->stepping = true; + return FRT_STUB_PENDING; + } + + if (l) { + l->stepping = false; + } + + return secLinkHandshake(link); +} + + bool secLinkIsReady(void *link) { StubLinkT *l = (StubLinkT *)link; @@ -635,6 +778,11 @@ static void stubReset(void) { } +static bool stubSerBreak(int32_t com) { + return com >= 0 && com < FRT_STUB_PORTS && sStubPorts[com].brk; +} + + static void stubSerFail(bool openFails, bool writeFails) { sStubSerOpenFails = openFails; sStubSerWriteFails = writeFails; @@ -681,6 +829,17 @@ static const uint8_t *stubSerTx(int32_t com, int32_t *len) { } +// The int32_t setting at offset in an open port's StubPortT (the rs232 +// "not open" code otherwise). +static int32_t stubSetting(int32_t com, size_t offset) { + if (stubLine(com, true) < 0) { + return FRT_STUB_ERR_NOT_OPEN; + } + + return *(const int32_t *)((const char *)&sStubPorts[com] + offset); +} + + static const char *stubShellLastArgs(void) { return sStubShellArgs; } diff --git a/src/test/formrt/frtStubs.h b/src/test/formrt/frtStubs.h index cb0ec33d..7e770671 100644 --- a/src/test/formrt/frtStubs.h +++ b/src/test/formrt/frtStubs.h @@ -45,6 +45,12 @@ #define FRT_STUB_PASS_MAX 64 #define FRT_STUB_CHANNELS 128 #define FRT_STUB_ERR_AUTH (-8) // mirrors SECLINK_ERR_AUTH +#define FRT_STUB_ERR_NOT_OPEN (-2) // mirrors RS232_ERR_NOT_OPEN +#define FRT_STUB_PENDING 1 // mirrors SECLINK_PENDING +#define FRT_STUB_LSR 0x62 // overrun (0x02) plus the transmitter-empty bits +#define FRT_STUB_MSR 0x50 // ring indicator (0x40) and CTS +#define FRT_STUB_DROPPED 3 // secLinkGetDropped of every open link +#define FRT_STUB_HLPC_ERROR "error: guide.dhs:3: stub compile failure" // what a failing fake compile reports typedef struct { // Reset every fake to its load-time state. @@ -58,6 +64,8 @@ typedef struct { // Force rs232Open / rs232WriteBuf to fail. void (*serFail)(bool openFails, bool writeFails); bool (*serIsOpen)(int32_t com); + // True while rs232SetBreak holds the port's line in the break state. + bool (*serBreak)(int32_t com); // ---- secLink (Comm*) ---- // Force secLinkOpen to return NULL / secLinkSendBuf to fail. diff --git a/src/test/formrt/testFormRt.c b/src/test/formrt/testFormRt.c index 17a6693b..6a2bc18e 100644 --- a/src/test/formrt/testFormRt.c +++ b/src/test/formrt/testFormRt.c @@ -36,17 +36,21 @@ #include "../../apps/kpunch/dvxbasic/basRes.h" #include "../../apps/kpunch/dvxbasic/runtime/screen.h" #include "thirdparty/stb_ds_wrap.h" +#include "canvas/canvas.h" #include "timer/timer.h" #include "dvxRes.h" #include "shellApp.h" +#include "dvxCur.h" #include "dvxDlg.h" #include "platform/dvxPlatHost.h" #include "dvxWgtP.h" #include "dvxVideo.h" #include "dvxWm.h" #include "../../libs/kpunch/serial/seclink/secLink.h" +#include "../../libs/kpunch/hlpfile/hlpFormat.h" +#include #include #include #include @@ -57,6 +61,7 @@ #define SCREEN_W 800 #define SCREEN_H 600 #define SCREEN_BPP 32 +#define SCREEN_BPP_16 16 #define FORM_W 400 #define FORM_H 300 @@ -79,6 +84,28 @@ #define SQL_FAKE_ROWS 2 #define RES_HANDLE_PROBE 9 #define FRT_EXT_WIDE_RESULT 0x12345 +#define FRT_EXT_BIN_MAX 64 // frtExtXor result buffer +#define FRT_FIT_BIG_W 600 // caseFormWindowMembers: the size FitWidth / FitHeight shrink from +#define FRT_FIT_BIG_H 400 +#define FRT_SRC_BUF_LEN 4096 // a test program built with snprintf +#define FRT_DRAW_WANT_LEN 128 // caseDrawThemed: the expected output +#define FRT_VBAR_X 100 // caseDrawThemed Form1_Draw: the upright DrawScrollBar's left column +#define FRT_VBAR_END_Y 79 // and its last row (length 80 from 0) +#define FRT_CBAR_X 60 // Form1_Ctrl: the control-style DrawScrollBar's left column +#define FRT_CBAR_Y 40 // a row inside its trough +#define FRT_HBAR_Y 82 // caseDrawThemed Form1_HBar: the horizontal DrawScrollBar's top row (it starts at x 0) +#define FRT_HBAR_END 95 // and its last column (length 96) +#define FRT_HBAR_THUMB_X 31 // its thumb's first column: arrow 16 + pos 20 * (track 64 - thumb 16) / range 64 +#define FRT_HBAR_THUMB_END 46 // and last: 16 * 64 / 80 = 12 wide, raised to the 16 minimum +#define FRT_HBAR_TROUGH_X 20 // a column of the trough left of the thumb +#define FRT_SARROW_X 40 // caseDrawThemed Form1_HBar: DrawScrollArrow's button (16 square, pointing right) +#define FRT_SARROW_Y 30 +#define FRT_PAINT_PUMPS 3 // event pumps that paint a newly shown window +#define FRT_EXT_BIN_NULL (-1) // frtExtXor key: return NULL +#define FRT_EXT_BIN_NOLEN 99 // frtExtXor key: leave *outLen alone +#define FRT_EXT_LEN_WEIGHT 100 // frtExtBinSum: weight of the first length +#define FRT_EXT_TAG_WEIGHT 10000 // frtExtBinSum: weight of the text length +#define FRT_EXT_LEN2_WEIGHT 1000000 // frtExtBinSum: weight of the second length #define FRT_MIN_W_PROBE 50 #define FRT_HUGE_DIM 1000000000 #define FRT_MAX_DIM 32767 @@ -86,6 +113,10 @@ #define FRT_PROBE_ARG "abc" #define FRT_PROBE_BAS_ARG "hello" #define FRT_PROBE_NUM_BUF 16 +#define FRT_SEVENTH_WEIGHT 100 // frtExtSeven: the seventh argument counts this much +#define FRT_TERM_COLS 100 // caseTermBind: the Terminal's new size +#define FRT_TERM_ROWS 30 +#define FRT_BIND_MAX 64 // caseTermBind: bytes the bound stream keeps #define FRT_APPS_DIR FRT_INCLUDE_DIR "/../../apps/kpunch" #define FRT_APP_PATH_MAX 512 #define FRT_CLASS_WRAP_LEN (BAS_MAX_IDENT + 64) // the BEGINCLASS / ENDCLASS lines around a class module, separators and NUL @@ -95,6 +126,30 @@ #define FRT_DRAG_BAD_ACTION 7 #define FRT_FORM_EDGE 30 // above the form's content bottom (empty form area, clear of the resize border) +// Program-port cases (menu keys, Default/Cancel, list methods). +#define SETTLE_FRAMES_FRT 2 // frames for a key-released button's Click +#define MENU_SHORTCUT_COUNT 3 // Shortcut items of caseMenuShortcuts +#define FRT_ICON_FRM_LEN 4096 // caseProgramIcon: its .frm, with the files' paths +#define SCAN_B 0x30 // scancode of B +#define KEY_CTRL_B 0x02 // Ctrl+B's ASCII code +#define LV_SECOND_COL_X 140 // inside the second column of "Name,14|Size,12" (from the list's left edge) +#define LV_HEADER_Y 6 // inside the header row (from the list's top edge) +#define FRT_CHOICE_MANY 300 // DlgChoice items in caseDialogWrappers (over 64, over 1024 bytes) +#define FRT_CHOICE_ITEM_LEN 16 // room for one "|keyword nnn" +#define FRT_HELP_OUT_MAX 256 // caseHelpPane: the expected PRINT output +#define FRT_SRC_BUF 32768 // a case's BASIC source with an include file in front +#define FRT_GUTTER_X 4 // inside a TextArea's line-number gutter (from its left edge) +#define FRT_LINE_Y 4 // inside a TextArea's first line (from its top edge) +#define FRT_PARDSGN_FRM FRT_INCLUDE_DIR "/../../test/dos/fixtures/pardsgn/pardsgn.frm" +#define FRT_PARDSGN_TICKS 50 // clock steps the fixture gets to run its checks +#define FRT_PARDSGN_TICK_MS 100 +#define FRT_DYN_TYPE "frtdyn" // caseDynamicControl's widget type +#define FRT_DYN_BAS "FrtDyn" +#define FRT_DYN_LEVEL 9 +#define FRT_DYN_TEXT 64 +#define SIZE_TEST_CHARS 10 // caseSizeUnits: SizeChars(10) +#define SIZE_PERCENT_MAX 100 // SizePercent's cap + // Widget-independent line prefix of every round-trip form. #define RT_FORM_HEADER \ "VERSION DVX 1.00\n" \ @@ -124,74 +179,81 @@ // BASIC-facing entry points live in formrt.c without a header (they // are reached through DECLARE LIBRARY on DOS). static void caseObjectRefs(void); -void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); -void CommClose(int32_t handle); -void CommDetach(int32_t handle); -int32_t CommHandshake(int32_t handle, const char *passphrase); -int32_t CommIsReady(int32_t handle); -int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); -int32_t CommPending(int32_t handle); -int32_t CommPoll(int32_t handle); -const char *CommRecv(int32_t handle, int32_t channel); -int32_t CommRecvEncrypted(int32_t handle, int32_t channel); -int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt); -int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain); -int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx); -int32_t DlgColor(const char *title, int32_t defaultColor); -const char *DlgFileOpen(const char *title, const char *filter); -const char *DlgFileSave(const char *title, const char *filter); -const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText); -int32_t DlgInputCancelled(void); -int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal); -int32_t DlgPromptSave(const char *title); -int32_t HelpCompile(const char *inputFile, const char *outputFile); -void HelpView(const char *hlpFile); -int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); -int32_t ResAddText(const char *path, const char *name, const char *text); -void ResClose(int32_t handle); -int32_t ResCount(int32_t handle); -int32_t ResExtract(const char *path, const char *name, const char *outFile); -const char *ResGetText(const char *path, const char *name); -const char *ResName(int32_t handle, int32_t index); -int32_t ResOpen(const char *path); -int32_t ResRemove(const char *path, const char *name); -int32_t ResSize(int32_t handle, int32_t index); -int32_t ResType(int32_t handle, int32_t index); -void SerAttach(int32_t com, const char *termCtrlName); -int32_t SerAvailable(int32_t com); -void SerClose(int32_t com); -void SerDetach(int32_t com); -void SerFlush(int32_t com); -int32_t SerGetBase(int32_t com); -int32_t SerGetCts(int32_t com); -int32_t SerGetDcd(int32_t com); -int32_t SerGetDsr(int32_t com); -int32_t SerGetDtr(int32_t com); -int32_t SerGetIrq(int32_t com); -int32_t SerGetRts(int32_t com); -int32_t SerGetUart(int32_t com); -int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); -const char *SerRead(int32_t com); -void SerSetBase(int32_t com, int32_t base); -void SerSetDtr(int32_t com, int32_t on); -void SerSetIrq(int32_t com, int32_t irq); -void SerSetRts(int32_t com, int32_t on); -int32_t SerWrite(int32_t com, const char *data); -int32_t SQLAffected(int32_t db); -void SQLClose(int32_t db); -int32_t SQLEof(int32_t rs); -const char *SQLError(int32_t db); -int32_t SQLExec(int32_t db, const char *sql); -const char *SQLField(int32_t rs, const char *colName); -int32_t SQLFieldCount(int32_t rs); -double SQLFieldDbl(int32_t rs, int32_t col); -int32_t SQLFieldInt(int32_t rs, int32_t col); -const char *SQLFieldName(int32_t rs, int32_t col); -const char *SQLFieldText(int32_t rs, int32_t col); -void SQLFreeResult(int32_t rs); -int32_t SQLNext(int32_t rs); -int32_t SQLOpen(const char *path); -int32_t SQLQuery(int32_t db, const char *sql); +void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); +void CommClose(int32_t handle); +void CommDetach(int32_t handle); +int32_t CommHandshake(int32_t handle, const char *passphrase); +int32_t CommIsReady(int32_t handle); +int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +int32_t CommPending(int32_t handle); +int32_t CommPoll(int32_t handle); +const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen); +int32_t CommRecvEncrypted(int32_t handle, int32_t channel); +int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt); +int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain); +int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx); +int32_t DlgColor(const char *title, int32_t defaultColor); +const char *DlgFileOpen(const char *title, const char *filter); +const char *DlgFileOpenIn(const char *title, const char *filter, const char *folder); +const char *DlgFileSave(const char *title, const char *filter); +const char *DlgFileSaveIn(const char *title, const char *filter, const char *folder); +const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText); +int32_t DlgInputCancelled(void); +int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal); +int32_t DlgIntInputStep(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t stepVal); +int32_t DlgPromptSave(const char *title); +int32_t HelpCompile(const char *inputFile, const char *outputFile); +const char *HelpCompileMessages(void); +void HelpView(const char *hlpFile); +void HelpViewTopic(const char *hlpFile, const char *topicId); +int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len); +int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); +int32_t ResAddText(const char *path, const char *name, const char *text); +void ResClose(int32_t handle); +int32_t ResCount(int32_t handle); +int32_t ResExtract(const char *path, const char *name, const char *outFile); +const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen); +const char *ResGetText(const char *path, const char *name); +const char *ResName(int32_t handle, int32_t index); +int32_t ResOpen(const char *path); +int32_t ResRemove(const char *path, const char *name); +int32_t ResSize(int32_t handle, int32_t index); +int32_t ResType(int32_t handle, int32_t index); +void SerAttach(int32_t com, const char *termCtrlName); +int32_t SerAvailable(int32_t com); +void SerClose(int32_t com); +void SerDetach(int32_t com); +void SerFlush(int32_t com); +int32_t SerGetBase(int32_t com); +int32_t SerGetCts(int32_t com); +int32_t SerGetDcd(int32_t com); +int32_t SerGetDsr(int32_t com); +int32_t SerGetDtr(int32_t com); +int32_t SerGetIrq(int32_t com); +int32_t SerGetRts(int32_t com); +int32_t SerGetUart(int32_t com); +int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +const uint8_t *SerRead(int32_t com, int32_t *outLen); +void SerSetBase(int32_t com, int32_t base); +void SerSetDtr(int32_t com, int32_t on); +void SerSetIrq(int32_t com, int32_t irq); +void SerSetRts(int32_t com, int32_t on); +int32_t SerWrite(int32_t com, const uint8_t *data, int32_t len); +int32_t SQLAffected(int32_t db); +void SQLClose(int32_t db); +int32_t SQLEof(int32_t rs); +const char *SQLError(int32_t db); +int32_t SQLExec(int32_t db, const char *sql); +const char *SQLField(int32_t rs, const char *colName); +int32_t SQLFieldCount(int32_t rs); +double SQLFieldDbl(int32_t rs, int32_t col); +int32_t SQLFieldInt(int32_t rs, int32_t col); +const char *SQLFieldName(int32_t rs, int32_t col); +const char *SQLFieldText(int32_t rs, int32_t col); +void SQLFreeResult(int32_t rs); +int32_t SQLNext(int32_t rs); +int32_t SQLOpen(const char *path); +int32_t SQLQuery(int32_t db, const char *sql); typedef struct { @@ -201,6 +263,13 @@ typedef struct { char button[16]; } DialogCtxT; +// caseNewArgs' message box script +typedef struct { + FrtAppT *app; + int32_t seen; + bool firedDuring; // NewArgs ran while the box was open +} NewArgsDlgT; + // Modal-dialog driver: type text and accept with OK, or cancel with // Esc when no text is given. @@ -211,9 +280,9 @@ typedef struct { } FileDlgCtxT; -// frtStubs.so handle + control API (loaded per case, never linked). -static void *sStubHandle = NULL; -static const FrtStubApiT *sStub = NULL; +// caseVisibleKeepsModal's dialog script: the app and how often it ran. +static FrtAppT *sModalCheckApp = NULL; +static int32_t sModalChecks = 0; // frmParse unit-test recorder. @@ -235,6 +304,13 @@ typedef struct { } ParseRecT; +// Cases in their own files (testLibBas.c, testFrmModel.c, testUserCtl.c, +// testUcDesign.c) +void frtFrmModelCases(void); +void frtLibBasCases(void); +void frtUcDesignCases(void); +void frtUserCtlCases(void); + static bool appBuild(FrtAppT *app, const char *frmPath, const char *clsPath, const char *stubs, const char *tests); static BasValueT callIntM(FrtAppT *app, BasControlT *ctrl, const char *method, const int32_t *vals, int32_t count); static void caseAppBasDemo(void); @@ -242,35 +318,60 @@ static void caseAppCommandLine(void); static void caseAppIconEd(void); static void caseAppNotepad(void); static void caseAppNotepadCloseRequest(void); +static void caseBoxLayoutProps(void); static void caseBuildProject(void); static void caseClipboard(void); static void caseCommonMethods(void); static void caseControlArrays(void); +static void caseControlMembers2(void); static void caseCreateFormDynamic(void); +static void caseCustomEvents(void); static void caseDataBinding(void); +static void caseDefaultCancelButtons(void); +static void caseDesignerLive(void); +static void caseDesignerRegistry(void); static void caseDialogWrappers(void); static void caseDragDrop(void); static void caseDrawMethods(void); static void caseDrawPaint(void); static void caseDrawScale(void); +static void caseDrawThemed(void); +static void caseDynamicControl(void); +static void caseEditorMembers(void); static void caseEventLoopExits(void); static void caseEvents(void); static void caseExternCalls(void); +static void caseExternLive(void); static void caseExternMarshal(void); +static void caseFormInstances(void); static void caseFormShowHide(void); static void caseFormVars(void); +static void caseFormWindowMembers(void); +static void caseFrameMissed(void); static void caseFrmLoadEdges(void); static void caseFrmParserUnit(void); +static void caseHandlerContext(void); +static void caseHandlerError(void); static void caseHelpHooks(void); +static void caseHelpPane(void); static void caseHelpTopic(void); +static void caseListTreeMethods(void); static void caseLoadForm2(void); +static void caseMenuBarRuntime(void); +static void caseMenuCaption(void); +static void caseMenuRuntime(void); static void caseMenus(void); +static void caseMenuShortcuts(void); static void caseMethodRetIntStr(void); static void caseMethodSignatures(void); +static void caseModalShow(void); static void caseMsgBox(void); +static void caseNewArgs(void); +static void caseParDsgnFixture(void); static void casePendingUnloadDrain(void); static void casePictureLoadSave(void); static void casePopupMenu(void); +static void caseProgramIcon(void); static void caseProgramScreen(void); static void caseProgramScreenModes(void); static void casePropEdges(void); @@ -283,21 +384,38 @@ static void caseResources(void); static void caseRoundTrip(void); static void caseRunSimple(void); static void caseRuntimeError(void); +static void caseScreenMembers(void); +static void caseScreenObject(void); static void caseSerialLoopback(void); static void caseSerialUnresolved(void); static void caseSetEvent(void); static void caseSqlWrappers(void); +static void caseTermBind(void); +static void caseTextBoxProps(void); static void caseTimers(void); static void caseToolbarClearFromHandler(void); +static void caseTwoRuntimes(void); static void caseUnloadDeferred(void); static void caseValidateCancel(void); static void caseVisibleKeepsModal(void); +static void caseWidgetParity(void); +static void caseWindowState(void); static WidgetT *childAt(WidgetT *w, int32_t idx); +static const char *commRecvText(int32_t handle, int32_t channel); +static int32_t commSendText(int32_t handle, const char *text, int32_t channel, int32_t encrypt); static void dialogClick(AppContextT *ctx, WindowT *modal, void *userCtx); static void dragCallMethod(FrtAppT *app, BasControlT *ctrl, int32_t action); +static void drawThemedHBarCheck(WidgetT *pic); +static void errorDialogCheck(AppContextT *ctx, WindowT *modal, void *userCtx); static bool expectBuild(FrtAppT *app, const char *bas, const char *const *frms, int32_t frmCount); static void fileDialogType(AppContextT *ctx, WindowT *modal, void *userCtx); +static WidgetT *firstFocusable(WidgetT *root); +static bool frtDynCall(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret); +static WidgetT *frtDynCreate(WidgetT *parent, const WgtIfaceT *iface); +static bool frtDynGet(WidgetT *w, int32_t propIdx, WgtValueT *out); +static bool frtDynSet(WidgetT *w, int32_t propIdx, const WgtValueT *value); static int32_t frtProbeRetIntStr(WidgetT *w, const char *text); +static void newArgsDuringDialog(AppContextT *ctx, WindowT *modal, void *userCtx); static void outContains(const FrtAppT *app, const char *needle); static void parseOnCtrlBegin(void *userData, const char *typeName, const char *name); static void parseOnCtrlEnd(void *userData); @@ -309,6 +427,8 @@ static void parseOnMenuBegin(void *userData, const char *name, int32_t le static void parseOnMenuEnd(void *userData); static void parseOnMenuProp(void *userData, const char *key, const char *value); static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize); +static const char *serReadText(int32_t com); +static int32_t serWriteText(int32_t com, const char *text); static void setStrProp(FrtAppT *app, BasControlT *ctrl, const char *propName, const char *text); static int32_t sqlFakeAffected(int32_t db); static void sqlFakeClose(int32_t db); @@ -325,8 +445,10 @@ static void sqlFakeFree(int32_t rs); static bool sqlFakeNext(int32_t rs); static int32_t sqlFakeOpen(const char *path); static int32_t sqlFakeQuery(int32_t db, const char *sql); -static void stubsClose(void); -static const FrtStubApiT *stubsOpen(void); +static void termBindUnbound(void *ctx); +static int32_t termBindWrite(void *ctx, const uint8_t *data, int32_t len); +static void unloadModalCheck(AppContextT *ctx, WindowT *modal, void *userCtx); +static void visibleKeepsModalCheck(AppContextT *ctx, WindowT *modal, void *userCtx); static bool writeText(const char *path, const char *text); int main(void); @@ -334,12 +456,63 @@ int main(void); // LIBRARY declarations resolve them through basExternResolve's // dlsym(NULL, ...). The host basNativeCall stub never actually calls // them; only their addresses matter. -double frtExtDbl(double d, float s); -int32_t frtExtFlag(int32_t a); -int32_t frtExtInt(int32_t a); -int32_t frtExtLong(int32_t a, int32_t b); -const char *frtExtStr(const char *s); -void frtExtSub(int32_t a); +int32_t frtExtBinSum(const uint8_t *d, int32_t len, const char *tag, const uint8_t *d2, int32_t len2); +double frtExtDbl(double d, float s); +int32_t frtExtFlag(int32_t a); +int32_t frtExtInt(int32_t a); +int32_t frtExtLong(int32_t a, int32_t b); +double frtExtMix(const char *s, double d, int32_t n, float f); +int32_t frtExtSeven(int32_t a, int32_t b, int32_t c, int32_t d, int32_t e, int32_t f, int32_t g); +const char *frtExtStr(const char *s); +void frtExtSub(int32_t a); +const uint8_t *frtExtXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen); + +// caseTermBind: what the bound stream received, and how often it was +// told the binding ended. +static char sBindText[FRT_BIND_MAX]; +static int32_t sBindLen = 0; +static int32_t sBindUnbound = 0; + +// errorDialogCheck: how often a modal box came up, and the last one's +// title +typedef struct { + int32_t seen; + char title[64]; +} ErrorDlgT; + +// caseDynamicControl: a control class with a dynamic interface; Title +// is stored here, Level reads 9, Ping returns its argument count and +// records the argument kinds (S / I / F / B). +static char sFrtDynTitle[FRT_DYN_TEXT]; +static char sFrtDynArgTypes[FRT_DYN_TEXT]; +static int32_t sFrtDynType = -1; + +static const WidgetClassT sFrtDynClass = { .version = WGT_CLASS_VERSION }; + +static const WgtPropDescT sFrtDynProps[] = { + { "Title", WGT_IFACE_STRING, WGT_DYN_ACCESS, WGT_DYN_ACCESS, NULL }, + { "Level", WGT_IFACE_INT, WGT_DYN_ACCESS, NULL, NULL }, +}; + +static const WgtMethodDescT sFrtDynMethods[] = { + { "Ping", WGT_SIG_DYNAMIC, WGT_DYN_ACCESS }, +}; + +static const WgtDynIfaceT sFrtDynHooks = { + .create = frtDynCreate, + .getProp = frtDynGet, + .setProp = frtDynSet, + .callMethod = frtDynCall +}; + +static const WgtIfaceT sFrtDynIface = { + .basName = FRT_DYN_BAS, + .props = sFrtDynProps, + .propCount = DVX_ARRAY_LEN(sFrtDynProps), + .methods = sFrtDynMethods, + .methodCount = DVX_ARRAY_LEN(sFrtDynMethods), + .dyn = &sFrtDynHooks +}; // Test-only method table: a status-returning string method // (WGT_SIG_RET_INT_STR) that no shipped widget has to provide. @@ -879,6 +1052,66 @@ static void caseAppNotepadCloseRequest(void) { } +// Padding / Spacing of a form (its content box) and Alignment / Padding / +// Spacing of a box control, from the .frm and from code (0 = the default). +static void caseBoxLayoutProps(void) { + tCase("box layout: form Padding/Spacing, HBox Alignment/Padding/Spacing"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Boxes\"\n" + " Spacing = 8\n" + " Padding = 6\n" + " Begin HBox Box1\n" + " Alignment = Right\n" + " Spacing = 7\n" + " Padding = 3\n" + " Begin CommandButton Cmd1\n" + " Caption = \"A\"\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print Me.Spacing; Me.Padding; Box1.Alignment; Box1.Spacing; Box1.Padding\n" + " Me.Spacing = 5\n" + " Box1.Padding = 0\n" + " Box1.Alignment = \"Center\"\n" + " Print Me.Spacing; Box1.Padding; Box1.Alignment\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *box = form ? frtCtrl(form, "Box1") : NULL; + + TEST_TRUE(form != NULL && box != NULL); + + if (form && box) { + TEST_EQ(form->contentBox->spacing, wgtPixels(8)); + TEST_EQ(form->contentBox->padding, wgtPixels(6)); + TEST_EQ(box->widget->align, AlignEndE); + TEST_EQ(box->widget->spacing, wgtPixels(7)); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "8 6 2 7 3 \n5 0 1 \n"); + TEST_EQ(form->contentBox->spacing, wgtPixels(5)); + TEST_EQ(box->widget->padding, 0); + TEST_EQ(box->widget->align, AlignCenterE); + } + + frtDestroy(&app); + tShutdown(); +} + + static void caseBuildProject(void) { tCase("basBuild project concat, source map and build errors"); @@ -1100,7 +1333,7 @@ static void caseCommonMethods(void) { TEST_TRUE(!cmd->widget->enabled); TEST_TRUE(!cmd->widget->visible); TEST_EQ(form->popupMenuCount, 2); - TEST_EQ(form->menuIdMapCount, 2); + TEST_EQ(form->menuIdMapCount, 3); TEST_STR_EQ(form->popupMenus[0].name, "dyn"); TEST_STR_EQ(form->popupMenus[1].name, "dynSub"); TEST_TRUE(form->popupMenus[0].ownsMenu); @@ -1109,7 +1342,8 @@ static void caseCommonMethods(void) { TEST_TRUE(form->popupMenus[0].menu->items[1].separator); TEST_PTR_EQ(form->popupMenus[0].menu->items[2].subMenu, form->popupMenus[1].menu); TEST_STR_EQ(form->menuIdMap[0].name, "dynItem"); - TEST_STR_EQ(form->menuIdMap[1].name, "dynSubItem"); + TEST_STR_EQ(form->menuIdMap[1].name, "dynSub"); + TEST_STR_EQ(form->menuIdMap[2].name, "dynSubItem"); TEST_TRUE(frtCtrl(form, "TbImg") != NULL); TEST_TRUE(frtCtrl(form, "TbTxt") != NULL); TEST_STR_EQ(wgtGetText(frtCtrl(form, "TbTxt")->widget), "Text"); @@ -1122,6 +1356,7 @@ static void caseCommonMethods(void) { frtFire(&app, "Form1", "Destroy"); TEST_TRUE(!app.rt->terminated); TEST_EQ(form->popupMenuCount, 0); + TEST_EQ(form->menuIdMapCount, 0); frtDestroy(&app); tShutdown(); @@ -1194,6 +1429,98 @@ static void caseControlArrays(void) { } +// Control members of round 2 from BASIC: TextBox SelStart / SelLength / +// SelText / SelectAll, SpinButton RealValue / SetRealRange, Image +// MaskColor / UseMaskColor, the Data members without a database, +// ScrollPane.ScrollToControl, and the TextArea Colorize event (ByRef +// Colors written back through the runtime). +static void caseControlMembers2(void) { + tCase("TextBox Sel*, SpinButton RealValue, Image MaskColor, Data members, ScrollToControl, Colorize"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Members\"\n" + " Begin TextBox Text1\n" + " End\n" + " Begin SpinButton Spn1\n" + " End\n" + " Begin Image Img1\n" + " End\n" + " Begin Data Dat1\n" + " End\n" + " Begin ScrollPane Sp1\n" + " Begin Label Lbl1\n" + " Caption = \"Inside\"\n" + " End\n" + " End\n" + " Begin TextArea Area1\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Run\n" + " Text1.Text = \"hello world\"\n" + " Text1.SelStart = 6\n" + " Text1.SelLength = 5\n" + " Print Text1.SelText; \"|\";\n" + " Text1.SelText = \"there\"\n" + " Text1.SelectAll\n" + " Print Text1.Text; Text1.SelLength\n" + " Spn1.RealMode = True\n" + " Spn1.SetRealRange 0.5, 2.5\n" + " Spn1.RealValue = 9\n" + " Print Spn1.RealValue\n" + " Img1.MaskColor = vbRed\n" + " Img1.UseMaskColor = True\n" + " Print Img1.MaskColor = vbRed; Img1.UseMaskColor\n" + " Print Dat1.RecordCount; Dat1.FieldCount; \"[\"; Dat1.FieldName(0); Dat1.GetField(\"x\"); Dat1.GetCell(0, 0); \"]\"\n" + " Sp1.ScrollToControl Lbl1\n" + " Sp1.ScrollToControl \"Lbl1\"\n" + " Area1.Text = \"kkk\"\n" + " Area1.SetSyntaxMode \"custom\"\n" + "End Sub\n" + "Sub Form1_Count\n" + " Print Area1.Tag\n" + "End Sub\n" + "Sub Area1_Colorize(LineText As String, Colors As String)\n" + " Area1.Tag = LineText\n" + " Colors = String$(Len(LineText), \"1\")\n" + "End Sub\n" + "Sub Form1_BadScroll\n" + " Text1.ScrollToControl Lbl1\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Run")); + TEST_STR_EQ(frtOut(&app), "world|hello there11 \n2.5 \nTrue True \n0 0 []\n"); + tPump(FRT_PAINT_PUMPS); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Count")); + TEST_STR_EQ(frtOut(&app), "kkk\n"); + + // The handler's Colors comes back to the widget (ByRef String) + BasControlT *area = frtCtrl(frtForm(&app, "Form1"), "Area1"); + WgtValueT colorArgs[2]; + + colorArgs[0] = wgtValueStr("ab"); + colorArgs[1] = wgtValueStr("00"); + TEST_TRUE(area && wgtFireEvent(area->widget, "Colorize", colorArgs, 2)); + TEST_STR_EQ(colorArgs[1].s, "11"); + TEST_TRUE(!app.rt->terminated); + frtFire(&app, "Form1", "BadScroll"); + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + tShutdown(); +} + + static void caseCreateFormDynamic(void) { tCase("CreateForm/CreateControl at runtime"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -1240,12 +1567,18 @@ static void caseCreateFormDynamic(void) { // Direct type names (already-canonical widget names) resolve too. TEST_TRUE(basFormRtCreateCtrl(app.rt, f2, "button", "RawB") != NULL); - // Unloading the modal form clears the modal gate. + // Unloading the modal form (from the dialog script, while Show runs) + // clears the modal gate and ends the Show. + sModalCheckApp = &app; + sModalChecks = 0; + tDialogScript(unloadModalCheck, f3); basFormRtShowForm(app.rt, f3, true); - TEST_PTR_EQ(tCtx()->modalWindow, f3->window); - basFormRtUnloadForm(app.rt, f3); tPump(1); + TEST_EQ(sModalChecks, 1); TEST_PTR_EQ(tCtx()->modalWindow, NULL); + TEST_PTR_EQ(frtForm(&app, "Form3"), NULL); + sModalCheckApp = NULL; + frtDialogsDefault(&app); // The window factory's auto-size and explicit-position paths. WidgetT *auxRoot = NULL; @@ -1294,6 +1627,104 @@ static void caseCreateFormDynamic(void) { } +// Named events a widget fires with wgtFireEvent reach CtrlName_Event +// through onEvent with their arguments; parameters a handler changes are +// written back (ByRef), Strings included. +// Universal names go to the universal callbacks. Also the PictureBox's +// BorderStyle (the canvas border-less mode) from the .frm and from code. +static void caseCustomEvents(void) { + tCase("custom events through onEvent, ByRef write-back, PictureBox BorderStyle"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Custom\"\n" + " Begin ListView Lv1\n" + " End\n" + " Begin CommandButton Cmd1\n" + " Caption = \"Go\"\n" + " End\n" + " Begin PictureBox Pic1\n" + " BorderStyle = None\n" + " End\n" + "End\n" + "Sub Lv1_ColumnClick(Column As Integer)\n" + " If Column = 2 Then Print \"col\"\n" + " Column = 7\n" + "End Sub\n" + "Sub Cmd1_Click\n" + " Print \"click\"\n" + "End Sub\n" + "Sub Cmd1_ItemOpen(Index As Integer, Label As String, Ok As Boolean)\n" + " If Index = 5 And Label = \"x\" And Ok Then Print \"open\"\n" + " Label = \"changed\"\n" + " Ok = False\n" + "End Sub\n" + "Sub Form1_Border\n" + " If Pic1.BorderStyle = 0 Then Print \"none\";\n" + " Pic1.BorderStyle = 1\n" + " If Pic1.BorderStyle = 1 Then Print \" fixed\";\n" + " Pic1.BorderStyle = \"None\"\n" + " If Pic1.BorderStyle = 0 Then Print \" none\"\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *lv = form ? frtCtrl(form, "Lv1") : NULL; + BasControlT *cmd = form ? frtCtrl(form, "Cmd1") : NULL; + BasControlT *pic = form ? frtCtrl(form, "Pic1") : NULL; + + TEST_TRUE(lv && cmd && pic); + + if (lv && cmd && pic) { + WgtValueT args[3]; + + frtOutClear(&app); + args[0] = wgtValueInt(2); + TEST_TRUE(wgtFireEvent(lv->widget, "ColumnClick", args, 1)); + TEST_STR_EQ(frtOut(&app), "col\n"); + TEST_EQ(args[0].i, 7); + + frtOutClear(&app); + TEST_TRUE(wgtFireEvent(cmd->widget, "click", NULL, 0)); + TEST_STR_EQ(frtOut(&app), "click\n"); + + frtOutClear(&app); + args[0] = wgtValueInt(5); + args[1] = wgtValueStr("x"); + args[2] = wgtValueBool(true); + TEST_TRUE(wgtFireEvent(cmd->widget, "ItemOpen", args, 3)); + TEST_STR_EQ(frtOut(&app), "open\n"); + TEST_STR_EQ(args[1].s, "changed"); + TEST_EQ(args[2].type, WGT_IFACE_BOOL); + TEST_EQ(args[2].i, 0); + + // No handler: the arguments stay as they were + args[0] = wgtValueInt(5); + TEST_TRUE(wgtFireEvent(cmd->widget, "NoHandlerEvent", args, 1)); + TEST_EQ(args[0].i, 5); + + WgtValueT v; + + TEST_TRUE(wgtGetProp(pic->widget, "BorderStyle", &v) && v.i == 0); + TEST_EQ(pic->widget->contentOffX, 0); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Border")); + TEST_STR_EQ(frtOut(&app), "none fixed none\n"); + } + + frtDestroy(&app); + tShutdown(); +} + + static void caseDataBinding(void) { tCase("Data control binding without a database behind it"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -1361,6 +1792,379 @@ static void caseDataBinding(void) { } +// CommandButton Default / Cancel: Enter presses the Default button unless +// a button has the focus (which Enter pressed itself), Escape the Cancel +// button wherever the focus is. +static void caseDefaultCancelButtons(void) { + tCase("CommandButton Default and Cancel: Enter and Escape on the form"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Dialog\"\n" + " Begin TextBox Text1\n" + " End\n" + " Begin CommandButton CmdOther\n" + " Caption = \"Other\"\n" + " End\n" + " Begin CommandButton CmdOk\n" + " Caption = \"OK\"\n" + " Default = True\n" + " End\n" + " Begin CommandButton CmdCancel\n" + " Caption = \"Cancel\"\n" + " Cancel = True\n" + " End\n" + "End\n" + "\n" + "Sub CmdOther_Click\n" + " Print \"other\"\n" + "End Sub\n" + "Sub CmdOk_Click\n" + " Print \"ok\"; CmdOk.Default; CmdCancel.Cancel; CmdOther.Default\n" + "End Sub\n" + "Sub CmdCancel_Click\n" + " Print \"cancel\"\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *text = form ? frtCtrl(form, "Text1") : NULL; + BasControlT *other = form ? frtCtrl(form, "CmdOther") : NULL; + + TEST_TRUE(text != NULL && other != NULL); + + if (text && other) { + frtOutClear(&app); + wgtSetFocused(text->widget); + tKeyEnter(); + tKeyEsc(); + tPump(SETTLE_FRAMES_FRT); + TEST_STR_EQ(frtOut(&app), "okTrue True False \ncancel\n"); + + frtOutClear(&app); + wgtSetFocused(other->widget); + tKeyEnter(); + tPump(SETTLE_FRAMES_FRT); + TEST_STR_EQ(frtOut(&app), "other\n"); + } + + frtDestroy(&app); + tShutdown(); +} + + +// The design surface and the live control tree: CallByName, Controls, +// Parent / Container, MoveBefore, LoadFormText, DesignMode with +// DesignMouse / DesignKey and the selection handles. +static void caseDesignerLive(void) { + tCase("designer: CallByName, Controls, Container, MoveBefore, LoadFormText, DesignMode"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Dsgn\"\n" + " Width = 400\n" + " Height = 300\n" + " Begin CommandButton Command1\n" + " Caption = \"One\"\n" + " End\n" + " Begin Frame Frame1\n" + " Caption = \"Box\"\n" + " Begin TextBox Text1\n" + " Text = \"hi\"\n" + " End\n" + " End\n" + " Begin Label Label1\n" + " Caption = \"L\"\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Names\n" + " Dim c As Object\n" + " Print Me.Controls.Count;\n" + " For Each c In Me.Controls\n" + " Print c.Name; \" \";\n" + " Next\n" + " Print\n" + " Print Me.Controls(\"Text1\").Text; \" \"; Me.Controls(1).Name; \" \"; Text1.Parent.Name; \" \"; Text1.Container.Name; \" \"; Command1.Container.Name\n" + "End Sub\n" + "Sub Form1_ByName\n" + " Dim n As String\n" + " Dim col As New Collection\n" + " n = \"Text\"\n" + " Print CallByName(Command1, \"Caption\", vbGet); \" \";\n" + " CallByName Command1, \"Caption\", vbLet, \"Two\"\n" + " Print Command1.Caption; \" \"; CallByName(Text1, n, vbGet); \" \"; CallByName(Me, \"Caption\", vbGet)\n" + " col.Add \"first\"\n" + " col.Add \"second\"\n" + " Print CallByName(col, \"Count\", vbGet); CallByName(col, \"Item\", vbMethod, 2)\n" + " CallByName Text1, \"SetFocus\", vbMethod\n" + " Print IsEmpty(CallByName(Command1, \"Caption\", vbLet, \"Three\")); Command1.Caption\n" + "End Sub\n" + "Sub Form1_Move\n" + " Dim c As Object\n" + " Label1.MoveBefore Command1\n" + " Set Command1.Container = Frame1\n" + " Command1.MoveBefore Text1\n" + " For Each c In Me.Controls\n" + " Print c.Name; \" \";\n" + " Next\n" + " Print Command1.Container.Name\n" + "End Sub\n" + "Sub Form1_BadMove\n" + " Set Frame1.Container = Text1\n" + "End Sub\n" + "Sub Form1_FromText\n" + " Dim f As Object\n" + " Dim t As String\n" + " t = \"Begin Form Gen1\" & Chr$(10) & \" Caption = \" & Chr$(34) & \"Made\" & Chr$(34) & Chr$(10) & \" Begin CommandButton Go\" & Chr$(10) & \" Caption = \" & Chr$(34) & \"Go!\" & Chr$(34) & Chr$(10) & \" End\" & Chr$(10) & \"End\" & Chr$(10)\n" + " Set f = LoadFormText(t)\n" + " Print f.Caption; f.Controls.Count; f.Controls(1).Caption; TypeName(f)\n" + " Unload f\n" + "End Sub\n" + "Sub Form1_Design\n" + " Me.DesignMode = True\n" + " Me.DesignSelection = \"Command1\"\n" + " Print Me.DesignMode; Me.DesignSelection\n" + "End Sub\n" + "Sub Form1_EndDesign\n" + " Me.DesignMode = False\n" + "End Sub\n" + "Sub Form1_DesignMouse(Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer)\n" + " If Action <> 2 Then Print \"dm\"; Action; Button; CtrlName; Handle\n" + "End Sub\n" + "Sub Form1_DesignKey(KeyCode As Integer, Shift As Integer)\n" + " Print \"dk\"; KeyCode\n" + "End Sub\n" + "Sub Command1_Click\n" + " Print \"clicked\"\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Names")); + TEST_STR_EQ(frtOut(&app), "4 Command1 Frame1 Text1 Label1 \nhi Command1 Form1 Frame1 Form1\n"); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "ByName")); + TEST_STR_EQ(frtOut(&app), "One Two hi Dsgn\n2 second\nTrue Three\n"); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *cmd = form ? frtCtrl(form, "Command1") : NULL; + BasControlT *frame = form ? frtCtrl(form, "Frame1") : NULL; + BasControlT *text = form ? frtCtrl(form, "Text1") : NULL; + + TEST_TRUE(cmd && frame && text); + + if (!cmd || !frame || !text) { + frtDestroy(&app); + tShutdown(); + return; + } + + // The layout follows: the button is the frame's first child now + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Move")); + TEST_STR_EQ(frtOut(&app), "Label1 Frame1 Command1 Text1 Frame1\n"); + TEST_PTR_EQ(cmd->widget->parent, frame->widget); + TEST_PTR_EQ(frame->widget->firstChild, cmd->widget); + TEST_PTR_EQ(cmd->widget->nextSibling, text->widget); + + // A control cannot go inside a non-container + TEST_TRUE(frtFire(&app, "Form1", "BadMove")); + TEST_TRUE(app.rt->terminated); + frtClearError(&app); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "FromText")); + TEST_STR_EQ(frtOut(&app), "Made1 Go!Form\n"); + TEST_PTR_EQ(frtForm(&app, "Gen1"), NULL); + + // Design mode: the window takes the mouse and keys, no Click fires + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Design")); + TEST_STR_EQ(frtOut(&app), "True Command1\n"); + tPump(2); + + int32_t cx; + int32_t cy; + int32_t hx; + int32_t hy; + + tWidgetCenter(cmd->widget, &cx, &cy); + frtOutClear(&app); + tClick(cx, cy); + tPump(2); + TEST_STR_EQ(frtOut(&app), "dm1 1 Command1-1 \ndm3 1 Command1-1 \n"); + + // The SE handle of the selection, painted black over the widgets + basDsgnHandleOrigin(cmd->widget, BAS_DSGN_HANDLE_SE, &hx, &hy); + + int32_t bpp = tCtx()->display.format.bytesPerPixel; + uint32_t pixel = 0; + uint32_t black = packColor(&tCtx()->display, 0, 0, 0); + + memcpy(&pixel, form->window->contentBuf + (hy + 1) * form->window->contentPitch + (hx + 1) * bpp, (size_t)bpp); + TEST_EQ(pixel, black); + TEST_EQ(basDsgnHandleAt(cmd->widget, hx + 1, hy + 1), BAS_DSGN_HANDLE_SE); + TEST_EQ(basDsgnCursorAt(cmd->widget, hx + 1, hy + 1), CURSOR_RESIZE_DIAG_NWSE); + + frtOutClear(&app); + tClick(cx - cmd->widget->w / 2 + (hx + 1 - cmd->widget->x), cy - cmd->widget->h / 2 + (hy + 1 - cmd->widget->y)); + tPump(2); + TEST_TRUE(strstr(frtOut(&app), "dm1 1 Command14 \n") != NULL); + + frtOutClear(&app); + tKey(0, 'q', 0); + tPump(2); + TEST_STR_EQ(frtOut(&app), "dk113 \n"); + + // Off again: the button clicks + TEST_TRUE(frtFire(&app, "Form1", "EndDesign")); + tPump(2); + frtOutClear(&app); + tClick(cx, cy); + tPump(2); + TEST_STR_EQ(frtOut(&app), "clicked\n"); + + frtDestroy(&app); + tShutdown(); +} + + +// designer.bas: the control type registry from BASIC, with the shipped +// include file, as the C IDE reads it (basCtl*). +static void caseDesignerRegistry(void) { + tCase("designer.bas: control types, properties, methods, events"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *code = + "Function FindProp(t As String, p As String) As Integer\n" + " Dim i As Integer\n" + " FindProp = -1\n" + " For i = 0 To CtlPropCount(t) - 1\n" + " If CtlPropName$(t, i) = p Then FindProp = i\n" + " Next\n" + "End Function\n" + "Sub Form1_Types\n" + " Dim i As Integer\n" + " Dim found As Integer\n" + " Dim unsorted As Integer\n" + " For i = 0 To CtlTypeCount() - 1\n" + " If CtlTypeName$(i) = \"TextArea\" Then found = found + 1\n" + " If LCase$(CtlTypeName$(i - 1)) >= LCase$(CtlTypeName$(i)) Then unsorted = unsorted + 1\n" + " Next\n" + " Print found; CtlTypeName$(CtlTypeCount()) = \"\"; CtlTypeName$(-1) = \"\"; unsorted\n" + " Print CtlTypeFlags(\"Frame\"); CtlTypeFlags(\"CommandButton\"); CtlTypeFlags(\"Image\"); CtlTypeFlags(\"Nope\")\n" + " Print CtlTypeNamePrefix$(\"CommandButton\"); \" \"; CtlTypeDefaultEvent$(\"TextBox\"); \" \"; CtlTypeDefaultEvent$(\"Form\"); \" \"; CtlTypeTooltip$(\"Label\") <> \"\"\n" + " i = FindProp(\"TextArea\", \"SelText\")\n" + " Print CtlPropType(\"TextArea\", i) = CTL_PROP_STRING; (CtlPropType(\"TextArea\", FindProp(\"TextArea\", \"LineCount\")) And CTL_PROP_READONLY) <> 0\n" + " Print CtlPropType(\"Form\", FindProp(\"Form\", \"Controls\")) = CTL_PROP_OBJECT + CTL_PROP_READONLY; CtlPropType(\"Form\", FindProp(\"Form\", \"DesignMode\")) = CTL_PROP_BOOLEAN\n" + " Print FindProp(\"Menu\", \"Checked\") >= 0; FindProp(\"Menu\", \"Text\"); CtlPropType(\"TextBox\", 999); CtlPropName$(\"TextBox\", 999) = \"\"\n" + " Print CtlEventList$(\"Menu\"); \" \"; InStr(CtlEventList$(\"TextArea\"), \"|GutterClick\") > 0; InStr(CtlEventList$(\"Form\"), \"DesignMouse\") > 0\n" + " Print CtlEventParams$(\"KeyDown\"); \"|\"; CtlEventParams$(\"Click\"); \"|\"\n" + " Print InStr(CtlMethodList$(\"TextArea\"), \"|Undo|\") > 0; Left$(CtlMethodList$(\"Form\"), 9); \" \"; InStr(CtlMethodList$(\"Label\"), \"MoveBefore\") > 0; CtlMethodList$(\"Menu\") = \"\"\n" + "End Sub\n" + "Sub Form1_FrmNames\n" + " Dim t As String\n" + " t = \"Begin Form Gen1\" & Chr$(10) & \" Begin Label L1\" & Chr$(10) & \" End\" & Chr$(10) & \" Begin Menu mnuFile\" & Chr$(10) & \" End\" & Chr$(10) & \"End\" & Chr$(10) & \"Sub Gen1_Load\" & Chr$(10)\n" + " Print FrmFormName$(t); \" \"; FrmControlNames$(t); \" [\"; FrmCode$(t); \"]\"\n" + " Print ClsClassName$(\"Attribute VB_Name = \" & Chr$(34) & \"Acc\" & Chr$(34) & Chr$(10) & \"Dim x\", \"\"); \" \"; ClsClassName$(\"Dim x\", \"C:\\DIR\\THING.CLS\")\n" + "End Sub\n" + "Sub Form1_Enum\n" + " Dim t As String\n" + " Dim i As Integer\n" + " Dim j As Integer\n" + " For j = 0 To CtlTypeCount() - 1\n" + " t = CtlTypeName$(j)\n" + " For i = 0 To CtlPropCount(t) - 1\n" + " If (CtlPropType(t, i) And CTL_PROP_TYPEMASK) = CTL_PROP_ENUM And CtlPropEnum$(t, i) = \"\" Then Print \"empty enum \"; t; \".\"; CtlPropName$(t, i)\n" + " If (CtlPropType(t, i) And CTL_PROP_TYPEMASK) <> CTL_PROP_ENUM And CtlPropEnum$(t, i) <> \"\" Then Print \"stray enum \"; t; \".\"; CtlPropName$(t, i)\n" + " Next\n" + " Next\n" + " Print \"enums done\"\n" + "End Sub\n"; + const char *frm = "Begin Form Form1\n Caption = \"Reg\"\nEnd\n"; + int32_t len = 0; + char *inc = platformReadFile(FRT_INCLUDE_DIR "/designer.bas", &len); + char *src = (char *)malloc(FRT_SRC_BUF); + FrtAppT app; + + TEST_TRUE(inc != NULL && src != NULL); + + if (!inc || !src) { + free(inc); + free(src); + tShutdown(); + return; + } + + snprintf(src, FRT_SRC_BUF, "%s\n%s", inc, code); + free(inc); + + if (!expectBuild(&app, src, &frm, 1)) { + free(src); + tShutdown(); + return; + } + + free(src); + basNativeHostSetLive(true); + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Types")); + TEST_STR_EQ(frtOut(&app), + "1 True True 0 \n" + "3 2 0 -1 \n" + "Command Change Load True \n" + "True True \n" + "True True \n" + "True -1 -1 True \n" + "Click True True \n" + "KeyCode As Integer, Shift As Integer||\n" + "True Show|Hide True True \n"); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Enum")); + TEST_STR_EQ(frtOut(&app), "enums done\n"); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "FrmNames")); + TEST_STR_EQ(frtOut(&app), "Gen1 L1|mnuFile [Sub Gen1_Load\n]\nAcc THING\n"); + basNativeHostSetLive(false); + frtDestroy(&app); + + // The C side the IDE uses + BasCtlPropT prop; + + TEST_TRUE(basCtlPropFind("TextBox", "Mask", &prop) && prop.type == WGT_IFACE_STRING && prop.writable); + TEST_TRUE(basCtlPropFind(FRM_KW_FORM, BAS_PROP_CAPTION, &prop) && prop.writable); + TEST_TRUE(!basCtlPropFind("TextBox", "NoSuchProp", &prop)); + TEST_TRUE(basCtlIsContainer("Frame") && !basCtlIsContainer("Label")); + TEST_STR_EQ(basCtlDefaultEvent(FRM_KW_FORM), BAS_EVT_LOAD); + TEST_STR_EQ(basCtlEventParams("TextArea", BAS_EVT_GUTTERCLICK), "LineNumber As Integer"); + TEST_STR_EQ(basCtlEventParams(NULL, BAS_EVT_COLUMNCLICK), "Column As Integer"); + TEST_STR_EQ(basCtlEventParams("HelpPane", BAS_EVT_LINKCLICK), "Topic As String"); + TEST_STR_EQ(basCtlEventParams("TextBox", BAS_EVT_KEYDOWN), "KeyCode As Integer, Shift As Integer"); + TEST_STR_EQ(basCtlEventParams("Data", BAS_EVT_VALIDATE), "Cancel As Integer"); + TEST_STR_EQ(basCtlEventParams("TextBox", "NoSuchEvent"), ""); + TEST_TRUE(basEventParams(BAS_EVT_GUTTERCLICK) == NULL); + tShutdown(); +} + + static void caseDialogWrappers(void) { tCase("choice/input/file dialog wrappers incl. no-runtime guards"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -1370,9 +2174,13 @@ static void caseDialogWrappers(void) { TEST_STR_EQ(DlgInputBox("t", "p", "d"), ""); TEST_EQ(DlgInputCancelled(), -1); TEST_EQ(DlgIntInput("t", "p", 7, 0, 10), 7); + TEST_EQ(DlgInputCancelled(), -1); + TEST_EQ(DlgIntInputStep("t", "p", 7, 0, 10, 5), 7); TEST_EQ(DlgPromptSave("t"), DVX_SAVE_NO); TEST_STR_EQ(DlgFileOpen("t", "*.txt"), ""); TEST_STR_EQ(DlgFileSave("t", ""), ""); + TEST_STR_EQ(DlgFileOpenIn("t", "", "x"), ""); + TEST_STR_EQ(DlgFileSaveIn("t", "", "x"), ""); TEST_EQ(DlgColor("t", FRT_COLOR_START), -1); const char *frm = "Begin Form Form1\n Caption = \"Dlg\"\nEnd\n"; @@ -1393,14 +2201,35 @@ static void caseDialogWrappers(void) { TEST_EQ(DlgChoice("Pick", "p", "A|B|C", 1), 1); TEST_EQ(DlgChoice("Pick", "p", "", 0), -1); TEST_EQ(DlgChoice("Pick", "p", NULL, 0), -1); + + // A list far longer than a fixed buffer (an index of hundreds of + // keywords): every item reaches the dialog + char *many = (char *)malloc(FRT_CHOICE_MANY * FRT_CHOICE_ITEM_LEN); + int32_t pos = 0; + + TEST_TRUE(many != NULL); + + if (many) { + for (int32_t i = 0; i < FRT_CHOICE_MANY; i++) { + pos += snprintf(many + pos, FRT_CHOICE_ITEM_LEN, i ? "|keyword %03d" : "keyword %03d", (int)i); + } + + TEST_EQ(DlgChoice("Pick", "p", many, FRT_CHOICE_MANY - 1), FRT_CHOICE_MANY - 1); + free(many); + } TEST_EQ(DlgColor("Color", FRT_COLOR_START), FRT_COLOR_START); TEST_STR_EQ(dlg.title, "Color"); TEST_STR_EQ(DlgInputBox("T", "P", "dflt"), "dflt"); TEST_EQ(DlgInputCancelled(), 0); TEST_EQ(DlgIntInput("T", "P", 5, 0, 9), 5); + TEST_EQ(DlgInputCancelled(), 0); + TEST_EQ(DlgIntInputStep("T", "P", 20, 0, 100, 10), 20); + TEST_EQ(DlgInputCancelled(), 0); snprintf(dlg.button, sizeof(dlg.button), "Cancel"); + TEST_EQ(DlgIntInputStep("T", "P", 20, 0, 100, 10), 20); + TEST_EQ(DlgInputCancelled(), -1); TEST_EQ(DlgColor("Color", FRT_COLOR_START), -1); TEST_STR_EQ(DlgInputBox("T", "P", "dflt"), ""); TEST_EQ(DlgInputCancelled(), -1); @@ -1445,6 +2274,18 @@ static void caseDialogWrappers(void) { TEST_TRUE(strstr(savePick, "ts_save_pick.txt") != NULL); + // The In forms start in the folder given: a typed name lands there + TEST_EQ(platformMkdirRecursive("ts_in_dir"), 0); + memset(&typer, 0, sizeof(typer)); + typer.typeText = "ts_in_pick.txt"; + tDialogScript(fileDialogType, &typer); + savePick = DlgFileSaveIn("Save", NULL, "ts_in_dir"); + TEST_TRUE(strstr(savePick, "ts_in_dir") != NULL && strstr(savePick, "ts_in_pick.txt") != NULL); + memset(&typer, 0, sizeof(typer)); + tDialogScript(fileDialogType, &typer); + TEST_STR_EQ(DlgFileOpenIn("Open", "", "ts_in_dir"), ""); + TEST_TRUE(typer.seen >= 1); + // MsgBox default title. memset(&dlg, 0, sizeof(dlg)); snprintf(dlg.button, sizeof(dlg.button), "OK"); @@ -1553,6 +2394,10 @@ static void caseDragDrop(void) { return; } + // Every form window's own key handler (Default / Cancel buttons), which + // a drag takes over and gives back. + void (*formKey)(WindowT *w, int32_t key, int32_t mod) = win->onKey; + int32_t fx = win->x + win->w / 2; int32_t fy = win->y + win->contentY + win->contentH - FRT_FORM_EDGE; @@ -1575,7 +2420,7 @@ static void caseDragDrop(void) { TEST_TRUE(strstr(frtOut(&app), "SrcClick") == NULL); TEST_TRUE(strstr(frtOut(&app), "SrcDown") == NULL); TEST_TRUE(win->onMouse == widgetOnMouse); - TEST_TRUE(win->onKey == widgetOnKey); + TEST_TRUE(win->onKey == formKey); // Over, then Leave onto the form, dropped on the form. frtOutClear(&app); @@ -1678,7 +2523,7 @@ static void caseDragDrop(void) { TEST_TRUE(late && late->window && late->window->onMouse != widgetOnMouse); dragCallMethod(&app, src, BAS_DRAG_CANCEL); - TEST_TRUE(late && late->window && late->window->onMouse == widgetOnMouse && late->window->onKey == widgetOnKey); + TEST_TRUE(late && late->window && late->window->onMouse == widgetOnMouse && late->window->onKey == formKey); // Removing the dragged control cancels its drag. frtOutClear(&app); @@ -1700,6 +2545,76 @@ static void caseDragDrop(void) { } +// DrawFocusRect on a PictureBox: libdvx's dotted focus pattern +// (drawFocusRectDots) in the given color or ForeColor, in the object's +// scale. At 16 bpp Point reads white and black back exactly, so a dot +// compares equal to ForeColor and the gap to BackColor (parhlp's check). +static void caseDrawFocusRect(void) { + tCase("DrawFocusRect drawing method"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Focus\"\n" + " Begin PictureBox Pic\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Pic.Resize 40, 20\n" + " Pic.BackColor = RGB(0, 0, 255)\n" + " Pic.Cls\n" + " Pic.DrawFocusRect 0, 0, 10, 6, RGB(255, 0, 0)\n" + " Print Pic.Point(0, 0); Pic.Point(1, 0); Pic.Point(2, 0); Pic.Point(1, 5); Pic.Point(0, 2); Pic.Point(0, 1); Pic.Point(9, 1)\n" + " Pic.ForeColor = RGB(0, 255, 0)\n" + " Pic.DrawFocusRect 20, 10, -6, -4\n" + " Print Pic.Point(14, 6); Pic.Point(15, 6)\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), + "255 16711680 255 255 255 16711680 255 \n" + "65280 16711680 \n"); + frtDestroy(&app); + tShutdown(); + + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP_16); + + const char *frm16 = + "Begin Form Form1\n" + " Caption = \"Focus16\"\n" + " Begin PictureBox Pic\n" + " Visible = False\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Pic.Resize 20, 8\n" + " Pic.BackColor = RGB(255, 255, 255)\n" + " Pic.ForeColor = RGB(0, 0, 0)\n" + " Pic.Cls\n" + " Pic.DrawFocusRect 0, 0, 20, 8\n" + " Print Pic.Point(0, 0); Pic.Point(1, 0); Pic.Point(2, 0); Pic.Point(1, 0) = Pic.BackColor\n" + "End Sub\n"; + + if (!expectBuild(&app, "", &frm16, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), "0 16777215 0 True \n"); + frtDestroy(&app); + tShutdown(); +} + + // Form / PictureBox drawing methods (Phase 4): the QBasic drawing code // on canvases, with RGB colours. static void caseDrawMethods(void) { @@ -1746,6 +2661,9 @@ static void caseDrawMethods(void) { " Print Pic.Point(15, 15); Pic.CurrentX; Pic.AutoRedraw\n" " Me.Line (0, 0)-(5, 5), RGB(255, 0, 0)\n" " Me.Circle (20, 20), 5\n" + " Dim lowest As Double\n" + " lowest = -2147483648#\n" + " Pic.Print Tab(lowest); \"z\"\n" "End Sub\n"; FrtAppT app; @@ -1920,6 +2838,311 @@ static void caseDrawScale(void) { } +// The drawing members that draw DVX's own look through libdvx on a +// PictureBox (DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar upright +// and horizontal, DrawScrollArrow, PrintAccel), the measuring ones +// (AccelKey, TextWidthAccel, ScrollBarClick along either axis and in a +// scale), PaintPicture's vbGrayCopy and vbTransparentCopy, and the +// errors for bad arguments. +static void caseDrawThemed(void) { + tCase("DrawBevel / DrawButtonFace / DrawArrow / DrawScrollBar / DrawScrollArrow / PrintAccel / AccelKey / vbGrayCopy"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Themed\"\n" + " Begin PictureBox Pic1\n" + " Width = 120\n" + " Height = 100\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Draw\n" + " Pic1.DrawBevel 0, 0, 20, 20\n" + " Pic1.DrawBevel 30, 0, 20, 20, True, vbRed, 3\n" + " Pic1.DrawButtonFace 60, 0, 20, 20, True, vbGreen\n" + " Pic1.DrawArrow 10, 40, 4, dvxArrowDown, vbBlue\n" + " Pic1.DrawScrollBar 100, 0, 80, 5, 10, 50\n" + " Pic1.CurrentX = 0\n" + " Pic1.CurrentY = 60\n" + " Pic1.PrintAccel \"&Go\"\n" + " Print Pic1.Point(40, 10) = vbRed; Pic1.Point(70, 10) = vbGreen; Pic1.Point(10, 40) = vbBlue; Pic1.CurrentX = Pic1.TextWidthAccel(\"&Go\")\n" + " Print Pic1.AccelKey(\"&File\"); \"|\"; Pic1.AccelKey(\"None\"); \"|\"; Pic1.TextWidthAccel(\"&File\") = Pic1.TextWidth(\"File\")\n" + " Print Pic1.ScrollBarClick(80, 2, 5, 10, 50); Pic1.ScrollBarClick(80, 78, 5, 10, 50); vbLeftButton\n" + "End Sub\n" + "Sub Form1_Gray\n" + " Pic1.Cls\n" + " Pic1.PaintPicture LoadPicture(\"" FRT_FIXTURE_DIR "/icon.bmp\"), 0, 0, 32, 32, 0, 0, 32, 32, vbGrayCopy\n" + " c = Pic1.Point(16, 16)\n" + " Print (c And &HFF) = ((c \\ &H100) And &HFF)\n" + "End Sub\n" + "Sub Form1_Keyed\n" + " Pic1.BackColor = vbBlue\n" + " Pic1.PaintPicture LoadPicture(\"" FRT_FIXTURE_DIR "/icon.bmp\"), 0, 0, 32, 32, 0, 0, 32, 32, vbTransparentCopy\n" + " Print Pic1.Point(0, 0) = vbBlue\n" + "End Sub\n" + "Sub Form1_HBar\n" + " Pic1.Cls\n" + " Pic1.DrawScrollBar 0, 82, 96, 20, 16, 80, True\n" + " Pic1.DrawScrollArrow 40, 30, 16, dvxArrowRight\n" + " Print Pic1.ScrollBarClick(96, 2, 20, 16, 80, True); Pic1.ScrollBarClick(96, 94, 20, 16, 80, True); Pic1.ScrollBarClick(96, 20, 20, 16, 80, True); Pic1.ScrollBarClick(96, 70, 20, 16, 80, True); Pic1.ScrollBarClick(96, 40, 20, 16, 80, True)\n" + " Pic1.Scale (0, 0)-(Pic1.ScaleWidth * 2, Pic1.ScaleHeight / 2)\n" + " Print Pic1.ScrollBarClick(192, 20, 20, 16, 80, True); Pic1.ScrollBarClick(192, 20, 20, 16, 80)\n" + " Pic1.Scale\n" + " Pic1.Scale (0, 100)-(Pic1.ScaleWidth, 100 + Pic1.ScaleHeight)\n" + " Print Pic1.ScrollBarClick(80, 78, 5, 10, 50)\n" + "End Sub\n" + "Sub Form1_Ctrl\n" + " Pic1.Scale\n" + " Pic1.Cls\n" + " Pic1.Line (0, 0)-(39, 39), vbRed, BF\n" + " Pic1.DrawBevel 0, 0, 40, 40, True, -1\n" + " Pic1.DrawScrollBar 60, 0, 80, 5, 10, 50, False, True\n" + " Print Pic1.Point(20, 20) = vbRed; Pic1.ScrollBarClick(80, 2, 5, 10, 50, False, True); Pic1.ScrollBarClick(80, 78, 5, 10, 50, False, True); Pic1.ScrollBarClick(80, 60, 5, 10, 50, False, True)\n" + "End Sub\n" + "Sub Form1_BadArrow\n" + " Pic1.DrawArrow 10, 10, 4, 9\n" + "End Sub\n" + "Sub Form1_BadBevel\n" + " Pic1.DrawBevel 1, 2\n" + "End Sub\n" + "Sub Form1_BadScrollArrow\n" + " Pic1.DrawScrollArrow 1, 2, 16, dvxArrowUp, vbRed\n" + "End Sub\n" + "Sub Form1_BadScrollBar\n" + " Pic1.DrawScrollBar 0, 0, 50, 0, 1, 2, True, True, 9\n" + "End Sub\n" + "Sub Form1_BadBarClick\n" + " Print Pic1.ScrollBarClick(50, 2, 0, 1, 2, True, True, 9)\n" + "End Sub\n"; + const char *bad[] = { "BadArrow", "BadBevel", "BadScrollArrow", "BadScrollBar", "BadBarClick" }; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *pic = form ? frtCtrl(form, "Pic1") : NULL; + + TEST_TRUE(pic != NULL); + + if (!pic) { + frtDestroy(&app); + tShutdown(); + return; + } + + char want[FRT_DRAW_WANT_LEN]; + + snprintf(want, sizeof(want), "True True True True \nf||True \n4 6 %d \n", (int)MOUSE_LEFT); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Draw")); + TEST_STR_EQ(frtOut(&app), want); + TEST_EQ(wgtCanvasGetPixel(pic->widget, 0, 0), tCtx()->colors.windowHighlight); + TEST_EQ(wgtCanvasGetPixel(pic->widget, 30, 0), tCtx()->colors.windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic->widget, 19, 19), tCtx()->colors.windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic->widget, FRT_VBAR_X, FRT_VBAR_END_Y), tCtx()->colors.windowShadow); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Gray")); + TEST_TRUE(frtFire(&app, "Form1", "Keyed")); + TEST_STR_EQ(frtOut(&app), "True \nTrue \n"); + + // A horizontal bar: arrows one item, the trough a page (left and + // right of the thumb), the thumb nothing; its length and offset in + // the horizontal scale (a vertical bar in the same numbers is far + // longer); an offset counts from the bar, not the scale's origin. + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "HBar")); + TEST_STR_EQ(frtOut(&app), "19 21 4 36 20 \n19 4 \n6 \n"); + drawThemedHBarCheck(pic->widget); + + // DrawBevel's face -1 keeps the inside; a control's scroll bar (as + // ListBox's: WGT_SB_W wide, its rule for arrows and trough) + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Ctrl")); + TEST_STR_EQ(frtOut(&app), "True 4 6 15 \n"); + TEST_TRUE(wgtCanvasGetPixel(pic->widget, FRT_CBAR_X + WGT_SB_W - 1, FRT_CBAR_Y) != wgtCanvasGetPixel(pic->widget, FRT_CBAR_X + WGT_SB_W, FRT_CBAR_Y)); + TEST_TRUE(!app.rt->terminated); + frtDestroy(&app); + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(bad); i++) { + if (!expectBuild(&app, "", &frm, 1)) { + break; + } + + frtStart(&app, NULL); + frtFire(&app, "Form1", bad[i]); + + if (!app.rt->terminated) { + printf(" drawing error case %s did not stop the program\n", bad[i]); + } + + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + } + + tShutdown(); +} + + +// A control whose interface is dynamic (WgtDynIfaceT, as a BASIC +// UserControl will be): the .frm, property reads and writes and a +// WGT_SIG_DYNAMIC method with mixed arguments all go through the hooks. +static void caseDynamicControl(void) { + tCase("a dynamic-interface control from a .frm and from BASIC code"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + if (sFrtDynType < 0) { + sFrtDynType = wgtRegisterClass(&sFrtDynClass); + wgtRegisterIface(FRT_DYN_TYPE, &sFrtDynIface); + wgtIfaceSetType(FRT_DYN_TYPE, sFrtDynType); + } + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Dyn\"\n" + " Begin " FRT_DYN_BAS " Dyn1\n" + " Title = \"start\"\n" + " End\n" + "End\n" + "Sub Form1_Dyn\n" + " If Dyn1.Title = \"start\" Then Print \"t1\";\n" + " Dyn1.Title = 42\n" + " If Dyn1.Title = \"42\" Then Print \" t2\";\n" + " If Dyn1.Level = 9 Then Print \" lv\";\n" + " If Dyn1.Ping(\"a\", 2, 2.5, True) = 4 Then Print \" ping\"\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Dyn")); + TEST_STR_EQ(frtOut(&app), "t1 t2 lv ping\n"); + TEST_STR_EQ(sFrtDynArgTypes, "SIDB"); + frtDestroy(&app); + tShutdown(); +} + + +// The code editor members from BASIC: TextArea selection, lines, +// clipboard commands, ReplaceSelection, syntax colours, line markers, +// MaxLength and GutterClick; TextBox Mask. +static void caseEditorMembers(void) { + tCase("TextArea editor members, GutterClick and TextBox Mask from BASIC"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Ed\"\n" + " Width = 400\n" + " Height = 300\n" + " Begin TextArea Ed\n" + " Weight = 1\n" + " End\n" + " Begin TextBox Phone\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Edit\n" + " Ed.Text = \"one\" & Chr$(10) & \"two three\" & Chr$(10) & \"four\"\n" + " Ed.SetShowLineNumbers True\n" + " Print Ed.LineCount; Ed.GetLine(2); \"|\"; Ed.MaxLength\n" + " Ed.SetCursor 2, 5\n" + " Print Ed.CursorLine; Ed.CursorCol; Ed.SelStart\n" + " Ed.SelLength = 5\n" + " Print Ed.SelText; Ed.SelLength\n" + " Ed.SelText = \"3\"\n" + " Print Ed.GetLine(2); \"|\"\n" + " Ed.SelStart = 0\n" + " Ed.SelLength = 3\n" + " Ed.Cut\n" + " Print \"[\"; Ed.GetLine(1); \"]\"\n" + " Ed.Undo\n" + " Ed.CursorToEnd\n" + " Ed.Paste\n" + " Print Ed.GetLine(3); \"|\"\n" + " Ed.SelectAll\n" + " Ed.Copy\n" + " Print Ed.SelLength = Len(Ed.Text); Clipboard.GetText() = Ed.Text\n" + " Ed.SetCursor 1, 1\n" + " Print Ed.FindNext(\"two\", False, True); Ed.ReplaceSelection(\"two\", \"2\", False, True); Ed.GetLine(2)\n" + " Ed.SetSyntaxColor 1, RGB(255, 0, 0)\n" + " Print Ed.GetSyntaxColor(1) = RGB(255, 0, 0); Ed.GetSyntaxColor(0)\n" + " Ed.SetSyntaxColor 1, -1\n" + " Ed.SetLineMarker 2, RGB(200, 0, 0), -1\n" + " Ed.ClearLineMarkers\n" + " Ed.MaxLength = 4\n" + " Print Ed.MaxLength; Ed.Text\n" + " Ed.MaxLength = 0\n" + " Phone.Text = \"5551234\"\n" + " Phone.Mask = \"(###) ###-####\"\n" + " Print Phone.Text; \" \"; Phone.Mask\n" + " Phone.Mask = \"\"\n" + " Print Phone.Text; \" [\"; Phone.Mask; \"]\"\n" + "End Sub\n" + "Sub Form1_Lines\n" + " Ed.Text = \"a\" & Chr$(10) & \"b\" & Chr$(10) & \"c\"\n" + "End Sub\n" + "Sub Ed_GutterClick(LineNumber As Integer)\n" + " Print \"gutter\"; LineNumber\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Edit")); + TEST_STR_EQ(frtOut(&app), + "3 two three|65536 \n" + "2 5 8 \n" + "three5 \n" + "two 3|\n" + "[]\n" + "fourone|\n" + "True True \n" + "True True 2 3\n" + "True -1 \n" + "4 one\n\n" + "(555) 123-4___ (###) ###-####\n" + "(555) 123-4___ []\n"); + + // A click in the line-number gutter: GutterClick with the line + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *ed = form ? frtCtrl(form, "Ed") : NULL; + + TEST_TRUE(ed != NULL); + + if (ed) { + int32_t cx; + int32_t cy; + + TEST_TRUE(frtFire(&app, "Form1", "Lines")); + tPump(2); + tWidgetCenter(ed->widget, &cx, &cy); + frtOutClear(&app); + tClick(cx - ed->widget->w / 2 + FRT_GUTTER_X, cy - ed->widget->h / 2 + FRT_LINE_Y + tCtx()->font.charHeight); + TEST_STR_EQ(frtOut(&app), "gutter2 \n"); + } + + frtDestroy(&app); + tShutdown(); +} + + static void caseEventLoopExits(void) { tCase("basFormRtEventLoop exit conditions"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -2294,20 +3517,73 @@ static void caseExternCalls(void) { } +// basNativeHostSetLive: the host makes the calls (x86-64 registers, and +// the stack past six integer arguments), so each value is the C +// function's real result. A native SUB called as a statement leaves +// nothing behind: a thousand of them in a row do not fill the stack. +static void caseExternLive(void) { + tCase("DECLARE LIBRARY calls made for real on the host"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *bas = + "DECLARE LIBRARY \"test\"\n" + " DECLARE FUNCTION frtExtInt(BYVAL a AS INTEGER) AS INTEGER\n" + " DECLARE FUNCTION frtExtLong(BYVAL a AS LONG, BYVAL b AS LONG) AS LONG\n" + " DECLARE FUNCTION frtExtDbl(BYVAL d AS DOUBLE, BYVAL s AS SINGLE) AS DOUBLE\n" + " DECLARE FUNCTION frtExtMix(BYVAL s AS STRING, BYVAL d AS DOUBLE, BYVAL n AS LONG, BYVAL f AS SINGLE) AS DOUBLE\n" + " DECLARE FUNCTION frtExtStr(BYVAL s AS STRING) AS STRING\n" + " DECLARE FUNCTION frtExtFlag(BYVAL a AS INTEGER) AS BOOLEAN\n" + " DECLARE FUNCTION frtExtSeven(BYVAL a AS LONG, BYVAL b AS LONG, BYVAL c AS LONG, BYVAL d AS LONG, BYVAL e AS LONG, BYVAL f AS LONG, BYVAL g AS LONG) AS LONG\n" + " DECLARE SUB frtExtSub(BYVAL a AS LONG)\n" + " DECLARE FUNCTION frtExtXor$(BYVAL d AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING\n" + " DECLARE FUNCTION frtExtBinSum(BYVAL d AS BINARY STRING, BYVAL tag AS STRING, BYVAL d2 AS BINARY STRING) AS LONG\n" + "END DECLARE\n" + "DIM i AS LONG\n" + "DIM b AS STRING\n" + "b = frtExtXor$(\"A\" + CHR$(0) + \"B\", 1)\n" + "PRINT LEN(b); ASC(MID$(b, 1, 1)); ASC(MID$(b, 2, 1)); ASC(MID$(b, 3, 1))\n" + "PRINT LEN(frtExtXor$(b, -1)); LEN(frtExtXor$(b, 99)); frtExtXor$(CHR$(1) + CHR$(1), 1) = CHR$(0) + CHR$(0)\n" + "PRINT frtExtBinSum(CHR$(0) + CHR$(0) + CHR$(5), \"xy\", CHR$(0))\n" + "PRINT frtExtInt(5); frtExtLong(-1, 3); frtExtDbl(1.5, 2.5)\n" + "PRINT frtExtMix(\"abcd\", 0.5, 10, 0.25)\n" + "PRINT \"[\" & frtExtStr(\"abc\") & \"]\"; frtExtFlag(1)\n" + "PRINT frtExtSeven(1, 2, 3, 4, 5, 6, 7)\n" + "FOR i = 1 TO 1000\n" + " frtExtSub i\n" + " CALL frtExtSub(i)\n" + "NEXT i\n" + "PRINT \"subs\"\n"; + const char *frm = "Begin Form Form1\n Caption = \"Ext\"\nEnd\n"; + FrtAppT app; + + if (!expectBuild(&app, bas, &frm, 1)) { + tShutdown(); + return; + } + + basNativeHostSetLive(true); + frtStart(&app, NULL); + basNativeHostSetLive(false); + TEST_STR_EQ(frtOut(&app), "3 64 1 67 \n0 0 True \n1020305 \n6 2 4 \n14.75 \n[abc]True \n721 \nsubs\n"); + frtDestroy(&app); + tShutdown(); +} + + static void caseExternMarshal(void) { tCase("DECLARE LIBRARY marshalling words and return narrowing"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); - uint32_t words[BAS_EXTERN_MAX_NATIVE_WORDS]; - BasValueT args[2]; - BasValueT r; + BasNativeWordT words[BAS_EXTERN_MAX_NATIVE_WORDS]; + BasValueT args[2]; + BasValueT r; union { double d; uint32_t u[2]; } dconv; union { float f; uint32_t u; } fconv; // DOUBLE takes two words, SINGLE one word holding the float bits. args[0] = basValDouble(1.5); args[1] = basValSingle(2.5f); - r = basExternCall(NULL, (void *)frtExtDbl, "test", "frtExtDbl", args, 2, BAS_TYPE_DOUBLE); + r = basExternCall(NULL, (void *)frtExtDbl, "test", "frtExtDbl", args, 2, BAS_TYPE_DOUBLE, 0); dconv.d = 1.5; fconv.f = 2.5f; TEST_EQ(basNativeHostLastArgs(words, BAS_EXTERN_MAX_NATIVE_WORDS), 3); @@ -2318,17 +3594,38 @@ static void caseExternMarshal(void) { // SINGLE returns come back as Single, not Double. basNativeHostSetResult(0, 2.5); - r = basExternCall(NULL, (void *)frtExtDbl, "test", "frtExtDbl", args, 0, BAS_TYPE_SINGLE); + r = basExternCall(NULL, (void *)frtExtDbl, "test", "frtExtDbl", args, 0, BAS_TYPE_SINGLE, 0); TEST_EQ(r.type, BAS_TYPE_SINGLE); TEST_TRUE(r.sngVal == 2.5f); // INTEGER narrows to 16 bits; LONG keeps all 32. basNativeHostSetResult(FRT_EXT_WIDE_RESULT, 0.0); - r = basExternCall(NULL, (void *)frtExtInt, "test", "frtExtInt", args, 0, BAS_TYPE_INTEGER); + r = basExternCall(NULL, (void *)frtExtInt, "test", "frtExtInt", args, 0, BAS_TYPE_INTEGER, 0); TEST_EQ(basValToNumber(r), (double)(int16_t)FRT_EXT_WIDE_RESULT); - r = basExternCall(NULL, (void *)frtExtLong, "test", "frtExtLong", args, 0, BAS_TYPE_LONG); + r = basExternCall(NULL, (void *)frtExtLong, "test", "frtExtLong", args, 0, BAS_TYPE_LONG, 0); TEST_EQ(basValToNumber(r), (double)FRT_EXT_WIDE_RESULT); + // A BINARY STRING argument is the pointer and its length (zero bytes + // counted); a BINARY STRING result adds the length pointer last, and + // a callee that never sets it returns "". + args[0].type = BAS_TYPE_STRING; + args[0].strVal = basStringNew("a\0b", 3); + args[1] = basValLong(7); + r = basExternCall(NULL, (void *)frtExtXor, "test", "frtExtXor", args, 2, BAS_TYPE_STRING, 1u | BAS_EXTERN_BINARY_RET); + TEST_EQ(basNativeHostLastArgs(words, BAS_EXTERN_MAX_NATIVE_WORDS), 4); + TEST_TRUE(words[0] != 0); + TEST_EQ(words[1], 3); + TEST_EQ(words[2], 7); + TEST_TRUE(words[3] != 0); + TEST_EQ(r.type, BAS_TYPE_STRING); + TEST_EQ(r.strVal->len, 0); + basValRelease(&r); + + // The same string as plain STRING: one word + r = basExternCall(NULL, (void *)frtExtStr, "test", "frtExtStr", args, 1, BAS_TYPE_LONG, 0); + TEST_EQ(basNativeHostLastArgs(words, BAS_EXTERN_MAX_NATIVE_WORDS), 1); + basValRelease(&args[0]); + // sql.bas declares the int32_t-returning SQL functions AS LONG, so // a wide column value comes through intact. char *sqlBas = NULL; @@ -2382,6 +3679,142 @@ static void caseExternMarshal(void) { } +// New FormName: each copy has its own window, controls, menus, +// form-level variables and Me; its events run against it; the form's +// name stays the first copy; unloading a copy keeps the others. A +// FUNCTION As the form makes and returns a copy. +static void caseFormInstances(void) { + tCase("form instancing: New FormName copies"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *bas = + "Function MakeCopy() As Form1\n" + " Dim c As Form1\n" + " Set c = New Form1\n" + " c.Caption = \"Copy\"\n" + " c.Text1.Text = \"copytext\"\n" + " Set MakeCopy = c\n" + "End Function\n"; + const char *frm = + "Begin Form Form1\n" + " Caption = \"Main\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuAdd\n" + " Caption = \"&Add\"\n" + " End\n" + " End\n" + " Begin TextBox Text1\n" + " End\n" + " Begin CommandButton Cmd1\n" + " Caption = \"Go\"\n" + " End\n" + "End\n" + "\n" + "Dim Shared clicks As Integer\n" + "Dim Shared other As Form1\n" + "\n" + "Sub Form1_Load\n" + " Text1.Text = \"loaded\"\n" + "End Sub\n" + "\n" + "Sub Cmd1_Click\n" + " clicks = clicks + 1\n" + " Text1.Text = \"c\" & clicks\n" + " Print Me.Caption; clicks\n" + "End Sub\n" + "\n" + "Sub mnuAdd_Click\n" + " clicks = clicks + 10\n" + " Print Me.Caption; clicks\n" + "End Sub\n" + "\n" + "Sub Form1_Test\n" + " Set other = MakeCopy()\n" + " Dim f As New Form1\n" + " f.Caption = \"Third\"\n" + " Print Me.Caption; \"|\"; other.Caption; \"|\"; f.Caption; \"|\"; Text1.Text; \"|\"; other.Text1.Text; \"|\"; f.Text1.Text; \"|\"; Form1.Caption\n" + " Unload f\n" + "End Sub\n" + "\n" + "Sub Form1_Drop\n" + " Unload other\n" + " Set other = Nothing\n" + " Print Form1.Caption; \" \"; Text1.Text\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, bas, &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "Main|Copy|Third|loaded|copytext|loaded|Main\n"); + + // The third copy was unloaded by the handler; the first and the copy + // in gCopy remain, each with its own window and controls. + BasFormT *first = NULL; + BasFormT *copy = NULL; + int32_t count = 0; + + for (int32_t i = 0; i < (int32_t)arrlen(app.rt->forms); i++) { + if (strcasecmp(app.rt->forms[i]->name, "Form1") == 0) { + count++; + first = first ? first : app.rt->forms[i]; + copy = (app.rt->forms[i] != first) ? app.rt->forms[i] : copy; + } + } + + TEST_EQ(count, 2); + TEST_PTR_EQ(frtForm(&app, "Form1"), first); + TEST_TRUE(copy != NULL && copy->window != NULL && copy->window != first->window); + TEST_TRUE(copy != NULL && copy->window != NULL && !copy->window->visible); + + if (first && copy) { + BasControlT *btn1 = frtCtrl(first, "Cmd1"); + BasControlT *btn2 = frtCtrl(copy, "Cmd1"); + + TEST_TRUE(btn1 != NULL && btn2 != NULL && btn1 != btn2); + TEST_TRUE(copy->formVars != NULL && copy->formVars != first->formVars); + TEST_TRUE(copy->window->menuBar != NULL && copy->window->menuBar != first->window->menuBar); + + // Clicks on each copy count in that copy's own variable and + // write to its own Text1 + frtOutClear(&app); + btn2->widget->onClick(btn2->widget); + btn2->widget->onClick(btn2->widget); + btn1->widget->onClick(btn1->widget); + + MenuT *file = copy->window->menuBar->menus[0]; + + copy->window->onMenu(copy->window, file->items[0].id); + TEST_STR_EQ(frtOut(&app), "Copy1 \nCopy2 \nMain1 \nCopy12 \n"); + TEST_STR_EQ(wgtGetText(frtCtrl(copy, "Text1")->widget), "c2"); + TEST_STR_EQ(wgtGetText(frtCtrl(first, "Text1")->widget), "c1"); + + // Unloading the copy keeps the first + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Drop")); + tPump(1); + TEST_STR_EQ(frtOut(&app), "Main c1\n"); + TEST_PTR_EQ(frtForm(&app, "Form1"), first); + count = 0; + + for (int32_t i = 0; i < (int32_t)arrlen(app.rt->forms); i++) { + count += (strcasecmp(app.rt->forms[i]->name, "Form1") == 0) ? 1 : 0; + } + + TEST_EQ(count, 1); + } + + frtDestroy(&app); + tShutdown(); +} + + static void caseFormShowHide(void) { tCase("Show modal / non-modal and Hide"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -2390,15 +3823,23 @@ static void caseFormShowHide(void) { "Begin Form Form1\n" " Caption = \"One\"\n" "End\n"; + // Show True returns when the form is hidden: its first Paint hides it. const char *frm2 = "Begin Form Form2\n" " Caption = \"Two\"\n" - "End\n"; + "End\n" + "\n" + "Sub Form2_Paint\n" + " Static painted As Boolean\n" + " If Not painted Then\n" + " painted = True\n" + " Print \"modal=\"; Form2.Visible\n" + " Form2.Hide\n" + " End If\n" + "End Sub\n"; const char *frms[2] = { frm1, frm2 }; const char *bas = "Form2.Show True\n" - "Print \"modal=\"; Form2.Visible\n" - "Form2.Hide\n" "Print \"hidden=\"; Form2.Visible\n" "Form2.Show\n" "Form2.Visible = False\n" @@ -2432,12 +3873,17 @@ static void caseFormShowHide(void) { TEST_TRUE(f2->window->visible); TEST_PTR_EQ(tCtx()->modalWindow, NULL); - // Modal gate directly: Show True sets it, Hide clears it. + // Modal gate directly: Show True sets it while the form is up (the + // dialog script checks, then hides the form), Hide clears it. + sModalCheckApp = &app; + sModalChecks = 0; + tDialogScript(visibleKeepsModalCheck, f2); basFormRtShowForm(app.rt, f2, true); - TEST_PTR_EQ(tCtx()->modalWindow, f2->window); - basFormRtHideForm(app.rt, f2); + TEST_EQ(sModalChecks, 1); TEST_PTR_EQ(tCtx()->modalWindow, NULL); TEST_TRUE(!f2->window->visible); + sModalCheckApp = NULL; + frtDialogsDefault(&app); frtDestroy(&app); tShutdown(); @@ -2503,8 +3949,221 @@ static void caseFormVars(void) { } +// Form and control members of round 2: Tag, MousePointer (controls, the +// form, Screen; the pointer each shows), Screen / Form.ActiveControl, +// Form.Icon, FitWidth / FitHeight, SaveScreenshot, the mouse button and +// pointer constants, and the errors for bad values. +static void caseFormWindowMembers(void) { + tCase("Tag, MousePointer, ActiveControl, Icon, FitWidth / FitHeight, SaveScreenshot"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + char shotPath[DVX_MAX_PATH]; + char src[FRT_SRC_BUF_LEN]; + + snprintf(shotPath, sizeof(shotPath), "%s/frtShot.bmp", tTempDir()); + unlink(shotPath); + snprintf(src, sizeof(src), + "Sub Form1_Read\n" + " Print Text1.Tag; \"|\"; Form1.Tag; \"|\"; Text1.MousePointer; Form1.MousePointer\n" + " Text1.Tag = \"t2\"\n" + " Form1.Tag = \"f2\"\n" + " Print Text1.Tag; Form1.Tag\n" + "End Sub\n" + "Sub Form1_Focus\n" + " Form2.Show\n" + " Form1.Show\n" + " Text2.SetFocus\n" + " Print Screen.ActiveControl.Name; \" \"; Form1.ActiveControl.Name; \" \"; Form2.ActiveControl Is Nothing\n" + "End Sub\n" + "Sub Form1_Pointers\n" + " Text2.MousePointer = vbSizeWE\n" + " Print vbLeftButton; vbRightButton; vbMiddleButton; vbArrow; vbCrosshair; vbNoDrop; vbSyntaxType\n" + "End Sub\n" + "Sub Form1_ScreenPointer\n" + " Screen.MousePointer = vbSizeNS\n" + " Print Screen.MousePointer\n" + "End Sub\n" + "Sub Form1_ScreenDefault\n" + " Screen.MousePointer = vbDefault\n" + "End Sub\n" + "Sub Form1_SetIcon\n" + " Form1.Icon = LoadPicture(\"%s/icon.bmp\")\n" + " Print Form1.Icon.Width > 0\n" + "End Sub\n" + "Sub Form1_ClearIcon\n" + " Form1.Icon = \"\"\n" + " Print Form1.Icon.Width\n" + "End Sub\n" + "Sub Form1_Fit\n" + " Form1.Width = 600\n" + " Form1.Height = 400\n" + " Form1.FitWidth\n" + " Me.FitHeight\n" + "End Sub\n" + "Sub Form1_Shot\n" + " Me.SaveScreenshot \"%s\"\n" + "End Sub\n" + "Sub Form1_BadPointer\n" + " Text1.MousePointer = 3\n" + "End Sub\n" + "Sub Form1_BadIcon\n" + " Form1.Icon = \"/no/such/icon.bmp\"\n" + "End Sub\n" + "Sub Form1_BadShot\n" + " Me.SaveScreenshot \"/no/such/dir/x.bmp\"\n" + "End Sub\n", + FRT_FIXTURE_DIR, shotPath); + + const char *frm1 = + "Begin Form Form1\n" + " Caption = \"Members\"\n" + " Tag = \"f1\"\n" + " MousePointer = 2\n" + " Begin TextBox Text1\n" + " Tag = \"t1\"\n" + " MousePointer = 12\n" + " End\n" + " Begin TextBox Text2\n" + " End\n" + "End\n"; + const char *frm2 = + "Begin Form Form2\n" + " Caption = \"Other\"\n" + "End\n"; + const char *frms[2] = { frm1, frm2 }; + const char *bad[] = { "BadPointer", "BadIcon", "BadShot" }; + FrtAppT app; + + if (!expectBuild(&app, src, frms, 2)) { + tShutdown(); + return; + } + + frtStart(&app, "Form1"); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *text1 = form ? frtCtrl(form, "Text1") : NULL; + BasControlT *text2 = form ? frtCtrl(form, "Text2") : NULL; + + TEST_TRUE(text1 != NULL && text2 != NULL); + + if (!text1 || !text2) { + frtDestroy(&app); + tShutdown(); + return; + } + + WindowT *win = form->window; + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Read")); + TEST_STR_EQ(frtOut(&app), "t1|f1|12 2 \nt2f2\n"); + + // ActiveControl: the focused control; Nothing on a form without it + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Focus")); + TEST_STR_EQ(frtOut(&app), "Text2 Text2 True \n"); + + // The pointer over a control is its own, else the form's + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Pointers")); + TEST_STR_EQ(frtOut(&app), "1 2 4 1 2 12 6 \n"); + TEST_EQ(win->onCursorQuery(win, text1->widget->x + 1, text1->widget->y + 1), CURSOR_NO_DROP); + TEST_EQ(win->onCursorQuery(win, text2->widget->x + 1, text2->widget->y + 1), CURSOR_RESIZE_H); + TEST_EQ(win->onCursorQuery(win, 0, 0), CURSOR_CROSSHAIR); + + // Screen.MousePointer wins over every control + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "ScreenPointer")); + TEST_STR_EQ(frtOut(&app), "7 \n"); + TEST_EQ(win->onCursorQuery(win, text1->widget->x + 1, text1->widget->y + 1), CURSOR_RESIZE_V); + TEST_TRUE(frtFire(&app, "Form1", "ScreenDefault")); + TEST_EQ(win->onCursorQuery(win, text1->widget->x + 1, text1->widget->y + 1), CURSOR_NO_DROP); + + // Icon: a picture for the minimized window, "" puts DVX's back + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "SetIcon")); + TEST_TRUE(win->iconData != NULL); + TEST_TRUE(frtFire(&app, "Form1", "ClearIcon")); + TEST_TRUE(win->iconData == NULL); + TEST_STR_EQ(frtOut(&app), "True \n0 \n"); + + // FitWidth / FitHeight shrink-wrap one side each + TEST_TRUE(frtFire(&app, "Form1", "Fit")); + TEST_TRUE(win->w < FRT_FIT_BIG_W && win->h < FRT_FIT_BIG_H); + + // SaveScreenshot writes the window's picture + TEST_TRUE(frtFire(&app, "Form1", "Shot")); + TEST_TRUE(!app.rt->terminated); + TEST_TRUE(access(shotPath, F_OK) == 0); + unlink(shotPath); + frtDestroy(&app); + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(bad); i++) { + if (!expectBuild(&app, src, frms, 2)) { + break; + } + + frtStart(&app, "Form1"); + frtFire(&app, "Form1", bad[i]); + + if (!app.rt->terminated) { + printf(" form member error case %s did not stop the program\n", bad[i]); + } + + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + } + + tShutdown(); +} + + +// game.bas FrameMissed reads FRAMEWAIT's own pacer (the game library is +// linked into this test, so FRAMEWAIT runs, on the real-time clock): 0 +// before the first FRAMEWAIT and after it, at least 4 once the program +// got back to FRAMEWAIT 55 ms late at 100 frames a second, and 0 again +// after a new rate starts the clock over. +static void caseFrameMissed(void) { + tCase("game.bas FrameMissed counts the frames FRAMEWAIT skipped"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *bas = + "DECLARE LIBRARY \"game\"\n" + " DECLARE FUNCTION FrameMissed() AS LONG\n" + "END DECLARE\n" + "DECLARE LIBRARY \"test\"\n" + " DECLARE FUNCTION usleep(BYVAL us AS LONG) AS LONG\n" + "END DECLARE\n" + "PRINT FrameMissed()\n" + "FRAMEWAIT 100\n" + "PRINT FrameMissed()\n" + "PRINT usleep(55000)\n" + "FRAMEWAIT 100\n" + "PRINT FrameMissed() >= 4\n" + "FRAMEWAIT 50\n" + "PRINT FrameMissed()\n"; + const char *frm = "Begin Form Form1\n Caption = \"Frames\"\nEnd\n"; + FrtAppT app; + + if (!expectBuild(&app, bas, &frm, 1)) { + tShutdown(); + return; + } + + hostClockSetRealTime(true); + basNativeHostSetLive(true); + frtStart(&app, NULL); + basNativeHostSetLive(false); + hostClockSetRealTime(false); + TEST_STR_EQ(frtOut(&app), "0 \n0 \n0 \nTrue \n0 \n"); + frtDestroy(&app); + tShutdown(); +} + + static void caseFrmLoadEdges(void) { - tCase(".frm load edges: version, unknown type, geometry, nesting"); + tCase(".frm load edges: version, unknown type, geometry, late lines, nesting"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); // A .frm claiming a too-new VB version is refused whole. @@ -2646,6 +4305,48 @@ static void caseFrmLoadEdges(void) { frtClearError(&app); frtDestroy(&app); + // A block's lines after the blocks inside it are the block's, as the + // designer's model reads them (testFrmModel): a Frame's Index and + // Caption after its child, a Frame's Caption after a menu block in it. + const char *frmLate = + "Begin Form FormL\n" + " Begin Frame Fr\n" + " Begin Label L\n" + " End\n" + " Index = 2\n" + " Caption = \"late\"\n" + " End\n" + " Begin Frame Box\n" + " Begin Menu mnuInner\n" + " End\n" + " Caption = \"after\"\n" + " End\n" + "End\n"; + + if (!expectBuild(&app, "", &frmLate, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *fl = frtForm(&app, "FormL"); + BasControlT *fr2 = fl ? frtCtrlIdx(fl, "Fr", 2) : NULL; + BasControlT *lab = fl ? frtCtrl(fl, "L") : NULL; + BasControlT *box = fl ? frtCtrl(fl, "Box") : NULL; + bool inside = false; + + for (const WidgetT *w = lab ? lab->widget : NULL; w; w = w->parent) { + inside = inside || (fr2 && w == fr2->widget); + } + + TEST_TRUE(fr2 && lab && box); + TEST_STR_EQ(fr2 ? wgtGetText(fr2->widget) : "", "late"); + TEST_TRUE(inside); + TEST_STR_EQ(box ? wgtGetText(box->widget) : "", "after"); + TEST_TRUE(!app.rt->terminated); + frtDestroy(&app); + // Nesting deeper than the parser allows: rejected, form discarded. char deep[FRM_TEXT_MAX]; int32_t pos = snprintf(deep, sizeof(deep), "Begin Form FormD\n"); @@ -2917,11 +4618,168 @@ static void caseGapClosure(void) { } +// A SetEvent handler for a control on a form made by CreateForm runs with +// Me = that form (each job window of a program can share one handler), and +// module code can call a form's procedures. +static void caseHandlerContext(void) { + tCase("SetEvent handler Me on a CreateForm form; module calls a form SUB"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *bas = + "Sub ModuleCalls\n" + " Form1Hello\n" + "End Sub\n"; + const char *frm = + "Begin Form Form1\n" + " Caption = \"Context\"\n" + "End\n" + "\n" + "Sub Form1Hello\n" + " Print \"hello\"\n" + "End Sub\n" + "Sub Form1_Make\n" + " Dim f As Form\n" + " Dim b As Control\n" + " Set f = CreateForm(\"Job7\", 200, 100)\n" + " Set b = CreateControl(f, \"CommandButton\", \"JobCancel\")\n" + " SetEvent b, \"Click\", \"AnyCancel\"\n" + " f.Show\n" + " ModuleCalls\n" + "End Sub\n" + "Sub AnyCancel\n" + " Print \"me=\"; Me.Name\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, bas, &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Make")); + + BasFormT *job = frtForm(&app, "Job7"); + BasControlT *btn = job ? frtCtrl(job, "JobCancel") : NULL; + + TEST_TRUE(btn != NULL); + + if (btn) { + btn->widget->onClick(btn->widget); + } + + TEST_STR_EQ(frtOut(&app), "hello\nme=Job7\n"); + frtDestroy(&app); + tShutdown(); +} + + +// An untrapped runtime error in an event handler is shown and ends the +// program, as in module-level code and in VB: the one error path +// (title, VM message, halt). A trapped error is not reported; the IDE +// (suppressErrorDialog) gets the halt and the message without a box. +static void caseHandlerError(void) { + tCase("an untrapped error in an event handler is shown and ends the program"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Err\"\n" + "End\n" + "\n" + "Sub Form1_Divide\n" + " Dim z As Integer\n" + " Print \"before\"\n" + " Print 1 \\ z\n" + " Print \"after\"\n" + "End Sub\n" + "Sub Form1_Trapped\n" + " Dim z As Integer\n" + " On Error Resume Next\n" + " Print 1 \\ z\n" + " Print \"trapped\"\n" + "End Sub\n"; + const char *bas = "Dim z As Integer\nPrint 1 \\ z\nPrint \"unreached\"\n"; + const char *plain = "Begin Form Form1\n Caption = \"Mod\"\nEnd\n"; + FrtAppT app; + ErrorDlgT dlg; + char *log; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + memset(&dlg, 0, sizeof(dlg)); + frtStart(&app, NULL); + tDialogScript(errorDialogCheck, &dlg); + + // Trapped: nothing shown, the program goes on + frtFire(&app, "Form1", "Trapped"); + TEST_EQ(dlg.seen, 0); + TEST_TRUE(!app.rt->terminated); + TEST_STR_EQ(frtOut(&app), "trapped\n"); + + // Untrapped: the box, the log, the halt; the program ends + frtOutClear(&app); + frtFire(&app, "Form1", "Divide"); + TEST_EQ(dlg.seen, 1); + TEST_STR_EQ(dlg.title, "BASIC Runtime Error"); + TEST_STR_EQ(frtOut(&app), "before\n"); + TEST_TRUE(app.rt->terminated); + TEST_TRUE(app.vm->ended); + TEST_TRUE(!basFormRtKeepsRunning(app.rt)); + TEST_TRUE(strstr(basVmGetError(app.vm), "Division by zero") != NULL); + TEST_PTR_EQ(tCtx()->modalWindow, NULL); + log = platformReadFile(DVX_LOG_FILE, NULL); + TEST_TRUE(log && strstr(log, "BASIC RUNTIME ERROR: Runtime error 11 on line") != NULL && strstr(log, "\n Division by zero") != NULL); + free(log); + + // Nothing more runs or is shown once it has ended + frtFire(&app, "Form1", "Divide"); + TEST_EQ(dlg.seen, 1); + tDialogScript(NULL, NULL); + frtDestroy(&app); + + // Module-level code: the same box + if (expectBuild(&app, bas, &plain, 1)) { + memset(&dlg, 0, sizeof(dlg)); + basFormRtLoadAllForms(app.rt, NULL); + tDialogScript(errorDialogCheck, &dlg); + basFormRtRunSimple(app.rt); + tDialogScript(NULL, NULL); + TEST_EQ(dlg.seen, 1); + TEST_STR_EQ(dlg.title, "BASIC Runtime Error"); + TEST_TRUE(app.rt->terminated); + TEST_TRUE(strstr(frtOut(&app), "unreached") == NULL); + frtDestroy(&app); + } + + // The IDE: halted, the message kept, no box + if (expectBuild(&app, "", &frm, 1)) { + frtStart(&app, NULL); + app.rt->suppressErrorDialog = true; + memset(&dlg, 0, sizeof(dlg)); + tDialogScript(errorDialogCheck, &dlg); + frtFire(&app, "Form1", "Divide"); + tDialogScript(NULL, NULL); + TEST_EQ(dlg.seen, 0); + TEST_TRUE(app.rt->terminated); + TEST_TRUE(app.vm->ended); + TEST_TRUE(strstr(basVmGetError(app.vm), "Division by zero") != NULL); + frtDestroy(&app); + } + + tShutdown(); +} + + static void caseHelpHooks(void) { tCase("HelpCompile and HelpView through the stub library"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); - const FrtStubApiT *api = stubsOpen(); + const FrtStubApiT *api = frtStubsOpen(); if (!api) { TEST_TRUE(api != NULL); @@ -2935,7 +4793,7 @@ static void caseHelpHooks(void) { FrtAppT app; if (!expectBuild(&app, "", &frm, 1)) { - stubsClose(); + frtStubsClose(); tShutdown(); return; } @@ -2944,9 +4802,12 @@ static void caseHelpHooks(void) { TEST_EQ(HelpCompile("guide.dhs", "out.hlp"), -1); TEST_STR_EQ(api->hlpcLastOutput(), "out.hlp"); + TEST_STR_EQ(HelpCompileMessages(), ""); api->hlpcRc(2); TEST_EQ(HelpCompile("guide.dhs", "fail.hlp"), 0); + TEST_STR_EQ(HelpCompileMessages(), FRT_STUB_HLPC_ERROR "\n"); TEST_EQ(HelpCompile(NULL, "x.hlp"), 0); + TEST_TRUE(HelpCompileMessages()[0] != '\0'); TEST_EQ(HelpCompile("x.dhs", NULL), 0); // A bare help file name resolves against the app directory. @@ -2961,9 +4822,163 @@ static void caseHelpHooks(void) { TEST_STR_EQ(api->shellLastArgs(), "/tmp/abs.hlp"); HelpView(NULL); + // "" is the system help (the DVX user guide) + HelpView(""); + TEST_STR_EQ(api->shellLastArgs(), DVX_SYSTEM_HELP_FILE_LITERAL); + + // HelpViewTopic: the same path rules, then the topic; a path with + // blanks is quoted so the topic stays a separate argument + HelpViewTopic("topic.hlp", "a.topic"); + snprintf(expected, sizeof(expected), "%s" DVX_PATH_SEP "topic.hlp a.topic", app.vm->appPath); + TEST_STR_EQ(api->shellLastArgs(), expected); + HelpViewTopic("/tmp/my help.hlp", "b.topic"); + TEST_STR_EQ(api->shellLastArgs(), "\"/tmp/my help.hlp\" b.topic"); + HelpViewTopic("/tmp/abs.hlp", ""); + TEST_STR_EQ(api->shellLastArgs(), "/tmp/abs.hlp"); + HelpViewTopic("", "c.topic"); + TEST_STR_EQ(api->shellLastArgs(), DVX_SYSTEM_HELP_FILE_LITERAL " c.topic"); + HelpViewTopic("x.hlp", NULL); + TEST_STR_EQ(api->shellLastArgs(), DVX_SYSTEM_HELP_FILE_LITERAL " c.topic"); + frtDestroy(&app); basFormRtTestReset(); - stubsClose(); + frtStubsClose(); + tShutdown(); +} + + +// A help viewer's pieces from BASIC (hlpfile.bas called for real, the +// HelpPane control): topics, table of contents, index, search, records +// and wrapping; ShowTopic / Topic / LinkTopic / Clear; a click on a link +// fires LinkClick, whose handler shows the target topic. The CONSTs of +// hlpfile.bas must match hlpFormat.h. +static void caseHelpPane(void) { + tCase("HelpPane control and hlpfile.bas from BASIC"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + // The module: hlpfile.bas, then code that uses its CONSTs (the + // harness compiles form code before the module) + static const char moduleCode[] = + "SUB CheckRecords(BYVAL h AS LONG)\n" + " Chk HlpRecordType(h, 0) = HLP_REC_HEADING1 AND HlpRecordText$(h, 0) = \"Extra\", \"heading\"\n" + " Chk HlpRecordType(h, 1) = HLP_REC_LINK AND HlpRecordLink$(h, 1) = \"fx.welcome\" AND HlpRecordText$(h, 1) = \"Back to welcome\", \"link\"\n" + "END SUB\n" + "SUB PrintConsts\n" + " PRINT HLP_REC_TEXT; HLP_REC_IMAGE; HLP_REC_LINK; HLP_REC_TABLE; HLP_REC_CODE; HLP_NOTE_WARNING; HLP_IMG_RIGHT\n" + "END SUB\n"; + int32_t len = 0; + char *incl = platformReadFile(FRT_INCLUDE_DIR "/hlpfile.bas", &len); + char *bas = incl ? (char *)malloc((size_t)len + sizeof(moduleCode) + 1) : NULL; + + TEST_TRUE(bas != NULL); + + if (bas) { + snprintf(bas, (size_t)len + sizeof(moduleCode) + 1, "%s\n%s", incl, moduleCode); + } + + free(incl); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Help\"\n" + " Begin HelpPane HP1\n" + " Weight = 1\n" + " End\n" + "End\n" + "\n" + "Dim Shared hlp As Long\n" + "\n" + "Sub Chk(ByVal ok As Integer, ByVal what As String)\n" + " If ok Then\n" + " Print \"ok \";\n" + " Else\n" + " Print \"FAIL:\"; what; \" \";\n" + " End If\n" + "End Sub\n" + "\n" + "Sub Form1_Open\n" + " Dim t As Long, i As Long, n As Long, found As Integer\n" + " hlp = HlpOpen(\"" DVXTEST_HELP_HLP "\")\n" + " Chk hlp > 0, \"open\"\n" + " Chk HlpOpen(\"/nonexistent/x.hlp\") = 0, \"missing\"\n" + " t = HlpFindTopic(hlp, \"fx.extra\")\n" + " Chk t >= 0 And t < HlpTopicCount(hlp), \"find\"\n" + " Chk HlpTopicTitle$(hlp, t) = \"Extra File\", \"title\"\n" + " Chk HlpTopicId$(hlp, t) = \"fx.extra\", \"id\"\n" + " Chk HlpStartTopic(hlp, \"\") = HlpFindTopic(hlp, \"fx.welcome\"), \"start\"\n" + " found = 0\n" + " For i = 0 To HlpTocCount(hlp) - 1\n" + " If HlpTocTitle$(hlp, i) = \"Guide\" Then found = (HlpTocTopic(hlp, i) = -1)\n" + " Next i\n" + " Chk found, \"toc\"\n" + " found = 0\n" + " For i = 0 To HlpIndexCount(hlp) - 1\n" + " If HlpIndexKeyword$(hlp, i) = \"zebra\" Then found = (HlpIndexTopic(hlp, i) = HlpFindTopic(hlp, \"fx.deep\"))\n" + " Next i\n" + " Chk found, \"index\"\n" + " Chk HlpSearch(hlp, \"continuation\") = 1, \"search\"\n" + " Chk HlpTopicId$(hlp, HlpSearchTopic(hlp, 0)) = \"fx.blocks\", \"result\"\n" + " n = HlpLoadTopic(hlp, t)\n" + " Chk n = 3, \"records\"\n" + " CheckRecords hlp\n" + " Chk HlpWrap$(\"aaaa bbbb cccc\", 10) = \"aaaa bbbb\" & Chr$(10) & \"cccc\", \"wrap\"\n" + " HP1.ShowTopic hlp, t\n" + " Chk HP1.Topic = t, \"topic\"\n" + " Chk HP1.LinkTopic = \"\", \"linktopic\"\n" + " Print\n" + " PrintConsts\n" + "End Sub\n" + "\n" + "Sub HP1_LinkClick(Topic As String)\n" + " Print \"link \"; Topic; \" \"; HP1.LinkTopic\n" + " HP1.ShowTopic hlp, HlpFindTopic(hlp, Topic)\n" + "End Sub\n" + "\n" + "Sub Form1_Report\n" + " Print HP1.Topic = HlpFindTopic(hlp, \"fx.welcome\")\n" + " HP1.Clear\n" + " Print HP1.Topic\n" + " HlpClose hlp\n" + " Print HlpTopicCount(hlp)\n" + "End Sub\n"; + FrtAppT app; + + if (!bas || !expectBuild(&app, bas, &frm, 1)) { + free(bas); + tShutdown(); + return; + } + + free(bas); + basNativeHostSetLive(true); + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Open")); + + char expected[FRT_HELP_OUT_MAX]; + + snprintf(expected, sizeof(expected), "ok ok ok ok ok ok ok ok ok ok ok ok ok ok ok ok \n%d %d %d %d %d %d %d \n", HLP_REC_TEXT, HLP_REC_IMAGE, HLP_REC_LINK, HLP_REC_TABLE, HLP_REC_CODE, HLP_NOTE_WARNING, HLP_IMG_RIGHT); + TEST_STR_EQ(frtOut(&app), expected); + + // A click on the topic's link: LinkClick shows the welcome topic + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *hp = form ? frtCtrl(form, "HP1") : NULL; + WidgetT *link = hp ? firstFocusable(hp->widget) : NULL; + + TEST_TRUE(link != NULL); + + if (link) { + frtOutClear(&app); + tWidgetClick(link); + tPump(SETTLE_FRAMES_FRT); + TEST_STR_EQ(frtOut(&app), "link fx.welcome fx.welcome\n"); + } + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Report")); + TEST_STR_EQ(frtOut(&app), "True \n-1 \n0 \n"); + basNativeHostSetLive(false); + frtDestroy(&app); tShutdown(); } @@ -3055,6 +5070,96 @@ static void caseHelpTopic(void) { } +// ListView SetColumns alignment, SetRows, SelectedRows$ and ColumnClick; +// TreeView ListIndex, item tags and HasChildren. +static void caseListTreeMethods(void) { + tCase("ListView SetRows / SelectedRows / ColumnClick, TreeView ListIndex / tags"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Lists\"\n" + " Begin ListView LV1\n" + " Weight = 1\n" + " End\n" + " Begin TreeView TV1\n" + " Weight = 1\n" + " End\n" + "End\n" + "\n" + "Dim Shared changes As Integer\n" + "\n" + "Sub Form1_Fill\n" + " LV1.SetColumns \"Name,14|Size,12,Right|Type\"\n" + " LV1.SetRows \"a\" & Chr$(9) & \"1\" & Chr$(9) & \"T1\" & Chr$(10) & \"b\" & Chr$(9) & \"22\" & Chr$(10) & \"c\" & Chr$(9) & \"3\" & Chr$(9) & \"T3\" & Chr$(9) & \"extra\" & Chr$(10)\n" + " Print LV1.RowCount(); LV1.GetCell(1, 1); \"[\"; LV1.GetCell(1, 2); \"]\"; LV1.GetCell(2, 2); LV1.ListIndex\n" + " LV1.SetMultiSelect True\n" + " LV1.SetItemSelected 0, True\n" + " LV1.SetItemSelected 2, True\n" + " Print \"[\"; LV1.SelectedRows(); \"]\"\n" + " LV1.ClearSelection\n" + " Print \"[\"; LV1.SelectedRows(); \"]\"\n" + " LV1.SetRows \"\"\n" + " Print LV1.RowCount(); LV1.ListIndex\n" + " LV1.SetRows \"x\" & Chr$(10) & \"y\"\n" + "End Sub\n" + "Sub LV1_ColumnClick(Column As Integer)\n" + " Print \"col\"; Column\n" + " LV1.SetSort -1, 0\n" + "End Sub\n" + "Sub Form1_Tree\n" + " TV1.AddItem \"root\"\n" + " TV1.AddChildItem 0, \"b\"\n" + " TV1.AddChildItem 0, \"c\"\n" + " TV1.SetItemTag 1, \"path-b\"\n" + " TV1.AddChildItem 1, \"b1\"\n" + " Print TV1.ItemCount(); TV1.GetItemText(2); TV1.GetItemText(3); TV1.GetItemTag(1); \"[\"; TV1.GetItemTag(3); \"]\"; TV1.HasChildren(1); TV1.HasChildren(3)\n" + " changes = 0\n" + " TV1.ListIndex = 3\n" + " Print TV1.ListIndex; changes\n" + " TV1.SetItemTag 1, \"\"\n" + " TV1.ListIndex = -1\n" + " Print \"[\"; TV1.GetItemTag(1); \"]\"; TV1.ListIndex\n" + "End Sub\n" + "Sub TV1_Change\n" + " changes = changes + 1\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Fill")); + TEST_STR_EQ(frtOut(&app), "3 22[]T30 \n[0 2]\n[]\n0 -1 \n"); + + // A click on the second column's header: ColumnClick 1 + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *lv = form ? frtCtrl(form, "LV1") : NULL; + + TEST_TRUE(lv != NULL); + + if (lv) { + int32_t cx; + int32_t cy; + + tWidgetCenter(lv->widget, &cx, &cy); + frtOutClear(&app); + tClick(cx - lv->widget->w / 2 + LV_SECOND_COL_X, cy - lv->widget->h / 2 + LV_HEADER_Y); + TEST_STR_EQ(frtOut(&app), "col1 \n"); + } + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Tree")); + TEST_STR_EQ(frtOut(&app), "4 b1cpath-b[]True False \n3 1 \n[]-1 \n"); + frtDestroy(&app); + tShutdown(); +} + + static void caseLoadForm2(void) { tCase("Load Form2 from Form1_Load populates Form2"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -3205,6 +5310,345 @@ static void caseLoadPicture(void) { } +// Menus made at run time: AddMenu (a menu bar on a form without one), +// AddMenuItem with a shortcut key, AddMenuRadioItem, a menu item's +// Shortcut read and changed, RemoveMenu (the bar goes with its last +// menu), and the errors. +static void caseMenuBarRuntime(void) { + tCase("AddMenu / RemoveMenu / AddMenuRadioItem / Shortcut at run time"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Menus\"\n" + " Begin TextBox Text1\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Build\n" + " Me.AddMenu \"mnuDyn\", \"&Dynamic\"\n" + " Me.AddMenuItem \"mnuDyn\", \"mnuAlpha\", \"&Alpha\", \"Ctrl+K\"\n" + " Me.AddMenuRadioItem \"mnuDyn\", \"mnuOne\", \"One\", True\n" + " Me.AddMenuRadioItem \"mnuDyn\", \"mnuTwo\", \"Two\"\n" + " Me.AddMenu \"mnuMore\", \"&More\"\n" + " Print mnuAlpha.Shortcut; \"|\"; mnuOne.Shortcut; \"|\"\n" + "End Sub\n" + "Sub Form1_Rekey\n" + " mnuAlpha.Shortcut = \"F5\"\n" + " mnuAlpha.Caption = \"Al&pha\"\n" + " Print mnuAlpha.Shortcut; mnuAlpha.Caption\n" + "End Sub\n" + "Sub Form1_Unkey\n" + " mnuAlpha.Shortcut = \"\"\n" + "End Sub\n" + "Sub Form1_RemoveDyn\n" + " Me.RemoveMenu \"mnuDyn\"\n" + "End Sub\n" + "Sub Form1_RemoveMore\n" + " Me.RemoveMenu \"mnuMore\"\n" + "End Sub\n" + "Sub mnuAlpha_Click\n" + " Print \"alpha\"\n" + "End Sub\n" + "Sub Form1_BadKey\n" + " mnuAlpha.Shortcut = \"Hyper+Q\"\n" + "End Sub\n" + "Sub Form1_DupMenu\n" + " Me.AddMenu \"mnuDyn\", \"Again\"\n" + "End Sub\n" + "Sub Form1_RemovePopup\n" + " Me.CreateMenu \"mnuPop\"\n" + " Me.RemoveMenu \"mnuPop\"\n" + "End Sub\n"; + const char *bad[] = { "BadKey", "DupMenu", "RemovePopup" }; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + WindowT *win = form ? form->window : NULL; + + TEST_TRUE(win != NULL && win->menuBar == NULL); + + if (!win) { + frtDestroy(&app); + tShutdown(); + return; + } + + int32_t contentH = win->contentH; + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Build")); + TEST_STR_EQ(frtOut(&app), "Ctrl+K||\n"); + TEST_TRUE(win->menuBar != NULL && win->menuBar->menuCount == 2); + TEST_TRUE(win->contentH < contentH); + + if (!win->menuBar || win->menuBar->menuCount < 1) { + frtDestroy(&app); + tShutdown(); + return; + } + + MenuT *dyn = win->menuBar->menus[0]; + + TEST_EQ(dyn->itemCount, 3); + TEST_STR_EQ(dyn->items[0].label, "&Alpha\tCtrl+K"); + TEST_EQ(dyn->items[1].type, MenuItemRadioE); + TEST_TRUE(dyn->items[1].checked && !dyn->items[2].checked); + TEST_TRUE(form->accel != NULL && win->accelTable == form->accel); + TEST_EQ(form->accel ? form->accel->count : 0, 1); + + // A new key replaces the old one; the caption keeps the key text + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Rekey")); + TEST_STR_EQ(frtOut(&app), "F5Al&pha\n"); + TEST_STR_EQ(dyn->items[0].label, "Al&pha\tF5"); + TEST_EQ(form->accel ? form->accel->count : 0, 1); + frtOutClear(&app); + win->onMenu(win, dyn->items[0].id); + TEST_STR_EQ(frtOut(&app), "alpha\n"); + TEST_TRUE(frtFire(&app, "Form1", "Unkey")); + TEST_STR_EQ(dyn->items[0].label, "Al&pha"); + TEST_EQ(form->accel ? form->accel->count : 0, 0); + + // RemoveMenu takes the menu and its items; the last one the bar + TEST_TRUE(frtFire(&app, "Form1", "RemoveDyn")); + TEST_TRUE(!app.rt->terminated); + TEST_TRUE(win->menuBar != NULL && win->menuBar->menuCount == 1); + TEST_TRUE(basFormRtFindCtrl(app.rt, form, "mnuAlpha") == NULL); + TEST_TRUE(frtFire(&app, "Form1", "RemoveMore")); + TEST_TRUE(win->menuBar == NULL); + TEST_EQ(win->contentH, contentH); + frtDestroy(&app); + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(bad); i++) { + if (!expectBuild(&app, "", &frm, 1)) { + break; + } + + frtStart(&app, NULL); + frtFire(&app, "Form1", "Build"); + frtFire(&app, "Form1", bad[i]); + + if (!app.rt->terminated) { + printf(" menu error case %s did not stop the program\n", bad[i]); + } + + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + } + + tShutdown(); +} + + +// A menu item's Caption at run time: read without its key text, set with +// the key text kept. +static void caseMenuCaption(void) { + tCase("menu Caption at run time keeps the key text"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Menu\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuOpen\n" + " Caption = \"&Open\"\n" + " Shortcut = \"Ctrl+O\"\n" + " End\n" + " Begin Menu mnuPlain\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Rename\n" + " Print mnuOpen.Caption\n" + " mnuOpen.Caption = \"&Load\"\n" + " mnuPlain.Caption = \"Pla&in\"\n" + " Print mnuOpen.Caption; \"|\"; mnuPlain.Caption\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtFire(&app, "Form1", "Rename"); + TEST_STR_EQ(frtOut(&app), "&Open\n&Load|Pla&in\n"); + + BasFormT *form = frtForm(&app, "Form1"); + MenuT *file = (form && form->window && form->window->menuBar) ? form->window->menuBar->menus[0] : NULL; + + TEST_TRUE(file != NULL); + + if (file) { + TEST_STR_EQ(file->items[0].label, "&Load\tCtrl+O"); + TEST_EQ(file->items[0].accelKey, 'l'); + TEST_STR_EQ(file->items[1].label, "Pla&in"); + } + + frtDestroy(&app); + tShutdown(); +} + + +// Menus at run time: a .frm Checked = False item is a check item; a +// nested Visible = False item starts hidden; Visible hides and shows items +// back in their places; AddMenuItem / AddMenuSeparator on a menu-bar menu +// (an MRU list) and RemoveMenuItem; submenus refuse to hide or go. +static void caseMenuRuntime(void) { + tCase("menus at run time: Checked = False, Visible, bar AddMenuItem / RemoveMenuItem"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Menus\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuA\n" + " Caption = \"A\"\n" + " End\n" + " Begin Menu mnuB\n" + " Caption = \"B\"\n" + " Checked = False\n" + " End\n" + " Begin Menu mnuC\n" + " Caption = \"C\"\n" + " Visible = False\n" + " End\n" + " Begin Menu mnuD\n" + " Caption = \"D\"\n" + " End\n" + " Begin Menu mnuSub\n" + " Caption = \"Sub\"\n" + " Begin Menu mnuSubItem\n" + " Caption = \"Nested\"\n" + " End\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub mnuMru1_Click\n" + " Print \"mru1\"\n" + "End Sub\n" + "Sub Form1_Hide\n" + " Print mnuC.Visible; mnuB.Visible; mnuB.Checked\n" + " mnuB.Visible = False\n" + " mnuB.Caption = \"Bee\"\n" + " mnuB.Checked = True\n" + " Print mnuB.Visible; mnuB.Caption; mnuB.Checked\n" + "End Sub\n" + "Sub Form1_ShowC\n" + " mnuC.Visible = True\n" + "End Sub\n" + "Sub Form1_ShowB\n" + " mnuB.Visible = True\n" + "End Sub\n" + "Sub Form1_Mru\n" + " Me.AddMenuSeparator \"mnuFile\", \"mnuMruSep\"\n" + " Me.AddMenuItem \"mnuFile\", \"mnuMru1\", \"&1 one.txt\"\n" + " Me.AddMenuItem \"mnuSub\", \"mnuSub2\", \"Two\"\n" + "End Sub\n" + "Sub Form1_Unmru\n" + " Me.RemoveMenuItem \"mnuMru1\"\n" + " Me.RemoveMenuItem \"mnuMruSep\"\n" + " Me.RemoveMenuItem \"mnuNoSuch\"\n" + "End Sub\n" + "Sub Form1_RemoveSub\n" + " Me.RemoveMenuItem \"mnuSub\"\n" + "End Sub\n" + "Sub Form1_HideSub\n" + " mnuSub.Visible = False\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + MenuT *file = (form && form->window->menuBar) ? form->window->menuBar->menus[0] : NULL; + + TEST_TRUE(file != NULL); + + if (!file) { + frtDestroy(&app); + tShutdown(); + return; + } + + // C starts hidden; B is a check item, unchecked + TEST_EQ(file->itemCount, 4); + TEST_STR_EQ(file->items[1].label, "B"); + TEST_EQ(file->items[1].type, MenuItemCheckE); + TEST_TRUE(!file->items[1].checked); + TEST_STR_EQ(file->items[2].label, "D"); + + // Hidden items keep their properties; shown in either order, each + // goes back to its own place + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Hide")); + TEST_STR_EQ(frtOut(&app), "False True False \nFalse BeeTrue \n"); + TEST_EQ(file->itemCount, 3); + TEST_TRUE(frtFire(&app, "Form1", "ShowC")); + TEST_TRUE(frtFire(&app, "Form1", "ShowB")); + TEST_EQ(file->itemCount, 5); + TEST_STR_EQ(file->items[0].label, "A"); + TEST_STR_EQ(file->items[1].label, "Bee"); + TEST_TRUE(file->items[1].checked); + TEST_STR_EQ(file->items[2].label, "C"); + TEST_STR_EQ(file->items[3].label, "D"); + TEST_STR_EQ(file->items[4].label, "Sub"); + + // A recent-files list on the menu bar's File menu, and an item added + // to a .frm submenu + TEST_TRUE(frtFire(&app, "Form1", "Mru")); + TEST_TRUE(!app.rt->terminated); + TEST_EQ(file->itemCount, 7); + TEST_TRUE(file->items[5].separator); + TEST_STR_EQ(file->items[6].label, "&1 one.txt"); + TEST_EQ(file->items[4].subMenu->itemCount, 2); + frtOutClear(&app); + form->window->onMenu(form->window, file->items[6].id); + TEST_STR_EQ(frtOut(&app), "mru1\n"); + + // Removed again (an unknown name is ignored) + TEST_TRUE(frtFire(&app, "Form1", "Unmru")); + TEST_TRUE(!app.rt->terminated); + TEST_EQ(file->itemCount, 5); + TEST_STR_EQ(file->items[4].label, "Sub"); + + // A submenu can be neither removed nor hidden + frtFire(&app, "Form1", "RemoveSub"); + TEST_TRUE(app.rt->terminated); + TEST_EQ(file->itemCount, 5); + frtDestroy(&app); + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtFire(&app, "Form1", "HideSub"); + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + tShutdown(); +} + + static void caseMenus(void) { tCase(".frm menus: bar, check, radio, disabled, submenu, popup"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -3307,14 +5751,23 @@ static void caseMenus(void) { TEST_TRUE(!file->items[2].checked); TEST_TRUE(file->items[4].enabled); - // Popup registered by name; ids dispatch Click handlers whether - // they come from the bar or the popup. - TEST_EQ(form->popupMenuCount, 1); - TEST_STR_EQ(form->popupMenus[0].name, "mnuPop"); + // Every menu with items is named: the bar's File menu and the + // submenu (not owned: the bar owns them), the popup (owned). Ids + // dispatch Click handlers whether they come from the bar or the + // popup. + TEST_EQ(form->popupMenuCount, 3); + TEST_STR_EQ(form->popupMenus[0].name, "mnuFile"); + TEST_PTR_EQ(form->popupMenus[0].menu, file); + TEST_TRUE(!form->popupMenus[0].ownsMenu); + TEST_STR_EQ(form->popupMenus[1].name, "mnuSub"); + TEST_PTR_EQ(form->popupMenus[1].menu, file->items[5].subMenu); + TEST_TRUE(!form->popupMenus[1].ownsMenu); + TEST_STR_EQ(form->popupMenus[2].name, "mnuPop"); + TEST_TRUE(form->popupMenus[2].ownsMenu); frtOutClear(&app); form->window->onMenu(form->window, file->items[0].id); form->window->onMenu(form->window, file->items[5].subMenu->items[0].id); - form->window->onMenu(form->window, form->popupMenus[0].menu->items[0].id); + form->window->onMenu(form->window, form->popupMenus[2].menu->items[0].id); TEST_STR_EQ(frtOut(&app), "open\nnested\npop\n"); } @@ -3323,6 +5776,128 @@ static void caseMenus(void) { } +// Menu Shortcut (an accelerator plus its key text) and ShortcutText (the +// text only), and Checked set on an item the .frm did not make checkable. +static void caseMenuShortcuts(void) { + tCase("menu Shortcut / ShortcutText keys and labels, Checked on a plain item"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Keys\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuRename\n" + " Caption = \"&Rename...\"\n" + " Shortcut = \"f2\"\n" + " End\n" + " Begin Menu mnuDelete\n" + " Caption = \"&Delete\"\n" + " ShortcutText = \"Del\"\n" + " End\n" + " Begin Menu mnuProps\n" + " Caption = \"P&roperties\"\n" + " Shortcut = \"Alt+Enter\"\n" + " End\n" + " Begin Menu mnuBoth\n" + " Caption = \"Both\"\n" + " Shortcut = \"Ctrl+B\"\n" + " ShortcutText = \"Ctrl+B or F9\"\n" + " End\n" + " Begin Menu mnuDesc\n" + " Caption = \"D&escending\"\n" + " End\n" + " End\n" + " Begin TextBox Text1\n" + " End\n" + "End\n" + "\n" + "Sub mnuRename_Click\n" + " Print \"rename\"\n" + "End Sub\n" + "Sub mnuDelete_Click\n" + " Print \"delete\"\n" + "End Sub\n" + "Sub mnuProps_Click\n" + " Print \"props\"\n" + "End Sub\n" + "Sub mnuBoth_Click\n" + " Print \"both\"\n" + "End Sub\n" + "Sub mnuDesc_Click\n" + " mnuDesc.Checked = Not mnuDesc.Checked\n" + " Print \"desc\"; mnuDesc.Checked\n" + "End Sub\n"; + const char *badFrm = + "Begin Form Form2\n" + " Caption = \"Bad\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuOdd\n" + " Caption = \"Odd\"\n" + " Shortcut = \"Hyper+Q\"\n" + " End\n" + " End\n" + "End\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *text = form ? frtCtrl(form, "Text1") : NULL; + MenuBarT *bar = form ? form->window->menuBar : NULL; + + TEST_TRUE(bar != NULL && text != NULL); + + if (bar && text) { + MenuT *file = bar->menus[0]; + + TEST_STR_EQ(file->items[0].label, "&Rename...\tF2"); + TEST_STR_EQ(file->items[1].label, "&Delete\tDel"); + TEST_STR_EQ(file->items[2].label, "P&roperties\tAlt+Enter"); + TEST_STR_EQ(file->items[3].label, "Both\tCtrl+B or F9"); + TEST_STR_EQ(file->items[4].label, "D&escending"); + TEST_EQ(file->items[4].type, MenuItemNormalE); + TEST_TRUE(form->window->accelTable == form->accel && form->accel != NULL); + TEST_EQ(form->accel ? form->accel->count : 0, MENU_SHORTCUT_COUNT); + + // F2, Alt+Enter and Ctrl+B run their items; Del reaches the text box. + frtOutClear(&app); + wgtSetFocused(text->widget); + tKey(KEY_SCAN_F2, 0, 0); + tKey(KEY_SCAN_DELETE, 0, 0); + tKey(KEY_SCAN_ENTER, 0, KEY_MOD_ALT); + tKey(SCAN_B, KEY_CTRL_B, KEY_MOD_CTRL); + TEST_STR_EQ(frtOut(&app), "rename\nprops\nboth\n"); + + // Checked on a plain item gives it a check mark + frtOutClear(&app); + form->window->onMenu(form->window, file->items[4].id); + TEST_EQ(file->items[4].type, MenuItemCheckE); + TEST_TRUE(file->items[4].checked); + form->window->onMenu(form->window, file->items[4].id); + TEST_TRUE(!file->items[4].checked); + TEST_STR_EQ(frtOut(&app), "descTrue \ndescFalse \n"); + } + + frtDestroy(&app); + + // An unknown key stops the program when the form loads + if (expectBuild(&app, "", &badFrm, 1)) { + frtStart(&app, NULL); + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + } + + tShutdown(); +} + + // WGT_SIG_RET_INT_STR: the string argument reaches the widget function // and its int32_t status comes back to BASIC as a Long, both through // basFormRtCallMethod and from compiled code. A test interface stands in @@ -3616,6 +6191,58 @@ static void caseMethodSignatures(void) { } +// Show vbModal returns when the form is hidden or unloaded, and gives the +// input gate back to the window that had it. +static void caseModalShow(void) { + tCase("Show vbModal waits until the form is hidden or unloaded"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frms[] = { + "Begin Form Form1\n" + " Caption = \"Main\"\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print \"before\"\n" + " Form2.Show vbModal\n" + " Print \"after\"; Form2.Visible\n" + " Form3.Show 1\n" + " Print \"after3\"\n" + "End Sub\n", + "Begin Form Form2\n" + " Caption = \"Dialog\"\n" + "End\n" + "\n" + "Sub Form2_Paint\n" + " Print \"paint\"\n" + " Me.Hide\n" + "End Sub\n", + "Begin Form Form3\n" + " Caption = \"Dialog 3\"\n" + "End\n" + "\n" + "Sub Form3_Paint\n" + " Unload Me\n" + "End Sub\n" + }; + FrtAppT app; + + if (!expectBuild(&app, "", frms, DVX_ARRAY_LEN(frms))) { + tShutdown(); + return; + } + + frtStart(&app, "Form1"); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "before\npaint\nafterFalse \nafter3\n"); + TEST_PTR_EQ(tCtx()->modalWindow, NULL); + TEST_PTR_EQ(frtForm(&app, "Form3"), NULL); + frtDestroy(&app); + tShutdown(); +} + + static void caseMsgBox(void) { tCase("MsgBox driven by tDialogScript"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -3667,6 +6294,79 @@ static void caseMsgBox(void) { } +// New launch arguments for the running program (the stub's onNewArgs): +// COMMAND$ changes and NewArgs fires on the forms that handle it; while a +// handler (here with a message box open) is running they are held and +// only the latest is given once it is done. A program without a NewArgs +// handler does not take them. +static void caseNewArgs(void) { + tCase("NewArgs: new launch arguments for a running program"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frms[2] = { + "Begin Form Form1\n" + " Caption = \"Args\"\n" + "End\n" + "\n" + "Sub Form1_NewArgs\n" + " Print \"[\"; Command$; \"]\"; Command$(1); \"|\"; Command$(2)\n" + "End Sub\n" + "\n" + "Sub Form1_Ask\n" + " MsgBox \"Busy\"\n" + " Print \"asked\"\n" + "End Sub\n", + "Begin Form Form2\n" + " Caption = \"Other\"\n" + "End\n" + "\n" + "Sub Form2_NewArgs\n" + " Print \"two \"; Command$(2)\n" + "End Sub\n" + }; + FrtAppT app; + + if (!expectBuild(&app, "", frms, 2)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + TEST_TRUE(basFormRtWantsNewArgs(app.rt)); + frtOutClear(&app); + basFormRtNewArgs(app.rt, "\"my file.hlp\" topic.x"); + TEST_STR_EQ(frtOut(&app), "[\"my file.hlp\" topic.x]my file.hlp|topic.x\ntwo topic.x\n"); + + // During a handler's message box: held, the latest given afterwards + NewArgsDlgT dlg; + + memset(&dlg, 0, sizeof(dlg)); + dlg.app = &app; + tDialogScript(newArgsDuringDialog, &dlg); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Ask")); + TEST_EQ(dlg.seen, 1); + TEST_TRUE(!dlg.firedDuring); + TEST_STR_EQ(frtOut(&app), "asked\n[b.hlp second]b.hlp|second\ntwo second\n"); + frtDialogsDefault(&app); + basFormRtNewArgs(NULL, "x"); + frtDestroy(&app); + + // No NewArgs handler: the program does not take new arguments + const char *plain = "Begin Form Form1\n Caption = \"Plain\"\nEnd\n"; + + if (expectBuild(&app, "", &plain, 1)) { + frtStart(&app, NULL); + TEST_TRUE(!basFormRtWantsNewArgs(app.rt)); + basFormRtNewArgs(app.rt, "a b"); + frtDestroy(&app); + } + + TEST_TRUE(!basFormRtWantsNewArgs(NULL)); + tShutdown(); +} + + // Controls and forms are object values: a bare control or form name where // an object is expected (SET, IS, TYPEOF, an object parameter) is that // control or form; TypeName / TypeOf know their types; they go into a @@ -3722,6 +6422,73 @@ static void caseObjectRefs(void) { } +// The DOS feasibility fixture pardsgn on the host: its checks run from a +// Timer and put the form in design mode; a click on the Surface frame and +// a key, as the DOS script gives them, finish it with "PARDSGN PASS". +static void caseParDsgnFixture(void) { + tCase("pardsgn fixture: designer and editor checks, design-mode click and key"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + // designer.bas goes in front of the form's code, where the project + // build's module order puts it (the harness compiles forms first) + int32_t incLen = 0; + int32_t frmLen = 0; + char *inc = platformReadFile(FRT_INCLUDE_DIR "/designer.bas", &incLen); + char *frm = platformReadFile(FRT_PARDSGN_FRM, &frmLen); + char *src = (char *)malloc(FRT_SRC_BUF); + FrtAppT app; + bool built = false; + + if (inc && frm && src) { + int32_t codeAt = basFindFormEndPos(frm, frmLen); + + snprintf(src, FRT_SRC_BUF, "%.*s\n%s\n%s", (int)codeAt, frm, inc, frm + codeAt); + built = expectBuild(&app, "", (const char *const *)&src, 1); + } + + free(inc); + free(frm); + free(src); + + if (!built) { + tShutdown(); + return; + } + + basNativeHostSetLive(true); + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "ParDsgn"); + + for (int32_t i = 0; form && i < FRT_PARDSGN_TICKS && strcmp(form->window->title, "PARDSGN RUN") == 0; i++) { + tAdvanceClock(FRT_PARDSGN_TICK_MS); + tPump(1); + } + + TEST_TRUE(form != NULL); + + if (form) { + BasControlT *surface = frtCtrl(form, "Surface"); + int32_t cx = 0; + int32_t cy = 0; + + TEST_STR_EQ(form->window->title, "PARDSGN READY"); + tPump(2); + tWidgetCenter(surface->widget, &cx, &cy); + tClick(cx, cy); + tPump(2); + TEST_STR_EQ(form->window->title, "PARDSGN CLICKED"); + tKey(0, 'a', 0); + tPump(2); + TEST_TRUE(strncmp(form->window->title, "PARDSGN PASS ", strlen("PARDSGN PASS ")) == 0); + } + + basNativeHostSetLive(false); + frtDestroy(&app); + tShutdown(); +} + + static void casePendingUnloadDrain(void) { tCase("deferred unloads of several forms drain after the handler"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -3919,6 +6686,83 @@ static void casePopupMenu(void) { } +// A DVX program, widget or library file is a picture: its icon (the +// program's icon32, a widget's icon24) for LoadPicture, Picture properties, +// PaintPicture and DragIcon; one without an icon is not a picture. +static void caseProgramIcon(void) { + tCase("program, widget and library files as pictures (their icons)"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + char appPath[DVX_MAX_PATH]; + char wgtPath[DVX_MAX_PATH]; + char libPath[DVX_MAX_PATH]; + + snprintf(appPath, sizeof(appPath), "%s/iconProg.app", tTempDir()); + snprintf(wgtPath, sizeof(wgtPath), "%s/iconCtrl.wgt", tTempDir()); + snprintf(libPath, sizeof(libPath), "%s/noIcon.lib", tTempDir()); + TEST_TRUE(tWriteFile(appPath, "PROGRAM", 7)); + TEST_TRUE(tWriteFile(wgtPath, "WIDGET", 6)); + TEST_TRUE(tWriteFile(libPath, "LIBRARY", 7)); + TEST_EQ(ResAddFile(appPath, DVX_RES_NAME_ICON, DVX_RES_ICON, FRT_FIXTURE_DIR "/icon.bmp"), -1); + TEST_EQ(ResAddFile(wgtPath, DVX_RES_NAME_WIDGET_ICON, DVX_RES_ICON, FRT_FIXTURE_DIR "/icon.bmp"), -1); + TEST_EQ(ResAddText(libPath, "name", "No icon"), -1); + + char frm[FRT_ICON_FRM_LEN]; + + snprintf(frm, sizeof(frm), + "Begin Form Form1\n" + " Caption = \"Icons\"\n" + " Begin Label Src\n" + " Caption = \"Source\"\n" + " End\n" + " Begin Image Img\n" + " End\n" + " Begin PictureBox Pic\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Use\n" + " Dim p As Object\n" + " Set p = LoadPicture(\"%s\")\n" + " Print p.Type; CLng(Pic.ScaleX(p.Width, vbHimetric, vbPixels))\n" + " Img.Picture = p\n" + " Print Img.ImageWidth; Img.ImageHeight\n" + " Pic.PaintPicture \"%s\", 0, 0\n" + " Set Src.DragIcon = p\n" + " Set p = LoadPicture(\"%s\")\n" + " Img.Picture = \"%s\"\n" + " Print CLng(Pic.ScaleY(p.Height, vbHimetric, vbPixels)); Img.ImageWidth\n" + "End Sub\n" + "Sub Form1_Bad\n" + " On Error GoTo failed\n" + " Set Src.DragIcon = LoadPicture(\"%s\")\n" + " Exit Sub\n" + "failed:\n" + " Print \"err\"; Err\n" + "End Sub\n", + appPath, appPath, wgtPath, wgtPath, libPath); + + const char *frms[1] = { frm }; + FrtAppT app; + + if (!expectBuild(&app, "", frms, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Use")); + TEST_STR_EQ(frtOut(&app), "1 32 \n32 32 \n32 32 \n"); + TEST_TRUE(!app.rt->terminated); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Bad")); + TEST_STR_EQ(frtOut(&app), "err481 \n"); + frtDestroy(&app); + tShutdown(); +} + + // The program screen's window (Phase 4): made by the first console // statement at the mode's size, keys to INKEY$, Alt+Enter and FULLSCREEN // through the exclusive API, closing it ends the program. @@ -4336,6 +7180,28 @@ static void casePropEdges(void) { v = basFormRtGetProp(app.rt, cmd, "ListCount"); TEST_EQ(basValToInt32(v), 0); + // A ListView counts its rows too (ListCount = RowCount). + BasControlT *lvw = (BasControlT *)basFormRtCreateCtrl(app.rt, form, "ListView", "LvwCount"); + + TEST_TRUE(lvw != NULL); + + if (lvw) { + BasValueT cols = basValStringFromC("Name,8"); + + basFormRtCallMethod(app.rt, lvw, "SetColumns", &cols, 1); + basValRelease(&cols); + } + + for (int32_t i = 0; lvw && i < 2; i++) { + BasValueT row = basValStringFromC("r"); + + basFormRtCallMethod(app.rt, lvw, "AddItem", &row, 1); + basValRelease(&row); + } + + v = basFormRtGetProp(app.rt, lvw, "ListCount"); + TEST_EQ(basValToInt32(v), 2); + // Control ContextMenu binds by popup name. setStrProp(&app, cmd, "ContextMenu", "mnuPop"); v = basFormRtGetProp(app.rt, cmd, "ContextMenu"); @@ -4501,7 +7367,8 @@ static void casePropTables(void) { TEST_TRUE(app.rt->terminated); frtClearError(&app); - // Menu item table; the .frm-only menu keys are not properties. + // Menu item table; the .frm-only menu keys (RadioCheck, Shortcut, + // ShortcutText, Visible) are not properties. TEST_TRUE(mnu != NULL); props = basFormRtMenuProps(&count); @@ -4518,8 +7385,8 @@ static void casePropTables(void) { TEST_PTR_EQ(basFormRtFindMenuProp(pd->name), pd); } - TEST_PTR_EQ(basFormRtFindMenuProp(BAS_PROP_CAPTION), NULL); - basFormRtGetProp(app.rt, mnu, BAS_PROP_CAPTION); + TEST_PTR_EQ(basFormRtFindMenuProp(BAS_PROP_SHORTCUTTEXT), NULL); + basFormRtGetProp(app.rt, mnu, BAS_PROP_SHORTCUTTEXT); TEST_TRUE(app.rt->terminated); frtClearError(&app); @@ -5024,6 +7891,64 @@ static void caseResources(void) { TEST_EQ(ResExtract(resPath, "missing", outPath), 0); TEST_EQ(ResExtract("/no/such.res", "blob", outPath), 0); + // Any bytes, zeros included, in and out (BINARY STRING in resource.bas) + static const uint8_t zeroBytes[] = { 'a', 0, 'b', 0 }; + const uint8_t *bin; + int32_t binLen = -1; + + TEST_EQ(ResAddData(NULL, "n", DVX_RES_BINARY, zeroBytes, 1), 0); + TEST_EQ(ResAddData(resPath, "n", DVX_RES_BINARY, NULL, 1), 0); + TEST_EQ(ResAddData(resPath, "bin0", DVX_RES_BINARY, zeroBytes, (int32_t)sizeof(zeroBytes)), -1); + bin = ResGetData(resPath, "bin0", &binLen); + TEST_EQ(binLen, (int32_t)sizeof(zeroBytes)); + TEST_TRUE(bin != NULL && memcmp(bin, zeroBytes, sizeof(zeroBytes)) == 0); + bin = ResGetData(resPath, "blob", &binLen); + TEST_TRUE(bin != NULL && binLen == 7 && memcmp(bin, "BINDATA", 7) == 0); + TEST_PTR_EQ(ResGetData(resPath, "missing", &binLen), NULL); + TEST_EQ(binLen, 0); + TEST_PTR_EQ(ResGetData(NULL, "blob", &binLen), NULL); + + // The same from BASIC through resource.bas + char *resBas = NULL; + int32_t resLen = 0; + + resBas = (char *)platformReadFile(FRT_INCLUDE_DIR "/resource.bas", &resLen); + TEST_TRUE(resBas != NULL); + + if (resBas) { + char mainSrc[FRT_APP_PATH_MAX * 2]; + size_t srcLen; + char *src; + const char *frm = "Begin Form Form1\n Caption = \"Res\"\nEnd\n"; + FrtAppT app; + + snprintf(mainSrc, sizeof(mainSrc), + "DIM b AS STRING\n" + "PRINT ResAddData(\"%s\", \"bas0\", RES_TYPE_BINARY, \"x\" + CHR$(0) + \"y\")\n" + "b = ResGetData$(\"%s\", \"bas0\")\n" + "PRINT LEN(b); ASC(MID$(b, 2, 1)); MID$(b, 3, 1); LEN(ResGetData$(\"%s\", \"nope\")); RES_NAME_ICON; RES_NAME_MAX\n", + resPath, resPath, resPath); + srcLen = strlen(resBas) + strlen(mainSrc) + 2; + src = (char *)malloc(srcLen); + TEST_TRUE(src != NULL); + + if (src) { + snprintf(src, srcLen, "%s\n%s", resBas, mainSrc); + basNativeHostSetLive(true); + + if (expectBuild(&app, src, &frm, 1)) { + frtStart(&app, NULL); + TEST_STR_EQ(frtOut(&app), "-1 \n3 0 y0 icon3232 \n"); + frtDestroy(&app); + } + + basNativeHostSetLive(false); + free(src); + } + + free(resBas); + } + TEST_EQ(ResRemove(resPath, "blob"), -1); TEST_EQ(ResExtract(resPath, "greet", "/no/dir/x.out"), 0); @@ -5346,11 +8271,242 @@ static void caseRuntimeError(void) { } +// The Screen object's BitsPerPixel, DebugLayout, MousePointer and Arrange +// (VB's arrangements by what they look like, and DVX's grid), and the +// errors for bad values. +static void caseScreenMembers(void) { + tCase("Screen.BitsPerPixel / DebugLayout / MousePointer / Arrange"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm1 = + "Begin Form Form1\n" + " Caption = \"One\"\n" + " Width = 200\n" + " Height = 150\n" + "End\n" + "\n" + "Sub Form1_Read\n" + " Print Screen.BitsPerPixel; Screen.DebugLayout; Screen.MousePointer\n" + " Screen.DebugLayout = True\n" + " Screen.MousePointer = vbHourglass\n" + " Print Screen.DebugLayout; Screen.MousePointer = vbHourglass\n" + "End Sub\n" + "Sub Form1_Restore\n" + " Screen.DebugLayout = False\n" + " Screen.MousePointer = vbDefault\n" + " Print Screen.DebugLayout; Screen.MousePointer\n" + "End Sub\n" + "Sub Form1_Busy\n" + " Screen.MousePointer = vbHourglass\n" + "End Sub\n" + "Sub Form1_Cascade\n" + " Screen.Arrange vbCascade\n" + "End Sub\n" + "Sub Form1_TileH\n" + " Screen.Arrange vbTileHorizontal\n" + "End Sub\n" + "Sub Form1_TileV\n" + " Screen.Arrange vbTileVertical\n" + "End Sub\n" + "Sub Form1_Grid\n" + " Screen.Arrange vbTileGrid\n" + "End Sub\n" + "Sub Form1_BadArrange\n" + " Screen.Arrange 3\n" + "End Sub\n" + "Sub Form1_BadPointer\n" + " Screen.MousePointer = 3\n" + "End Sub\n" + "Sub Form1_BadMethod\n" + " Screen.Refresh\n" + "End Sub\n" + "Sub Form1_BadProp\n" + " Screen.BitsPerPixel = 8\n" + "End Sub\n"; + const char *frm2 = + "Begin Form Form2\n" + " Caption = \"Two\"\n" + " Width = 200\n" + " Height = 150\n" + "End\n"; + const char *frms[2] = { frm1, frm2 }; + const char *bad[] = { "BadArrange", "BadPointer", "BadMethod", "BadProp" }; + FrtAppT app; + + if (!expectBuild(&app, "Form2.Show\n", frms, 2)) { + tShutdown(); + return; + } + + frtStart(&app, "Form1"); + + WindowT *w1 = frtForm(&app, "Form1")->window; + WindowT *w2 = frtForm(&app, "Form2")->window; + + TEST_TRUE(w1->visible && w2->visible); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Read")); + TEST_STR_EQ(frtOut(&app), "32 False 0 \nTrue True \n"); + TEST_TRUE(wgtGetDebugLayout()); + TEST_TRUE(tCtx()->busy); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Restore")); + TEST_STR_EQ(frtOut(&app), "False 0 \n"); + TEST_TRUE(!wgtGetDebugLayout()); + TEST_TRUE(!tCtx()->busy); + + // vbTileVertical: side by side, full height + TEST_TRUE(frtFire(&app, "Form1", "TileV")); + TEST_EQ(w1->y, 0); + TEST_EQ(w2->y, 0); + TEST_TRUE(w1->x != w2->x); + TEST_EQ(w1->h, SCREEN_H); + + // vbTileHorizontal: stacked, full width + TEST_TRUE(frtFire(&app, "Form1", "TileH")); + TEST_EQ(w1->x, 0); + TEST_EQ(w2->x, 0); + TEST_TRUE(w1->y != w2->y); + TEST_EQ(w1->w, SCREEN_W); + + // vbCascade: each window down and to the right of the one before + TEST_TRUE(frtFire(&app, "Form1", "Cascade")); + TEST_TRUE(w1->x != w2->x && w1->y != w2->y); + + // vbTileGrid: no window overlaps another + TEST_TRUE(frtFire(&app, "Form1", "Grid")); + TEST_TRUE(w1->x + w1->w <= w2->x || w2->x + w2->w <= w1->x || w1->y + w1->h <= w2->y || w2->y + w2->h <= w1->y); + TEST_TRUE(!app.rt->terminated); + + // A busy pointer the program leaves on ends with it + TEST_TRUE(frtFire(&app, "Form1", "Busy")); + TEST_TRUE(tCtx()->busy); + frtDestroy(&app); + TEST_TRUE(!tCtx()->busy); + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(bad); i++) { + if (!expectBuild(&app, "", frms, 2)) { + break; + } + + frtStart(&app, "Form1"); + frtFire(&app, "Form1", bad[i]); + + if (!app.rt->terminated) { + printf(" Screen error case %s did not stop the program\n", bad[i]); + } + + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + } + + tShutdown(); +} + + +// The Screen object: the desktop's size in pixels. +static void caseScreenObject(void) { + tCase("Screen.Width / Screen.Height, read-only"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Screen\"\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print Screen.Width; Screen.Height\n" + "End Sub\n" + "Sub Form1_Bad\n" + " Screen.Width = 5\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "800 600 \n"); + TEST_TRUE(frtFire(&app, "Form1", "Bad")); + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + tShutdown(); +} + + +// A ScrollPane fires Change when the user moves it (here: the Down key), +// not when the program sets ScrollTop; the window-scrollbar route of the +// C demo (a large PictureBox in a NoBorder ScrollPane). +static void caseScrollPaneChange(void) { + tCase("ScrollPane Change when its position changes"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Scroll\"\n" + " Width = 200\n" + " Height = 150\n" + " Begin ScrollPane Sp\n" + " NoBorder = True\n" + " Begin PictureBox Big\n" + " MinWidth = 600\n" + " MinHeight = 900\n" + " End\n" + " End\n" + "End\n" + "Sub Sp_Change ()\n" + " Print \"CHANGE\"; Sp.ScrollTop; Sp.ScrollLeft\n" + "End Sub\n" + "Sub Form1_T ()\n" + " Sp.ScrollTop = 40\n" + " Print \"SET\"; Sp.ScrollTop\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *sp = form ? frtCtrl(form, "Sp") : NULL; + + TEST_TRUE(sp != NULL); + + if (sp) { + WidgetT *w = sp->widget; + + dvxUpdate(tCtx()); + wclsOnKey(w, KEY_DOWN, 0); + TEST_STR_EQ(frtOut(&app), "CHANGE16 0 \n"); + frtOutClear(&app); + + // At the top already: no change, no event + wclsOnKey(w, KEY_HOME, 0); + wclsOnKey(w, KEY_HOME, 0); + TEST_STR_EQ(frtOut(&app), "CHANGE0 0 \n"); + frtOutClear(&app); + + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), "SET40 \n"); + } + + frtDestroy(&app); + tShutdown(); +} + + static void caseSerialLoopback(void) { tCase("Comm/Ser through the stub serial library"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); - const FrtStubApiT *api = stubsOpen(); + const FrtStubApiT *api = frtStubsOpen(); if (!api) { TEST_TRUE(api != NULL); @@ -5372,7 +8528,7 @@ static void caseSerialLoopback(void) { if (!expectBuild(&app, "", &frm, 1)) { basFormRtTestReset(); - stubsClose(); + frtStubsClose(); tShutdown(); return; } @@ -5385,20 +8541,20 @@ static void caseSerialLoopback(void) { TEST_TRUE(api->serIsOpen(0)); SerAttach(1, "Term1"); TEST_TRUE(api->idleCount() >= 1); - TEST_EQ(SerWrite(1, "hi"), -1); + TEST_EQ(serWriteText(1, "hi"), -1); int32_t txLen = 0; const uint8_t *tx = api->serTx(0, &txLen); TEST_TRUE(txLen == 2 && memcmp(tx, "hi", 2) == 0); - TEST_EQ(SerWrite(1, ""), -1); + TEST_EQ(serWriteText(1, ""), -1); api->serPush(0, "echo", 4); TEST_EQ(SerAvailable(1), 4); api->idleRun(); tPump(1); TEST_EQ(SerAvailable(1), 0); api->serPush(0, "rd", 2); - TEST_STR_EQ(SerRead(1), "rd"); + TEST_STR_EQ(serReadText(1), "rd"); SerFlush(1); TEST_EQ(SerGetBase(1), SER_STUB_BASE); TEST_EQ(SerGetIrq(1), SER_STUB_IRQ); @@ -5461,7 +8617,7 @@ static void caseSerialLoopback(void) { CommAttach(h, "Term1", 2, 1); CommAttach(h, "Term1", SECLINK_NUM_CHANNELS, 0); api->idleRun(); - TEST_EQ(CommSend(h, "ping", 2, 1), -1); + TEST_EQ(commSendText(h, "ping", 2, 1), -1); int32_t sentLen = 0; uint8_t sentChannel = 0; @@ -5471,20 +8627,20 @@ static void caseSerialLoopback(void) { TEST_TRUE(sentLen == 4 && memcmp(sent, "ping", 4) == 0); TEST_EQ(sentChannel, 2); TEST_TRUE(sentEnc); - TEST_STR_EQ(CommRecv(h, 2), "ping"); + TEST_STR_EQ(commRecvText(h, 2), "ping"); TEST_EQ(CommRecvEncrypted(h, 2), -1); - TEST_STR_EQ(CommRecv(h, 2), ""); + TEST_STR_EQ(commRecvText(h, 2), ""); // Data that arrived in the clear is reported as such to BASIC - TEST_EQ(CommSend(h, "clr", 4, 0), -1); - TEST_STR_EQ(CommRecv(h, 4), "clr"); + TEST_EQ(commSendText(h, "clr", 4, 0), -1); + TEST_STR_EQ(commRecvText(h, 4), "clr"); TEST_EQ(CommRecvEncrypted(h, 4), 0); TEST_EQ(CommRecvEncrypted(h, SECLINK_NUM_CHANNELS), 0); - TEST_STR_EQ(CommRecv(h, -1), ""); + TEST_STR_EQ(commRecvText(h, -1), ""); api->commInject("inj", 3, 5); - TEST_STR_EQ(CommRecv(h, 5), "inj"); - TEST_EQ(CommSend(h, "", 0, 0), -1); - TEST_EQ(CommSend(h, "x", SECLINK_NUM_CHANNELS, 0), 0); + TEST_STR_EQ(commRecvText(h, 5), "inj"); + TEST_EQ(commSendText(h, "", 0, 0), -1); + TEST_EQ(commSendText(h, "x", SECLINK_NUM_CHANNELS, 0), 0); TEST_EQ(CommPending(h), 0); TEST_EQ(CommPoll(h), 0); TEST_EQ(CommIsReady(h), -1); @@ -5539,7 +8695,7 @@ static void caseSerialLoopback(void) { h = CommOpen(1, 9600, 8, NULL, 1, 0); TEST_EQ(h, 1); api->commFail(false, true); - TEST_EQ(CommSend(h, "y", 0, 0), 0); + TEST_EQ(commSendText(h, "y", 0, 0), 0); api->commFail(false, false); CommClose(h); @@ -5555,7 +8711,7 @@ static void caseSerialLoopback(void) { TEST_TRUE(!api->serIsOpen(2)); frtDestroy(&app); basFormRtTestReset(); - stubsClose(); + frtStubsClose(); tShutdown(); } @@ -5587,8 +8743,8 @@ static void caseSerialUnresolved(void) { SerAttach(1, "Term1"); SerAttach(9, "Term1"); SerAttach(1, "NoSuchTerm"); - TEST_EQ(SerWrite(1, "hello"), 0); - TEST_STR_EQ(SerRead(1), ""); + TEST_EQ(serWriteText(1, "hello"), 0); + TEST_STR_EQ(serReadText(1), ""); TEST_EQ(SerAvailable(1), 0); SerDetach(1); SerDetach(0); @@ -5597,7 +8753,7 @@ static void caseSerialUnresolved(void) { TEST_EQ(CommOpen(1, 9600, 8, "N", 1, 0), 0); CommAttach(1, "Term1", 0, 0); CommAttach(0, "Term1", 0, 0); - TEST_EQ(CommSend(1, "hello", 0, 0), 0); + TEST_EQ(commSendText(1, "hello", 0, 0), 0); CommDetach(1); CommClose(1); CommClose(0); @@ -5696,6 +8852,67 @@ static void caseSetEvent(void) { } +// SizeChars / SizePercent (layout.bas): the size properties keep the +// tagged size for the layout and read back the pixels it stands for. +static void caseSizeUnits(void) { + tCase("SizeChars and SizePercent sizes"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Sizes\"\n" + " Begin TextBox Txt\n" + " End\n" + "End\n" + "DECLARE LIBRARY \"basrt\"\n" + " DECLARE FUNCTION SizeChars(BYVAL n AS LONG) AS LONG\n" + " DECLARE FUNCTION SizePercent(BYVAL n AS LONG) AS LONG\n" + "END DECLARE\n" + "Sub Form1_T ()\n" + " Txt.MinWidth = SizeChars(10)\n" + " Txt.MaxWidth = SizePercent(150)\n" + " Print Txt.MinWidth\n" + "End Sub\n" + "Sub Form1_U ()\n" + " Txt.MinWidth = 33\n" + " Txt.MaxWidth = 0\n" + " Print Txt.MinWidth; Txt.MaxWidth\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + // layout.bas is called for real + basNativeHostSetLive(true); + frtStart(&app, NULL); + TEST_TRUE(frtFire(&app, "Form1", "T")); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *txt = form ? frtCtrl(form, "Txt") : NULL; + char want[FRT_OUT_MAX]; + + snprintf(want, sizeof(want), "%" PRId32 " \n", (int32_t)(SIZE_TEST_CHARS * tCtx()->font.charWidth)); + TEST_STR_EQ(frtOut(&app), want); + TEST_TRUE(txt != NULL); + + if (txt) { + TEST_EQ(txt->widget->minW, wgtChars(SIZE_TEST_CHARS)); + TEST_EQ(txt->widget->maxW, wgtPercent(SIZE_PERCENT_MAX)); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "U")); + TEST_STR_EQ(frtOut(&app), "33 0 \n"); + TEST_EQ(txt->widget->minW, wgtPixels(33)); + } + + basNativeHostSetLive(false); + frtDestroy(&app); + tShutdown(); +} + + static void caseSqlWrappers(void) { tCase("SQL* wrappers through fake callbacks and without any"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -5778,6 +8995,142 @@ static void caseSqlWrappers(void) { } +// A library's byte stream on a Terminal control (basFormRtTermBind, as +// the connection library's ConnAttach uses it): only a Terminal binds; +// what is typed or pasted reaches the stream; a new binding or the +// form's end tells the old one; Unbind and Forget end it quietly. The +// Terminal's Cols / Rows, SetColor, ResetColors, Copy and Paste. +static void caseTermBind(void) { + tCase("Terminal: library binding, size, colors, copy, paste"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Term\"\n" + " Begin Terminal Term1\n" + " End\n" + " Begin Label Lbl1\n" + " Caption = \"x\"\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Paste\n" + " Term1.Paste\n" + "End Sub\n" + "Sub Form1_Size\n" + " Term1.Cols = 100\n" + " Term1.Rows = 30\n" + " Print Term1.Cols; Term1.Rows\n" + "End Sub\n" + "Sub Form1_Colors\n" + " Term1.SetColor 1, RGB(1, 2, 3)\n" + " Term1.SetColor 99, RGB(1, 2, 3)\n" + " Term1.ResetColors\n" + " Print Term1.Copy()\n" + "End Sub\n"; + FrtAppT app; + int32_t ctxA = 0; + int32_t ctxB = 0; + WidgetT *term; + int32_t w; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + sBindLen = 0; + sBindUnbound = 0; + term = frtCtrl(frtForm(&app, "Form1"), "Term1")->widget; + + TEST_PTR_EQ(basFormRtTermBind("Lbl1", &ctxA, termBindWrite, termBindUnbound), NULL); + TEST_PTR_EQ(basFormRtTermBind("Nope", &ctxA, termBindWrite, termBindUnbound), NULL); + TEST_PTR_EQ(basFormRtTermBind("Term1", &ctxA, termBindWrite, termBindUnbound), term); + + // Paste sends the clipboard (new lines as CR) to the stream + dvxClipboardCopy("a\nb", 3); + frtFire(&app, "Form1", "Paste"); + TEST_EQ(sBindLen, 3); + TEST_TRUE(memcmp(sBindText, "a\rb", 3) == 0); + + // Binding ctxA again is not an end; ctxB taking the control is + TEST_PTR_EQ(basFormRtTermBind("Term1", &ctxA, termBindWrite, termBindUnbound), term); + TEST_EQ(sBindUnbound, 0); + TEST_PTR_EQ(basFormRtTermBind("Term1", &ctxB, termBindWrite, termBindUnbound), term); + TEST_EQ(sBindUnbound, 1); + + // Unbind and Forget end quietly; typing reaches nobody + basFormRtTermUnbind(&ctxB); + basFormRtTermForget(&ctxA); + sBindLen = 0; + frtFire(&app, "Form1", "Paste"); + TEST_EQ(sBindLen, 0); + TEST_EQ(sBindUnbound, 1); + + // Size and colors + w = term->calcMinW; + frtFire(&app, "Form1", "Size"); + tPump(1); + TEST_STR_EQ(frtOut(&app), "100 30 \n"); + TEST_TRUE(term->calcMinW > w); + frtOutClear(&app); + frtFire(&app, "Form1", "Colors"); + TEST_STR_EQ(frtOut(&app), "False \n"); + + // The form's end ends a binding + TEST_PTR_EQ(basFormRtTermBind("Term1", &ctxA, termBindWrite, termBindUnbound), term); + frtDestroy(&app); + TEST_EQ(sBindUnbound, 2); + tShutdown(); +} + + +// TextBox MaxLength (the text is cut; 0 = the default) and PasswordChar. +static void caseTextBoxProps(void) { + tCase("TextBox MaxLength and PasswordChar"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Text\"\n" + " Begin TextBox Txt1\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Max\n" + " Txt1.Text = \"abcdefgh\"\n" + " Txt1.MaxLength = 5\n" + " Print Txt1.Text; Txt1.MaxLength\n" + " Txt1.Text = \"123456789\"\n" + " Print Txt1.Text\n" + " Txt1.MaxLength = 0\n" + " Print Txt1.MaxLength\n" + "End Sub\n" + "Sub Form1_Pass\n" + " Txt1.PasswordChar = \"*\"\n" + " Print \"[\" & Txt1.PasswordChar & \"]\"\n" + " Txt1.PasswordChar = \"\"\n" + " Print \"[\" & Txt1.PasswordChar & \"]\"\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + frtFire(&app, "Form1", "Max"); + TEST_STR_EQ(frtOut(&app), "abcde5 \n12345\n256 \n"); + frtOutClear(&app); + frtFire(&app, "Form1", "Pass"); + TEST_STR_EQ(frtOut(&app), "[*]\n[]\n"); + frtDestroy(&app); + tShutdown(); +} + + static void caseTimers(void) { tCase("Timer events on the fake clock"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -5879,6 +9232,72 @@ static void caseToolbarClearFromHandler(void) { } +// Two programs at once (each with its own runtime): menus and the close +// box of the first one's window reach the first program after the second +// was started. +static void caseTwoRuntimes(void) { + tCase("two BASIC programs at once: window events reach their own runtime"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Twin\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuGo\n" + " Caption = \"&Go\"\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub mnuGo_Click\n" + " Print \"go\"\n" + "End Sub\n" + "Sub Form1_Unload\n" + " Print \"bye\"\n" + "End Sub\n"; + FrtAppT first; + FrtAppT second; + + if (!expectBuild(&first, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&first, NULL); + + if (!expectBuild(&second, "", &frm, 1)) { + frtDestroy(&first); + tShutdown(); + return; + } + + frtStart(&second, NULL); + + BasFormT *form1 = frtForm(&first, "Form1"); + BasFormT *form2 = frtForm(&second, "Form1"); + + TEST_TRUE(form1 != NULL && form2 != NULL && form1 != form2); + + if (form1 && form2) { + MenuT *file = form1->window->menuBar->menus[0]; + + frtOutClear(&first); + frtOutClear(&second); + form1->window->onMenu(form1->window, file->items[0].id); + form1->window->onClose(form1->window); + TEST_STR_EQ(frtOut(&first), "go\nbye\n"); + TEST_STR_EQ(frtOut(&second), ""); + TEST_PTR_EQ(frtForm(&first, "Form1"), NULL); + TEST_TRUE(frtForm(&second, "Form1") != NULL); + } + + frtDestroy(&second); + frtDestroy(&first); + tShutdown(); +} + + static void caseUnloadDeferred(void) { tCase("Unload Me inside a handler is deferred until it returns"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -6005,13 +9424,17 @@ static void caseVisibleKeepsModal(void) { TEST_TRUE(form != NULL && form->window != NULL); + // Show vbModal runs until the form is hidden: the checks run in the + // dialog script, while the form is up. if (form && form->window) { + sModalCheckApp = &app; + sModalChecks = 0; + tDialogScript(visibleKeepsModalCheck, form); basFormRtShowForm(app.rt, form, true); - TEST_TRUE(form->window->modal); - basFormRtSetProp(app.rt, form, "Visible", basValBool(true)); - TEST_TRUE(form->window->modal); - TEST_PTR_EQ(app.rt->ctx->modalWindow, form->window); - app.rt->ctx->modalWindow = NULL; + TEST_EQ(sModalChecks, 1); + TEST_PTR_EQ(app.rt->ctx->modalWindow, NULL); + sModalCheckApp = NULL; + frtDialogsDefault(&app); } frtDestroy(&app); @@ -6019,6 +9442,192 @@ static void caseVisibleKeepsModal(void) { } +// The widget properties a BASIC port of DVX Demo needs: ProgressBar and +// Splitter Vertical, ScrollPane Padding / Spacing / ScrollTop and the +// ScrollToTop method, ComboBox MaxLength, and Labels nested in a +// StatusBar (from the .frm and with CreateControl), at design time and +// from code. +static void caseWidgetParity(void) { + tCase("widget parity: Vertical, ScrollPane gaps and scroll, ComboBox MaxLength, StatusBar panes"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Parity\"\n" + " Begin ProgressBar Pb1\n" + " Vertical = True\n" + " Value = 75\n" + " End\n" + " Begin Splitter Spl1\n" + " Vertical = False\n" + " Begin Label TopPane\n" + " Caption = \"Top\"\n" + " End\n" + " Begin Label BottomPane\n" + " Caption = \"Bottom\"\n" + " End\n" + " End\n" + " Begin ScrollPane Sp1\n" + " Padding = 4\n" + " Spacing = 3\n" + " Begin Label SpA\n" + " Caption = \"A\"\n" + " End\n" + " End\n" + " Begin ComboBox Cb1\n" + " MaxLength = 8\n" + " End\n" + " Begin StatusBar Sb1\n" + " Begin Label Pane1\n" + " Caption = \"Ready\"\n" + " End\n" + " End\n" + "End\n" + "\n" + "Sub Form1_Test\n" + " Print Pb1.Vertical; Spl1.Vertical; Sp1.Padding; Sp1.Spacing; Cb1.MaxLength\n" + " Pb1.Vertical = False\n" + " Spl1.Vertical = True\n" + " Sp1.Padding = 0\n" + " Cb1.Text = \"abcdefghij\"\n" + " Print Pb1.Vertical; Spl1.Vertical; Sp1.Padding; Cb1.Text\n" + " Cb1.MaxLength = 3\n" + " Print Cb1.MaxLength; Cb1.Text\n" + " Sp1.ScrollTop = 0\n" + " Sp1.ScrollToTop\n" + " Print Sp1.ScrollTop; Sp1.ScrollLeft\n" + " Dim p As Object\n" + " Set p = CreateControl(Me, \"Label\", \"Pane2\", Sb1)\n" + " p.Caption = \"Line 1\"\n" + "End Sub\n"; + FrtAppT app; + + if (!expectBuild(&app, "", &frm, 1)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *sb = form ? frtCtrl(form, "Sb1") : NULL; + BasControlT *pane1 = form ? frtCtrl(form, "Pane1") : NULL; + + TEST_TRUE(form != NULL && sb != NULL && pane1 != NULL); + + if (form && sb && pane1) { + TEST_PTR_EQ(pane1->widget->parent, sb->widget); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Test")); + TEST_STR_EQ(frtOut(&app), "True False 4 3 8 \nFalse True 0 abcdefgh\n3 abc\n0 0 \n"); + + BasControlT *pane2 = frtCtrl(form, "Pane2"); + + TEST_TRUE(pane2 != NULL && pane2->widget->parent == sb->widget); + } + + frtDestroy(&app); + tShutdown(); +} + + +// Form.WindowState: from the .frm (applied at the first Show), read while +// hidden, set at run time (maximizing resizes, which fires Resize), set on +// a hidden form, and an invalid value. +static void caseWindowState(void) { + tCase("Form.WindowState: .frm, run time, hidden form, invalid value"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + const char *frm1 = + "Begin Form Form1\n" + " Caption = \"One\"\n" + "End\n"; + const char *frm2 = + "Begin Form Form2\n" + " Caption = \"Two\"\n" + " WindowState = 1\n" + "End\n" + "\n" + "Sub Form2_Resize\n" + " Print \"resize\"\n" + "End Sub\n" + "Sub Form2_Before\n" + " Print Form2.Visible; Form2.WindowState\n" + " Form2.Show\n" + " Print Form2.WindowState = vbMinimized\n" + "End Sub\n" + "Sub Form2_Max\n" + " Form2.WindowState = vbMaximized\n" + " Print Form2.WindowState\n" + "End Sub\n" + "Sub Form2_Normal\n" + " Form2.WindowState = vbNormal\n" + " Print Form2.WindowState\n" + "End Sub\n" + "Sub Form2_Hidden\n" + " Form2.Hide\n" + " Form2.WindowState = vbMaximized\n" + " Print Form2.WindowState\n" + "End Sub\n" + "Sub Form2_Again\n" + " Form2.Show\n" + "End Sub\n" + "Sub Form2_Bad\n" + " Form2.WindowState = 7\n" + "End Sub\n"; + const char *frms[2] = { frm1, frm2 }; + FrtAppT app; + + if (!expectBuild(&app, "", frms, 2)) { + tShutdown(); + return; + } + + frtStart(&app, "Form1"); + + BasFormT *f2 = frtForm(&app, "Form2"); + + TEST_TRUE(f2 != NULL); + + if (!f2) { + frtDestroy(&app); + tShutdown(); + return; + } + + WindowT *win = f2->window; + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form2", "Before")); + TEST_TRUE(win->visible && win->minimized); + outContains(&app, "False 1 \nTrue \n"); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form2", "Max")); + TEST_TRUE(!win->minimized && win->maximized); + outContains(&app, "resize\n"); + outContains(&app, "2 \n"); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form2", "Normal")); + TEST_TRUE(!win->minimized && !win->maximized); + outContains(&app, "0 \n"); + + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form2", "Hidden")); + TEST_STR_EQ(frtOut(&app), "2 \n"); + TEST_TRUE(!win->visible && !win->maximized); + TEST_TRUE(frtFire(&app, "Form2", "Again")); + TEST_TRUE(win->visible && win->maximized); + + TEST_TRUE(!app.rt->terminated); + frtFire(&app, "Form2", "Bad"); + TEST_TRUE(app.rt->terminated); + frtDestroy(&app); + tShutdown(); +} + + static void caseWithEvents(void) { tCase("WithEvents variables on forms: each form's handler runs with its own form variables"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -6122,6 +9731,24 @@ static WidgetT *childAt(WidgetT *w, int32_t idx) { } +// CommRecv's bytes as a C string (these tests send text). +static const char *commRecvText(int32_t handle, int32_t channel) { + static char text[FRT_OUT_MAX]; + int32_t len = 0; + const uint8_t *bytes = CommRecv(handle, channel, &len); + + len = DVX_MIN(len, (int32_t)sizeof(text) - 1); + memcpy(text, bytes, (size_t)len); + text[len] = '\0'; + return text; +} + + +static int32_t commSendText(int32_t handle, const char *text, int32_t channel, int32_t encrypt) { + return CommSend(handle, (const uint8_t *)text, (int32_t)strlen(text), channel, encrypt); +} + + static void dialogClick(AppContextT *ctx, WindowT *modal, void *userCtx) { DialogCtxT *dlg = (DialogCtxT *)userCtx; @@ -6144,6 +9771,58 @@ static void dragCallMethod(FrtAppT *app, BasControlT *ctrl, int32_t action) { } +// caseDrawThemed Form1_HBar's pixels: a horizontal bar (the top row runs +// left button, trough, thumb, trough, right button; the glyphs point left +// and right) and DrawScrollArrow's button (raised 1px bevel on the +// scrollbar face, the arrow's tip pointing right). +static void drawThemedHBarCheck(WidgetT *pic) { + const ColorSchemeT *cs = &tCtx()->colors; + int32_t midY = FRT_HBAR_Y + SCROLLBAR_WIDTH / 2; + int32_t rightX = FRT_HBAR_END + 1 - SCROLLBAR_WIDTH; + int32_t leftTip = SCROLLBAR_WIDTH / 2 - SB_ARROW_ROWS / 2; + int32_t rightTip = rightX + SCROLLBAR_WIDTH / 2 + SB_ARROW_ROWS / 2; + int32_t buttonEnd = SCROLLBAR_WIDTH - 1; + int32_t arrowTipX = FRT_SARROW_X + SCROLLBAR_WIDTH / 2 + SB_ARROW_ROWS / 2; + int32_t arrowMidY = FRT_SARROW_Y + SCROLLBAR_WIDTH / 2; + + TEST_EQ(wgtCanvasGetPixel(pic, 0, FRT_HBAR_Y), cs->windowHighlight); + TEST_EQ(wgtCanvasGetPixel(pic, SCROLLBAR_WIDTH, FRT_HBAR_Y), cs->windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_THUMB_X - 1, FRT_HBAR_Y), cs->windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_THUMB_X, FRT_HBAR_Y), cs->windowHighlight); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_THUMB_END, FRT_HBAR_Y), cs->windowHighlight); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_THUMB_END + 1, FRT_HBAR_Y), cs->windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic, rightX - 1, FRT_HBAR_Y), cs->windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic, rightX, FRT_HBAR_Y), cs->windowHighlight); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_END, FRT_HBAR_Y + buttonEnd), cs->windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_TROUGH_X, midY), cs->scrollbarTrough); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_HBAR_THUMB_X + SCROLLBAR_WIDTH / 2, midY), cs->scrollbarBg); + TEST_EQ(wgtCanvasGetPixel(pic, leftTip, midY), cs->scrollbarFg); + TEST_EQ(wgtCanvasGetPixel(pic, leftTip - 1, midY), cs->scrollbarBg); + TEST_EQ(wgtCanvasGetPixel(pic, leftTip, midY - 1), cs->scrollbarBg); + TEST_EQ(wgtCanvasGetPixel(pic, rightTip, midY), cs->scrollbarFg); + TEST_EQ(wgtCanvasGetPixel(pic, rightTip + 1, midY), cs->scrollbarBg); + TEST_EQ(wgtCanvasGetPixel(pic, rightTip, midY - 1), cs->scrollbarBg); + + TEST_EQ(wgtCanvasGetPixel(pic, FRT_SARROW_X, FRT_SARROW_Y), cs->windowHighlight); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_SARROW_X + buttonEnd, FRT_SARROW_Y + buttonEnd), cs->windowShadow); + TEST_EQ(wgtCanvasGetPixel(pic, FRT_SARROW_X + 1, FRT_SARROW_Y + 1), cs->scrollbarBg); + TEST_EQ(wgtCanvasGetPixel(pic, arrowTipX, arrowMidY), cs->scrollbarFg); + TEST_EQ(wgtCanvasGetPixel(pic, arrowTipX + 1, arrowMidY), cs->scrollbarBg); + TEST_EQ(wgtCanvasGetPixel(pic, arrowTipX, arrowMidY - 1), cs->scrollbarBg); +} + + +// A modal box (the runtime error box): counted, its title kept, OK. +static void errorDialogCheck(AppContextT *ctx, WindowT *modal, void *userCtx) { + ErrorDlgT *dlg = (ErrorDlgT *)userCtx; + + (void)ctx; + dlg->seen++; + snprintf(dlg->title, sizeof(dlg->title), "%s", modal->title); + tDialogClickButton(modal, "OK"); +} + + static bool expectBuild(FrtAppT *app, const char *bas, const char *const *frms, int32_t frmCount) { bool ok = frtBuild(app, bas, frms, frmCount); @@ -6198,6 +9877,82 @@ static void fileDialogType(AppContextT *ctx, WindowT *modal, void *userCtx) { } +// The first widget under root that takes the keyboard focus (a HelpPane +// link), or NULL. +static WidgetT *firstFocusable(WidgetT *root) { + if (!root) { + return NULL; + } + + if (root->wclass && (root->wclass->flags & WCLASS_FOCUSABLE)) { + return root; + } + + for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { + WidgetT *hit = firstFocusable(c); + + if (hit) { + return hit; + } + } + + return NULL; +} + + +static bool frtDynCall(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + static const char kinds[] = { [WGT_IFACE_STRING] = 'S', [WGT_IFACE_INT] = 'I', [WGT_IFACE_BOOL] = 'B', [WGT_IFACE_FLOAT] = 'F', [WGT_IFACE_ENUM] = 'E', [WGT_IFACE_NONE] = 'N', [WGT_IFACE_DOUBLE] = 'D' }; + int32_t n = 0; + + (void)w; + (void)methodIdx; + + for (int32_t i = 0; i < argc && n < FRT_DYN_TEXT - 1; i++) { + sFrtDynArgTypes[n++] = (args[i].type < sizeof(kinds)) ? kinds[args[i].type] : '?'; + } + + sFrtDynArgTypes[n] = '\0'; + *ret = wgtValueInt(argc); + return true; +} + + +static WidgetT *frtDynCreate(WidgetT *parent, const WgtIfaceT *iface) { + (void)iface; + return widgetAlloc(parent, sFrtDynType); +} + + +static bool frtDynGet(WidgetT *w, int32_t propIdx, WgtValueT *out) { + (void)w; + *out = (propIdx == 0) ? wgtValueStr(sFrtDynTitle) : wgtValueInt(FRT_DYN_LEVEL); + return true; +} + + +static bool frtDynSet(WidgetT *w, int32_t propIdx, const WgtValueT *value) { + (void)w; + (void)propIdx; + snprintf(sFrtDynTitle, sizeof(sFrtDynTitle), "%s", value->s); + return true; +} + + +// BINARY STRING arguments: both lengths, the bytes of the first (zeros +// included) and the length of the text argument between them. +int32_t frtExtBinSum(const uint8_t *d, int32_t len, const char *tag, const uint8_t *d2, int32_t len2) { + int32_t sum = 0; + + (void)d2; + + for (int32_t i = 0; i < len; i++) { + sum += d[i]; + } + + return len * FRT_EXT_LEN_WEIGHT + sum + (int32_t)strlen(tag) * FRT_EXT_TAG_WEIGHT + len2 * FRT_EXT_LEN2_WEIGHT; +} + + double frtExtDbl(double d, float s) { return d + (double)s; } @@ -6218,6 +9973,18 @@ int32_t frtExtLong(int32_t a, int32_t b) { } +// Mixed classes: integer-class and floating-point arguments interleaved. +double frtExtMix(const char *s, double d, int32_t n, float f) { + return (double)strlen(s) + d + (double)n + (double)f; +} + + +// Seven integer arguments: the seventh goes on the host's stack. +int32_t frtExtSeven(int32_t a, int32_t b, int32_t c, int32_t d, int32_t e, int32_t f, int32_t g) { + return a + b + c + d + e + f + g * FRT_SEVENTH_WEIGHT; +} + + const char *frtExtStr(const char *s) { return s; } @@ -6228,6 +9995,30 @@ void frtExtSub(int32_t a) { } +// A BINARY STRING result: every byte XORed with key (zero bytes survive +// both ways); FRT_EXT_BIN_NULL returns NULL, FRT_EXT_BIN_NOLEN leaves the +// length unset. +const uint8_t *frtExtXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen) { + static uint8_t buf[FRT_EXT_BIN_MAX]; + int32_t n = DVX_MIN(len, FRT_EXT_BIN_MAX); + + if (key == FRT_EXT_BIN_NULL) { + *outLen = n; + return NULL; + } + + for (int32_t i = 0; i < n; i++) { + buf[i] = (uint8_t)(data[i] ^ key); + } + + if (key != FRT_EXT_BIN_NOLEN) { + *outLen = n; + } + + return buf; +} + + // sProbeMethods target: records its argument and returns a status. static int32_t frtProbeRetIntStr(WidgetT *w, const char *text) { (void)w; @@ -6236,6 +10027,20 @@ static int32_t frtProbeRetIntStr(WidgetT *w, const char *text) { } +// caseNewArgs' message box: two sets of new arguments arrive while it is +// open (nothing may run yet), then OK. +static void newArgsDuringDialog(AppContextT *ctx, WindowT *modal, void *userCtx) { + NewArgsDlgT *dlg = (NewArgsDlgT *)userCtx; + + (void)ctx; + dlg->seen++; + basFormRtNewArgs(dlg->app->rt, "a.hlp first"); + basFormRtNewArgs(dlg->app->rt, "b.hlp second"); + dlg->firedDuring = strchr(frtOut(dlg->app), '[') != NULL; + tDialogClickButton(modal, "OK"); +} + + static void outContains(const FrtAppT *app, const char *needle) { bool found = strstr(frtOut(app), needle) != NULL; @@ -6339,7 +10144,8 @@ static void parseOnMenuProp(void *userData, const char *key, const char *value) static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize) { (void)basName; - if (strcasecmp(p->name, "ListIndex") == 0 || strcasecmp(p->name, "SelectedRow") == 0) { + // No row: a Data control without a database has none + if (strcasecmp(p->name, "ListIndex") == 0 || strcasecmp(p->name, "SelectedRow") == 0 || strcasecmp(p->name, "AbsolutePosition") == 0) { return "-1"; } @@ -6353,6 +10159,12 @@ static const char *roundTripValue(const char *basName, const WgtPropDescT *p, ch return "100"; } + // An empty ScrollPane has nothing to scroll: the layout clamps the + // position to 0. + if (strcasecmp(p->name, "ScrollLeft") == 0 || strcasecmp(p->name, "ScrollTop") == 0) { + return "0"; + } + // The spinner keeps separate integer and real values, so switching // RealMode after Value would read back the other one. if (strcasecmp(p->name, "RealMode") == 0) { @@ -6368,6 +10180,26 @@ static const char *roundTripValue(const char *basName, const WgtPropDescT *p, ch return "False"; } + // Room for the caption, which a smaller TextBox would cut; a password + // box keeps one character. + if (strcasecmp(p->name, "MaxLength") == 0) { + return "40"; + } + + if (strcasecmp(p->name, "PasswordChar") == 0) { + return "*"; + } + + // The selection is run-time state: an empty one at the start, and a + // plain box (a Mask would reshape the caption) + if (strcasecmp(p->name, "SelStart") == 0 || strcasecmp(p->name, "SelLength") == 0) { + return "0"; + } + + if (strcasecmp(p->name, "SelText") == 0 || strcasecmp(p->name, "Mask") == 0) { + return ""; + } + switch (p->type) { case WGT_IFACE_STRING: return "Sample"; @@ -6395,6 +10227,24 @@ static const char *roundTripValue(const char *basName, const WgtPropDescT *p, ch // Set a string property from a C literal, releasing the temporary. +// SerRead's bytes as a C string (these tests send text). +static const char *serReadText(int32_t com) { + static char text[FRT_OUT_MAX]; + int32_t len = 0; + const uint8_t *bytes = SerRead(com, &len); + + len = DVX_MIN(len, (int32_t)sizeof(text) - 1); + memcpy(text, bytes, (size_t)len); + text[len] = '\0'; + return text; +} + + +static int32_t serWriteText(int32_t com, const char *text) { + return SerWrite(com, (const uint8_t *)text, (int32_t)strlen(text)); +} + + static void setStrProp(FrtAppT *app, BasControlT *ctrl, const char *propName, const char *text) { BasValueT v = basValStringFromC(text); @@ -6505,35 +10355,48 @@ static int32_t sqlFakeQuery(int32_t db, const char *sql) { } -static void stubsClose(void) { - sStub = NULL; - - if (sStubHandle) { - dlclose(sStubHandle); - sStubHandle = NULL; - } +// The dialog script of caseCreateFormDynamic: the form shown modal holds +// the input gate; unloading it ends the Show. +static void termBindUnbound(void *ctx) { + (void)ctx; + sBindUnbound++; } -static const FrtStubApiT *stubsOpen(void) { - sStubHandle = dlopen(FRT_STUBS_SO, RTLD_NOW | RTLD_GLOBAL); +// The bound stream's writer: keeps what it is sent. +static int32_t termBindWrite(void *ctx, const uint8_t *data, int32_t len) { + (void)ctx; - if (!sStubHandle) { - printf(" dlopen(%s): %s\n", FRT_STUBS_SO, dlerror()); - return NULL; + for (int32_t i = 0; i < len && sBindLen < FRT_BIND_MAX; i++) { + sBindText[sBindLen++] = (char)data[i]; } - const FrtStubApiT *(*getApi)(void) = (const FrtStubApiT *(*)(void))dlsym(sStubHandle, "frtStubApi"); + return len; +} - if (!getApi) { - dlclose(sStubHandle); - sStubHandle = NULL; - return NULL; - } - sStub = getApi(); - sStub->reset(); - return sStub; +static void unloadModalCheck(AppContextT *ctx, WindowT *modal, void *userCtx) { + BasFormT *form = (BasFormT *)userCtx; + + sModalChecks++; + TEST_PTR_EQ(modal, form->window); + TEST_PTR_EQ(ctx->modalWindow, form->window); + basFormRtUnloadForm(sModalCheckApp->rt, form); +} + + +// The dialog script of caseVisibleKeepsModal: with the form shown modal, +// Visible = True keeps it modal; then it is hidden, which ends the Show. +static void visibleKeepsModalCheck(AppContextT *ctx, WindowT *modal, void *userCtx) { + BasFormT *form = (BasFormT *)userCtx; + + sModalChecks++; + TEST_PTR_EQ(modal, form->window); + TEST_TRUE(form->window->modal); + basFormRtSetProp(sModalCheckApp->rt, form, "Visible", basValBool(true)); + TEST_TRUE(form->window->modal); + TEST_PTR_EQ(ctx->modalWindow, form->window); + basFormRtHideForm(sModalCheckApp->rt, form); } @@ -6579,9 +10442,11 @@ int main(void) { caseFormShowHide(); caseUnloadDeferred(); caseFormVars(); + caseFormInstances(); caseWithEvents(); caseControlArrays(); caseMsgBox(); + caseNewArgs(); caseObjectRefs(); caseTimers(); caseSerialUnresolved(); @@ -6597,25 +10462,62 @@ int main(void) { caseDragDrop(); caseLoadPicture(); caseDrawMethods(); + caseDrawFocusRect(); + caseSizeUnits(); + caseScrollPaneChange(); caseDrawPaint(); caseDrawScale(); caseEventLoopExits(); caseExternCalls(); caseExternMarshal(); + caseExternLive(); + caseFrameMissed(); caseProgramScreen(); caseProgramScreenModes(); caseRefValidation(); caseFrmLoadEdges(); caseFrmParserUnit(); caseHelpHooks(); + caseHelpPane(); caseHelpTopic(); casePendingUnloadDrain(); casePictureLoadSave(); caseResources(); caseRunSimple(); caseSerialLoopback(); + frtLibBasCases(); + frtFrmModelCases(); caseSqlWrappers(); + caseBoxLayoutProps(); + caseDefaultCancelButtons(); + caseHandlerContext(); + caseHandlerError(); + caseListTreeMethods(); + caseDesignerLive(); + caseDesignerRegistry(); + caseEditorMembers(); + caseParDsgnFixture(); + caseMenuShortcuts(); + caseMenuCaption(); + caseModalShow(); + caseScreenObject(); + caseScreenMembers(); + caseFormWindowMembers(); + caseMenuBarRuntime(); + caseDrawThemed(); + caseControlMembers2(); + caseWindowState(); + caseMenuRuntime(); + caseProgramIcon(); + caseTermBind(); + caseTextBoxProps(); + caseWidgetParity(); + caseTwoRuntimes(); caseResetCycles(); + caseCustomEvents(); + caseDynamicControl(); + frtUserCtlCases(); + frtUcDesignCases(); return tSuiteEnd(); } diff --git a/src/test/formrt/testFrmModel.c b/src/test/formrt/testFrmModel.c new file mode 100644 index 00000000..ec91982e --- /dev/null +++ b/src/test/formrt/testFrmModel.c @@ -0,0 +1,858 @@ +// 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. + + +// testFrmModel.c -- the .frm document model (suite T5, round 2) +// +// formrt/frmModel.c from C: every .frm in the source tree loads and +// writes back unchanged by a second round (the canonical text the IDE +// saves); the edit operations (add, remove, rename, move, reparent, +// properties, control arrays, the OptionButton rule), copy and paste of +// a control subtree, the menu operations and checks, form properties +// the runtime added (Icon, Tag, MousePointer), defaults left out. Then +// designer.bas from BASIC: the Frm* / FrmCtl* / FrmMenu* bindings and +// FormText$ of a live form. + +#include "frtHarness.h" +#include "../../apps/kpunch/dvxbasic/formrt/frmModel.h" +#include "dvxPlat.h" +#include "stb_ds_wrap.h" + +#include +#include +#include +#include +#include + +#define FM_SCREEN_W 640 +#define FM_SCREEN_H 480 +#define FM_SCREEN_BPP 32 +#define FM_SRC_ROOT FRT_INCLUDE_DIR "/../.." // the source tree +#define FM_FRM_EXT ".frm" +#define FM_MIN_REPO_FRMS 30 // the tree holds at least this many forms +#define FM_PATH_MAX 1024 +#define FM_SRC_MAX (FRT_SRC_MAX * 2) +#define FM_REASON_MAX 160 + +// Prototypes +static void caseFrmModelBas(void); +static void caseFrmModelBasic(void); +static void caseFrmModelEdits(void); +static void caseFrmModelFormProps(void); +static void caseFrmModelLateLines(void); +static void caseFrmModelMalformed(void); +static void caseFrmModelMenus(void); +static void caseFrmModelPaste(void); +static void caseFrmModelRepo(void); +static void collectFrms(const char *dir, char ***out); +static bool fmIdent(const char *name); +static char *fmText(const FrmModelT *m); +void frtFrmModelCases(void); + + +// designer.bas from BASIC: a model built, copied, pasted, written and +// read back; menus; a handle that is not one; FormText$ of the loaded +// form (its controls in their containers, defaults left out). +static void caseFrmModelBas(void) { + tCase("frm model: designer.bas Frm* bindings and FormText$"); + + static const char probeFrm[] = + "VERSION DVX 1.00\n" + "Begin Form Probe\n" + " Caption = \"Probe Form\"\n" + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuQuit\n" + " Caption = \"&Quit\"\n" + " End\n" + " End\n" + " Begin Frame fraA\n" + " Caption = \"Group\"\n" + " Begin CheckBox chkOne\n" + " Caption = \"One\"\n" + " End\n" + " End\n" + "End\n"; + static const char code[] = + "DIM h AS LONG\n" + "DIM h2 AS LONG\n" + "DIM i AS LONG\n" + "DIM t AS STRING\n" + "h = FrmNew(\"Gen1\")\n" + "PRINT h > 0; FrmSet(h, \"Caption\", \"Generated\"); FrmSet(h, \"Visible\", \"False\")\n" + "i = FrmCtlAdd(h, \"Frame\", \"\", -1)\n" + "PRINT i; FrmCtlName$(h, i); FrmCtlAdd(h, \"Label\", \"LblA\", i); FrmCtlAdd(h, \"TextBox\", \"TxtB\", -1); FrmCtlAdd(h, \"Label\", \"LblA\", -1)\n" + "PRINT FrmCtlSet(h, 1, \"Caption\", \"A\"); FrmCtlSet(h, 2, \"Text\", \"B\"); FrmCtlSet(h, 1, \"Tag\", \"t\")\n" + "PRINT FrmCtlGet$(h, 1, \"Caption\"); FrmCtlParent(h, 1); FrmCtlType$(h, 2); FrmCtlIndex(h, 2); FrmCtlFind(h, \"TxtB\", -1)\n" + "PRINT FrmCtlPropCount(h, 1); FrmCtlPropName$(h, 1, 1); FrmCtlRemoveProp(h, 1, \"Tag\"); FrmCtlPropCount(h, 1)\n" + "t = FrmCtlText$(h, 0)\n" + "PRINT FrmCtlPaste(h, t, -1); FrmCtlName$(h, 3); FrmCtlName$(h, 4); FrmCtlParent(h, 4)\n" + "PRINT FrmCtlMove(h, 3, 0); FrmCtlName$(h, 0); FrmCtlReparent(h, 2, 2); FrmCtlReparent(h, 4, -1)\n" + "PRINT FrmMenuAdd(h, -1, 0); FrmMenuSet(h, 0, \"Caption\", \"&File\"); FrmMenuSet(h, 0, \"Name\", \"mnuFile\")\n" + "PRINT FrmMenuAdd(h, -1, 3); FrmMenuSet(h, 1, \"Name\", \"mnuFile\"); FrmMenuGet$(h, 1, \"Level\"); FrmMenuIndent(h, 1, -1); FrmMenuMove(h, 1, -1)\n" + "PRINT FrmSetCode(h, \"' code\")\n" + "t = FrmText$(h)\n" + "h2 = FrmOpen(t)\n" + "PRINT FrmCtlCount(h2); FrmMenuCount(h2); FrmGet$(h2, \"Caption\"); FrmGetCode$(h2); FrmText$(h2) = t\n" + "FrmCtlRemove h2, 0\n" + "FrmMenuRemove h2, 0\n" + "PRINT FrmCtlCount(h2); FrmMenuCount(h2); FrmOpen(\"not a form\")\n" + "FrmFree h2\n" + "FrmFree h\n" + "PRINT FrmCtlCount(h); FrmText$(h) = \"\"\n" + "t = FormText$(\"Probe\")\n" + "PRINT INSTR(t, \"Begin Frame fraA\") > 0; INSTR(t, \" Begin CheckBox chkOne\") > 0; INSTR(t, \"Caption = \" + CHR$(34) + \"One\") > 0; INSTR(t, \"Begin Menu mnuQuit\") > 0; INSTR(t, \"Value\") = 0\n" + "h = FrmOpen(t)\n" + "PRINT FrmCtlCount(h); FrmMenuCount(h); FrmGet$(h, \"Caption\"); FormText$(\"NoSuchForm\") = \"\"\n" + "FrmFree h\n"; + + char *src = (char *)malloc(FM_SRC_MAX); + int32_t len = 0; + char *inc = platformReadFile(FRT_INCLUDE_DIR "/designer.bas", &len); + const char *frms[1]; + FrtAppT app; + + frms[0] = probeFrm; + tInit(FM_SCREEN_W, FM_SCREEN_H, FM_SCREEN_BPP); + + if (src && inc) { + snprintf(src, FM_SRC_MAX, "%s\n%s", inc, code); + } + + bool built = src && inc && frtBuild(&app, src, frms, 1); + + if (!built && src && inc) { + printf(" compile error: %s\n", app.error); + } + + TEST_TRUE(built); + free(src); + free(inc); + + if (built) { + basNativeHostSetLive(true); + frtStart(&app, "Probe"); + basNativeHostSetLive(false); + TEST_STR_EQ(frtOut(&app), + "True -1 0 \n" + "0 Frame11 2 -1 \n" + "-1 -1 -1 \n" + "A0 TextBox-1 2 \n" + "2 Tag-1 1 \n" + "3 Frame2Label13 \n" + "-1 Frame20 -1 \n" + "0 -1 -1 \n" + "1 0 1-1 0 \n" + "-1 \n" + "5 2 Generated' code\nTrue \n" + "3 1 0 \n" + "0 True \n" + "True True True True True \n" + "2 2 Probe FormTrue \n"); + + // The same live model from C: the frame holds the check box + BasFormT *form = frtForm(&app, "Probe"); + FrmModelT *live = form ? frmModelFromForm(app.rt, form) : NULL; + + TEST_TRUE(live && arrlen(live->controls) == 2 && arrlen(live->menuItems) == 2); + TEST_TRUE(live && strcmp(live->controls[1]->parentName, "fraA") == 0); + TEST_TRUE(live && strcmp(live->caption, "Probe Form") == 0); + frmModelFree(live); + frtDestroy(&app); + } + + tShutdown(); +} + + +// Loading edge cases and the sizes the writer works with. +static void caseFrmModelBasic(void) { + tCase("frm model: load failures, code section, write limits"); + + TEST_TRUE(frmModelLoad(NULL, 0) == NULL); + TEST_TRUE(frmModelLoad("VERSION 5.00\nBegin Form X\nEnd\n", (int32_t)strlen("VERSION 5.00\nBegin Form X\nEnd\n")) == NULL); + + static const char src[] = "VERSION DVX 1.00\nBegin Form Code1\nEnd\n\n\n ' first\nSub Go ()\nEnd Sub"; + + FrmModelT *m = frmModelLoad(src, (int32_t)strlen(src)); + char buf[FRM_MAX_LINE_LEN]; + + TEST_TRUE(m && m->code && strcmp(m->code, " ' first\nSub Go ()\nEnd Sub") == 0); + TEST_TRUE(m && !m->dirty); + TEST_EQ(m ? frmModelWrite(m, buf, (int32_t)sizeof("VERSION")) : 0, FRM_WRITE_TOO_LARGE); + + // A value too long for its line is refused, not cut + if (m) { + char *longCap = (char *)malloc(FRM_MAX_LINE_LEN); + int32_t len = 0; + + memset(longCap, 'x', FRM_MAX_LINE_LEN - 1); + longCap[FRM_MAX_LINE_LEN - 1] = '\0'; + frmModelSetFormProp(m, "Caption", longCap); + TEST_TRUE(frmModelText(m, &len) == NULL); + TEST_EQ(len, FRM_WRITE_LINE_TOO_LONG); + free(longCap); + } + + frmModelFree(m); +} + + +// Edits through the model: the text it writes afterwards. +static void caseFrmModelEdits(void) { + tCase("frm model: add, remove, rename, move, reparent, properties"); + + FrmModelT *m = frmModelNew("Edit1"); + + TEST_TRUE(m != NULL); + + if (!m) { + return; + } + + FrmCtrlT *frame = frmModelAddCtrl(m, "Frame", "", NULL); + FrmCtrlT *label = frmModelAddCtrl(m, "Label", "lblA", frame); + FrmCtrlT *btn = frmModelAddCtrl(m, "CommandButton", NULL, NULL); + FrmCtrlT *opt1 = frmModelAddCtrl(m, "OptionButton", NULL, frame); + FrmCtrlT *opt2 = frmModelAddCtrl(m, "OptionButton", NULL, frame); + + TEST_TRUE(frame && label && btn && opt1 && opt2); + + if (!frame || !label || !btn || !opt1 || !opt2) { + frmModelFree(m); + return; + } + + TEST_STR_EQ(frame->name, "Frame1"); + TEST_STR_EQ(btn->name, "Command1"); + TEST_STR_EQ(opt2->name, "Option2"); + TEST_TRUE(frmModelAddCtrl(m, "Label", "lblA", NULL) == NULL); + TEST_TRUE(frmModelAddCtrl(m, "Label", NULL, label) == NULL); + + // Tree order: the frame's children follow it, the button after + TEST_EQ(frmModelCtrlPos(m, label), 1); + TEST_EQ(frmModelCtrlPos(m, opt2), 3); + TEST_EQ(frmModelCtrlPos(m, btn), 4); + TEST_PTR_EQ(frmModelFindParent(m, opt1), frame); + TEST_EQ(frmModelCtrlDepth(m, opt1), 2); + + // Properties: built-ins, stored, OptionButton group, Index + TEST_TRUE(frmModelSetCtrlProp(m, label, "Caption", "Hello")); + TEST_TRUE(frmModelSetCtrlProp(m, label, "Width", "150")); + TEST_TRUE(frmModelSetCtrlProp(m, label, "Visible", "False")); + TEST_TRUE(frmModelSetCtrlProp(m, label, "HelpTopic", "lbl.help")); + TEST_TRUE(frmModelSetCtrlProp(m, opt1, "Value", "True")); + TEST_TRUE(frmModelSetCtrlProp(m, opt2, "Value", "True")); + TEST_STR_EQ(frmCtrlGetProp(opt1, "Value"), "False"); + TEST_EQ(label->width, 150); + + char val[FRM_MAX_TEXT]; + + TEST_TRUE(frmCtrlPropText(label, "MinWidth", val, sizeof(val)) && strcmp(val, "150") == 0); + TEST_TRUE(frmCtrlPropText(label, "Visible", val, sizeof(val)) && strcmp(val, "False") == 0); + TEST_TRUE(!frmCtrlPropText(label, "Nothing", val, sizeof(val))); + + // Rename: the frame's children follow; a taken name is refused + TEST_TRUE(!frmModelRenameCtrl(m, "Frame1", "Command1")); + TEST_TRUE(frmModelRenameCtrl(m, "Frame1", "fraBox")); + TEST_STR_EQ(label->parentName, "fraBox"); + TEST_TRUE(!frmModelSetCtrlProp(m, label, "Name", "edit1")); + + // Move the button before the frame, then back to the end + TEST_TRUE(frmModelMoveCtrl(m, btn, frame)); + TEST_EQ(frmModelCtrlPos(m, btn), 0); + TEST_TRUE(!frmModelMoveCtrl(m, btn, label)); + TEST_TRUE(frmModelMoveCtrl(m, btn, NULL)); + TEST_EQ(frmModelCtrlPos(m, btn), 4); + TEST_EQ(frmModelSibling(m, 0, 1), 4); + TEST_EQ(frmModelSibling(m, 4, -1), 0); + TEST_EQ(frmModelSibling(m, 1, -1), FRM_NO_INDEX); + + // Reparent: the label leaves the frame; nothing goes into itself or a + // control that holds none + TEST_TRUE(frmModelReparent(m, label, NULL)); + TEST_EQ(frmModelCtrlPos(m, label), 4); + TEST_TRUE(!frmModelReparent(m, frame, opt1)); + TEST_TRUE(!frmModelReparent(m, opt1, btn)); + + char *text = fmText(m); + + TEST_STR_EQ(text ? text : "", + "VERSION DVX 1.00\n" + "Begin Form Edit1\n" + " Caption = \"Edit1\"\n" + " Width = 400\n" + " Height = 300\n" + " Resizable = True\n" + " AutoSize = False\n" + " Centered = True\n" + " Layout = \"VBox\"\n" + " Begin Frame fraBox\n" + " Caption = \"Frame1\"\n" + " MinWidth = 100\n" + " MinHeight = 30\n" + " Begin OptionButton Option1\n" + " Caption = \"Option1\"\n" + " MinWidth = 100\n" + " MinHeight = 30\n" + " Value = False\n" + " End\n" + " Begin OptionButton Option2\n" + " Caption = \"Option2\"\n" + " MinWidth = 100\n" + " MinHeight = 30\n" + " Value = True\n" + " End\n" + " End\n" + " Begin CommandButton Command1\n" + " Caption = \"Command1\"\n" + " MinWidth = 100\n" + " MinHeight = 30\n" + " End\n" + " Begin Label lblA\n" + " Caption = \"Hello\"\n" + " MinWidth = 150\n" + " MinHeight = 30\n" + " HelpTopic = \"lbl.help\"\n" + " Visible = False\n" + " End\n" + "End\n"); + free(text); + + // A control array: the frame's children follow its index + TEST_TRUE(frmModelSetCtrlProp(m, frame, "Index", "0")); + TEST_EQ(opt1->parentIndex, 0); + TEST_EQ(frmModelFindCtrl(m, "fraBox", 0), 0); + TEST_EQ(frmModelFindCtrl(m, "fraBox", 1), FRM_NO_INDEX); + + // Remove the frame: its subtree goes too + frmModelRemoveCtrl(m, frame); + TEST_EQ((int32_t)arrlen(m->controls), 2); + TEST_TRUE(m->dirty); + + // Code section + TEST_TRUE(frmModelSetCode(m, "Sub Command1_Click ()\nEnd Sub")); + text = fmText(m); + TEST_TRUE(text && strstr(text, "End\n\nSub Command1_Click ()\nEnd Sub\n") != NULL); + free(text); + frmModelFree(m); +} + + +// Form properties: the runtime's newer ones survive, run-time-only ones +// are refused, defaults are not written, and a PictureBox's BorderStyle +// is written only when set. +static void caseFrmModelFormProps(void) { + tCase("frm model: form properties and defaults"); + + static const char src[] = + "VERSION DVX 1.00\n" + "Begin Form Props\n" + " Caption = \"P\"\n" + " Icon = \"app.bmp\"\n" + " Tag = \"t\"\n" + " MousePointer = 11\n" + " DesignMode = True\n" + " Begin PictureBox Pic1\n" + " End\n" + " Begin PictureBox Pic2\n" + " BorderStyle = None\n" + " End\n" + "End\n"; + + FrmModelT *m = frmModelLoad(src, (int32_t)strlen(src)); + char val[FRM_MAX_TEXT]; + + TEST_TRUE(m != NULL); + + if (!m) { + return; + } + + TEST_TRUE(frmModelFormProp(m, "Icon", val, sizeof(val)) && strcmp(val, "app.bmp") == 0); + TEST_TRUE(frmModelFormProp(m, "MousePointer", val, sizeof(val)) && strcmp(val, "11") == 0); + TEST_TRUE(!frmModelFormProp(m, "DesignMode", val, sizeof(val))); + TEST_TRUE(!frmModelSetFormProp(m, "Visible", "False")); + TEST_TRUE(!frmModelSetFormProp(m, "NoSuchProp", "1")); + + char *text = fmText(m); + + TEST_TRUE(text && strstr(text, " Icon = \"app.bmp\"\n Tag = \"t\"\n MousePointer = 11\n") != NULL); + TEST_TRUE(text && strstr(text, "DesignMode") == NULL); + TEST_TRUE(text && strstr(text, "BorderStyle = None") != NULL); + TEST_TRUE(text && strstr(text, "BorderStyle = FixedSingle") == NULL); + free(text); + + // An empty value takes the property out again + TEST_TRUE(frmModelSetFormProp(m, "Tag", "")); + TEST_TRUE(frmModelSetFormProp(m, "Name", "Renamed")); + text = fmText(m); + TEST_TRUE(text && strstr(text, "Tag") == NULL && strstr(text, "Begin Form Renamed\n") != NULL); + free(text); + frmModelFree(m); +} + + +// Lines that are neither a block line nor a property (a bare Begin, +// "End = x", "VERSION=x"; QA bug 5: a bare Begin was stored as a property +// and written back as "Begin = """, a phantom control on the next load) +// are ignored, and the model never stores a key the writer could only +// give back as something else. The first source is the fuzzer's. Two +// controls with one name and index (a child's parent is ambiguous) make +// the file malformed, and a NUL byte ends the code section (fuzzFrmModel: +// both changed the text between two write / load rounds). +// A block's lines after the blocks inside it are the block's, as the +// runtime's loader reads them: a Frame's Index and Caption after its +// child (a plain Frame of that name beside it), a menu's Caption after +// its item's, a Frame's Caption after a menu block inside it. The +// writer puts them back before the children, and that text reads back +// the same. +static void caseFrmModelLateLines(void) { + tCase("frm model: a block's lines after the blocks inside it (Index, Caption, menus)"); + + static const char late[] = + "VERSION DVX 1.00\n" + "Begin Form F\n" + " Begin Menu mnuFile\n" + " Begin Menu mnuOpen\n" + " Caption = \"Open\"\n" + " End\n" + " Caption = \"&File\"\n" + " End\n" + " Begin Frame Fr\n" + " Begin Label L\n" + " End\n" + " Index = 2\n" + " Caption = \"late\"\n" + " End\n" + " Begin Frame Fr\n" + " Begin Menu mnuInner\n" + " End\n" + " Caption = \"after\"\n" + " End\n" + "End\n"; + FrmModelT *m = frmModelLoad(late, (int32_t)strlen(late)); + char *one = m ? fmText(m) : NULL; + FrmModelT *m2 = one ? frmModelLoad(one, (int32_t)strlen(one)) : NULL; + char *two = m2 ? fmText(m2) : NULL; + + TEST_TRUE(m != NULL); + + if (m && arrlen(m->controls) == 3 && arrlen(m->menuItems) == 3) { + FrmCtrlT *fr2 = m->controls[0]; + FrmCtrlT *label = m->controls[1]; + FrmCtrlT *plain = m->controls[2]; + const char *cap2 = frmCtrlGetProp(fr2, BAS_PROP_CAPTION); + const char *capP = frmCtrlGetProp(plain, BAS_PROP_CAPTION); + + TEST_EQ(fr2->index, 2); + TEST_STR_EQ(cap2 ? cap2 : "", "late"); + TEST_PTR_EQ(frmModelFindParent(m, label), fr2); + TEST_EQ(label->parentIndex, 2); + TEST_EQ(plain->index, FRM_NO_INDEX); + TEST_STR_EQ(capP ? capP : "", "after"); + TEST_STR_EQ(m->menuItems[0].caption, "&File"); + TEST_STR_EQ(m->menuItems[1].caption, "Open"); + TEST_EQ(m->menuItems[1].level, 1); + } else { + TEST_EQ(m ? (int32_t)arrlen(m->controls) : -1, 3); + TEST_EQ(m ? (int32_t)arrlen(m->menuItems) : -1, 3); + } + + // Written with the Index before the child, and stable from there + const char *index = one ? strstr(one, "Index = 2") : NULL; + const char *child = one ? strstr(one, "Begin Label L") : NULL; + + TEST_TRUE(index && child && index < child); + TEST_TRUE(one && two && strcmp(one, two) == 0); + free(one); + free(two); + frmModelFree(m); + frmModelFree(m2); +} + + +static void caseFrmModelMalformed(void) { + tCase("frm model: malformed block lines, keys that cannot be properties, duplicate controls, NUL in code"); + + static const char *const srcs[] = { + "Begin Form\nBegin n\nBegin", + "VERSION DVX 1.00\nBegin Form F\n Begin Label L\n Begin\n Begin=1\n End = 2\n end=3\n VERSION=4\n Tag = \"t\"\n End\nEnd\n" + }; + + for (int32_t i = 0; i < (int32_t)(sizeof(srcs) / sizeof(srcs[0])); i++) { + FrmModelT *m = frmModelLoad(srcs[i], (int32_t)strlen(srcs[i])); + char *one = m ? fmText(m) : NULL; + FrmModelT *m2 = one ? frmModelLoad(one, (int32_t)strlen(one)) : NULL; + char *two = m2 ? fmText(m2) : NULL; + + TEST_TRUE(one && two && strcmp(one, two) == 0); + TEST_EQ(m ? (int32_t)arrlen(m->controls) : -1, 1); + TEST_EQ(m2 ? (int32_t)arrlen(m2->controls) : -1, 1); + TEST_TRUE(m && arrlen(m->controls) == 1 && frmCtrlGetProp(m->controls[0], "Begin") == NULL && frmCtrlGetProp(m->controls[0], "End") == NULL && frmCtrlGetProp(m->controls[0], "VERSION") == NULL); + free(one); + free(two); + frmModelFree(m); + frmModelFree(m2); + } + + static const char *const badKeys[] = { "Begin", "end", "Version", "", "a b", "k=v", "'c", "t\tab" }; + FrmCtrlT *ctrl = frmCtrlNew("Label", "L"); + + for (int32_t i = 0; ctrl && i < (int32_t)(sizeof(badKeys) / sizeof(badKeys[0])); i++) { + frmCtrlSetProp(ctrl, badKeys[i], "x"); + } + + TEST_EQ(ctrl ? (int32_t)arrlen(ctrl->props) : -1, 0); + + if (ctrl) { + frmCtrlSetProp(ctrl, "Tag", "x"); + } + + TEST_TRUE(ctrl && frmCtrlGetProp(ctrl, "Tag") != NULL); + frmCtrlFree(ctrl); + + static const char twins[] = "VERSION DVX 1.00\nBegin Form F\n Begin Label L\n End\n Begin Label l\n End\nEnd\n"; + static const char members[] = "VERSION DVX 1.00\nBegin Form F\n Begin Label L\n Index = 0\n End\n Begin Label L\n Index = 1\n End\nEnd\n"; + FrmModelT *arr = frmModelLoad(members, (int32_t)strlen(members)); + + TEST_TRUE(frmModelLoad(twins, (int32_t)strlen(twins)) == NULL); + TEST_EQ(arr ? (int32_t)arrlen(arr->controls) : -1, 2); + frmModelFree(arr); + + // " " then a NUL after the End: no code (written as " " it read back + // as none) + static const char nulCode[] = "VERSION DVX 1.00\nBegin Form F\nEnd\n \0x\n"; + FrmModelT *noCode = frmModelLoad(nulCode, (int32_t)sizeof(nulCode) - 1); + + TEST_TRUE(noCode && noCode->code == NULL); + frmModelFree(noCode); +} + + +// Menus: insert, properties, indent / move rules, the checks and the +// replace that renamed items' ContextMenu bindings follow. +static void caseFrmModelMenus(void) { + tCase("frm model: menu items"); + + FrmModelT *m = frmModelNew("Menus"); + + TEST_TRUE(m != NULL); + + if (!m) { + return; + } + + FrmCtrlT *lst = frmModelAddCtrl(m, "ListBox", "lst", NULL); + + TEST_EQ(frmMenuInsert(&m->menuItems, 0, 0), 0); + TEST_EQ(frmMenuInsert(&m->menuItems, 1, 5), 1); // clamped to level 1 + TEST_EQ(m->menuItems[1].level, 1); + TEST_EQ(frmMenuInsert(&m->menuItems, 2, 1), 2); + TEST_EQ(frmMenuInsert(&m->menuItems, 3, 0), 3); + TEST_TRUE(frmModelSetMenuProp(m, 0, "Caption", "&File")); + TEST_TRUE(frmModelSetMenuProp(m, 0, "Name", "mnuFile")); + TEST_TRUE(frmModelSetMenuProp(m, 1, "Caption", "&Open")); + TEST_TRUE(frmModelSetMenuProp(m, 1, "Shortcut", "Ctrl+O")); + TEST_TRUE(frmModelSetMenuProp(m, 2, "Caption", "-")); + TEST_TRUE(frmModelSetMenuProp(m, 3, "Caption", "Pop")); + TEST_TRUE(frmModelSetMenuProp(m, 3, "Name", "mnuPop")); + TEST_TRUE(frmModelSetMenuProp(m, 3, "Visible", "False")); + TEST_TRUE(!frmModelSetMenuProp(m, 3, "Name", "lst")); + TEST_TRUE(!frmModelSetMenuProp(m, 3, "Level", "1")); + + frmMenuAutoName(m->menuItems, 1, m->menuItems[1].name, FRM_MAX_NAME); + frmMenuAutoName(m->menuItems, 2, m->menuItems[2].name, FRM_MAX_NAME); + TEST_STR_EQ(m->menuItems[1].name, "mnuOpen"); + TEST_STR_EQ(m->menuItems[2].name, "mnuSep1"); + + // A popup bound to the form and a control follows a rename + snprintf(m->contextMenu, sizeof(m->contextMenu), "%s", "mnuPop"); + frmCtrlSetProp(lst, "ContextMenu", "mnuPop"); + TEST_TRUE(frmModelSetMenuProp(m, 3, "Name", "mnuPopup")); + TEST_STR_EQ(m->contextMenu, "mnuPopup"); + TEST_STR_EQ(frmCtrlGetProp(lst, "ContextMenu"), "mnuPopup"); + + // Indent / outdent / move rules + TEST_TRUE(!frmMenuIndent(m->menuItems, 0, 1)); + TEST_TRUE(!frmMenuIndent(m->menuItems, 1, 1)); + TEST_TRUE(frmMenuIndent(m->menuItems, 2, 1)); + TEST_TRUE(frmMenuIndent(m->menuItems, 2, -1)); + TEST_EQ(frmMenuMove(&m->menuItems, 1, -1), FRM_NO_INDEX); + TEST_EQ(frmMenuMove(&m->menuItems, 1, 1), 2); + TEST_STR_EQ(m->menuItems[2].name, "mnuOpen"); + TEST_EQ(frmMenuMove(&m->menuItems, 3, -1), 0); // the popup before File, with no children + TEST_STR_EQ(m->menuItems[0].name, "mnuPopup"); + TEST_EQ(frmMenuMove(&m->menuItems, 0, 1), 3); + TEST_EQ(frmMenuCheckLevels(m->menuItems), FRM_NO_INDEX); + + char *text = fmText(m); + + TEST_STR_EQ(text ? strstr(text, " Begin Menu") : "", + " Begin Menu mnuFile\n" + " Caption = \"&File\"\n" + " Begin Menu mnuSep1\n" + " Caption = \"-\"\n" + " End\n" + " Begin Menu mnuOpen\n" + " Caption = \"&Open\"\n" + " Shortcut = \"Ctrl+O\"\n" + " End\n" + " End\n" + " Begin Menu mnuPopup\n" + " Caption = \"Pop\"\n" + " Visible = False\n" + " End\n" + " Begin ListBox lst\n" + " Caption = \"lst\"\n" + " MinWidth = 100\n" + " MinHeight = 30\n" + " ContextMenu = \"mnuPopup\"\n" + " End\n" + "End\n"); + free(text); + + // The checks an editor runs before replacing the menus + FrmMenuItemT *work = NULL; + char reason[FM_REASON_MAX]; + + for (int32_t i = 0; i < (int32_t)arrlen(m->menuItems); i++) { + FrmMenuItemT mi = m->menuItems[i]; + + mi.id = i + 1; + arrput(work, mi); + } + + TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), FRM_NO_INDEX); + snprintf(work[1].shortcut, sizeof(work[1].shortcut), "%s", "Ctrl+Nothing"); + TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 1); + TEST_TRUE(strstr(reason, "Unknown shortcut key") != NULL); + work[1].shortcut[0] = '\0'; + snprintf(work[2].name, sizeof(work[2].name), "%s", "mnuFile"); + TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 2); + snprintf(work[2].name, sizeof(work[2].name), "%s", "bad name"); + TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 2); + TEST_EQ(frmModelMenuValidate(m, work, NULL, reason, sizeof(reason)), FRM_NO_INDEX); + snprintf(work[2].name, sizeof(work[2].name), "%s", "lst"); + TEST_EQ(frmModelMenuValidate(m, work, fmIdent, reason, sizeof(reason)), 2); + + // Replace: the renamed popup's bindings follow by id; a blank childless + // item is stripped + snprintf(work[2].name, sizeof(work[2].name), "%s", "mnuOpen"); + snprintf(work[3].name, sizeof(work[3].name), "%s", "mnuCtx"); + TEST_EQ(frmMenuInsert(&work, 4, 0), 4); + frmMenuStripBlank(&work); + TEST_EQ((int32_t)arrlen(work), 4); + TEST_TRUE(frmModelMenuReplace(m, work)); + TEST_STR_EQ(m->contextMenu, "mnuCtx"); + TEST_STR_EQ(frmCtrlGetProp(lst, "ContextMenu"), "mnuCtx"); + + frmMenuRemove(&m->menuItems, 0); + TEST_EQ((int32_t)arrlen(m->menuItems), 1); + arrfree(work); + frmModelFree(m); +} + + +// Copy a subtree as text, paste it into the same form (taken names get +// free ones, children follow) and into another (names kept). +static void caseFrmModelPaste(void) { + tCase("frm model: copy and paste a control subtree"); + + static const char src[] = + "VERSION DVX 1.00\n" + "Begin Form Src\n" + " Begin Frame Frame1\n" + " Caption = \"Box\"\n" + " Begin Label Label1\n" + " Caption = \"In\"\n" + " End\n" + " Begin CheckBox chkA\n" + " Index = 0\n" + " End\n" + " Begin CheckBox chkA\n" + " Index = 1\n" + " End\n" + " End\n" + "End\n"; + + FrmModelT *m = frmModelLoad(src, (int32_t)strlen(src)); + + TEST_TRUE(m != NULL); + + if (!m) { + return; + } + + int32_t len = 0; + char *clip = frmModelCtrlText(m, m->controls[0], &len); + + TEST_TRUE(clip != NULL && len > 0); + TEST_TRUE(clip && strncmp(clip, "Begin Frame Frame1\n Caption = \"Box\"\n", strlen("Begin Frame Frame1\n Caption = \"Box\"\n")) == 0); + TEST_TRUE(clip && strstr(clip, " Begin Label Label1\n") != NULL); + + // Into the same form: every name is taken + FrmModelT *pasted = clip ? frmModelCtrlsFromText(clip, len) : NULL; + FrmCtrlT *root = pasted ? frmModelInsertTree(m, pasted, pasted->controls[0], NULL, NULL, FRM_NO_INDEX) : NULL; + + frmModelFree(pasted); + TEST_TRUE(root != NULL); + TEST_EQ((int32_t)arrlen(m->controls), 8); + + if (root) { + int32_t at = frmModelCtrlPos(m, root); + + TEST_STR_EQ(root->name, "Frame2"); + TEST_STR_EQ(m->controls[at + 1]->name, "Label2"); + TEST_STR_EQ(m->controls[at + 1]->parentName, "Frame2"); + // Both members of the array get one new name and keep their index + TEST_STR_EQ(m->controls[at + 2]->name, m->controls[at + 3]->name); + TEST_TRUE(strcmp(m->controls[at + 2]->name, "chkA") != 0); + TEST_EQ(m->controls[at + 3]->index, 1); + } + + // Into another form: the names are free and kept + FrmModelT *dst = frmModelNew("Dst"); + + pasted = (dst && clip) ? frmModelCtrlsFromText(clip, len) : NULL; + root = pasted ? frmModelInsertTree(dst, pasted, pasted->controls[0], NULL, NULL, FRM_NO_INDEX) : NULL; + frmModelFree(pasted); + TEST_TRUE(root && strcmp(root->name, "Frame1") == 0); + TEST_EQ(dst ? frmModelFindCtrl(dst, "chkA", 1) : -1, 3); + + // Text without a control + TEST_TRUE(frmModelCtrlsFromText("Caption = \"x\"\n", (int32_t)strlen("Caption = \"x\"\n")) == NULL); + + free(clip); + frmModelFree(dst); + frmModelFree(m); +} + + +// Every .frm in the source tree: it loads, and writing what was loaded +// and loading that again writes the same text (the canonical form). +static void caseFrmModelRepo(void) { + tCase("frm model: every .frm of the source tree round-trips"); + + char **paths = NULL; + int32_t loaded = 0; + + collectFrms(FM_SRC_ROOT, &paths); + TEST_TRUE((int32_t)arrlen(paths) >= FM_MIN_REPO_FRMS); + + for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { + int32_t len = 0; + char *src = platformReadFile(paths[i], &len); + FrmModelT *m = src ? frmModelLoad(src, len) : NULL; + char *one = m ? fmText(m) : NULL; + FrmModelT *m2 = one ? frmModelLoad(one, (int32_t)strlen(one)) : NULL; + char *two = m2 ? fmText(m2) : NULL; + bool same = one && two && strcmp(one, two) == 0; + + if (!same) { + printf(" round trip differs: %s\n", paths[i]); + } + + TEST_TRUE(same); + loaded += same ? 1 : 0; + + // Every control and menu item of the file is in the text + for (int32_t c = 0; m && one && c < (int32_t)arrlen(m->controls); c++) { + TEST_TRUE(strstr(one, m->controls[c]->name) != NULL); + } + + free(src); + free(one); + free(two); + frmModelFree(m); + frmModelFree(m2); + free(paths[i]); + } + + printf(" %" PRId32 " forms round-tripped\n", loaded); + arrfree(paths); +} + + +// Every *.frm file below dir (malloc'd paths appended to *out). +static void collectFrms(const char *dir, char ***out) { + DIR *d = opendir(dir); + struct dirent *e; + + while (d && (e = readdir(d)) != NULL) { + char path[FM_PATH_MAX]; + int32_t nameLen = (int32_t)strlen(e->d_name); + + if (e->d_name[0] == '.') { + continue; + } + + snprintf(path, sizeof(path), "%s/%s", dir, e->d_name); + + if (e->d_type == DT_DIR) { + collectFrms(path, out); + } else if (nameLen > (int32_t)strlen(FM_FRM_EXT) && strcmp(e->d_name + nameLen - strlen(FM_FRM_EXT), FM_FRM_EXT) == 0) { + arrput(*out, strdup(path)); + } + } + + if (d) { + closedir(d); + } +} + + +// Identifier check standing in for the compiler's: letters, digits and +// underscores, not starting with a digit. +static bool fmIdent(const char *name) { + for (const char *p = name; *p; p++) { + bool alpha = (*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || *p == '_'; + bool digit = *p >= '0' && *p <= '9'; + + if (!alpha && !(digit && p != name)) { + return false; + } + } + + return name[0] != '\0'; +} + + +// The model's .frm text (malloc'd), NULL on failure. +static char *fmText(const FrmModelT *m) { + int32_t len = 0; + + return frmModelText(m, &len); +} + + +void frtFrmModelCases(void) { + caseFrmModelBas(); + caseFrmModelBasic(); + caseFrmModelEdits(); + caseFrmModelPaste(); + caseFrmModelFormProps(); + caseFrmModelLateLines(); + caseFrmModelMalformed(); + caseFrmModelMenus(); + caseFrmModelRepo(); +} diff --git a/src/test/formrt/testLibBas.c b/src/test/formrt/testLibBas.c new file mode 100644 index 00000000..75ce0914 --- /dev/null +++ b/src/test/formrt/testLibBas.c @@ -0,0 +1,635 @@ +// 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. + +// testLibBas.c -- library include files from BASIC (suite T5, round 2) +// +// One source for the CONSTs: every CONST of comm.bas, conn.bas, xfer.bas, +// print.bas, listhelp.bas and security.bas that mirrors a C value is +// compared with that value by the compiled BASIC program itself, so an +// include file can never drift from its header. listhelp.bas and +// security.bas are then called for real (the selection model, keyboard +// navigation, type-ahead and reorder rules; random bytes, the key +// exchange, the cipher, hashes, HMAC, HKDF and the compare). + +#include "frtHarness.h" +#include "listHelp.h" +#include "../../libs/kpunch/conn/conn.h" +#include "../../libs/kpunch/print/print.h" +#include "../../libs/kpunch/serial/rs232/rs232.h" +#include "../../libs/kpunch/serial/seclink/secLink.h" +#include "../../libs/kpunch/serial/security/secHash.h" +#include "../../libs/kpunch/serial/security/security.h" +#include "../../libs/kpunch/xfer/xfer.h" +#include "dvxPlat.h" + +#include +#include +#include + +#define LIB_SCREEN_W 640 +#define LIB_SCREEN_H 480 +#define LIB_SCREEN_BPP 32 +#define LIB_SRC_MAX (FRT_SRC_MAX * 4) +#define LIB_LINE_MAX 160 +#define BAS_BAD_COMM 999 + +// A CONST of an include file and the C value it mirrors +typedef struct { + const char *name; + long value; +} LibConstT; + +#define LIB_CONST(basName, cValue) { basName, (long)(cValue) } + +static const LibConstT sCommConsts[] = { + LIB_CONST("SER_UART_NONE", RS232_UART_UNKNOWN), + LIB_CONST("SER_UART_8250", RS232_UART_8250), + LIB_CONST("SER_UART_16450", RS232_UART_16450), + LIB_CONST("SER_UART_16550", RS232_UART_16550), + LIB_CONST("SER_UART_16550A", RS232_UART_16550A), + LIB_CONST("SER_HANDSHAKE_NONE", RS232_HANDSHAKE_NONE), + LIB_CONST("SER_HANDSHAKE_XONXOFF", RS232_HANDSHAKE_XONXOFF), + LIB_CONST("SER_HANDSHAKE_RTSCTS", RS232_HANDSHAKE_RTSCTS), + LIB_CONST("SER_HANDSHAKE_DTRDSR", RS232_HANDSHAKE_DTRDSR), + LIB_CONST("SER_OK", RS232_SUCCESS), + LIB_CONST("SER_ERR_UNKNOWN", RS232_ERR_UNKNOWN), + LIB_CONST("SER_ERR_NOT_OPEN", RS232_ERR_NOT_OPEN), + LIB_CONST("SER_ERR_ALREADY_OPEN", RS232_ERR_ALREADY_OPEN), + LIB_CONST("SER_ERR_NO_UART", RS232_ERR_NO_UART), + LIB_CONST("SER_ERR_INVALID_PORT", RS232_ERR_INVALID_PORT), + LIB_CONST("SER_ERR_INVALID_BASE", RS232_ERR_INVALID_BASE), + LIB_CONST("SER_ERR_INVALID_IRQ", RS232_ERR_INVALID_IRQ), + LIB_CONST("SER_ERR_INVALID_BAUD", RS232_ERR_INVALID_BPS), + LIB_CONST("SER_ERR_INVALID_DATA", RS232_ERR_INVALID_DATA), + LIB_CONST("SER_ERR_INVALID_PARITY", RS232_ERR_INVALID_PARITY), + LIB_CONST("SER_ERR_INVALID_STOP", RS232_ERR_INVALID_STOP), + LIB_CONST("SER_ERR_INVALID_HANDSHAKE", RS232_ERR_INVALID_HANDSHAKE), + LIB_CONST("SER_ERR_INVALID_FIFO", RS232_ERR_INVALID_FIFO), + LIB_CONST("SER_ERR_NULL_PTR", RS232_ERR_NULL_PTR), + LIB_CONST("SER_ERR_IRQ_NOT_FOUND", RS232_ERR_IRQ_NOT_FOUND), + LIB_CONST("SER_ERR_LOCK_MEM", RS232_ERR_LOCK_MEM), + LIB_CONST("SER_LINE_OVERRUN", RS232_LSR_OVERRUN), + LIB_CONST("SER_LINE_PARITY", RS232_LSR_PARITY_ERR), + LIB_CONST("SER_LINE_FRAMING", RS232_LSR_FRAME_ERR), + LIB_CONST("SER_LINE_BREAK", RS232_LSR_BREAK), + LIB_CONST("COMM_OK", SECLINK_SUCCESS), + LIB_CONST("COMM_ERR_PARAM", SECLINK_ERR_PARAM), + LIB_CONST("COMM_ERR_SERIAL", SECLINK_ERR_SERIAL), + LIB_CONST("COMM_ERR_ALLOC", SECLINK_ERR_ALLOC), + LIB_CONST("COMM_ERR_HANDSHAKE", SECLINK_ERR_HANDSHAKE), + LIB_CONST("COMM_ERR_NOT_READY", SECLINK_ERR_NOT_READY), + LIB_CONST("COMM_ERR_SEND", SECLINK_ERR_SEND), + LIB_CONST("COMM_ERR_TIMEOUT", SECLINK_ERR_TIMEOUT), + LIB_CONST("COMM_ERR_AUTH", SECLINK_ERR_AUTH), + LIB_CONST("COMM_PENDING", SECLINK_PENDING), + LIB_CONST("COMM_MAX_CHANNEL", SECLINK_MAX_CHANNEL) +}; + +static const LibConstT sConnConsts[] = { + LIB_CONST("CONN_SERIAL", ConnSerialE), + LIB_CONST("CONN_MODEM", ConnModemE), + LIB_CONST("CONN_SECURE", ConnSecureE), + LIB_CONST("CONN_TELNET", ConnTelnetE), + LIB_CONST("CONN_CLOSED", ConnClosedE), + LIB_CONST("CONN_OPENING", ConnOpeningE), + LIB_CONST("CONN_OPEN", ConnOpenE), + LIB_CONST("CONN_OK", CONN_OK), + LIB_CONST("CONN_ERR_PARAM", CONN_ERR_PARAM), + LIB_CONST("CONN_ERR_PORT", CONN_ERR_PORT), + LIB_CONST("CONN_ERR_HOST", CONN_ERR_HOST), + LIB_CONST("CONN_FLOW_NONE", RS232_HANDSHAKE_NONE), + LIB_CONST("CONN_FLOW_XONXOFF", RS232_HANDSHAKE_XONXOFF), + LIB_CONST("CONN_FLOW_RTSCTS", RS232_HANDSHAKE_RTSCTS) +}; + +static const LibConstT sXferConsts[] = { + LIB_CONST("XFER_MAX_FILES", XFER_MAX_FILES), + LIB_CONST("XFER_WINDOW_MAX", XFER_WINDOW_MAX), + LIB_CONST("XFER_XMODEM", XferXmodemE), + LIB_CONST("XFER_XMODEM1K", XferXmodem1kE), + LIB_CONST("XFER_YMODEM", XferYmodemE), + LIB_CONST("XFER_ZMODEM", XferZmodemE), + LIB_CONST("XFER_IDLE", XferIdleE), + LIB_CONST("XFER_RUNNING", XferRunningE), + LIB_CONST("XFER_ASKING", XferAskingE), + LIB_CONST("XFER_DONE", XferDoneE), + LIB_CONST("XFER_FAILED", XferFailedE), + LIB_CONST("XFER_CANCELLED", XferCancelledE), + LIB_CONST("XFER_EXIST_ASK", XferExistAskE), + LIB_CONST("XFER_EXIST_RENAME", XferExistRenameE), + LIB_CONST("XFER_EXIST_RESUME", XferExistResumeE), + LIB_CONST("XFER_EXIST_OVERWRITE", XferExistOverwriteE), + LIB_CONST("XFER_EXIST_SKIP", XferExistSkipE) +}; + +static const LibConstT sPrintConsts[] = { + LIB_CONST("PRINT_STATUS_TIMEOUT", PRINT_STATUS_TIMEOUT), + LIB_CONST("PRINT_STATUS_IO_ERROR", PRINT_STATUS_IO_ERROR), + LIB_CONST("PRINT_STATUS_SELECTED", PRINT_STATUS_SELECTED), + LIB_CONST("PRINT_STATUS_PAPER_OUT", PRINT_STATUS_PAPER_OUT), + LIB_CONST("PRINT_STATUS_ACK", PRINT_STATUS_ACK), + LIB_CONST("PRINT_STATUS_NOT_BUSY", PRINT_STATUS_NOT_BUSY), + LIB_CONST("PRINT_LINES_STANDARD", PRINT_LINES_STANDARD), + LIB_CONST("PRINT_LINES_LASER", PRINT_LINES_LASER), + LIB_CONST("PRINT_LINES_MIN", PRINT_LINES_MIN), + LIB_CONST("PRINT_LINES_MAX", PRINT_LINES_MAX), + LIB_CONST("PRINT_COLUMNS_DEFAULT", PRINT_COLUMNS_DEFAULT), + LIB_CONST("PRINT_COLUMNS_MIN", PRINT_COLUMNS_MIN), + LIB_CONST("PRINT_COLUMNS_MAX", PRINT_COLUMNS_MAX), + LIB_CONST("PRINT_MARGIN_TB_DEFAULT", PRINT_MARGIN_TB_DEFAULT), + LIB_CONST("PRINT_MARGIN_LR_DEFAULT", PRINT_MARGIN_LR_DEFAULT), + LIB_CONST("PRINT_TAB_DEFAULT", PRINT_TAB_DEFAULT), + LIB_CONST("PRINT_TAB_MAX", PRINT_TAB_MAX), + LIB_CONST("PRINT_BODY_MIN_LINES", PRINT_BODY_MIN_LINES), + LIB_CONST("PRINT_BODY_MIN_COLUMNS", PRINT_BODY_MIN_COLUMNS), + LIB_CONST("PRINT_COPIES_MAX", PRINT_COPIES_MAX) +}; + +static const LibConstT sListConsts[] = { + LIB_CONST("LISTSEL_KEY_NONE", ListSelKeyNoneE), + LIB_CONST("LISTSEL_KEY_ALL", ListSelKeyAllE), + LIB_CONST("LISTSEL_KEY_TOGGLE", ListSelKeyToggleE), + LIB_CONST("LISTICON_VIEW_LARGE", ListIconLargeE), + LIB_CONST("LISTICON_VIEW_SMALL", ListIconSmallE), + LIB_CONST("LISTICON_VIEW_LIST", ListIconListE), + LIB_CONST("LISTICON_BY_ORDER", LIST_ICON_BY_ORDER), + LIB_CONST("LISTICON_CELL_W", ListIconCellWE), + LIB_CONST("LISTICON_CELL_H", ListIconCellHE), + LIB_CONST("LISTICON_PICTURE_X", ListIconPictureXE), + LIB_CONST("LISTICON_PICTURE_Y", ListIconPictureYE), + LIB_CONST("LISTICON_PICTURE_SIZE", ListIconPictureSizeE), + LIB_CONST("LISTICON_TEXT_X", ListIconTextXE), + LIB_CONST("LISTICON_TEXT_Y", ListIconTextYE), + LIB_CONST("LISTICON_NAME_CHARS", ListIconNameCharsE), + LIB_CONST("LISTICON_DETAIL_X", ListIconDetailXE), + LIB_CONST("LISTICON_INNER_X", ListIconInnerXE), + LIB_CONST("LISTICON_INNER_Y", ListIconInnerYE), + LIB_CONST("LISTICON_INNER_W", ListIconInnerWE), + LIB_CONST("LISTICON_INNER_H", ListIconInnerHE), + LIB_CONST("LISTICON_COLUMNS", ListIconColumnsE), + LIB_CONST("LISTICON_ROWS_SHOWN", ListIconRowsShownE), + LIB_CONST("LISTICON_ROWS", ListIconRowsE), + LIB_CONST("LISTICON_TOP_ROW_MAX", ListIconTopRowMaxE), + LIB_CONST("LISTICON_SCROLLBAR", ListIconScrollBarE), + LIB_CONST("LISTICON_EMPTY_X", ListIconEmptyXE), + LIB_CONST("LISTICON_EMPTY_Y", ListIconEmptyYE), + LIB_CONST("LISTICON_FRAME", ListIconFrameE), + LIB_CONST("LISTICON_WHEEL_ROWS", ListIconWheelRowsE) +}; + +static const LibConstT sSecConsts[] = { + LIB_CONST("SEC_DH_KEY_SIZE", SEC_DH_KEY_SIZE), + LIB_CONST("SEC_XTEA_KEY_SIZE", SEC_XTEA_KEY_SIZE), + LIB_CONST("SEC_SHA256_SIZE", SEC_SHA256_SIZE), + LIB_CONST("SEC_HKDF_MAX_OUT", SEC_HKDF_MAX_OUT), + LIB_CONST("SEC_OK", SEC_SUCCESS), + LIB_CONST("SEC_ERR_PARAM", SEC_ERR_PARAM), + LIB_CONST("SEC_ERR_NOT_READY", SEC_ERR_NOT_READY), + LIB_CONST("SEC_ERR_IO", SEC_ERR_IO) +}; + +// One include file and its CONSTs +typedef struct { + const char *file; + const LibConstT *consts; + int32_t count; +} LibIncludeT; + +static const LibIncludeT sIncludes[] = { + { "comm.bas", sCommConsts, DVX_ARRAY_LEN(sCommConsts) }, + { "conn.bas", sConnConsts, DVX_ARRAY_LEN(sConnConsts) }, + { "xfer.bas", sXferConsts, DVX_ARRAY_LEN(sXferConsts) }, + { "print.bas", sPrintConsts, DVX_ARRAY_LEN(sPrintConsts) }, + { "listhelp.bas", sListConsts, DVX_ARRAY_LEN(sListConsts) }, + { "security.bas", sSecConsts, DVX_ARRAY_LEN(sSecConsts) } +}; + +// comm.bas entry points (formrt.c, reached through DECLARE LIBRARY) +void CommClose(int32_t handle); +int32_t CommDropped(int32_t handle); +int32_t CommHandshake(int32_t handle, const char *passphrase); +int32_t CommHandshakeStep(int32_t handle, const char *passphrase); +int32_t CommLastError(int32_t handle); +int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen); +int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt); +void SerClose(int32_t com); +void SerFlushTx(int32_t com); +int32_t SerGetBaud(int32_t com); +int32_t SerGetDataBits(int32_t com); +int32_t SerGetHandshake(int32_t com); +const char *SerGetParity(int32_t com); +int32_t SerGetRi(int32_t com); +int32_t SerGetStopBits(int32_t com); +int32_t SerLastError(int32_t com); +int32_t SerLineErrors(int32_t com); +int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +const uint8_t *SerRead(int32_t com, int32_t *outLen); +int32_t SerSet(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake); +void SerSetBreak(int32_t com, int32_t on); +int32_t SerTxPending(int32_t com); +int32_t SerWrite(int32_t com, const uint8_t *data, int32_t len); + +// ============================================================ +// Prototypes +// ============================================================ + +static bool appendInclude(char *src, size_t size, const char *file); +static void caseCommAdditions(void); +static void caseLibConsts(void); +static void caseListHelp(void); +static void caseSecurity(void); +void frtLibBasCases(void); +static bool runModule(const char *const *files, int32_t fileCount, const char *code, char *out, size_t outSize); + + +// Append src/include/basic/ to src. False when it cannot be read or +// does not fit. +static bool appendInclude(char *src, size_t size, const char *file) { + char path[LIB_LINE_MAX]; + int32_t len = 0; + char *text; + size_t used = strlen(src); + + snprintf(path, sizeof(path), "%s/%s", FRT_INCLUDE_DIR, file); + text = platformReadFile(path, &len); + + if (!text || used + (size_t)len + 2 > size) { + free(text); + return false; + } + + memcpy(src + used, text, (size_t)len); + src[used + (size_t)len] = '\n'; + src[used + (size_t)len + 1] = '\0'; + free(text); + return true; +} + + +// The round 2 Ser / Comm calls on the stub serial library: port settings +// read back and changed, the last error, transmit queue, break, ring and +// line errors; the non-blocking handshake, the last link error and the +// dropped-frame count. +static void caseCommAdditions(void) { + FrtAppT app; + int32_t len = 0; + const uint8_t *tx; + const uint8_t *rx; + + tCase("comm.bas round 2 calls through the stub serial library"); + tInit(LIB_SCREEN_W, LIB_SCREEN_H, LIB_SCREEN_BPP); + + const FrtStubApiT *api = frtStubsOpen(); + + TEST_TRUE(api != NULL); + + if (!api || !frtBuild(&app, "", NULL, 0)) { + frtStubsClose(); + tShutdown(); + return; + } + + basFormRtTestReset(); + TEST_EQ(SerOpen(1, 9600, 8, "E", 1, RS232_HANDSHAKE_RTSCTS), -1); + TEST_EQ(SerLastError(1), RS232_SUCCESS); + TEST_EQ(SerGetBaud(1), 9600); + TEST_EQ(SerGetDataBits(1), 8); + TEST_STR_EQ(SerGetParity(1), "E"); + TEST_EQ(SerGetStopBits(1), 1); + TEST_EQ(SerGetHandshake(1), RS232_HANDSHAKE_RTSCTS); + TEST_EQ(SerSet(1, 19200, 7, "", 2, RS232_HANDSHAKE_NONE), -1); + TEST_EQ(SerGetBaud(1), 19200); + TEST_EQ(SerGetDataBits(1), 7); + TEST_STR_EQ(SerGetParity(1), "N"); + TEST_EQ(SerGetStopBits(1), 2); + TEST_EQ(SerGetHandshake(1), RS232_HANDSHAKE_NONE); + TEST_EQ(SerSet(1, 9600, 9, "N", 1, RS232_HANDSHAKE_NONE), 0); + TEST_EQ(SerLastError(1), RS232_ERR_INVALID_DATA); + TEST_EQ(SerLastError(0), RS232_ERR_INVALID_PORT); + TEST_EQ(SerWrite(1, (const uint8_t *)"a\0c", 3), -1); + TEST_EQ(SerTxPending(1), 3); + tx = api->serTx(0, &len); + TEST_TRUE(len == 3 && memcmp(tx, "a\0c", 3) == 0); + api->serPush(0, "x\0y", 3); + rx = SerRead(1, &len); + TEST_TRUE(len == 3 && memcmp(rx, "x\0y", 3) == 0); + SerFlushTx(1); + TEST_EQ(SerTxPending(1), 0); + SerSetBreak(1, -1); + TEST_TRUE(api->serBreak(0)); + SerSetBreak(1, 0); + TEST_TRUE(!api->serBreak(0)); + TEST_EQ(SerGetRi(1), -1); + TEST_EQ(SerLineErrors(1), RS232_LSR_OVERRUN); + SerClose(1); + TEST_EQ(SerGetBaud(1), RS232_ERR_NOT_OPEN); + TEST_STR_EQ(SerGetParity(1), ""); + TEST_EQ(SerGetRi(1), 0); + TEST_EQ(SerLineErrors(1), 0); + TEST_EQ(SerTxPending(1), RS232_ERR_NOT_OPEN); + TEST_EQ(SerOpen(1, 9600, 4, "N", 1, RS232_HANDSHAKE_NONE), 0); + TEST_EQ(SerLastError(1), RS232_ERR_INVALID_DATA); + + int32_t h = CommOpen(2, 9600, 8, "N", 1, RS232_HANDSHAKE_NONE); + + TEST_TRUE(h > 0); + api->commPeerPass("pw"); + TEST_EQ(CommHandshakeStep(h, "pw"), SECLINK_PENDING); + TEST_EQ(CommLastError(h), SECLINK_PENDING); + TEST_EQ(CommHandshakeStep(h, "ignored while running"), SECLINK_SUCCESS); + TEST_EQ(CommLastError(h), SECLINK_SUCCESS); + TEST_EQ(CommHandshake(h, "wrong"), 0); + TEST_EQ(CommLastError(h), SECLINK_ERR_AUTH); + TEST_EQ(CommHandshakeStep(h, "wrong"), SECLINK_PENDING); + TEST_EQ(CommHandshakeStep(h, ""), SECLINK_ERR_AUTH); + TEST_EQ(CommDropped(h), FRT_STUB_DROPPED); + TEST_EQ(CommSend(h, (const uint8_t *)"p\0q", 3, 1, 0), -1); + rx = CommRecv(h, 1, &len); + TEST_TRUE(len == 3 && memcmp(rx, "p\0q", 3) == 0); + api->commFail(false, true); + TEST_EQ(CommSend(h, (const uint8_t *)"x", 1, 0, 0), 0); + TEST_EQ(CommLastError(h), SECLINK_ERR_PARAM); + TEST_EQ(CommDropped(BAS_BAD_COMM), -1); + TEST_EQ(CommLastError(BAS_BAD_COMM), SECLINK_ERR_PARAM); + TEST_EQ(CommHandshakeStep(BAS_BAD_COMM, ""), SECLINK_ERR_PARAM); + CommClose(h); + frtDestroy(&app); + basFormRtTestReset(); + frtStubsClose(); + tShutdown(); +} + + +// Every CONST that mirrors a C value equals it (the program prints the +// ones that differ). +static void caseLibConsts(void) { + tCase("library include CONSTs match the C headers"); + + for (int32_t f = 0; f < DVX_ARRAY_LEN(sIncludes); f++) { + const LibIncludeT *inc = &sIncludes[f]; + char *code = (char *)malloc(FRT_SRC_MAX); + char out[FRT_OUT_MAX]; + size_t used = 0; + + TEST_TRUE(code != NULL); + + if (!code) { + return; + } + + code[0] = '\0'; + + for (int32_t i = 0; i < inc->count; i++) { + used += (size_t)snprintf(code + used, FRT_SRC_MAX - used, "If %s <> %ld Then Print \"%s differs\"\n", inc->consts[i].name, inc->consts[i].value, inc->consts[i].name); + } + + snprintf(code + used, FRT_SRC_MAX - used, "Print \"%s checked\"\n", inc->file); + + bool ran = runModule(&inc->file, 1, code, out, sizeof(out)); + char expected[LIB_LINE_MAX]; + + snprintf(expected, sizeof(expected), "%s checked\n", inc->file); + TEST_TRUE(ran); + TEST_STR_EQ(out, expected); + free(code); + } +} + + +// listhelp.bas called for real: click / Shift / Ctrl selection, Ctrl+A and +// Space, navigation in a list and a grid, insert / remove / reorder, the +// drop-slot rule and type-ahead; the icon view rules (an 8 x 16 font, a +// 330 x 150 view: four large cells a row, two rows shown, a scroll bar and +// three columns for nine items): the grid's values, hits, the drag scroll +// and target, the drop place, the drag slop, the label lines, the arrow +// keys among items in cells with gaps and the scroll bar's arrows, thumb +// grab and thumb drag. +static void caseListHelp(void) { + static const char *const files[] = { "listhelp.bas" }; + static const char code[] = + "Dim s As Long, t As Long, u As Long\n" + "s = ListSelCreate(True, 5)\n" + "Print s > 0; ListSelCount(s); ListSelCursor(s); ListSelSelectedCount(s)\n" + "ListSelClick s, 1, 0\n" + "ListSelClick s, 3, vbShiftMask\n" + "Print ListSelSelectedCount(s); ListSelIsSelected(s, 2); ListSelIsSelected(s, 0); ListSelCursor(s)\n" + "ListSelClick s, 2, vbCtrlMask\n" + "Print ListSelSelectedCount(s); ListSelIsSelected(s, 2)\n" + "Print ListSelKey(s, Asc(\"A\"), vbCtrlMask) = LISTSEL_KEY_ALL; ListSelSelectedCount(s)\n" + "Print ListSelKey(s, vbKeySpace, 0) = LISTSEL_KEY_TOGGLE; ListSelIsSelected(s, 2); ListSelKey(s, Asc(\"q\"), 0) = LISTSEL_KEY_NONE\n" + "Print ListSelNavigate(s, vbKeyHome, 0, 1, 2); ListSelCursor(s); ListSelNavigate(s, vbKeyHome, 0, 1, 2)\n" + "Print ListSelNavigate(s, vbKeyDown, vbShiftMask, 1, 2); ListSelSelectedCount(s); ListSelIsSelected(s, 1); ListSelIsSelected(s, 2)\n" + "Print ListSelNavigate(s, vbKeyDown, 0, 3, 2); ListSelNavigate(s, vbKeyRight, 0, 3, 2); ListSelNavigate(s, Asc(\"x\"), 0, 1, 2)\n" + "ListSelMove s, 0, 0\n" + "Print ListSelCursor(s); ListSelSelectedCount(s)\n" + "ListSelFree s\n" + "Print ListSelCount(s); ListSelCursor(s); ListSelCreate(True, -1)\n" + "t = ListSelCreate(True, 3)\n" + "ListSelSetItem t, 2, True\n" + "ListSelInsert t, 0\n" + "Print ListSelCount(t); ListSelIsSelected(t, 3); ListSelIsSelected(t, 2)\n" + "ListSelRemove t, 1\n" + "Print ListSelCount(t); ListSelIsSelected(t, 2)\n" + "ListSelReorder t, 2, 0\n" + "Print ListSelIsSelected(t, 0); ListSelCursor(t); ListSelIsSelected(t, 2)\n" + "ListSelReset t, 2\n" + "Print ListSelCount(t); ListSelSelectedCount(t); ListSelCursor(t)\n" + "ListSelSetAll t, True\n" + "Print ListSelSelectedCount(t)\n" + "ListSelSetAll t, False\n" + "ListSelSetCursor t, 1\n" + "Print ListSelSelectedCount(t); ListSelIsSelected(t, 1); ListSelCursor(t)\n" + "u = ListSelCreate(False, 4)\n" + "ListSelClick u, 2, vbCtrlMask\n" + "Print ListSelCursor(u); ListSelIsSelected(u, 2); ListSelSelectedCount(u); ListSelKey(u, vbKeySpace, 0) = LISTSEL_KEY_NONE\n" + "Print ListReorderDest(0, 3, 3); ListReorderDest(1, 1, 3); ListReorderDest(2, 0, 3)\n" + "Print ListNavigateIndex(vbKeyEnd, 0, 7, 3); ListNavigateIndex(vbKeyPageDown, 1, 7, 3); ListNavigateIndex(Asc(\"z\"), 1, 7, 3)\n" + "Print ListGridNavigate(vbKeyDown, 1, 7, 3, 2); ListGridNavigate(vbKeyDown, 4, 7, 3, 2); ListGridNavigate(vbKeyRight, 2, 7, 3, 2); ListGridNavigate(vbKeyPageDown, 0, 7, 3, 2); ListGridNavigate(vbKeyLeft, -1, 7, 3, 2); ListGridNavigate(vbKeyUp, 4, 7, 3, 2)\n" + "Print ListTypeAhead(\"b\", \"apple\" & Chr$(10) & \"Banana\" & Chr$(10) & \"berry\", 1); ListTypeAhead(\"B\", \"apple\" & Chr$(10) & \"Banana\" & Chr$(10) & \"berry\", 2); ListTypeAhead(\"z\", \"apple\", 0); ListTypeAhead(\"\", \"apple\", 0)\n" + "Print ListTypeAheadMatch(\"a\", \"Apple\"); ListTypeAheadMatch(\"b\", \"Apple\")\n" + "Dim lg As Long, li As Long, by As Long\n" + "lg = LISTICON_VIEW_LARGE: li = LISTICON_VIEW_LIST: by = LISTICON_BY_ORDER\n" + "Print ListIconMetric(lg, LISTICON_CELL_W, 8, 16, 330, 150, 8, by); ListIconMetric(lg, LISTICON_CELL_H, 8, 16, 330, 150, 8, by); ListIconMetric(lg, LISTICON_COLUMNS, 8, 16, 330, 150, 8, by); ListIconMetric(lg, LISTICON_SCROLLBAR, 8, 16, 330, 150, 9, by); ListIconMetric(lg, LISTICON_COLUMNS, 8, 16, 330, 150, 9, by); ListIconMetric(li, LISTICON_DETAIL_X, 8, 16, 330, 150, 3, by); ListIconMetric(lg, LISTICON_ROWS, 8, 16, 330, 150, 2, 5)\n" + "Print ListIconCellAt(lg, 8, 16, 330, 150, 8, by, 0, 87, 77); ListIconCellAt(lg, 8, 16, 330, 150, 8, by, 1, 87, 77); ListIconCellAt(lg, 8, 16, 330, 150, 8, by, 0, 1, 77)\n" + "Print ListIconDragScroll(lg, 8, 16, 330, 150, 9, by, 1, 0); ListIconDragScroll(lg, 8, 16, 330, 150, 9, by, 0, 148); ListIconDropCell(lg, 8, 16, 330, 150, 8, by, 0, 87, -100)\n" + "Print ListIconDropDest(0, 2, 6); ListIconDropDest(0, 9, 6); ListIconDropDest(3, 3, 6); ListIconDragStarted(4, 0); ListIconDragStarted(3, -3)\n" + "Print ListIconLabel$(\"BASIC Demo App\", 10, 1) & \"|\" & ListIconLabel$(\"BASIC Demo App\", 10, 2) & \"|\" & ListIconLabel$(\"Help Editor\", 10, 2) & \"|\" & ListIconLabel$(\"Clock\", 10, 2) & \"|\" & ListIconLabel$(\"Clock\", 10, 3)\n" + "Print ListIconNavigate(vbKeyRight, 0, \"0,0,2,0,1,2\", 3, 2); ListIconNavigate(vbKeyDown, 1, \"0,0,2,0,1,2\", 3, 2); ListIconNavigate(vbKeyUp, 2, \"0,0,2,0,1,2\", 3, 2); ListIconNavigate(vbKeyLeft, 0, \"0,0,2,0,1,2\", 3, 2); ListIconNavigate(vbKeyHome, -1, \"\", 3, 2)\n" + "Print ListIconScrollPress(lg, 8, 16, 330, 150, 9, by, 0, 320, 147); ListIconScrollPress(lg, 8, 16, 330, 150, 9, by, 0, 100, 147); ListIconScrollGrab(lg, 8, 16, 330, 150, 9, by, 0, 320, 21); ListIconScrollGrab(lg, 8, 16, 330, 150, 9, by, 0, 320, 147); ListIconScrollDrag(lg, 8, 16, 330, 150, 9, by, 5, 300); ListIconScrollDrag(lg, 8, 16, 330, 150, 9, by, 5, -50)\n"; + char out[FRT_OUT_MAX]; + + tCase("listhelp.bas from BASIC"); + TEST_TRUE(runModule(files, DVX_ARRAY_LEN(files), code, out, sizeof(out))); + TEST_STR_EQ(out, + "True 5 -1 0 \n" + "3 -1 0 3 \n" + "2 0 \n" + "True 5 \n" + "True 0 True \n" + "0 0 -1 \n" + "1 2 -1 -1 \n" + "4 -1 -1 \n" + "0 2 \n" + "0 -1 0 \n" + "4 -1 0 \n" + "3 -1 \n" + "-1 0 0 \n" + "2 1 0 \n" + "2 \n" + "1 -1 1 \n" + "2 -1 1 True \n" + "2 -1 0 \n" + "6 4 -1 \n" + "4 6 3 6 0 1 \n" + "2 1 -1 -1 \n" + "-1 0 \n" + "80 70 4 -1 3 222 5 \n" + "5 9 -1 \n" + "0 1 1 \n" + "2 5 -1 -1 0 \n" + "BASIC Demo|App|Editor||\n" + "1 2 0 -1 -1 \n" + "1 -1 5 -1 1 0 \n"); +} + + +// security.bas called for real against the C library: hashes and HMAC +// against secHash.c, HKDF, the key exchange both ways, the cipher round +// trip with zero bytes in the data, the compare, the handle rules. +static void caseSecurity(void) { + static const char *const files[] = { "security.bas" }; + static const char code[] = + "Dim a As Long, b As Long, c As Long, d As Long, h As Long\n" + "Dim k As String, x As String, y As String, z As String\n" + "Print Len(SecRandom$(40)); Len(SecRandom$(0)); SecRandom$(16) <> SecRandom$(16)\n" + "SecAddEntropy \"some\" & Chr$(0) & \"noise\"\n" + "x = SecSha256$(\"abc\")\n" + "Print Len(x); Hex$(Asc(Left$(x, 1))); Hex$(Asc(Right$(x, 1)))\n" + "h = SecSha256Open()\n" + "SecSha256Add h, \"a\"\n" + "SecSha256Add h, \"bc\"\n" + "Print SecSha256End$(h) = x; SecSha256End$(h) = \"\"\n" + "y = SecHmac$(\"key\", \"The quick brown fox jumps over the lazy dog\")\n" + "Print Len(y); Hex$(Asc(Left$(y, 1))); Hex$(Asc(Right$(y, 1)))\n" + "h = SecHmacOpen(\"key\")\n" + "SecHmacAdd h, \"The quick brown fox \"\n" + "SecHmacAdd h, \"jumps over the lazy dog\"\n" + "Print SecEqual(SecHmacEnd$(h), y); SecEqual(\"ab\", \"ac\"); SecEqual(\"ab\", \"abc\"); SecEqual(\"\", \"\")\n" + "z = SecHkdf$(\"salt\", \"secret\", \"info\", 42)\n" + "Print Len(z); Len(SecHkdf$(\"\", \"secret\", \"\", 0)); Len(SecHkdf$(\"\", \"secret\", \"\", SEC_HKDF_MAX_OUT + 1))\n" + "a = SecDhCreate()\n" + "b = SecDhCreate()\n" + "Print a > 0; b > 0; Len(SecDhPublicKey$(a)); SecDhSecret$(a) = \"\"\n" + "Print SecDhComputeSecret(a, SecDhPublicKey$(b)) = SEC_OK; SecDhComputeSecret(b, SecDhPublicKey$(a)) = SEC_OK; SecDhComputeSecret(a, \"short\") = SEC_ERR_PARAM\n" + "Print SecDhSecret$(a) = SecDhSecret$(b); Len(SecDhSecret$(a))\n" + "k = SecHkdf$(\"\", SecDhSecret$(a), \"cipher\", SEC_XTEA_KEY_SIZE)\n" + "c = SecCipherCreate(k)\n" + "d = SecCipherCreate(k)\n" + "x = \"zero\" & Chr$(0) & \"inside\" & Chr$(0)\n" + "y = SecCipherCrypt$(c, x)\n" + "Print Len(y); y <> x; SecCipherCrypt$(d, y) = x\n" + "SecCipherSetCounter c, 0\n" + "SecCipherSetCounter d, 0\n" + "Print SecCipherCrypt$(d, SecCipherCrypt$(c, x)) = x; SecCipherCreate(\"short\"); SecCipherCrypt$(a, x) = \"\"\n" + "SecDhFree a\n" + "SecDhFree b\n" + "SecCipherFree c\n" + "SecCipherFree d\n" + "Print SecDhPublicKey$(a) = \"\"; SecCipherCrypt$(c, x) = \"\"\n"; + uint8_t digest[SEC_SHA256_SIZE]; + uint8_t mac[SEC_SHA256_SIZE]; + char out[FRT_OUT_MAX]; + char expected[FRT_OUT_MAX]; + + tCase("security.bas from BASIC"); + secSha256((const uint8_t *)"abc", 3, digest); + secHmacSha256((const uint8_t *)"key", 3, (const uint8_t *)"The quick brown fox jumps over the lazy dog", 43, mac); + snprintf(expected, sizeof(expected), + "40 0 True \n" + "32 %X%X\n" + "True True \n" + "32 %X%X\n" + "-1 0 0 -1 \n" + "42 0 0 \n" + "True True 128 True \n" + "True True True \n" + "True 128 \n" + "12 True True \n" + "True 0 True \n" + "True True \n", + digest[0], digest[SEC_SHA256_SIZE - 1], mac[0], mac[SEC_SHA256_SIZE - 1]); + TEST_TRUE(runModule(files, DVX_ARRAY_LEN(files), code, out, sizeof(out))); + TEST_STR_EQ(out, expected); +} + + +void frtLibBasCases(void) { + caseCommAdditions(); + caseLibConsts(); + caseListHelp(); + caseSecurity(); +} + + +// Compile the include files plus code as one module and run it with the +// library calls made for real; its PRINT output lands in out. +static bool runModule(const char *const *files, int32_t fileCount, const char *code, char *out, size_t outSize) { + char *src = (char *)malloc(LIB_SRC_MAX); + FrtAppT app; + bool ok = src != NULL; + + out[0] = '\0'; + + if (src) { + src[0] = '\0'; + } + + for (int32_t i = 0; ok && i < fileCount; i++) { + ok = appendInclude(src, LIB_SRC_MAX, files[i]); + } + + if (ok && strlen(src) + strlen(code) + 1 > LIB_SRC_MAX) { + ok = false; + } + + if (ok) { + strcat(src, code); + tInit(LIB_SCREEN_W, LIB_SCREEN_H, LIB_SCREEN_BPP); + ok = frtBuild(&app, src, NULL, 0); + + if (!ok) { + printf(" compile error: %s\n", app.error); + } else { + basNativeHostSetLive(true); + ok = frtRunMain(&app) == BAS_VM_HALTED; + basNativeHostSetLive(false); + snprintf(out, outSize, "%s", frtOut(&app)); + frtDestroy(&app); + } + + tShutdown(); + } + + free(src); + return ok; +} diff --git a/src/test/formrt/testUcDesign.c b/src/test/formrt/testUcDesign.c new file mode 100644 index 00000000..3d06038d --- /dev/null +++ b/src/test/formrt/testUcDesign.c @@ -0,0 +1,346 @@ +// 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. + + +// testUcDesign.c -- a form designer written in BASIC and the UserControls +// of the project it edits (round 2): controls registered for a program's +// design forms (basCtlLoadDesign with the program's runtime) are its own +// control types -- in the designer.bas lists with their Description and +// ToolboxIcon, created by CreateControl, UserMode False on a form in +// DesignMode, PropertyChanged in their code raising DesignPropertyChanged +// -- private to it beside a C designer's set of the same name, and let go +// when it ends. Also the "UserControl" object's events (CtlEventList$, +// CtlTypeEventParams$). + +#include "frtHarness.h" +#include "../../apps/kpunch/dvxbasic/basBuild.h" +#include "../../apps/kpunch/dvxbasic/compiler/basEvents.h" +#include "dvxPlat.h" + +#include +#include +#include + +#define UD_SCREEN_W 800 +#define UD_SCREEN_H 600 +#define UD_SCREEN_BPP 32 +#define UD_SRC_MAX 65536 +#define UD_KEY_MAX 64 +#define UD_ICON_NAME "meter.bmp" +#define UD_ICON_BYTES "BM not really a picture" +#define UD_ICON_OUT "out.bmp" +#define UD_PROJECT "meters.dbp" + +// The project's control (a class of the design module): PropertyChanged +// from its Bump method, UserMode and its name from Info. +static const char sMeterClass[] = + "BEGINCLASS \"Meter\" USERCONTROL\n" + "Public Event Moved(ByVal Amount AS INTEGER, Undo AS BOOLEAN)\n" + "Private mLevel AS INTEGER\n" + "Public Property Get Level() AS INTEGER\n" + " Level = mLevel\n" + "End Property\n" + "Public Property Let Level(v AS INTEGER)\n" + " mLevel = v\n" + "End Property\n" + "Public Sub Bump()\n" + " mLevel = mLevel + 1\n" + " UserControl.PropertyChanged \"Level\"\n" + "End Sub\n" + "Public Function Info() AS STRING\n" + " Info = UserControl.Name & \" \" & UserControl.UserMode & \" \" & mLevel\n" + "End Function\n" + "ENDCLASS\n"; + +#define UD_HEADER \ + "VERSION DVX 1.00\n" \ + "Begin UserControl Meter\n" \ + " Description = \"A meter\"\n" \ + " ToolboxIcon = \"" UD_ICON_NAME "\"\n" \ + " DefaultEvent = \"Moved\"\n" \ + "End\n" + +// The designer: its toolbox and property sheet questions about Meter (its +// Level property and Bump method among them), a Meter placed on its form +// in design mode, PropertyChanged reported, a .frm text holding a Meter +// loaded. +static const char sDesigner[] = + "Sub Form1_T\n" + " Dim i As Integer\n" + " Dim found As Integer\n" + " Dim m As Object\n" + " For i = 0 To CtlTypeCount() - 1\n" + " If CtlTypeName$(i) = \"Meter\" Then found = found + 1\n" + " Next\n" + " Print found; CtlTypeFlags(\"Meter\"); CtlTypeTooltip$(\"Meter\"); \" \"; CtlTypeDefaultEvent$(\"Meter\"); \" \"; CtlTypeIconFile(\"Meter\", ICON_OUT)\n" + " Print CtlEventList$(\"Meter\"); \" [\"; CtlTypeEventParams$(\"Meter\", \"Moved\"); \"]\"\n" + " For i = 0 To CtlPropCount(\"Meter\") - 1\n" + " If CtlPropName$(\"Meter\", i) = \"Level\" Then Print \"Level \"; CtlPropType(\"Meter\", i); InStr(CtlMethodList$(\"Meter\"), \"Bump\") > 0\n" + " Next\n" + " Me.DesignMode = True\n" + " Set m = CreateControl(Me, \"Meter\", \"Meter1\")\n" + " Print m.Info()\n" + " m.Bump\n" + " m.Bump\n" + " Me.DesignMode = False\n" + " m.Bump\n" + " Print m.Info()\n" + "End Sub\n" + "Sub Form1_DesignPropertyChanged(CtrlName As String, PropertyName As String)\n" + " Dim c As Object\n" + " Set c = Me.Controls(CtrlName)\n" + " Print \"changed \"; CtrlName; \" \"; PropertyName; \" \"; c.Level\n" + "End Sub\n" + "Sub Form1_Lft\n" + " Dim f As Object\n" + " Set f = LoadFormText(\"Begin Form Gen1\" & Chr$(10) & \" Begin Meter M1\" & Chr$(10) & \" End\" & Chr$(10) & \"End\" & Chr$(10))\n" + " f.DesignMode = True\n" + " Print f.Controls.Count; f.Controls(1).Info()\n" + " Unload f\n" + "End Sub\n" + "Sub Form1_Uc\n" + " Print CtlEventList$(\"UserControl\")\n" + " Print CtlTypeEventParams$(\"UserControl\", \"MouseDown\"); \"|\"; CtlTypeEventParams$(\"UserControl\", \"Click\"); \"|\"; CtlTypeEventParams$(\"UserControl\", \"Load\"); \"|\"; CtlTypeEventParams$(\"TextBox\", \"KeyDown\"); \"|\"; CtlEventParams$(\"MouseDown\")\n" + " Print InStr(CtlEventList$(\"Form\"), \"|DesignPropertyChanged\") > 0; \" \"; CtlEventParams$(\"DesignPropertyChanged\")\n" + "End Sub\n" + "Sub Form1_Gone\n" + " Dim i As Integer\n" + " Dim found As Integer\n" + " For i = 0 To CtlTypeCount() - 1\n" + " If CtlTypeName$(i) = \"Meter\" Then found = found + 1\n" + " Next\n" + " Print found; CtlTypeFlags(\"Meter\")\n" + "End Sub\n"; + +// Prototypes +static void caseUcDesignClash(void); +static void caseUcDesignProgram(void); +static bool designerBuild(FrtAppT *app, const char *frm, const char *iconOut); +static BasModuleT *designModule(void); +void frtUcDesignCases(void); + + +// A program's design controls and its own may not share a name; a C +// designer's set and the program's are separate, and letting one go +// leaves the other. +static void caseUcDesignClash(void) { + tCase("UserControls for design: one owner each, names unique per owner"); + tInit(UD_SCREEN_W, UD_SCREEN_H, UD_SCREEN_BPP); + + const char *headers[] = { UD_HEADER }; + BasModuleT *mod = designModule(); + FrtAppT app; + const char *own = + "BEGINCLASS \"Meter\" USERCONTROL\n" + "ENDCLASS\n"; + + TEST_TRUE(mod != NULL); + + if (!mod || !frtBuild(&app, own, NULL, 0)) { + basModuleFree(mod); + tShutdown(); + return; + } + + // The program's own Meter first: a design Meter for it is refused + TEST_TRUE(basFormRtRegisterCtl(app.rt, UD_HEADER, (int32_t)strlen(UD_HEADER))); + TEST_EQ(basCtlLoadDesign(app.rt, mod, headers, 1, UD_PROJECT), 0); + TEST_TRUE(basUcOwnIface(app.rt, "Meter") != NULL); + + // A C designer's Meter beside it, and let go again + TEST_EQ(basCtlLoadDesign(NULL, mod, headers, 1, UD_PROJECT), 1); + TEST_TRUE(basUcOwnIface(NULL, "Meter") != NULL); + TEST_TRUE(basUcOwnIface(NULL, "Meter") != basUcOwnIface(app.rt, "Meter")); + TEST_TRUE(strcmp(basUcResolveType(NULL, "Meter"), basUcResolveType(app.rt, "Meter")) != 0); + TEST_TRUE(!basUcIfaceVisible(app.rt, basUcOwnIface(NULL, "Meter"))); + TEST_TRUE(!basUcIfaceVisible(NULL, basUcOwnIface(app.rt, "Meter"))); + TEST_TRUE(basCtlUnloadDesign(app.rt)); + TEST_TRUE(basUcOwnIface(NULL, "Meter") != NULL); + TEST_TRUE(basCtlUnloadDesign(NULL)); + TEST_PTR_EQ(basUcOwnIface(NULL, "Meter"), NULL); + TEST_TRUE(basUcOwnIface(app.rt, "Meter") != NULL); + frtDestroy(&app); + basModuleFree(mod); + tShutdown(); +} + + +// A designer program's own set: the toolbox lists, CreateControl, +// UserMode, DesignPropertyChanged; a C designer's Meter beside it stays +// out of its lists; the set goes with the program. +static void caseUcDesignProgram(void) { + tCase("UserControls for a BASIC designer: types, toolbox, design forms, PropertyChanged"); + tInit(UD_SCREEN_W, UD_SCREEN_H, UD_SCREEN_BPP); + + char dir[DVX_MAX_PATH]; + char dbp[DVX_MAX_PATH]; + char iconPath[DVX_MAX_PATH]; + char outPath[DVX_MAX_PATH]; + char want[DVX_MAX_PATH * 2]; + char key[UD_KEY_MAX]; + + snprintf(dir, sizeof(dir), "%s/ucdesign", tTempDir()); + snprintf(dbp, sizeof(dbp), "%s/" UD_PROJECT, dir); + snprintf(iconPath, sizeof(iconPath), "%s/" UD_ICON_NAME, dir); + snprintf(outPath, sizeof(outPath), "%s/" UD_ICON_OUT, dir); + platformMkdirRecursive(dir); + + FILE *f = fopen(iconPath, "wb"); + + TEST_TRUE(f != NULL); + + if (f) { + fputs(UD_ICON_BYTES, f); + fclose(f); + } + + const char *headers[] = { UD_HEADER }; + BasModuleT *mod = designModule(); + BasModuleT *ideMod = designModule(); + FrtAppT app; + + TEST_TRUE(mod != NULL && ideMod != NULL); + + if (!mod || !ideMod || !designerBuild(&app, "Begin Form Form1\n Caption = \"Designer\"\nEnd\n", outPath)) { + basModuleFree(mod); + basModuleFree(ideMod); + tShutdown(); + return; + } + + // The C designer's Meter, then the program's own set + TEST_EQ(basCtlLoadDesign(NULL, ideMod, headers, 1, dbp), 1); + TEST_EQ(basCtlLoadDesign(app.rt, mod, headers, 1, dbp), 1); + + const WgtIfaceT *iface = basUcOwnIface(app.rt, "Meter"); + + TEST_TRUE(iface != NULL); + TEST_TRUE(basUcIfaceVisible(app.rt, iface)); + TEST_TRUE(!basUcIfaceVisible(NULL, iface)); + TEST_TRUE(basUcIconPath(iface, want, sizeof(want))); + TEST_STR_EQ(want, iconPath); + TEST_TRUE(!basUcIconPath(NULL, want, sizeof(want))); + snprintf(key, sizeof(key), "%s", basUcResolveType(app.rt, "Meter")); + TEST_TRUE(wgtGetIface(key) == iface); + + // The designer.bas calls are made for real + basNativeHostSetLive(true); + frtStart(&app, NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), + "1 2 A meter Moved -1 \n" + "Moved|GotFocus|LostFocus|Validate|DragDrop|DragOver [ByVal Amount As Integer, Undo As Boolean]\n" + "Level 1 True \n" + "Meter1 False 0\n" + "changed Meter1 Level 1 \n" + "changed Meter1 Level 2 \n" + "Meter1 True 3\n"); + + int32_t len = 0; + char *copy = platformReadFile(outPath, &len); + + TEST_TRUE(copy != NULL); + TEST_STR_EQ(copy ? copy : "", UD_ICON_BYTES); + free(copy); + + // A .frm text holding one: LoadFormText knows the type too + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Lft")); + TEST_STR_EQ(frtOut(&app), "1 M1 False 0\n"); + + // The UserControl object's events and the new form event + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Uc")); + snprintf(want, sizeof(want), + "Initialize|Terminate|Resize|Paint|Click|DblClick|MouseDown|MouseMove|MouseUp|MouseWheel|KeyDown|KeyUp|KeyPress|GotFocus|LostFocus\n" + "%s|||KeyCode As Integer, Shift As Integer|Button As Integer, X As Integer, Y As Integer\n" + "True CtrlName As String, PropertyName As String\n", BAS_UC_MOUSE_PARAMS); + TEST_STR_EQ(frtOut(&app), want); + + // Let go: no Meter for the program; the C designer's stays + TEST_TRUE(basCtlUnloadDesign(app.rt)); + TEST_PTR_EQ(wgtGetIface(key), NULL); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Gone")); + TEST_STR_EQ(frtOut(&app), "0 -1 \n"); + TEST_TRUE(basUcOwnIface(NULL, "Meter") != NULL); + TEST_PTR_EQ(basUcResolveType(app.rt, "Meter"), NULL); + basNativeHostSetLive(false); + + // Registered again, it goes when the program ends + TEST_EQ(basCtlLoadDesign(app.rt, mod, headers, 1, dbp), 1); + snprintf(key, sizeof(key), "%s", basUcResolveType(app.rt, "Meter")); + TEST_TRUE(wgtGetIface(key) != NULL); + frtDestroy(&app); + TEST_PTR_EQ(wgtGetIface(key), NULL); + TEST_TRUE(basUcOwnIface(NULL, "Meter") != NULL); + TEST_TRUE(basCtlUnloadDesign(NULL)); + basModuleFree(mod); + basModuleFree(ideMod); + tShutdown(); +} + + +// The designer program: designer.bas and sDesigner with form frm, the +// toolbox icon written to iconOut (CONST ICON_OUT). +static bool designerBuild(FrtAppT *app, const char *frm, const char *iconOut) { + int32_t len = 0; + char *inc = platformReadFile(FRT_INCLUDE_DIR "/designer.bas", &len); + char *src = (char *)malloc(UD_SRC_MAX); + bool ok = false; + + if (inc && src) { + const char *frms[] = { frm }; + + snprintf(src, UD_SRC_MAX, "%s\nCONST ICON_OUT = \"%s\"\n%s", inc, iconOut, sDesigner); + ok = frtBuild(app, src, frms, 1); + + if (!ok) { + printf(" compile error: %s\n", app->error); + } + } + + free(inc); + free(src); + TEST_TRUE(ok); + return ok; +} + + +// The project's code as the designer compiles it: the Meter class. +static BasModuleT *designModule(void) { + BasCompileErrorT err; + BasModuleT *mod = basBuildCompile(sMeterClass, (int32_t)strlen(sMeterClass), false, NULL, NULL, &err); + + if (!mod) { + printf(" design module: %s\n", err.message); + } + + return mod; +} + + +void frtUcDesignCases(void) { + caseUcDesignProgram(); + caseUcDesignClash(); +} diff --git a/src/test/formrt/testUserCtl.c b/src/test/formrt/testUserCtl.c new file mode 100644 index 00000000..5db707b1 --- /dev/null +++ b/src/test/formrt/testUserCtl.c @@ -0,0 +1,1222 @@ +// 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. + + +// testUserCtl.c -- UserControls (widget classes written in BASIC) in the +// form runtime: a program's own control registered from its header and +// used from its .frm (properties from the .frm, Initialize / Resize / +// Paint, drawing on the UserControl object, the host's handlers of its +// events with ByRef arguments, the universal Click, methods, the mouse +// and keys, UserMode in design mode, unregistration at the end), the +// generic C access (create by name, properties, methods, onEvent), and an +// installed control file (.wgt) built with basBuildControl and loaded +// with basCtlLoadFile, used by a program's .frm and by C. + +#include "frtHarness.h" +#include "../../apps/kpunch/dvxbasic/basBuild.h" +#include "../../apps/kpunch/dvxbasic/basRes.h" +#include "canvas/canvas.h" +#include "listHelp.h" +#include "dvxRes.h" +#include "dvxPlat.h" +#include "dvxVideo.h" + +#include +#include +#include + +#define UC_SCREEN_W 800 +#define UC_SCREEN_H 600 +#define UC_SCREEN_BPP 32 +#define UC_SETTLE 4 // frames for layout, then the poll's Resize and Paint +#define UC_TEXT_MAX 128 +#define UC_ITEM 3 // the index Open raises ItemOpen with +#define UC_RENAMED "new" // what the C host writes back into Rename's Text +#define UC_PAINT_SPOT 20 // a pixel Paint leaves in BackColor +#define UC_KEY_A 'a' +#define UC_FILE_NAME "ucfile.wgt" +#define UC_ICONVIEW_CTL FRT_INCLUDE_DIR "/../../test/dos/fixtures/iconview/iconview.ctl" +#define UC_LISTHELP_BAS FRT_INCLUDE_DIR "/listhelp.bas" +#define UC_ICON_ITEMS 6 // the IconView form's first items +#define UC_ICON_MORE 40 // and the ones Form1_More adds +#define UC_GAMMA 2 // Gamma's index (Alpha is dragged onto its cell) +#define UC_DELTA 3 // Delta's index (type-ahead "d" selects it) +#define UC_PLACED_ITEMS 3 // Form1_Manual's items, in cells of their own +#define UC_PLACED_ROWS 2 // the rows they take +#define UC_THEME_BACK 0xC0 // the fake theme (sUcFakeColors): grey content, +#define UC_THEME_SEL 0x30 // dark grey selection (every channel the same) +#define UC_PIXEL_IN 1 // a pixel this far inside a highlighted name box +#define UC_DBLCLICK_MS 50 // well within the double-click time +#define UC_ENUM_FILE "ucenum.wgt" +#define UC_DIR_COUNT 4 // ArrowDirE's and CompassDirE's members: North 0, East 1, South 2, West 3 +#define UC_DIR_EAST 1 +#define UC_DIR_SOUTH 2 +#define UC_DIR_WEST 3 +#define UC_SPAN_TINY 4 // ArrowSpanE's Tiny (its values are not 0, 1, 2 ...) + +// The control: a gauge with a Double Level, a String Title (Get / Let; Caption is every control's own), +// a public Count, a method raising ItemOpen with a ByRef Accept, one +// raising Rename with a ByRef String, a Sub raising Click, and its +// handlers recording what reached it. +#define UC_CLASS \ + "BEGINCLASS \"Gauge\" USERCONTROL\n" \ + "Public Event ItemOpen(ByVal Index AS INTEGER, Accept AS BOOLEAN)\n" \ + "Public Event Click()\n" \ + "Public Event Rename(Text AS STRING)\n" \ + "Private mLevel AS DOUBLE\n" \ + "Private mTitle AS STRING\n" \ + "Public Count AS INTEGER\n" \ + "Public Property Get Level() AS DOUBLE\n" \ + " Level = mLevel\n" \ + "End Property\n" \ + "Public Property Let Level(v AS DOUBLE)\n" \ + " mLevel = v\n" \ + "End Property\n" \ + "Public Property Get Title() AS STRING\n" \ + " Title = mTitle\n" \ + "End Property\n" \ + "Public Property Let Title(v AS STRING)\n" \ + " mTitle = v\n" \ + "End Property\n" \ + "Public Function Open(i AS INTEGER) AS BOOLEAN\n" \ + " DIM ok AS BOOLEAN\n" \ + " RaiseEvent ItemOpen(i, ok)\n" \ + " Open = ok\n" \ + "End Function\n" \ + "Public Function AskName() AS STRING\n" \ + " DIM n AS STRING\n" \ + " n = \"old\"\n" \ + " RaiseEvent Rename(n)\n" \ + " AskName = n\n" \ + "End Function\n" \ + "Public Sub Poke()\n" \ + " RaiseEvent Click\n" \ + "End Sub\n" \ + "Public Function Info() AS STRING\n" \ + " Info = UserControl.Name & \" \" & UserControl.MinWidth & \" \" & UserControl.UserMode & \" \" & TypeName(UserControl)\n" \ + "End Function\n" \ + "Private Sub UserControl_Initialize()\n" \ + " mTitle = \"init\"\n" \ + " UserControl.MinWidth = 50\n" \ + "End Sub\n" \ + "Private Sub UserControl_Resize()\n" \ + " Count = Count + 100\n" \ + "End Sub\n" \ + "Private Sub UserControl_Paint()\n" \ + " UserControl.Line (0, 0)-(9, 9), RGB(255, 0, 0), BF\n" \ + " Count = Count + 1\n" \ + "End Sub\n" \ + "Private Sub UserControl_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n" \ + " mTitle = \"down\" & Button\n" \ + "End Sub\n" \ + "Private Sub UserControl_Click()\n" \ + " mTitle = mTitle & \" click\"\n" \ + "End Sub\n" \ + "Private Sub UserControl_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)\n" \ + " mTitle = \"key\" & KeyCode\n" \ + "End Sub\n" \ + "ENDCLASS\n" + +#define UC_HEADER \ + "VERSION DVX 1.00\n" \ + "Begin UserControl Gauge\n" \ + " Width = 40\n" \ + " Height = 30\n" \ + " BackColor = &H00FFFFFF&\n" \ + " Description = \"A gauge\"\n" \ + " DefaultEvent = \"ItemOpen\"\n" \ + "End\n" + +// A control with Enum properties: Direction (Get / Let, ArrowDirE North, +// East, South, West: 0 .. 3) and Span (a Public variable, ArrowSpanE Tiny +// 4, Huge 40). +#define UC_ENUM_CLASS \ + "Public Enum ArrowDirE\n" \ + " North\n" \ + " East\n" \ + " South\n" \ + " West\n" \ + "End Enum\n" \ + "Public Enum ArrowSpanE\n" \ + " Tiny = 4\n" \ + " Huge = 40\n" \ + "End Enum\n" \ + "BEGINCLASS \"Arrow\" USERCONTROL\n" \ + "Private mDir AS ArrowDirE\n" \ + "Public Span AS ArrowSpanE\n" \ + "Public Property Get Direction() AS ArrowDirE\n" \ + " Direction = mDir\n" \ + "End Property\n" \ + "Public Property Let Direction(v AS ArrowDirE)\n" \ + " mDir = v\n" \ + "End Property\n" \ + "Private Sub UserControl_Initialize()\n" \ + " mDir = East\n" \ + " Span = Tiny\n" \ + "End Sub\n" \ + "ENDCLASS\n" + +#define UC_ENUM_HEADER \ + "Begin UserControl Arrow\n" \ + " Width = 20\n" \ + " Height = 20\n" \ + "End\n" + +// The form using it. +static const char *sForm = + "Begin Form Form1\n" + " Caption = \"UC\"\n" + " Begin Gauge Gauge1\n" + " Title = \"fromfrm\"\n" + " Level = 0.1\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Print Gauge1.Title; \" \"; Gauge1.Level = 0.1; Gauge1.Count\n" + " If Gauge1.Open(3) Then Print \"opened\"\n" + " Gauge1.Poke\n" + " Print Gauge1.Info()\n" + " Print Gauge1.AskName()\n" + "End Sub\n" + "Sub Form1_C ()\n" + " Print Gauge1.Title\n" + "End Sub\n" + "Sub Gauge1_ItemOpen (ByVal Index As Integer, Accept As Boolean)\n" + " Print \"itemopen\"; Index\n" + " Accept = True\n" + "End Sub\n" + "Sub Gauge1_Click ()\n" + " Print \"hostclick\"\n" + "End Sub\n" + "Sub Gauge1_Rename (Text As String)\n" + " Text = Text & \"!\"\n" + "End Sub\n"; + +// What a C host's onEvent saw. +typedef struct { + char name[UC_TEXT_MAX]; + int32_t argc; + int32_t first; +} UcSeenT; + +static UcSeenT sSeen; + +// The theme colors IconView reads through system.bas (SysColorId, +// SysGetColor), whose library this suite does not link: a stand-in +// module with a grey content area and a dark grey selection. +static const char sUcFakeColors[] = + "Function SysColorId(ByVal colorName As String) As Long\n" + " SysColorId = InStr(\"contentBg contentFg menuHighlightBg menuHighlightFg\", colorName)\n" + "End Function\n" + "Function SysGetColor(ByVal id As Long) As Long\n" + " Select Case id\n" + " Case 1\n" + " SysGetColor = &HC0C0C0\n" + " Case 21\n" + " SysGetColor = &H303030\n" + " Case 37\n" + " SysGetColor = &HFFFFFF\n" + " Case Else\n" + " SysGetColor = 0\n" + " End Select\n" + "End Function\n"; + +// Prototypes +static void caseUcCHost(void); +static void caseUcDesignMode(void); +static void caseUcEnumProps(void); +static void caseUcIconView(void); +static void caseUcInstalled(void); +static void caseUcProgram(void); +static void caseUcRegisterErrors(void); +static void caseUcTwoPrograms(void); +void frtUserCtlCases(void); +static bool ucBuild(FrtAppT *app, const char *frm); +static bool ucBuildCtl(FrtAppT *app, const char *cls, const char *header, const char *frm); +static bool ucBuildPlain(FrtAppT *app, const char *frm); +static void ucIconGrid(const WidgetT *w, ListIconViewE view, int32_t count, int32_t rowsTaken, ListIconGridT *g); +static void ucIndexPoint(const WidgetT *w, const ListIconGridT *g, int32_t index, int32_t *sx, int32_t *sy); +static void ucItemPoint(const WidgetT *w, const ListIconGridT *g, int32_t col, int32_t row, int32_t *sx, int32_t *sy); +static void ucOnEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); +static bool ucPainted(WidgetT *w); +static void ucViewPoint(const WidgetT *w, int32_t x, int32_t y, int32_t *sx, int32_t *sy); + + +// A C program's use of a BASIC control: wgtCreateByName, the generic +// property and method access, events through onEvent with an argument +// written back. +static void caseUcCHost(void) { + tCase("UserControl from C: create by name, properties, methods, onEvent"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + FrtAppT app; + + if (!ucBuild(&app, NULL)) { + tShutdown(); + return; + } + + WindowT *win = dvxCreateWindow(tCtx(), "C host", 0, 0, UC_SCREEN_W / 2, UC_SCREEN_H / 2, true); + WidgetT *root = win ? wgtInitWindow(tCtx(), win) : NULL; + WidgetT *w = root ? wgtCreateByName("Gauge", root) : NULL; + + TEST_TRUE(w != NULL); + + if (w) { + WgtValueT v; + WgtValueT ret; + WgtValueT arg = wgtValueInt(UC_ITEM); + + memset(&sSeen, 0, sizeof(sSeen)); + w->onEvent = ucOnEvent; + TEST_TRUE(wgtGetIfaceOf(w, NULL) == wgtGetIface(wgtResolveTypeName("Gauge"))); + TEST_TRUE(wgtGetProp(w, "Title", &v)); + TEST_STR_EQ(v.s, "init"); + v = wgtValueDouble(0.1); + TEST_TRUE(wgtSetProp(w, "Level", &v)); + TEST_TRUE(wgtGetProp(w, "Level", &v)); + TEST_EQ(v.type, WGT_IFACE_DOUBLE); + TEST_TRUE(v.f == 0.1); + TEST_TRUE(wgtCallMethod(w, "Open", &arg, 1, &ret)); + TEST_STR_EQ(sSeen.name, "ItemOpen"); + TEST_EQ(sSeen.argc, 2); + TEST_EQ(sSeen.first, UC_ITEM); + TEST_EQ(ret.type, WGT_IFACE_BOOL); + TEST_EQ(ret.i, 1); + TEST_TRUE(wgtCallMethod(w, "Poke", NULL, 0, &ret)); + TEST_EQ(ret.type, WGT_IFACE_NONE); + TEST_TRUE(wgtCallMethod(w, "AskName", NULL, 0, &ret)); + TEST_STR_EQ(sSeen.name, "Rename"); + TEST_EQ(ret.type, WGT_IFACE_STRING); + TEST_STR_EQ(ret.s, UC_RENAMED); + TEST_TRUE(!(wgtGetProp(w, "NoSuch", &v))); + tPump(UC_SETTLE); + TEST_TRUE(ucPainted(w)); + wgtDestroy(w); + } + + if (win) { + dvxDestroyWindow(tCtx(), win); + } + + frtDestroy(&app); + TEST_PTR_EQ(wgtResolveTypeName("Gauge"), NULL); + tShutdown(); +} + + +// Form.DesignMode: the controls on the form see UserMode False. +static void caseUcDesignMode(void) { + tCase("UserControl UserMode on a form in design mode"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + const char *frm = + "Begin Form Form1\n" + " Begin Gauge Gauge1\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Form1.DesignMode = True\n" + " Print Gauge1.Info()\n" + " Form1.DesignMode = False\n" + " Print Gauge1.Info()\n" + "End Sub\n"; + FrtAppT app; + + if (!ucBuild(&app, frm)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), "Gauge1 50 False UserControl\nGauge1 50 True UserControl\n"); + frtDestroy(&app); + tShutdown(); +} + + +// Enum-typed properties: an Enum whose values are 0, 1, 2 ... is an ENUM +// property with its member names in value order (another stays a whole +// number); the .frm sets it by name (a number or an unknown name is a bad +// value, as for a widget's enumerated property); BASIC sets it by value +// or by name (the form's code comes before the class here, so it cannot +// use the members as constants); C reads and writes it by value and by +// name; the form written back names it and loads again; a control file +// built for release keeps the names. +static void caseUcEnumProps(void) { + tCase("UserControl Enum properties: ENUM names, .frm by name, BASIC and C access, release build"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + static const char *dirNames[] = { "North", "East", "South", "West" }; + static const char *frm = + "Begin Form Form1\n" + " Begin Arrow Arrow1\n" + " Direction = South\n" + " End\n" + " Begin Arrow Arrow2\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Print Arrow1.Direction; Arrow2.Direction; Arrow2.Span\n" + " Arrow2.Direction = 3\n" + " Print Arrow2.Direction;\n" + " Arrow2.Direction = \"north\"\n" + " Print Arrow2.Direction\n" + "End Sub\n"; + FrtAppT app; + + if (!ucBuildCtl(&app, UC_ENUM_CLASS, UC_ENUM_HEADER, frm)) { + tShutdown(); + return; + } + + // The interface: Direction ENUM with the names, Span a whole number + const WgtIfaceT *iface = wgtGetIface(basUcResolveType(app.rt, "Arrow")); + const WgtPropDescT *dir = wgtIfaceFindProp(iface, "Direction"); + const WgtPropDescT *span = wgtIfaceFindProp(iface, "Span"); + + TEST_TRUE(dir != NULL && span != NULL); + + if (!dir || !span) { + frtDestroy(&app); + tShutdown(); + return; + } + + TEST_EQ(dir->type, WGT_IFACE_ENUM); + TEST_TRUE(dir->getFn != NULL && dir->setFn != NULL && dir->enumNames != NULL); + + for (int32_t i = 0; dir->enumNames && i < UC_DIR_COUNT; i++) { + TEST_STR_EQ(dir->enumNames[i] ? dir->enumNames[i] : "", dirNames[i]); + } + + TEST_PTR_EQ(dir->enumNames ? dir->enumNames[UC_DIR_COUNT] : NULL, NULL); + TEST_EQ(span->type, WGT_IFACE_INT); + TEST_PTR_EQ(span->enumNames, NULL); + + // The .frm by name; BASIC by value and by name + frtStart(&app, NULL); + tPump(UC_SETTLE); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), "2 1 4 \n3 0 \n"); + + // C: by value and by name, the text form, an unknown name refused + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *ctrl = form ? frtCtrl(form, "Arrow1") : NULL; + WidgetT *w = ctrl ? ctrl->widget : NULL; + WgtValueT v; + char text[UC_TEXT_MAX]; + + TEST_TRUE(w != NULL); + + if (w) { + TEST_TRUE(wgtGetProp(w, "Direction", &v)); + TEST_EQ(v.type, WGT_IFACE_ENUM); + TEST_EQ(v.i, UC_DIR_SOUTH); + v = wgtValueInt(UC_DIR_WEST); + TEST_TRUE(wgtSetProp(w, "Direction", &v)); + TEST_TRUE(wgtIfaceGetPropText(iface, w, dir, text, (int32_t)sizeof(text))); + TEST_STR_EQ(text, "West"); + v = wgtValueStr("east"); + TEST_TRUE(wgtSetProp(w, "Direction", &v)); + v = wgtValueStr("Sideways"); + TEST_TRUE(!(wgtSetProp(w, "Direction", &v))); + TEST_TRUE(wgtGetProp(w, "Direction", &v)); + TEST_EQ(v.i, UC_DIR_EAST); + TEST_TRUE(wgtIfaceSetPropText(iface, w, dir, "South")); + TEST_TRUE(wgtGetProp(w, "Span", &v)); + TEST_EQ(v.type, WGT_IFACE_INT); + TEST_EQ(v.i, UC_SPAN_TINY); + } + + // The form written back names the values, and loads again + char saved[FRT_SRC_MAX]; + + TEST_TRUE(form && frtSerializeForm(form, saved, (int32_t)sizeof(saved)) > 0); + TEST_TRUE(strstr(saved, "Direction = South") != NULL); + TEST_TRUE(strstr(saved, "Direction = North") != NULL); + frtDestroy(&app); + + if (ucBuildCtl(&app, UC_ENUM_CLASS, UC_ENUM_HEADER, saved)) { + frtStart(&app, NULL); + form = frtForm(&app, "Form1"); + ctrl = form ? frtCtrl(form, "Arrow1") : NULL; + TEST_TRUE(!app.rt->terminated); + TEST_TRUE(ctrl && ctrl->widget && wgtGetProp(ctrl->widget, "Direction", &v) && v.i == UC_DIR_SOUTH); + frtDestroy(&app); + } + + // A number or an unknown name in the .frm is a bad value + static const char *badFrms[] = { + "Begin Form Form1\n Begin Arrow Arrow1\n Direction = 2\n End\nEnd\n", + "Begin Form Form1\n Begin Arrow Arrow1\n Direction = Sideways\n End\nEnd\n" + }; + + for (int32_t i = 0; i < DVX_ARRAY_LEN(badFrms); i++) { + if (ucBuildCtl(&app, UC_ENUM_CLASS, UC_ENUM_HEADER, badFrms[i])) { + frtStart(&app, NULL); + TEST_TRUE(app.rt->terminated); + frtClearError(&app); + frtDestroy(&app); + } + } + + // A control file built for release keeps the names; C uses them + static const char *fileSource = + "Public Enum CompassDirE\n" + " North\n" + " East\n" + " South\n" + " West\n" + "End Enum\n" + "BEGINCLASS \"Compass\" USERCONTROL\n" + "Public Heading AS CompassDirE\n" + "ENDCLASS\n"; + static const char *fileHeader = "Begin UserControl Compass\nEnd\n"; + BasCompileErrorT err; + BasModuleT *mod = basBuildCompile(fileSource, (int32_t)strlen(fileSource), false, NULL, NULL, &err); + char path[DVX_MAX_PATH]; + BasBuildSpecT spec; + const char *headers[] = { fileHeader }; + + snprintf(path, sizeof(path), "%s/%s", tTempDir(), UC_ENUM_FILE); + TEST_TRUE(mod != NULL); + memset(&spec, 0, sizeof(spec)); + spec.module = mod; + spec.projectDir = tTempDir(); + spec.ctlSources = headers; + spec.ctlCount = 1; + spec.release = true; + + const char *failure = mod ? basBuildControl(path, &spec) : NULL; + + TEST_PTR_EQ(failure, NULL); + basModuleFree(mod); + TEST_TRUE(basCtlLoadFile(path)); + + const WgtPropDescT *heading = wgtIfaceFindProp(wgtGetIface("Compass"), "Heading"); + + TEST_TRUE(heading && heading->type == WGT_IFACE_ENUM && heading->enumNames && strcmp(heading->enumNames[UC_DIR_WEST], dirNames[UC_DIR_WEST]) == 0); + + WindowT *win = dvxCreateWindow(tCtx(), "C host", 0, 0, UC_SCREEN_W / 2, UC_SCREEN_H / 2, true); + WidgetT *root = win ? wgtInitWindow(tCtx(), win) : NULL; + WidgetT *c = root ? wgtCreateByName("Compass", root) : NULL; + + TEST_TRUE(c != NULL); + + if (c) { + v = wgtValueStr("West"); + TEST_TRUE(wgtSetProp(c, "Heading", &v)); + TEST_TRUE(wgtGetProp(c, "Heading", &v)); + TEST_EQ(v.i, UC_DIR_WEST); + wgtDestroy(c); + } + + if (win) { + dvxDestroyWindow(tCtx(), win); + } + + tShutdown(); +} + + +// IconView (the parpm fixture's control, iconview.ctl, Program Manager's +// view in BASIC): the theme's colors; a click, the keys (arrows, Home, +// End, type-ahead, Enter, Del, F2); a double click; a right click on an +// item and on empty space; a click on empty space; a drag onto another +// item's cell that reorders; the wheel and the scroll bar; items in cells +// of their own, a drag to a free cell reported for the program to place +// and the arrow keys among them; the empty text. +static void caseUcIconView(void) { + tCase("IconView: Program Manager's view rules from BASIC"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + int32_t ctlLen = 0; + int32_t helpLen = 0; + char *ctl = platformReadFile(UC_ICONVIEW_CTL, &ctlLen); + char *help = platformReadFile(UC_LISTHELP_BAS, &helpLen); + + TEST_TRUE(ctl != NULL && help != NULL); + + if (!ctl || !help) { + free(ctl); + free(help); + tShutdown(); + return; + } + + // The program: listhelp.bas, the stand-in colors, the control's code, + // a form with one + int32_t codeOff = basClassCodePos(ctl, ctlLen); + size_t cap = (size_t)ctlLen + (size_t)helpLen + sizeof(sUcFakeColors) + UC_TEXT_MAX; + char *source = (char *)malloc(cap); + + snprintf(source, cap, "%s\n%s\nBEGINCLASS \"IconView\" USERCONTROL\n%s\nENDCLASS\n", help, sUcFakeColors, ctl + codeOff); + ctl[codeOff] = '\0'; + + const char *frm = + "Begin Form Form1\n" + " Caption = \"Icons\"\n" + " Begin IconView Icons\n" + " EmptyText = \"nothing here\"\n" + " End\n" + "End\n" + "Sub Form1_Load ()\n" + " Dim n As Variant\n" + " For Each n In Array(\"Alpha\", \"Beta\", \"Gamma\", \"Delta\", \"Epsilon\", \"Zeta\")\n" + " Icons.AddItem n, \"\", \"tag\" & n\n" + " Next n\n" + "End Sub\n" + "Sub Form1_More ()\n" + " Dim i As Integer\n" + " For i = 1 To 40\n" + " Icons.AddItem \"Item\" & i, \"\", \"\"\n" + " Next i\n" + "End Sub\n" + "Sub Form1_Show ()\n" + " Print Icons.ItemCount; Icons.SelectedIndex; Icons.ItemName(1); \" \"; Icons.ItemTag(1); \" \"; Icons.TopOffset\n" + "End Sub\n" + "Sub Form1_Small ()\n" + " Icons.View = 1\n" + "End Sub\n" + "Sub Form1_Large ()\n" + " Icons.View = 0\n" + "End Sub\n" + "Sub Form1_Manual ()\n" + " Icons.Clear\n" + " Icons.AddItem \"One\", \"\", \"\"\n" + " Icons.AddItem \"Two\", \"\", \"\"\n" + " Icons.AddItem \"Three\", \"\", \"\"\n" + " Icons.AutoArrange = False\n" + " Icons.SetItemCell 0, 0, 0\n" + " Icons.SetItemCell 1, 1, 0\n" + " Icons.SetItemCell 2, 0, 1\n" + "End Sub\n" + "Sub Form1_Cells ()\n" + " Print Icons.ItemCol(0); Icons.ItemRow(0); Icons.AutoArrange\n" + "End Sub\n" + "Sub Form1_Empty ()\n" + " Icons.Clear\n" + " Print Icons.ItemCount; Icons.EmptyText\n" + "End Sub\n" + "Sub Icons_SelChange (ByVal Index As Integer)\n" + " Print \"sel\"; Index\n" + "End Sub\n" + "Sub Icons_ItemOpen (ByVal Index As Integer)\n" + " Print \"open\"; Index\n" + "End Sub\n" + "Sub Icons_ItemDelete (ByVal Index As Integer)\n" + " Print \"del\"; Index\n" + "End Sub\n" + "Sub Icons_ItemRename (ByVal Index As Integer)\n" + " Print \"ren\"; Index\n" + "End Sub\n" + "Sub Icons_ItemContext (ByVal Index As Integer, ByVal X As Integer, ByVal Y As Integer)\n" + " Print \"ctx\"; Index\n" + "End Sub\n" + "Sub Icons_ItemMoved (ByVal FromIndex As Integer, ByVal ToIndex As Integer)\n" + " Print \"moved\"; FromIndex; ToIndex\n" + "End Sub\n" + "Sub Icons_ItemDropped (ByVal Index As Integer, ByVal Col As Integer, ByVal Row As Integer)\n" + " Print \"dropped\"; Index; Col; Row\n" + " Icons.SetItemCell Index, Col, Row\n" + "End Sub\n" + "Sub Icons_ColumnsChange (ByVal Columns As Integer)\n" + " Print \"cols\"; Columns\n" + "End Sub\n"; + const char *frms[] = { frm }; + FrtAppT app; + bool built = frtBuild(&app, source, frms, 1); + + free(source); + free(help); + + if (!built) { + printf(" compile error: %s\n", app.error); + } + + TEST_TRUE(built); + + if (!built) { + free(ctl); + tShutdown(); + return; + } + + TEST_TRUE(basFormRtRegisterCtl(app.rt, ctl, codeOff)); + free(ctl); + + // listhelp.bas's library is called for real + basNativeHostSetLive(true); + frtStart(&app, NULL); + tPump(UC_SETTLE); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *ctrl = form ? frtCtrl(form, "Icons") : NULL; + WidgetT *w = ctrl ? ctrl->widget : NULL; + int32_t sx = 0; + int32_t sy = 0; + int32_t ex = 0; + int32_t ey = 0; + char expect[UC_TEXT_MAX]; + ListIconGridT g; + + TEST_TRUE(w != NULL); + + if (!w) { + frtDestroy(&app); + basNativeHostSetLive(false); + tShutdown(); + return; + } + + ucIconGrid(w, ListIconLargeE, UC_ICON_ITEMS, LIST_ICON_BY_ORDER, &g); + + // A click selects (and focuses); keys move the selection, type ahead, + // open, ask to delete and to rename + frtOutClear(&app); + ucIndexPoint(w, &g, 1, &sx, &sy); + tClick(sx, sy); + tPump(1); + tKey(KEY_SCAN_RIGHT, 0, 0); + tKey(KEY_SCAN_END, 0, 0); + tKey(KEY_SCAN_HOME, 0, 0); + tKey(0, 'd', 0); + tKeyEnter(); + tKey(KEY_SCAN_DELETE, 0, 0); + tKey(KEY_SCAN_F2, 0, 0); + tPump(1); + TEST_STR_EQ(frtOut(&app), "sel1 \nsel2 \nsel5 \nsel0 \nsel3 \nopen3 \ndel3 \nren3 \n"); + + // The theme's colors: the content area and the selected name's box + // ("Delta", one line under its picture) + WidgetT *canvas = w->firstChild; + const DisplayT *disp = &tCtx()->display; + int32_t nameW = (int32_t)strlen("Delta") * tCtx()->font.charWidth; + int32_t boxX = g.innerX + (UC_DELTA % g.cols) * g.cell.cellW + (g.cell.cellW - nameW) / 2; + int32_t boxY = g.innerY + (UC_DELTA / g.cols) * g.cell.cellH + g.cell.textY; + + TEST_EQ(wgtCanvasGetPixel(canvas, g.innerX + g.innerW - UC_PIXEL_IN, g.innerY + UC_PIXEL_IN), packColor(disp, UC_THEME_BACK, UC_THEME_BACK, UC_THEME_BACK)); + TEST_EQ(wgtCanvasGetPixel(canvas, boxX + UC_PIXEL_IN, boxY + UC_PIXEL_IN), packColor(disp, UC_THEME_SEL, UC_THEME_SEL, UC_THEME_SEL)); + + // A double click opens; a right click selects and asks for the item's + // menu, over empty space for the view's; a click on empty space + // clears the selection + frtOutClear(&app); + ucIndexPoint(w, &g, UC_GAMMA, &sx, &sy); + tClick(sx, sy); + tAdvanceClock(UC_DBLCLICK_MS); + tClick(sx, sy); + tPump(1); + ucIndexPoint(w, &g, UC_DELTA, &sx, &sy); + tMouseDown(sx, sy, MOUSE_RIGHT); + tMouseUp(sx, sy, MOUSE_RIGHT); + tPump(1); + ucIndexPoint(w, &g, UC_ICON_ITEMS, &ex, &ey); + tMouseDown(ex, ey, MOUSE_RIGHT); + tMouseUp(ex, ey, MOUSE_RIGHT); + tPump(1); + tAdvanceClock(UC_DBLCLICK_MS); + tClick(ex, ey); + tPump(1); + TEST_STR_EQ(frtOut(&app), "sel2 \nopen2 \nsel3 \nctx3 \nctx-1 \nsel-1 \n"); + + // Alpha dragged onto Gamma's cell takes its place (and stays selected) + frtOutClear(&app); + ucIndexPoint(w, &g, 0, &sx, &sy); + ucIndexPoint(w, &g, UC_GAMMA, &ex, &ey); + tDrag(sx, sy, ex, ey); + tPump(1); + TEST_STR_EQ(frtOut(&app), "sel0 \nmoved0 2 \nsel2 \n"); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Show")); + TEST_STR_EQ(frtOut(&app), "6 2 Gamma tagGamma 0 \n"); + + // Forty items added at once paint once (UserControl.Invalidate): one + // ColumnsChange + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "More")); + tPump(UC_SETTLE); + ucIconGrid(w, ListIconLargeE, UC_ICON_ITEMS + UC_ICON_MORE, LIST_ICON_BY_ORDER, &g); + snprintf(expect, sizeof(expect), "cols%d \n", g.cols); + TEST_STR_EQ(frtOut(&app), expect); + TEST_TRUE(g.needSb); + + // The row under the last whole row shows as far as it fits (the first + // picture of the third row), the frame drawn over it (its bottom edge + // the colour of its right edge) + int32_t partX = g.innerX + g.cell.pictureX; + int32_t partY = g.innerY + g.rowsVisible * g.cell.cellH + g.cell.pictureY; + + TEST_TRUE(g.rowsVisible * g.cell.cellH + g.cell.pictureY < g.innerH); + TEST_TRUE(wgtCanvasGetPixel(canvas, partX, partY) != wgtCanvasGetPixel(canvas, g.innerX + g.innerW - UC_PIXEL_IN, g.innerY + UC_PIXEL_IN)); + TEST_EQ(wgtCanvasGetPixel(canvas, partX, w->h - 1), wgtCanvasGetPixel(canvas, w->w - 1, w->h / 2)); + + // The wheel scrolls a long list a row a notch, the scroll bar's down + // arrow another row + tWidgetCenter(w, &sx, &sy); + tMouseMove(sx, sy); + tWheel(LIST_ICON_WHEEL_ROWS); + tPump(1); + ucViewPoint(w, g.innerX + g.innerW + LIST_ICON_SB_W / 2, g.innerY + g.innerH - UC_PIXEL_IN, &sx, &sy); + tClick(sx, sy); + tPump(1); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Show")); + snprintf(expect, sizeof(expect), "%d 2 Gamma tagGamma %d \n", UC_ICON_ITEMS + UC_ICON_MORE, (LIST_ICON_WHEEL_ROWS + LIST_ICON_LINE_ROWS) * g.cell.cellH); + TEST_STR_EQ(frtOut(&app), expect); + + // The thumb dragged far below the control (the mouse stays the + // control's while the button is down) scrolls to the last rows + int32_t thumbPos = 0; + int32_t thumbSize = 0; + + widgetScrollbarThumb(g.innerH - LIST_ICON_SB_W * 2, g.totalRows, g.rowsVisible, LIST_ICON_WHEEL_ROWS + LIST_ICON_LINE_ROWS, &thumbPos, &thumbSize); + ucViewPoint(w, g.innerX + g.innerW + LIST_ICON_SB_W / 2, g.innerY + LIST_ICON_SB_W + thumbPos + thumbSize / 2, &sx, &sy); + tDrag(sx, sy, sx, sy + w->h * 2); + tPump(1); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Show")); + snprintf(expect, sizeof(expect), "%d 2 Gamma tagGamma %d \n", UC_ICON_ITEMS + UC_ICON_MORE, g.maxScroll * g.cell.cellH); + TEST_STR_EQ(frtOut(&app), expect); + + // A new view tells the program its columns once + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Small")); + tPump(1); + ucIconGrid(w, ListIconSmallE, UC_ICON_ITEMS + UC_ICON_MORE, LIST_ICON_BY_ORDER, &g); + snprintf(expect, sizeof(expect), "cols%d \n", g.cols); + TEST_STR_EQ(frtOut(&app), expect); + TEST_TRUE(frtFire(&app, "Form1", "Large")); + tPump(1); + + // Items in cells of their own: One dragged to the free cell (1, 1) is + // reported and placed there by the program; Up from it goes to Two + // above it, Left from Two finds nothing, Down goes back to One + TEST_TRUE(frtFire(&app, "Form1", "Manual")); + tPump(UC_SETTLE); + ucIconGrid(w, ListIconLargeE, UC_PLACED_ITEMS, UC_PLACED_ROWS, &g); + frtOutClear(&app); + ucItemPoint(w, &g, 0, 0, &sx, &sy); + ucItemPoint(w, &g, 1, 1, &ex, &ey); + tDrag(sx, sy, ex, ey); + tPump(1); + tKey(KEY_SCAN_UP, 0, 0); + tKey(KEY_SCAN_LEFT, 0, 0); + tKey(KEY_SCAN_DOWN, 0, 0); + tPump(1); + TEST_TRUE(frtFire(&app, "Form1", "Cells")); + TEST_STR_EQ(frtOut(&app), "sel0 \ndropped0 1 1 \nsel1 \nsel0 \n1 1 False \n"); + + // No items: the empty text (the items replaced: the columns again, + // when it paints) + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "Empty")); + tPump(UC_SETTLE); + ucIconGrid(w, ListIconLargeE, 0, 0, &g); + snprintf(expect, sizeof(expect), "0 nothing here\ncols%d \n", g.cols); + TEST_STR_EQ(frtOut(&app), expect); + frtDestroy(&app); + basNativeHostSetLive(false); + tShutdown(); +} + + +// A control project built into a control file and installed: its +// controls work in a program's .frm and from C, its module code ran once +// and its VM is shared. +static void caseUcInstalled(void) { + tCase("installed control file (.wgt): build, load, use from BASIC and C"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + static const char *ctlSource = + "DIM SHARED madeAt AS LONG\n" + "madeAt = 7\n" + "BEGINCLASS \"Dial\" USERCONTROL\n" + "Public Event Turned(ByVal Steps AS LONG)\n" + "Private mValue AS LONG\n" + "Public Property Get Value() AS LONG\n" + " Value = mValue\n" + "End Property\n" + "Public Sub Turn(n AS LONG)\n" + " mValue = mValue + n\n" + " RaiseEvent Turned(mValue)\n" + "End Sub\n" + "Public Function Made() AS LONG\n" + " Made = madeAt\n" + "End Function\n" + "ENDCLASS\n"; + static const char *ctlHeader = + "Begin UserControl Dial\n" + " Width = 20\n" + " Height = 20\n" + " Description = \"A dial\"\n" + "End\n"; + + BasCompileErrorT err; + BasModuleT *mod = basBuildCompile(ctlSource, (int32_t)strlen(ctlSource), false, NULL, NULL, &err); + char path[DVX_MAX_PATH]; + + snprintf(path, sizeof(path), "%s/%s", tTempDir(), UC_FILE_NAME); + TEST_TRUE(mod != NULL); + + if (!mod) { + printf(" compile error: %s\n", err.message); + tShutdown(); + return; + } + + BasBuildSpecT spec; + const char *headers[] = { ctlHeader }; + + memset(&spec, 0, sizeof(spec)); + spec.module = mod; + spec.projectDir = tTempDir(); + spec.ctlSources = headers; + spec.ctlCount = 1; + spec.release = true; + + const char *failure = basBuildControl(path, &spec); + + basModuleFree(mod); + TEST_PTR_EQ(failure, NULL); + + // The file: the marker, then resources + DvxResHandleT *res = dvxResOpen(path); + uint32_t size = 0; + char *name = res ? (char *)dvxResRead(res, BAS_CTL_RES_NAME, &size) : NULL; + FILE *f = fopen(path, "rb"); + char head[DVX_RES_ONLY_MAGIC_LEN]; + + TEST_TRUE(f && fread(head, 1, sizeof(head), f) == sizeof(head) && memcmp(head, DVX_RES_ONLY_MAGIC, sizeof(head)) == 0); + TEST_STR_EQ(name ? name : "", "A dial"); + + if (f) { + fclose(f); + } + + free(name); + + if (res) { + dvxResClose(res); + } + + TEST_TRUE(basCtlLoadFile(path)); + TEST_TRUE(wgtGetIface("Dial") != NULL); + TEST_TRUE(!(basCtlLoadFile(tTempDir()))); + + // From BASIC: a program's form + const char *frm = + "Begin Form Form1\n" + " Begin Dial Dial1\n" + " End\n" + "End\n" + "Sub Form1_T ()\n" + " Dial1.Turn 2\n" + " Dial1.Turn 3\n" + " Print Dial1.Value; Dial1.Made()\n" + "End Sub\n" + "Sub Dial1_Turned (ByVal Steps As Long)\n" + " Print \"turned\"; Steps\n" + "End Sub\n"; + FrtAppT app; + + if (!ucBuildPlain(&app, frm)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + tPump(UC_SETTLE); + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), "turned2 \nturned5 \n5 7 \n"); + frtDestroy(&app); + + // From C: the same VM + WindowT *win = dvxCreateWindow(tCtx(), "C host", 0, 0, UC_SCREEN_W / 2, UC_SCREEN_H / 2, true); + WidgetT *root = win ? wgtInitWindow(tCtx(), win) : NULL; + WidgetT *w = root ? wgtCreateByName("Dial", root) : NULL; + + TEST_TRUE(w != NULL); + + if (w) { + WgtValueT v = wgtValueInt(4); + WgtValueT ret; + + memset(&sSeen, 0, sizeof(sSeen)); + w->onEvent = ucOnEvent; + TEST_TRUE(wgtCallMethod(w, "Turn", &v, 1, NULL)); + TEST_STR_EQ(sSeen.name, "Turned"); + TEST_EQ(sSeen.first, 4); + TEST_TRUE(wgtGetProp(w, "Value", &ret)); + TEST_EQ(ret.i, 4); + wgtDestroy(w); + } + + if (win) { + dvxDestroyWindow(tCtx(), win); + } + + tShutdown(); +} + + +// The program's own control from its .frm. +static void caseUcProgram(void) { + tCase("UserControl of a program: .frm, events, methods, mouse and keys"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + FrtAppT app; + + if (!ucBuild(&app, sForm)) { + tShutdown(); + return; + } + + frtStart(&app, NULL); + tPump(UC_SETTLE); + + BasFormT *form = frtForm(&app, "Form1"); + BasControlT *ctrl = form ? frtCtrl(form, "Gauge1") : NULL; + WidgetT *w = ctrl ? ctrl->widget : NULL; + + TEST_TRUE(w != NULL); + + if (!w) { + frtDestroy(&app); + tShutdown(); + return; + } + + TEST_TRUE(ucPainted(w)); + TEST_EQ(w->calcMinW, 50); + + // Count: Resize (100) and Paint (1) once each so far + TEST_TRUE(frtFire(&app, "Form1", "T")); + TEST_STR_EQ(frtOut(&app), "fromfrm True 101 \nitemopen3 \nopened\nhostclick\nGauge1 50 True UserControl\nold!\n"); + + // A click: MouseDown then Click in the control; the host's Click + // only when the control raises it (it does not here) + int32_t sx = 0; + int32_t sy = 0; + + tWidgetCenter(w, &sx, &sy); + frtOutClear(&app); + tClick(sx, sy); + tPump(1); + TEST_STR_EQ(frtOut(&app), ""); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "C")); + TEST_STR_EQ(frtOut(&app), "down1 click\n"); + + // The click gave it the focus: keys reach it + TEST_TRUE(wgtGetFocused() == w); + tKey(0, UC_KEY_A, 0); + tPump(1); + frtOutClear(&app); + TEST_TRUE(frtFire(&app, "Form1", "C")); + TEST_STR_EQ(frtOut(&app), "key97\n"); + + frtDestroy(&app); + + // The program's classes left the registry with it + TEST_PTR_EQ(wgtResolveTypeName("Gauge"), NULL); + tShutdown(); +} + + +// A header without a class of its name, and a name already taken. +static void caseUcRegisterErrors(void) { + tCase("UserControl registration errors"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + FrtAppT app; + + if (!ucBuild(&app, NULL)) { + tShutdown(); + return; + } + + static const char *noClass = "Begin UserControl Nope\nEnd\n"; + + TEST_TRUE(!(basFormRtRegisterCtl(app.rt, noClass, (int32_t)strlen(noClass)))); + frtClearError(&app); + TEST_TRUE(!(basFormRtRegisterCtl(app.rt, UC_HEADER, (int32_t)strlen(UC_HEADER)))); + frtClearError(&app); + TEST_TRUE(!(basFormRtRegisterCtl(app.rt, "", 0))); + frtClearError(&app); + TEST_TRUE(wgtGetIface("canvas") != NULL); + frtDestroy(&app); + tShutdown(); +} + + +// Two programs (or two copies of one) with a control of the same name: +// each has its own, registered under a key of its own; the name refers +// to the program's own control in each. +static void caseUcTwoPrograms(void) { + tCase("UserControl of the same name in two programs"); + tInit(UC_SCREEN_W, UC_SCREEN_H, UC_SCREEN_BPP); + + FrtAppT first; + FrtAppT second; + + if (!ucBuild(&first, NULL)) { + tShutdown(); + return; + } + + if (!ucBuild(&second, NULL)) { + frtDestroy(&first); + tShutdown(); + return; + } + + const char *firstKey = basUcResolveType(first.rt, "Gauge"); + const char *secondKey = basUcResolveType(second.rt, "Gauge"); + + TEST_TRUE(firstKey != NULL && secondKey != NULL); + TEST_TRUE(firstKey && secondKey && strcmp(firstKey, secondKey) != 0); + TEST_TRUE(wgtGetIface(firstKey) != NULL && wgtGetIface(firstKey) != wgtGetIface(secondKey)); + TEST_STR_EQ(wgtGetIface(secondKey) ? wgtGetIface(secondKey)->basName : "", "Gauge"); + frtDestroy(&first); + TEST_TRUE(basUcResolveType(second.rt, "Gauge") != NULL); + TEST_TRUE(wgtResolveTypeName("Gauge") != NULL); + frtDestroy(&second); + TEST_PTR_EQ(wgtResolveTypeName("Gauge"), NULL); + tShutdown(); +} + + +void frtUserCtlCases(void) { + caseUcProgram(); + caseUcCHost(); + caseUcDesignMode(); + caseUcEnumProps(); + caseUcRegisterErrors(); + caseUcTwoPrograms(); + caseUcIconView(); + caseUcInstalled(); +} + + +// The gauge program (with frm as its form when given), the control +// registered from its header. +static bool ucBuild(FrtAppT *app, const char *frm) { + return ucBuildCtl(app, UC_CLASS, UC_HEADER, frm); +} + + +// A program of control class cls (with frm as its form when given), the +// control registered from its header. +static bool ucBuildCtl(FrtAppT *app, const char *cls, const char *header, const char *frm) { + const char *frms[] = { frm }; + bool ok = frtBuild(app, cls, frms, frm ? 1 : 0); + + if (!ok) { + printf(" compile error: %s\n", app->error); + } + + TEST_TRUE(ok); + + if (ok) { + TEST_TRUE(basFormRtRegisterCtl(app->rt, header, (int32_t)strlen(header))); + } + + return ok; +} + + +// A program of frm alone (installed controls only). +static bool ucBuildPlain(FrtAppT *app, const char *frm) { + const char *frms[] = { frm }; + bool ok = frtBuild(app, "", frms, frm ? 1 : 0); + + if (!ok) { + printf(" compile error: %s\n", app->error); + } + + TEST_TRUE(ok); + return ok; +} + + +// IconView w's grid in a view holding count items (rowsTaken: +// listIconGrid's), as the control figures it with the font's characters. +static void ucIconGrid(const WidgetT *w, ListIconViewE view, int32_t count, int32_t rowsTaken, ListIconGridT *g) { + const BitmapFontT *font = &tCtx()->font; + + listIconGrid(view, font->charWidth, font->charHeight, w->w, w->h, count, rowsTaken, g); +} + + +// The screen point on the picture of item index of IconView w, its items +// in order in grid g (scrolled to the top). +static void ucIndexPoint(const WidgetT *w, const ListIconGridT *g, int32_t index, int32_t *sx, int32_t *sy) { + ucItemPoint(w, g, index % g->cols, index / g->cols, sx, sy); +} + + +// The screen point on the picture of IconView w's cell col, row of grid g +// (scrolled to the top), where an item is hit. +static void ucItemPoint(const WidgetT *w, const ListIconGridT *g, int32_t col, int32_t row, int32_t *sx, int32_t *sy) { + const ListIconCellT *cell = &g->cell; + + ucViewPoint(w, g->innerX + col * cell->cellW + cell->pictureX + cell->pictureSize / 2, g->innerY + row * cell->cellH + cell->pictureY + cell->pictureSize / 2, sx, sy); +} + + +static void ucOnEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + (void)w; + snprintf(sSeen.name, sizeof(sSeen.name), "%s", name); + sSeen.argc = argc; + sSeen.first = (argc > 0) ? wgtValueToInt(&args[0]) : 0; + + // ItemOpen's Accept and Rename's Text (ByRef) come back to the control + if (argc > 1 && args[1].type == WGT_IFACE_BOOL) { + args[1] = wgtValueBool(true); + } + + if (strcmp(name, "Rename") == 0 && argc > 0) { + args[0] = wgtValueStr(UC_RENAMED); + } +} + + +// True when the control's Paint drew (its corner differs from BackColor). +static bool ucPainted(WidgetT *w) { + WidgetT *canvas = w ? w->firstChild : NULL; + + return canvas && wgtCanvasGetPixel(canvas, 0, 0) != wgtCanvasGetPixel(canvas, UC_PAINT_SPOT, UC_PAINT_SPOT); +} + + +// The screen point x, y pixels into IconView w. +static void ucViewPoint(const WidgetT *w, int32_t x, int32_t y, int32_t *sx, int32_t *sy) { + int32_t cx = 0; + int32_t cy = 0; + + tWidgetCenter(w, &cx, &cy); + *sx = cx - w->w / 2 + x; + *sy = cy - w->h / 2 + y; +} diff --git a/src/test/fuzz/Makefile b/src/test/fuzz/Makefile new file mode 100644 index 00000000..eff7c6a3 --- /dev/null +++ b/src/test/fuzz/Makefile @@ -0,0 +1,116 @@ +# 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. + +# src/test/fuzz -- libFuzzer harnesses for DVX's file and wire parsers +# +# make -C src/test/fuzz build every fuzzer +# make -C src/test/fuzz run run each for FUZZ_SECONDS +# make -C src/test/fuzz run FUZZ_SECONDS=600 FUZZERS="fuzzPrefs fuzzRes" +# make -C src/test/fuzz clean remove obj/fuzz +# +# Harnesses (each fuzz.c's header says what it checks): +# fuzzBasCompile BASIC compiler front end (source -> module -> run) +# fuzzBasModule BASIC module deserializer + VM (runtime/serialize.c) +# fuzzFrmModel .frm form model parser / writer (formrt/frmModel.c) +# fuzzHlpc help compiler (.dhs -> .hlp, src/tools/dvxhlpc.c) +# fuzzHlpFile compiled help file reader (libs/kpunch/hlpfile) +# fuzzImage libdvx image loaders (stb_image + pixel conversion) +# fuzzPacket serial packet layer (HDLC deframer, CRC, Go-Back-N) +# fuzzPrefs INI / preferences reader and writer (dvxPrefs.c) +# fuzzRes DVX resource container reader and writer +# +# Everything is built with clang -fsanitize=fuzzer,address,undefined +# into obj/fuzz (objects under obj/fuzz/, binaries in +# obj/fuzz/bin) from the product sources in place; the source lists come +# from the product Makefiles through ../lint/clangHost.mk. Not part of +# make test: run it deliberately (a bounded run, FUZZ_SECONDS each). +# +# run (runFuzz.sh) gives each fuzzer its seeds -- /seeds in this +# directory plus seeds generated from tree files (sample programs, .frm, +# INI, help sources, images, the sample goldens' modules) -- a corpus +# and a work directory under obj/fuzz, runs FUZZ_JOBS fuzzers at a time +# (niced), saves crashes under obj/fuzz/crashes// and prints the +# command that reproduces each one. It exits 1 when anything crashed. +# A UBSan or LeakSanitizer report that every input hits goes into +# ubsan.supp / lsan.supp here, with its reason, until it is fixed. +# +# Needs clang with libFuzzer (clang 18 here); the generated seeds use +# bin/host/dvxres and bin/host/dvxhlpc (make tools) when present. + +CH_VARIANT := fuzz +CH_INSTR := -fsanitize=fuzzer-no-link,address,undefined +CH_LINKFLAGS := -fsanitize=fuzzer,address,undefined +include ../lint/clangHost.mk + +# clangHost.mk's dependency files and per-object rules define targets +# first. +.DEFAULT_GOAL := all + +FUZZ_DIR := $(CH_ROOT)/src/test/fuzz +FUZZ_SECONDS ?= 60 +FUZZ_JOBS ?= 3 + +FUZZERS ?= fuzzBasCompile fuzzBasModule fuzzFrmModel fuzzHlpc fuzzHlpFile fuzzImage fuzzPacket fuzzPrefs fuzzRes + +# Product sources, from the product Makefiles' lists (clangHost.mk) or, +# for single files no list names, by path. The form model needs the +# whole host form runtime (control interfaces, form properties). +FZ_BAS := $(CH_BAS_COMP) $(CH_BAS_VMLINK) $(CH_BAS_SUPPORT) +FZ_PREFS := $(filter %/dvxPrefs.c,$(CH_BAS_VMLINK)) $(CH_BAS_SUPPORT) +FZ_RES := $(filter-out %/dvxres.c,$(CH_TOOL_RES)) +FZ_HLPFILE := $(CH_ROOT)/src/libs/kpunch/hlpfile/hlpFile.c +FZ_PACKET := $(CH_ROOT)/src/libs/kpunch/serial/packet/packet.c +FZ_LOOPSHIM := $(CH_ROOT)/src/test/serial/loopShim.c +$(foreach v,FZ_PREFS FZ_RES,$(call chRequire,$v)) + +# Harness-side sources +FZ_COMMON := $(CH_ROOT)/src/test/basic/fuzzCommon.c +FZ_UTIL := $(FUZZ_DIR)/fuzzUtil.c +FZ_HLPWALK := $(FUZZ_DIR)/fuzzHlpWalk.c + +CH_SRCS_fuzzBasCompile := $(FUZZ_DIR)/fuzzBasCompile.c $(FZ_UTIL) $(FZ_COMMON) $(FZ_BAS) +CH_SRCS_fuzzBasModule := $(FUZZ_DIR)/fuzzBasModule.c $(FZ_COMMON) $(FZ_BAS) +CH_SRCS_fuzzFrmModel := $(FUZZ_DIR)/fuzzFrmModel.c $(FZ_UTIL) $(CH_BAS_RT) $(CH_LIBDVX) +CH_SRCS_fuzzHlpc := $(FUZZ_DIR)/fuzzHlpc.c $(FZ_HLPWALK) $(FZ_UTIL) $(FZ_HLPFILE) $(CH_TOOL_HLPC) +CH_SRCS_fuzzHlpFile := $(FUZZ_DIR)/fuzzHlpFile.c $(FZ_HLPWALK) $(FZ_UTIL) $(FZ_HLPFILE) $(CH_BAS_SUPPORT) +CH_SRCS_fuzzImage := $(FUZZ_DIR)/fuzzImage.c $(FZ_UTIL) $(CH_LIBDVX) +CH_SRCS_fuzzPacket := $(FUZZ_DIR)/fuzzPacket.c $(FZ_PACKET) $(FZ_LOOPSHIM) +CH_SRCS_fuzzPrefs := $(FUZZ_DIR)/fuzzPrefs.c $(FZ_UTIL) $(FZ_PREFS) +CH_SRCS_fuzzRes := $(FUZZ_DIR)/fuzzRes.c $(FZ_UTIL) $(FZ_RES) + +# Per-object extras: the help compiler in library mode; packet.c on the +# serial suite's simulated clock (as src/test/serial builds it). +$(call chObjs,$(filter %/dvxhlpc.c,$(CH_TOOL_HLPC))): CH_EXTRA = -DHLPC_NO_MAIN +$(call chObjs,$(FZ_PACKET)): CH_EXTRA = -include $(CH_ROOT)/src/test/serial/loopShim.h +$(call chObjs,$(FUZZ_DIR)/fuzzPacket.c): CH_EXTRA = -I$(dir $(FZ_PACKET)) -I$(dir $(FZ_LOOPSHIM)) + +.PHONY: all run clean + +all: $(addprefix $(CH_BIN)/,$(FUZZERS)) + +run: all + $(FUZZ_DIR)/runFuzz.sh $(CH_BIN) $(FUZZ_SECONDS) $(FUZZ_JOBS) $(FUZZERS) + +$(foreach f,$(FUZZERS),$(eval $(call CH_PROG_RULE,$f))) + +clean: + rm -rf $(CH_OBJ) diff --git a/src/test/fuzz/basCompile/seeds/edges.bas b/src/test/fuzz/basCompile/seeds/edges.bas new file mode 100644 index 00000000..ed400fe9 --- /dev/null +++ b/src/test/fuzz/basCompile/seeds/edges.bas @@ -0,0 +1,44 @@ +' fuzz seed: many statement kinds in a few lines +OPTION EXPLICIT +TYPE PointT + x AS INTEGER + y AS INTEGER +END TYPE +CONST LIMIT = 5 +DIM p AS PointT +DIM a(1 TO LIMIT) AS LONG +DIM s AS STRING +DIM i AS INTEGER +p.x = 3 +p.y = -4 +FOR i = 1 TO LIMIT + a(i) = i * i +NEXT i +SELECT CASE p.x + CASE 1 TO 2 + s = "low" + CASE IS > 2 + s = "high" + CASE ELSE + s = "none" +END SELECT +DO WHILE i > 0 + i = i - 1 + IF i MOD 2 = 0 THEN + s = s + CHR$(65 + i) + ELSE + s = LEFT$(s, 3) + END IF +LOOP +PRINT Twice(p.y); s; UCASE$(MID$("abcdef", 2, 3)); a(LIMIT) +ON ERROR GOTO handler +PRINT 1 / 0 +PRINT "after" +END +handler: +PRINT "err"; ERR +RESUME NEXT + +FUNCTION Twice(n AS INTEGER) AS INTEGER + Twice = n * 2 +END FUNCTION diff --git a/src/test/fuzz/frmModel/seeds/edges.frm b/src/test/fuzz/frmModel/seeds/edges.frm new file mode 100644 index 00000000..e832ec9a --- /dev/null +++ b/src/test/fuzz/frmModel/seeds/edges.frm @@ -0,0 +1,38 @@ +VERSION DVX 1.00 + +Begin Form EdgeForm + Caption = "Edge ""quoted"" &Caption" + Layout = HBox + AutoSize = False + Resizable = True + Width = 320 + Height = 200 + + Begin Frame fraOuter + Caption = "Outer" + Weight = 1 + + Begin OptionButton optA + Caption = "&A" + Value = True + End + + Begin OptionButton optB + Caption = "&B" + Value = True + End + End + + Begin TextBox txtName + Text = "" + Weight = 2 + End + + Begin Unknown thing1 + Mystery = 42 + End +End + +SUB optA_Click + txtName.Text = "a" +END SUB diff --git a/src/test/fuzz/fuzzBasCompile.c b/src/test/fuzz/fuzzBasCompile.c new file mode 100644 index 00000000..ae371821 --- /dev/null +++ b/src/test/fuzz/fuzzBasCompile.c @@ -0,0 +1,96 @@ +// 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. + + +// fuzzBasCompile.c -- libFuzzer harness: BASIC compiler front end +// +// Input: BASIC source text. Compiling must succeed or fail cleanly. A +// module that compiles must serialize and deserialize again (a module +// the compiler built that the loader rejects is a finding), and, unless +// it uses SHELL or SLEEP, run headless to a halt, a runtime error or the +// step cap. Seeds: src/test/fuzz/basCompile/seeds, every program +// src/test/basic/extractCorpus.py pulls out of the C harnesses and the +// basrun fixtures (runFuzz.sh). + +#include "../basic/fuzzCommon.h" +#include "fuzzUtil.h" + +#include "compiler/compiler.h" +#include "runtime/serialize.h" + +#include +#include + +#define FUZZ_COMPILE_STEP_CAP 20000 + +// Function prototypes (alphabetical) +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +static void roundTrip(const BasModuleT *mod); + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size > INT32_MAX) { + return 0; + } + + char *src = fuzzUtilCString(data, size); + BasCompilerT *compiler = basCompilerCreate(src, (int32_t)size); + + if (compiler && basCompile(compiler)) { + BasModuleT *mod = basCompilerBuildModule(compiler); + + if (mod) { + roundTrip(mod); + + if (fuzzSourceIsSafe(src)) { + fuzzRunModule(mod, FUZZ_COMPILE_STEP_CAP, NULL); + } + + basModuleFree(mod); + } + } + + basCompilerDestroy(compiler); + free(src); + return 0; +} + + +// The serialized form of a freshly compiled module must load again. +static void roundTrip(const BasModuleT *mod) { + int32_t len = 0; + uint8_t *blob = basModuleSerialize(mod, &len); + + if (!blob) { + return; + } + + BasModuleT *back = basModuleDeserialize(blob, len); + + if (!back) { + fprintf(stderr, "fuzzBasCompile: basModuleDeserialize rejected a module the compiler built (%ld bytes)\n", (long)len); + abort(); + } + + basModuleFree(back); + free(blob); +} diff --git a/src/test/fuzz/fuzzBasModule.c b/src/test/fuzz/fuzzBasModule.c new file mode 100644 index 00000000..4f92373d --- /dev/null +++ b/src/test/fuzz/fuzzBasModule.c @@ -0,0 +1,82 @@ +// 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. + + +// fuzzBasModule.c -- libFuzzer harness: BASIC module deserializer + VM +// +// Input: a serialized module, optionally followed by a debug blob and a +// 4-byte little-endian trailer giving the debug blob's length: +// module bytes | debug bytes | uint32 debugLen +// (an input whose trailer does not fit is all module). The module must +// be rejected by basModuleDeserialize or load, take its debug info and +// run to a halt, a runtime error or the step cap -- never crash, trip a +// sanitizer or leak. The run uses the BASIC fuzzers' headless VM loop +// (src/test/basic/fuzzCommon.c, which also neuters SHELL and SLEEP). +// Seeds: the MODULE + DEBUG resources of the sample goldens, laid out as +// above by runFuzz.sh. + +#include "../basic/fuzzCommon.h" + +#include "runtime/serialize.h" + +#include +#include + +#define FUZZ_MODULE_STEP_CAP 20000 +#define FUZZ_MODULE_TRAILER_LEN ((size_t)sizeof(uint32_t)) + +// Function prototypes (alphabetical) +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + size_t modLen = size; + size_t dbgLen = 0; + + if (size > INT32_MAX) { + return 0; + } + + if (size >= FUZZ_MODULE_TRAILER_LEN) { + uint32_t trailer; + memcpy(&trailer, data + size - FUZZ_MODULE_TRAILER_LEN, sizeof(trailer)); + + if (trailer <= size - FUZZ_MODULE_TRAILER_LEN) { + dbgLen = trailer; + modLen = size - FUZZ_MODULE_TRAILER_LEN - dbgLen; + } + } + + BasModuleT *mod = basModuleDeserialize(data, (int32_t)modLen); + + if (!mod) { + return 0; + } + + if (dbgLen > 0) { + basDebugDeserialize(mod, data + modLen, (int32_t)dbgLen); + } + + fuzzRunModule(mod, FUZZ_MODULE_STEP_CAP, NULL); + basModuleFree(mod); + return 0; +} diff --git a/src/test/fuzz/fuzzFrmModel.c b/src/test/fuzz/fuzzFrmModel.c new file mode 100644 index 00000000..6a15adc9 --- /dev/null +++ b/src/test/fuzz/fuzzFrmModel.c @@ -0,0 +1,114 @@ +// 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. + + +// fuzzFrmModel.c -- libFuzzer harness: the .frm form model parser +// +// Input: .frm text. frmModelLoad must reject it or build a model, and a +// model must write back out with these properties: +// - frmModelWrite into a frmModelWriteSize buffer never reports +// FRM_WRITE_TOO_LARGE (the size is documented as always enough); +// - the written text loads again, and writing that model gives the +// same text (load / write is a fixed point after one round). +// Seeds: src/test/fuzz/frmModel/seeds plus every .frm in the tree +// (runFuzz.sh). The form runtime links against the host libdvx; no +// widget is registered, so control types resolve to "unknown". + +#include "fuzzUtil.h" + +#include "formrt/frmModel.h" + +#include +#include +#include + +// Function prototypes (alphabetical) +static void checkWrite(const FrmModelT *m); +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +static void roundTrip(const FrmModelT *m); + + +// The documented buffer size must always be enough. +static void checkWrite(const FrmModelT *m) { + int32_t size = frmModelWriteSize(m); + char *buf = (char *)malloc((size_t)size); + + if (!buf) { + return; + } + + if (frmModelWrite(m, buf, size) == FRM_WRITE_TOO_LARGE) { + fprintf(stderr, "fuzzFrmModel: frmModelWrite overflowed a frmModelWriteSize (%ld) buffer\n", (long)size); + abort(); + } + + free(buf); +} + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size > INT32_MAX) { + return 0; + } + + char *text = fuzzUtilCString(data, size); + FrmModelT *m = frmModelLoad(text, (int32_t)size); + + if (m) { + checkWrite(m); + roundTrip(m); + frmModelFree(m); + } + + free(text); + return 0; +} + + +// write(m) must load, and write(load(write(m))) must equal write(m). +static void roundTrip(const FrmModelT *m) { + int32_t len1 = 0; + char *text1 = frmModelText(m, &len1); + + if (!text1) { + return; + } + + FrmModelT *m2 = frmModelLoad(text1, len1); + + if (!m2) { + fprintf(stderr, "fuzzFrmModel: frmModelLoad rejected frmModelText output:\n%s\n", text1); + abort(); + } + + int32_t len2 = 0; + char *text2 = frmModelText(m2, &len2); + + if (text2 && (len1 != len2 || memcmp(text1, text2, (size_t)len1) != 0)) { + fprintf(stderr, "fuzzFrmModel: write / load / write is not stable\n--- first\n%s\n--- second\n%s\n", text1, text2); + abort(); + } + + free(text2); + frmModelFree(m2); + free(text1); +} diff --git a/src/test/fuzz/fuzzHlpFile.c b/src/test/fuzz/fuzzHlpFile.c new file mode 100644 index 00000000..0ced3433 --- /dev/null +++ b/src/test/fuzz/fuzzHlpFile.c @@ -0,0 +1,59 @@ +// 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. + + +// fuzzHlpFile.c -- libFuzzer harness: the compiled help file reader +// +// Input: a .hlp file (written to a scratch file: hlpFileOpen takes a +// path). It must be rejected or read in full by fuzzHlpWalk without a +// crash, sanitizer report or leak. Seeds: small help files compiled +// from src/test/fuzz/hlpc/seeds and the help-compiler fixture +// (runFuzz.sh). + +#include "fuzzHlpWalk.h" +#include "fuzzUtil.h" + +#include + +// Function prototypes (alphabetical) +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + char path[FUZZ_UTIL_PATH_MAX]; + + fuzzUtilTempPath(path, sizeof(path), ".hlp"); + + if (!fuzzUtilWriteFile(path, data, size)) { + return 0; + } + + HlpFileT *f = hlpFileOpen(path); + + if (f) { + fuzzHlpWalk(f); + hlpFileClose(f); + } + + remove(path); + return 0; +} diff --git a/src/test/fuzz/fuzzHlpWalk.c b/src/test/fuzz/fuzzHlpWalk.c new file mode 100644 index 00000000..b32f2fd7 --- /dev/null +++ b/src/test/fuzz/fuzzHlpWalk.c @@ -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. + + +// fuzzHlpWalk.c -- see fuzzHlpWalk.h +// +// Topics (ID, title, lookup by ID, every content record: wrapped text, +// links, images read from the pool), the table of contents, the keyword +// index, the start topic and a full-text search. Large files are walked +// partly (FUZZ_HLP_MAX_*) so one input stays fast. + +#include "fuzzHlpWalk.h" + +#include + +#define FUZZ_HLP_MAX_TOPICS 64 +#define FUZZ_HLP_MAX_ENTRIES 256 +#define FUZZ_HLP_WRAP_COLS 40 +#define FUZZ_HLP_QUERY "help topic" + +// Function prototypes (alphabetical) +void fuzzHlpWalk(HlpFileT *f); +static bool walkRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); + + +void fuzzHlpWalk(HlpFileT *f) { + int32_t topics = hlpFileTopicCount(f); + + for (int32_t t = 0; t < topics && t < FUZZ_HLP_MAX_TOPICS; t++) { + (void)hlpFileTopicTitle(f, t); + (void)hlpFileFindTopic(f, hlpFileTopicId(f, t)); + (void)hlpFileForEachRecord(f, t, walkRecord, f); + } + + int32_t toc = hlpFileTocCount(f); + + for (int32_t i = 0; i < toc && i < FUZZ_HLP_MAX_ENTRIES; i++) { + const HlpTocEntryT *e = hlpFileToc(f, i); + + if (e) { + (void)hlpFileString(f, e->titleStr); + } + } + + int32_t index = hlpFileIndexCount(f); + + for (int32_t i = 0; i < index && i < FUZZ_HLP_MAX_ENTRIES; i++) { + const HlpIndexEntryT *e = hlpFileIndex(f, i); + + if (e) { + (void)hlpFileString(f, e->keywordStr); + } + } + + (void)hlpFileStartTopic(f, NULL); + + if (topics > 0) { + uint8_t *matches = (uint8_t *)calloc((size_t)topics, 1); + + if (matches) { + (void)hlpFileSearch(f, FUZZ_HLP_QUERY, matches); + free(matches); + } + } +} + + +static bool walkRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { + HlpFileT *f = (HlpFileT *)userData; + HlpImageRefT ref; + const char *target; + const char *display; + + if (hlpRecordImage(hdr, payload, &ref)) { + uint32_t size = 0; + uint8_t *image = hlpFileReadImage(f, &ref, &size); + + free(image); + } else if (hlpRecordLink(hdr, payload, &target, &display)) { + (void)hlpFileFindTopic(f, target); + } else { + free(hlpWrapText(payload, FUZZ_HLP_WRAP_COLS)); + } + + return true; +} diff --git a/src/test/fuzz/fuzzHlpWalk.h b/src/test/fuzz/fuzzHlpWalk.h new file mode 100644 index 00000000..2d88eec6 --- /dev/null +++ b/src/test/fuzz/fuzzHlpWalk.h @@ -0,0 +1,35 @@ +// 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. + + +// fuzzHlpWalk.h -- reads everything a help viewer reads from an open +// help file (shared by the hlpFile and hlpc harnesses). + +#ifndef FUZZ_HLP_WALK_H +#define FUZZ_HLP_WALK_H + +#include "hlpFile.h" + +// Function prototypes (alphabetical) +void fuzzHlpWalk(HlpFileT *f); + +#endif // FUZZ_HLP_WALK_H diff --git a/src/test/fuzz/fuzzHlpc.c b/src/test/fuzz/fuzzHlpc.c new file mode 100644 index 00000000..97b66b4c --- /dev/null +++ b/src/test/fuzz/fuzzHlpc.c @@ -0,0 +1,87 @@ +// 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. + + +// fuzzHlpc.c -- libFuzzer harness: the help compiler (src/tools/dvxhlpc.c) +// +// Input: .dhs help source. dvxhlpc.c is built with HLPC_NO_MAIN (its +// library mode, as the loader uses it) and compiled quietly with lenient +// links, also emitting HTML. The compile must succeed or fail cleanly, +// and a .hlp it writes must open with hlpFileOpen and read in full (a +// help file the compiler wrote that the reader rejects is a finding). +// Seeds: src/test/fuzz/hlpc/seeds plus the help-compiler fixtures in +// src/test/tools/fixtures/help, whose images runFuzz.sh puts in the +// working directory. + +#include "fuzzHlpWalk.h" +#include "fuzzUtil.h" + +#include "hlpcCompile.h" + +#include +#include + +// Function prototypes (alphabetical) +static void discardMessage(void *ctx, const char *line); +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + + +static void discardMessage(void *ctx, const char *line) { + (void)ctx; + (void)line; +} + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + char srcPath[FUZZ_UTIL_PATH_MAX]; + char hlpPath[FUZZ_UTIL_PATH_MAX]; + char htmlPath[FUZZ_UTIL_PATH_MAX]; + + fuzzUtilTempPath(srcPath, sizeof(srcPath), ".dhs"); + fuzzUtilTempPath(hlpPath, sizeof(hlpPath), ".hlp"); + fuzzUtilTempPath(htmlPath, sizeof(htmlPath), ".html"); + + if (!fuzzUtilWriteFile(srcPath, data, size)) { + return 0; + } + + const char *inputs[] = { srcPath }; + + hlpcSetMessageFn(discardMessage, NULL); + + if (hlpcCompile(inputs, 1, hlpPath, ".", htmlPath, HLPC_QUIET | HLPC_LENIENT_LINKS, NULL, NULL) == 0) { + HlpFileT *f = hlpFileOpen(hlpPath); + + if (!f) { + fprintf(stderr, "fuzzHlpc: hlpFileOpen rejected a help file hlpcCompile wrote\n"); + abort(); + } + + fuzzHlpWalk(f); + hlpFileClose(f); + } + + remove(srcPath); + remove(hlpPath); + remove(htmlPath); + return 0; +} diff --git a/src/test/fuzz/fuzzImage.c b/src/test/fuzz/fuzzImage.c new file mode 100644 index 00000000..7f558915 --- /dev/null +++ b/src/test/fuzz/fuzzImage.c @@ -0,0 +1,147 @@ +// 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. + + +// fuzzImage.c -- libFuzzer harness: the libdvx image loaders +// +// Input: an image file (BMP, PNG, JPEG or GIF, the formats dvxImage.c +// builds stb_image with) or a DVX file whose icon resource is one. The +// input's length modulo FUZZ_IMAGE_DEPTHS picks the display depth the +// pixels are converted to (8-bit palette, 16-bit or 32-bit, each set up +// by the host platform's platformVideoInit), so seeds stay plain image +// files. The image goes through every libdvx entry point that decodes +// one: dvxLoadImageFromMemory and dvxLoadImageAlpha, then, from a +// scratch file, dvxImageInfo and dvxLoadImage (which falls back to a DVX +// file's icon). A decoded image must have a positive size and a pitch of +// width * bytes per pixel, and its last pixel byte must be readable. +// Images dvxImageInfo reports as larger than FUZZ_IMAGE_MAX_PIXELS are +// not decoded: a tiny file can declare a huge image, and decoding and +// converting one takes seconds, which would stall the fuzzer. +// Seeds: the small images in the tree (runFuzz.sh). + +#include "fuzzUtil.h" + +#include "dvxApp.h" +#include "dvxPlat.h" + +#include +#include + +#define FUZZ_IMAGE_DEPTHS 3 +#define FUZZ_IMAGE_BPP_PAL 8 +#define FUZZ_IMAGE_BPP_HIGH 16 +#define FUZZ_IMAGE_BPP_TRUE 32 +#define FUZZ_IMAGE_SCREEN_W 64 +#define FUZZ_IMAGE_SCREEN_H 48 +#define FUZZ_IMAGE_MAX_SIDE 512 +#define FUZZ_IMAGE_MAX_PIXELS (FUZZ_IMAGE_MAX_SIDE * FUZZ_IMAGE_MAX_SIDE) + +static const int32_t sDepths[FUZZ_IMAGE_DEPTHS] = { FUZZ_IMAGE_BPP_PAL, FUZZ_IMAGE_BPP_HIGH, FUZZ_IMAGE_BPP_TRUE }; + +static AppContextT *sCtx = NULL; +static DisplayT sDisplays[FUZZ_IMAGE_DEPTHS]; + +// Function prototypes (alphabetical) +static void checkImage(const char *what, uint8_t *pixels, int32_t w, int32_t h, int32_t pitch); +int LLVMFuzzerInitialize(int *argc, char ***argv); +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + + +// A decoded image's geometry must match its buffer; frees it. +static void checkImage(const char *what, uint8_t *pixels, int32_t w, int32_t h, int32_t pitch) { + if (!pixels) { + return; + } + + if (w <= 0 || h <= 0 || pitch != w * sCtx->display.format.bytesPerPixel) { + fprintf(stderr, "fuzzImage: %s returned %ldx%ld, pitch %ld at %ld bytes per pixel\n", what, (long)w, (long)h, (long)pitch, (long)sCtx->display.format.bytesPerPixel); + abort(); + } + + volatile uint8_t last = pixels[(size_t)pitch * (size_t)h - 1]; + + (void)last; + dvxFreeImage(pixels); +} + + +// One display per depth, made once by the host platform layer. +int LLVMFuzzerInitialize(int *argc, char ***argv) { + (void)argc; + (void)argv; + + sCtx = (AppContextT *)calloc(1, sizeof(AppContextT)); + + if (!sCtx) { + abort(); + } + + for (int32_t i = 0; i < FUZZ_IMAGE_DEPTHS; i++) { + if (platformVideoInit(&sDisplays[i], FUZZ_IMAGE_SCREEN_W, FUZZ_IMAGE_SCREEN_H, sDepths[i]) != 0) { + fprintf(stderr, "fuzzImage: platformVideoInit failed for %ld bpp\n", (long)sDepths[i]); + abort(); + } + } + + return 0; +} + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size == 0 || size > INT32_MAX) { + return 0; + } + + char path[FUZZ_UTIL_PATH_MAX]; + + fuzzUtilTempPath(path, sizeof(path), ".img"); + + if (!fuzzUtilWriteFile(path, data, size)) { + return 0; + } + + sCtx->display = sDisplays[size % FUZZ_IMAGE_DEPTHS]; + + int32_t len = (int32_t)size; + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + bool hasAlpha = false; + uint32_t keyColor = 0; + + if (dvxImageInfo(path, &w, &h) && (int64_t)w * h > FUZZ_IMAGE_MAX_PIXELS) { + remove(path); + return 0; + } + + uint8_t *pixels = dvxLoadImageFromMemory(sCtx, data, len, &w, &h, &pitch); + + checkImage("dvxLoadImageFromMemory", pixels, w, h, pitch); + + pixels = dvxLoadImageAlpha(sCtx, data, len, &w, &h, &pitch, &hasAlpha, &keyColor); + checkImage("dvxLoadImageAlpha", pixels, w, h, pitch); + + pixels = dvxLoadImage(sCtx, path, &w, &h, &pitch); + checkImage("dvxLoadImage", pixels, w, h, pitch); + remove(path); + return 0; +} diff --git a/src/test/fuzz/fuzzPacket.c b/src/test/fuzz/fuzzPacket.c new file mode 100644 index 00000000..d078b71a --- /dev/null +++ b/src/test/fuzz/fuzzPacket.c @@ -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. + + +// fuzzPacket.c -- libFuzzer harness: the serial packet layer +// (serial/packet/packet.c: HDLC deframer, CRC check, Go-Back-N window) +// +// The connection runs over a PktStreamT whose read side returns bytes +// the input supplies and whose write side swallows everything. packet.c +// is built with -include loopShim.h, as the serial suite builds it, so +// its retransmit timers run on loopShim's simulated clock, which only +// the input moves. Input: a window-size byte (modulo PKT_MAX_WINDOW + 2, +// so invalid sizes are tried too), then operations, each an opcode byte +// (modulo OP_COUNT) and its operands: +// OP_FRAME seq type len payload[len]: a well-formed frame (the CRC +// and byte stuffing are done here so the fuzzer gets past +// the CRC check into the window state machine) +// OP_RAW n bytes[n]: raw line bytes (deframer, CRC rejection) +// OP_SEND n: pktSend of n input bytes, non-blocking +// OP_CLOCK n: advance the simulated clock n * FUZZ_PKT_CLOCK_STEP +// ticks (retransmit timeouts) +// OP_RESET pktReset +// pktPoll runs after every operation; a delivered payload must fit +// PKT_MAX_PAYLOAD. The framing constants below mirror packet.c's +// private ones (frame layout: SEQ TYPE LEN payload CRC16-CCITT, low +// byte first, flag 0x7E, escape 0x7D ^ 0x20). +// Seeds: src/test/fuzz/packet/seeds (written by packet/mkSeeds.py). + +#include "packet.h" +#include "loopShim.h" + +#include +#include +#include + +#define FUZZ_PKT_FLAG 0x7E +#define FUZZ_PKT_ESC 0x7D +#define FUZZ_PKT_ESC_XOR 0x20 +#define FUZZ_PKT_CRC_INIT 0xFFFFu +#define FUZZ_PKT_CRC_POLY 0x1021u +#define FUZZ_PKT_CRC_TOP 0x8000u +#define FUZZ_PKT_BITS 8 +#define FUZZ_PKT_BYTE_MASK 0xFFu +#define FUZZ_PKT_HEADER 3 +#define FUZZ_PKT_LEN_INDEX 2 +#define FUZZ_PKT_FLAG_COUNT 2 +#define FUZZ_PKT_CRC_SIZE 2 +#define FUZZ_PKT_FRAME_MAX (FUZZ_PKT_HEADER + PKT_MAX_PAYLOAD + FUZZ_PKT_CRC_SIZE) +#define FUZZ_PKT_RX_MAX (FUZZ_PKT_FLAG_COUNT + FUZZ_PKT_FRAME_MAX * 2) +#define FUZZ_PKT_CLOCK_STEP ((clock_t)CLOCKS_PER_SEC / 100) + +typedef enum { + OP_FRAME, + OP_RAW, + OP_SEND, + OP_CLOCK, + OP_RESET, + OP_COUNT +} FuzzPktOpE; + +// Input cursor plus the bytes queued for the connection to read. +typedef struct { + const uint8_t *data; + size_t size; + size_t pos; + uint8_t rx[FUZZ_PKT_RX_MAX]; + int32_t rxLen; + int32_t rxPos; +} FuzzPktStateT; + +// Function prototypes (alphabetical) +static uint16_t crc16(const uint8_t *data, int32_t len); +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +static void onReceive(void *ctx, const uint8_t *data, int len); +static size_t operand(FuzzPktStateT *st, uint8_t *out, size_t want); +static void queueByte(FuzzPktStateT *st, uint8_t byte); +static void queueFrame(FuzzPktStateT *st); +static void queueRaw(FuzzPktStateT *st); +static int32_t streamRead(void *ctx, uint8_t *buf, int32_t len); +static int32_t streamWrite(void *ctx, const uint8_t *data, int32_t len); + + +// CRC-16-CCITT, MSB first, as packet.c computes it (bitwise here). +static uint16_t crc16(const uint8_t *data, int32_t len) { + uint16_t crc = FUZZ_PKT_CRC_INIT; + + for (int32_t i = 0; i < len; i++) { + crc ^= (uint16_t)(data[i] << FUZZ_PKT_BITS); + + for (int32_t bit = 0; bit < FUZZ_PKT_BITS; bit++) { + crc = (crc & FUZZ_PKT_CRC_TOP) ? (uint16_t)((crc << 1) ^ FUZZ_PKT_CRC_POLY) : (uint16_t)(crc << 1); + } + } + + return crc; +} + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + FuzzPktStateT *st = (FuzzPktStateT *)calloc(1, sizeof(FuzzPktStateT)); + uint8_t windowByte; + + if (!st) { + abort(); + } + + st->data = data; + st->size = size; + loopReset(); + + if (operand(st, &windowByte, 1) == 0) { + free(st); + return 0; + } + + PktStreamT stream = { streamRead, streamWrite, st }; + PktConnT *conn = pktOpenStream(&stream, windowByte % (PKT_MAX_WINDOW + 2), onReceive, NULL); + + if (!conn) { + free(st); + return 0; + } + + uint8_t op; + + while (operand(st, &op, 1) == 1) { + uint8_t n = 0; + uint8_t payload[UINT8_MAX]; + + switch (op % OP_COUNT) { + case OP_FRAME: + queueFrame(st); + break; + + case OP_RAW: + queueRaw(st); + break; + + case OP_SEND: + (void)operand(st, &n, 1); + (void)pktSend(conn, payload, (int)operand(st, payload, n), false); + break; + + case OP_CLOCK: + (void)operand(st, &n, 1); + loopClockAdvance(FUZZ_PKT_CLOCK_STEP * n); + break; + + default: + (void)pktReset(conn); + break; + } + + (void)pktPoll(conn); + } + + pktClose(conn); + free(st); + return 0; +} + + +static void onReceive(void *ctx, const uint8_t *data, int len) { + (void)ctx; + + if (!data || len < 0 || len > PKT_MAX_PAYLOAD) { + fprintf(stderr, "fuzzPacket: delivered a %d-byte payload\n", len); + abort(); + } +} + + +// Up to want input bytes into out; the count copied. +static size_t operand(FuzzPktStateT *st, uint8_t *out, size_t want) { + size_t left = st->size - st->pos; + size_t n = want < left ? want : left; + + memcpy(out, st->data + st->pos, n); + st->pos += n; + return n; +} + + +static void queueByte(FuzzPktStateT *st, uint8_t byte) { + if (st->rxLen < FUZZ_PKT_RX_MAX) { + st->rx[st->rxLen++] = byte; + } +} + + +// seq type len payload[len] -> a stuffed, flagged frame with a good CRC. +static void queueFrame(FuzzPktStateT *st) { + uint8_t raw[FUZZ_PKT_FRAME_MAX]; + size_t n = operand(st, raw, FUZZ_PKT_HEADER); + + if (n < FUZZ_PKT_HEADER) { + return; + } + + int32_t len = FUZZ_PKT_HEADER + (int32_t)operand(st, raw + FUZZ_PKT_HEADER, raw[FUZZ_PKT_LEN_INDEX]); + uint16_t crc = crc16(raw, len); + + raw[len++] = (uint8_t)(crc & FUZZ_PKT_BYTE_MASK); + raw[len++] = (uint8_t)(crc >> FUZZ_PKT_BITS); + + queueByte(st, FUZZ_PKT_FLAG); + + for (int32_t i = 0; i < len; i++) { + if (raw[i] == FUZZ_PKT_FLAG || raw[i] == FUZZ_PKT_ESC) { + queueByte(st, FUZZ_PKT_ESC); + queueByte(st, (uint8_t)(raw[i] ^ FUZZ_PKT_ESC_XOR)); + } else { + queueByte(st, raw[i]); + } + } + + queueByte(st, FUZZ_PKT_FLAG); +} + + +// n raw bytes straight onto the line. +static void queueRaw(FuzzPktStateT *st) { + uint8_t n = 0; + uint8_t bytes[UINT8_MAX]; + + (void)operand(st, &n, 1); + + size_t got = operand(st, bytes, n); + + for (size_t i = 0; i < got; i++) { + queueByte(st, bytes[i]); + } +} + + +// The queued bytes, then nothing (0: no data yet) until more are queued. +static int32_t streamRead(void *ctx, uint8_t *buf, int32_t len) { + FuzzPktStateT *st = (FuzzPktStateT *)ctx; + int32_t n = st->rxLen - st->rxPos; + + if (n > len) { + n = len; + } + + memcpy(buf, st->rx + st->rxPos, (size_t)n); + st->rxPos += n; + + if (st->rxPos == st->rxLen) { + st->rxPos = 0; + st->rxLen = 0; + } + + return n; +} + + +static int32_t streamWrite(void *ctx, const uint8_t *data, int32_t len) { + (void)ctx; + (void)data; + return len; +} diff --git a/src/test/fuzz/fuzzPrefs.c b/src/test/fuzz/fuzzPrefs.c new file mode 100644 index 00000000..78104311 --- /dev/null +++ b/src/test/fuzz/fuzzPrefs.c @@ -0,0 +1,157 @@ +// 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. + + +// fuzzPrefs.c -- libFuzzer harness: the INI / preferences reader +// +// Input: INI text, written to a scratch file because prefsLoad takes a +// path. Every section named in the input ("[name]", plus the unnamed +// leading section) has its keys enumerated and read through every getter; +// then a key is set in the unnamed leading section and in a new section, +// the handle saved under a second name and the saved file loaded again, +// where both values must read back from the same sections. +// Seeds: src/test/fuzz/prefs/seeds plus every INI file in the tree +// (runFuzz.sh). + +#include "fuzzUtil.h" + +#include "dvxPrefs.h" + +#include +#include +#include + +#define FUZZ_PREFS_MAX_SECTIONS 16 +#define FUZZ_PREFS_NAME_MAX 64 +#define FUZZ_PREFS_KEY "fuzzKey" +#define FUZZ_PREFS_NEW_SECTION "fuzzSection" +#define FUZZ_PREFS_VALUE "fuzz value" +#define FUZZ_PREFS_INT_DEFAULT (-7) + +typedef struct { + char names[FUZZ_PREFS_MAX_SECTIONS][FUZZ_PREFS_NAME_MAX]; + int32_t count; +} SectionListT; + +// Function prototypes (alphabetical) +static void findSections(const char *text, SectionListT *list); +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +static void readSection(PrefsHandleT *h, const char *section); +static void saveAndReload(PrefsHandleT *h, const char *savePath); + + +// The first FUZZ_PREFS_MAX_SECTIONS "[name]" headers of the text (name +// and ']' on one line), plus "". +static void findSections(const char *text, SectionListT *list) { + list->count = 1; + list->names[0][0] = '\0'; + + for (const char *p = strchr(text, '['); p && list->count < FUZZ_PREFS_MAX_SECTIONS; p = strchr(p + 1, '[')) { + size_t len = strcspn(p + 1, "]\r\n"); + + if (p[1 + len] != ']' || len >= FUZZ_PREFS_NAME_MAX) { + continue; + } + + memcpy(list->names[list->count], p + 1, len); + list->names[list->count][len] = '\0'; + list->count++; + } +} + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + char inPath[FUZZ_UTIL_PATH_MAX]; + char savePath[FUZZ_UTIL_PATH_MAX]; + + fuzzUtilTempPath(inPath, sizeof(inPath), ".ini"); + fuzzUtilTempPath(savePath, sizeof(savePath), "-saved.ini"); + + if (!fuzzUtilWriteFile(inPath, data, size)) { + return 0; + } + + PrefsHandleT *h = prefsLoad(inPath); + + if (h) { + char *text = fuzzUtilCString(data, size); + SectionListT list; + + findSections(text, &list); + + for (int32_t i = 0; i < list.count; i++) { + readSection(h, list.names[i]); + } + + saveAndReload(h, savePath); + prefsClose(h); + free(text); + } + + remove(inPath); + remove(savePath); + return 0; +} + + +// Every key of the section through every getter. +static void readSection(PrefsHandleT *h, const char *section) { + const char *key; + + for (int32_t i = 0; (key = prefsKeyName(h, section, i)) != NULL; i++) { + (void)prefsGetString(h, section, key, NULL); + (void)prefsGetInt(h, section, key, FUZZ_PREFS_INT_DEFAULT); + (void)prefsGetBool(h, section, key, false); + } +} + + +// Set a key in the unnamed section and in a new one, save as savePath, +// reload: both must read back from the same sections. +static void saveAndReload(PrefsHandleT *h, const char *savePath) { + static const char *const sections[] = { "", FUZZ_PREFS_NEW_SECTION }; + + for (size_t i = 0; i < sizeof(sections) / sizeof(sections[0]); i++) { + prefsSetString(h, sections[i], FUZZ_PREFS_KEY, FUZZ_PREFS_VALUE); + } + + if (!prefsSaveAs(h, savePath)) { + return; + } + + PrefsHandleT *back = prefsLoad(savePath); + + if (!back) { + return; + } + + for (size_t i = 0; i < sizeof(sections) / sizeof(sections[0]); i++) { + const char *value = prefsGetString(back, sections[i], FUZZ_PREFS_KEY, NULL); + + if (!value || strcmp(value, FUZZ_PREFS_VALUE) != 0) { + fprintf(stderr, "fuzzPrefs: [%s] %s did not survive save / load (got %s)\n", sections[i], FUZZ_PREFS_KEY, value ? value : "(null)"); + abort(); + } + } + + prefsClose(back); +} diff --git a/src/test/fuzz/fuzzRes.c b/src/test/fuzz/fuzzRes.c new file mode 100644 index 00000000..cd136afa --- /dev/null +++ b/src/test/fuzz/fuzzRes.c @@ -0,0 +1,163 @@ +// 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. + + +// fuzzRes.c -- libFuzzer harness: the DVX resource container +// +// Input: a DVX file (any bytes; the resource block is found from the +// end), written to a scratch file because the API takes paths. The +// runtime reader (dvxResOpen) must reject it or return entries every +// lookup (dvxResFind, dvxResRead) can read, and the writer's reader +// (dvxResLoad, shared with the dvxres tool) must load it or fail +// cleanly. Then one resource is appended and removed again: +// - after dvxResAppend succeeds, dvxResOpen must return exactly the +// appended bytes for it; +// - after dvxResRemove succeeds, dvxResOpen must no longer find it. +// The round trip is checked only when neither reader saw a resource of +// that name in the input. Seeds: src/test/fuzz/res/seeds (made with +// dvxres), the sample goldens and src/test/tools' malformed resource +// fixtures (runFuzz.sh). + +#include "fuzzUtil.h" + +#include "dvxRes.h" +#include "dvxResWrite.h" + +#include +#include +#include + +#define FUZZ_RES_NAME "qaFuzzAppended" +#define FUZZ_RES_PAYLOAD "appended by fuzzRes" +#define FUZZ_RES_MAX_READS 64 + +// Function prototypes (alphabetical) +static void appendAndRemove(const char *path); +static bool layoutHasName(const DvxResLayoutT *layout); +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +static bool readAll(DvxResHandleT *h); + + +// Append FUZZ_RES_NAME, read it back, remove it, check it is gone. +static void appendAndRemove(const char *path) { + if (dvxResAppend(path, FUZZ_RES_NAME, DVX_RES_BINARY, FUZZ_RES_PAYLOAD, sizeof(FUZZ_RES_PAYLOAD)) != 0) { + return; + } + + DvxResHandleT *h = dvxResOpen(path); + uint32_t size = 0; + uint8_t *back = h ? (uint8_t *)dvxResRead(h, FUZZ_RES_NAME, &size) : NULL; + + if (!back || size != sizeof(FUZZ_RES_PAYLOAD) || memcmp(back, FUZZ_RES_PAYLOAD, size) != 0) { + fprintf(stderr, "fuzzRes: a resource dvxResAppend wrote did not read back (%s)\n", back ? "wrong data" : "missing"); + abort(); + } + + free(back); + dvxResClose(h); + + if (dvxResRemove(path, FUZZ_RES_NAME) != 0) { + fprintf(stderr, "fuzzRes: dvxResRemove failed on a resource dvxResAppend just wrote\n"); + abort(); + } + + h = dvxResOpen(path); + + if (h && dvxResFind(h, FUZZ_RES_NAME)) { + fprintf(stderr, "fuzzRes: a resource dvxResRemove removed is still listed\n"); + abort(); + } + + dvxResClose(h); +} + + +// True when the writer's view of the file already has FUZZ_RES_NAME. +static bool layoutHasName(const DvxResLayoutT *layout) { + for (uint32_t i = 0; i < layout->count; i++) { + if (strncmp(layout->entries[i].name, FUZZ_RES_NAME, DVX_RES_NAME_MAX) == 0) { + return true; + } + } + + return false; +} + + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + char path[FUZZ_UTIL_PATH_MAX]; + + fuzzUtilTempPath(path, sizeof(path), ".res"); + + if (!fuzzUtilWriteFile(path, data, size)) { + return 0; + } + + bool seen = false; + DvxResHandleT *h = dvxResOpen(path); + + if (h) { + seen = readAll(h); + dvxResClose(h); + } + + DvxResLayoutT layout; + uint8_t **blobs = NULL; + + if (dvxResLoad(path, &layout, &blobs) == 0) { + seen = seen || layoutHasName(&layout); + dvxResFreeBlobs(blobs, layout.count); + free(layout.entries); + } + + if (!seen) { + appendAndRemove(path); + } + + remove(path); + return 0; +} + + +// The first FUZZ_RES_MAX_READS entries through dvxResFind and dvxResRead +// (names copied out and terminated: a name field need not hold a NUL). +// True when any entry is FUZZ_RES_NAME. +static bool readAll(DvxResHandleT *h) { + bool seen = false; + + for (uint32_t i = 0; i < h->entryCount; i++) { + char name[DVX_RES_NAME_MAX + 1]; + + memcpy(name, h->entries[i].name, DVX_RES_NAME_MAX); + name[DVX_RES_NAME_MAX] = '\0'; + seen = seen || strcmp(name, FUZZ_RES_NAME) == 0; + + if (i < FUZZ_RES_MAX_READS) { + uint32_t size = 0; + + (void)dvxResFind(h, name); + free(dvxResRead(h, name, &size)); + } + } + + return seen; +} diff --git a/src/test/fuzz/fuzzUtil.c b/src/test/fuzz/fuzzUtil.c new file mode 100644 index 00000000..ed96d8ba --- /dev/null +++ b/src/test/fuzz/fuzzUtil.c @@ -0,0 +1,66 @@ +// 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. + + +// fuzzUtil.c -- see fuzzUtil.h + +#include "fuzzUtil.h" + +#include +#include +#include +#include + + +// NUL-terminated heap copy of the input (the caller frees it); aborts +// when memory runs out, which is a harness failure, not a finding. +char *fuzzUtilCString(const uint8_t *data, size_t size) { + char *text = (char *)malloc(size + 1); + + if (!text) { + abort(); + } + + memcpy(text, data, size); + text[size] = '\0'; + return text; +} + + +// "in-" in the working directory. +void fuzzUtilTempPath(char *out, size_t outSize, const char *suffix) { + snprintf(out, outSize, "in-%ld%s", (long)getpid(), suffix); +} + + +bool fuzzUtilWriteFile(const char *path, const uint8_t *data, size_t size) { + FILE *f = fopen(path, "wb"); + + if (!f) { + return false; + } + + size_t wrote = fwrite(data, 1, size, f); + bool ok = fclose(f) == 0; + + return ok && wrote == size; +} diff --git a/src/test/fuzz/fuzzUtil.h b/src/test/fuzz/fuzzUtil.h new file mode 100644 index 00000000..bc115374 --- /dev/null +++ b/src/test/fuzz/fuzzUtil.h @@ -0,0 +1,45 @@ +// 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. + + +// fuzzUtil.h -- helpers shared by the libFuzzer harnesses (src/test/fuzz) +// +// Readers that only take a path get the fuzz input through a scratch +// file in the working directory (the run script gives every fuzzer its +// own directory under obj/fuzz/work); the name carries the process id +// so parallel jobs never share one. + +#ifndef FUZZ_UTIL_H +#define FUZZ_UTIL_H + +#include +#include +#include + +#define FUZZ_UTIL_PATH_MAX 256 + +// Function prototypes (alphabetical) +char *fuzzUtilCString(const uint8_t *data, size_t size); +void fuzzUtilTempPath(char *out, size_t outSize, const char *suffix); +bool fuzzUtilWriteFile(const char *path, const uint8_t *data, size_t size); + +#endif // FUZZ_UTIL_H diff --git a/src/test/fuzz/hlpc/seeds/mini.dhs b/src/test/fuzz/hlpc/seeds/mini.dhs new file mode 100644 index 00000000..f66bf39b --- /dev/null +++ b/src/test/fuzz/hlpc/seeds/mini.dhs @@ -0,0 +1,44 @@ +# mini.dhs -- small help source touching most directives + +.section Seed + +.topic seed.one +.title First Topic +.toc 0 First Topic +.default +.index first +.index seed topic + +.h1 First + +A paragraph with brackets & "quotes". +A second line joined to it. + +.link seed.two The second topic + +.list +.item One. +.item Two +that continues. +.endlist + +.note +A note block. +.endnote + +.topic seed.two +.title Second Topic +.toc 1 Second Topic + +.code +PRINT "code is literal" +.not a directive +.endcode + +.table + Name Value + ---- ----- + a 1 +.endtable + +.hr diff --git a/src/test/fuzz/lsan.supp b/src/test/fuzz/lsan.supp new file mode 100644 index 00000000..e85df728 --- /dev/null +++ b/src/test/fuzz/lsan.supp @@ -0,0 +1,9 @@ +# lsan.supp -- LeakSanitizer suppressions for the fuzzers (runFuzz.sh +# passes this file; FUZZ_LSAN_SUPP= uses another). +# +# A leak report that every run produces, so it hides everything else, +# goes here until it is fixed. One line per finding, preceded by its +# reason: +# # : ; +# leak: +# Remove the entry when the code is fixed. diff --git a/src/test/fuzz/packet/mkSeeds.py b/src/test/fuzz/packet/mkSeeds.py new file mode 100755 index 00000000..6892a529 --- /dev/null +++ b/src/test/fuzz/packet/mkSeeds.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# 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. + +"""mkSeeds.py -- writes the fuzzPacket seed files (packet/seeds/*.bin). + +The seeds are checked in; rerun this after changing fuzzPacket.c's input +format. The layout (see fuzzPacket.c): a window-size byte, then +operations -- OP_FRAME seq type len payload, OP_RAW n bytes, OP_SEND n +bytes, OP_CLOCK n, OP_RESET -- with the frame types of packet.c (DATA, +ACK, NAK, RST). + +Usage: mkSeeds.py OUTDIR +""" + +import os +import sys + +OP_FRAME, OP_RAW, OP_SEND, OP_CLOCK, OP_RESET = range(5) +DATA, ACK, NAK, RST = range(4) +RETRANSMIT_TICKS = 60 # OP_CLOCK steps past packet.c's retransmit timeout +FULL_PAYLOAD = 255 # PKT_MAX_PAYLOAD + + +def clock(steps): + return bytes([OP_CLOCK, steps]) + + +def frame(seq, typ, payload=b""): + return bytes([OP_FRAME, seq, typ, len(payload)]) + payload + + +def raw(data): + return bytes([OP_RAW, len(data)]) + data + + +def send(payload): + return bytes([OP_SEND, len(payload)]) + payload + + +def main(): + if len(sys.argv) != 2: + sys.stderr.write("usage: mkSeeds.py OUTDIR\n") + return 2 + out = sys.argv[1] + os.makedirs(out, exist_ok=True) + seeds = { + "inOrder.bin": bytes([4]) + frame(0, DATA, b"hello") + frame(1, DATA, b"world") + frame(2, DATA, b"\x7e\x7d stuffed"), + "sendAck.bin": bytes([4]) + send(b"abc") + send(b"def") + frame(0, ACK) + frame(1, ACK) + send(b"ghi"), + "nakRetransmit.bin": bytes([2]) + send(b"one") + send(b"two") + send(b"three") + frame(0, NAK) + clock(RETRANSMIT_TICKS) + frame(1, ACK), + "outOfOrder.bin": bytes([4]) + frame(1, DATA, b"late") + frame(0, DATA, b"first") + frame(1, DATA, b"late") + frame(0, DATA, b"dup"), + "reset.bin": bytes([4]) + send(b"x") + frame(0, RST) + frame(1, RST) + bytes([OP_RESET]) + frame(0, DATA, b"after reset"), + "rawNoise.bin": bytes([3]) + raw(b"\x7e\x00\x00\x7e\x7d\x5e\x7d\x5d\x7e\x7e\x01\x02\x03\x04\x05\x06\x7e") + raw(b"\x7d\x7e"), + "fullPayload.bin": bytes([8]) + frame(0, DATA, bytes(range(FULL_PAYLOAD))) + send(bytes(FULL_PAYLOAD)), + } + for name, data in seeds.items(): + with open(os.path.join(out, name), "wb") as handle: + handle.write(data) + print("mkSeeds: wrote %d seeds to %s" % (len(seeds), out)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/test/fuzz/packet/seeds/fullPayload.bin b/src/test/fuzz/packet/seeds/fullPayload.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce445d2ae39b118f7c1c7adcb891304db3aa134a GIT binary patch literal 517 zcmd;JU|{&qz{teR!pg?Z!O6wV!^_7nASfg(A}S^>At@y-BP%Dbps1v*qN=8@p{b>< zqpPQHU}$7)Vrph?VQFP;V{2#c;OOM+;_Bw^;pye=IRvK4a#r*>mR3o4;V;qQy&=E?d50<*LVdJLFTefc7zGLUE-Fx=#+kfET zp~FXx9y@;GWe&gn?+js8XyZ_+fqsLF4K70P+<*V0k-oAVP b;p3;zU%r0({^RGb-+%u8WBLyagpmvYM#F%L literal 0 HcmV?d00001 diff --git a/src/test/fuzz/packet/seeds/inOrder.bin b/src/test/fuzz/packet/seeds/inOrder.bin new file mode 100644 index 0000000000000000000000000000000000000000..110c7459ba87a9ffcf69e1c69c613d84d9573c1e GIT binary patch literal 33 ocmZQ!U|?X)NX^N~XJBMtEzd8?Nnv1O;Hs-tC@v{YOG`}w0CQ^yWB>pF literal 0 HcmV?d00001 diff --git a/src/test/fuzz/packet/seeds/nakRetransmit.bin b/src/test/fuzz/packet/seeds/nakRetransmit.bin new file mode 100644 index 0000000000000000000000000000000000000000..f33609e483031b729334cefe64bc25501df65433 GIT binary patch literal 28 jcmZQ#V$RP?WnwNV&u3yS$tX%qWnf@pV76gkWMlvUR!#*A literal 0 HcmV?d00001 diff --git a/src/test/fuzz/packet/seeds/outOfOrder.bin b/src/test/fuzz/packet/seeds/outOfOrder.bin new file mode 100644 index 0000000000000000000000000000000000000000..d2833a2972caa1fc37792d7ccc54b9ca573c13f3 GIT binary patch literal 33 fcmZQ!U}Rv)Ni0caU|?WP%PcA`K@wn2DJ=j1W_1SC literal 0 HcmV?d00001 diff --git a/src/test/fuzz/packet/seeds/rawNoise.bin b/src/test/fuzz/packet/seeds/rawNoise.bin new file mode 100644 index 0000000000000000000000000000000000000000..c925376cf9c650b76abce687453d874e9e7b45f8 GIT binary patch literal 24 dcmZQ(6s%)lsH=^ujjgL=WMXDvWdl;RbpSw|1yle4 literal 0 HcmV?d00001 diff --git a/src/test/fuzz/packet/seeds/reset.bin b/src/test/fuzz/packet/seeds/reset.bin new file mode 100644 index 0000000000000000000000000000000000000000..68ee3eea2fea8a044cdbce7ee5a8503e37bce7f2 GIT binary patch literal 28 jcmZQ!Vys|bU}j)oWM*JtU|`@*Oe;w(QYcC-PAvfdCy4~> literal 0 HcmV?d00001 diff --git a/src/test/fuzz/packet/seeds/sendAck.bin b/src/test/fuzz/packet/seeds/sendAck.bin new file mode 100644 index 0000000000000000000000000000000000000000..e4ace099da5252b9a05ebc3ccaa206e2fb6e2b2a GIT binary patch literal 24 dcmZQ!Vopp-W@1iBO=DnS1R_QTCg${vOaLsB1Ec@| literal 0 HcmV?d00001 diff --git a/src/test/fuzz/prefs/seeds/edges.ini b/src/test/fuzz/prefs/seeds/edges.ini new file mode 100644 index 00000000..16153e20 --- /dev/null +++ b/src/test/fuzz/prefs/seeds/edges.ini @@ -0,0 +1,20 @@ +; semicolon comment +# hash comment +topKey = before any section +[general] +name = DVX +empty = +spaces = padded value +dup = first +dup = second +number = -42 +big = 99999999999 +flag = yes +[ spaced section ] +key=value=with=equals +[general] +again = reopened section +noequals line +[] +=no key +lastline = no newline \ No newline at end of file diff --git a/src/test/fuzz/res/seeds/resOnly.res b/src/test/fuzz/res/seeds/resOnly.res new file mode 100644 index 0000000000000000000000000000000000000000..33f353e96476e8a9f21cd60e9fda2f429e206ba3 GIT binary patch literal 398 zcmZ<>iwJTJ_V@FNWGG54&Mz%WPSwrN%c)dI%gjk-xX)S9dNF55u}y~Tn!lniBi(Mg zB<0(_TG`M$iL>5NNwDhNl!joLWet99E{kKDx6M=C-g)Bs3HyMJjN2UjRz{d_QslTV zcHp={alTge1?HN|KCzp0Ub_f?Q~0|iO>TXt=KZQSH$Db#wPc?C%-Xo`$_7!l8z-8V z{|{9EQM&P@&HDL~0Z~i#9AgU&Z=U2EBNg`c`g7sS3OX@4WBMUUW=L&%nVsBtXXEO~Wyvs`~xGAMXCCvSMXk7J#_Br0~=r gaq=beYxAbGz7;-Z+YXATM literal 0 HcmV?d00001 diff --git a/src/test/fuzz/runFuzz.sh b/src/test/fuzz/runFuzz.sh new file mode 100755 index 00000000..fd4c6d21 --- /dev/null +++ b/src/test/fuzz/runFuzz.sh @@ -0,0 +1,267 @@ +#!/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. + +# runFuzz.sh -- runs libFuzzer harnesses for a bounded time each +# (make -C src/test/fuzz run; see the Makefile). +# +# Usage: runFuzz.sh BINDIR SECONDS JOBS FUZZER... +# +# Per fuzzer NAME (binary BINDIR/NAME, seed directory /seeds here, +# being NAME without "fuzz", first letter lower case): +# obj/fuzz/corpus/NAME the corpus: generated seeds (rebuilt every run +# from tree files) plus what libFuzzer adds +# obj/fuzz/work/NAME working directory (harness scratch files, +# files fuzzed BASIC programs write) +# obj/fuzz/crashes/NAME crash-*, leak-*, timeout-*, oom-* reproducers; +# kept across runs until deleted (a fixed +# finding's file goes once it no longer +# reproduces), listed every run +# obj/fuzz/logs/NAME.log the fuzzer's output +# The tree's seed directory is passed read-only after the corpus. At +# most JOBS fuzzers run at once, each a single niced process bounded by +# -max_total_time=SECONDS and a wall-clock timeout; their PIDs are +# listed in obj/fuzz/pids.txt while they run. +# +# Sanitizer settings: allocations over FUZZ_MAX_ALLOC_MB return NULL +# (a corrupt length must be handled like an out-of-memory on DOS, not +# abort the run), leaks are findings, UBSan stops at the first report +# unless ubsan.supp (beside this script) suppresses it: a known UBSan +# finding that every input hits (one blocks a fuzzer on its first, +# empty input) goes there, with its reason, until it is fixed +# (FUZZ_UBSAN_SUPP= uses another file). lsan.supp does the same +# for LeakSanitizer (FUZZ_LSAN_SUPP=). +# +# Exit status: 0 = nothing found, 1 = crashes (each listed with the +# command that reproduces it), 2 = setup failure. + +set -u + +if [ "$#" -lt 4 ]; then + echo "usage: runFuzz.sh BINDIR SECONDS JOBS FUZZER..." >&2 + exit 2 +fi + +BINDIR="$(cd "$1" 2> /dev/null && pwd)" || { echo "runFuzz: no directory $1" >&2; exit 2; } +SECONDS_EACH="$2" +JOBS="$3" +shift 3 + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../../.." && pwd)" +OUT="$ROOT/obj/fuzz" +PIDS="$OUT/pids.txt" +DVXRES="$ROOT/bin/host/dvxres" +DVXHLPC="$ROOT/bin/host/dvxhlpc" +HELP_FIXTURE="$ROOT/src/test/tools/fixtures/help" +GOLDEN="$ROOT/src/test/samples/golden" +SEED_MAX_BYTES=65536 +TIMEOUT_EACH=25 +RSS_LIMIT_MB=2048 +FUZZ_MAX_ALLOC_MB=256 +WALL_SLACK=120 +EXTRA_SIGNAL_GRACE=30 + +export ASAN_OPTIONS="allocator_may_return_null=1:max_allocation_size_mb=$FUZZ_MAX_ALLOC_MB:detect_leaks=1" +UBSAN_SUPP="${FUZZ_UBSAN_SUPP:-$HERE/ubsan.supp}" +LSAN_SUPP="${FUZZ_LSAN_SUPP:-$HERE/lsan.supp}" +export LSAN_OPTIONS="suppressions=$LSAN_SUPP" +export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1:suppressions=$UBSAN_SUPP" + +cd "$ROOT" || exit 2 +mkdir -p "$OUT/corpus" "$OUT/work" "$OUT/crashes" "$OUT/logs" +: > "$PIDS" +STAMP="$OUT/runStart" +touch "$STAMP" + + +# copyTreeFiles DEST PATHSPEC... +# Copies tracked or untracked (not ignored) files matching the pathspecs +# and no bigger than SEED_MAX_BYTES into DEST, named after their path. +copyTreeFiles() { + local dest="$1" + local f + + shift + git ls-files -z -co --exclude-standard -- "$@" | while IFS= read -r -d '' f; do + if [ -f "$f" ] && [ "$(stat -c %s "$f")" -le "$SEED_MAX_BYTES" ]; then + cp "$f" "$dest/$(printf '%s' "$f" | tr '/' '_')" + fi + done +} + + +# genSeeds NAME CORPUS WORK +# Generated seeds for one fuzzer (tree files, tool output) into CORPUS; +# fixtures a harness reads by name go into WORK. +genSeeds() { + local name="$1" + local corpus="$2" + local work="$3" + local app + local base + + case "$name" in + fuzzBasCompile) + python3 "$ROOT/src/test/basic/extractCorpus.py" "$corpus" \ + "$ROOT/src/apps/kpunch/dvxbasic/test_suite.c" \ + "$ROOT/src/apps/kpunch/dvxbasic/test_compiler.c" \ + "$ROOT/src/apps/kpunch/dvxbasic/test_compact.c" > /dev/null + copyTreeFiles "$corpus" 'src/test/basic/fixtures/*.bas' + ;; + fuzzBasModule) + # MODULE | DEBUG | uint32 little-endian DEBUG length (fuzzBasModule.c) + if [ ! -x "$DVXRES" ]; then + echo "runFuzz: $DVXRES missing (make tools); $name gets no module seeds" >&2 + return 0 + fi + for app in "$GOLDEN"/*.app; do + base="$corpus/$(basename "$app" .app)" + "$DVXRES" get "$app" MODULE "$base.mod" > /dev/null 2>&1 || continue + if ! "$DVXRES" get "$app" DEBUG "$base.dbg" > /dev/null 2>&1; then + : > "$base.dbg" + fi + python3 -c 'import struct, sys; mod, dbg, out = sys.argv[1:]; d = open(dbg, "rb").read(); open(out, "wb").write(open(mod, "rb").read() + d + struct.pack("&2 + return 0 + fi + (cd "$HELP_FIXTURE" && "$DVXHLPC" -o "$corpus/fixture.hlp" --quiet -i . main.dhs @inputs.txt > /dev/null) + "$DVXHLPC" -o "$corpus/mini.hlp" --quiet "$HERE/hlpc/seeds/mini.dhs" > /dev/null + ;; + fuzzImage) + copyTreeFiles "$corpus" '*.bmp' '*.png' '*.gif' '*.jpg' + ;; + fuzzPrefs) + copyTreeFiles "$corpus" '*.ini' '*.INI' + ;; + fuzzRes) + copyTreeFiles "$corpus" 'src/test/samples/golden/*.app' 'src/test/tools/fixtures/res/*.bin' + ;; + esac + return 0 +} + + +# runOne NAME +# Prepares and runs one fuzzer; its exit status is the fuzzer's. +runOne() { + local name="$1" + local seedName="${name#fuzz}" + local corpus="$OUT/corpus/$name" + local work="$OUT/work/$name" + local crashes="$OUT/crashes/$name" + local log="$OUT/logs/$name.log" + local seeds=() + + seedName="${seedName,}" + rm -rf "$work" + mkdir -p "$corpus" "$work" "$crashes" + genSeeds "$name" "$corpus" "$work" + if [ -d "$HERE/$seedName/seeds" ]; then + seeds=("$HERE/$seedName/seeds") + fi + cd "$work" || return 2 + timeout --kill-after="$EXTRA_SIGNAL_GRACE" "$((SECONDS_EACH + WALL_SLACK))" \ + nice -n 10 "$BINDIR/$name" -max_total_time="$SECONDS_EACH" -timeout="$TIMEOUT_EACH" \ + -rss_limit_mb="$RSS_LIMIT_MB" -close_fd_mask=1 -print_final_stats=1 \ + -artifact_prefix="$crashes/" "$corpus" "${seeds[@]}" > "$log" 2>&1 +} + + +# summary NAME +# One line from the fuzzer's log: executions, coverage, corpus size. +summary() { + local log="$OUT/logs/$1.log" + local runs + local last + + runs="$(sed -n 's/^stat::number_of_executed_units: *//p' "$log" | tail -n 1)" + last="$(grep -E '^#[0-9]+[[:space:]]+(DONE|pulse|NEW|REDUCE|INITED)' "$log" | tail -n 1 | sed -E 's/^#[0-9]+[[:space:]]+[A-Z]+[[:space:]]+//; s/[[:space:]]+(lim|exec\/s|rss|L):.*$//')" + printf '%-16s %10s runs %s\n' "$1" "${runs:-?}" "${last:-no coverage line}" +} + + +for name in "$@"; do + if [ ! -x "$BINDIR/$name" ]; then + echo "runFuzz: $BINDIR/$name not built" >&2 + exit 2 + fi +done + +echo "runFuzz: $# fuzzer(s), ${SECONDS_EACH}s each, $JOBS at a time; logs in $OUT/logs" +running=0 +for name in "$@"; do + if [ "$running" -ge "$JOBS" ]; then + wait -n + running=$((running - 1)) + fi + runOne "$name" & + echo "$! $name" >> "$PIDS" + running=$((running + 1)) +done +wait +: > "$PIDS" + +echo "" +echo "runFuzz: results" +found=0 +for name in "$@"; do + summary "$name" +done +for name in "$@"; do + for crash in "$OUT/crashes/$name"/*; do + if [ ! -f "$crash" ]; then + continue + fi + if [ "$found" -eq 0 ]; then + echo "" + echo "runFuzz: FINDINGS (reproduce with the command shown; the log has the report)" + fi + found=$((found + 1)) + when="earlier run" + if [ "$crash" -nt "$STAMP" ]; then + when="this run" + fi + echo " $name ($when): $crash" + echo " (cd $OUT/work/$name && ASAN_OPTIONS='$ASAN_OPTIONS' UBSAN_OPTIONS='$UBSAN_OPTIONS' LSAN_OPTIONS='$LSAN_OPTIONS' $BINDIR/$name $crash)" + done +done + +if [ "$found" -ne 0 ]; then + echo "runFuzz: $found reproducer(s) under $OUT/crashes" + exit 1 +fi +echo "runFuzz: no crashes, leaks or timeouts" +exit 0 diff --git a/src/test/fuzz/ubsan.supp b/src/test/fuzz/ubsan.supp new file mode 100644 index 00000000..dd2ec140 --- /dev/null +++ b/src/test/fuzz/ubsan.supp @@ -0,0 +1,18 @@ +# ubsan.supp -- UndefinedBehaviorSanitizer suppressions for the fuzzers +# (runFuzz.sh passes this file; FUZZ_UBSAN_SUPP= uses another). +# +# The fuzzers stop at the first UBSan report. A report that every input +# reaches -- one hit by the empty input stops a fuzzer before it has run +# anything -- goes here until it is fixed, so the fuzzer can look past +# it. One line per finding, preceded by its reason: +# # : ; +# : +# e.g. nonnull-attribute:pass3SearchIndex +# Checks are UBSan's names (nonnull-attribute, alignment, shift-base, +# signed-integer-overflow, ...). Remove the entry when the code is fixed. + +# stb_image.h:5546 (THIRDPARTY): stbi__bmp_load does abs((int)img_y), UB +# for a BMP height of INT_MIN (0x80000000; gcc gives INT_MIN back and the +# STBI_MAX_DIMENSIONS check then rejects the image). thirdparty/ is not +# patched here; remove when stb_image is updated or patched with tracking. +signed-integer-overflow:stbi__bmp_load diff --git a/src/test/game/gameTest.c b/src/test/game/gameTest.c index cc015e17..f23af261 100644 --- a/src/test/game/gameTest.c +++ b/src/test/game/gameTest.c @@ -31,7 +31,8 @@ // snapshot, clearing, a full queue, bare releases // dispatch game input windows in the test GUI: the reserved keys, // everything else to onKey, raw events, onKeyUp, the release -// of held keys when focus moves, gameKeyIsDown, GameKeyDown +// of held keys when focus moves, gameKeyIsDown, GameKeyDown, +// GameKeyClear, GameWaitRetrace // exit exit() / abort() of a module reach the hook // pacing gameFrameWait on time, missed frames, no drift, clamping, // gameFrameNext (the next frame without the wait), @@ -258,6 +259,24 @@ static void dispatchTests(void) { TEST_EQ(GameKeyDown(SCAN_W), -1); TEST_EQ(GameKeyDown(SCAN_A), 0); + tCase("dispatch: GameKeyClear forgets held keys, only for the focused program"); + int32_t ownApp = ctx->currentAppId; + + ctx->currentAppId = OWNER_A; + GameKeyClear(); + TEST_TRUE(platformKeyIsDown(SCAN_W)); + ctx->currentAppId = ownApp; + GameKeyClear(); + TEST_TRUE(!platformKeyIsDown(SCAN_W)); + TEST_EQ(GameKeyDown(SCAN_W), 0); + + // Held again at its next repeat; GameWaitRetrace has no retrace to + // wait for on the host. + platformInjectScan(SCAN_W); + tPump(1); + GameWaitRetrace(); + TEST_EQ(GameKeyDown(SCAN_W), -1); + tCase("dispatch: Alt+Tab stays with DVX; the game gets its held keys released"); gameLog.rawCount = 0; tAltTab(); diff --git a/src/test/hlpfile/Makefile b/src/test/hlpfile/Makefile new file mode 100644 index 00000000..661d48c3 --- /dev/null +++ b/src/test/hlpfile/Makefile @@ -0,0 +1,49 @@ +# 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 file library (hlpfile) tests: the reader and the BASIC entry points +# against the help compiler's every-directive fixture (DVXTEST_HELP_HLP, +# compiled by dvxtest.mk). The library itself is in libdvxtest.a. + +include ../dvxtest/dvxtest.mk + +.DEFAULT_GOAL := all + +BINDIR = $(DVXTEST_BINDIR)/hlpfile +TEST_BIN = $(BINDIR)/hlpFileTest +CFLAGS = $(DVXTEST_CFLAGS) + +.PHONY: all run clean + +all: run + +$(TEST_BIN): hlpFileTest.c $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) | dvxtest-libs + @mkdir -p $(BINDIR) + $(HOSTCC) $(CFLAGS) -o $@ $< $(DVXTEST_LINK) + +-include $(TEST_BIN).d + +run: $(TEST_BIN) $(DVXTEST_HELP_HLP) + cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./hlpFileTest + +clean: + rm -rf $(BINDIR) diff --git a/src/test/hlpfile/hlpFileTest.c b/src/test/hlpfile/hlpFileTest.c new file mode 100644 index 00000000..536959df --- /dev/null +++ b/src/test/hlpfile/hlpFileTest.c @@ -0,0 +1,673 @@ +// 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. + +// hlpFileTest.c -- the help file library (hlpfile) against the help +// compiler's every-directive fixture (src/test/tools/fixtures/help, +// compiled by dvxtest.mk): opening, topics, table of contents, index, +// search, records, images, wrapping, and the BASIC entry points +// (hlpfile.bas) with their per-program handles. + +#include "dvxtest.h" +#include "../../libs/kpunch/hlpfile/hlpFile.h" +#include "dvxMem.h" +#include "dvxPlat.h" + +#include +#include +#include +#include +#include +#include + +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_BAD_HANDLE 99 +#define PNG_MAGIC "\x89PNG" // the fixture's pictures are tiny.png +#define PNG_MAGIC_LEN 4 +#define WRAP_POOL_OFFSET 8 // any pool offset inside the header-only file +#define MAX_RECORDS 32 +#define WRAP_COLS 10 +#define WRAP_TOO_NARROW 3 +#define APP_A 7 +#define APP_B 8 +#define HDR_VERSION_BAD 2 +#define JUNK_FILE "junk.hlp" +#define BADVER_FILE "badver.hlp" +#define IMAGE_OUT "image.png" +#define MISSING_FILE "/nonexistent/none.hlp" + +// The records of one topic as hlpFileForEachRecord gave them. +typedef struct { + uint8_t types[MAX_RECORDS]; + uint8_t flags[MAX_RECORDS]; + int32_t count; + int32_t stopAfter; // stop the walk after this many (0: never) + int32_t imagesRead; // images read back inside the walk + HlpFileT *file; +} RecWalkT; + +// The shell's app-cleanup registry, provided here (the BASIC entry points +// look it up by name) so the test can end a "program". +typedef void (*CleanupFnT)(int32_t appId); + +static CleanupFnT sCleanup = NULL; + +// ============================================================ +// Prototypes +// ============================================================ + +// Entry points of hlpFileBas.c (declared by hlpfile.bas, not a header) +void HlpClose(int32_t handle); +int32_t HlpFindTopic(int32_t handle, const char *topicId); +int32_t HlpIndexCount(int32_t handle); +const char *HlpIndexKeyword(int32_t handle, int32_t i); +int32_t HlpIndexTopic(int32_t handle, int32_t i); +int32_t HlpLoadTopic(int32_t handle, int32_t topic); +int32_t HlpOpen(const char *path); +int32_t HlpRecordFlags(int32_t handle, int32_t n); +int32_t HlpRecordImageSave(int32_t handle, int32_t n, const char *path); +const char *HlpRecordLink(int32_t handle, int32_t n); +const char *HlpRecordText(int32_t handle, int32_t n); +int32_t HlpRecordType(int32_t handle, int32_t n); +int32_t HlpSearch(int32_t handle, const char *query); +int32_t HlpSearchTopic(int32_t handle, int32_t n); +int32_t HlpStartTopic(int32_t handle, const char *topicId); +int32_t HlpTocCount(int32_t handle); +int32_t HlpTocDepth(int32_t handle, int32_t i); +int32_t HlpTocExpanded(int32_t handle, int32_t i); +const char *HlpTocTitle(int32_t handle, int32_t i); +int32_t HlpTocTopic(int32_t handle, int32_t i); +int32_t HlpTopicCount(int32_t handle); +const char *HlpTopicId(int32_t handle, int32_t topic); +const char *HlpTopicTitle(int32_t handle, int32_t topic); +const char *HlpWrap(const char *text, int32_t cols); + +static void caseBasic(void); +static void caseBasicCleanup(void); +static void caseIndex(void); +static void caseOpenBad(void); +static void caseRecords(void); +static void caseSearch(void); +static void caseToc(void); +static void caseTopics(void); +static void caseTrigramRule(void); +static void caseWrap(void); +static bool fileStartsPng(const char *path); +static int32_t indexOf(HlpFileT *f, const char *keyword); +static int32_t searchCount(HlpFileT *f, const char *query, int32_t expectTopic); +void shellRegisterAppCleanup(CleanupFnT fn); +static int32_t tocFind(HlpFileT *f, const char *title); +static bool walkRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); +int main(void); + + +// The BASIC face: every entry point on the fixture, results kept in the +// handle, bad handles and bad numbers. +static void caseBasic(void) { + tCase("BASIC entry points"); + + int32_t h = HlpOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(h > 0); + TEST_EQ(HlpOpen(MISSING_FILE), 0); + TEST_TRUE(hlpFileBasHandle(h) != NULL); + TEST_PTR_EQ(hlpFileBasHandle(BAS_BAD_HANDLE), NULL); + + int32_t welcome = HlpFindTopic(h, "fx.welcome"); + int32_t deep = HlpFindTopic(h, "fx.deep"); + + TEST_TRUE(welcome >= 0 && deep >= 0); + TEST_TRUE(HlpTopicCount(h) > deep); + TEST_STR_EQ(HlpTopicId(h, deep), "fx.deep"); + TEST_STR_EQ(HlpTopicTitle(h, deep), "Deep Topic"); + TEST_EQ(HlpStartTopic(h, ""), welcome); + TEST_EQ(HlpStartTopic(h, "fx.deep"), deep); + TEST_EQ(HlpFindTopic(h, "nope"), -1); + + // Table of contents and index + int32_t tocDeep = -1; + + for (int32_t i = 0; i < HlpTocCount(h); i++) { + if (strcmp(HlpTocTitle(h, i), "Deep Topic") == 0) { + tocDeep = i; + } + } + + TEST_TRUE(tocDeep >= 0); + TEST_EQ(HlpTocTopic(h, tocDeep), deep); + TEST_TRUE(HlpTocDepth(h, tocDeep) > 0); + TEST_TRUE(HlpTocExpanded(h, tocDeep) == BAS_TRUE || HlpTocExpanded(h, tocDeep) == BAS_FALSE); + TEST_EQ(HlpTocTopic(h, HlpTocCount(h)), -1); + TEST_STR_EQ(HlpTocTitle(h, -1), ""); + + int32_t zebra = -1; + + for (int32_t i = 0; i < HlpIndexCount(h); i++) { + if (strcmp(HlpIndexKeyword(h, i), "zebra") == 0) { + zebra = i; + } + } + + TEST_TRUE(zebra >= 0); + TEST_EQ(HlpIndexTopic(h, zebra), deep); + TEST_EQ(HlpIndexTopic(h, HlpIndexCount(h)), -1); + + // Search results are kept for HlpSearchTopic + TEST_EQ(HlpSearch(h, "continuation"), 1); + TEST_EQ(HlpSearchTopic(h, 0), HlpFindTopic(h, "fx.blocks")); + TEST_EQ(HlpSearchTopic(h, 1), -1); + TEST_EQ(HlpSearch(h, "qqqzzz"), 0); + TEST_EQ(HlpSearchTopic(h, 0), -1); + + // Records of the welcome topic: a link, a picture saved as a .bmp + int32_t count = HlpLoadTopic(h, welcome); + int32_t link = -1; + int32_t image = -1; + int32_t rule = -1; + + TEST_TRUE(count > 0); + + for (int32_t i = 0; i < count; i++) { + if (HlpRecordType(h, i) == HLP_REC_LINK && link < 0) { + link = i; + } else if (HlpRecordType(h, i) == HLP_REC_IMAGE && HlpRecordFlags(h, i) == HLP_IMG_CENTER) { + image = i; + } else if (HlpRecordType(h, i) == HLP_REC_HRULE) { + rule = i; + } + } + + TEST_TRUE(link >= 0 && image >= 0 && rule >= 0); + TEST_STR_EQ(HlpRecordText(h, link), "Go to blocks"); + TEST_STR_EQ(HlpRecordLink(h, link), "fx.blocks"); + TEST_STR_EQ(HlpRecordText(h, 0), "Welcome Heading"); + TEST_STR_EQ(HlpRecordLink(h, 0), ""); + TEST_STR_EQ(HlpRecordText(h, image), ""); + TEST_STR_EQ(HlpRecordText(h, rule), ""); + unlink(IMAGE_OUT); + TEST_EQ(HlpRecordImageSave(h, image, IMAGE_OUT), BAS_TRUE); + TEST_TRUE(fileStartsPng(IMAGE_OUT)); + unlink(IMAGE_OUT); + TEST_EQ(HlpRecordImageSave(h, link, IMAGE_OUT), BAS_FALSE); + TEST_EQ(HlpRecordImageSave(h, image, MISSING_FILE), BAS_FALSE); + TEST_EQ(HlpRecordType(h, count), -1); + TEST_EQ(HlpRecordFlags(h, -1), -1); + TEST_STR_EQ(HlpRecordText(h, count), ""); + TEST_STR_EQ(HlpRecordLink(h, count), ""); + TEST_EQ(HlpLoadTopic(h, HlpTopicCount(h)), -1); + TEST_EQ(HlpRecordType(h, 0), -1); + + // Wrapping + TEST_STR_EQ(HlpWrap("aaaa bbbb cccc", WRAP_COLS), "aaaa bbbb\ncccc"); + + // Bad handles do nothing + TEST_EQ(HlpTopicCount(BAS_BAD_HANDLE), 0); + TEST_STR_EQ(HlpTopicTitle(0, 0), ""); + TEST_EQ(HlpSearch(BAS_BAD_HANDLE, "x"), -1); + TEST_EQ(HlpLoadTopic(0, 0), -1); + TEST_EQ(HlpTocCount(0), 0); + TEST_EQ(HlpIndexCount(0), 0); + TEST_EQ(HlpStartTopic(0, ""), -1); + HlpClose(BAS_BAD_HANDLE); + + // A closed handle is free for the next open + HlpClose(h); + TEST_EQ(HlpTopicCount(h), 0); + TEST_EQ(HlpOpen(DVXTEST_HELP_HLP), h); + HlpClose(h); +} + + +// Handles belong to the program that opened them: the shell's app-cleanup +// hook closes what a program left open, and only its own. +static void caseBasicCleanup(void) { + tCase("BASIC handles are closed when their program ends"); + + // The running program as the shell's memory tracking reports it + int32_t appId = APP_A; + int32_t *savedPtr = dvxMemAppIdPtr; + + dvxMemAppIdPtr = &appId; + + int32_t a = HlpOpen(DVXTEST_HELP_HLP); + + appId = APP_B; + + int32_t b = HlpOpen(DVXTEST_HELP_HLP); + + dvxMemAppIdPtr = savedPtr; + TEST_TRUE(a > 0 && b > 0 && a != b); + TEST_TRUE(sCleanup != NULL); + + if (sCleanup) { + sCleanup(APP_A); + } + + TEST_EQ(HlpTopicCount(a), 0); + TEST_TRUE(HlpTopicCount(b) > 0); + HlpClose(b); +} + + +// Keyword index entries are in order and name their topics. +static void caseIndex(void) { + tCase("keyword index"); + + HlpFileT *f = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(f != NULL); + + if (!f) { + return; + } + + int32_t zebra = indexOf(f, "zebra"); + int32_t extra = indexOf(f, "extra"); + + TEST_TRUE(zebra >= 0 && extra >= 0); + TEST_EQ(hlpFileIndex(f, zebra)->topicIdx, hlpFileFindTopic(f, "fx.deep")); + TEST_EQ(hlpFileIndex(f, extra)->topicIdx, hlpFileFindTopic(f, "fx.extra")); + TEST_PTR_EQ(hlpFileIndex(f, hlpFileIndexCount(f)), NULL); + TEST_PTR_EQ(hlpFileIndex(f, -1), NULL); + + for (int32_t i = 1; i < hlpFileIndexCount(f); i++) { + TEST_TRUE(strcasecmp(hlpFileString(f, hlpFileIndex(f, i - 1)->keywordStr), hlpFileString(f, hlpFileIndex(f, i)->keywordStr)) <= 0); + } + + hlpFileClose(f); +} + + +// Files that cannot be opened: missing, not a help file, wrong version. +static void caseOpenBad(void) { + tCase("bad help files are refused"); + + static const char junk[] = "this is not a help file at all, just some text bytes"; + HlpHeaderT hdr; + + TEST_PTR_EQ(hlpFileOpen(NULL), NULL); + TEST_PTR_EQ(hlpFileOpen(MISSING_FILE), NULL); + TEST_TRUE(tWriteFile(JUNK_FILE, junk, (int32_t)sizeof(junk))); + TEST_PTR_EQ(hlpFileOpen(JUNK_FILE), NULL); + + memset(&hdr, 0, sizeof(hdr)); + hdr.magic = HLP_MAGIC; + hdr.version = HDR_VERSION_BAD; + TEST_TRUE(tWriteFile(BADVER_FILE, &hdr, (int32_t)sizeof(hdr))); + TEST_PTR_EQ(hlpFileOpen(BADVER_FILE), NULL); + + // An empty file (header only, no topics) opens and has nothing + hdr.version = HLP_VERSION; + TEST_TRUE(tWriteFile(BADVER_FILE, &hdr, (int32_t)sizeof(hdr))); + + HlpFileT *empty = hlpFileOpen(BADVER_FILE); + uint8_t matches = 0; + + TEST_TRUE(empty != NULL); + TEST_EQ(hlpFileTopicCount(empty), 0); + TEST_EQ(hlpFileStartTopic(empty, NULL), -1); + TEST_EQ(hlpFileSearch(empty, "anything", &matches), 0); + hlpFileClose(empty); + hlpFileClose(NULL); + + // An image offset that wraps past 4 GB with the pool offset (a damaged + // file) fails instead of reading the start of the file as the image. + HlpImageRefT wrapRef = { UINT32_MAX - WRAP_POOL_OFFSET + 1, PNG_MAGIC_LEN }; + + hdr.imagePoolOffset = WRAP_POOL_OFFSET; + TEST_TRUE(tWriteFile(BADVER_FILE, &hdr, (int32_t)sizeof(hdr))); + empty = hlpFileOpen(BADVER_FILE); + TEST_TRUE(empty != NULL); + TEST_PTR_EQ(hlpFileReadImage(empty, &wrapRef, NULL), NULL); + hlpFileClose(empty); + + // NULL handles answer nothing + TEST_EQ(hlpFileTopicCount(NULL), 0); + TEST_EQ(hlpFileTocCount(NULL), 0); + TEST_EQ(hlpFileIndexCount(NULL), 0); + TEST_EQ(hlpFileFindTopic(NULL, "x"), -1); + TEST_STR_EQ(hlpFileString(NULL, 0), ""); + TEST_EQ(hlpFileSearch(NULL, "x", &matches), -1); + unlink(JUNK_FILE); + unlink(BADVER_FILE); +} + + +// A topic's records in order; a walk that stops early; images read inside +// the walk without upsetting it. +static void caseRecords(void) { + tCase("topic records and images"); + + HlpFileT *f = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(f != NULL); + + if (!f) { + return; + } + + static const uint8_t expected[] = { HLP_REC_HEADING1, HLP_REC_TEXT, HLP_REC_TEXT, HLP_REC_IMAGE, HLP_REC_IMAGE, HLP_REC_IMAGE, HLP_REC_HEADING2, HLP_REC_LINK, HLP_REC_LINK, HLP_REC_HEADING3, HLP_REC_HRULE }; + int32_t welcome = hlpFileFindTopic(f, "fx.welcome"); + RecWalkT walk; + + memset(&walk, 0, sizeof(walk)); + walk.file = f; + TEST_TRUE(hlpFileForEachRecord(f, welcome, walkRecord, &walk)); + TEST_EQ(walk.count, (int32_t)sizeof(expected)); + TEST_EQ(walk.imagesRead, 3); + + for (int32_t i = 0; i < walk.count && i < (int32_t)sizeof(expected); i++) { + TEST_EQ(walk.types[i], expected[i]); + } + + TEST_EQ(walk.flags[3], HLP_IMG_LEFT); + TEST_EQ(walk.flags[4], HLP_IMG_CENTER); + TEST_EQ(walk.flags[5], HLP_IMG_RIGHT); + + // Stopping early + memset(&walk, 0, sizeof(walk)); + walk.file = f; + walk.stopAfter = 2; + TEST_TRUE(hlpFileForEachRecord(f, welcome, walkRecord, &walk)); + TEST_EQ(walk.count, 2); + + // Notes carry their kind; tables and code are their own types + int32_t blocks = hlpFileFindTopic(f, "fx.blocks"); + + memset(&walk, 0, sizeof(walk)); + walk.file = f; + TEST_TRUE(hlpFileForEachRecord(f, blocks, walkRecord, &walk)); + + int32_t notes = 0; + bool code = false; + bool table = false; + bool items = false; + + for (int32_t i = 0; i < walk.count; i++) { + if (walk.types[i] == HLP_REC_NOTE) { + TEST_EQ(walk.flags[i], notes); + notes++; + } + + code = code || walk.types[i] == HLP_REC_CODE; + table = table || walk.types[i] == HLP_REC_TABLE; + items = items || walk.types[i] == HLP_REC_LIST_ITEM; + } + + TEST_EQ(notes, HLP_NOTE_WARNING + 1); + TEST_TRUE(code && table && items); + + // Bad topics + TEST_TRUE(!hlpFileForEachRecord(f, -1, walkRecord, &walk)); + TEST_TRUE(!hlpFileForEachRecord(f, hlpFileTopicCount(f), walkRecord, &walk)); + TEST_PTR_EQ(hlpFileReadImage(f, NULL, NULL), NULL); + hlpFileClose(f); +} + + +// Full-text search: body words, any case, every word of several, keyword +// matches, and the caller's query is not changed. +static void caseSearch(void) { + tCase("full-text search"); + + HlpFileT *f = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(f != NULL); + + if (!f) { + return; + } + + int32_t blocks = hlpFileFindTopic(f, "fx.blocks"); + int32_t deep = hlpFileFindTopic(f, "fx.deep"); + char query[] = " Continuation "; + + // "continuation" is only in the Blocks body + TEST_EQ(searchCount(f, query, blocks), 1); + TEST_STR_EQ(query, " Continuation "); + TEST_EQ(searchCount(f, "CONTINUAT", blocks), 1); + + // Every word must be there + TEST_EQ(searchCount(f, "continuation qqqzzz", -1), 0); + + // An index keyword holding the whole query + TEST_EQ(searchCount(f, "zebra", deep), 1); + + // Too short for a trigram: titles and keywords only + TEST_EQ(searchCount(f, "zz", -1), 0); + TEST_TRUE(searchCount(f, "we", -1) >= 1); + + // Nothing to search for + TEST_EQ(searchCount(f, " ", -1), 0); + TEST_EQ(searchCount(f, "", -1), 0); + hlpFileClose(f); +} + + +// The table of contents: section headings with no topic, depths, titles. +static void caseToc(void) { + tCase("table of contents"); + + HlpFileT *f = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(f != NULL); + + if (!f) { + return; + } + + int32_t guide = tocFind(f, "Guide"); + int32_t welcome = tocFind(f, "Welcome"); + int32_t deep = tocFind(f, "Deep Topic"); + + TEST_TRUE(guide >= 0 && welcome >= 0 && deep >= 0); + TEST_EQ(hlpFileToc(f, guide)->topicIdx, HLP_NO_TOPIC); + TEST_EQ(hlpFileToc(f, welcome)->topicIdx, hlpFileFindTopic(f, "fx.welcome")); + TEST_EQ(hlpFileToc(f, deep)->topicIdx, hlpFileFindTopic(f, "fx.deep")); + TEST_TRUE(hlpFileToc(f, welcome)->depth > hlpFileToc(f, guide)->depth); + TEST_PTR_EQ(hlpFileToc(f, hlpFileTocCount(f)), NULL); + TEST_PTR_EQ(hlpFileToc(f, -1), NULL); + hlpFileClose(f); +} + + +// Topics by number and ID, and the topic a request starts on. +static void caseTopics(void) { + tCase("topics and the start topic"); + + HlpFileT *f = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(f != NULL); + + if (!f) { + return; + } + + int32_t welcome = hlpFileFindTopic(f, "fx.welcome"); + int32_t deep = hlpFileFindTopic(f, "fx.deep"); + int32_t extra = hlpFileFindTopic(f, "fx.extra"); + + TEST_TRUE(welcome >= 0 && deep >= 0 && extra >= 0); + TEST_TRUE(hlpFileTopicCount(f) > extra); + TEST_STR_EQ(hlpFileTopicId(f, deep), "fx.deep"); + TEST_STR_EQ(hlpFileTopicTitle(f, welcome), "Welcome"); + TEST_STR_EQ(hlpFileTopicTitle(f, extra), "Extra File"); + TEST_STR_EQ(hlpFileTopicTitle(f, hlpFileTopicCount(f)), ""); + TEST_STR_EQ(hlpFileTopicId(f, -1), ""); + TEST_EQ(hlpFileFindTopic(f, "fx.none"), -1); + TEST_EQ(hlpFileFindTopic(f, NULL), -1); + + // The named topic, else the default (.default), else the first + TEST_EQ(hlpFileStartTopic(f, "fx.deep"), deep); + TEST_EQ(hlpFileStartTopic(f, NULL), welcome); + TEST_EQ(hlpFileStartTopic(f, ""), welcome); + TEST_EQ(hlpFileStartTopic(f, "fx.none"), welcome); + TEST_STR_EQ(hlpFileString(f, UINT32_MAX), ""); + hlpFileClose(f); +} + + +// The one trigram character rule (hlpFormat.h) the compiler and reader +// share, and the record types searched. +static void caseTrigramRule(void) { + tCase("trigram characters and searchable records"); + + TEST_EQ(hlpTrigramSymbol('0'), 0); + TEST_EQ(hlpTrigramSymbol('9'), HLP_TRIGRAM_DIGITS - 1); + TEST_EQ(hlpTrigramSymbol('a'), HLP_TRIGRAM_DIGITS); + TEST_EQ(hlpTrigramSymbol('z'), HLP_TRIGRAM_ALPHABET - 1); + TEST_EQ(hlpTrigramSymbol('A'), -1); + TEST_EQ(hlpTrigramSymbol(' '), -1); + TEST_TRUE(hlpRecordSearchable(HLP_REC_TEXT)); + TEST_TRUE(hlpRecordSearchable(HLP_REC_CODE)); + TEST_TRUE(!hlpRecordSearchable(HLP_REC_LINK)); + TEST_TRUE(!hlpRecordSearchable(HLP_REC_TABLE)); + TEST_TRUE(!hlpRecordSearchable(HLP_REC_IMAGE)); +} + + +// Word wrapping as the viewer wraps paragraphs. +static void caseWrap(void) { + tCase("word wrap"); + + char *w = hlpWrapText("aaaa bbbb cccc", WRAP_COLS); + + TEST_STR_EQ(w, "aaaa bbbb\ncccc"); + free(w); + + // Narrower than the minimum wraps at the minimum + w = hlpWrapText("aaaa bbbb cccc", WRAP_TOO_NARROW); + TEST_STR_EQ(w, "aaaa bbbb\ncccc"); + free(w); + + // A newline is kept; leading blanks of a line go; a long word is not split + w = hlpWrapText("ab\n cd averyveryverylongword x", WRAP_COLS); + TEST_STR_EQ(w, "ab\ncd\naveryveryverylongword\nx"); + free(w); + + w = hlpWrapText(NULL, WRAP_COLS); + TEST_STR_EQ(w, ""); + free(w); +} + + +static bool fileStartsPng(const char *path) { + int32_t len = 0; + char *data = platformReadFile(path, &len); + bool ok = data && len > PNG_MAGIC_LEN && memcmp(data, PNG_MAGIC, PNG_MAGIC_LEN) == 0; + + free(data); + return ok; +} + + +static int32_t indexOf(HlpFileT *f, const char *keyword) { + for (int32_t i = 0; i < hlpFileIndexCount(f); i++) { + if (strcmp(hlpFileString(f, hlpFileIndex(f, i)->keywordStr), keyword) == 0) { + return i; + } + } + + return -1; +} + + +// hlpFileSearch's count; with expectTopic >= 0 that topic must be found. +static int32_t searchCount(HlpFileT *f, const char *query, int32_t expectTopic) { + uint8_t *matches = (uint8_t *)malloc(hlpFileTopicCount(f)); + int32_t count = matches ? hlpFileSearch(f, query, matches) : -1; + + if (matches && expectTopic >= 0) { + TEST_EQ(matches[expectTopic], 1); + } + + free(matches); + return count; +} + + +// Stand-in for the shell's registry (shellApp.h). +void shellRegisterAppCleanup(CleanupFnT fn) { + sCleanup = fn; +} + + +static int32_t tocFind(HlpFileT *f, const char *title) { + for (int32_t i = 0; i < hlpFileTocCount(f); i++) { + if (strcmp(hlpFileString(f, hlpFileToc(f, i)->titleStr), title) == 0) { + return i; + } + } + + return -1; +} + + +static bool walkRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { + RecWalkT *walk = (RecWalkT *)userData; + HlpImageRefT ref; + + if (walk->count < MAX_RECORDS) { + walk->types[walk->count] = hdr->type; + walk->flags[walk->count] = hdr->flags; + } + + walk->count++; + + if (hlpRecordImage(hdr, payload, &ref)) { + uint32_t size = 0; + uint8_t *data = hlpFileReadImage(walk->file, &ref, &size); + + if (data && size == ref.imageSize && memcmp(data, PNG_MAGIC, PNG_MAGIC_LEN) == 0) { + walk->imagesRead++; + } + + free(data); + } + + if (hdr->type == HLP_REC_LINK) { + const char *target = NULL; + const char *display = NULL; + + TEST_TRUE(hlpRecordLink(hdr, payload, &target, &display)); + TEST_TRUE(target[0] != '\0' && display[0] != '\0'); + } + + return walk->stopAfter == 0 || walk->count < walk->stopAfter; +} + + +int main(void) { + platformSetLogPath("hlpFileTest.log"); + tSuiteBegin("hlpfile"); + caseOpenBad(); + caseTopics(); + caseToc(); + caseIndex(); + caseRecords(); + caseSearch(); + caseTrigramRule(); + caseWrap(); + caseBasic(); + caseBasicCleanup(); + return tSuiteEnd(); +} diff --git a/src/test/ide/Makefile b/src/test/ide/Makefile index d2299c03..8302af82 100644 --- a/src/test/ide/Makefile +++ b/src/test/ide/Makefile @@ -61,13 +61,15 @@ STUBTARGET := $(BINDIR)/testStub IDE_SRCS := ideDesigner.c ideProperties.c ideProject.c ideMenuEditor.c ideToolbox.c COMP_SRCS := lexer.c ast.c scope.c parse.c parseExpr.c parseStmt.c parseDecl.c resolve.c resolveExpr.c typecheck.c fold.c gen.c genExpr.c genStmt.c compiler.c codegen.c strip.c obfuscate.c compact.c RT_SRCS := vm.c values.c dates.c serialize.c gfx.c screen.c -FRM_SRCS := formrt.c formGfx.c frmParser.c basNativeHost.c +FRM_SRCS := formrt.c formGfx.c formDesign.c formCtl.c frmParser.c frmModel.c frmModelBas.c basNativeHost.c basRunHost.c +DEV_SRCS := basValidate.c basDebug.c basProcs.c basSource.c basHelpMap.c basProject.c OBJS := $(BINDIR)/testIde.o $(BINDIR)/ideStubs.o OBJS += $(patsubst %.c,$(BINDIR)/ide/%.o,$(IDE_SRCS)) OBJS += $(patsubst %.c,$(BINDIR)/compiler/%.o,$(COMP_SRCS)) OBJS += $(patsubst %.c,$(BINDIR)/runtime/%.o,$(RT_SRCS)) OBJS += $(patsubst %.c,$(BINDIR)/formrt/%.o,$(FRM_SRCS)) +OBJS += $(patsubst %.c,$(BINDIR)/basdev/%.o,$(DEV_SRCS)) OBJS += $(BINDIR)/basBuild.o STUBOBJS := $(BINDIR)/testStub.o $(BINDIR)/stub/basModuleRes.o @@ -106,6 +108,10 @@ $(BINDIR)/formrt/%.o: $(BASIC)/formrt/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) $(HOSTCC) $(CFLAGS) -c -o $@ $< +$(BINDIR)/basdev/%.o: $(BASIC)/basdev/%.c $(DVX_MAKEFILES) + @mkdir -p $(dir $@) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + $(BINDIR)/stub/%.o: $(BASIC)/stub/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) $(HOSTCC) $(CFLAGS) -c -o $@ $< diff --git a/src/test/ide/ideStubs.c b/src/test/ide/ideStubs.c index 433eccc1..745f87a0 100644 --- a/src/test/ide/ideStubs.c +++ b/src/test/ide/ideStubs.c @@ -32,6 +32,7 @@ // enough to drive the designer's data-binding editors; every other // path fails cleanly. +#include "ideStubs.h" #include "dvxApp.h" #include "dvxPlat.h" #include "shellApp.h" diff --git a/src/test/ide/ideStubs.h b/src/test/ide/ideStubs.h new file mode 100644 index 00000000..d38df4c5 --- /dev/null +++ b/src/test/ide/ideStubs.h @@ -0,0 +1,36 @@ +// 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. + +// ideStubs.h -- the spy counters ideStubs.c keeps for the IDE suite + +#ifndef IDESTUBS_H +#define IDESTUBS_H + +#include "dvxPlat.h" + +#include + +// shellOpenHelp calls seen, and the args of the last one. +extern int32_t ideStubHelpCount; +extern char ideStubHelpArgs[DVX_MAX_PATH]; + +#endif // IDESTUBS_H diff --git a/src/test/ide/testIde.c b/src/test/ide/testIde.c index 2101dbaf..46132364 100644 --- a/src/test/ide/testIde.c +++ b/src/test/ide/testIde.c @@ -42,9 +42,11 @@ #define tCase(name) (printf("CASE %s\n", (name)), (tCase)(name)) #include "ideMain.c" +#include "ideStubs.h" #include "treeView/treeView.h" #include "spinner/spinner.h" +#include #include #include #include @@ -117,7 +119,7 @@ #define TITLE_INVALID_ICON "Invalid Icon" #define TITLE_PROJECT_WIN "Project" #define PROP_ROW_NAME "Name" -#define MODULE_FRAME_NAME IDE_MODULE_FRAME +#define MODULE_FRAME_NAME BAS_DBG_MODULE_FRAME #define CMD_BUF (DVX_MAX_PATH * 3 + 64) #define LINE_BUF 256 @@ -1475,7 +1477,7 @@ static void runIdle(void *userCtx) { if (sAc->frameCount - sRunStartFrame > RUN_FRAME_LIMIT) { if (sDbgState != DbgIdleE) { - TEST_TRUE(!"run script timed out"); + TEST_FAIL("run script timed out"); onMenu(sWin, CMD_STOP); } @@ -1529,10 +1531,10 @@ static void runScriptKey(RunStepFnT fn, WindowT *win, int32_t scancode, int32_t } -// Copy bin/host/bascomp next to the test binary: it is both the IDE's -// "own executable" (STUB / noicon resources for Make Executable) and the -// reference compiler, and a concurrent rebuild of bin/host must not -// change it mid-suite. +// Copy bin/host/bascomp next to the test binary: it is both the carrier +// of the STUB / noicon resources for Make Executable (copied to where +// the basdev library would be) and the reference compiler, and a +// concurrent rebuild of bin/host must not change it mid-suite. static void snapshotBascomp(void) { int32_t len = 0; char *data = platformReadFile(IDETEST_BASCOMP, &len); @@ -1545,6 +1547,14 @@ static void snapshotBascomp(void) { if (data) { TEST_TRUE(tWriteFile(sBascompPath, data, len)); TEST_EQ(chmod(sBascompPath, EXEC_MODE), 0); + + // A build inside DVX takes them from the basdev library, found + // relative to the DVX folder (here: the working directory) + char carrierDir[DVX_MAX_PATH]; + + basBuildDirOf(BAS_BUILD_STUB_CARRIER, carrierDir, sizeof(carrierDir)); + platformMkdirRecursive(carrierDir); + TEST_TRUE(tWriteFile(BAS_BUILD_STUB_CARRIER, data, len)); free(data); } } @@ -1594,7 +1604,7 @@ static void stepBreakInHandler(void) { dvxRaiseWindow(sAc, sImmWin); wgtSetFocused(sImmediate); tPump(1); - tType(KW_PRINT " " FIXTURE_GLOBAL_NAME); + tType("PRINT " FIXTURE_GLOBAL_NAME); tKeyEnter(); const char *immText = wgtGetText(sImmediate); @@ -1755,6 +1765,7 @@ static void treeSelectFile(int32_t fileIdx) { #include "testIdeDesign.c" #include "testIdeDebug.c" #include "testIdeRegress.c" +#include "testIdeUserCtl.c" int main(void) { @@ -1782,6 +1793,7 @@ int main(void) { runDesignCases(); runDebugCases(); runRegressCases(); + runUserCtlCases(); return tSuiteEnd(); } diff --git a/src/test/ide/testIdeDebug.c b/src/test/ide/testIdeDebug.c index 8203170e..ac412284 100644 --- a/src/test/ide/testIdeDebug.c +++ b/src/test/ide/testIdeDebug.c @@ -754,11 +754,11 @@ static void dgStepWatch(void) { evaluateImmediate("msg = \"a\" + \"b\""); const char *rhsEnd = NULL; - BasValueT *rhsSlot = resolveVarPath("counter", &rhsEnd); + BasValueT *rhsSlot = basDbgResolvePath(sVm, "counter", &rhsEnd); TEST_TRUE(rhsSlot != NULL); TEST_EQ(rhsSlot ? rhsSlot->intVal : -1, 43); - rhsSlot = resolveVarPath("msg", &rhsEnd); + rhsSlot = basDbgResolvePath(sVm, "msg", &rhsEnd); TEST_TRUE(rhsSlot != NULL && rhsSlot->strVal != NULL); TEST_STR_EQ((rhsSlot && rhsSlot->strVal) ? rhsSlot->strVal->data : "", "ab"); evaluateImmediate("counter = 42"); @@ -766,14 +766,14 @@ static void dgStepWatch(void) { evaluateImmediate("PRINT )("); const char *end = NULL; - BasValueT *slot = resolveVarPath("counter", &end); + BasValueT *slot = basDbgResolvePath(sVm, "counter", &end); TEST_TRUE(slot != NULL); TEST_EQ(slot ? slot->intVal : -1, 42); - slot = resolveVarPath("arr(2)", &end); + slot = basDbgResolvePath(sVm, "arr(2)", &end); TEST_TRUE(slot != NULL); TEST_EQ(slot ? slot->intVal : -1, 9); - slot = resolveVarPath("pt.py", &end); + slot = basDbgResolvePath(sVm, "pt.py", &end); TEST_TRUE(slot != NULL); TEST_EQ(slot ? slot->intVal : -1, 8); diff --git a/src/test/ide/testIdeDesign.c b/src/test/ide/testIdeDesign.c index 4359295d..fee23251 100644 --- a/src/test/ide/testIdeDesign.c +++ b/src/test/ide/testIdeDesign.c @@ -70,17 +70,19 @@ #define DP_TEST_COLOR_TEXT "Blue 48" #define DC_MIN_CTRL_SIZE 8 // ideDesigner.c MIN_CTRL_SIZE (private to that file) #define DM_DESC_INPUT 6 // Project Properties: text input index of Description +#define DM_STARTUP_DROPDOWN 1 // Project Properties: dropdown index of Startup Form (Type is 0) #define DF_SAVE_BUF 65536 // .frm save buffer for the data-level cases -#define DF_DEEP_CAP_LEN (FRM_MAX_LINE_LEN - 1 - (DSGN_MAX_MENU_LEVEL + 2) * DSGN_INDENT_SPACES - (int32_t)(sizeof(BAS_PROP_CAPTION " = \"\"") - 1)) // longest caption on the deepest menu's "Caption = \"...\"" line +#define DF_DEEP_CAP_LEN (FRM_MAX_LINE_LEN - 1 - (FRM_MAX_MENU_LEVEL + 2) * FRM_INDENT_SPACES - (int32_t)(sizeof(BAS_PROP_CAPTION " = \"\"") - 1)) // longest caption on the deepest menu's "Caption = \"...\"" line #define DF_LONG_VALUE_LEN 450 // property value longer than the old per-value cap #define DM_POP_MENU "mnuPop" #define DM_POP_RENAMED "mnuPopup" +#define DR_SRC_ROOT IDETEST_FIXTURE_DIR "/../../.." // the source tree +#define DR_FRM_EXT ".frm" +#define DR_MIN_FRMS 30 // the tree holds at least this many forms +#define DR_PATH_MAX 1024 +#define DR_WIN_SIZE 200 // design window of the round-trip case -// Spy counters exported by ideStubs.c. -extern int32_t ideStubHelpCount; -extern char ideStubHelpArgs[DVX_MAX_PATH]; - // Menu editor script state (per pass). static int32_t sMedStep = 0; @@ -92,12 +94,14 @@ static int32_t sPpdErrors = 0; static void caseDsgnBadAndMissingFiles(void); static void caseDsgnCopyPaste(void); +static void caseDsgnCopySubtree(void); static void caseDsgnDeleteTree(void); static void caseDsgnFrmLimits(void); static void caseDsgnFrmRoundTrip(void); static void caseDsgnNestPlace(void); static void caseDsgnPlaceTools(void); static void caseDsgnReorderSubtree(void); +static void caseDsgnRepoRoundTrip(void); static void caseDsgnSelectResizeReorder(void); static void caseFindScopes(void); static void caseGapData(void); @@ -125,6 +129,7 @@ static void dcPlace(const char *toolName, int32_t x, int32_t y); static void dcPrepareDesign(void); static void dcSelectCtrl(int32_t idx); static bool dcToolClick(const char *toolName); +static int32_t dcToolsOutOfOrder(void); static char *dfBuildDeepFrm(int32_t ctrlDepth, int32_t menuLevels, const char *deepCaption, bool autoSize); static int32_t dfCount(const char *hay, const char *needle); static void dmMenuLimitScript(AppContextT *ctx, WindowT *modal, void *userCtx); @@ -133,17 +138,18 @@ static void dmPpdScript(AppContextT *ctx, WindowT *modal, void *userCtx); static void dmSeedMenu(const char *name, int32_t level); static void dmSelectRow(WidgetT *lb, int32_t row); static void dmWriteBmp(const char *path, int32_t w, int32_t h); -static DsgnControlT *dpCtrlByName(const char *name); +static FrmCtrlT *dpCtrlByName(const char *name); static void dpDismissOk(WindowT *modal); static void dpEditRow(const char *rowName, DialogPlanT *plan); static WidgetT *dpFindNthByText(WidgetT *root, const char *text, int32_t nth); static void dpFindRadio(const char *caption); static int32_t dpLayoutIndex(const char *layoutName); static void dpPrefsColors(WindowT *modal); -static void dpSelectByPtr(DsgnControlT *ctrl); +static void dpSelectByPtr(FrmCtrlT *ctrl); static void dpTreeAppend(WidgetT *parent, WidgetT *item); static WidgetT *dpTreeFind(WidgetT *parent, const char *prefix); static void dpTreeInsertFirst(WidgetT *parent, WidgetT *item); +static void drCollectFrms(const char *dir, char ***out); static bool evtListed(const char *name); void runDesignCases(void); @@ -207,13 +213,13 @@ static void caseDsgnCopyPaste(void) { // Enrich the fixture button so the emitted block carries every // optional field the paste parser understands. - DsgnControlT *btn = sDesigner.form->controls[0]; + FrmCtrlT *btn = sDesigner.form->controls[0]; btn->maxWidth = 150; btn->maxHeight = 80; btn->weight = 2; - snprintf(btn->helpTopic, DSGN_MAX_NAME, "%s", "topic.go"); - dsgnSetPropValue(btn, "Text", "GoText"); + snprintf(btn->helpTopic, FRM_MAX_NAME, "%s", "topic.go"); + frmCtrlSetProp(btn, "Text", "GoText"); dcSelectCtrl(0); dcFormKey(KEY_CTRL_C, ACCEL_CTRL); @@ -285,6 +291,72 @@ static void caseDsgnCopyPaste(void) { } +// Copy a container: its children come along as nested blocks; pasting +// it into the same form gives the container and the child free names, +// the child stays inside, and both get live widgets. +static void caseDsgnCopySubtree(void) { + tCase("designer copy/paste of a container with its children"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + fixturePrepare(); + ideStart(true); + openViaRecent(); + dcPrepareDesign(); + + dcPlace(DC_TOOL_FRAME, DC_EMPTY_X, DC_PLACE_Y); + dcLayout(); + + WidgetT *frameWidget = dcCtrlWidget(1); + + TEST_TRUE(frameWidget != NULL); + + if (!frameWidget) { + ideExit(); + tShutdown(); + return; + } + + dcPlace(DC_TOOL_LABEL, frameWidget->x + 4, frameWidget->y + 4); + dcLayout(); + TEST_EQ((int32_t)arrlen(sDesigner.form->controls), 3); + TEST_STR_EQ(sDesigner.form->controls[2]->parentName, "Frame1"); + + sDesigner.selectedIdx = 1; + dcFormKey(KEY_CTRL_C, ACCEL_CTRL); + + int32_t clipLen = 0; + const char *clip = dvxClipboardGet(&clipLen); + + TEST_TRUE(clip != NULL && strncmp(clip, "Begin Frame Frame1\n", strlen("Begin Frame Frame1\n")) == 0); + TEST_TRUE(clip != NULL && strstr(clip, "\n Begin Label Label1\n") != NULL); + + DialogPlanT plan; + + dialogPlanInit(&plan, BTN_NO); + tDialogScript(dcPasteScript, &plan); + dcFormKey(KEY_CTRL_V, ACCEL_CTRL); + tPump(1); + tDialogScript(NULL, NULL); + TEST_EQ((int32_t)arrlen(sDesigner.form->controls), 5); + + int32_t at = frmModelFindCtrl(sDesigner.form, "Frame2", FRM_NO_INDEX); + FrmCtrlT *frame = (at >= 0) ? sDesigner.form->controls[at] : NULL; + FrmCtrlT *child = (at >= 0 && at + 1 < (int32_t)arrlen(sDesigner.form->controls)) ? sDesigner.form->controls[at + 1] : NULL; + + TEST_TRUE(frame != NULL && child != NULL); + TEST_EQ(sDesigner.selectedIdx, at); + + if (frame && child) { + TEST_STR_EQ(child->name, "Label2"); + TEST_STR_EQ(child->parentName, "Frame2"); + TEST_TRUE(dsgnWidget(frame) != NULL && dsgnWidget(child) != NULL); + TEST_TRUE(dsgnWidget(child) && dsgnWidget(child)->parent == dsgnWidget(frame)); + } + + ideExit(); + tShutdown(); +} + + // Delete on a container removes the container and every descendant, // transitively; both the Del key on the form window and Edit / Delete. static void caseDsgnDeleteTree(void) { @@ -379,8 +451,8 @@ static void caseDsgnFrmLimits(void) { TEST_TRUE(dsgnLoadFrm(&ds, arrFrm, (int32_t)strlen(arrFrm))); TEST_EQ((int32_t)arrlen(ds.form->controls), 4); TEST_EQ(ds.form->controls[3]->parentIndex, 1); - TEST_PTR_EQ(dsgnFindParent(ds.form, ds.form->controls[1]), ds.form->controls[0]); - TEST_PTR_EQ(dsgnFindParent(ds.form, ds.form->controls[3]), ds.form->controls[2]); + TEST_PTR_EQ(frmModelFindParent(ds.form, ds.form->controls[1]), ds.form->controls[0]); + TEST_PTR_EQ(frmModelFindParent(ds.form, ds.form->controls[3]), ds.form->controls[2]); len = dsgnSaveFrm(&ds, buf, DF_SAVE_BUF); TEST_TRUE(len > 0); TEST_EQ(dfCount(buf, "Begin Label Label1"), 1); @@ -403,10 +475,10 @@ static void caseDsgnFrmLimits(void) { TEST_TRUE(dsgnLoadFrm(&ds, tabFrm, (int32_t)strlen(tabFrm))); - const char *tabIdx = dsgnControlGetPropValue(ds.form->controls[0], BAS_PROP_TABINDEX); + const char *tabIdx = frmCtrlGetProp(ds.form->controls[0], BAS_PROP_TABINDEX); TEST_STR_EQ(tabIdx ? tabIdx : "", "2"); - TEST_TRUE(dsgnControlGetPropValue(ds.form->controls[1], BAS_PROP_TABINDEX) == NULL); + TEST_TRUE(frmCtrlGetProp(ds.form->controls[1], BAS_PROP_TABINDEX) == NULL); len = dsgnSaveFrm(&ds, buf, DF_SAVE_BUF); TEST_TRUE(len > 0 && strstr(buf, "TabIndex = 2") != NULL); TEST_TRUE(strstr(buf, "TabIndex = 3") == NULL); @@ -449,7 +521,7 @@ static void caseDsgnFrmLimits(void) { memset(longCap, 'x', DF_DEEP_CAP_LEN); longCap[DF_DEEP_CAP_LEN] = '\0'; - char *deep = dfBuildDeepFrm(DSGN_MAX_NEST_DEPTH, DSGN_MAX_MENU_LEVEL + 1, longCap, false); + char *deep = dfBuildDeepFrm(FRM_MAX_NEST_DEPTH, FRM_MAX_MENU_LEVEL + 1, longCap, false); TEST_TRUE(deep != NULL); @@ -462,9 +534,9 @@ static void caseDsgnFrmLimits(void) { int32_t ctrlCount = (int32_t)arrlen(ds.form->controls); int32_t menuCount = (int32_t)arrlen(ds.form->menuItems); - TEST_EQ(ctrlCount, DSGN_MAX_NEST_DEPTH); - TEST_EQ(dsgnControlDepth(ds.form, ds.form->controls[ctrlCount - 1]), DSGN_MAX_NEST_DEPTH); - TEST_EQ(ds.form->menuItems[menuCount - 1].level, DSGN_MAX_MENU_LEVEL); + TEST_EQ(ctrlCount, FRM_MAX_NEST_DEPTH); + TEST_EQ(frmModelCtrlDepth(ds.form, ds.form->controls[ctrlCount - 1]), FRM_MAX_NEST_DEPTH); + TEST_EQ(ds.form->menuItems[menuCount - 1].level, FRM_MAX_MENU_LEVEL); len = dsgnSaveFrm(&ds, buf, DF_SAVE_BUF); TEST_TRUE(len > 0); TEST_TRUE(dsgnLoadFrm(&ds, buf, len)); @@ -473,7 +545,7 @@ static void caseDsgnFrmLimits(void) { if (ds.form) { int32_t menuCount = (int32_t)arrlen(ds.form->menuItems); - TEST_EQ((int32_t)arrlen(ds.form->controls), DSGN_MAX_NEST_DEPTH); + TEST_EQ((int32_t)arrlen(ds.form->controls), FRM_MAX_NEST_DEPTH); TEST_EQ((int32_t)strlen(ds.form->menuItems[menuCount - 1].caption), DF_DEEP_CAP_LEN); } @@ -481,7 +553,7 @@ static void caseDsgnFrmLimits(void) { // of writing a line the parser would cut. longCap[DF_DEEP_CAP_LEN] = 'x'; longCap[DF_DEEP_CAP_LEN + 1] = '\0'; - deep = dfBuildDeepFrm(DSGN_MAX_NEST_DEPTH, DSGN_MAX_MENU_LEVEL + 1, longCap, false); + deep = dfBuildDeepFrm(FRM_MAX_NEST_DEPTH, FRM_MAX_MENU_LEVEL + 1, longCap, false); TEST_TRUE(deep != NULL); if (deep) { @@ -493,7 +565,7 @@ static void caseDsgnFrmLimits(void) { int32_t menuCount = (int32_t)arrlen(ds.form->menuItems); TEST_EQ((int32_t)strlen(ds.form->menuItems[menuCount - 1].caption), DF_DEEP_CAP_LEN + 1); - TEST_EQ(dsgnSaveFrm(&ds, buf, DF_SAVE_BUF), DSGN_SAVE_LINE_TOO_LONG); + TEST_EQ(dsgnSaveFrm(&ds, buf, DF_SAVE_BUF), FRM_WRITE_LINE_TOO_LONG); } // A long value on a shallow line (any line the parser accepts) is kept @@ -511,7 +583,7 @@ static void caseDsgnFrmLimits(void) { TEST_TRUE(dsgnLoadFrm(&ds, longFrm, (int32_t)strlen(longFrm))); if (ds.form && arrlen(ds.form->controls) == 1) { - const char *rs = dsgnControlGetPropValue(ds.form->controls[0], "RecordSource"); + const char *rs = frmCtrlGetProp(ds.form->controls[0], "RecordSource"); TEST_TRUE(rs != NULL && strcmp(rs, longSql) == 0); len = dsgnSaveFrm(&ds, buf, DF_SAVE_BUF); @@ -522,7 +594,7 @@ static void caseDsgnFrmLimits(void) { } // One control block deeper is beyond the parser. - deep = dfBuildDeepFrm(DSGN_MAX_NEST_DEPTH + 1, 0, "", false); + deep = dfBuildDeepFrm(FRM_MAX_NEST_DEPTH + 1, 0, "", false); if (deep) { TEST_TRUE(!dsgnLoadFrm(&ds, deep, (int32_t)strlen(deep))); @@ -537,10 +609,10 @@ static void caseDsgnFrmLimits(void) { " Caption = \"L\"\n" " End\n" "End\n"; - char autoName[DSGN_MAX_NAME]; + char autoName[FRM_MAX_NAME]; TEST_TRUE(dsgnLoadFrm(&ds, menuNameFrm, (int32_t)strlen(menuNameFrm))); - dsgnAutoName(&ds, DC_TOOL_LABEL, autoName, sizeof(autoName)); + frmModelAutoName(ds.form, DC_TOOL_LABEL, autoName, sizeof(autoName)); TEST_STR_EQ(autoName, "Label2"); // One selected OptionButton per container group: on load the last of @@ -564,18 +636,18 @@ static void caseDsgnFrmLimits(void) { TEST_TRUE(dsgnLoadFrm(&ds, optionFrm, (int32_t)strlen(optionFrm))); if (ds.form && arrlen(ds.form->controls) == 4) { - DsgnControlT *opt1 = ds.form->controls[1]; - DsgnControlT *opt2 = ds.form->controls[2]; - DsgnControlT *opt3 = ds.form->controls[3]; + FrmCtrlT *opt1 = ds.form->controls[1]; + FrmCtrlT *opt2 = ds.form->controls[2]; + FrmCtrlT *opt3 = ds.form->controls[3]; - TEST_TRUE(!frmParseBool(dsgnControlGetPropValue(opt1, "Value"))); - TEST_TRUE(frmParseBool(dsgnControlGetPropValue(opt2, "Value"))); - TEST_TRUE(frmParseBool(dsgnControlGetPropValue(opt3, "Value"))); + TEST_TRUE(!frmParseBool(frmCtrlGetProp(opt1, "Value"))); + TEST_TRUE(frmParseBool(frmCtrlGetProp(opt2, "Value"))); + TEST_TRUE(frmParseBool(frmCtrlGetProp(opt3, "Value"))); - dsgnSetPropValue(opt1, "Value", "True"); - dsgnOptionSelected(ds.form, opt1); - TEST_TRUE(!frmParseBool(dsgnControlGetPropValue(opt2, "Value"))); - TEST_TRUE(frmParseBool(dsgnControlGetPropValue(opt3, "Value"))); + frmCtrlSetProp(opt1, "Value", "True"); + frmModelOptionSelected(ds.form, opt1); + TEST_TRUE(!frmParseBool(frmCtrlGetProp(opt2, "Value"))); + TEST_TRUE(frmParseBool(frmCtrlGetProp(opt3, "Value"))); len = dsgnSaveFrm(&ds, buf, DF_SAVE_BUF); TEST_TRUE(len > 0); @@ -596,21 +668,21 @@ static void caseDsgnFrmLimits(void) { TEST_TRUE(dsgnLoadFrm(&ds, arrayFrm, (int32_t)strlen(arrayFrm))); if (ds.form && arrlen(ds.form->controls) == 2) { - DsgnControlT *frame = ds.form->controls[0]; - DsgnControlT *label = ds.form->controls[1]; - DsgnControlT *other = (DsgnControlT *)calloc(1, sizeof(DsgnControlT)); + FrmCtrlT *frame = ds.form->controls[0]; + FrmCtrlT *label = ds.form->controls[1]; + FrmCtrlT *other = (FrmCtrlT *)calloc(1, sizeof(FrmCtrlT)); - dsgnMakeArrayMember(ds.form, frame, 0); + frmModelMakeArrayMember(ds.form, frame, 0); TEST_EQ(frame->index, 0); TEST_EQ(label->parentIndex, 0); if (other) { - dsgnControlInit(other); + frmCtrlInit(other); snprintf(other->name, sizeof(other->name), "%s", frame->name); snprintf(other->typeName, sizeof(other->typeName), "%s", frame->typeName); other->index = 1; arrins(ds.form->controls, 0, other); - TEST_PTR_EQ(dsgnFindParent(ds.form, label), frame); + TEST_PTR_EQ(frmModelFindParent(ds.form, label), frame); } } @@ -633,54 +705,54 @@ static void caseDsgnFrmRoundTrip(void) { openViaRecent(); dcPrepareDesign(); - DsgnFormT *form = sDesigner.form; + FrmModelT *form = sDesigner.form; // Menu tree: File (with Open, separator, sub menu Recent > First // [checked]), popup-only Context menu with a radio item, and a // disabled top-level item. - DsgnMenuItemT mi; + FrmMenuItemT mi; - dsgnMenuItemInit(&mi); - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "&File"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuFile"); + frmMenuItemInit(&mi); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "&File"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuFile"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.level = 1; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "&Open"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuOpen"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "&Open"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuOpen"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.level = 1; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "-"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuSep1"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "-"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuSep1"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.level = 1; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "&Recent"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuRecent"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "&Recent"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuRecent"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.level = 2; mi.checked = true; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "First"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuFirst"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "First"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuFirst"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.visible = false; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "Context"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuContext"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "Context"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuContext"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.level = 1; mi.radioCheck = true; mi.checked = true; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "Mode"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuMode"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "Mode"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuMode"); arrput(form->menuItems, mi); - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.enabled = false; - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", "Off"); - snprintf(mi.name, DSGN_MAX_NAME, "%s", "mnuOff"); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", "Off"); + snprintf(mi.name, FRM_MAX_NAME, "%s", "mnuOff"); arrput(form->menuItems, mi); // Controls: label with an enum interface property, checkbox with a @@ -690,28 +762,28 @@ static void caseDsgnFrmRoundTrip(void) { dcPlace(DC_TOOL_CHECKBOX, DC_EMPTY_X, DC_PLACE_Y); dcPlace(DC_TOOL_TIMER, DC_EMPTY_X, DC_PLACE_Y); - DsgnControlT *label = sDesigner.form->controls[1]; - DsgnControlT *check = sDesigner.form->controls[2]; - DsgnControlT *timer = sDesigner.form->controls[3]; + FrmCtrlT *label = sDesigner.form->controls[1]; + FrmCtrlT *check = sDesigner.form->controls[2]; + FrmCtrlT *timer = sDesigner.form->controls[3]; - dsgnSetPropValue(label, "Alignment", "Center"); - dsgnSetPropValue(check, "Value", "True"); - dsgnSetPropValue(timer, "Interval", "250"); - dsgnSetPropValue(label, "Custom", "raw value"); + frmCtrlSetProp(label, "Alignment", "Center"); + frmCtrlSetProp(check, "Value", "True"); + frmCtrlSetProp(timer, "Interval", "250"); + frmCtrlSetProp(label, "Custom", "raw value"); - DsgnControlT *btn = sDesigner.form->controls[0]; + FrmCtrlT *btn = sDesigner.form->controls[0]; btn->visible = false; btn->enabled = false; btn->left = 7; btn->top = 9; - snprintf(btn->helpTopic, DSGN_MAX_NAME, "%s", "topic.btn"); + snprintf(btn->helpTopic, FRM_MAX_NAME, "%s", "topic.btn"); // Form-level non-defaults. form->centered = false; form->left = 11; form->top = 13; - snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", "topic.form"); + snprintf(form->helpTopic, FRM_MAX_NAME, "%s", "topic.form"); form->dirty = true; sProject.files[FILE_FORM].modified = true; @@ -754,7 +826,7 @@ static void caseDsgnFrmRoundTrip(void) { TEST_EQ(form->left, 11); TEST_TRUE(!form->centered); - const char *align = dsgnControlGetPropValue(form->controls[1], "Alignment"); + const char *align = frmCtrlGetProp(form->controls[1], "Alignment"); TEST_STR_EQ(align ? align : "", "Center"); @@ -782,7 +854,7 @@ static void caseDsgnNestPlace(void) { tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); fixturePrepare(); - char *deep = dfBuildDeepFrm(DSGN_MAX_NEST_DEPTH, 0, "", true); + char *deep = dfBuildDeepFrm(FRM_MAX_NEST_DEPTH, 0, "", true); TEST_TRUE(deep != NULL); @@ -795,14 +867,14 @@ static void caseDsgnNestPlace(void) { openViaRecent(); dcPrepareDesign(); - char innerName[DSGN_MAX_NAME]; - char outerName[DSGN_MAX_NAME]; + char innerName[FRM_MAX_NAME]; + char outerName[FRM_MAX_NAME]; - snprintf(innerName, sizeof(innerName), "Frame%d", (int)DSGN_MAX_NEST_DEPTH); - snprintf(outerName, sizeof(outerName), "Frame%d", (int)(DSGN_MAX_NEST_DEPTH - 1)); + snprintf(innerName, sizeof(innerName), "Frame%d", (int)FRM_MAX_NEST_DEPTH); + snprintf(outerName, sizeof(outerName), "Frame%d", (int)(FRM_MAX_NEST_DEPTH - 1)); - DsgnControlT *inner = dpCtrlByName(innerName); - WidgetT *w = inner ? inner->widget : NULL; + FrmCtrlT *inner = dpCtrlByName(innerName); + WidgetT *w = inner ? dsgnWidget(inner) : NULL; TEST_TRUE(w != NULL && w->w > 0 && w->h > 0); @@ -810,7 +882,7 @@ static void caseDsgnNestPlace(void) { dcPlace(DC_TOOL_LABEL, w->x + w->w / 2, w->y + w->h / 2); dcLayout(); - DsgnControlT *label = dpCtrlByName("Label1"); + FrmCtrlT *label = dpCtrlByName("Label1"); TEST_TRUE(label != NULL); TEST_STR_EQ(label ? label->parentName : "", outerName); @@ -831,7 +903,7 @@ static void caseDsgnNestPlace(void) { dsgnInit(&ds, sAc); TEST_TRUE(dsgnLoadFrm(&ds, text, len)); - TEST_EQ(ds.form ? (int32_t)arrlen(ds.form->controls) : 0, DSGN_MAX_NEST_DEPTH + 1); + TEST_EQ(ds.form ? (int32_t)arrlen(ds.form->controls) : 0, FRM_MAX_NEST_DEPTH + 1); dsgnFree(&ds); free(text); } @@ -852,6 +924,10 @@ static void caseDsgnPlaceTools(void) { dcPrepareDesign(); TEST_EQ((int32_t)arrlen(sDesigner.form->controls), 1); + // The tools are in alphabetical order of type name. + TEST_EQ(dcToolsOutOfOrder(), 0); + TEST_STR_EQ(sDesigner.activeTool, ""); + // Tool toggle: select then reselect deselects. TEST_TRUE(dcToolClick(DC_TOOL_LABEL)); TEST_STR_EQ(sDesigner.activeTool, DC_TOOL_LABEL); @@ -887,7 +963,7 @@ static void caseDsgnPlaceTools(void) { // A frame whose Layout is HBox gets a tagged content box exactly // once: place two children into it. - dsgnSetPropValue(sDesigner.form->controls[5], "Layout", "HBox"); + frmCtrlSetProp(sDesigner.form->controls[5], "Layout", "HBox"); frameWidget = dcCtrlWidget(5); TEST_TRUE(frameWidget != NULL); dcPlace(DC_TOOL_LABEL, frameWidget->x + 4, frameWidget->y + 4); @@ -921,8 +997,8 @@ static void caseDsgnReorderSubtree(void) { dcPlace(DC_TOOL_FRAME, DC_EMPTY_X, DC_PLACE_Y); dcLayout(); - DsgnControlT *frame = dpCtrlByName("Frame1"); - WidgetT *fw = frame ? frame->widget : NULL; + FrmCtrlT *frame = dpCtrlByName("Frame1"); + WidgetT *fw = frame ? dsgnWidget(frame) : NULL; TEST_TRUE(fw != NULL); @@ -937,12 +1013,12 @@ static void caseDsgnReorderSubtree(void) { dcPlace(DC_TOOL_LABEL, DC_EMPTY_X, DC_EMPTY_Y); dcLayout(); - DsgnControlT *child = dpCtrlByName("Label1"); - DsgnControlT *last = dpCtrlByName("Label2"); + FrmCtrlT *child = dpCtrlByName("Label1"); + FrmCtrlT *last = dpCtrlByName("Label2"); TEST_TRUE(child != NULL && last != NULL); - if (!child || !last || !last->widget) { + if (!child || !last || !dsgnWidget(last)) { ideExit(); tShutdown(); return; @@ -956,10 +1032,10 @@ static void caseDsgnReorderSubtree(void) { // Drag the last label above the frame's midpoint: it passes the frame // and its child as one block, keeping its own widget. - WidgetT *lastWidget = last->widget; + WidgetT *lastWidget = dsgnWidget(last); int32_t dragX = lastWidget->x + lastWidget->w / 2; - fw = frame->widget; + fw = dsgnWidget(frame); tAdvanceClock(DC_CLICK_GAP_MS); dcFormMouse(dragX, lastWidget->y + lastWidget->h / 2, true); dcFormMouse(dragX, fw->y, true); @@ -967,15 +1043,76 @@ static void caseDsgnReorderSubtree(void) { TEST_PTR_EQ(sDesigner.form->controls[1], last); TEST_PTR_EQ(sDesigner.form->controls[2], frame); TEST_PTR_EQ(sDesigner.form->controls[3], child); - TEST_PTR_EQ(last->widget, lastWidget); + TEST_PTR_EQ(dsgnWidget(last), lastWidget); dcLayout(); - TEST_TRUE(lastWidget->y < frame->widget->y); + TEST_TRUE(lastWidget->y < dsgnWidget(frame)->y); ideExit(); tShutdown(); } +// Every .frm of the source tree opened in a design window (live widgets +// made for every control) saves exactly the text the .frm model writes: +// what the widgets hold by default (a PictureBox's BorderStyle, a +// TextBox's MaxLength ...) is never written. +static void caseDsgnRepoRoundTrip(void) { + tCase("every .frm of the tree saves as the model writes it, widgets or not"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + char **paths = NULL; + char *buf = (char *)malloc(DF_SAVE_BUF); + + drCollectFrms(DR_SRC_ROOT, &paths); + TEST_TRUE((int32_t)arrlen(paths) >= DR_MIN_FRMS); + + for (int32_t i = 0; buf && i < (int32_t)arrlen(paths); i++) { + int32_t len = 0; + char *src = platformReadFile(paths[i], &len); + FrmModelT *m = src ? frmModelLoad(src, len) : NULL; + char *model = NULL; + DsgnStateT ds; + WidgetT *root = NULL; + WidgetT *box = NULL; + + dsgnInit(&ds, tCtx()); + TEST_TRUE(m != NULL && src && dsgnLoadFrm(&ds, src, len)); + model = m ? frmModelText(m, &len) : NULL; + + WindowT *win = ds.form ? basFormRtCreateFormWindow(tCtx(), "rt", ds.form->layout, true, false, false, DR_WIN_SIZE, DR_WIN_SIZE, 0, 0, &root, &box) : NULL; + + if (win) { + dsgnCreateWidgets(&ds, box); + } + + int32_t n = ds.form ? dsgnSaveFrm(&ds, buf, DF_SAVE_BUF) : -1; + bool same = model && n >= 0 && strcmp(buf, model) == 0; + + if (!same) { + printf(" designer save differs: %s\n", paths[i]); + } + + TEST_TRUE(same); + TEST_TRUE(n < 0 || strstr(buf, "BorderStyle = FixedSingle") == NULL || strstr(src, "BorderStyle = FixedSingle") != NULL); + + if (win) { + dvxDestroyWindow(tCtx(), win); + tPump(1); + } + + dsgnFree(&ds); + frmModelFree(m); + free(model); + free(src); + free(paths[i]); + } + + arrfree(paths); + free(buf); + tShutdown(); +} + + // Selection, resize handles (E, S, SE, cursor shapes, minimum size) and // drag-reorder of siblings on the canvas. static void caseDsgnSelectResizeReorder(void) { @@ -1050,7 +1187,7 @@ static void caseDsgnSelectResizeReorder(void) { // Drag-reorder: pull the button below the first label. Only plain // coordinates captured beforehand are used after the move (the layout // has not run yet). - DsgnControlT *first = sDesigner.form->controls[0]; + FrmCtrlT *first = sDesigner.form->controls[0]; WidgetT *below = dcCtrlWidget(1); w = dcCtrlWidget(0); @@ -1226,13 +1363,13 @@ static void caseGapData(void) { dcPlace(DC_TOOL_DATA, DC_EMPTY_X, DC_PLACE_Y); dcLayout(); - DsgnControlT *textBox = sDesigner.form->controls[1]; - DsgnControlT *data = sDesigner.form->controls[2]; + FrmCtrlT *textBox = sDesigner.form->controls[1]; + FrmCtrlT *data = sDesigner.form->controls[2]; - dsgnSetPropValue(data, "DatabaseName", "stub.db"); - dsgnSetPropValue(data, "RecordSource", "tbl"); - dsgnSetPropValue(textBox, "DataSource", "Data1"); - dsgnSetPropValue(textBox, "DataField", "colB"); + frmCtrlSetProp(data, "DatabaseName", "stub.db"); + frmCtrlSetProp(data, "RecordSource", "tbl"); + frmCtrlSetProp(textBox, "DataSource", "Data1"); + frmCtrlSetProp(textBox, "DataField", "colB"); DialogPlanT plan; @@ -1243,7 +1380,7 @@ static void caseGapData(void) { plan.choiceIndex = 1; dpEditRow("RecordSource", &plan); - const char *rec = dsgnControlGetPropValue(data, "RecordSource"); + const char *rec = frmCtrlGetProp(data, "RecordSource"); TEST_STR_EQ(rec ? rec : "", "tblA"); @@ -1252,7 +1389,7 @@ static void caseGapData(void) { plan.choiceTitle = "RecordSource"; plan.choiceIndex = -1; dpEditRow("RecordSource", &plan); - rec = dsgnControlGetPropValue(data, "RecordSource"); + rec = frmCtrlGetProp(data, "RecordSource"); TEST_STR_EQ(rec ? rec : "", "tblA"); // (none) clears it. @@ -1260,9 +1397,9 @@ static void caseGapData(void) { plan.choiceTitle = "RecordSource"; plan.choiceIndex = 0; dpEditRow("RecordSource", &plan); - rec = dsgnControlGetPropValue(data, "RecordSource"); + rec = frmCtrlGetProp(data, "RecordSource"); TEST_STR_EQ(rec ? rec : "", ""); - dsgnSetPropValue(data, "RecordSource", "tblA"); + frmCtrlSetProp(data, "RecordSource", "tblA"); prpRefresh(&sDesigner); tPump(1); @@ -1274,17 +1411,17 @@ static void caseGapData(void) { plan.choiceIndex = 1; dpEditRow("DataField", &plan); - const char *fld = dsgnControlGetPropValue(textBox, "DataField"); + const char *fld = frmCtrlGetProp(textBox, "DataField"); TEST_STR_EQ(fld ? fld : "", "colA"); // A RecordSource that is already a SELECT keeps its own query. - dsgnSetPropValue(data, "RecordSource", "SELECT colA FROM tblA"); + frmCtrlSetProp(data, "RecordSource", "SELECT colA FROM tblA"); dialogPlanInit(&plan, BTN_NO); plan.choiceTitle = "DataField"; plan.choiceIndex = 2; dpEditRow("DataField", &plan); - fld = dsgnControlGetPropValue(textBox, "DataField"); + fld = frmCtrlGetProp(textBox, "DataField"); TEST_STR_EQ(fld ? fld : "", "colB"); // KeyColumn on the Data control itself. @@ -1294,7 +1431,7 @@ static void caseGapData(void) { plan.choiceIndex = 1; dpEditRow("KeyColumn", &plan); - const char *key = dsgnControlGetPropValue(data, "KeyColumn"); + const char *key = frmCtrlGetProp(data, "KeyColumn"); TEST_STR_EQ(key ? key : "", "colA"); @@ -1615,7 +1752,7 @@ static void caseHelpQuery(void) { tPump(1); helpQueryHandler(NULL); TEST_STR_EQ(sDxe.helpTopic, "ide.toolbox"); - snprintf(sDesigner.activeTool, DSGN_MAX_NAME, "%s", DC_TOOL_LABEL); + snprintf(sDesigner.activeTool, FRM_MAX_NAME, "%s", DC_TOOL_LABEL); helpQueryHandler(NULL); TEST_STR_EQ(sDxe.helpTopic, "ctrl.label"); sDesigner.activeTool[0] = '\0'; @@ -1722,7 +1859,7 @@ static void caseMenuEd(void) { TEST_TRUE((int32_t)arrlen(sDesigner.form->menuItems) >= 4); TEST_TRUE(sFormWin != NULL && sFormWin->menuBar != NULL); - DsgnMenuItemT *found = NULL; + FrmMenuItemT *found = NULL; for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { if (strcasecmp(sDesigner.form->menuItems[i].name, "mnuMain") == 0) { @@ -1785,8 +1922,8 @@ static void caseMenuEdLimits(void) { openViaRecent(); dcPrepareDesign(); - DsgnFormT *form = sDesigner.form; - char name[DSGN_MAX_NAME]; + FrmModelT *form = sDesigner.form; + char name[FRM_MAX_NAME]; // Pass 0: a chain down to the deepest level, ending in two siblings // there; neither the leaf nor the chain's root may indent further. @@ -1796,12 +1933,12 @@ static void caseMenuEdLimits(void) { dmSeedMenu("mnuSib", 1); dmSeedMenu("mnuX", 1); - for (int32_t lv = 2; lv <= DSGN_MAX_MENU_LEVEL; lv++) { + for (int32_t lv = 2; lv <= FRM_MAX_MENU_LEVEL; lv++) { snprintf(name, sizeof(name), "mnuL%d", (int)lv); dmSeedMenu(name, lv); } - dmSeedMenu("mnuLeaf", DSGN_MAX_MENU_LEVEL); + dmSeedMenu("mnuLeaf", FRM_MAX_MENU_LEVEL); sMedStep = 0; sMedErrors = 0; @@ -1818,7 +1955,7 @@ static void caseMenuEdLimits(void) { maxLevel = DVX_MAX(form->menuItems[i].level, maxLevel); } - TEST_EQ(maxLevel, DSGN_MAX_MENU_LEVEL); + TEST_EQ(maxLevel, FRM_MAX_MENU_LEVEL); TEST_EQ(form->menuItems[2].level, 1); char *buf = (char *)malloc(DF_SAVE_BUF); @@ -1892,8 +2029,8 @@ static void caseMenuEdLimits(void) { dmSeedMenu(DM_POP_MENU, 0); form->menuItems[0].visible = false; dmSeedMenu("mnuItem", 1); - snprintf(form->contextMenu, DSGN_MAX_NAME, "%s", DM_POP_MENU); - dsgnSetPropValue(form->controls[0], BAS_PROP_CONTEXTMENU, DM_POP_MENU); + snprintf(form->contextMenu, FRM_MAX_NAME, "%s", DM_POP_MENU); + frmCtrlSetProp(form->controls[0], BAS_PROP_CONTEXTMENU, DM_POP_MENU); sMedStep = 0; sMedPass = 4; tDialogScript(dmMenuLimitScript, NULL); @@ -1903,7 +2040,7 @@ static void caseMenuEdLimits(void) { TEST_STR_EQ(form->menuItems[0].name, DM_POP_RENAMED); TEST_STR_EQ(form->contextMenu, DM_POP_RENAMED); - const char *ref = dsgnControlGetPropValue(form->controls[0], BAS_PROP_CONTEXTMENU); + const char *ref = frmCtrlGetProp(form->controls[0], BAS_PROP_CONTEXTMENU); TEST_STR_EQ(ref ? ref : "", DM_POP_RENAMED); @@ -2421,36 +2558,36 @@ static void casePropGrid(void) { dcPlace(DC_TOOL_FRAME, DC_EMPTY_X, DC_PLACE_Y); dcLayout(); - DsgnControlT *textBox = sDesigner.form->controls[1]; - DsgnControlT *label = sDesigner.form->controls[2]; - DsgnControlT *timer = sDesigner.form->controls[3]; + FrmCtrlT *textBox = sDesigner.form->controls[1]; + FrmCtrlT *label = sDesigner.form->controls[2]; + FrmCtrlT *timer = sDesigner.form->controls[3]; WidgetT *frameW = dcCtrlWidget(4); TEST_TRUE(frameW != NULL); dcPlace(DC_TOOL_CHECKBOX, frameW->x + 4, frameW->y + 4); dcLayout(); - DsgnControlT *frame = sDesigner.form->controls[4]; - DsgnControlT *child = sDesigner.form->controls[5]; + FrmCtrlT *frame = sDesigner.form->controls[4]; + FrmCtrlT *child = sDesigner.form->controls[5]; DialogPlanT plan; // Seed the data-binding rows the way a loaded .frm would. - dsgnSetPropValue(textBox, "DataSource", ""); - dsgnSetPropValue(textBox, "DataField", ""); + frmCtrlSetProp(textBox, "DataSource", ""); + frmCtrlSetProp(textBox, "DataField", ""); // DataSource with no Data control on the form: refused silently. dpSelectByPtr(textBox); dialogPlanInit(&plan, BTN_NO); dpEditRow("DataSource", &plan); - const char *seeded = dsgnControlGetPropValue(textBox, "DataSource"); + const char *seeded = frmCtrlGetProp(textBox, "DataSource"); TEST_STR_EQ(seeded ? seeded : "(missing)", ""); dcPlace(DC_TOOL_DATA, DC_EMPTY_X, DC_PLACE_Y); dcLayout(); - DsgnControlT *data = sDesigner.form->controls[6]; + FrmCtrlT *data = sDesigner.form->controls[6]; // Read-only rows do nothing. dpSelectByPtr(textBox); @@ -2473,17 +2610,17 @@ static void casePropGrid(void) { dialogPlanInit(&plan, BTN_NO); dpEditRow("Visible", &plan); TEST_TRUE(!frame->visible); - TEST_TRUE(frame->widget && frame->widget->visible); - TEST_TRUE(child->widget && child->widget->visible); + TEST_TRUE(dsgnWidget(frame) && dsgnWidget(frame)->visible); + TEST_TRUE(dsgnWidget(child) && dsgnWidget(child)->visible); dpEditRow("Visible", &plan); TEST_TRUE(frame->visible); child->enabled = false; dpEditRow("Enabled", &plan); TEST_TRUE(!frame->enabled); - TEST_TRUE(child->widget && !child->widget->enabled); + TEST_TRUE(dsgnWidget(child) && !dsgnWidget(child)->enabled); dpEditRow("Enabled", &plan); TEST_TRUE(frame->enabled); - TEST_TRUE(child->widget && !child->widget->enabled); + TEST_TRUE(dsgnWidget(child) && !dsgnWidget(child)->enabled); child->enabled = true; dsgnApplyEnabled(sDesigner.form); @@ -2492,7 +2629,7 @@ static void casePropGrid(void) { dialogPlanInit(&plan, BTN_NO); dpEditRow("Alignment", &plan); - const char *align = dsgnControlGetPropValue(label, "Alignment"); + const char *align = frmCtrlGetProp(label, "Alignment"); TEST_STR_EQ(align ? align : "", "Center"); @@ -2506,7 +2643,7 @@ static void casePropGrid(void) { plan.intValue = DP_TIMER_MS; dpEditRow("Interval", &plan); - const char *interval = dsgnControlGetPropValue(timer, "Interval"); + const char *interval = frmCtrlGetProp(timer, "Interval"); TEST_STR_EQ(interval ? interval : "", "500"); @@ -2519,14 +2656,14 @@ static void casePropGrid(void) { dialogPlanInit(&plan, BTN_NO); plan.inputText = "New Caption"; dpEditRow("Caption", &plan); - TEST_TRUE(label->widget && strcmp(wgtGetText(label->widget), "New Caption") == 0); - dsgnSetPropValue(label, "Custom", "one"); + TEST_TRUE(dsgnWidget(label) && strcmp(wgtGetText(dsgnWidget(label)), "New Caption") == 0); + frmCtrlSetProp(label, "Custom", "one"); prpRefresh(&sDesigner); dialogPlanInit(&plan, BTN_NO); plan.inputText = "two"; dpEditRow("Custom", &plan); - const char *custom = dsgnControlGetPropValue(label, "Custom"); + const char *custom = frmCtrlGetProp(label, "Custom"); TEST_STR_EQ(custom ? custom : "", "two"); @@ -2542,7 +2679,7 @@ static void casePropGrid(void) { plan.choiceIndex = 1; dpEditRow("DataSource", &plan); - const char *src = dsgnControlGetPropValue(textBox, "DataSource"); + const char *src = frmCtrlGetProp(textBox, "DataSource"); TEST_STR_EQ(src ? src : "", "Data1"); @@ -2551,14 +2688,14 @@ static void casePropGrid(void) { plan.inputText = "colA"; dpEditRow("DataField", &plan); - const char *fld = dsgnControlGetPropValue(textBox, "DataField"); + const char *fld = frmCtrlGetProp(textBox, "DataField"); TEST_STR_EQ(fld ? fld : "", "colA"); // With DatabaseName/RecordSource set the SQL probe runs (and fails // through the stub SQL API), still falling back to free text. - dsgnSetPropValue(data, "DatabaseName", "test.db"); - dsgnSetPropValue(data, "RecordSource", "tbl"); + frmCtrlSetProp(data, "DatabaseName", "test.db"); + frmCtrlSetProp(data, "RecordSource", "tbl"); dialogPlanInit(&plan, BTN_NO); plan.inputText = "colB"; dpEditRow("DataField", &plan); @@ -2569,7 +2706,7 @@ static void casePropGrid(void) { dialogPlanInit(&plan, BTN_NO); plan.inputText = "orders"; dpEditRow("RecordSource", &plan); - dsgnSetPropValue(data, "DatabaseName", "/abs/test.db"); + frmCtrlSetProp(data, "DatabaseName", "/abs/test.db"); dialogPlanInit(&plan, BTN_NO); plan.inputText = "id"; dpEditRow("KeyColumn", &plan); @@ -2581,7 +2718,7 @@ static void casePropGrid(void) { plan.choiceIndex = dpLayoutIndex("HBox"); dpEditRow("Layout", &plan); - const char *layout = dsgnControlGetPropValue(frame, "Layout"); + const char *layout = frmCtrlGetProp(frame, "Layout"); TEST_STR_EQ(layout ? layout : "", "HBox"); dcLayout(); @@ -2614,7 +2751,7 @@ static void casePropGrid(void) { plan.inputText = "dataMain"; dpEditRow(PROP_ROW_NAME, &plan); - const char *ref = dsgnControlGetPropValue(textBox, "DataSource"); + const char *ref = frmCtrlGetProp(textBox, "DataSource"); TEST_STR_EQ(ref ? ref : "", "dataMain"); @@ -2714,7 +2851,7 @@ static void casePropTree(void) { // Sibling reorder: move the (selected) label above the button. The // selection follows the label to its new index. - DsgnControlT *labelCtrl = dpCtrlByName("Label1"); + FrmCtrlT *labelCtrl = dpCtrlByName("Label1"); labelItem->onClick(labelItem); TEST_EQ(sDesigner.selectedIdx, 2); @@ -3002,7 +3139,7 @@ static WidgetT *dcCtrlWidget(int32_t idx) { return NULL; } - return sDesigner.form->controls[idx]->widget; + return dsgnWidget(sDesigner.form->controls[idx]); } @@ -3113,6 +3250,25 @@ static bool dcToolClick(const char *toolName) { } +// Clicks each toolbox tool in button order (and again to deselect it): +// how many come after a tool whose type name sorts with or after theirs. +static int32_t dcToolsOutOfOrder(void) { + char prev[FRM_MAX_NAME] = ""; + int32_t bad = 0; + + for (WidgetT *row = sToolboxWin->widgetRoot->firstChild; row; row = row->nextSibling) { + for (WidgetT *btn = row->firstChild; btn; btn = btn->nextSibling) { + btn->onClick(btn); + bad += (strcasecmp(prev, sDesigner.activeTool) >= 0) ? 1 : 0; + snprintf(prev, sizeof(prev), "%s", sDesigner.activeTool); + btn->onClick(btn); + } + } + + return bad; +} + + // Generate a .frm with ctrlDepth frames nested in one another // (Frame1..FrameN) and a chain of menuLevels menus (levels 0..N-1, the // deepest captioned deepCaption). Returns malloc'd text. @@ -3125,25 +3281,25 @@ static char *dfBuildDeepFrm(int32_t ctrlDepth, int32_t menuLevels, const char *d return NULL; } - pos = emitClamped(buf, size, pos, "VERSION DVX 1.00\nBegin Form Form1\n AutoSize = %s\n", autoSize ? "True" : "False"); + pos = frmEmitClamped(buf, size, pos, "VERSION DVX 1.00\nBegin Form Form1\n AutoSize = %s\n", autoSize ? "True" : "False"); for (int32_t lv = 0; lv < menuLevels; lv++) { - pos = emitClamped(buf, size, pos, "Begin Menu mnuD%d\n Caption = \"%s\"\n", (int)lv, lv == menuLevels - 1 ? deepCaption : "M"); + pos = frmEmitClamped(buf, size, pos, "Begin Menu mnuD%d\n Caption = \"%s\"\n", (int)lv, lv == menuLevels - 1 ? deepCaption : "M"); } for (int32_t lv = 0; lv < menuLevels; lv++) { - pos = emitClamped(buf, size, pos, "End\n"); + pos = frmEmitClamped(buf, size, pos, "End\n"); } for (int32_t d = 1; d <= ctrlDepth; d++) { - pos = emitClamped(buf, size, pos, "Begin Frame Frame%d\n", (int)d); + pos = frmEmitClamped(buf, size, pos, "Begin Frame Frame%d\n", (int)d); } for (int32_t d = 1; d <= ctrlDepth; d++) { - pos = emitClamped(buf, size, pos, "End\n"); + pos = frmEmitClamped(buf, size, pos, "End\n"); } - emitClamped(buf, size, pos, "End\n"); + frmEmitClamped(buf, size, pos, "End\n"); return buf; } @@ -3531,7 +3687,7 @@ static void dmPpdScript(AppContextT *ctx, WindowT *modal, void *userCtx) { wgtSetText(desc, "About the fixture"); } - WidgetT *dropdown = findOfClass(modal->widgetRoot, sDropdownClass, 0); + WidgetT *dropdown = findOfClass(modal->widgetRoot, sDropdownClass, DM_STARTUP_DROPDOWN); TEST_TRUE(dropdown != NULL); @@ -3580,12 +3736,12 @@ static void dmPpdScript(AppContextT *ctx, WindowT *modal, void *userCtx) { // Minimal 24-bit BMP writer for icon validation tests. // Append a menu item with the given name (also its caption) and level. static void dmSeedMenu(const char *name, int32_t level) { - DsgnMenuItemT mi; + FrmMenuItemT mi; - dsgnMenuItemInit(&mi); + frmMenuItemInit(&mi); mi.level = level; - snprintf(mi.name, DSGN_MAX_NAME, "%s", name); - snprintf(mi.caption, DSGN_MAX_TEXT, "%s", name); + snprintf(mi.name, FRM_MAX_NAME, "%s", name); + snprintf(mi.caption, FRM_MAX_TEXT, "%s", name); arrput(sDesigner.form->menuItems, mi); } @@ -3625,7 +3781,7 @@ static void dmWriteBmp(const char *path, int32_t w, int32_t h) { // Control record by name (first match). -static DsgnControlT *dpCtrlByName(const char *name) { +static FrmCtrlT *dpCtrlByName(const char *name) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { if (strcasecmp(sDesigner.form->controls[i]->name, name) == 0) { return sDesigner.form->controls[i]; @@ -3773,7 +3929,7 @@ static void dpPrefsColors(WindowT *modal) { // Select a control on the canvas by record pointer. -static void dpSelectByPtr(DsgnControlT *ctrl) { +static void dpSelectByPtr(FrmCtrlT *ctrl) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { if (sDesigner.form->controls[i] == ctrl) { sDesigner.selectedIdx = i; @@ -3783,7 +3939,7 @@ static void dpSelectByPtr(DsgnControlT *ctrl) { } } - TEST_TRUE(!"control not found"); + TEST_FAIL("control not found"); } @@ -3834,6 +3990,35 @@ static void dpTreeInsertFirst(WidgetT *parent, WidgetT *item) { } +// Every *.frm file below dir (malloc'd paths appended to *out). +static void drCollectFrms(const char *dir, char ***out) { + DIR *d = opendir(dir); + struct dirent *e; + + while (d && (e = readdir(d)) != NULL) { + char path[DR_PATH_MAX]; + int32_t nameLen = (int32_t)strlen(e->d_name); + int32_t extLen = (int32_t)strlen(DR_FRM_EXT); + + if (e->d_name[0] == '.') { + continue; + } + + snprintf(path, sizeof(path), "%s/%s", dir, e->d_name); + + if (e->d_type == DT_DIR) { + drCollectFrms(path, out); + } else if (nameLen > extLen && strcmp(e->d_name + nameLen - extLen, DR_FRM_EXT) == 0) { + arrput(*out, strdup(path)); + } + } + + if (d) { + closedir(d); + } +} + + // True when the Event dropdown lists event name (implemented, or // bracketed when it has no handler yet). static bool evtListed(const char *name) { @@ -3857,7 +4042,9 @@ void runDesignCases(void) { caseDsgnReorderSubtree(); caseDsgnDeleteTree(); caseDsgnCopyPaste(); + caseDsgnCopySubtree(); caseDsgnFrmRoundTrip(); + caseDsgnRepoRoundTrip(); caseDsgnFrmLimits(); caseDsgnNestPlace(); caseDsgnBadAndMissingFiles(); diff --git a/src/test/ide/testIdeRegress.c b/src/test/ide/testIdeRegress.c index 777916dd..702b495d 100644 --- a/src/test/ide/testIdeRegress.c +++ b/src/test/ide/testIdeRegress.c @@ -103,7 +103,6 @@ #define RG_VIA_MENU_NAME "Area" #define RG_EVENTS_CLASS RG_CLASS_HEADER "Public Event Changed(ByVal n As Integer, s As String)\nEvent Done\n' Event Fake(x)\n" #define RG_EVENTS_MODULE "Dim WithEvents w As " RG_CLASS_NAME ", k As Integer\nPrivate WithEvents w2 As " RG_CLASS_NAME "\n" -#define RG_COLOR_LINE "d = #2000-01-02#: CLOSE #1: Me.Width = 3: CLS: x.Name" // Module variables seen from a SUB: g is hidden (not SHARED), s is DIM // SHARED (and named again by SHARED), h comes in through a SHARED @@ -206,12 +205,12 @@ static void caseRgReplaceSkeleton(void); static void caseRgRunUsesBuffers(void); static void caseRgSkeletonEdits(void); static void caseRgStopThenRun(void); -static void caseRgSyntaxColors(void); static void caseRgValidatorAmbiguous(void); static void caseRgViewDesignOnModule(void); static void caseRgWithEventsDropdowns(void); static bool rgBpOnText(const char *text); static void rgBusyScript(AppContextT *ctx, WindowT *modal, void *userCtx); +static bool rgCodeWinHasRgb(uint32_t rgb); static BasCompilerT *rgCompilerInForm(const char *formName, char *src, int32_t srcSize); static int32_t rgCountToken(const char *text, const char *token); static int32_t rgCountVisibleVar(const BasModuleT *mod, const char *name); @@ -347,8 +346,11 @@ static void caseRgBreakpointShift(void) { treeOpenFile(FILE_MAIN); showProc(-1); + // The code editor shows the breakpoint as a line marker (gutter dot) + TEST_TRUE(!rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); toggleBreakpointLine(RG_GENERAL_BP_LINE); TEST_EQ((int32_t)arrlen(sBreakpoints), 1); + TEST_TRUE(rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); // Go To Line selects the line; Home leaves just the caret at column 0 editorFocus(); @@ -363,6 +365,11 @@ static void caseRgBreakpointShift(void) { tKeyEnter(); TEST_EQ((int32_t)arrlen(sBreakpoints), 1); TEST_EQ(arrlen(sBreakpoints) > 0 ? sBreakpoints[0].codeLine : -1, RG_GENERAL_BP_LINE + 1); + TEST_TRUE(rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); + + toggleBreakpointLine(RG_GENERAL_BP_LINE + 1); + TEST_EQ((int32_t)arrlen(sBreakpoints), 0); + TEST_TRUE(!rgCodeWinHasRgb(IDE_BP_GUTTER_RGB)); ideExit(); tShutdown(); @@ -476,15 +483,15 @@ static void caseRgClassDebugger(void) { // Plain values v = basValEmpty(); - formatValue(&v, buf, sizeof(buf)); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); TEST_STR_EQ(buf, "Empty"); v = basValNull(); - formatValue(&v, buf, sizeof(buf)); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); TEST_STR_EQ(buf, "Null"); v = basValObject(NULL); - formatValue(&v, buf, sizeof(buf)); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); TEST_STR_EQ(buf, "Nothing"); - TEST_STR_EQ(typeNameStr(BAS_TYPE_VARIANT), "Variant"); + TEST_STR_EQ(basDbgTypeName(BAS_TYPE_VARIANT), "Variant"); // A Collection shows its item count BasObjectT *coll = basObjectNew(BAS_OBJ_COLLECTION, 0, NULL); @@ -494,9 +501,9 @@ static void caseRgClassDebugger(void) { if (coll) { v = basValInstance(coll); coll->itemCount = RG_DBG_ITEMS; - formatValue(&v, buf, sizeof(buf)); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); TEST_STR_EQ(buf, "Collection (3 items)"); - TEST_STR_EQ(instanceTypeName(coll), IDE_COLLECTION_TYPE); + TEST_STR_EQ(basDbgInstanceTypeName(sVm, coll), BAS_DBG_COLLECTION_TYPE); coll->itemCount = 0; basValRelease(&v); } @@ -511,7 +518,7 @@ static void caseRgClassDebugger(void) { if (arr) { v.type = BAS_TYPE_ARRAY; v.arrVal = arr; - formatValue(&v, buf, sizeof(buf)); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); TEST_STR_EQ(buf, "Variant(0 To 2) [3]"); basValRelease(&v); } @@ -519,12 +526,12 @@ static void caseRgClassDebugger(void) { // An instance of the class, held by the module-level c and running Bump const BasClassInfoT *cls = &mod->classes[0]; BasObjectT *obj = basObjectNew(0, cls->varCount, NULL); - const BasDebugVarT *cv = findDebugVar(RG_DBG_OBJ_VAR); + const BasDebugVarT *cv = basDbgFindVar(sVm, RG_DBG_OBJ_VAR); TEST_TRUE(obj != NULL && cv != NULL); if (obj && cv) { - BasValueT *cSlot = getDebugVarSlot(cv); + BasValueT *cSlot = basDbgVarSlot(sVm, cv); TEST_TRUE(cSlot != NULL); @@ -533,28 +540,28 @@ static void caseRgClassDebugger(void) { } v = basValInstance(obj); - formatValue(&v, buf, sizeof(buf)); + basDbgFormatValue(sVm, &v, buf, sizeof(buf)); TEST_STR_EQ(buf, "<" RG_DBG_CLASS ">"); // Module level: the members are out of scope, but c.n reaches // the member through the object - int32_t memberIdx = findMemberVarIdx(obj, RG_DBG_MEMBER); - BasValueT watched; - sVm->callDepth = 1; TEST_EQ(rgCountVisibleVar(mod, RG_DBG_MEMBER), 0); TEST_EQ(rgCountVisibleVar(mod, "tag"), 0); - TEST_TRUE(memberIdx >= 0); - if (memberIdx >= 0) { - obj->fields[memberIdx] = basValInteger(RG_DBG_MEMBER_VALUE); + BasValueT *member = basDbgResolvePath(sVm, RG_DBG_OBJ_VAR "." RG_DBG_MEMBER, NULL); + + TEST_TRUE(member != NULL); + + if (member) { + *member = basValInteger(RG_DBG_MEMBER_VALUE); } - memset(&watched, 0, sizeof(watched)); - TEST_TRUE(lookupWatchVar(RG_DBG_OBJ_VAR "." RG_DBG_MEMBER, &watched)); - TEST_EQ(watched.type, BAS_TYPE_INTEGER); - TEST_EQ(watched.intVal, RG_DBG_MEMBER_VALUE); - TEST_TRUE(!lookupWatchVar(RG_DBG_OBJ_VAR ".nosuch", &watched)); + char watched[BAS_DBG_VALUE_LEN]; + + TEST_TRUE(basDbgEval(sVm, RG_DBG_OBJ_VAR "." RG_DBG_MEMBER, watched, sizeof(watched))); + TEST_EQ(atoi(watched), RG_DBG_MEMBER_VALUE); + TEST_PTR_EQ(basDbgResolvePath(sVm, RG_DBG_OBJ_VAR ".nosuch", NULL), NULL); // Inside Bump for obj: both members show, reading obj's slots sVm->callDepth = 2; @@ -563,12 +570,12 @@ static void caseRgClassDebugger(void) { TEST_EQ(rgCountVisibleVar(mod, RG_DBG_MEMBER), 1); TEST_EQ(rgCountVisibleVar(mod, "tag"), 1); - const BasDebugVarT *nv = findDebugVar(RG_DBG_MEMBER); + const BasDebugVarT *nv = basDbgFindVar(sVm, RG_DBG_MEMBER); TEST_TRUE(nv != NULL && nv->scope == SCOPE_MEMBER); if (nv) { - TEST_PTR_EQ(getDebugVarSlot(nv), &obj->fields[nv->index]); + TEST_PTR_EQ(basDbgVarSlot(sVm, nv), &obj->fields[nv->index]); } // A frame running for no object shows no members @@ -810,6 +817,7 @@ static void caseRgDebugSharedScope(void) { if (proc && sVm) { sDbgModule = mod; + sVm->module = mod; sVm->callDepth = RG_SCOPE_CALL_DEPTH; sVm->pc = proc->codeAddr; @@ -819,9 +827,9 @@ static void caseRgDebugSharedScope(void) { TEST_EQ(rgCountVisibleVar(mod, "st"), 1); TEST_EQ(rgCountVisibleVar(mod, "l"), 1); - const BasDebugVarT *hv = findDebugVar("h"); + const BasDebugVarT *hv = basDbgFindVar(sVm, "h"); - TEST_TRUE(hv != NULL && hv->procIndex >= 0 && getDebugVarSlot(hv) == &sVm->globals[hv->index]); + TEST_TRUE(hv != NULL && hv->procIndex >= 0 && basDbgVarSlot(sVm, hv) == &sVm->globals[hv->index]); sVm->callDepth = 1; TEST_EQ(rgCountVisibleVar(mod, "g"), 1); @@ -1064,16 +1072,18 @@ static void caseRgFormVarsAndFrames(void) { TEST_TRUE(sVm != NULL); if (sVm) { + sVm->module = &mod; sVm->currentForm = form2; sVm->currentFormVars = formVals; sVm->currentFormVarCount = RG_FORM_VAR_COUNT; sVm->callDepth = 1; sVm->pc = RG_MODULE_PC; - TEST_PTR_EQ(findDebugVar(RG_FORM_VAR), &vars[1]); - TEST_PTR_EQ(findDebugVar(RG_LOCAL_VAR), NULL); + TEST_PTR_EQ(basDbgFindVar(sVm, RG_FORM_VAR), &vars[1]); + TEST_PTR_EQ(basDbgFindVar(sVm, RG_LOCAL_VAR), NULL); sVm->currentFormVars = NULL; + sVm->module = NULL; basVmDestroy(sVm); } @@ -1380,7 +1390,7 @@ static void caseRgPaste(void) { tDialogScript(NULL, NULL); TEST_EQ((int32_t)arrlen(sDesigner.form->controls), 2); - DsgnControlT *pasted = arrlen(sDesigner.form->controls) > 1 ? sDesigner.form->controls[1] : NULL; + FrmCtrlT *pasted = arrlen(sDesigner.form->controls) > 1 ? sDesigner.form->controls[1] : NULL; TEST_TRUE(pasted != NULL); TEST_TRUE(pasted && pasted->visible && pasted->enabled); @@ -1684,45 +1694,14 @@ static void caseRgStopThenRun(void) { } -// The code window's colors: #date# literals as numbers (a file-number '#' -// is not one), unreserved words such as Width and CLS as keywords, but not -// after a dot (Me.Width), where only reserved words (x.Name) keep theirs. -static void caseRgSyntaxColors(void) { - tCase("regress: date literals and color-only words in the code window"); - - const char *line = RG_COLOR_LINE; - int32_t len = (int32_t)strlen(line); - uint8_t colors[sizeof(RG_COLOR_LINE)]; - - basicColorize(line, len, colors, NULL); - - const char *date = strstr(line, "#2000"); - const char *hash = strstr(line, "#1"); - const char *width = strstr(line, "Width"); - const char *cls = strstr(line, "CLS"); - const char *name = strstr(line, "Name"); - - TEST_EQ(colors[date - line], TEXT_SYNTAX_NUMBER); - TEST_EQ(colors[date - line + (int32_t)strlen("#2000-01-02#") - 1], TEXT_SYNTAX_NUMBER); - TEST_EQ(colors[hash - line], TEXT_SYNTAX_DEFAULT); - TEST_EQ(colors[width - line], TEXT_SYNTAX_DEFAULT); - TEST_EQ(colors[cls - line], TEXT_SYNTAX_KEYWORD); - TEST_EQ(colors[name - line], TEXT_SYNTAX_KEYWORD); - TEST_EQ(classifyWord("PCOPY", 5, false), TEXT_SYNTAX_KEYWORD); - TEST_EQ(classifyWord("Enum", 4, false), TEXT_SYNTAX_KEYWORD); - TEST_EQ(classifyWord("Variant", 7, true), TEXT_SYNTAX_DEFAULT); - TEST_EQ(classifyWord("Variant", 7, false), TEXT_SYNTAX_TYPE); -} - - // The compile-time member validator resolves a control name on the form // being compiled first; elsewhere it skips a name that has different // types on different forms instead of checking the wrong one. static void caseRgValidatorAmbiguous(void) { tCase("regress: validator resolves names per form, skips ambiguous ones"); - IdeValidatorCtxT ctx; - IdeCtrlMapEntryT e; + BasValidatorT ctx; + BasCtrlMapEntryT e; char src[RG_SRC_BUF]; memset(&ctx, 0, sizeof(ctx)); @@ -1738,33 +1717,34 @@ static void caseRgValidatorAmbiguous(void) { arrput(ctx.entries, e); // No compile context (and module-level code): cross-form rule only. - TEST_PTR_EQ(validatorLookupCtrlType(&ctx, RG_SHARED_CTRL), NULL); - TEST_STR_EQ(validatorLookupCtrlType(&ctx, RG_UNIQUE_CTRL), RG_LISTBOX_TYPE); + TEST_PTR_EQ(basValidateLookupCtrlType(&ctx, RG_SHARED_CTRL), NULL); + TEST_STR_EQ(basValidateLookupCtrlType(&ctx, RG_UNIQUE_CTRL), RG_LISTBOX_TYPE); // Inside a form's code its own control wins, as at run time. BasCompilerT *inForm = rgCompilerInForm(RG_FORM1, src, (int32_t)sizeof(src)); ctx.compiler = inForm; - TEST_STR_EQ(validatorLookupCtrlType(&ctx, RG_SHARED_CTRL), RG_TEXTBOX_TYPE); - TEST_STR_EQ(validatorLookupCtrlType(&ctx, RG_UNIQUE_CTRL), RG_LISTBOX_TYPE); + TEST_STR_EQ(basValidateLookupCtrlType(&ctx, RG_SHARED_CTRL), RG_TEXTBOX_TYPE); + TEST_STR_EQ(basValidateLookupCtrlType(&ctx, RG_UNIQUE_CTRL), RG_LISTBOX_TYPE); basCompilerDestroy(inForm); inForm = rgCompilerInForm(RG_FORM2, src, (int32_t)sizeof(src)); ctx.compiler = inForm; - TEST_STR_EQ(validatorLookupCtrlType(&ctx, RG_SHARED_CTRL), RG_LISTBOX_TYPE); + TEST_STR_EQ(basValidateLookupCtrlType(&ctx, RG_SHARED_CTRL), RG_LISTBOX_TYPE); basCompilerDestroy(inForm); inForm = rgCompilerInForm(RG_OLD_FORM_NAME, src, (int32_t)sizeof(src)); ctx.compiler = inForm; - TEST_PTR_EQ(validatorLookupCtrlType(&ctx, RG_SHARED_CTRL), NULL); + TEST_PTR_EQ(basValidateLookupCtrlType(&ctx, RG_SHARED_CTRL), NULL); basCompilerDestroy(inForm); ctx.compiler = NULL; - // Menu items accept exactly the runtime's menu properties: .frm-only - // keys such as Caption are compile errors, not runtime errors. - TEST_TRUE(validatorIsPropValid(&ctx, IDE_MENU_TYPE_NAME, BAS_PROP_CHECKED)); - TEST_TRUE(validatorIsPropValid(&ctx, IDE_MENU_TYPE_NAME, BAS_PROP_ENABLED)); - TEST_TRUE(!validatorIsPropValid(&ctx, IDE_MENU_TYPE_NAME, BAS_PROP_CAPTION)); - TEST_TRUE(!validatorIsPropValid(&ctx, IDE_MENU_TYPE_NAME, BAS_PROP_RADIOCHECK)); - validatorFree(&ctx); + // Menu items accept exactly the runtime's menu properties (Caption + // included); .frm-only keys such as RadioCheck are compile errors, not + // runtime errors. + TEST_TRUE(basValidateIsPropValid(&ctx, FRM_KW_MENU, BAS_PROP_CHECKED)); + TEST_TRUE(basValidateIsPropValid(&ctx, FRM_KW_MENU, BAS_PROP_ENABLED)); + TEST_TRUE(basValidateIsPropValid(&ctx, FRM_KW_MENU, BAS_PROP_CAPTION)); + TEST_TRUE(!basValidateIsPropValid(&ctx, FRM_KW_MENU, BAS_PROP_RADIOCHECK)); + basValidateFree(&ctx); } @@ -1876,6 +1856,35 @@ static void rgBusyScript(AppContextT *ctx, WindowT *modal, void *userCtx) { } +// True when the code window, repainted, has a pixel of colour rgb +// (0xRRGGBB): the editor's line markers are drawn in their own colours. +static bool rgCodeWinHasRgb(uint32_t rgb) { + if (!sCodeWin) { + return false; + } + + dvxInvalidateWindow(sAc, sCodeWin); + tPump(1); + + uint32_t packed = packColor(&sAc->display, DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), DVX_RGB_BLUE(rgb)); + int32_t bpp = sAc->display.format.bytesPerPixel; + + for (int32_t y = 0; y < sCodeWin->contentH; y++) { + for (int32_t x = 0; x < sCodeWin->contentW; x++) { + uint32_t pixel = 0; + + memcpy(&pixel, sCodeWin->contentBuf + y * sCodeWin->contentPitch + x * bpp, (size_t)bpp); + + if (pixel == packed) { + return true; + } + } + } + + return false; +} + + // A compiler whose compilation stopped inside form formName's code (a // BEGINFORM without its ENDFORM): its form scope is that form. src // holds the source while the compiler lives. @@ -1905,12 +1914,12 @@ static int32_t rgCountToken(const char *text, const char *token) { // paused (sVm / sDbgModule set by the caller). static int32_t rgCountVisibleVar(const BasModuleT *mod, const char *name) { int32_t n = 0; - int32_t procIdx = vmCurProcIdx(); + int32_t procIdx = basDbgCurProcIdx(sVm); for (int32_t i = 0; i < mod->debugVarCount; i++) { const BasDebugVarT *dv = &mod->debugVars[i]; - if (strcasecmp(dv->name, name) == 0 && debugVarVisible(dv, procIdx)) { + if (strcasecmp(dv->name, name) == 0 && basDbgVarVisible(sVm, dv, procIdx)) { n++; } } @@ -2133,7 +2142,6 @@ void runRegressCases(void) { caseRgRunUsesBuffers(); caseRgSkeletonEdits(); caseRgStopThenRun(); - caseRgSyntaxColors(); caseRgValidatorAmbiguous(); caseRgViewDesignOnModule(); caseRgWithEventsDropdowns(); diff --git a/src/test/ide/testIdeUserCtl.c b/src/test/ide/testIdeUserCtl.c new file mode 100644 index 00000000..4c6317d5 --- /dev/null +++ b/src/test/ide/testIdeUserCtl.c @@ -0,0 +1,1074 @@ +// 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. + + +// testIdeUserCtl.c -- T6 cases for UserControls (.ctl) in the IDE: File > +// New UserControl, the Controls group, the code window's UserControl +// object and events, the header in the Properties window, the project's +// own controls registered for the designer (toolbox, placing one on a +// form, its properties and events, UserMode False, a separate key from a +// run's), control projects (Type, Make Control byte-identical to +// bascomp), and an installed control file in the toolbox. +// +// Compiled as part of testIde.c (see the include there); every helper +// and the IDE's file-scope state are directly visible. The installed +// control file stays registered for the rest of the process, so +// caseUcControlProject runs last. + +#define UC_NEW_NAME "UserControl1" +#define UC_NEW_FILE UC_NEW_NAME BAS_CTL_EXT +#define UC_RENAMED "meter2" +#define UC_RENAMED_FILE UC_RENAMED BAS_CTL_EXT +#define UC_METER "Meter" +#define UC_METER_FILE "meter.ctl" +#define UC_METER_ICON "meter.bmp" +#define UC_METER_DESC "A meter" +#define UC_METER_PLACED "Meter1" +#define UC_METER_CODE "Public Event Changed(NewValue As Integer)\n" +#define UC_METER_BP_LINE " mValue = n" +#define UC_DIAL "Dial" +#define UC_DIAL_DIR "dialprj" +#define UC_DIAL_DBP "dial.dbp" +#define UC_DIAL_FILE "dial.ctl" +#define UC_DIAL_ICON "dial.bmp" +#define UC_DIAL_DESC "A dial" +#define UC_DIAL_WGT "dial.wgt" +#define UC_DIAL_REF "dialref.wgt" +#define UC_APP_NAME "uc.app" +#define UC_TOOL_ICON_SIZE 24 // a toolbox picture is 24 x 24 +#define UC_DESIGN_MODE 2 // Mode after UserControl_Initialize with UserMode False +#define UC_INIT_VALUE "7" // Value after UserControl_Initialize +#define UC_SET_VALUE 42 // Value set through the property grid +#define UC_SET_VALUE_TEXT "42" +#define UC_WIDTH 120 // Width set through the property grid +#define UC_WIDTH_LINE " Width = 120\n" +#define UC_PICK_RGB 0x102030 // BackColor picked in the color dialog (RGB) +#define UC_PICK_BASIC "&H00302010&" // the same color as BASIC writes it +#define UC_KEY_SEP '#' // a private registry key: name#serial +#define UC_TITLE_PRJPROPS "Project Properties" +#define UC_TITLE_MAKE_CTL "Make Control" +#define UC_TYPE_CONTROL 1 // Project Properties: Type row of a control project +#define UC_NO_EVENT 0 // DefaultEvent choice: (none) +#define UC_RES_CTL0 "CTL0" +#define UC_DOS_FIXTURE IDETEST_FIXTURE_DIR "/../../dos/fixtures/ucide" +#define UC_DOS_DIR "ucide" +#define UC_DOS_DBP "ucide.dbp" +#define UC_DOS_FRM "ucide.frm" +#define UC_DOS_DESC "UCIDE meter" +#define UC_DOS_PLACED "UcMeter1" +#define UC_DOS_VALUE 9 // UcMeter1's Value in ucide.frm +#define UC_DOS_PLACED2 "UcMeter2" +#define UC_DOS_LOAD "Sub UcIde_Load ()" +#define UC_DOS_PASS "UCIDE PASS" // the running form's caption after its Load handler +#define UC_RUN_PASSED 1 // ucStepRunPass saw the caption +#define UC_LOOK_STAR 2 // MeterLookE's Star +#define UC_SPAN_TINY "4" // MeterSpanE's Tiny (its values are not 0, 1, 2 ...: a whole number) + +// A control with a property (Get / Let raising PropertyChanged), a +// public variable showing UserMode at Initialize, an event and handlers. +#define UC_CTL_TEXT(name, desc, icon) \ + "VERSION DVX 1.00\n" \ + "Begin UserControl " name "\n" \ + " Width = 80\n" \ + " Height = 40\n" \ + " ToolboxIcon = \"" icon "\"\n" \ + " Description = \"" desc "\"\n" \ + " DefaultEvent = \"Changed\"\n" \ + "End\n" \ + "Option Explicit\n" \ + UC_METER_CODE \ + "Public Mode As Integer\n" \ + "Private mValue As Integer\n" \ + "\n" \ + "Public Property Get Value() As Integer\n" \ + " Value = mValue\n" \ + "End Property\n" \ + "\n" \ + "Public Property Let Value(n As Integer)\n" \ + UC_METER_BP_LINE "\n" \ + " UserControl.PropertyChanged \"Value\"\n" \ + "End Property\n" \ + "\n" \ + "Private Sub UserControl_Initialize()\n" \ + " mValue = " UC_INIT_VALUE "\n" \ + " If UserControl.UserMode Then\n" \ + " Mode = 1\n" \ + " Else\n" \ + " Mode = 2\n" \ + " End If\n" \ + "End Sub\n" \ + "\n" \ + "Private Sub UserControl_Paint()\n" \ + " UserControl.Cls\n" \ + "End Sub\n" + +// The fixture project with the Meter control +#define UC_FIXTURE_DBP \ + "[Project]\n" \ + "Name = " FIXTURE_NAME "\n" \ + "\n" \ + "[Modules]\n" \ + "File0 = " FIXTURE_BAS "\n" \ + "\n" \ + "[Controls]\n" \ + "File0 = " UC_METER_FILE "\n" \ + "\n" \ + "[Forms]\n" \ + "File0 = " FIXTURE_FRM "\n" \ + "\n" \ + "[Settings]\n" \ + "StartupForm = Form1\n" + +// A project holding only the Dial control (a program until its Type is +// set) +#define UC_DIAL_DBP_TEXT \ + "[Project]\n" \ + "Name = " UC_DIAL "\n" \ + "\n" \ + "[Controls]\n" \ + "File0 = " UC_DIAL_FILE "\n" + + +static void caseUcControlProject(void); +static void caseUcDesigner(void); +static void caseUcDosFixture(void); +static void caseUcEnumProps(void); +static void caseUcHeaderProps(void); +static void caseUcNewControl(void); +static void runUserCtlCases(void); +static void ucColorPick(WindowT *modal); +static int32_t ucEvtIndex(const char *label); +static int32_t ucFileIdx(const char *path); +static void ucObjSelect(const char *name); +static void ucPpdControl(WindowT *modal); +static void ucPrepare(void); +static const char *ucRowValue(const char *rowName); +static void ucStepRunPass(void); +static WidgetT *ucToolByTip(WidgetT *root, const char *tooltip); +static int32_t ucToolCount(WidgetT *root, const char *tooltip); + + +// A control project: Project Properties' Type makes it one, the File +// menu offers Make Control, Run refuses, Make Control writes the same +// resource-only .wgt bascomp does, and that file loads as an installed +// control whose toolbox entry shows its picture and name. +static void caseUcControlProject(void) { + tCase("UserControl: control project, Make Control, installed control file in the toolbox"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + fixturePrepare(); + + char dir[DVX_MAX_PATH]; + char dbp[DVX_MAX_PATH * 2]; + char path[DVX_MAX_PATH * 2]; + char cwd[DVX_MAX_PATH]; + char wgtPath[DVX_MAX_PATH * 2]; + char refPath[DVX_MAX_PATH * 2]; + char cmd[CMD_BUF * 2]; + + // The control file outlives the case's temp dir: the toolbox reads its + // resources through the (process-wide) interface path + TEST_TRUE(getcwd(cwd, sizeof(cwd)) != NULL); + snprintf(wgtPath, sizeof(wgtPath), "%s" DVX_PATH_SEP UC_DIAL_WGT, cwd); + snprintf(refPath, sizeof(refPath), "%s" DVX_PATH_SEP UC_DIAL_REF, tTempDir()); + remove(wgtPath); + + snprintf(dir, sizeof(dir), "%s" DVX_PATH_SEP UC_DIAL_DIR, tTempDir()); + TEST_EQ(platformMkdirRecursive(dir), 0); + snprintf(dbp, sizeof(dbp), "%s" DVX_PATH_SEP UC_DIAL_DBP, dir); + TEST_TRUE(tWriteFile(dbp, UC_DIAL_DBP_TEXT, (int32_t)strlen(UC_DIAL_DBP_TEXT))); + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP UC_DIAL_FILE, dir); + TEST_TRUE(tWriteFile(path, UC_CTL_TEXT(UC_DIAL, UC_DIAL_DESC, UC_DIAL_ICON), (int32_t)strlen(UC_CTL_TEXT(UC_DIAL, UC_DIAL_DESC, UC_DIAL_ICON)))); + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP UC_DIAL_ICON, dir); + dmWriteBmp(path, UC_TOOL_ICON_SIZE, UC_TOOL_ICON_SIZE); + + ideStart(false); + openProjectPath(dbp); + TEST_TRUE(hasProject()); + TEST_TRUE(!prjIsControlProject(&sProject)); + + MenuItemT *makeItem = wmMenuFindItemInMenu(sFileMenu, CMD_MAKE_EXE); + + TEST_TRUE(makeItem != NULL && strstr(makeItem->label, "Executable") != NULL); + + // Project Properties: Type Control + DialogPlanT plan; + + dialogPlanInit(&plan, BTN_NO); + plan.custom = ucPpdControl; + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_PRJ_PROPS); + tDialogScript(NULL, NULL); + TEST_TRUE(prjIsControlProject(&sProject)); + TEST_TRUE(makeItem != NULL && strstr(makeItem->label, "Control") != NULL); + TEST_TRUE(saveProjectFile()); + TEST_TRUE(fileHas(dbp, BAS_INI_KEY_TYPE " = " BAS_PROJECT_TYPE_CONTROL)); + + // A control project is not run + ideMenu(CMD_RUN); + TEST_PTR_EQ(sVm, NULL); + TEST_STR_EQ(wgtGetText(sStatus), IDE_CTL_NO_RUN); + + // Make Control: the file bascomp makes from the same project + dialogPlanInit(&plan, BTN_NO); + plan.fileTitle = UC_TITLE_MAKE_CTL; + plan.fileDir = cwd; + plan.fileName = UC_DIAL_WGT; + plan.fileButton = BTN_SAVE; + plan.choiceIndex = BUILD_MODE_RELEASE; + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_MAKE_EXE); + tDialogScript(NULL, NULL); + TEST_STR_EQ(plan.lastTitle, TITLE_BUILD_MODE); + TEST_EQ(plan.errors, 0); + + FILE *f = fopen(wgtPath, "rb"); + char head[DVX_RES_ONLY_MAGIC_LEN]; + + TEST_TRUE(f && fread(head, 1, sizeof(head), f) == sizeof(head) && memcmp(head, DVX_RES_ONLY_MAGIC, sizeof(head)) == 0); + + if (f) { + fclose(f); + } + + snprintf(cmd, sizeof(cmd), "%s %s -o %s -release > /dev/null", sBascompPath, dbp, refPath); + TEST_EQ(system(cmd), 0); + TEST_TRUE(filesIdentical(wgtPath, refPath)); + + DvxResHandleT *res = dvxResOpen(wgtPath); + uint32_t size = 0; + char *ctl0 = res ? (char *)dvxResRead(res, UC_RES_CTL0, &size) : NULL; + char *name = res ? (char *)dvxResRead(res, BAS_CTL_RES_NAME, &size) : NULL; + void *icon = res ? dvxResRead(res, BAS_CTL_RES_ICON, &size) : NULL; + + TEST_TRUE(ctl0 && strstr(ctl0, "Begin UserControl " UC_DIAL) != NULL && strstr(ctl0, "UserControl_Paint") == NULL); + TEST_STR_EQ(name ? name : "", UC_DIAL_DESC); + TEST_TRUE(icon != NULL); + free(ctl0); + free(name); + free(icon); + + if (res) { + dvxResClose(res); + } + + dialogPlanInit(&plan, BTN_NO); + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_PRJ_CLOSE); + tDialogScript(NULL, NULL); + + // Installed the way the loader does it (basCtlLoadFile, then the + // file's path recorded for its interfaces): the toolbox shows the + // control's picture and name, and it can be placed at design time + TEST_TRUE(basCtlLoadFile(wgtPath)); + + const char *key = wgtResolveTypeName(UC_DIAL); + + TEST_TRUE(key != NULL); + + if (key) { + TEST_STR_EQ(key, UC_DIAL); + wgtIfaceSetPath(key, wgtPath); + } + + openProjectPath(sDbpPath); + TEST_TRUE(hasProject()); + treeOpenFile(ucFileIdx(FIXTURE_FRM)); + dcLayout(); + TEST_TRUE(sToolboxWin != NULL); + + WidgetT *tool = sToolboxWin ? ucToolByTip(sToolboxWin->widgetRoot, UC_DIAL_DESC) : NULL; + + TEST_TRUE(tool != NULL); + TEST_PTR_EQ(sToolboxWin ? tFindByText(sToolboxWin->widgetRoot, UC_DIAL) : NULL, NULL); + + if (tool && tool->onClick) { + tool->onClick(tool); + dcFormClick(DC_EMPTY_X, DC_PLACE_Y); + dcLayout(); + } + + FrmCtrlT *dial = sDesigner.form ? dpCtrlByName(UC_DIAL "1") : NULL; + WgtValueT mode = wgtValueInt(0); + + TEST_TRUE(dial != NULL && dsgnWidget(dial) != NULL); + TEST_TRUE(dial && dsgnWidget(dial) && wgtGetProp(dsgnWidget(dial), "Mode", &mode)); + TEST_EQ(wgtValueToInt(&mode), UC_DESIGN_MODE); + + ideExit(); + tShutdown(); +} + + +// The project's own controls in the designer: registered under a key of +// their own when the project opens, in the toolbox with their picture +// and description, placed on a form as live instances (Initialize saw +// UserMode False), their properties in the grid (PropertyChanged re-reads +// it), their events with parameters and the default event on a +// double-click, the .frm saved; a run registers the program's own copy +// under another key, the toolbox leaves that copy out; a saved .ctl +// re-registers them; the built program carries their headers; closing +// the project lets them go. +static void caseUcDesigner(void) { + tCase("UserControl: project controls in the toolbox and designer, keys, events, save, run, build"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + ucPrepare(); + ideStart(true); + openViaRecent(); + + // Registered for the designer under a key of its own + const WgtIfaceT *own = basUcOwnIface(NULL, UC_METER); + const char *key = basUcResolveType(NULL, UC_METER); + char designKey[BAS_MAX_IDENT * 2]; + + TEST_TRUE(own != NULL); + TEST_TRUE(key != NULL && strchr(key, UC_KEY_SEP) != NULL); + TEST_PTR_EQ(wgtGetIface(UC_METER), NULL); + snprintf(designKey, sizeof(designKey), "%s", key ? key : ""); + + // The toolbox: its picture, its description as the tooltip + int32_t frmIdx = ucFileIdx(FIXTURE_FRM); + int32_t ctlIdx = ucFileIdx(UC_METER_FILE); + + treeOpenFile(frmIdx); + dcLayout(); + TEST_TRUE(sToolboxWin != NULL && sPropsWin != NULL); + + if (!sToolboxWin || !sDesigner.form) { + ideExit(); + tShutdown(); + return; + } + + WidgetT *tool = ucToolByTip(sToolboxWin->widgetRoot, UC_METER_DESC); + + TEST_TRUE(tool != NULL); + TEST_PTR_EQ(tFindByText(sToolboxWin->widgetRoot, UC_METER), NULL); + + // Placed on the form: a live instance whose Initialize saw UserMode False + if (tool && tool->onClick) { + tool->onClick(tool); + dcFormClick(DC_EMPTY_X, DC_PLACE_Y); + dcLayout(); + } + + FrmCtrlT *meter = dpCtrlByName(UC_METER_PLACED); + WgtValueT mode = wgtValueInt(0); + + TEST_TRUE(meter != NULL && dsgnWidget(meter) != NULL); + + if (!meter || !dsgnWidget(meter)) { + ideExit(); + tShutdown(); + return; + } + + TEST_TRUE(wgtGetProp(dsgnWidget(meter), "Mode", &mode)); + TEST_EQ(wgtValueToInt(&mode), UC_DESIGN_MODE); + TEST_TRUE(basDsgnIsDesignWindow(sFormWin)); + + // Its properties in the grid; a value set there runs its Let, whose + // PropertyChanged has the grid read it again + DialogPlanT plan; + + dpSelectByPtr(meter); + TEST_STR_EQ(ucRowValue("Value"), UC_INIT_VALUE); + TEST_TRUE(ucRowValue("Mode") != NULL); + dialogPlanInit(&plan, BTN_NO); + plan.useSpinner = true; + plan.intValue = UC_SET_VALUE; + dpEditRow("Value", &plan); + TEST_STR_EQ(ucRowValue("Value"), UC_SET_VALUE_TEXT); + + const char *stored = frmCtrlGetProp(meter, "Value"); + + TEST_STR_EQ(stored ? stored : "", UC_SET_VALUE_TEXT); + + // A double-click opens its default event with the event's parameters; + // the event list has its own events, then the host's + navigateToEventSub(); + TEST_TRUE(sEditor && strstr(wgtGetText(sEditor), "Sub " UC_METER_PLACED "_Changed (NewValue As Integer)") != NULL); + ucObjSelect(UC_METER_PLACED); + TEST_TRUE(ucEvtIndex("Changed") >= 0); + TEST_TRUE(ucEvtIndex("[GotFocus]") >= 0); + TEST_EQ(ucEvtIndex("[Paint]"), -1); + + // Saved with the form; the compile knows the project's own type + ideMenu(CMD_SAVE_ALL); + TEST_TRUE(fileHas(sFrmPath, "Begin " UC_METER " " UC_METER_PLACED)); + TEST_TRUE(compileProject()); + + // A run registers the program's own Meter under another key; the + // toolbox leaves the program's copy out + BasRunSpecT spec; + const char *hostError = NULL; + + memset(&spec, 0, sizeof(spec)); + + BasFormRtT *rt = sCachedModule ? basRunHostCreate(sAc, sCachedModule, &spec, &hostError) : NULL; + + TEST_TRUE(rt != NULL); + + if (rt) { + loadRunFiles(rt); + + const char *runKey = basUcResolveType(rt, UC_METER); + + TEST_TRUE(runKey != NULL && strcmp(runKey, designKey) != 0); + TEST_STR_EQ(basUcResolveType(NULL, UC_METER), designKey); + TEST_TRUE(basUcIfaceVisible(rt, wgtGetIface(runKey))); + TEST_TRUE(!basUcIfaceVisible(NULL, wgtGetIface(runKey))); + tbxDestroy(sAc, sToolboxWin); + sToolboxWin = NULL; + showToolbox(); + TEST_EQ(ucToolCount(sToolboxWin ? sToolboxWin->widgetRoot : NULL, UC_METER_DESC), 1); + basRunHostDestroy(rt); + } + + // A saved .ctl registers its controls again; the form's instance is new + if (ctlIdx >= 0) { + if (!sProject.files[ctlIdx].buffer) { + sProject.files[ctlIdx].buffer = prjReadFileText(&sProject, ctlIdx, NULL); + } + + TEST_TRUE(writeProjectFile(ctlIdx)); + } + + key = basUcResolveType(NULL, UC_METER); + TEST_TRUE(key != NULL && strcmp(key, designKey) != 0); + TEST_TRUE(dsgnWidget(meter) != NULL); + + // The built program carries the control's header + char appPath[DVX_MAX_PATH * 2]; + + snprintf(appPath, sizeof(appPath), "%s" DVX_PATH_SEP UC_APP_NAME, tTempDir()); + dialogPlanInit(&plan, BTN_NO); + plan.fileDir = tTempDir(); + plan.fileName = UC_APP_NAME; + plan.fileButton = BTN_SAVE; + plan.choiceIndex = BUILD_MODE_RELEASE; + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_MAKE_EXE); + tDialogScript(NULL, NULL); + + DvxResHandleT *res = dvxResOpen(appPath); + uint32_t size = 0; + char *ctl0 = res ? (char *)dvxResRead(res, UC_RES_CTL0, &size) : NULL; + + TEST_TRUE(ctl0 && strstr(ctl0, "Begin UserControl " UC_METER) != NULL); + free(ctl0); + + if (res) { + dvxResClose(res); + } + + // Closing the project lets the designer's controls go + dialogPlanInit(&plan, BTN_NO); + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_PRJ_CLOSE); + tDialogScript(NULL, NULL); + TEST_PTR_EQ(basUcOwnIface(NULL, UC_METER), NULL); + TEST_PTR_EQ(sDesignModule, NULL); + + ideExit(); + tShutdown(); +} + + +// The DOS script's fixture (src/test/dos/fixtures/ucide, scripts/ucide.scr) +// on the host: its control is in the toolbox, the form's instance is made +// in the designer with the .frm's Value, the program runs (its Load +// handler sees the control's Value), and after a second control is placed +// the saved form keeps its code and still runs. +static void caseUcDosFixture(void) { + tCase("UserControl: the ucide DOS fixture in the IDE"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + static const char *const files[] = { UC_DOS_DBP, UC_DOS_FRM, "uclog.bas", "ucmeter.ctl", "ucmeter.bmp" }; + char dir[DVX_MAX_PATH]; + char dbp[DVX_MAX_PATH * 2]; + + snprintf(dir, sizeof(dir), "%s" DVX_PATH_SEP UC_DOS_DIR, tTempDir()); + TEST_EQ(platformMkdirRecursive(dir), 0); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(files); i++) { + char src[DVX_MAX_PATH * 2]; + char dst[DVX_MAX_PATH * 2]; + int32_t len = 0; + + snprintf(src, sizeof(src), "%s" DVX_PATH_SEP "%s", UC_DOS_FIXTURE, files[i]); + snprintf(dst, sizeof(dst), "%s" DVX_PATH_SEP "%s", dir, files[i]); + + char *data = platformReadFile(src, &len); + + TEST_TRUE(data != NULL && tWriteFile(dst, data, len)); + free(data); + } + + snprintf(dbp, sizeof(dbp), "%s" DVX_PATH_SEP UC_DOS_DBP, dir); + ideStart(false); + openProjectPath(dbp); + TEST_TRUE(hasProject()); + treeOpenFile(ucFileIdx(UC_DOS_FRM)); + dcLayout(); + + FrmCtrlT *meter = sDesigner.form ? dpCtrlByName(UC_DOS_PLACED) : NULL; + WgtValueT value = wgtValueInt(0); + WidgetT *tool = sToolboxWin ? ucToolByTip(sToolboxWin->widgetRoot, UC_DOS_DESC) : NULL; + + TEST_TRUE(tool != NULL); + TEST_TRUE(meter && dsgnWidget(meter) && wgtGetProp(dsgnWidget(meter), "Value", &value)); + TEST_EQ(wgtValueToInt(&value), UC_DOS_VALUE); + TEST_TRUE(compileProject()); + + // F5: the program's own control, its Load handler reading the + // control's Value + runScript(ucStepRunPass, CMD_RUN); + TEST_EQ(sRunStep, UC_RUN_PASSED); + + // A second one placed from the toolbox: the AutoSize form's design + // window grows to show it. Save on Run writes the form with its + // code, and the run still passes + char frmPath[DVX_MAX_PATH * 2]; + int32_t winH = sFormWin ? sFormWin->h : 0; + + snprintf(frmPath, sizeof(frmPath), "%s" DVX_PATH_SEP UC_DOS_FRM, dir); + + if (tool && tool->onClick) { + tool->onClick(tool); + dcFormClick(DC_PLACE_X, DC_PLACE_Y); + dcLayout(); + } + + FrmCtrlT *placed = sDesigner.form ? dpCtrlByName(UC_DOS_PLACED2) : NULL; + WidgetT *box = sDesigner.contentBox; + + TEST_TRUE(placed != NULL); + TEST_TRUE(sFormWin && sFormWin->h > winH); + TEST_TRUE(placed && box && dsgnWidget(placed) && dsgnWidget(placed)->y + dsgnWidget(placed)->h <= box->y + box->h); + runScript(ucStepRunPass, CMD_RUN); + TEST_EQ(sRunStep, UC_RUN_PASSED); + TEST_TRUE(fileHas(frmPath, "Begin UcMeter " UC_DOS_PLACED2)); + TEST_TRUE(fileHas(frmPath, UC_DOS_LOAD)); + + ideExit(); + tShutdown(); +} + + +// A project control's Enum properties in the Properties window: an Enum +// whose values are 0, 1, 2 ... shows its member names and a double-click +// steps to the next one (every name in turn, the last wrapping to the +// first), as a built-in control's; the .frm stores the +// name, and the reopened project's control takes it again. An Enum with +// other values shows the number. +static void caseUcEnumProps(void) { + tCase("UserControl: Enum properties by name in the Properties window and the .frm"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + ucPrepare(); + + static const char *ctlText = + "VERSION DVX 1.00\n" + "Begin UserControl " UC_METER "\n" + " Width = 80\n" + " Height = 40\n" + " ToolboxIcon = \"" UC_METER_ICON "\"\n" + " Description = \"" UC_METER_DESC "\"\n" + "End\n" + "Option Explicit\n" + "Public Enum MeterLookE\n" + " Square\n" + " Circle\n" + " Star\n" + "End Enum\n" + "Public Enum MeterSpanE\n" + " Tiny = 4\n" + " Huge = 40\n" + "End Enum\n" + "Private mLook As MeterLookE\n" + "Public Span As MeterSpanE\n" + "\n" + "Public Property Get Look() As MeterLookE\n" + " Look = mLook\n" + "End Property\n" + "\n" + "Public Property Let Look(v As MeterLookE)\n" + " mLook = v\n" + " UserControl.PropertyChanged \"Look\"\n" + "End Property\n" + "\n" + "Private Sub UserControl_Initialize()\n" + " mLook = Circle\n" + " Span = Tiny\n" + "End Sub\n"; + char path[DVX_MAX_PATH * 2]; + + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP UC_METER_FILE, sProjectDir); + TEST_TRUE(tWriteFile(path, ctlText, (int32_t)strlen(ctlText))); + ideStart(true); + openViaRecent(); + treeOpenFile(ucFileIdx(FIXTURE_FRM)); + dcLayout(); + + WidgetT *tool = sToolboxWin ? ucToolByTip(sToolboxWin->widgetRoot, UC_METER_DESC) : NULL; + + TEST_TRUE(tool != NULL && sDesigner.form != NULL); + + if (!tool || !tool->onClick || !sDesigner.form) { + ideExit(); + tShutdown(); + return; + } + + tool->onClick(tool); + dcFormClick(DC_EMPTY_X, DC_PLACE_Y); + dcLayout(); + + // The rows: the name of the value, a double-click steps to the next + FrmCtrlT *meter = dpCtrlByName(UC_METER_PLACED); + DialogPlanT plan; + WgtValueT look = wgtValueInt(0); + + TEST_TRUE(meter != NULL && dsgnWidget(meter) != NULL); + + if (!meter || !dsgnWidget(meter)) { + ideExit(); + tShutdown(); + return; + } + + dpSelectByPtr(meter); + TEST_STR_EQ(ucRowValue("Look"), "Circle"); + TEST_STR_EQ(ucRowValue("Span"), UC_SPAN_TINY); + dialogPlanInit(&plan, BTN_NO); + dpEditRow("Look", &plan); + TEST_STR_EQ(ucRowValue("Look"), "Star"); + + // Every name in value order, the last one wrapping to the first + static const char *round[] = { "Square", "Circle", "Star" }; + + for (int32_t i = 0; i < DVX_ARRAY_LEN(round); i++) { + dpEditRow("Look", &plan); + TEST_STR_EQ(ucRowValue("Look"), round[i]); + } + + const char *stored = frmCtrlGetProp(meter, "Look"); + + TEST_STR_EQ(stored ? stored : "", "Star"); + TEST_TRUE(wgtGetProp(dsgnWidget(meter), "Look", &look)); + TEST_EQ(look.i, UC_LOOK_STAR); + + // Saved by name; the reopened project's control has it + ideMenu(CMD_SAVE_ALL); + TEST_TRUE(fileHas(sFrmPath, "Look = Star")); + dialogPlanInit(&plan, BTN_NO); + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_PRJ_CLOSE); + tDialogScript(NULL, NULL); + openViaRecent(); + treeOpenFile(ucFileIdx(FIXTURE_FRM)); + dcLayout(); + meter = sDesigner.form ? dpCtrlByName(UC_METER_PLACED) : NULL; + look = wgtValueInt(0); + TEST_TRUE(meter && dsgnWidget(meter) && wgtGetProp(dsgnWidget(meter), "Look", &look)); + TEST_EQ(look.i, UC_LOOK_STAR); + + if (meter) { + dpSelectByPtr(meter); + TEST_STR_EQ(ucRowValue("Look"), "Star"); + } + + ideExit(); + tShutdown(); +} + + +// The header of the active .ctl in the Properties window: every key with +// the value the control takes, each edited by kind (spin box, toggle, +// color dialog, the control's events, text), the code untouched, a +// breakpoint following a header line added or removed, Name refused when +// taken or no name; another file active shows the designer again. +static void caseUcHeaderProps(void) { + tCase("UserControl: header properties in the Properties window"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + ucPrepare(); + ideStart(true); + openViaRecent(); + + int32_t ctlIdx = ucFileIdx(UC_METER_FILE); + + TEST_TRUE(ctlIdx >= 0); + + if (ctlIdx < 0) { + ideExit(); + tShutdown(); + return; + } + + treeOpenFile(ctlIdx); + ideMenu(CMD_WIN_PROPS); + TEST_TRUE(sPropsWin != NULL); + TEST_STR_EQ(ucRowValue("Name"), UC_METER); + TEST_STR_EQ(ucRowValue("Width"), "80"); + TEST_STR_EQ(ucRowValue("Height"), "40"); + TEST_STR_EQ(ucRowValue("CanGetFocus"), "True"); + TEST_STR_EQ(ucRowValue("BackColor"), ""); + TEST_STR_EQ(ucRowValue("ToolboxIcon"), UC_METER_ICON); + TEST_STR_EQ(ucRowValue("Description"), UC_METER_DESC); + TEST_STR_EQ(ucRowValue("DefaultEvent"), "Changed"); + + // A breakpoint in the code + const char *src = getFullSource(); + const char *bpText = strstr(src, UC_METER_BP_LINE); + IdeBreakpointT bp; + + memset(&bp, 0, sizeof(bp)); + bp.fileIdx = ctlIdx; + bp.codeLine = 1; + + for (const char *p = src; bpText && p < bpText; p++) { + bp.codeLine += (*p == '\n') ? 1 : 0; + } + + arrput(sBreakpoints, bp); + + int32_t bpIdx = (int32_t)arrlen(sBreakpoints) - 1; + int32_t bpLine = bp.codeLine; + + // Each kind of key; CanGetFocus and BackColor add a line, an empty + // DefaultEvent removes one + DialogPlanT plan; + + dialogPlanInit(&plan, BTN_NO); + plan.useSpinner = true; + plan.intValue = UC_WIDTH; + dpEditRow("Width", &plan); + TEST_TRUE(strstr(sProject.files[ctlIdx].buffer, UC_WIDTH_LINE) != NULL); + TEST_TRUE(sProject.files[ctlIdx].modified); + + dialogPlanInit(&plan, BTN_NO); + dpEditRow("CanGetFocus", &plan); + TEST_STR_EQ(ucRowValue("CanGetFocus"), "False"); + TEST_EQ(sBreakpoints[bpIdx].codeLine, bpLine + 1); + + dialogPlanInit(&plan, BTN_NO); + plan.custom = ucColorPick; + dpEditRow("BackColor", &plan); + TEST_STR_EQ(ucRowValue("BackColor"), UC_PICK_BASIC); + TEST_EQ(sBreakpoints[bpIdx].codeLine, bpLine + 2); + + dialogPlanInit(&plan, BTN_NO); + plan.choiceTitle = BAS_UC_KEY_DEFAULTEVENT; + plan.choiceIndex = UC_NO_EVENT; + dpEditRow("DefaultEvent", &plan); + TEST_STR_EQ(ucRowValue("DefaultEvent"), ""); + TEST_EQ(sBreakpoints[bpIdx].codeLine, bpLine + 1); + + dialogPlanInit(&plan, BTN_NO); + plan.inputText = "Needle"; + dpEditRow("Description", &plan); + TEST_STR_EQ(ucRowValue("Description"), "Needle"); + + // Name: the form's name and a non-name are refused + dialogPlanInit(&plan, BTN_NO); + plan.inputText = "Form1"; + dpEditRow("Name", &plan); + TEST_EQ(plan.errors, 1); + dialogPlanInit(&plan, BTN_NO); + plan.inputText = "2bad"; + dpEditRow("Name", &plan); + TEST_EQ(plan.errors, 1); + TEST_STR_EQ(ucRowValue("Name"), UC_METER); + dialogPlanInit(&plan, BTN_NO); + plan.inputText = "Speedo"; + dpEditRow("Name", &plan); + TEST_STR_EQ(ucRowValue("Name"), "Speedo"); + + // The code is untouched; the editor shows the new header + TEST_TRUE(strstr(sProject.files[ctlIdx].buffer, UC_METER_CODE "Public Mode As Integer\n") != NULL); + TEST_TRUE(sEditor && strstr(wgtGetText(sEditor), "Begin UserControl Speedo") != NULL); + TEST_TRUE(basUcOwnIface(NULL, "Speedo") != NULL); + + // Another file active: the designer's (no form: nothing) + treeOpenFile(ucFileIdx(FIXTURE_BAS)); + TEST_PTR_EQ(ucRowValue("DefaultEvent"), NULL); + + ideExit(); + tShutdown(); +} + + +// File > New UserControl: UserControlN.ctl from prjCtlNew in a Controls +// group of the project window; its code window has the UserControl object +// with the compiler's UserControl events, a new handler is Private with +// the event's parameters; it compiles as a UserControl class, Save All +// writes it and [Controls], and a rename takes the control's name along. +static void caseUcNewControl(void) { + tCase("UserControl: New UserControl, project window, code window, compile, save, rename"); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + fixturePrepare(); + + char ctlPath[DVX_MAX_PATH * 2]; + char renamedPath[DVX_MAX_PATH * 2]; + + snprintf(ctlPath, sizeof(ctlPath), "%s" DVX_PATH_SEP UC_NEW_FILE, sProjectDir); + snprintf(renamedPath, sizeof(renamedPath), "%s" DVX_PATH_SEP UC_RENAMED_FILE, sProjectDir); + remove(ctlPath); + remove(renamedPath); + ideStart(true); + openViaRecent(); + ideMenu(CMD_PRJ_NEW_CTL); + + int32_t idx = ucFileIdx(UC_NEW_FILE); + + TEST_TRUE(idx >= 0); + + if (idx < 0) { + ideExit(); + tShutdown(); + return; + } + + char *fresh = prjCtlNew(UC_NEW_NAME); + + TEST_EQ(sProject.files[idx].kind, PrjKindControlE); + TEST_TRUE(sProject.files[idx].modified); + TEST_EQ(sEditorFileIdx, idx); + TEST_STR_EQ(getFullSource(), fresh ? fresh : ""); + free(fresh); + TEST_TRUE(basUcOwnIface(NULL, UC_NEW_NAME) != NULL); + + // The project window lists it under Controls + WidgetT *tree = treeOfProjectWindow(); + + TEST_TRUE(tree != NULL && tFindByText(tree, "Controls") != NULL); + TEST_TRUE(tree != NULL && findTreeItem(tree, idx) != NULL); + + // The code window: UserControl and its events (Paint has code) + ucObjSelect(BAS_UC_OBJECT); + TEST_EQ((int32_t)arrlen(sEvtItems), BAS_UC_EVT_COUNT); + TEST_TRUE(ucEvtIndex(BAS_EVT_PAINT) >= 0); + TEST_TRUE(ucEvtIndex("[" BAS_UC_EVT_INIT "]") >= 0); + + int32_t down = ucEvtIndex("[" BAS_EVT_MOUSEDOWN "]"); + + TEST_TRUE(down >= 0); + + if (down >= 0) { + wgtDropdownSetSelected(sEvtDropdown, down); + onEvtDropdownChange(sEvtDropdown); + } + + TEST_TRUE(sEditor && strstr(wgtGetText(sEditor), "Private Sub UserControl_MouseDown (" BAS_UC_MOUSE_PARAMS ")") != NULL); + + // A UserControl class of the module + TEST_TRUE(compileProject()); + + bool isControl = false; + + for (int32_t i = 0; sCachedModule && i < sCachedModule->classCount; i++) { + isControl = isControl || (strcasecmp(sCachedModule->classes[i].name, UC_NEW_NAME) == 0 && (sCachedModule->classes[i].flags & BAS_CLASS_USERCONTROL)); + } + + TEST_TRUE(isControl); + + // Saved with the project's [Controls] + ideMenu(CMD_SAVE_ALL); + TEST_TRUE(fileHas(ctlPath, "Begin UserControl " UC_NEW_NAME)); + TEST_TRUE(fileHas(sDbpPath, "[" BAS_INI_SECTION_CONTROLS "]")); + TEST_TRUE(fileHas(sDbpPath, UC_NEW_FILE)); + + // Named after its file, it follows a rename + char name[BAS_MAX_IDENT] = ""; + + TEST_TRUE(renameProjectFileTo(idx, UC_RENAMED)); + TEST_STR_EQ(sProject.files[idx].path, UC_RENAMED_FILE); + TEST_TRUE(prjClassName(&sProject, idx, sProject.files[idx].buffer, name, (int32_t)sizeof(name))); + TEST_STR_EQ(name, UC_RENAMED); + TEST_TRUE(basUcOwnIface(NULL, UC_RENAMED) != NULL); + TEST_PTR_EQ(basUcOwnIface(NULL, UC_NEW_NAME), NULL); + + // The next new control takes the first free name again; removed (and + // saved first, as answered), the designer lets it go + ideMenu(CMD_PRJ_NEW_CTL); + + int32_t again = ucFileIdx(UC_NEW_FILE); + + TEST_TRUE(again >= 0); + TEST_TRUE(basUcOwnIface(NULL, UC_NEW_NAME) != NULL); + + if (again >= 0) { + DialogPlanT plan; + + treeSelectFile(again); + dialogPlanInit(&plan, BTN_YES); + tDialogScript(dialogAnswer, &plan); + ideMenu(CMD_PRJ_REMOVE); + tDialogScript(NULL, NULL); + TEST_EQ(ucFileIdx(UC_NEW_FILE), -1); + TEST_PTR_EQ(basUcOwnIface(NULL, UC_NEW_NAME), NULL); + TEST_TRUE(basUcOwnIface(NULL, UC_RENAMED) != NULL); + } + + ideExit(); + tShutdown(); +} + + +static void runUserCtlCases(void) { + caseUcNewControl(); + caseUcHeaderProps(); + caseUcDesigner(); + caseUcDosFixture(); + caseUcEnumProps(); + caseUcControlProject(); +} + + +// Color dialog script: pick UC_PICK_RGB. +static void ucColorPick(WindowT *modal) { + TEST_TRUE(tColorDialogPick(modal, UC_PICK_RGB)); +} + + +// Index of label in the code window's event dropdown, -1 when absent. +static int32_t ucEvtIndex(const char *label) { + for (int32_t i = 0; i < (int32_t)arrlen(sEvtItems); i++) { + if (strcmp(sEvtItems[i], label) == 0) { + return i; + } + } + + return -1; +} + + +// Index of project file path, -1 when the project has none. +static int32_t ucFileIdx(const char *path) { + return prjFileIndexByPath(&sProject, path); +} + + +// Pick name in the code window's object dropdown. +static void ucObjSelect(const char *name) { + int32_t idx = -1; + + for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) { + idx = (strcmp(sObjItems[i], name) == 0) ? i : idx; + } + + TEST_TRUE(idx >= 0 && sObjDropdown != NULL); + + if (idx >= 0 && sObjDropdown) { + wgtDropdownSetSelected(sObjDropdown, idx); + onObjDropdownChange(sObjDropdown); + } +} + + +// Project Properties script: Type Control, OK. +static void ucPpdControl(WindowT *modal) { + if (strcmp(modal->title, UC_TITLE_PRJPROPS) != 0) { + return; + } + + WidgetT *type = findOfClass(modal->widgetRoot, sDropdownClass, 0); + + TEST_TRUE(type != NULL); + + if (type) { + wgtDropdownSetSelected(type, UC_TYPE_CONTROL); + } + + TEST_TRUE(tDialogClickButton(modal, BTN_OK)); +} + + +// The fixture project with the Meter control (and its toolbox picture) +// in its [Controls] section. +static void ucPrepare(void) { + char path[DVX_MAX_PATH * 2]; + + fixturePrepare(); + TEST_TRUE(tWriteFile(sDbpPath, UC_FIXTURE_DBP, (int32_t)strlen(UC_FIXTURE_DBP))); + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP UC_METER_FILE, sProjectDir); + TEST_TRUE(tWriteFile(path, UC_CTL_TEXT(UC_METER, UC_METER_DESC, UC_METER_ICON), (int32_t)strlen(UC_CTL_TEXT(UC_METER, UC_METER_DESC, UC_METER_ICON)))); + snprintf(path, sizeof(path), "%s" DVX_PATH_SEP UC_METER_ICON, sProjectDir); + dmWriteBmp(path, UC_TOOL_ICON_SIZE, UC_TOOL_ICON_SIZE); +} + + +// The value cell of the Properties window's row rowName, NULL when the +// window or the row is not there. +static const char *ucRowValue(const char *rowName) { + WidgetT *list = sPropsWin ? findOfClass(sPropsWin->widgetRoot, sListViewClass, 0) : NULL; + int32_t row = listRowByName(list, rowName); + + return (row >= 0) ? wgtListViewGetCell(list, row, 1) : NULL; +} + + +// Run script: once the program's window shows "UCIDE PASS 9" sRunStep is +// UC_RUN_PASSED and the run is stopped (also, with sRunStep left at 0, +// when it does not show in half the run's frame budget). +static void ucStepRunPass(void) { + if (sDbgState != DbgRunningE) { + return; + } + + char caption[MAX_TITLE_LEN]; + + // STR$ of a positive number starts with a space + snprintf(caption, sizeof(caption), "%s %d", UC_DOS_PASS, UC_DOS_VALUE); + + if (findWindowPrefix(caption)) { + sRunStep = UC_RUN_PASSED; + onMenu(sWin, CMD_STOP); + } else if (sAc->frameCount - sRunStartFrame > RUN_FRAME_LIMIT / 2) { + onMenu(sWin, CMD_STOP); + } +} + + +// The first widget under root with tooltip (depth first), NULL for none. +static WidgetT *ucToolByTip(WidgetT *root, const char *tooltip) { + for (WidgetT *c = root ? root->firstChild : NULL; c; c = c->nextSibling) { + WidgetT *hit = (c->tooltip && strcmp(c->tooltip, tooltip) == 0) ? c : ucToolByTip(c, tooltip); + + if (hit) { + return hit; + } + } + + return NULL; +} + + +// How many widgets under root have tooltip. +static int32_t ucToolCount(WidgetT *root, const char *tooltip) { + int32_t n = 0; + + for (WidgetT *c = root ? root->firstChild : NULL; c; c = c->nextSibling) { + n += ((c->tooltip && strcmp(c->tooltip, tooltip) == 0) ? 1 : 0) + ucToolCount(c, tooltip); + } + + return n; +} diff --git a/src/test/joystick/joystickTest.c b/src/test/joystick/joystickTest.c index cbc567b9..5060e40a 100644 --- a/src/test/joystick/joystickTest.c +++ b/src/test/joystick/joystickTest.c @@ -97,6 +97,9 @@ #define DZ_BIG_OUT 7233 // (20000 - 16383) * 32767 / 16384 #define DZ_BIG_NEG_OUT 7232 // (20000 - 16384) * 32768 / 16384 #define DZ_EDGE 2621 // 8 % of 32767 +#define BAS_BAD_HANDLE 99 +#define SETUP_ZONE 20 +#define PAD_RAW_BUTTONS 5 #define QB_POS_MIN 1 #define QB_POS_MID 100 @@ -156,8 +159,14 @@ static int32_t pollsInSecond(int32_t handle); static void qbasicTests(void); static void readingTests(void); static void setFalls(int32_t ax, int32_t ay, int32_t bx, int32_t by); +static void setupTests(void); void shellAppCallEnter(ShellAppT *app); void shellAppCallLeave(ShellAppT *app); + +// prefs.bas (libdvx dvxPrefsBas.c) +void PrefsClose(int32_t handle); +int32_t PrefsCreate(void); +const char *PrefsGet(int32_t handle, const char *section, const char *key, const char *defaultValue); AppContextT *shellContext(void); ShellAppT *shellGetApp(int32_t appId); void shellRegisterAppCleanup(ShellAppCleanupFnT fn); @@ -591,6 +600,8 @@ static void keyboardTests(void) { int32_t hB = joyOpen(ctx, kbd); TEST_TRUE(kbd >= 0 && hA >= 0 && hB >= 0); + TEST_EQ(JoyIsKeyboard(kbd + 1), QB_TRUE); + TEST_EQ(JoyCanCalibrate(kbd + 1), 0); tCase("keyboard: arrows and buttons reach the program with the focus only"); dvxRaiseWindow(ctx, winA); @@ -1122,6 +1133,170 @@ static void setFalls(int32_t ax, int32_t ay, int32_t bx, int32_t by) { } +// joystick.bas: setting joysticks up (the Game Controllers page's needs) +static void setupTests(void) { + AppContextT *ctx = tCtx(); + int32_t dev = findDevice("gameport0"); + int32_t pad = findDevice("testpad0"); + JoyAxisCalT cal; + + ctx->currentAppId = OWNER_A; + + tCase("setup: hats, axes present, calibration, connected"); + TEST_EQ(JoyHats(pad + 1), PAD_HATS); + TEST_EQ(JoyHats(dev + 1), 0); + TEST_EQ(JoyHats(0), 0); + TEST_EQ(JoyAxisPresent(pad + 1, 1), QB_TRUE); + TEST_EQ(JoyAxisPresent(pad + 1, PAD_AXES + 1), 0); + TEST_EQ(JoyAxisPresent(pad + 1, 0), 0); + TEST_EQ(JoyAxisPresent(pad + 1, JOY_MAX_AXES + 1), 0); + TEST_EQ(JoyAxisPresent(0, 1), 0); + TEST_EQ(JoyCanCalibrate(dev + 1), QB_TRUE); + TEST_EQ(JoyCanCalibrate(pad + 1), 0); + TEST_EQ(JoyCanCalibrate(0), 0); + TEST_EQ(JoyIsKeyboard(dev + 1), 0); + TEST_EQ(JoyIsKeyboard(pad + 1), 0); + TEST_EQ(JoyIsKeyboard(0), 0); + TEST_EQ(JoyConnected(pad + 1), QB_TRUE); + TEST_EQ(JoyConnected(0), 0); + + tCase("setup: raw readings"); + sPad.raw.axes[0] = PAD_MOVE; + sPad.raw.buttons = PAD_RAW_BUTTONS; + JoyX(pad + 1); + pollNext(); + TEST_EQ(JoyRawAxis(pad + 1, 1), PAD_MOVE); + TEST_EQ(JoyRawButtons(pad + 1), PAD_RAW_BUTTONS); + TEST_EQ(JoyRawAxis(pad + 1, 0), 0); + TEST_EQ(JoyRawAxis(pad + 1, JOY_MAX_AXES + 1), 0); + TEST_EQ(JoyRawAxis(0, 1), 0); + TEST_EQ(JoyRawButtons(0), 0); + sPad.raw.axes[0] = 0; + sPad.raw.buttons = 0; + pollNext(); + + tCase("setup: calibration set and cleared"); + TEST_EQ(JoySetCalibration(dev + 1, 1, CAL_MIN, CAL_CENTER, CAL_MAX), QB_TRUE); + TEST_TRUE(joyGetCalibration(dev, 0, &cal) && cal.min == CAL_MIN && cal.center == CAL_CENTER && cal.max == CAL_MAX); + TEST_STR_EQ(JoySetting("gameport0.axis0", ""), "100,500,900"); + TEST_EQ(JoyCalibration(dev + 1, 1, JOY_CAL_MIN), CAL_MIN); + TEST_EQ(JoyCalibration(dev + 1, 1, JOY_CAL_CENTER), CAL_CENTER); + TEST_EQ(JoyCalibration(dev + 1, 1, JOY_CAL_MAX), CAL_MAX); + TEST_EQ(JoyCalibration(dev + 1, 1, JOY_CAL_MAX + 1), 0); + TEST_EQ(JoyCalibration(dev + 1, 0, JOY_CAL_MIN), 0); + TEST_EQ(JoyCalibration(pad + 1, 1, JOY_CAL_MIN), 0); + TEST_EQ(JoyCalibration(0, 1, JOY_CAL_MIN), 0); + TEST_EQ(JoySetCalibration(dev + 1, 1, CAL_MAX, CAL_CENTER, CAL_MIN), 0); + TEST_EQ(JoySetCalibration(pad + 1, 1, CAL_MIN, CAL_CENTER, CAL_MAX), 0); + TEST_EQ(JoySetCalibration(dev + 1, 0, CAL_MIN, CAL_CENTER, CAL_MAX), 0); + TEST_EQ(JoyClearCalibration(dev + 1, 1), QB_TRUE); + TEST_STR_EQ(JoySetting("gameport0.axis0", "none"), "none"); + TEST_EQ(JoyClearCalibration(0, 1), 0); + + tCase("setup: dead zone"); + TEST_EQ(JoyDeadZone(dev + 1), JOY_DEADZONE_DEFAULT); + TEST_EQ(JoySetDeadZone(dev + 1, SETUP_ZONE), QB_TRUE); + TEST_EQ(JoyDeadZone(dev + 1), SETUP_ZONE); + TEST_EQ(JoySetDeadZone(dev + 1, ZONE_TOO_BIG), 0); + TEST_EQ(JoySetDeadZone(0, SETUP_ZONE), 0); + TEST_EQ(JoyDeadZone(0), -1); + JoySetSetting("gameport0.deadZone", ""); + TEST_EQ(JoyDeadZone(dev + 1), JOY_DEADZONE_DEFAULT); + + tCase("setup: any setting by key; \"\" removes it"); + TEST_STR_EQ(JoySetting(JOY_KEY_POLL_HZ, "x"), "x"); + TEST_STR_EQ(JoySetting(JOY_KEY_POLL_HZ, NULL), ""); + JoySetSetting(JOY_KEY_POLL_HZ, "100"); + TEST_STR_EQ(JoySetting(JOY_KEY_POLL_HZ, "x"), "100"); + JoySetSetting(JOY_KEY_POLL_HZ, ""); + TEST_STR_EQ(JoySetting(JOY_KEY_POLL_HZ, "d"), "d"); + + tCase("setup: listed settings (game port mode, keyboard fallback)"); + TEST_EQ(JoyChoiceCount(JoyChoiceGamePortE), JOY_GAMEPORT_CHOICE_COUNT); + TEST_EQ(JoyChoiceCount(JoyChoiceFallbackE), JOY_FALLBACK_CHOICE_COUNT); + TEST_EQ(JoyChoiceCount(JoyChoiceCountE), 0); + TEST_STR_EQ(JoyChoiceLabel(JoyChoiceGamePortE, 1), "One joystick with 4 axes"); + TEST_STR_EQ(JoyChoiceLabel(JoyChoiceFallbackE, 2), "Never"); + TEST_STR_EQ(JoyChoiceLabel(JoyChoiceGamePortE, JOY_GAMEPORT_CHOICE_COUNT), ""); + TEST_STR_EQ(JoyChoiceLabel(JoyChoiceCountE, 0), ""); + TEST_EQ(JoyChoice(JoyChoiceGamePortE), 0); + TEST_EQ(JoyChoice(JoyChoiceFallbackE), 0); + TEST_EQ(JoyChoice(JoyChoiceCountE), -1); + TEST_EQ(JoySetChoice(JoyChoiceGamePortE, 1), QB_TRUE); + TEST_STR_EQ(joySettingGet(JOY_KEY_GAMEPORT_MODE, ""), JOY_GAMEPORT_FOUR); + TEST_EQ(JoyChoice(JoyChoiceGamePortE), 1); + TEST_EQ(JoySetChoice(JoyChoiceGamePortE, JOY_GAMEPORT_CHOICE_COUNT), 0); + TEST_EQ(JoySetChoice(JoyChoiceCountE, 0), 0); + joySettingSet(JOY_KEY_GAMEPORT_MODE, "bogus"); + TEST_EQ(JoyChoice(JoyChoiceGamePortE), 0); + joySettingSet(JOY_KEY_GAMEPORT_MODE, NULL); + TEST_EQ(JoySetChoice(JoyChoiceFallbackE, 2), QB_TRUE); + TEST_STR_EQ(joySettingGet(JOY_KEY_FALLBACK, ""), JOY_FALLBACK_NEVER); + joySettingSet(JOY_KEY_FALLBACK, NULL); + + tCase("setup: keyboard joystick keys by slot"); + TEST_EQ(JoyKeyCount(), joyKeyCount()); + TEST_STR_EQ(JoyKeyName(0), "Up"); + TEST_STR_EQ(JoyKeyName(-1), ""); + TEST_EQ(joyKeyIndex("lctrl"), joyKeyIndex("LCtrl")); + TEST_EQ(joyKeyIndex("NoSuchKey"), -1); + TEST_EQ(joyKeyIndex(NULL), -1); + TEST_EQ(JoyKeyIndex("lctrl"), joyKeyIndex("LCtrl")); + TEST_EQ(JoyKeyIndex("NoSuchKey"), -1); + TEST_EQ(JoyKeyIndex(NULL), -1); + TEST_EQ(JoyKeyCode(0), KEY_RAW_UP); + TEST_EQ(JoyKeyCode(JoyKeyIndex("LCtrl")), KEY_RAW_LCTRL); + TEST_EQ(JoyKeyCode(-1), -1); + TEST_STR_EQ(JoyKeyNameOf(KEY_RAW_RSHIFT), "RShift"); + TEST_STR_EQ(JoyKeyNameOf(KEY_RAW_PAUSE), ""); + TEST_EQ(JoyKbdSlotCount(), JOY_KBD_SLOT_COUNT); + TEST_EQ(JoyKbdSlot(0), joyKeyIndex("Up")); + TEST_EQ(JoyKbdSlot(4), joyKeyIndex("LCtrl")); + TEST_EQ(JoyKbdSlot(JOY_KBD_SLOT_COUNT), -1); + TEST_EQ(JoyKbdSlot(-1), -1); + TEST_EQ(JoySetKbdSlot(2, joyKeyIndex("A")), QB_TRUE); + TEST_STR_EQ(joySettingGet("keyboard.left", ""), "A"); + TEST_EQ(JoyKbdSlot(2), joyKeyIndex("A")); + joySettingSet("keyboard.left", "NoSuchKey"); + TEST_EQ(JoyKbdSlot(2), joyKeyIndex("Left")); + TEST_EQ(JoySetKbdSlot(2, -1), 0); + TEST_EQ(JoySetKbdSlot(2, joyKeyCount()), 0); + TEST_EQ(JoySetKbdSlot(JOY_KBD_SLOT_COUNT, 0), 0); + joySettingSet("keyboard.left", NULL); + + tCase("setup: settings into and back from a settings handle"); + int32_t h = PrefsCreate(); + + JoySetSetting(JOY_KEY_POLL_HZ, "90"); + JoySettingsStore(h); + TEST_STR_EQ(PrefsGet(h, JOY_INI_SECTION, JOY_KEY_POLL_HZ, ""), "90"); + JoySetSetting(JOY_KEY_POLL_HZ, ""); + JoySettingsLoad(h); + TEST_STR_EQ(JoySetting(JOY_KEY_POLL_HZ, ""), "90"); + JoySetSetting(JOY_KEY_POLL_HZ, ""); + JoySettingsLoad(BAS_BAD_HANDLE); + JoySettingsStore(BAS_BAD_HANDLE); + TEST_STR_EQ(JoySetting(JOY_KEY_POLL_HZ, ""), ""); + PrefsClose(h); + + tCase("setup: JoyRescan closes the program's own joysticks first"); + int32_t count = joyCount(); + + JoyX(dev + 1); + TEST_EQ(joyRescan(), -1); + TEST_EQ(JoyRescan(), count); + ctx->currentAppId = OWNER_B; + + int32_t other = joyOpen(ctx, dev); + + ctx->currentAppId = OWNER_A; + TEST_EQ(JoyRescan(), -1); + joyClose(other); + TEST_EQ(JoyRescan(), count); + TEST_EQ(joyRescanApp(OWNER_A), count); +} + + void shellAppCallEnter(ShellAppT *app) { (void)app; sCallDepth++; @@ -1206,6 +1381,7 @@ int main(void) { eventTests(); cleanupTests(); basicTests(); + setupTests(); qbasicTests(); joyAppCleanup(OWNER_A); joyAppCleanup(OWNER_B); diff --git a/src/test/lint/Makefile b/src/test/lint/Makefile index a7e456e6..1280d310 100644 --- a/src/test/lint/Makefile +++ b/src/test/lint/Makefile @@ -1,15 +1,42 @@ -# src/test/lint/Makefile -- T11 static + style gate. +# src/test/lint/Makefile -- T11 static + style gate, and the report-only +# quality tools. # -# make run styleLint, asciiCheck, staticGate (fails on new findings) -# make baseline regenerate all three baselines from the current tree +# make run styleLint, asciiCheck, the staticGate self-test, +# shellcheck, codespell, clang-tidy and staticGate (fails +# on new findings) +# make baseline regenerate the baselines of everything `make` runs +# (staticGate's merge rules for each) # make style styleLint only # make ascii asciiCheck only # make static staticGate (cppcheck + -fanalyzer; slow) # make cppcheck staticGate cppcheck pass only # make selftest test staticGate's baseline merge logic (fast) +# make shellcheck shellcheck over every *.sh (qaGate.sh; seconds) +# make codespell codespell over sources and docs (qaGate.sh; seconds) +# make tidy clang-tidy (clangTidy.yaml) over the host compile +# database (qaGate.sh; ~2 min) +# make tidy-baseline merge the current clang-tidy findings into +# clangTidyBaseline.txt # -# cppcheckBaseline.txt / analyzerBaseline.txt list accepted findings as -# " ", each preceded by its justification comment: +# Separate, slower targets (not in `make`; see claudeDocs/qaTools.md): +# make clang clang build of every host translation unit with +# extra warnings; report in obj/clang/report.txt +# (hostDb.py; report only) +# make complexity lizard: functions over CCN 15 or 100 lines, +# obj/qa/complexity (report only, never a gate) +# make msan MemorySanitizer: BASIC harnesses, basrun / bascomp +# runs and the basdev / formrt suites (msan.mk; runs +# make in product directories, so take the shared +# build lock around it when other builds run) +# make valgrind valgrind memcheck over bascomp, basrun, dvxhlpc and +# dvxres (valgrind.mk) +# make qa all of the separate targets above except the +# baseline merge +# +# cppcheckBaseline.txt, analyzerBaseline.txt, clangTidyBaseline.txt, +# shellcheckBaseline.txt and codespellBaseline.txt list +# accepted findings as " ", each preceded by its +# justification comment: # # FP: triaged false positive # # THIRDPARTY: upstream code, not modified by DVX # # UNTRIAGED ... accepted by an update, nobody has looked yet @@ -19,15 +46,17 @@ # UNTRIAGED line. "make static" warns loudly about UNTRIAGED entries but # only fails on new/increased findings, so triage each UNTRIAGED entry # (fix it or write the FP reason) before committing a baseline update. +# A qaGate.sh tool without a baseline file yet is report-only: it prints +# its findings under a banner and passes. # # staticGate needs cppcheck and fails (it never passes silently) when it # is missing. Install cppcheck, or run e.g. # make STATIC_SKIP_CPPCHECK=1 # to skip only the cppcheck pass deliberately (reported on stderr). -.PHONY: all style ascii static cppcheck baseline selftest +.PHONY: all style ascii static cppcheck baseline selftest shellcheck codespell tidy tidy-baseline clang complexity msan valgrind qa -all: style ascii selftest static +all: style ascii selftest shellcheck codespell tidy static style: python3 ./styleLint.py @@ -44,7 +73,43 @@ cppcheck: selftest: ./staticGate.sh --self-test +shellcheck: + ./qaGate.sh shellcheck + +codespell: + ./qaGate.sh codespell + baseline: python3 ./styleLint.py --update-baseline --summary ./asciiCheck.sh --update + ./qaGate.sh shellcheck --update + ./qaGate.sh codespell --update + ./qaGate.sh tidy --update ./staticGate.sh --update + +tidy: + ./qaGate.sh tidy + +tidy-baseline: + ./qaGate.sh tidy --update + +clang: + python3 ./hostDb.py clang + +complexity: + ./qaGate.sh complexity + +# Objects in parallel, then the steps in order. +msan: + $(MAKE) -f msan.mk msan-build + $(MAKE) -f msan.mk -j1 msan + +valgrind: + $(MAKE) -f valgrind.mk + +# Report-only tools; every one runs even when another reports findings. +qa: + -$(MAKE) clang + -$(MAKE) complexity + -$(MAKE) msan + -$(MAKE) valgrind diff --git a/src/test/lint/__pycache__/hostDb.cpython-312.pyc b/src/test/lint/__pycache__/hostDb.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14a12f2b1afbcabff68a62f234da48bbbb7837b5 GIT binary patch literal 15448 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 H1lE56g|&VR diff --git a/src/test/lint/analyzerBaseline.txt b/src/test/lint/analyzerBaseline.txt index dbcd3f8c..01294f60 100644 --- a/src/test/lint/analyzerBaseline.txt +++ b/src/test/lint/analyzerBaseline.txt @@ -9,17 +9,11 @@ # FP: value is stored via arrput in sFileValues (freed on page close); stb_ds OOM hook never returns, so arrput cannot fail and drop it. 1 src/apps/kpunch/cpanel/cpSounds.c|addFile|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: the malloc is stored in the module being built (mod->classes / mod->withEvents entries or an interface map), which basModuleFree releases on success and on every failure path; the analyzer loses the store into the caller-owned module. -2 src/apps/kpunch/dvxbasic/compiler/compiler.c|buildClasses|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# FP: the malloc is stored in the module being built (mod->classes / mod->withEvents entries or an interface map), which basModuleFree releases on success and on every failure path; the analyzer loses the store into the caller-owned module. 3 src/apps/kpunch/dvxbasic/compiler/compiler.c|buildWithEvents|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# FP: the malloc is stored in the module being built (mod->classes / mod->withEvents entries or an interface map), which basModuleFree releases on success and on every failure path; the analyzer loses the store into the caller-owned module. -1 src/apps/kpunch/dvxbasic/compiler/compiler.c|buildInterfaceMap|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# FP: joinParamNames stores the malloc in *out, a class member of the module being built (member paramNames), which basModuleFree releases on success and on every failure path; the analyzer loses the store into the caller-owned module. -1 src/apps/kpunch/dvxbasic/compiler/compiler.c|joinParamNames|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: the malloc is stored in sw->full (sw is rt->screenWin, heap), freed by screenModeChanged and basFormRtScreenClose; analyzer loses the store into the caller's heap struct. 2 src/apps/kpunch/dvxbasic/formrt/formGfx.c|screenFullFrame|leak of [CWE-401] [-Wanalyzer-malloc-leak] -# FP: parents[] holds count entries (one arrput per control); the path needs normalizeOrder's loop to run 0 times while normalizeAppend's runs, same count. -1 src/apps/kpunch/dvxbasic/ide/ideDesigner.c|normalizeAppend|dereference of NULL [CWE-476] [-Wanalyzer-null-dereference] +# FP: the malloc'd text becomes newGeneral, which stashEditor stores in sGeneralBuf (freed with the next edit or the project); every failure path frees it. +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|generalAppend|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: strdup result is stored via arrput in the static sLabelBufs array, freed on the next rebuild and in the dialog teardown. 1 src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c|rebuildList|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: both strdups are stored via arrput in the static sCellData array, released by freeCellData. @@ -29,23 +23,11 @@ # FP: path needs realloc to both succeed and return NULL; on real failure the old buffer stays in b->buf and the caller frees it. 1 src/apps/kpunch/dvxbasic/runtime/serialize.c|bufGrow|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: values filled by variadic popArgs (writes through va_arg pointers the analyzer does not follow; FOR_INIT step/limit, RGB, STRING$ added more sites); was the basVmStep key before the basVmStepExec split. -13 src/apps/kpunch/dvxbasic/runtime/vm.c|basVmStepExec|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value] -# FP: a/b are written by popArgs/popStringArg (varargs out-pointers the analyzer cannot follow) before any use; failure returns first. -2 src/apps/kpunch/dvxbasic/runtime/vm.c|execDevice|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value] -# FP: recnoVal is filled by variadic popArgs (writes through va_arg pointers the analyzer does not follow); popArgs returns false before any use when the stack underflows. -1 src/apps/kpunch/dvxbasic/runtime/vm.c|execFileOp|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value] +7 src/apps/kpunch/dvxbasic/runtime/vm.c|basVmStepExec|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value] # FP: MID$ assignment operands are filled by variadic popArgs (writes through va_arg pointers the analyzer does not follow); underflow returns before any use. 4 src/apps/kpunch/dvxbasic/runtime/vm.c|execStringOp|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value] -# FP: payload comes from forEachRecord, which always allocates before the first record (uint16 length + 1 > 0 == initial size); the NULL path is infeasible. -1 src/apps/kpunch/dvxhelp/dvxhelp.c|displayRecord|dereference of NULL [CWE-476] [-Wanalyzer-null-dereference] -# FP: payload comes from forEachRecord, which always allocates before the first record (uint16 length + 1 > 0 == initial size); the NULL path is infeasible. -2 src/apps/kpunch/dvxhelp/dvxhelp.c|displayRecord|use of NULL where non-null expected [CWE-476] [-Wanalyzer-null-argument] -# FP: every failure path calls closeHelpFile (fclose); on success the FILE lives in sFile. Analyzer treats the fread into sFile.header as clobbering sFile.fp. -1 src/apps/kpunch/dvxhelp/dvxhelp.c|openHelpFile|leak of FILE [CWE-775] [-Wanalyzer-file-leak] -# FP: copy is stored via arrput in *paths (the selection list, freed by freeSelection); stb_ds grow aborts rather than failing. -1 src/apps/kpunch/fileMgr/fileMgr.c|selectionAdd|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: copy is stored via arrins at m->history[0] (trimmed by the pop loop, freed with the model); stb_ds grow aborts rather than failing. -1 src/apps/kpunch/progman/pmModel.c|pmHistoryAdd|leak of [CWE-401] [-Wanalyzer-malloc-leak] +1 src/libs/kpunch/dvxshell/pmModel.c|pmHistoryAdd|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: name copy is stored in fd->entries[] and freed by the listing free loop. 1 src/libs/kpunch/libdvx/dvxDialog.c|fdAddEntry|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: listItems malloc'd in fdLoadDir is freed by the free(fd->listItems) just above this NULL store; analyzer loses the pointer after the per-entry free loop. @@ -68,6 +50,8 @@ 2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|memFileStore|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: inlined memFileStore - same arrput store into sMemFiles, held until DVX exits; stb_ds OOM hook never returns. 2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|prefsSaveAs.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: each procBuf is stored via arrput into *outProcs (freed by the caller, and by splitProcs itself on its failure path); reported at the stb_ds macro expansion. +1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|splitProcs|leak of [CWE-401] [-Wanalyzer-malloc-leak] # THIRDPARTY: upstream stbi_write_hdr does not check its scratch malloc. DVX only calls stbi_write_png/stbi_write_bmp (dvxApp.c, dvxPlatformHost.c), never the HDR writer. Upstream code not modified. 1 src/libs/kpunch/libdvx/thirdparty/stb_image_write.h|stbiw__write_hdr_scanline|dereference of possibly-NULL [CWE-690] [-Wanalyzer-possible-null-dereference] # FP: the stack block is stored in task->stack (tasks[] entry, freed by tsKill/tsExit/tsShutdown); the analyzer loses it at the dummy-return-address store into the block. @@ -94,8 +78,12 @@ 2 src/widgets/kpunch/dbGrid/widgetDbGrid.c|dbGridBuildSortIndex|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: each successful realloc result is stored in d->columns / d->resolvedW before return. 2 src/widgets/kpunch/dbGrid/widgetDbGrid.c|dbGridSetDataWidget|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: the strdup'd target is stored in hv->linkTarget (the view's heap data, freed on the next link and when the widget is destroyed); the analyzer loses the store into the heap struct. +1 src/widgets/kpunch/helpView/widgetHelpView.c|helpLinkFollow|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: title strdup stored via arrput in titles, which becomes lv->ownedColTitles (NULL titles are tolerated). 1 src/widgets/kpunch/listView/widgetListView.c|basSetColumns.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: each cell copy is stored by arrput in lv->ownedCells (freed by widgetFreeOwnedStrings); stb_ds OOM hook never returns, so it cannot be dropped. +1 src/widgets/kpunch/listView/widgetListView.c|basSetRows.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: the !idx || !tmp check returns before any use; analyzer loses the merged condition at -O2. idx ends in lv->sortIndex, tmp is freed. 4 src/widgets/kpunch/listView/widgetListView.c|listViewBuildSortIndex|dereference of possibly-NULL [CWE-690] [-Wanalyzer-possible-null-dereference] # FP: the !idx || !tmp check returns before any use; analyzer loses the merged condition at -O2. idx ends in lv->sortIndex, tmp is freed. @@ -108,6 +96,8 @@ 1 src/widgets/kpunch/listView/widgetListView.c|wgtListViewSetCell.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: inlined listViewBuildSortIndex; same false positive as above. 1 src/widgets/kpunch/listView/widgetListView.c|widgetListViewOnMouse|leak of [CWE-401] [-Wanalyzer-malloc-leak] +# FP: the strdup'd mask is stored in ti->mask (freed by the next SetMask and with the widget); the failure path frees it. +1 src/widgets/kpunch/textInput/widgetTextInput.c|wgtTextInputSetMask|leak of [CWE-401] [-Wanalyzer-malloc-leak] # FP: item was validated non-NULL (VALIDATE_WIDGET_VOID) before the .part split; treeViewOf's loop test makes the analyzer assume NULL. 1 src/widgets/kpunch/treeView/widgetTreeView.c|treeSetExpanded|dereference of NULL [CWE-476] [-Wanalyzer-null-dereference] # FP: w validated non-NULL by VALIDATE_WIDGET_VOID before the .part split; same treeViewOf loop artefact. diff --git a/src/test/lint/asciiCheck.sh b/src/test/lint/asciiCheck.sh index 87ca2aab..1701a1f4 100755 --- a/src/test/lint/asciiCheck.sh +++ b/src/test/lint/asciiCheck.sh @@ -31,7 +31,7 @@ if [ "${#TARGETS[@]}" -eq 0 ]; then TARGETS=(src) fi -cd "$HERE/../../.." +cd "$HERE/../../.." || exit 1 CURRENT="$(mktemp)" trap 'rm -f "$CURRENT"' EXIT @@ -48,7 +48,7 @@ while IFS= read -r -d '' f; do FOUND=$((FOUND + 1)) baseCount=0 if [ "$STRICT" -eq 0 ] && [ "$UPDATE" -eq 0 ] && [ -f "$BASELINE" ]; then - baseCount="$(awk -v k="$f" '$2 == k { print $1 }' "$BASELINE" | head -n 1)" + baseCount="$(KEY="$f" awk '$2 == ENVIRON["KEY"] { print $1 }' "$BASELINE" | head -n 1)" baseCount="${baseCount:-0}" fi if [ "$count" -gt "$baseCount" ]; then diff --git a/src/test/lint/baseline.txt b/src/test/lint/baseline.txt index 7d221353..211d8a2a 100644 --- a/src/test/lint/baseline.txt +++ b/src/test/lint/baseline.txt @@ -1,151 +1,399 @@ # styleLint baseline -- generated by styleLint.py --update-baseline # format: || -1 src/apps/kpunch/clock/clock.c|plainInt|snprintf(sState.timeStr, sizeof(sState.timeStr), "", (int)hour12, tm->tm_min, tm->tm_sec, ampm); -1 src/apps/kpunch/cpanel/cpanel.c|plainInt|snprintf(buf, sizeof(buf), "", (long)count); -2 src/apps/kpunch/cpanel/cpanel.c|plainInt|snprintf(buf, sizeof(buf), "", (long)val); -1 src/apps/kpunch/cpanel/cpanel.c|plainInt|snprintf(buf, sizeof(buf), "", (long)wgtSliderGetValue(sDblClickSldr)); -1 src/apps/kpunch/cpanel/cpanel.c|plainInt|snprintf(msgBuf, sizeof(msgBuf), "", (long)secondsLeft); +1 src/apps/kpunch/cpanel/cpNet.c|plainInt|snprintf(label, sizeof(label), "", (int)(i + 1)); +1 src/apps/kpunch/cpanel/cpNet.c|plainInt|snprintf(line, sizeof(line), "", (unsigned long)st.rxPackets, (unsigned long)st.rxBytes, (unsigned long)st.rxDropped, (unsigned long)st.txPackets, (unsigned long)st.txBytes); +1 src/apps/kpunch/cpanel/cpSounds.c|plainInt|snprintf(text, sizeof(text), "", (long)wgtSliderGetValue(sVolSldr)); +1 src/apps/kpunch/cpanel/cpSounds.c|plainInt|static int compareNames(const void *a, const void *b) { +1 src/apps/kpunch/cpanel/cpSounds.c|plainInt|static int compareNames(const void *a, const void *b); +1 src/apps/kpunch/cpanel/cpTypes.c|plainInt|snprintf(out, outSize, "", (int)len, value); +1 src/apps/kpunch/cpanel/cpanel.c|plainInt|snprintf(buf, sizeof(buf), "", prefix, (long)val, suffix); 1 src/apps/kpunch/cpanel/cpanel.c|plainInt|snprintf(sLabelBufs[i], sizeof(sLabelBufs[i]), "", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName); 1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|buildLog(spec, "", (int)frmCount); 1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|buildLog(spec, "", (int)removed); 1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|snprintf(key, sizeof(key), "", (int)i); +1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_ICON, (int)index); +1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_NAME, (int)index); +1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); 1 src/apps/kpunch/dvxbasic/basBuild.c|plainInt|snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx); -1 src/apps/kpunch/dvxbasic/compiler/lexer.c|plainInt|long long val = strtoll(lex->token.text, NULL, 10); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|dvxStrAppendf(buf, bufSize, pos, "", (int)arr->totalElements); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|pos = dvxStrAppendf(buf, bufSize, pos, "", (int)arr->lbound[d], (int)arr->ubound[d]); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|snprintf(buf, bufSize, "", (int)(bufSize - DBG_QUOTE_OVERHEAD), v->strVal->data); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|snprintf(buf, bufSize, "", (int)v->intVal); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|snprintf(buf, bufSize, "", (long)v->longVal); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|snprintf(buf, bufSize, BAS_DBG_COLLECTION_TYPE "", (int)v->instVal->itemCount); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|snprintf(confirm, sizeof(confirm), "", (int)lhsLen, lhs); +1 src/apps/kpunch/dvxbasic/basdev/basDebug.c|plainInt|snprintf(lineBuf, sizeof(lineBuf), "", (int)curLine); +1 src/apps/kpunch/dvxbasic/basdev/basProject.c|plainInt|snprintf(size, sizeof(size), "", (int)BAS_UC_DEFAULT_SIZE); +1 src/apps/kpunch/dvxbasic/compiler/compiler.c|plainInt|basErrorAt(c, c->lex.token.line, 0, "", (int)c->nextGlobalSlot, (int)BAS_VM_MAX_GLOBALS); +1 src/apps/kpunch/dvxbasic/compiler/compiler.c|plainInt|if (n < 0 || n >= (int)sizeof(qualified) || strchr(md->name, BAS_MEMBER_QUALIFIER[0]) != NULL) { +1 src/apps/kpunch/dvxbasic/compiler/compiler.c|plainInt|int n = snprintf(qualified, sizeof(qualified), "" BAS_CLASS_SEP "", ifc->name, md->name); +1 src/apps/kpunch/dvxbasic/compiler/compiler.c|plainInt|snprintf(c->error, sizeof(c->error), "", (int)line, (int)col, text); +1 src/apps/kpunch/dvxbasic/compiler/compiler.c|plainInt|snprintf(c->error, sizeof(c->error), "", (int)line, text); +2 src/apps/kpunch/dvxbasic/compiler/genExpr.c|plainInt|basErrorNode(c, n, "", (int)n->kind); +1 src/apps/kpunch/dvxbasic/compiler/genStmt.c|plainInt|basErrorNode(c, n, "", (int)n->kind); +1 src/apps/kpunch/dvxbasic/compiler/genStmt.c|plainInt|basErrorNode(c, s, "", (int)s->kind); +1 src/apps/kpunch/dvxbasic/compiler/genStmt.c|plainInt|basErrorNode(c, target, "", (int)target->kind); 1 src/apps/kpunch/dvxbasic/compiler/lexer.c|plainInt|snprintf(buf, sizeof(buf), "", (int)BAS_MAX_STRING_LEN); -1 src/apps/kpunch/dvxbasic/compiler/lexer.c|plainInt|snprintf(lex->error, sizeof(lex->error), "", (int)lex->line, (int)lex->col, msg); -1 src/apps/kpunch/dvxbasic/compiler/obfuscate.c|plainInt|snprintf(mapped, sizeof(mapped), "", (long)(m->count + 1)); -2 src/apps/kpunch/dvxbasic/compiler/strip.c|plainInt|snprintf(cand, sizeof(cand), "", (long)nextMangled++); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|"", ctrlName, (int)index); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|for (int i = 0; i < len; i++) { +1 src/apps/kpunch/dvxbasic/compiler/obfuscate.c|plainInt|snprintf(mapped, sizeof(mapped), "", (long)m->count + 1); +1 src/apps/kpunch/dvxbasic/compiler/parseDecl.c|plainInt|basParseError(c, "", (int)BAS_ARRAY_MAX_DIMS); +1 src/apps/kpunch/dvxbasic/compiler/parseDecl.c|plainInt|basParseError(c, "", (int)BAS_MAX_PARAMS); +1 src/apps/kpunch/dvxbasic/compiler/parseDecl.c|plainInt|basParseError(c, "", (int)UINT16_MAX); +1 src/apps/kpunch/dvxbasic/compiler/parseExpr.c|plainInt|basParseError(c, "", (int)count); +1 src/apps/kpunch/dvxbasic/compiler/parseExpr.c|plainInt|basParseError(c, "", what, (int)BAS_ARRAY_MAX_DIMS); +1 src/apps/kpunch/dvxbasic/compiler/parseStmt.c|plainInt|basParseError(c, "", (int)BAS_LINE_NUMBER_MAX); +1 src/apps/kpunch/dvxbasic/compiler/parseStmt.c|plainInt|snprintf(text, sizeof(text), "", (int)value); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|BasSymT *proc = (n > 0 && n < (int)sizeof(name)) ? basScopeFind(&c->moduleTab, name) : NULL; +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|basErrorAt(c, cls->implementsLine, 0, "", cls->name, what, procName, (int)paramCount, paramArray ? "" : "", c->classes[iface].name); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|basErrorAt(c, sym->line, 0, "", BAS_UC_OBJECT, info->name, (int)want, (want > 0) ? "" : "", info->params); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|basErrorNode(c, &s->h, "", ev->name, (int)ev->paramCount, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|basErrorNode(c, at, "", name, (int)BAS_MAX_IDENT); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|basErrorNode(c, target, "", (int)BAS_VM_MAX_CALL_ARGS); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|basErrorNode(c, unit, "", proc->sym->name, (int)localCount, (int)BAS_VM_MAX_LOCALS); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|if (filePrivate ? !basFilePrivateName(c, member, c->fileIdx, bareName, (int32_t)sizeof(bareName)) : snprintf(bareName, sizeof(bareName), "", member) >= (int)sizeof(bareName)) { +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|if (filePrivate ? !basFilePrivateName(c, s->name, c->fileIdx, symName, (int32_t)sizeof(symName)) : snprintf(symName, sizeof(symName), "", s->name) >= (int)sizeof(symName)) { +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|if (n < 0 || n >= (int)sizeof(member) || !basQualify(c, classIdx, member, out, outSize)) { +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|if (n < 0 || n >= (int)sizeof(qualified)) { +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|if (n > 0 && n < (int)sizeof(procName) && procSymbolName(c, classIdx, procName, kind, symName, (int32_t)sizeof(symName), &at)) { +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|int n = snprintf(member, sizeof(member), "", name, suffix); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|int n = snprintf(name, sizeof(name), "" BAS_EVENT_HANDLER_SEP "", def->var->name, ev->name); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|int n = snprintf(out, outSize, "" BAS_CLASS_SEP "", c->classes[classIdx].name, name); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|int n = snprintf(procName, sizeof(procName), "" BAS_EVENT_HANDLER_SEP "", c->classes[iface].name, name); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|int n = snprintf(qualified, sizeof(qualified), "" BAS_CLASS_SEP "", enumSym->name, member); +1 src/apps/kpunch/dvxbasic/compiler/resolve.c|plainInt|snprintf(name, sizeof(name), "", BAS_HIDDEN_PREFIX, (int)c->hiddenCount++); +2 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, &call->h, "", (int)BAS_ARRAY_MAX_DIMS); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, &call->h, "", (int)BAS_VM_MAX_CALL_ARGS); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, a, "", (int)BAS_PARAMARRAY_MAX_ARGS); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, arg, "", (int)(idx + 1), procName, typeDisplayName(refType), typeDisplayName(dataType)); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, arg, "", (int)(idx + 1), sym->name); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, arg, "", (int)(idx + 1), sym->name, wantName, haveName); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, args, "", (int)BAS_VM_MAX_CALL_ARGS); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", (int)BAS_VM_MAX_CALL_ARGS); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", b->name, (int)minArgs, (int)maxArgs, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", basCollMemberName(member), (int)minArgs, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", basCollMemberName(member), (int)minArgs, (int)maxArgs, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", what, sym->name, (int)paramCount, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", what, sym->name, (int)required, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, at, "", what, sym->name, (int)required, (int)paramCount, (int)argc); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|basErrorNode(c, n, "", (int)BAS_ARRAY_MAX_DIMS); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|if (n > 0 && n < (int)sizeof(qualified)) { +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|int n = snprintf(out, (size_t)outSize, "" BAS_FILE_PRIVATE_SEP "", name, (int)fileIdx); +1 src/apps/kpunch/dvxbasic/compiler/resolveExpr.c|plainInt|int n = snprintf(qualified, sizeof(qualified), "" BAS_CLASS_SEP "", c->parseClassName, name); +1 src/apps/kpunch/dvxbasic/compiler/strip.c|plainInt|snprintf(cand, sizeof(cand), "", (long)nextMangled++); +1 src/apps/kpunch/dvxbasic/compiler/strip.c|plainInt|static int compareNamesCI(const void *a, const void *b) { +1 src/apps/kpunch/dvxbasic/compiler/strip.c|plainInt|static int compareNamesCI(const void *a, const void *b); +1 src/apps/kpunch/dvxbasic/formrt/formCtl.c|plainInt|int32_t len = snprintf(out, (size_t)outSize, "", (int)dirLen, cls->file->path, cls->header.toolboxIcon); +1 src/apps/kpunch/dvxbasic/formrt/formCtl.c|plainInt|n += snprintf(text + n, sizeof(text) - (size_t)n, "", (k > 0) ? "" : "", (ev->byValParams & (1u << k)) ? "" : "", (int)nameLen, name, typeName ? typeName : ""); +1 src/apps/kpunch/dvxbasic/formrt/formCtl.c|plainInt|snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); +1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|plainInt|snprintf(out, (size_t)outSize, DSGN_LABEL_FMT, ctrl->name, (int)ctrl->index); +1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|plainInt|snprintf(resName, (size_t)resNameSize, BAS_CTL_RES_SUFFIX_FMT, resBase, (int)pathIdx); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|(int)how); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|(int)pointer); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|*outLen = sSerApi.read(com - 1, (char *)buf, (int)sizeof(buf)); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|basFormRtRuntimeError(rt, "", "", form->name, (int)vc); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|ctrlDisplayName(ctrl), (int)action); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|frm->name, (int)state); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*clearRxBuffer)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*close)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getBase)(int); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getCts)(int); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getDcd)(int); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getDsr)(int); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getDtr)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getIrq)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getPending)(void *); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getRts)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getRxBuffered)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*getUartType)(int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*handshake)(void *); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*handshakeStep)(void *); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*open)(int, int32_t, int, char, int, int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*poll)(void *); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*read)(int, char *, int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*send)(void *, const uint8_t *, int, uint8_t, bool, bool); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*sendBuf)(void *, const uint8_t *, int, uint8_t, bool); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*setBase)(int, int); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*setChannelPlain)(void *, uint8_t, bool); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*setDtr)(int, bool); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*setIrq)(int, int); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*setPassphrase)(void *, const char *); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*setRts)(int, bool); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*write)(int, const char *, int); 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int (*writeBuf)(int, const char *, int); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int rc = sCommApi.send(slot->link, data, len, (uint8_t)slot->termChannel, slot->termEncrypt, false); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int rc = sCommApi.sendBuf(slot->link, (const uint8_t *)data, len, (uint8_t)channel, encrypt != 0); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int rc = sSerApi.open(com - 1, baud, dataBits, parityChar, stopBits, handshake); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int rc = sSerApi.writeBuf(com - 1, data, len); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int rc = sSerApi.writeBuf(sa->com, (const char *)data, len); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|int32_t n = sSerApi.read(com - 1, buf, (int)(sizeof(buf) - 1)); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|snprintf(out, outSize, "", (int)v); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) { -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel); -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t), void *); -1 src/apps/kpunch/dvxbasic/ide/ideDesigner.c|plainInt|pos = emitClamped(buf, bufSize, pos, "", pad, (int)ctrl->index); -1 src/apps/kpunch/dvxbasic/ide/ideDesigner.c|plainInt|pos = emitClamped(buf, bufSize, pos, "", pad, ip->name, (int)val); -1 src/apps/kpunch/dvxbasic/ide/ideDesigner.c|plainInt|snprintf(buf, bufSize, "", prefix, (int)(highest + 1)); -1 src/apps/kpunch/dvxbasic/ide/ideDesigner.c|plainInt|snprintf(out, outSize, "", (int)num); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|(int)errLocalLine, vm->errorMsg); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|emitClamped(buf, bufSize, pos, "", (int)arr->totalElements); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|errFile, (int)errLocalLine, msg); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|errFile, (int)errLocalLine, vm->errorMsg); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|pos = emitClamped(buf, bufSize, pos, "", (int)arr->lbound[d], (int)arr->ubound[d]); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|procName, (int)procLine, msg); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|procName, (int)procLine, vm->errorMsg); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", (int)IDE_MAX_SOURCE, sProject.files[i].path); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", sProject.files[i].path, (int)IDE_MAX_SOURCE); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(bBuf, sizeof(bBuf), "", (int)b); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(buf, bufSize, "", (int)(bufSize - IDE_QUOTE_OVERHEAD), v->strVal->data); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(buf, bufSize, "", (int)v->intVal); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(buf, bufSize, "", (long)v->longVal); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(gBuf, sizeof(gBuf), "", (int)g); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|snprintf(caption, sizeof(caption), "", sep ? (int)(sep - item->label) : (int)strlen(item->label), item->label); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|snprintf(rt->lastLookupName, sizeof(rt->lastLookupName), "", ctrlName, (int)index); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|snprintf(summary, sizeof(summary), "", (int)(sep - msg), msg); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static int32_t serLineGet(int (*const *get)(int), int32_t com) { +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static int32_t serLineGet(int (*const *get)(int), int32_t com); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on) { +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on); +1 src/apps/kpunch/dvxbasic/formrt/formrt.c|plainInt|void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t, bool), void *); +1 src/apps/kpunch/dvxbasic/formrt/frmModelBas.c|plainInt|snprintf(mi.caption, sizeof(mi.caption), "", (int)len, label); +1 src/apps/kpunch/dvxbasic/formrt/frmParser.c|plainInt|dvxLog("", (int)lineNum, FRM_MAX_LINE_LEN - 1); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", errFile, (int)errLocalLine, msg); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", procName, (int)procLine, msg); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(buf, sizeof(buf), "", channel, (int)value); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(formName, sizeof(formName), "", (int)strcspn(base, ""), base); 3 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i); 2 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(label, sizeof(label), "", (long)(i + 1), name); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(label, sizeof(label), "", (long)i + 1, name); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(lineBuf, sizeof(lineBuf), "", (int)sBreakpoints[i].codeLine); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(lineBuf, sizeof(lineBuf), "", (int)sDbgCurrentLine); -1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(rBuf, sizeof(rBuf), "", (int)r); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(name, sizeof(name), control ? IDE_NEW_CTL_FMT : IDE_NEW_CLASS_FMT, (int)n); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(report, sizeof(report), "", (int)errLocalLine, basVmGetError(vm)); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(report, sizeof(report), "", errFile, (int)errLocalLine, basVmGetError(vm)); +1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(report, sizeof(report), "", procName, (int)procLine, basVmGetError(vm)); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(statusBuf, sizeof(statusBuf), "", (int)totalCount); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(tabBuf, sizeof(tabBuf), "", (int)prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH)); +2 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|snprintf(temp, sizeof(temp), IDE_RENAME_TEMP_FMT, (int)r); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|static int cmpEvtPtrs(const void *a, const void *b) { 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|static int cmpEvtPtrs(const void *a, const void *b); 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|static int cmpStrPtrs(const void *a, const void *b) { 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|plainInt|static int cmpStrPtrs(const void *a, const void *b); -1 src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c|plainInt|snprintf(autoName, DSGN_MAX_NAME, MED_SEP_PREFIX "", (int)sepNum); -1 src/apps/kpunch/dvxbasic/ide/ideProject.c|plainInt|snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)i); -1 src/apps/kpunch/dvxbasic/ide/ideProject.c|plainInt|snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++); 1 src/apps/kpunch/dvxbasic/ide/ideProject.c|plainInt|snprintf(msg, sizeof(msg), "", PRJ_ICON_SIZE, PRJ_ICON_SIZE, (int)infoW, (int)infoH); -1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|plainInt|snprintf(buf, sizeof(buf), "", (int)*(const int32_t *)((const char *)ctrl + ip->offset)); 1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|plainInt|snprintf(buf, sizeof(buf), "", (int)ctrl->index); 1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|plainInt|snprintf(buf, sizeof(buf), "", ctrl->name, (int)ctrl->index, ctrl->typeName); 1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|plainInt|snprintf(msg, sizeof(msg), "", (int)maxLen); 1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|plainInt|snprintf(newValue, sizeof(newValue), "", (int)intVal); -1 src/apps/kpunch/dvxbasic/ide/ideToolbox.c|plainInt|snprintf(iconResName, sizeof(iconResName), TBX_RES_SUFFIX_FMT, TBX_RES_ICON, (int)pathIdx); -1 src/apps/kpunch/dvxbasic/ide/ideToolbox.c|plainInt|snprintf(nameResName, sizeof(nameResName), TBX_RES_SUFFIX_FMT, TBX_RES_NAME, (int)pathIdx); +1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|plainInt|snprintf(query, sizeof(query), "", (int)srcLen, recSrc); +1 src/apps/kpunch/dvxbasic/runtime/dates.c|plainInt|snprintf(num, sizeof(num), "", (int)minDigits, (int)value); +1 src/apps/kpunch/dvxbasic/runtime/values.c|plainInt|int32_t len = snprintf(buf, (size_t)bufSize, "", (scaled < 0) ? "" : "", (unsigned long long)(mag / BAS_CURRENCY_SCALE)); 1 src/apps/kpunch/dvxbasic/runtime/values.c|plainInt|int32_t len = snprintf(buf, bufSize, "", (int)sigDigits, n); -1 src/apps/kpunch/dvxbasic/runtime/values.c|plainInt|snprintf(buf, sizeof(buf), "", (int)v.intVal); -1 src/apps/kpunch/dvxbasic/runtime/values.c|plainInt|snprintf(buf, sizeof(buf), "", (long)v.longVal); +1 src/apps/kpunch/dvxbasic/runtime/values.c|plainInt|return len + snprintf(buf + len, (size_t)(bufSize - len), "", (int)width, (unsigned)frac); +1 src/apps/kpunch/dvxbasic/runtime/values.c|plainInt|snprintf(buf + len, sizeof(buf) - len, "", (int)(expVal + expShift)); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|for (long i = used; i < recLen; i++) { -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|fseek((FILE *)vm->files[channel].handle, (long)(pos - 1), SEEK_SET); +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|fseek((FILE *)vm->files[channel].handle, (long)(pos - 1) * unit, SEEK_SET); +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|if ((long)(pos - 1) > LONG_MAX / unit) { +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|if (snprintf(numBuf, sizeof(numBuf), "", (int)decimals, absN) >= (int)sizeof(numBuf)) { +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|int32_t digitCount = snprintf(digits, sizeof(digits), "", (long)n); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long curPos = ftell(fp); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long endPos = ftell(fp); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long length = ftell(fp); -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long offset = (vm->files[channel].mode == BAS_FILE_MODE_BINARY) ? (long)(recno - 1) : (long)(recno - 1) * vm->files[channel].recLen; 2 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long pos = ftell((FILE *)vm->files[channel].handle); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long recLen = vm->files[channel].recLen; 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long savedPos = ftell(fp); -3 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long start = recordBegin(vm, channel, recno); +2 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long start = 0; +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long unit = (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) ? vm->files[channel].recLen : 1; 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|long used = ftell(fp) - start; 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|sleep((unsigned int)secs); 2 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(buf, sizeof(buf), "", (unsigned int)n); -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(numBuf, sizeof(numBuf), "", (int)decimals, absN); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(sciFmt, sizeof(sciFmt), "", (int)fieldLen, (int)decimals); -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(vm->errorMsg, sizeof(vm->errorMsg), "", (int)errNum, (int)vm->currentLine, msg); -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(vm->errorMsg, sizeof(vm->errorMsg), "", (int)errNum, (int)vm->pc, msg); +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(vm->errorMsg, sizeof(vm->errorMsg), "" BAS_VM_ERROR_TEXT_SEP "", (int)errNum, (int)vm->currentLine, msg); +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(vm->errorMsg, sizeof(vm->errorMsg), "" BAS_VM_ERROR_TEXT_SEP "", (int)errNum, (int)vm->currentOpPc, msg); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|snprintf(vm->errorMsg, sizeof(vm->errorMsg), "", (int)module->globalCount, (int)BAS_VM_MAX_GLOBALS); -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|srand((unsigned int)n); +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|srand((unsigned int)(int64_t)reduced); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|srand((unsigned int)time(NULL)); +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|static BasVmResultE recordBegin(BasVmT *vm, int32_t channel, int32_t recno, long *start) { +1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|static BasVmResultE recordBegin(BasVmT *vm, int32_t channel, int32_t recno, long *start); 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|static BasVmResultE recordEnd(BasVmT *vm, int32_t channel, long start, bool writing) { 1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|static BasVmResultE recordEnd(BasVmT *vm, int32_t channel, long start, bool writing); -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|static long recordBegin(BasVmT *vm, int32_t channel, int32_t recno) { -1 src/apps/kpunch/dvxbasic/runtime/vm.c|plainInt|static long recordBegin(BasVmT *vm, int32_t channel, int32_t recno); -1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|fprintf(stderr, "", (int)parser->errorLine, parser->error); +1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|fprintf(stderr, "", (int)err.line, err.message); +1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|fprintf(stderr, "", errPath, (int)errLine, err.message); 1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|long endPos = ftell(szf); 1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|printf("", (int)fileCount); 1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|printf("", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount); 1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|printf("", outputPath, (int)outBytes); +1 src/apps/kpunch/dvxbasic/stub/bascomp.c|plainInt|snprintf(outBuf, sizeof(outBuf), "", projectDir, (int)baseLen, base, ext); 1 src/apps/kpunch/dvxbasic/stub/basrun.c|plainInt|fprintf(stderr, "", (int)errNum, (int)vm->errorLine, basVmGetError(vm)); 1 src/apps/kpunch/dvxbasic/stub/basrun.c|plainInt|fprintf(stderr, "", (int)opt->steps, (int)vm->currentLine); -1 src/apps/kpunch/dvxbasic/stub/basrun.c|plainInt|fprintf(stderr, "", label, (int)parser->errorLine, parser->error); +1 src/apps/kpunch/dvxbasic/stub/basrun.c|plainInt|fprintf(stderr, "", errPath, (int)errLine, basCompilerError(compiler)); 1 src/apps/kpunch/dvxbasic/stub/basstub.c|plainInt|snprintf(app->name, SHELL_APP_NAME_MAX, "", (int)nameSize, appName); 1 src/apps/kpunch/dvxbasic/stub/basstub.c|plainInt|snprintf(ctx->helpFile, sizeof(ctx->helpFile), "" DVX_PATH_SEP "", ctx->appDir, (int)helpNameSize, helpName); +1 src/apps/kpunch/dvxbasic/stub/basstub.c|plainInt|snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i); 1 src/apps/kpunch/dvxbasic/stub/basstub.c|plainInt|snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)i); 1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", (int)(sTotal - sFailed), (int)sTotal); -1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", (int)compLen, comp); -1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", (int)origLen, orig); +1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", (int)comp.len, comp.buf); +1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", (int)op, (int)size); +1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", (int)orig.len, orig.buf); +1 src/apps/kpunch/dvxbasic/test_compact.c|plainInt|printf("", name, (int)removed); +6 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "", (int)i); +1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|len += snprintf(src + len, TEST_SCALE_LINE_MAX, "", (int)i, (int)i, (i > 0) ? "" : ""); +1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|len += snprintf(src + len, sizeof(src) - len, "", i > 0 ? "" : "", (int)i, TEST_NAMED_PAD, 0); 1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|printf("", (int)gFailCount); 1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|printf("", (int)i, mod->procs[i].name, (int)mod->procs[i].paramCount); 1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|printf("", (int)mod->procCount); 1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|printf("", (int)mod->procCount, (int)mod->codeLen); +1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|printf("", (int)n); +1 src/apps/kpunch/dvxbasic/test_compiler.c|plainInt|snprintf(name, sizeof(name), "", (int)i); +2 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|manyPos += snprintf(manySrc + manyPos, sizeof(manySrc) - manyPos, "", (int)ch, (int)ch); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|n += snprintf(detail + n, sizeof(detail) - n, "", (int)mod->codeLen); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|printf("", (int)sPassCount, (int)sFailCount); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)called, (int)countdown); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)called, (int)sBpCount, cap.buf); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)cut); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)first, (int)second, cap.buf); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)i, (int32_t)r, cap.buf); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)i, basTokenName(expected[i].type), expected[i].text, basTokenName(lex.token.type), lex.token.text); -1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)rc, cap.buf, expected); +2 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)k); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)mod->classCount); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)mod->enumCount); +3 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)rc, cap.buf, expected); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)rc, err); 2 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)rc, out); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)result, basVmGetError(vm), cap.buf); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int)sawHandlerConst, (int)sawTextConst); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int32_t)r, (int)countdown, cap.buf); +5 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int32_t)r, (int)vm->currentLine); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int32_t)r, (int)vm->currentLine, cap.buf); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(detail, sizeof(detail), "", (int32_t)r, cap.buf, (int)sExternResolves); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(line, sizeof(line), "", message, (int)flags, title); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(line, sizeof(line), "", uiStubFormName(ctrlRef), methodName, (int)argc); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(path, sizeof(path), "", uiStubFormName(formRef), ctrlName, (int)index); +1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "", (long)hz); 1 src/apps/kpunch/dvxbasic/test_suite.c|plainInt|used += snprintf(src + used, TOO_MANY_GLOBALS_SRC_MAX - used, "", (int)i); -1 src/apps/kpunch/dvxhelp/dvxhelp.c|plainInt|fseek(sHlpFile, -(long)sizeof(HlpHeaderT), SEEK_END); -1 src/apps/kpunch/dvxhelp/dvxhelp.c|plainInt|long savedPos = ftell(sHlpFile); -1 src/apps/kpunch/progman/progman.c|plainInt|dvxLog("", (long)sAppCount); -1 src/apps/kpunch/progman/progman.c|plainInt|qsort(sAppFiles, sAppCount, sizeof(AppEntryT), (int (*)(const void *, const void *))appEntryCmpName); -1 src/apps/kpunch/progman/progman.c|plainInt|snprintf(buf, sizeof(buf), "", (long)count); +1 src/apps/kpunch/dvxhelp/dvxhelp.c|plainInt|snprintf(prompt, sizeof(prompt), "", (long)n); +1 src/installer/pkgBuild.c|plainInt|if (!pkgPartName((int32_t)disk, name, sizeof(name)) || snprintf(path, sizeof(path), "", outDir, name) >= (int)sizeof(path) || !writePart(path, zip + offset, len, &h, NULL, 0)) { +1 src/installer/pkgBuild.c|plainInt|if (snprintf(name, sizeof(name), "", rel, e->d_name) >= (int)sizeof(name) || !pkgNameIsValid(name)) { +1 src/installer/pkgBuild.c|plainInt|long len = ok ? ftell(f) : -1; +1 src/installer/pkgBuild.c|plainInt|static int byName(const void *a, const void *b) { +1 src/installer/pkgBuild.c|plainInt|static int byName(const void *a, const void *b); +1 src/installer/pkgFormat.c|plainInt|int disk = 0; +1 src/installer/pkgFormat.c|plainInt|int32_t n = snprintf(out, outSize, PKG_PART_FMT, (int)disk); +1 src/installer/pkgFormat.c|plainInt|long size = ftell(f); +2 src/installer/pkgRead.c|plainInt|fail(job, "", (int)job->disk); +2 src/installer/pkgRead.c|plainInt|fail(job, "", job->name, (int)job->disk); +1 src/installer/pkgRead.c|plainInt|int flags = job->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0; +2 src/installer/pkgRead.c|plainInt|return fail(job, "", (int)job->disk); +1 src/installer/pkgRead.c|plainInt|return fail(job, "", (int)job->filesDone, (int)job->src.info.fileCount); +1 src/installer/pkgRead.c|plainInt|return fail(job, "", job->name, (int)job->disk); +1 src/installer/pkgRead.c|plainInt|snprintf(out, outSize, "", (int)(end - path), path); +1 src/installer/pkgRead.c|plainInt|snprintf(rel, sizeof(rel), "", (int)i, relName); +1 src/installer/pkgRead.c|plainInt|} else if (fseek(f, (long)start, SEEK_SET) != 0) { +1 src/installer/pkgTool.c|plainInt|printf("", (int)disks); +1 src/installer/pkgTool.c|plainInt|static int cmdInfo(const char *path) { +1 src/installer/pkgTool.c|plainInt|static int cmdInfo(const char *path); +1 src/installer/pkgTool.c|plainInt|static int usage(void) { +1 src/installer/pkgTool.c|plainInt|static int usage(void); +1 src/installer/setup.c|plainInt|dvxLog("", (int)disk, (int)problem); +1 src/installer/setup.c|plainInt|dvxLog("", (int)disk, dir); +1 src/installer/setup.c|plainInt|dvxLog("", (int)sSetup.askDisk); +1 src/installer/setup.c|plainInt|dvxLog("", sSetup.exePath, sFoundNames[sSetup.src.found], (int)sSetup.src.info.diskCount, (int)sSetup.src.info.fileCount); +1 src/installer/setup.c|plainInt|snprintf(line, sizeof(line), "", (int)p.disk, p.current); +1 src/installer/setup.c|plainInt|snprintf(msg, sizeof(msg), "", (int)disk, (int)disk, (int)sSetup.src.info.diskCount, where); +1 src/installer/setup.c|plainInt|snprintf(msg, sizeof(msg), "", (int)disk, (int)sSetup.src.info.diskCount, where); +2 src/installer/setup.c|plainInt|snprintf(msg, sizeof(msg), "", where, (int)disk, (int)sSetup.src.info.diskCount); +1 src/installer/setupCore.c|plainInt|snprintf(bare, sizeof(bare), "", (int)len, name); +1 src/installer/setupCore.c|plainInt|snprintf(err, errSize, "", (int)failed, destDir); +1 src/installer/setupCore.c|plainInt|snprintf(one, sizeof(one), "", (int)(end - start), text + start); +1 src/installer/setupCore.c|plainInt|static int byLengthDesc(const void *a, const void *b) { +1 src/installer/setupCore.c|plainInt|static int byLengthDesc(const void *a, const void *b); +1 src/libs/kpunch/audio/audio.c|plainInt|dvxLog("", drv->hdr.name, info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels == CHANNELS_MAX ? "" : "", (unsigned long)sDev.voiceLimit); +1 src/libs/kpunch/audio/audioBas.c|plainInt|snprintf(sInfo, sizeof(sInfo), "", (long)value); +1 src/libs/kpunch/audio/audioBas.c|plainInt|snprintf(sInfo, sizeof(sInfo), "", info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels > 1 ? "" : ""); +3 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|_farsetsel((unsigned short)sSb.dosSelector); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|dvxLog("", (int)major, (int)minor, (unsigned long)sSb.base, (unsigned long)sSb.irq, (unsigned long)sSb.dma, (int)sSb.plan.mode); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|dvxLog("", (unsigned long)sSb.base); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|dvxLog("", (unsigned long)sSb.irq); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|int segment = __dpmi_allocate_dos_memory((int)(allocBytes / PARAGRAPH_BYTES), &selector); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|int selector = 0; +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|int vector = irqVector(irq); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|return irq < IRQ_SLAVE_FIRST ? VEC_MASTER_BASE + (int)irq : VEC_SLAVE_BASE + (int)(irq - IRQ_SLAVE_FIRST); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|sSb.newVector.pm_offset = (unsigned long)sbIsr; +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|snprintf(info->description, sizeof(info->description), "", sbModelName(sSb.dspVersion), (unsigned long)major, (unsigned long)minor, (unsigned long)sSb.base); +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|static int irqVector(uint32_t irq) { +1 src/libs/kpunch/audio/drivers/sb/sbDrv.c|plainInt|static int irqVector(uint32_t irq); +3 src/libs/kpunch/conn/conn.c|plainInt|fail(c, "", (int)(c->cfg.comPort + COM_NUMBER_BASE)); +1 src/libs/kpunch/conn/conn.c|plainInt|fail(c, "", (int)(c->cfg.comPort + COM_NUMBER_BASE), why); +1 src/libs/kpunch/conn/conn.c|plainInt|fail(c, "", c->cfg.host, (int)c->cfg.tcpPort); +1 src/libs/kpunch/conn/conn.c|plainInt|int err = 0; +1 src/libs/kpunch/conn/conn.c|plainInt|int rc = secLinkHandshakeStep(c->link); +1 src/libs/kpunch/conn/conn.c|plainInt|int rc; +3 src/libs/kpunch/conn/conn.c|plainInt|setStatus(c, "", (int)(c->cfg.comPort + COM_NUMBER_BASE)); +1 src/libs/kpunch/conn/conn.c|plainInt|setStatus(c, "", (int)(c->cfg.comPort + COM_NUMBER_BASE), (long)c->cfg.bps); +3 src/libs/kpunch/conn/conn.c|plainInt|setStatus(c, "", c->cfg.host, (int)c->cfg.tcpPort); +1 src/libs/kpunch/conn/conn.c|plainInt|setStatus(c, "", c->cfg.phone, (int)c->attempt, (int)(c->cfg.redialCount + 1)); +1 src/libs/kpunch/conn/conn.c|plainInt|setStatus(c, "", reason, (int)c->cfg.redialDelay); +1 src/libs/kpunch/conn/conn.c|plainInt|snprintf(port, sizeof(port), "", (int)c->cfg.tcpPort); +1 src/libs/kpunch/conn/conn.c|plainInt|static void secureRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/libs/kpunch/conn/conn.c|plainInt|static void secureRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +1 src/libs/kpunch/dvxnet/dvxNet.c|plainInt|if (v > PART_MAX || end - s > (long)sizeof("") - 1) { +1 src/libs/kpunch/dvxnet/dvxNet.c|plainInt|long v; +2 src/libs/kpunch/dvxnet/dvxNet.c|plainInt|snprintf(key, sizeof(key), KEY_DNS_FORMAT, (int)(i + 1)); +1 src/libs/kpunch/dvxnet/include/arpa/inet.h|plainInt|const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); +1 src/libs/kpunch/dvxnet/include/arpa/inet.h|plainInt|int inet_aton(const char *text, struct in_addr *addr); +1 src/libs/kpunch/dvxnet/include/arpa/inet.h|plainInt|int inet_pton(int af, const char *src, void *dst); +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|const char *gai_strerror(int err); +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|extern int h_errno; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int ai_family; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int ai_flags; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int ai_protocol; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int ai_socktype; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int getnameinfo(const struct sockaddr *addr, socklen_t addrLen, char *host, socklen_t hostLen, char *serv, socklen_t servLen, int flags); +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int h_addrtype; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|int h_length; +1 src/libs/kpunch/dvxnet/include/netdb.h|plainInt|struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type); +1 src/libs/kpunch/dvxnet/include/poll.h|plainInt|int fd; +1 src/libs/kpunch/dvxnet/include/poll.h|plainInt|int poll(struct pollfd *fds, nfds_t count, int timeoutMs); +1 src/libs/kpunch/dvxnet/include/poll.h|plainInt|short events; +1 src/libs/kpunch/dvxnet/include/poll.h|plainInt|short revents; +1 src/libs/kpunch/dvxnet/include/poll.h|plainInt|typedef unsigned int nfds_t; +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int accept(int fd, struct sockaddr *addr, socklen_t *addrLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int bind(int fd, const struct sockaddr *addr, socklen_t addrLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int connect(int fd, const struct sockaddr *addr, socklen_t addrLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int getpeername(int fd, struct sockaddr *addr, socklen_t *addrLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int getsockname(int fd, struct sockaddr *addr, socklen_t *addrLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int getsockopt(int fd, int level, int name, void *value, socklen_t *valueLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int l_linger; +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int l_onoff; +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int listen(int fd, int backlog); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int setsockopt(int fd, int level, int name, const void *value, socklen_t valueLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int shutdown(int fd, int how); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|int socket(int domain, int type, int protocol); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|ssize_t recv(int fd, void *buf, size_t len, int flags); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|ssize_t recvfrom(int fd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromLen); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|ssize_t send(int fd, const void *buf, size_t len, int flags); +1 src/libs/kpunch/dvxnet/include/sys/socket.h|plainInt|ssize_t sendto(int fd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t toLen); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|int client; +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|int fd = handleFd(handle); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|int fd = socket(AF_INET, type, 0); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|int n = 0; +3 src/libs/kpunch/dvxnet/netBas.c|plainInt|int on = 1; +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|int rc; +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|int value = (on != 0); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|long n; +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (long)c->cfg.vector); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (long)st.openSockets); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (long)st.vector); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (unsigned long)st.rxBytes); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (unsigned long)st.rxDropped); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (unsigned long)st.rxPackets); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (unsigned long)st.txBytes); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|snprintf(sField, sizeof(sField), "", (unsigned long)st.txPackets); +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|static int32_t setFlag(int32_t handle, int level, int name, int32_t on) { +1 src/libs/kpunch/dvxnet/netBas.c|plainInt|static int32_t setFlag(int32_t handle, int level, int name, int32_t on); +1 src/libs/kpunch/dvxnet/netDos.c|plainInt|int fcntl(int fd, int cmd, ...) { +1 src/libs/kpunch/dvxnet/netDos.c|plainInt|int fd = __FSEXT_alloc_fd(fsextHandler); +1 src/libs/kpunch/dvxnet/netDos.c|plainInt|int fd = va_arg(args, int); +1 src/libs/kpunch/dvxnet/netDos.c|plainInt|static int fsextHandler(__FSEXT_Fnumber fn, int *rv, va_list args) { +1 src/libs/kpunch/dvxnet/netDos.c|plainInt|static int fsextHandler(__FSEXT_Fnumber fn, int *rv, va_list args); +1 src/libs/kpunch/dvxnet/netDos.c|plainInt|typedef int (*FcntlFnT)(int fd, int cmd, ...); +1 src/libs/kpunch/dvxnet/netInt.h|plainInt|typedef int NetIoctlReqT; +1 src/libs/kpunch/dvxnet/netInt.h|plainInt|typedef int NetNameLenT; +1 src/libs/kpunch/dvxnet/netInt.h|plainInt|typedef unsigned long NetIoctlReqT; +1 src/libs/kpunch/dvxnet/netOrder.c|plainInt|return (unsigned short)((val << BYTE_BITS) | (val >> BYTE_BITS)); +1 src/libs/kpunch/dvxnet/netOrder.c|plainInt|unsigned long htonl(unsigned long val) { +1 src/libs/kpunch/dvxnet/netOrder.c|plainInt|unsigned long ntohl(unsigned long val) { +1 src/libs/kpunch/dvxnet/netOrder.c|plainInt|unsigned short htons(unsigned short val) { +1 src/libs/kpunch/dvxnet/netOrder.c|plainInt|unsigned short ntohs(unsigned short val) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|const char *gai_strerror(int err) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|const char *inet_ntop(int af, const void *src, char *dst, socklen_t size) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|if (serv && servLen > 0 && snprintf(serv, servLen, "", (unsigned)ntohs(in.sin_port)) >= (int)servLen) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int family = hints ? hints->ai_family : AF_UNSPEC; +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int flags = hints ? hints->ai_flags : 0; +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int gethostname(char *name, NetNameLenT len) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int getnameinfo(const struct sockaddr *addr, socklen_t addrLen, char *host, socklen_t hostLen, char *serv, socklen_t servLen, int flags) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int h_errno = 0; +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int inet_aton(const char *text, struct in_addr *addr) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int inet_pton(int af, const char *src, void *dst) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|int sockType = hints ? hints->ai_socktype : 0; +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|long v = strtol(service, &end, DECIMAL_BASE); +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|static const int types[] = { SOCK_STREAM, SOCK_DGRAM }; +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type) { +1 src/libs/kpunch/dvxnet/netResolve.c|plainInt|unsigned long v; # gai_strerror, inet_*: standard BSD socket API names, snake_case by definition. 1 src/libs/kpunch/dvxnet/netResolve.c|snakeCase|gai_strerror 1 src/libs/kpunch/dvxnet/netResolve.c|snakeCase|inet_addr @@ -153,50 +401,103 @@ 1 src/libs/kpunch/dvxnet/netResolve.c|snakeCase|inet_ntoa 1 src/libs/kpunch/dvxnet/netResolve.c|snakeCase|inet_ntop 1 src/libs/kpunch/dvxnet/netResolve.c|snakeCase|inet_pton +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|arg = va_arg(ap, int *); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|for (int fd = 0; fd < nfds; fd++) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int *arg; +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int accept(int fd, struct sockaddr *addr, socklen_t *addrLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int bind(int fd, const struct sockaddr *addr, socklen_t addrLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int connect(int fd, const struct sockaddr *addr, socklen_t addrLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int getpeername(int fd, struct sockaddr *addr, socklen_t *addrLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int getsockname(int fd, struct sockaddr *addr, socklen_t *addrLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int getsockopt(int fd, int level, int name, void *value, socklen_t *valueLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int ioctl(int fd, NetIoctlReqT cmd, ...) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int listen(int fd, int backlog) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int poll(struct pollfd *fds, nfds_t count, int timeoutMs) { +2 src/libs/kpunch/dvxnet/netSock.c|plainInt|int ready = 0; +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int select(int nfds, fd_set *readFds, fd_set *writeFds, fd_set *exceptFds, struct timeval *timeout) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int setsockopt(int fd, int level, int name, const void *value, socklen_t valueLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int shutdown(int fd, int how) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|int socket(int domain, int type, int protocol) { +2 src/libs/kpunch/dvxnet/netSock.c|plainInt|int v; +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|ssize_t recv(int fd, void *buf, size_t len, int flags) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|ssize_t recvfrom(int fd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|ssize_t send(int fd, const void *buf, size_t len, int flags) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|ssize_t sendto(int fd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t toLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static SockT *lookup(int fd) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static SockT *lookup(int fd); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static int fail(int32_t err) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static int fail(int32_t err); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static int32_t optionFor(int level, int name) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static int32_t optionFor(int level, int name); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static ssize_t recvCommon(int fd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static ssize_t recvCommon(int fd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromLen); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static ssize_t sendCommon(int fd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t toLen) { +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|static ssize_t sendCommon(int fd, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t toLen); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|tv->tv_sec = (long)(ms / MS_PER_SEC); +1 src/libs/kpunch/dvxnet/netSock.c|plainInt|tv->tv_usec = (long)((ms % MS_PER_SEC) * USEC_PER_MS); +1 src/libs/kpunch/dvxnet/netStack.c|plainInt|dvxLog("", msg, file, (long)line); 1 src/libs/kpunch/dvxshell/shellApp.c|plainInt|dvxLog("", app->name, (long)entryResult); 1 src/libs/kpunch/dvxshell/shellApp.c|plainInt|dvxLog("", app->name, (long)id, app->hasMainLoop ? "" : "", (unsigned long)entry, (unsigned long)desc); +1 src/libs/kpunch/dvxshell/shellApp.c|plainInt|dvxLog("", path, (unsigned long)desc->abiVersion, DVX_ABI_VERSION); +1 src/libs/kpunch/dvxshell/shellApp.c|plainInt|snprintf(dxeCtx->configDir, sizeof(dxeCtx->configDir), SHELL_CONFIG_ROOT "" SHELL_CONFIG_APPS "", (int)len, base); 1 src/libs/kpunch/dvxshell/shellApp.c|plainInt|snprintf(out, outSize, "" DVX_PATH_SEP "", (long)id, dot); 1 src/libs/kpunch/dvxshell/shellApp.c|plainInt|snprintf(out, outSize, "" DVX_PATH_SEP "", tmpDir, (long)id, dot); -2 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)crashedAppId); -1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)result); +1 src/libs/kpunch/dvxshell/shellAssoc.c|plainInt|pos = dvxStrAppendf(args, argsSize, pos, "", (int)(hit - p), p, documentPath); +1 src/libs/kpunch/dvxshell/shellInfo.c|plainInt|dvxLog("", (int)(eol - line), line); +2 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)endedAppId); 1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)sCtx.wheelDirection, (long)sCtx.dblClickMs, (long)sCtx.mouseAccel, (long)sCtx.mouseMickeys, (long)sCtx.wheelStep); 1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)videoW, (long)videoH, (long)videoBpp); -1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)w, (long)h, (long)bpp); -1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (unsigned long)crashedApp->mainTaskId); -1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", wheelStr, (long)dblClick, accelStr, (long)speed, (long)wheelStep); +2 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (long)w, (long)h, (long)bpp); +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", (unsigned long)app->mainTaskId); +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|dvxLog("", app ? app->name : "", (long)sAppExitStatus); +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|snprintf(msg, sizeof(msg), "", (long)savedW, (long)savedH, (long)savedBpp, (long)w, (long)h, (long)bpp); +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|snprintf(msg, sizeof(msg), "", (long)w, (long)h, (long)bpp, (long)savedW, (long)savedH, (long)savedBpp); +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|static void appExitHook(int status) { +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|static void appExitHook(int status); 1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|static volatile int sCrashSignal = 0; +1 src/libs/kpunch/dvxshell/shellMain.c|plainInt|typedef int (*RngSeedFileFnT)(const char *path); +1 src/libs/kpunch/fileops/fileOps.c|plainInt|int used = 0; 1 src/libs/kpunch/fileops/fileOps.c|plainInt|static int compareEntries(const void *a, const void *b) { 1 src/libs/kpunch/fileops/fileOps.c|plainInt|static int compareEntries(const void *a, const void *b); -1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int b; +1 src/libs/kpunch/fileops/fileOpsSys.c|plainInt|int rc = findfirst(pattern, &ff, DOS_FIND_ATTR); +1 src/libs/kpunch/hlpfile/hlpFile.c|plainInt|fseek(f->fp, -(long)sizeof(HlpHeaderT), SEEK_END) == 0 && +1 src/libs/kpunch/hlpfile/hlpFile.c|plainInt|if (data && (fseek(f->fp, (long)imagePos, SEEK_SET) != 0 || fread(data, 1, ref->imageSize, f->fp) != ref->imageSize)) { +1 src/libs/kpunch/hlpfile/hlpFile.c|plainInt|long savedPos = ftell(f->fp); +1 src/libs/kpunch/hlpfile/hlpFile.c|plainInt|return *out && fseek(f->fp, (long)offset, SEEK_SET) == 0 && fread(*out, entrySize, count, f->fp) == count; +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|dvxLog("", (long)d->width, (long)d->height, (long)d->format.bitsPerPixel); +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|dvxLog("", (long)ex->reqW, (long)ex->reqH); +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|dvxStrAppendf(out, outSize, pos, ACCEL_FKEY_FMT, (long)i + 1); 4 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int channels; 1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int comp = 0; -1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int g; 1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int h = 0; 4 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int imgH; 4 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int imgW; -1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int r; +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int ok; 1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|int w = 0; +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|long num = strtol(part + 1, &end, ACCEL_FKEY_BASE); +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|static uint8_t *rejectEmptyImage(uint8_t *pixels, const int *imgW, const int *imgH) { +1 src/libs/kpunch/libdvx/dvxApp.c|plainInt|static uint8_t *rejectEmptyImage(uint8_t *pixels, const int *imgW, const int *imgH); +1 src/libs/kpunch/libdvx/dvxDialog.c|plainInt|int n = snprintf(out, outSize, "" DVX_PATH_SEP "", dir, name); +1 src/libs/kpunch/libdvx/dvxDialog.c|plainInt|snprintf(buf, bufSize, "", (int)(close - open - 1), open + 1); +1 src/libs/kpunch/libdvx/dvxDialog.c|plainInt|snprintf(buf, sizeof(buf), "", (int)lineLen, line); +1 src/libs/kpunch/libdvx/dvxDialog.c|plainInt|snprintf(dirPart, sizeof(dirPart), "", (int)dirLen, name); 1 src/libs/kpunch/libdvx/dvxDialog.c|plainInt|static int fdEntryCompare(const void *a, const void *b) { 1 src/libs/kpunch/libdvx/dvxDialog.c|plainInt|static int fdEntryCompare(const void *a, const void *b); -1 src/libs/kpunch/libdvx/dvxPrefs.c|plainInt|int ch; -1 src/libs/kpunch/libdvx/dvxPrefs.c|plainInt|int d = tolower((unsigned char)*a) - tolower((unsigned char)*b); +1 src/libs/kpunch/libdvx/dvxDraw.c|plainInt|dvxLog("", (int)d->format.bytesPerPixel); 1 src/libs/kpunch/libdvx/dvxPrefs.c|plainInt|long n = strtol(val, &end, 10); 1 src/libs/kpunch/libdvx/dvxPrefs.c|plainInt|snprintf(buf, sizeof(buf), "", (long)value); -1 src/libs/kpunch/libdvx/dvxPrefs.c|plainInt|static int strcmpci(const char *a, const char *b) { -1 src/libs/kpunch/libdvx/dvxPrefs.c|plainInt|static int strcmpci(const char *a, const char *b); -1 src/libs/kpunch/libdvx/dvxResource.c|plainInt|long dxeSize = dvxResDxeContentSize(path); 1 src/libs/kpunch/libdvx/dvxResource.c|plainInt|static int dvxResEntryCmp(const void *a, const void *b) { -1 src/libs/kpunch/libdvx/dvxWm.c|plainInt|dvxLog("", (long)bufSize); -1 src/libs/kpunch/libdvx/dvxWm.c|plainInt|int channels; -1 src/libs/kpunch/libdvx/dvxWm.c|plainInt|int imgH; -1 src/libs/kpunch/libdvx/dvxWm.c|plainInt|int imgW; +1 src/libs/kpunch/libdvx/dvxResource.c|plainInt|static int dvxResEntryCmp(const void *a, const void *b); +1 src/libs/kpunch/libdvx/dvxWm.c|plainInt|dvxLog("", (long)win->contentPitch * win->contentH); +1 src/libs/kpunch/libdvx/platform/dvxPlat.h|plainInt|typedef void (*PlatformAppExitFnT)(int status); +1 src/libs/kpunch/libdvx/platform/dvxPlat.h|plainInt|void platformAppExit(int status) __attribute__((noreturn)); 1 src/libs/kpunch/libdvx/platform/dvxPlat.h|plainInt|void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn); 1 src/libs/kpunch/libdvx/platform/dvxPlat.h|plainInt|void platformLogCrashDetail(int sig, PlatformLogFnT logFn); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|codeRegion.address = codeAddr + (unsigned long)int9Handler; -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|dataRegion.address = dataAddr + (unsigned long)&sKeyUp; 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern double __floatdidf(long long); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern float __floatdisf(long long); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern long long __divdi3(long long, long long); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern long long __divmoddi4(long long, long long, long long *); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern long long __fixdfdi(double); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern long long __fixsfdi(float); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern long long __moddi3(long long, long long); @@ -213,16 +514,16 @@ 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern void *stbds_shmode_func(size_t elemsize, int mode); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|extern void __dj_assert(const char *, const char *, int); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|fprintf(stderr, "", (long)requestedW, (long)requestedH); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|fprintf(stderr, "", (unsigned long)fbSize); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|fprintf(stderr, "", (unsigned long)physAddr); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|region.address = codeAddr + (unsigned long)int9Handler; -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|region.address = dataAddr + (unsigned long)&sKeyUp; +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|int selector = 0; +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|out->address = base + (unsigned long)addr; +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sNewInt8.pm_offset = (unsigned long)int8Handler; 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sNewInt9.pm_offset = (unsigned long)int9Handler; +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|static uint32_t pagesToKb(unsigned long pages) { +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|static uint32_t pagesToKb(unsigned long pages); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|static void platformCrashHandler(int sig) { 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|static void platformCrashHandler(int sig); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|static volatile int *sPlatformCrashSignal = NULL; -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)(r.x.ax & 0xFF), (long)((r.x.ax >> 8) & 0xFF)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)(r.x.ax & KBD_BYTE_MASK), (long)((r.x.ax >> DOS_VERSION_BYTE_SHIFT) & KBD_BYTE_MASK)); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)display->format.bitsPerPixel); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)display->format.blueBits, (long)display->format.blueShift); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)display->format.greenBits, (long)display->format.greenShift); @@ -230,45 +531,73 @@ 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)display->pitch); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)display->width, (long)display->height); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)family, (long)model, (long)stepping); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)r.x.bx); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)(fbSize / 1024)); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)(totalMem64k * 64)); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)freeKb, (unsigned long)(freeKb / 1024)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (long)sMouseButtons); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)(fbSize / BYTES_PER_KB)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)(totalMem64k * VBE_MEM_BLOCK_KB)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)freeKb, (unsigned long)(freeKb / KB_PER_MB)); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)freeLinearKb); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)largestKb, (unsigned long)(largestKb / 1024)); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)linearKb, (unsigned long)(linearKb / 1024)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)largestKb, (unsigned long)(largestKb / KB_PER_MB)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)linearKb, (unsigned long)(linearKb / KB_PER_MB)); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)mhz); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)pagingKb, (unsigned long)(pagingKb / 1024)); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)totalKb, (unsigned long)(totalKb / 1024)); -1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", letter, driveType, (unsigned long)totalMb, (unsigned long)freeMb); -2 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|unsigned long codeAddr; -2 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|unsigned long dataAddr; +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)pagingKb, (unsigned long)(pagingKb / KB_PER_MB)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", (unsigned long)totalKb, (unsigned long)(totalKb / KB_PER_MB)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|sysInfoAppend("", letter, (unsigned long)(total / (BYTES_PER_KB * KB_PER_MB)), (unsigned long)(avail / (BYTES_PER_KB * KB_PER_MB))); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|unsigned long base; +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|videoError("", (long)requestedW, (long)requestedH); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|videoError("", (unsigned long)fbSize); +1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|videoError("", (unsigned long)physAddr); 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn) { 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|plainInt|void platformLogCrashDetail(int sig, PlatformLogFnT logFn) { -1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|(long)display->width, (long)display->height, (long)display->format.bitsPerPixel); -1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|(unsigned long)totalKb, (unsigned long)freeKb, 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|bool ok = stbi_write_png(path, d->width, d->height, PNG_COMPONENTS, rgb, (int)rowBytes) != 0; +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|dvxStrAppendf(sSysInfoBuf, sizeof(sSysInfoBuf), pos, "", (long)display->width, (long)display->height, (long)display->format.bitsPerPixel); +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|int32_t pos = dvxStrAppendf(sSysInfoBuf, sizeof(sSysInfoBuf), 0, "", (unsigned long)totalKb, (unsigned long)freeKb); 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|memset(dst, (int)(color & 0xFF), (size_t)count); 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|static void crashHandler(int sig) { 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|static void crashHandler(int sig); 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|static volatile int *sCrashSignal = NULL; +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|void __wrap_exit(int status) __attribute__((noreturn)); +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|void __wrap_exit(int status) { 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn) { 1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|plainInt|void platformLogCrashDetail(int sig, PlatformLogFnT logFn) { -1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|plainInt|int drive = toupper((unsigned char)path[0]) - ''; +1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|plainInt|dvxLog("", (unsigned long)bytes); +1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|plainInt|int32_t n = snprintf(out, (size_t)outSize, "", (int)baseLen, path, ext, (int)keep, oldExt); 1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|plainInt|long size = ftell(f); +1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|plainInt|void platformAppExit(int status) { +1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|plainInt|void platformAppExit(int status); +1 src/libs/kpunch/libdvx/widgetClass.c|plainInt|dvxLog("", (unsigned long)wclass->version, WGT_CLASS_VERSION); +1 src/libs/kpunch/libdvx/widgetClass.c|plainInt|dvxLog("", name, (unsigned long)hdr->abiVersion, (unsigned long)hdr->size, DVX_ABI_VERSION, (unsigned long)minSize); +1 src/libs/kpunch/libdvx/widgetIface.c|plainInt|snprintf(buf, bufSize, "", (long)v->i); +1 src/libs/kpunch/music/musicSmf.c|plainInt|static int rawCompare(const void *a, const void *b) { +1 src/libs/kpunch/music/musicSmf.c|plainInt|static int rawCompare(const void *a, const void *b); +1 src/libs/kpunch/music/musicXmp.c|plainInt|if (xmp_start_player(p->ctx, (int)rate, channels == 1 ? XMP_FORMAT_MONO : 0) != 0) { +1 src/libs/kpunch/music/musicXmp.c|plainInt|int bytes = (int)(frames * player->channels * XMP_BYTES_16); +1 src/libs/kpunch/music/musicXmp.c|plainInt|int rc = xmp_load_module_from_memory(p->ctx, data, (long)size); +1 src/libs/kpunch/music/musicXmp.c|plainInt|int rc = xmp_play_buffer(player->ctx, out, bytes, loop ? XMP_LOOP_FOREVER : XMP_LOOP_ONCE); +1 src/libs/kpunch/music/musicXmp.c|plainInt|xmp_seek_time(player->ctx, (int)ms); +1 src/libs/kpunch/print/printJob.c|plainInt|dvxLog("", ok ? "" : "", (unsigned long)job->bytes, job->where); +1 src/libs/kpunch/print/printJob.c|plainInt|snprintf(out, outSize, "", (int)(dest + PRINT_PORT_NUMBER_BASE)); +1 src/libs/kpunch/print/printRaster.c|plainInt|snprintf(cmd, sizeof(cmd), PCL_ROW_FMT, (int)n); +1 src/libs/kpunch/print/printSys.c|plainInt|snprintf(name, sizeof(name), HOST_PORT_FILE, (int)(port + PRINT_PORT_NUMBER_BASE)); +1 src/libs/kpunch/print/printText.c|plainInt|snprintf(out, outSize, "", (int)n, line); +1 src/libs/kpunch/print/printText.c|plainInt|snprintf(stamp, sizeof(stamp), "", (int)page); +1 src/libs/kpunch/print/printText.c|plainInt|snprintf(stamp, sizeof(stamp), "", (int)pages); +1 src/libs/kpunch/print/printUi.c|plainInt|snprintf(allText, sizeof(allText), "", (int)pageCount); 1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { +1 src/libs/kpunch/serial/packet/packet.c|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); 1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < conn->txCount; i++) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < len; i++) { -1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < nRead; i++) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = 0; i < rawLen; i++) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|for (int i = idx; i < conn->txCount; i++) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int com; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int delivered = 0; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int frameLen = conn->rxFrameLen; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int idx = txSlotIndex(conn, seq); +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int lastCount; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int len; -1 src/libs/kpunch/serial/packet/packet.c|plainInt|int nRead; +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int n = conn->stream.write(conn->stream.ctx, stuffed + sent, out - sent); +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int nRead = conn->stream.read(conn->stream.ctx, conn->rxChunk, RX_READ_CHUNK); 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int out; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int payloadLen; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktGetPending(PktConnT *conn) { @@ -279,11 +608,20 @@ 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktReset(PktConnT *conn); 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktWaitSpace(PktConnT *conn) { +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pktWaitSpace(PktConnT *conn); +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int pollDepth; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rawLen; +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rc = pktWaitSpace(conn); +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rxChunkLen; +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rxChunkPos; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int rxFrameLen; +1 src/libs/kpunch/serial/packet/packet.c|plainInt|int sent; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int txCount; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int txHead; 1 src/libs/kpunch/serial/packet/packet.c|plainInt|int windowSize; +1 src/libs/kpunch/serial/packet/packet.c|plainInt|static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { +1 src/libs/kpunch/serial/packet/packet.c|plainInt|static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); 1 src/libs/kpunch/serial/packet/packet.c|plainInt|static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|static bool processFrame(PktConnT *conn, const uint8_t *frame, int len); 1 src/libs/kpunch/serial/packet/packet.c|plainInt|static int seqInWindow(uint8_t seq, uint8_t base, int size) { @@ -295,252 +633,224 @@ 1 src/libs/kpunch/serial/packet/packet.c|plainInt|static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { 1 src/libs/kpunch/serial/packet/packet.c|plainInt|static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); 1 src/libs/kpunch/serial/packet/packet.h|plainInt|PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); +1 src/libs/kpunch/serial/packet/packet.h|plainInt|PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx); 1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktGetPending(PktConnT *conn); 1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktPoll(PktConnT *conn); 1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktReset(PktConnT *conn); 1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); +1 src/libs/kpunch/serial/packet/packet.h|plainInt|int pktWaitSpace(PktConnT *conn); 1 src/libs/kpunch/serial/packet/packet.h|plainInt|typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|char rs232GetParity(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|char rs232GetParity(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|codeRegion.address = codeAddr + (unsigned long)comGeneralIsr; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|dataRegion.address = dataAddr + (unsigned long)sComPorts; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|for (int i = 0; i < FIFO_DEFAULT_THRESHOLD; i++) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|for (int i = 0; i < FIFO_SIZE; i++) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int cnt; -2 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int count; -3 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int i; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rc = RS232_SUCCESS; -2 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rc; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232ClearRxBuffer(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232ClearRxBuffer(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232ClearTxBuffer(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232ClearTxBuffer(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Close(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Close(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetBase(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetBase(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetCts(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetCts(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetData(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetData(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetDsr(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetDsr(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetDtr(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetDtr(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetHandshake(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetHandshake(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetIrq(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetIrq(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetLsr(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetLsr(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetMcr(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetMcr(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetMsr(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetMsr(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetRts(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetRts(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetRxBuffered(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetRxBuffered(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetStop(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetStop(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetTxBuffered(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetTxBuffered(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetUartType(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232GetUartType(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Read(int com, char *data, int len) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Read(int com, char *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetBase(int com, int base) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetBase(int com, int base); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetBps(int com, int32_t bps) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetBps(int com, int32_t bps); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetData(int com, int dataBits) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetData(int com, int dataBits); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetDtr(int com, bool dtr) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetDtr(int com, bool dtr); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetFifoThreshold(int com, int threshold) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetFifoThreshold(int com, int threshold); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetHandshake(int com, int handshake) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetHandshake(int com, int handshake); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetIrq(int com, int irq) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetIrq(int com, int irq); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetMcr(int com, int mcr) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetMcr(int com, int mcr); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetParity(int com, char parity) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetParity(int com, char parity); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetRts(int com, bool rts) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetRts(int com, bool rts); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetStop(int com, int stopBits) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232SetStop(int com, int stopBits); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Write(int com, const char *data, int len) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232Write(int com, const char *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232WriteBuf(int com, const char *data, int len) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int rs232WriteBuf(int com, const char *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int32_t rs232GetBps(int com) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|int32_t rs232GetBps(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|region.address = baseAddr + (unsigned long)comGeneralIsr; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|region.address = baseAddr + (unsigned long)sComPorts; 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|sIsrAddr.pm_offset = (unsigned long)comGeneralIsr; 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int dpmiLockMemory(void) { 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int dpmiLockMemory(void); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int findIrq(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int findIrq(int comport) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int installIrqHandler(int irq) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static int installIrqHandler(int irq); 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info) { 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info); 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info) { 1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void freeIrq(int com); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void freeIrq(int comport) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void removeIrqHandler(int irq) { -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|static void removeIrqHandler(int irq); -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|unsigned long baseAddr; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|unsigned long codeAddr; -1 src/libs/kpunch/serial/rs232/rs232.c|plainInt|unsigned long dataAddr; -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|char rs232GetParity(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232ClearRxBuffer(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232ClearTxBuffer(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232Close(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetBase(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetCts(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetData(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetDsr(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetDtr(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetHandshake(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetIrq(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetLsr(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetMcr(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetMsr(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetRts(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetRxBuffered(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetStop(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetTxBuffered(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232GetUartType(int com); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232Read(int com, char *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetBase(int com, int base); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetBps(int com, int32_t bps); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetData(int com, int dataBits); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetDtr(int com, bool dtr); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetFifoThreshold(int com, int threshold); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetHandshake(int com, int handshake); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetIrq(int com, int irq); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetMcr(int com, int mcr); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetParity(int com, char parity); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetRts(int com, bool rts); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232SetStop(int com, int stopBits); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232Write(int com, const char *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int rs232WriteBuf(int com, const char *data, int len); -1 src/libs/kpunch/serial/rs232/rs232.h|plainInt|int32_t rs232GetBps(int com); 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|for (int i = 0; i < SEC_XTEA_KEY_SIZE; i++) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|if (payloadLen > (int)sizeof(plaintext)) { -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int chunk = len - offset; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int com; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int len; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int offset = 0; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int payloadLen = len - 1; -5 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int rc; +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int pubLen = SEC_DH_KEY_SIZE; 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkGetPending(SecLinkT *link) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkGetPending(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshake(SecLinkT *link) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshake(SecLinkT *link); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshakeStep(SecLinkT *link) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkHandshakeStep(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkPoll(SecLinkT *link) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkPoll(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int state; -1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|payloadLen = (int)sizeof(plaintext); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|int secretLen = SEC_DH_KEY_SIZE; +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int handshakeStatus(const SecLinkT *link) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int handshakeStatus(const SecLinkT *link); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int leaveCall(SecLinkT *link, int32_t rc) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int leaveCall(SecLinkT *link, int32_t rc); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int pollOnce(SecLinkT *link) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static int pollOnce(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void internalRecv(void *ctx, const uint8_t *data, int len) { 1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void internalRecv(void *ctx, const uint8_t *data, int len); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onCipher(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onCipher(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onConfirm(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onConfirm(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onHello(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onHello(SecLinkT *link, const uint8_t *data, int len); +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onPlain(SecLinkT *link, const uint8_t *data, int len) { +1 src/libs/kpunch/serial/seclink/secLink.c|plainInt|static void onPlain(SecLinkT *link, const uint8_t *data, int len); 1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); 1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkGetPending(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkHandshake(SecLinkT *link); +1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkHandshakeStep(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkPoll(SecLinkT *link); 1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); 1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); -1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel); -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < BN_BYTES; i++) { -5 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < BN_WORDS; i++) { +1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain); +1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|int secLinkSetPassphrase(SecLinkT *link, const char *pass); +1 src/libs/kpunch/serial/seclink/secLink.h|plainInt|typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +2 src/libs/kpunch/serial/security/secBas.c|plainInt|int len = SEC_DH_KEY_SIZE; +1 src/libs/kpunch/serial/security/secHash.c|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen) { +1 src/libs/kpunch/serial/security/secHash.c|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); +1 src/libs/kpunch/serial/security/secHash.h|plainInt|int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); +7 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < BN_WORDS; i++) { 1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < M0INV_NEWTON_ITERS; i++) { 1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < R2_DOUBLE_ITERATIONS; i++) { 1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < XTEA_ROUNDS; i++) { -3 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < len; i++) { -2 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = BN_WORDS - 1; i >= 0; i--) { -1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = bits - 1; i >= 0; i--) { +1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = 0; i < len; i++) { +1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = BN_WORDS - 1; i >= 0; i--) { +1 src/libs/kpunch/serial/security/security.c|plainInt|for (int i = DH_PRIVATE_BITS - 1; i >= 0; i--) { 1 src/libs/kpunch/serial/security/security.c|plainInt|for (int j = 0; j < BN_WORDS; j++) { 1 src/libs/kpunch/serial/security/security.c|plainInt|for (int j = 1; j < BN_WORDS; j++) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int bits = i * 32; -1 src/libs/kpunch/serial/security/security.c|plainInt|int bits; 1 src/libs/kpunch/serial/security/security.c|plainInt|int carry = bnShiftLeft1(r2); -1 src/libs/kpunch/serial/security/security.c|plainInt|int got = secRngGatherEntropy(entropy, sizeof(entropy)); 2 src/libs/kpunch/serial/security/security.c|plainInt|int j = (BN_WORDS - 1 - i) * 4; -1 src/libs/kpunch/serial/security/security.c|plainInt|int out = 0; 1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { 1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen); 1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGenerateKeys(SecDhT *dh) { 1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGenerateKeys(SecDhT *dh); 1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len) { 1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); -1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngGatherEntropy(uint8_t *buf, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngGatherEntropy(uint8_t *buf, int len); +1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len) { +1 src/libs/kpunch/serial/security/security.c|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngLoadSeed(const char *path) { +1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngLoadSeed(const char *path); +1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngSaveSeed(const char *path) { +1 src/libs/kpunch/serial/security/security.c|plainInt|int secRngSaveSeed(const char *path); 1 src/libs/kpunch/serial/security/security.c|plainInt|return (int)borrow; +1 src/libs/kpunch/serial/security/security.c|plainInt|secRngAddEntropy(seed, (int)got); 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnBit(const BigNumT *a, int n) { 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnBit(const BigNumT *a, int n); -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnBitLength(const BigNumT *a) { -1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnBitLength(const BigNumT *a); 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnCmp(const BigNumT *a, const BigNumT *b) { 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnCmp(const BigNumT *a, const BigNumT *b); 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnShiftLeft1(BigNumT *a) { 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnShiftLeft1(BigNumT *a); 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b) { 1 src/libs/kpunch/serial/security/security.c|plainInt|static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b); -1 src/libs/kpunch/serial/security/security.c|plainInt|static void secureZero(void *ptr, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|static void secureZero(void *ptr, int len); 1 src/libs/kpunch/serial/security/security.c|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) { 1 src/libs/kpunch/serial/security/security.c|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); -1 src/libs/kpunch/serial/security/security.c|plainInt|void secRngAddEntropy(const uint8_t *data, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|void secRngAddEntropy(const uint8_t *data, int len); -1 src/libs/kpunch/serial/security/security.c|plainInt|void secRngBytes(uint8_t *buf, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|void secRngBytes(uint8_t *buf, int len); -1 src/libs/kpunch/serial/security/security.c|plainInt|void secRngSeed(const uint8_t *entropy, int len) { -1 src/libs/kpunch/serial/security/security.c|plainInt|void secRngSeed(const uint8_t *entropy, int len); +1 src/libs/kpunch/serial/security/security.c|plainInt|void secSecureZero(void *ptr, int len) { +1 src/libs/kpunch/serial/security/security.c|plainInt|void secSecureZero(void *ptr, int len); 1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen); 1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhGenerateKeys(SecDhT *dh); 1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); -1 src/libs/kpunch/serial/security/security.h|plainInt|int secRngGatherEntropy(uint8_t *buf, int len); +1 src/libs/kpunch/serial/security/security.h|plainInt|int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len); +1 src/libs/kpunch/serial/security/security.h|plainInt|int secRngLoadSeed(const char *path); +1 src/libs/kpunch/serial/security/security.h|plainInt|int secRngSaveSeed(const char *path); 1 src/libs/kpunch/serial/security/security.h|plainInt|void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|void secRngAddEntropy(const uint8_t *data, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|void secRngBytes(uint8_t *buf, int len); -1 src/libs/kpunch/serial/security/security.h|plainInt|void secRngSeed(const uint8_t *entropy, int len); +1 src/libs/kpunch/serial/security/security.h|plainInt|void secSecureZero(void *ptr, int len); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_end(void) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_end(void); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_init(void) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|int sqlite3_os_init(void); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsAccess(sqlite3_vfs *vfs, const char *name, int flags, int *result) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsAccess(sqlite3_vfs *vfs, const char *name, int flags, int *result); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsCheckReservedLock(sqlite3_file *file, int *result) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsCheckReservedLock(sqlite3_file *file, int *result); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsClose(sqlite3_file *file) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsClose(sqlite3_file *file); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsCurrentTime(sqlite3_vfs *vfs, double *now) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsCurrentTime(sqlite3_vfs *vfs, double *now); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsCurrentTimeInt64(sqlite3_vfs *vfs, sqlite3_int64 *now) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsCurrentTimeInt64(sqlite3_vfs *vfs, sqlite3_int64 *now); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsDelete(sqlite3_vfs *vfs, const char *name, int syncDir) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsDelete(sqlite3_vfs *vfs, const char *name, int syncDir); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsDeviceCharacteristics(sqlite3_file *file) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsDeviceCharacteristics(sqlite3_file *file); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsFileControl(sqlite3_file *file, int op, void *arg) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsFileControl(sqlite3_file *file, int op, void *arg); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsFileSize(sqlite3_file *file, sqlite3_int64 *size) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsFileSize(sqlite3_file *file, sqlite3_int64 *size); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsFullPathname(sqlite3_vfs *vfs, const char *name, int outSize, char *out) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsFullPathname(sqlite3_vfs *vfs, const char *name, int outSize, char *out); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsGetLastError(sqlite3_vfs *vfs, int bufSize, char *buf) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsGetLastError(sqlite3_vfs *vfs, int bufSize, char *buf); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsLock(sqlite3_file *file, int level) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsLock(sqlite3_file *file, int level); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsOpen(sqlite3_vfs *vfs, sqlite3_filename name, sqlite3_file *file, int flags, int *outFlags) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsOpen(sqlite3_vfs *vfs, sqlite3_filename name, sqlite3_file *file, int flags, int *outFlags); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsRandomness(sqlite3_vfs *vfs, int len, char *out) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsRandomness(sqlite3_vfs *vfs, int len, char *out); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsRead(sqlite3_file *file, void *buf, int amount, sqlite3_int64 offset) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsRead(sqlite3_file *file, void *buf, int amount, sqlite3_int64 offset); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsSectorSize(sqlite3_file *file) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsSectorSize(sqlite3_file *file); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsSleep(sqlite3_vfs *vfs, int microseconds) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsSleep(sqlite3_vfs *vfs, int microseconds); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsSync(sqlite3_file *file, int flags) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsSync(sqlite3_file *file, int flags); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsTempName(char *out, int32_t outSize) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsTempName(char *out, int32_t outSize); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsTruncate(sqlite3_file *file, sqlite3_int64 size) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsTruncate(sqlite3_file *file, sqlite3_int64 size); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsUnlock(sqlite3_file *file, int level) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsUnlock(sqlite3_file *file, int level); +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsWrite(sqlite3_file *file, const void *buf, int amount, sqlite3_int64 offset) { +1 src/libs/kpunch/sql/dvxSqlVfs.c|plainInt|static int vfsWrite(sqlite3_file *file, const void *buf, int amount, sqlite3_int64 offset); # sqlite3_os_init / sqlite3_os_end: SQLite requires these exact names for the DVX VFS entry points. 1 src/libs/kpunch/sql/dvxSqlVfs.c|snakeCase|sqlite3_os_end 1 src/libs/kpunch/sql/dvxSqlVfs.c|snakeCase|sqlite3_os_init 1 src/libs/kpunch/taskmgr/shellTaskMgr.c|plainInt|int32_t pos = snprintf(buf, sizeof(buf), "", (long)count); -1 src/libs/kpunch/taskmgr/shellTaskMgr.c|plainInt|snprintf(buf + pos, sizeof(buf) - pos, "", (unsigned long)(usedKb / 1024), (unsigned long)(totalKb / 1024)); -1 src/libs/kpunch/taskmgr/shellTaskMgr.c|plainInt|snprintf(row.mem, sizeof(row.mem), "", (unsigned long)(memKb / 1024)); +1 src/libs/kpunch/taskmgr/shellTaskMgr.c|plainInt|snprintf(buf + pos, sizeof(buf) - pos, "", (unsigned long)(usedKb / TM_KB_PER_MB), (unsigned long)(totalKb / TM_KB_PER_MB)); +1 src/libs/kpunch/taskmgr/shellTaskMgr.c|plainInt|snprintf(row.mem, sizeof(row.mem), "", (unsigned long)(memKb / TM_KB_PER_MB)); 1 src/libs/kpunch/taskmgr/shellTaskMgr.c|plainInt|snprintf(row.mem, sizeof(row.mem), "", (unsigned long)memKb); -1 src/loader/loaderMain.c|plainInt|(int)sLibCount, (int)sWidgetCount); -1 src/loader/loaderMain.c|plainInt|(int)storedLibs, (int)sLibCount, (int)storedWidgets, (int)sWidgetCount); +1 src/libs/kpunch/xfer/xfer.c|plainInt|long local = fileSizeOf(x->askPath); +1 src/libs/kpunch/xfer/xfer.c|plainInt|long size = fileSizeOf(x->paths[i]); +1 src/libs/kpunch/xfer/xfer.c|plainInt|long size = ftell(fp); +1 src/libs/kpunch/xfer/xfer.c|plainInt|long size = ftell(x->fp); +1 src/libs/kpunch/xfer/xfer.c|plainInt|long size = strtol(sizeText, &end, DECIMAL); +1 src/libs/kpunch/xfer/xfer.c|plainInt|return nameLen + 1 + snprintf(out + nameLen + 1, (size_t)(outSize - nameLen - 1), "", (long)x->prog.fileSize, mode, (long)left, (long)bytes); +1 src/libs/kpunch/xfer/xfer.c|plainInt|snprintf(base, sizeof(base), "", (int)baseLen, name); +1 src/libs/kpunch/xfer/xfer.c|plainInt|snprintf(cand, sizeof(cand), "", (int)keep, base, suffix, ext); +1 src/libs/kpunch/xfer/xfer.c|plainInt|snprintf(suffix, sizeof(suffix), "", (int)n); +1 src/libs/kpunch/xfer/xfer.c|plainInt|static long fileSizeOf(const char *path) { +1 src/libs/kpunch/xfer/xfer.c|plainInt|static long fileSizeOf(const char *path); +1 src/libs/kpunch/xfer/xferZmodem.c|plainInt|if (fseek(x->fp, (long)pos, SEEK_SET) != 0) { +1 src/libs/kpunch/xfer/xferZmodem.c|plainInt|long vals[INFO_FIELDS]; +1 src/loader/loaderMain.c|plainInt|dvxLog("", (int)(total - loaded), (int)total); +1 src/loader/loaderMain.c|plainInt|dvxLog("", (int)failures, (int)retries, HELP_MAX_RETRIES); +1 src/loader/loaderMain.c|plainInt|dvxLog("", (int)failures, HELP_MAX_RETRIES); 1 src/loader/loaderMain.c|plainInt|dvxLog("", (int)rc); -1 src/loader/loaderMain.c|plainInt|dvxLog("", hcfPath, ctx.outputFile, (int)inputCount); +1 src/loader/loaderMain.c|plainInt|dvxLog("", (int)sLibCount, (int)sWidgetCount); +1 src/loader/loaderMain.c|plainInt|dvxLog("", (int)storedLibs, (int)sLibCount, (int)storedWidgets, (int)sWidgetCount); +1 src/loader/loaderMain.c|plainInt|dvxLog("", job->hcfPath, job->outputFile, (int)inputCount); +1 src/loader/loaderMain.c|plainInt|dvxLog("", path, (unsigned long)*stamp, DVX_ABI_VERSION); 1 src/loader/loaderMain.c|plainInt|fprintf(stderr, "", (int)(total - loaded), (int)total); 1 src/loader/loaderMain.c|plainInt|fprintf(stderr, "", (int)errors); 1 src/loader/loaderMain.c|plainInt|int result = shellMain(argc, argv); +1 src/loader/loaderMain.c|plainInt|snprintf(key, sizeof(key), "", (int)klen, p); +1 src/loader/loaderMain.c|plainInt|snprintf(pattern, sizeof(pattern), "", (int)(bang - p), p); 1 src/loader/loaderMain.c|plainInt|typedef int (*ShellMainFnT)(int, char **); 1 src/loader/loaderMain.c|plainInt|while (fgets(line, (int)sizeof(line), f)) { 1 src/loader/stddclmr.h|blockComment|/* +1 src/test/audio/audioTest.c|plainInt|fprintf(f, "", sGolden[i].name, (unsigned long long)sGolden[i].hash); +1 src/test/audio/audioTest.c|plainInt|unsigned long long hash = 0; +1 src/test/basdev/testBasDev.c|plainInt|snprintf(ctl0, sizeof(ctl0), BAS_RES_CTL_FMT, (long)0); +1 src/test/basdev/testBasDev.c|plainInt|snprintf(expect, sizeof(expect), "" BAS_DBG_MODULE_FRAME "", (int)bp); +1 src/test/basic/basBench.c|plainInt|fprintf(f, "", BENCH_FORMAT_VERSION, date, (int)opt->runs, (int)opt->large, (int)opt->many, opt->release ? "" : "", cpu); +1 src/test/basic/basBench.c|plainInt|fprintf(f, "", row->name, row->medianMs, (unsigned long long)row->instrs, (unsigned long long)row->allocs, (unsigned long long)row->peakBytes, (unsigned long long)row->codeBytes, row->check); +1 src/test/basic/basBench.c|plainInt|fprintf(stderr, "", label, (int)basCompilerErrorLine(compiler), basCompilerError(compiler)); +1 src/test/basic/basBench.c|plainInt|fprintf(stderr, "", path, (int)vm->errorNumber, (int)vm->errorLine, basVmGetError(vm)); +1 src/test/basic/basBench.c|plainInt|printf("", sOpNames[best], (unsigned long long)basBenchOpCounts[best], (double)basBenchOpCounts[best] * BENCH_PERCENT / (double)total); +1 src/test/basic/basBench.c|plainInt|snprintf(line, sizeof(line), "", (int)scr->width, (int)scr->height, (unsigned)hash); +1 src/test/basic/basBench.c|plainInt|snprintf(out, (size_t)outSize, "", (int)len, start); +1 src/test/basic/basBench.c|plainInt|snprintf(outPath, sizeof(outPath), "", (int)(strlen(basPath) - strlen("")), basPath); +1 src/test/basic/basBench.c|plainInt|static int compareDoubles(const void *a, const void *b) { +1 src/test/basic/basBench.c|plainInt|static int compareDoubles(const void *a, const void *b); +1 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)(i + 1)); +1 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)(i - 1)); +1 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)(procs - 1)); +7 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)i); +1 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)i, (int)(i - 1), (int)i); +4 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)i, (int)i); +2 src/test/basic/basBench.c|plainInt|textAppend(&tb, "", (int)procs); +1 src/test/basic/basBench.c|plainInt|unsigned long long allocs = 0; +1 src/test/basic/basBench.c|plainInt|unsigned long long code = 0; +1 src/test/basic/basBench.c|plainInt|unsigned long long instrs = 0; +1 src/test/basic/basBench.c|plainInt|unsigned long long peak = 0; 1 src/test/basic/fuzzCommon.c|plainInt|int system(const char *command) { 1 src/test/basic/fuzzCommon.c|plainInt|int system(const char *command); 1 src/test/basic/fuzzCommon.c|plainInt|long size = ftell(f); @@ -559,6 +869,37 @@ 1 src/test/basic/fuzzModule.c|plainInt|snprintf(path, sizeof(path), "", dumpDir, (int)mutants); 1 src/test/basic/fuzzModule.c|plainInt|snprintf(path, sizeof(path), FUZZ_LEAK_FILE, (int)i); 1 src/test/dialogs/dialogsTest.c|plainInt|printf("", (long)loaded); +1 src/test/dos/gameTest/gameTest.c|plainInt|dvxLog("", (unsigned long)frames, (unsigned long)p->missed, (unsigned long)minUs, (unsigned long)maxUs, (unsigned long)(frames ? elapsedUs / frames : 0), (unsigned long)elapsedUs); +1 src/test/dos/gameTest/gameTest.c|plainInt|dvxLog("", (unsigned long)key, (unsigned long)mod); +1 src/test/dos/gameTest/gameTest.c|plainInt|dvxLog("", (unsigned long)sTicks, (unsigned long)expectTicks, (unsigned long)bios, (unsigned long)expectBios); +1 src/test/dos/gameTest/gameTest.c|plainInt|dvxLog("", (unsigned long)st.rateHz); +1 src/test/dos/gameTest/gameTest.c|plainInt|dvxLog("", evt->down ? "" : "", evt->scancode, evt->extended ? 1 : 0, evt->repeat ? 1 : 0, (unsigned long)evt->modifiers); +1 src/test/dos/imgCmp.c|plainInt|fprintf(stderr, "", path, (int)lineNo); +1 src/test/dos/imgCmp.c|plainInt|int ah; +1 src/test/dos/imgCmp.c|plainInt|int aw; +1 src/test/dos/imgCmp.c|plainInt|int gh; +1 src/test/dos/imgCmp.c|plainInt|int gw; +2 src/test/dos/imgCmp.c|plainInt|int h; +2 src/test/dos/imgCmp.c|plainInt|int n; +1 src/test/dos/imgCmp.c|plainInt|int ok = stbi_write_png(out, w, h, CHANNELS, pixels, w * CHANNELS); +2 src/test/dos/imgCmp.c|plainInt|int w; +1 src/test/dos/imgCmp.c|plainInt|int x; +1 src/test/dos/imgCmp.c|plainInt|int y; +1 src/test/dos/imgCmp.c|plainInt|printf("", (int)diffCount, (int)minX, (int)maxX, (int)minY, (int)maxY); +1 src/test/dos/musTest/musTest.c|plainInt|dvxLog("", mode, (int)info.where, (int)wantOut); +1 src/test/dos/musTest/musTest.c|plainInt|dvxLog("", mode, (long)problems); +1 src/test/dos/musTest/musTest.c|plainInt|dvxLog("", mode, (long)sEndedMs, (long)due, ok ? "" : ""); +1 src/test/dos/musTest/musTest.c|plainInt|dvxLog("", mode, isMod ? "" : "", (int)info.where, info.output); +1 src/test/dos/script/scriptLang.c|plainInt|long v = strtol(text, &end, 10); +1 src/test/dos/script/scriptRun.c|plainInt|dvxLog(SCRIPT_LOG_PREFIX "", step->text, (unsigned long)elapsedMs(0)); +1 src/test/dos/script/scriptRun.c|plainInt|snprintf(line, sizeof(line), "", (long long)tv.tv_sec * CS_PER_SECOND + (long long)(tv.tv_usec / US_PER_CS)); +1 src/test/dos/sndTest/sndTest.c|plainInt|dvxLog("", (unsigned long)st.renders, (unsigned long)st.frames, (unsigned long)st.underruns, (unsigned long)st.clipped, (unsigned long)st.dropped); +1 src/test/dos/sndTest/sndTest.c|plainInt|dvxLog("", mode, (long)problems); +1 src/test/dos/sndTest/sndTest.c|plainInt|dvxLog("", mode, info.driver, info.description, (unsigned long)info.rate, (unsigned long)info.bits, (unsigned long)info.channels); +1 src/test/dos/sndTest/sndTest.c|plainInt|dvxLog("", s->name, (long)s->startedMs, (long)s->endedMs, (long)due, ok ? "" : ""); +1 src/test/dos/sndTest/sndTest.c|plainInt|dvxLog("", sSteps[i].name, (int)reason); +1 src/test/dos/sndTest/sndTest.c|plainInt|fprintf(f, "", (unsigned long long)sEpochUs); +1 src/test/dos/sndTest/sndTest.c|plainInt|unsigned long long epoch = 0; 1 src/test/dvxtest/dvxtest.c|plainInt|fprintf(stderr, "", (long long)expected, (long long)actual); 1 src/test/dvxtest/dvxtest.c|plainInt|fprintf(stderr, "", (long)badX, (long)badY); 1 src/test/dvxtest/dvxtest.c|plainInt|fprintf(stderr, "", (long)gw, (long)gh, (long)w, (long)h); @@ -568,17 +909,140 @@ 1 src/test/dvxtest/dvxtest.c|plainInt|printf("", sSuiteName, (long)sPassCount, (long)sFailCount); 1 src/test/dvxtest/dvxtest.c|plainInt|static int32_t removeTempEntry(const char *path, const struct stat *st, int typeflag, struct FTW *ftw) { 1 src/test/dvxtest/dvxtest.c|plainInt|static int32_t removeTempEntry(const char *path, const struct stat *st, int typeflag, struct FTW *ftw); +1 src/test/dvxtest/dvxtest.c|plainInt|static void caseTimeout(int sig) { +1 src/test/dvxtest/dvxtest.c|plainInt|static void caseTimeout(int sig); +1 src/test/dvxtest/selfTest.c|plainInt|int devNull = open("", O_WRONLY); +1 src/test/dvxtest/selfTest.c|plainInt|int status = 0; 1 src/test/dvxtest/selfTest.c|plainInt|printf("", (long)loaded); 1 src/test/formrt/frtHarness.c|plainInt|pos = emitLine(buf, bufSize, pos, indent + 1, "", (int)ctrl->index); 1 src/test/formrt/frtHarness.c|plainInt|pos = emitLine(buf, bufSize, pos, indent, "", name, (int)basValToNumber(value)); +1 src/test/formrt/testFormRt.c|plainInt|pos += snprintf(many + pos, FRT_CHOICE_ITEM_LEN, i ? "" : "", (int)i); 1 src/test/formrt/testFormRt.c|plainInt|printf("", (int)tested); +1 src/test/formrt/testFormRt.c|plainInt|snprintf(expect, sizeof(expect), "", (int)(dst->widget->w / 2), (int)(dst->widget->h / 2)); 2 src/test/formrt/testFormRt.c|plainInt|snprintf(expect, sizeof(expect), "", (int)(lbl->widget->w / 2), (int)(lbl->widget->h / 2)); +1 src/test/formrt/testFormRt.c|plainInt|snprintf(src, FRT_SRC_BUF, "", (int)codeAt, frm, inc, frm + codeAt); +1 src/test/formrt/testFormRt.c|plainInt|snprintf(want, sizeof(want), "", (int)(FRT_PROBE_BIAS + strlen(FRT_PROBE_BAS_ARG))); +1 src/test/formrt/testFormRt.c|plainInt|snprintf(want, sizeof(want), "", (int)MOUSE_LEFT); +1 src/test/formrt/testLibBas.c|plainInt|long value; +1 src/test/fuzz/fuzzBasCompile.c|plainInt|fprintf(stderr, "", (long)len); +1 src/test/fuzz/fuzzBasCompile.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzBasCompile.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzBasModule.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzBasModule.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzFrmModel.c|plainInt|fprintf(stderr, "", (long)size); +1 src/test/fuzz/fuzzFrmModel.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzFrmModel.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzHlpFile.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzHlpFile.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzHlpc.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzHlpc.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzImage.c|plainInt|fprintf(stderr, "", (long)sDepths[i]); +1 src/test/fuzz/fuzzImage.c|plainInt|fprintf(stderr, "", what, (long)w, (long)h, (long)pitch, (long)sCtx->display.format.bytesPerPixel); +1 src/test/fuzz/fuzzImage.c|plainInt|int LLVMFuzzerInitialize(int *argc, char ***argv) { +1 src/test/fuzz/fuzzImage.c|plainInt|int LLVMFuzzerInitialize(int *argc, char ***argv); +1 src/test/fuzz/fuzzImage.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzImage.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzPacket.c|plainInt|(void)pktSend(conn, payload, (int)operand(st, payload, n), false); +1 src/test/fuzz/fuzzPacket.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzPacket.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzPacket.c|plainInt|static void onReceive(void *ctx, const uint8_t *data, int len) { +1 src/test/fuzz/fuzzPacket.c|plainInt|static void onReceive(void *ctx, const uint8_t *data, int len); +1 src/test/fuzz/fuzzPrefs.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzPrefs.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzRes.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +1 src/test/fuzz/fuzzRes.c|plainInt|int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); +1 src/test/fuzz/fuzzUtil.c|plainInt|snprintf(out, outSize, "", (long)getpid(), suffix); +1 src/test/game/gameTest.c|plainInt|static void exitHook(int status) { +1 src/test/game/gameTest.c|plainInt|static void exitHook(int status); +1 src/test/game/gameTest.c|plainInt|void __wrap_exit(int status); 1 src/test/host/smoke.c|plainInt|int status = 0; 1 src/test/host/smoke.c|plainInt|printf("", (long)loaded); 1 src/test/host/smoke.c|plainInt|printf("", (long)sClickCount, argv[2]); 1 src/test/ide/testIde.c|plainInt|printf("", pathA, (long)lenA, pathB, (long)lenB); 1 src/test/ide/testIde.c|plainInt|snprintf(expected, sizeof(expected), "", (int)IDE_MAX_TAB_WIDTH); -2 src/test/serial/loopShim.c|plainInt|for (int com = 0; com < RS232_NUM_PORTS; com++) { +1 src/test/ide/testIdeDesign.c|plainInt|pos = frmEmitClamped(buf, size, pos, "", (int)d); +1 src/test/ide/testIdeDesign.c|plainInt|pos = frmEmitClamped(buf, size, pos, "", (int)lv, lv == menuLevels - 1 ? deepCaption : ""); +1 src/test/ide/testIdeDesign.c|plainInt|snprintf(innerName, sizeof(innerName), "", (int)FRM_MAX_NEST_DEPTH); +1 src/test/ide/testIdeDesign.c|plainInt|snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)TEXT_SYNTAX_STRING); +1 src/test/ide/testIdeDesign.c|plainInt|snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i); +1 src/test/ide/testIdeDesign.c|plainInt|snprintf(name, sizeof(name), "", (int)lv); +1 src/test/ide/testIdeDesign.c|plainInt|snprintf(outerName, sizeof(outerName), "", (int)(FRM_MAX_NEST_DEPTH - 1)); +1 src/test/ide/testIdeDesign.c|plainInt|snprintf(val, sizeof(val), DM_RECENT_FILL, (int)i); +1 src/test/music/musicTest.c|plainInt|fprintf(f, "", sGolden[i].name, (unsigned long long)sGolden[i].hash); +1 src/test/music/musicTest.c|plainInt|printf("", name, (unsigned long long)hash); +1 src/test/music/musicTest.c|plainInt|printf("", name, (unsigned long long)sGolden[i].hash, (unsigned long long)hash); +1 src/test/music/musicTest.c|plainInt|unsigned long long h; +1 src/test/net/fakeDrv.c|plainInt|static int sWire = -1; +1 src/test/net/fakeDrv.c|plainInt|void fakeDrvAttach(int wireFd, const uint8_t *mac) { +1 src/test/net/hostFd.c|plainInt|int close(int fd); +1 src/test/net/hostFd.c|plainInt|int fcntl(int fd, int cmd, ...); +1 src/test/net/hostFd.c|plainInt|int netHostClose(int fd) { +1 src/test/net/hostFd.c|plainInt|int netHostFcntl(int fd, int cmd, ...) { +1 src/test/net/hostFd.c|plainInt|ssize_t netHostRead(int fd, void *buf, size_t len) { +1 src/test/net/hostFd.c|plainInt|ssize_t netHostWrite(int fd, const void *buf, size_t len) { +1 src/test/net/hostFd.c|plainInt|ssize_t read(int fd, void *buf, size_t len); +1 src/test/net/hostFd.c|plainInt|ssize_t write(int fd, const void *buf, size_t len); +1 src/test/net/netTest.c|plainInt|int a = udpOn(PORT_A); +1 src/test/net/netTest.c|plainInt|int avail = -1; +1 src/test/net/netTest.c|plainInt|int avail; +1 src/test/net/netTest.c|plainInt|int b = udpOn(PORT_B); +1 src/test/net/netTest.c|plainInt|int burst; +1 src/test/net/netTest.c|plainInt|int c; +1 src/test/net/netTest.c|plainInt|int client = connectTo("", PORT_A); +2 src/test/net/netTest.c|plainInt|int client = connectTo("", PORT_B); +1 src/test/net/netTest.c|plainInt|int client = socket(AF_INET, SOCK_STREAM, 0); +1 src/test/net/netTest.c|plainInt|int conn = accept(server, (struct sockaddr *)&peer, &len); +2 src/test/net/netTest.c|plainInt|int conn = accept(server, NULL, NULL); +1 src/test/net/netTest.c|plainInt|int conn; +1 src/test/net/netTest.c|plainInt|int count; +1 src/test/net/netTest.c|plainInt|int dhcp; +1 src/test/net/netTest.c|plainInt|int dns; +1 src/test/net/netTest.c|plainInt|int echo; +1 src/test/net/netTest.c|plainInt|int err = -1; +1 src/test/net/netTest.c|plainInt|int err; +1 src/test/net/netTest.c|plainInt|int fd = socket(AF_INET, SOCK_DGRAM, 0); +5 src/test/net/netTest.c|plainInt|int fd = socket(AF_INET, SOCK_STREAM, 0); +2 src/test/net/netTest.c|plainInt|int fd; +1 src/test/net/netTest.c|plainInt|int mine; +4 src/test/net/netTest.c|plainInt|int on = 1; +1 src/test/net/netTest.c|plainInt|int other; +1 src/test/net/netTest.c|plainInt|int pair[2]; +2 src/test/net/netTest.c|plainInt|int server = listenOn(PORT_A); +2 src/test/net/netTest.c|plainInt|int server = listenOn(PORT_B); +1 src/test/net/netTest.c|plainInt|int server; +1 src/test/net/netTest.c|plainInt|int udp = socket(AF_INET, SOCK_DGRAM, 0); +1 src/test/net/netTest.c|plainInt|int udp; +1 src/test/net/netTest.c|plainInt|int v = 1; +1 src/test/net/netTest.c|plainInt|int v; +1 src/test/net/netTest.c|plainInt|snprintf(endpoint, sizeof(endpoint), "", (long)TcpLocalPort(client)); +1 src/test/net/netTest.c|plainInt|snprintf(program, sizeof(program), "", (long)client, (long)server, (long)client); +1 src/test/net/netTest.c|plainInt|snprintf(program, sizeof(program), "", (long)conn); +1 src/test/net/netTest.c|plainInt|static int connectTo(const char *addr, uint16_t port) { +1 src/test/net/netTest.c|plainInt|static int connectTo(const char *addr, uint16_t port); +1 src/test/net/netTest.c|plainInt|static int listenOn(uint16_t port) { +1 src/test/net/netTest.c|plainInt|static int listenOn(uint16_t port); +1 src/test/net/netTest.c|plainInt|static int udpOn(uint16_t port) { +1 src/test/net/netTest.c|plainInt|static int udpOn(uint16_t port); +1 src/test/net/netTest.c|plainInt|static void dhcpAnswer(int fd) { +1 src/test/net/netTest.c|plainInt|static void dhcpAnswer(int fd); +1 src/test/net/netTest.c|plainInt|static void dnsAnswer(int fd) { +1 src/test/net/netTest.c|plainInt|static void dnsAnswer(int fd); +1 src/test/net/netTest.c|plainInt|static void peerMain(int wire) { +1 src/test/net/netTest.c|plainInt|static void peerMain(int wire); +1 src/test/net/netTestHost.h|plainInt|int netHostClose(int fd); +1 src/test/net/netTestHost.h|plainInt|int netHostFcntl(int fd, int cmd, ...); +1 src/test/net/netTestHost.h|plainInt|ssize_t netHostRead(int fd, void *buf, size_t len); +1 src/test/net/netTestHost.h|plainInt|ssize_t netHostWrite(int fd, const void *buf, size_t len); +1 src/test/net/netTestHost.h|plainInt|void fakeDrvAttach(int wireFd, const uint8_t *mac); +1 src/test/print/printTest.c|plainInt|int ch; +1 src/test/print/printTest.c|plainInt|printf("", (int)got, data, (int)len, expect); +1 src/test/print/printTest.c|plainInt|printf("", path, (int)got, (int)len); +1 src/test/print/printTest.c|plainInt|snprintf(path, sizeof(path), "", (int)vp->opens); +1 src/test/print/printTest.c|plainInt|used += snprintf(text + used, sizeof(text) - (size_t)used, "", (long)i); +1 src/test/serial/loopShim.c|plainInt|bool loopGetBreak(int com) { +1 src/test/serial/loopShim.c|plainInt|bool loopGetDtr(int com) { +1 src/test/serial/loopShim.c|plainInt|bool loopIsOpen(int com) { +3 src/test/serial/loopShim.c|plainInt|for (int com = 0; com < RS232_NUM_PORTS; com++) { 2 src/test/serial/loopShim.c|plainInt|for (int i = 0; i < HOLD_SIZE; i++) { 1 src/test/serial/loopShim.c|plainInt|for (int i = 0; i < len; i++) { 3 src/test/serial/loopShim.c|plainInt|for (int k = 0; k < LOOP_FAULT_COUNT; k++) { @@ -595,68 +1059,264 @@ 1 src/test/serial/loopShim.c|plainInt|static void transmitByte(int com, uint8_t byte, uint32_t forced) { 1 src/test/serial/loopShim.c|plainInt|static void transmitByte(int com, uint8_t byte, uint32_t forced); 1 src/test/serial/loopShim.c|plainInt|void loopGetStats(int com, LoopStatsT *stats) { +1 src/test/serial/loopShim.c|plainInt|void loopSetDcd(int com, bool dcd) { 1 src/test/serial/loopShim.c|plainInt|void loopSetFault(int com, LoopFaultE kind, uint32_t rate, uint32_t seed) { 1 src/test/serial/loopShim.c|plainInt|void loopSetFaultParam(int com, LoopFaultE kind, uint32_t param) { 1 src/test/serial/loopShim.c|plainInt|void loopSetFaultWindow(int com, uint32_t firstByte, uint32_t count) { 1 src/test/serial/loopShim.c|plainInt|void loopSetScope(int com, LoopScopeE scope) { +1 src/test/serial/loopShim.c|plainInt|void loopSetWriteCap(int com, int32_t cap) { +1 src/test/serial/loopShim.h|plainInt|bool loopGetBreak(int com); +1 src/test/serial/loopShim.h|plainInt|bool loopGetDtr(int com); +1 src/test/serial/loopShim.h|plainInt|bool loopIsOpen(int com); 1 src/test/serial/loopShim.h|plainInt|int loopPeer(int com); -1 src/test/serial/loopShim.h|plainInt|int rs232Close(int com); -1 src/test/serial/loopShim.h|plainInt|int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); -1 src/test/serial/loopShim.h|plainInt|int rs232Read(int com, char *data, int len); -1 src/test/serial/loopShim.h|plainInt|int rs232Write(int com, const char *data, int len); 1 src/test/serial/loopShim.h|plainInt|void loopGetStats(int com, LoopStatsT *stats); +1 src/test/serial/loopShim.h|plainInt|void loopSetDcd(int com, bool dcd); 1 src/test/serial/loopShim.h|plainInt|void loopSetFault(int com, LoopFaultE kind, uint32_t rate, uint32_t seed); 1 src/test/serial/loopShim.h|plainInt|void loopSetFaultParam(int com, LoopFaultE kind, uint32_t param); 1 src/test/serial/loopShim.h|plainInt|void loopSetFaultWindow(int com, uint32_t firstByte, uint32_t count); 1 src/test/serial/loopShim.h|plainInt|void loopSetScope(int com, LoopScopeE scope); +1 src/test/serial/loopShim.h|plainInt|void loopSetWriteCap(int com, int32_t cap); +1 src/test/serial/serialTest.h|plainInt|printf("", (int)sPassCount, (int)sFailCount); +1 src/test/serial/serialTest.h|plainInt|printf("", file, line, name, (long long)actual, (long long)expected); +1 src/test/serial/serialTest.h|plainInt|static inline int testSummary(void) { +1 src/test/serial/serialTest.h|plainInt|static inline int testSummary(void); +1 src/test/serial/serialTest.h|plainInt|static inline void checkEq(const char *name, int64_t actual, int64_t expected, const char *file, int line) { +1 src/test/serial/serialTest.h|plainInt|static inline void checkEq(const char *name, int64_t actual, int64_t expected, const char *file, int line); +1 src/test/serial/serialTest.h|plainInt|static inline void checkMem(const char *name, const void *actual, const void *expected, int32_t len, const char *file, int line) { +1 src/test/serial/serialTest.h|plainInt|static inline void checkMem(const char *name, const void *actual, const void *expected, int32_t len, const char *file, int line); +1 src/test/serial/serialTest.h|plainInt|static inline void checkTrue(const char *name, bool cond, const char *file, int line) { +1 src/test/serial/serialTest.h|plainInt|static inline void checkTrue(const char *name, bool cond, const char *file, int line); +1 src/test/serial/testProxy.c|plainInt|SecLinkT *__real_secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); +1 src/test/serial/testProxy.c|plainInt|SecLinkT *__wrap_secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx) { +1 src/test/serial/testProxy.c|plainInt|SecLinkT *__wrap_secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); +1 src/test/serial/testProxy.c|plainInt|TEST_EQ("", (int)recv(sp[1], buf, sizeof(buf), 0), 2); +1 src/test/serial/testProxy.c|plainInt|TEST_EQ("", (int)send(sp[1], "", 2, 0), 2); +1 src/test/serial/testProxy.c|plainInt|TEST_EQ("", pktSend(conn, payload, (int)sizeof(payload), false), PKT_SUCCESS); +1 src/test/serial/testProxy.c|plainInt|TEST_EQ("", rs232Read(RS232_COM3, (char *)buf, (int)sizeof(buf)), 0); +1 src/test/serial/testProxy.c|plainInt|TEST_EQ("", rs232Read(RS232_COM3, (char *)buf, (int)sizeof(buf)), 2); +1 src/test/serial/testProxy.c|plainInt|TEST_EQ("", rs232Read(RS232_COM3, (char *)buf, (int)sizeof(buf)), RS232_ERR_NOT_OPEN); +1 src/test/serial/testProxy.c|plainInt|int __real_accept(int fd, struct sockaddr *addr, socklen_t *len); +1 src/test/serial/testProxy.c|plainInt|int __real_listen(int fd, int backlog); +1 src/test/serial/testProxy.c|plainInt|int __wrap_accept(int fd, struct sockaddr *addr, socklen_t *len) { +1 src/test/serial/testProxy.c|plainInt|int __wrap_accept(int fd, struct sockaddr *addr, socklen_t *len); +1 src/test/serial/testProxy.c|plainInt|int __wrap_listen(int fd, int backlog) { +1 src/test/serial/testProxy.c|plainInt|int __wrap_listen(int fd, int backlog); +1 src/test/serial/testProxy.c|plainInt|int bbsFd; +1 src/test/serial/testProxy.c|plainInt|int bbsListen; +1 src/test/serial/testProxy.c|plainInt|int bbsPort; +1 src/test/serial/testProxy.c|plainInt|int blocker; +7 src/test/serial/testProxy.c|plainInt|int fd; +1 src/test/serial/testProxy.c|plainInt|int fds[2]; +1 src/test/serial/testProxy.c|plainInt|int flags; +1 src/test/serial/testProxy.c|plainInt|int len; +1 src/test/serial/testProxy.c|plainInt|int out; +2 src/test/serial/testProxy.c|plainInt|int port = -1; +8 src/test/serial/testProxy.c|plainInt|int port; +1 src/test/serial/testProxy.c|plainInt|int pp[2]; +1 src/test/serial/testProxy.c|plainInt|int pr; +1 src/test/serial/testProxy.c|plainInt|int rc1; +1 src/test/serial/testProxy.c|plainInt|int rc2; +1 src/test/serial/testProxy.c|plainInt|int sndbuf = SMALL_SNDBUF; +3 src/test/serial/testProxy.c|plainInt|int sp[2]; +2 src/test/serial/testProxy.c|plainInt|int status = 0; +1 src/test/serial/testProxy.c|plainInt|snprintf(path, sizeof(path), "", PASS_FILE_DIR, (int)getpid()); +1 src/test/serial/testProxy.c|plainInt|static bool clientConnect(ClientT *c, int port) { +1 src/test/serial/testProxy.c|plainInt|static bool clientConnect(ClientT *c, int port); +1 src/test/serial/testProxy.c|plainInt|static bool clientPump(ClientT *c, int32_t wantLen, int timeoutMs) { +1 src/test/serial/testProxy.c|plainInt|static bool clientPump(ClientT *c, int32_t wantLen, int timeoutMs); +1 src/test/serial/testProxy.c|plainInt|static bool outWait(ProxyProcT *p, const char *needle, int timeoutMs) { +1 src/test/serial/testProxy.c|plainInt|static bool outWait(ProxyProcT *p, const char *needle, int timeoutMs); +1 src/test/serial/testProxy.c|plainInt|static bool readFull(int fd, void *data, int32_t len, int timeoutMs) { +1 src/test/serial/testProxy.c|plainInt|static bool readFull(int fd, void *data, int32_t len, int timeoutMs); +1 src/test/serial/testProxy.c|plainInt|static bool writeAll(int fd, const void *data, int32_t len) { +1 src/test/serial/testProxy.c|plainInt|static bool writeAll(int fd, const void *data, int32_t len); +1 src/test/serial/testProxy.c|plainInt|static int acceptTimeout(int listenFd, int timeoutMs) { +1 src/test/serial/testProxy.c|plainInt|static int acceptTimeout(int listenFd, int timeoutMs); +1 src/test/serial/testProxy.c|plainInt|static int connectTcp(int port) { +1 src/test/serial/testProxy.c|plainInt|static int connectTcp(int port); +1 src/test/serial/testProxy.c|plainInt|static int freePort(void) { +1 src/test/serial/testProxy.c|plainInt|static int freePort(void); +1 src/test/serial/testProxy.c|plainInt|static int listenTcp(int *port) { +1 src/test/serial/testProxy.c|plainInt|static int listenTcp(int *port); +1 src/test/serial/testProxy.c|plainInt|static int proxyWait(ProxyProcT *p) { +1 src/test/serial/testProxy.c|plainInt|static int proxyWait(ProxyProcT *p); +1 src/test/serial/testProxy.c|plainInt|static void clientRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/test/serial/testProxy.c|plainInt|static void clientRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +1 src/test/serial/testProxy.c|plainInt|static void outPump(ProxyProcT *p, int waitMs) { +1 src/test/serial/testProxy.c|plainInt|static void outPump(ProxyProcT *p, int waitMs); +1 src/test/serial/testProxy.c|plainInt|static void sigAlrmHandler(int sig) { +1 src/test/serial/testProxy.c|plainInt|static void sigAlrmHandler(int sig); 1 src/test/serial/testSerial.c|plainInt|TEST_EQ("", secDhGetPublicKey(x, pubX, &(int){ SEC_DH_KEY_SIZE }), SEC_ERR_NOT_READY); +1 src/test/serial/testSerial.c|plainInt|TEST_EQ("", secLinkSendBuf(pair.a, bulk, (int)sizeof(bulk), TEST_CHANNEL, false), SECLINK_ERR_SEND); +1 src/test/serial/testSerial.c|plainInt|TEST_EQ("", secRngGatherEntropy(big, sizeof(big)), (int)sizeof(big)); +1 src/test/serial/testSerial.c|plainInt|TEST_EQ("", sinkPeer.packetLen[2], SECLINK_FRAME_HDR_SIZE + (int)sizeof(msg) + SECLINK_MAC_SIZE); 1 src/test/serial/testSerial.c|plainInt|for (int b = 0; b < 8; b++) { 1 src/test/serial/testSerial.c|plainInt|for (int i = 0; i < 4; i++) { 1 src/test/serial/testSerial.c|plainInt|for (int i = 0; i < XTEA_ROUNDS; i++) { 1 src/test/serial/testSerial.c|plainInt|int bHandshakeRc; -1 src/test/serial/testSerial.c|plainInt|int len = SEC_DH_KEY_SIZE; -1 src/test/serial/testSerial.c|plainInt|int len; +1 src/test/serial/testSerial.c|plainInt|int com; +1 src/test/serial/testSerial.c|plainInt|int expectRc; +1 src/test/serial/testSerial.c|plainInt|int fds[2]; +3 src/test/serial/testSerial.c|plainInt|int len = SEC_DH_KEY_SIZE; +2 src/test/serial/testSerial.c|plainInt|int len; 1 src/test/serial/testSerial.c|plainInt|int n; -1 src/test/serial/testSerial.c|plainInt|printf("", (int)sPassCount, (int)sFailCount); -1 src/test/serial/testSerial.c|plainInt|printf("", file, line, name, (long long)actual, (long long)expected); +1 src/test/serial/testSerial.c|plainInt|int rc; +1 src/test/serial/testSerial.c|plainInt|int status = 0; +1 src/test/serial/testSerial.c|plainInt|snprintf(missing, sizeof(missing), "", SEED_DIR, (int)getpid()); +1 src/test/serial/testSerial.c|plainInt|snprintf(pathA, sizeof(pathA), "", SEED_DIR, (int)getpid()); +1 src/test/serial/testSerial.c|plainInt|snprintf(pathB, sizeof(pathB), "", SEED_DIR, (int)getpid()); 1 src/test/serial/testSerial.c|plainInt|static bool linkPairOpen(LinkPairT *pair, int portA, int portB, SinkT *sinkA, SinkT *sinkB) { 1 src/test/serial/testSerial.c|plainInt|static bool linkPairOpen(LinkPairT *pair, int portA, int portB, SinkT *sinkA, SinkT *sinkB); +1 src/test/serial/testSerial.c|plainInt|static int32_t readPipe(int fd, uint8_t *buf, int32_t len) { +1 src/test/serial/testSerial.c|plainInt|static int32_t readPipe(int fd, uint8_t *buf, int32_t len); 1 src/test/serial/testSerial.c|plainInt|static int32_t readRaw(int com, uint8_t *buf, int32_t max) { 1 src/test/serial/testSerial.c|plainInt|static int32_t readRaw(int com, uint8_t *buf, int32_t max); -1 src/test/serial/testSerial.c|plainInt|static void checkEq(const char *name, int64_t actual, int64_t expected, const char *file, int line) { -1 src/test/serial/testSerial.c|plainInt|static void checkEq(const char *name, int64_t actual, int64_t expected, const char *file, int line); -1 src/test/serial/testSerial.c|plainInt|static void checkMem(const char *name, const void *actual, const void *expected, int32_t len, const char *file, int line) { -1 src/test/serial/testSerial.c|plainInt|static void checkMem(const char *name, const void *actual, const void *expected, int32_t len, const char *file, int line); -1 src/test/serial/testSerial.c|plainInt|static void checkTrue(const char *name, bool cond, const char *file, int line) { -1 src/test/serial/testSerial.c|plainInt|static void checkTrue(const char *name, bool cond, const char *file, int line); +1 src/test/serial/testSerial.c|plainInt|static void closeInCallbackRecv(void *ctx, const uint8_t *data, int len) { +1 src/test/serial/testSerial.c|plainInt|static void closeInCallbackRecv(void *ctx, const uint8_t *data, int len); +1 src/test/serial/testSerial.c|plainInt|static void closeLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/test/serial/testSerial.c|plainInt|static void closeLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); 1 src/test/serial/testSerial.c|plainInt|static void pktPairOpen(PktPairT *pair, int window, SinkT *sinkA, SinkT *sinkB) { 1 src/test/serial/testSerial.c|plainInt|static void pktPairOpen(PktPairT *pair, int window, SinkT *sinkA, SinkT *sinkB); -1 src/test/serial/testSerial.c|plainInt|static void sinkLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) { -1 src/test/serial/testSerial.c|plainInt|static void sinkLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t channel); +1 src/test/serial/testSerial.c|plainInt|static void reentrantPollRecv(void *ctx, const uint8_t *data, int len) { +1 src/test/serial/testSerial.c|plainInt|static void reentrantPollRecv(void *ctx, const uint8_t *data, int len); +1 src/test/serial/testSerial.c|plainInt|static void reentrantRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/test/serial/testSerial.c|plainInt|static void reentrantRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +1 src/test/serial/testSerial.c|plainInt|static void sinkLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/test/serial/testSerial.c|plainInt|static void sinkLinkRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); 1 src/test/serial/testSerial.c|plainInt|static void sinkPktRecv(void *ctx, const uint8_t *data, int len) { 1 src/test/serial/testSerial.c|plainInt|static void sinkPktRecv(void *ctx, const uint8_t *data, int len); -1 src/test/shell/tasksTest.c|plainInt|snprintf(entry, sizeof(entry), "", (unsigned long)id); -1 src/test/shell/tasksTest.c|plainInt|snprintf(name, sizeof(name), "", (long)i + 1); -1 src/test/shell/tasksTest.c|plainInt|snprintf(name, sizeof(name), "", (long)i); -1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)rc); -1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)st.calls, (int)(expected + 1)); +1 src/test/serial/testSerial.c|plainInt|static void sinkPktRecvCtx(void *ctx, const uint8_t *data, int len) { +1 src/test/serial/testSerial.c|plainInt|static void sinkPktRecvCtx(void *ctx, const uint8_t *data, int len); +1 src/test/serial/testSerial.c|plainInt|unsigned int v; +1 src/test/setup/setupTest.c|plainInt|dvxLog("", (int)i); +1 src/test/setup/setupTest.c|plainInt|snprintf(last, sizeof(last), "", (int)(LOG_FILL_LINES - 1)); +1 src/test/setup/setupTest.c|plainInt|snprintf(sim->log + used, sizeof(sim->log) - used, "", (int)disk, names[problem]); +1 src/test/shell/fixtures/fixClock.c|plainInt|snprintf(text, sizeof(text), "", (long)sShown); +1 src/test/shell/shellMainTest.c|plainInt|int secRngLoadSeed(const char *path) { +1 src/test/shell/shellMainTest.c|plainInt|int secRngSaveSeed(const char *path) { +2 src/test/shell/shellMainTest.c|plainInt|long mark; +1 src/test/shell/shellMainTest.c|plainInt|long n = 0; +1 src/test/shell/shellMainTest.c|plainInt|static bool logHasSince(long offset, const char *text) { +1 src/test/shell/shellMainTest.c|plainInt|static bool logHasSince(long offset, const char *text); +1 src/test/shell/shellMainTest.c|plainInt|static long logSize(void) { +1 src/test/shell/shellMainTest.c|plainInt|static long logSize(void); +3 src/test/terminal/testConn.c|plainInt|int peerCom = PEER_COM; +1 src/test/terminal/testConn.c|plainInt|int32_t n = rs232Read(*(int *)ctx, (char *)buf, maxLen); +1 src/test/terminal/testConn.c|plainInt|return rs232Write(*(int *)ctx, (const char *)data, len); +1 src/test/terminal/testConn.c|plainInt|rs232Write(PEER_COM, text, (int)strlen(text)); +1 src/test/terminal/testConn.c|plainInt|static void peerRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/test/terminal/testConn.c|plainInt|static void peerRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +2 src/test/terminal/testConnProxy.c|plainInt|int bbsListen = listenTcp(&bbsPort); +2 src/test/terminal/testConnProxy.c|plainInt|int bbsPort; +1 src/test/terminal/testConnProxy.c|plainInt|int devNull = open("", O_WRONLY); +2 src/test/terminal/testConnProxy.c|plainInt|int fd = socket(AF_INET, SOCK_STREAM, 0); +1 src/test/terminal/testConnProxy.c|plainInt|int fd; +1 src/test/terminal/testConnProxy.c|plainInt|int one = 1; +2 src/test/terminal/testConnProxy.c|plainInt|int port; +2 src/test/terminal/testConnProxy.c|plainInt|int probe = listenTcp(&port); +1 src/test/terminal/testConnProxy.c|plainInt|int status = 0; +1 src/test/terminal/testConnProxy.c|plainInt|static int connectTcp(int port) { +1 src/test/terminal/testConnProxy.c|plainInt|static int connectTcp(int port); +1 src/test/terminal/testConnProxy.c|plainInt|static int listenTcp(int *port) { +1 src/test/terminal/testConnProxy.c|plainInt|static int listenTcp(int *port); +1 src/test/terminal/testConnProxy.c|plainInt|static pid_t proxyStart(int port, int bbsPort, const char *pass) { +1 src/test/terminal/testConnProxy.c|plainInt|static pid_t proxyStart(int port, int bbsPort, const char *pass); +1 src/test/terminal/testLrzsz.c|plainInt|int ca = fgetc(a); +1 src/test/terminal/testLrzsz.c|plainInt|int cb = (i < origSize) ? fgetc(b) : SUB_BYTE; +1 src/test/terminal/testLrzsz.c|plainInt|int devNull = open("", O_WRONLY); +1 src/test/terminal/testLrzsz.c|plainInt|int fd; +1 src/test/terminal/testLrzsz.c|plainInt|int status = 0; +1 src/test/terminal/testLrzsz.c|plainInt|int sv[2]; +1 src/test/terminal/testLrzsz.c|plainInt|long size = fileSizeOf(path); +1 src/test/terminal/testLrzsz.c|plainInt|printf("", path, (int)i, ca, cb); +1 src/test/terminal/testLrzsz.c|plainInt|printf("", path, (int)padTo); +1 src/test/terminal/testLrzsz.c|plainInt|return (long)st.st_size; +1 src/test/terminal/testLrzsz.c|plainInt|static long fileSizeOf(const char *path) { +1 src/test/terminal/testLrzsz.c|plainInt|static long fileSizeOf(const char *path); +1 src/test/terminal/testTelnet.c|plainInt|int fd = socket(AF_INET, SOCK_STREAM, 0); +1 src/test/terminal/testTelnet.c|plainInt|int fd; +2 src/test/terminal/testTelnet.c|plainInt|int lis = listenLocal(&port); +1 src/test/terminal/testTelnet.c|plainInt|static int listenLocal(uint16_t *port) { +1 src/test/terminal/testTelnet.c|plainInt|static int listenLocal(uint16_t *port); +1 src/test/terminal/testTermApp.c|plainInt|int c = fgetc(fp); +1 src/test/terminal/testTermApp.c|plainInt|int listener; +1 src/test/terminal/testTermApp.c|plainInt|int peer = -1; +1 src/test/terminal/testTermApp.c|plainInt|rs232Write(PEER_COM, text, (int)strlen(text)); +1 src/test/terminal/testTermApp.c|plainInt|snprintf(args, sizeof(args), "", LOCALHOST, (int)ntohs(addr.sin_port)); +1 src/test/terminal/testTermApp.c|plainInt|snprintf(expect, sizeof(expect), "", LOCALHOST, (int)ntohs(addr.sin_port)); +1 src/test/terminal/testXfer.c|plainInt|int ca = fgetc(a); +1 src/test/terminal/testXfer.c|plainInt|int cb = (i < origSize) ? fgetc(b) : SUB_BYTE; +1 src/test/terminal/testXfer.c|plainInt|long left = (protos[p] == XferZmodemE) ? fileSizeOf(RECV_DIR "") : fileSizeOf(protos[p] == XferYmodemE ? RECV_DIR "" : target); +1 src/test/terminal/testXfer.c|plainInt|long part = fileSizeOf(RECV_DIR ""); +1 src/test/terminal/testXfer.c|plainInt|long size = fileSizeOf(path); +1 src/test/terminal/testXfer.c|plainInt|printf("", (int)proto, (int)size, xferProgress(s)->message, xferProgress(r)->message); +1 src/test/terminal/testXfer.c|plainInt|printf("", (int)xferProgress(a)->status, (int)xferProgress(b)->status); +1 src/test/terminal/testXfer.c|plainInt|printf("", path, (int)i, ca, cb); +1 src/test/terminal/testXfer.c|plainInt|printf("", path, (int)padTo); +1 src/test/terminal/testXfer.c|plainInt|return (long)st.st_size; +4 src/test/terminal/testXfer.c|plainInt|snprintf(name, sizeof(name), "", (int)protos[p]); +1 src/test/terminal/testXfer.c|plainInt|snprintf(name, sizeof(name), "", (int)protos[p], (unsigned)run); +1 src/test/terminal/testXfer.c|plainInt|snprintf(name, sizeof(name), "", (int)side, (int)protos[p]); +1 src/test/terminal/testXfer.c|plainInt|static long fileSizeOf(const char *path) { +1 src/test/terminal/testXfer.c|plainInt|static long fileSizeOf(const char *path); +1 src/test/tools/hlpcLibTest.c|plainInt|for (long i = 0; i + (long)needleLen <= len && !found; i++) { +2 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)rc); +1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)st.calls, (int)expected); +1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)st.calls, (int)st.lastCurrent, (int)hlpcProgressTotal(1)); 1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)st.lastCurrent, (int)expected); -1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)st.total); 1 src/test/tools/hlpcLibTest.c|plainInt|fprintf(stderr, "", (int)st.total, (int)st.totalStable, (int)expected); +1 src/test/tools/hlpcLibTest.c|plainInt|long len = 0; +1 src/test/tools/hlpcLibTest.c|plainInt|long len = ftell(f); 1 src/test/tools/hlpcLibTest.c|plainInt|printf("", (int)expected, (int)st.calls); 1 src/test/tools/hlpcLibTest.c|plainInt|printf("", (int)rc, (int)st.lastCurrent, (int)st.total); +1 src/test/tools/hlpcLibTest.c|plainInt|static char *readAll(const char *path, long *outLen) { +1 src/test/tools/hlpcLibTest.c|plainInt|static char *readAll(const char *path, long *outLen); +1 src/test/tools/hlpcOomTest.c|plainInt|fprintf(stderr, "", (int)rc); +4 src/test/tools/hlpcOomTest.c|plainInt|fprintf(stderr, "", (int)sAllocFailAt); +1 src/test/tools/hlpcOomTest.c|plainInt|printf("", (int)(runs - 1)); +1 src/test/tools/resFailTest.c|funcOrder|runDirect +1 src/test/tools/resFailTest.c|plainInt|static int32_t testFseek(FILE *stream, long offset, int whence) { +1 src/test/tools/resFailTest.c|plainInt|static int32_t testFseek(FILE *stream, long offset, int whence); 1 src/test/unit/testClock.c|plainInt|ts.tv_nsec = (long)(ms % CLOCK_HOST_HZ) * CLOCK_NS_PER_MS; -1 src/test/unit/testDraw.c|plainInt|unitFail(__FILE__, __LINE__, "", (long)ci, (long)bpp); -1 src/test/unit/testDraw.c|plainInt|unitFail(__FILE__, __LINE__, "", (long)ci, (long)bpp, (long)firstBad); -1 src/test/unit/testDraw.c|plainInt|unitFail(__FILE__, line, "", (long)used); +2 src/test/unit/testPlatform.c|plainInt|int comp = 0; +2 src/test/unit/testPlatform.c|plainInt|int h = 0; +2 src/test/unit/testPlatform.c|plainInt|int status = 0; +2 src/test/unit/testPlatform.c|plainInt|int w = 0; +2 src/test/unit/testPlatform.c|plainInt|volatile int crashSignal = 0; +1 src/test/unit/testStbDsOom.c|plainInt|fprintf(stderr, "", (long)line, what); +1 src/test/unit/testStbDsOom.c|plainInt|printf("", (long)sPassCount, (long)sFailCount); +1 src/test/unit/testStbDsOom.c|plainInt|snprintf(expect, sizeof(expect), "", (unsigned long)bytes); +1 src/test/unit/testStbDsOom.c|plainInt|snprintf(name, sizeof(name), "", (long)i); +1 src/test/unit/testStbDsOom.c|plainInt|static void onSegv(int sig) { +1 src/test/unit/testStbDsOom.c|plainInt|static void onSegv(int sig); 1 src/test/unit/testUtil.c|plainInt|static int compareNames(const void *a, const void *b) { 1 src/test/unit/testUtil.c|plainInt|static int compareNames(const void *a, const void *b); 1 src/test/unit/unitMain.c|plainInt|fprintf(stderr, "", file, (long)line); 1 src/test/unit/unitMain.c|plainInt|printf("", (long)unitPassCount, (long)unitFailCount); 1 src/test/unit/unitMain.c|plainInt|printf("", sSections[i].name, (long)ran, (long)sSectionFails); +1 src/test/widgets/covMain.c|plainInt|printf("", (int)tProcessInit(DVXTEST_WIDGET_DIR)); +3 src/test/widgets/testCovListView.c|plainInt|snprintf(text[i], sizeof(text[i]), "", (int)i); +1 src/test/widgets/testCovMisc.c|plainInt|snprintf(text, sizeof(text), "", (int)i); +1 src/test/widgets/testCovMisc.c|plainInt|snprintf(title, sizeof(title), "", (int)i); +1 src/test/widgets/testCovPopups.c|plainInt|snprintf(moved, sizeof(moved), "", (int)(maxScroll + 5)); +2 src/test/widgets/testCovPopups.c|plainInt|snprintf(nameBuf, sizeof(nameBuf), "", (int)i); +1 src/test/widgets/testCovTerm.c|plainInt|printf("", (int)c->sentLen); +1 src/test/widgets/testCovTerm.c|plainInt|printf("", (int)len, (int)(clip ? len : 0), clip ? clip : ""); +1 src/test/widgets/testCovTerm.c|plainInt|snprintf(expect, sizeof(expect), "", (long)row, (long)col); +3 src/test/widgets/testCovTerm.c|plainInt|snprintf(line, sizeof(line), "", (int)i); +1 src/test/widgets/testCovText.c|plainInt|pos += snprintf(many + pos, sizeof(many) - pos, "", (int)i, i < 12 ? "" : ""); +1 src/test/widgets/testCovText.c|plainInt|pos += snprintf(text + pos, sizeof(text) - pos, "", (int)i); +1 src/test/widgets/testCovText.c|plainInt|printf("", (int)(clip ? len : 0), clip ? clip : "", (int)len, expect); +7 src/test/widgets/testCovTreeView.c|plainInt|snprintf(name, sizeof(name), "", (int)i); 1 src/test/widgets/testDestroy.c|plainInt|snprintf(text, sizeof(text), "", (int)k); +3 src/test/widgets/testGeneric.c|plainInt|logAdd("", (int)button); +1 src/test/widgets/testGeneric.c|plainInt|logAdd("", (int)button, (int)x, (int)y); +2 src/test/widgets/testGeneric.c|plainInt|logAdd("", (int)keyCode); +1 src/test/widgets/testGeneric.c|plainInt|logAdd("", (int)keyCode, (int)shift); +1 src/test/widgets/testGeneric.c|plainInt|logAdd("", actionChar[action], (int)button, (int)x, (int)y, (int)shift); 1 src/test/widgets/testInput.c|plainInt|printf("", (int)len, expect); 2 src/test/widgets/testInput.c|plainInt|snprintf(text, sizeof(text), "", (int)i); +1 src/test/widgets/testLists.c|plainInt|snprintf(nameBuf, sizeof(nameBuf), "", (int)i); 1 src/test/widgets/testMisc.c|plainInt|printf("", (int)c->sentLen); 1 src/test/widgets/testMisc.c|plainInt|snprintf(line, sizeof(line), "", (int)i); 1 src/test/widgets/testProps.c|plainInt|printf("", (int)count); @@ -664,44 +1324,42 @@ 1 src/test/widgets/testProps.c|plainInt|snprintf(minStr, sizeof(minStr), "", (int)INT32_MIN); 1 src/test/widgets/testRange.c|plainInt|snprintf(text, sizeof(text), "", (int)i); 1 src/test/widgets/widgetsMain.c|plainInt|printf("", (int)tProcessInit(DVXTEST_WIDGET_DIR)); -1 src/test/widgets/wtCommon.c|plainInt|snprintf(out, outSize, "", (int)((int32_t (*)(const WidgetT *))p->getFn)(w)); -1 src/tools/bmp2raw.c|plainInt|printf("", argv[1], argv[2], (int)(sizeof(vgaPal) + PIXEL_COUNT)); -1 src/tools/dvxResWrite.h|plainInt|earliest = (long)entry.offset; -2 src/tools/dvxResWrite.h|plainInt|fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); -1 src/tools/dvxResWrite.h|plainInt|if ((long)entry.offset < earliest) { -1 src/tools/dvxResWrite.h|plainInt|if (fileSize < (long)sizeof(DvxResFooterT)) { -1 src/tools/dvxResWrite.h|plainInt|if (footer.entryCount > (uint32_t)((fileSize - (long)sizeof(DvxResFooterT)) / (long)sizeof(DvxResDirEntryT))) { -1 src/tools/dvxResWrite.h|plainInt|long dxeSize = dvxResDxeContentSize(path); -1 src/tools/dvxResWrite.h|plainInt|long earliest = footer.dirOffset; -2 src/tools/dvxResWrite.h|plainInt|long fileSize = ftell(f); -1 src/tools/dvxResWrite.h|plainInt|long maxDirOffset = fileSize - (long)sizeof(DvxResFooterT); -1 src/tools/dvxResWrite.h|plainInt|static int32_t dvxResReadExisting(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData) { -1 src/tools/dvxResWrite.h|plainInt|static int32_t dvxResReadExisting(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData); -1 src/tools/dvxResWrite.h|plainInt|static int32_t dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) { -1 src/tools/dvxResWrite.h|plainInt|static int32_t dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data); -1 src/tools/dvxResWrite.h|plainInt|static long dvxResDxeContentSize(const char *path) { -1 src/tools/dvxResWrite.h|plainInt|static long dvxResDxeContentSize(const char *path); -1 src/tools/dvxhlpc.c|plainInt|fprintf(stderr, "", (int)errorCount); -2 src/tools/dvxhlpc.c|plainInt|fprintf(stderr, "", currentFile ? currentFile : "", (int)currentLine); -1 src/tools/dvxhlpc.c|plainInt|long imgSize = (long)imgLen; +1 src/tools/bmp2raw.c|plainInt|if (fseek(fp, (long)palOffset, SEEK_SET) != 0 || fread(bmpPal, BMP_PALETTE_ENTRY_SIZE, clrUsed, fp) != clrUsed) { +1 src/tools/bmp2raw.c|plainInt|if (fseek(fp, (long)pixelOffset, SEEK_SET) != 0) { +1 src/tools/bmp2raw.c|plainInt|printf("", argv[1], argv[2], (int)(sizeof(vgaPal) + SPLASH_PIXEL_BYTES)); +1 src/tools/dvxResWrite.c|plainInt|if (!data[i] || fseek(f, (long)e->offset, SEEK_SET) != 0 || fread(data[i], 1, e->size, f) != e->size) { +1 src/tools/dvxResWrite.c|plainInt|if ((long)footer.dirOffset + (long)footer.entryCount * (long)sizeof(DvxResDirEntryT) != dirEnd) { +1 src/tools/dvxResWrite.c|plainInt|if (footer.dirOffset > (uint32_t)dirEnd || footer.entryCount > (uint32_t)(dirEnd / (long)sizeof(DvxResDirEntryT))) { +1 src/tools/dvxResWrite.c|plainInt|if (fseek(f, (long)footer.dirOffset, SEEK_SET) != 0 || fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { +1 src/tools/dvxResWrite.c|plainInt|int32_t dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) { +1 src/tools/dvxResWrite.c|plainInt|layout->dxeSize = (long)earliest; +1 src/tools/dvxResWrite.c|plainInt|long dirEnd = fileSize - (long)sizeof(DvxResFooterT); +1 src/tools/dvxResWrite.c|plainInt|long fileSize = ftell(f); +1 src/tools/dvxResWrite.h|plainInt|int32_t dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data); +1 src/tools/dvxResWrite.h|plainInt|long dxeSize; +1 src/tools/dvxResWrite.h|plainInt|long fileSize; +1 src/tools/dvxhlpc.c|plainInt|int ch = getc(f); +1 src/tools/dvxhlpc.c|plainInt|static int compareIds(const void *a, const void *b) { +1 src/tools/dvxhlpc.c|plainInt|static int compareIds(const void *a, const void *b); 1 src/tools/dvxhlpc.c|plainInt|static int compareIndexEntries(const void *a, const void *b) { 1 src/tools/dvxhlpc.c|plainInt|static int compareIndexEntries(const void *a, const void *b); 1 src/tools/dvxhlpc.c|plainInt|static int compareTrigrams(const void *a, const void *b) { 1 src/tools/dvxhlpc.c|plainInt|static int compareTrigrams(const void *a, const void *b); -1 src/tools/dvxhlpc.c|plainInt|while (fgets(rline, (int)sizeof(rline), rf)) { -2 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum); -1 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum, dataArg); -1 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum, typeStr); -1 src/tools/dvxres.c|plainInt|fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); -2 src/tools/dvxres.c|plainInt|long dxeSize = dvxResDxeContentSize(dxePath); -1 src/tools/dvxres.c|plainInt|long fileSize = ftell(f); -1 src/tools/mkicon.c|plainInt|int32_t x = 15 + (int)(11.0 * __builtin_cos(rad)); -1 src/tools/mkicon.c|plainInt|int32_t x = 15 + (int)(13.0 * __builtin_cos(rad)); -1 src/tools/mkicon.c|plainInt|int32_t y = 15 + (int)(11.0 * __builtin_sin(rad)); -1 src/tools/mkicon.c|plainInt|int32_t y = 15 + (int)(13.0 * __builtin_sin(rad)); -1 src/tools/mkwgticon.c|plainInt|uint16_t bits = (ch >= '' && ch <= '') ? font[(int)ch] : 0; +4 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum); +1 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum, (int)typeLen, typeStart); +1 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum, DVX_RES_NAME_MAX - 1); +1 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum, MANIFEST_LINE_MAX - 2); +1 src/tools/dvxres.c|plainInt|fprintf(stderr, "", manifestPath, (long)lineNum, dp); +1 src/tools/dvxres.c|plainInt|snprintf(name, sizeof(name), "", (int)nameLen, p); +1 src/tools/dvxres.c|plainInt|snprintf(typeStr, sizeof(typeStr), "", (int)typeLen, typeStart); +1 src/tools/mkicon.c|plainInt|int32_t x = 15 + (int)(11.0 * cos(rad)); +1 src/tools/mkicon.c|plainInt|int32_t x = 15 + (int)(13.0 * cos(rad)); +1 src/tools/mkicon.c|plainInt|int32_t y = 15 + (int)(11.0 * sin(rad)); +1 src/tools/mkicon.c|plainInt|int32_t y = 15 + (int)(13.0 * sin(rad)); 1 src/tools/proxy/proxy.c|plainInt|for (int i = 0; i < inLen; i++) { +1 src/tools/proxy/proxy.c|plainInt|for (int i = 0; i < len && outLen < (int)sizeof(out) - 1; i++) { 1 src/tools/proxy/proxy.c|plainInt|for (int i = 0; i < len; i++) { +1 src/tools/proxy/proxy.c|plainInt|int argi = 1; 1 src/tools/proxy/proxy.c|plainInt|int bbsFd = *(int *)ctx; 1 src/tools/proxy/proxy.c|plainInt|int bbsFd = -1; 1 src/tools/proxy/proxy.c|plainInt|int bbsPort = DEFAULT_BBS_PORT; @@ -710,54 +1368,56 @@ 2 src/tools/proxy/proxy.c|plainInt|int fd; 1 src/tools/proxy/proxy.c|plainInt|int listenFd; 1 src/tools/proxy/proxy.c|plainInt|int listenPort = DEFAULT_LISTEN_PORT; +1 src/tools/proxy/proxy.c|plainInt|int n = 0; 1 src/tools/proxy/proxy.c|plainInt|int opt = 1; -1 src/tools/proxy/proxy.c|plainInt|int outLen = 0; -1 src/tools/proxy/proxy.c|plainInt|int pr = poll(&listenPoll, 1, 500); -1 src/tools/proxy/proxy.c|plainInt|int rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false); -1 src/tools/proxy/proxy.c|plainInt|int rc; +2 src/tools/proxy/proxy.c|plainInt|int outLen = 0; +1 src/tools/proxy/proxy.c|plainInt|int pr = poll(&listenPoll, 1, ACCEPT_POLL_MS); +1 src/tools/proxy/proxy.c|plainInt|int rc = SECLINK_PENDING; +2 src/tools/proxy/proxy.c|plainInt|int rc; +1 src/tools/proxy/proxy.c|plainInt|int sendRc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false); 1 src/tools/proxy/proxy.c|plainInt|int sent = 0; +1 src/tools/proxy/proxy.c|plainInt|return got == (int)sizeof(entropy); 1 src/tools/proxy/proxy.c|plainInt|secLinkSend(link, (const uint8_t *)testMsg, (int)strlen(testMsg), CHANNEL_TERMINAL, true, false); 1 src/tools/proxy/proxy.c|plainInt|sent += (int)n; +1 src/tools/proxy/proxy.c|plainInt|static bool readPassphrase(const char *path, char *buf, int bufSize) { +1 src/tools/proxy/proxy.c|plainInt|static bool readPassphrase(const char *path, char *buf, int bufSize); 1 src/tools/proxy/proxy.c|plainInt|static int connectToBbs(const char *host, int port) { 1 src/tools/proxy/proxy.c|plainInt|static int connectToBbs(const char *host, int port); 1 src/tools/proxy/proxy.c|plainInt|static int createListenSocket(int port) { 1 src/tools/proxy/proxy.c|plainInt|static int createListenSocket(int port); +1 src/tools/proxy/proxy.c|plainInt|static int pumpLink(SecLinkT *link) { +1 src/tools/proxy/proxy.c|plainInt|static int pumpLink(SecLinkT *link); 1 src/tools/proxy/proxy.c|plainInt|static int sClientFd = -1; +1 src/tools/proxy/proxy.c|plainInt|static int sSbLen = 0; 1 src/tools/proxy/proxy.c|plainInt|static int sTelState = TS_DATA; 1 src/tools/proxy/proxy.c|plainInt|static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) { 1 src/tools/proxy/proxy.c|plainInt|static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out); -1 src/tools/proxy/proxy.c|plainInt|static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) { -1 src/tools/proxy/proxy.c|plainInt|static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel); +1 src/tools/proxy/proxy.c|plainInt|static int waitForHandshake(SecLinkT *link, int fd) { +1 src/tools/proxy/proxy.c|plainInt|static int waitForHandshake(SecLinkT *link, int fd); +1 src/tools/proxy/proxy.c|plainInt|static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) { +1 src/tools/proxy/proxy.c|plainInt|static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); 1 src/tools/proxy/proxy.c|plainInt|static void sigHandler(int sig) { 1 src/tools/proxy/proxy.c|plainInt|static void sigHandler(int sig); +1 src/tools/proxy/proxy.c|plainInt|static void telnetFinishSb(int bbsFd) { +1 src/tools/proxy/proxy.c|plainInt|static void telnetFinishSb(int bbsFd); +1 src/tools/proxy/proxy.c|plainInt|static void telnetOnDo(int bbsFd, uint8_t opt, bool enable) { +1 src/tools/proxy/proxy.c|plainInt|static void telnetOnDo(int bbsFd, uint8_t opt, bool enable); +1 src/tools/proxy/proxy.c|plainInt|static void telnetOnWill(int bbsFd, uint8_t opt, bool enable) { +1 src/tools/proxy/proxy.c|plainInt|static void telnetOnWill(int bbsFd, uint8_t opt, bool enable); 1 src/tools/proxy/proxy.c|plainInt|static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt) { 1 src/tools/proxy/proxy.c|plainInt|static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt); +1 src/tools/proxy/proxy.c|plainInt|static void telnetSendSb(int bbsFd, const uint8_t *data, int len) { +1 src/tools/proxy/proxy.c|plainInt|static void telnetSendSb(int bbsFd, const uint8_t *data, int len); +1 src/tools/proxy/proxy.c|plainInt|telnetSendSb(bbsFd, naws, (int)sizeof(naws)); +1 src/tools/proxy/proxy.c|plainInt|telnetSendSb(bbsFd, reply, (int)sizeof(reply)); 1 src/tools/proxy/sockShim.c|plainInt|int flags = fcntl(fd, F_GETFL, 0); -1 src/tools/proxy/sockShim.c|plainInt|int rs232Close(int com) { -1 src/tools/proxy/sockShim.c|plainInt|int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) { -1 src/tools/proxy/sockShim.c|plainInt|int rs232Read(int com, char *data, int len) { -1 src/tools/proxy/sockShim.c|plainInt|int rs232Write(int com, const char *data, int len) { -1 src/tools/proxy/sockShim.c|plainInt|int sent = 0; -1 src/tools/proxy/sockShim.c|plainInt|return (int)n; -1 src/tools/proxy/sockShim.c|plainInt|sent += (int)n; -1 src/tools/proxy/sockShim.c|plainInt|static int sFds[RS232_NUM_PORTS] = { -1, -1, -1, -1 }; -1 src/tools/proxy/sockShim.c|plainInt|void sockShimSetFd(int com, int fd) { -1 src/tools/proxy/sockShim.h|plainInt|int rs232Close(int com); -1 src/tools/proxy/sockShim.h|plainInt|int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); -1 src/tools/proxy/sockShim.h|plainInt|int rs232Read(int com, char *data, int len); -1 src/tools/proxy/sockShim.h|plainInt|int rs232Write(int com, const char *data, int len); -1 src/tools/proxy/sockShim.h|plainInt|void sockShimSetFd(int com, int fd); -1 src/tools/proxy/stubs/pc.h|plainInt|static inline unsigned char inportb(unsigned short port) { -1 src/tools/proxy/stubs/pc.h|plainInt|static inline void outportb(unsigned short port, unsigned char val) { -1 src/tools/proxy/stubs/sys/farptr.h|plainInt|static inline unsigned long _farpeekl(unsigned short sel, unsigned long ofs) { -1 src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c|plainInt|int32_t len = snprintf(reply, sizeof(reply), "", (long)(at->cursorRow + 1), (long)(at->cursorCol + 1)); +1 src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c|plainInt|int32_t len = snprintf(reply, sizeof(reply), "", (long)at->cursorRow + 1, (long)at->cursorCol + 1); 1 src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c|plainInt|int32_t len = snprintf(reply, sizeof(reply), "", (long)at->rows, (long)at->cols); -1 src/widgets/kpunch/spinner/widgetSpinner.c|plainInt|d->len = snprintf(d->buf, sizeof(d->buf), "", (int)d->decimals, d->realValue); -1 src/widgets/kpunch/spinner/widgetSpinner.c|plainInt|d->len = snprintf(d->buf, sizeof(d->buf), "", (int)d->value); +1 src/widgets/kpunch/dbGrid/widgetDbGrid.c|plainInt|static int dbGridSortCmp(const void *a, const void *b) { +1 src/widgets/kpunch/dbGrid/widgetDbGrid.c|plainInt|static int dbGridSortCmp(const void *a, const void *b); +1 src/widgets/kpunch/listView/widgetListView.c|plainInt|int32_t len = snprintf(num, sizeof(num), arrlen(text) > 0 ? "" : "", (long)r); +1 src/widgets/kpunch/spinner/widgetSpinner.c|plainInt|snprintf(text, sizeof(text), "", (int)d->decimals, d->realValue); +1 src/widgets/kpunch/spinner/widgetSpinner.c|plainInt|snprintf(text, sizeof(text), "", (int)d->value); 1 src/widgets/kpunch/textInput/widgetTextInput.c|plainInt|int32_t numLen = snprintf(numBuf, sizeof(numBuf), "", (int)(row + 1)); -1 src/test/unit/testPlatform.c|plainInt|volatile int crashSignal = 0; -1 src/test/unit/testPlatform.c|plainInt|int status = 0; -2 src/test/unit/testPlatform.c|plainInt|int w = 0; -2 src/test/unit/testPlatform.c|plainInt|int h = 0; -2 src/test/unit/testPlatform.c|plainInt|int comp = 0; -1 src/test/tools/resFailTest.c|funcOrder|runDirect +1 src/widgets/kpunch/textInput/widgetTextInput.c|plainInt|static int basKeywordCmp(const void *key, const void *entry) { +1 src/widgets/kpunch/textInput/widgetTextInput.c|plainInt|static int basKeywordCmp(const void *key, const void *entry); diff --git a/src/test/lint/clangHost.mk b/src/test/lint/clangHost.mk new file mode 100644 index 00000000..ab231268 --- /dev/null +++ b/src/test/lint/clangHost.mk @@ -0,0 +1,130 @@ +# 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. + +# clangHost.mk -- instrumented builds of DVX host code that live outside +# the product build: the libFuzzer harnesses (src/test/fuzz), the +# MemorySanitizer build (msan.mk) and the plain build valgrind runs +# (valgrind.mk, with CH_CC=gcc), both beside this file. +# +# Set before including: +# CH_VARIANT object tree name: every object goes to +# obj//.o, so +# two sources never collide and nothing touches the +# product object trees +# CH_INSTR instrumentation flags for compiling (and, unless +# CH_LINKFLAGS is set, linking) +# CH_CC compiler (default clang) +# CH_OPT optimization / debug flags (default -O1 -g) +# +# Provides: +# CH_ROOT CH_OBJ CH_BIN repo root, obj/, its bin/ +# CH_CFLAGS the compile flags (warnings off: the clang host build +# reports them, lint/hostDb.py) +# chObjs SRCS objects for absolute source paths +# chMakeVar DIR,VAR VAR as DIR/Makefile defines it, paths made absolute +# CH_* source lists, read from the product Makefiles with chMakeVar so +# no list is copied here (a renamed variable fails the +# build instead of silently building less) +# CH_SRCS_ the link sets of the BASIC harnesses and host tools +# (test_compiler, test_compact, test_suite, bascomp, +# basrun, dvxhlpc, dvxres), composed as the product +# Makefiles compose them +# CH_PROG_RULE $(eval $(call CH_PROG_RULE,name)) links +# $(CH_BIN)/name from $(CH_SRCS_name); bascomp also +# gets the STUB and noicon resources the dvxbasic +# Makefile appends (the DOS build's basstub.app, via +# bin/host/dvxres) +# the object pattern rule; a target-specific CH_EXTRA on an object adds +# flags for that object only (e.g. -DHLPC_NO_MAIN). +# +# Reading a variable runs `make -s -C DIR` with an --eval'd print rule: +# it only parses the product Makefile, never builds anything there. + +CH_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))../../..) +CH_OBJ := $(CH_ROOT)/obj/$(CH_VARIANT) +CH_BIN := $(CH_OBJ)/bin +CH_CC ?= clang +CH_OPT ?= -O1 -g +CH_LINKFLAGS ?= $(CH_INSTR) + +# chMakeVar DIR,VAR -> the value of VAR in DIR/Makefile, each word made +# absolute relative to DIR. +chAbs = $(foreach p,$2,$(if $(filter /%,$p),$p,$(abspath $1/$p))) +chMakeVar = $(call chAbs,$1,$(shell env -u MAKEFLAGS -u MAKELEVEL -u MFLAGS make -s --no-print-directory -C $1 --eval 'chPrintVar: ; @echo $$($2)' chPrintVar 2> /dev/null)) +# chRequire NAME -> fails the build when the list NAME came back empty. +chRequire = $(if $($1),,$(error clangHost.mk: $1 is empty; a product Makefile variable was renamed)) + +CH_BASDIR := $(CH_ROOT)/src/apps/kpunch/dvxbasic +CH_LIBDVXDIR := $(CH_ROOT)/src/libs/kpunch/libdvx +CH_TOOLSDIR := $(CH_ROOT)/src/tools + +# dvxbasic: compiler, VM (+ dvxPrefs as linked by standalone programs), +# support code, resource reader/writer, the CLI pipeline and the host +# runtime archive (compiler + VM + form runtime). +CH_BAS_COMP := $(call chMakeVar,$(CH_BASDIR),COMP_SRCS) +CH_BAS_VMLINK := $(call chMakeVar,$(CH_BASDIR),VM_LINK_SRCS) +CH_BAS_SUPPORT := $(call chMakeVar,$(CH_BASDIR),SUPPORT_SRCS) +CH_BAS_CLI := $(call chMakeVar,$(CH_BASDIR),CLI_SRCS) +CH_BAS_RT := $(call chMakeVar,$(CH_BASDIR),HOSTRT_SRCS) +# libdvx host archive sources and the help compiler / resource tool. +CH_LIBDVX := $(call chMakeVar,$(CH_LIBDVXDIR),HOST_SRCS) $(call chMakeVar,$(CH_LIBDVXDIR),TOOL_SRCS) +CH_TOOL_HLPC := $(call chMakeVar,$(CH_TOOLSDIR),DVXHLPC_SRCS) +CH_TOOL_RES := $(call chMakeVar,$(CH_TOOLSDIR),DVXRES_SRCS) + +$(foreach v,CH_BAS_COMP CH_BAS_VMLINK CH_BAS_SUPPORT CH_BAS_CLI CH_BAS_RT CH_LIBDVX CH_TOOL_HLPC CH_TOOL_RES,$(call chRequire,$v)) + +# Program link sets (the dvxbasic Makefile's TEST_*_OBJS, BASCOMP_OBJS, +# BASRUN_OBJS; src/tools' DVXHLPC_SRCS, DVXRES_SRCS). +CH_SRCS_test_compiler := $(CH_BASDIR)/test_compiler.c $(CH_BAS_COMP) $(CH_BAS_VMLINK) $(CH_BAS_SUPPORT) +CH_SRCS_test_compact := $(CH_BASDIR)/test_compact.c $(CH_BAS_COMP) $(CH_BAS_VMLINK) $(CH_BAS_SUPPORT) +CH_SRCS_test_suite := $(CH_BASDIR)/test_suite.c $(CH_BAS_CLI) +CH_SRCS_bascomp := $(CH_BASDIR)/stub/bascomp.c $(CH_BAS_CLI) +CH_SRCS_basrun := $(CH_BASDIR)/stub/basrun.c $(CH_BASDIR)/stub/basModuleRes.c $(CH_BAS_CLI) +CH_SRCS_dvxhlpc := $(CH_TOOL_HLPC) +CH_SRCS_dvxres := $(CH_TOOL_RES) + +# What the dvxbasic Makefile appends to bascomp, and its per-object flag +# for test_suite. +CH_DVXRES := $(CH_ROOT)/bin/host/dvxres +CH_BASSTUB := $(CH_ROOT)/obj/dvxbasic/basstub.app +CH_NOICON := $(CH_BASDIR)/noicon.bmp + +CH_INCLUDES = -I$(CH_LIBDVXDIR) -I$(CH_LIBDVXDIR)/platform -I$(CH_LIBDVXDIR)/thirdparty -I$(CH_ROOT)/src/libs/kpunch/libtasks -I$(CH_ROOT)/src/libs/kpunch/texthelp -I$(CH_ROOT)/src/libs/kpunch/listhelp -I$(CH_ROOT)/src/libs/kpunch/hlpfile -I$(CH_ROOT)/src/libs/kpunch/dvxshell -I$(CH_ROOT)/src/widgets/kpunch -I$(CH_BASDIR) -I$(CH_TOOLSDIR) +CH_CFLAGS = $(CH_OPT) -fno-omit-frame-pointer -D_GNU_SOURCE -DDVX_HOST_TEST -w -MMD -MP $(CH_INCLUDES) $(CH_INSTR) + +chObjs = $(patsubst $(CH_ROOT)/%.c,$(CH_OBJ)/%.o,$1) + +$(call chObjs,$(CH_BASDIR)/test_suite.c): CH_EXTRA = -DBAS_TEST_SRC_DIR='"$(CH_BASDIR)"' + +$(CH_OBJ)/%.o: $(CH_ROOT)/%.c + @mkdir -p $(dir $@) + $(CH_CC) $(CH_CFLAGS) $(CH_EXTRA) -c -o $@ $< + +# One link rule per program; $(sort) drops sources two lists share. +define CH_PROG_RULE +$(CH_BIN)/$1: $$(call chObjs,$$(sort $$(CH_SRCS_$1))) + @mkdir -p $$(dir $$@) + $$(CH_CC) $$(CH_LINKFLAGS) -o $$@ $$^ -ldl -lm + $$(if $$(filter bascomp,$1),$$(CH_DVXRES) add $$@ STUB binary @$$(CH_BASSTUB) > /dev/null && $$(CH_DVXRES) add $$@ noicon icon @$$(CH_NOICON) > /dev/null) +endef + +-include $(shell find $(CH_OBJ) -name '*.d' 2> /dev/null) diff --git a/src/test/lint/clangTidy.yaml b/src/test/lint/clangTidy.yaml new file mode 100644 index 00000000..66009557 --- /dev/null +++ b/src/test/lint/clangTidy.yaml @@ -0,0 +1,99 @@ +# clangTidy.yaml -- clang-tidy configuration for the DVX gate +# (qaGate.sh tidy; passed with --config-file, so it applies only there). +# +# Enabled families: bugprone, cert (the C rules), clang-analyzer (the +# clang static analyzer: core, deadcode, unix, security, nullability, +# optin), misc, performance, portability, readability. C++-only checks +# in those families never fire on C and are left on. Every disabled +# check carries its reason; a check is off when it duplicates +# styleLint.py, contradicts the project style (CLAUDE.md) or is +# overwhelmingly noise on this code base. Third-party code is not +# analyzed (qaGate.sh skips thirdparty/ files and drops findings located +# in them). +Checks: + - '-*' + - 'bugprone-*' + - 'cert-*' + - 'clang-analyzer-*' + - 'misc-*' + - 'performance-*' + - 'portability-*' + - 'readability-*' + # ---- bugprone ---- + # C APIs here are (x, y, w, h) and (ctx, a, b) everywhere; thousands of + # hits, no way to restructure a C geometry API around it. + - '-bugprone-easily-swappable-parameters' + # The target is i386, where ptrdiff_t / size_t are 32 bits: "int + # multiplication widened to ptrdiff_t" only happens on the x86_64 host. + - '-bugprone-implicit-widening-of-multiplication-result' + # C converts void * to and from any object pointer; this fires on every + # stb_ds arr*() macro and every qsort / memcpy of a pointer array. + - '-bugprone-multi-level-implicit-pointer-conversion' + # int <-> unsigned of the same width is two's complement on every DVX + # target and host, and size_t / long (64 bits on the x86_64 host) into + # int32_t is the same width on the i386 target: host-only artifacts. + - '-bugprone-narrowing-conversions' + # ---- cert ---- + # printf / fclose / fputc results are deliberately ignored all over + # (best-effort logging and output); the useful part (malloc / fopen + # results) is what clang-analyzer unix.Malloc and review cover. + - '-cert-err33-c' + # atoi / atof read .frm, IDE property and lexer text, where junk reading + # as 0 is the intended VB-style behaviour (Val), not a missed error. + - '-cert-err34-c' + # ---- clang-analyzer ---- + # Annex K (memcpy_s, ...) does not exist in DJGPP or glibc. + - '-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling' + # Struct padding of the x86_64 host build is not the i386 target's + # (pointers are 8 bytes here); the i386 layout has its own audit. + - '-clang-analyzer-optin.performance.Padding' + # ---- misc ---- + # An include-what-you-use pass for C++ headers; it wants direct + # includes of stdint.h etc. in every file that already gets them from + # dvxTypes.h. + - '-misc-include-cleaner' + # The BASIC compiler is a recursive-descent parser and the widget tree + # is walked recursively by design. + - '-misc-no-recursion' + # Callback signatures are fixed by their function-pointer types; unused + # parameters are marked with (void) per house style. + - '-misc-unused-parameters' + # ---- readability ---- + # CLAUDE.md requires a prototype block for every function at the top of + # its file, including functions the file's header already declares. + - '-readability-redundant-declaration' + # Nested ?: for compact value selection is accepted style here and not + # a CLAUDE.md rule. + - '-readability-avoid-nested-conditional-operator' + # Duplicates styleLint (braces rule). + - '-readability-braces-around-statements' + # Duplicates styleLint (multiDecl rule: one declaration per statement). + - '-readability-isolate-declaration' + # Complexity and size are reported by lizard (make -C src/test/lint + # complexity), never gated. + - '-readability-function-cognitive-complexity' + - '-readability-function-size' + # Naming is owned by styleLint (snakeCase) and review; clang-tidy cannot + # express "typedef'd struct ends in T, typedef'd enum ends in E". + - '-readability-identifier-naming' + # Short names (x, y, w, h, i, d) are the house style for geometry and + # loops. + - '-readability-identifier-length' + # if (!ptr) / while (count) is the house style. + - '-readability-implicit-bool-conversion' + # Not a CLAUDE.md rule; early-return-then-else is used deliberately. + - '-readability-else-after-return' + # 0x1Fu vs 0x1FU: not a project rule. + - '-readability-uppercase-literal-suffix' + # Thousands of pixel / geometry / color constants; magic numbers are a + # review item (CLAUDE.md), not something this check can gate usefully. + - '-readability-magic-numbers' +CheckOptions: + # sizeof(T *) is how arrays of pointers are sized, and stb_ds's arr*() + # macros take sizeof(*a) of pointer elements. + bugprone-sizeof-expression.WarnOnSizeOfPointerToAggregate: false +WarningsAsErrors: '' +# Report findings in project headers too; thirdparty/ ones are dropped by +# qaGate.sh. +HeaderFilterRegex: '.*/src/.*' +FormatStyle: none diff --git a/src/test/lint/clangTidyBaseline.txt b/src/test/lint/clangTidyBaseline.txt new file mode 100644 index 00000000..716dbfc0 --- /dev/null +++ b/src/test/lint/clangTidyBaseline.txt @@ -0,0 +1,146 @@ +# tidy baseline -- maintained by staticGate.sh (see its header) +# format: , each preceded by its +# justification: # FP: | # THIRDPARTY: | # UNTRIAGED + +# FP: shellAssocNormalize(extension, out) gets the typed text then the output buffer, its parameter order; only the names differ +1 src/apps/kpunch/cpanel/cpTypes.c|readability-suspicious-call-argument|1st argument 'typed' (passed to 'extension') looks like it might be swapped with the 2nd, 'ext' (passed to 'out') +# FP: buildProcs walks the same symbol list twice; the path counts procedures on the first walk and then assumes the second finds none before reading procs[j], but j < procCount only reaches entries the loop already wrote +1 src/apps/kpunch/dvxbasic/compiler/compiler.c|clang-analyzer-core.UndefinedBinaryOperatorResult|The left operand of '>' is a garbage value +# FP: basGenTemp: arrlen(NULL) is 0, so (*pool)[tempUsed] runs only when tempUsed < 0, and tempUsed only counts up from 0 per statement +1 src/apps/kpunch/dvxbasic/compiler/gen.c|clang-analyzer-core.NullDereference|Array access results in a null pointer dereference +# FP: COLOR / WIDTH / PALETTE each take their own named argument count (BAS_COLOR_ARGS, BAS_WIDTH_ARGS, BAS_PALETTE_ARGS) that happen to be 3; merging the cases would tie three statements' arities together +1 src/apps/kpunch/dvxbasic/compiler/opcodes.h|bugprone-branch-clone|switch has 3 consecutive identical branches +# FP: WGT_DYN_ACCESS is libdvx's non-NULL sentinel for a BASIC-implemented property / method accessor ((void *)(uintptr_t)1); it is compared, never dereferenced +3 src/apps/kpunch/dvxbasic/formrt/formCtl.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: column / BAS_PRINT_ZONE_WIDTH is the print-zone index; integer division is the rule (the next zone starts at a whole zone) +1 src/apps/kpunch/dvxbasic/formrt/formGfx.c|bugprone-integer-division|result of integer division used in a floating point context; possible loss of precision +# FP: count * scaleX * bpp is one scaled screen row (at most the window width times 4 bytes), far from int32 overflow; size_t is 32-bit on the i386 target anyway +1 src/apps/kpunch/dvxbasic/formrt/formGfx.c|bugprone-misplaced-widening-cast|either cast from 'int32_t' (aka 'int') to 'size_t' (aka 'unsigned long') is ineffective, or there is loss of precision before the conversion +# FP: rawResult is BasNativeWordT (uintptr_t), the native call's pointer-sized return register; a STRING-returning DECLARE LIBRARY function returns a char * in it +2 src/apps/kpunch/dvxbasic/formrt/formrt.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: frmMenuPropText: the path assumes text = mi->name is NULL, but the address of an array member is never NULL, so mi is not NULL +1 src/apps/kpunch/dvxbasic/formrt/frmModel.c|clang-analyzer-core.NullDereference|Access to field 'level' results in a dereference of a null pointer (loaded from variable 'mi') +# FP: frmModelReparent refuses a new parent inside ctrl: frmModelIsDescendant(m, parent, ctrl) asks whether parent descends from ancestor ctrl, the intended order +1 src/apps/kpunch/dvxbasic/formrt/frmModel.c|readability-suspicious-call-argument|2nd argument 'parent' (passed to 'ctrl') looks like it might be swapped with the 3rd, 'ctrl' (passed to 'ancestor') +# FP: the project tree keeps a file index (or -1 for group rows) in the widget's void *userData, the only per-item slot the tree widget offers; it is cast back, never dereferenced +3 src/apps/kpunch/dvxbasic/ide/ideProject.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: validateNewName(newValue, max, oldName): the new name is the one validated and oldName the one excepted from the in-use check; the order is right +2 src/apps/kpunch/dvxbasic/ide/ideProperties.c|readability-suspicious-call-argument|1st argument 'newValue' (passed to 'name') looks like it might be swapped with the 3rd, 'oldName' (passed to 'exceptName') +# FP: a toolbox button keeps its tool index in void *userData (the widget's only per-item slot); cast back, never dereferenced +1 src/apps/kpunch/dvxbasic/ide/ideToolbox.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: pixel math: basGfxHome takes the centre pixel (integer) of the viewport and clipLine pads the view by half the pen width rounded down plus one pixel; both are meant to be whole pixels +3 src/apps/kpunch/dvxbasic/runtime/gfx.c|bugprone-integer-division|result of integer division used in a floating point context; possible loss of precision +# FP: text-viewport cells (at most 80 x 60) and a span x1 - x0 + 1 clipped to the framebuffer width: no int32 overflow possible +2 src/apps/kpunch/dvxbasic/runtime/screen.c|bugprone-misplaced-widening-cast|either cast from 'int' to 'size_t' (aka 'unsigned long') is ineffective, or there is loss of precision before the conversion +# FP: rows * cols of the text page: rows and cols come from the SCREEN / WIDTH mode table (at most 80 x 60), far from int32 overflow; size_t is 32-bit on the i386 target +2 src/apps/kpunch/dvxbasic/runtime/screen.c|bugprone-misplaced-widening-cast|either cast from 'int32_t' (aka 'int') to 'size_t' (aka 'unsigned long') is ineffective, or there is loss of precision before the conversion +# FP: stringFind: last - i + 1 with start <= i <= last, both within a string length (< INT32_MAX), cannot overflow before the size_t cast +1 src/apps/kpunch/dvxbasic/runtime/vm.c|bugprone-misplaced-widening-cast|either cast from 'int' to 'size_t' (aka 'unsigned long') is ineffective, or there is loss of precision before the conversion +# FP: the BASIC SHELL statement runs a command line by definition (QBasic / VB SHELL) +1 src/apps/kpunch/dvxbasic/runtime/vm.c|cert-env33-c|calling 'system' uses a command processor +# FP: BASIC RND: QBasic-compatible pseudo-random numbers, not a security use +1 src/apps/kpunch/dvxbasic/runtime/vm.c|cert-msc30-c,cert-msc50-cpp|rand() has limited randomness +# FP: BASIC RANDOMIZE seeds RND from the given or timer value as QBasic does; predictable sequences are the documented behaviour +1 src/apps/kpunch/dvxbasic/runtime/vm.c|cert-msc32-c,cert-msc51-cpp|random number generator seeded with a disallowed source of seed value will generate a predictable sequence of values +# FP: TypeOf: the path assumes objectClass's &classes[classIdx] is NULL (the address of an element of an allocated table with classIdx < classCount never is) and then that classes[i].name is NULL +1 src/apps/kpunch/dvxbasic/runtime/vm.c|clang-analyzer-core.NonNullParamChecker|Null pointer passed to 1st parameter expecting 'nonnull' +# FP: gcFind: a non-zero hash slot holds idx + 1 of a node gcAdd wrote before entering it, and gcGrow re-enters only indices < count of the realloc-kept nodes; the path forgets realloc keeps the contents +1 src/apps/kpunch/dvxbasic/runtime/vm.c|clang-analyzer-core.UndefinedBinaryOperatorResult|The left operand of '==' is a garbage value +# FP: onMenu switches on the CMD_* ids this app put in its menu; no other id can arrive and none needs handling +1 src/apps/kpunch/dvxhelp/dvxhelp.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: TOPIC_TO_USERDATA keeps topic index + 1 in the tree item's userData on purpose (NULL marks a section header) +1 src/apps/kpunch/dvxhelp/dvxhelp.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: the hotkey's slot index is passed as dvxRegisterHotkey's user pointer on purpose +1 src/apps/kpunch/progman/progman.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: the two PkgDiskDamagedE branches are different failures tested in order (unreadable header first, failed seek only after the set and disk checks); merging them would seek before the checks +1 src/installer/pkgRead.c|bugprone-branch-clone|repeated branch body in conditional chain +# FP: audioLockSpan computes the address hull of the lock items in uintptr_t and hands its start back to platformLockRegion as a pointer, by design +2 src/libs/kpunch/audio/audioMix.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: the allocation (newObject / calloc) is the last test of a guard chain, made only after the cheap checks pass; parenthesized and negated on purpose +2 src/libs/kpunch/dvxnet/netProto.c|bugprone-assignment-in-if-condition|an assignment within an 'if' condition is bug-prone +# FP: protoBufferSize returns TCP_SND_BUF or TCP_WND, two separate lwIP options that happen to be equal in this lwipopts.h +1 src/libs/kpunch/dvxnet/netProto.c|bugprone-branch-clone|conditional operator with identical true and false expressions +# FP: lwIP's tcp_pcb starts with the IP_PCB members; reaching them through struct ip_pcb is lwIP's own idiom for the shared socket options +1 src/libs/kpunch/dvxnet/netProto.c|bugprone-casting-through-void|do not cast 'struct tcp_pcb *' to 'struct ip_pcb *' through 'void *' +# FP: lwIP's udp_pcb starts with the IP_PCB members; reaching them through struct ip_pcb is lwIP's own idiom for the shared socket options +1 src/libs/kpunch/dvxnet/netProto.c|bugprone-casting-through-void|do not cast 'struct udp_pcb *' to 'struct ip_pcb *' through 'void *' +# FP: the resolver slot index is dns_gethostbyname's callback argument on purpose +1 src/libs/kpunch/dvxnet/netProto.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: both strcpy calls follow a strlen check against the destination size (ENAMETOOLONG / ENOSPC returned otherwise) +2 src/libs/kpunch/dvxnet/netResolve.c|clang-analyzer-security.insecureAPI.strcpy|Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119 +# FP: name is cut to sizeof(name) - sizeof(suffix) before the strcat and the suffix is shorter than its buffer, so it always fits +1 src/libs/kpunch/dvxshell/pmModel.c|clang-analyzer-security.insecureAPI.strcpy|Call to function 'strcat' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcat'. CWE-119 +# FP: allocSlot scans from slot 1; every slot past 0 is a ShellAppT allocSlot itself calloc'd and appended only when non-NULL +1 src/libs/kpunch/dvxshell/shellApp.c|clang-analyzer-core.NullDereference|Access to field 'state' results in a dereference of a null pointer +# FP: the app id is passed as the task argument on purpose +1 src/libs/kpunch/dvxshell/shellApp.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: the BASIC hotkey id is passed as dvxRegisterHotkey's user pointer on purpose +1 src/libs/kpunch/dvxshell/shellBas.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: platformAltScanToChar returns 0 or an ASCII letter or digit from its table, never a negative char +1 src/libs/kpunch/libdvx/dvxApp.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first. +# FP: executeSysMenuCmd switches on the SysMenu*E ids openSysMenu put in the menu; handleMouseButton leaves the other HIT_* parts (none, border) without action on purpose +2 src/libs/kpunch/libdvx/dvxApp.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: the message box button id is kept in the button's userData on purpose +1 src/libs/kpunch/libdvx/dvxDialog.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: realpath(resolved, canon): the path to resolve, then the output buffer, which is realpath's parameter order +1 src/libs/kpunch/libdvx/dvxDialog.c|readability-suspicious-call-argument|1st argument 'resolved' (passed to '__name') looks like it might be swapped with the 2nd, 'canon' (passed to '__resolved') +# FP: GNU ld --wrap=abort (HOST_APP_LDFLAGS) requires exactly the name __wrap_abort +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '__wrap_abort', which is a reserved identifier +# FP: GNU ld --wrap=exit (HOST_APP_LDFLAGS) requires exactly the name __wrap_exit +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '__wrap_exit', which is a reserved identifier +# FP: platformVideoExclusiveCapture's signature is the platform interface; the DOS layer writes out, the host stub has nothing to write +1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|readability-non-const-parameter|pointer parameter 'out' can be pointer to const +# FP: platformMkdirRecursive returns -1 when strlen(path) >= sizeof(buf), the test right before the strcpy +1 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|clang-analyzer-security.insecureAPI.strcpy|Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119 +# FP: callStatic runs only for a signature found in the signature table (an unknown one returns false first) and WGT_SIG_DYNAMIC is dispatched before it; every table signature has a case +1 src/libs/kpunch/libdvx/widgetIface.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: the dummy return address is written at the aligned stack top, which is computed as an integer on purpose +1 src/libs/kpunch/libtasks/taskswitch.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: sVibrato is a signed int8_t table of pitch steps (-2..2); the sign is meant +1 src/libs/kpunch/music/musicSynth.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first. +# FP: outLine builds a length-delimited line (margin, text, CR LF) that outBytes writes with its length; no NUL is wanted +1 src/libs/kpunch/print/printText.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated +# FP: the frame-type switch drops unknown types from the wire on purpose, and rxState only ever holds RX_STATE_HUNT, ACTIVE or ESCAPE, each with a case +2 src/libs/kpunch/serial/packet/packet.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: the COM port number is the packet stream's context pointer on purpose +1 src/libs/kpunch/serial/packet/packet.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities +# FP: destroyLink runs only when depth returns to 0; secLinkHandshake holds depth >= 1 around its secLinkHandshakeStep calls (enterCall / leaveCall always pair, depth starts at 0), so the inner leaveCall never frees +1 src/libs/kpunch/serial/seclink/secLink.c|clang-analyzer-unix.Malloc|Use of memory after it is freed +# FP: the result buffer is allocated as the last test of the guard, only after the argument checks pass; parenthesized and negated on purpose +4 src/libs/kpunch/serial/security/secBas.c|bugprone-assignment-in-if-condition|an assignment within an 'if' condition is bug-prone +# FP: a link record is 'target NUL display' and addRecord gets its length; the target copy includes its NUL, the display text needs none +1 src/tools/dvxhlpc.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated +# FP: _POSIX_C_SOURCE is the POSIX feature-test macro; the program must define it before any include +1 src/tools/dvxhlpc.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '_POSIX_C_SOURCE', which is a reserved identifier +# FP: sTelState only holds the eight TS_* states and each has a case +1 src/tools/proxy/proxy.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: parseState only holds PARSE_NORMAL, PARSE_ESC or PARSE_CSI, each with a case +1 src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: the bullet is copied without its NUL and the payload copy right after includes its NUL (pLen + 1) +1 src/widgets/kpunch/helpView/widgetHelpView.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated +# FP: record types the viewer does not draw (newer or unknown ones in the file) are skipped on purpose +1 src/widgets/kpunch/helpView/widgetHelpView.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases +# FP: the loop stops at sAlignNames' NULL sentinel (index 3), so a is 0..2, the three ListViewAlignE values +1 src/widgets/kpunch/listView/widgetListView.c|clang-analyzer-optin.core.EnumCastOutOfRange|The value '3' provided to the cast expression is not in the valid range of values for the enum +# FP: count > resolvedColCap with count 0 needs a negative capacity; it starts at 0 and only grows to a count +1 src/widgets/kpunch/listView/widgetListView.c|clang-analyzer-optin.portability.UnixAPI|Call to 'realloc' has an allocation size of 0 bytes +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/statusBar/widgetStatusBar.c|readability-non-const-parameter|pointer parameter 'borderW' can be pointer to const +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/statusBar/widgetStatusBar.c|readability-non-const-parameter|pointer parameter 'extraTop' can be pointer to const +# FP: NULL reaches memcpy only for a negative length: colorize is called with colorLen >= 1 (textHelp paint) and textOut gets a line length or hi - lo of a sorted selection; not the null + zero-count pattern +2 src/widgets/kpunch/textInput/widgetTextInput.c|clang-analyzer-core.NonNullParamChecker|Null pointer passed to 1st parameter expecting 'nonnull' +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/toolbar/widgetToolbar.c|readability-non-const-parameter|pointer parameter 'borderW' can be pointer to const +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/toolbar/widgetToolbar.c|readability-non-const-parameter|pointer parameter 'extraTop' can be pointer to const +# FP: count > indexCap with count 0 needs a negative capacity; it starts at 0 and only grows to a count +1 src/widgets/kpunch/treeView/widgetTreeView.c|clang-analyzer-optin.portability.UnixAPI|Call to 'realloc' has an allocation size of 0 bytes +# FP: treeItemInSubtree(item, candidate) asks whether the drop target lies in the dragged item's subtree: dropTarget is the item, dragItem the candidate +1 src/widgets/kpunch/treeView/widgetTreeView.c|readability-suspicious-call-argument|1st argument 'dropTarget' (passed to 'item') looks like it might be swapped with the 2nd, 'dragItem' (passed to 'candidate') +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/wrapBox/widgetWrapBox.c|readability-non-const-parameter|pointer parameter 'borderW' can be pointer to const +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/wrapBox/widgetWrapBox.c|readability-non-const-parameter|pointer parameter 'extraTop' can be pointer to const +# FP: the getLayoutMetrics class-method signature is shared by every box class; the classes that have these metrics write them, this one leaves them unset +1 src/widgets/kpunch/wrapBox/widgetWrapBox.c|readability-non-const-parameter|pointer parameter 'gap' can be pointer to const diff --git a/src/test/lint/codespellBaseline.txt b/src/test/lint/codespellBaseline.txt new file mode 100644 index 00000000..d2f03e87 --- /dev/null +++ b/src/test/lint/codespellBaseline.txt @@ -0,0 +1,4 @@ +# codespell baseline -- maintained by staticGate.sh (see its header) +# format: , each preceded by its +# justification: # FP: | # THIRDPARTY: | # UNTRIAGED + diff --git a/src/test/lint/codespellWords.txt b/src/test/lint/codespellWords.txt new file mode 100644 index 00000000..5cde8948 --- /dev/null +++ b/src/test/lint/codespellWords.txt @@ -0,0 +1,46 @@ +# codespellWords.txt -- project words codespell must not "fix" (qaGate.sh +# codespell). One word per line, lower case (matching is case +# insensitive). codespell 2.2 has no comment syntax: a '#' line is read +# as an ignore "word" that never occurs, so reasons go on their own +# lines above the words they explain. camelCase / PascalCase +# identifiers, menu accelerators ("Projec&t") and BASIC hex literals +# (&HAA) are skipped by qaGate.sh's --ignore-regex, not listed here. +# +# BASIC keywords and functions (QBasic / VB names). +strig +defint +iif +# +# Short C variable names used throughout (text edit, name, answer, +# serialized blob, node, from-indexes) and the x86 "Nd" asm constraint. +te +nam +ans +ser +nd +froms +# +# Correct English codespell flags anyway: scrollbar trough, static +# variables, a test program that crashes on purpose. +trough +statics +crasher +# +# Protocol and hardware names: telnet WONT, the UART THRE interrupt. +wont +thre +# +# INI key name [mouse] doubleclick (config/README.md, dvxshell). +doubleclick +# +# Accepted spellings: "Property Get's", "invokable", "unparseable", +# "keep-alives". +get's +invokable +unparseable +keep-alives +# +# Deliberate prefixes in test data (wildcard window-title and help +# search tests). +hel +alph diff --git a/src/test/lint/cppcheckBaseline.txt b/src/test/lint/cppcheckBaseline.txt index c2fce5ec..affb943c 100644 --- a/src/test/lint/cppcheckBaseline.txt +++ b/src/test/lint/cppcheckBaseline.txt @@ -8,7 +8,6 @@ # Entries with no comment predate this convention (not yet triaged). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. -1 src/apps/kpunch/clock/clock.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'onPaint' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/cpanel/cpTypes.c|constParameterCallback|Parameter 'dlg' can be declared as pointer to const. However it seems that 'typeDlgOk' is a callback function, if 'dlg' is declared with const you might also need to cast function pointer(s). # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. @@ -19,17 +18,23 @@ 1 src/apps/kpunch/cpanel/cpanel.c|unsignedLessThanZero|Checking if unsigned expression '(*labels)?((struct stbds_array_header*)(*labels)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/cpanel/cpanel.c|unsignedLessThanZero|Checking if unsigned expression '(sVideoLabels)?((struct stbds_array_header*)(sVideoLabels)-1)->capacity:0' is less than zero. +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/apps/kpunch/dvxbasic/basdev/basDevBas.c|unsignedLessThanZero|Checking if unsigned expression '(sCompError)?((struct stbds_array_header*)(sCompError)-1)->capacity:0' is less than zero. +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/apps/kpunch/dvxbasic/basdev/basDevBas.c|unsignedLessThanZero|Checking if unsigned expression '(sText)?((struct stbds_array_header*)(sText)-1)->capacity:0' is less than zero. +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/apps/kpunch/dvxbasic/basdev/basDevBas.c|unsignedLessThanZero|Checking if unsigned expression 'r->breakpoints?((struct stbds_array_header*)(r->breakpoints)-1)->capacity:0' is less than zero. # FP: style suggestion only (variable scope), no defect. 1 src/apps/kpunch/dvxbasic/compiler/compact.c|variableScope|The scope of the variable 'lineCount' can be reduced. -# UNTRIAGED: new finding accepted by --update; fix it or replace this line with # FP: +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/dvxbasic/compiler/compiler.c|unsignedLessThanZero|Checking if unsigned expression 'c->symChanges?((struct stbds_array_header*)(c->symChanges)-1)->capacity:0' is less than zero. -# UNTRIAGED: new finding accepted by --update; fix it or replace this line with # FP: +# FP: deliberate rounding of a constant to SINGLE precision (BASIC SINGLE semantics). 1 src/apps/kpunch/dvxbasic/compiler/fold.c|suspiciousFloatingPointCast|Floating-point cast causes loss of precision. -# UNTRIAGED: new finding accepted by --update; fix it or replace this line with # FP: +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/dvxbasic/compiler/gen.c|unsignedLessThanZero|Checking if unsigned expression 'c->procTempSlots?((struct stbds_array_header*)(c->procTempSlots)-1)->capacity:0' is less than zero. -# UNTRIAGED: new finding accepted by --update; fix it or replace this line with # FP: +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/dvxbasic/compiler/gen.c|unsignedLessThanZero|Checking if unsigned expression 'c->tempSlots?((struct stbds_array_header*)(c->tempSlots)-1)->capacity:0' is less than zero. -# UNTRIAGED: new finding accepted by --update; fix it or replace this line with # FP: +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/dvxbasic/compiler/genStmt.c|unsignedLessThanZero|Checking if unsigned expression '(failJumps)?((struct stbds_array_header*)(failJumps)-1)->capacity:0' is less than zero. # FP: redundant but harmless: a leading digit was already taken by the number branch above; the extra test documents that identifiers cannot start with a digit. 1 src/apps/kpunch/dvxbasic/compiler/lexer.c|knownConditionTrueFalse|Condition '!isdigit((unsigned char)c)' is always true @@ -41,6 +46,16 @@ 1 src/apps/kpunch/dvxbasic/compiler/strip.c|variableScope|The scope of the variable 'nConst' can be reduced. # FP: style suggestion only (variable scope), no defect. 1 src/apps/kpunch/dvxbasic/compiler/strip.c|variableScope|The scope of the variable 'nKept' can be reduced. +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/apps/kpunch/dvxbasic/formrt/basRunHost.c|unsignedLessThanZero|Checking if unsigned expression '(*saved)?((struct stbds_array_header*)(*saved)-1)->capacity:0' is less than zero. +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'dsgnOnCursor' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'dsgnOnKey' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'dsgnOnMouse' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|unsignedLessThanZero|Checking if unsigned expression '(sText)?((struct stbds_array_header*)(sText)-1)->capacity:0' is less than zero. # FP: upperCopy bounds the destination; src[i] is read inside the NUL-terminated source string, so the order is safe. 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|arrayIndexThenCheck|Array index 'i' is used before limits check. # FP: signature fixed by the BasDeviceCallbacksT frameBusy pointer type (void *ctx shared with callbacks that write through it). @@ -49,10 +64,6 @@ 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'onFormClose' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/dvxbasic/formrt/formrt.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'onFormMenu' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s). -# FP: only the first nativeCount words of nativeArgs are filled and passed; the rest are never read. -1 src/apps/kpunch/dvxbasic/formrt/formrt.c|uninitvar|Uninitialized variable: nativeArgs -# FP: inForm = false is a dead store only because parsing stops at the outer End; harmless (formrt owner may drop it). -1 src/apps/kpunch/dvxbasic/formrt/frmParser.c|unreadVariable|Variable 'inForm' is assigned a value that is never used. # FP: redundant (menuId >= DSGN_MENU_ID_BASE already implies idx >= 0) but keeps the array bound check self-contained. 1 src/apps/kpunch/dvxbasic/ide/ideMain.c|knownConditionTrueFalse|Condition 'idx>=0' is always true # FP: general is checked through ok (ok = general != NULL) and the function returns before the write when it is NULL. @@ -83,12 +94,8 @@ 1 src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c|unsignedLessThanZero|Checking if unsigned expression '(sLabelBufs)?((struct stbds_array_header*)(sLabelBufs)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c|unsignedLessThanZero|Checking if unsigned expression '(sLabels)?((struct stbds_array_header*)(sLabels)-1)->capacity:0' is less than zero. -# FP: prjSingleLine bounds the destination; src[i] is read inside the NUL-terminated source string, so the order is safe. -1 src/apps/kpunch/dvxbasic/ide/ideProject.c|arrayIndexThenCheck|Array index 'i' is used before limits check. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/dvxbasic/ide/ideProject.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onTreeItemDblClick' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). -# FP: style suggestion only (variable scope), no defect. -1 src/apps/kpunch/dvxbasic/ide/ideProject.c|variableScope|The scope of the variable 'line' can be reduced. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/dvxbasic/ide/ideProperties.c|unsignedLessThanZero|Checking if unsigned expression '(sCellData)?((struct stbds_array_header*)(sCellData)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. @@ -111,10 +118,6 @@ 1 src/apps/kpunch/dvxbasic/test_compact.c|nullPointerOutOfMemory|If memory allocation fails, then there is a possible null pointer dereference: mod # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/dvxbasic/test_compiler.c|constParameterCallback|Parameter 'args' can be declared as pointer to const. However it seems that 'stubCallExtern' is a callback function, if 'args' is declared with const you might also need to cast function pointer(s). -# FP: style only (test code keeps a value for readability), no defect. -1 src/apps/kpunch/dvxbasic/test_compiler.c|unreadVariable|Variable 'len' is assigned a value that is never used. -# FP: style only (test code keeps a value for readability), no defect. -1 src/apps/kpunch/dvxbasic/test_compiler.c|unreadVariable|Variable 'n' is assigned a value that is never used. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/dvxbasic/test_suite.c|constParameterCallback|Parameter 'args' can be declared as pointer to const. However it seems that 'externCallStub' is a callback function, if 'args' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. @@ -127,30 +130,19 @@ 1 src/apps/kpunch/dvxbasic/test_suite.c|nullPointerOutOfResources|If resource allocation fails, then there is a possible null pointer dereference: f # FP: style only (test or setup code assigns a value twice for clarity), no defect. 1 src/apps/kpunch/dvxbasic/test_suite.c|redundantAssignment|Variable 'proc.localCount' is reassigned a value before the old one has been used. -# FP: style only (test code keeps a value for readability), no defect. -1 src/apps/kpunch/dvxbasic/test_suite.c|unreadVariable|Variable 'manyPos' is assigned a value that is never used. # FP: style suggestion only (variable scope), no defect. 1 src/apps/kpunch/dvxbasic/test_suite.c|variableScope|The scope of the variable 'detail' can be reduced. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. -1 src/apps/kpunch/dvxhelp/dvxhelp.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'helpRulePaint' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. -1 src/apps/kpunch/fileMgr/fileMgr.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onDriveClick' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). -# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. -1 src/apps/kpunch/fileMgr/fileMgr.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onPathKey' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (variable scope), no defect. -1 src/apps/kpunch/fileMgr/fileMgr.c|variableScope|The scope of the variable 'dateText' can be reduced. # FP: style suggestion only (variable scope), no defect. -1 src/apps/kpunch/fileMgr/fileMgr.c|variableScope|The scope of the variable 'err' can be reduced. # FP: style suggestion only (variable scope), no defect. -1 src/apps/kpunch/fileMgr/fileMgr.c|variableScope|The scope of the variable 'msg' can be reduced. # FP: style suggestion only (variable scope), no defect. 1 src/apps/kpunch/progman/pmProps.c|variableScope|The scope of the variable 'msg' can be reduced. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/progman/progman.c|constParameterCallback|Parameter 'userCtx' can be declared as pointer to const. However it seems that 'onHotkey' is a callback function, if 'userCtx' is declared with const you might also need to cast function pointer(s). # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/apps/kpunch/progman/progman.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onSearchChange' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). -# FP: empty keeps EMPTY_GROUP unless a filter is set; the overwrite is conditional. -1 src/apps/kpunch/progman/progman.c|redundantInitialization|Redundant initialization for 'empty'. The initialized value is overwritten before it is read. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/progman/progman.c|unsignedLessThanZero|Checking if unsigned expression '(sEntries)?((struct stbds_array_header*)(sEntries)-1)->capacity:0' is less than zero. # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. @@ -158,7 +150,6 @@ # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. 1 src/apps/kpunch/progman/progman.c|unsignedLessThanZero|Checking if unsigned expression '(sHotkeys)?((struct stbds_array_header*)(sHotkeys)-1)->capacity:0' is less than zero. # FP: signature fixed by the widget onClick callback type (WidgetT *). -1 src/apps/kpunch/terminal/terminal.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onAskAnswer' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). # FP: public audio.h API taking the program's AudioCtxT handle (non-const by convention); cppcheck calls it a callback only because its address is in the audioSynthLock pin table. 1 src/libs/kpunch/audio/audioSynth.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'audioFmFree' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s). # FP: public audio.h API taking the program's AudioCtxT handle (non-const by convention); cppcheck calls it a callback only because its address is in the audioSynthLock pin table. @@ -181,16 +172,20 @@ 1 src/libs/kpunch/dvxshell/shellApp.c|constParameterCallback|Parameter 'arg' can be declared as pointer to const. However it seems that 'appTaskWrapper' is a callback function, if 'arg' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (variable scope), no defect. 1 src/libs/kpunch/dvxshell/shellAssoc.c|variableScope|The scope of the variable 'tokenLen' can be reduced. +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/libs/kpunch/dvxshell/shellBas.c|constParameterCallback|Parameter 'userCtx' can be declared as pointer to const. However it seems that 'hotkeyPressed' is a callback function, if 'userCtx' is declared with const you might also need to cast function pointer(s). # FP: style suggestion only (variable scope), no defect. 1 src/libs/kpunch/dvxshell/shellRun.c|variableScope|The scope of the variable 'resolved' can be reduced. -# FP: e.name is stored in entries by arrput (stb_ds) and freed by fileOpsFreeEntries. -1 src/libs/kpunch/fileops/fileOps.c|memleak|Memory leak: e.name +# FP: the strdup'd name moves into scan->entries by arrput; stb_ds OOM hook never returns, so it cannot be dropped. +1 src/libs/kpunch/fileops/fileOps.c|memleak|Memory leak: entry.name +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/libs/kpunch/fileops/fileOpsBas.c|unsignedLessThanZero|Checking if unsigned expression '(sText)?((struct stbds_array_header*)(sText)-1)->capacity:0' is less than zero. +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/libs/kpunch/hlpfile/hlpFileBas.c|unsignedLessThanZero|Checking if unsigned expression 'hd->found?((struct stbds_array_header*)(hd->found)-1)->capacity:0' is less than zero. # FP: style suggestion only (could be const), no defect. 1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'keyRec' can be declared as const array # FP: style suggestion only (could be const), no defect. 1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'mouseRec' can be declared as const array -# FP: defensive: thumbGeometry keeps the range <= 0 case (all content visible) for callers that pass an empty range. -1 src/libs/kpunch/libdvx/dvxWm.c|knownConditionTrueFalse|Condition 'range<=0' is always false # FP: the gate defines DXE_EXPORT as empty, which leaves DXE_EXPORT(strerror) as a bare (strerror) cppcheck cannot parse; the real macro builds the export table entry. 1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|unknownMacro|There is an unknown macro here somewhere. Configuration is required. If strerror is a macro then please configure it. # FP: raise() returns when no handler longjmps out (DOS without a recovery point); the abort() after it is the intended fallback. @@ -259,14 +254,22 @@ 1 src/widgets/kpunch/dataCtrl/widgetDataCtrl.c|variableScope|The scope of the variable 'captionX' can be reduced. # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. 1 src/widgets/kpunch/dbGrid/widgetDbGrid.c|constParameterCallback|Parameter 'dead' can be declared as pointer to const. However it seems that 'dbGridOnWidgetDestroyed' is a callback function, if 'dead' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/widgets/kpunch/helpView/widgetHelpView.c|constParameterCallback|Parameter 'userData' can be declared as pointer to const. However it seems that 'buildRecord' is a callback function, if 'userData' is declared with const you might also need to cast function pointer(s). +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/widgets/kpunch/helpView/widgetHelpView.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'helpRulePaint' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). # FP: defensive bound check; the header branch above normally returns first, and the check keeps a negative row index impossible if that branch changes. 1 src/widgets/kpunch/listView/widgetListView.c|knownConditionTrueFalse|Condition 'relY<0' is always false +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/widgets/kpunch/listView/widgetListView.c|unsignedLessThanZero|Checking if unsigned expression '(text)?((struct stbds_array_header*)(text)-1)->capacity:0' is less than zero. # FP: style suggestion only (variable scope), no defect. 1 src/widgets/kpunch/listView/widgetListView.c|variableScope|The scope of the variable 'textY' can be reduced. # FP: right == left on the first and last rows of the diamond (dist 0), where no fill line is drawn. 1 src/widgets/kpunch/radio/widgetRadio.c|knownConditionTrueFalse|Condition 'right>left' is always true -# FP: re-entrancy guard: a user callback run just before can destroy widgets and bump sWidgetGen; cppcheck cannot see callbacks change the global. -1 src/widgets/kpunch/textInput/widgetTextInput.c|knownConditionTrueFalse|Condition 'sWidgetGen!=gen' is always false +# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. +1 src/widgets/kpunch/textInput/widgetTextInput.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'wgtTextAreaCopy' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s). +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic. +1 src/widgets/kpunch/textInput/widgetTextInput.c|unsignedLessThanZero|Checking if unsigned expression 'ta->markers?((struct stbds_array_header*)(ta->markers)-1)->capacity:0' is less than zero. # FP: style only: the widget API macro's local sApi shadows the file-scope cache on purpose; no defect. 1 src/widgets/kpunch/toolbar/toolbar.h|shadowVariable|Local variable 'sApi' shadows outer variable # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const. diff --git a/src/test/lint/hostDb.py b/src/test/lint/hostDb.py new file mode 100755 index 00000000..7acb95b3 --- /dev/null +++ b/src/test/lint/hostDb.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# hostDb.py -- compile database and clang build of the DVX host code. +# +# hostDb.py db write obj/qa/compile_commands.json +# hostDb.py clang rewrite the database, then build every translation +# unit in it with clang (-c, the database's flags plus +# CLANG_WARNINGS) into obj/clang; report in +# obj/clang/report.txt +# +# The database is not hand-written: it comes from dry runs (make -n -B -k +# -w) of every host build -- the host suites (src/test/suites.mk), the +# BASIC harnesses, bascomp, basrun and fuzzers (dvxbasic "tests"), the +# host tools (src/tools), secproxy (src/tools/proxy) and dvxpkg +# (src/installer) -- so it always matches the Makefiles. A dry run +# executes nothing but $(MAKE) lines (which run as dry runs too), so it +# is safe while another build is running. Only host compiles (gcc / cc / +# clang) are kept; DJGPP compiles are not. One entry per source file +# (the first compile seen); flags are made clang-friendly: -Werror, +# dependency-file flags and gcc-only options are dropped and +# -Wno-unknown-warning-option is added. +# +# The clang build is report-only: nothing links or runs. Diagnostics are +# de-duplicated (a header warning is reported by every file including it) +# and counted per warning flag and per directory; those in thirdparty/ +# directories or system headers are only counted. +# +# Environment: QA_JOBS (default 6) parallel clang processes, run niced. +# +# Python 3 stdlib only, ASCII only. + +import json +import os +import re +import shlex +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.normpath(os.path.join(HERE, "..", "..", "..")) +QA_DIR = os.path.join(ROOT, "obj", "qa") +DB_PATH = os.path.join(QA_DIR, "compile_commands.json") +CLANG_DIR = os.path.join(ROOT, "obj", "clang") +DEFAULT_JOBS = 6 +NICE = ["nice", "-n", "10"] + +HOST_COMPILERS = ("gcc", "cc", "clang") + +# Flags that are dropped (no argument) and dropped with their argument. +DROP_FLAGS = set(["-Werror", "-MMD", "-MP", "-MD", "-fanalyzer", "-shared", "-rdynamic", "-c"]) +DROP_WITH_ARG = set(["-o", "-MF", "-MT", "-MQ"]) +# gcc-only warning switches clang does not know. +GCC_ONLY_PREFIX = ("-Wno-stringop-", "-Wstringop-", "-Wno-format-truncation", "-Wformat-truncation") + +# Clang-only (or clang-better) warnings for the report-only clang build. +# Chosen for bug finding on this code base; left out on purpose: +# -Wshorten-64-to-32 / -Wconversion (thousands of int32_t <-> size_t +# hits on i386-oriented code), -Wpadded (see the i386 padding audit), +# -Wsign-compare / -Wtype-limits (disabled by the project's WARNFLAGS). +CLANG_WARNINGS = [ + "-Wall", + "-Wextra", + "-Warray-bounds-pointer-arithmetic", + "-Wassign-enum", + "-Wcast-function-type-strict", + "-Wcomma", + "-Wconditional-uninitialized", + "-Wcovered-switch-default", + "-Wduplicate-enum", + "-Wfour-char-constants", + "-Wimplicit-fallthrough", + "-Wincompatible-function-pointer-types-strict", + "-Wloop-analysis", + "-Wmissing-prototypes", + "-Wmissing-variable-declarations", + "-Wpointer-arith", + "-Wstrict-prototypes", + "-Wstring-conversion", + "-Wunreachable-code-aggressive", + "-Wno-sign-compare", + "-Wno-type-limits", + "-Wno-unknown-warning-option", +] + +DIAG_RE = re.compile(r"^(?P[^:\s][^:]*):(?P\d+):(?P\d+): (?Pwarning|error): (?P.*?)(?: \[(?P[^\]]+)\])?$") +ENTER_RE = re.compile(r"^make(?:\[\d+\])?: Entering directory '(?P[^']+)'") +LEAVE_RE = re.compile(r"^make(?:\[\d+\])?: Leaving directory '(?P[^']+)'") + + +def clangBuild(jobs): + """Builds every database entry with clang; writes the report.""" + entries = loadDb() + objDir = os.path.join(CLANG_DIR, "obj") + os.makedirs(objDir, exist_ok=True) + work = [] + for idx, entry in enumerate(entries): + out = os.path.join(objDir, "%04d.o" % idx) + args = ["clang"] + entry["arguments"][1:] + CLANG_WARNINGS + ["-c", "-o", out] + work.append((entry["directory"], args)) + with ThreadPoolExecutor(max_workers=jobs) as pool: + results = list(pool.map(runCompile, work)) + rawPath = os.path.join(CLANG_DIR, "raw.txt") + failed = 0 + diags = set() + with open(rawPath, "w") as raw: + for (directory, args), (code, text) in zip(work, results): + raw.write(text) + if code != 0: + failed += 1 + for line in text.splitlines(): + m = DIAG_RE.match(line) + if m: + path = os.path.normpath(os.path.join(directory, m.group("file"))) + diags.add((path, int(m.group("line")), int(m.group("col")), m.group("kind"), m.group("msg"), m.group("flag") or "-")) + writeClangReport(sorted(diags), len(entries), failed) + return 0 + + +def collectDb(): + """Runs the dry runs and returns the database entries.""" + entries = [] + seen = set() + for directory, targets in dryRunTargets(): + text = dryRun(directory, targets) + for entry in parseDryRun(text, directory): + if entry["file"] not in seen: + seen.add(entry["file"]) + entries.append(entry) + return entries + + +def compileEntries(args, directory): + """Database entries for one compiler command (one per C source).""" + sources = [a for a in args[1:] if a.endswith(".c")] + if not sources: + return [] + flags = [] + skip = False + for arg in args[1:]: + if skip: + skip = False + continue + if arg in DROP_WITH_ARG: + skip = True + continue + if arg in DROP_FLAGS or arg.endswith((".c", ".o", ".a", ".so")): + continue + if arg.startswith(("-Wl,", "-l")) or arg.startswith(GCC_ONLY_PREFIX): + continue + flags.append(arg) + flags.append("-Wno-unknown-warning-option") + out = [] + for src in sources: + path = os.path.normpath(os.path.join(directory, src)) + out.append({"directory": directory, "file": path, "arguments": ["clang"] + flags + ["-c", path]}) + return out + + +def dryRun(directory, targets): + """make -n -B -k -w output for targets in directory (relative to ROOT).""" + cmd = ["make", "-n", "-B", "-k", "-w", "-C", os.path.join(ROOT, directory)] + targets + proc = subprocess.run(cmd, env=makeEnv(), capture_output=True, text=True) + return proc.stdout + + +def dryRunTargets(): + """(directory, [targets]) for every host build.""" + suites = subprocess.run(["make", "-s", "--no-print-directory", "-f", os.path.join(ROOT, "src", "test", "suites.mk"), "--eval", "qaPrintSuites: ; @echo $(HOST_SUITES)", "qaPrintSuites"], env=makeEnv(), capture_output=True, text=True, check=True).stdout.split() + out = [("src/test/" + s, []) for s in suites] + out.append(("src/apps/kpunch/dvxbasic", ["tests"])) + out.append(("src/tools", ["host"])) + out.append(("src/tools/proxy", [])) + out.append(("src/installer", [])) + return out + + +def joinContinuations(text): + """Joins backslash-continued recipe lines.""" + return text.replace("\\\n", " ").splitlines() + + +def loadDb(): + with open(DB_PATH) as handle: + return json.load(handle) + + +def makeEnv(): + """The environment for a make run of our own: LC_ALL=C and none of the + calling make's flags (under make -j they bring the jobserver and + directory lines into the output).""" + env = dict(os.environ) + env["LC_ALL"] = "C" + for name in ("MAKEFLAGS", "MAKELEVEL", "MFLAGS"): + env.pop(name, None) + return env + + +def parseDryRun(text, startDir): + """Database entries for every host compile in one dry run's output.""" + stack = [os.path.join(ROOT, startDir)] + entries = [] + for line in joinContinuations(text): + m = ENTER_RE.match(line) + if m: + stack.append(m.group("dir")) + continue + m = LEAVE_RE.match(line) + if m: + if len(stack) > 1: + stack.pop() + continue + directory = stack[-1] + # "cd DIR && cmd" chains: follow the cd for the rest of the line. + for part in re.split(r"&&|;", line): + try: + args = shlex.split(part) + except ValueError: + break + if not args: + continue + if args[0] == "cd" and len(args) > 1: + directory = os.path.normpath(os.path.join(directory, args[1])) + continue + if os.path.basename(args[0]) in HOST_COMPILERS: + entries.extend(compileEntries(args, directory)) + return entries + + +def runCompile(job): + directory, args = job + proc = subprocess.run(NICE + args, cwd=directory, capture_output=True, text=True) + return proc.returncode, proc.stderr + + +def writeClangReport(diags, units, failed): + """report.txt: counts per flag and per directory, then every diagnostic + in the tree outside thirdparty/ (those and system headers are only + counted).""" + perFlag = {} + perDir = {} + lines = [] + skipped = 0 + for path, line, col, kind, msg, flag in diags: + rel = os.path.relpath(path, ROOT) + if rel.startswith("..") or "/thirdparty/" in rel: + skipped += 1 + continue + key = flag if kind == "warning" else "error" + perFlag[key] = perFlag.get(key, 0) + 1 + top = os.path.dirname(rel) + perDir[top] = perDir.get(top, 0) + 1 + lines.append("%s:%d:%d: %s: %s [%s]" % (rel, line, col, kind, msg, flag)) + reportPath = os.path.join(CLANG_DIR, "report.txt") + with open(reportPath, "w") as out: + out.write("clang host build: %d translation unit(s), %d failed to compile, %d unique diagnostic(s) (%d more in thirdparty/ or system headers, not listed)\n\n" % (units, failed, len(lines), skipped)) + out.write("per warning flag:\n") + for key, count in sorted(perFlag.items(), key=lambda kv: (-kv[1], kv[0])): + out.write("%7d %s\n" % (count, key)) + out.write("\nper directory:\n") + for key, count in sorted(perDir.items(), key=lambda kv: (-kv[1], kv[0])): + out.write("%7d %s\n" % (count, key)) + out.write("\ndiagnostics:\n") + out.write("\n".join(lines) + "\n") + print("hostDb: clang build of %d unit(s): %d failed, %d unique diagnostic(s) outside thirdparty/; report %s" % (units, failed, len(lines), reportPath)) + + +def writeDb(): + entries = collectDb() + os.makedirs(QA_DIR, exist_ok=True) + with open(DB_PATH, "w") as handle: + json.dump(entries, handle, indent=1) + print("hostDb: %d translation unit(s) -> %s" % (len(entries), DB_PATH)) + return 0 if entries else 2 + + +def main(): + if len(sys.argv) != 2 or sys.argv[1] not in ("db", "clang"): + sys.stderr.write("usage: hostDb.py db|clang\n") + return 2 + status = writeDb() + if sys.argv[1] == "db" or status != 0: + return status + return clangBuild(int(os.environ.get("QA_JOBS", DEFAULT_JOBS))) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/test/lint/msan.mk b/src/test/lint/msan.mk new file mode 100644 index 00000000..0001f055 --- /dev/null +++ b/src/test/lint/msan.mk @@ -0,0 +1,106 @@ +# 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. + +# msan.mk -- MemorySanitizer runs of the host test code +# (make -C src/test/lint msan; report-only, not part of make test). +# +# MSan needs every linked object instrumented (libc and libm excepted). +# Two parts: +# +# msan-basic the BASIC harnesses and command-line tools, built here +# with clangHost.mk from the product Makefiles' source +# lists into obj/msan: test_compiler, test_compact and +# test_suite run in obj/msan/work, then basrun / bascomp +# drive src/test/basic/runBasic.sh and +# src/test/samples/runSamples.sh (their scratch output in +# obj/msan/test). bascomp gets its STUB / noicon +# resources from the DOS build's basstub.app and +# bin/host/dvxres, as the dvxbasic Makefile does. +# msan-suites the GUI host suites in MSAN_SUITES (default basdev, +# formrt and widgets), built by their own Makefiles with the host build +# redirected: HOSTCC=clang, HOST_SUB=msan (objects under +# obj/msan: libdvx.a, libtasks.a, basrt.a, every widget +# .so, libdvxtest.a), HOST_INSTFLAGS = the MSan flags. Only +# each suite's binary is built (its run target would rebuild +# bin/host tools); the binary is then run from its object +# directory. This runs make in product directories, so the +# lint Makefile wraps it in the shared build lock. +# +# A finding is a "WARNING: MemorySanitizer: use-of-uninitialized-value" +# report with the origin of the value (track-origins=2); the runtime +# writes every report of every process to obj/msan/logs/reports and +# keeps going. The summary lists each failed step and each distinct +# report (by its SUMMARY line); either fails the target. + +# Recoverable, so one run lists every report (msanStep.sh sets +# halt_on_error=0); changing these needs `rm -rf obj/msan` (objects do +# not depend on flags). +MSAN_FLAGS := -fsanitize=memory -fsanitize-recover=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer +CH_VARIANT := msan +CH_INSTR := $(MSAN_FLAGS) +include $(dir $(lastword $(MAKEFILE_LIST)))clangHost.mk + +.DEFAULT_GOAL := msan + +MSAN_LOGS := $(CH_OBJ)/logs +MSAN_SUITES ?= basdev formrt widgets +DVXHLPC := $(CH_ROOT)/bin/host/dvxhlpc +MSAN_STEP := $(CH_ROOT)/src/test/lint/msanStep.sh + +# The harnesses and tools (link sets from clangHost.mk). +MSAN_PROGS := test_compiler test_compact test_suite bascomp basrun + +$(foreach p,$(MSAN_PROGS),$(eval $(call CH_PROG_RULE,$p))) + +# The suites' host build, redirected to obj/msan with clang + MSan. +MSAN_SUITE_VARS := HOSTCC=clang HOST_SUB=$(CH_VARIANT) HOST_INSTFLAGS='$(MSAN_FLAGS)' WARNFLAGS=-w + +.PHONY: msan msan-reset msan-basic msan-suites msan-build + +# Serial make (the lint Makefile runs it so): the reset runs first. +msan: msan-reset msan-basic msan-suites + @$(MSAN_STEP) --summary $(MSAN_LOGS) + +msan-reset: + rm -rf $(MSAN_LOGS) + +msan-build: $(addprefix $(CH_BIN)/,$(MSAN_PROGS)) + +msan-basic: msan-build + @mkdir -p $(CH_OBJ)/work $(MSAN_LOGS) + -@cd $(CH_OBJ)/work && $(MSAN_STEP) $(MSAN_LOGS)/test_compiler.log $(CH_BIN)/test_compiler + -@cd $(CH_OBJ)/work && $(MSAN_STEP) $(MSAN_LOGS)/test_compact.log $(CH_BIN)/test_compact + -@cd $(CH_OBJ)/work && $(MSAN_STEP) $(MSAN_LOGS)/test_suite.log $(CH_BIN)/test_suite + -@$(MSAN_STEP) $(MSAN_LOGS)/runBasic.log env BASRUN=$(CH_BIN)/basrun BASCOMP=$(CH_BIN)/bascomp BASIC_OUT=$(CH_OBJ)/test/basic $(CH_ROOT)/src/test/basic/runBasic.sh + -@$(MSAN_STEP) $(MSAN_LOGS)/runSamples.log env BASCOMP=$(CH_BIN)/bascomp SAMPLES_OUT=$(CH_OBJ)/test/samples $(CH_ROOT)/src/test/samples/runSamples.sh + +# Each suite: build its binary through its own Makefile (the path comes +# from the suite Makefile's TARGET), the help fixture with the release +# help compiler (as dvxtest.mk does), then run it from its directory. +msan-suites: + @mkdir -p $(MSAN_LOGS) $(CH_OBJ)/test/help + cd $(CH_ROOT)/src/test/tools/fixtures/help && $(DVXHLPC) -o $(CH_OBJ)/test/help/fixture.hlp --quiet -i . main.dhs @inputs.txt + -@for s in $(MSAN_SUITES); do \ + t=$$(env -u MAKEFLAGS -u MAKELEVEL make -s --no-print-directory -C $(CH_ROOT)/src/test/$$s $(MSAN_SUITE_VARS) --eval 'msanTarget: ; @echo $$(abspath $$(TARGET))' msanTarget); \ + $(MSAN_STEP) $(MSAN_LOGS)/$$s-build.log env -u MAKEFLAGS -u MAKELEVEL nice -n 10 make -C $(CH_ROOT)/src/test/$$s -j6 $(MSAN_SUITE_VARS) $$t || continue; \ + (cd $$(dirname $$t) && $(MSAN_STEP) $(MSAN_LOGS)/$$s.log $$t); \ + done diff --git a/src/test/lint/msanStep.sh b/src/test/lint/msanStep.sh new file mode 100755 index 00000000..5dbed0cf --- /dev/null +++ b/src/test/lint/msanStep.sh @@ -0,0 +1,89 @@ +#!/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. + +# msanStep.sh -- one step of the MemorySanitizer run (msan.mk) +# +# Usage: +# msanStep.sh LOG COMMAND... run COMMAND, output to LOG, exit status +# to LOG.status; returns COMMAND's status +# msanStep.sh --summary DIR list every step under DIR that failed and +# every MemorySanitizer report; exit 1 if +# there is any +# +# Every MemorySanitizer report from any process a step starts (programs +# run by runBasic.sh included) is written by the runtime to +# DIR/reports/msan. (MSAN_OPTIONS log_path), so reports inside +# scripts that hide their programs' stderr are still counted; the +# programs keep running after a report (built with +# -fsanitize-recover=memory, halt_on_error=0). + +set -u + +if [ "${1:-}" = "--summary" ]; then + dir="$2" + bad=0 + echo "msan: summary" + for status in "$dir"/*.log.status; do + if [ ! -f "$status" ]; then + continue + fi + log="${status%.status}" + rc="$(cat "$status")" + printf ' %-24s exit %s\n' "$(basename "$log" .log)" "$rc" + if [ "$rc" != "0" ]; then + bad=$((bad + 1)) + fi + done + # Distinct reports by their SUMMARY line (file:line:col in function), + # with the number of times each was hit. + reports=0 + if compgen -G "$dir/reports/msan.*" > /dev/null; then + cat "$dir"/reports/msan.* | sed -n 's/^SUMMARY: MemorySanitizer: //p' | sort | uniq -c | sort -rn > "$dir/reports/summary.txt" + reports="$(wc -l < "$dir/reports/summary.txt")" + sed 's/^/ /' "$dir/reports/summary.txt" + fi + echo "msan: $bad failed step(s), $reports distinct MemorySanitizer report(s); full reports in $dir/reports" + if [ "$bad" -ne 0 ] || [ "$reports" -ne 0 ]; then + exit 1 + fi + exit 0 +fi + +if [ "$#" -lt 2 ]; then + echo "usage: msanStep.sh LOG COMMAND... | --summary DIR" >&2 + exit 2 +fi + +log="$1" +shift +mkdir -p "$(dirname "$log")/reports" +logDir="$(cd "$(dirname "$log")" && pwd)" +export MSAN_OPTIONS="log_path=$logDir/reports/msan:halt_on_error=0" +echo "msan: $(basename "$log" .log)" +nice -n 10 "$@" > "$log" 2>&1 +rc=$? +echo "$rc" > "$log.status" +if [ "$rc" -ne 0 ]; then + echo "msan: $(basename "$log" .log) exited $rc (see $log)" +fi +exit "$rc" diff --git a/src/test/lint/qaGate.sh b/src/test/lint/qaGate.sh new file mode 100755 index 00000000..323e4345 --- /dev/null +++ b/src/test/lint/qaGate.sh @@ -0,0 +1,257 @@ +#!/bin/bash +# +# qaGate.sh -- clang-tidy, shellcheck and codespell gates for the DVX +# tree, and the lizard complexity report. +# +# Usage: +# qaGate.sh TOOL [--update] +# TOOL = tidy: clang-tidy (clangTidy.yaml) over every product C +# file of the host build (compile database from +# hostDb.py); slow, minutes +# TOOL = shellcheck: every *.sh in the tree; seconds +# TOOL = codespell: C, help, Markdown, BASIC and script sources with +# codespellWords.txt; seconds +# TOOL = complexity: lizard over the product C sources (src/, not +# src/test/): every function over CCN_MAX +# cyclomatic complexity or LENGTH_MAX lines, most +# complex first; a report, never a gate (no +# baseline, always exits 0); seconds +# --update: merge the current findings into the baseline +# (staticGate.sh rules: comments kept, new or grown +# keys marked UNTRIAGED, zero-count keys dropped) +# +# Findings become line-number-free keys and go to +# staticGate.sh --compare TOOL KEYS Baseline.txt +# so these gates share cppcheck's baseline format and merge logic: +# (tidy) src/file.c|check-name|message +# (shellcheck) path/file.sh|SCnnnn|message +# (codespell) path/file|word ==> suggestion(s) +# +# Bootstrap: while a tool has no baseline file the gate is REPORT-ONLY: +# it prints the findings summary under a loud banner and exits 0, so +# wiring a new tool in never breaks `make test`. The first --update +# writes the baseline (every entry UNTRIAGED); from then on the gate +# fails on new or grown keys like staticGate. +# +# Everything a run produces stays in obj/qa//: raw.txt (tool +# output), keys.txt (normalized keys), summary.txt (counts per check and +# per directory). Files: git ls-files (tracked + untracked, honouring +# .gitignore, so toolchains/, just-stuff/, obj/, bin/ are never seen); +# every thirdparty/ directory is skipped. +# +# Environment: QA_JOBS (default 6) parallel clang-tidy processes; all +# work runs under nice -n 10. +# +# Exit status: 0 = no new findings (or report-only), 1 = new findings, +# 2 = tool failure. + +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../../.." && pwd)" +QA_OUT="$ROOT/obj/qa" +JOBS="${QA_JOBS:-6}" +NICE=(nice -n 10) + +TOOL="${1:-}" +UPDATE_ARGS=() +if [ "${2:-}" = "--update" ]; then + UPDATE_ARGS=(--update) +elif [ -n "${2:-}" ]; then + echo "qaGate: unknown option $2" >&2 + exit 2 +fi + +CCN_MAX=15 +LENGTH_MAX=100 +COMPLEXITY_TOP=40 + +case "$TOOL" in + tidy) BASELINE="$HERE/clangTidyBaseline.txt" ;; + shellcheck) BASELINE="$HERE/shellcheckBaseline.txt" ;; + codespell) BASELINE="$HERE/codespellBaseline.txt" ;; + complexity) BASELINE="" ;; + *) + echo "usage: qaGate.sh tidy|shellcheck|codespell|complexity [--update]" >&2 + exit 2 + ;; +esac + +OUT="$QA_OUT/$TOOL" +RAW="$OUT/raw.txt" +KEYS="$OUT/keys.txt" +SUMMARY="$OUT/summary.txt" +rm -rf "$OUT" +mkdir -p "$OUT" +cd "$ROOT" || exit 2 + + +# requireTool NAME +# Fails the gate (exit 2) when NAME is not on PATH. +requireTool() { + if ! command -v "$1" > /dev/null 2>&1; then + echo "qaGate: $1 is not installed; the $TOOL gate cannot run" >&2 + exit 2 + fi +} + + +# sourceFiles PATTERN... +# NUL-separated tracked and untracked (not ignored) files matching the +# git pathspecs, minus third-party code and tracked files deleted from +# the working tree. +sourceFiles() { + local f + + git ls-files -z -co --exclude-standard -- "$@" | grep -z -v '/thirdparty/' | while IFS= read -r -d '' f; do + if [ -f "$f" ]; then + printf '%s\0' "$f" + fi + done +} + + +# summarize +# Writes summary.txt from keys.txt: findings per check (field 2) and per +# directory (field 1's directory), largest first. +summarize() { + { + echo "$TOOL: $(wc -l < "$KEYS") finding(s)" + echo "" + echo "per check:" + awk -F'|' '{ print $2 }' "$KEYS" | sort | uniq -c | sort -rn + echo "" + echo "per directory:" + awk -F'|' '{ n = split($1, p, "/"); d = (n > 1) ? p[1] : "."; for (i = 2; i < n; i++) { d = d "/" p[i] } print d }' "$KEYS" | sort | uniq -c | sort -rn + } > "$SUMMARY" +} + + +# runCodespell +# codespell over prose-bearing sources. Skipped by regex: camelCase / +# PascalCase words (identifiers, not prose), menu accelerator strings +# ("Projec&t") and BASIC hex literals (&HAA); project words (BASIC +# keywords, protocol names, short variable names) are in +# codespellWords.txt. +runCodespell() { + requireTool codespell + sourceFiles '*.c' '*.h' '*.dhs' '*.bhs' '*.md' '*.bas' '*.frm' '*.py' '*.sh' '*.mk' '*Makefile' \ + | "${NICE[@]}" xargs -0 codespell --quiet-level=2 -I "$HERE/codespellWords.txt" \ + --ignore-regex '\b[a-z]+[A-Z][A-Za-z0-9]*\b|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b|\b[A-Za-z]+&[A-Za-z]+\b|&H[0-9A-Fa-f]+\b' > "$RAW" + # file:line: word ==> fix -> file|word ==> fix + sed -E 's/^([^:]+):[0-9]+: /\1|/' "$RAW" > "$KEYS" +} + + +# runComplexity +# lizard warnings (clang/gcc format, sorted by CCN) for product sources; +# keys: file|over-ccn, over-length or over-both|function CCN n, n lines. +runComplexity() { + requireTool lizard + sourceFiles 'src/*.c' 'src/*.h' | grep -z -v '^src/test/' | tr '\0' '\n' > "$OUT/files.txt" + "${NICE[@]}" lizard -l c -C "$CCN_MAX" -L "$LENGTH_MAX" -w -s cyclomatic_complexity -t "$JOBS" -f "$OUT/files.txt" > "$RAW" + # file:line: warning: NAME has N NLOC, C CCN, T token, P PARAM, L length, D ND + awk -v ccnMax="$CCN_MAX" -v lenMax="$LENGTH_MAX" ' + match($0, /^[^:]+:[0-9]+: warning: /) { + split($0, head, ":") + rest = substr($0, RLENGTH + 1) + name = rest + sub(/ has .*$/, "", name) + ccn = rest + sub(/^.* NLOC, /, "", ccn) + sub(/ CCN.*$/, "", ccn) + len = rest + sub(/^.* PARAM, /, "", len) + sub(/ length.*$/, "", len) + kind = (ccn + 0 > ccnMax) ? ((len + 0 > lenMax) ? "over-both" : "over-ccn") : "over-length" + print head[1] "|" kind "|" name " CCN " ccn ", " len " lines" + } + ' "$RAW" > "$KEYS" +} + + +# runShellcheck +# Runs shellcheck -x (follow sourced files) over every shell script. +runShellcheck() { + requireTool shellcheck + sourceFiles '*.sh' | "${NICE[@]}" xargs -0 shellcheck -x --source-path=SCRIPTDIR -f gcc > "$RAW" + # file:line:col: severity: msg [SCnnnn] -> file|SCnnnn|msg + sed -nE 's/^([^:]+):[0-9]+:[0-9]+: [a-z]+: (.*) \[(SC[0-9]+)\]$/\1|\3|\2/p' "$RAW" > "$KEYS" +} + + +# runTidy +# clang-tidy over the host compile database (hostDb.py) for every product +# C file (src/, not src/test/, not thirdparty/). A header finding is +# reported once per including file (under as many "a/../b" spellings of +# its path), so paths are normalized first and identical diagnostics +# (same file, line, column, text) are counted once. Findings outside the +# tree (system headers) or in thirdparty/ are dropped. +runTidy() { + requireTool clang-tidy + requireTool run-clang-tidy + python3 "$HERE/hostDb.py" db || exit 2 + "${NICE[@]}" run-clang-tidy -quiet -p "$QA_OUT" -j "$JOBS" -config-file "$HERE/clangTidy.yaml" \ + "^$ROOT/src/(?!test/)(?!.*/thirdparty/).*\\.c$" > "$RAW" 2> "$OUT/stderr.txt" + # file:line:col: warning|error: msg [checks] -> file|checks|msg + awk -v root="$ROOT/" ' + /^\/[^:]+:[0-9]+:[0-9]+: (warning|error): / { + file = $0 + sub(/:[0-9]+:[0-9]+: (warning|error): .*$/, "", file) + rest = substr($0, length(file) + 2) + pos = rest + sub(/: (warning|error): .*$/, "", pos) + msg = rest + sub(/^[0-9]+:[0-9]+: (warning|error): /, "", msg) + check = msg + if (match(msg, / \[[^]]+\]$/)) { + check = substr(msg, RSTART + 2, RLENGTH - 3) + msg = substr(msg, 1, RSTART - 1) + } else { + check = "clang-diagnostic" + } + while (file ~ /[^\/]+\/\.\.\//) { + sub(/[^\/]+\/\.\.\//, "", file) + } + if (index(file, root) != 1 || file ~ /\/thirdparty\//) { + next + } + file = substr(file, length(root) + 1) + if (seen[file ":" pos ":" check ":" msg]++) { + next + } + print file "|" check "|" msg + } + ' "$RAW" > "$KEYS" +} + + +case "$TOOL" in + tidy) runTidy ;; + shellcheck) runShellcheck ;; + codespell) runCodespell ;; + complexity) runComplexity ;; +esac +summarize +echo "qaGate: $TOOL raw output, keys and summary in $OUT" + +# The complexity report is never gated. +if [ -z "$BASELINE" ]; then + head -n 20 "$SUMMARY" + echo "" + echo "most complex $COMPLEXITY_TOP (all in $RAW):" + head -n "$COMPLEXITY_TOP" "$RAW" + exit 0 +fi + +if [ ! -f "$BASELINE" ] && [ "${#UPDATE_ARGS[@]}" -eq 0 ]; then + echo "qaGate: ************************************************************" + echo "qaGate: REPORT-ONLY: no $TOOL baseline yet ($BASELINE)" + echo "qaGate: $(wc -l < "$KEYS") finding(s); nothing is gated until" + echo "qaGate: 'qaGate.sh $TOOL --update' writes the baseline." + echo "qaGate: ************************************************************" + head -n 40 "$SUMMARY" + exit 0 +fi + +"$HERE/staticGate.sh" --compare "$TOOL" "$KEYS" "$BASELINE" "${UPDATE_ARGS[@]}" diff --git a/src/test/lint/shellcheckBaseline.txt b/src/test/lint/shellcheckBaseline.txt new file mode 100644 index 00000000..f92f95c4 --- /dev/null +++ b/src/test/lint/shellcheckBaseline.txt @@ -0,0 +1,48 @@ +# shellcheck baseline -- maintained by staticGate.sh (see its header) +# format: , each preceded by its +# justification: # FP: | # THIRDPARTY: | # UNTRIAGED + +# FP: '$(CC)' is a make expression in the --eval rule and must reach make unexpanded +1 analyze.sh|SC2016|Expressions don't expand in single quotes, use double quotes for that. +# FP: usage is meant to run when either argument-count test fails +1 src/sdk/newProject.sh|SC2015|Note that A && B || C is not if-then-else. C may run when A is true. +# FP: tr a-z A-Z is the intended ASCII-only case map for C identifiers and 8.3 names; [:lower:] would follow the locale +5 src/sdk/newProject.sh|SC2018|Use '[:lower:]' to support accents and foreign alphabets. +# FP: tr A-Z a-z is the intended ASCII-only case map for C identifiers and 8.3 names; [:upper:] would follow the locale +5 src/sdk/newProject.sh|SC2019|Use '[:upper:]' to support accents and foreign alphabets. +# FP: $args is the fixture's .args file, a space-separated argument list split on purpose +2 src/test/basic/runBasic.sh|SC2086|Double quote to prevent globbing and word splitting. +# FP: '$(HOST_SUITES)' is a make expression in the --eval rule +1 src/test/coverage/runCoverage.sh|SC2016|Expressions don't expand in single quotes, use double quotes for that. +# FP: covMake is called through runStep "$@" +1 src/test/coverage/runCoverage.sh|SC2317|Command appears to be unreachable. Check usage (or ignore if invoked indirectly). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|BOX86_BIN appears unused. Verify use (or export if used externally). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|BOX86_HOME appears unused. Verify use (or export if used externally). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|BOX86_ROMS appears unused. Verify use (or export if used externally). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|CPPCHECK_BIN appears unused. Verify use (or export if used externally). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|DOSBOX_BIN appears unused. Verify use (or export if used externally). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|DOSBOX_HOME appears unused. Verify use (or export if used externally). +# FP: emuIsolatedEnv's result array, used by the caller (env "${EMU_ENV[@]}" in runDos.sh) +1 src/test/dos/emuEnv.sh|SC2034|EMU_ENV appears unused. Verify use (or export if used externally). +# FP: set for the scripts that source emuEnv.sh (runDos.sh, installToolchain.sh) +1 src/test/dos/emuEnv.sh|SC2034|FREEDOS_DIR appears unused. Verify use (or export if used externally). +# FP: tr '\\' '/' maps DOS backslashes to slashes; the backslash is meant literally +2 src/test/dos/runDos.sh|SC1003|Want to escape a single quote? echo 'This is how it'\''s done'. +# FP: ${iniSet} ('section key value') is split into argv on purpose; the Python joins the value words back +1 src/test/dos/runDos.sh|SC2086|Double quote to prevent globbing and word splitting. +# FP: cleanup runs from 'trap cleanup EXIT' +11 src/test/dos/runDos.sh|SC2317|Command appears to be unreachable. Check usage (or ignore if invoked indirectly). +# FP: $(CC), $(DJGPP_PREFIX) and $(CWSDSTUB) are make expressions in --eval rules +2 src/test/lint/staticGate.sh|SC2016|Expressions don't expand in single quotes, use double quotes for that. +# FP: the printf writes BASIC source whose $ type suffixes must stay literal +1 src/test/sdk/sdkTest.sh|SC2016|Expressions don't expand in single quotes, use double quotes for that. +# FP: ASCII upper-casing of DOS 8.3 help file names is intended; [:lower:] would follow the locale +3 src/tools/helpFiles.sh|SC2018|Use '[:lower:]' to support accents and foreign alphabets. +# FP: ASCII upper-casing of DOS 8.3 help file names is intended; [:upper:] would follow the locale +3 src/tools/helpFiles.sh|SC2019|Use '[:upper:]' to support accents and foreign alphabets. diff --git a/src/test/lint/staticGate.sh b/src/test/lint/staticGate.sh index 17afd5d0..3b472127 100755 --- a/src/test/lint/staticGate.sh +++ b/src/test/lint/staticGate.sh @@ -14,6 +14,14 @@ # staticGate.sh --cppcheck-only # skip the (slow) analyzer build # staticGate.sh --analyzer-only # staticGate.sh --self-test # test the baseline merge logic only +# staticGate.sh --compare NAME KEYS BASELINE [--update] +# # gate a key list another tool made +# +# --compare is how the other gates in this directory (qaGate.sh: +# clang-tidy, shellcheck, codespell) reuse the baseline logic below +# instead of copying it: KEYS is a file of normalized keys, one finding +# per line (duplicates count), BASELINE is that tool's baseline file. +# Same exit codes; cppcheck and the analyzer do not run. # # The analyzer pass builds a throwaway copy of the tree in a temp dir so # the real object files are never touched (and a concurrent normal build @@ -66,14 +74,28 @@ UPDATE=0 SELF_TEST=0 RUN_CPPCHECK=1 RUN_ANALYZER=1 -for arg in "$@"; do - case "$arg" in +COMPARE_NAME="" +COMPARE_KEYS="" +COMPARE_BASE="" +while [ "$#" -gt 0 ]; do + case "$1" in --update) UPDATE=1 ;; --cppcheck-only) RUN_ANALYZER=0 ;; --analyzer-only) RUN_CPPCHECK=0 ;; --self-test) SELF_TEST=1 ;; - *) echo "staticGate: unknown option $arg" >&2; exit 2 ;; + --compare) + if [ "$#" -lt 4 ]; then + echo "staticGate: --compare needs NAME KEYS BASELINE" >&2 + exit 2 + fi + COMPARE_NAME="$2" + COMPARE_KEYS="$3" + COMPARE_BASE="$4" + shift 3 + ;; + *) echo "staticGate: unknown option $1" >&2; exit 2 ;; esac + shift done WORK="$(mktemp -d)" @@ -262,7 +284,9 @@ compareToBaseline() { local key="${line#* }" total=$((total + count)) local baseCount - baseCount="$(grep -F -- " $key" "$baseCounts" | awk -v k="$key" '{ n=index($0," "); if (substr($0,n+1)==k) print $1 }' | head -n 1)" + # The key reaches awk through the environment: awk -v would expand + # backslash escapes in it (shellcheck's SC1003 message holds one). + baseCount="$(grep -F -- " $key" "$baseCounts" | KEY="$key" awk '{ n=index($0," "); if (substr($0,n+1)==ENVIRON["KEY"]) print $1 }' | head -n 1)" baseCount="${baseCount:-0}" if [ "$count" -gt "$baseCount" ]; then echo "NEW [$name] $key (now $count, baseline $baseCount)" @@ -401,6 +425,30 @@ TESTEOF LC_ALL=C sort "$t/entries.txt" > "$t/entriesSorted.txt" selfTestCheck "merged baseline entries equal the findings" "$t/curSorted.txt" "$t/entriesSorted.txt" || rc=1 + # compareToBaseline (the gate itself, also behind --compare): equal or + # lower counts pass; a grown key and an unknown key fail by name. + # The key with a backslash must match itself (no escape processing). + printf '%s\n' '# header' '' '# FP: x' '2 a.c|fnA|leak' '1 b.c|fnB|null deref' '1 s.sh|SC1003|tr a\tb' > "$t/base7.txt" + printf '%s\n' 'a.c|fnA|leak' 'b.c|fnB|null deref' 's.sh|SC1003|tr a\tb' > "$t/keys7.txt" + if compareToBaseline cmpok "$t/keys7.txt" "$t/base7.txt" > "$t/out7.txt"; then + echo "staticGate self-test: ok compare passes equal / lower counts" + else + echo "staticGate self-test: FAIL compare rejected a clean key list" + cat "$t/out7.txt" + rc=1 + fi + printf '%s\n' 'b.c|fnB|null deref' 'b.c|fnB|null deref' 'n.c|fnN|new one' > "$t/keys8.txt" + if compareToBaseline cmpnew "$t/keys8.txt" "$t/base7.txt" > "$t/out8.txt"; then + echo "staticGate self-test: FAIL compare passed a grown and a new key" + rc=1 + elif grep -qF 'NEW [cmpnew] b.c|fnB|null deref (now 2, baseline 1)' "$t/out8.txt" && grep -qF 'NEW [cmpnew] n.c|fnN|new one (now 1, baseline 0)' "$t/out8.txt"; then + echo "staticGate self-test: ok compare fails on grown and new keys" + else + echo "staticGate self-test: FAIL compare output" + cat "$t/out8.txt" + rc=1 + fi + if [ "$rc" -eq 0 ]; then echo "staticGate self-test: PASS" else @@ -409,16 +457,41 @@ TESTEOF return "$rc" } +# finish +# Prints the verdict for STATUS and exits with it. +finish() { + if [ "$STATUS" -ne 0 ]; then + echo "staticGate: FAIL" + else + echo "staticGate: PASS" + fi + exit "$STATUS" +} + if [ "$SELF_TEST" -eq 1 ]; then selfTest exit $? fi +# --compare: gate (or with --update merge) a key list some other tool +# produced, with the same baseline rules, then stop. +if [ -n "$COMPARE_NAME" ]; then + if [ ! -f "$COMPARE_KEYS" ]; then + echo "staticGate: --compare: key file $COMPARE_KEYS not found" >&2 + exit 2 + fi + compareToBaseline "$COMPARE_NAME" "$COMPARE_KEYS" "$COMPARE_BASE" || STATUS=1 + reportUntriaged "$COMPARE_NAME" "$COMPARE_BASE" + finish +fi + # DJGPP_CC overrides; otherwise the compiler the build uses (src/common.mk # finds the toolchain, DJGPP_PREFIX in the environment overrides that). -REAL_CC="${DJGPP_CC:-$(make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printCc: ; @echo $(CC)' printCc)}" +# These queries drop the calling make's flags: under make -j they bring +# its jobserver, and the query then prints directory lines into the value. +REAL_CC="${DJGPP_CC:-$(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printCc: ; @echo $(CC)' printCc)}" # The copied tree has no toolchains/, so its make gets the real tree's. -TOOLCHAIN_ARGS=( $(make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printTc: ; @echo DJGPP_PREFIX=$(DJGPP_PREFIX) CWSDSTUB=$(CWSDSTUB)' printTc) ) +read -ra TOOLCHAIN_ARGS < <(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printTc: ; @echo DJGPP_PREFIX=$(DJGPP_PREFIX) CWSDSTUB=$(CWSDSTUB)' printTc) # ------------------------------------------------------------ # cppcheck @@ -443,7 +516,7 @@ if [ "$RUN_CPPCHECK" -eq 1 ]; then echo "staticGate: the cppcheck pass on purpose." >&2 exit 2 fi - cd "$ROOT" + cd "$ROOT" || exit 2 INCLUDES=( -Isrc/libs/kpunch -Isrc/libs/kpunch/libdvx @@ -620,10 +693,4 @@ fi reportUntriaged cppcheck "$CPPCHECK_BASE" reportUntriaged analyzer "$ANALYZER_BASE" - -if [ "$STATUS" -ne 0 ]; then - echo "staticGate: FAIL" -else - echo "staticGate: PASS" -fi -exit "$STATUS" +finish diff --git a/src/test/lint/styleLint.py b/src/test/lint/styleLint.py index 656bccce..c5dca665 100755 --- a/src/test/lint/styleLint.py +++ b/src/test/lint/styleLint.py @@ -52,6 +52,12 @@ LAST_FUNCS = set(["main", "appMain", "wgtRegister"]) REQUIRED_BLANK_LINES = 2 +# The generated first lines of baseline.txt (written by every update). +BASELINE_HEADER = ( + "# styleLint baseline -- generated by styleLint.py --update-baseline", + "# format: ||", +) + C_EXTENSIONS = (".c", ".h") TEXT_EXTENSIONS = (".c", ".h", ".dhs", ".bhs", ".md", ".bas", ".frm", ".dbp", ".py", ".sh", ".txt", ".hcf", ".mk") TEXT_BASENAMES = ("Makefile",) @@ -580,13 +586,43 @@ def loadBaseline(path): return counts +# The justification comments of an existing baseline: the comment lines +# above each key, by key (the generated header is not one). An update +# writes each above its key again, so a reason outlives the regeneration. +def loadBaselineComments(path): + comments = {} + pending = [] + if not os.path.isfile(path): + return comments + with open(path, "r") as f: + for line in f: + line = line.rstrip("\n") + if not line or line in BASELINE_HEADER: + continue + if line.startswith("#"): + pending.append(line) + continue + countText, sep, key = line.partition(" ") + if sep and pending: + comments[key] = pending + pending = [] + return comments + + def saveBaseline(path, violations): - counts = countKeys(violations) + counts = countKeys(violations) + comments = loadBaselineComments(path) with open(path, "w") as f: - f.write("# styleLint baseline -- generated by styleLint.py --update-baseline\n") - f.write("# format: ||\n") + for line in BASELINE_HEADER: + f.write(line + "\n") for key in sorted(counts): + for line in comments.pop(key, []): + f.write(line + "\n") f.write("%d %s\n" % (counts[key], key)) + for key in sorted(comments): + sys.stdout.write("styleLint: dropped the comment of a key no longer found: %s\n" % key) + for line in comments[key]: + sys.stdout.write(" %s\n" % line) def countKeys(violations): diff --git a/src/test/lint/valgrind.mk b/src/test/lint/valgrind.mk new file mode 100644 index 00000000..f81987a3 --- /dev/null +++ b/src/test/lint/valgrind.mk @@ -0,0 +1,65 @@ +# 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. + +# valgrind.mk -- valgrind memcheck over the native host tools +# (make -C src/test/lint valgrind; report-only, not part of make test). +# +# bascomp, basrun, dvxhlpc and dvxres are built here with gcc at the +# release optimization plus -g (clangHost.mk, CH_CC=gcc, objects and +# binaries under obj/valgrind -- the bin/host tools are -O2 without +# debug info and other builds replace them at any time), then +# valgrindRun.sh runs them under memcheck on inputs from the tree: +# basrun / bascomp src/test/basic/runBasic.sh (every fixture and +# sample project) and src/test/samples/runSamples.sh, +# through wrappers that start each run under valgrind +# dvxhlpc the every-directive help fixture (with HTML) and +# each negative help fixture +# dvxres list / get / add / build / strip on copies of the +# sample goldens and the resource fixtures +# Every process writes obj/valgrind/logs/vg..; one that holds +# an error (invalid read / write, uninitialised use, definite leak) is a +# finding. The summary lists each finding's first stack frame and +# fails the target when there is any. +# +# libtasks' task switching never runs here (no tool links it), so no +# VALGRIND_STACK_REGISTER build flag is needed. secproxy is left out: +# it only runs against a live socket peer and a serial link (its packet +# and security layers are covered by fuzzPacket and the serial suite). + +CH_VARIANT := valgrind +CH_CC := gcc +CH_OPT := -O2 -g +CH_INSTR := +include $(dir $(lastword $(MAKEFILE_LIST)))clangHost.mk + +.DEFAULT_GOAL := valgrind + +VG_PROGS := bascomp basrun dvxhlpc dvxres + +$(foreach p,$(VG_PROGS),$(eval $(call CH_PROG_RULE,$p))) + +.PHONY: valgrind valgrind-build + +valgrind-build: $(addprefix $(CH_BIN)/,$(VG_PROGS)) + +valgrind: valgrind-build + $(CH_ROOT)/src/test/lint/valgrindRun.sh $(CH_OBJ) diff --git a/src/test/lint/valgrindRun.sh b/src/test/lint/valgrindRun.sh new file mode 100755 index 00000000..c2c7dbf1 --- /dev/null +++ b/src/test/lint/valgrindRun.sh @@ -0,0 +1,204 @@ +#!/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. + +# valgrindRun.sh -- runs the host tools under valgrind memcheck +# (valgrind.mk builds them; see its header for what runs). +# +# Usage: valgrindRun.sh OBJDIR +# OBJDIR obj/valgrind: tools in OBJDIR/bin; wrappers, scratch files and +# logs go under it +# +# Exit status: 0 = no memcheck error, 1 = errors (listed), 2 = setup +# failure. A failing test script is reported but is not a memcheck +# finding (the BASIC goldens can be out of date while the compiler +# changes); its output is in OBJDIR/logs/.log. + +set -u + +if [ "$#" -ne 1 ]; then + echo "usage: valgrindRun.sh OBJDIR" >&2 + exit 2 +fi + +OBJ="$(cd "$1" && pwd)" || exit 2 +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../../.." && pwd)" +BIN="$OBJ/bin" +WRAP="$OBJ/wrap" +LOGS="$OBJ/logs" +WORK="$OBJ/work" +HELP_FIXTURE="$ROOT/src/test/tools/fixtures/help" +RES_FIXTURE="$ROOT/src/test/tools/fixtures/res" +GOLDEN="$ROOT/src/test/samples/golden" +VG_FRAMES=3 +VG_OPTS="-q --tool=memcheck --leak-check=full --show-leak-kinds=definite,indirect --errors-for-leak-kinds=definite --track-origins=yes --num-callers=24 --fullpath-after=$ROOT/" + +if ! command -v valgrind > /dev/null 2>&1; then + echo "valgrindRun: valgrind is not installed" >&2 + exit 2 +fi + +rm -rf "$WRAP" "$LOGS" "$WORK" +mkdir -p "$WRAP" "$LOGS" "$WORK" +cd "$ROOT" || exit 2 + + +# makeWrapper TOOL +# WRAP/TOOL starts BIN/TOOL under memcheck, one log per process. +makeWrapper() { + cat > "$WRAP/$1" < "$LOGS/$name.log" 2>&1; then + echo "valgrind: $name exited non-zero (see $LOGS/$name.log)" + failedSteps="$failedSteps $name" + fi +} + + +# resSteps +# dvxres list / get on every resource fixture and golden; build, add, +# get and strip on a scratch copy. Run through step (indirectly). +# shellcheck disable=SC2317 +resSteps() { + local f + local copy="$WORK/res/copy.app" + + mkdir -p "$WORK/res" + for f in "$RES_FIXTURE"/*.bin "$GOLDEN"/*.app; do + "$WRAP/dvxres" list "$f" + "$WRAP/dvxres" get "$f" name "$WORK/res/name.out" + done + cp "$RES_FIXTURE"/* "$WORK/res/" + (cd "$WORK/res" && "$WRAP/dvxres" build "$WORK/res/module.bin" manifest.res) + cp "$GOLDEN/clock-debug.app" "$copy" + "$WRAP/dvxres" add "$copy" qaText text "valgrind run" + "$WRAP/dvxres" add "$copy" qaBlob binary "@$RES_FIXTURE/blob.bin" + "$WRAP/dvxres" get "$copy" qaBlob "$WORK/res/blob.out" + "$WRAP/dvxres" strip "$copy" + "$WRAP/dvxres" list "$copy" + return 0 +} + + +# helpSteps +# The every-directive help fixture (with HTML output), then each +# negative fixture on its own (those fail to compile on purpose; only +# memcheck errors count). Run through step (indirectly). +# shellcheck disable=SC2317 +helpSteps() { + local f + + mkdir -p "$WORK/help" + (cd "$HELP_FIXTURE" && "$WRAP/dvxhlpc" -o "$WORK/help/fixture.hlp" --quiet --html "$WORK/help/fixture.html" -i . main.dhs @inputs.txt) || return 1 + for f in "$HELP_FIXTURE"/*.dhs; do + (cd "$HELP_FIXTURE" && "$WRAP/dvxhlpc" -o "$WORK/help/one.hlp" --quiet -i . "$(basename "$f")") + done + return 0 +} + + +for tool in bascomp basrun dvxhlpc dvxres; do + if [ ! -x "$BIN/$tool" ]; then + echo "valgrindRun: $BIN/$tool not built" >&2 + exit 2 + fi + makeWrapper "$tool" +done + +failedSteps="" +step runBasic env BASRUN="$WRAP/basrun" BASCOMP="$WRAP/bascomp" BASIC_OUT="$WORK/basic" "$ROOT/src/test/basic/runBasic.sh" +step runSamples env BASCOMP="$WRAP/bascomp" SAMPLES_OUT="$WORK/samples" "$ROOT/src/test/samples/runSamples.sh" +step dvxhlpc helpSteps +step dvxres resSteps + +# Findings: every non-empty per-process log (-q prints errors only). +echo "" +echo "valgrind: summary" +processes=0 +bad=0 +: > "$LOGS/findings.txt" +for log in "$LOGS"/vg.*; do + if [ ! -f "$log" ]; then + continue + fi + processes=$((processes + 1)) + if [ -s "$log" ]; then + bad=$((bad + 1)) + # Each error / leak record: its headline (numbers dropped so + # records group) and its first VG_FRAMES frames in tree sources + # (allocation helpers come first in a leak, so one is not enough). + awk -v frames="$VG_FRAMES" ' + function flush() { + if (what != "") { + print what " -- " chain + } + what = "" + } + /^==[0-9]+== [^ ]/ && !/^==[0-9]+== +(at|by) / { + flush() + what = $0 + sub(/^==[0-9]+== /, "", what) + gsub(/[0-9][0-9,]*/, "N", what) + chain = "" + n = 0 + next + } + what != "" && n < frames && /(at|by) 0x/ && /\(src\/[^)]*:[0-9]+\)/ { + frame = $0 + sub(/^.*(at|by) 0x[0-9A-F]+: /, "", frame) + chain = (n == 0) ? frame : chain " < " frame + n++ + } + END { + flush() + } + ' "$log" >> "$LOGS/findings.txt" + fi +done +sort "$LOGS/findings.txt" | uniq -c | sort -rn > "$LOGS/findingsSummary.txt" +sed 's/^/ /' "$LOGS/findingsSummary.txt" +echo "valgrind: $processes process(es) checked, $bad with memcheck errors; logs in $LOGS" +if [ -n "$failedSteps" ]; then + echo "valgrind: steps that exited non-zero (not memcheck findings by themselves):$failedSteps" +fi +if [ "$processes" -eq 0 ]; then + echo "valgrindRun: no process ran under valgrind" >&2 + exit 2 +fi +if [ "$bad" -ne 0 ]; then + exit 1 +fi +exit 0 diff --git a/src/test/music/musicTest.c b/src/test/music/musicTest.c index cfa86b29..97d3ffaf 100644 --- a/src/test/music/musicTest.c +++ b/src/test/music/musicTest.c @@ -226,15 +226,27 @@ static void basicTests(void) { TEST_STR_EQ(MusicError(), "No error"); TEST_EQ(MusicLength(h), 1000); TEST_EQ(MusicPlaying(h), 0); + TEST_STR_EQ(MusicInfo(h, "kind"), "midi"); + TEST_STR_EQ(MusicInfo(h, "WHERE"), ""); + TEST_STR_EQ(MusicInfo(h, "title"), ""); + TEST_STR_EQ(MusicInfo(h, "nonsense"), ""); + TEST_STR_EQ(MusicInfo(h, NULL), ""); + TEST_EQ(MusicGetVolume(h), 100); MusicPlay(h, 0); + TEST_STR_EQ(MusicInfo(h, "where"), "software"); + TEST_TRUE(MusicInfo(h, "output")[0] != '\0'); + TEST_STR_EQ(MusicInfo(h, "looping"), "0"); TEST_EQ(MusicPlaying(h), -1); MusicVolume(h, 50); TEST_STR_EQ(MusicError(), "No error"); + TEST_EQ(MusicGetVolume(h), 50); pump(RATE_DEV / 10); TEST_TRUE(MusicPosition(h) > 0); MusicPause(h); TEST_EQ(MusicPlaying(h), 0); + TEST_STR_EQ(MusicInfo(h, "paused"), "-1"); MusicResume(h); + TEST_STR_EQ(MusicInfo(h, "paused"), "0"); TEST_EQ(MusicPlaying(h), -1); MusicSeek(h, 500); TEST_EQ(MusicPosition(h), 500); @@ -246,6 +258,8 @@ static void basicTests(void) { int32_t live = MusicMidiOpen(); TEST_TRUE(live > 0); + TEST_STR_EQ(MusicInfo(live, "kind"), "live"); + TEST_STR_EQ(MusicInfo(live, "looping"), "-1"); MusicProgram(live, 1, 73); TEST_STR_EQ(MusicError(), "No error"); MusicNoteOn(live, 10, 36, 100); @@ -263,6 +277,8 @@ static void basicTests(void) { MusicStop(h); TEST_STR_EQ(MusicError(), "Invalid argument"); TEST_EQ(MusicPlaying(h), 0); + TEST_STR_EQ(MusicInfo(h, "kind"), ""); + TEST_EQ(MusicGetVolume(h), 0); TEST_EQ(MusicLoad("NOSUCH.MID"), 0); TEST_STR_EQ(MusicError(), "The file could not be read"); sAppId = APP_A; @@ -332,7 +348,7 @@ static void checkGolden(const char *name, uint64_t hash) { } printf(" golden %s missing (UPDATE_GOLDENS=1 to record %016llx)\n", name, (unsigned long long)hash); - TEST_TRUE(!"golden missing"); + TEST_FAIL("golden missing"); } @@ -1707,7 +1723,7 @@ static void saveGoldens(void) { FILE *f = fopen(MUSIC_GOLDEN, "w"); if (!f) { - TEST_TRUE(!"cannot write the golden file"); + TEST_FAIL("cannot write the golden file"); return; } @@ -2081,7 +2097,7 @@ int main(void) { platformTimerInit(); if (chdir(MUSIC_WORKDIR) != 0 || (mkdir(DVX_CONFIG_DIR, DIR_MODE) != 0 && access(DVX_CONFIG_DIR, F_OK) != 0)) { - TEST_TRUE(!"cannot prepare the work folder"); + TEST_FAIL("cannot prepare the work folder"); return tSuiteEnd(); } diff --git a/src/test/net/Makefile b/src/test/net/Makefile index ddccd4cf..e709fa01 100644 --- a/src/test/net/Makefile +++ b/src/test/net/Makefile @@ -25,6 +25,8 @@ # and run for real: # # loopback TCP and UDP through 127.0.0.1 (lwIP's loopback interface) +# BASIC the settings / status functions of net.bas, also from a +# program compiled with the real net.bas and prefs.bas # wire a second process runs another copy of the stack as the # "other computer" (DHCP server, DNS server, echo server) # at the far end of a socket pair that stands in for the @@ -51,12 +53,12 @@ LWIP_SRCS = $(wildcard $(LWIP_DIR)/core/*.c) $(wildcard $(LWIP_DIR)/core/ipv4/* NET_OBJS = $(patsubst $(NETDIR)/%.c,$(BINDIR)/net/%.o,$(NET_SRCS)) LWIP_OBJS = $(patsubst $(LWIP_DIR)/%.c,$(BINDIR)/lwip/%.o,$(LWIP_SRCS)) HOST_OBJS = $(BINDIR)/hostFd.o $(BINDIR)/fakeDrv.o -LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) +LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) # Standard names renamed; plain (non-GNU, unfortified) prototypes so the # library's definitions match the C library's declarations. NET_INCS = -I. -I$(NETDIR) -I$(NETDIR)/port -I$(LWIP_DIR)/include -I$(DVXTEST_ROOT)/src/libs/kpunch/dvxshell -NET_CFLAGS = $(DVXTEST_CFLAGS) $(NET_INCS) -U_GNU_SOURCE -D_DEFAULT_SOURCE -U_FORTIFY_SOURCE -include netHostNames.h +NET_CFLAGS = $(DVXTEST_CFLAGS) $(NET_INCS) -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' -U_GNU_SOURCE -D_DEFAULT_SOURCE -U_FORTIFY_SOURCE -include netHostNames.h DRV_CFLAGS = $(DVXTEST_CFLAGS) $(NET_INCS) .PHONY: all run clean @@ -81,7 +83,7 @@ $(BINDIR)/fakeDrv.o: fakeDrv.c $(DVX_MAKEFILES) $(NET_TEST): netTest.c $(NET_OBJS) $(LWIP_OBJS) $(HOST_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs @mkdir -p $(BINDIR) - $(HOSTCC) $(NET_CFLAGS) -o $@ $< $(NET_OBJS) $(LWIP_OBJS) $(HOST_OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(NET_CFLAGS) -o $@ $< $(NET_OBJS) $(LWIP_OBJS) $(HOST_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm -include $(NET_OBJS:.o=.d) $(LWIP_OBJS:.o=.d) $(HOST_OBJS:.o=.d) $(NET_TEST).d diff --git a/src/test/net/netTest.c b/src/test/net/netTest.c index f87227c6..81e58854 100644 --- a/src/test/net/netTest.c +++ b/src/test/net/netTest.c @@ -32,6 +32,9 @@ #include "netInt.h" #include "netTestHost.h" #include "pktRing.h" +#include "compiler/compiler.h" +#include "runtime/vm.h" +#include "formrt/formrt.h" #include #include @@ -59,6 +62,9 @@ #define PORT_C 7003 #define PORT_D 7004 #define PORT_CLOSED 7099 +#define BAS_BAD_SOCKET 999 +#define BAS_ENDPOINT_TEXT 32 +#define BAS_PROGRAM_MAX 256 // The simulated network: this process is the DHCP client, the child is // the "other computer" at PEER_ADDR (DHCP, DNS and echo server). @@ -127,6 +133,8 @@ #define BYTE_MASK 0xFF #define PATTERN_MOD 251 #define LINE_MAX_T 128 +#define BAS_SRC_JOINS 3 // the two newlines between the sources and the terminator +#define BAS_OUT_MAX 1024 // Peer: one echo connection typedef struct { @@ -144,9 +152,11 @@ static const uint8_t sPeerMac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }; // Prototypes // ============================================================ +static void basOut(void *ctx, const char *text, bool newline); static void caseBasic(void); static void caseBulk(void); static void caseConfig(void); +static void caseConfigBasic(void); static void caseErrors(void); static void caseInet(void); static void caseNonBlocking(void); @@ -167,6 +177,8 @@ static void makeAddr(struct sockaddr_in *sa, const char *addr, uint16_t port static uint32_t nowMs(void); static uint8_t pattern(size_t i); static void peerMain(int wire); +static const char *recvText(int32_t handle); +static bool runBasic(const char *program, char *out, int32_t outSize); static int udpOn(uint16_t port); static bool waitUp(void); static void writeIni(void); @@ -175,20 +187,54 @@ int main(void); // The BASIC functions (netBas.c), over the same sockets. const char *HostLookup(const char *name); +const char *HostName(const char *address); +int32_t NetConfigChanged(int32_t config); +void NetConfigFree(int32_t config); +const char *NetConfigGet(int32_t config, const char *field); +int32_t NetConfigLoad(int32_t prefs); +const char *NetConfigProblem(int32_t config); +int32_t NetConfigSet(int32_t config, const char *field, const char *value); +int32_t NetConfigStore(int32_t config, int32_t prefs); const char *NetError(void); +int32_t NetRestart(void); +const char *NetStatus(const char *field); +void NetStop(void); +int32_t NetVector(int32_t index); +int32_t NetVectorCount(void); int32_t TcpAccept(int32_t handle); void TcpClose(int32_t handle); int32_t TcpConnect(const char *host, int32_t port); int32_t TcpConnected(int32_t handle); int32_t TcpListen(int32_t port); +int32_t TcpLocalPort(int32_t handle); +const char *TcpPeer(int32_t handle); int32_t TcpPending(int32_t handle); -const char *TcpRecv(int32_t handle); -int32_t TcpSend(int32_t handle, const char *text); +const uint8_t *TcpRecv(int32_t handle, int32_t *outLen); +int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len); +int32_t TcpSetKeepAlive(int32_t handle, int32_t on); +int32_t TcpSetNoDelay(int32_t handle, int32_t on); +int32_t TcpShutdown(int32_t handle); void UdpClose(int32_t handle); const char *UdpFrom(void); int32_t UdpOpen(int32_t port); -const char *UdpRecv(int32_t handle); -int32_t UdpSend(int32_t handle, const char *host, int32_t port, const char *text); +const uint8_t *UdpRecv(int32_t handle, int32_t *outLen); +int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *data, int32_t len); + +// prefs.bas (libdvx dvxPrefsBas.c) +void PrefsClose(int32_t handle); +int32_t PrefsCreate(void); +const char *PrefsGet(int32_t handle, const char *section, const char *key, const char *defaultValue); +int32_t PrefsLoad(const char *path); +int32_t PrefsSave(int32_t handle); + + +// PRINT output of a BASIC program, appended to the buffer in ctx. +static void basOut(void *ctx, const char *text, bool newline) { + char *out = (char *)ctx; + size_t used = strlen(out); + + snprintf(out + used, BAS_OUT_MAX - used, "%s%s", text, newline ? "\n" : ""); +} static void caseBasic(void) { @@ -197,8 +243,12 @@ static void caseBasic(void) { int32_t conn = 0; int32_t udpA; int32_t udpB; - const char *text = ""; - uint32_t start; + const uint8_t *bytes = NULL; + int32_t len = 0; + uint32_t start; + char endpoint[BAS_ENDPOINT_TEXT]; + char program[BAS_PROGRAM_MAX]; + char out[BAS_OUT_MAX]; tCase("basic"); TEST_STR_EQ(HostLookup("localhost"), "127.0.0.1"); @@ -219,7 +269,7 @@ static void caseBasic(void) { } TEST_TRUE(conn > 0); - TEST_EQ(TcpSend(client, "hello from BASIC"), 16); + TEST_EQ(TcpSend(client, (const uint8_t *)"hello from BASIC", 16), 16); start = nowMs(); while (TcpPending(conn) < 16 && nowMs() - start < WAIT_MS) { @@ -227,9 +277,56 @@ static void caseBasic(void) { } TEST_EQ(TcpPending(conn), 16); - TEST_STR_EQ(TcpRecv(conn), "hello from BASIC"); - TEST_STR_EQ(TcpRecv(conn), ""); + TEST_STR_EQ(recvText(conn), "hello from BASIC"); + TEST_STR_EQ(recvText(conn), ""); TEST_EQ(TcpConnected(conn), 1); + + // Bytes with a zero in them, sent and received by a BASIC program + snprintf(program, sizeof(program), "PRINT TcpSend(%ld, \"a\" & CHR$(0) & \"b\"); TcpLocalPort(%ld); TcpPeer$(%ld) <> \"\"; HostName$(\"127.0.0.1\")\n", (long)client, (long)server, (long)client); + TEST_TRUE(runBasic(program, out, sizeof(out))); + snprintf(endpoint, sizeof(endpoint), "3 %d True 127.0.0.1\n", PORT_C); + TEST_STR_EQ(out, endpoint); + start = nowMs(); + + while (TcpPending(conn) < 3 && nowMs() - start < WAIT_MS) { + netPoll(); + } + + snprintf(program, sizeof(program), "DIM x AS STRING\nx = TcpRecv$(%ld)\nPRINT LEN(x); ASC(MID$(x, 2, 1)); RIGHT$(x, 1)\n", (long)conn); + TEST_TRUE(runBasic(program, out, sizeof(out))); + TEST_STR_EQ(out, "3 0 b\n"); + + // Endpoints, options, half-close (round 2 additions) + TEST_EQ(TcpLocalPort(server), PORT_C); + TEST_EQ(TcpLocalPort(conn), PORT_C); + snprintf(endpoint, sizeof(endpoint), "127.0.0.1:%ld", (long)TcpLocalPort(client)); + TEST_STR_EQ(TcpPeer(conn), endpoint); + snprintf(endpoint, sizeof(endpoint), "127.0.0.1:%d", PORT_C); + TEST_STR_EQ(TcpPeer(client), endpoint); + TEST_STR_EQ(TcpPeer(server), ""); + TEST_EQ(TcpLocalPort(BAS_BAD_SOCKET), -1); + TEST_EQ(TcpSetNoDelay(client, 1), -1); + TEST_EQ(TcpSetKeepAlive(client, 1), -1); + TEST_EQ(TcpSetKeepAlive(client, 0), -1); + TEST_EQ(TcpSetNoDelay(BAS_BAD_SOCKET, 1), 0); + TEST_STR_EQ(HostName("127.0.0.1"), "127.0.0.1"); + TEST_STR_EQ(HostName("not an address"), ""); + TEST_EQ(TcpShutdown(client), -1); + start = nowMs(); + + while (TcpConnected(conn) && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_EQ(TcpConnected(conn), 0); + TEST_EQ(TcpSend(conn, (const uint8_t *)"late", 4), 4); + start = nowMs(); + + while (TcpPending(client) < 4 && nowMs() - start < WAIT_MS) { + netPoll(); + } + + TEST_STR_EQ(recvText(client), "late"); TcpClose(client); start = nowMs(); @@ -245,17 +342,19 @@ static void caseBasic(void) { udpA = UdpOpen(PORT_A); udpB = UdpOpen(0); TEST_TRUE(udpA > 0 && udpB > 0); - TEST_EQ(UdpSend(udpB, "127.0.0.1", PORT_A, "ping"), 4); + TEST_EQ(UdpSend(udpB, "127.0.0.1", PORT_A, (const uint8_t *)"ping\0!", 6), 6); start = nowMs(); - while (text[0] == '\0' && nowMs() - start < WAIT_MS) { + while (len == 0 && nowMs() - start < WAIT_MS) { netPoll(); - text = UdpRecv(udpA); + bytes = UdpRecv(udpA, &len); } - TEST_STR_EQ(text, "ping"); + TEST_EQ(len, 6); + TEST_TRUE(memcmp(bytes, "ping\0!", 6) == 0); TEST_TRUE(strncmp(UdpFrom(), "127.0.0.1:", 10) == 0); - TEST_STR_EQ(UdpRecv(udpA), ""); + UdpRecv(udpA, &len); + TEST_EQ(len, 0); UdpClose(udpA); UdpClose(udpB); TEST_EQ(TcpListen(-1), 0); @@ -368,6 +467,23 @@ static void caseConfig(void) { cfg.vector = 0x62; TEST_PTR_EQ(netConfigProblem(&cfg), NULL); + // Equal compares the settings, not the bytes after the text + back = cfg; + TEST_TRUE(netConfigEqual(&cfg, &back)); + back.address[strlen(back.address) + 1] = 'x'; + TEST_TRUE(netConfigEqual(&cfg, &back)); + back.dhcp = !back.dhcp; + TEST_TRUE(!netConfigEqual(&cfg, &back)); + back = cfg; + back.vector++; + TEST_TRUE(!netConfigEqual(&cfg, &back)); + back = cfg; + strcpy(back.dns[1], "8.8.8.8"); + TEST_TRUE(!netConfigEqual(&cfg, &back)); + back = cfg; + strcpy(back.hostName, "other"); + TEST_TRUE(!netConfigEqual(&cfg, &back)); + netConfigToPrefs(h, &cfg); TEST_STR_EQ(prefsGetString(h, "network", "vector", ""), "0x62"); TEST_STR_EQ(prefsGetString(h, "network", "dhcp", ""), "false"); @@ -395,6 +511,120 @@ static void caseConfig(void) { } +// The network settings handles of net.bas (no stack needed). +static void caseConfigBasic(void) { + int32_t app = TEST_APP_ID; + int32_t *saved = dvxMemAppIdPtr; + int32_t prefs = PrefsCreate(); + int32_t cfg; + int32_t other; + + tCase("config BASIC"); + TEST_TRUE(prefs > 0); + cfg = NetConfigLoad(0); + TEST_TRUE(cfg > 0); + TEST_STR_EQ(NetConfigGet(cfg, "dhcp"), "-1"); + TEST_STR_EQ(NetConfigGet(cfg, "VECTOR"), "0"); + TEST_STR_EQ(NetConfigGet(cfg, "hostname"), "DVX"); + TEST_STR_EQ(NetConfigGet(cfg, "address"), ""); + TEST_STR_EQ(NetConfigGet(cfg, "nonsense"), ""); + TEST_STR_EQ(NetConfigGet(cfg, "dns0"), ""); + TEST_STR_EQ(NetConfigGet(cfg, "dns3"), ""); + TEST_STR_EQ(NetConfigGet(cfg, "dns1x"), ""); + TEST_STR_EQ(NetConfigGet(cfg, NULL), ""); + TEST_STR_EQ(NetConfigProblem(cfg), ""); + TEST_EQ(NetConfigChanged(cfg), 0); + + // Edit: fixed address, a bad mask first + TEST_EQ(NetConfigSet(cfg, "dhcp", "0"), -1); + TEST_EQ(NetConfigSet(cfg, "Address", "192.168.1.10"), -1); + TEST_EQ(NetConfigSet(cfg, "netmask", "255.0.255.0"), -1); + TEST_TRUE(NetConfigProblem(cfg)[0] != '\0'); + TEST_EQ(NetConfigSet(cfg, "netmask", "255.255.255.0"), -1); + TEST_EQ(NetConfigSet(cfg, "gateway", "192.168.1.1"), -1); + TEST_EQ(NetConfigSet(cfg, "dns2", "8.8.4.4"), -1); + TEST_EQ(NetConfigSet(cfg, "hostname", "retro-486"), -1); + TEST_EQ(NetConfigSet(cfg, "vector", "0x61"), -1); + TEST_EQ(NetConfigSet(cfg, "bogus", "1"), 0); + TEST_EQ(NetConfigSet(cfg, NULL, "1"), 0); + TEST_EQ(NetConfigSet(cfg, "address", "192.168.1.100000000000000000000"), -1); + TEST_EQ((int32_t)strlen(NetConfigGet(cfg, "address")), NET_ADDR_MAX - 1); + TEST_EQ(NetConfigSet(cfg, "address", "192.168.1.10"), -1); + TEST_STR_EQ(NetConfigProblem(cfg), ""); + TEST_STR_EQ(NetConfigGet(cfg, "dhcp"), "0"); + TEST_STR_EQ(NetConfigGet(cfg, "vector"), "97"); + TEST_STR_EQ(NetConfigGet(cfg, "dns2"), "8.8.4.4"); + TEST_EQ(NetConfigChanged(cfg), -1); + + // Store, then a second handle reads the same settings back + TEST_EQ(NetConfigStore(cfg, prefs), -1); + TEST_EQ(NetConfigStore(cfg, 0), 0); + TEST_STR_EQ(PrefsGet(prefs, "network", "vector", ""), "0x61"); + other = NetConfigLoad(prefs); + TEST_TRUE(other > 0 && other != cfg); + TEST_STR_EQ(NetConfigGet(other, "address"), "192.168.1.10"); + TEST_STR_EQ(NetConfigGet(other, "hostname"), "retro-486"); + TEST_EQ(NetConfigChanged(other), 0); + TEST_EQ(NetConfigSet(other, "hostname", "retro-486"), -1); + TEST_EQ(NetConfigChanged(other), 0); + NetConfigFree(other); + TEST_STR_EQ(NetConfigGet(other, "hostname"), ""); + TEST_EQ(NetConfigChanged(other), 0); + TEST_STR_EQ(NetConfigProblem(other), ""); + NetConfigFree(cfg); + NetConfigFree(0); + PrefsClose(prefs); + + // Interrupt choices: find it, then 60h..80h + TEST_EQ(NetVectorCount(), NET_VECTOR_LAST - NET_VECTOR_FIRST + 2); + TEST_EQ(NetVector(0), NET_VECTOR_AUTO); + TEST_EQ(NetVector(1), NET_VECTOR_FIRST); + TEST_EQ(NetVector(NetVectorCount() - 1), NET_VECTOR_LAST); + TEST_EQ(NetVector(NetVectorCount()), -1); + TEST_EQ(NetVector(-1), -1); + + // Status of a stopped network + netStop(); + TEST_STR_EQ(NetStatus("message"), netStateText(NetStoppedE)); + TEST_STR_EQ(NetStatus("up"), "0"); + TEST_STR_EQ(NetStatus("rxdropped"), "0"); + TEST_STR_EQ(NetStatus("unknown"), ""); + TEST_STR_EQ(NetStatus(NULL), ""); + + // A program with the real net.bas and prefs.bas + { + char out[BAS_OUT_MAX]; + + TEST_TRUE(runBasic( + "DIM p AS LONG\n" + "DIM c AS LONG\n" + "p = PrefsCreate()\n" + "c = NetConfigLoad(p)\n" + "PRINT NetConfigGet$(c, \"hostname\"); NetConfigChanged(c)\n" + "PRINT NetConfigSet(c, \"hostname\", \"basic-pc\"); NetConfigChanged(c)\n" + "PRINT NetConfigSet(c, \"hostname\", \"bad name\"); LEN(NetConfigProblem$(c)) > 0\n" + "PRINT NetConfigSet(c, \"hostname\", \"basic-pc\"); NetConfigStore(c, p)\n" + "PRINT PrefsGet$(p, \"network\", \"hostname\", \"?\")\n" + "PRINT NetVectorCount(); NetVector(1)\n" + "PRINT NetStatus$(\"up\")\n" + "NetConfigFree c\n" + "PrefsClose p\n", out, sizeof(out))); + TEST_STR_EQ(out, "DVX0 \n-1 -1 \n-1 True \n-1 -1 \nbasic-pc\n34 96 \n0\n"); + } + + // An app's handles are freed when it ends + dvxMemAppIdPtr = &app; + cfg = NetConfigLoad(0); + dvxMemAppIdPtr = saved; + other = NetConfigLoad(0); + TEST_STR_EQ(NetConfigGet(cfg, "hostname"), "DVX"); + netCloseAppSockets(TEST_APP_ID); + TEST_STR_EQ(NetConfigGet(cfg, "hostname"), ""); + TEST_STR_EQ(NetConfigGet(other, "hostname"), "DVX"); + NetConfigFree(other); +} + + static void caseErrors(void) { int fd = socket(AF_INET, SOCK_STREAM, 0); int other; @@ -1098,6 +1328,48 @@ static void caseWire(void) { TEST_TRUE(memcmp(buf, "static", 6) == 0); close(fd); + // The same through net.bas and prefs.bas: change the address, restart + { + int32_t prefs = PrefsLoad(DVX_INI_PATH); + int32_t cfg = NetConfigLoad(prefs); + + TEST_STR_EQ(NetConfigGet(cfg, "address"), "10.0.2.99"); + TEST_EQ(NetConfigSet(cfg, "address", "10.0.2.98"), -1); + TEST_EQ(NetConfigChanged(cfg), -1); + TEST_EQ(NetConfigStore(cfg, prefs), -1); + TEST_EQ(PrefsSave(prefs), -1); + NetConfigFree(cfg); + PrefsClose(prefs); + } + + TEST_EQ(NetRestart(), -1); + TEST_STR_EQ(NetStatus("up"), "-1"); + TEST_STR_EQ(NetStatus("link"), "-1"); + TEST_STR_EQ(NetStatus("dhcp"), "0"); + TEST_STR_EQ(NetStatus("address"), "10.0.2.98"); + TEST_STR_EQ(NetStatus("Netmask"), SUBNET_MASK); + TEST_STR_EQ(NetStatus("gateway"), PEER_ADDR); + TEST_STR_EQ(NetStatus("dns1"), PEER_ADDR); + TEST_STR_EQ(NetStatus("hostname"), TEST_HOSTNAME); + TEST_STR_EQ(NetStatus("mac"), "02:00:00:00:00:15"); + TEST_STR_EQ(NetStatus("driver"), "FAKE NE2000"); + TEST_STR_EQ(NetStatus("message"), netStateText(NetUpE)); + TEST_STR_EQ(NetStatus("sockets"), "0"); + TEST_TRUE(atoi(NetStatus("vector")) >= 0); + TEST_TRUE(atoi(NetStatus("txpackets")) >= 0 && atoi(NetStatus("rxpackets")) >= 0); + TEST_TRUE(atoi(NetStatus("txbytes")) >= 0 && atoi(NetStatus("rxbytes")) >= 0); + fd = connectTo(PEER_ADDR, ECHO_PORT); + TEST_TRUE(fd >= 0); + TEST_EQ(send(fd, "basic", 5, 0), 5); + TEST_EQ(recv(fd, buf, 5, MSG_WAITALL), 5); + TEST_TRUE(memcmp(buf, "basic", 5) == 0); + close(fd); + + // NetStop: down until NetRestart + NetStop(); + TEST_STR_EQ(NetStatus("up"), "0"); + TEST_STR_EQ(NetStatus("message"), netStateText(NetStoppedE)); + kill(peer, SIGKILL); waitpid(peer, NULL, 0); netStop(); @@ -1396,6 +1668,71 @@ static void peerMain(int wire) { } +// TcpRecv's bytes as a C string (the tests send text there). +static const char *recvText(int32_t handle) { + static char text[BAS_OUT_MAX]; + int32_t len = 0; + const uint8_t *bytes = TcpRecv(handle, &len); + + len = DVX_MIN(len, (int32_t)sizeof(text) - 1); + memcpy(text, bytes, (size_t)len); + text[len] = '\0'; + return text; +} + + +// Compile prefs.bas + net.bas + program and run it with the library +// calls made for real; its PRINT output lands in out. +static bool runBasic(const char *program, char *out, int32_t outSize) { + int32_t prefsLen = 0; + int32_t netLen = 0; + char *prefsBas = platformReadFile(BASIC_INCLUDE_DIR "/prefs.bas", &prefsLen); + char *netBas = platformReadFile(BASIC_INCLUDE_DIR "/net.bas", &netLen); + size_t srcSize = (size_t)prefsLen + (size_t)netLen + strlen(program) + BAS_SRC_JOINS; + char *src = malloc(srcSize); + BasCompilerT *comp = NULL; + BasModuleT *mod = NULL; + BasVmT *vm = NULL; + bool ok = false; + + out[0] = '\0'; + + if (prefsBas && netBas && src && outSize >= BAS_OUT_MAX) { + snprintf(src, srcSize, "%s\n%s\n%s", prefsBas, netBas, program); + comp = basCompilerCreate(src, (int32_t)strlen(src)); + + if (basCompile(comp)) { + mod = basCompilerBuildModule(comp); + } else { + printf(" compile error: %s\n", basCompilerError(comp)); + } + } + + if (mod) { + BasExternCallbacksT ext = { basExternResolve, basExternCall, NULL }; + + vm = basVmCreate(); + basVmLoadModule(vm, mod); + basVmSetPrintCallback(vm, basOut, out); + basVmSetExternCallbacks(vm, &ext); + basNativeHostSetLive(true); + + BasVmResultE r = basVmRun(vm); + + basNativeHostSetLive(false); + ok = (r == BAS_VM_HALTED || r == BAS_VM_OK); + basVmDestroy(vm); + basModuleFree(mod); + } + + basCompilerDestroy(comp); + free(src); + free(prefsBas); + free(netBas); + return ok; +} + + static int udpOn(uint16_t port) { int fd = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in sa; @@ -1456,6 +1793,7 @@ int main(void) { caseRing(); caseConfig(); + caseConfigBasic(); caseInet(); caseTcp(); caseNonBlocking(); diff --git a/src/test/print/Makefile b/src/test/print/Makefile index 42259bea..f9f2635a 100644 --- a/src/test/print/Makefile +++ b/src/test/print/Makefile @@ -36,7 +36,7 @@ PRINT_SRCS = $(wildcard $(PRINT_DIR)/*.c) PRINT_OBJS = $(patsubst $(PRINT_DIR)/%.c,$(BINDIR)/print/%.o,$(PRINT_SRCS)) LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) -CFLAGS = $(DVXTEST_CFLAGS) -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(PRINT_DIR) -I$(DVXTEST_BASICDIR) -DPRINT_GOLDEN_DIR='"$(abspath golden)"' +CFLAGS = $(DVXTEST_CFLAGS) -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(PRINT_DIR) -I$(DVXTEST_BASICDIR) -DPRINT_GOLDEN_DIR='"$(abspath golden)"' -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"' .PHONY: all run clean diff --git a/src/test/print/printTest.c b/src/test/print/printTest.c index 48cce80b..885c07f7 100644 --- a/src/test/print/printTest.c +++ b/src/test/print/printTest.c @@ -33,6 +33,9 @@ // golden byte streams, plus decoded checks // BASIC LPRINT, LPRINT USING and OPEN "LPT1:" / PRINT # / CLOSE run // through the VM into print jobs; the errors 54, 57 and 68 +// device the printer model / destination / file settings shared by +// the Control Panel and print.bas, in C and from a BASIC +// program that includes print.bas and prefs.bas // dialogs the Print and Page Setup dialogs, the Retry / Cancel box // and the Printing window's Cancel, driven in the test GUI // @@ -45,6 +48,7 @@ #include "compiler/compiler.h" #include "runtime/vm.h" #include "runtime/basErrors.h" +#include "formrt/formrt.h" #include "checkbox/checkbox.h" #include "dropdown/dropdown.h" #include "radio/radio.h" @@ -75,7 +79,17 @@ #define PIC_H 32 #define CAPTURE_MAX 1024 #define MODE_DIR 0755 +#define PAGES_TEXT_LINES 50 +#define PAGES_TEXT_MAX 1024 +#define PAGES_LINES 20 +#define PAGES_FILE "pages.prn" +#define PAGES_SRC_MAX 65536 +#define FORM_FEED 0x0C #define ESC_CHAR 0x1B +#define DEVICE_COLUMNS 100 +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAD_HANDLE 99 // A scripted printer port typedef struct { @@ -134,6 +148,8 @@ static void addExpectPage(char *buf, int32_t *len, const char *header, static void captureOut(void *ctx, const char *text, bool newline); static void caseBasic(void); static void caseBasicErrors(void); +static void caseBasicPages(void); +static void caseDevice(void); static void caseDialogs(void); static void caseFileErrors(void); static void caseHeaderCodes(void); @@ -145,6 +161,7 @@ static void caseText(void); static void caseTextOptions(void); static bool checkGolden(const char *name, const char *path); static BasModuleT *compile(const char *src); +static int32_t countFormFeeds(const char *path); static bool fakePresent(void *ctx, int32_t port); static uint8_t fakeStatus(void *ctx, int32_t port); static uint8_t fakeWrite(void *ctx, int32_t port, uint8_t byte); @@ -167,6 +184,36 @@ static void *vmOpen(void *ctx, int32_t port); static bool vmWrite(void *ctx, void *job, const char *data, int32_t len); static void writePicture(const PrintDriverT *drv, const uint8_t *rgb, int32_t w, int32_t h, const char *path); +// print.bas / prefs.bas entry points (printBas.c, dvxPrefsBas.c) +void PrefsClose(int32_t handle); +int32_t PrefsCreate(void); +int32_t PrintChoiceCopies(void); +int32_t PrintChoiceDest(void); +const char *PrintChoiceFile(void); +int32_t PrintChoiceFirst(void); +int32_t PrintChoiceLast(void); +int32_t PrintChoose(const char *title, int32_t pageCount); +int32_t PrintDestCount(void); +int32_t PrintDestIsFile(int32_t dest); +const char *PrintDestLabel(int32_t dest); +int32_t PrintModelCount(void); +const char *PrintModelLabel(int32_t model); +int32_t PrintPageCount(const char *text); +int32_t PrintPageLines(int32_t prefs, int32_t model); +int32_t PrintPageSetting(int32_t prefs, const char *field); +int32_t PrintPageSettingsStore(int32_t prefs, int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer); +const char *PrintPageSetupProblem(int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer); +const char *PrintPageText(int32_t prefs, const char *field); +int32_t PrintPortPresent(int32_t port); +int32_t PrintPortStatus(int32_t port); +int32_t PrintSettingDest(int32_t prefs); +const char *PrintSettingFile(int32_t prefs); +int32_t PrintSettingModel(int32_t prefs); +const char *PrintSettingsProblem(int32_t dest, const char *filePath); +int32_t PrintSettingsStore(int32_t prefs, int32_t model, int32_t dest, const char *filePath); +const char *PrintStatusProblem(int32_t status, int32_t afterWrite, const char *where); +int32_t PrintTextPages(const char *text, const char *title, int32_t firstPage, int32_t lastPage, int32_t copies); + static const PrintPortOpsT sFakeOps = { fakePresent, fakeStatus, fakeWrite, NULL }; @@ -306,6 +353,239 @@ static void caseBasicErrors(void) { } +// The printer model / destination / file the Control Panel edits: the +// shared C functions, the print.bas entry points, and a BASIC program +// using the real print.bas and prefs.bas include files. +// The round 2 print.bas calls for real: page settings through a prefs.bas +// handle (also from a BASIC program, which checks the eleven-argument +// call), page counts, PrintTextPages into a file (page range, copies), +// PrintChoose, the ports through the fake port layer and the status words. +static void caseBasicPages(void) { + tCase("print.bas pages, page setup, ports, Print dialog"); + + PrintSettingsT s; + DlgScriptT ds; + char text[PAGES_TEXT_MAX]; + char msg[PRINT_MSG_MAX]; + int32_t used = 0; + int32_t pages; + int32_t prefs; + + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + printBasSetContext(tCtx()); + mkdir("CONFIG", MODE_DIR); + unlink(DVX_INI_PATH); + + for (int32_t i = 0; i < PAGES_TEXT_LINES; i++) { + used += snprintf(text + used, sizeof(text) - (size_t)used, "line %ld\n", (long)i); + } + + // Page settings through a prefs.bas handle + prefs = PrefsCreate(); + TEST_EQ(PrintPageSettingsStore(prefs, 30, 70, 2, 2, 4, 4, 4, BAS_TRUE, "H &f", "F &p"), BAS_TRUE); + TEST_EQ(PrintPageSetting(prefs, "Lines"), 30); + TEST_EQ(PrintPageSetting(prefs, "columns"), 70); + TEST_EQ(PrintPageSetting(prefs, "MarginLeft"), 4); + TEST_EQ(PrintPageSetting(prefs, "TabWidth"), 4); + TEST_EQ(PrintPageSetting(prefs, "Wrap"), BAS_TRUE); + TEST_EQ(PrintPageSetting(prefs, "Nope"), -1); + TEST_STR_EQ(PrintPageText(prefs, "Header"), "H &f"); + TEST_STR_EQ(PrintPageText(prefs, "footer"), "F &p"); + TEST_STR_EQ(PrintPageText(prefs, "Nope"), ""); + TEST_TRUE(PrintPageSetupProblem(PRINT_LINES_MIN - 1, 70, 2, 2, 4, 4, 4, 0, "", "")[0] != '\0'); + TEST_STR_EQ(PrintPageSetupProblem(30, 70, 2, 2, 4, 4, 4, 0, "", ""), ""); + TEST_EQ(PrintPageSettingsStore(prefs, PRINT_LINES_MIN - 1, 70, 2, 2, 4, 4, 4, 0, "", ""), BAS_FALSE); + TEST_EQ(PrintPageSetting(prefs, "Lines"), 30); + TEST_EQ(PrintPageLines(prefs, PrintModelTextE), 30); + TEST_EQ(PrintPageLines(prefs, PrintModelCountE), -1); + TEST_EQ(PrintPageSettingsStore(prefs, 0, 70, 2, 2, 4, 4, 4, 0, "", ""), BAS_TRUE); + TEST_EQ(PrintPageLines(prefs, PrintModelLaserJetE), PRINT_LINES_LASER); + TEST_EQ(PrintPageSettingsStore(BAD_HANDLE, 30, 70, 2, 2, 4, 4, 4, 0, "", ""), BAS_FALSE); + PrefsClose(prefs); + + // Page count and PrintTextPages with DVX.INI: a file, 20 lines a page + printSettingsDefaults(&s); + s.dest = PrintDestFileE; + s.page.linesPerPage = PAGES_LINES; + snprintf(s.filePath, sizeof(s.filePath), "%s", PAGES_FILE); + TEST_TRUE(printSettingsSave(&s)); + pages = printTextPageCount(text, used, &s.page, s.model); + TEST_TRUE(pages > 2); + TEST_EQ(PrintPageCount(text), pages); + TEST_EQ(PrintPageCount(""), 1); + TEST_EQ(PrintTextPages(text, "doc", 2, 2, 1), BAS_TRUE); + TEST_EQ(countFormFeeds(PAGES_FILE), 1); + TEST_EQ(PrintTextPages(text, "doc", 0, 0, 2), BAS_TRUE); + TEST_EQ(countFormFeeds(PAGES_FILE), pages * 2); + + // PrintChoose: the dialog script picks a file, 2 copies, pages 2-3 + memset(&ds, 0, sizeof(ds)); + snprintf(ds.file, sizeof(ds.file), "%s", "choose.prn"); + tDialogScript(printDlgScript, &ds); + TEST_EQ(PrintChoose("Print", 4), BAS_TRUE); + tDialogScript(NULL, NULL); + TEST_EQ(PrintChoiceDest(), PrintDestFileE); + TEST_STR_EQ(PrintChoiceFile(), "choose.prn"); + TEST_EQ(PrintChoiceCopies(), 2); + TEST_EQ(PrintChoiceFirst(), 2); + TEST_EQ(PrintChoiceLast(), 3); + + // Ports (1-3) through the fake port layer, and the status words + resetFake(); + sFake.missing[2] = true; + sFake.status[0] = PAPER_OUT; + sFake.statusCount = 1; + printSetPortOps(&sFakeOps); + TEST_EQ(PrintPortPresent(1), BAS_TRUE); + TEST_EQ(PrintPortPresent(3), BAS_FALSE); + TEST_EQ(PrintPortPresent(0), BAS_FALSE); + TEST_EQ(PrintPortPresent(PRINT_PORT_COUNT + 1), BAS_FALSE); + TEST_EQ(PrintPortStatus(1), PAPER_OUT); + TEST_EQ(PrintPortStatus(2), READY); + TEST_EQ(PrintPortStatus(PRINT_PORT_COUNT + 1), 0); + printSetPortOps(NULL); + TEST_TRUE(printStatusProblem(PAPER_OUT, false, "LPT1", msg, sizeof(msg))); + TEST_STR_EQ(PrintStatusProblem(PAPER_OUT, BAS_FALSE, "LPT1"), msg); + TEST_STR_EQ(PrintStatusProblem(READY, BAS_FALSE, "LPT1"), ""); + + // The same page settings calls from a BASIC program + int32_t prefsLen = 0; + int32_t printLen = 0; + char *prefsBas = platformReadFile(BASIC_INCLUDE_DIR "/prefs.bas", &prefsLen); + char *printBas = platformReadFile(BASIC_INCLUDE_DIR "/print.bas", &printLen); + char *src = (char *)malloc(PAGES_SRC_MAX); + + TEST_TRUE(prefsBas && printBas && src); + + if (prefsBas && printBas && src) { + snprintf(src, PAGES_SRC_MAX, "%s\n%s\n" + "DIM p AS LONG\n" + "p = PrefsCreate()\n" + "PRINT PrintPageSettingsStore(p, 40, 60, 1, 2, 3, 4, 5, True, \"Head\", \"Foot\")\n" + "PRINT PrintPageSetting(p, \"Lines\"); PrintPageSetting(p, \"MarginBottom\"); PrintPageSetting(p, \"MarginRight\"); PrintPageSetting(p, \"TabWidth\"); PrintPageText$(p, \"Footer\")\n" + "PRINT PrintPageSetupProblem$(40, 60, 1, 2, 3, 4, 5, 0, \"\", \"\") = \"\"; PrintPageCount(\"x\"); PrintPortPresent(9)\n" + "PrefsClose p\n", prefsBas, printBas); + basNativeHostSetLive(true); + + BasVmResultE rr = runProgram(src, NULL, false); + + basNativeHostSetLive(false); + TEST_TRUE(rr == BAS_VM_HALTED || rr == BAS_VM_OK); + TEST_STR_EQ(sCapture, "-1 \n40 2 4 5 Foot\nTrue 1 0 \n"); + } + + free(src); + free(prefsBas); + free(printBas); + printBasSetContext(NULL); + unlink(PAGES_FILE); + unlink("choose.prn"); + unlink(DVX_INI_PATH); + tShutdown(); +} + + +static void caseDevice(void) { + tCase("device settings"); + + PrintSettingsT s; + PrintSettingsT r; + PrefsHandleT *h; + int32_t prefs; + + // DVX.INI holds a page setup the store must keep + mkdir("CONFIG", MODE_DIR); + printSettingsDefaults(&s); + s.page.columns = DEVICE_COLUMNS; + TEST_TRUE(printSettingsSave(&s)); + + TEST_TRUE(printDeviceProblem(PrintDestFileE, "") != NULL); + TEST_TRUE(printDeviceProblem(PrintDestFileE, NULL) != NULL); + TEST_TRUE(printDeviceProblem(PrintDestFileE, "X.PRN") == NULL); + TEST_TRUE(printDeviceProblem(PrintDestLpt2E, "") == NULL); + + h = prefsCreate(); + printDeviceStore(h, PrintModelLaserJetE, PrintDestFileE, "A.PRN"); + printSettingsRead(h, &r); + TEST_EQ(r.model, PrintModelLaserJetE); + TEST_EQ(r.dest, PrintDestFileE); + TEST_STR_EQ(r.filePath, "A.PRN"); + TEST_EQ(r.page.columns, DEVICE_COLUMNS); + printDeviceStore(h, PrintModelTextE, PrintDestLpt3E, NULL); + printSettingsRead(h, &r); + TEST_EQ(r.dest, PrintDestLpt3E); + TEST_STR_EQ(r.filePath, ""); + prefsClose(h); + + // The BASIC entry points + TEST_EQ(PrintModelCount(), PrintModelCountE); + TEST_EQ(PrintDestCount(), PrintDestCountE); + TEST_STR_EQ(PrintModelLabel(PrintModelEpson9E), printModelLabel(PrintModelEpson9E)); + TEST_STR_EQ(PrintModelLabel(PrintModelCountE), ""); + TEST_STR_EQ(PrintModelLabel(-1), ""); + TEST_STR_EQ(PrintDestLabel(PrintDestLpt1E), "LPT1"); + TEST_STR_EQ(PrintDestLabel(PrintDestCountE), ""); + TEST_EQ(PrintDestIsFile(PrintDestFileE), BAS_TRUE); + TEST_EQ(PrintDestIsFile(PrintDestLpt1E), BAS_FALSE); + TEST_STR_EQ(PrintSettingsProblem(PrintDestFileE, ""), printDeviceProblem(PrintDestFileE, "")); + TEST_STR_EQ(PrintSettingsProblem(PrintDestFileE, "B.PRN"), ""); + TEST_TRUE(PrintSettingsProblem(-1, "B.PRN")[0] != '\0'); + + prefs = PrefsCreate(); + TEST_EQ(PrintSettingModel(prefs), PrintModelTextE); + TEST_EQ(PrintSettingDest(prefs), PrintDestLpt1E); + TEST_STR_EQ(PrintSettingFile(0), PRINT_FILE_DEFAULT); + TEST_EQ(PrintSettingsStore(prefs, PrintModelEpson24E, PrintDestFileE, "C.PRN"), BAS_TRUE); + TEST_EQ(PrintSettingModel(prefs), PrintModelEpson24E); + TEST_EQ(PrintSettingDest(prefs), PrintDestFileE); + TEST_STR_EQ(PrintSettingFile(prefs), "C.PRN"); + TEST_EQ(PrintSettingsStore(BAD_HANDLE, PrintModelTextE, PrintDestLpt1E, ""), BAS_FALSE); + TEST_EQ(PrintSettingsStore(prefs, PrintModelCountE, PrintDestLpt1E, ""), BAS_FALSE); + TEST_EQ(PrintSettingsStore(prefs, PrintModelTextE, -1, ""), BAS_FALSE); + TEST_EQ(PrintSettingModel(prefs), PrintModelEpson24E); + PrefsClose(prefs); + + // A BASIC program with the real include files, calls made for real + int32_t prefsLen = 0; + int32_t printLen = 0; + char *prefsBas = platformReadFile(BASIC_INCLUDE_DIR "/prefs.bas", &prefsLen); + char *printBas = platformReadFile(BASIC_INCLUDE_DIR "/print.bas", &printLen); + char src[EXPECT_MAX * 4]; + + TEST_TRUE(prefsBas && printBas); + + if (prefsBas && printBas) { + snprintf(src, sizeof(src), "%s\n%s\n" + "DIM p AS LONG\n" + "DIM i AS LONG\n" + "p = PrefsCreate()\n" + "FOR i = 0 TO PrintModelCount() - 1\n" + " IF PrintModelLabel$(i) = \"\" THEN PRINT \"empty\"\n" + "NEXT i\n" + "PRINT PrintDestCount(); PrintDestLabel$(0); PrintDestIsFile(3)\n" + "PRINT \"[\" & PrintSettingsProblem$(3, \"\") & \"]\"\n" + "PRINT PrintSettingsStore(p, 1, 3, \"B.PRN\")\n" + "PRINT PrintSettingModel(p); PrintSettingDest(p); PrintSettingFile$(p)\n" + "PrefsClose p\n", prefsBas, printBas); + + basNativeHostSetLive(true); + BasVmResultE rr = runProgram(src, NULL, false); + basNativeHostSetLive(false); + + TEST_TRUE(rr == BAS_VM_HALTED || rr == BAS_VM_OK); + + char expect[CAPTURE_MAX]; + + snprintf(expect, sizeof(expect), "4 LPT1-1 \n[%s]\n-1 \n1 3 B.PRN\n", printDeviceProblem(PrintDestFileE, "")); + TEST_STR_EQ(sCapture, expect); + } + + free(prefsBas); + free(printBas); + unlink(DVX_INI_PATH); +} + + static void caseDialogs(void) { tCase("dialogs"); @@ -982,6 +1262,26 @@ static BasModuleT *compile(const char *src) { } +// How many form feeds (page ends) a printed file holds; -1 when it +// cannot be read. +static int32_t countFormFeeds(const char *path) { + FILE *f = fopen(path, "rb"); + int32_t count = 0; + int ch; + + if (!f) { + return -1; + } + + while ((ch = fgetc(f)) != EOF) { + count += (ch == FORM_FEED) ? 1 : 0; + } + + fclose(f); + return count; +} + + static bool fakePresent(void *ctx, int32_t port) { (void)ctx; return !sFake.missing[port]; @@ -1051,10 +1351,12 @@ static PrintJobT *fileJob(const char *path, HookT *h) { static PrintAnswerE hookAsk(void *ctx, const char *message) { - HookT *h = (HookT *)ctx; + HookT *h = (HookT *)ctx; + PrintAnswerE answer = h->asks < h->answerCount ? h->answers[h->asks] : PrintAnswerCancelE; snprintf(h->lastMessage, sizeof(h->lastMessage), "%s", message); - return h->asks < h->answerCount ? h->answers[h->asks++] : (h->asks++, PrintAnswerCancelE); + h->asks++; + return answer; } @@ -1197,6 +1499,11 @@ static BasVmResultE runProgram(const char *src, VmPrinterT *vp, bool withPrinter basVmLoadModule(vm, mod); basVmSetPrintCallback(vm, captureOut, NULL); + // DECLARE LIBRARY calls (made for real only under basNativeHostSetLive) + BasExternCallbacksT ext = { basExternResolve, basExternCall, NULL }; + + basVmSetExternCallbacks(vm, &ext); + if (withPrinter) { cb.open = vmOpen; cb.write = vmWrite; @@ -1283,6 +1590,8 @@ int main(void) { casePictures(); caseBasic(); caseBasicErrors(); + caseBasicPages(); + caseDevice(); caseDialogs(); return tSuiteEnd(); diff --git a/src/test/samples/golden/basdemo-debug.app b/src/test/samples/golden/basdemo-debug.app index 5553e31baa99f6482e0314581c565d65e6ec6382..9df54dec8a7a6716c80ec1736941cf5344de86c1 100644 GIT binary patch delta 9765 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 diff --git a/src/test/samples/golden/basdemo-release.app b/src/test/samples/golden/basdemo-release.app index 9ae5ef2487d8857a2be2df9c8c957742d92315ae..37c76ed068cf9837dbe92879b14285e05e2c9927 100644 GIT binary patch delta 7213 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 diff --git a/src/test/samples/golden/clock-debug.app b/src/test/samples/golden/clock-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..3625c5f2b4427fcdea03fa79d7c9475a617c33e4 GIT binary patch literal 9777 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 P!nu=Nv5GNFiI@KYirD-3 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/clock-release.app b/src/test/samples/golden/clock-release.app new file mode 100644 index 0000000000000000000000000000000000000000..9fe3a9aaa8d5ceee770a9fe86760162be6d50529 GIT binary patch literal 9376 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 J_&6o`{{>ZCiv$1w literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/fileMgr-debug.app b/src/test/samples/golden/fileMgr-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..19b93bc3b94cc3728d373d00fe9b651ba84ae88e GIT binary patch literal 34279 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 LJNM|NdA|P-%WN~- literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/fileMgr-release.app b/src/test/samples/golden/fileMgr-release.app new file mode 100644 index 0000000000000000000000000000000000000000..8d2ba106ce36e8bf38ccf46ee64ced072edc21b2 GIT binary patch literal 29110 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 ZyI%xtzG~C literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/helpedit-debug.app b/src/test/samples/golden/helpedit-debug.app index 6237b5d5e8e708ad8b16f639a317df67f46b6446..4731726bbab489a64c6a77992f30ad7b79614279 100644 GIT binary patch literal 11918 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 diff --git a/src/test/samples/golden/helpedit-release.app b/src/test/samples/golden/helpedit-release.app index 2626515f28420a989b0ab3b882a682d38f52fe4e..79ecdc0fb29a20d8246e24806730511ee698c46c 100644 GIT binary patch literal 10786 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 M4r1HnI)~@~017>g6aWAK literal 11611 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 diff --git a/src/test/samples/golden/iconed-release.app b/src/test/samples/golden/iconed-release.app index 224376573455d8c2e775af9471526488f6db48c1..2a045294c51625eea15cf5c4217aa36a1b67ced2 100644 GIT binary patch delta 4578 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 delta 4899 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 DKe6*+ diff --git a/src/test/samples/golden/imgview-debug.app b/src/test/samples/golden/imgview-debug.app index a4665b040ebdfaaf3eaab4a46dc62761db5f05e4..c7084f54bcd224ee5b97b433af84bec15a90a918 100644 GIT binary patch literal 9253 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 V{|NBkp@HDQmJ?dJkRH+E_zxN$jm`i7 literal 9620 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 diff --git a/src/test/samples/golden/imgview-release.app b/src/test/samples/golden/imgview-release.app index 639182a403bd4d690191080e209a245dfe18f911..f7ca43269f82f04fc6522ca949a1c28ca3aa8be8 100644 GIT binary patch literal 8834 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 literal 9201 zcmeHNe{dAl9e;^AxP-s~22te4ijV?o%mu_U4VC0_2~-1{J8}UL)63l^cj4~#dbh`~ zS{uwoxg5u&tteBmYPB<&TAeyV#kOiBmeg@{O6jySYK7_0x|vX`t-pXO{e0isyIg+J z3jCvW=f1n|eSg32`@Y?`-@U4JcI(58v9mZ9;wl4j0DqduSf0_Z!Ih4A1>!o_Xg_@k zV_o@-#Q`KwM=9Ek?r^wDiFzXbu%<@X&%sV8r!S^?)h!{`W(K0k9j#Tpv4C=m>h?O6 zDC>e*VykugL&6T7dj}?VWgqp4zqX$&Oxn$+%c12kW4<{nU+D1NhK8(vlUtL>T36zvwP#~*b65Y(LjP)2@~FYEKcIM}ARnjH ztj0oK$sdw5pFb*LqyfLjt@+hZ3A0Bc2vo=&RM`BIMN#Hc0^w%JviZ^5IHGG!Oq0}R zDX0Y1$TrCnQ6L#=fht9+tZ=TfNs(Ac^9Pj@h>(8V*a5;CJ>_!{2kYtZifYEC2G0XbuPeuGYe279v zk0pN8#8_X)9q5s8inp9{@&M6&CxLS5GYpwt%+d-{gy~(GSEuI+Jg~C~wTG0n_vASp zzV{&4x>H5(BcR5LrhJ zA310$8jpm^Bb46G1&W;d_O#|WX?D3%_PCsUjt0=6zlGsZzIAxfrSUQ#^p%iE9H#CJ zbY(a~WZ9)(4?=Pr1c>It@^Yk}e{6gXYh^yJTc5;j6`~&|Tl%N}rV&zq1@FIuKAcVM zCAU*|?=$5vr>_ic!|B&Jzm&S~;c-N{Ax&{4_GoZHe-d=3@1``mL4S}76eT&?q3=dF zQ9e(<8`t$Pw82z_H4e{XK=j$~B`^EzyTql7E-Cx2##_O#mMbburf8_iS(FrYE~mbn zqY)rlD>UkP7&R2rjBd?q&}RucCS?xh7Lg)8J|a^$yrfw^eTL^$Uq=`uGMPAV8(PM) zO+HBIlzp$$_bn2bEbA|Gfx?u1XPCY2Ud*cIw|V zC>;T4B~P*V+&upEc%Pk?0Bid?v&0X;a^j<|lvd0zum-&b zni74O=S~*yvFtfiUSQeNTRzdU=f(IDOMDr)<8N5vR$Q%b4CHgU@&c_W@s_PKZw^Vk zNX&=gM`C*W8IxtV1)PpnlW!ZY4SN4+9uRogLAl*oV=9+4b9ug{<85?*iIigl6H}Er z%l>Ie{TUL8L!Eik!E&-ZU(-`drY}4yvZ4JN~UKqlWd^Sn1_mGuiQDIGj9^?ybn9?<{Zg6)tS?M%o6uPv(Pdy zr@5=#{N1bCPv2yT*8{z?YFc+?XWkTIt3|)O;Lc;^6Jy7Oe@Sj8Rb}eNZNAgqJ#anl z+wtFPljCn_g`G7yHSr!Tue>1kKEiu%&wR>_oW8c3n5AP2ZXFN+eKzI~Sw$0w`R_Io zOxAyLipP-BP!RPW2u$)h%BPwHza)9iEvE;frQ`dwHi@5>ev;>)tZ#%=a-&(~?W6P% z>TBO?W^6Kr>4AM{-J0lK--`RmVsIvA&eN|U3A?$^M9g&bwUK~mkpLaUm`egn=IN(D zK6`dU@&K(Xm;QHjkXI??eQVE#+Z*wiTQWXzsDB{_jN$bk<=40SU%@r;yHu<@=iUAn z(1zbG{VB+{vgC4Fo3ih3FoX5a^%c4Fd(l{D+^F<>M27|{>)SaCnLLO%Fm5%i#&-}H zLzP4D4g3*AxI)6VCtAhR*=L4c&M4~>LF=dNO4C*j(%$=s z2c%yH83S_a$ALgF`d4Cv={-X2nyUI+>{3LDsT?I)!^`*94HC1YmU7r{)q^xK68xP7%WBzhZ?_T#vZ ztRg>W&P(*^Hwp5=QZRR%)mHY|PmqF?{Y0JY+r~A_6fFF?G+j>CpKv%E^zlN2e)hxk zY$rbEac}NA*Z?u?L9AyJxtJ0IpB{Zz7p;D=A*cw`Gvs^@nr7Msk}f*N0UDbliozD) zet8$b57bOJ@*Ln307=k}j?A^STdr_%wW&5}y;egnW!ijXAu%|IL?p`?H)@YwpyUFX1I22DT67_FZSO^w3 zXIT}F(vG$w5TH(!y*xYsgAP*!qO5y_d^!xL)1q+Lxyc`Hh_VB?>C&_`0)Kd&S@w;mgO0b zr%eq8-61c#CyOA4?1+V&{$Ln8)+2B_?P0AFjk;SDigP2T^QVBrfPVlcr2W87HV{xf zY!kc}UTBe!#~oEX!7#fNnnC~wu*PU8O!L{yW`b`UpagEL1~dY4A-))po?rxLO;`!B zCxPI&Om^C6@2s#FNQl&eASS+=GTZ`FF+f^|kGo2>Z&pHDG?Pk`Ae`n90dVr1$-Lq` z1hbRs6e2Y3R0 zJ_C3i;DoR10Y3yh1b7VaD?kt67~lk;A8-c1Fx4giJ?4G`cpWeW&xTb&dr4BK{ViCY6eRn<8k<+>L2*+J(-`pOa5Pe8Gg|a^Eg&~hB5zF{loat z{Y?77e-0BPm7jK@QA; zd)YXX#9kw-pyhHOu7czB^=C~C@97b30;>wNRHLLvFZY>_)4f^Yopf1e3b7fLrjQ3! z4aJ*JD4S39d(y0;6lLakHK0Z&QcI5DtDPnkQo#u&5Bi8_X4Ii~RTJY?2n*xIrM1dd ztqMg&wIzQLF;z5WzekHj6hST=f}3RY@B)mISS&nE^lk1C*J?;cDS9+eiN7kBiZ|w{ zv|v$5Q!xCsMJ3@-3(l=!I=A?n7v6!YlSI+sJc|}y!fe*E@yt+{i{5x3`iLiSETEtX~Mo+LfMT5BQ^UYQLSVG^9Nh#jk?4a2p8i^le#4; z4Sn;@ym%Xu(UMu`esjmNX;Sf(GrKHcd~oLZGqa~Gnn3-*HqYWQe zHxZ9mgu{7qS+$j@FJ%j6+R$*rpoaXR>J2XN+&IWXUui}jg|JZ8Sk)BBcr$%eKImFD zsB5mlGn6$Sx~zjtM5FjeLdNE&fqv~0j%{Pjk8?))fyqqVD4R`3PLm25^GIU(ahR|&)LWp|f zNXM;x+!rGL{YEKLY+a2EX5toC&b!C% z9)XrWF7a9FYI~Ix5d}p^byB3Fg!G5nv_x{115+PJ{6QibMJiHgMWm&68!4^YLesk6 z%+LwxX5Gin zk9zv4g@jyPOvq&jji)USeV>%ghGjEls98%l$R)VbuDFu3()u$Rxzvn~e#wmJ>0CzM zsY~gYY!VT))GZ>Z8t0+M$?2tIl`ZiyWwSr9g_geS?}M<~ZhtT`)9MH<&5yPaGDV1z z2a3p|MEA!C7&*7hp6Qz`{$dTqfB8MYjh3vogoo+CrFf9EmnO*$Et|85=UALrOa0+C zGSkAMoW!!x($qp?eIXikBFm#ONh(q%0hzG4@nZI2sp~R?JY+9pXOrB&xQB2BO^EO;I=I$670B zSK{bMsVN>)9zxL2R?*$a%psWK?pp3z7LHN)qHwH7R32gAO9POH;W^d(8zi?tbpQ4* zmRDIg&I67kn^i2}3b(LqHpOD16FH8YqRli{5t{O4D9y{Ua+N09<-W!OmdD*!e9Uo* zDT+m5Tui!qF>Ja*Mh{`0XdU)rk@Ha&`_4OCo9y>RcMzd(VtEn!YN0Bb?kSM(s%6J^wLZ+v3CGtNWH;%=T%2hc zDdzTb@S^-iLd_goVz;b~47FKSkl{$V{j#T*a`7lWP%`vv?;Y-3K`} zo-Bhs_GFJETovQ)jT>w>?XvqJG?d_U9?qk)R(DyrMgfY#wWz3EUL@H-=+YkEB`VF8P@bW6&g9@Xnc(qekDgUiWET*aEq(A1dx>@GhscEA8N7M%j`nAeECL)@SYEN5)~%Zs%$A`a zCPn0=fc2AXF?momE!pU@GUOcc@~vL5Z{B7Mlc!*&b=-$^@TQN+Qkpyu{|_M{kiH1} zO~{`?vY}#F9#oehOO{M-z|$uQ%l)~*%Jc<43>1lMn))9ABO8&4_hV+eY|#mP6$md> z0P!gRQCuC);teWdtUd|1`7^T;a4@n#L}b&H24z~+E9hH@yw^bX`b9#*nT(zyn=mLA zV`^DL)vN(>0pL;aAA#jMgmLk*?9R#>(a^z?4kl?ET?j7$WuuQCteiNTz2Iln+sMw( zJvod4^Ir`+5C<-fGB`kT)S0A#X$8h5Q}zF=Pc23PGZf zB*cY$8k74NM1tT)(=h3bJkg3A(4K&-gM>U;KL`b}s&1Q_KYiR89Q5pr{F62mg*J<^!*f z-ylUD@Y~|wmZCm*BlvYFZVTQZeocx-z#GS}qG%BOj_?DD#=-9tAO0e1P#SFW-Cs4s zdFJ;he-4ZELESh-gZT%59KUgjM(_i0!1DU@V`YEMdZP2T_;o*LoC=e4Tu zQwP5)u(C2-4{B278T8kwPo0p;@Iar>zh?gPIR|Jpm6}m=@oT!(1f1JBH?|6YuESiA z`aM+$RrvK|@51{3fzL;AkB;Ha-pC`th5-8&_T9+um~AH5SQ*w5f^8KxPYKD>Win4(i;|&Q zhka|@(zDds=6QEZTG~{k{c;QKU>iB6^&m)Q>~w3*usROnJB`$z;jY^)Nm4>2><1TAG8&RVi(6!lEz z0cP~Zxs+W5lfZ3ZL(gXAG`9s+<)Fc3+>*1cEE?&F8T?m*2k&4XFb*$aCSE44^E&zK zX}&Z2h(DhZ{n#&)pW>e{*!QxgrgUxN#u literal 7480 zcmds6e{37|75^r)TPGxS*@8f*fPt2^V=4qXB&2k0eq56Z(q8PwZYd>m?Mu(8e_@|P z!#2o>4RwyIvaMy+C<6wQqA}1AEtG8x(`@w+8d}$BlVGFO)QyXhG1&OkcF=v^_v>>! zCw0?K-5)#U`QG>a_`LUh-*w$v`3$*J26wW0;f3qo%ZA?5-#qbU?7#pX`hUQ(@)S zm_O)Nl57NF$*j{Kii$Y&J2p{2QhIb}|1vaLr%Mj|B|tgIm^uh6`EklYHeW{uL%*1x zo!m5L?7fEkAKwl|CZ{>!n3Wux#-DQs$)4joxR>5NKa)FH z&9tk#y7bP5F1NZ726}g$TfH5OY<`7Db5XRB>||qZ(^&7w_$>l|P0SUDMdG2b5_CoQ zJYBu9R5a)cMO~U2O1d!9a46u{Lb2#lCM6OmRMa0)*rKIhNHSFk$9v(+7A0@zf|0c; z%@yl)MU+S^G2jX$6nI9r07`MSG<#OJx)Q0V7K$iK;X+=mYuBt=-A-Iv^SWkM$-g(2 zh)lV!TI=py6b`Z$Wee6}6?Dvao+@L!Ouwj)&+GZdC6f(lP}X@C>W4Ho_X>TMqAZhTk4^^YqB?XP zF)Af!Z%k%no>D57O{b z3}qxjYT0XE2ZcTd4y03YdAW_BKRLIG^)Z!WoAY?AI*j8K%l!0TG(#G%&n^9BUkXm7wZj?7aK1=bdoB0D}K`zW%cRX%Z&_f$R;hl)aG_uUSzaM|jQ zkl2uRd(=D0VZLm>${p$qX?tGUP7b2o&=b3n=u-lY8o`P>fDL|`In(KF9{iZYBh;!qc6E)ngsa9*qzSTNxyMjDkA>#w- zLn(9Uq}{p02}^gMT^+#LXO5rX1wn=x>h0lndy`9ZG}SmWZ)5lqv>cnLH(IK0{)f}} z6Fjm9hHV$aWU{G7GmT{zFF7o_Vd$jIxnnP^3(2e#!?1kK#5zER9ZFhx>N=x7z&l zn_As)dsTZnr`einQ}3g^J9CSuH>&c3E0{BL7j7MJfc|o41qBHX?x;Dw4E`6Q%ploh}m%hm%`t}KW2<3-%I~bc! zWqNoIxZAR08~SiRxe=Dx1q;n<$wPAF?PN?xevljlM-Ft5VKq4{TWFs6_|&P5`aZg@ zyyo99KwlZk`;EDccl6*fw`^|q!1xkO7}Fa+%+GI+zlL-6H%4l->fP~|!6R?4`7C_< zn7*8@OikE9+Kr|(U>Nl&z6d{ zNiE&&Y(zZo4C(05q$@8S#dMjTa>!}|E9Eus5ExtPXI51;T_smoAp|?qhC`&zvg|%u^p)tDXIr*S&XSzYlKM zgEXGXR%1yDa%=V@BXsqP4MB69o+0OQ&@$65kg({O1M2BbC<*TB&tZ1cC^jmFb$II z)$#&NB}^4ave9Y&RvAuMu1MIkH5B(H**@HKX<2$8!lv1+_=D`vm{bR{?1JulNcd>U zg0v}{Q(H=B^ns-WKzBUo*A&(TJU+=Jww>X2MWfC+0nj5QnelqI#v&1aG{_z(QHUwK zQ&CSS633495R$es>{OCT{}zSn+=J!(A?P6J@1P5;Jg}1uhhqV@71@g{v_v%EPbz^( zoP7>RQ4kzhPcj;(_3UK}U^f6-iZs@MdO+21zY%0DFp9G_u0+{05O7(sJ6mb*tgx5K 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 O+#MVHH2*w6&;J9BQ@F_h literal 7428 zcmds6e{37|75^r)TPGxS*@8f*fEg`o$5aUIkdV@~`EgAuNPDpxyQP%OwJ$xV{)K%G z4cj0iHq<$;@;1t=Vg zlhjQ+b${%X=X>Ax~o%Bm;?1T#F^xPhd_WkDAhgvAd#d&;h|Ff3h5NHW zkyGv~*SI?ugoCU_*^G5q2^}+DpvoAp&@bxa^LoBHF#;Z|ptC2bz~F}R4%uA&si=QO zMHMo6XZoO>u|nqS7?HEVIghOGA-y^Qk=MMu=rf8{T0uZq-VR%rSuNy{UP+=I1TvJj zdEDxI@atP4n@2vOXf9|y=I=Q_w;&Bd$z($sly#nk`XP!C);1s?!acs(uAEMVHrOF5_Yp zh^`fy^?b}4z_g-UZ9ek~p~s?Jg|$Vc$d1p*K7uT1m5-m`JypoWp`wu4J-34^Ubgz9 zBsQd-9`#Ogm@AvFafdoX+E$RZk%MSA^u$gi`m}(fMsTLTRE;#dbZOfP*<*gIsL6zZ zNZRjW?$GIbNML;9$1gbnw%;}jH;;ckU6AM!V1r*_&h#Ec&VH0Lv}+kA<}=%Ylr3PL zhgmxB+_A5z*12P}souHc)${>p`es{9~I5!>|ui<`8wOT{=t=3`NRpjw18ShIU zNSQ+??9S~@Si1Y{>Hy9@bL==T2r|r2Zx6TIn_QZssm7Uk8^fQX<=A+=(NcBuKb^*( z;gQ`pY`YXD6HPUmX)L{T(LvD-Lnmy`?Ym(u_u15YVdyotpMrjbpjX>OXAB;5X*Gk# z5P~j(Y=)ZYC9EVHuPd%Y^Ubf{bJ4JE4&uCzIPX>+s5&-WHFDUQ4gy)Aj9=B8YjWIs z_0aL<&U81#+uAM}X&JU%OlF-JH`U&AxT!vMSmdYkFh-kw1h@HNX=MC*+_%%e)8?iR zX?4TxRqg4#W^1ZVy^r!flwUx-QB@dR&YYRMaO;2r^jBj2&{ed6SpSh?gURL(kMSB( z8w#cQ9U;+QrhaPA@{`VcZW%ogotbabwMq7L>Fc})Wpg9E^o%NcXJa8xfgX{JgNnw?uDcX$u5+6=LAPFilmI>EqN3q5ed6h?I)KX!WwRc-G3qW$*Ktbs-bcJ3V=~m3kjFd% z0g5r!ff-tRgw8c>-M31vgp!P<5&^~4>*tSPE#?X;NhJ~TheLPcAvxJB8q>t{*;27K zsinJ}<;3&Okd6#ZxC+t{Oqb~?hpZ;BQeN{8kpXUBn|;|)pDrE2ePk8oId6WpU@jNx zy*I!(b5iRlNJk04kdAiA>HtTWCrtQr=~7wmKI-=P%-I6LJo%xu+S!kJ-FtI;eQ?7b zr15078cR}?TeHvQ=;{|6g623qL(b=*Wu{#qVbL)Q)YF?#6t)oe%lkkfh~^=YmqDL^ z$b)utv|Vs(ZfqTrnvz(Fy@L;ymew2bC>q~%wfe*1P5!_Z_9?*d$DO?qY#OyhY=BkM zV;E#P3fK;8iq^*Bp#WP957JUdX)7W{36Sl={n~QjvE)Q3s+D5BgFsvz?BB|>Xo+oQ zyEu?YKk%5dCrMZ%sfp|~%}_TZ*V%hCf8HqCCuA7p>Qq&kpg7j)l8!bi#$ zq)pkB+FU-P4=g1By5m8=rm!yH@ku7J?F_do8gI=e09|C|ft_qP91E~5$X;ZjC87a;QVB%j z>~lbhg5bbM=XD^!vy8+N*q_G;*1FDAm5|FjPD9)O=5@pXqz-6WGY^A-k!d@XK zf`vjX{7UL@Cqkt_mJgqITTJR#qFS=lN^{_BS%?C7c+Zqxah?ORW#EmVrecSG3y|Sz zEnjOmmQ>hdsMvi_96T*-G0ok`@e~ksmPnTUhLVTs)HQ%9$HHjJ9f+_Hx@OQ_AOrL; z=m)_06Q}{6Euav{1bqy0z~>s!GSFJki=Yoc*P*2KSO69D4Dx&t^d`uIT-SrX2YL+j z6zFH5Jm@g!C}<3H0>rS?b`U-0eg%3HbTOU{ct47;md=~%V1nZ&&_a-_NLB1Ia5##3 zF%Z9l7yig!utVg2!Qk`%OZr-Dnldx<}j{j~X? z$+(0zEiZ0Usn z?=HvNOEYCA3vbi5$nEac<3O868P9;^2->hl=fMDB{5{sy9TEa@ettB~&pq3PD znY6XcASv9)HV||$dKae*YdBX-k%S&4T2$jZMgPTIO`H@BBnIM|QdG*ZaQGswl;ME? zCDrpm2j1_Ks(*`8$A`#Q4&xtk!g3Y=;4o0jRbqmI7c(qdg40<3f34Fbrw{P{$-Xv6 zd^R9WC=iP-x$$(H@)51~AndI-NAWSQ+~RZ|@3HKK%NhR`K$&#vITb;vJT?eNJwL`-H$sf)`!IX zpq(ek7bs_9|w?h$SuEAXi|{x&ms-it0~mBExDxdwcpf}sdP<9WVw%TNW2Oh0h{qyfE2e8ZiDVc^q!vynWb4j{O_E5(tyoMm zt*{nRh^oYsV?ZKX&BI*XcgXME8jq5m#}BYu?w2g`+^g)0_=iG)XO_N&I39Ck^OMcDWgc5kNI^QmeC+w0bRrH{3S)djg9mh(-&L2&D2`nP}a zxC+uK;cyDkY~c>qg@#A7$M2UN?*B1vS zWmpu=Jc;9HLjse5ssXb;dd*RdxL0hy3YKM!NZv`aWmE_Yd2{y#CzK3Z02wbDL}75H|+2=E7MCwhjT@T?_%sS{TW&l z*a%%OnND+q4os3vPoTay3riohh2O%TumY#NhkAntB#)t(ddi3>p0P0BvMk*bRZPW* zh2ycWD?3T0nWkG}KP#z7uhbpvCsSho$w@aRmpldO28KV9P#I$_kqBLBW;O#3NF?bF@!jR`7KZLNq6Qf}IHsl;+ z0kRDF1R~!LxeEC`~B1wJq<4DN|#rSD4oRa2}42UJ!3RZvs~uYrG06n6rzjbA54 z4e-0--<6^^cs=+vDDDbgCw^6mdcf<)ub`+C{GRY#iu%Ft6(9C8FR0X6?(}?Bbr+GZ zQTzfH;e)zo=KcCPfTz+Y*w5~Oxd z1wsXWZQoUB|3C1>DAwrO+db@kz* zRa(n6gPPl9xt2<>`x@LZu)q7Mu7Ew6Epk{I7HsK~3 z0ZU0Xb3;9@8=Orx@YUF2d}V=Gt@5wMRW+g$FJ|N?_{z^dH?R_m=-Tc_M7`RTW$P^L pOYdFclTS5E(Z}97-$t@&(L{Uzs%?MmLGi-Ota?Mk{@<0g{{#2mHv|9x literal 7222 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 diff --git a/src/test/samples/golden/inc-commdlg-release.app b/src/test/samples/golden/inc-commdlg-release.app index 048e32d38829471f80d45558b57d70f5c829e236..97b9142ce46ff962356f8768ebeff218b49cae03 100644 GIT binary patch literal 6262 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 diff --git a/src/test/samples/golden/inc-compiler-debug.app b/src/test/samples/golden/inc-compiler-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..0fb3dea1b4f507653bc76719ab0931f97288802b GIT binary patch literal 6908 zcmds6eQZpd5S8kq6%rWLmFDym-Yn`Qw?D;Nc=%7R#h5O*+iwsk`1Yww!yk(zjN=i zV;sjACicg!a^5@leEiP2=ic|8?@h-ZrR^AFY#MDP?kZ>=$g_yC2DiTrcWTx{^8TdD zpPbLwwS|mb0g*qoH28gyWU_;seMUTK=?1$Da}ot(DJ!ZU(Aech2=+%zpB_ym_zpc1 z4RDjG2uo(ZNL-U~=sG^Rc&xmI-k@mqhqltxd*cHHtkl~dj!ZRYm8Qlgn;4s5ES3g} zD!f$ZM{pQ9-)c|wj^}>9n(*IvA8@02t0m-OW?_;WW^MU#)~zK|7IO`AXV#EC%toe~ zgjY7yI+~xD&#X`R{aN2Ke<1b^7-U1uf!HB1s;;!M3i}V@1R@mmy~(#ZMq`=o^bHxsc(Ttm9p%6Q~+=z z`6ku*&c8){M)NJ1?F$$yD2K_%p2#VOgKDf9GZc0PrHws~s%<7ml)J0J?*xjz)^ZYs z4v*v;f`Qnha2njAIva%A169yjBTWm+5rWStM|#xQ69Rm331lfOCmMf)3vu&$f=f+miV-`fby>-jnt(4#8{k%i(ZVIk83c+rG4O;bXbVCr0yGU%Dl; zS~#DWXjq7ngid6AnLya@ya_9N<4WgsX}#^`X?pOjXL@3ra35?9I%%P4&M7Af$_Zo% z@6khu7E~QBo$Rxt`BWw=a%h{jQ|ZYy?&G3vLq3>*Ojvck0Aq@@UOJ6TZ?Qh_BKE{K zyTO(UB?W_58X@MpAux*6PiaM^(qZRM|D2xAhMKay8G8l22nydkf_Yev-0WA5f_v_T zK^Alhu(Ch7(s>(qOiw1CiV;9>Cg_|b08CaRc~dK-2|;p%Ah}~D?zqblo(|_d*;i?v z0?vL3p5uRQ%YWsCeR#%m)@oWRg*17$0?1C2_v8rPCxa}ziJr9B`;f;LV$fm7Q$jd} zJ`*6Y!#Wg;Kv=2H?c1_?AK{L{OkReG@3=p^LckGH(0LEKJz244Rp+AXaMpGBF2=4p zKSOU2FT#?S>KqdW%P>i*a~S;O8Cd!qTl(GjBUa#;Z&ycXhvG4~si%xS?&*(+SC*xF zqTJ+0Jd%iiov&k+W}0rv_gPWJI+XU%HZ~^TKRM;v$dsp`T*I(gHr1rY&f%L7c9zJ{ zc(V-j*yBA}<(e9FZru{IX&0QE;8227X&8@ATU`a^IsxRA>wYzMQ6OBBCgOeY39R<( zfnbmGD~aIzSSm4P_CG|m`^QdwA8vSOWA)CTk7da zl4#VY?_Z~G3)%1OCS>{i?NV= zpgV!O@EO>xC*yr=ClG`#I($%k#M`YNu`tsr%U7SI-0C0#R@(QH1F5R@Cwha9x zmP1Z4M7=XwfDgn?iyK}m!OkPEqSkdTsymG#_B_;7$9+f#U-|%#MA<8_zX|dn=xfm5 z2K^a9)|U;-h4LCaVsUm0mc<|)e1B@7Jbk6;h7h4-l5?@diV=V@ey5h{N034KJ5I%02kpWIsy$acUfSw^$@3oJi<>Y$FCG zd`vBA#5Jp*T?Duv@ee_B10-U}vg}H7jcE{wq)U_3Nekh1plm3jmsUr-1K{lhHkP=FnSE0U7!f)HP8=1^mAfA zB7GZl7IXtN4SEWhZv|Ze{SNd8&_^H(;eQWW4!l*M=Rl`Hmq71;-UIy|^f72T67qoj zpfJb*J&wtJ8WaJ!Kbj2I?t8Wwd7wQDS_|^HG>_2_R5VvFJMx--@Gbwqn#}!y!RP;% z_-of*z~6sAmU5hm{L5AM7pMZE0w0(a2KN+Yw(o5G`wlT198gv9?;}K2@EZ8{6k;y$ z+W2*br~!Ug{JVyz4PFm^jSzPQuM@v&h5$FB%cC-^<#mqOGJey{kjS49I;XPfK! zs_HH?e?a+5Sfr28^@pf4{}Q0YuRlaR_$6?_%KFP|xx8vUA-pSo4G=`Bp_LJ9R3I1>Nuxk_V)3puH%;DIY*8)-i literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-compiler-release.app b/src/test/samples/golden/inc-compiler-release.app new file mode 100644 index 0000000000000000000000000000000000000000..c587d1162b2c58980aa0bf37cc7760825e1742f5 GIT binary patch literal 6856 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 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-conn-release.app b/src/test/samples/golden/inc-conn-release.app new file mode 100644 index 0000000000000000000000000000000000000000..f295f93754b48a91b7be596cb59faff331e062b5 GIT binary patch literal 6484 zcmds6eQX>@6`v$_Fu}ntHKig%I*EKyM2MTDLPC-JwYCGfWbZC_=Z3gpeK+==bKbq) z?j-^8#}%I?R?8u)O4X=}gj*tE(eNc&NF}3Q;^0f9{E=3nszjs)luB!Cs8kS;P%yul z*}b^u%jIHZ{$S*H`*z-Yzu%iTb2~Tdu0E-Cf)KKRwh31mvI^o`M@XG}--9b_=10W! zQCB~=nvk1o3Aqko@vLQ`A5l`NE^5TmiIl0Q$u+pMu8^8B7zzP6u zw9p_s-}^VO&sd=;_tiCo6s1!v#+u1Xr$VyYh#87F!@|d!M%DJPK$N?;F5m>cSaSpG z3ZEJ+)P;iTLkJpflbxO1?1L%fY!$9W=`@4SOQ)l<`UnSKUjlg;o-_5oL2|2P=WqYw zc@?EIBH#?N*~9~G2n)|5s{xiraP|tFep{ZYhQs07=tJbkD=Vczy5^`ShFeezM zEa#;OIpQ3~u-OV3JArv(bvTMeE=HN}zihYFS?|cs5JF$W@*>v7)&ji$@-jS%(pd(? znJsAwJ@qXB8++4|3g(*8i*p~& z%CRgOcosSE+s$KdHswBt`__eg|2gZ`lZcx8N+e=SlWlUq@@JjPABa^xGgh$u*{0kU z9y~cyw-zM{Pul)mFcNTHf|s>xlk`)P8T^K85ayHMQb-~JH=eD|E7r6)dU;=Ux+4&Nj8Pg6CG`i4ce$hpYs(W0r zhq;lFAu0=s)h-1_k@-1WQJHMS`O`la7VL0?-Ji2IvIjx&+ov%PJCK|8>=|fJJ~d22 zP7z+#XEr&ny$R+dH2jpBJSOzhq4o2I^veswJd9dUdmg!8y3 z90Yz?hkOxmFWI?sM^x{9TrrrrYjE+O4cHqw9775@Z^71Q^EE3wSKNRLZov03cG>v_ z?uPLoEJexA1b1k`B+1Sx=yOx>3^Xx;Z-UD@r#pY+`2`Wc4EUgoyKTL5(NN zP}G`^+R{xqJ^S~Rk-ly!Kbj=Zv{h9 z=T`#3`Kd5s%B+9zY7dT|{~^Ng%*N_neYgMaj>ERRj`q;rPG5T}#mJV?*yOTZ6->Ob zd$#{|>*8fN}N0k|Mwt1kUkIl9mt-diAty&@suWKxfjT^JOPF|<@V zp_zl^3cv&4KMBh$gma0q97s`(Xz1WcmnNy3Erb_j&A+JHMLf(M91^GMV1IPv>qna$hm{!v7L~+qTR2 z`|roH7^f`%#ir#El_8Yj1GCKGfmoKtF2!Fq#ZquURm5KgMMdx`_zy&JFYxO4HBwXo zzc2oMDXN3lf?tK=zTh?DSEQ%~ymtIDiW-_{BJ&l> zU%(=LP}fdTWBw&TfnPgCE%+sHz>4~d&&B-}>xs_$;#UCyOBJhNtd3tjT-2(%PZj*K zz{QmjdZ{L5o~8aO^{EoFIJ~6a%fDj&_i`?wRa7cQEyb_sRuOP%=hE0R{G|>{L8|wZ zA(Y`)k6niK{{vr)Vue1tI{N&Nlnw+quMl>o^V&ikKU=ILoj75LBjYGAYa< z6USJQ4^uNnvtZ%6NJ%(G(+Sg!W@l@WnM}2KLK_lEcPd&34y8xE5ovLLE5O@LQ;DfU z@1jgEjZKQFdTAi-nsJ<_2b9=RVeeE@W+v?p^blTR(~%&3I)k?ccWJuuw#dypNMkf{ zjPjlmX5S!9ivo!leq=pJPnh1cRJ!u_;FSz+AC8>lXZZgGj*h$~aA@55xO-FK!j{6T j@b?at;$>C^N4JZersuGd*kPWvu1H_-6`VU+KWqO15psa> literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-debug-debug.app b/src/test/samples/golden/inc-debug-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..185687857e47d7b0a1367e33e46643fa99670ab2 GIT binary patch literal 6418 zcmds6e{3699e-_;;FXkQEQ~>^WQqP_h|;X>goKLHB-f;)Zq9b?WTRQR*q5AB=g#=f zS%LoH5nJMP7)4X3RZ}IhQHh9^Ujjxa1yEg?6W2)PDf@vNnxA5{{GZq4JxS&Ms`2EN`I~7@PgJEqMm#-vVK!-F|ywvDuM4jSqPUnIl9? z14ZUhGW{_ECN8$xi-WVdUu|<`1`t zMGud1Lao{S+*)FN+V6LKZT^7vO*lBA=74qricC8=o0qXo+KcV^!P()-`OgacLxwkE zj3r`mHR>JXb?^=wDLv|q>0V2VnO;;P9*Zbe%+PlbDVaneb!AK?TXsBblCf0Wip6x( zQuK&QG&P?+*u`Ui>z6RLLRVX9a*Z zk@v{-yZ`3($>v)!UtUK@K|0A|?75tDGAL`!n4vHo6+ZSXsx5w(>@2Ku?;I%YhYdUB_7Zgq%RgRrx>Oz z=cFk)OpjpLY=vZxW1d(Yj$x6DQRW6OIUNo5TQVI%=qp%W#J=($bQ?GrkCCmt9&k-cYNuV%vK&e zJ=f5Ll7yxmUnUUt(-+}o@7_$G7v4QjFR=&TdZb_5gX?H(kfyn%IVVjQq-kV{=@U2srhn>jHe&#f^$DbS}L0W*9 z{i)6LRa`MWnS4rP0E3wzJ<9+vS&b~3wL+R92#p~KZK|Z5aW#f#Ab7xemd#Uu4his_ z`q%dSSDrkIXFO+ZWlN=yX3?zxvePVjVFK@yQR3`jPnz~7`n4tuI!wJKgmbtj90Yz? zhkOxmFPYxHEvok}t{BY1Rk--h_?=B0jv)o<8?X&He9g-AvKw&T4fr0$F4LdmZWJ%V zQj|=mxI-HzNv0>EFU-KxPi+x5|3|FANnc-g=%D0Hs-}@jMpW;x!e3dI;f<=Mnv5y& z*f-Q2q|!|{Eb%@onn<_Q8R{XE;{B78ZcHqC3(^e?n>Z84-SE^~w{!iB#NK91FXGZ5^jzY+-a zC&Gv+v;V=XJv@2t2MEJE8>@Hi`~C0lIBd)7+!s91<=vM^FtQaiHo0t91ru-Vp6!48 zjpSIs>Q{8jJfuFgFF9h8z1VZvf;&2#R8{g@csAkTeF8FscgYoq36u?3NIu91fqJnG z95xcM2ss10;sxn*u-}IK86-Q) zhUG$aB~`^z$t`#mNxIdc)JS>yQZWn^p+uDVe+`U$L?-sfc#mqa3H%`to-G3sQvjl1 zY%GBtRK{3~B*IplnN5I$kq^SBnx-_9lBqSV<8xgV_gW@r!o=C=Y zYnWUHxF7r{V3~(-E>V`ggsKxA9X#pMB<*Jl;d!9!Dx#NGPLR)D>1)+%$gY@scnkv; z-x>}e4ot=Bcf}1;B`pA455pJBK;3e)?_+G}CfNh0k0P=cqClR5{0PFnP7HzRJCO5` zdB_svlgNA@t$RW)2>zDoHE zSfmf?`YGznzXT}o>!+v(zXT3gQGfAS-e0wz=)5a_4G^%@unNZ7__f1Dt!n$!z^@1_ zuZ+-3HL36{_1CCRjga#2l71)us`=l^xrA0#sT#E$zp7hRz~!CGV=M5NJ1hsO-BW>3 zfnPgz1=jx$d@+hu`t0r;@I6>M5a7TG7?;2N4fh+_s~dCwK{P|JeDwUC^S8+ zOIA{*`eAOg)WiYZGD*}m!VrRJq13P1Tn)*8j<^Wj}cMg zCXegF$rJ;M9OJIixDpr2aaFh?wXh=S+*58@iK?pnSLv!2(TM{f`6>RJfWsSav52AX zdc?h{a$)oGGW?4tJn_>~6-T#=ooyE}tpOH@CVprVdbLmH|0Ns}aoFUoJ3J8REW7^? D_0d%4 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-debug-release.app b/src/test/samples/golden/inc-debug-release.app new file mode 100644 index 0000000000000000000000000000000000000000..9ae6ffefa5fde0dbd647e92e602c21607b615a5b GIT binary patch literal 6366 zcmds6e{3699e+)e%quC$SQvv+$rAm=5T#k$2?-UYNv=sp-JE^3lZ|HOVqbDjojc<@ zX9fC)M{J4HVH8cBR!x=2Mx{lp{1PxaDQIp};#TS(-BhMYR9ZoubRmIB0|70A@Okf@ zUC(x1Hz@qUQ@+0Mz3=z?`F!8^y}Re<^b86e(}a*^v`x5*kTnqRIzk%V`(9jWGwoYJqH$ACkSlPfU6PtKBl^=CxzdD=VcGEMkz`Cc zpv#efVh|Cu)XgVHHO@mn=N8vbmA2rEl+AkA5cHFTM@9OwMc#x|lXxvJ<2uH%Ins@uW#y$L#D@>JPWc zB@c_TgKe4Id;>8*A(22KHF;bio3}q=knv>9j7BxX zl(n!zR3#Q40TS749A@Ty{Q=+RScG&RIzTVkS83+i*Xbwn9|%d$EPV$$s$`QN&;mf4 z%y~rTd;ezj$>dtoUs+2?UN}Kxtof{PLK4*$%uvV~<383LsFHDlNmAmGJlN3HHoE#9fBP59 zD=(bl0jH47CKhmwTUa)`0s+zXokC9jCYq}VP5nHSrgp4cwUKr?udsl2$$6=OIZZJ| zF)K`qA?GlLO;Ba>AfL5=O({jTv<5_HtvfIw zO|$T@EYje&+cIyqraz1Oj^*so8S9tF5w-YuC}ayWouc3JrJRc&@Krva$=SYCYkCU{ zo|$iKMoEG*wl5tB`JI>GW$oJJyuiJCpIN34zWL~Yx*ONAHpxjbOG{Rm$qO^c649CC zV3R~g;X&59OfH$WSq>e`Rx-7?)qPyVUgU!bNQXq{i*Tk)J9yCKa;NzP7jZz{2KIj`f2=}G63Dg_uyOU@YzfXQm2(X{1L6oG3Lfos!6?X;^=JRQM<_H%Te0?sf8 z&nkcI$bI#xBY4KM<`%kC@+lhK1|Ta%qZcRfJ{cqSZu+FDZ=+vr#-Kxvw}@~S_k@AK z59^RE0_G(;w{G$3y^kvfvv>tAzSDktBZH$zlJgd9gEm{UqI1a&IOhg@A7dAtpW|)} zFT!G!=u9(*c1)7!oPfSK3s1je@wkOQVg*k5`g?*01aCqy^kgEect>RR$})9tL@|^^ zRE|Zzsca|ZZklfL_gTS2dW5cEFPY-+pR90wa><()u4CB5PI^T3Jobc;^HENXH_PaN zH8)@j*G0*>d6Uhim3Q8Qh7z1j!8x;R_T_~e6d)_y@QdmthH#m?u=l|yu-b0~qygtw z9KrbsH)6`Hf3RwgOr3ohVR&a_^)}q@zrEwoEw5{@w7=WCHy)>CD`<3b>8=VU*62Om z|MuyL@qjrXYo>8fd3tYR)F6AX=h6jtWF(;|4w0WEk&~%Mb%7>#>l0kPid( zVH-H4$D?6#5D3f`nK;Hau`c}>nJsh{cV6t#g&8Dk0S6YAS0JhN>4pijnb5yMvdBpV z>xbC_%4pOul|;80BNvcY!RiJV%=;4Kq~P zL{fA-jvZ9QSPLZFR+yQMfP;|@!lxL9JgU&DUPE64^4Ysa*@Z!|7(zisst+4gXB7e#@;|Y$g7RDJsDMRTX~)6ji}%;NKI)oxp43*GW+W z{I2+SrKk;F4}J}byMot=UzMUB@cQw~DCz{iC;TEs{owbC4}Xz0D0Q|wJzrJBdFHE> zKZiy7pst^y&ispj9KU{wdhmxs_0;@1EHOARY$tc_ngoY$(hPYwLC zz|zWay;zen&tiX#`qT(14KM0<@~@izot%qkRh6nymH1WNssdJaR>qd$S2|RJ)b1%m zD8sKEy8`R~2R6C)Gb?cfL)soNx;F4wP;w^ sc0TIfRJqVOc@_SJ(;fd=sfwf9MbEWMn9=|ZMC%C+2EL2~Anm8^KPS;s9smFU literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-designer-debug.app b/src/test/samples/golden/inc-designer-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..c976488a95b48056f05c1615787df47a17167c38 GIT binary patch literal 6861 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 N-QgRbkgqDJ{~tE`>rnsz literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-fileops-debug.app b/src/test/samples/golden/inc-fileops-debug.app index 8a1d85220807310d56551ba77af81eb6e1302f89..bc8f1e17d3d42b6a4e575183bdea617449cfa1cf 100644 GIT binary patch literal 7298 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 Z`AAFrC9AN0)2GFM_i=sC8kc+6{6B44v!nn3 diff --git a/src/test/samples/golden/inc-fileops-release.app b/src/test/samples/golden/inc-fileops-release.app index 7bad8407b45381a7ab9f6066206cc9b9fc7c237e..bfdb5f96fc1556940502335b88318a1f309c7470 100644 GIT binary patch literal 7246 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 literal 7124 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 diff --git a/src/test/samples/golden/inc-game-release.app b/src/test/samples/golden/inc-game-release.app index e38bb6c4519558fa3df5feb9fdcea9b5379ea01e..fc3bfe6f479bf7de2bbd1dbe4248eff4c794104d 100644 GIT binary patch literal 6149 zcmds5e{3699e+)eF4|I(v9L{)N|xv^Hc^_josdwaG|43?tJ||Z+u7EvTmye5|vg^C%ursq=A5zf%&}m z&Nt6-T{kHF!IQuD-o5Yl`}us|_q}`X&V55t*EAudgt7^>3|Ry5+(}5Yb3cHZ74r^J z-|w_fH4$=k9U+$?%%7DE+J}{7vQIT4saVp~Q{)oNSywd|yu z-LETAziJQ}w9L$_#1evsUFR0pPgR!S%Z$zXz>->r7v2Y9wch$*a;eRhT3haKC1jou zEdvypd&%^taF{&TVJ!{M<$tw>;lKPI;Bp1CJ?LQCFv&kfx(ah-Um}?{iDQ_b-OB7? zHo4Twz3gB|t}x$3%rE+Uwzt#g*S-w{JJ{yej-nycZqDXmY*W@!S7CT=bZX&2fqy{v zMD&SdEUrd96Fd)|Q9YfAdSVHWsl^NrG7*nO6f>qLwi79pLLw8&gi1DVf50FU>9`q- zB@9zZL{y@w@#H9w$Y$dZSN9+EdpF0Ur1#PNtjhnIMV^0)T@imc6nJjwyJ#azHu)h- z09uoUR+)bP-#kCLLVNa+wS*Lv?v{C@OkNYSk~_4;OkwGdtf==@*4!VMy7xJ z7muqboe&Nu5X~m;a7Ac%G`swM+4i15OuiNttBjWRWi(nkv2wL$)B*1X%7JODdRaC(kzLrIS|hTv58n%!<#|Rvtwb{B}p~o%ZaPaNkwR51+DLI|{GGuZKdmG}A5nEN_OM|5&W@`CP&FX4^E#v?IAeRW{*aHp0xN6RCi;@F9(bL<#TQ zQLqJMstPCTOswWX0%|cRe0jb!+6H?<`%Y8iW%nJ0U#^GycZ|2pNtcGH+#~w_t3Ac!=OXd<072KJ>ej* z!#d=PfLqD*#to6Zk5FSUibC#4#CIu%hpqYB@$Ox+Vz4K)>0 z;<0Dd?WEc)p_^hqE2~JK)Ds*aQ)2(gOIIhCJVog$hE42rtE`>HnGm9%5!BdO#=_QI z*p{x!0eby9pG~VsKST>DIGurUu4E1trE3fzFJ1G=+69hqQJC=k;6AMOYyLo(zAg~x z&x8_FX8nU_dvxmbkKl$q8>_eJlm1Um9CqaO><#Se_3TY18QBU7n_PCPf{7P)&(6Po zJvHGs!%D(54yaG>O^q3356)b+;0}+bRF(V|mh15FJ`NedUUCs)0A)QEk{9x6pg|l1 zkLt--gd6|@*F~Q?!Vj??{Ro*Yce*Dp&gk+ClC^*X3y;g6P7LaX3ALHhpCNg~q=EI* zY<_htW|(TK*Nl^Mh^wr1oXhI|)C74PYL>^t2nQ#tRBhsq6-dU^2?=zXe7@%E|HdlNi{(d=-@%Slhn@^!b?EeSw?qPPJqvz`&#ujqASN9n!td? zTf;v1fvKGR-nee4q#b~FLh-c- zfRrGgN921Umm$A{`~mV2#035CAsc|V8S)t94ah~v+mLr5e}{Yw*?@pN5FaE2p^(pE zavy*wkTLcS)Z=})4KbiR4A}j!B`)^zPrd(eVaP?RgRUJ z5p6doRhDjlo&3~ssdRVSH?yx9|ILhDw3>~YUd!=on$-kc-nrbj3V*r5a*+BxRR~r1 z^?g^M{r|uhqgbVDU(b;DUiX6l-z)A1IqNJn^KXm0$N;`D9#Nm^!#~w+<(uBPs-*5B z{VC|YF~d-!?OYXBVy0g;Q%Xd2fonAQAGbC7%U$?JB0t9;?)cW>B^J>WJ0EgxYE&%t j*I-}%3KIXm)G&0q*yrCOhW@N`-xnJ4tM`e|Le}*kBESui literal 7072 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 literal 7135 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 diff --git a/src/test/samples/golden/inc-help-release.app b/src/test/samples/golden/inc-help-release.app index 5b80dcfda23456ba6e3e78984114ae45c4743b74..7cade026685939b6435e9a263e8387cc78d2b544 100644 GIT binary patch literal 6152 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 diff --git a/src/test/samples/golden/inc-hlpfile-debug.app b/src/test/samples/golden/inc-hlpfile-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..1fce0bbb8604ce76f5d5e86e5a8056b426c8a055 GIT binary patch literal 6486 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 AI{*Lx literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-joystick-debug.app b/src/test/samples/golden/inc-joystick-debug.app index 4b1b451af119916dfd19c2846e2fe9bfb7b2874c..af012b8a1ae62cf4578668af65b0dfc85568563b 100644 GIT binary patch literal 6653 zcmds6e{2)i9e+TQrYtn+$hvL}$ON`QCls`+mQl&-Z=byF1^ddqCSp@McC8WXa_u$N$`H(n2 z?&>EN6LPhQkjoGj&srM#J|&UprbZ;GCQLm^F2S7*1!E~Qs_)mxrA7=4DTYsvrs8ym zu0#XWATnr~n@>?S!NZPIGs`B+Tj+JhW_@T$&4brJ0AZ!w`fy^l#g>{IA8#gPnvhr; zC^CG{d z`dC7Z)2L^R*TFNar?jX?)jVcQH9V+9T#YEEs%z_sluV+KnleVo>h(_=WGodoRaG-g zMT<}pqw&NrkjQFdFE?-774WT&M~U~j9qg3*CCfbbHajB!{&4W6*>6Kfm2C2TRsd)d z`DWSq?!S3`via7`_9cWAq(dymn$Agwf^w_{D-?D{g^x9bs%>F`D0gpz-w70B%@u4Y zbZ8>q5Ddf~MbPM0+1bd=9+-m8TH#ud4m0?ibhuxRJ;uS8l|YumbGq?2NN$nr{Ow;n zuYz<$1ROy&t9ZZ_Vd2?q3It@^cLX{48(FS0wAfQn8r!jRV-0M`d5Z_M2c0*Hm`53= zEa#-7a@g66X|o-Y9mhJcI=p~QE@qhvkf`vm=!u#kS;Q}f^>P3pRRljCy<}Ef6XXmzP0*gXNfy8 zDZ{d8=2>LGZ?|RNZOuG^e#cyH@Pze?gNT}WHXOF4<6C9FDJ5| z9(;Vdp$R1k9k+d%K-llR0WWLQD(5xf-Sg5MyYS7Y`(vAN9&HOcX>MuBNyiJ)ab$_; z>_M;vWrvC&>r6JE%Gf-IjyWrpo>}W&E^-g@!2)E$vhyW4Go}q9Xku=w`9&A8Keojs zTh5J)3{h!NEOsd{i_}lqj!LD&&Y%7{H)n^M?ZJ$-lHCXj-#UzS*nr%umybZ({ro5i zIt6%HpIha;jWd=flTXDMz+fimoL~T0tVR~iS|QC4gvJnrcC@4&b2WyiBY41mg{@P- z84}<*`qz&9SD)X9YdmMJVOyn;X3=c`veGPiW&-z9FG|A)G{? za1i)mAM#DWy=3RsEm6IXaK>b2F2Tij%x|yca11Hvya(HW&G)SATyO)aPNx+(5wMHA_kxvu8X-G@z-`eN$N597OCKkvi-V;nFP_oG6(mal2Ns@JAf@%{h6%Ho)DMsx za*BcVPPPCYQ4NzOy=I)8LtaIz8(cJROOBB}Fta-DLppfU2dEMyFT?)^!~@b-VZR0W zGe|a+P0NMyCaGdlauc3Kl5RSb8Yxd-Du#g~l!!9_uY-}#$iVY4-a}2cfNuifl`s(J-ui;?#l$lhX+kZ?S%N602jipLmQ zBB^TTFu4G5Kll&Aavj3CL|Jwvs75pl@T5zNw2f_q*MPFIh+f(`LB4vWpH=T5yJGI) zF-%zeYUo28n2Oc+#&v^|Rsb%A;VWgJZn@d-F*bFBY=+Y(5!nk-Ag@Ax2w^`bhQRb4 z$Qj6W$QGA0Qt=Owj)xvI2OkA-f@`AQvI;K;DD=9r7_`1rqW={E#rj zfqV*!`y@nxjOqsrQ;obpx_r;HAP=->AnPC=R~8ZW0~O7+i;g&^AAHL{uqJYUVDS0> zCH}f~=kfR7&!rrvBL8yJ{Sj3lRNw=%!r`7+=Eu&*zi*29;DD-%e;*W8!E4~(6U9Q{ zwejnur~!Ug{JT=r2CoOd2E|>$>%^~0Q4e_i_!Sg&g5MKVc%EdnzcLHuKaa$whLu-D$w##lh%AK(|Tv0=RHlAquoCl9+mYDCvIKJ9j@oY?+5 v1^?oktoVvn#nByNukkmqeZS0z(8M=2p;tRH|0g~pzCkd1cX%N1R9XK&*3zjH literal 7199 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 diff --git a/src/test/samples/golden/inc-layout-debug.app b/src/test/samples/golden/inc-layout-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..4f5fbc685e237aaada344dfa0f69e92e5d2ecb98 GIT binary patch literal 6171 zcmds5e{3699e+)e5=Ke_3)@7gWQqP_h|;X>goG-kNv=s*-Jb2)&Puc5#Fw0db7y?# ztz-Se!?wifFp8#5tENh1qY@D-zXaPlDQIp};#TS(-BhMYR9Zou^g=Qw4Ft3d%;&v# z=Xs9fx|zZsJo$U?-TQvOpU?Mw-@Etj+&3t7O%Xy$D4S5rkTnp`U4%3{_x-3@F+U;d z`<(WfCPJ>SBjhTC`LmKn`)DMQ=u?c5WGrFoNpc0|tSg|V%&7jjMy|A=V>n`X^=K-t z9MB_CzhV#>w9L#KiD`m|U1t~8Pga)T^Nh`U$C6ry=HCWkwcdIsv)E=!tu6Pp5;8}K zng)u@y=3}xIAkt#Sc^lm`QL0|_|LxuxLm<(4?37OO!AMBuEH$YuO(6@aSZb_TbVt~ zGK;O;%MNzr3Uf`w{G!iidpmu8^+_1m!8X5o91WRvb2blSo3s|Y3PZD_lk;B|_y={* zh(3{s#g(XMg6F|As;9K5C#HE!HD-8_iFj-zV#aiBJCTw}BvOk^C}i{Y`wcRYikq>R zW|$FeL?NmYPmBVIY&MQ?^}a)X@8)=v^geumRr!}$C8Q{wU_RDdUOEww)i%sfh>i;zYZh7C!yJ+Bfo31|yT0ZI z))hRFDKrQC>Sy3I-YwIeTK(`9hqI8nM=cSWjS-ppYuP=k#4a>Qf-yyg)GX49% zd0a*5lyEqOXf|<&YeK`L+2!}kw)Yfb^0ly7Wwg|)?HqI8*=6{V|_yt?)+R3M+Rf6FLkzO=$JUF!@? z%Cab0c@$aj+a0+#+OuE6eODyVP*wC&CMLq7TftgKy|=ySrl|EUst@XZIp>TcBI9RZr=nzp<&U6iH~CA@RT z!4{CIBAl#qxk4&y^BB5HRw})))p=axe#CNdaXAcoaF4%yvY z$;jZ921S!Yfl;J>#a2`*9is34t5mXst@cpX+Q>G7;`dHs9(Eu$>*-TyJ@V){3D6>} ztS@Y$uc5~DWD6;k0SsjW^b7;QWVJAFR*GqcAX*GTw5H0f(@u-w>2MyjpJDUlr^5m~ zul%j6@Xbe$;u+7ITi8-5rkQsKfUGq0UdUiS87KB`_N1wApyR%3ZY9%Ow?y{dM~%TOT!D%Aw9nqi;TTeYz6sr+&DX3@xi|?#8hZ zmc3*;#SJ*3m%X>NyX4p$q~gf8sS@(se7V|p(JCG zc89AvTovh)dV>9AQtUr@>3U|-Qtz1a8=~v3i?6?Emn@VMkui-oXA|&)!6Wk*%Px$z`W1n0R6L?EKrO zCnx-7IHH-xLFMti$uWcM!I{ez+|kjbqLAOiavdJt!;oR@C6^%vP}XB1c_AMK8o)8| zu%3vGkb^+ry69ua_#xJ#A0snvXZhsC8STy>SqnI@@VNXbZ9q3nsLiDQ1j!>N6|5g+ z^DAR9!&H*JW}I9=T&~t}cGdfm6XY<|ERRPK4o-T%5{Z(hVgDY)1JY-qzXkaVNOn{V z%Yo`mDiKp5H(}|L^eMxsvC8lz*9{cGM3mWo2aJ3~2F}NLzhbfp{3#HgsQ?mF0HQ!_ zB7qZB##mhvp>tsc+wZulPb`R(+vC|3ek`{{>zw1)u-` literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-layout-release.app b/src/test/samples/golden/inc-layout-release.app new file mode 100644 index 0000000000000000000000000000000000000000..9835cb19a1005380a8f286456b19608328a295a9 GIT binary patch literal 6119 zcmds5e{3699e+)eE{v39ENl~{k|p|ULzHH1CnQuUO>#}j>h^5Uc2=4dC%)tyoIB$? zZyodx58D!_!zh|Mt(q#4jY>qUNK6wv>^i%!cCxYrUu107`1tb^-#!`rn_aI7q1^DB+zu z3bueu72#x^$rVyro5#>qvQp`V&CcT@_ah!mKsF@PufmuyZ4*wJQn&dP2QjSfa>(xG zN=62+G$@)J3XCH4OSYm?=@9+kpQVx=Y_*57)_S%P6u);I^RNxES+vVXNq`n% zWqn}-eG@gNCtFCV3}7f5pr;rBCaZ;cvrO7@7|(d#+{BhjG0nU?0A!_^_d*8y$vClhvL{V_7yarQ3_3(TWrWkXCmaNJ zSciNOa4VVKxFNFl5o!!(;Sx-|Cw=yM4#$uJ^d0C1ZN6q@dckox<2d{XW0&cxxEset zSoV_X6gTL^B+2v`+6yzV^ifOr&Ho82aMF9AFSt+gBo#wXB}Wv`XoPQBrtXOKIwFfNwNRrrK_1mPf@yxVG}#mDywI4CWPo`1T}V+v9L88 zwxz3bfL_1OXVWUu579yjPN!jxF4(inm-VxuL}hF zbD_kPS^wbK9-Tb>6S!f|#_Dal*?;rIVMkui?!ew&&+bHmku9UJ$z`W1n0R6L?EKrK zCnx-7IHH-xe&wm%$uWcM!kNn!+~LurqLAOgat$8dgOFkDB^MzEP}X80c_E(!8o)8| zn4XA@ko`d5y6BTf_#xJ#A0abtXZhsC8STy>Sq(U_@VNXbZ9q3nsLiDQG|3|-6|A3R z^DAR9!&H*JW}KWuT&~t}cGY{56XYP&ERTl~4o-T%5{Z&$VgDh-1JdWAzXAC(NVZiB z%Yo`mDiKp5*J0_B^eMxsvC8lz*9{cGM3mWo8;pEJ2F}NLzhbfp{3#Hgs{j&H0HQ!_ zB7qZB##mhvp>ta0^gYYVb5gAihRVr3eUx(th;0@x}q-X@Zar`QZ2Ep$Lzf93M_?_azUgiaj2FtCUubS>6@)e3- zz#@E5H%`%D{AECa-#A4h_+@awiu{XfWq-|lqVu-+bwI#UM=KcXbO+8m)p0puNnWXjLT>>8#TR_;@32*3AnU#sc#kjQiG)+^?Rxi zs_^UkE<^kOfiFg}Lf5{YLGOL#4+4C#@DC%mqtrt1X_YiTMFRLwqgRb2jZgEYtl*yy z&2;T*)%>s7S_-ERUqR#-_)DFA2D1_y(Y5Uli*~Imi|uvTyI(ot|CL&nqK|#xeSo1I PbWHj}gZ}>eZ;tOjt^fni literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-listhelp-debug.app b/src/test/samples/golden/inc-listhelp-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..b8e21a65deece6f1af2920404c30a6dd8cfaccd4 GIT binary patch literal 6707 zcmds6e{2)i9e+SVGZvb(WL-CgWNrLfr!i8XnlvF1k|U(WaJI3NcBJfLzvP@cKJV^K zVdD>XaipDYo}!AXs6ra;kQ%M*m-Y*!Of`hXAn^ySSXF6AWfPSyOE#o#+6L>EecpS% zi|aVXWXk^7Q@+0UzCS*n@B6;zJHJaukJNga5HgFl9#H>`IUH5@UYK`>D8m9E&M8Dv);F)hTf_7Kv-_K-XET6w55jnM;i#4Bt*>s zMdnem^CJWdUuw2ydM9!}-^lP^eHU;ed9x{8#I)g(J3?CX6QnDdN}HtUm>b{3{NXk{ z)4-$baPvrhaycY(B;|YR$Cfdnfuwr@kQY_iMhW zHk67dXv{an>)`9x(#e=Fp7fb&-0-0iiFi~o<63eHk#rq}Oe#Z^Y}oRcL59)^GagSG zrjm?OqS8dFA4p__afqAu90>Y1Bx0of;68TA{gP##dy{=4|L#cWg_&=ok1E;Z`>X)y z4d)wV=ez&r^%==GWuIC>NI^QmVywxWbRs0HjaZ?GGbntl2~=$-3q-m1)&-oP8*8p* zs_=>7d|fE0K7^pbU9z){n|&~aoK3>DAf06JIq77#tUkiQS9>69;5k|U8zi?(cK-G+ zo>xISB?3+%oAo^4hOqE#wg-c5 zDa$$Ov>b5`VcKkmj2y)}u{u15O)h4c>%DC6sG{d~zgj`!h}1 zjXZd4vTh|x5+1Yt*zg%t>PfX$)B+ zdgM6RLb5|ekab}spU&Dmht^puotfTL{JO}U$Oj9MjmXZI;mnw}ilE`yUFMgHh~4VW zBH0>lWMqiSfMR)(0<%c}laU~J|2Hir+-IAIq?q{xvbV$3yon%zpe{#~z;Tc~+x`}BMJKZ3w7x7GpI1dVH z+*t;?t%+`1x+#a8J9qeMS_S7edMLrU44g-1&AkQb76Zshw*sF1s}?2)&-E!5TSYfGgV*&-TST=rB2 z6K`zHo_~8ZeJE&lD@oJXPoHno2Mn?k&s?_Q4)^PnlHbB}CBD4RK>Bbmxe76WvKkx7 z5BVt2UOWa4YN>dX><0q3#f}{153$|aQ8Ml-y(cf8(e4V86@UW^&nuWt?$rzvW>eRW zksNYT!TM3QARUMsCe_=`1i6H~Tx&7dHSf`f$TKjrIvz$kc+v-{5+g6d|2D)2(wAYs z3;8ohww6q*2-U7r#iZm8JYA9w+Ls3dM&~c!J89t4ktm?#iqK9E^Mterg!X0A*FZ0o`)sy%BQAEfNw=B(x~mj!E$t zBbn0UNwc3^0eArX$6=X*a4u1n11Xv$Nho;I-XiT`8{rk8Y;)1QofG1#=l!gD3)#83 zM}{zA@vET=abR+*-=5G6N}2$;3Wl$gfELTmevh%K8)OHZ9!BI|hyr;T@faYSVZW%$4>bGR>-`LXly7fmrA98eYU7eP@GybAt(Q7i;r z9lu73D&Y6Tzb8d?@LKSzP}~!|M*NBtwSd=-Uq(?Q_xj<^0$({P_;^ zL8@!Y5X$hY$1cM9|A8-Nu|%I8yLd&^hWJ?e_Jxb+uP?>xt>qaV@+;DP|XIli=RXnM2j9HM4B z*F5BEx(sARqb7~DxGGn{Gk9=LYj&A*$U_EHTO#2NyOKElo8B-;hmeLB9Hg2aqq<1C zIAAXXL>lfCx{@(^ROH&J$78NqfF3-SqHQV;`w#PsyrEw)qpBe4ifTzeHT8IOZrqO3 zdRs9sX5Xju(S+b&LziO>2W6TLT^R_OijIm1CQljFwM0p1hpwd<`dny;d&qdZWz2;N zWv8z6mFpPOTqX>Tm84_?RqXQD_7yEci4!vU3I5&kv7%2LbK3U!!U1g8o`=7C{uS@u l7WQX{|J&H~DMo}Q-o%N1rBD8{;8Wtw1^18i1fwP6{{Y4|!kz#C literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-listhelp-release.app b/src/test/samples/golden/inc-listhelp-release.app new file mode 100644 index 0000000000000000000000000000000000000000..9be9c97fea3faff03d4ce71b7b6f8c3e5a52eb13 GIT binary patch literal 6655 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 M>5B{nwTkwC0Ds|VcK`qY literal 7346 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 Af&c&j literal 7294 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 diff --git a/src/test/samples/golden/inc-net-release.app b/src/test/samples/golden/inc-net-release.app index 550b9f48cebc382407100361ffbc6dcf0f85df1d..f6d0701f0fb2c8b0915fc42cae8fd6c98d192813 100644 GIT binary patch literal 6508 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 RSRh)rf5@|l11alg?LYYTg`)rf literal 7228 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 Zi}yb^bOU3ja`G0>TG!rBi#J#j{RfFp$cX>| diff --git a/src/test/samples/golden/inc-prefs-debug.app b/src/test/samples/golden/inc-prefs-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..03dcf90954c57ca856f9912d922b8639d10ad7d5 GIT binary patch literal 6560 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 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-prefs-release.app b/src/test/samples/golden/inc-prefs-release.app new file mode 100644 index 0000000000000000000000000000000000000000..2f86ec93853ae49694d537a9ff2a5e254b47057b GIT binary patch literal 6508 zcmds6e{3699e+)e5K6jaTi7PLN|xv^hA7S2PDrRyeqDaBx;@*oosDM2iO;zUw$J#^ zUB~)|M;wXMVH8cBR!xqU{1PxaDQIp};#SHZ-BhMYR9ZltbRmIB0|70A@Okf@ zua51yZl>@DPx<=ZeSdsD-}im*?)f=AgF^chA!G$@8?GW`1H`q7kb3*R4_DgsPx0$R zcK>W0Aq$O!T!qkh+S2GB4J8sik~Wf561tisSKv-nJ~^d_)o0@5N&^(bAyZ zHI`7KQrHz^b#O)0R6Oib;x1iQG#4rnRYpR(qQ-X zhvFj=k)>!N0wl6sJHpJphrRCY(J<+L;vl_b-=LXi-=d$$KN#>mzx-YFQ6-c7fEEC~ z@m!N=eg8kKJ`=f?%;Osg$qOfGj4_uLPWnW-88Z~H#<-6$i>e)y~pwvV)ZRl z!YK-$6;1_3`2hyL*#Ws9o^uVqMRFTN>+k<& zdF6%EJm564*~S8{aSO|4pVupz?$gN0(?D|-(UZS~PD2}3u3S%5*6S>w&1bzw?#DH}K!=@`_;yC7s*5R92udl-+FPx!3 zjM)%oplN6W*u*=AkkdDgU#QOtXAJ+vyf9DQ^1{_gcDeQqTtGf!{)SSDY-t5WYok3d zAw#ohVp(LsZ?;an(~|iD?%P+gLuZX&oj}ym*8%}knC=ighC6Lt`k1fsxrvHyUXndtZ|FVr3ln>Zs z_cJ3ULsS|Rbv6Y?k@`7ZQK@vm`t!e5R!o1BIg~NB(icJgJEt%Ydyt#)!fEuLe0q%d ztUSDohqqa8;fm?W>kBa5j<$VNax9G z4Ri3U^tblh*PlL$XFRL#pi3p6rqQhcGSW19X&mp9F=FngPn!H5^l~Ex9k5&u!a3X% z1_D2~U%9k0!Ja=L5g9owNuX}w# z>(?B?`YAVJ%8Y-qYDXr|{SaYzXJhr&-O}IMap;!U)#)4Pc6BBalxz)+PA=V5!NeN9 zr~6;8nv8k%U?{F@hoomZlcO3rfIXKkxTBGzB$402vk?#PQ;=c2OXeXOP&Q*Bxgo28 z`mqf>p(d0OatH{_7Cv^IZDL*OaWYd-Iy*1+=)w$=jer9S%gdXJ_p6!?vz}C+C0XPo zgLO5VR~l6`T}pQAQF0M^6|8n}!Q7jSk*8p$bv%l6u+w{`P?)>`{|_N9kiG=_O~_wB zvZrWRHdJ>~3h5HL0nY+Sk2IVbElyu3gn`1J2vh%Wfsu_!!~PiUlXNKC zK;%NV)a5SopkbM{wi_zkV zq!QO7GuO0=(^zCn0Y_<{@uG-i7=F@-bu!5^_O2kO0Jj zd=``Y2qXkaB&CQ(y4;U7BMG#}AiE$gTQ(5-?G(+n^Bew|UjLR`x5jf{H~8HD5`WjO zOZfZbV=2Zd$-mgNHlh-Q5`18mINTLWd2BiUnkmY`0aX!y4HOl@tKi=i#qGeW`k=0zqQ?9kK#pHKMJ@OaIAD4G`Dd}dVm+a}BYqVSuvD>f#_IUh!+EW$`&7X% z2`sJ*?>jXq@pSZ6>Qg18INa&q&c9;*w{v#TDt0PHmE%`*s|Z-GERQY0FLx*hsjewO zD8a8Dy9Vq32R>>QVYm@sz$0XZ}c009A zb|&=nW*d6w;a<6y8-N^8!jg*L)TI`#V)sEMtjn!MihfBMmG#XWkl-6 z@6h@x&B4&H6tz*g4^4HIOCS_Yt=0_0!_x5wIS&tFn!^$1dS9`jM~Z~-i}pQShq7{i z+sC=@O{%e?2`lTe-^a_ew-h*vlAq!K8aPI>ri`fZy^q>A6)tpzyaE5hS(E?BRl(6# Y(Np#^7T5v}MC%C*dgpN*rFz=_3!_?!;s5{u literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-print-debug.app b/src/test/samples/golden/inc-print-debug.app index 341ee704327cb6ffab79a51f68b8441c4d3e0b39..99b9624c1f10fa7331d8af403454132a89bc5a0e 100644 GIT binary patch literal 6739 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 literal 7214 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 diff --git a/src/test/samples/golden/inc-resource-release.app b/src/test/samples/golden/inc-resource-release.app index e905505c4fbdef8dacca7e76942d6c0b15b8d377..a0e2ed1eca668cd1bfe5e53465f470bb6ceb2f88 100644 GIT binary patch literal 6236 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 Q-PPkC@XS0UPMfUjKdZMX$^ZZW literal 7179 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 DIf+{K literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-security-release.app b/src/test/samples/golden/inc-security-release.app new file mode 100644 index 0000000000000000000000000000000000000000..9a468d91fce7b27cb1a3bc23bbe20c6154834712 GIT binary patch literal 6385 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 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-shell-release.app b/src/test/samples/golden/inc-shell-release.app new file mode 100644 index 0000000000000000000000000000000000000000..558bcc5401acf4d2ea53c4d0f518d05d94649199 GIT binary patch literal 7527 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 E4`3?yu>b%7 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-sound-debug.app b/src/test/samples/golden/inc-sound-debug.app index 66b45802ea0d8f3ba7721589da1f49673b7ff257..12277058c47e812077b4a78565227b1ce14be893 100644 GIT binary patch literal 6961 zcmds6e{2)i9e+SVGZvb(&AO_rlBMx$oyJIks?vl+NG`w{!`a3*?MT_heu*zT-x=ST z9~*zTizDrH^AuH7MHNzMhcvXZU)t7DiK&LL7$p9n6{{)@scfRsv1F^%P1|7Ivd??( ze03aS3={ifPx;=x_xR+L3AwtMkjoGj&srY+JxV6iN6kc5%~(d3T!K4OMN&B{Y3$d@rB*106fPwUAh!4~3jyS16qN795;tdpLCn9ogN)*#eAh!k+0a4NeYET>qlL-);mF zMmnQvG#N6~V%s%~0} zo}eT}wahS($O>~eH}~%d2UloG(z|njor-U;%!_ZaBl3^OBG1o!2Ypn@A>U^OKyR$n zCcEGJH?PllsiW}pB0|d2VHRUg6{W)wIn|C8in*i0$DTyhHnKpJduvO`4Oe2VWlR-4 zJXUIngi{Y7Xmpe8uH)taOc8gLa4kzm7<^GW5|>jCbMU1;$V2d)YW)q8TOhlC`xnov zEFBdAN0H5P9&klicsA?9Vc7{DMNXksmaB|j>PzUfc46nHTA0dxodB`q}0{Lm@*Njr;TPrTRi@b?R z1(rn{&!PZ+r*r(Bj=~o(?w&0Up0s~?2vO5tjl~@4#3ngp2lMXvkHs#Z8ZSA)d`Dp= z4?Z!~vKS?ao^XPNa4h7$1}}U4a`$E7z2*5?cHvu3#8Vq^9_@^{d2VShN+-(F31o@r z@k3yX$SxH@_L=cguHf(-x@YZNetMO6xyW0P4;G*hlijD_%$U}Spt0Fa)|WlRcxt0Z z_7FERGDPJ;vCyNyEOI|%J1UotxqtfS?5q=Qa|R3cW9&vy{`L{9!&>BKzi<@2XPz4+ z5w{F4`*X|Pw{XVt6iT@i0~jns+>;Cdi`B}aSu5umg6J^>(L3h%j(a_ZXCQc&^CDZP zusbBci}bJErLR4=2iJJfTFJIbInSay0c7V{^z<0+C!@sKz^=5^d(fvAW707<;3J&E zm~asIVIT5Mz`bPm=1oz(k8s9hrZ2%Ics%4h#^D%J#C;dGT@K%~vU|Y`IO7Gpg1O7? z&oLUsjnI#h-DBLL3yUPXhtZ!t4bPBki@585#15PY?&ymSNP#Rhja)WC1H%fxWm!fb zNlltn6;1snT|;W!bi)$&vxfv$b`866s4eO zBXRdv0>S->Fk;E@FHxT=rB2 z6K`zHo`3yDHXXL&if)

    HePVh)Fi$naeiZp5ZK|QPlwz9@S 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 Fe*<0Fy&eDn diff --git a/src/test/samples/golden/inc-sound-release.app b/src/test/samples/golden/inc-sound-release.app index 1d2b8ca03252811f1ed82740c9b3cbe49c2b9e8a..a374c3c6cf2897bd69082e966ef7f30190c0f4a9 100644 GIT binary patch literal 6909 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 literal 7181 zcmds6du&tJ89&KvFpyxnrJ`cA^;YO$ooG`S5>goCaY;}K+}Om10?kZ(gB@&N&%Wlh ztxX%d#ksx6+n_@$tnJ!K(O9chg+jM6T(aR+)v7V3O)6VK6I*YTtZO&g@>t7$-#NDN zoL~YI`=eKUzUO?8-}gA*x%XVsv`VUdnlW|>*KFKnPzC6Z)r?tk^Jd)XnlVw_Kc5@F z^eM*D6BtW?$eylyFz)b1B27xHGa86!YLxv1>cn#SFxGoxt*S2`RF(W!k)&KG0MuLL7quw^r2YdIsmBCa{q zE=NcSsnM;D&Zq**@CHCBj)r>I(nd!#9@YXOWe!Zps&U2gB}i1Bf6|XC zBs2EJ2`gjSBE%rWnh>sut*)F=`>7b z1VUojZQc!mJ`M&X^J9BCjbA)BzJhf#KgTvFb6-`MN644?@xLiUnlI+_cQ8k=#%>DR zcyy1og1P+nVw?;54X*Dno_Km3NxqzyI8(bcgkZh^xy!#O&-R#mxj~K2#ZGevrm4Ca z=A*c;hNCss8tidI9tR^U?W8EP(&OTmrkf!>-f=$^R&t;!-Jn$UTd679+%9t=XQM#0 zS19Wl$Qr=3qFXE;^Hw3pqTG(PMWslOXJqY1l(fnhF7lqrCL<7$%hZ9j80EIDzL(gB zwASS2Hv-)!;HVMoi7!G#{~;*4%^jDapCstz7SS0!=N;OFp7U@) zn;?~>W_krH$%d+O>rlVo^#>>QSti5JS@?Ok;&{dRzKZ@+_Jj|}0%hp-u5_L4q0jYR zSY%JMgT1}!=Kh90%T1)#ih13{2T#>i$4`m)bneDzvi9RK-zW7C-HqpV;&A#D3?RP^>xZtQ1;qOI=Ne2le|VnPklIiP z&F=_~ewg~HImK@}@41EaLbNBpO?#8{Y3u8}2W4{|tn_s@(YMdiODNmB+s4=wD$|pD zFuFfAu(})1ley4LO`l=jMHZ4RYb9m6vOQ!VMr1%2DOQrfycy<&bC)iy(GSqRa+`n0 z1bt;FXKOQS9_YYpZr=FR(ZShB80ih3;PU4P)?yY^alv(5qUHD z;WDQ=AF|c@UIKKPvvZ2icZ;{P-v(T+D4W%g4N`aIWi6Lw?|sM%GAKiggk0unFi?!a z7G#(|Bebq)YF{QfqDoASM>`cqmzTeSHPzu$VoEgN4F~G>gb9p3Y&%J6VbDh)vY-yhzZi(K)Ggqc0G3DxL9YEU-yoVR+nB#z4dqV+ZiirDf>=3x{T>;`On=AgLBa*#_D75%B4v1!+>& z$2Syb^nP9lfcA*bt0}Avcs$7%j-A0~MWfES0MJuKk@0#qs-ciK>|>7=2}H`yc-R#P zMQ~z0j-c}qwkolhcY{K8?!a>X2y_he574B195~4agK8(+i0DNWS~T40jVYa>2>T3> z!XOy1j#xNC>)FMoLvJf+4#HRt>Ht;3d@d-zz$ng&h!SQmfWdi%?rfy9v%-#$5y3(r z7JdnJxE-$Ipu7#wyGfNcDPb*EXr;+8&TEJQxOmSLK5^BrVM3?sfQdWa#%3bg<3$l99W{?4T0`vpm z{0US8%LY&YWP;9tY_Pclv=FosbP)7D=q{AB8VleDJ&!mKg5CtV5bJ8t_drj9o&)_1 zlmVRrodFGkE`k`A+6toA+^;}yf^NdQ0Z(Z3QbX(fD(K*v51IvX1f>44F%o2rb@-otPB>r`~7zqxj zQSq^ zxEMS5wc8|k|p}Z5T#k$2?js4%9QnQH-1ENg^StLh=icLU`-Y{?8A3<_Wh-hKvH{|_hmc15egHKq#-~L6 zpxr**K*;rGLasoVKPxG;j|SrLK1B~EB5^}YkjpS{)u%gmgCNKEjs>-^HD>GI-ziLsgQnNrKh;=3TM)SK^3Ew@@yOVfibge(xE zrhp=IFPZ)v4pZmb&E=8#?5o=u{!3Q@m&qAzUK`VbN%kqynVTmEV)3LwY{Tr_4rULt zspS^#WqI2(xrGK|e9`5yoELp_b0hQN>BTP#{2|Q| z)F$GQs1kBa@H{xeS~3=LL}Cs@jpzjEEN7MWjRmiHrp%6tZpCLpqsAMvX`$ zrW=7+P$8-kjfa6mw&_Q>y8n>dxh)zZy@wC7D*GCXJo_fQB7VQm^X&5X(MFam@?(|& zw5D<`GX3Gdd44jvw)8hQ5|WpWGaqvyD;@X9YAa^QN5_SYIghOEXO2ksV55t=i@wHY z*5y4um2334)z89dyi2BgxY_}ghwc!jdFcd$&q^o!vU)!U-&6v*50(o}ze8{vWcs&% z@woERN#Sr3(QM@o*Mx>gv)ApGE$2zZ#Yx7ssrx23;?`_4jkwbf)NqkHsoq$mA?%sx7^p zJI^jOHX|k8S<9Jr`&{&8Sebjb(iesGz_SJR;2RJ7)qSYP+dVYJHLY1`HZRR0N_c0E zfz2aRML3yfGr45i;xTj<%w%e5hyA$71BeF`koL*+YcOU^yM@zKq09KHjp$eR+hq4~ zB_o4Z3KR`C1xAtl6v=X$ZaON! zv&vsPbB{l96wi3p*v^(pKE=G-0c56__tF&JC*#E0$DTCxD*DxC4BAH>C4@7$CmaNJ zSciNOa4VVKx+Svr5o!!(=`u{5r(D(+4#$u@^d0DiExu-DdeL?`Ydic1W0&c#aW{?^ zVaZFTGu)s9lO)sQXfMsd(nU?-xA;C*;I#8lpZB2TNGQ6NOav82IKW?7hUN$GQbTV(Yd_5>gOjG)GwWz28R z`z`6Z?4dVr^4T==^h30ef-@-?X9~t(Ub?{mveFHgtX|{@mxKv_A3T87e#7nY)87gN z`U|1Nl$rnF*$z*i`3cZ>u|Z8Ypf(cPGbD?cRIq-M z&8>_@bVEt>8c}i{aTT?;b5Y%&m>`cq&GL8@;b5nCD}fMs4)z~H93XuG`dg3>K(f1R zST*#bBy`5>H%t_Q{xmep(MYe3xFAxDZyLcr0e79@KyDDI=j;)zJi z2$PEdcY*&HG>Z_uV2c}~7d!w4JkTw9`1I5?NK<#w1|6^?EI@t%Kd*L|<2|!+e{0zeWPK<)-2avOn zMMweic|_g~xdQnEsCbU>jt0uU*hlFc>%xw zek|oU74es=)<#r;P=OE33WK|1S?RkHf6Wvt!2wkje+?8>!E4~(6~$`cwejnur~!UQ z{5w+A2CoOd2E`r0>%^~0Q4e_i_!Sg&g5MQ>iK2e+yTymS%nKTImeroGs_r84b&6lW zB79KSPf=(5B|w2+KSe$GC2+us{EKUOf7N`V^N#p6K)_N%D;R6z*LD}Vs%=vPzrwLR zGooF}Nrh#pzeav)xRkq>+N;@Djej-c5?a+pRj-x!Rn4jbuIyatTY2(Efkmi&3o8wXbK`d4K6Zfa41PL{sc2G!dMu68OaJaUEnwu@=_ghb7)6O)5d& z#Sc}y4{Q_h3CO;wQZf6h4~Qc(Q}_9X-Pg+Q{{sg8GFt!u literal 7283 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 diff --git a/src/test/samples/golden/inc-sql-release.app b/src/test/samples/golden/inc-sql-release.app index 0f768ec6c52d61734a2ccccdc43b8afb72e2beae..d6ad6cde9e102e8509fc78f3b7d1a92068eb7d44 100644 GIT binary patch literal 6264 zcmds5ZERat89q&u5RH@s7Pg5}$rAlyh|;X>goG;P>n17G>9t+k*=SZA`#RUb@%4PK zw+#Bj!?wiL#VDFOt(q#4jY>qUe2D>_6g0OfaVzylH7jHJ@5NI&wJi;?ma$tU{va!C4>}FwxX6H8z7GR2x+$OhfuR(d`#32 z+3nL!gj{bW1{uWo1fFI@#(Hg9xzZA=R$xu-~Xet{f_#ghiH4RiB5m_5wW zD{b7%@^)tPi%rD%oXcf7yIgMd+c2=a?QZoL8ZzzSY!1dYW3F`P#}>jfOJ5N9BbpVXZ}zy=Pr_-kN2YtY+5we^?hvL0=>&t%NhkcW`Tz&tR0g>pmWwUFLvR~p`nP}a zxC+uq;cybsY~>Eugoa16*X@=q=SjrmYGJX;XsKUBqooTgS8Zlp^cC*V<)JT^FlQO2 zEa#+I*+-9H*ldMlr!h|~58uQh7o*ILU9ftZ&9`Mb0oT{CyohyaJ1Ybcz9S zW>cC+PfHiTvTvGF-q@Hv*PN41ncnjS=@K(5NLOZfb?qCdKt6B%mQl)lY58Tk(H@wT zVNta4C^F!;I8A%b9WeT=ZpFnR~a=7lrlEvqkpc8;|(aeW)ipJv7BN?Kx?#Ak864cxR7+ z%_CDqIGJa&`DDi8F?1KrWNLYb{kX_OhzApp@yYbdFlJ1X*^sD=AvirG` zk-;kkiYA)^qe%XOt*B(mNB{iKV$t%pSz{S<3wse1zHphZbOE zes(K;9W|yWlTWG)U@YUIrx^eytA%;9Qb;ib(P9XqHCt|-vRe#KgY&5MJewys9T(s^ z<*(iOuRn1V&v?$*&X!6c#k@NKWTu$+avJZGNn-6|Pnvoa{c0-)?W2w|!WrBX4gx!@ zL%s;Ol}vBl650CzH3qYM879tCE^7;iV@MwQ7IdQ)U$Zj3Xgi#>9e#kZ%k-DHo5YK- z>?PA#ZqS8ElId}@m*-*WqNea$dKW8j#(8MKdr)#D6kSUuf{G&?;IAx0bA%LKNkjtC z$hVYTq}nW|8RC6bQjr0v&pSkB#QP^FT~Dt#3et59n^?&p9Tx=#9r2*%}I)T(+x%i5GUy_P;?b zG37S=ftaC>C{On$CUmkNdoEjWN5ct4A-{)ZD<0m*A>(+LT!QF8*@T7UgnSfe7~8;M zEglJy5g>3~=&5PGiS=pIWWLl{-g&V{mu8S`1RPj+T<&CSSkn!tjfD0L$sr~ctRH1_ zD-#jjP!jz{l$=LgC9UmTQV%Ak$m3A6JRU_j*y-I$AVi*n{reCHNMC^d7UVsU?5-G= z4b`7e0)|3v!m>m%po}LcD#MpbZlLhSL(Kk3F!B-U*dL=qioquE$3S?#0!T~&h&++0 zICfAOV=a*gU1?^v01iq%2&ba!feD3W^&0w`5chV-ky4Tna5Sm~$zBYK`{=QFA`&yg zb{nF%m-u54t=_gKQzZ2$Ve~^zzE_@YyT>t$G8|m16fzVZh>F z!vXk#sg(WxsHQ8V1AzBI@s$ctJKgO67#q4y_QB{=@EnE&ATL0E0%3nA#=-PG$XUn| zqzL&8BJYJCb@1uxh2UJ3z$b2x>mWOdwXp_2Eb%UBS_$$l zeyHLtS5iql!<8e-w9(4!B`v&()q@cw8tTXQT%)~wJA$vbx9<+ag`;rOkC&|u#EjcF z1LIKvhch4!&iLrdb-m%R5;T-hDfU`5|5jW}VfW!!Mt*L8<7ZuwpcdQnh^?$qv3$J> i`_joveDJAZXm_y_=etPvd-nZ+Z`6JDL2(jgUH<`XhcEd6 literal 7231 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 NcOLk(_=qLee*g|%#)<#{ diff --git a/src/test/samples/golden/inc-system-debug.app b/src/test/samples/golden/inc-system-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..ab9e75e9d78256a59848aab972d48b1aaacc1368 GIT binary patch literal 6852 zcmds6e{2)i9e+SVGZvb(u&x_JvNV3JQyD2xO`4D($t9s+IQy_2I#TA?FS$!?pXWOR z+4#d<9BHSUr>LSTs*pxIq@kAm(teamOf`hXAn^ySSXF6AWfPSyOS(>7wGGxS`@Hw; zi{lt$GO<7Ql<(bp-yfgP_kG{HJKv?VS86*+2$?}!gR=}-1o12*q@g(O#+fzq0dan` z*gvzFkQ+-0xdvhJtYy$Y7)~ZTsS!zONmEaet8iy3MNOMg{cwU@ZG@sfYT*vTVeiv|~d9x)@#I)g(J4D*@<78(dnKnt$F?Vh~^M~8W zbQ6!V1FfU^iN(bHqR(f0SNiAUl-#0!mHu+_Nzeo2( z^mtN>(WocR>);vC(}}1@OL$CGGd!q7OpAm~O;2njQYwW)Cc<$_)^2>vAn|m})UAs}=*{4@`=)Ubq&d;|xA09S_OsBOH9W3$g;96OF$`a*Jf=Z~x+X z6{Hg)-~_T+!vn4h3(sbY-!I$V6UfQe$a0m@Qy)jCaV2)H+Q3xKYdm13;=EGAJjpO+ zIVYWzgU$g=o9&R%BUmR^hi9?L#Vm7um+b8g)_bxuh|o8%y@++8Ef4R%z6Otibcz9S zW=lE;P2);{jlOM3d2`9gg@&AT$_iX8NLQF!LAo}^PuIVJ6UfimzhRUz-&!HrSyG&s zlx0~o@hr07w_8WwZpnTL-=awowa!mZ8KInGqt{Wxyaqf2Mdr5%Fb8e%$PQbpplvF=2wb{A$41k zYy~$mGDKxSvA9TqS)_l?c2qhObUyg!%#0mqviq{uYIY+ieCIgUVH0w*ob&uENE8%4e_Ua12Rt-hr*x=6hClE*AsN7XyBPxy#Nk zFdD**(2bIvliXn?7D;xFp+9vFo<7GCag%?-4jl9D?F{UeJSl4E=~RSz2EzQ7W$KD+6C*2sC_7tQWm^QJ~O|p6c&xD}!prFQ` zWiVuohivJFtT?xB@zt~n&WGrs1ZOjF9-T3J3ersmkdtouWc4yfxFTHm{ou3M?Kk~O z$a!5LI6oCeESdEWUhRRgvp+%@?%CMAi)Zz-PaO8hYu};l?C|VJCK=fR8e3fUR0R`n zY|Ng2U3x0+H$&lsY3!kgcccalvJKB%w&4yAq$nl7gXdCQyiY^=aWA<7F@Ulh8_5fq z3)F+hz&<^xMaUi?a9i}y5&jTs*N>2MC8hi1#WT9Jf@BHcz{2zLrxQK8VZv;t^kI@i zPAXXEvia$tW|%b9VaCWsEW3e60+$SZ?-vj7{AjTjBIDB6}cV$cvC4L)g!WelUFx zavm}XnSp#BneTvHgZv)yN61GI6ZC(8tODLz$bQHhkSmb4An!o_4*3MK3JG~2K1dMa zKt6-TeGC$Y7{l0fankO6vKd*RJqg(W@f2kdVLwmNigqy&$ISh2`TN#H?)MEo|G&iF zu;CK^{`o1wsWrFe@DHiDiE5eEbDd%m)WlRs01|R0Xere@_&50AR>&LI4s1y92@Lh`f!S59x{xWaq)YC!FRy{E9x(f<@&1igz~QVH9){p!zvhSDgu@(699p;17 z)>I%=;Ma~_fc5_aU(Dh@9Xs25y^pvr1bAa1kGmth!ZZ=QiSf6^hlnz41hpvD@r{~x z;KQsrR~6Etruy(4uO6xms%Fa^U3)Ug6$&+7wB^i0iQu&^VW_&vhaGxMPjQ;r;qGuh zjWsi8Zry{Azsuaw&giFEA1`^_$OjPRxl>ex(ZgUR;HYz;iYpmoM)~Rqbg0otEUZt!FwT< z(paaK;#6GIsi*s6v;!Z%#(1`T=!KCE8=QC+Mloq}HiYNF=fgc%4xeJF z^HJ|?cmFJGd)hShgu#o3X>(6mH#nxEY6GTX;>!bHC50xUyXY{VrAaWjG@9>5dI2Rlk0?Y)@D1 z^8X`LwFo6%*~!oF{~z|vhBYmsCpJGJ`qhr?LV6wkrB`zCF|eAa(6O%(AK*G%!a8W; a)1c^AJ2JPwd`^5yVfM~oum9t+@qYk)iuiH> literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/inc-system-release.app b/src/test/samples/golden/inc-system-release.app new file mode 100644 index 0000000000000000000000000000000000000000..d32c98931aeae56bc7ffa0e28e2ddef7b339da5e GIT binary patch literal 6800 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 diff --git a/src/test/samples/golden/inc-xfer-release.app b/src/test/samples/golden/inc-xfer-release.app index 91d51c70e555059bec82f6115a97558aa29ff919..bdc2227da876be7919d845c69fdbef589a045df9 100644 GIT binary patch literal 6418 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 diff --git a/src/test/samples/golden/notepad-debug.app b/src/test/samples/golden/notepad-debug.app index 50f255fcd4ba25cacee4b67393bf87ecc146a517..c99caa0841376ca5bded8d4589e20fede321c1b0 100644 GIT binary patch literal 12039 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 J4;P5%e*u`Y;zj@f literal 14074 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 bw>Yr*E{y+`jreBA*+j9*Mv_~gql5gvrs4T~ literal 12533 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 dvr~b-Kms1iq9haCUH5q5^<#(&GY5><)<3a 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 Z4J7`l_u$_b8UENtn0ng>^`>=V{6A-`OLqVO literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/terminal-release.app b/src/test/samples/golden/terminal-release.app new file mode 100644 index 0000000000000000000000000000000000000000..36188cc4679e55462320082742339c856dd6084d GIT binary patch literal 40999 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 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/welcome-debug.app b/src/test/samples/golden/welcome-debug.app new file mode 100644 index 0000000000000000000000000000000000000000..13173832d0a57452a54d430920383fb501fe08dd GIT binary patch literal 10033 zcmeHNeT*DOl`oGy&N8-RCw2%T1a*wPIfAn1bv}d+S@L>!*1N=G_s)1`oe;kLoKmo49k{!==a>EO{d}||0U8^|5-iVb@bwopm61Q{bw_mH#X8; z$#--ycAhbF8YsGu(!*~+VCMNP^~*=kHUFWT@W1o}z|GD3oAU~@0hZf-w({y|48I@Ow0;U%pq{Gg3HfnCxpTk}oJ)8L7URWy9dv3pqBb>T?csB*Tm z=Pr*`gNkoiw&xpmku#H5YU4m+JH3a+@Zh0rW@n|u`VSwVOY@(|^5(bb7xs@9a*tj9 zB--#&gMEb@fY!`>mmYrl_rgDO^P6WMSj*T#`f-w}pKqog&*|nyM5quRmpt`z@Y)^{ zgu92gL-8_@)-f zR&btA{ws`Iqldrzov>>m{iGCl64vYx0hStlr2x37bZeWUG#rc{du#ZCJVH zIwFN%7XsUI;g=)K(}bz(&Gc!#5I&4)(+Zh8i8zrTCa}omESpEa-PpUX{v$msL-dxBw@9W@LNIykDLUTQR7L?>RfX#ico}TyD&OEoSnSQFCe|{nTBC#!`Up^%+ zuY4F6C_mfy7gDK4Swr zBYkGCK2pz2hu{9KT;=EI<{O#m&9mJ?_{{lr>*138nMP(dTNnwy46gdUJHjtW?){G~ z(T(r#9yRa7_4t-tI4wptHq&Po(q~{Lq~{)mwwxYvDO7)UZa$c82pf8r>cRA-JJs!? z?}t4Iz-&Pe-w$Tev`Y%jEbaCGQX!6-dz7-RVvv+UY8omMN(E*S{2i^RV7d_g_ivV# z8u_lq(b@X#v=JvXI2lxh*41K}f$&rzJR;g|$(@?Z4Mf9R28xW}7*H!YQgX_DRo zp!zgPUz)*wa-22pqdU#~0qEv>Ou7(iEre•I#SchT}2rfN*{dMWRpW}+jTzUyC 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 literal 0 HcmV?d00001 diff --git a/src/test/samples/golden/welcome-release.app b/src/test/samples/golden/welcome-release.app new file mode 100644 index 0000000000000000000000000000000000000000..c366335ece366f4a907e0d3eee2897927e4ce991 GIT binary patch literal 9669 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 delta 5098 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 CIv7|0 delta 4753 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 EKiJZroB#j- diff --git a/src/test/sdk/sdkTest.sh b/src/test/sdk/sdkTest.sh index 29bed1d2..fe4f63e6 100755 --- a/src/test/sdk/sdkTest.sh +++ b/src/test/sdk/sdkTest.sh @@ -33,6 +33,9 @@ # resources, the ABI stamp and symbol resolution against dvx.exe and # the built modules (src/test/shell/checkDxeExports.sh), help compiled # - the library's BASIC declarations compile in a BASIC project +# - the control template (a control written in BASIC) builds into a +# control file: the resource-only marker, its module, its control's +# header, toolbox icon and name, and a BASIC program can use it # - the game templates (C and BASIC) build; the C game carries its # icon, sound and music resources and a help file with its topics # - package/install/clean lay out and remove the right files @@ -59,7 +62,7 @@ BMP_HEIGHT_OFFSET=22 NOICON="$ROOT/src/apps/kpunch/dvxbasic/noicon.bmp" # The generated projects, and the magic bytes of the C game's sound # (WAV) and music (MIDI) resources -BASE_PROJECTS="sdkApp sdkWgt sdkLib sdkBas" +BASE_PROJECTS="sdkApp sdkWgt sdkLib sdkBas sdkCtl" GAME_PROJECTS="sdkGame sdkBGame" PROJECTS="$BASE_PROJECTS $GAME_PROJECTS" WAV_MAGIC=RIFF @@ -125,6 +128,12 @@ fail() { } +# fileMagic FILE MAGIC -- FILE starts with MAGIC. +fileMagic() { + [ "$(head -c ${#2} "$1")" = "$2" ] +} + + # hasExports MODULE SYMBOL... -- MODULE exports every SYMBOL. hasExports() { local module=$1 @@ -315,6 +324,7 @@ check "generate app sdkApp" newProject app sdkApp check "generate widget sdkWgt" newProject widget sdkWgt check "generate library sdkLib" newProject library sdkLib check "generate basic sdkBas" newProject basic sdkBas +check "generate control sdkCtl" newProject control sdkCtl check "generate game sdkGame" newProject game sdkGame check "generate basicgame sdkBGame" newProject basicgame sdkBGame checkFails "newProject.sh refuses an existing directory" newProject app sdkApp @@ -331,8 +341,11 @@ check "sdkApp: real 32x32 icon" iconOk "$PROJ/sdkApp/sdkApp.app" icon32 32 check "sdkWgt: exports wgtRegister, its API and ABI stamp" hasExports "$PROJ/sdkWgt/sdkWgt.wgt" _wgtRegister _wgtSdkWgt _sdkwgtAbiVersion check "sdkWgt: resources" hasResources "$PROJ/sdkWgt/sdkWgt.wgt" icon24 name check "sdkWgt: real 24x24 toolbox icon" iconOk "$PROJ/sdkWgt/sdkWgt.wgt" icon24 24 -check "sdkLib: exports its API and ABI stamp" hasExports "$PROJ/sdkLib/sdkLib.lib" _sdkLibAdd _sdkLibGreet _sdkLibVersion _sdklibAbiVersion +check "sdkLib: exports its API and ABI stamp" hasExports "$PROJ/sdkLib/sdkLib.lib" _sdkLibAdd _sdkLibGreet _sdkLibVersion _sdkLibXor _sdklibAbiVersion check "sdkLib: resources" hasResources "$PROJ/sdkLib/sdkLib.lib" icon32 name +check "sdkCtl: a control file (resources only, no code)" fileMagic "$PROJ/sdkCtl/sdkCtl.wgt" DVXRESONLY +check "sdkCtl: resources" hasResources "$PROJ/sdkCtl/sdkCtl.wgt" MODULE CTL0 icon24 name +check "sdkCtl: real 24x24 toolbox icon" iconOk "$PROJ/sdkCtl/sdkCtl.wgt" icon24 24 check "sdkBas: exports appDescriptor and appMain" hasExports "$PROJ/sdkBas/sdkBas.app" _appDescriptor _appMain check "sdkBas: resources" hasResources "$PROJ/sdkBas/sdkBas.app" icon32 name check "sdkBas: real 32x32 icon" iconOk "$PROJ/sdkBas/sdkBas.app" icon32 32 @@ -355,7 +368,7 @@ check "sdkLib.bas declares only functions sdkLib.lib exports" basDeclsExported " # A copy of sdkBas beside sdkLib, so the relative paths still hold. cp -R "$PROJ/sdkBas" "$PROJ/basLib" sed -i -e 's|^Count = 1$|Count = 2|' -e '/^File0 = .*help.bas$/a File1 = ../sdkLib/sdkLib.bas' "$PROJ/basLib/sdkBas.dbp" -printf '\n\nSUB cmdAdd_Click\n MsgBox sdkLibGreet$("DVX") + STR$(sdkLibAdd(1, 2))\nEND SUB\n' >> "$PROJ/basLib/sdkBas.frm" +printf '\n\nSUB cmdAdd_Click\n MsgBox sdkLibGreet$("DVX") + STR$(sdkLibAdd(1, 2)) + STR$(LEN(sdkLibXor$("A" + CHR$(0), 1)))\nEND SUB\n' >> "$PROJ/basLib/sdkBas.frm" check "sdkLib.bas compiles in a BASIC project" "$SDK/bin/bascomp" "$PROJ/basLib/sdkBas.dbp" -o "$PROJ/basLib/basLib.app" rm -rf "$PROJ/basLib" @@ -420,6 +433,10 @@ for f in apps/test/sdkApp/sdkApp.app apps/test/sdkApp/sdkApp.hlp; do check "sdkApp package has $f" test -f "$PROJ/sdkApp/pkg/$f" done +for f in widgets/test/sdkCtl/sdkCtl.wgt widgets/test/sdkCtl/SDKCTL.BHS; do + check "sdkCtl package has $f" test -f "$PROJ/sdkCtl/pkg/$f" +done + for f in widgets/test/sdkWgt/sdkWgt.wgt widgets/test/sdkWgt/SDKWGT.DHS widgets/test/sdkWgt/SDKWGT.BHS; do check "sdkWgt package has $f" test -f "$PROJ/sdkWgt/pkg/$f" done diff --git a/src/test/setup/Makefile b/src/test/setup/Makefile index 27a3c146..881d0120 100644 --- a/src/test/setup/Makefile +++ b/src/test/setup/Makefile @@ -22,7 +22,8 @@ # SETUP.EXE's logic (src/installer: the package format, building, finding # and unpacking it, setupCore.c) on its own, and the Welcome program -# driven through the shell fixture. +# (compiled from its project, run by the BASIC stub) driven through the +# shell fixture. include ../dvxtest/dvxtest.mk @@ -43,7 +44,7 @@ SETUP_TEST = $(BINDIR)/setupTest OBJS = $(CORE_OBJ) $(FILEOPS_OBJS) $(SHELL_OBJS) $(SHFIX_OBJ) LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) -CFLAGS = $(DVXTEST_CFLAGS) -fno-omit-frame-pointer -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(SETUP_DIR) -I$(MINIZ_DIR) -I$(SHELLDIR) -I$(SHELLTESTDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(BINDIR)"' +CFLAGS = $(DVXTEST_CFLAGS) -fno-omit-frame-pointer -I$(DVXTEST_ROOT)/src/libs/kpunch -I$(SETUP_DIR) -I$(MINIZ_DIR) -I$(SHELLDIR) -I$(SHELLTESTDIR) -I$(DVXTEST_BASICDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(BINDIR)"' -DSHELLTEST_BASSTUB_SO='"$(DVXTEST_BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(DVXTEST_BASCOMP)"' .PHONY: all run clean host-tools @@ -69,9 +70,9 @@ $(SHFIX_OBJ): $(SHELLTESTDIR)/shellFixture.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(SETUP_TEST): setupTest.c $(OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs dvxtest-apps +$(SETUP_TEST): setupTest.c $(OBJS) $(DVX_MAKEFILES) $(LIBS_A) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) | dvxtest-libs dvxtest-apps dvxtest-bascomp @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -o $@ $< $(OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(CFLAGS) -o $@ $< $(OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_BASRT_A) $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm host-tools: $(MAKE) -C $(DVXTEST_ROOT)/src/tools host COV=0 diff --git a/src/test/setup/setupTest.c b/src/test/setup/setupTest.c index 646eb489..87d88b0a 100644 --- a/src/test/setup/setupTest.c +++ b/src/test/setup/setupTest.c @@ -28,6 +28,7 @@ // through the shell fixture. #include "shellFixture.h" +#include "formrt/formrt.h" #include "setupCore.h" #include "pkgBuild.h" #include "pkgRead.h" @@ -44,6 +45,8 @@ #include #define APP_WELCOME "apps/kpunch/welcome/welcome.app" +#define WELCOME_DBP "src/apps/kpunch/welcome/welcome.dbp" // under fixRepoRoot +#define WELCOME_FRAMES 400 // frames for the program to start or end #define APP_CLOCK "apps/kpunch/clock/clock.app" #define SUITE_LOG "setupTest.log" #define MODE_DIR 0755 @@ -59,7 +62,7 @@ #define INI_TEST_W 800 #define INI_TEST_H 600 #define INI_TEST_BPP 16 -#define CMD_HELP_ABOUT 101 // welcome.c menu id +#define MENU_ABOUT "&About Welcome..." #define HELP_VIEWER_DIR "APPS/KPUNCH/DVXHELP" #define HELP_SYS_DIR "APPS/KPUNCH/PROGMAN" #define FAKE_EXE "fake program bytes" @@ -115,6 +118,7 @@ static void caseAutoexec(void); static void caseCancel(void); static void caseCheckDest(void); static void caseCrc(void); +static void caseEmptyLists(void); static void caseFind(void); static void caseFormat(void); static void caseInstallAppended(void); @@ -348,6 +352,35 @@ static void caseCrc(void) { } +// Nothing to sort: a package of an empty folder (no names) and an +// uninstall whose list names no folder. Both used to hand qsort a NULL +// array, which is undefined even for a count of 0. +static void caseEmptyLists(void) { + tCase("empty package folder, uninstall without folders"); + + char dir[PATH_MAX_T]; + char zip[PATH_MAX_T]; + char path[PATH_MAX_T]; + char err[PKG_BUILD_ERR_MAX]; + char topName[] = "top.txt"; + char *names[] = { topName }; + + snprintf(dir, sizeof(dir), "%s/emptyPkg", tTempDir()); + TEST_EQ(platformMkdirRecursive(dir), 0); + snprintf(zip, sizeof(zip), "%s/empty.zip", tTempDir()); + TEST_TRUE(pkgBuildZip(dir, zip, err, sizeof(err))); + TEST_TRUE(exists(zip)); + + snprintf(dir, sizeof(dir), "%s/flatDest", tTempDir()); + TEST_EQ(platformMkdirRecursive(dir), 0); + snprintf(path, sizeof(path), "%s/%s", dir, topName); + writeText(path, "x"); + TEST_TRUE(setupListWrite(dir, names, 1)); + TEST_TRUE(setupUninstall(dir, true, err, sizeof(err))); + TEST_TRUE(!exists(dir)); +} + + // pkgFind: nothing, a damaged first part, an appended package. static void caseFind(void) { tCase("finding the package: none, damaged, appended, parts"); @@ -807,17 +840,21 @@ static void caseUpgradeUninstall(void) { } -// Welcome: closing with "Don't show this again" ticked removes [shell] -// welcome; unticked writes it back with the program's path. +// Welcome (compiled from its project, its library calls made for real): +// closing with "Don't show this again" ticked removes [shell] welcome; +// unticked writes it back with the program's path. The window's +// callbacks run as the program, as the shell dispatches them. static void caseWelcomeApp(void) { tCase("Welcome program: the [shell] welcome flag"); fixShellUp(SCREEN_BPP); - fixLinkApp("welcome"); + TEST_TRUE(fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, WELCOME_DBP, APP_WELCOME)); TEST_EQ(platformMkdirRecursive(DVX_CONFIG_DIR), 0); writeText(DVX_INI_PATH, "[shell]\r\nwelcome = " APP_WELCOME "\r\n"); + basNativeHostSetLive(true); - char key[PATH_MAX_T]; - char src[PATH_MAX_T]; + AppContextT *ctx = tCtx(); + char key[PATH_MAX_T]; + char src[PATH_MAX_T]; // The help viewer and the system help under their canonical (upper // case) spellings, which is how shellOpenHelp names them. @@ -829,10 +866,15 @@ static void caseWelcomeApp(void) { TEST_TRUE(fixCopyFileTo(src, DVX_SYSTEM_HELP_FILE_LITERAL)); for (int32_t pass = 0; pass < 2; pass++) { - int32_t id = shellLoadApp(tCtx(), APP_WELCOME); - WindowT *win = tFindWindow(WELCOME_TITLE); + int32_t id = shellLoadApp(ctx, APP_WELCOME); + WindowT *win = NULL; - TEST_TRUE(id > 0 && win != NULL); + for (int32_t i = 0; i < WELCOME_FRAMES && !(win && win->visible); i++) { + fixPumpShell(1); + win = tFindWindow(WELCOME_TITLE); + } + + TEST_TRUE(id > 0 && win != NULL && win->visible); if (!win) { break; @@ -841,33 +883,46 @@ static void caseWelcomeApp(void) { WidgetT *box = tFindByText(win->widgetRoot, "Don't show this again"); TEST_TRUE(box != NULL && wgtCheckboxIsChecked(box)); + ctx->currentAppId = id; if (pass == 0) { // The user guide opens in the help viewer; About is a message. - int32_t before = tCtx()->stack.count; + int32_t before = ctx->stack.count; tWidgetClick(tFindByText(win->widgetRoot, "Open the User Guide")); fixPumpShell(SETTLE_FRAMES); - TEST_TRUE(tCtx()->stack.count > before); + TEST_TRUE(ctx->stack.count > before); fixDialogsOn("OK"); fixDialogs = 0; - win->onMenu(win, CMD_HELP_ABOUT); + ctx->currentAppId = id; + win->onMenu(win, fixMenuId(win, MENU_ABOUT)); TEST_EQ(fixDialogs, 1); fixDialogsOff(); + ctx->currentAppId = id; tWidgetClick(tFindByText(win->widgetRoot, "Close")); - fixPumpShell(SETTLE_FRAMES); - TEST_TRUE(!welcomeKey(key, sizeof(key))); } else { wgtCheckboxSetChecked(box, false); win->onClose(win); - fixPumpShell(SETTLE_FRAMES); + } + + ctx->currentAppId = 0; + + for (int32_t i = 0; i < WELCOME_FRAMES && shellGetApp(id); i++) { + fixPumpShell(1); + } + + TEST_TRUE(shellGetApp(id) == NULL); + TEST_TRUE(tFindWindow(WELCOME_TITLE) == NULL); + + if (pass == 0) { + TEST_TRUE(!welcomeKey(key, sizeof(key))); + } else { TEST_TRUE(welcomeKey(key, sizeof(key))); TEST_STR_EQ(key, APP_WELCOME); } - - TEST_TRUE(tFindWindow(WELCOME_TITLE) == NULL); } + basNativeHostSetLive(false); fixShellDown(); } @@ -1098,6 +1153,7 @@ int main(void) { caseInstallAppended(); caseSplitSwap(); caseCrc(); + caseEmptyLists(); caseCancel(); caseUpgradeUninstall(); caseReadOnly(); diff --git a/src/test/shell/Makefile b/src/test/shell/Makefile index 1527ff18..aa3ef032 100644 --- a/src/test/shell/Makefile +++ b/src/test/shell/Makefile @@ -38,7 +38,6 @@ APPS_TEST = $(BINDIR)/appsTest SHMAIN_TEST = $(BINDIR)/shellMainTest PM_TEST = $(BINDIR)/progmanTest PM_DIR = $(DVXTEST_ROOT)/src/apps/kpunch/progman -PM_MODEL = $(BINDIR)/pmModel.o LOADER_TEST = $(BINDIR)/loaderTest HLPC_SRC = $(DVXTEST_ROOT)/src/tools/dvxhlpc.c HLPC_OBJ = $(BINDIR)/dvxhlpcLib.o @@ -48,7 +47,7 @@ FIX_SOS = $(patsubst fixtures/%.c,$(FIXDIR)/%.so,$(FIX_SRCS)) FIX_HLP = $(FIXDIR)/fixture.hlp HLPC = $(DVXTEST_ROOT)/bin/host/dvxhlpc HELP_FIX = $(DVXTEST_ROOT)/src/test/tools/fixtures/help -SHELL_SRCS = $(SHELLDIR)/shellApp.c $(SHELLDIR)/shellMain.c $(SHELLDIR)/shellInfo.c $(SHELLDIR)/shellAssoc.c $(SHELLDIR)/shellProg.c $(SHELLDIR)/shellRun.c +SHELL_SRCS = $(SHELLDIR)/shellApp.c $(SHELLDIR)/shellBas.c $(SHELLDIR)/shellSysBas.c $(SHELLDIR)/shellMain.c $(SHELLDIR)/shellInfo.c $(SHELLDIR)/shellAssoc.c $(SHELLDIR)/shellProg.c $(SHELLDIR)/shellRun.c $(SHELLDIR)/pmModel.c $(SHELLDIR)/pmModelBas.c SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(BINDIR)/%.o,$(SHELL_SRCS)) SHELL_BINS = $(SHELL_TEST) $(APPS_TEST) $(SHMAIN_TEST) $(PM_TEST) SHFIX_OBJ = $(BINDIR)/shellFixture.o @@ -57,13 +56,8 @@ TEST_BINS = $(TASKS_TEST) $(SHELL_BINS) $(LOADER_TEST) LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) # The frame-pointer check in tasksTest reads __builtin_frame_address. -# A BASIC program on the host: the stub (basstub.c) as a .so that takes -# its compiled program from resources, like a .app from bascomp; the -# runtime it calls is linked into appsTest (whole archive, exported). -BASIC_DIR = $(DVXTEST_BASICDIR) -BASSTUB_SO = $(BINDIR)/basstub.so -BASCOMP = $(DVXTEST_ROOT)/bin/host/bascomp -BASCOMPREL = ../../../../bin/host/bascomp +# A BASIC program on the host (dvxtest.mk): the stub as a .so, the +# runtime linked into appsTest. # File Manager (run by appsTest to open a document) calls the fileops # library, which a DOS system loads as a .lib. @@ -82,9 +76,9 @@ JOYKEYS_DRV = $(BINDIR)/joykeys.drv AUDIO_DIR = $(DVXTEST_ROOT)/src/libs/kpunch/audio AUDIO_OBJS = $(patsubst $(AUDIO_DIR)/%.c,$(BINDIR)/audio/%.o,$(filter-out $(AUDIO_DIR)/audioShell.c,$(wildcard $(AUDIO_DIR)/*.c))) -CFLAGS = $(DVXTEST_CFLAGS) -fno-omit-frame-pointer -I$(SHELLDIR) -I$(PM_DIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(FIXDIR)"' -DSHELLTEST_BASSTUB_SO='"$(BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(BASCOMP)"' -DSHELLTEST_JOYKEYS_DRV='"$(JOYKEYS_DRV)"' +CFLAGS = $(DVXTEST_CFLAGS) -fno-omit-frame-pointer -I$(SHELLDIR) -I$(PM_DIR) -DDVXTEST_APPS_DIR='"$(APPS_SO_DIR)"' -DDVXTEST_FIXTURES_DIR='"$(FIXDIR)"' -DSHELLTEST_BASSTUB_SO='"$(DVXTEST_BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(DVXTEST_BASCOMP)"' -DSHELLTEST_JOYKEYS_DRV='"$(JOYKEYS_DRV)"' -.PHONY: all run clean host-tools bascomp +.PHONY: all run clean host-tools all: run @@ -97,7 +91,8 @@ $(SHFIX_OBJ): shellFixture.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< -# The help compiler's library entry point, for loaderTest. +# The help compiler's library entry point, for loaderTest and appsTest +# (BASIC HelpCompile in the parhlp fixture). $(HLPC_OBJ): $(HLPC_SRC) $(DVX_MAKEFILES) @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -DHLPC_NO_MAIN -c -o $@ $< @@ -112,16 +107,9 @@ $(TASKS_TEST): tasksTest.c $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs $(SHFIX_BINS): EXTRA_OBJS = $(SHFIX_OBJ) $(SHFIX_BINS): $(SHFIX_OBJ) -# Program Manager's model (plain data, no widgets) for progmanTest. -$(PM_MODEL): $(PM_DIR)/pmModel.c $(DVX_MAKEFILES) - @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(PM_TEST): EXTRA_OBJS = $(SHFIX_OBJ) $(PM_MODEL) -$(PM_TEST): $(PM_MODEL) - -$(APPS_TEST): EXTRA_OBJS = $(SHFIX_OBJ) $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) -Wl,--whole-archive $(DVXTEST_BASRT_A) -Wl,--no-whole-archive -$(APPS_TEST): $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) $(DVXTEST_BASRT_A) $(BASSTUB_SO) $(JOYKEYS_DRV) | bascomp +$(APPS_TEST): EXTRA_OBJS = $(SHFIX_OBJ) $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) $(HLPC_OBJ) -Wl,--whole-archive $(DVXTEST_BASRT_A) -Wl,--no-whole-archive +$(APPS_TEST): $(FILEOPS_OBJS) $(JOY_OBJS) $(AUDIO_OBJS) $(HLPC_OBJ) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) $(JOYKEYS_DRV) | dvxtest-bascomp $(JOYKEYS_DRV): $(JOY_DIR)/drivers/joykeys/joyKeys.c $(DVX_MAKEFILES) @mkdir -p $(BINDIR) @@ -139,13 +127,6 @@ $(BINDIR)/fileops/%.o: $(FILEOPS_DIR)/%.c $(DVX_MAKEFILES) @mkdir -p $(dir $@) $(HOSTCC) $(CFLAGS) -I$(DVXTEST_ROOT)/src/libs/kpunch -c -o $@ $< -$(BASSTUB_SO): $(BASIC_DIR)/stub/basstub.c $(BASIC_DIR)/stub/basModuleRes.c $(DVX_MAKEFILES) - @mkdir -p $(BINDIR) - $(HOSTCC) $(CFLAGS) -I$(BASIC_DIR) -fPIC -shared $(HOST_APP_LDFLAGS) -o $@ $(BASIC_DIR)/stub/basstub.c $(BASIC_DIR)/stub/basModuleRes.c - -bascomp: - $(MAKE) -C $(BASIC_DIR) $(BASCOMPREL) - $(SHELL_BINS): $(BINDIR)/%: %.c $(SHELL_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs dvxtest-apps @mkdir -p $(BINDIR) $(HOSTCC) $(CFLAGS) -o $@ $< $(SHELL_OBJS) $(EXTRA_OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm @@ -178,7 +159,7 @@ $(FIX_HLP): $(HELP_FIX)/main.dhs $(HELP_FIX)/inc.dhs $(HELP_FIX)/extra.dhs $(HLP @mkdir -p $(FIXDIR) cd $(HELP_FIX) && $(HLPC) -o $(FIX_HLP) --quiet -i . main.dhs @inputs.txt --include $(SHELL_OBJS:.o=.d) $(FILEOPS_OBJS:.o=.d) $(JOY_OBJS:.o=.d) $(AUDIO_OBJS:.o=.d) $(SHFIX_OBJ:.o=.d) $(PM_MODEL:.o=.d) $(HLPC_OBJ:.o=.d) $(addsuffix .d,$(TEST_BINS)) $(FIX_SOS:.so=.d) $(BASSTUB_SO:.so=.d) $(JOYKEYS_DRV:.drv=.d) +-include $(SHELL_OBJS:.o=.d) $(FILEOPS_OBJS:.o=.d) $(JOY_OBJS:.o=.d) $(AUDIO_OBJS:.o=.d) $(SHFIX_OBJ:.o=.d) $(HLPC_OBJ:.o=.d) $(addsuffix .d,$(TEST_BINS)) $(FIX_SOS:.so=.d) $(JOYKEYS_DRV:.drv=.d) run: $(TASKS_TEST) $(SHELL_TEST) $(APPS_TEST) $(SHMAIN_TEST) $(PM_TEST) $(LOADER_TEST) $(FIX_SOS) $(FIX_HLP) cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./tasksTest diff --git a/src/test/shell/appsTest.c b/src/test/shell/appsTest.c index 575b15e8..f28789cd 100644 --- a/src/test/shell/appsTest.c +++ b/src/test/shell/appsTest.c @@ -46,11 +46,14 @@ #include "slider/slider.h" #include "tabControl/tabCtrl.h" #include "treeView/treeView.h" -#include "../../apps/kpunch/dvxhelp/hlpformat.h" +#include "../../libs/kpunch/hlpfile/hlpFormat.h" +#include "listHelp.h" +#include "../../apps/kpunch/dvxbasic/formrt/formrt.h" #include "../../libs/kpunch/joystick/joystick.h" #include "../../libs/kpunch/audio/audio.h" #include "../../libs/kpunch/audio/audioDrv.h" +#include #include #include #include @@ -81,6 +84,37 @@ #define CP_SND_VOLUME_TRY 10 // one tried and cancelled #define CP_SND_MIDI_MPU 1 // MIDI music: the MIDI port #define CP_SND_FILES 3 // (None) and the two files in the sounds folder +#define BAS_TRUE (-1) // BASIC True / False from the entry points +#define BAS_FALSE 0 +#define SYS_COLOR_ID 2 // a system color the system.bas case changes +#define SYS_GLYPH_CHAR 'A' // a character the system.bas case reads the glyph of +#define SYS_GLYPH_ROW 6 // a row of it that has pixels set +#define SYS_RED 0x11 +#define SYS_GREEN 0x22 +#define SYS_BLUE 0x33 +#define SYS_BASIC_RGB (SYS_RED | (SYS_GREEN << 8) | (SYS_BLUE << 16)) // RGB(&H11, &H22, &H33) +#define SYS_RGB_TEXT "17,34,51" +#define SYS_DBL_MS 700 +#define SYS_MICKEYS 12 +#define SYS_WHEEL_STEP 5 +#define SYS_STEP_TOO_BIG 99 +#define SYS_BAD_MODE 99 +#define SYS_THEME "SYSBAS.THM" +#define SYS_WALLPAPER "CONFIG/WPAPER/SAMPLE.BMP" +#define SYS_SHOT "SYSSHOT.PNG" // SysScreenshot's picture +#define SYS_KEY_WIN_W 200 // caseSysInputBas's key window +#define SYS_KEY_WIN_H 100 +#define SYS_MOUSE_X 37 // a pointer position it moves to +#define SYS_MOUSE_Y 41 +#define SYS_KEYS_SENT 4 // keys of its first SysSendKey batch +#define SYS_KEY_FRAMES 12 // frames that batch needs (one key per idle frame) +#define SYS_SEND_KEY_QUEUE 64 // shellSysBas.c SYS_SEND_KEY_MAX +#define SYS_KEYS_MAX (SYS_KEYS_SENT + SYS_SEND_KEY_QUEUE) +#define RUN_NO_APP 999 // an app id no program has +#define PARCP_DBP "src/test/dos/fixtures/parcp/parcp.dbp" // under fixRepoRoot +#define PARCP_APP "apps/kpunch/parcp/parcp.app" +#define PARCP_TITLE "PARCP " +#define PARCP_PASS "PARCP PASS" #define PERCENT_MAX 100 #define SCAN_F 0x21 // BIOS scancode of the F key #define ASCII_CTRL_F 0x06 @@ -142,9 +176,8 @@ #define PM_EMPTY_SEARCH "No programs match the search." #define PM_INI "CONFIG/apps/kpunch/progman/progman.ini" #define PM_INI_DIR "CONFIG/apps/kpunch/progman" -#define PM_VIEW_BORDER 2 // pmView.c PV_BORDER -#define PM_CELL_W 80 // pmView.c PV_LARGE_CELL_W -#define PM_CELL_H 70 // large cell: pad + icon + gap + two 16-pixel lines + pad +#define PM_OTHER_W 160 // the window put in front of Program Manager +#define PM_OTHER_H 140 #define PM_DRAG_COL 2 #define PM_DRAG_ROW 1 #define PM_STEP_FIELDS 4 @@ -154,17 +187,66 @@ #define ASSOC_STORE "CONFIG/ASSOC.INI" #define NOTEPAD_DIR "apps/kpunch/notepad" #define NOTEPAD_APP NOTEPAD_DIR "/notepad.app" -#define NOTEPAD_DBP "/src/apps/kpunch/notepad/notepad.dbp" // under fixRepoRoot -#define NOTEPAD_BUILT "npBuilt.app" // bascomp output (DOS stub + program) +#define NOTEPAD_DBP "src/apps/kpunch/notepad/notepad.dbp" // under fixRepoRoot #define NOTEPAD_DOC "letter.txt" #define NOTEPAD_DOC_TEXT "Dear DVX\nline two" #define NOTEPAD_TITLE NOTEPAD_DOC " - Notepad" #define NOTEPAD_EDITOR "Editor" #define NOTEPAD_WAIT_FRAMES 200 #define FM_APP "apps/kpunch/fileMgr/fileMgr.app" +#define FM_DBP "src/apps/kpunch/fileMgr/fileMgr.dbp" #define FM_LIST "fmList" -#define FM_CMD_FILE_OPEN 100 -#define CMD_BUF_MAX (DVX_MAX_PATH * 3) +#define FM_MENU_OPEN "&Open" +#define CLOCK_DBP "src/apps/kpunch/clock/clock.dbp" +#define CLOCK_TIME "lblTime" +#define CLOCK_DATE "lblDate" +#define CLOCK_DATE_FMT "%Y-%m-%d" +#define CLOCK_TEXT_MAX 32 +#define SHBIND_DBP "src/test/shell/basic/shbind/shbind.dbp" +#define SHBIND_APP "apps/test/shbind.app" +#define SHBIND_IDS "lblIds" +#define SHBIND_RUN_TYPED "apps/kpunch/clock/clock.app" // shbind.frm RUN_TYPED +#define SHBIND_IDS_COUNT 3 +#define NEWARGS_DBP "src/test/shell/basic/newargs/newargs.dbp" +#define NEWARGS_APP "apps/test/newargs.app" +#define PARDEMO_DIR "src/test/dos/fixtures/pardemo" +#define PARDEMO_DBP PARDEMO_DIR "/pardemo.dbp" +#define PARDEMO_APP "apps/test/pardemo.app" +#define PARDEMO_DATA "apps/test/pardemo" // its pictures, beside the program as on DOS +#define PARDEMO_TAG "PARDEMO " +#define PARDEMO_WAIT "PARDEMO WAIT input" +#define PARDEMO_PASS "PARDEMO PASS " +#define PARDEMO_PICTURE "picDraw" +#define PARDEMO_STROKE_X0 20 // a mouse stroke across the PictureBox, in it +#define PARDEMO_STROKE_X1 200 +#define PARDEMO_STROKE_Y 30 +#define PARDEMO_SCAN_N 0x31 // BIOS scancode of N (Ctrl+N: the New item's Shortcut) +#define PARDEMO_ASCII_CTRL_N 0x0E // what the keyboard gives for Ctrl+N +#define PARDEMO_WAIT_SECS 40 // real seconds before giving up +#define PARDEMO_FRAME_MS 50 // test clock step per frame (the fixture's Timer is 200 ms) +#define PARHLP_DIR "src/test/dos/fixtures/parhlp" +#define PARHLP_DBP PARHLP_DIR "/parhlp.dbp" +#define PARHLP_DHS PARHLP_DIR "/parhlp.dhs" +#define PARHLP_APP "apps/test/parhlp.app" +#define PARHLP_TAG "PARHLP " +#define PARHLP_PASS "PARHLP PASS" +#define PARHLP_ARGS_THIRD "PARHLP ARGS Third Page" +#define PARHLP_LINK_HOME "PARHLP LINK Home" +#define PARHLP_LINK_SECOND "PARHLP LINK Second Page" +#define PARHLP_PANE "Pane" +#define PARPM_DBP "src/test/dos/fixtures/parpm/parpm.dbp" +#define PARPM_APP "apps/test/parpm.app" +#define PARPM_TAG "PARPM " +#define PARPM_WAIT "PARPM WAIT" +#define PARPM_PASS "PARPM PASS " +#define PARPM_SCAN_P 0x19 // BIOS scancode of P +#define PARPM_TICK_MS 100 // parpm.frm's Timer interval +#define PARPM_TICKS 400 // its own limit is 300 ticks +#define PARPM_ICONS "pmIcons" // its IconView (a UserControl of the project) +#define PARPM_ICON_TYPE "IconView" +#define PARPM_DRAG_ITEMS 2 // the programs the placement drags move +#define BAS_DOC_FOLDER "docs" +#define BAS_DOC BAS_DOC_FOLDER "/notes.txt" // dvxdemo menu ids (dvxdemo.c) #define DM_CMD_FILE_OPEN 101 @@ -209,6 +291,13 @@ typedef struct { // Prototypes // ============================================================ +// prefs.bas entry points (dvxPrefsBas.c; BASIC finds them by name) +void PrefsClose(int32_t handle); +int32_t PrefsCreate(void); +const char *PrefsGet(int32_t handle, const char *section, const char *key, const char *defaultValue); +int32_t PrefsGetInt(int32_t handle, const char *section, const char *key, int32_t defaultValue); +void PrefsSet(int32_t handle, const char *section, const char *key, const char *value); + static void caseClockTick(void); static void caseCpanelCancelKeepsDblClick(void); static void caseCpanelColors(void); @@ -216,7 +305,9 @@ static void caseCpanelDesktop(void); static void caseCpanelFileTypes(void); static void caseCpanelJoystick(void); static void caseCpanelMouse(void); +static void caseCpanelParity(void); static void caseCpanelSounds(void); +static void caseCpanelSysBas(void); static void caseCpanelVideo(void); static void caseDemoLowDepth(void); static void caseDemoMenus(void); @@ -236,6 +327,10 @@ static void caseHelpResizeRewrap(void); static void caseHelpSearchFullText(void); static void caseHelpViewer(void); static void caseLoadErrors(void); +static void caseNewArgsRelaunch(void); +static void casePardemo(void); +static void caseParhlp(void); +static void caseParpm(void); static void caseProgmanEmpty(void); static void caseProgmanHotkeysGlobal(void); static void caseProgmanInstalledChange(void); @@ -244,7 +339,10 @@ static void caseProgmanMenus(void); static void caseProgmanStartup(void); static void caseProgmanSysinfoEmpty(void); static void caseProgmanViews(void); +static void caseShellBasBindings(void); static void caseShellInfo(void); +static void caseShellRunningBas(void); +static void caseSysInputBas(void); static void caseTempCopyEdges(void); static WidgetT *clickButton(WindowT *win, const char *caption); static WidgetT *clickButtonIn(WidgetT *root, const char *caption); @@ -258,9 +356,9 @@ static WidgetT *helpContentBox(WindowT *win); static WindowT *helpOpen(int32_t *outId); static void hotkeyIgnore(void *userCtx); static bool iniHas(const char *section, const char *key, const char *value); -static bool installNotepad(void); static int32_t loadApp(const char *path); static bool menuEnabled(WindowT *win, int32_t id); +static int32_t parpmItemCell(WidgetT *icons, const char *method, int32_t index); static void pmField(const char *label, const char *text); static void pmScriptRun(AppContextT *ctx, WindowT *modal, void *userCtx); static void pmScriptStart(void); @@ -268,6 +366,7 @@ static void pmStep(const char *button); static void pmViewCellPoint(const WidgetT *items, int32_t col, int32_t row, int32_t *sx, int32_t *sy); static WindowT *progmanUp(int32_t *outId); static void selectTreeTopic(WindowT *win, const char *title); +static void sysKeyRecord(WindowT *win, int32_t key, int32_t mod); static bool tabTo(const WidgetT *target); static void testCtrlEsc(AppContextT *ctx); static WindowT *waitForWindow(const char *title); @@ -283,19 +382,35 @@ static int32_t sCtrlEscCalls = 0; static char sExitNotes[EXIT_NOTES_MAX]; // fixSave outcomes (fixExitNote) static PmScriptT sPmScript; // progman dialog answers static int32_t sF1Step = 0; // f1DuringIndex progress +static int32_t sSysKeys[SYS_KEYS_MAX]; // keys caseSysInputBas's window got +static int32_t sSysMods[SYS_KEYS_MAX]; +static int32_t sSysKeyCount = 0; -// The clock repaints when the wall-clock second rolls over. +// The Clock program (compiled from its project) shows the time and date +// and keeps them current; closing its window ends it. static void caseClockTick(void) { - tCase("clock repaints on the second boundary"); + tCase("clock shows the time and date and keeps them current"); fixShellUp(SCREEN_BPP); - int32_t id = loadApp(APP_CLOCK); + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, CLOCK_DBP, APP_CLOCK)) { + fixShellDown(); + return; + } - TEST_TRUE(id > 0); - fixPumpShell(TASK_FRAMES); - TEST_TRUE(tFindWindow("Clock") != NULL); + int32_t id = loadApp(APP_CLOCK); + WindowT *win = waitForWindow("Clock"); + WidgetT *timeLbl = win ? tFind(win, CLOCK_TIME) : NULL; + WidgetT *dateLbl = win ? tFind(win, CLOCK_DATE) : NULL; + TEST_TRUE(id > 0 && timeLbl != NULL && dateLbl != NULL); + + if (!timeLbl || !dateLbl) { + fixShellDown(); + return; + } + + // After the second rolls over the labels show the new time. time_t start = time(NULL); while (time(NULL) == start) { @@ -303,8 +418,27 @@ static void caseClockTick(void) { } fixPumpShell(TASK_FRAMES); - TEST_TRUE(tFindWindow("Clock") != NULL); - fixKillAllApps(); + + time_t now = time(NULL); + struct tm *tm = localtime(&now); + char want[CLOCK_TEXT_MAX]; + + strftime(want, sizeof(want), CLOCK_DATE_FMT, tm); + TEST_STR_EQ(wgtGetText(dateLbl), want); + + const char *shown = wgtGetText(timeLbl); + size_t len = strlen(shown); + + TEST_TRUE(len > strlen(" AM") && (strcmp(shown + len - strlen("AM"), "AM") == 0 || strcmp(shown + len - strlen("PM"), "PM") == 0)); + TEST_TRUE(strchr(shown, ':') != NULL); + + dvxRequestWindowClose(tCtx(), win); + + for (int32_t i = 0; i < NOTEPAD_WAIT_FRAMES && shellGetApp(id); i++) { + fixPumpShell(1); + } + + TEST_TRUE(shellGetApp(id) == NULL); fixShellDown(); } @@ -767,6 +901,57 @@ static void caseCpanelMouse(void) { } +// The Control Panel feasibility sample (src/test/dos/fixtures/parcp, also +// run on DOS by parcp.scr): a BASIC program doing the hardest parts of +// Control Panel reports PASS in its title. The network and printing +// libraries are not linked here, so it lists them as not installed; +// sound and joysticks are. +static void caseCpanelParity(void) { + tCase("Control Panel parity sample (BASIC)"); + fixShellUp(SCREEN_BPP); + + char src[DVX_MAX_PATH]; + + TEST_EQ(platformMkdirRecursive("CONFIG/WPAPER"), 0); + snprintf(src, sizeof(src), "%s" DEMO_BMP_SRC "/sample.bmp", fixRepoRoot); + TEST_TRUE(fixCopyFileTo(src, SYS_WALLPAPER)); + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, PARCP_DBP, PARCP_APP)) { + fixShellDown(); + return; + } + + basNativeHostSetLive(true); + + int32_t id = loadApp(PARCP_APP); + WindowT *win = waitForWindow(PARCP_TITLE); + + TEST_TRUE(id > 0 && win != NULL); + + if (win) { + TEST_TRUE(strncmp(win->title, PARCP_PASS, strlen(PARCP_PASS)) == 0); + TEST_TRUE(strstr(win->title, "(no sound)") == NULL); + TEST_TRUE(strstr(win->title, "(no joystick)") == NULL); + + if (strncmp(win->title, PARCP_PASS, strlen(PARCP_PASS)) != 0) { + fprintf(stderr, "parcp: %s\n", win->title); + } + + dvxRequestWindowClose(tCtx(), win); + + for (int32_t i = 0; i < NOTEPAD_WAIT_FRAMES && shellGetApp(id); i++) { + fixPumpShell(1); + } + + TEST_TRUE(shellGetApp(id) == NULL); + } + + fixKillAllApps(); + basNativeHostSetLive(false); + fixShellDown(); +} + + // The Sounds page: each event's file from the sounds folder, the volume // live (Cancel puts it back), OK writing [sounds] and [audio]. There is // no sound driver here, so nothing is heard and the driver list is just @@ -859,6 +1044,197 @@ static void caseCpanelSounds(void) { } +// system.bas (shellSysBas.c): screen modes, colors and themes, wallpaper, +// mouse, and the Store / Load pairs through prefs.bas handles. +static void caseCpanelSysBas(void) { + tCase("system.bas entry points"); + fixShellUp(SCREEN_BPP); + + AppContextT *ctx = tCtx(); + int32_t count = 0; + const VideoModeInfoT *modes = dvxGetVideoModes(ctx, &count); + + // Screen modes: the list, a change and back, and a refused change + TEST_EQ(SysVideoModeCount(), count); + TEST_TRUE(count >= 2); + TEST_EQ(SysVideoModeWidth(count), 0); + TEST_EQ(SysVideoModeHeight(-1), 0); + TEST_EQ(SysVideoModeBpp(count), 0); + + int32_t origW = ctx->display.width; + int32_t origH = ctx->display.height; + int32_t origBpp = ctx->display.format.bitsPerPixel; + int32_t other = 0; + + while (other < count && modes[other].w == origW && modes[other].h == origH) { + other++; + } + + TEST_TRUE(other < count); + + if (other < count) { + int32_t w = SysVideoModeWidth(other); + int32_t h = SysVideoModeHeight(other); + int32_t bpp = SysVideoModeBpp(other); + + TEST_EQ(w, modes[other].w); + TEST_EQ(h, modes[other].h); + TEST_EQ(bpp, modes[other].bpp); + TEST_EQ(SysSetVideoMode(w, h, bpp), BAS_TRUE); + TEST_EQ(ctx->display.width, w); + TEST_EQ(SysSetVideoMode(origW, origH, origBpp), BAS_TRUE); + TEST_EQ(ctx->display.width, origW); + ctx->exclusive.active = true; + TEST_EQ(SysSetVideoMode(w, h, bpp), BAS_FALSE); + ctx->exclusive.active = false; + TEST_EQ(ctx->display.width, origW); + } + + // Colors: ids, names, BASIC RGB values, store / load, themes + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + char upper[LEAF_MAX]; + + TEST_EQ(SysColorCount(), ColorCountE); + TEST_STR_EQ(SysColorLabel(0), dvxColorLabel((ColorIdE)0)); + TEST_STR_EQ(SysColorLabel(ColorCountE), ""); + TEST_STR_EQ(SysColorName(SYS_COLOR_ID), dvxColorName((ColorIdE)SYS_COLOR_ID)); + TEST_STR_EQ(SysColorName(-1), ""); + snprintf(upper, sizeof(upper), "%s", dvxColorName((ColorIdE)SYS_COLOR_ID)); + + for (char *c = upper; *c; c++) { + *c = (char)toupper((unsigned char)*c); + } + + TEST_EQ(SysColorId(upper), SYS_COLOR_ID); + TEST_EQ(SysColorId("noSuchColor"), -1); + TEST_EQ(SysColorId(NULL), -1); + + int32_t oldColor = SysGetColor(SYS_COLOR_ID); + + SysSetColor(SYS_COLOR_ID, SYS_BASIC_RGB); + SysSetColor(-1, SYS_BASIC_RGB); + dvxGetColor(ctx, (ColorIdE)SYS_COLOR_ID, &r, &g, &b); + TEST_EQ(r, SYS_RED); + TEST_EQ(g, SYS_GREEN); + TEST_EQ(b, SYS_BLUE); + TEST_EQ(SysGetColor(SYS_COLOR_ID), SYS_BASIC_RGB); + TEST_EQ(SysGetColor(ColorCountE), 0); + + int32_t snap = PrefsCreate(); + int32_t empty = PrefsCreate(); + + SysColorsStore(snap); + SysColorsStore(0); + TEST_STR_EQ(PrefsGet(snap, "colors", dvxColorName((ColorIdE)SYS_COLOR_ID), ""), SYS_RGB_TEXT); + SysResetColors(); + TEST_EQ(SysGetColor(SYS_COLOR_ID), oldColor); + TEST_EQ(SysColorsLoad(snap), BAS_TRUE); + TEST_EQ(SysGetColor(SYS_COLOR_ID), SYS_BASIC_RGB); + TEST_EQ(SysColorsLoad(empty), BAS_FALSE); + TEST_EQ(SysColorsLoad(0), BAS_FALSE); + TEST_EQ(SysSaveTheme(SYS_THEME), BAS_TRUE); + SysResetColors(); + TEST_EQ(SysLoadTheme(SYS_THEME), BAS_TRUE); + TEST_EQ(SysGetColor(SYS_COLOR_ID), SYS_BASIC_RGB); + TEST_EQ(SysLoadTheme("missing.thm"), BAS_FALSE); + SysResetColors(); + unlink(SYS_THEME); + + // Wallpaper: set, mode, clear, store / load + char src[DVX_MAX_PATH]; + + TEST_EQ(platformMkdirRecursive("CONFIG/WPAPER"), 0); + snprintf(src, sizeof(src), "%s" DEMO_BMP_SRC "/sample.bmp", fixRepoRoot); + TEST_TRUE(fixCopyFileTo(src, SYS_WALLPAPER)); + TEST_STR_EQ(SysWallpaper(), ""); + SysWallpaperStore(snap); + SysWallpaperStore(0); + TEST_STR_EQ(PrefsGet(snap, "desktop", "mode", ""), WALLPAPER_MODE_DEFAULT); + SysSetWallpaperMode(WallpaperTileE); + SysSetWallpaperMode(SYS_BAD_MODE); + SysSetWallpaperMode(-1); + TEST_EQ(SysWallpaperMode(), WallpaperTileE); + TEST_EQ(SysSetWallpaper(SYS_WALLPAPER), BAS_TRUE); + TEST_EQ(SysSetWallpaper("missing.bmp"), BAS_FALSE); + TEST_STR_EQ(SysWallpaper(), SYS_WALLPAPER); + + int32_t withWp = PrefsCreate(); + + SysWallpaperStore(withWp); + TEST_STR_EQ(PrefsGet(withWp, "desktop", "wallpaper", ""), SYS_WALLPAPER); + TEST_STR_EQ(PrefsGet(withWp, "desktop", "mode", ""), "tile"); + TEST_EQ(SysWallpaperLoad(snap), BAS_TRUE); + TEST_STR_EQ(SysWallpaper(), ""); + TEST_EQ(SysWallpaperMode(), prefsChoiceIndex(prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)); + TEST_EQ(SysWallpaperLoad(withWp), BAS_TRUE); + TEST_STR_EQ(SysWallpaper(), SYS_WALLPAPER); + TEST_EQ(SysWallpaperMode(), WallpaperTileE); + SysSetWallpaperMode(WallpaperCenterE); + TEST_EQ(SysWallpaperLoad(withWp), BAS_TRUE); + TEST_EQ(SysWallpaperMode(), WallpaperTileE); + TEST_EQ(SysWallpaperLoad(0), BAS_FALSE); + PrefsSet(withWp, "desktop", "wallpaper", "missing.bmp"); + TEST_EQ(SysWallpaperLoad(withWp), BAS_FALSE); + TEST_EQ(SysSetWallpaper(""), BAS_TRUE); + TEST_STR_EQ(SysWallpaper(), ""); + + // Mouse: apply, read back, store / load, defaults + SysSetMouse(MOUSE_WHEEL_DIR_REVERSED, SYS_DBL_MS, MOUSE_ACCEL_HIGH, SYS_MICKEYS, SYS_WHEEL_STEP); + TEST_EQ(SysMouseWheelDir(), MOUSE_WHEEL_DIR_REVERSED); + TEST_EQ(SysMouseDoubleClick(), SYS_DBL_MS); + TEST_EQ(SysMouseAccel(), MOUSE_ACCEL_HIGH); + TEST_EQ(SysMouseMickeys(), SYS_MICKEYS); + TEST_EQ(SysMouseWheelStep(), SYS_WHEEL_STEP); + TEST_EQ(ctx->dblClickTicks, PLATFORM_MS_TO_TICKS(SYS_DBL_MS)); + SysMouseStore(snap); + SysMouseStore(0); + TEST_STR_EQ(PrefsGet(snap, "mouse", "wheel", ""), "reversed"); + TEST_STR_EQ(PrefsGet(snap, "mouse", "acceleration", ""), "high"); + TEST_EQ(PrefsGetInt(snap, "mouse", "doubleclick", 0), SYS_DBL_MS); + TEST_EQ(PrefsGetInt(snap, "mouse", "speed", 0), SYS_MICKEYS); + TEST_EQ(PrefsGetInt(snap, "mouse", "wheelspeed", 0), SYS_WHEEL_STEP); + + // 0 leaves acceleration and speed alone; the wheel step is clamped + SysSetMouse(MOUSE_WHEEL_DIR_NORMAL, MOUSE_DBLCLICK_DEFAULT_MS, 0, 0, SYS_STEP_TOO_BIG); + TEST_EQ(SysMouseAccel(), MOUSE_ACCEL_HIGH); + TEST_EQ(SysMouseMickeys(), SYS_MICKEYS); + TEST_EQ(SysMouseWheelStep(), MOUSE_WHEEL_STEP_MAX); + SysMouseLoad(snap); + TEST_EQ(SysMouseWheelDir(), MOUSE_WHEEL_DIR_REVERSED); + TEST_EQ(SysMouseDoubleClick(), SYS_DBL_MS); + TEST_EQ(SysMouseWheelStep(), SYS_WHEEL_STEP); + SysMouseLoad(0); + TEST_EQ(SysMouseWheelDir(), MOUSE_WHEEL_DIR_REVERSED); + SysMouseLoad(empty); + TEST_EQ(SysMouseWheelDir(), MOUSE_WHEEL_DIR_NORMAL); + TEST_EQ(SysMouseDoubleClick(), MOUSE_DBLCLICK_DEFAULT_MS); + TEST_EQ(SysMouseAccel(), MOUSE_ACCEL_MEDIUM); + TEST_EQ(SysMouseMickeys(), MOUSE_SPEED_DEFAULT); + TEST_EQ(SysMouseWheelStep(), MOUSE_WHEEL_STEP_DEFAULT); + + // The system font's glyphs (what DVX Demo draws by hand) + const BitmapFontT *font = dvxGetFont(tCtx()); + int32_t glyph = SYS_GLYPH_CHAR - font->firstChar; + + TEST_EQ(SysFontWidth(), font->charWidth); + TEST_EQ(SysFontHeight(), font->charHeight); + TEST_EQ(SysFontFirstChar(), font->firstChar); + TEST_EQ(SysFontCharCount(), font->numChars); + TEST_EQ(SysFontGlyphRow(SYS_GLYPH_CHAR, SYS_GLYPH_ROW), font->glyphData[glyph * font->charHeight + SYS_GLYPH_ROW]); + TEST_TRUE(SysFontGlyphRow(SYS_GLYPH_CHAR, SYS_GLYPH_ROW) != 0); + TEST_EQ(SysFontGlyphRow(SYS_GLYPH_CHAR, font->charHeight), 0); + TEST_EQ(SysFontGlyphRow(SYS_GLYPH_CHAR, -1), 0); + TEST_EQ(SysFontGlyphRow(font->firstChar + font->numChars, 0), 0); + + PrefsClose(withWp); + PrefsClose(empty); + PrefsClose(snap); + fixShellDown(); +} + + static void caseCpanelVideo(void) { tCase("cpanel video tab: apply, keep, revert, cancel restore"); fixShellUp(SCREEN_BPP); @@ -1101,21 +1477,28 @@ static void caseDocumentOpensNotepad(void) { TEST_TRUE(writeBytes(NOTEPAD_DOC, NOTEPAD_DOC_TEXT, (int32_t)strlen(NOTEPAD_DOC_TEXT))); shellAssocTestReset(NULL, NULL); - if (!installNotepad()) { + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, NOTEPAD_DBP, NOTEPAD_APP)) { shellAssocTestReset(NULL, NULL); fixShellDown(); return; } for (int32_t pass = 0; pass < 2; pass++) { + WindowT *fm = NULL; + if (pass == 0) { TEST_TRUE(shellAssocOpenDocument(tCtx(), NOTEPAD_DOC)); } else { - fixLinkApp("fileMgr"); + TEST_TRUE(fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, FM_DBP, FM_APP)); + + // File Manager calls its libraries (fileops, the shell): for real + basNativeHostSetLive(true); int32_t fmId = shellLoadAppWithArgs(tCtx(), FM_APP, "."); - WindowT *fm = waitForWindow("File Manager"); - WidgetT *list = fm ? tFind(fm, FM_LIST) : NULL; + WidgetT *list; + + fm = waitForWindow("File Manager"); + list = fm ? tFind(fm, FM_LIST) : NULL; TEST_TRUE(fmId > 0 && list != NULL); @@ -1123,13 +1506,18 @@ static void caseDocumentOpensNotepad(void) { break; } + // The listing fills once the program's Load has run. + for (int32_t i = 0; i < NOTEPAD_WAIT_FRAMES && wgtListViewGetRowCount(list) == 0; i++) { + fixPumpShell(1); + } + for (int32_t row = 0; row < wgtListViewGetRowCount(list); row++) { if (strcmp(wgtListViewGetCell(list, row, 0), NOTEPAD_DOC) == 0) { wgtListViewSetSelected(list, row); } } - fm->onMenu(fm, FM_CMD_FILE_OPEN); + fm->onMenu(fm, fixMenuId(fm, FM_MENU_OPEN)); } WindowT *np = waitForWindow(NOTEPAD_TITLE); @@ -1138,22 +1526,14 @@ static void caseDocumentOpensNotepad(void) { TEST_TRUE(np != NULL && editor != NULL); TEST_STR_EQ(editor ? wgtGetText(editor) : "", NOTEPAD_DOC_TEXT); - // Closed like a user would, so the program ends and frees its - // runtime (killing its task would not). - int32_t npId = np ? np->appId : 0; - - if (np) { - dvxRequestWindowClose(tCtx(), np); - } - - for (int32_t i = 0; i < NOTEPAD_WAIT_FRAMES && shellGetApp(npId); i++) { - fixPumpShell(1); - } - - TEST_TRUE(npId > 0 && shellGetApp(npId) == NULL); + // Closed like a user would, so the programs end and free their + // runtimes (killing their tasks would not). + TEST_TRUE(np && fixCloseApp(np)); + TEST_TRUE(!fm || fixCloseApp(fm)); fixKillAllApps(); } + basNativeHostSetLive(false); shellAssocTestReset(NULL, NULL); fixShellDown(); } @@ -1582,7 +1962,7 @@ static void caseHelpNewArgs(void) { win->onClose(win); fixPumpShell(SETTLE_FRAMES); - TEST_EQ(slotsBefore - fixClassSlotsInUse(), HELP_CLASS_COUNT); + TEST_EQ(fixClassSlotsInUse(), slotsBefore); TEST_EQ(shellRunningAppCount(), 0); fixShellDown(); } @@ -1849,6 +2229,281 @@ static void caseLoadErrors(void) { } +// A program that allows one copy and has a NewArgs procedure: starting it +// again hands the running copy the new arguments (no second copy, no +// "already running" box), and the load gives the running copy's id. +static void caseNewArgsRelaunch(void) { + tCase("starting a running single-copy BASIC program hands it new arguments"); + fixShellUp(SCREEN_BPP); + + AppContextT *ctx = tCtx(); + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, NEWARGS_DBP, NEWARGS_APP)) { + fixShellDown(); + return; + } + + fixDialogs = 0; + fixDialogsOn("OK"); + + int32_t first = shellLoadAppWithArgs(ctx, NEWARGS_APP, "one"); + WindowT *win = waitForWindow("NEWARGS 0 one"); + + TEST_TRUE(first > 0 && win != NULL); + TEST_EQ(shellRunningAppCount(), 1); + + int32_t again = shellLoadAppWithArgs(ctx, NEWARGS_APP, "two words"); + + TEST_EQ(again, first); + TEST_TRUE(waitForWindow("NEWARGS 1 two words") != NULL); + TEST_EQ(shellRunningAppCount(), 1); + TEST_EQ(fixDialogs, 0); + fixDialogsOff(); + + if (win) { + fixCloseApp(win); + } + + fixShellDown(); +} + + +// The DVX Demo feasibility fixture (src/test/dos/fixtures/pardemo) on the +// host: its own checks of the controls, menus, window state, copies, +// arrangement and pictures, then a mouse stroke on its PictureBox and +// Ctrl+N, which it waits for before it reports. +static void casePardemo(void) { + static const char *const pictures[] = { "logo.bmp", "new.bmp", "open.bmp", "save.bmp" }; + + tCase("BASIC DVX Demo feasibility (pardemo)"); + fixShellUp(SCREEN_BPP); + TEST_EQ(platformMkdirRecursive(PARDEMO_DATA), 0); + + for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(pictures); i++) { + char src[DVX_MAX_PATH]; + char dst[DVX_MAX_PATH]; + + snprintf(src, sizeof(src), "%s/%s/%s", fixRepoRoot, PARDEMO_DIR, pictures[i]); + snprintf(dst, sizeof(dst), "%s/%s", PARDEMO_DATA, pictures[i]); + TEST_TRUE(fixCopyFileTo(src, dst)); + } + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, PARDEMO_DBP, PARDEMO_APP)) { + fixShellDown(); + return; + } + + // Its library calls (system.bas: the font) are made for real + basNativeHostSetLive(true); + + int32_t id = shellLoadApp(tCtx(), PARDEMO_APP); + WindowT *win = waitForWindow(PARDEMO_TAG); + + TEST_TRUE(id > 0 && win != NULL); + + if (win) { + time_t until = time(NULL) + PARDEMO_WAIT_SECS; + + // The test clock moves only when told: the fixture's Timer needs it + while (strncmp(win->title, PARDEMO_TAG "RUN", strlen(PARDEMO_TAG "RUN")) == 0 && time(NULL) < until) { + tAdvanceClock(PARDEMO_FRAME_MS); + fixPumpShell(1); + } + + WidgetT *pic = wgtFind(win->widgetRoot, PARDEMO_PICTURE); + + TEST_STR_EQ(win->title, PARDEMO_WAIT); + TEST_TRUE(pic != NULL); + + if (pic) { + int32_t x0 = win->x + win->contentX + pic->x + PARDEMO_STROKE_X0; + int32_t x1 = win->x + win->contentX + pic->x + PARDEMO_STROKE_X1; + int32_t y = win->y + win->contentY + pic->y + PARDEMO_STROKE_Y; + + tDrag(x0, y, x1, y); + fixPumpShell(1); + } + + tKey(PARDEMO_SCAN_N, PARDEMO_ASCII_CTRL_N, KEY_MOD_CTRL); + + while (strcmp(win->title, PARDEMO_WAIT) == 0 && time(NULL) < until) { + tAdvanceClock(PARDEMO_FRAME_MS); + fixPumpShell(1); + } + + dvxLog("appsTest: pardemo title %s", win->title); + TEST_TRUE(strncmp(win->title, PARDEMO_PASS, strlen(PARDEMO_PASS)) == 0); + TEST_TRUE(fixCloseApp(win)); + } + + basNativeHostSetLive(false); + + fixKillAllApps(); + fixShellDown(); +} + + +// The Help Viewer feasibility fixture (src/test/dos/fixtures/parhlp) on +// the host: it compiles its help source, reads it with hlpfile.bas and +// shows topics in a HelpPane (title "PARHLP PASS"); new launch arguments +// while it runs go to its NewArgs handler (the shell's onNewArgs, set by +// the BASIC stub because the program handles NewArgs), and a click on a +// link in the pane fires LinkClick. +static void caseParhlp(void) { + tCase("BASIC Help Viewer feasibility (parhlp)"); + fixShellUp(SCREEN_BPP); + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, PARHLP_DBP, PARHLP_APP)) { + fixShellDown(); + return; + } + + char dhs[DVX_MAX_PATH]; + + snprintf(dhs, sizeof(dhs), "%s/" PARHLP_DHS, fixRepoRoot); + basNativeHostSetLive(true); + + int32_t id = shellLoadAppWithArgs(tCtx(), PARHLP_APP, dhs); + WindowT *win = waitForWindow(PARHLP_TAG); + ShellAppT *app = id > 0 ? shellGetApp(id) : NULL; + + TEST_TRUE(id > 0 && win != NULL); + + if (win && app) { + TEST_STR_EQ(win->title, PARHLP_PASS); + + // As parhlp.scr does: Tab from the tree to the pane's first link + // (the start page's), Enter follows it + WidgetT *pane = wgtFind(win->widgetRoot, PARHLP_PANE); + + tKeyTab(); + fixPumpShell(1); + TEST_TRUE(pane && wgtGetFocused() == firstFocusable(pane)); + tKeyEnter(); + fixPumpShell(SETTLE_FRAMES); + TEST_STR_EQ(win->title, PARHLP_LINK_SECOND); + TEST_TRUE(app->dxeCtx && app->dxeCtx->onNewArgs); + + if (app->dxeCtx && app->dxeCtx->onNewArgs) { + char args[DVX_MAX_PATH * 2]; + + snprintf(args, sizeof(args), "%s ph.third", dhs); + SHELL_APP_CALL(app, app->dxeCtx->onNewArgs(app->dxeCtx->newArgsCtx, args)); + fixPumpShell(SETTLE_FRAMES); + TEST_STR_EQ(win->title, PARHLP_ARGS_THIRD); + } + + // A click on the third page's link leads home + WidgetT *link = firstFocusable(pane); + + TEST_TRUE(link != NULL); + + if (link) { + tWidgetClick(link); + fixPumpShell(SETTLE_FRAMES); + TEST_STR_EQ(win->title, PARHLP_LINK_HOME); + } + + TEST_TRUE(fixCloseApp(win)); + } + + fixKillAllApps(); + basNativeHostSetLive(false); + fixShellDown(); +} + + +// The Program Manager feasibility fixture (src/test/dos/fixtures/parpm) +// on the host: installed programs, a groups file, the system information +// and file types, an icon grid, a second copy started minimized and seen +// to start and end, and a key for every program pressed here. +static void caseParpm(void) { + tCase("BASIC Program Manager feasibility (parpm)"); + fixShellUp(SCREEN_BPP); + shellInfoInit(tCtx()); + TEST_EQ(platformMkdirRecursive("CONFIG"), 0); + shellAssocTestReset(NULL, NULL); + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, PARPM_DBP, PARPM_APP)) { + fixShellDown(); + return; + } + + basNativeHostSetLive(true); + + int32_t id = shellLoadApp(tCtx(), PARPM_APP); + WindowT *win = waitForWindow(PARPM_TAG); + + TEST_TRUE(id > 0 && win != NULL); + + if (win) { + tKey(PARPM_SCAN_P, 0, KEY_MOD_CTRL | KEY_MOD_ALT); + + for (int32_t i = 0; i < PARPM_TICKS && strcmp(win->title, PARPM_WAIT) == 0; i++) { + tAdvanceClock(PARPM_TICK_MS); + fixPumpShell(1); + } + + dvxLog("appsTest: parpm title %s", win->title); + TEST_TRUE(strncmp(win->title, PARPM_PASS, strlen(PARPM_PASS)) == 0); + + // The programs are in its IconView, a control written in BASIC + // (its own: registered under a key of its own, its BASIC name the + // class name) + WidgetT *icons = tFind(win, PARPM_ICONS); + const WgtIfaceT *iface = icons ? wgtGetIfaceOf(icons, NULL) : NULL; + WgtValueT count; + WgtValueT pics; + + TEST_TRUE(iface != NULL && iface->basName && strcmp(iface->basName, PARPM_ICON_TYPE) == 0); + TEST_TRUE(icons != NULL && wgtGetProp(icons, "ItemCount", &count) && count.i > 0); + TEST_TRUE(icons != NULL && wgtGetProp(icons, "PictureCount", &pics) && pics.i > 0); + + // Auto Arrange off, each program in its own cell from the groups + // model: the first dragged to the free cell after the last program + // takes it, the second dragged onto it swaps with it (the model's + // rule, ShellItemPlace, as Program Manager's) + WgtValueT cols; + bool placing = icons != NULL && wgtGetProp(icons, "Columns", &cols) && cols.i > 0 && count.i >= PARPM_DRAG_ITEMS; + + TEST_TRUE(placing); + + if (placing) { + int32_t freeCol = count.i % cols.i; + int32_t freeRow = count.i / cols.i; + int32_t x0; + int32_t y0; + int32_t x1; + int32_t y1; + + pmViewCellPoint(icons, 0, 0, &x0, &y0); + pmViewCellPoint(icons, freeCol, freeRow, &x1, &y1); + tDrag(x0, y0, x1, y1); + fixPumpShell(1); + TEST_EQ(parpmItemCell(icons, "ItemCol", 0), freeCol); + TEST_EQ(parpmItemCell(icons, "ItemRow", 0), freeRow); + pmViewCellPoint(icons, 1, 0, &x0, &y0); + tDrag(x0, y0, x1, y1); + fixPumpShell(1); + TEST_EQ(parpmItemCell(icons, "ItemCol", 0), 1); + TEST_EQ(parpmItemCell(icons, "ItemRow", 0), 0); + TEST_EQ(parpmItemCell(icons, "ItemCol", 1), freeCol); + TEST_EQ(parpmItemCell(icons, "ItemRow", 1), freeRow); + } + + TEST_TRUE(fixCloseApp(win)); + } + + fixKillAllApps(); + + // The program's own control (both copies') left the registry with it + TEST_TRUE(wgtResolveTypeName(PARPM_ICON_TYPE) == NULL); + basNativeHostSetLive(false); + shellAssocTestReset(NULL, NULL); + fixShellDown(); +} + + // Program Manager without an apps/ tree: Installed Programs is empty and // says so, also after a Refresh with an empty apps/ directory. static void caseProgmanEmpty(void) { @@ -1896,7 +2551,7 @@ static void caseProgmanHotkeysGlobal(void) { AppContextT *ctx = tCtx(); int32_t id; WindowT *pm = progmanUp(&id); - WindowT *other = dvxCreateWindow(ctx, "Other", 0, 0, PM_CELL_W * 2, PM_CELL_H * 2, false); + WindowT *other = dvxCreateWindow(ctx, "Other", 0, 0, PM_OTHER_W, PM_OTHER_H, false); fixPumpShell(1); TEST_TRUE(pm != NULL && other != NULL); @@ -2434,6 +3089,100 @@ static void caseProgmanViews(void) { } +// Every shell.bas binding called from a BASIC program (shbind.frm checks +// what it can see itself and names the first failure in its title); here +// the programs it started, Run Minimized, Task Manager, and the hotkey, +// program list and Exit DVX request it leaves behind. +static void caseShellBasBindings(void) { + static const char store[] = "[associations]\nTXT = " APP_CLOCK ",/doc %1\n"; + int32_t ids[SHBIND_IDS_COUNT]; + + tCase("shell.bas: every binding from BASIC"); + fixShellUp(SCREEN_BPP); + + AppContextT *ctx = tCtx(); + + shellInfoInit(ctx); + TEST_EQ(platformMkdirRecursive("CONFIG"), 0); + TEST_EQ(platformMkdirRecursive(BAS_DOC_FOLDER), 0); + TEST_TRUE(writeBytes(ASSOC_STORE, store, (int32_t)strlen(store))); + TEST_TRUE(writeBytes(BAS_DOC, "x", 1)); + shellAssocTestReset(NULL, NULL); + + if (!fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, SHBIND_DBP, SHBIND_APP)) { + shellAssocTestReset(NULL, NULL); + fixShellDown(); + return; + } + + sCtrlEscCalls = 0; + shellCtrlEscFn = testCtrlEsc; + fixDialogs = 0; + fixDialogsOn("OK"); + fixDialogsType(SHBIND_RUN_TYPED, "OK"); + basNativeHostSetLive(true); + + int32_t id = shellLoadApp(ctx, SHBIND_APP); + WindowT *win = waitForWindow("SHBIND"); + WidgetT *idsW = win ? tFind(win, SHBIND_IDS) : NULL; + + TEST_TRUE(id > 0 && win != NULL && idsW != NULL); + dvxLog("appsTest: shbind title %s", win ? win->title : "(no window)"); + TEST_STR_EQ(win ? win->title : "", "SHBIND PASS"); + TEST_EQ(sCtrlEscCalls, 1); + + // The missing document's error and the Run dialog. + TEST_EQ(fixDialogs, 2); + fixDialogsOff(); + + if (idsW && sscanf(wgtGetText(idsW), "%" SCNd32 " %" SCNd32 " %" SCNd32, &ids[0], &ids[1], &ids[2]) == SHBIND_IDS_COUNT) { + const ShellAppT *doc = shellGetApp(ids[0]); + const ShellAppT *cmd = shellGetApp(ids[1]); + + TEST_STR_EQ(doc ? doc->dxeCtx->args : "", "/doc " BAS_DOC); + TEST_STR_EQ(cmd ? cmd->dxeCtx->args : "", "hello"); + + // Run Minimized catches the window the Clock opens from its task. + fixPumpShell(TASK_FRAMES); + + WindowT *minWin = fixWindowForApp(ids[2]); + WindowT *docWin = fixWindowForApp(ids[0]); + + TEST_TRUE(minWin != NULL && minWin->minimized); + TEST_TRUE(docWin != NULL && !docWin->minimized); + } else { + TEST_TRUE(false); + } + + // The program's hotkey is taken while it runs; a press waits for it. + TEST_EQ(dvxRegisterHotkey(ctx, 'K', ACCEL_CTRL | ACCEL_ALT, hotkeyIgnore, NULL), DVX_HOTKEY_NONE); + tKey(PM_SCAN_K, 0, KEY_MOD_CTRL | KEY_MOD_ALT); + fixPumpShell(1); + + // Exit DVX, asked for by the program: every program closes, then DVX. + TEST_TRUE(ctx->running); + shellRunExitRequest(ctx, 0); + TEST_TRUE(!ctx->running); + ctx->running = true; + fixPumpShell(SETTLE_FRAMES); + TEST_PTR_EQ(shellGetApp(id), NULL); + TEST_EQ(shellRunningAppCount(), 0); + + // Its hotkey went with it. + int32_t mine = dvxRegisterHotkey(ctx, 'K', ACCEL_CTRL | ACCEL_ALT, hotkeyIgnore, NULL); + + TEST_TRUE(mine != DVX_HOTKEY_NONE); + TEST_TRUE(dvxUnregisterHotkey(ctx, mine)); + + shellCtrlEscFn = NULL; + sCtrlEscCalls = 0; + fixKillAllApps(); + basNativeHostSetLive(false); + shellAssocTestReset(NULL, NULL); + fixShellDown(); +} + + static void caseShellInfo(void) { tCase("shellInfoInit gathers and logs system information"); fixShellUp(SCREEN_BPP); @@ -2447,6 +3196,220 @@ static void caseShellInfo(void) { } +// shell.bas running programs from C: the list, find, bring to the front, +// priority, close (asked at the main loop's top, its save prompt runs), +// end at once, and command lines split by the shell's rule. +static void caseShellRunningBas(void) { + tCase("shell.bas: running programs, priority, command lines"); + fixShellUp(SCREEN_BPP); + fixDialogsOn("Cancel"); + + AppContextT *ctx = tCtx(); + int32_t clk = loadApp(APP_CLOCK); + int32_t dirty = shellLoadAppWithArgs(ctx, FIX_SAVE, "dirty"); + + fixPumpShell(TASK_FRAMES); + TEST_TRUE(clk > 0 && dirty > 0); + + // The list, names, paths, find + TEST_EQ(ShellRunningCount(), 2); + TEST_EQ(ShellRunningId(0), clk); + TEST_EQ(ShellRunningId(1), dirty); + TEST_EQ(ShellRunningId(2), 0); + TEST_EQ(ShellRunningId(-1), 0); + TEST_STR_EQ(ShellRunningName(clk), "Clock"); + TEST_STR_EQ(ShellRunningPath(clk), APP_CLOCK); + TEST_STR_EQ(ShellRunningName(RUN_NO_APP), ""); + TEST_STR_EQ(ShellRunningPath(0), ""); + TEST_EQ(ShellRunningFind(APP_CLOCK), clk); + TEST_EQ(ShellRunningFind("apps/none/none.app"), 0); + TEST_EQ(ShellRunningFind(NULL), 0); + TEST_EQ(ShellAppIsHung(clk), 0); + TEST_EQ(ShellAppIsHung(RUN_NO_APP), 0); + TEST_EQ(ShellAppMemory(clk), (int32_t)dvxMemGetAppUsage(clk)); + TEST_EQ(ShellAppMemory(RUN_NO_APP), 0); + + // Bring to the front: a minimized window is restored + WindowT *clkWin = fixWindowForApp(clk); + + TEST_TRUE(clkWin != NULL); + + if (clkWin) { + dvxMinimizeWindow(ctx, clkWin); + TEST_TRUE(clkWin->minimized); + TEST_EQ(ShellRunningActivate(clk), -1); + TEST_TRUE(!clkWin->minimized); + } + + TEST_EQ(ShellRunningActivate(RUN_NO_APP), 0); + + // This program's id and priority (the Clock runs at low priority) + ctx->currentAppId = clk; + TEST_EQ(ShellAppId(), clk); + TEST_EQ(ShellPriority(), TS_PRIORITY_LOW); + TEST_EQ(ShellSetPriority(TS_PRIORITY_HIGH), -1); + TEST_EQ(ShellPriority(), TS_PRIORITY_HIGH); + TEST_EQ(tsGetPriority(shellGetApp(clk)->mainTaskId), TS_PRIORITY_HIGH); + TEST_EQ(ShellSetPriority(TS_PRIORITY_HIGH + 1), 0); + TEST_EQ(ShellPriority(), TS_PRIORITY_HIGH); + + // A program that starts another is the running program again after + // the start (the shell restores the caller's id, not the shell's) + int32_t extra = shellLoadApp(ctx, APP_CLOCK); + + TEST_TRUE(extra > 0 && extra != clk); + TEST_EQ(ctx->currentAppId, clk); + TEST_EQ(ShellAppId(), clk); + + // A program cannot end itself with ShellRunningKill + TEST_EQ(ShellRunningKill(clk), 0); + ctx->currentAppId = 0; + TEST_EQ(ShellAppId(), 0); + TEST_EQ(ShellPriority(), -1); + TEST_EQ(ShellSetPriority(TS_PRIORITY_NORMAL), 0); + TEST_EQ(ShellRunningKill(extra), -1); + fixPumpShell(SETTLE_FRAMES); + TEST_PTR_EQ(shellGetApp(extra), NULL); + + // Close: only recorded; the main-loop step asks. The document's save + // prompt is answered Cancel, so it stays; the Clock closes. + fixDialogs = 0; + TEST_EQ(ShellRunningClose(dirty), -1); + TEST_EQ(ShellRunningClose(clk), -1); + TEST_EQ(ShellRunningClose(clk), -1); + TEST_EQ(ShellRunningClose(RUN_NO_APP), 0); + TEST_TRUE(shellGetApp(clk) != NULL); + TEST_EQ(fixDialogs, 0); + shellRunCloseRequests(ctx); + fixPumpShell(SETTLE_FRAMES); + TEST_EQ(fixDialogs, 1); + TEST_TRUE(shellGetApp(dirty) != NULL); + TEST_PTR_EQ(shellGetApp(clk), NULL); + TEST_EQ(ShellRunningClose(clk), 0); + shellRunCloseRequests(ctx); + TEST_EQ(fixDialogs, 1); + + // A request for a program that ends before the step is dropped + TEST_EQ(ShellRunningClose(dirty), -1); + TEST_EQ(ShellRunningKill(dirty), -1); + fixPumpShell(SETTLE_FRAMES); + TEST_PTR_EQ(shellGetApp(dirty), NULL); + TEST_EQ(ShellRunningKill(dirty), 0); + shellRunCloseRequests(ctx); + TEST_EQ(fixDialogs, 1); + TEST_EQ(ShellRunningCount(), 0); + + // Command lines by the shell's rule + TEST_STR_EQ(ShellCommandProgram("\"my prog.app\" a \"b c\" "), "my prog.app"); + TEST_STR_EQ(ShellCommandArgs("\"my prog.app\" a \"b c\" "), "a \"b c\""); + TEST_STR_EQ(ShellCommandProgram(" "), ""); + TEST_STR_EQ(ShellCommandArgs(NULL), ""); + TEST_EQ(ShellArgCount("x.app a \"b c\" d"), 4); + TEST_STR_EQ(ShellArg("x.app a \"b c\" d", 0), "x.app"); + TEST_STR_EQ(ShellArg("x.app a \"b c\" d", 2), "b c"); + TEST_STR_EQ(ShellArg("x.app a \"b c\" d", 4), ""); + TEST_STR_EQ(ShellArg("x.app a \"b c\" d", -1), ""); + TEST_EQ(ShellArgCount(""), 0); + TEST_EQ(ShellArgCount(NULL), 0); + + fixDialogsOff(); + fixShellDown(); +} + + +// system.bas pointer, keyboard, screen and memory entry points: the input +// state the desktop sees, the pointer moved, keys typed into the focused +// window one per frame (modifiers released after the last), a screenshot. +static void caseSysInputBas(void) { + tCase("system.bas: pointer, shift state, SysSendKey, screenshot, memory"); + fixShellUp(SCREEN_BPP); + + AppContextT *ctx = tCtx(); + WindowT *win = dvxCreateWindow(ctx, "Keys", 0, 0, SYS_KEY_WIN_W, SYS_KEY_WIN_H, false); + + TEST_TRUE(win != NULL); + + if (!win) { + fixShellDown(); + return; + } + + win->onKey = sysKeyRecord; + sSysKeyCount = 0; + dvxRaiseWindow(ctx, win); + + // The pointer and buttons wherever they are + tMouseMove(SYS_MOUSE_X, SYS_MOUSE_Y); + TEST_EQ(SysMouseX(), SYS_MOUSE_X); + TEST_EQ(SysMouseY(), SYS_MOUSE_Y); + TEST_EQ(SysMouseButtons(), 0); + tMouseDown(SYS_MOUSE_X, SYS_MOUSE_Y, MOUSE_RIGHT); + TEST_EQ(SysMouseButtons(), MOUSE_RIGHT); + tMouseUp(SYS_MOUSE_X, SYS_MOUSE_Y, MOUSE_RIGHT); + TEST_EQ(SysMouseButtons(), 0); + SysSetMousePos(SYS_MOUSE_X * 2, SYS_MOUSE_Y * 2); + fixPumpShell(1); + TEST_EQ(SysMouseX(), SYS_MOUSE_X * 2); + TEST_EQ(SysMouseY(), SYS_MOUSE_Y * 2); + SysSetMousePos(-1, ctx->display.height * 2); + fixPumpShell(1); + TEST_EQ(SysMouseX(), 0); + TEST_EQ(SysMouseY(), ctx->display.height - 1); + + // Shift / Ctrl / Alt held now + platformInjectModifiers(KEY_MOD_CTRL | KEY_MOD_ALT); + fixPumpShell(1); + TEST_EQ(SysShiftState(), KEY_MOD_CTRL | KEY_MOD_ALT); + platformInjectModifiers(0); + fixPumpShell(1); + TEST_EQ(SysShiftState(), 0); + + // Keys typed into the focused window, in order, each with its own + // modifiers; nothing arrives before DVX gets control + TEST_EQ(SysSendKey('a', 0), -1); + TEST_EQ(SysSendKey(KEY_F2, 0), -1); + TEST_EQ(SysSendKey('s', KEY_MOD_CTRL), -1); + TEST_EQ(SysSendKey('B', KEY_MOD_SHIFT), -1); + TEST_EQ(sSysKeyCount, 0); + fixPumpShell(SYS_KEY_FRAMES); + TEST_EQ(sSysKeyCount, SYS_KEYS_SENT); + TEST_EQ(sSysKeys[0], 'a'); + TEST_EQ(sSysMods[0] & KEY_MOD_ALL, 0); + TEST_EQ(sSysKeys[1], KEY_F2); + TEST_EQ(sSysKeys[2], 's' - 'a' + KEY_CTRL_A); + TEST_EQ(sSysMods[2] & KEY_MOD_ALL, KEY_MOD_CTRL); + TEST_EQ(sSysKeys[3], 'B'); + TEST_EQ(sSysMods[3] & KEY_MOD_SHIFT, KEY_MOD_SHIFT); + TEST_EQ(platformKeyboardGetModifiers() & KEY_MOD_ALL, 0); + + // The queue has room for SYS_SEND_KEY_MAX keys + int32_t queued = 0; + + while (queued <= SYS_SEND_KEY_QUEUE && SysSendKey('x', 0)) { + queued++; + } + + TEST_EQ(queued, SYS_SEND_KEY_QUEUE); + fixPumpShell(SYS_SEND_KEY_QUEUE + SYS_KEY_FRAMES); + TEST_EQ(sSysKeyCount, SYS_KEYS_SENT + SYS_SEND_KEY_QUEUE); + + // The whole screen as a picture + TEST_EQ(SysScreenshot(SYS_SHOT), -1); + TEST_TRUE(access(SYS_SHOT, F_OK) == 0); + TEST_EQ(SysScreenshot(""), 0); + TEST_EQ(SysScreenshot(NULL), 0); + TEST_EQ(SysScreenshot("no-such-dir/x.png"), 0); + unlink(SYS_SHOT); + + // Memory: known on the host, free never above total + TEST_TRUE(SysMemoryTotalKb() > 0); + TEST_TRUE(SysMemoryFreeKb() >= 0 && SysMemoryFreeKb() <= SysMemoryTotalKb()); + + dvxDestroyWindow(ctx, win); + fixShellDown(); +} + + // Multi-instance temp-copy corner cases: TEMP dir override, missing // source, unwritable destination, write failure, extension-less names. static void caseTempCopyEdges(void) { @@ -2640,7 +3603,8 @@ void fixExitNote(const char *event) { static WidgetT *helpContentBox(WindowT *win) { WidgetT *splitter = win->widgetRoot->firstChild ? win->widgetRoot->firstChild->nextSibling : NULL; - WidgetT *scroll = (splitter && splitter->firstChild) ? splitter->firstChild->nextSibling : NULL; + WidgetT *view = (splitter && splitter->firstChild) ? splitter->firstChild->nextSibling : NULL; + WidgetT *scroll = view ? view->firstChild : NULL; return scroll ? scroll->firstChild : NULL; } @@ -2676,33 +3640,6 @@ static bool iniHas(const char *section, const char *key, const char *value) { } -// NOTEPAD_APP for the host: the BASIC stub built as a .so, carrying every -// resource bascomp put in the real program (its compiled code and forms). -static bool installNotepad(void) { - char cmd[CMD_BUF_MAX]; - - snprintf(cmd, sizeof(cmd), "%s %s%s -o %s > /dev/null", SHELLTEST_BASCOMP, fixRepoRoot, NOTEPAD_DBP, NOTEPAD_BUILT); - TEST_EQ(system(cmd), 0); - TEST_EQ(platformMkdirRecursive(NOTEPAD_DIR), 0); - TEST_TRUE(fixCopyFileTo(SHELLTEST_BASSTUB_SO, NOTEPAD_APP)); - - DvxResHandleT *built = dvxResOpen(NOTEPAD_BUILT); - bool ok = built != NULL; - - for (uint32_t i = 0; ok && i < built->entryCount; i++) { - uint32_t size = 0; - void *data = dvxResRead(built, built->entries[i].name, &size); - - ok = data && dvxResAppend(NOTEPAD_APP, built->entries[i].name, built->entries[i].type, data, size) == 0; - free(data); - } - - dvxResClose(built); - TEST_TRUE(ok); - return ok; -} - - // Load an app and settle a frame; keeps call sites short. static int32_t loadApp(const char *path) { int32_t id = shellLoadApp(tCtx(), path); @@ -2726,6 +3663,16 @@ static bool menuEnabled(WindowT *win, int32_t id) { } +// parpm's IconView's ItemCol or ItemRow (method) of item index; -1 when +// the call fails. +static int32_t parpmItemCell(WidgetT *icons, const char *method, int32_t index) { + WgtValueT args[] = { wgtValueInt(index) }; + WgtValueT ret; + + return wgtCallMethod(icons, method, args, DVX_ARRAY_LEN(args), &ret) ? wgtValueToInt(&ret) : -1; +} + + static void pmField(const char *label, const char *text) { PmStepT *step = &sPmScript.steps[sPmScript.count - 1]; @@ -2784,12 +3731,17 @@ static void pmStep(const char *button) { } -// Screen point in the middle of a large-icon cell. +// Screen point in the middle of a large-icon cell of an icon view -- +// Program Manager's, or parpm's IconView written in BASIC: both follow +// the list helper library's icon view rules. static void pmViewCellPoint(const WidgetT *items, int32_t col, int32_t row, int32_t *sx, int32_t *sy) { - const WindowT *win = items->window; + const WindowT *win = items->window; + const BitmapFontT *font = &tCtx()->font; + ListIconCellT cell; - *sx = win->x + win->contentX + items->x + PM_VIEW_BORDER + col * PM_CELL_W + PM_CELL_W / 2; - *sy = win->y + win->contentY + items->y + PM_VIEW_BORDER + row * PM_CELL_H + PM_CELL_H / 2; + listIconCellLayout(ListIconLargeE, font->charWidth, font->charHeight, 0, &cell); + *sx = win->x + win->contentX + items->x + LIST_ICON_FRAME + col * cell.cellW + cell.cellW / 2; + *sy = win->y + win->contentY + items->y + LIST_ICON_FRAME + row * cell.cellH + cell.cellH / 2; } @@ -2830,6 +3782,19 @@ static void selectTreeTopic(WindowT *win, const char *title) { } +// caseSysInputBas's window: record each key and its modifiers. +static void sysKeyRecord(WindowT *win, int32_t key, int32_t mod) { + (void)win; + + if (sSysKeyCount < SYS_KEYS_MAX) { + sSysKeys[sSysKeyCount] = key; + sSysMods[sSysKeyCount] = mod; + } + + sSysKeyCount++; +} + + // Press Tab until target has the keyboard focus; false if it never does. static bool tabTo(const WidgetT *target) { for (int32_t i = 0; i < TAB_LIMIT && wgtGetFocused() != target; i++) { @@ -2855,7 +3820,7 @@ static WindowT *waitForWindow(const char *text) { for (int32_t w = 0; w < ctx->stack.count; w++) { WindowT *win = ctx->stack.windows[w]; - if (strstr(win->title, text) && !win->destroyPending) { + if (strstr(win->title, text) && win->visible && !win->destroyPending) { return win; } } @@ -2918,6 +3883,11 @@ int main(void) { RUN_CASE(caseProgmanSysinfoEmpty); RUN_CASE(caseShellInfo); + RUN_CASE(caseShellBasBindings); + RUN_CASE(caseShellRunningBas); + RUN_CASE(caseNewArgsRelaunch); + RUN_CASE(casePardemo); + RUN_CASE(caseParpm); RUN_CASE(caseLoadErrors); RUN_CASE(caseTempCopyEdges); RUN_CASE(caseClockTick); @@ -2930,11 +3900,14 @@ int main(void) { RUN_CASE(caseDocumentOpensNotepad); RUN_CASE(caseProgmanInstalledChange); RUN_CASE(caseCpanelMouse); + RUN_CASE(caseCpanelParity); RUN_CASE(caseCpanelColors); RUN_CASE(caseCpanelDesktop); RUN_CASE(caseCpanelFileTypes); RUN_CASE(caseCpanelJoystick); RUN_CASE(caseCpanelSounds); + RUN_CASE(caseCpanelSysBas); + RUN_CASE(caseSysInputBas); RUN_CASE(caseCpanelVideo); RUN_CASE(caseDemoMenus); RUN_CASE(caseDemoNoAssets); @@ -2947,6 +3920,7 @@ int main(void) { RUN_CASE(caseHelpKeyboard); RUN_CASE(caseHelpSearchFullText); RUN_CASE(caseHelpResizeRewrap); + RUN_CASE(caseParhlp); RUN_CASE(caseCpanelCancelKeepsDblClick); RUN_CASE(caseExitCleanApps); RUN_CASE(caseExitDirtyPrompts); diff --git a/src/test/shell/basic/newargs/newargs.dbp b/src/test/shell/basic/newargs/newargs.dbp new file mode 100644 index 00000000..505cbdde --- /dev/null +++ b/src/test/shell/basic/newargs/newargs.dbp @@ -0,0 +1,17 @@ +[Project] +Name = New Arguments +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Shell suite fixture: a single-copy program handed new arguments + +[Modules] +Count = 0 + +[Forms] +File0 = newargs.frm +Count = 1 + +[Settings] +StartupForm = NewArgsForm diff --git a/src/test/shell/basic/newargs/newargs.frm b/src/test/shell/basic/newargs/newargs.frm new file mode 100644 index 00000000..2e7ca50d --- /dev/null +++ b/src/test/shell/basic/newargs/newargs.frm @@ -0,0 +1,57 @@ +VERSION DVX 1.00 + +Begin Form NewArgsForm + Caption = "New Arguments" + Layout = VBox + AutoSize = False + Resizable = False + Width = 240 + Height = 60 +End + +' 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. + +' newargs -- shell suite fixture. A program that allows one copy and +' has a NewArgs procedure: starting it again hands it the new arguments. +' The title shows "NEWARGS ", count being how many +' times it was handed arguments (0 at the start). + +OPTION EXPLICIT + +DIM SHARED handed AS LONG + + +SUB ShowArgs + NewArgsForm.Caption = "NEWARGS " & handed & " " & COMMAND$ +END SUB + + +SUB NewArgsForm_Load + ShowArgs +END SUB + + +SUB NewArgsForm_NewArgs + handed = handed + 1 + ShowArgs +END SUB diff --git a/src/test/shell/basic/shbind/shbind.dbp b/src/test/shell/basic/shbind/shbind.dbp new file mode 100644 index 00000000..f59f48eb --- /dev/null +++ b/src/test/shell/basic/shbind/shbind.dbp @@ -0,0 +1,21 @@ +[Project] +Name = Shell Bindings +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Shell suite fixture: every shell.bas binding (and the system.bas, prefs.bas and fileops.bas additions) called from BASIC + +[Modules] +File0 = ../../../../include/basic/shell.bas +File1 = ../../../../include/basic/prefs.bas +File2 = ../../../../include/basic/system.bas +File3 = ../../../../include/basic/fileops.bas +Count = 4 + +[Forms] +File0 = shbind.frm +Count = 1 + +[Settings] +StartupForm = ShBind diff --git a/src/test/shell/basic/shbind/shbind.frm b/src/test/shell/basic/shbind/shbind.frm new file mode 100644 index 00000000..8e055775 --- /dev/null +++ b/src/test/shell/basic/shbind/shbind.frm @@ -0,0 +1,318 @@ +VERSION DVX 1.00 + +Begin Form ShBind + Caption = "Shell Bindings" + Layout = VBox + AutoSize = False + Resizable = False + Width = 320 + Height = 90 + + Begin Label lblIds + Caption = "" + End +End + +' 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. + + +' shbind -- shell suite fixture (appsTest caseShellBasBindings): every +' shell.bas binding called from BASIC, in the store and folders the test +' lays out (CONFIG\ASSOC.INI with TXT opened by the Clock with "/doc %1", +' docs\notes.txt, the system information gathered). Each check that +' fails is named in the title, "SHBIND FAIL "; "SHBIND PASS" when +' all held. lblIds holds the ids of the programs it started, for the +' test to look at: " ". The Run dialog is +' answered by the test, the hotkey Ctrl+Alt+K is left registered (the +' test checks it goes with the program), as is a program list (freed when +' the program ends), and Exit DVX is asked for last. CheckRunningMore, +' CheckCommandLines, CheckSystem and CheckFileOps cover the running- +' program, command-line, priority, system.bas, prefs.bas and fileops.bas +' additions; the Clock it starts there is asked to close and then ended +' (the kill drops the waiting close request). + +OPTION EXPLICIT + +CONST CLOCK_APP = "apps/kpunch/clock/clock.app" +CONST PROGMAN_APP = "apps/kpunch/progman/progman.app" +CONST CLOCK_ASSOC = "apps/kpunch/clock/clock.app,/doc %1" +CONST RUN_TYPED = "apps/kpunch/clock/clock.app" +CONST NEW_DESKTOP = "apps/kpunch/fake/desk.app" +CONST NEW_WELCOME = "apps/kpunch/fake/hello.app" +CONST NOTES_DOC = "docs/notes.txt" +CONST STAMP = "2001-02-03 04:05" + +DIM SHARED failText AS STRING + + +SUB Check(BYVAL ok AS BOOLEAN, BYVAL what AS STRING) + IF NOT ok AND failText = "" THEN + failText = what + END IF +END SUB + + +SUB CheckRunningMore + DIM myId AS LONG + DIM i AS LONG + DIM found AS BOOLEAN + DIM other AS LONG + + myId = ShellAppId() + Check myId > 0, "AppId" + + FOR i = 0 TO ShellRunningCount() - 1 + IF ShellRunningId(i) = myId THEN + found = True + END IF + NEXT i + + Check found, "RunningId" + Check ShellRunningId(-1) = 0 AND ShellRunningId(ShellRunningCount()) = 0, "RunningId range" + Check ShellRunningPath$(myId) = ShellAppPath$() AND ShellRunningName$(myId) <> "", "RunningName/Path" + Check ShellRunningName$(0) = "" AND ShellRunningPath$(0) = "", "RunningName/Path 0" + Check ShellRunningFind(ShellAppPath$()) = myId AND ShellRunningFind("apps/none/none.app") = 0, "RunningFind" + Check NOT ShellAppIsHung(myId) AND ShellAppMemory(0) = 0 AND ShellAppMemory(myId) >= 0, "AppIsHung/AppMemory" + Check NOT ShellRunningActivate(0) AND NOT ShellRunningClose(0) AND NOT ShellRunningKill(0), "Running 0" + Check NOT ShellRunningKill(myId), "RunningKill myId" + + Check ShellPriority() = SHELL_PRIORITY_NORMAL, "Priority" + Check ShellSetPriority(SHELL_PRIORITY_HIGH) AND ShellPriority() = SHELL_PRIORITY_HIGH, "SetPriority" + Check NOT ShellSetPriority(SHELL_PRIORITY_HIGH + 1), "SetPriority bad" + Check ShellSetPriority(SHELL_PRIORITY_NORMAL), "SetPriority normal" + + other = ShellRunProgram(CLOCK_APP, "", "") + Check other > 0 AND ShellRunningFind(CLOCK_APP) > 0, "Run other" + Check ShellRunningClose(other), "RunningClose" + Check ShellRunningKill(other) AND ShellRunningName$(other) = "", "RunningKill" +END SUB + + +SUB CheckSystem + DIM snap AS LONG + + snap = PrefsCreate() + Check PrefsVideoWidth(snap) = SYS_VIDEO_DEFAULT_WIDTH AND PrefsVideoHeight(snap) = SYS_VIDEO_DEFAULT_HEIGHT AND PrefsVideoBpp(snap) = SYS_VIDEO_DEFAULT_BPP, "PrefsVideo defaults" + PrefsSetVideoMode snap, 800, 600, 32 + Check PrefsVideoWidth(snap) = 800 AND PrefsVideoHeight(snap) = 600 AND PrefsVideoBpp(snap) = 32, "PrefsVideo" + PrefsClose snap + + Check PrefsChoiceDefault(PREFS_CHOICE_WALLPAPER) = PrefsChoiceIndex(PREFS_CHOICE_WALLPAPER, "stretch"), "ChoiceDefault" + Check PrefsChoiceDefault(99) = -1, "ChoiceDefault bad" + + Check SysMouseX() >= 0 AND SysMouseY() >= 0 AND SysMouseButtons() = 0, "SysMouse" + Check SysShiftState() = 0, "SysShiftState" + Check SysMemoryTotalKb() > 0 AND SysMemoryFreeKb() <= SysMemoryTotalKb(), "SysMemory" + Check NOT SysScreenshot(""), "SysScreenshot empty" +END SUB + + +SUB CheckPrograms + DIM list AS LONG + DIM i AS LONG + DIM n AS LONG + DIM kept AS LONG + + Check ShellIsProgram("A/B.APP"), "IsProgram app" + Check NOT ShellIsProgram("docs/notes.txt"), "IsProgram document" + Check ShellProgramName$("some/dir/widget.app") = "Widget", "ProgramName fallback" + Check ShellProgramDescription$("some/dir/widget.app") = "", "ProgramDescription none" + + list = ShellProgramScan("apps", PROGMAN_APP) + n = ShellProgramCount(list) + Check n > 0, "ProgramScan count" + + FOR i = 0 TO n - 1 + Check LCASE$(ShellProgramPath$(list, i)) <> PROGMAN_APP, "ProgramScan skip" + Check ShellIsProgram(ShellProgramPath$(list, i)), "ProgramScan path" + NEXT i + + Check ShellProgramPath$(list, -1) = "" AND ShellProgramPath$(list, n) = "", "ProgramPath range" + Check ShellProgramScan("no-such-dir", "") = 0, "ProgramScan none" + Check ShellProgramCount(0) = 0 AND ShellProgramPath$(0, 0) = "", "Program list 0" + ShellProgramFree 0 + ShellProgramFree list + Check ShellProgramCount(list) = 0, "ProgramFree" + + ' Left for the end of the program to free. + kept = ShellProgramScan("", "") + Check ShellProgramCount(kept) = n + 1, "ProgramScan default folder" +END SUB + + +SUB CheckFileTypes + Check ShellAssocCount() = 1, "AssocCount" + Check ShellAssocExt$(0) = "TXT" AND ShellAssocValue$(0) = CLOCK_ASSOC, "AssocExt/Value" + Check ShellAssocExt$(1) = "" AND ShellAssocValue$(-1) = "", "Assoc range" + Check ShellAssocProgram$("txt") = CLOCK_ASSOC, "AssocProgram" + Check ShellAssocCommand$("docs/notes.txt") = CLOCK_APP, "AssocCommand" + Check ShellAssocCommand$("x.zzz") = "", "AssocCommand none" + Check ShellAssocNormalize$(".Txt") = "TXT", "AssocNormalize" + Check ShellAssocNormalize$("toolongext") = "" AND ShellAssocNormalize$("a.b") = "", "AssocNormalize bad" + Check NOT ShellAssocSet("a.b", CLOCK_APP), "AssocSet bad" + Check ShellAssocSet(".md", CLOCK_APP) AND ShellAssocCount() = 2, "AssocSet" + Check ShellAssocRemove("MD") AND NOT ShellAssocRemove("MD") AND ShellAssocCount() = 1, "AssocRemove" +END SUB + + +SUB CheckHotkeys + DIM taken AS LONG + DIM other AS LONG + + Check HotkeyText$("ctrl+alt+k") = "Ctrl+Alt+K" AND HotkeyText$("shift+f2") = "Shift+F2", "HotkeyText" + Check HotkeyText$("") = "" AND HotkeyText$("nonsense") = "", "HotkeyText bad" + + taken = HotkeyAdd("Ctrl+Alt+K") + Check taken <> 0, "HotkeyAdd" + Check HotkeyAdd("ctrl+alt+k") = 0, "HotkeyAdd taken" + Check HotkeyAdd("q") = 0 AND HotkeyAdd("nonsense") = 0, "HotkeyAdd bad" + + other = HotkeyAdd("Ctrl+Alt+J") + Check other <> 0 AND other <> taken, "HotkeyAdd second" + Check HotkeyNext() = 0, "HotkeyNext none" + HotkeyRemove other + HotkeyRemove 0 + Check HotkeyAdd("Ctrl+Alt+J") <> 0, "HotkeyRemove" +END SUB + + +SUB CheckRunning + DIM running AS LONG + DIM changes AS LONG + DIM docId AS LONG + DIM cmdId AS LONG + DIM minId AS LONG + + running = ShellRunningCount() + changes = ShellChangeCount() + Check running >= 1 AND ShellInstanceCount() = 1 AND ShellAppPath$() <> "", "this program" + + docId = ShellRunProgram("notes.txt", "", "docs") + Check docId > 0, "RunProgram document in folder" + Check ShellRunningCount() = running + 1 AND ShellChangeCount() <> changes, "RunningCount/ChangeCount" + + cmdId = ShellRunCommand(CHR$(34) + CLOCK_APP + CHR$(34) + " hello") + Check cmdId > 0 AND cmdId <> docId, "RunCommand" + Check ShellRunCommand(" ") = 0, "RunCommand empty" + + minId = ShellRunProgram(CLOCK_APP, "", "") + Check minId > 0, "RunProgram" + ShellMinimizeApp minId + + Check ShellRunProgram("missing.txt", "", "") = 0, "RunProgram missing" + Check ShellRun(CLOCK_APP, "x"), "Run" + Check NOT ShellRun("", ""), "Run empty" + + lblIds.Caption = LTRIM$(STR$(docId)) + " " + LTRIM$(STR$(cmdId)) + " " + LTRIM$(STR$(minId)) +END SUB + + +SUB CheckCommandLines + DIM q AS STRING + + q = CHR$(34) + Check ShellCommandProgram$(q + "my prog.app" + q + " a " + q + "b c" + q) = "my prog.app", "CommandProgram" + Check ShellCommandArgs$(q + "my prog.app" + q + " a " + q + "b c" + q + " ") = "a " + q + "b c" + q, "CommandArgs" + Check ShellCommandProgram$(" ") = "" AND ShellCommandArgs$("x.app") = "", "Command empty" + Check ShellArgCount("x.app a " + q + "b c" + q + " d") = 4 AND ShellArgCount("") = 0, "ArgCount" + Check ShellArg$("x.app a " + q + "b c" + q, 2) = "b c" AND ShellArg$("x.app", 0) = "x.app", "Arg" + Check ShellArg$("x.app", 1) = "" AND ShellArg$("x.app", -1) = "", "Arg range" +END SUB + + +SUB CheckDesktop + DIM ini AS LONG + DIM i AS LONG + DIM found AS BOOLEAN + + Check ShellSetDesktop(NEW_DESKTOP) AND ShellSetWelcome(NEW_WELCOME), "SetDesktop/SetWelcome" + ini = PrefsLoad(PrefsDvxIni$()) + Check PrefsGet$(ini, "shell", "desktop", "") = NEW_DESKTOP AND PrefsGet$(ini, "shell", "welcome", "") = NEW_WELCOME, "SetDesktop saved" + PrefsClose ini + + Check ShellSetDesktop(""), "SetDesktop none" + ini = PrefsLoad(PrefsDvxIni$()) + Check PrefsGet$(ini, "shell", "desktop", "gone") = "gone", "SetDesktop removed" + found = False + + FOR i = 0 TO 99 + IF PrefsKeyName$(ini, "shell", i) = "welcome" THEN + found = True + END IF + NEXT i + + Check found AND PrefsKeyName$(ini, "shell", 99) = "" AND PrefsKeyName$(ini, "shell", -1) = "", "PrefsKeyName" + PrefsClose ini +END SUB + + +SUB CheckFileOps + DIM job AS LONG + + Check FILEOPS_ATTR_EDITABLE = FILEOPS_ATTR_READONLY + FILEOPS_ATTR_HIDDEN + FILEOPS_ATTR_SYSTEM + FILEOPS_ATTR_ARCHIVE, "ATTR_EDITABLE" + Check FileOpsPathSep$() <> "" AND LEN(FileOpsPathSep$()) = 1, "PathSep" + Check FileOpsFilterMatch("A.TXT", "*.bmp;*.txt") AND NOT FileOpsFilterMatch("a.png", "*.txt"), "FilterMatch" + Check FileOpsPathIsInside(NOTES_DOC, "docs") AND NOT FileOpsPathIsInside("docs", NOTES_DOC), "PathIsInside" + Check NOT FileOpsPathIsAbsolute(NOTES_DOC) AND FileOpsPathIsAbsolute("/x"), "PathIsAbsolute" + Check FileOpsIsWritable("docs") AND NOT FileOpsIsWritable("no-such-dir"), "IsWritable" + Check NOT FileOpsReadOnlySystem(), "ReadOnlySystem" + Check FileOpsTotalBytes(".") >= FileOpsFreeBytes(".") AND FileOpsTotalBytes(".") > 0, "TotalBytes" + Check FileOpsDriveType(LEFT$(FileOpsDriveList$(), 1)) = FILEOPS_DRIVE_FIXED AND FileOpsDriveType("Q") = -1, "DriveType" + Check FileOpsBootDrive$() = "", "BootDrive" + Check LEN(FileOpsTempDir$()) >= 0, "TempDir" + Check FileOpsSetModified(NOTES_DOC, STAMP) AND FileOpsModified$(NOTES_DOC) = STAMP, "SetModified" + Check NOT FileOpsSetModified(NOTES_DOC, "2001-02-30 04:05"), "SetModified bad" + + job = FileOpsJobCreate(FILEOPS_DELETE, "") + FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY + Check FileOpsJobBytesTotal(job) = 0 AND FileOpsJobBytesDone(job) = 0, "JobBytes" + FileOpsJobFree job +END SUB + + +SUB ShBind_Load + DIM typed AS STRING + + CheckPrograms + CheckFileTypes + CheckHotkeys + CheckRunning + CheckRunningMore + CheckCommandLines + CheckDesktop + CheckSystem + CheckFileOps + Check LEN(ShellSystemInfo$()) > 0, "SystemInfo" + + typed = ShellRunDialog$("first.app" + CHR$(10) + CHR$(13) + CHR$(10) + "second.app arg") + Check typed = RUN_TYPED, "RunDialog: " + typed + + ShellTaskManager + ShellExitDvx + + IF failText = "" THEN + ShBind.Caption = "SHBIND PASS" + ELSE + ShBind.Caption = "SHBIND FAIL " + failText + END IF +END SUB diff --git a/src/test/shell/fixtures/fixClasses.c b/src/test/shell/fixtures/fixClasses.c new file mode 100644 index 00000000..79fd1aed --- /dev/null +++ b/src/test/shell/fixtures/fixClasses.c @@ -0,0 +1,68 @@ +// 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. + +// fixClasses.c -- shell suite fixture: a callback-only app with a window +// that registers FIX_CLASS_COUNT private widget classes in appMain and +// unregisters them in appShutdown, for the force-kill class release case +// (a force kill skips appShutdown, so the shell must release them). + +#include "shellApp.h" +#include "dvxWgtP.h" +#include "../shellFixture.h" + +#define FIX_WIN_POS 10 +#define FIX_WIN_SIZE 100 + +static int32_t sTypeIds[FIX_CLASS_COUNT]; + +static const WidgetClassT sClassFix = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { 0 } +}; + +int32_t appMain(DxeAppContextT *ctx); +void appShutdown(void); + +SHELL_APP_DESCRIPTOR( + .name = "Classes", + .hasMainLoop = false, + .multiInstance = false, + .stackSize = SHELL_STACK_DEFAULT, + .priority = TS_PRIORITY_NORMAL +); + + +void appShutdown(void) { + for (int32_t i = 0; i < FIX_CLASS_COUNT; i++) { + wgtUnregisterClass(sTypeIds[i]); + } +} + + +int32_t appMain(DxeAppContextT *ctx) { + for (int32_t i = 0; i < FIX_CLASS_COUNT; i++) { + sTypeIds[i] = wgtRegisterClass(&sClassFix); + } + + return dvxCreateWindow(ctx->shellCtx, "Classes", FIX_WIN_POS, FIX_WIN_POS, FIX_WIN_SIZE, FIX_WIN_SIZE, false) ? 0 : -1; +} diff --git a/src/test/shell/fixtures/fixClock.c b/src/test/shell/fixtures/fixClock.c new file mode 100644 index 00000000..348d16db --- /dev/null +++ b/src/test/shell/fixtures/fixClock.c @@ -0,0 +1,165 @@ +// 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. + +// fixClock.c -- host stand-in for the Clock program in the shell suites. +// +// Clock is a BASIC program now; the suites that test the shell itself +// (tasks, priorities, several copies, hotkeys, Task Manager, file types) +// keep the small main-loop program the C Clock was: a "Clock" app with +// its own low-priority task, more than one copy allowed, one fixed-size +// "Clock" window cascaded per copy, a Help menu, and a main loop that +// repaints (the time as a number) when the second changes and ends when the window closes or the +// shell asks it to stop. dvxtest.mk builds it as the host apps' clock.so. + +#include "dvxApp.h" +#include "dvxDraw.h" +#include "dvxWm.h" +#include "shellApp.h" +#include "taskSwch.h" + +#include +#include +#include +#include +#include + +#define CLOCK_TITLE "Clock" +#define CLOCK_WIN_W 200 +#define CLOCK_WIN_H 120 +#define CLOCK_MARGIN 40 +#define CLOCK_CASCADE 16 +#define CMD_HELP_CONTENTS 100 +#define CLOCK_TEXT_MAX 16 + +static DxeAppContextT *sCtx = NULL; +static bool sQuit = false; +static time_t sShown = 0; + +void appShutdown(void); +static int32_t countClockWindows(const AppContextT *ac); +static void onClose(WindowT *win); +static void onMenu(WindowT *win, int32_t menuId); +static void onPaint(WindowT *win, RectT *dirty); +int32_t appMain(DxeAppContextT *ctx); + +SHELL_APP_DESCRIPTOR( + .name = "Clock", + .hasMainLoop = true, + .multiInstance = true, + .stackSize = SHELL_STACK_DEFAULT, + .priority = TS_PRIORITY_LOW +); + + +// Task Manager's End Task and shell shutdown end the main loop. +void appShutdown(void) { + sQuit = true; +} + + +// Clock windows already open, to cascade a new copy. +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; +} + + +static void onClose(WindowT *win) { + (void)win; + sQuit = true; +} + + +static void onMenu(WindowT *win, int32_t menuId) { + (void)win; + + if (menuId == CMD_HELP_CONTENTS) { + shellOpenHelp(sCtx->shellCtx, sCtx->helpFile); + } +} + + +// The seconds since the epoch, centred. +static void onPaint(WindowT *win, RectT *dirty) { + AppContextT *ac = sCtx->shellCtx; + const BlitOpsT *ops = dvxGetBlitOps(ac); + const BitmapFontT *font = dvxGetFont(ac); + const ColorSchemeT *colors = dvxGetColors(ac); + DisplayT cd; + char text[CLOCK_TEXT_MAX]; + + (void)dirty; + snprintf(text, sizeof(text), "%ld", (long)sShown); + dvxContentDisplay(ac, win, &cd); + rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg); + drawText(&cd, ops, font, (win->contentW - textWidth(font, text)) / 2, (win->contentH - font->charHeight) / 2, text, colors->contentFg, colors->contentBg, true); +} + + +int32_t appMain(DxeAppContextT *ctx) { + AppContextT *ac = ctx->shellCtx; + int32_t offset = countClockWindows(ac) * CLOCK_CASCADE; + + sCtx = ctx; + sQuit = false; + sShown = 0; + + WindowT *win = dvxCreateWindow(ac, CLOCK_TITLE, ac->display.width - CLOCK_WIN_W - CLOCK_MARGIN - offset, CLOCK_MARGIN + offset, CLOCK_WIN_W, CLOCK_WIN_H, false); + + if (!win) { + return -1; + } + + win->onClose = onClose; + win->onMenu = onMenu; + win->onPaint = onPaint; + + MenuBarT *menuBar = wmAddMenuBar(win); + MenuT *helpMenu = menuBar ? wmAddMenu(menuBar, "&Help") : NULL; + + if (helpMenu) { + wmAddMenuItem(helpMenu, "&Contents\tF1", CMD_HELP_CONTENTS); + } + + wmReallocContentBuf(win, &ac->display); + + while (!sQuit) { + time_t now = time(NULL); + + if (now != sShown) { + sShown = now; + dvxInvalidateWindow(ac, win); + } + + tsYield(); + } + + dvxDestroyWindow(ac, win); + return 0; +} diff --git a/src/test/shell/fixtures/fixDesktop.c b/src/test/shell/fixtures/fixDesktop.c index 2cb0834d..6677a50d 100644 --- a/src/test/shell/fixtures/fixDesktop.c +++ b/src/test/shell/fixtures/fixDesktop.c @@ -32,6 +32,8 @@ // quit dvxQuit immediately // exit shellRequestExit (Exit DVX: other apps are asked to close) // waitquit dvxQuit once this desktop is the only app left +// close destroy this desktop's window, so the desktop ends (the +// shell then exits DVX by itself) // // Every idle frame first dismisses any open modal dialog by clicking its // OK/Yes/Open/Close button, so error and fault boxes never block the run. @@ -190,6 +192,9 @@ static void fixIdle(void *userCtx) { dvxQuit(sAc); } else if (strcmp(cmd, "exit") == 0) { shellRequestExit(); + } else if (strcmp(cmd, "close") == 0) { + dvxDestroyWindow(sAc, sWin); + sWin = NULL; } else if (strcmp(cmd, "waitquit") == 0) { if (shellRunningAppCount() > 1) { // Not alone yet: rewind so the command runs again next frame. diff --git a/src/test/shell/loaderTest.c b/src/test/shell/loaderTest.c index a8a179d0..1503b442 100644 --- a/src/test/shell/loaderTest.c +++ b/src/test/shell/loaderTest.c @@ -40,6 +40,11 @@ #define HCF_CAPTURE_MAX 8 #define STALE_COUNT 1 #define FRESH_COUNT 2 +#define RES_ONLY_CTL "ctl.wgt" +#define RES_ONLY_NATIVE "native.wgt" +#define RES_ONLY_SHORT "short.wgt" +#define RES_ONLY_MISSING "missing.wgt" +#define RES_ONLY_BASE "ctl" #define FKEY_F1 0x3B #define FKEY_F10 0x44 #define FKEY_F11 0x85 @@ -70,6 +75,7 @@ typedef struct { // Prototypes // ============================================================ +bool basCtlLoadFile(const char *path); static void captureEntry(const char *key, const char *val, const char *exclude, void *user); static void caseAbiStamp(void); static void caseFKeyNormalize(void); @@ -78,7 +84,9 @@ static void caseHcfLongPattern(void); static void caseHelpFailureRetries(void); static void caseHelpLenientLinks(void); static void caseMkdirTooLong(void); +static void caseResourceOnlyWidget(void); static bool fileContains(const char *path, const char *needle); +static void writeBytes(const char *path, const void *data, size_t len); static void writeFile(const char *path, const char *text); @@ -89,12 +97,27 @@ static void writeFile(const char *path, const char *text); static HcfEntryT sEntries[HCF_CAPTURE_MAX]; static int32_t sEntryCount = 0; +// What this test's basCtlLoadFile was handed, and its answer +static int32_t sCtlLoads = 0; +static bool sCtlAccept = true; +static char sCtlLoaded[DVX_MAX_PATH]; + // Stamps for caseAbiStamp: one as DVX_ABI_STAMP writes it, one from // another ABI. -rdynamic exports both to dlsym. DVX_ABI_STAMP(abiunitok); +extern const uint32_t abiunitoldAbiVersion; const uint32_t abiunitoldAbiVersion = DVX_ABI_VERSION + 1; +// The BASIC run-time library's entry as the loader finds it (dlsym(NULL); +// -rdynamic exports it): records what it was handed. +bool basCtlLoadFile(const char *path) { + sCtlLoads++; + snprintf(sCtlLoaded, sizeof(sCtlLoaded), "%s", path); + return sCtlAccept; +} + + static void captureEntry(const char *key, const char *val, const char *exclude, void *user) { (void)user; @@ -320,6 +343,56 @@ static void caseMkdirTooLong(void) { } +// A control written in BASIC (a .wgt starting with DVX_RES_ONLY_MAGIC) is +// told from a native one by its first bytes, never dlopen'd, and handed to +// the BASIC run-time library's basCtlLoadFile once every module is loaded; +// a file it refuses is skipped. +static void caseResourceOnlyWidget(void) { + tCase("resource-only .wgt files go to the BASIC run-time library"); + + static const char ctlData[] = DVX_RES_ONLY_MAGIC "\0module"; + + writeBytes(RES_ONLY_CTL, ctlData, sizeof(ctlData)); + writeFile(RES_ONLY_NATIVE, "\177ELF native code"); + writeFile(RES_ONLY_SHORT, RES_ONLY_BASE); + TEST_TRUE(isResourceOnly(RES_ONLY_CTL)); + TEST_TRUE(!isResourceOnly(RES_ONLY_NATIVE)); + TEST_TRUE(!isResourceOnly(RES_ONLY_SHORT)); + TEST_TRUE(!isResourceOnly(RES_ONLY_MISSING)); + + // Queued without a dlopen (which would be fatal for it) + ModuleT *mods = NULL; + ModuleT ctl; + + memset(&ctl, 0, sizeof(ctl)); + snprintf(ctl.path, sizeof(ctl.path), "%s", RES_ONLY_CTL); + snprintf(ctl.baseName, sizeof(ctl.baseName), "%s", RES_ONLY_BASE); + arrput(mods, ctl); + loadInOrder(mods); + TEST_TRUE(mods[0].loaded); + TEST_PTR_EQ(mods[0].handle, NULL); + TEST_EQ((int32_t)arrlen(sResOnlyWidgets), 1); + arrfree(mods); + + // Handed over once every module is there, then the list is gone + sCtlLoads = 0; + sCtlAccept = true; + loadBasicControls(); + TEST_EQ(sCtlLoads, 1); + TEST_STR_EQ(sCtlLoaded, RES_ONLY_CTL); + TEST_PTR_EQ(sResOnlyWidgets, NULL); + + // A file the library refuses is skipped and logged + arrput(sResOnlyWidgets, strdup(RES_ONLY_CTL)); + sCtlAccept = false; + loadBasicControls(); + TEST_EQ(sCtlLoads, 2); + TEST_PTR_EQ(sResOnlyWidgets, NULL); + TEST_TRUE(fileContains("loaderTest.log", "no usable BASIC control")); + sCtlAccept = true; +} + + static bool fileContains(const char *path, const char *needle) { char line[HCF_LINE_MAX]; bool found = false; @@ -338,18 +411,23 @@ static bool fileContains(const char *path, const char *needle) { } -static void writeFile(const char *path, const char *text) { - FILE *f = fopen(path, "w"); +static void writeBytes(const char *path, const void *data, size_t len) { + FILE *f = fopen(path, "wb"); TEST_TRUE(f != NULL); if (f) { - fputs(text, f); + TEST_EQ((int32_t)fwrite(data, 1, len, f), (int32_t)len); fclose(f); } } +static void writeFile(const char *path, const char *text) { + writeBytes(path, text, strlen(text)); +} + + int main(void) { char startDir[DVX_MAX_PATH]; @@ -367,6 +445,7 @@ int main(void) { caseHcfLongKey(); caseHcfLongPattern(); caseMkdirTooLong(); + caseResourceOnlyWidget(); caseHelpFailureRetries(); caseHelpLenientLinks(); diff --git a/src/test/shell/progmanTest.c b/src/test/shell/progmanTest.c index 79cf539f..a1db2e43 100644 --- a/src/test/shell/progmanTest.c +++ b/src/test/shell/progmanTest.c @@ -24,18 +24,22 @@ // // The group/item model (pmModel.c) and PROGMAN.INI round trip, including // damaged files; the file association store (shellAssoc.c) with a fake -// launcher; installed-program discovery and icons (shellProg.c); and the -// command-line helpers and shellRunProgram (shellRun.c). Program -// Manager's window itself is driven by appsTest.c. +// launcher; installed-program discovery and icons (shellProg.c); the +// command-line helpers, shellRunProgram and Run Minimized (shellRun.c); +// the desktop change counter and Task Manager hook; and the icon view +// rules Program Manager's view shares with BASIC icon views (listHelp.c). +// Program Manager's window itself is driven by appsTest.c. #include "shellFixture.h" #include "shellAssoc.h" #include "shellProg.h" #include "shellRun.h" #include "pmModel.h" +#include "listHelp.h" #include "dvxPrefs.h" #include "stb_ds_wrap.h" +#include #include #include #include @@ -55,20 +59,38 @@ #define APP_COUNT_TREE 5 // clock, progman, cpanel, dvxdemo, dvxhelp #define DEMO_BMP "apps/kpunch/dvxdemo/logo.bmp" #define LONG_EXT "ABCDEFGHI" +#define NO_APP_ID 9999 +#define BAS_TRUE (-1) +#define TEXT_MAX DVX_MAX_PATH +#define ICON_CHAR_W 8 // the icon view cases' font +#define ICON_CHAR_H 16 +#define ICON_VIEW_W 330 // a view four large cells wide (and the frame) +#define ICON_VIEW_H 150 // two large rows tall +#define ICON_LIST_W 300 // a list's inner width +#define ICON_LABEL_MAX 10 // a large label's characters (an 80-pixel cell) +#define ICON_ROWS_OWN 5 // rows items in cells of their own take + // ============================================================ // Prototypes // ============================================================ static void caseAssoc(void); +static void caseDesktopServices(void); +static void caseGroupBindings(void); static void caseHotkeys(void); +static void caseIconViewRules(void); +static void caseItemIcon(void); static void caseModelDamaged(void); static void caseModelEdits(void); +static void caseModelFields(void); static void caseModelFirstRun(void); static void caseModelRoundTrip(void); static void caseProgScan(void); static void caseRunHelpers(void); +static void caseRunMinimized(void); static void caseRunProgram(void); +static void countTaskManager(AppContextT *ctx); static int32_t fakeLaunch(AppContextT *ctx, const char *path, const char *args); static PmItemT makeItem(const char *name, const char *program); static void writeText(const char *path, const char *text); @@ -77,6 +99,7 @@ static char sLaunchPath[DVX_MAX_PATH]; static char sLaunchArgs[SHELL_ARGS_MAX]; static int32_t sLaunchResult = LAUNCH_OK; static int32_t sLaunches = 0; +static int32_t sTaskManagers = 0; // The association store: normalized keys, lookup, enumeration, set and @@ -191,6 +214,126 @@ static void caseAssoc(void) { // Shortcut keys: Ctrl with Alt or Shift plus a letter, digit or F-key, in // any case and order; formatting reads back the same way. +// The change counter every shellDesktopUpdate moves (what a BASIC desktop +// polls), and opening Task Manager with and without the library. +static void caseDesktopServices(void) { + tCase("desktop change counter and Task Manager"); + fixShellUp(SCREEN_BPP); + + int32_t before = shellDesktopChangeCount(); + + shellDesktopUpdate(); + TEST_EQ(shellDesktopChangeCount(), before + 1); + + int32_t id = shellLoadApp(tCtx(), APP_CLOCK); + + TEST_TRUE(id > 0); + TEST_EQ(shellDesktopChangeCount(), before + 2); + fixKillAllApps(); + + sTaskManagers = 0; + shellCtrlEscFn = NULL; + shellOpenTaskManager(tCtx()); + shellCtrlEscFn = countTaskManager; + shellOpenTaskManager(tCtx()); + TEST_EQ(sTaskManagers, 1); + shellCtrlEscFn = NULL; + fixShellDown(); +} + + +// shell.bas program groups from C: a groups file opened, filled, changed, +// saved and read back the way a BASIC Program Manager does; the shortcut +// rule and the programs folder. +static void caseGroupBindings(void) { + tCase("shell.bas program groups (ShellGroups*, ShellItem*)"); + fixShellUp(SCREEN_BPP); + remove(INI_FILE); + + int32_t h = ShellGroupsLoad(INI_FILE); + + TEST_TRUE(h > 0); + TEST_EQ(ShellGroupCount(h), 3); + TEST_STR_EQ(ShellGroupName(h, 0), PM_INSTALLED_NAME); + TEST_EQ(ShellGroupFind(h, "startup"), 2); + TEST_EQ(ShellGroupsScan(h, APP_PROGMAN), APP_COUNT_TREE - 1); + TEST_EQ(ShellItemCount(h, 0), APP_COUNT_TREE - 1); + TEST_EQ(ShellGroupsChanged(h, APP_PROGMAN), 0); + TEST_EQ(ShellGroupsChanged(h, ""), BAS_TRUE); + TEST_TRUE(ShellItemGet(h, 0, 0, "name")[0] != '\0'); + TEST_EQ(ShellItemSet(h, 0, 0, "name", "X"), 0); + + // Groups and items + int32_t g = ShellGroupAdd(h, "Tools"); + + TEST_EQ(g, 3); + TEST_EQ(ShellGroupNameValid(h, "tools", -1), 0); + TEST_EQ(ShellGroupNameValid(h, "tools", g), BAS_TRUE); + TEST_EQ(ShellGroupRename(h, g, "Kit"), BAS_TRUE); + TEST_EQ(ShellItemAdd(h, g, "Demo", APP_DVXDEMO), 0); + TEST_EQ(ShellItemAdd(h, g, "Notes", DOC_FILE), 1); + TEST_EQ(ShellItemAdd(h, g, "", APP_DVXDEMO), -1); + TEST_EQ(ShellItemAdd(h, 0, "Nope", APP_DVXDEMO), -1); + TEST_EQ(ShellItemSet(h, g, 0, "key", "ctrl+alt+d"), BAS_TRUE); + TEST_STR_EQ(ShellItemGet(h, g, 0, "key"), "Ctrl+Alt+D"); + TEST_EQ(ShellItemSet(h, g, 0, "key", "Ctrl+D"), 0); + TEST_EQ(ShellItemSet(h, g, 0, "minimized", "1"), BAS_TRUE); + TEST_EQ(ShellItemSet(h, g, 0, "args", " -x "), BAS_TRUE); + TEST_STR_EQ(ShellItemGet(h, g, 0, "args"), "-x"); + TEST_EQ(ShellItemSet(h, g, 0, "bogus", "1"), 0); + TEST_STR_EQ(ShellItemGet(h, g, 0, "bogus"), ""); + TEST_EQ(ShellItemMatches(h, g, 1, "NOTE"), BAS_TRUE); + TEST_EQ(ShellItemMatches(h, g, 1, "zzz"), 0); + TEST_EQ(ShellItemReorder(h, g, 1, 0), 0); + TEST_STR_EQ(ShellItemGet(h, g, 0, "name"), "Notes"); + ShellGroupArrange(h, g, GRID_COLS); + TEST_STR_EQ(ShellItemGet(h, g, 1, "col"), "1"); + TEST_EQ(ShellGroupCellItem(h, g, 1, 0), 1); + ShellItemPlace(h, g, 1, 2, 2); + TEST_EQ(ShellGroupCellItem(h, g, 2, 2), 1); + TEST_EQ(ShellItemCopy(h, g, 1, 1), 0); + TEST_EQ(ShellItemMove(h, g, 0, 2), 0); + TEST_EQ(ShellItemCount(h, g), 1); + TEST_EQ(ShellItemDelete(h, 2, 0), BAS_TRUE); + TEST_EQ(ShellItemDelete(h, 2, 0), 0); + + // Options and history + TEST_EQ(ShellGroupsSet(h, "view", PmViewListE), BAS_TRUE); + TEST_EQ(ShellGroupsSet(h, "view", PmViewCountE), 0); + TEST_EQ(ShellGroupsSet(h, "autoArrange", 0), BAS_TRUE); + TEST_EQ(ShellGroupsSet(h, "group", g), BAS_TRUE); + TEST_EQ(ShellGroupsGet(h, "autoArrange"), 0); + TEST_EQ(ShellGroupsGet(h, "minimizeOnRun"), 0); + TEST_EQ(ShellGroupsGet(h, "nope"), -1); + ShellGroupsHistoryAdd(h, "first.app"); + ShellGroupsHistoryAdd(h, "second.app -x"); + TEST_STR_EQ(ShellGroupsHistory(h), "second.app -x\nfirst.app"); + TEST_EQ(ShellGroupsSave(h, INI_FILE), BAS_TRUE); + ShellGroupsFree(h); + TEST_EQ(ShellGroupCount(h), 0); + + h = ShellGroupsLoad(INI_FILE); + TEST_EQ(ShellGroupsGet(h, "view"), PmViewListE); + TEST_EQ(ShellGroupsGet(h, "group"), g); + TEST_EQ(ShellGroupsGet(h, "autoArrange"), 0); + TEST_STR_EQ(ShellItemGet(h, g, 0, "key"), "Ctrl+Alt+D"); + TEST_STR_EQ(ShellItemGet(h, g, 0, "minimized"), "1"); + TEST_STR_EQ(ShellGroupsHistory(h), "second.app -x\nfirst.app"); + TEST_EQ(ShellGroupDelete(h, g), BAS_TRUE); + TEST_EQ(ShellGroupDelete(h, 0), 0); + ShellGroupsFree(h); + + // The shortcut rule and the programs folder + TEST_EQ(ShellItemKeyValid(""), BAS_TRUE); + TEST_EQ(ShellItemKeyValid("Ctrl+Shift+F5"), BAS_TRUE); + TEST_EQ(ShellItemKeyValid("Ctrl+P"), 0); + TEST_EQ(ShellItemKeyValid("Alt+Shift+P"), 0); + TEST_STR_EQ(ShellProgramsFolder(), SHELL_PROG_DIR); + remove(INI_FILE); + fixShellDown(); +} + + static void caseHotkeys(void) { int32_t key = -1; int32_t mods = -1; @@ -206,14 +349,16 @@ static void caseHotkeys(void) { TEST_TRUE(pmHotkeyParse(" Shift + Ctrl + F12 ", &key, &mods)); TEST_EQ(key, KEY_F12); TEST_EQ(mods, ACCEL_CTRL | ACCEL_SHIFT); - pmHotkeyFormat(key, mods, text, sizeof(text)); + dvxAccelFormat(key, mods, text, sizeof(text)); TEST_STR_EQ(text, "Ctrl+Shift+F12"); TEST_TRUE(pmHotkeyParse("Ctrl+Alt+7", &key, &mods)); - pmHotkeyFormat(key, mods, text, sizeof(text)); + dvxAccelFormat(key, mods, text, sizeof(text)); TEST_STR_EQ(text, "Ctrl+Alt+7"); TEST_TRUE(pmHotkeyParse("Ctrl+Alt+F", &key, &mods)); TEST_EQ(key, 'F'); - pmHotkeyFormat(0, ACCEL_CTRL, text, sizeof(text)); + TEST_TRUE(pmHotkeyParse("Ctrl+Shift+Home", &key, &mods)); + TEST_EQ(key, KEY_HOME); + dvxAccelFormat(0, ACCEL_CTRL, text, sizeof(text)); TEST_STR_EQ(text, ""); TEST_TRUE(!pmHotkeyParse("P", &key, &mods)); @@ -230,6 +375,257 @@ static void caseHotkeys(void) { } +// Program Manager's icon view rules (listHelp.c, shared with BASIC +// icon views): the cells of the three views, the grid with a scroll bar +// only when the rows do not fit, hits, the drag scroll and target, the +// drop rule, the drag slop, the two label lines and the arrow keys among +// items in order and in cells of their own. +static void caseIconViewRules(void) { + ListIconCellT cell; + ListIconGridT g; + ListIconGridT more; + ListIconGridT own; + int32_t col; + int32_t row; + int32_t len1; + int32_t len2; + const char *line2; + + tCase("icon view rules (listHelp.c)"); + + // Large icons: the picture centred at the top, two label lines (80 x + // 70 with an 8 x 16 font, as Program Manager has always drawn them) + listIconCellLayout(ListIconLargeE, ICON_CHAR_W, ICON_CHAR_H, 0, &cell); + TEST_EQ(cell.cellW, LIST_ICON_LARGE_CELL_W); + TEST_EQ(cell.cellH, 70); + TEST_EQ(cell.pictureX, (LIST_ICON_LARGE_CELL_W - LIST_ICON_LARGE) / 2); + TEST_EQ(cell.pictureY, LIST_ICON_PAD); + TEST_EQ(cell.pictureSize, LIST_ICON_LARGE); + TEST_EQ(cell.textX, 0); + TEST_EQ(cell.textY, LIST_ICON_PAD + LIST_ICON_LARGE + LIST_ICON_LABEL_GAP); + TEST_EQ(cell.nameChars, ICON_LABEL_MAX); + TEST_EQ(cell.detailX, 0); + + // Small icons and the list: a row the height of the font or the + // picture; the list spans the grid and has a detail column + listIconCellLayout(ListIconSmallE, ICON_CHAR_W, ICON_CHAR_H, 0, &cell); + TEST_EQ(cell.cellH, ICON_CHAR_H + LIST_ICON_PAD * 2); + TEST_EQ(cell.textX, LIST_ICON_PAD + LIST_ICON_SMALL + LIST_ICON_TEXT_GAP); + TEST_EQ(cell.cellW, cell.textX + LIST_ICON_SMALL_CHARS * ICON_CHAR_W + LIST_ICON_PAD); + TEST_EQ(cell.pictureSize, LIST_ICON_SMALL); + TEST_EQ(cell.nameChars, LIST_ICON_SMALL_CHARS); + TEST_EQ(cell.detailX, 0); + listIconCellLayout(ListIconListE, ICON_CHAR_W, ICON_CHAR_H, ICON_LIST_W, &cell); + TEST_EQ(cell.cellW, ICON_LIST_W); + TEST_EQ(cell.nameChars, LIST_ICON_LIST_CHARS); + TEST_EQ(cell.detailX, cell.textX + (LIST_ICON_LIST_CHARS + LIST_ICON_COLUMN_GAP) * ICON_CHAR_W); + listIconCellLayout(ListIconListE, 0, 0, 0, &cell); + TEST_TRUE(cell.cellW > 0 && cell.cellH > 0); + + // The grid: eight items fit four columns and two rows; nine need a + // scroll bar, which narrows the grid to three columns + listIconGrid(ListIconLargeE, ICON_CHAR_W, ICON_CHAR_H, ICON_VIEW_W, ICON_VIEW_H, 8, LIST_ICON_BY_ORDER, &g); + TEST_EQ(g.innerX, LIST_ICON_FRAME); + TEST_EQ(g.innerW, ICON_VIEW_W - LIST_ICON_FRAME * 2); + TEST_EQ(g.cols, 4); + TEST_EQ(g.rowsVisible, 2); + TEST_EQ(g.totalRows, 2); + TEST_EQ(g.maxScroll, 0); + TEST_TRUE(!g.needSb); + TEST_EQ(g.emptyX, LIST_ICON_FRAME + LIST_ICON_PAD); + listIconGrid(ListIconLargeE, ICON_CHAR_W, ICON_CHAR_H, ICON_VIEW_W, ICON_VIEW_H, 9, LIST_ICON_BY_ORDER, &more); + TEST_TRUE(more.needSb); + TEST_EQ(more.innerW, ICON_VIEW_W - LIST_ICON_FRAME * 2 - LIST_ICON_SB_W); + TEST_EQ(more.cols, 3); + TEST_EQ(more.totalRows, 3); + TEST_EQ(more.maxScroll, 1); + TEST_EQ(listIconGridMetric(&more, ListIconScrollBarE), 1); + TEST_EQ(listIconGridMetric(&more, ListIconColumnsE), 3); + TEST_EQ(listIconGridMetric(&more, ListIconTopRowMaxE), 1); + TEST_EQ(listIconGridMetric(&more, ListIconCellHE), more.cell.cellH); + TEST_EQ(listIconGridMetric(&more, ListIconFrameE), LIST_ICON_FRAME); + TEST_EQ(listIconGridMetric(&more, ListIconWheelRowsE), LIST_ICON_WHEEL_ROWS); + TEST_EQ(listIconGridMetric(&more, ListIconMetricCountE), 0); + + // Items in cells of their own take their rows whatever the count + listIconGrid(ListIconLargeE, ICON_CHAR_W, ICON_CHAR_H, ICON_VIEW_W, ICON_VIEW_H, 2, ICON_ROWS_OWN, &own); + TEST_EQ(own.totalRows, ICON_ROWS_OWN); + TEST_TRUE(own.needSb); + + // Hits: the cell (reading order) under a point, scrolled or not; + // nothing outside the grid or right of the last column + int32_t inX = g.innerX + g.cell.cellW + LIST_ICON_PAD; + int32_t inY = g.innerY + g.cell.cellH + LIST_ICON_PAD; + + TEST_EQ(listIconCellAt(&g, inX, inY, 0, &col, &row), g.cols + 1); + TEST_EQ(col, 1); + TEST_EQ(row, 1); + TEST_EQ(listIconCellAt(&g, inX, inY, 1, &col, &row), g.cols * 2 + 1); + TEST_EQ(listIconCellAt(&g, 0, inY, 0, &col, &row), LIST_ICON_NO_CELL); + TEST_EQ(col, LIST_ICON_NO_CELL); + TEST_EQ(listIconCellAt(&g, g.innerX + g.cols * g.cell.cellW, inY, 0, &col, &row), LIST_ICON_NO_CELL); + TEST_EQ(row, LIST_ICON_NO_CELL); + + // Dragging: a row up above the grid, down below it, up to the last + // top row; the target held inside the grid's height + TEST_EQ(listIconDragScroll(&more, 0, 1), 0); + TEST_EQ(listIconDragScroll(&more, 0, 0), 0); + TEST_EQ(listIconDragScroll(&more, more.innerY + more.innerH, 0), 1); + TEST_EQ(listIconDragScroll(&more, more.innerY + more.innerH, 1), 1); + TEST_EQ(listIconDragScroll(&more, inY, 0), 0); + TEST_EQ(listIconDropCell(&g, inX, -ICON_VIEW_H, 0, &col, &row), 1); + TEST_EQ(listIconDropCell(&g, inX, ICON_VIEW_H * 2, 0, &col, &row), (g.innerH - 1) / g.cell.cellH * g.cols + 1); + TEST_TRUE(!listIconDragStarted(LIST_ICON_DRAG_SLOP - 1, -(LIST_ICON_DRAG_SLOP - 1))); + TEST_TRUE(listIconDragStarted(LIST_ICON_DRAG_SLOP, 0)); + TEST_TRUE(listIconDragStarted(0, -LIST_ICON_DRAG_SLOP)); + + // The scroll bar: nothing left of it or without one; its arrows a + // row, the thumb grabbed (where in it) and dragged to the end + int32_t top = 0; + int32_t grab = -1; + int32_t barX = more.innerX + more.innerW + LIST_ICON_SB_W / 2; + int32_t tPos = 0; + int32_t tLen = 0; + + TEST_EQ(listIconScrollPress(&more, more.innerX, more.innerY, &top, &grab), ListIconScrollNoneE); + TEST_EQ(listIconScrollPress(&g, barX, g.innerY, &top, &grab), ListIconScrollNoneE); + TEST_EQ(listIconScrollPress(&more, barX, more.innerY + more.innerH - 1, &top, &grab), ListIconScrollMovedE); + TEST_EQ(top, 1); + TEST_EQ(listIconScrollPress(&more, barX, more.innerY, &top, &grab), ListIconScrollMovedE); + TEST_EQ(top, 0); + widgetScrollbarThumb(more.innerH - LIST_ICON_SB_W * 2, more.totalRows, more.rowsVisible, 0, &tPos, &tLen); + TEST_EQ(listIconScrollPress(&more, barX, more.innerY + LIST_ICON_SB_W + tPos + 1, &top, &grab), ListIconScrollThumbE); + TEST_EQ(grab, 1); + TEST_EQ(listIconScrollDrag(&more, ICON_VIEW_H * 2, grab), more.maxScroll); + TEST_EQ(listIconScrollDrag(&more, -ICON_VIEW_H, grab), 0); + + // Dropped on a cell: its place, the last for one past the end + TEST_EQ(listIconDropDest(0, 2, 6), 2); + TEST_EQ(listIconDropDest(0, 9, 6), 5); + TEST_EQ(listIconDropDest(3, 3, 6), -1); + TEST_EQ(listIconDropDest(5, 9, 6), -1); + TEST_EQ(listIconDropDest(-1, 0, 6), -1); + TEST_EQ(listIconDropDest(0, -1, 6), -1); + + // Label lines: at the last blank that fits, else cut; the second line + // after the blanks, cut + listIconLabelSplit("Clock", ICON_LABEL_MAX, &len1, &line2, &len2); + TEST_EQ(len1, 5); + TEST_EQ(len2, 0); + listIconLabelSplit("BASIC Demo App", ICON_LABEL_MAX, &len1, &line2, &len2); + TEST_EQ(len1, 10); + TEST_STR_EQ(line2, "App"); + listIconLabelSplit("Help Editor", ICON_LABEL_MAX, &len1, &line2, &len2); + TEST_EQ(len1, 4); + TEST_EQ(len2, 6); + TEST_STR_EQ(line2, "Editor"); + listIconLabelSplit("Supercalifragilistic", ICON_LABEL_MAX, &len1, &line2, &len2); + TEST_EQ(len1, ICON_LABEL_MAX); + TEST_EQ(len2, ICON_LABEL_MAX); + TEST_TRUE(strncmp(line2, "ragilistic", (size_t)len2) == 0); + listIconLabelSplit("Resource Editor Program", ICON_LABEL_MAX, &len1, &line2, &len2); + TEST_EQ(len1, 8); + TEST_EQ(len2, ICON_LABEL_MAX); + TEST_TRUE(strncmp(line2, "Editor Pro", (size_t)len2) == 0); + listIconLabelSplit("Clock", 0, &len1, &line2, &len2); + TEST_EQ(len1, 0); + TEST_EQ(len2, 0); + listIconLabelSplit(NULL, ICON_LABEL_MAX, &len1, &line2, &len2); + TEST_EQ(len1, 0); + TEST_EQ(len2, 0); + + // The arrow keys: seven items in order, three a row (as the grid + // rule of widgetGridNavigateIndex), then three in cells with gaps + static const int32_t orderCols[] = { 0, 1, 2, 0, 1, 2, 0 }; + static const int32_t orderRows[] = { 0, 0, 0, 1, 1, 1, 2 }; + static const int32_t ownCols[] = { 0, 2, 1 }; + static const int32_t ownRows[] = { 0, 0, 2 }; + int32_t count = (int32_t)DVX_ARRAY_LEN(orderCols); + int32_t ownCount = (int32_t)DVX_ARRAY_LEN(ownCols); + int32_t keys[] = { KEY_RIGHT, KEY_LEFT, KEY_DOWN, KEY_DOWN, KEY_UP, KEY_HOME, KEY_END, KEY_PGDN, KEY_PGUP }; + int32_t froms[] = { 2, 3, 1, 4, 0, 5, 0, 1, 6 }; + + for (int32_t k = 0; k < (int32_t)DVX_ARRAY_LEN(keys); k++) { + int32_t want = widgetGridNavigateIndex(keys[k], froms[k], count, GRID_COLS, 2); + + if (want == froms[k]) { + want = -1; + } + + TEST_EQ(listIconNavigate(keys[k], froms[k], orderCols, orderRows, count, GRID_COLS, 2), want); + } + + TEST_EQ(listIconNavigate(KEY_RIGHT, -1, orderCols, orderRows, count, GRID_COLS, 2), 0); + TEST_EQ(listIconNavigate(KEY_END, -1, orderCols, orderRows, count, GRID_COLS, 2), count - 1); + TEST_EQ(listIconNavigate('x', 0, orderCols, orderRows, count, GRID_COLS, 2), -1); + TEST_EQ(listIconNavigate(KEY_RIGHT, 0, orderCols, orderRows, 0, GRID_COLS, 2), -1); + TEST_EQ(listIconNavigate(KEY_RIGHT, 0, ownCols, ownRows, ownCount, GRID_COLS, 2), 1); + TEST_EQ(listIconNavigate(KEY_DOWN, 1, ownCols, ownRows, ownCount, GRID_COLS, 2), 2); + TEST_EQ(listIconNavigate(KEY_UP, 2, ownCols, ownRows, ownCount, GRID_COLS, 2), 0); + TEST_EQ(listIconNavigate(KEY_LEFT, 0, ownCols, ownRows, ownCount, GRID_COLS, 2), -1); +} + + +// The item icon rule: the icon file, else the program's own icon (or the +// picture itself), else the icon of the program that opens a document. +static void caseItemIcon(void) { + int32_t pitch = 0; + int32_t bpp; + uint8_t *icon; + + tCase("item icons"); + fixShellUp(SCREEN_BPP); + bpp = tCtx()->display.format.bytesPerPixel; + writeText(ASSOC_FILE, "[associations]\nTXT = " APP_DVXDEMO ",%1\n"); + writeText(DOC_FILE, "x"); + writeText("other.xyz", "x"); + shellAssocTestReset(ASSOC_FILE, NULL); + + // The icon file, at either size, in place of the program's. + icon = shellItemIcon(tCtx(), DEMO_BMP, "apps/kpunch/progman/progman.app", SHELL_PROG_ICON_SMALL, &pitch); + TEST_TRUE(icon != NULL); + TEST_EQ(pitch, SHELL_PROG_ICON_SMALL * bpp); + dvxFreeImage(icon); + + // A missing icon file: the program's own icon; NULL and "" mean none. + icon = shellItemIcon(tCtx(), "missing.bmp", APP_DVXDEMO, SHELL_PROG_ICON, &pitch); + TEST_TRUE(icon != NULL); + dvxFreeImage(icon); + icon = shellItemIcon(tCtx(), NULL, APP_DVXDEMO, SHELL_PROG_ICON, &pitch); + TEST_TRUE(icon != NULL); + dvxFreeImage(icon); + + // A picture is its own icon; a document gets its program's. + icon = shellItemIcon(tCtx(), "", DEMO_BMP, SHELL_PROG_ICON, &pitch); + TEST_TRUE(icon != NULL); + dvxFreeImage(icon); + icon = shellItemIcon(tCtx(), "", DOC_FILE, SHELL_PROG_ICON, &pitch); + TEST_TRUE(icon != NULL); + TEST_EQ(pitch, SHELL_PROG_ICON * bpp); + dvxFreeImage(icon); + + // None: a program without an icon, a document of no known type. + TEST_PTR_EQ(shellItemIcon(tCtx(), "", "apps/kpunch/progman/progman.app", SHELL_PROG_ICON, &pitch), NULL); + TEST_PTR_EQ(shellItemIcon(tCtx(), "", "other.xyz", SHELL_PROG_ICON, &pitch), NULL); + TEST_PTR_EQ(shellItemIcon(tCtx(), NULL, NULL, SHELL_PROG_ICON, &pitch), NULL); + TEST_PTR_EQ(shellItemIcon(NULL, "", APP_DVXDEMO, SHELL_PROG_ICON, &pitch), NULL); + + // The same rule naming the file the icon comes from (ShellItemIcon$) + char file[TEXT_MAX]; + + TEST_TRUE(shellItemIconFile(tCtx(), DEMO_BMP, APP_PROGMAN, file, (int32_t)sizeof(file))); + TEST_STR_EQ(file, DEMO_BMP); + TEST_TRUE(shellItemIconFile(tCtx(), "missing.bmp", APP_DVXDEMO, file, (int32_t)sizeof(file))); + TEST_STR_EQ(file, APP_DVXDEMO); + TEST_STR_EQ(ShellItemIcon("", DOC_FILE), APP_DVXDEMO); + TEST_TRUE(!shellItemIconFile(tCtx(), "", "other.xyz", file, (int32_t)sizeof(file))); + TEST_STR_EQ(file, ""); + TEST_STR_EQ(ShellItemIcon("", APP_PROGMAN), ""); + shellAssocTestReset(NULL, NULL); + fixShellDown(); +} + + // Damaged PROGMAN.INI: junk bytes, absurd counts, nameless groups and // items, duplicate names, bad shortcuts and cells are all survived. static void caseModelDamaged(void) { @@ -422,7 +818,7 @@ static void caseModelEdits(void) { for (int32_t i = 0; i < PM_HISTORY_MAX + 3; i++) { char cmd[32]; - snprintf(cmd, sizeof(cmd), "p%ld.app", (long)i); + snprintf(cmd, sizeof(cmd), "p%" PRId32 ".app", i); pmHistoryAdd(&m, cmd); } @@ -439,7 +835,7 @@ static void caseModelEdits(void) { for (int32_t i = 0; i < PM_GROUP_MAX + 2; i++) { char name[32]; - snprintf(name, sizeof(name), "G%ld", (long)i); + snprintf(name, sizeof(name), "G%" PRId32, i); pmGroupAdd(&m, name); } @@ -449,6 +845,70 @@ static void caseModelEdits(void) { } +// Item fields and options by their PROGMAN.INI names (what the BASIC +// bindings read and write), and the automatic group from the installed +// programs. +static void caseModelFields(void) { + PmModelT m; + char text[TEXT_MAX]; + int32_t value = 0; + bool isFlag = false; + + tCase("model fields, options and the installed programs"); + fixShellUp(SCREEN_BPP); + pmModelInit(&m); + + PmItemT item = makeItem("Alpha", "a.app"); + + TEST_TRUE(pmItemGetField(&item, PM_FIELD_NAME, text, (int32_t)sizeof(text))); + TEST_STR_EQ(text, "Alpha"); + TEST_TRUE(pmItemSetField(&item, "PROGRAM", " b.app ")); + TEST_STR_EQ(item.program, "b.app"); + TEST_TRUE(!pmItemSetField(&item, PM_FIELD_PROGRAM, " ")); + TEST_TRUE(!pmItemSetField(&item, PM_FIELD_NAME, "")); + TEST_TRUE(pmItemSetField(&item, PM_FIELD_KEY, "ctrl+shift+f2")); + TEST_TRUE(pmItemGetField(&item, PM_FIELD_KEY, text, (int32_t)sizeof(text))); + TEST_STR_EQ(text, "Ctrl+Shift+F2"); + TEST_TRUE(!pmItemSetField(&item, PM_FIELD_KEY, "F2")); + TEST_TRUE(pmItemSetField(&item, PM_FIELD_KEY, "")); + TEST_EQ(item.hotkeyKey, 0); + TEST_TRUE(pmItemSetField(&item, PM_FIELD_COL, "-5")); + TEST_EQ(item.col, PM_CELL_NONE); + TEST_TRUE(pmItemSetField(&item, PM_FIELD_ROW, "4")); + TEST_TRUE(pmItemGetField(&item, PM_FIELD_ROW, text, (int32_t)sizeof(text))); + TEST_STR_EQ(text, "4"); + TEST_TRUE(pmItemSetField(&item, PM_FIELD_MINIMIZED, "1")); + TEST_TRUE(item.runMinimized); + TEST_TRUE(pmItemSetField(&item, PM_FIELD_DESCRIPTION, "Tip")); + TEST_STR_EQ(item.description, "Tip"); + TEST_TRUE(!pmItemSetField(&item, "nope", "x")); + TEST_TRUE(!pmItemGetField(&item, "nope", text, (int32_t)sizeof(text))); + TEST_TRUE(!pmItemSetField(&item, NULL, "x")); + TEST_TRUE(!pmItemGetField(&item, NULL, text, (int32_t)sizeof(text))); + + TEST_TRUE(pmOptionGet(&m, "autoarrange", &value, &isFlag)); + TEST_TRUE(isFlag); + TEST_EQ(value, 1); + TEST_TRUE(pmOptionSet(&m, "minimizeOnRun", 1)); + TEST_TRUE(m.minOnRun); + TEST_TRUE(pmOptionSet(&m, "view", PmViewSmallE)); + TEST_TRUE(pmOptionGet(&m, "view", &value, &isFlag)); + TEST_TRUE(!isFlag); + TEST_EQ(value, PmViewSmallE); + TEST_TRUE(!pmOptionSet(&m, "view", -1)); + TEST_TRUE(!pmOptionSet(&m, "group", 1)); + TEST_TRUE(pmOptionSet(&m, "group", PM_INSTALLED_IDX)); + TEST_TRUE(!pmOptionGet(&m, "nope", &value, &isFlag)); + + TEST_EQ(pmInstalledScan(&m, APP_PROGMAN, NULL, NULL), APP_COUNT_TREE - 1); + TEST_EQ(arrlen(m.groups[PM_INSTALLED_IDX].items), APP_COUNT_TREE - 1); + TEST_TRUE(!pmInstalledChanged(&m, APP_PROGMAN)); + TEST_TRUE(pmInstalledChanged(&m, NULL)); + pmModelFree(&m); + fixShellDown(); +} + + // No PROGMAN.INI yet: Main and Startup, Installed Programs selected. static void caseModelFirstRun(void) { PmModelT m; @@ -584,21 +1044,21 @@ static void caseProgScan(void) { fixShellUp(SCREEN_BPP); int32_t count = shellProgramScan(NULL, APP_PROGMAN, true, NULL, NULL, &list); - bool clock = false; + bool demo = false; TEST_EQ(count, APP_COUNT_TREE - 1); for (int32_t i = 0; i < count; i++) { TEST_TRUE(!shellPathsMatch(list[i].path, APP_PROGMAN)); - if (shellPathsMatch(list[i].path, APP_CLOCK)) { - clock = true; - TEST_STR_EQ(list[i].name, "Clock"); + if (shellPathsMatch(list[i].path, APP_DVXDEMO)) { + demo = true; + TEST_STR_EQ(list[i].name, "DVX Demo"); TEST_TRUE(list[i].description[0] != '\0'); } } - TEST_TRUE(clock); + TEST_TRUE(demo); shellProgramListFree(list); count = shellProgramScan(SHELL_PROG_DIR, NULL, false, NULL, NULL, &list); @@ -624,12 +1084,12 @@ static void caseProgScan(void) { // Icons: native size, scaled down, from a picture, and none. int32_t bpp = tCtx()->display.format.bytesPerPixel; - uint8_t *large = shellProgramIcon(tCtx(), APP_CLOCK, SHELL_PROG_ICON, &pitch); + uint8_t *large = shellProgramIcon(tCtx(), APP_DVXDEMO, SHELL_PROG_ICON, &pitch); TEST_TRUE(large != NULL); dvxFreeImage(large); - uint8_t *small = shellProgramIcon(tCtx(), APP_CLOCK, SHELL_PROG_ICON_SMALL, &pitch); + uint8_t *small = shellProgramIcon(tCtx(), APP_DVXDEMO, SHELL_PROG_ICON_SMALL, &pitch); TEST_TRUE(small != NULL); TEST_EQ(pitch, SHELL_PROG_ICON_SMALL * bpp); @@ -685,6 +1145,47 @@ static void caseRunHelpers(void) { } +// Run Minimized: the window a main-loop program opens from its task is +// minimized, and so is one restored during the next idle frames; after +// SHELL_MINIMIZE_FRAMES the program is left alone, and an ended program +// is forgotten. +static void caseRunMinimized(void) { + tCase("Run Minimized"); + fixShellUp(SCREEN_BPP); + shellMinimizeApp(tCtx(), NO_APP_ID); + shellMinimizeApp(NULL, NO_APP_ID); + + int32_t id = shellLoadApp(tCtx(), APP_CLOCK); + + TEST_TRUE(id > 0); + shellMinimizeApp(tCtx(), id); + fixPumpShell(TASK_FRAMES); + + WindowT *win = fixWindowForApp(id); + + TEST_TRUE(win != NULL && win->minimized); + + if (win) { + dvxRestoreWindow(tCtx(), win); + TEST_TRUE(!win->minimized); + fixPumpShell(SETTLE_FRAMES); + TEST_TRUE(win->minimized); + + fixPumpShell(SHELL_MINIMIZE_FRAMES); + dvxRestoreWindow(tCtx(), win); + fixPumpShell(SETTLE_FRAMES); + TEST_TRUE(!win->minimized); + } + + // Asked again, then ended before its frames are over. + shellMinimizeApp(tCtx(), id); + TEST_TRUE(win != NULL && win->minimized); + fixKillAllApps(); + fixPumpShell(SETTLE_FRAMES); + fixShellDown(); +} + + // shellRunProgram: programs start with resolved arguments; documents open // in their associated program, from the working folder too; problems are // reported, and a command line runs the same way. @@ -740,6 +1241,12 @@ static void caseRunProgram(void) { } +static void countTaskManager(AppContextT *ctx) { + (void)ctx; + sTaskManagers++; +} + + static int32_t fakeLaunch(AppContextT *ctx, const char *path, const char *args) { (void)ctx; sLaunches++; @@ -774,6 +1281,7 @@ int main(void) { } caseHotkeys(); + caseIconViewRules(); caseModelEdits(); caseModelFirstRun(); caseModelRoundTrip(); @@ -782,6 +1290,11 @@ int main(void) { caseRunHelpers(); caseProgScan(); caseRunProgram(); + caseRunMinimized(); + caseItemIcon(); + caseModelFields(); + caseGroupBindings(); + caseDesktopServices(); return tSuiteEnd(); } diff --git a/src/test/shell/shellFixture.c b/src/test/shell/shellFixture.c index 4ca1abdf..1270c331 100644 --- a/src/test/shell/shellFixture.c +++ b/src/test/shell/shellFixture.c @@ -25,16 +25,22 @@ #include "shellFixture.h" #include "taskSwch.h" #include "dvxWgtP.h" +#include "dvxRes.h" #include "stb_ds_wrap.h" #include +#include #include #include #define COPY_CHUNK 4096 +#define BUILT_SUFFIX ".built" // bascomp output beside the app +#define CMD_MAX (DVX_MAX_PATH * 4) +#define CLOSE_FRAMES_MAX 400 // fixCloseApp: frames to wait for the end #define REPO_ROOT_DEPTH 3 // DVXTEST_APPS_DIR is /obj/host/apps -static void dismissDialog(AppContextT *ctx, WindowT *modal, void *userCtx); +static void dismissDialog(AppContextT *ctx, WindowT *modal, void *userCtx); +static int32_t menuFind(const MenuT *menu, const char *caption); char fixRepoRoot[DVX_MAX_PATH]; char fixStartDir[DVX_MAX_PATH]; @@ -117,6 +123,19 @@ int32_t fixClassSlotsInUse(void) { } +bool fixCloseApp(WindowT *win) { + int32_t appId = win->appId; + + dvxRequestWindowClose(tCtx(), win); + + for (int32_t i = 0; i < CLOSE_FRAMES_MAX && shellGetApp(appId); i++) { + fixPumpShell(1); + } + + return shellGetApp(appId) == NULL; +} + + bool fixCopyFileTo(const char *src, const char *dst) { FILE *in = fopen(src, "rb"); FILE *out = NULL; @@ -182,6 +201,43 @@ void fixDialogsType(const char *name, const char *answer) { } +bool fixInstallBasicApp(const char *bascomp, const char *stubSo, const char *dbp, const char *appPath) { + char cmd[CMD_MAX]; + char built[DVX_MAX_PATH]; + char dir[DVX_MAX_PATH]; + + snprintf(built, sizeof(built), "%s" BUILT_SUFFIX, appPath); + snprintf(dir, sizeof(dir), "%s", appPath); + + char *slash = strrchr(dir, '/'); + + if (slash) { + *slash = '\0'; + TEST_EQ(platformMkdirRecursive(dir), 0); + } + + snprintf(cmd, sizeof(cmd), "%s %s/%s -o %s > /dev/null", bascomp, fixRepoRoot, dbp, built); + TEST_EQ(system(cmd), 0); + TEST_TRUE(fixCopyFileTo(stubSo, appPath)); + + DvxResHandleT *res = dvxResOpen(built); + bool ok = res != NULL; + + for (uint32_t i = 0; ok && i < res->entryCount; i++) { + uint32_t size = 0; + void *data = dvxResRead(res, res->entries[i].name, &size); + + ok = data && dvxResAppend(appPath, res->entries[i].name, res->entries[i].type, data, size) == 0; + free(data); + } + + dvxResClose(res); + remove(built); + TEST_TRUE(ok); + return ok; +} + + void fixInstallTree(void) { static const char *bmps[] = { "logo.bmp", "new.bmp", "open.bmp", "sample.bmp", "save.bmp" }; char path[DVX_MAX_PATH]; @@ -229,6 +285,17 @@ void fixLinkApp(const char *name) { } +int32_t fixMenuId(const WindowT *win, const char *caption) { + int32_t id = -1; + + for (int32_t m = 0; id < 0 && win->menuBar && m < win->menuBar->menuCount; m++) { + id = menuFind(win->menuBar->menus[m], caption); + } + + return id; +} + + bool fixProcessInit(const char *logName) { static char logPath[DVX_MAX_PATH]; @@ -301,3 +368,26 @@ WindowT *fixWindowForApp(int32_t appId) { return NULL; } + + +// fixMenuId in one menu and its submenus. +static int32_t menuFind(const MenuT *menu, const char *caption) { + size_t len = strlen(caption); + + for (int32_t i = 0; i < menu->itemCount; i++) { + const MenuItemT *item = &menu->items[i]; + const char *label = item->label; + + if (item->subMenu) { + int32_t id = menuFind(item->subMenu, caption); + + if (id >= 0) { + return id; + } + } else if (strncmp(label, caption, len) == 0 && (label[len] == '\0' || label[len] == '\t')) { + return item->id; + } + } + + return -1; +} diff --git a/src/test/shell/shellFixture.h b/src/test/shell/shellFixture.h index d1c215da..53331a44 100644 --- a/src/test/shell/shellFixture.h +++ b/src/test/shell/shellFixture.h @@ -49,7 +49,7 @@ #define APP_DVXDEMO "apps/kpunch/dvxdemo/dvxdemo.app" #define APP_DVXHELP "apps/kpunch/dvxhelp/dvxhelp.app" -#define HELP_CLASS_COUNT 6 // private widget classes dvxhelp registers +#define FIX_CLASS_COUNT 2 // private widget classes fixClasses registers #define HELP_FILE_SRC "/bin/apps/kpunch/dvxhelp/dvxhelp.hlp" #define DEMO_BMP_SRC "/src/apps/kpunch/dvxdemo" @@ -68,6 +68,10 @@ int32_t fixAppWindowCount(int32_t appId); // Widget class table slots currently holding a class. int32_t fixClassSlotsInUse(void); +// Close win as a user would and run the shell until its program has +// ended (a BASIC program frees its runtime only then). True when it did. +bool fixCloseApp(WindowT *win); + // Byte copy; false on any open/read/write failure. bool fixCopyFileTo(const char *src, const char *dst); @@ -85,6 +89,12 @@ void fixDialogsOn(const char *answer); // search input (NULL for none) and answer with `answer`. void fixDialogsType(const char *name, const char *answer); +// A BASIC program for the host: compile the project (dbp, relative to +// fixRepoRoot) with bascomp, then write appPath as the BASIC stub .so +// carrying every resource bascomp put in the program. The suite links +// the BASIC runtime (and any library the program declares) itself. +bool fixInstallBasicApp(const char *bascomp, const char *stubSo, const char *dbp, const char *appPath); + // Lay out the app tree (.so files as .app, help file, demo bitmaps). void fixInstallTree(void); @@ -95,6 +105,10 @@ void fixKillAllApps(void); // apps/kpunch//.app -> obj/host/apps/.so void fixLinkApp(const char *name); +// The command id of the menu item whose caption (the label before any +// tab and shortcut text) is caption, in any menu of win; -1 if none. +int32_t fixMenuId(const WindowT *win, const char *caption); + // Suite start: remember the start directory, point the log at // /, derive fixRepoRoot and load the widget modules. bool fixProcessInit(const char *logName); diff --git a/src/test/shell/shellMainTest.c b/src/test/shell/shellMainTest.c index fc9ef2f8..b6c0ca01 100644 --- a/src/test/shell/shellMainTest.c +++ b/src/test/shell/shellMainTest.c @@ -312,18 +312,29 @@ int main(void) { // Exit DVX through the real main loop: the running app is asked to // close (it closes at once) and the shell then quits by itself. + long mark; + runScenario("Exit DVX closes the other apps, then quits", INI_VIDEO_OK INI_DESKTOP, "load=" FIXTURE("fixSave.so") " load=" FIXTURE("fixLoop.so") " exit", 0); + // The desktop ending any way but Exit DVX ends DVX too: the other + // apps are asked to close, then the shell quits by itself. + mark = logSize(); + runScenario("the desktop ending exits DVX", + INI_VIDEO_OK INI_DESKTOP, + "load=" FIXTURE("fixLoop.so") " close", + 0); + TEST_TRUE(logHasSince(mark, "the desktop ended; exiting DVX")); + runScenario("crash recovery inside an app task", INI_VIDEO_OK INI_DESKTOP, "load=" FIXTURE("fixCrash.so") " waitquit", 0); // exit() / abort() in a program end that program only, quietly. - long mark = logSize(); + mark = logSize(); runScenario("exit() in an app task ends only that app", INI_VIDEO_OK INI_DESKTOP, diff --git a/src/test/shell/shellTest.c b/src/test/shell/shellTest.c index 15557230..07d5ac6d 100644 --- a/src/test/shell/shellTest.c +++ b/src/test/shell/shellTest.c @@ -593,7 +593,7 @@ static void caseForceKillReleasesClasses(void) { #if defined(__SANITIZE_ADDRESS__) __lsan_disable(); #endif - int32_t id = shellLoadApp(tCtx(), APP_DVXHELP); + int32_t id = shellLoadApp(tCtx(), DVXTEST_FIXTURES_DIR "/fixClasses.so"); fixPumpShell(SETTLE_FRAMES); #if defined(__SANITIZE_ADDRESS__) @@ -601,7 +601,7 @@ static void caseForceKillReleasesClasses(void) { #endif TEST_TRUE(id > 0); - TEST_EQ(fixClassSlotsInUse(), before + HELP_CLASS_COUNT); + TEST_EQ(fixClassSlotsInUse(), before + FIX_CLASS_COUNT); shellForceKillApp(tCtx(), shellGetApp(id)); fixPumpShell(SETTLE_FRAMES); diff --git a/src/test/suites.mk b/src/test/suites.mk index 862fc829..1742ba84 100644 --- a/src/test/suites.mk +++ b/src/test/suites.mk @@ -25,4 +25,4 @@ # and src/test/coverage/runCoverage.sh. Every suite builds and runs with # a plain `make` in its directory and honours SAN and COV (common.mk). -HOST_SUITES = dvxtest sql unit wm widgets dialogs formrt ide shell serial host script abi fileops terminal sdk net print setup game joystick audio music +HOST_SUITES = dvxtest sql unit wm widgets dialogs formrt ide basdev shell serial host script abi fileops hlpfile terminal sdk net print setup game joystick audio music diff --git a/src/test/terminal/Makefile b/src/test/terminal/Makefile index ee54abb1..a6a0664d 100644 --- a/src/test/terminal/Makefile +++ b/src/test/terminal/Makefile @@ -20,8 +20,8 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -# Terminal suite -- the file transfer library, the connection library, -# the Terminal's phone book and the Terminal program, all on the host. +# Terminal suite -- the file transfer library, the connection library +# and the Terminal program (a DVX BASIC program), all on the host. # # testXfer XMODEM/YMODEM/ZMODEM engines against each other through # fault-injecting pipes, and against scripted peers @@ -29,14 +29,16 @@ # by installLrzsz.sh); reports "skipped" when absent # testConn serial, modem and secure-link connections over the # serial suite's loopback shim, with transfers on top, and -# the transfer library's BASIC functions +# the BASIC functions of the transfer and connection +# libraries # testConnProxy the secure link through the real host proxy # (proxy.c, main renamed) to a fake telnet BBS, over the # proxy's serial socket shim and directly over TCP # testTelnet the Telnet filter byte for byte, and Telnet sessions # against a host played by the test on a loopback socket -# testSession TERMINAL.INI round trip -# testTermApp the Terminal program in the host shell, end to end +# testTermApp the Terminal program (compiled from its project, run by +# the BASIC stub) in the host shell, end to end, and its +# phone book module on its own (bookTest/bookTest.dbp) # # `make` builds and runs everything (SAN/COV per common.mk). @@ -53,20 +55,24 @@ SHELLDIR = $(LIBS)/dvxshell PROXYDIR = $(ROOT)/src/tools/proxy SERTEST = $(ROOT)/src/test/serial SHELLTEST = $(ROOT)/src/test/shell -APPDIR = $(ROOT)/src/apps/kpunch/terminal +FILEOPS = $(LIBS)/fileops LRZSZ_BIN = $(ROOT)/toolchains/lrzsz/bin OBJDIR = $(DVXTEST_BINDIR)/terminal APPS_SO = $(DVXTEST_OBJDIR)/apps # Plain library tests need only the library headers; the app test uses # the dvxtest and shell includes as well. -CFLAGS = $(HOST_CFLAGS) -I$(LIBS) -I$(XFERDIR) -I$(CONNDIR) -I$(LIBS)/libdvx -I$(LIBS)/libdvx/platform -I$(LIBS)/libdvx/thirdparty -APP_CFLAGS = $(DVXTEST_CFLAGS) -I$(LIBS) -I$(SHELLDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO)"' -DDVXTEST_FIXTURES_DIR='"$(OBJDIR)"' +CFLAGS = $(HOST_CFLAGS) -I$(LIBS) -I$(XFERDIR) -I$(CONNDIR) -I$(LIBS)/libdvx -I$(LIBS)/libdvx/platform -I$(LIBS)/libdvx/thirdparty -I$(ROOT)/src/widgets/kpunch +APP_CFLAGS = $(DVXTEST_CFLAGS) -I$(LIBS) -I$(SHELLDIR) -I$(DVXTEST_BASICDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO)"' -DDVXTEST_FIXTURES_DIR='"$(OBJDIR)"' -DSHELLTEST_BASSTUB_SO='"$(DVXTEST_BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(DVXTEST_BASCOMP)"' SHIM = -include $(SERTEST)/loopShim.h XFER_OBJS = $(OBJDIR)/xfer.o $(OBJDIR)/xferZmodem.o -# The connection library (Telnet and TCP use the host's own sockets) +# The connection library (Telnet and TCP use the host's own sockets), and +# the BASIC faces of both libraries (these need libdvx) CONN_OBJS = $(OBJDIR)/conn.o $(OBJDIR)/connTelnet.o +BAS_OBJS = $(OBJDIR)/connBas.o $(OBJDIR)/xferBas.o +# The file operations library (the Terminal makes its download folder) +FILEOPS_OBJS = $(patsubst $(FILEOPS)/%.c,$(OBJDIR)/fileops/%.o,$(wildcard $(FILEOPS)/*.c)) # The serial stack over the loopback shim (clock() redirected), and over # the proxy's socket shim. LOOP_OBJS = $(OBJDIR)/loopShim.o $(OBJDIR)/loopPacket.o $(OBJDIR)/loopSecLink.o $(OBJDIR)/secHash.o $(OBJDIR)/security.o @@ -74,7 +80,7 @@ SOCK_OBJS = $(OBJDIR)/sockShim.o $(OBJDIR)/sockPacket.o $(OBJDIR)/sockSecLink.o # Every shell source (the app test runs the real shell). SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(OBJDIR)/shell/%.o,$(wildcard $(SHELLDIR)/*.c)) $(OBJDIR)/shell/shellFixture.o -BINS = $(OBJDIR)/testXfer $(OBJDIR)/testLrzsz $(OBJDIR)/testConn $(OBJDIR)/testConnProxy $(OBJDIR)/testTelnet $(OBJDIR)/testSession $(OBJDIR)/testTermApp +BINS = $(OBJDIR)/testXfer $(OBJDIR)/testLrzsz $(OBJDIR)/testConn $(OBJDIR)/testConnProxy $(OBJDIR)/testTelnet $(OBJDIR)/testTermApp .PHONY: all build run clean @@ -83,14 +89,14 @@ all: run build: $(BINS) # Each binary runs in the object directory (it makes its temp folders -# there). The app test needs the widget and app shared objects. +# there). The app test needs the widget shared objects, the BASIC stub +# and the BASIC compiler. run: build cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testXfer cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testLrzsz cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testConn cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testConnProxy cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testTelnet - cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1 ./testSession cd $(OBJDIR) && ASAN_OPTIONS=detect_leaks=1:detect_odr_violation=0 ./testTermApp $(OBJDIR)/testXfer: testXfer.c $(XFER_OBJS) $(DVX_MAKEFILES) @@ -101,9 +107,9 @@ $(OBJDIR)/testLrzsz: testLrzsz.c $(XFER_OBJS) $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -DLRZSZ_BIN='"$(LRZSZ_BIN)"' -o $@ testLrzsz.c $(XFER_OBJS) $(HOST_LDFLAGS) -$(OBJDIR)/testConn: testConn.c $(CONN_OBJS) $(OBJDIR)/xferBas.o $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILES) | dvxtest-libs +$(OBJDIR)/testConn: testConn.c $(CONN_OBJS) $(BAS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILES) | dvxtest-libs @mkdir -p $(OBJDIR) - $(HOSTCC) $(CFLAGS) -I$(SERIAL) -o $@ testConn.c $(CONN_OBJS) $(OBJDIR)/xferBas.o $(LOOP_OBJS) $(XFER_OBJS) $(HOST_LDFLAGS) $(DVXTEST_LIBDVX_A) -ldl -lm + $(HOSTCC) $(CFLAGS) -I$(SERIAL) -o $@ testConn.c $(CONN_OBJS) $(BAS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(HOST_LDFLAGS) -rdynamic $(DVXTEST_LIBDVX_A) -ldl -lm $(OBJDIR)/testConnProxy: testConnProxy.c $(CONN_OBJS) $(SOCK_OBJS) $(XFER_OBJS) $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) @@ -113,14 +119,11 @@ $(OBJDIR)/testTelnet: testTelnet.c $(CONN_OBJS) $(LOOP_OBJS) $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -o $@ testTelnet.c $(CONN_OBJS) $(LOOP_OBJS) $(HOST_LDFLAGS) -$(OBJDIR)/testSession: testSession.c $(OBJDIR)/termSession.o $(CONN_OBJS) $(LOOP_OBJS) $(DVXTEST_LIBDVX_A) $(DVX_MAKEFILES) | dvxtest-libs +# The BASIC stub resolves the runtime and the libraries from the +# executable (-rdynamic). +$(OBJDIR)/testTermApp: testTermApp.c $(SHELL_OBJS) $(CONN_OBJS) $(BAS_OBJS) $(FILEOPS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) $(DVX_MAKEFILES) | dvxtest-libs dvxtest-apps dvxtest-bascomp @mkdir -p $(OBJDIR) - $(HOSTCC) $(CFLAGS) -o $@ testSession.c $(OBJDIR)/termSession.o $(CONN_OBJS) $(LOOP_OBJS) $(HOST_LDFLAGS) $(DVXTEST_LIBDVX_A) -ldl -lm - -# The app's .so resolves the libraries from the executable (-rdynamic). -$(OBJDIR)/testTermApp: testTermApp.c $(SHELL_OBJS) $(CONN_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVX_MAKEFILES) | dvxtest-libs dvxtest-apps - @mkdir -p $(OBJDIR) - $(HOSTCC) $(APP_CFLAGS) -I$(SHELLTEST) -o $@ testTermApp.c $(SHELL_OBJS) $(CONN_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm + $(HOSTCC) $(APP_CFLAGS) -I$(SHELLTEST) -o $@ testTermApp.c $(SHELL_OBJS) $(CONN_OBJS) $(BAS_OBJS) $(FILEOPS_OBJS) $(LOOP_OBJS) $(XFER_OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_BASRT_A) $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm $(OBJDIR)/xfer.o: $(XFERDIR)/xfer.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) @@ -138,12 +141,16 @@ $(OBJDIR)/conn.o: $(CONNDIR)/conn.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< +$(OBJDIR)/connBas.o: $(CONNDIR)/connBas.c $(DVX_MAKEFILES) + @mkdir -p $(OBJDIR) + $(HOSTCC) $(CFLAGS) -c -o $@ $< + $(OBJDIR)/connTelnet.o: $(CONNDIR)/connTelnet.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR) $(HOSTCC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/termSession.o: $(APPDIR)/termSession.c $(DVX_MAKEFILES) - @mkdir -p $(OBJDIR) +$(OBJDIR)/fileops/%.o: $(FILEOPS)/%.c $(DVX_MAKEFILES) + @mkdir -p $(OBJDIR)/fileops $(HOSTCC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/loopShim.o: $(SERTEST)/loopShim.c $(DVX_MAKEFILES) @@ -190,7 +197,7 @@ $(OBJDIR)/shell/shellFixture.o: $(SHELLTEST)/shellFixture.c $(DVX_MAKEFILES) @mkdir -p $(OBJDIR)/shell $(HOSTCC) $(APP_CFLAGS) -I$(SHELLTEST) -c -o $@ $< --include $(wildcard $(OBJDIR)/*.d $(OBJDIR)/shell/*.d) +-include $(wildcard $(OBJDIR)/*.d $(OBJDIR)/shell/*.d $(OBJDIR)/fileops/*.d) clean: rm -rf $(OBJDIR) diff --git a/src/test/terminal/bookTest/bookTest.bas b/src/test/terminal/bookTest/bookTest.bas new file mode 100644 index 00000000..15ef040f --- /dev/null +++ b/src/test/terminal/bookTest/bookTest.bas @@ -0,0 +1,335 @@ +' 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. + +' bookTest.bas -- checks of the Terminal's phone book module +' (src/apps/kpunch/terminal/terminal.bas) on its own: the defaults, a +' round trip of every field through TERMINAL.INI, hand-edited files with +' bad values, passphrases that must not be stored, the add / remove +' limits, the safe save, and "telnet://host[:port]". Run by testTermApp +' (the BASIC stub with live library calls); each check writes "PASS name" +' or "FAIL name" to RESULTS_FILE, which the test reads. + +OPTION EXPLICIT + +CONST RESULTS_FILE = "BOOKTEST.TXT" +CONST INI_FILE = "BOOKTEST.INI" +CONST MISSING_FILE = "NONE.INI" +CONST BAD_DIR_FILE = "nodir/BOOKTEST.INI" +CONST SECRET = "swordfish" +CONST OUT_FILE = 1 +CONST IN_FILE = 2 + +DIM SHARED failures AS LONG + + +' One check's outcome on the results file. +SUB Check(BYVAL checkName AS STRING, BYVAL ok AS BOOLEAN) + IF ok THEN + PRINT #OUT_FILE, "PASS " & checkName + ELSE + PRINT #OUT_FILE, "FAIL " & checkName + failures = failures + 1 + END IF +END SUB + + +' True when the text file path contains text. +FUNCTION FileHas(BYVAL path AS STRING, BYVAL text AS STRING) AS BOOLEAN + DIM lineText AS STRING + + FileHas = False + + IF Dir$(path) = "" THEN + EXIT FUNCTION + END IF + + OPEN path FOR INPUT AS #IN_FILE + + DO WHILE NOT EOF(IN_FILE) + LINE INPUT #IN_FILE, lineText + + IF InStr(lineText, text) > 0 THEN + FileHas = True + END IF + LOOP + + CLOSE #IN_FILE +END FUNCTION + + +' Every field of two sessions is the same. +FUNCTION SameSession(a AS TermSession, b AS TermSession) AS BOOLEAN + SameSession = a.sessionName = b.sessionName AND a.connType = b.connType AND a.comPort = b.comPort AND a.bps = b.bps AND a.dataBits = b.dataBits AND a.parity = b.parity AND a.stopBits = b.stopBits AND a.flow = b.flow AND a.redialCount = b.redialCount AND a.redialDelay = b.redialDelay AND a.tcpPort = b.tcpPort AND a.initString = b.initString AND a.dialPrefix = b.dialPrefix AND a.phone = b.phone AND a.passphrase = b.passphrase AND a.host = b.host AND a.cols = b.cols AND a.rows = b.rows AND a.scrollback = b.scrollback AND a.scheme = b.scheme AND a.askPassphrase = b.askPassphrase AND a.secureNet = b.secureNet AND a.localEcho = b.localEcho AND a.answerback = b.answerback +END FUNCTION + + +' A hand-edited file with nonsense values loads as valid sessions; names +' match in any case. +SUB CaseBadValues + OPEN INI_FILE FOR OUTPUT AS #IN_FILE + PRINT #IN_FILE, "[phonebook]" + PRINT #IN_FILE, "entries = 2" + PRINT #IN_FILE, "current = 7" + PRINT #IN_FILE, "[transfer]" + PRINT #IN_FILE, "protocol = kermit" + PRINT #IN_FILE, "exists = maybe" + PRINT #IN_FILE, "[entry1]" + PRINT #IN_FILE, "name =" + PRINT #IN_FILE, "type = pigeon" + PRINT #IN_FILE, "port = COM9" + PRINT #IN_FILE, "speed = 12345" + PRINT #IN_FILE, "databits = 9" + PRINT #IN_FILE, "parity = Q" + PRINT #IN_FILE, "stopbits = 3" + PRINT #IN_FILE, "flow = magic" + PRINT #IN_FILE, "redial = 500" + PRINT #IN_FILE, "redialdelay = 0" + PRINT #IN_FILE, "tcpport = 99999" + PRINT #IN_FILE, "cols = 5" + PRINT #IN_FILE, "rows = 900" + PRINT #IN_FILE, "scrollback = -4" + PRINT #IN_FILE, "scheme = neon" + PRINT #IN_FILE, "localecho = perhaps" + PRINT #IN_FILE, "[entry2]" + PRINT #IN_FILE, "name = Second" + PRINT #IN_FILE, "type = modem" + PRINT #IN_FILE, "port = com2" + PRINT #IN_FILE, "speed = 2400" + PRINT #IN_FILE, "parity = e" + CLOSE #IN_FILE + + iniPath = INI_FILE + BookLoad + Check "bad count", bookCount = 2 + Check "bad current", bookCurrent = 1 + Check "bad protocol", xferProto = XFER_ZMODEM + Check "bad exists", xferExists = XFER_EXIST_ASK + Check "bad name", book(0).sessionName = TERM_DEFAULT_NAME + Check "bad type", book(0).connType = CONN_SERIAL + Check "bad port", book(0).comPort = 1 + Check "bad speed", book(0).bps = 9600 + Check "bad data", book(0).dataBits = 8 + Check "bad parity", book(0).parity = "N" + Check "bad stop", book(0).stopBits = 1 + Check "bad flow", book(0).flow = CONN_FLOW_NONE + Check "bad redial", book(0).redialCount = TERM_REDIAL_MAX + Check "bad delay", book(0).redialDelay = TERM_REDIAL_DELAY_MIN + Check "bad tcp", book(0).tcpPort = TERM_TCP_PORT_MAX + Check "bad cols", book(0).cols = TERM_COLS_MIN + Check "bad rows", book(0).rows = TERM_ROWS_MAX + Check "bad scrollback", book(0).scrollback = TERM_SCROLLBACK_MIN + Check "bad scheme", book(0).scheme = SCHEME_STANDARD + Check "bad echo", NOT book(0).localEcho + Check "second name", book(1).sessionName = "Second" + Check "second type", book(1).connType = CONN_MODEM + Check "second port", book(1).comPort = 2 + Check "second speed", book(1).bps = 2400 + Check "second parity", book(1).parity = "E" +END SUB + + +SUB CaseDefaults + iniPath = MISSING_FILE + BookLoad + Check "defaults count", bookCount = 1 + Check "defaults current", bookCurrent = 0 + Check "defaults protocol", xferProto = XFER_ZMODEM + Check "defaults exists", xferExists = XFER_EXIST_ASK + Check "defaults auto zmodem", autoZmodem + Check "defaults dir", downloadDir = TERM_DEFAULT_DIR + Check "defaults name", book(0).sessionName = TERM_DEFAULT_NAME + Check "defaults type", book(0).connType = CONN_SERIAL + Check "defaults speed", book(0).bps = 9600 + Check "defaults cols", book(0).cols = TERM_COLS_DEFAULT + Check "defaults rows", book(0).rows = TERM_ROWS_DEFAULT + Check "defaults init", book(0).initString = "ATZ" + Check "defaults prefix", book(0).dialPrefix = "ATDT" + + ' No settings folder: the defaults, and nothing is read + iniPath = "" + BookLoad + Check "no folder defaults", bookCount = 1 AND book(0).sessionName = TERM_DEFAULT_NAME +END SUB + + +SUB CaseLimits + DIM s AS TermSession + DIM i AS LONG + DIM added AS BOOLEAN + + BookDefaults + SessionDefaults s + added = True + + FOR i = 1 TO TERM_BOOK_MAX - 1 + s.sessionName = "Entry " & CStr(i) + added = added AND BookAdd(s) = i + NEXT i + + Check "add", added + Check "add full", BookAdd(s) = -1 + bookCurrent = TERM_BOOK_MAX - 1 + BookRemove TERM_BOOK_MAX - 1 + Check "remove last count", bookCount = TERM_BOOK_MAX - 1 + Check "remove last current", bookCurrent = TERM_BOOK_MAX - 2 + BookRemove 0 + Check "remove first shifts", book(0).sessionName = "Entry 1" + BookRemove -1 + BookRemove bookCount + Check "remove out of range", bookCount = TERM_BOOK_MAX - 2 + + DO WHILE bookCount > 1 + BookRemove 0 + LOOP + + BookRemove 0 + Check "keeps one", bookCount = 1 + Check "choice index", ChoiceIndex(SPEED_NAMES, "115200") = 8 + Check "choice missing", ChoiceIndex(SPEED_NAMES, "7") = -1 + Check "choice any case", ChoiceIndex(FLOW_NAMES, "XonXoff") = CONN_FLOW_XONXOFF +END SUB + + +' A session that asks for its passphrase never has one written; one that +' stores it round-trips. +SUB CasePassphrase + BookDefaults + iniPath = INI_FILE + book(0).connType = CONN_SECURE + book(0).passphrase = SECRET + book(0).askPassphrase = True + Check "ask save", BookSave() + Check "ask not written", NOT FileHas(INI_FILE, SECRET) + + book(0).askPassphrase = False + Check "store save", BookSave() + Check "store written", FileHas(INI_FILE, SECRET) + BookLoad + Check "store loaded", book(0).passphrase = SECRET +END SUB + + +' Every field survives save and load; a save that cannot write keeps the +' old file. +SUB CaseRoundTrip + DIM s AS TermSession + DIM first AS TermSession + DIM second AS TermSession + + BookDefaults + SessionDefaults s + s.sessionName = "My BBS" + s.connType = CONN_MODEM + s.comPort = 3 + s.bps = 57600 + s.dataBits = 7 + s.parity = "O" + s.stopBits = 2 + s.flow = CONN_FLOW_RTSCTS + s.redialCount = 12 + s.redialDelay = 45 + s.tcpPort = 2323 + s.cols = 132 + s.rows = 43 + s.scrollback = 5000 + s.scheme = SCHEME_AMBER + s.localEcho = True + s.secureNet = True + s.initString = "AT&F E1 V1 X4" + s.dialPrefix = "ATDP" + s.phone = "1-555-0100" + s.host = "bbs.example.org" + s.answerback = "DVX Terminal" + Check "rt add", BookAdd(s) = 1 + bookCurrent = 1 + xferProto = XFER_YMODEM + xferExists = XFER_EXIST_RENAME + autoZmodem = False + escapeCtl = True + resumeUploads = True + downloadDir = "C:\FILES\IN" + first = book(0) + second = book(1) + + iniPath = INI_FILE + Check "rt save", BookSave() + BookDefaults + BookLoad + Check "rt count", bookCount = 2 + Check "rt current", bookCurrent = 1 + Check "rt protocol", xferProto = XFER_YMODEM + Check "rt exists", xferExists = XFER_EXIST_RENAME + Check "rt auto", NOT autoZmodem + Check "rt escape", escapeCtl + Check "rt resume", resumeUploads + Check "rt dir", downloadDir = "C:\FILES\IN" + Check "rt session", SameSession(book(1), second) + Check "rt first", SameSession(book(0), first) + + ' Saving into a folder that does not exist fails and writes nothing. + iniPath = BAD_DIR_FILE + Check "rt bad path", NOT BookSave() + Check "rt old kept", FileHas(INI_FILE, "My BBS") +END SUB + + +' "telnet://host[:port]" and finding its phone book entry. +SUB CaseTelnetUrl + DIM s AS TermSession + + Check "url host port", SessionFromUrl("telnet://bbs.example.org:2323", s) + Check "url type", s.connType = CONN_TELNET + Check "url port", s.tcpPort = 2323 + Check "url host", s.host = "bbs.example.org" + Check "url name", s.sessionName = "bbs.example.org" + Check "url default port", SessionFromUrl("TELNET://10.0.2.2/", s) AND s.tcpPort = 23 AND s.host = "10.0.2.2" + Check "url not telnet", NOT SessionFromUrl("ssh://host", s) + Check "url no host", NOT SessionFromUrl("telnet://:23", s) + Check "url bad port", NOT SessionFromUrl("telnet://host:0", s) + Check "url port text", NOT SessionFromUrl("telnet://host:2x", s) + Check "url empty port", NOT SessionFromUrl("telnet://host:", s) + Check "url hex port", NOT SessionFromUrl("telnet://host:&H17", s) + Check "url exponent port", NOT SessionFromUrl("telnet://host:1e3", s) + Check "url sign only", NOT SessionFromUrl("telnet://host:+", s) + Check "url signed port", SessionFromUrl("telnet://host:+23/", s) AND s.tcpPort = 23 + Check "url empty", NOT SessionFromUrl("", s) + + BookDefaults + SessionFromUrl "telnet://bbs.example.org:2323", s + Check "url not in book", BookFindTelnet(s) = -1 + Check "url added", BookAdd(s) = 1 + SessionFromUrl "telnet://BBS.example.org:2323", s + Check "url found", BookFindTelnet(s) = 1 + SessionFromUrl "telnet://bbs.example.org:23", s + Check "url other port", BookFindTelnet(s) = -1 +END SUB + + +OPEN RESULTS_FILE FOR OUTPUT AS #OUT_FILE +CaseDefaults +CaseRoundTrip +CaseBadValues +CasePassphrase +CaseLimits +CaseTelnetUrl +PRINT #OUT_FILE, "END " & CStr(failures) +CLOSE #OUT_FILE diff --git a/src/test/terminal/bookTest/bookTest.dbp b/src/test/terminal/bookTest/bookTest.dbp new file mode 100644 index 00000000..663ee076 --- /dev/null +++ b/src/test/terminal/bookTest/bookTest.dbp @@ -0,0 +1,18 @@ +[Project] +Name = Book Test +Author = Scott Duensing +Publisher = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Terminal phone book checks (testTermApp) + +[Modules] +File0 = ../../../include/basic/conn.bas +File1 = ../../../include/basic/prefs.bas +File2 = ../../../include/basic/xfer.bas +File3 = ../../../apps/kpunch/terminal/terminal.bas +File4 = bookTest.bas +Count = 5 + +[Settings] +StartupForm = diff --git a/src/test/terminal/testConn.c b/src/test/terminal/testConn.c index f4fafd2c..0457d38f 100644 --- a/src/test/terminal/testConn.c +++ b/src/test/terminal/testConn.c @@ -25,8 +25,10 @@ // clock, fault injection). The test plays the far end: a raw port, a // Hayes modem (commands in, result codes and DCD out) or a secLink peer. // Transfers (XMODEM, ZMODEM) also run across connections, including a -// secure link on both ends and a noisy serial line, and through the -// transfer library's BASIC functions (xferBas.c) on a raw port. +// secure link on both ends and a noisy serial line, and through the BASIC +// functions of both libraries: the transfer library's on a raw port +// (xferBas.c) and the connection library's (connBas.c), whose Terminal +// part the Terminal program's test drives. #include "../serial/serialTest.h" #include "../serial/loopShim.h" @@ -62,18 +64,72 @@ #define PASS_TEXT "open sesame" #define PASS_OTHER "wrong words" #define BAS_BAD_HANDLE 99 -#define BAS_SLOTS 4 // transfers xferBas.c runs at once +#define BAS_SLOTS 8 // transfers xferBas.c runs at once +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_COM1 1 // BASIC numbers COM ports from 1 +#define BAS_FILE_SEP "\n" // ConnSendFiles: one path per line +#define SETTLE_MS 500 // the BASIC side's last frames after the peer ended +#define RX_WINDOW_TEST 1024 // ConnSetXferWindow for the download +#define CRC_CHECK_TEXT "123456789" +#define CRC_CHECK_LEN 9 +#define CRC16_CHECK 0x31C3 // CRC-16/XMODEM of CRC_CHECK_TEXT +#define CRC32_CHECK 0xCBF43926u // CRC-32 of CRC_CHECK_TEXT -// The BASIC functions (xferBas.c; BASIC finds them by name) +// The BASIC functions (connBas.c, xferBas.c; BASIC finds them by name) +int32_t ConnAttach(int32_t handle, const char *termCtrlName); +int32_t ConnCanBreak(int32_t type); +int32_t ConnCapture(int32_t handle, const char *path); +void ConnClose(int32_t handle); +int32_t ConnCreate(void); +void ConnDetach(int32_t handle); +const char *ConnErrorText(int32_t err); +void ConnFree(int32_t handle); +int32_t ConnOpen(int32_t handle, int32_t type); +int32_t ConnPending(int32_t handle); +void ConnPoll(int32_t handle); +const uint8_t *ConnRead(int32_t handle, int32_t *outLen); +int32_t ConnReceiveFiles(int32_t handle, int32_t proto, const char *target, int32_t exists, int32_t escapeCtl); +int32_t ConnSendBreak(int32_t handle); +int32_t ConnSendFiles(int32_t handle, int32_t proto, const char *files, int32_t escapeCtl, int32_t resumeFiles); +void ConnSetAnswerback(int32_t handle, const char *text); +void ConnSetEcho(int32_t handle, int32_t on); +void ConnSetHost(int32_t handle, const char *host, int32_t port); +void ConnSetModem(int32_t handle, const char *initString, const char *dialPrefix, const char *phone, int32_t redialCount, int32_t redialDelay); +void ConnSetPassphrase(int32_t handle, const char *passphrase); +void ConnSetSerial(int32_t handle, int32_t com, int32_t bps, int32_t dataBits, const char *parity, int32_t stopBits, int32_t flow); +void ConnSetWindowSize(int32_t handle, int32_t cols, int32_t rows); +void ConnSetXferWindow(int32_t handle, int32_t bytes); +int32_t ConnState(int32_t handle); +const char *ConnStatus(int32_t handle); +int32_t ConnStatusSerial(int32_t handle); +int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len); +int32_t ConnZmodemRequest(int32_t handle); +void XferAnswer(int32_t handle, int32_t action); +int32_t XferAskRemoteSize(int32_t handle); +int32_t XferAskSize(int32_t handle); +int32_t XferBatchBytes(int32_t handle); +int32_t XferBatchSize(int32_t handle); int32_t XferBytes(int32_t handle); void XferCancel(int32_t handle); +int32_t XferCanResume(int32_t handle); void XferClose(int32_t handle); +int32_t XferCrc16(const uint8_t *data, int32_t len); +int32_t XferCrc32(const uint8_t *data, int32_t len); +int32_t XferErrors(int32_t handle); const char *XferFile(int32_t handle); +int32_t XferFileCount(int32_t handle); +int32_t XferFileIndex(int32_t handle); const char *XferMessage(int32_t handle); int32_t XferPoll(int32_t handle); +int32_t XferProtocol(int32_t handle); int32_t XferReceive(int32_t com, int32_t proto, const char *target); +int32_t XferResumedAt(int32_t handle); +const char *XferSafeName(const char *fileName); int32_t XferSendFile(int32_t com, int32_t proto, const char *path); +int32_t XferSending(int32_t handle); int32_t XferSize(int32_t handle); +int32_t XferStatus(int32_t handle); typedef struct { uint8_t data[BIG_DATA * 2]; @@ -87,6 +143,7 @@ typedef struct { // ============================================================ static void caseBasic(void); +static void caseConnBasic(void); static void caseModem(void); static void caseModemFailures(void); static void caseSecure(void); @@ -99,6 +156,7 @@ static int32_t connReadFn(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t connWriteFn(void *ctx, const uint8_t *data, int32_t len); static bool drainUntil(ConnT *c, const char *needle, uint32_t *now, uint32_t limitMs); static void peerRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted); +static void peerRun(XferT *peer, int32_t handle); static bool peerSaw(const char *needle); static void peerSay(const char *text); static void pollFor(ConnT *c, uint32_t *now, uint32_t ms); @@ -212,6 +270,214 @@ static void caseBasic(void) { } +// The connection library's BASIC functions without a Terminal (the +// Terminal program's test covers ConnAttach, the screen, capture, +// answerback, echo and ZMODEM start): set up and open a serial line, +// text both ways, break, status; transfers on the connection with the +// xfer.bas progress functions -- a ZMODEM batch upload, a download that +// asks about an existing file, a start that fails, one the line's drop +// cancels; and bad handles. +static void caseConnBasic(void) { + XferT *peer = xferCreate(); + XferIoT io; + uint8_t data[XFER_SIZE]; + const char *pathA = TMP_DIR "/CA.DAT"; + const char *pathB = TMP_DIR "/CB.DAT"; + const char *peerFiles[1] = { pathA }; + char files[BUF_SIZE]; + char safe[XFER_NAME_MAX]; + int peerCom = PEER_COM; + int32_t h; + int32_t x; + int32_t serial; + int32_t len = 0; + const uint8_t *bytes; + FILE *fp; + + mkdir(TMP_DIR, 0755); + mkdir(TMP_DIR "/cup", 0755); + mkdir(TMP_DIR "/cdown", 0755); + unlink(TMP_DIR "/cup/CA.DAT"); + unlink(TMP_DIR "/cup/CB.DAT"); + fillPattern(data, XFER_SIZE, SEED + 2); + + for (int32_t i = 0; i < 2; i++) { + fp = fopen(i == 0 ? pathA : pathB, "wb"); + fwrite(data, 1, XFER_SIZE, fp); + fclose(fp); + } + + loopReset(); + rs232Open(PEER_COM, 9600, 8, 'N', 1, RS232_HANDSHAKE_NONE); + io.ctx = &peerCom; + io.read = rawRead; + io.write = rawWrite; + io.pending = NULL; + sPeerLen = 0; + + h = ConnCreate(); + TEST_TRUE("cb create", h > 0); + TEST_EQ("cb closed", ConnState(h), ConnClosedE); + TEST_TRUE("cb status empty", ConnStatus(h)[0] == '\0'); + serial = ConnStatusSerial(h); + TEST_EQ("cb attach without forms", ConnAttach(h, "Term1"), BAS_FALSE); + ConnDetach(h); + TEST_EQ("cb send closed", ConnSendFiles(h, XferZmodemE, pathA, BAS_FALSE, BAS_FALSE), 0); + + ConnSetSerial(h, BAS_COM1, 9600, 8, "n", 1, RS232_HANDSHAKE_NONE); + ConnSetModem(h, "ATZ", "ATDT", "", 0, 1); + ConnSetHost(h, "", 23); + ConnSetPassphrase(h, ""); + ConnSetWindowSize(h, 80, 25); + ConnSetEcho(h, BAS_TRUE); + ConnSetAnswerback(h, "ID"); + TEST_EQ("cb open", ConnOpen(h, ConnSerialE), CONN_OK); + ConnPoll(h); + TEST_EQ("cb open state", ConnState(h), ConnOpenE); + TEST_TRUE("cb open status", strcmp(ConnStatus(h), "Connected to COM1 at 9600 bps") == 0); + TEST_TRUE("cb status serial", ConnStatusSerial(h) != serial); + + TEST_EQ("cb write", ConnWrite(h, (const uint8_t *)"hello", 5), 5); + ConnPoll(h); + TEST_TRUE("cb peer saw", peerSaw("hello")); + TEST_EQ("cb pending", ConnPending(h), 0); + peerSay("world"); + ConnPoll(h); + bytes = ConnRead(h, &len); + TEST_TRUE("cb read", len == 5 && memcmp(bytes, "world", 5) == 0); + ConnRead(h, &len); + TEST_EQ("cb read empty", len, 0); + TEST_EQ("cb write zero byte", ConnWrite(h, (const uint8_t *)"a\0b", 3), 3); + ConnPoll(h); + TEST_EQ("cb no zmodem", ConnZmodemRequest(h), BAS_FALSE); + TEST_EQ("cb capture", ConnCapture(h, TMP_DIR "/CAP.TXT"), BAS_TRUE); + TEST_EQ("cb capture stop", ConnCapture(h, ""), BAS_FALSE); + TEST_EQ("cb capture bad", ConnCapture(h, TMP_DIR "/nodir/CAP.TXT"), BAS_FALSE); + TEST_EQ("cb can break", ConnCanBreak(ConnSerialE), BAS_TRUE); + TEST_EQ("cb cannot break", ConnCanBreak(ConnTelnetE), BAS_FALSE); + TEST_EQ("cb break", ConnSendBreak(h), BAS_TRUE); + TEST_TRUE("cb break on", loopGetBreak(LOCAL_COM)); + + for (int32_t i = 0; i < BREAK_WAIT_MS / TICK_MS; i++) { + hostClockAdvance(TICK_MS); + ConnPoll(h); + } + + TEST_TRUE("cb break off", !loopGetBreak(LOCAL_COM)); + + // A ZMODEM batch upload: two files + snprintf(files, sizeof(files), "%s" BAS_FILE_SEP "%s" BAS_FILE_SEP, pathA, pathB); + x = ConnSendFiles(h, XferZmodemE, files, BAS_FALSE, BAS_FALSE); + TEST_TRUE("cb send handle", x > 0); + TEST_EQ("cb send running", XferStatus(x), XferRunningE); + TEST_EQ("cb busy", ConnSendFiles(h, XferZmodemE, pathA, BAS_FALSE, BAS_FALSE), 0); + TEST_EQ("cb typing dropped", ConnWrite(h, (const uint8_t *)"x", 1), 1); + xferStartReceive(peer, XferZmodemE, &io, NULL, TMP_DIR "/cup", 0); + peerRun(peer, h); + TEST_EQ("cb send done", XferStatus(x), XferDoneE); + TEST_EQ("cb send sending", XferSending(x), BAS_TRUE); + TEST_EQ("cb send proto", XferProtocol(x), XferZmodemE); + TEST_EQ("cb send count", XferFileCount(x), 2); + TEST_EQ("cb send batch", XferBatchBytes(x), XFER_SIZE * 2); + TEST_EQ("cb send batch size", XferBatchSize(x), XFER_SIZE * 2); + TEST_EQ("cb send resumed", XferResumedAt(x), 0); + TEST_TRUE("cb send errors", XferErrors(x) >= 0); + TEST_EQ("cb peer done", xferProgress(peer)->status, XferDoneE); + XferClose(x); + TEST_EQ("cb closed handle", XferStatus(x), XferIdleE); + + // A download of a file that exists, asking: Keep both + fp = fopen(TMP_DIR "/cdown/CA.DAT", "wb"); + fwrite(data, 1, XFER_SIZE / 2, fp); + fclose(fp); + unlink(TMP_DIR "/cdown/CA~1.DAT"); + ConnSetXferWindow(h, XFER_WINDOW_MAX + 1); + ConnSetXferWindow(h, RX_WINDOW_TEST); + x = ConnReceiveFiles(h, XferZmodemE, TMP_DIR "/cdown", XferExistAskE, BAS_FALSE); + TEST_TRUE("cb recv handle", x > 0); + xferStartSend(peer, XferZmodemE, &io, NULL, peerFiles, 1, 0); + + for (int32_t i = 0; i < LIMIT_MS / TICK_MS && XferStatus(x) == XferRunningE; i++) { + xferPoll(peer, (uint32_t)(i * TICK_MS)); + hostClockAdvance(TICK_MS); + ConnPoll(h); + } + + TEST_EQ("cb asking", XferStatus(x), XferAskingE); + TEST_EQ("cb ask size", XferAskSize(x), XFER_SIZE / 2); + TEST_EQ("cb ask remote size", XferAskRemoteSize(x), XFER_SIZE); + TEST_EQ("cb can resume", XferCanResume(x), BAS_TRUE); + TEST_TRUE("cb ask file", strstr(XferFile(x), "CA.DAT") != NULL); + XferAnswer(x, XferExistRenameE); + peerRun(peer, h); + TEST_EQ("cb recv done", XferStatus(x), XferDoneE); + TEST_EQ("cb recv receiving", XferSending(x), BAS_FALSE); + TEST_EQ("cb recv index past the batch", XferFileIndex(x), 1); + TEST_TRUE("cb recv renamed", strstr(XferFile(x), "CA~1.DAT") != NULL); + XferClose(x); + + // A start that fails: a handle that says why + x = ConnSendFiles(h, XferXmodemE, TMP_DIR "/NOPE.DAT", BAS_FALSE, BAS_FALSE); + TEST_TRUE("cb fail handle", x > 0); + TEST_EQ("cb fail status", XferStatus(x), XferFailedE); + TEST_TRUE("cb fail message", XferMessage(x)[0] != '\0'); + XferClose(x); + TEST_EQ("cb bad proto", ConnReceiveFiles(h, XferProtoCountE, TMP_DIR, XferExistAskE, BAS_FALSE), 0); + + // The line drops during a transfer: it is cancelled + x = ConnReceiveFiles(h, XferZmodemE, TMP_DIR "/cdown", XferExistOverwriteE, BAS_FALSE); + TEST_TRUE("cb drop handle", x > 0); + ConnClose(h); + ConnPoll(h); + TEST_EQ("cb drop cancelled", XferStatus(x), XferCancelledE); + TEST_EQ("cb drop closed", ConnState(h), ConnClosedE); + XferClose(x); + + // Free, and bad handles + ConnFree(h); + TEST_EQ("cb freed", ConnState(h), ConnClosedE); + TEST_TRUE("cb bad status", ConnStatus(BAS_BAD_HANDLE)[0] == '\0'); + TEST_EQ("cb bad serial", ConnStatusSerial(BAS_BAD_HANDLE), 0); + TEST_EQ("cb bad open", ConnOpen(BAS_BAD_HANDLE, ConnSerialE), CONN_ERR_PARAM); + TEST_EQ("cb bad write", ConnWrite(BAS_BAD_HANDLE, (const uint8_t *)"x", 1), 0); + ConnRead(BAS_BAD_HANDLE, &len); + TEST_EQ("cb bad read", len, 0); + ConnSetXferWindow(BAS_BAD_HANDLE, 0); + TEST_EQ("cb bad ask remote", XferAskRemoteSize(BAS_BAD_HANDLE), -1); + TEST_TRUE("cb error text", strcmp(ConnErrorText(CONN_ERR_HOST), connErrorText(CONN_ERR_HOST)) == 0); + TEST_TRUE("cb error text ok", strcmp(ConnErrorText(CONN_OK), connErrorText(CONN_OK)) == 0); + TEST_EQ("cb crc16", XferCrc16((const uint8_t *)CRC_CHECK_TEXT, CRC_CHECK_LEN), CRC16_CHECK); + TEST_EQ("cb crc32", (uint32_t)XferCrc32((const uint8_t *)CRC_CHECK_TEXT, CRC_CHECK_LEN), CRC32_CHECK); + TEST_EQ("cb crc32 zero byte", (uint32_t)XferCrc32((const uint8_t *)"a\0b", 3), xferCrc32((const uint8_t *)"a\0b", 3)); + xferSafeName("../dir/Long File Name.txt", safe); + TEST_TRUE("cb safe name", strcmp(XferSafeName("../dir/Long File Name.txt"), safe) == 0); + TEST_EQ("cb bad pending", ConnPending(BAS_BAD_HANDLE), 0); + TEST_EQ("cb bad break", ConnSendBreak(BAS_BAD_HANDLE), BAS_FALSE); + TEST_EQ("cb bad capture", ConnCapture(BAS_BAD_HANDLE, "x"), BAS_FALSE); + TEST_EQ("cb bad zmodem", ConnZmodemRequest(BAS_BAD_HANDLE), BAS_FALSE); + TEST_EQ("cb bad attach", ConnAttach(BAS_BAD_HANDLE, "Term1"), BAS_FALSE); + TEST_EQ("cb bad send", ConnSendFiles(BAS_BAD_HANDLE, XferZmodemE, pathA, BAS_FALSE, BAS_FALSE), 0); + ConnPoll(BAS_BAD_HANDLE); + ConnClose(BAS_BAD_HANDLE); + ConnDetach(BAS_BAD_HANDLE); + ConnSetSerial(BAS_BAD_HANDLE, BAS_COM1, 9600, 8, "N", 1, 0); + ConnSetModem(BAS_BAD_HANDLE, "", "", "", 0, 1); + ConnSetHost(BAS_BAD_HANDLE, "", 23); + ConnSetPassphrase(BAS_BAD_HANDLE, ""); + ConnSetWindowSize(BAS_BAD_HANDLE, 80, 25); + ConnSetEcho(BAS_BAD_HANDLE, BAS_TRUE); + ConnSetAnswerback(BAS_BAD_HANDLE, "x"); + ConnFree(BAS_BAD_HANDLE); + + // A bad COM port number + h = ConnCreate(); + ConnSetSerial(h, RS232_NUM_PORTS + 1, 9600, 8, "N", 1, 0); + TEST_EQ("cb bad com", ConnOpen(h, ConnSerialE), CONN_ERR_PARAM); + ConnFree(h); + xferDestroy(peer); +} + + static void caseModem(void) { ConnT *c = connCreate(); ConnConfigT cfg; @@ -752,6 +1018,27 @@ static void peerRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, b } +// Run the test's transfer and a BASIC transfer on connection handle (moved +// along by ConnPoll) until both have ended. +static void peerRun(XferT *peer, int32_t handle) { + for (int32_t i = 0; i < LIMIT_MS / TICK_MS; i++) { + XferStatusE st = xferPoll(peer, (uint32_t)(i * TICK_MS)); + + hostClockAdvance(TICK_MS); + ConnPoll(handle); + + if (st != XferRunningE && st != XferAskingE) { + break; + } + } + + for (int32_t i = 0; i < SETTLE_MS / TICK_MS; i++) { + hostClockAdvance(TICK_MS); + ConnPoll(handle); + } +} + + // Read what the local side sent to the peer port; true (and forget it) // once needle is in there. static bool peerSaw(const char *needle) { @@ -820,5 +1107,6 @@ int main(void) { caseSecureFailures(); caseTransfers(); caseBasic(); + caseConnBasic(); return testSummary(); } diff --git a/src/test/terminal/testSession.c b/src/test/terminal/testSession.c deleted file mode 100644 index 86b4807b..00000000 --- a/src/test/terminal/testSession.c +++ /dev/null @@ -1,343 +0,0 @@ -// 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. - -// testSession.c -- the Terminal phone book and TERMINAL.INI: defaults, a -// full round trip of every field, passphrases that must not be stored, -// hand-edited files with bad values, add / remove limits and the safe -// save (a failed save keeps the old file). - -#include "../serial/serialTest.h" -#include "../../apps/kpunch/terminal/termSession.h" -#include "serial/rs232/rs232.h" - -#include -#include -#include - -#define TMP_DIR "sessionTmp" -#define INI_PATH TMP_DIR "/TERMINAL.INI" -#define MISSING_PATH TMP_DIR "/NONE.INI" -#define BAD_DIR_PATH TMP_DIR "/nodir/TERMINAL.INI" -#define TEXT_MAX 4096 -#define SECRET "swordfish" -#define ARRAY_LEN(a) ((int32_t)(sizeof(a) / sizeof((a)[0]))) - -// A hand-edited TERMINAL.INI full of values out of range or unknown -static const char *const sBadIni[] = { - "[phonebook]", - "entries = 2", - "current = 7", - "[transfer]", - "protocol = kermit", - "exists = maybe", - "[entry1]", - "name =", - "type = pigeon", - "port = COM9", - "speed = 12345", - "databits = 9", - "parity = Q", - "stopbits = 3", - "flow = magic", - "redial = 500", - "redialdelay = 0", - "tcpport = 99999", - "cols = 5", - "rows = 900", - "scrollback = -4", - "scheme = neon", - "localecho = perhaps", - "[entry2]", - "name = Second", - "type = modem", - "port = com2", - "speed = 2400", - "parity = e" -}; - - -// ============================================================ -// Prototypes -// ============================================================ - -static void caseBadValues(void); -static void caseDefaults(void); -static void caseLimits(void); -static void casePassphrase(void); -static void caseRoundTrip(void); -static void caseTelnetUrl(void); -static bool fileHas(const char *path, const char *needle); -static bool sessionsEqual(const TermSessionT *a, const TermSessionT *b); -static void writeLines(const char *path, const char *const *lines, int32_t count); -int main(void); - - -// A hand-edited file with nonsense values loads as valid sessions. -static void caseBadValues(void) { - TermBookT book; - - writeLines(INI_PATH, sBadIni, ARRAY_LEN(sBadIni)); - termBookLoad(&book, INI_PATH); - - TermSessionT *a = &book.entries[0]; - TermSessionT *b = &book.entries[1]; - - TEST_EQ("bad count", book.count, 2); - TEST_EQ("bad current", book.current, 1); - TEST_EQ("bad protocol", book.protocol, XferZmodemE); - TEST_EQ("bad exists", book.exists, XferExistAskE); - TEST_TRUE("bad name", strcmp(a->name, TERM_DEFAULT_NAME) == 0); - TEST_EQ("bad type", a->conn.type, ConnSerialE); - TEST_EQ("bad port", a->conn.comPort, RS232_COM1); - TEST_EQ("bad speed", a->conn.bps, 9600); - TEST_EQ("bad data", a->conn.dataBits, 8); - TEST_EQ("bad parity", a->conn.parity, 'N'); - TEST_EQ("bad stop", a->conn.stopBits, 1); - TEST_EQ("bad flow", a->conn.flow, RS232_HANDSHAKE_NONE); - TEST_EQ("bad redial", a->conn.redialCount, TERM_REDIAL_MAX); - TEST_EQ("bad delay", a->conn.redialDelay, TERM_REDIAL_DELAY_MIN); - TEST_EQ("bad tcp", a->conn.tcpPort, TERM_TCP_PORT_MAX); - TEST_EQ("bad cols", a->cols, TERM_COLS_MIN); - TEST_EQ("bad rows", a->rows, TERM_ROWS_MAX); - TEST_EQ("bad scrollback", a->scrollback, TERM_SCROLLBACK_MIN); - TEST_EQ("bad scheme", a->scheme, SchemeStandardE); - TEST_TRUE("bad echo", !a->localEcho); - - // Names match in any case. - TEST_TRUE("second name", strcmp(b->name, "Second") == 0); - TEST_EQ("second type", b->conn.type, ConnModemE); - TEST_EQ("second port", b->conn.comPort, RS232_COM2); - TEST_EQ("second speed", b->conn.bps, 2400); - TEST_EQ("second parity", b->conn.parity, 'E'); -} - - -static void caseDefaults(void) { - TermBookT book; - - unlink(MISSING_PATH); - termBookLoad(&book, MISSING_PATH); - TEST_EQ("defaults count", book.count, 1); - TEST_EQ("defaults current", book.current, 0); - TEST_EQ("defaults protocol", book.protocol, XferZmodemE); - TEST_EQ("defaults exists", book.exists, XferExistAskE); - TEST_TRUE("defaults auto zmodem", book.autoZmodem); - TEST_TRUE("defaults dir", strcmp(book.downloadDir, TERM_DEFAULT_DIR) == 0); - TEST_TRUE("defaults name", strcmp(book.entries[0].name, TERM_DEFAULT_NAME) == 0); - TEST_EQ("defaults type", book.entries[0].conn.type, ConnSerialE); - TEST_EQ("defaults speed", book.entries[0].conn.bps, 9600); - TEST_EQ("defaults cols", book.entries[0].cols, TERM_COLS_DEFAULT); - TEST_EQ("defaults rows", book.entries[0].rows, TERM_ROWS_DEFAULT); - TEST_TRUE("defaults init", strcmp(book.entries[0].conn.initString, "ATZ") == 0); - TEST_TRUE("defaults prefix", strcmp(book.entries[0].conn.dialPrefix, "ATDT") == 0); -} - - -static void caseLimits(void) { - TermBookT book; - TermSessionT s; - - termBookDefaults(&book); - termSessionDefaults(&s); - - for (int32_t i = 1; i < TERM_BOOK_MAX; i++) { - snprintf(s.name, sizeof(s.name), "Entry %d", (int)i); - TEST_EQ("add", termBookAdd(&book, &s), i); - } - - TEST_EQ("add full", termBookAdd(&book, &s), -1); - book.current = TERM_BOOK_MAX - 1; - termBookRemove(&book, TERM_BOOK_MAX - 1); - TEST_EQ("remove last count", book.count, TERM_BOOK_MAX - 1); - TEST_EQ("remove last current", book.current, TERM_BOOK_MAX - 2); - termBookRemove(&book, 0); - TEST_TRUE("remove first shifts", strcmp(book.entries[0].name, "Entry 1") == 0); - termBookRemove(&book, -1); - termBookRemove(&book, book.count); - TEST_EQ("remove out of range", book.count, TERM_BOOK_MAX - 2); - - while (book.count > 1) { - termBookRemove(&book, 0); - } - - termBookRemove(&book, 0); - TEST_EQ("keeps one", book.count, 1); - TEST_EQ("choice index", prefsChoiceIndexOfValue(termSpeedChoices, TERM_SPEED_CHOICE_COUNT, 115200), TERM_SPEED_CHOICE_COUNT - 1); - TEST_EQ("choice missing", prefsChoiceIndexOfValue(termSpeedChoices, TERM_SPEED_CHOICE_COUNT, 7), -1); -} - - -// A session that asks for its passphrase never has one written; one that -// stores it round-trips. -static void casePassphrase(void) { - TermBookT book; - - termBookDefaults(&book); - book.entries[0].conn.type = ConnSecureE; - snprintf(book.entries[0].conn.passphrase, sizeof(book.entries[0].conn.passphrase), "%s", SECRET); - book.entries[0].askPassphrase = true; - TEST_TRUE("ask save", termBookSave(&book, INI_PATH)); - TEST_TRUE("ask not written", !fileHas(INI_PATH, SECRET)); - - book.entries[0].askPassphrase = false; - TEST_TRUE("store save", termBookSave(&book, INI_PATH)); - TEST_TRUE("store written", fileHas(INI_PATH, SECRET)); - termBookLoad(&book, INI_PATH); - TEST_TRUE("store loaded", strcmp(book.entries[0].conn.passphrase, SECRET) == 0); -} - - -// Every field survives save and load; a save that cannot write keeps the -// old file. -static void caseRoundTrip(void) { - TermBookT book; - TermBookT back; - TermSessionT s; - - termBookDefaults(&book); - termSessionDefaults(&s); - snprintf(s.name, sizeof(s.name), "My BBS"); - s.conn.type = ConnModemE; - s.conn.comPort = RS232_COM3; - s.conn.bps = 57600; - s.conn.dataBits = 7; - s.conn.parity = 'O'; - s.conn.stopBits = 2; - s.conn.flow = RS232_HANDSHAKE_RTSCTS; - s.conn.redialCount = 12; - s.conn.redialDelay = 45; - s.conn.tcpPort = 2323; - s.cols = 132; - s.rows = 43; - s.scrollback = 5000; - s.scheme = SchemeAmberE; - s.localEcho = true; - s.secureNet = true; - snprintf(s.conn.initString, sizeof(s.conn.initString), "AT&F E1 V1 X4"); - snprintf(s.conn.dialPrefix, sizeof(s.conn.dialPrefix), "ATDP"); - snprintf(s.conn.phone, sizeof(s.conn.phone), "1-555-0100"); - snprintf(s.conn.host, sizeof(s.conn.host), "bbs.example.org"); - snprintf(s.answerback, sizeof(s.answerback), "DVX Terminal"); - TEST_EQ("rt add", termBookAdd(&book, &s), 1); - book.current = 1; - book.protocol = XferYmodemE; - book.exists = XferExistRenameE; - book.autoZmodem = false; - book.escapeCtl = true; - book.resumeUploads = true; - snprintf(book.downloadDir, sizeof(book.downloadDir), "C:\\FILES\\IN"); - - mkdir(TMP_DIR, 0755); - TEST_TRUE("rt save", termBookSave(&book, INI_PATH)); - termBookLoad(&back, INI_PATH); - TEST_EQ("rt count", back.count, 2); - TEST_EQ("rt current", back.current, 1); - TEST_EQ("rt protocol", back.protocol, XferYmodemE); - TEST_EQ("rt exists", back.exists, XferExistRenameE); - TEST_TRUE("rt auto", !back.autoZmodem); - TEST_TRUE("rt escape", back.escapeCtl); - TEST_TRUE("rt resume", back.resumeUploads); - TEST_TRUE("rt dir", strcmp(back.downloadDir, "C:\\FILES\\IN") == 0); - TEST_TRUE("rt session", sessionsEqual(&back.entries[1], &book.entries[1])); - TEST_TRUE("rt first", sessionsEqual(&back.entries[0], &book.entries[0])); - - // Saving into a folder that does not exist fails and writes nothing. - TEST_TRUE("rt bad path", !termBookSave(&book, BAD_DIR_PATH)); - TEST_TRUE("rt old kept", fileHas(INI_PATH, "My BBS")); -} - - -// "telnet://host[:port]" and finding its phone book entry. -static void caseTelnetUrl(void) { - TermBookT book; - TermSessionT s; - - TEST_TRUE("url host port", termSessionFromUrl("telnet://bbs.example.org:2323", &s)); - TEST_EQ("url type", s.conn.type, ConnTelnetE); - TEST_EQ("url port", s.conn.tcpPort, 2323); - TEST_TRUE("url host", strcmp(s.conn.host, "bbs.example.org") == 0); - TEST_TRUE("url name", strcmp(s.name, "bbs.example.org") == 0); - TEST_TRUE("url default port", termSessionFromUrl("TELNET://10.0.2.2/", &s) && s.conn.tcpPort == 23 && strcmp(s.conn.host, "10.0.2.2") == 0); - TEST_TRUE("url not telnet", !termSessionFromUrl("ssh://host", &s)); - TEST_TRUE("url no host", !termSessionFromUrl("telnet://:23", &s)); - TEST_TRUE("url bad port", !termSessionFromUrl("telnet://host:0", &s)); - TEST_TRUE("url port text", !termSessionFromUrl("telnet://host:2x", &s)); - TEST_TRUE("url empty port", !termSessionFromUrl("telnet://host:", &s)); - TEST_TRUE("url null", !termSessionFromUrl(NULL, &s)); - - termBookDefaults(&book); - termSessionFromUrl("telnet://bbs.example.org:2323", &s); - TEST_EQ("url not in book", termBookFindTelnet(&book, &s), -1); - TEST_EQ("url added", termBookAdd(&book, &s), 1); - termSessionFromUrl("telnet://BBS.example.org:2323", &s); - TEST_EQ("url found", termBookFindTelnet(&book, &s), 1); - termSessionFromUrl("telnet://bbs.example.org:23", &s); - TEST_EQ("url other port", termBookFindTelnet(&book, &s), -1); -} - - -static bool fileHas(const char *path, const char *needle) { - char text[TEXT_MAX]; - FILE *fp = fopen(path, "rb"); - size_t n; - - if (!fp) { - return false; - } - - n = fread(text, 1, sizeof(text) - 1, fp); - fclose(fp); - text[n] = '\0'; - return strstr(text, needle) != NULL; -} - - -// Every field (strings up to their end; bytes after the NUL do not count). -static bool sessionsEqual(const TermSessionT *a, const TermSessionT *b) { - const ConnConfigT *x = &a->conn; - const ConnConfigT *y = &b->conn; - - return strcmp(a->name, b->name) == 0 && x->type == y->type && x->comPort == y->comPort && x->bps == y->bps && x->dataBits == y->dataBits && x->parity == y->parity && x->stopBits == y->stopBits && x->flow == y->flow && x->redialCount == y->redialCount && x->redialDelay == y->redialDelay && x->tcpPort == y->tcpPort && strcmp(x->initString, y->initString) == 0 && strcmp(x->dialPrefix, y->dialPrefix) == 0 && strcmp(x->phone, y->phone) == 0 && strcmp(x->passphrase, y->passphrase) == 0 && strcmp(x->host, y->host) == 0 && a->cols == b->cols && a->rows == b->rows && a->scrollback == b->scrollback && a->scheme == b->scheme && a->askPassphrase == b->askPassphrase && a->secureNet == b->secureNet && a->localEcho == b->localEcho && strcmp(a->answerback, b->answerback) == 0; -} - - -static void writeLines(const char *path, const char *const *lines, int32_t count) { - FILE *fp = fopen(path, "wb"); - - for (int32_t i = 0; i < count; i++) { - fprintf(fp, "%s\r\n", lines[i]); - } - - fclose(fp); -} - - -int main(void) { - mkdir(TMP_DIR, 0755); - caseDefaults(); - caseRoundTrip(); - caseBadValues(); - casePassphrase(); - caseLimits(); - caseTelnetUrl(); - return testSummary(); -} diff --git a/src/test/terminal/testTermApp.c b/src/test/terminal/testTermApp.c index 2cab969b..eb81f36e 100644 --- a/src/test/terminal/testTermApp.c +++ b/src/test/terminal/testTermApp.c @@ -20,17 +20,24 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -// testTermApp.c -- the Terminal program itself, loaded into the host -// shell (the shell suite's fixture) with the connection library on the -// serial loopback shim. The test is the other end of COM1 (a BBS): it -// sends text and ZMODEM downloads, receives typing, pastes, the -// answerback and an XMODEM upload, and drives the menus and dialogs -// through the window's own callbacks. Settings come from a TERMINAL.INI -// written before the program starts. +// testTermApp.c -- the Terminal program itself (a DVX BASIC program, +// compiled from src/apps/kpunch/terminal/terminal.dbp and run by the +// BASIC stub with live library calls) in the host shell, with the +// connection library on the serial loopback shim. The test is the other +// end of COM1 (a BBS): it sends text and ZMODEM downloads, receives +// typing, pastes, the answerback and uploads, and drives the menus and +// dialogs the way a user does (dialog scripts answer the modal windows). +// Settings come from a TERMINAL.INI written before the program starts. +// The phone book module (terminal.bas) is also checked on its own by a +// small BASIC program, bookTest/bookTest.dbp, that reports each check. #include "../shell/shellFixture.h" #include "dvxDlg.h" #include "dvxPlat.h" +#include "formrt/formrt.h" +#include "dropdown/dropdown.h" +#include "listBox/listBox.h" +#include "spinner/spinner.h" #include "xfer/xfer.h" #include "serial/rs232/rs232.h" @@ -39,24 +46,35 @@ #include "../serial/loopShim.h" #undef clock +#include +#include +#include #include #include #include +#include #include #include #define APP_TERMINAL "apps/kpunch/terminal/terminal.app" +#define TERMINAL_DBP "src/apps/kpunch/terminal/terminal.dbp" +#define APP_BOOKTEST "apps/test/booktest/booktest.app" +#define BOOKTEST_DBP "src/test/terminal/bookTest/bookTest.dbp" +#define BOOKTEST_RESULTS "BOOKTEST.TXT" +#define BOOKTEST_PASS "PASS " +#define BOOKTEST_END "END " #define CONFIG_DIR "CONFIG/apps/kpunch/terminal" #define INI_FILE CONFIG_DIR "/TERMINAL.INI" #define DOWNLOAD_DIR "DL" #define LOCAL_COM RS232_COM1 #define PEER_COM RS232_COM2 #define TICK_MS 10 -#define MS_PER_SEC 1000 #define SETTLE_TICKS 20 #define LIMIT_TICKS 60000 #define BREAK_TICKS 50 +#define START_TICKS 400 #define BUF_SIZE 8192 +#define LINE_LEN 256 #define FILE_SIZE 30000 #define SEED 0x7E57A99u #define ANSWERBACK "DVXTEST" @@ -67,29 +85,54 @@ #define XS_A 13 #define XS_B 17 #define XS_C 5 +#define CANCEL_AFTER_BYTES 4096 +#define RESIZED_COLS 100 +#define PASSPHRASE "open sesame" +#define LOCALHOST "127.0.0.1" +#define SCRIPT_STEPS_MAX 4 +#define ENTRY_TEST_BBS 0 +#define ENTRY_SECURE 1 +#define ENTRY_NEW 2 +#define EXISTS_ASK 0 -// Terminal menu commands (terminal.c) -#define CMD_PHONEBOOK 100 -#define CMD_CONNECT 101 -#define CMD_DISCONNECT 102 -#define CMD_CAPTURE 103 -#define CMD_COPY 110 -#define CMD_PASTE 111 -#define CMD_CLEAR 112 -#define CMD_BREAK 113 -#define CMD_SESSION 120 -#define CMD_ECHO 121 -#define CMD_XFER_OPTIONS 122 -#define CMD_SEND 130 -#define CMD_SCHEME_BASE 200 -#define CMD_PROTO_BASE 220 +// Menu items (captions as the program's menus show them) +#define MENU_PHONEBOOK "&Phone Book..." +#define MENU_CONNECT "&Connect" +#define MENU_DISCONNECT "&Disconnect" +#define MENU_CAPTURE "C&apture to File..." +#define MENU_COPY "&Copy" +#define MENU_PASTE "&Paste" +#define MENU_CLEAR "C&lear Screen" +#define MENU_BREAK "Send &Break" +#define MENU_SESSION "&Session Settings..." +#define MENU_ECHO "&Local Echo" +#define MENU_XFER_OPTIONS "&Transfer Options..." +#define MENU_SEND "&Send Files..." +#define MENU_ABOUT "&About Terminal..." #define ARRAY_LEN(a) ((int32_t)(sizeof(a) / sizeof((a)[0]))) -// The settings the program starts with +// One step of a dialog script: when the modal window titled title comes +// up, prepare (optional) sets its controls, then button is pressed. +// Steps answer in order; a step is used up before its prepare runs, so a +// window that prepare opens is answered by a later step. +typedef struct { + const char *title; + void (*prepare)(WindowT *modal); + const char *button; +} ScriptStepT; + +typedef struct { + ScriptStepT steps[SCRIPT_STEPS_MAX]; + int32_t count; + int32_t answered; +} ScriptT; + +// The settings the program starts with: a serial BBS and a secure link +// that asks for its passphrase static const char *const sIni[] = { "[phonebook]", - "entries = 1", + "entries = 2", "current = 0", "[transfer]", "directory = " DOWNLOAD_DIR, @@ -101,7 +144,13 @@ static const char *const sIni[] = { "type = serial", "port = COM1", "speed = 9600", - "answerback = " ANSWERBACK + "answerback = " ANSWERBACK, + "[entry2]", + "name = Secure Ask", + "type = secure", + "port = COM1", + "speed = 9600", + "askpassphrase = true" }; @@ -109,45 +158,139 @@ static const char *const sIni[] = { // Prototypes // ============================================================ -static void caseConnectAndType(WindowT *win); -static void caseDialogs(WindowT *win); -static void caseDownloads(WindowT *win); -static void caseUpload(WindowT *win); -static bool fileMatches(const char *path, const uint8_t *data, int32_t len, int32_t padTo); -static void fillData(void); -static uint32_t nowMs(void); -static int32_t peerRead(void *ctx, uint8_t *buf, int32_t maxLen); -static bool peerSaw(const char *needle); -static void peerSay(const char *text); -static int32_t peerWrite(void *ctx, const uint8_t *data, int32_t len); -static void runPeer(XferT *peer); -static void tick(int32_t n); -int main(void); +static void caseBookModule(void); +static void caseClose(WindowT *win, int32_t id); +static void caseConnectAndType(WindowT *win); +static void caseDialogs(WindowT *win); +static void caseDownloads(WindowT *win); +static void casePassphrase(WindowT *win); +static void casePhoneBook(WindowT *win); +static void caseSessionSettings(WindowT *win); +static void caseTelnetUrl(void); +static void caseUpload(WindowT *win); +static bool fileHas(const char *path, const char *needle); +static bool fileMatches(const char *path, const uint8_t *data, int32_t len, int32_t padTo); +static void fillData(void); +static const char *labelText(WindowT *win, const char *name); +static void menu(WindowT *win, const char *caption); +static uint32_t nowMs(void); +static int32_t peerRead(void *ctx, uint8_t *buf, int32_t maxLen); +static bool peerSaw(const char *needle); +static void peerSay(const char *text); +static int32_t peerWrite(void *ctx, const uint8_t *data, int32_t len); +static void prepareAskExisting(WindowT *modal); +static void prepareDelete(WindowT *modal); +static void prepareEdit(WindowT *modal); +static void prepareNew(WindowT *modal); +static void preparePassphrase(WindowT *modal); +static void prepareRenameAndResize(WindowT *modal); +static void prepareRenameEntry(WindowT *modal); +static void prepareSelectSecure(WindowT *modal); +static void prepareSelectTestBbs(WindowT *modal); +static void prepareUploadPath(WindowT *modal); +static void runPeer(XferT *peer); +static void scriptAdd(ScriptT *s, const char *title, void (*prepare)(WindowT *modal), const char *button); +static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx); +static void scriptStart(ScriptT *s); +static void scriptStop(ScriptT *s); +static WindowT *shownWindow(int32_t appId); +static void tick(int32_t n); +int main(void); static char sPeerText[BUF_SIZE]; static int32_t sPeerLen; static uint8_t sData[FILE_SIZE]; +static char sUploadPath[PATH_LEN]; + + +// The phone book module on its own: bookTest.dbp writes PASS / FAIL per +// check and END with the number of failures. +static void caseBookModule(void) { + char line[LINE_LEN]; + int32_t passed = 0; + bool ended = false; + FILE *fp; + + tCase("terminal: phone book module (bookTest.dbp)"); + TEST_TRUE(fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, BOOKTEST_DBP, APP_BOOKTEST)); + remove(BOOKTEST_RESULTS); + TEST_TRUE(shellLoadApp(tCtx(), APP_BOOKTEST) > 0); + fixPumpShell(SETTLE_FRAMES); + fp = fopen(BOOKTEST_RESULTS, "rb"); + TEST_TRUE(fp != NULL); + + while (fp && fgets(line, sizeof(line), fp)) { + line[strcspn(line, "\r\n")] = '\0'; + + if (strncmp(line, BOOKTEST_END, strlen(BOOKTEST_END)) == 0) { + ended = true; + TEST_EQ(atoi(line + strlen(BOOKTEST_END)), 0); + } else if (strncmp(line, BOOKTEST_PASS, strlen(BOOKTEST_PASS)) == 0) { + passed++; + } else { + printf(" bookTest: %s\n", line); + TEST_TRUE(false); + } + } + + if (fp) { + fclose(fp); + } + + TEST_TRUE(ended); + TEST_TRUE(passed > 0); +} + + +// Closing while connected asks first: No keeps the window, Yes closes it, +// hangs up and the program ends. +static void caseClose(WindowT *win, int32_t id) { + ScriptT s = { 0 }; + + tCase("terminal: close asks while connected, then hangs up"); + menu(win, MENU_CONNECT); + tick(SETTLE_TICKS); + TEST_STR_EQ(labelText(win, "lblStatus"), "Connected to COM1 at 9600 bps"); + + scriptAdd(&s, "Terminal", NULL, "No"); + scriptStart(&s); + dvxRequestWindowClose(tCtx(), win); + tick(SETTLE_TICKS); + scriptStop(&s); + TEST_EQ(s.answered, 1); + TEST_TRUE(shownWindow(id) == win); + + scriptAdd(&s, "Terminal", NULL, "Yes"); + scriptStart(&s); + TEST_TRUE(fixCloseApp(win)); + scriptStop(&s); + TEST_EQ(s.answered, 1); + TEST_TRUE(!loopIsOpen(LOCAL_COM)); + TEST_TRUE(fileHas(INI_FILE, "name = Renamed BBS")); +} static void caseConnectAndType(WindowT *win) { - char buf[BUF_SIZE]; + char buf[BUF_SIZE]; FILE *fp; - tCase("terminal: connect, type, receive, capture, paste, answerback, break"); - TEST_TRUE(strcmp(win->title, "Test BBS") == 0); - TEST_TRUE(tFindByText(win->widgetRoot, "Not connected: choose File > Phone Book") != NULL); + tCase("terminal: connect, type, receive, capture, paste, answerback, echo, break"); + TEST_STR_EQ(win->title, "Test BBS"); + TEST_STR_EQ(labelText(win, "lblStatus"), "Not connected: choose File > Phone Book"); + TEST_STR_EQ(labelText(win, "lblInfo"), "COM1 9600 8N1 XMODEM"); - win->onMenu(win, CMD_CONNECT); + menu(win, MENU_CONNECT); tick(SETTLE_TICKS); - TEST_TRUE(tFindByText(win->widgetRoot, "Connected to COM1 at 9600 bps") != NULL); - TEST_TRUE(tFindByText(win->widgetRoot, "COM1 9600 8N1 XMODEM") != NULL); + TEST_STR_EQ(labelText(win, "lblStatus"), "Connected to COM1 at 9600 bps"); + TEST_TRUE(loopIsOpen(LOCAL_COM)); // Capture to a file (the file dialog is answered by the script). fixDialogsOn("OK"); fixDialogsType("CAP.TXT", "Save"); - win->onMenu(win, CMD_CAPTURE); + menu(win, MENU_CAPTURE); fixDialogsOff(); tick(SETTLE_TICKS); + TEST_STR_EQ(labelText(win, "lblInfo"), "COM1 9600 8N1 XMODEM Capture"); peerSay("Welcome to the test BBS\r\n"); tick(SETTLE_TICKS); @@ -165,25 +308,29 @@ static void caseConnectAndType(WindowT *win) { // Paste sends the clipboard with newlines as CR. dvxClipboardCopy("one\ntwo", 7); - win->onMenu(win, CMD_PASTE); + menu(win, MENU_PASTE); tick(SETTLE_TICKS); TEST_TRUE(peerSaw("one\rtwo")); - // Local echo on: typing shows up locally too (no crash, still sent). - win->onMenu(win, CMD_ECHO); + // Local echo on: typing shows up locally too (and is still sent); the + // setting is kept in the phone book. + menu(win, MENU_ECHO); tType("x"); tick(SETTLE_TICKS); TEST_TRUE(peerSaw("x")); - win->onMenu(win, CMD_ECHO); + TEST_TRUE(fileHas(INI_FILE, "localecho = true")); + menu(win, MENU_ECHO); + TEST_TRUE(fileHas(INI_FILE, "localecho = false")); // Break holds the line for a moment. - win->onMenu(win, CMD_BREAK); + menu(win, MENU_BREAK); TEST_TRUE(loopGetBreak(LOCAL_COM)); tick(BREAK_TICKS); TEST_TRUE(!loopGetBreak(LOCAL_COM)); // Stop the capture: the file holds what the BBS sent. - win->onMenu(win, CMD_CAPTURE); + menu(win, MENU_CAPTURE); + TEST_STR_EQ(labelText(win, "lblInfo"), "COM1 9600 8N1 XMODEM"); fp = fopen("CAP.TXT", "rb"); TEST_TRUE(fp != NULL); @@ -195,57 +342,55 @@ static void caseConnectAndType(WindowT *win) { TEST_TRUE(strstr(buf, "Welcome to the test BBS") != NULL); } - win->onMenu(win, CMD_COPY); - win->onMenu(win, CMD_CLEAR); + menu(win, MENU_COPY); + menu(win, MENU_CLEAR); } -// Every dialog opens and closes (answered by the script) without harm; -// settings the dialogs save land in TERMINAL.INI. +// Dialogs open and close without harm; settings the dialogs and menus +// change land in TERMINAL.INI. static void caseDialogs(WindowT *win) { - tCase("terminal: session, transfer options, phone book, colors, protocols"); + static const char *const schemes[] = { "Standard", "Green screen", "Amber screen", "Black on white" }; + ScriptT s = { 0 }; - fixDialogsOn("OK"); - win->onMenu(win, CMD_SESSION); - win->onMenu(win, CMD_XFER_OPTIONS); - fixDialogsOn("Cancel"); - win->onMenu(win, CMD_PHONEBOOK); - fixDialogsOff(); + tCase("terminal: transfer options, about, colors, protocols"); + scriptAdd(&s, "Transfer Options", NULL, "OK"); + scriptAdd(&s, "About Terminal", NULL, "OK"); + scriptStart(&s); + menu(win, MENU_XFER_OPTIONS); + menu(win, MENU_ABOUT); + scriptStop(&s); + TEST_EQ(s.answered, 2); + TEST_TRUE(fileHas(INI_FILE, "exists = rename")); - for (int32_t i = 0; i < 4; i++) { - win->onMenu(win, CMD_SCHEME_BASE + i); + for (int32_t i = 0; i < ARRAY_LEN(schemes); i++) { + menu(win, schemes[i]); tick(1); } - win->onMenu(win, CMD_PROTO_BASE + XferZmodemE); + TEST_TRUE(fileHas(INI_FILE, "scheme = light")); + menu(win, "ZMODEM"); tick(1); - TEST_TRUE(tFindByText(win->widgetRoot, "COM1 9600 8N1 ZMODEM") != NULL); - win->onMenu(win, CMD_PROTO_BASE + XferXmodemE); - - FILE *fp = fopen(INI_FILE, "rb"); - char text[BUF_SIZE]; - size_t n = fp ? fread(text, 1, sizeof(text) - 1, fp) : 0; - - if (fp) { - fclose(fp); - } - - text[n] = '\0'; - TEST_TRUE(strstr(text, "scheme = light") != NULL); - TEST_TRUE(strstr(text, "protocol = xmodem") != NULL); - TEST_TRUE(strstr(text, "name = Test BBS") != NULL); + TEST_STR_EQ(labelText(win, "lblInfo"), "COM1 9600 8N1 ZMODEM"); + TEST_TRUE(fileHas(INI_FILE, "protocol = zmodem")); + menu(win, "XMODEM"); + TEST_TRUE(fileHas(INI_FILE, "protocol = xmodem")); + TEST_TRUE(fileHas(INI_FILE, "name = Test BBS")); } // The BBS starts ZMODEM sends; the Terminal starts receiving on its own. // A second copy of the same file is kept beside the first (exists = -// rename in the settings). +// rename in the settings). With "Ask me" the transfer window asks and +// Keep both does the same; a download the BBS cancels ends with a note. static void caseDownloads(WindowT *win) { XferT *peer = xferCreate(); XferIoT io; const char *files[1] = { "HELLO.TXT" }; + WindowT *prog; + ScriptT s = { 0 }; + bool asked = false; - (void)win; tCase("terminal: ZMODEM downloads start automatically"); io.ctx = NULL; io.read = peerRead; @@ -256,41 +401,279 @@ static void caseDownloads(WindowT *win) { TEST_TRUE(xferStartSend(peer, XferZmodemE, &io, NULL, files, 1, nowMs())); runPeer(peer); TEST_EQ(xferProgress(peer)->status, XferDoneE); - TEST_TRUE(tFindWindow("File Transfer") != NULL); + prog = tFindWindow("File Transfer"); + TEST_TRUE(prog != NULL); + TEST_STR_EQ(labelText(prog, "lblTitle"), "Receiving with ZMODEM"); + TEST_STR_EQ(labelText(prog, "lblNote"), "Transfer complete"); + TEST_STR_EQ(labelText(prog, "lblBatchBytes"), "All files: 30,000 of 30,000 bytes"); + TEST_TRUE(tFindByText(prog->widgetRoot, "Close") != NULL); } TEST_TRUE(fileMatches(DOWNLOAD_DIR "/HELLO.TXT", sData, FILE_SIZE, FILE_SIZE)); TEST_TRUE(fileMatches(DOWNLOAD_DIR "/HELLO~1.TXT", sData, FILE_SIZE, FILE_SIZE)); + + tCase("terminal: a download that exists asks"); + scriptAdd(&s, "Transfer Options", prepareAskExisting, "OK"); + scriptStart(&s); + menu(win, MENU_XFER_OPTIONS); + scriptStop(&s); + TEST_EQ(s.answered, 1); + TEST_TRUE(fileHas(INI_FILE, "exists = ask")); + TEST_TRUE(xferStartSend(peer, XferZmodemE, &io, NULL, files, 1, nowMs())); + + for (int32_t i = 0; i < LIMIT_TICKS && xferPoll(peer, nowMs()) == XferRunningE; i++) { + tick(1); + prog = tFindWindow("File Transfer"); + + if (!asked && prog && strstr(labelText(prog, "lblAsk"), "HELLO.TXT already exists (30,000 bytes). What should happen to it?")) { + asked = true; + TEST_TRUE(tDialogClickButton(prog, "Keep both")); + } + } + + TEST_TRUE(asked); + tick(SETTLE_TICKS); + TEST_EQ(xferProgress(peer)->status, XferDoneE); + TEST_TRUE(fileMatches(DOWNLOAD_DIR "/HELLO~2.TXT", sData, FILE_SIZE, FILE_SIZE)); + + tCase("terminal: a cancelled download"); + TEST_TRUE(xferStartSend(peer, XferZmodemE, &io, NULL, files, 1, nowMs())); + + for (int32_t i = 0; i < LIMIT_TICKS && xferProgress(peer)->fileBytes < CANCEL_AFTER_BYTES; i++) { + xferPoll(peer, nowMs()); + tick(1); + } + + xferCancel(peer); + runPeer(peer); + prog = tFindWindow("File Transfer"); + TEST_TRUE(prog != NULL); + + if (prog) { + TEST_TRUE(strstr(labelText(prog, "lblNote"), "Cancelled") != NULL); + TEST_TRUE(tDialogClickButton(prog, "Close")); + tick(SETTLE_TICKS); + } + + prog = tFindWindow("File Transfer"); + TEST_TRUE(prog == NULL || !prog->visible); xferDestroy(peer); } -// An XMODEM upload: the file dialog is answered with the file's path. +// A secure-link session that asks for its passphrase: chosen in the phone +// book, it asks on Connect; Cancel does not connect, Connect starts the +// key exchange. The passphrase never reaches TERMINAL.INI. Then back to +// the serial session. +static void casePassphrase(WindowT *win) { + ScriptT s = { 0 }; + + tCase("terminal: secure link asks for its passphrase"); + scriptAdd(&s, "Phone Book", prepareSelectSecure, "Connect"); + scriptAdd(&s, "Secure Link Passphrase", NULL, "Cancel"); + scriptStart(&s); + menu(win, MENU_PHONEBOOK); + scriptStop(&s); + tick(SETTLE_TICKS); + TEST_EQ(s.answered, 2); + TEST_STR_EQ(win->title, "Secure Ask"); + TEST_STR_EQ(labelText(win, "lblStatus"), "Disconnected"); + + scriptAdd(&s, "Secure Link Passphrase", preparePassphrase, "Connect"); + scriptStart(&s); + menu(win, MENU_CONNECT); + scriptStop(&s); + TEST_EQ(s.answered, 1); + tick(SETTLE_TICKS); + TEST_STR_EQ(labelText(win, "lblStatus"), "Setting up the secure link on COM1"); + TEST_TRUE(!fileHas(INI_FILE, PASSPHRASE)); + menu(win, MENU_DISCONNECT); + tick(SETTLE_TICKS); + + scriptAdd(&s, "Phone Book", prepareSelectTestBbs, "Connect"); + scriptStart(&s); + menu(win, MENU_PHONEBOOK); + scriptStop(&s); + tick(SETTLE_TICKS); + TEST_STR_EQ(win->title, "Test BBS"); + TEST_STR_EQ(labelText(win, "lblStatus"), "Connected to COM1 at 9600 bps"); +} + + +// The phone book: New... adds a session (saved at once), Edit... changes +// it, Delete removes it after a question. +static void casePhoneBook(WindowT *win) { + ScriptT s = { 0 }; + + tCase("terminal: phone book new, edit, delete"); + scriptAdd(&s, "Phone Book", prepareNew, "Cancel"); + scriptAdd(&s, "New Session", NULL, "OK"); + scriptStart(&s); + menu(win, MENU_PHONEBOOK); + scriptStop(&s); + TEST_EQ(s.answered, 2); + TEST_TRUE(fileHas(INI_FILE, "entries = 3")); + TEST_TRUE(fileHas(INI_FILE, "name = New session")); + + scriptAdd(&s, "Phone Book", prepareEdit, "Cancel"); + scriptAdd(&s, "Edit Session", prepareRenameEntry, "OK"); + scriptStart(&s); + menu(win, MENU_PHONEBOOK); + scriptStop(&s); + TEST_EQ(s.answered, 2); + TEST_TRUE(fileHas(INI_FILE, "name = Edited")); + + scriptAdd(&s, "Phone Book", prepareDelete, "Cancel"); + scriptAdd(&s, "Phone Book", NULL, "Yes"); + scriptStart(&s); + menu(win, MENU_PHONEBOOK); + scriptStop(&s); + TEST_EQ(s.answered, 2); + TEST_TRUE(fileHas(INI_FILE, "entries = 2")); + TEST_TRUE(!fileHas(INI_FILE, "name = Edited")); + TEST_STR_EQ(win->title, "Test BBS"); +} + + +// Session Settings: a new name becomes the title, a new size rebuilds the +// terminal (the window grows), both are saved. +static void caseSessionSettings(WindowT *win) { + ScriptT s = { 0 }; + int32_t oldW = win->w; + + tCase("terminal: session settings"); + scriptAdd(&s, "Session Settings", prepareRenameAndResize, "OK"); + scriptStart(&s); + menu(win, MENU_SESSION); + scriptStop(&s); + tick(SETTLE_TICKS); + TEST_EQ(s.answered, 1); + TEST_STR_EQ(win->title, "Renamed BBS"); + TEST_TRUE(fileHas(INI_FILE, "name = Renamed BBS")); + TEST_TRUE(fileHas(INI_FILE, "cols = 100")); + TEST_TRUE(win->w > oldW); +} + + +// Started with "telnet://127.0.0.1:port": the host is added to the phone +// book and Terminal connects at once (to a listening socket here). +static void caseTelnetUrl(void) { + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + int listener; + int peer = -1; + char args[LINE_LEN]; + int32_t id; + WindowT *win; + + tCase("terminal: telnet://host:port connects at once"); + listener = socket(AF_INET, SOCK_STREAM, 0); + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr(LOCALHOST); + addr.sin_port = 0; + TEST_EQ(bind(listener, (struct sockaddr *)&addr, sizeof(addr)), 0); + TEST_EQ(listen(listener, 1), 0); + TEST_EQ(getsockname(listener, (struct sockaddr *)&addr, &addrLen), 0); + fcntl(listener, F_SETFL, O_NONBLOCK); + snprintf(args, sizeof(args), "telnet://%s:%d", LOCALHOST, (int)ntohs(addr.sin_port)); + + id = shellLoadAppWithArgs(tCtx(), APP_TERMINAL, args); + TEST_TRUE(id > 0); + win = NULL; + + for (int32_t i = 0; i < START_TICKS && (!win || strstr(labelText(win, "lblStatus"), "Connected") == NULL); i++) { + tick(1); + win = shownWindow(id); + + if (peer < 0) { + peer = accept(listener, NULL, NULL); + } + } + + TEST_TRUE(win != NULL); + + if (win) { + char expect[LINE_LEN]; + + snprintf(expect, sizeof(expect), "Connected to %s port %d", LOCALHOST, (int)ntohs(addr.sin_port)); + TEST_STR_EQ(win->title, LOCALHOST); + TEST_STR_EQ(labelText(win, "lblStatus"), expect); + TEST_TRUE(fileHas(INI_FILE, "type = telnet")); + close(peer); + peer = -1; + tick(SETTLE_TICKS); + TEST_TRUE(fixCloseApp(win)); + } + + if (peer >= 0) { + close(peer); + } + + close(listener); +} + + +// An XMODEM upload (the file dialog is answered with the file's path), +// then a ZMODEM one, which asks whether to add files to the batch. static void caseUpload(WindowT *win) { XferT *peer = xferCreate(); XferIoT io; - char path[PATH_LEN]; + ScriptT s = { 0 }; char *cwd = getcwd(NULL, 0); - tCase("terminal: XMODEM upload"); + tCase("terminal: XMODEM and ZMODEM uploads"); io.ctx = NULL; io.read = peerRead; io.write = peerWrite; io.pending = NULL; - snprintf(path, sizeof(path), "%s/HELLO.TXT", cwd); + snprintf(sUploadPath, sizeof(sUploadPath), "%s/HELLO.TXT", cwd); free(cwd); TEST_TRUE(xferStartReceive(peer, XferXmodemE, &io, NULL, "UP.DAT", nowMs())); - fixDialogsOn("OK"); - fixDialogsType(path, "Open"); - win->onMenu(win, CMD_SEND); - fixDialogsOff(); + scriptAdd(&s, "Send File", prepareUploadPath, "Open"); + scriptStart(&s); + menu(win, MENU_SEND); + scriptStop(&s); + TEST_EQ(s.answered, 1); runPeer(peer); TEST_EQ(xferProgress(peer)->status, XferDoneE); TEST_TRUE(fileMatches("UP.DAT", sData, FILE_SIZE, ((FILE_SIZE + XMODEM_BLOCK - 1) / XMODEM_BLOCK) * XMODEM_BLOCK)); + TEST_TRUE(tDialogClickButton(tFindWindow("File Transfer"), "Close")); + + menu(win, "ZMODEM"); + TEST_EQ(platformMkdirRecursive("UPZ"), 0); + TEST_TRUE(xferStartReceive(peer, XferZmodemE, &io, NULL, "UPZ", nowMs())); + scriptAdd(&s, "Send File", prepareUploadPath, "Open"); + scriptAdd(&s, "Send Files", NULL, "No"); + scriptStart(&s); + menu(win, MENU_SEND); + scriptStop(&s); + TEST_EQ(s.answered, 2); + runPeer(peer); + TEST_EQ(xferProgress(peer)->status, XferDoneE); + TEST_TRUE(fileMatches("UPZ/HELLO.TXT", sData, FILE_SIZE, FILE_SIZE)); + TEST_TRUE(tDialogClickButton(tFindWindow("File Transfer"), "Close")); + menu(win, "XMODEM"); xferDestroy(peer); } +static bool fileHas(const char *path, const char *needle) { + char text[BUF_SIZE]; + FILE *fp = fopen(path, "rb"); + size_t n; + + if (!fp) { + return false; + } + + n = fread(text, 1, sizeof(text) - 1, fp); + fclose(fp); + text[n] = '\0'; + return strstr(text, needle) != NULL; +} + + // path holds data followed by SUB padding up to padTo bytes. static bool fileMatches(const char *path, const uint8_t *data, int32_t len, int32_t padTo) { FILE *fp = fopen(path, "rb"); @@ -328,8 +711,35 @@ static void fillData(void) { } +// The text of a label (by control name) on a window, "" when missing. +static const char *labelText(WindowT *win, const char *name) { + WidgetT *w = win ? tFind(win, name) : NULL; + + return w ? wgtGetText(w) : ""; +} + + +// Choose a menu command by its caption, as the window's program (the +// shell attributes a window's callbacks to its app). +static void menu(WindowT *win, const char *caption) { + AppContextT *ctx = tCtx(); + int32_t id = fixMenuId(win, caption); + int32_t saved = ctx->currentAppId; + + TEST_TRUE(id >= 0); + + if (id < 0) { + return; + } + + ctx->currentAppId = win->appId; + win->onMenu(win, id); + ctx->currentAppId = saved; +} + + static uint32_t nowMs(void) { - return (uint32_t)(((uint64_t)platformClock() * MS_PER_SEC) / platformClockHz()); + return PLATFORM_TICKS_TO_MS(platformClock()); } @@ -372,6 +782,72 @@ static int32_t peerWrite(void *ctx, const uint8_t *data, int32_t len) { } +// Transfer Options: "Ask me" when a download exists. +static void prepareAskExisting(WindowT *modal) { + wgtDropdownSetSelected(tFind(modal, "ddExists"), EXISTS_ASK); +} + + +// Phone book: select the added entry and press Delete (its question is the +// next step). +static void prepareDelete(WindowT *modal) { + wgtListBoxSetSelected(tFind(modal, "lstBook"), ENTRY_NEW); + TEST_TRUE(tDialogClickButton(modal, "Delete")); +} + + +// Phone book: select the added entry and press Edit... (the session dialog +// is the next step). +static void prepareEdit(WindowT *modal) { + wgtListBoxSetSelected(tFind(modal, "lstBook"), ENTRY_NEW); + TEST_TRUE(tDialogClickButton(modal, "Edit...")); +} + + +// Phone book: press New... (the session dialog is the next step). +static void prepareNew(WindowT *modal) { + TEST_TRUE(tDialogClickButton(modal, "New...")); +} + + +static void preparePassphrase(WindowT *modal) { + wgtSetText(tFind(modal, "txtPassphrase"), PASSPHRASE); +} + + +static void prepareRenameAndResize(WindowT *modal) { + wgtSetText(tFind(modal, "txtName"), "Renamed BBS"); + wgtSpinnerSetValue(tFind(modal, "spnCols"), RESIZED_COLS); +} + + +static void prepareRenameEntry(WindowT *modal) { + wgtSetText(tFind(modal, "txtName"), "Edited"); +} + + +static void prepareSelectSecure(WindowT *modal) { + wgtListBoxSetSelected(tFind(modal, "lstBook"), ENTRY_SECURE); +} + + +static void prepareSelectTestBbs(WindowT *modal) { + wgtListBoxSetSelected(tFind(modal, "lstBook"), ENTRY_TEST_BBS); +} + + +// The file dialog's name box: the file to upload. +static void prepareUploadPath(WindowT *modal) { + WidgetT *label = tFindByText(modal->widgetRoot, "File name:"); + + TEST_TRUE(label && label->nextSibling); + + if (label && label->nextSibling) { + wgtSetText(label->nextSibling, sUploadPath); + } +} + + // Run the far end's transfer and the shell together until it ends. static void runPeer(XferT *peer) { for (int32_t i = 0; i < LIMIT_TICKS; i++) { @@ -386,6 +862,77 @@ static void runPeer(XferT *peer) { } +static void scriptAdd(ScriptT *s, const char *title, void (*prepare)(WindowT *modal), const char *button) { + if (s->count >= SCRIPT_STEPS_MAX) { + TEST_TRUE(false); + return; + } + + s->steps[s->count].title = title; + s->steps[s->count].prepare = prepare; + s->steps[s->count].button = button; + s->count++; +} + + +// Answer the modal window with the next step of the script, when its +// title is the step's. +static void scriptRun(AppContextT *ctx, WindowT *modal, void *userCtx) { + ScriptT *s = (ScriptT *)userCtx; + + (void)ctx; + + if (!modal || s->answered >= s->count) { + return; + } + + const ScriptStepT *step = &s->steps[s->answered]; + + if (strcmp(modal->title, step->title) != 0) { + return; + } + + s->answered++; + + if (step->prepare) { + step->prepare(modal); + } + + TEST_TRUE(tDialogClickButton(modal, step->button)); +} + + +// Answer modal windows with s's steps from now on. +static void scriptStart(ScriptT *s) { + s->answered = 0; + tDialogScript(scriptRun, s); +} + + +// Stop answering; s starts empty for the next script. +static void scriptStop(ScriptT *s) { + tDialogScript(NULL, NULL); + s->count = 0; +} + + +// The program's topmost shown window (its dialogs stay loaded, hidden), +// or NULL. +static WindowT *shownWindow(int32_t appId) { + AppContextT *ctx = tCtx(); + + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + WindowT *win = ctx->stack.windows[i]; + + if (win->appId == appId && win->visible && !win->destroyPending) { + return win; + } + } + + return NULL; +} + + // Frames with the clock moving (the Terminal's heartbeat is a timer). static void tick(int32_t n) { for (int32_t i = 0; i < n; i++) { @@ -405,8 +952,11 @@ int main(void) { return 1; } + basNativeHostSetLive(true); fixShellUp(SCREEN_BPP); - fixLinkApp("terminal"); + caseBookModule(); + + TEST_TRUE(fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, TERMINAL_DBP, APP_TERMINAL)); TEST_EQ(platformMkdirRecursive(CONFIG_DIR), 0); fp = fopen(INI_FILE, "wb"); @@ -426,7 +976,7 @@ int main(void) { TEST_TRUE(id > 0); tick(SETTLE_TICKS); - WindowT *win = fixWindowForApp(id); + WindowT *win = shownWindow(id); TEST_TRUE(win != NULL); @@ -435,17 +985,20 @@ int main(void) { caseDialogs(win); caseDownloads(win); caseUpload(win); + casePassphrase(win); + casePhoneBook(win); + caseSessionSettings(win); - tCase("terminal: disconnect and close"); - win->onMenu(win, CMD_DISCONNECT); + tCase("terminal: disconnect"); + menu(win, MENU_DISCONNECT); tick(SETTLE_TICKS); TEST_TRUE(!loopIsOpen(LOCAL_COM)); - TEST_TRUE(tFindByText(win->widgetRoot, "Disconnected") != NULL); - win->onClose(win); - fixPumpShell(SETTLE_FRAMES); - TEST_EQ(fixAppWindowCount(id), 0); + TEST_STR_EQ(labelText(win, "lblStatus"), "Disconnected"); + caseClose(win, id); } + caseTelnetUrl(); + basNativeHostSetLive(false); fixShellDown(); return tSuiteEnd(); } diff --git a/src/test/tools/checkModules.py b/src/test/tools/checkModules.py index 420f4648..67f8d20f 100644 --- a/src/test/tools/checkModules.py +++ b/src/test/tools/checkModules.py @@ -44,6 +44,9 @@ # .wgt, never another app # Also every .lib and .wgt must export its ABI stamp, + # DVX_ABI_SUFFIX (DVX_ABI_STAMP in dvxTypes.h), or the loader refuses it. +# A file that starts with DVX_RES_ONLY_MAGIC (dvxRes.h, beside the ABI +# header) holds only resources (a control written in BASIC): no code, so +# nothing to check. import os import re @@ -60,13 +63,18 @@ KIND_WGT = '.wgt' APP_EXTS = ('.app', '.dxe') MOD_EXTS = APP_EXTS + (KIND_DRV, KIND_LIB, KIND_WGT) ABI_PATTERN = r'#define\s+DVX_ABI_SUFFIX\s+(\w+)' +RES_HEADER = 'dvxRes.h' +RES_PATTERN = r'#define\s+DVX_RES_ONLY_MAGIC\s+"([^"]*)"' TAG = 'checkModules: ' -def addModule(path, mods): +def addModule(path, mods, resOnly): ext = os.path.splitext(path)[1].lower() if ext not in MOD_EXTS: return + with open(path, 'rb') as f: + if f.read(len(resOnly)) == resOnly: + return exps, unres = moduleSyms(path) rec = {'path': path, 'kind': ext, 'exps': exps, 'unres': unres, 'deps': readDeps(path)} mods[KIND_ALL].append(rec) @@ -130,16 +138,18 @@ def main(argv): sys.stderr.write('usage: checkModules.py EXE ABIHEADER PATH...\n') return 2 try: - exe = exeExports(argv[1]) + exe = exeExports(argv[1]) + magic = re.search(RES_PATTERN, open(os.path.join(os.path.dirname(argv[2]), RES_HEADER)).read()).group(1) + resOnly = magic.encode('ascii') + b'\0' mods = {KIND_ALL: [], KIND_APP: [], KIND_DRV: [], KIND_LIB: {}, KIND_WGT: {}} for top in argv[3:]: if os.path.isfile(top): - addModule(top, mods) + addModule(top, mods, resOnly) continue for root, dirs, files in os.walk(top): dirs.sort() for f in sorted(files): - addModule(os.path.join(root, f), mods) + addModule(os.path.join(root, f), mods, resOnly) except DxeError as e: print(TAG + str(e)) return 1 diff --git a/src/test/tools/runTools.sh b/src/test/tools/runTools.sh index 0e8d7275..095ee974 100755 --- a/src/test/tools/runTools.sh +++ b/src/test/tools/runTools.sh @@ -20,7 +20,7 @@ GOLD="$HERE/golden" UPDATE="${UPDATE_GOLDENS:-0}" HOSTCC="${HOSTCC:-gcc}" -SAN_FLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -Wall -Wextra -Werror -Wno-format-truncation" +SAN_FLAGS=(-O1 -g "-fsanitize=address,undefined" -fno-omit-frame-pointer -Wall -Wextra -Werror -Wno-format-truncation) # T9_COV=1 (src/test/coverage): the tools come from the gcov-instrumented # build under obj/hostcov/tools and the self-compiled binaries use @@ -29,7 +29,7 @@ COV="${T9_COV:-0}" if [ "$COV" = "1" ]; then HOST="$ROOT/obj/hostcov/tools/bin" MAKE_ARGS="COV=1" - SAN_FLAGS="-O1 -g --coverage -Wall -Wextra -Werror -Wno-format-truncation" + SAN_FLAGS=(-O1 -g --coverage -Wall -Wextra -Werror -Wno-format-truncation) WORK="${T9_WORK:-$ROOT/obj/hostcov/tools/work}" rm -rf "$WORK" else @@ -37,7 +37,7 @@ else MAKE_ARGS="" WORK="${T9_WORK:-$(mktemp -d "${TMPDIR:-/tmp}/t9.XXXXXX")}" fi -INCLUDES="-I$TOOLS -I$LIBDVX -I$LIBDVX/platform" +INCLUDES=(-I"$TOOLS" -I"$LIBDVX" -I"$LIBDVX/platform") PLATFORM_UTIL="$LIBDVX/platform/dvxPlatformUtil.c" STB_DS_IMPL="$LIBDVX/thirdparty/stb_ds_impl.c" @@ -454,6 +454,34 @@ expectGrep "hlpc over-long line message" "longLine.dhs:2: line exceeds 4094 char "$HOST/dvxhlpc" -o "$WORK/crlfMax.hlp" --quiet "$WORK/crlfMax.dhs" > /dev/null 2> "$WORK/crlfMax.err" expectRc "hlpc 4094-character CRLF line compiles" 0 $? +# A NUL byte in a line is an error at that line, also when it starts the +# line, and never hides the newline after it (no false over-long line). +# The sanitizer build re-runs these in section 5 (fuzzHlpc reproducers). +printf '.topic t1\n.title T\nab\0cd\n\0\0 tail\n' > "$WORK/nulByte.dhs" +"$HOST/dvxhlpc" -o "$WORK/nulByte.hlp" --quiet "$WORK/nulByte.dhs" > /dev/null 2> "$WORK/nulByte.err" +expectRc "hlpc NUL byte fails" 1 $? +expectGrep "hlpc NUL byte message" "nulByte.dhs:3: NUL byte in line" "$WORK/nulByte.err" +expectGrep "hlpc NUL byte at line start message" "nulByte.dhs:4: NUL byte in line" "$WORK/nulByte.err" +expectNoGrep "hlpc NUL byte is not an over-long line" "line exceeds" "$WORK/nulByte.err" +{ + printf '.topic t1\n' + printf 'a%.0s' $(seq 1 4096) + printf '\0 tail\n.title T\n' +} > "$WORK/longNul.dhs" +"$HOST/dvxhlpc" -o "$WORK/longNul.hlp" --quiet "$WORK/longNul.dhs" > /dev/null 2> "$WORK/longNul.err" +expectRc "hlpc over-long line ending in a NUL chunk fails" 1 $? +expectGrep "hlpc over-long line ending in a NUL chunk message" "longNul.dhs:2: line exceeds 4094 characters" "$WORK/longNul.err" +expectNoGrep "hlpc over-long line tail is skipped, NUL and all" "NUL byte" "$WORK/longNul.err" +printf 'main.dhs\n\0x.dhs\n' > "$WORK/nulList.txt" +"$HOST/dvxhlpc" -o "$WORK/nulList.hlp" --quiet "@$WORK/nulList.txt" > /dev/null 2> "$WORK/nulList.err" +expectRc "hlpc NUL byte in a response file fails" 1 $? +expectGrep "hlpc NUL byte in a response file message" "nulList.txt:2: NUL byte in line" "$WORK/nulList.err" + +# A source with no text compiles (an empty search index). +: > "$WORK/empty.dhs" +"$HOST/dvxhlpc" -o "$WORK/empty.hlp" --quiet "$WORK/empty.dhs" > /dev/null 2> "$WORK/empty.err" +expectRc "hlpc empty source compiles" 0 $? + # Write failures never replace a good .hlp: the temp file is a link to a # full device, so every write fails. cp "$WORK/main.hlp" "$WORK/full.hlp" @@ -797,7 +825,7 @@ done # 2c. dvxres -- injected allocator and I/O failures (resFailTest) # ------------------------------------------------------------------ echo "-- dvxres failure injection" -if $HOSTCC $SAN_FLAGS $INCLUDES -I"$TOOLS" -o "$WORK/resFailTest" "$HERE/resFailTest.c" "$LIBDVX/dvxResource.c" "$STB_DS_IMPL" > "$WORK/resFailBuild.log" 2>&1; then +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -o "$WORK/resFailTest" "$HERE/resFailTest.c" "$LIBDVX/dvxResource.c" "$STB_DS_IMPL" > "$WORK/resFailBuild.log" 2>&1; then pass "resFailTest builds" # A module with one resource; every failed add must leave it untouched. @@ -847,7 +875,7 @@ if $HOSTCC $SAN_FLAGS $INCLUDES -I"$TOOLS" -o "$WORK/resFailTest" "$HERE/resFail # 2d. dvxres with dvxResource.c compiled under the same wrappers: sweep # every injected call ordinal through an add; every failure must leave # the module untouched and no temp file behind. - if $HOSTCC $SAN_FLAGS $INCLUDES -I"$TOOLS" -DRES_WRAP_RESOURCE -o "$WORK/resReadFailTest" "$HERE/resFailTest.c" "$STB_DS_IMPL" > "$WORK/resReadFailBuild.log" 2>&1; then + if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -DRES_WRAP_RESOURCE -o "$WORK/resReadFailTest" "$HERE/resFailTest.c" "$STB_DS_IMPL" > "$WORK/resReadFailBuild.log" 2>&1; then pass "resReadFailTest builds" for cmd in add list get strip rmlast; do for mode in malloc realloc fopen fread fseek fwrite fclose rename; do @@ -1036,7 +1064,7 @@ expectRc "mkwgticon unwritable output fails" 1 $? # bmpDraw row padding and write-failure return (icon widths are all # multiples of four, so the generators never pad). -if $HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/bmpDrawTest" "$HERE/bmpDrawTest.c" "$TOOLS/bmpDraw.c" > "$WORK/bmpDrawBuild.log" 2>&1; then +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/bmpDrawTest" "$HERE/bmpDrawTest.c" "$TOOLS/bmpDraw.c" > "$WORK/bmpDrawBuild.log" 2>&1; then pass "bmpDrawTest builds" "$WORK/bmpDrawTest" "$WORK/pad.bmp" > "$WORK/bmpDrawTest.out" 2>&1 expectRc "bmpDrawTest run" 0 $? @@ -1119,7 +1147,7 @@ expectGrep "mkwgticon usage lists the types" "Types: button label" "$WORK/iu5.er # ------------------------------------------------------------------ echo "-- hlpcCompile library mode" cd "$HERE" || exit 1 -if $HOSTCC $SAN_FLAGS $INCLUDES -DHLPC_NO_MAIN -o "$WORK/hlpcLibTest" hlpcLibTest.c "$TOOLS/dvxhlpc.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/libBuild.log" 2>&1; then +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -DHLPC_NO_MAIN -o "$WORK/hlpcLibTest" hlpcLibTest.c "$TOOLS/dvxhlpc.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/libBuild.log" 2>&1; then pass "hlpcLibTest builds with -DHLPC_NO_MAIN under ASan/UBSan" "$WORK/hlpcLibTest" "$WORK/lib.hlp" "$WORK/lib.html" "$WORK/libNeg.hlp" "$WORK/libLenient.hlp" "$WORK/libLenient.html" > "$WORK/libRun.out" 2> "$WORK/libRun.err" expectRc "hlpcLibTest run" 0 $? @@ -1142,7 +1170,7 @@ fi # Out of memory inside hlpcCompile aborts that compile cleanly (dvx.exe # runs it in-process at boot) instead of exiting the process. -if $HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/hlpcOomTest" hlpcOomTest.c "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/oomBuild.log" 2>&1; then +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/hlpcOomTest" hlpcOomTest.c "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/oomBuild.log" 2>&1; then pass "hlpcOomTest builds" "$WORK/hlpcOomTest" "$WORK" > "$WORK/oomRun.out" 2> "$WORK/oomRun.err" expectRc "hlpcCompile recovers from every out-of-memory point" 0 $? @@ -1163,13 +1191,13 @@ fi # ------------------------------------------------------------------ echo "-- sanitizer builds of the CLI tools" sanOk=1 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/dvxhlpcSan" "$TOOLS/dvxhlpc.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/sanBuild.log" 2>&1 || sanOk=0 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/dvxresSan" "$TOOLS/dvxres.c" "$TOOLS/dvxResWrite.c" "$LIBDVX/dvxResource.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/mkiconSan" "$TOOLS/mkicon.c" "$TOOLS/bmpDraw.c" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/mktbiconSan" "$TOOLS/mktbicon.c" "$TOOLS/bmpDraw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/mkwgticonSan" "$TOOLS/mkwgticon.c" "$TOOLS/bmpDraw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/bmp2rawSan" "$TOOLS/bmp2raw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 -$HOSTCC $SAN_FLAGS $INCLUDES -o "$WORK/mksoundsSan" "$TOOLS/mkSounds.c" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dvxhlpcSan" "$TOOLS/dvxhlpc.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dvxresSan" "$TOOLS/dvxres.c" "$TOOLS/dvxResWrite.c" "$LIBDVX/dvxResource.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mkiconSan" "$TOOLS/mkicon.c" "$TOOLS/bmpDraw.c" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mktbiconSan" "$TOOLS/mktbicon.c" "$TOOLS/bmpDraw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mkwgticonSan" "$TOOLS/mkwgticon.c" "$TOOLS/bmpDraw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/bmp2rawSan" "$TOOLS/bmp2raw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mksoundsSan" "$TOOLS/mkSounds.c" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0 if [ "$sanOk" = "1" ]; then pass "sanitizer builds of all six tools" @@ -1183,6 +1211,14 @@ if [ "$sanOk" = "1" ]; then expectRc "dvxhlpc (ASan) missing image fails" 1 $? "$WORK/dvxhlpcSan" -o "$WORK/sanOut.hlp" --quiet outsideTopic.dhs >> "$WORK/san.err" 2>&1 expectRc "dvxhlpc (ASan) outside topic fails" 1 $? + # fuzzHlpc reproducers: an empty source (qsort of no trigrams) and NUL + # bytes, one starting the chunk after an over-long line (line[-1]). + "$WORK/dvxhlpcSan" -o "$WORK/sanEmpty.hlp" --quiet "$WORK/empty.dhs" >> "$WORK/san.err" 2>&1 + expectRc "dvxhlpc (ASan) empty source compiles" 0 $? + "$WORK/dvxhlpcSan" -o "$WORK/sanNul.hlp" --quiet "$WORK/nulByte.dhs" >> "$WORK/san.err" 2>&1 + expectRc "dvxhlpc (ASan) NUL byte fails" 1 $? + "$WORK/dvxhlpcSan" -o "$WORK/sanLongNul.hlp" --quiet "$WORK/longNul.dhs" >> "$WORK/san.err" 2>&1 + expectRc "dvxhlpc (ASan) over-long line ending in a NUL chunk fails" 1 $? cd "$FIX/res" || exit 1 cp module.bin "$WORK/san.app" "$WORK/dvxresSan" build "$WORK/san.app" manifest.res >> "$WORK/san.err" 2>&1 diff --git a/src/test/unit/Makefile b/src/test/unit/Makefile index 3efe2115..d4c5f0b1 100644 --- a/src/test/unit/Makefile +++ b/src/test/unit/Makefile @@ -43,7 +43,7 @@ CFLAGS = $(HOST_CFLAGS) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdpar LDFLAGS = -rdynamic $(HOST_LDFLAGS) LDLIBS = -ldl -lm -SRCS = unitMain.c unitDisplay.c testClass.c testClock.c testComp.c testDraw.c testEvent.c testLayout.c testMem.c testPlatform.c testPrefs.c testResource.c testUtil.c testVideo.c testWm.c +SRCS = unitMain.c unitDisplay.c testClass.c testClock.c testComp.c testDraw.c testEvent.c testIface.c testLayout.c testMem.c testPlatform.c testPrefs.c testResource.c testUtil.c testVideo.c testWm.c OBJS = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS)) TARGET = $(BINDIR)/unit diff --git a/src/test/unit/testDraw.c b/src/test/unit/testDraw.c index 49fcb29d..1df7cdd2 100644 --- a/src/test/unit/testDraw.c +++ b/src/test/unit/testDraw.c @@ -106,6 +106,7 @@ static void testInitDefault(void); static void testRectCopy(int32_t bpp); static void testRectFill(int32_t bpp); static void testRectFillOpsBpp(void); +static void testScrollbarArrowButton(void); static void testSpans(void); static void testTermRow(int32_t bpp); static void testText(int32_t bpp); @@ -367,6 +368,7 @@ void testDraw(void) { testEmbossed(); testInitDefault(); testRectFillOpsBpp(); + testScrollbarArrowButton(); } @@ -690,6 +692,43 @@ static void testRectFillOpsBpp(void) { } +// drawScrollbarArrowButton: the scrollbar's raised 1px button bevel (top +// and left highlight, bottom and right shadow) on scrollbarBg, with the +// drawScrollbarArrow glyph in scrollbarFg centred on it. +static void testScrollbarArrowButton(void) { + DisplayT d; + BlitOpsT ops; + ColorSchemeT cs; + int32_t right = BEVEL_X + SCROLLBAR_WIDTH - 1; + int32_t bottom = BEVEL_Y + SCROLLBAR_WIDTH - 1; + int32_t cx = BEVEL_X + SCROLLBAR_WIDTH / 2; + int32_t tipY = BEVEL_Y + SCROLLBAR_WIDTH / 2 + SB_ARROW_ROWS / 2; + int32_t inner = SCROLLBAR_WIDTH - 2; + + memset(&cs, 0, sizeof(cs)); + cs.windowHighlight = DRAW_HI; + cs.windowShadow = DRAW_SH; + cs.scrollbarBg = DRAW_FILL; + cs.scrollbarFg = DRAW_FG; + unitDisplayCreate(&d, UNIT_SCREEN_W, UNIT_SCREEN_H, 32); + drawInit(&ops, &d); + + drawScrollbarArrowButton(&d, &ops, &cs, BEVEL_X, BEVEL_Y, SCROLLBAR_WIDTH, ScrollArrowDownE); + TEST_EQ(unitPixelGet(&d, BEVEL_X, BEVEL_Y), DRAW_HI); + TEST_EQ(unitPixelGet(&d, BEVEL_X, bottom - 1), DRAW_HI); + TEST_EQ(unitPixelGet(&d, right, bottom), DRAW_SH); + TEST_EQ(unitPixelGet(&d, right, BEVEL_Y + 1), DRAW_SH); + TEST_EQ(unitPixelGet(&d, BEVEL_X + 1, BEVEL_Y + 1), DRAW_FILL); + TEST_EQ(unitPixelCount(&d, DRAW_HI), SCROLLBAR_WIDTH * 2 - 2); + TEST_EQ(unitPixelCount(&d, DRAW_SH), SCROLLBAR_WIDTH * 2 - 2); + TEST_EQ(unitPixelCount(&d, DRAW_FG), SB_ARROW_ROWS * SB_ARROW_ROWS); + TEST_EQ(unitPixelCount(&d, DRAW_FILL), inner * inner - SB_ARROW_ROWS * SB_ARROW_ROWS); + TEST_EQ(unitPixelGet(&d, cx, tipY), DRAW_FG); + TEST_EQ(unitPixelGet(&d, cx - 1, tipY), DRAW_FILL); + unitDisplayDestroy(&d); +} + + static void testSpans(void) { uint8_t dst[SPAN_MAX * 4 + 16]; uint8_t src[SPAN_MAX * 4]; diff --git a/src/test/unit/testIface.c b/src/test/unit/testIface.c new file mode 100644 index 00000000..09fe1476 --- /dev/null +++ b/src/test/unit/testIface.c @@ -0,0 +1,811 @@ +// 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. + +// testIface.c -- generic widget access (widgetIface.c) and the type binding +// +// A probe widget class with a static interface (every property type, a +// spread of method signatures, a WGT_SIG_DYNAMIC method) and a second +// class with a dynamic interface (WgtDynIfaceT hooks that record what +// they were handed) are registered here; everything goes through +// wgtCreateByName / wgtGetProp / wgtSetProp / wgtCallMethod and the +// descriptor-level forms. Also the WgtValueT coercions, wgtParseBool, +// wgtSigInfo and wgtFireEvent's universal mapping and onEvent. + +#include "unitTest.h" +#include "dvxWgtP.h" + +#include + +#define PROBE_NAME "unitgeneric" +#define PROBE_BAS "UnitProbeCtl" +#define DYN_NAME "unitdyn" +#define DYN_BAS "UnitDynCtl" +#define PROBE_ARG_A 3 +#define PROBE_ARG_B 4 +#define TEXT_BUF 64 +#define SIG_BEYOND 200 +#define WRITE_BACK 42 +#define FLOAT_EPS 0.0001 +#define BIG_DOUBLE 1e12 +#define RATIO_SET 2.5 +#define PRECISE_SET 0.1 // not exact as a float: DOUBLE keeps it +#define SUM_A 1 +#define SUM_B 2 +#define SUM_C 3 +#define MODE_HIGH 2 +#define EVT_ARG_COUNT 3 +#define DYN_LEVEL 9 + +// Probe widget state (one probe at a time) +typedef struct { + double precise; + char name[TEXT_BUF]; + char joined[TEXT_BUF]; + int32_t count; + int32_t mode; + int32_t sum; + int32_t createA; + int32_t createB; + int32_t dynArgc; + float ratio; + bool on; +} ProbeStateT; + +// What the dynamic hooks were handed +typedef struct { + WgtValueT lastSet; + char lastText[TEXT_BUF]; // lastSet's text (a value's text lives only for the call) + int32_t getIdx; + int32_t setIdx; + int32_t callIdx; + int32_t callArgc; + int32_t addA; + int32_t addB; + int32_t creates; +} DynStateT; + +// What the event callbacks saw +typedef struct { + const char *eventName; + int32_t clicks; + int32_t keyCode; + int32_t shift; + int32_t button; + int32_t mouseX; + int32_t mouseY; + int32_t scroll; + int32_t events; + int32_t eventArgc; +} EventStateT; + + +// ============================================================ +// Prototypes +// ============================================================ + +static bool dynCallMethod(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret); +static WidgetT *dynCreate(WidgetT *parent, const WgtIfaceT *iface); +static bool dynGetProp(WidgetT *w, int32_t propIdx, WgtValueT *out); +static bool dynSetProp(WidgetT *w, int32_t propIdx, const WgtValueT *value); +static void evClick(WidgetT *w); +static void evDestroying(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); +static void evEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); +static void evKeyDown(WidgetT *w, int32_t keyCode, int32_t shift); +static void evMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y); +static void evScroll(WidgetT *w, int32_t delta); +static WidgetT *probeCreate(WidgetT *parent, int32_t a, int32_t b); +static bool probeDyn(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); +static const char *probeEcho(const WidgetT *w, int32_t n); +static int32_t probeGetCount(const WidgetT *w); +static int32_t probeGetMode(const WidgetT *w); +static const char *probeGetName(const WidgetT *w); +static bool probeGetOn(const WidgetT *w); +static double probeGetPrecise(const WidgetT *w); +static float probeGetRatio(const WidgetT *w); +static bool probeHas(const WidgetT *w, int32_t n); +static void probeJoin(WidgetT *w, const char *a, const char *b); +static void probeSetCount(WidgetT *w, int32_t v); +static void probeSetMode(WidgetT *w, int32_t v); +static void probeSetName(WidgetT *w, const char *s); +static void probeSetOn(WidgetT *w, bool v); +static void probeSetPrecise(WidgetT *w, double v); +static void probeSetRatio(WidgetT *w, float v); +static void probeSum3(WidgetT *w, int32_t a, int32_t b, int32_t c); +static void testIfaceDynamic(void); +static void testIfaceEvents(void); +static void testIfaceStatic(void); +static void testIfaceTypeBinding(void); +static void testIfaceValues(void); + + +// ============================================================ +// Module state +// ============================================================ + +static ProbeStateT sProbe; +static DynStateT sDyn; +static EventStateT sEv; +static int32_t sProbeType = -1; +static int32_t sDynType = -1; + +static const WidgetClassT sProbeClass = { .version = WGT_CLASS_VERSION }; +static const WidgetClassT sDynClass = { .version = WGT_CLASS_VERSION }; + +static const char *sModeNames[] = { "Off", "Low", "High", NULL }; + +static const WgtPropDescT sProbeProps[] = { + { "Name", WGT_IFACE_STRING, (void *)probeGetName, (void *)probeSetName, NULL }, + { "Count", WGT_IFACE_INT, (void *)probeGetCount, (void *)probeSetCount, NULL }, + { "On", WGT_IFACE_BOOL, (void *)probeGetOn, (void *)probeSetOn, NULL }, + { "Ratio", WGT_IFACE_FLOAT, (void *)probeGetRatio, (void *)probeSetRatio, NULL }, + { "Precise", WGT_IFACE_DOUBLE, (void *)probeGetPrecise, (void *)probeSetPrecise, NULL }, + { "Mode", WGT_IFACE_ENUM, (void *)probeGetMode, (void *)probeSetMode, sModeNames }, + { "ReadOnly", WGT_IFACE_INT, (void *)probeGetCount, NULL, NULL }, + { "WriteOnly", WGT_IFACE_INT, NULL, (void *)probeSetCount, NULL }, +}; + +static const WgtMethodDescT sProbeMethods[] = { + { "Sum3", WGT_SIG_INT3, (void *)probeSum3 }, + { "Echo", WGT_SIG_RET_STR_INT, (void *)probeEcho }, + { "Has", WGT_SIG_RET_BOOL_INT, (void *)probeHas }, + { "Join", WGT_SIG_STR_STR, (void *)probeJoin }, + { "Dyn", WGT_SIG_DYNAMIC, (void *)probeDyn }, +}; + +static const WgtEventDescT sProbeEvents[] = { + { "ItemOpen", "Index As Integer" }, + { "Timer", NULL }, +}; + +static const WgtApiPrefixT sProbeApi = { .header = WGT_API_HEADER(WgtApiPrefixT), .create = (WgtApiFnT)probeCreate }; + +static const WgtIfaceT sProbeIface = { + .basName = PROBE_BAS, + .props = sProbeProps, + .propCount = DVX_ARRAY_LEN(sProbeProps), + .methods = sProbeMethods, + .methodCount = DVX_ARRAY_LEN(sProbeMethods), + .events = sProbeEvents, + .eventCount = DVX_ARRAY_LEN(sProbeEvents), + .createSig = WGT_CREATE_PARENT_INT_INT, + .createArgs = { PROBE_ARG_A, PROBE_ARG_B } +}; + +static const WgtPropDescT sDynProps[] = { + { "Title", WGT_IFACE_STRING, WGT_DYN_ACCESS, WGT_DYN_ACCESS, NULL }, + { "Level", WGT_IFACE_INT, WGT_DYN_ACCESS, NULL, NULL }, +}; + +static const WgtMethodDescT sDynMethods[] = { + { "Ping", WGT_SIG_DYNAMIC, WGT_DYN_ACCESS }, + { "Add", WGT_SIG_INT_INT, WGT_DYN_ACCESS }, +}; + +static const WgtDynIfaceT sDynHooks = { + .create = dynCreate, + .getProp = dynGetProp, + .setProp = dynSetProp, + .callMethod = dynCallMethod +}; + +static const WgtIfaceT sDynIface = { + .basName = DYN_BAS, + .props = sDynProps, + .propCount = DVX_ARRAY_LEN(sDynProps), + .methods = sDynMethods, + .methodCount = DVX_ARRAY_LEN(sDynMethods), + .dyn = &sDynHooks +}; + + +// Ping (any arguments) returns how many it got; Add (a fixed INT_INT on a +// dynamic interface) gets its two arguments already converted to INT. +static bool dynCallMethod(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + (void)w; + sDyn.callIdx = methodIdx; + sDyn.callArgc = argc; + + if (methodIdx == 1) { + sDyn.addA = args[0].i; + sDyn.addB = args[1].i; + return true; + } + + *ret = wgtValueInt(argc); + return true; +} + + +static WidgetT *dynCreate(WidgetT *parent, const WgtIfaceT *iface) { + sDyn.creates += (iface == &sDynIface) ? 1 : 0; + return widgetAlloc(parent, sDynType); +} + + +// Title reads "dyn"; Level answers with the wrong type (text) to show the +// generic layer converts a hook's result to the property's type. +static bool dynGetProp(WidgetT *w, int32_t propIdx, WgtValueT *out) { + (void)w; + sDyn.getIdx = propIdx; + *out = (propIdx == 0) ? wgtValueStr("dyn") : wgtValueStr("9"); + return true; +} + + +static bool dynSetProp(WidgetT *w, int32_t propIdx, const WgtValueT *value) { + (void)w; + sDyn.setIdx = propIdx; + sDyn.lastSet = *value; + + if (value->type == WGT_IFACE_STRING) { + snprintf(sDyn.lastText, sizeof(sDyn.lastText), "%s", value->s); + sDyn.lastSet.s = sDyn.lastText; + } + + return true; +} + + +static void evClick(WidgetT *w) { + (void)w; + sEv.clicks++; +} + + +// An onEvent handler that destroys widgets (simulated by moving the +// destroy generation). +static void evDestroying(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + (void)w; + (void)name; + (void)args; + (void)argc; + sWidgetGen++; +} + + +static void evEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + (void)w; + sEv.events++; + sEv.eventName = name; + sEv.eventArgc = argc; + + if (argc > 0) { + args[0].i = WRITE_BACK; + } +} + + +static void evKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { + (void)w; + sEv.keyCode = keyCode; + sEv.shift = shift; +} + + +static void evMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) { + (void)w; + sEv.button = button; + sEv.mouseX = x; + sEv.mouseY = y; +} + + +static void evScroll(WidgetT *w, int32_t delta) { + (void)w; + sEv.scroll = delta; +} + + +static WidgetT *probeCreate(WidgetT *parent, int32_t a, int32_t b) { + sProbe.createA = a; + sProbe.createB = b; + return widgetAlloc(parent, sProbeType); +} + + +static bool probeDyn(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + (void)w; + (void)args; + sProbe.dynArgc = argc; + *ret = wgtValueBool(true); + return true; +} + + +static const char *probeEcho(const WidgetT *w, int32_t n) { + (void)w; + return (n == 0) ? NULL : sProbe.name; +} + + +static int32_t probeGetCount(const WidgetT *w) { + (void)w; + return sProbe.count; +} + + +static int32_t probeGetMode(const WidgetT *w) { + (void)w; + return sProbe.mode; +} + + +static const char *probeGetName(const WidgetT *w) { + (void)w; + return sProbe.name; +} + + +static bool probeGetOn(const WidgetT *w) { + (void)w; + return sProbe.on; +} + + +static double probeGetPrecise(const WidgetT *w) { + (void)w; + return sProbe.precise; +} + + +static float probeGetRatio(const WidgetT *w) { + (void)w; + return sProbe.ratio; +} + + +static bool probeHas(const WidgetT *w, int32_t n) { + (void)w; + return n == sProbe.count; +} + + +static void probeJoin(WidgetT *w, const char *a, const char *b) { + (void)w; + snprintf(sProbe.joined, sizeof(sProbe.joined), "%s+%s", a, b); +} + + +static void probeSetCount(WidgetT *w, int32_t v) { + (void)w; + sProbe.count = v; +} + + +static void probeSetMode(WidgetT *w, int32_t v) { + (void)w; + sProbe.mode = v; +} + + +static void probeSetName(WidgetT *w, const char *s) { + (void)w; + snprintf(sProbe.name, sizeof(sProbe.name), "%s", s); +} + + +static void probeSetOn(WidgetT *w, bool v) { + (void)w; + sProbe.on = v; +} + + +static void probeSetPrecise(WidgetT *w, double v) { + (void)w; + sProbe.precise = v; +} + + +static void probeSetRatio(WidgetT *w, float v) { + (void)w; + sProbe.ratio = v; +} + + +static void probeSum3(WidgetT *w, int32_t a, int32_t b, int32_t c) { + (void)w; + sProbe.sum = a + b + c; +} + + +void testIface(void) { + sProbeType = wgtRegisterClass(&sProbeClass); + sDynType = wgtRegisterClass(&sDynClass); + wgtRegisterApi(PROBE_NAME, &sProbeApi); + wgtRegisterIface(PROBE_NAME, &sProbeIface); + wgtIfaceSetType(PROBE_NAME, sProbeType); + wgtRegisterIface(DYN_NAME, &sDynIface); + wgtIfaceSetType(DYN_NAME, sDynType); + + testIfaceValues(); + testIfaceTypeBinding(); + testIfaceStatic(); + testIfaceDynamic(); + testIfaceEvents(); +} + + +// The dynamic interface: creation, reads, writes and calls go to the hooks +// with the descriptor index; results are converted to the declared types. +static void testIfaceDynamic(void) { + memset(&sDyn, 0, sizeof(sDyn)); + + WidgetT *w = wgtCreateByName(DYN_BAS, NULL); + WgtValueT v; + WgtValueT args[EVT_ARG_COUNT]; + WgtValueT ret; + + TEST_PTR_NOT_NULL(w); + + if (!w) { + return; + } + + TEST_EQ(sDyn.creates, 1); + TEST_TRUE(wgtGetIfaceOf(w, NULL) == &sDynIface); + + TEST_TRUE(wgtGetProp(w, "title", &v)); + TEST_EQ(sDyn.getIdx, 0); + TEST_STR_EQ(v.s, "dyn"); + TEST_TRUE(wgtGetProp(w, "Level", &v)); + TEST_EQ(sDyn.getIdx, 1); + TEST_EQ(v.type, WGT_IFACE_INT); + TEST_EQ(v.i, DYN_LEVEL); + + // A number written to a STRING property arrives as text + v = wgtValueInt(DYN_LEVEL); + TEST_TRUE(wgtSetProp(w, "Title", &v)); + TEST_EQ(sDyn.setIdx, 0); + TEST_EQ(sDyn.lastSet.type, WGT_IFACE_STRING); + TEST_STR_EQ(sDyn.lastSet.s, "9"); + + // Level is read-only: the hook is not called + sDyn.setIdx = -1; + TEST_FALSE(wgtSetProp(w, "Level", &v)); + TEST_EQ(sDyn.setIdx, -1); + + args[0] = wgtValueStr("a"); + args[1] = wgtValueFloat(RATIO_SET); + args[2] = wgtValueBool(true); + TEST_TRUE(wgtCallMethod(w, "Ping", args, EVT_ARG_COUNT, &ret)); + TEST_EQ(sDyn.callIdx, 0); + TEST_EQ(sDyn.callArgc, EVT_ARG_COUNT); + TEST_EQ(ret.i, EVT_ARG_COUNT); + + // A fixed signature on a dynamic interface: converted arguments, no result + args[0] = wgtValueStr("5"); + args[1] = wgtValueFloat(RATIO_SET); + TEST_TRUE(wgtCallMethod(w, "Add", args, EVT_ARG_COUNT, &ret)); + TEST_EQ(sDyn.callIdx, 1); + TEST_EQ(sDyn.callArgc, 2); + TEST_EQ(sDyn.addA, 5); + TEST_EQ(sDyn.addB, 2); + TEST_EQ(ret.type, WGT_IFACE_NONE); + TEST_FALSE(wgtCallMethod(w, "Add", args, 1, &ret)); + + widgetAllocRollback(w); +} + + +// wgtFireEvent: universal names to their callbacks only, the rest to +// onEvent only (with write-back); false after a handler destroyed widgets. +static void testIfaceEvents(void) { + WidgetT w; + WgtValueT args[EVT_ARG_COUNT]; + + memset(&w, 0, sizeof(w)); + memset(&sEv, 0, sizeof(sEv)); + w.onClick = evClick; + w.onKeyDown = evKeyDown; + w.onMouseDown = evMouseDown; + w.onScroll = evScroll; + w.onEvent = evEvent; + + TEST_TRUE(wgtFireEvent(&w, "click", NULL, 0)); + TEST_EQ(sEv.clicks, 1); + TEST_EQ(sEv.events, 0); + + args[0] = wgtValueInt(KEY_F1); + args[1] = wgtValueInt(KEY_MOD_CTRL); + TEST_TRUE(wgtFireEvent(&w, "KeyDown", args, 2)); + TEST_EQ(sEv.keyCode, KEY_F1); + TEST_EQ(sEv.shift, KEY_MOD_CTRL); + + args[0] = wgtValueInt(MOUSE_RIGHT); + args[1] = wgtValueInt(SUM_B); + args[2] = wgtValueStr("3"); + TEST_TRUE(wgtFireEvent(&w, "MouseDown", args, EVT_ARG_COUNT)); + TEST_EQ(sEv.button, MOUSE_RIGHT); + TEST_EQ(sEv.mouseX, SUM_B); + TEST_EQ(sEv.mouseY, SUM_C); + + // A missing argument is 0 + sEv.scroll = -1; + TEST_TRUE(wgtFireEvent(&w, "Scroll", NULL, 0)); + TEST_EQ(sEv.scroll, 0); + + // No callback for a universal event: nothing happens, onEvent included + TEST_TRUE(wgtFireEvent(&w, "DblClick", NULL, 0)); + TEST_EQ(sEv.events, 0); + + args[0] = wgtValueInt(1); + TEST_TRUE(wgtFireEvent(&w, "ItemOpen", args, 1)); + TEST_EQ(sEv.events, 1); + TEST_STR_EQ(sEv.eventName, "ItemOpen"); + TEST_EQ(sEv.eventArgc, 1); + TEST_EQ(args[0].i, WRITE_BACK); + + w.onEvent = evDestroying; + TEST_FALSE(wgtFireEvent(&w, "ItemOpen", NULL, 0)); + TEST_TRUE(wgtFireEvent(NULL, "Click", NULL, 0)); + TEST_TRUE(wgtFireEvent(&w, NULL, NULL, 0)); +} + + +// The static interface: creation by BASIC and type name with the +// createArgs, every property type both ways and as text, and the method +// signatures including missing arguments and WGT_SIG_DYNAMIC. +static void testIfaceStatic(void) { + memset(&sProbe, 0, sizeof(sProbe)); + + WidgetT *w = wgtCreateByName(PROBE_BAS, NULL); + WgtValueT v; + WgtValueT args[EVT_ARG_COUNT]; + WgtValueT ret; + char text[TEXT_BUF]; + + TEST_PTR_NOT_NULL(w); + + if (!w) { + return; + } + + TEST_EQ(sProbe.createA, PROBE_ARG_A); + TEST_EQ(sProbe.createB, PROBE_ARG_B); + TEST_PTR_NULL(wgtCreateByName("unitNoSuchType", NULL)); + + v = wgtValueStr("hello"); + TEST_TRUE(wgtSetProp(w, "name", &v)); + TEST_TRUE(wgtGetProp(w, "Name", &v)); + TEST_EQ(v.type, WGT_IFACE_STRING); + TEST_STR_EQ(v.s, "hello"); + + v = wgtValueFloat(RATIO_SET + FLOAT_EPS); + TEST_TRUE(wgtSetProp(w, "Count", &v)); + TEST_EQ(sProbe.count, 2); + + v = wgtValueStr("True"); + TEST_TRUE(wgtSetProp(w, "On", &v)); + TEST_TRUE(sProbe.on); + TEST_TRUE(wgtGetProp(w, "On", &v)); + TEST_EQ(v.type, WGT_IFACE_BOOL); + TEST_EQ(v.i, 1); + + v = wgtValueFloat(RATIO_SET); + TEST_TRUE(wgtSetProp(w, "Ratio", &v)); + TEST_TRUE(wgtGetProp(w, "Ratio", &v)); + TEST_TRUE(fabs(v.f - RATIO_SET) < FLOAT_EPS); + + // A DOUBLE property keeps every digit, a FLOAT one only a float's + v = wgtValueDouble(PRECISE_SET); + TEST_TRUE(wgtSetProp(w, "Precise", &v)); + TEST_TRUE(sProbe.precise == PRECISE_SET); + TEST_TRUE(wgtGetProp(w, "Precise", &v)); + TEST_EQ(v.type, WGT_IFACE_DOUBLE); + TEST_TRUE(v.f == PRECISE_SET); + v = wgtValueStr("0.25"); + TEST_TRUE(wgtSetProp(w, "Precise", &v)); + TEST_TRUE(sProbe.precise == 0.25); + + // An enumerated property by name (any case) or by index + v = wgtValueStr("high"); + TEST_TRUE(wgtSetProp(w, "Mode", &v)); + TEST_EQ(sProbe.mode, MODE_HIGH); + v = wgtValueStr("Nope"); + TEST_FALSE(wgtSetProp(w, "Mode", &v)); + TEST_EQ(sProbe.mode, MODE_HIGH); + TEST_TRUE(wgtGetProp(w, "Mode", &v)); + TEST_EQ(v.type, WGT_IFACE_ENUM); + TEST_EQ(v.i, MODE_HIGH); + + // Read-only, write-only, unknown + TEST_FALSE(wgtSetProp(w, "ReadOnly", &v)); + TEST_FALSE(wgtGetProp(w, "WriteOnly", &v)); + TEST_FALSE(wgtGetProp(w, "NoSuchProp", &v)); + TEST_EQ(v.type, WGT_IFACE_NONE); + + // The text forms + const WgtPropDescT *mode = wgtIfaceFindProp(&sProbeIface, "Mode"); + const WgtPropDescT *on = wgtIfaceFindProp(&sProbeIface, "On"); + const WgtPropDescT *ratio = wgtIfaceFindProp(&sProbeIface, "Ratio"); + const WgtPropDescT *count = wgtIfaceFindProp(&sProbeIface, "Count"); + + TEST_TRUE(wgtIfaceGetPropText(&sProbeIface, w, mode, text, sizeof(text))); + TEST_STR_EQ(text, "High"); + TEST_TRUE(wgtIfaceSetPropText(&sProbeIface, w, on, "False")); + TEST_TRUE(wgtIfaceGetPropText(&sProbeIface, w, on, text, sizeof(text))); + TEST_STR_EQ(text, "False"); + TEST_TRUE(wgtIfaceSetPropText(&sProbeIface, w, on, "-1")); + TEST_TRUE(sProbe.on); + TEST_TRUE(wgtIfaceSetPropText(&sProbeIface, w, ratio, "0.5")); + TEST_TRUE(wgtIfaceGetPropText(&sProbeIface, w, ratio, text, sizeof(text))); + TEST_STR_EQ(text, "0.5"); + TEST_TRUE(wgtIfaceSetPropText(&sProbeIface, w, count, "-12")); + TEST_TRUE(wgtIfaceGetPropText(&sProbeIface, w, count, text, sizeof(text))); + TEST_STR_EQ(text, "-12"); + sProbe.mode = (int32_t)DVX_ARRAY_LEN(sModeNames); + TEST_FALSE(wgtIfaceGetPropText(&sProbeIface, w, mode, text, sizeof(text))); + TEST_FALSE(wgtIfaceSetPropText(&sProbeIface, w, mode, NULL)); + + // Methods: converted arguments, results typed, missing arguments refused + args[0] = wgtValueInt(SUM_A); + args[1] = wgtValueStr("2"); + args[2] = wgtValueFloat(SUM_C); + TEST_TRUE(wgtCallMethod(w, "sum3", args, EVT_ARG_COUNT, &ret)); + TEST_EQ(sProbe.sum, SUM_A + SUM_B + SUM_C); + TEST_EQ(ret.type, WGT_IFACE_NONE); + sProbe.sum = 0; + TEST_FALSE(wgtCallMethod(w, "Sum3", args, 2, &ret)); + TEST_EQ(sProbe.sum, 0); + + args[0] = wgtValueInt(1); + TEST_TRUE(wgtCallMethod(w, "Echo", args, 1, &ret)); + TEST_EQ(ret.type, WGT_IFACE_STRING); + TEST_STR_EQ(ret.s, "hello"); + args[0] = wgtValueInt(0); + TEST_TRUE(wgtCallMethod(w, "Echo", args, 1, &ret)); + TEST_STR_EQ(ret.s, ""); + TEST_FALSE(wgtCallMethod(w, "Echo", NULL, 0, &ret)); + TEST_STR_EQ(ret.s, ""); + + sProbe.count = SUM_C; + args[0] = wgtValueInt(SUM_C); + TEST_TRUE(wgtCallMethod(w, "Has", args, 1, &ret)); + TEST_EQ(ret.type, WGT_IFACE_BOOL); + TEST_EQ(ret.i, 1); + + args[0] = wgtValueStr("a"); + args[1] = wgtValueBool(true); + TEST_TRUE(wgtCallMethod(w, "Join", args, 2, NULL)); + TEST_STR_EQ(sProbe.joined, "a+True"); + + TEST_TRUE(wgtCallMethod(w, "Dyn", args, EVT_ARG_COUNT, &ret)); + TEST_EQ(sProbe.dynArgc, EVT_ARG_COUNT); + TEST_EQ(ret.type, WGT_IFACE_BOOL); + + TEST_FALSE(wgtCallMethod(w, "NoSuchMethod", args, 0, &ret)); + TEST_EQ(ret.type, WGT_IFACE_NONE); + + widgetAllocRollback(w); +} + + +// Interfaces bound to classes, name resolution, event / method lookup. +static void testIfaceTypeBinding(void) { + const char *name = NULL; + WidgetT w; + + memset(&w, 0, sizeof(w)); + w.type = sProbeType; + TEST_TRUE(wgtGetIfaceOf(&w, &name) == &sProbeIface); + TEST_STR_EQ(name, PROBE_NAME); + TEST_PTR_NULL(wgtGetIfaceOf(NULL, NULL)); + + TEST_STR_EQ(wgtResolveTypeName(PROBE_BAS), PROBE_NAME); + TEST_STR_EQ(wgtResolveTypeName(PROBE_NAME), PROBE_NAME); + TEST_STR_EQ(wgtResolveTypeName(DYN_NAME), DYN_NAME); + TEST_PTR_NULL(wgtResolveTypeName("unitNoSuchType")); + TEST_PTR_NULL(wgtResolveTypeName(NULL)); + + TEST_STR_EQ(wgtIfaceFindEvent(&sProbeIface, "itemopen")->params, "Index As Integer"); + TEST_PTR_NULL(wgtIfaceFindEvent(&sProbeIface, "Timer")->params); + TEST_PTR_NULL(wgtIfaceFindEvent(&sProbeIface, "Nope")); + TEST_PTR_NULL(wgtIfaceFindEvent(NULL, "Timer")); + TEST_TRUE(wgtIfaceFindMethod(&sProbeIface, "JOIN") == &sProbeMethods[3]); + TEST_PTR_NULL(wgtIfaceFindMethod(&sProbeIface, "Nope")); + + // An unregistered class loses its interface; a class taking the ID + // later does not inherit it. + static const WidgetClassT tmpClass = { .version = WGT_CLASS_VERSION }; + static const WgtIfaceT tmpIface = { .basName = "UnitTmpCtl" }; + int32_t tmpId = wgtRegisterClass(&tmpClass); + + wgtRegisterIface("unittmp", &tmpIface); + wgtIfaceSetType("unittmp", tmpId); + w.type = tmpId; + TEST_TRUE(wgtGetIfaceOf(&w, NULL) == &tmpIface); + wgtUnregisterClass(tmpId); + TEST_PTR_NULL(wgtGetIfaceOf(&w, NULL)); + wgtIfaceSetType("unitNoSuchIface", tmpId); + TEST_PTR_NULL(wgtGetIfaceOf(&w, NULL)); + + // An interface a program registered goes again with its owner + TEST_TRUE(wgtGetIface("unittmp") == &tmpIface); + wgtUnregisterIface("unittmp"); + TEST_PTR_NULL(wgtGetIface("unittmp")); + TEST_PTR_NULL(wgtResolveTypeName("UnitTmpCtl")); + wgtUnregisterIface("unittmp"); +} + + +// Coercions, the boolean text rule, the signature table. +static void testIfaceValues(void) { + WgtValueT v; + char buf[WGT_VALUE_TEXT_MAX]; + + v = wgtValueFloat(RATIO_SET); + TEST_EQ(wgtValueToInt(&v), 2); + v = wgtValueFloat(-RATIO_SET); + TEST_EQ(wgtValueToInt(&v), -2); + v = wgtValueFloat(BIG_DOUBLE); + TEST_EQ(wgtValueToInt(&v), INT32_MAX); + v = wgtValueFloat(-BIG_DOUBLE); + TEST_EQ(wgtValueToInt(&v), INT32_MIN); + v = wgtValueStr("42"); + TEST_EQ(wgtValueToInt(&v), WRITE_BACK); + TEST_TRUE(fabs(wgtValueToFloat(&v) - WRITE_BACK) < FLOAT_EPS); + TEST_FALSE(wgtValueToBool(&v)); + v = wgtValueBool(true); + TEST_EQ(wgtValueToInt(&v), 1); + TEST_STR_EQ(wgtValueToText(&v, buf, sizeof(buf)), "True"); + v.type = WGT_IFACE_NONE; + TEST_EQ(wgtValueToInt(&v), 0); + TEST_FALSE(wgtValueToBool(&v)); + TEST_TRUE(wgtValueToFloat(&v) == 0.0); + TEST_STR_EQ(wgtValueToText(&v, buf, sizeof(buf)), ""); + v = wgtValueInt(-7); + TEST_STR_EQ(wgtValueToText(&v, buf, sizeof(buf)), "-7"); + TEST_TRUE(wgtValueToBool(&v)); + v = wgtValueFloat(RATIO_SET); + TEST_STR_EQ(wgtValueToText(&v, buf, sizeof(buf)), "2.5"); + TEST_TRUE(wgtValueToBool(&v)); + v = wgtValueDouble(PRECISE_SET); + TEST_STR_EQ(wgtValueToText(&v, buf, sizeof(buf)), "0.1"); + TEST_EQ(wgtValueToInt(&v), 0); + TEST_TRUE(wgtValueToBool(&v)); + TEST_TRUE(wgtValueToFloat(&v) == PRECISE_SET); + v = wgtValueStr(NULL); + TEST_STR_EQ(v.s, ""); + v = wgtValueStr("-1"); + TEST_TRUE(wgtValueToBool(&v)); + + TEST_TRUE(wgtParseBool("TRUE", false)); + TEST_TRUE(wgtParseBool("-1", false)); + TEST_FALSE(wgtParseBool("false", true)); + TEST_FALSE(wgtParseBool("0", true)); + TEST_TRUE(wgtParseBool("maybe", true)); + TEST_FALSE(wgtParseBool(NULL, false)); + + for (int32_t sig = WGT_SIG_VOID; sig < WGT_SIG_DYNAMIC; sig++) { + const WgtSigInfoT *info = wgtSigInfo((uint8_t)sig); + + TEST_PTR_NOT_NULL(info); + TEST_TRUE(info && info->argc <= WGT_SIG_MAX_ARGS); + } + + TEST_PTR_NULL(wgtSigInfo(WGT_SIG_DYNAMIC)); + TEST_PTR_NULL(wgtSigInfo(SIG_BEYOND)); + TEST_EQ(wgtSigInfo(WGT_SIG_INT5)->argc, WGT_SIG_MAX_ARGS); + TEST_EQ(wgtSigInfo(WGT_SIG_RET_STR_INT)->retType, WGT_IFACE_STRING); + TEST_EQ(wgtSigInfo(WGT_SIG_STR_STR_BOOL)->retType, WGT_IFACE_INT); + TEST_EQ(wgtSigInfo(WGT_SIG_STR_STR_BOOL_BOOL)->argTypes[3], WGT_IFACE_BOOL); +} diff --git a/src/test/unit/testPrefs.c b/src/test/unit/testPrefs.c index 35e9e096..05a695ea 100644 --- a/src/test/unit/testPrefs.c +++ b/src/test/unit/testPrefs.c @@ -38,6 +38,17 @@ #define PREFS_BAK_NAME "prefs2.~in" #define PREFS_DIR_MODE 0700 #define PREFS_MISSING "no-such-dir/missing.ini" +#define PREFS_BAS_FILE "prefsbas.ini" +#define BAS_TRUE (-1) +#define BAS_FALSE 0 +#define BAS_BAD_HANDLE 99 +#define BAS_CHOICE_ACCEL 0 // prefs.bas PREFS_CHOICE_* +#define BAS_CHOICE_WALLPAPER 1 +#define BAS_CHOICE_WHEEL 2 +#define BAS_CHOICE_BAD 3 +#define VIDEO_TEST_W 800 +#define VIDEO_TEST_H 600 +#define VIDEO_TEST_BPP 16 #define PREFS_INT_BIG 2147483647 #define PREFS_INT_NEG (-123456) @@ -71,8 +82,40 @@ static const char sSeed[] = // Prototypes // ============================================================ +// The BASIC entry points (dvxPrefsBas.c, prefs.bas; BASIC finds them by +// name) +int32_t PrefsChoiceCount(int32_t table); +int32_t PrefsChoiceDefault(int32_t table); +int32_t PrefsChoiceIndex(int32_t table, const char *name); +int32_t PrefsChoiceIndexOfValue(int32_t table, int32_t value); +const char *PrefsChoiceLabel(int32_t table, int32_t index); +const char *PrefsChoiceName(int32_t table, int32_t index); +int32_t PrefsChoiceValue(int32_t table, int32_t index); +void PrefsClose(int32_t handle); +int32_t PrefsCreate(void); +const char *PrefsDvxIni(void); +const char *PrefsGet(int32_t handle, const char *section, const char *key, const char *defaultValue); +int32_t PrefsGetBool(int32_t handle, const char *section, const char *key, int32_t defaultValue); +int32_t PrefsGetChoice(int32_t handle, const char *section, const char *key, int32_t table); +int32_t PrefsGetChoiceFrom(int32_t handle, const char *section, const char *key, const char *names, const char *defaultName); +int32_t PrefsGetInt(int32_t handle, const char *section, const char *key, int32_t defaultValue); +const char *PrefsKeyName(int32_t handle, const char *section, int32_t index); +int32_t PrefsLoad(const char *path); +void PrefsRemove(int32_t handle, const char *section, const char *key); +int32_t PrefsSave(int32_t handle); +int32_t PrefsSaveAs(int32_t handle, const char *path); +void PrefsSet(int32_t handle, const char *section, const char *key, const char *value); +void PrefsSetBool(int32_t handle, const char *section, const char *key, int32_t value); +void PrefsSetInt(int32_t handle, const char *section, const char *key, int32_t value); +void PrefsSetVideoMode(int32_t handle, int32_t width, int32_t height, int32_t bpp); +int32_t PrefsVideoBpp(int32_t handle); +int32_t PrefsVideoHeight(int32_t handle); +int32_t PrefsVideoWidth(int32_t handle); + static char *readWhole(const char *path); static void testBackupRecovered(void); +static void testBasic(void); +static void testBasicChoices(void); static void testChoices(void); static void testCreateAndSave(void); static void testCrlfAndTopLevel(void); @@ -135,8 +178,172 @@ static void testBackupRecovered(void) { } +// The BASIC face: a new file from PrefsCreate and PrefsSaveAs, loading it +// back, changing it in place with PrefsSave, every getter's default, and +// bad handles doing nothing. +static void testBasic(void) { + char *text; + int32_t h; + int32_t loaded; + + unlink(PREFS_BAS_FILE); + h = PrefsCreate(); + TEST_NE(h, 0); + PrefsSet(h, "Book", "Name", "My BBS"); + PrefsSetInt(h, "Book", "Count", PREFS_INT_NEG); + PrefsSetBool(h, "Book", "Echo", BAS_TRUE); + PrefsSetBool(h, "Book", "Ask", BAS_FALSE); + PrefsSet(h, "Book", "Gone", "x"); + PrefsRemove(h, "Book", "Gone"); + TEST_EQ(PrefsSaveAs(h, PREFS_BAS_FILE), BAS_TRUE); + TEST_EQ(PrefsSaveAs(h, PREFS_MISSING), BAS_FALSE); + PrefsClose(h); + + text = readWhole(PREFS_BAS_FILE); + TEST_TRUE(text && strstr(text, "Name = My BBS") && strstr(text, "Echo = true") && strstr(text, "Ask = false") && !strstr(text, "Gone")); + free(text); + + loaded = PrefsLoad(PREFS_BAS_FILE); + TEST_NE(loaded, 0); + TEST_STR_EQ(PrefsGet(loaded, "book", "name", "?"), "My BBS"); + TEST_STR_EQ(PrefsGet(loaded, "Book", "Missing", "dflt"), "dflt"); + TEST_EQ(PrefsGetInt(loaded, "Book", "Count", 0), PREFS_INT_NEG); + TEST_EQ(PrefsGetInt(loaded, "Book", "Name", 7), 7); + TEST_EQ(PrefsGetBool(loaded, "Book", "Echo", BAS_FALSE), BAS_TRUE); + TEST_EQ(PrefsGetBool(loaded, "Book", "Ask", BAS_TRUE), BAS_FALSE); + TEST_EQ(PrefsGetBool(loaded, "Book", "Name", BAS_TRUE), BAS_TRUE); + PrefsSet(loaded, "Book", "Name", "Renamed"); + TEST_EQ(PrefsSave(loaded), BAS_TRUE); + PrefsClose(loaded); + text = readWhole(PREFS_BAS_FILE); + TEST_TRUE(text && strstr(text, "Name = Renamed")); + free(text); + + // A file that is not there reads as empty + loaded = PrefsLoad(PREFS_MISSING); + TEST_NE(loaded, 0); + TEST_STR_EQ(PrefsGet(loaded, "A", "B", "none"), "none"); + PrefsClose(loaded); + + // Bad handles + TEST_STR_EQ(PrefsGet(BAS_BAD_HANDLE, "A", "B", "d"), "d"); + TEST_STR_EQ(PrefsGet(0, "A", "B", NULL), ""); + TEST_EQ(PrefsGetInt(BAS_BAD_HANDLE, "A", "B", 3), 3); + TEST_EQ(PrefsGetBool(BAS_BAD_HANDLE, "A", "B", BAS_TRUE), BAS_TRUE); + TEST_EQ(PrefsSave(BAS_BAD_HANDLE), BAS_FALSE); + TEST_EQ(PrefsSaveAs(0, PREFS_BAS_FILE), BAS_FALSE); + PrefsSet(BAS_BAD_HANDLE, "A", "B", "c"); + PrefsSetInt(BAS_BAD_HANDLE, "A", "B", 1); + PrefsSetBool(BAS_BAD_HANDLE, "A", "B", BAS_TRUE); + PrefsRemove(BAS_BAD_HANDLE, "A", "B"); + PrefsClose(BAS_BAD_HANDLE); + PrefsClose(h); + unlink(PREFS_BAS_FILE); +} + + // The shared vocabularies map INI spellings to values, fall back to the // named default for missing or unknown values, and match case-insensitively. +// prefs.bas choice lists, PrefsGetChoice, PrefsDvxIni$, PrefsSetVideoMode +// and the prefsBasHandle bridge other libraries use. +static void testBasicChoices(void) { + TEST_EQ(PrefsChoiceCount(BAS_CHOICE_ACCEL), PREFS_ACCEL_CHOICE_COUNT); + TEST_EQ(PrefsChoiceCount(BAS_CHOICE_WALLPAPER), PREFS_WALLPAPER_CHOICE_COUNT); + TEST_EQ(PrefsChoiceCount(BAS_CHOICE_WHEEL), PREFS_WHEEL_DIR_CHOICE_COUNT); + TEST_EQ(PrefsChoiceCount(BAS_CHOICE_BAD), 0); + TEST_EQ(PrefsChoiceCount(-1), 0); + + for (int32_t i = 0; i < PREFS_ACCEL_CHOICE_COUNT; i++) { + TEST_STR_EQ(PrefsChoiceLabel(BAS_CHOICE_ACCEL, i), prefsAccelChoices[i].label); + TEST_STR_EQ(PrefsChoiceName(BAS_CHOICE_ACCEL, i), prefsAccelChoices[i].iniName); + TEST_EQ(PrefsChoiceValue(BAS_CHOICE_ACCEL, i), prefsAccelChoices[i].value); + TEST_EQ(PrefsChoiceIndexOfValue(BAS_CHOICE_ACCEL, prefsAccelChoices[i].value), i); + } + + TEST_STR_EQ(PrefsChoiceLabel(BAS_CHOICE_WALLPAPER, WallpaperTileE), "Tile"); + TEST_STR_EQ(PrefsChoiceName(BAS_CHOICE_WHEEL, 1), "reversed"); + TEST_EQ(PrefsChoiceValue(BAS_CHOICE_WHEEL, 1), MOUSE_WHEEL_DIR_REVERSED); + TEST_STR_EQ(PrefsChoiceLabel(BAS_CHOICE_WHEEL, PREFS_WHEEL_DIR_CHOICE_COUNT), ""); + TEST_STR_EQ(PrefsChoiceName(BAS_CHOICE_BAD, 0), ""); + TEST_EQ(PrefsChoiceValue(BAS_CHOICE_ACCEL, -1), 0); + TEST_EQ(PrefsChoiceIndex(BAS_CHOICE_WALLPAPER, "CENTER"), WallpaperCenterE); + TEST_EQ(PrefsChoiceIndex(BAS_CHOICE_WALLPAPER, "nope"), -1); + TEST_EQ(PrefsChoiceIndex(BAS_CHOICE_BAD, "tile"), -1); + TEST_EQ(PrefsChoiceIndexOfValue(BAS_CHOICE_WHEEL, 0), -1); + TEST_EQ(PrefsChoiceIndexOfValue(BAS_CHOICE_BAD, 0), -1); + + // Each table's default is the choice its C default names + TEST_EQ(PrefsChoiceDefault(BAS_CHOICE_ACCEL), prefsChoiceIndex(prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT)); + TEST_EQ(PrefsChoiceDefault(BAS_CHOICE_WALLPAPER), prefsChoiceIndex(prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)); + TEST_EQ(PrefsChoiceDefault(BAS_CHOICE_WHEEL), prefsChoiceIndex(prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, MOUSE_WHEEL_DIR_DEFAULT)); + TEST_TRUE(PrefsChoiceDefault(BAS_CHOICE_ACCEL) >= 0); + TEST_EQ(PrefsChoiceDefault(BAS_CHOICE_BAD), -1); + TEST_EQ(PrefsChoiceDefault(-1), -1); + + // A stored name, an unknown one and a missing key (the default) + int32_t h = PrefsCreate(); + + PrefsSet(h, "mouse", "acceleration", "High"); + PrefsSet(h, "mouse", "wheel", "sideways"); + TEST_EQ(PrefsGetChoice(h, "mouse", "acceleration", BAS_CHOICE_ACCEL), 3); + TEST_EQ(PrefsGetChoice(h, "mouse", "wheel", BAS_CHOICE_WHEEL), prefsChoiceIndex(prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, MOUSE_WHEEL_DIR_DEFAULT)); + TEST_EQ(PrefsGetChoice(h, "desktop", "mode", BAS_CHOICE_WALLPAPER), prefsChoiceIndex(prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)); + TEST_EQ(PrefsGetChoice(BAS_BAD_HANDLE, "mouse", "acceleration", BAS_CHOICE_ACCEL), prefsChoiceIndex(prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT)); + TEST_EQ(PrefsGetChoice(h, "mouse", "acceleration", BAS_CHOICE_BAD), -1); + + // A program's own list (Program Manager's view names) + PrefsSet(h, "options", "view", "SMALL"); + PrefsSet(h, "options", "odd", "huge"); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "view", "large|small|list", "large"), 1); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "odd", "large|small|list", "list"), 2); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "missing", "large|small|list", "small"), 1); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "missing", "large|small|list", "none"), -1); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "view", "", "small"), -1); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "view", NULL, "small"), -1); + TEST_EQ(PrefsGetChoiceFrom(h, "options", "view", "small", "small"), 0); + TEST_EQ(PrefsGetChoiceFrom(BAS_BAD_HANDLE, "options", "view", "a||small", "small"), 2); + + // The bridge: the same settings through the C API + PrefsHandleT *ph = prefsBasHandle(h); + + TEST_TRUE(ph != NULL); + TEST_STR_EQ(prefsGetString(ph, "mouse", "acceleration", ""), "High"); + TEST_TRUE(prefsBasHandle(0) == NULL); + TEST_TRUE(prefsBasHandle(BAS_BAD_HANDLE) == NULL); + TEST_TRUE(prefsBasHandle(-1) == NULL); + + int32_t w = 0; + int32_t ht = 0; + int32_t bpp = 0; + + TEST_EQ(PrefsVideoWidth(h), DVX_DEFAULT_VIDEO_W); + TEST_EQ(PrefsVideoHeight(h), DVX_DEFAULT_VIDEO_H); + TEST_EQ(PrefsVideoBpp(h), DVX_DEFAULT_VIDEO_BPP); + PrefsSetVideoMode(h, VIDEO_TEST_W, VIDEO_TEST_H, VIDEO_TEST_BPP); + prefsGetVideoMode(ph, &w, &ht, &bpp); + TEST_EQ(w, VIDEO_TEST_W); + TEST_EQ(ht, VIDEO_TEST_H); + TEST_EQ(bpp, VIDEO_TEST_BPP); + TEST_EQ(PrefsVideoWidth(h), VIDEO_TEST_W); + TEST_EQ(PrefsVideoHeight(h), VIDEO_TEST_H); + TEST_EQ(PrefsVideoBpp(h), VIDEO_TEST_BPP); + TEST_EQ(PrefsVideoWidth(BAS_BAD_HANDLE), DVX_DEFAULT_VIDEO_W); + + // Key names in file order, "" past the last and for a bad handle + TEST_STR_EQ(PrefsKeyName(h, "mouse", 0), "acceleration"); + TEST_STR_EQ(PrefsKeyName(h, "MOUSE", 1), "wheel"); + TEST_STR_EQ(PrefsKeyName(h, "mouse", 2), ""); + TEST_STR_EQ(PrefsKeyName(h, "mouse", -1), ""); + TEST_STR_EQ(PrefsKeyName(h, "nosuch", 0), ""); + TEST_STR_EQ(PrefsKeyName(BAS_BAD_HANDLE, "mouse", 0), ""); + PrefsSetVideoMode(BAS_BAD_HANDLE, 1, 1, 1); + PrefsClose(h); + TEST_TRUE(prefsBasHandle(h) == NULL); + + TEST_STR_EQ(PrefsDvxIni(), DVX_INI_PATH); +} + + static void testChoices(void) { PrefsHandleT *h = prefsCreate(); @@ -415,6 +622,8 @@ void testPrefs(void) { testUnreadableKept(); testTempNameIs83(); testBackupRecovered(); + testBasic(); + testBasicChoices(); } diff --git a/src/test/unit/testVideo.c b/src/test/unit/testVideo.c index 94a1713d..b5ffed2f 100644 --- a/src/test/unit/testVideo.c +++ b/src/test/unit/testVideo.c @@ -40,6 +40,14 @@ #define RGB565_G_DROP 2 #define RGB565_B_DROP 3 +// unpackColor widens a 565 field v back to 8 bits as +// (v << DROP) | (v >> REPEAT): its top bits repeated below it. +#define RGB565_R_REPEAT 2 +#define RGB565_G_REPEAT 4 +#define RGB565_B_REPEAT 2 +#define RGB565_R_HALF 0x10u // 5-bit field value 16 (unpacks to 132) +#define RGB565_HALF_8 132 + // 888 layout for 32 bpp. #define RGB888_R_SHIFT 16 #define RGB888_G_SHIFT 8 @@ -123,14 +131,17 @@ static void testPack16(void) { TEST_EQ(packColor(&d, 7, 3, 7), 0); TEST_EQ(packColor(&d, 8, 4, 8), (1u << RGB565_R_SHIFT) | (1u << RGB565_G_SHIFT) | 1u); - // Full sweep against a bit-exact 565 reference, and unpack restores - // the truncated value shifted back with zero low bits. + // Full sweep against a bit-exact 565 reference, and unpack widens the + // truncated value back with its top bits repeated in the low ones. bool packOk = true; bool unpackOk = true; for (int32_t v = 0; v < 256 && packOk && unpackOk; v++) { uint8_t c8 = (uint8_t)v; - uint32_t ref = ((uint32_t)(c8 >> RGB565_R_DROP) << RGB565_R_SHIFT) | ((uint32_t)(c8 >> RGB565_G_DROP) << RGB565_G_SHIFT) | (uint32_t)(c8 >> RGB565_B_DROP); + uint8_t rv = (uint8_t)(c8 >> RGB565_R_DROP); + uint8_t gv = (uint8_t)(c8 >> RGB565_G_DROP); + uint8_t bv = (uint8_t)(c8 >> RGB565_B_DROP); + uint32_t ref = ((uint32_t)rv << RGB565_R_SHIFT) | ((uint32_t)gv << RGB565_G_SHIFT) | (uint32_t)bv; uint32_t got = packColor(&d, c8, c8, c8); uint8_t r; uint8_t g; @@ -138,7 +149,7 @@ static void testPack16(void) { packOk = (got == ref); unpackColor(&d, got, &r, &g, &b); - unpackOk = (r == (c8 & ~((1 << RGB565_R_DROP) - 1))) && (g == (c8 & ~((1 << RGB565_G_DROP) - 1))) && (b == (c8 & ~((1 << RGB565_B_DROP) - 1))); + unpackOk = (r == (uint8_t)((rv << RGB565_R_DROP) | (rv >> RGB565_R_REPEAT))) && (g == (uint8_t)((gv << RGB565_G_DROP) | (gv >> RGB565_G_REPEAT))) && (b == (uint8_t)((bv << RGB565_B_DROP) | (bv >> RGB565_B_REPEAT))); } TEST_TRUE(packOk); @@ -154,6 +165,25 @@ static void testPack16(void) { TEST_EQ(g, 0); TEST_EQ(b, 0); + // White and black read back exactly (BASIC Point = RGB(255, 255, 255) + // after Cls); a half field repeats its top bit. + unpackColor(&d, 0xFFFF, &r, &g, &b); + TEST_EQ(r, 255); + TEST_EQ(g, 255); + TEST_EQ(b, 255); + + unpackColor(&d, RGB565_R_HALF << RGB565_R_SHIFT, &r, &g, &b); + TEST_EQ(r, RGB565_HALF_8); + TEST_EQ(g, 0); + + // A field the BIOS reports as 0 bits wide reads as 0 (no endless + // widening loop). + d.format.redBits = 0; + d.format.redMask = 0; + unpackColor(&d, 0xFFFF, &r, &g, &b); + TEST_EQ(r, 0); + TEST_EQ(g, 255); + unitDisplayDestroy(&d); } diff --git a/src/test/unit/testWm.c b/src/test/unit/testWm.c index 3e614f8a..e390629f 100644 --- a/src/test/unit/testWm.c +++ b/src/test/unit/testWm.c @@ -89,6 +89,13 @@ #define POPUP_SB_TOTAL 20 #define POPUP_SB_MID 3 +// testScrollbarAt: a window scrollbar's range [0, BAR_AT_MAX] a page of +// BAR_AT_PAGE at BAR_AT_VALUE = the standalone bar's BAR_AT_MAX + +// BAR_AT_PAGE items, BAR_AT_PAGE shown from BAR_AT_VALUE. +#define BAR_AT_MAX 60 +#define BAR_AT_PAGE 20 +#define BAR_AT_VALUE 25 + #define WM_BPP_8 8 typedef struct { @@ -119,6 +126,7 @@ static void testHitTest(void); static void testMenus(void); static void testMinSize(void); static void testResize(void); +static void testScrollbarAt(void); static void testScrollbars(void); static void testShortScrollbar(void); static void testStack(void); @@ -602,6 +610,17 @@ static void testMenus(void) { TEST_EQ(file->items[3].type, MenuItemCheckE); TEST_TRUE(file->items[3].checked); + // A new label brings its own accelerator; the item stays what it was. + wmMenuItemSetLabel(&file->items[1], "Sa&ve As\tF12"); + TEST_STR_EQ(file->items[1].label, "Sa&ve As\tF12"); + TEST_EQ(file->items[1].accelKey, 'v'); + TEST_EQ(file->items[1].id, MENU_ID_SAVE); + wmMenuItemSetLabel(&file->items[1], "&Save"); + TEST_EQ(file->items[1].accelKey, 's'); + wmMenuItemSetLabel(NULL, "x"); + wmMenuItemSetLabel(&file->items[1], NULL); + TEST_STR_EQ(file->items[1].label, "&Save"); + // Find, in-menu and through the bar. TEST_TRUE(wmMenuFindItemInMenu(file, MENU_ID_SAVE) == &file->items[1]); TEST_PTR_NULL(wmMenuFindItemInMenu(file, MENU_ID_MISSING)); @@ -925,6 +944,58 @@ static void testResize(void) { } +// The standalone bar of either orientation (wmDrawScrollbarAt) paints the +// same pixels as a window scrollbar of that orientation at the same place +// in the same state; the two orientations differ; wmDrawVScrollbarAt is +// the vertical case. +static void testScrollbarAt(void) { + WmFixtureT f; + BlitOpsT ops; + ColorSchemeT cs; + ScrollbarOrientE orients[] = { ScrollbarVerticalE, ScrollbarHorizontalE }; + + fixtureInit(&f); + drawInit(&ops, &f.d); + chromeColors(&cs); + + WindowT *win = wmCreateWindow(&f.stack, &f.d, "Bars", WIN_X, WIN_Y, WIN_W, WIN_H, false); + size_t bytes = (size_t)f.d.pitch * f.d.height; + uint8_t *ref = (uint8_t *)malloc(bytes); + RectT all = { 0, 0, WM_SCREEN_W, WM_SCREEN_H }; + + for (int32_t i = 0; i < DVX_ARRAY_LEN(orients); i++) { + bool vertical = orients[i] == ScrollbarVerticalE; + ScrollbarOrientE other = vertical ? ScrollbarHorizontalE : ScrollbarVerticalE; + ScrollbarT *sb; + + wmRemoveScrollbars(win); + sb = vertical ? wmAddVScrollbar(win, 0, BAR_AT_MAX, BAR_AT_PAGE) : wmAddHScrollbar(win, 0, BAR_AT_MAX, BAR_AT_PAGE); + sb->value = BAR_AT_VALUE; + memset(f.d.backBuf, GARBAGE_A, bytes); + wmDrawScrollbars(&f.d, &ops, &cs, win, &all); + memcpy(ref, f.d.backBuf, bytes); + memset(f.d.backBuf, GARBAGE_A, bytes); + wmDrawScrollbarAt(&f.d, &ops, &cs, orients[i], win->x + sb->x, win->y + sb->y, sb->length, BAR_AT_VALUE, BAR_AT_PAGE, BAR_AT_MAX + BAR_AT_PAGE); + TEST_MEM_EQ(ref, f.d.backBuf, bytes); + + // The other orientation in the same spot is a different picture. + memset(f.d.backBuf, GARBAGE_A, bytes); + wmDrawScrollbarAt(&f.d, &ops, &cs, other, win->x + sb->x, win->y + sb->y, sb->length, BAR_AT_VALUE, BAR_AT_PAGE, BAR_AT_MAX + BAR_AT_PAGE); + TEST_TRUE(memcmp(ref, f.d.backBuf, bytes) != 0); + } + + memset(f.d.backBuf, GARBAGE_A, bytes); + wmDrawVScrollbarAt(&f.d, &ops, &cs, WIN_X, WIN_Y, POPUP_SB_H, POPUP_SB_MID, POPUP_SB_VISIBLE, POPUP_SB_TOTAL); + memcpy(ref, f.d.backBuf, bytes); + memset(f.d.backBuf, GARBAGE_A, bytes); + wmDrawScrollbarAt(&f.d, &ops, &cs, ScrollbarVerticalE, WIN_X, WIN_Y, POPUP_SB_H, POPUP_SB_MID, POPUP_SB_VISIBLE, POPUP_SB_TOTAL); + TEST_MEM_EQ(ref, f.d.backBuf, bytes); + + free(ref); + fixtureDestroy(&f); +} + + static void testScrollbars(void) { WmFixtureT f; int32_t thumbPos; @@ -1246,6 +1317,7 @@ void testWm(void) { testDrag(); testHeldStill(); testScrollbars(); + testScrollbarAt(); testShortScrollbar(); testMenus(); testChromeCoverage(); diff --git a/src/test/unit/unitMain.c b/src/test/unit/unitMain.c index 2bbc5029..4d1af8ee 100644 --- a/src/test/unit/unitMain.c +++ b/src/test/unit/unitMain.c @@ -72,6 +72,7 @@ static const UnitSectionT sSections[] = { { "comp", testComp }, { "draw", testDraw }, { "event", testEvent }, + { "iface", testIface }, { "layout", testLayout }, { "mem", testMem }, { "platform", testPlatform }, diff --git a/src/test/unit/unitTest.h b/src/test/unit/unitTest.h index 775d2b2b..d088b0c9 100644 --- a/src/test/unit/unitTest.h +++ b/src/test/unit/unitTest.h @@ -72,6 +72,7 @@ void testClock(void); void testComp(void); void testDraw(void); void testEvent(void); +void testIface(void); void testLayout(void); void testMem(void); void testPlatform(void); diff --git a/src/test/widgets/Makefile b/src/test/widgets/Makefile index db53157f..f06b30bc 100644 --- a/src/test/widgets/Makefile +++ b/src/test/widgets/Makefile @@ -32,7 +32,7 @@ include ../dvxtest/dvxtest.mk BINDIR = $(DVXTEST_BINDIR)/widgets TARGET = $(BINDIR)/widgets -SRCS = wtCommon.c testData.c testProps.c testInput.c testLists.c testRange.c testMisc.c testFocus.c testDestroy.c testHover.c testCovListView.c testCovTreeView.c testCovTerm.c testCovPopups.c testCovMisc.c testCovText.c widgetsMain.c +SRCS = wtCommon.c testData.c testProps.c testInput.c testLists.c testRange.c testMisc.c testFocus.c testDestroy.c testHover.c testGeneric.c testCovListView.c testCovTreeView.c testCovTerm.c testCovPopups.c testCovMisc.c testCovText.c testHelpView.c widgetsMain.c OBJS = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS)) .PHONY: all run clean @@ -58,7 +58,7 @@ $(BINDIR)/cov%: covMain.c $(BINDIR)/wtCommon.o $(BINDIR)/testCov%.o $(DVXTEST_AR -include $(OBJS:.o=.d) -run: $(TARGET) +run: $(TARGET) $(DVXTEST_HELP_HLP) cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./widgets clean: diff --git a/src/test/widgets/testCovListView.c b/src/test/widgets/testCovListView.c index 04de793d..f6a1860e 100644 --- a/src/test/widgets/testCovListView.c +++ b/src/test/widgets/testCovListView.c @@ -885,14 +885,21 @@ static void caseSortHeader(void) { setup(lv, cols, 2, cells, LV_ROW_COUNT); wgtListViewSetHeaderClickCallback(lv, onHeader); + lv->onEvent = wtOnEvent; sHeaderHits = 0; + wtEventSpyReset(); wtSettle(); // First click: ascending; display row 0 is "Bravo" (strcmp order). + // The callback and then ColumnClick (Column, 0-based) through onEvent. wtClickAt(f.win, lv->x + LV_PAD, headerY(lv)); TEST_EQ(sHeaderHits, 1); TEST_EQ(sHeaderCol, 0); TEST_EQ(sHeaderDir, ListViewSortAscE); + TEST_EQ(wtEventSpy()->count, 1); + TEST_STR_EQ(wtEventSpy()->name, "ColumnClick"); + TEST_EQ(wtEventSpy()->argc, 1); + TEST_EQ(wtEventSpy()->arg0, 0); wtSettle(); wtClickAt(f.win, lv->x + LV_PAD, rowY(lv, 0)); TEST_EQ(wgtListViewGetSelected(lv), 3); diff --git a/src/test/widgets/testCovMisc.c b/src/test/widgets/testCovMisc.c index b5731b8f..bc2c1fc2 100644 --- a/src/test/widgets/testCovMisc.c +++ b/src/test/widgets/testCovMisc.c @@ -81,7 +81,7 @@ #define CM_FLIP_LABELS 20 #define CM_TAB_PAGES 3 #define CM_TAB_FAR 10 -#define CM_TAB_NEGATIVE -5 +#define CM_TAB_NEGATIVE (-5) #define CM_TIMER_REARMS 3 #define CM_PATH_MAX 512 @@ -129,7 +129,7 @@ static int32_t sCount = 0; static void caseBoxFrameTitle(void) { - tCase("frame: groove border and title paint"); + tCase("frame: groove border, title paint, title get and set"); WtFixtureT f = wtFixture("Frame"); WidgetT *fr = wgtFrame(f.root, "&Group"); @@ -138,6 +138,7 @@ static void caseBoxFrameTitle(void) { wtSettle(); TEST_EQ(widgetFrameBorderWidth(fr), 2); TEST_TRUE(fr->h > cmCharH()); + TEST_STR_EQ(wgtGetText(fr), "&Group"); // An untitled frame skips the title paint. WidgetT *plain = wgtFrame(f.root, ""); @@ -145,6 +146,13 @@ static void caseBoxFrameTitle(void) { wgtLabel(plain, "body"); wtSettle(); TEST_TRUE(plain->h > 0); + + // A title set later (a BASIC Caption, a .frm Caption line) is the + // frame's, accelerator included. + wgtSetText(plain, "&Late"); + wtSettle(); + TEST_STR_EQ(wgtGetText(plain), "&Late"); + TEST_EQ(plain->accelKey, 'l'); tShutdown(); } diff --git a/src/test/widgets/testCovTerm.c b/src/test/widgets/testCovTerm.c index 55dd9e25..e8aa5546 100644 --- a/src/test/widgets/testCovTerm.c +++ b/src/test/widgets/testCovTerm.c @@ -37,6 +37,9 @@ #define TERM_COLS 20 #define TERM_ROWS 5 +#define TERM_NEW_COLS_STR "30" // a Cols value other than TERM_COLS +#define TERM_NEW_ROWS_STR "7" // a Rows value other than TERM_ROWS +#define TERM_LIVE_LEN 4 // strlen("live") #define TERM_SB_LINES 10 #define TERM_SB_W 14 #define TERM_BORDER 2 @@ -69,6 +72,7 @@ typedef struct { static void caseTermCursorOps(void); static void caseTermExtensions(void); static void caseTermRegressions(void); +static void caseTermResizeHistory(void); static void caseTermScrollbackKeys(void); static void caseTermScrollRegion(void); static void caseTermSelection(void); @@ -147,6 +151,16 @@ static void caseTermCursorOps(void) { // parse and land back in normal state. feedTerm(&comm, term, "\f" "abcd\x1b[1;2H\x1b[J\x1b[1J\x1b[1K\x1b[2K"); expectCursor(&comm, term, 1, 2); + + // A new size (BASIC Cols) starts like a new terminal: the host's + // scroll region and origin mode are gone (home is row 1) and wrap is + // on again. + feedTerm(&comm, term, "\x1b[?7l\x1b[2;4r\x1b[?6h"); + TEST_TRUE(wtPropApply(term, wgtIfaceFindProp(wgtGetIface("ansiterm"), "Cols"), TERM_NEW_COLS_STR)); + feedTerm(&comm, term, "\x1b[H"); + expectCursor(&comm, term, 1, 1); + feedTerm(&comm, term, "\x1b[1;" TERM_NEW_COLS_STR "Hxy"); + expectCursor(&comm, term, 2, 2); wtSettle(); tShutdown(); } @@ -312,6 +326,49 @@ static void caseTermRegressions(void) { } +// A new size starts with an empty history: neither the old screen (old +// width) nor the new, not yet blanked, cell buffer reaches the scrollback. +// Shift+PgUp then has nothing to page to, so row 0 still shows the live +// text. (The new buffer used to be pushed before it was blanked: garbage +// rows in the history, and an uninitialized read under MSan.) +static void caseTermResizeHistory(void) { + tCase("ansiTerm: Rows / Cols start with an empty scrollback"); + + WtFixtureT f = wtFixture("TermRsz"); + WidgetT *term = wgtAnsiTerm(f.root, TERM_COLS, TERM_ROWS); + FakeCommT comm; + + memset(&comm, 0, sizeof(comm)); + wgtAnsiTermSetComm(term, &comm, fakeRead, fakeWrite); + wgtAnsiTermSetScrollback(term, TERM_SB_LINES); + wtSettle(); + + for (int32_t i = 0; i < TERM_PUSH_LINES; i++) { + char line[TERM_EXPECT_MAX]; + + snprintf(line, sizeof(line), "line %d\r\n", (int)i); + feedTerm(&comm, term, line); + } + + feedTerm(&comm, term, "old screen"); + TEST_TRUE(wtPropApply(term, wgtIfaceFindProp(wgtGetIface("ansiterm"), "Rows"), TERM_NEW_ROWS_STR)); + wtSettle(); + feedTerm(&comm, term, "\x1b[1;1H" "live"); + expectCursor(&comm, term, 1, TERM_LIVE_LEN + 1); + + wtClickAt(f.win, cellX(term, 0), cellY(term, 0)); + TEST_PTR_EQ(wgtGetFocused(), term); + comm.sentLen = 0; + tWidgetKey(f.win, KEY_PGUP, KEY_MOD_SHIFT); + TEST_EQ(comm.sentLen, 0); + wtDrag(f.win, cellX(term, 0), cellY(term, 0), cellX(term, TERM_COLS - 1), cellY(term, 0)); + tWidgetKey(f.win, KEY_CTRL_C, KEY_MOD_CTRL); + TEST_TRUE(clipIs("live")); + wtSettle(); + tShutdown(); +} + + static void caseTermScrollbackKeys(void) { tCase("ansiTerm: scrollbar paging, remaining key encodings"); @@ -628,6 +685,7 @@ void wtSectionCovTerm(void) { caseTermCursorOps(); caseTermExtensions(); caseTermRegressions(); + caseTermResizeHistory(); caseTermScrollRegion(); caseTermScrollbackKeys(); caseTermSelection(); diff --git a/src/test/widgets/testCovText.c b/src/test/widgets/testCovText.c index ec9cde68..c4b56bbc 100644 --- a/src/test/widgets/testCovText.c +++ b/src/test/widgets/testCovText.c @@ -30,6 +30,7 @@ #include "listHelp.h" #include "comboBox/comboBox.h" #include "dropdown/dropdown.h" +#include "image/image.h" #include "spinner/spinner.h" #include "textInput/textInpt.h" @@ -39,6 +40,7 @@ #define COV_TI_MAX 128 #define COV_AREA_MAX 4096 +#define COV_AREA_DEFAULT_MAX 65536 // TEXTAREA_DEFAULT_BUF: MaxLength = 0 #define COV_SMALL_MAX 8 #define COV_CLICK_GAP_MS 60 // < double-click window, so clicks chain #define COV_TA_BORDER 2 // TEXTAREA_BORDER (widgetTextInput.c) @@ -46,10 +48,14 @@ #define COV_TA_SB_W 14 // TEXTAREA_SB_W #define COV_SPIN_BTN_W 14 // SPINNER_BTN_W (widgetSpinner.c) #define COV_SPIN_BORDER 2 // SPINNER_BORDER +#define COV_COLORIZE_MAX 64 // covColorizeEvent: the longest line it colours #define COV_SPIN_MAXW 72 // narrow spinner so the text scrolls #define COV_COMBO_BTN_W 16 // DROPDOWN_BTN_WIDTH (comboBox drop button) #define COV_POP_ITEMS 12 // > DROPDOWN_MAX_VISIBLE so popups scroll #define COV_LONG_EXTRA 12 // characters past the visible field width +#define COV_MARK_LINE 2 // the marked line of the selection golden +#define COV_MARK_GUTTER_RGB 0xFF0000 // its gutter dot +#define COV_MARK_BACK_RGB 0x303030 // its background #define COV_KW_RGB 0xFF0000 // custom keyword color for the keyword-list case #define COV_EDGE_COLS 30 // visible columns of the exact-fit text area #define COV_EDGE_EXTRA 10 // line length past the visible columns @@ -58,6 +64,12 @@ typedef void (*CovStrMethodFnT)(WidgetT *w, const char *arg); typedef const char *(*CovRetStrMethodFnT)(const WidgetT *w); +typedef void (*CovVoidMethodFnT)(WidgetT *w); +typedef const char *(*CovRetStrIntMethodFnT)(const WidgetT *w, int32_t n); +typedef int32_t (*CovRetIntIntMethodFnT)(const WidgetT *w, int32_t n); +typedef void (*CovIntIntMethodFnT)(WidgetT *w, int32_t a, int32_t b); +typedef void (*CovInt3MethodFnT)(WidgetT *w, int32_t a, int32_t b, int32_t c); +typedef bool (*CovReplSelMethodFnT)(WidgetT *w, const char *needle, const char *repl, bool caseSens, bool forward); // One destroy-in-onChange scenario: which widget kind, the initial text, // up to three preparation keys, and the keystroke whose Change fires the @@ -78,9 +90,11 @@ typedef struct { // Prototypes // ============================================================ +static void caseAreaBasicMembers(void); static void caseAreaCacheLibrary(void); static void caseAreaCaretAtEdge(void); static void caseAreaColorize(void); +static void caseAreaColorizeEvent(void); static void caseAreaEditing(void); static void caseAreaKeywordList(void); static void caseAreaMouse(void); @@ -90,29 +104,210 @@ static void caseAreaSearch(void); static void caseChangeDestroys(void); static void caseDropdownPopup(void); static void caseEditRegressions(void); +static void caseImageMask(void); +static void caseInputBasicMembers(void); +static void caseInputMask(void); static void caseInputMouse(void); static void caseInputSelection(void); static void caseMaskedInput(void); +static void caseSpinnerReal(void); static void caseSpinnerText(void); static void covAreaClick(WindowT *win, WidgetT *ta, int32_t gutterW, int32_t row, int32_t col); static bool covClipIs(const char *expect); -static uint32_t covDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); +static void covColorizeEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); static void covDestroyOnChange(WidgetT *w); static void *covFindMethod(const char *ifaceName, const char *methodName); +static const WgtPropDescT *covFindProp(const char *ifaceName, const char *propName); +static int32_t covGetInt(WidgetT *w, const char *ifaceName, const char *propName); +static const char *covGetStr(WidgetT *w, const char *ifaceName, const char *propName); static void covOnChange(WidgetT *w); static void covOnGutter(WidgetT *w, int32_t line); static int32_t covPaintCountRgb(WidgetT *w, uint32_t rgb); static bool covPaintHasRgb(WidgetT *w, uint32_t rgb); +static void covSetInt(WidgetT *w, const char *ifaceName, const char *propName, int32_t v); +static void covSetStr(WidgetT *w, const char *ifaceName, const char *propName, const char *v); static void covType(WindowT *win, const char *text); -static int32_t sCovChanges = 0; -static int32_t sCovGutterLine = -1; +static int32_t sCovChanges = 0; +static int32_t sCovColorizeLines = 0; +static int32_t sCovGutterLine = -1; // Direct texthelp library calls: the line-cache fallback paths a widget // never reaches (it always keeps the cache warm or dirties it wholesale), // plus the scrollbar helper edge cases. +// The BASIC members a code editor needs: CursorCol, LineCount, GetLine, +// SelStart / SelLength / SelText, Cut / Copy / Paste / SelectAll / Undo, +// ReplaceSelection, CursorToEnd, MaxLength, syntax colours by index and +// line markers (painted in the gutter and as the line background). +static void caseAreaBasicMembers(void) { + tCase("textArea: BASIC editor members (selection, lines, clipboard, markers, colours)"); + + WtFixtureT f = wtFixture("CovBasArea"); + WidgetT *ta = wgtTextArea(f.root, COV_AREA_MAX); + CovRetStrIntMethodFnT getLine = (CovRetStrIntMethodFnT)covFindMethod("textarea", "GetLine"); + CovVoidMethodFnT cut = (CovVoidMethodFnT)covFindMethod("textarea", "Cut"); + CovVoidMethodFnT copy = (CovVoidMethodFnT)covFindMethod("textarea", "Copy"); + CovVoidMethodFnT paste = (CovVoidMethodFnT)covFindMethod("textarea", "Paste"); + CovVoidMethodFnT selAll = (CovVoidMethodFnT)covFindMethod("textarea", "SelectAll"); + CovVoidMethodFnT undo = (CovVoidMethodFnT)covFindMethod("textarea", "Undo"); + CovVoidMethodFnT toEnd = (CovVoidMethodFnT)covFindMethod("textarea", "CursorToEnd"); + CovVoidMethodFnT clrMarks = (CovVoidMethodFnT)covFindMethod("textarea", "ClearLineMarkers"); + CovInt3MethodFnT setMark = (CovInt3MethodFnT)covFindMethod("textarea", "SetLineMarker"); + CovIntIntMethodFnT setColor = (CovIntIntMethodFnT)covFindMethod("textarea", "SetSyntaxColor"); + CovRetIntIntMethodFnT getColor = (CovRetIntIntMethodFnT)covFindMethod("textarea", "GetSyntaxColor"); + CovReplSelMethodFnT replSel = (CovReplSelMethodFnT)covFindMethod("textarea", "ReplaceSelection"); + CovStrMethodFnT setMode = (CovStrMethodFnT)covFindMethod("textarea", "SetSyntaxMode"); + const WgtIfaceT *iface = wgtGetIface("textarea"); + + TEST_TRUE(getLine && cut && copy && paste && selAll && undo && toEnd && clrMarks && setMark && setColor && getColor && replSel && setMode); + + if (!getLine || !cut || !copy || !paste || !selAll || !undo || !toEnd || !clrMarks || !setMark || !setColor || !getColor || !replSel || !setMode) { + tShutdown(); + return; + } + + // GutterClick and Colorize are listed for the designer and the IDE, + // with their parameters + TEST_TRUE(iface && iface->eventCount == 2 && strcmp(iface->events[0].name, "GutterClick") == 0 && strcmp(iface->events[1].name, "Colorize") == 0); + TEST_STR_EQ(iface ? iface->events[0].params : NULL, "LineNumber As Integer"); + TEST_STR_EQ(iface ? iface->events[1].params : NULL, "LineText As String, Colors As String"); + + wtSettle(); + sCovChanges = 0; + ta->onChange = covOnChange; + + // Lines and the cursor column + wgtSetText(ta, "alpha\nbeta gamma\n\nend"); + TEST_EQ(covGetInt(ta, "textarea", "LineCount"), 4); + TEST_STR_EQ(getLine(ta, 2), "beta gamma"); + TEST_STR_EQ(getLine(ta, 3), ""); + TEST_STR_EQ(getLine(ta, 4), "end"); + TEST_STR_EQ(getLine(ta, 5), ""); + TEST_STR_EQ(getLine(ta, 0), ""); + wgtTextAreaSetCursor(ta, 2, 6); + TEST_EQ(covGetInt(ta, "textarea", "CursorCol"), 6); + TEST_EQ(wgtTextAreaGetCursorLine(ta), 2); + + // SelStart / SelLength / SelText (offsets from 0) + TEST_EQ(covGetInt(ta, "textarea", "SelStart"), 11); + TEST_EQ(covGetInt(ta, "textarea", "SelLength"), 0); + covSetInt(ta, "textarea", "SelStart", 6); + covSetInt(ta, "textarea", "SelLength", 4); + TEST_EQ(covGetInt(ta, "textarea", "SelStart"), 6); + TEST_EQ(covGetInt(ta, "textarea", "SelLength"), 4); + TEST_STR_EQ(covGetStr(ta, "textarea", "SelText"), "beta"); + covSetStr(ta, "textarea", "SelText", "BETA!"); + TEST_STR_EQ(wgtGetText(ta), "alpha\nBETA! gamma\n\nend"); + TEST_EQ(sCovChanges, 1); + TEST_EQ(covGetInt(ta, "textarea", "SelLength"), 0); + TEST_EQ(covGetInt(ta, "textarea", "SelStart"), 11); + covSetInt(ta, "textarea", "SelStart", 1000); + TEST_EQ(covGetInt(ta, "textarea", "SelStart"), (int32_t)strlen(wgtGetText(ta))); + covSetInt(ta, "textarea", "SelStart", 0); + covSetInt(ta, "textarea", "SelLength", 1000); + TEST_EQ(covGetInt(ta, "textarea", "SelLength"), (int32_t)strlen(wgtGetText(ta))); + + // Clipboard and undo, the same code as Ctrl+X/C/V/A/Z + covSetInt(ta, "textarea", "SelStart", 0); + covSetInt(ta, "textarea", "SelLength", 5); + copy(ta); + int32_t clipLen = 0; + const char *clip = clipboardGet(&clipLen); + TEST_TRUE(clipLen == 5 && strncmp(clip, "alpha", 5) == 0); + cut(ta); + TEST_STR_EQ(getLine(ta, 1), ""); + TEST_EQ(sCovChanges, 2); + undo(ta); + TEST_STR_EQ(getLine(ta, 1), "alpha"); + TEST_EQ(sCovChanges, 3); + toEnd(ta); + paste(ta); + TEST_STR_EQ(getLine(ta, 4), "endalpha"); + selAll(ta); + TEST_EQ(covGetInt(ta, "textarea", "SelLength"), (int32_t)strlen(wgtGetText(ta))); + + // A read-only area copies but does not cut, paste or undo + wgtSetReadOnly(ta, true); + cut(ta); + paste(ta); + undo(ta); + TEST_EQ(covGetInt(ta, "textarea", "SelLength"), (int32_t)strlen(wgtGetText(ta))); + wgtSetReadOnly(ta, false); + + // ReplaceSelection: replaces a matching selection, then finds the next + wgtSetText(ta, "one two one two"); + TEST_TRUE(wgtTextAreaFindNext(ta, "two", false, true)); + TEST_TRUE(replSel(ta, "TWO", "2", false, true)); + TEST_STR_EQ(wgtGetText(ta), "one 2 one two"); + TEST_STR_EQ(covGetStr(ta, "textarea", "SelText"), "two"); + + // MaxLength: a smaller limit cuts the text, 0 is the default + TEST_EQ(covGetInt(ta, "textarea", "MaxLength"), COV_AREA_MAX); + covSetInt(ta, "textarea", "MaxLength", 5); + TEST_EQ(covGetInt(ta, "textarea", "MaxLength"), 5); + TEST_STR_EQ(wgtGetText(ta), "one 2"); + covSetInt(ta, "textarea", "MaxLength", 0); + TEST_EQ(covGetInt(ta, "textarea", "MaxLength"), COV_AREA_DEFAULT_MAX); + TEST_STR_EQ(wgtGetText(ta), "one 2"); + + // Syntax colours by index (BASIC &HBBGGRR; -1 = the default) + uint32_t keywordRgb = textEditDefaultSyntaxRgb(TEXT_SYNTAX_KEYWORD); + TEST_EQ(getColor(ta, TEXT_SYNTAX_KEYWORD), (int32_t)wgtBasicColor(keywordRgb)); + TEST_EQ(getColor(ta, TEXT_SYNTAX_DEFAULT), -1); + TEST_EQ(getColor(ta, TEXT_SYNTAX_MAX), -1); + setColor(ta, TEXT_SYNTAX_KEYWORD, (int32_t)wgtBasicColor(COV_KW_RGB)); + TEST_EQ(getColor(ta, TEXT_SYNTAX_KEYWORD), (int32_t)wgtBasicColor(COV_KW_RGB)); + setMode(ta, "bas"); + wgtSetText(ta, "PRINT x"); + TEST_TRUE(covPaintHasRgb(ta, COV_KW_RGB)); + setColor(ta, TEXT_SYNTAX_KEYWORD, -1); + TEST_EQ(getColor(ta, TEXT_SYNTAX_KEYWORD), (int32_t)wgtBasicColor(keywordRgb)); + + // The merged colorizer: &H / &O and exponent numbers, '&' operator + uint32_t custom[TEXT_SYNTAX_MAX]; + + memset(custom, 0, sizeof(custom)); + custom[TEXT_SYNTAX_NUMBER] = COV_NUM_RGB; + custom[TEXT_SYNTAX_OPERATOR] = COV_TYPE_RGB; + wgtTextAreaSetSyntaxColors(ta, custom, TEXT_SYNTAX_MAX); + wgtSetText(ta, "&HFF&"); + int32_t hexPixels = covPaintCountRgb(ta, COV_NUM_RGB); + TEST_TRUE(hexPixels > 0); + TEST_TRUE(!covPaintHasRgb(ta, COV_TYPE_RGB)); + wgtSetText(ta, "a & b"); + TEST_TRUE(covPaintHasRgb(ta, COV_TYPE_RGB)); + TEST_TRUE(!covPaintHasRgb(ta, COV_NUM_RGB)); + wgtSetText(ta, "1.5E+3#"); + TEST_TRUE(covPaintHasRgb(ta, COV_NUM_RGB)); + TEST_TRUE(!covPaintHasRgb(ta, COV_TYPE_RGB)); + wgtTextAreaSetSyntaxColors(ta, NULL, 0); + + // Line markers: gutter dot and background, cleared, removed + wgtTextAreaSetShowLineNumbers(ta, true); + wgtSetText(ta, "a\nb\nc"); + setMark(ta, 2, (int32_t)wgtBasicColor(COV_KW_RGB), (int32_t)wgtBasicColor(COV_MARK_BACK_RGB)); + TEST_TRUE(covPaintHasRgb(ta, COV_KW_RGB)); + TEST_TRUE(covPaintHasRgb(ta, COV_MARK_BACK_RGB)); + setMark(ta, 2, -1, (int32_t)wgtBasicColor(COV_MARK_BACK_RGB)); + TEST_TRUE(!covPaintHasRgb(ta, COV_KW_RGB)); + TEST_TRUE(covPaintHasRgb(ta, COV_MARK_BACK_RGB)); + setMark(ta, 2, -1, -1); + TEST_TRUE(!covPaintHasRgb(ta, COV_MARK_BACK_RGB)); + wgtTextAreaSetLineMarker(ta, 1, COV_KW_RGB, TEXTAREA_MARKER_NONE); + wgtTextAreaSetLineMarker(ta, 3, TEXTAREA_MARKER_NONE, COV_MARK_BACK_RGB); + TEST_TRUE(covPaintHasRgb(ta, COV_KW_RGB) && covPaintHasRgb(ta, COV_MARK_BACK_RGB)); + clrMarks(ta); + TEST_TRUE(!covPaintHasRgb(ta, COV_KW_RGB) && !covPaintHasRgb(ta, COV_MARK_BACK_RGB)); + + // The C API's syntax mode is the method's + wgtTextAreaSetSyntaxMode(ta, "basic"); + wgtTextAreaSetSyntaxMode(ta, NULL); + tShutdown(); +} + + static void caseAreaCacheLibrary(void) { tCase("texthelp: line cache notify fallbacks and scrollbar helpers"); @@ -296,7 +491,7 @@ static void caseAreaColorize(void) { // a decorated line with a gutter marker. setMode(ta, "bas"); wgtTextAreaSetShowLineNumbers(ta, true); - wgtTextAreaSetLineDecorator(ta, covDecorator, NULL); + wgtTextAreaSetLineMarker(ta, COV_MARK_LINE, COV_MARK_GUTTER_RGB, COV_MARK_BACK_RGB); wgtSetText(ta, "PRINT \"hi\"\nREM note\nx = 1"); wtSettle(); covAreaClick(f.win, ta, (3 + 1) * tCtx()->font.charWidth, 0, 2); @@ -315,6 +510,33 @@ static void caseAreaColorize(void) { } +// SetSyntaxMode "custom": every painted line goes to the Colorize event, +// and the colours the handler writes back are the ones painted. +static void caseAreaColorizeEvent(void) { + tCase("textArea: SetSyntaxMode custom fires Colorize with ByRef Colors"); + + WtFixtureT f = wtFixture("CovColorEvt"); + WidgetT *ta = wgtTextArea(f.root, COV_TI_MAX); + + ta->weight = WGT_WEIGHT_FILL; + ta->onEvent = covColorizeEvent; + wgtSetText(ta, "kkk plain\nk"); + wgtTextAreaSetSyntaxMode(ta, "custom"); + sCovColorizeLines = 0; + wtSettle(); + TEST_TRUE(sCovColorizeLines >= 2); + TEST_TRUE(covPaintHasRgb(ta, textEditDefaultSyntaxRgb(TEXT_SYNTAX_KEYWORD))); + + // Another mode stops the events + wgtTextAreaSetSyntaxMode(ta, ""); + sCovColorizeLines = 0; + wgtInvalidatePaint(ta); + wtSettle(); + TEST_EQ(sCovColorizeLines, 0); + tShutdown(); +} + + // Selection-replacing edits, auto-indent, tab insertion in both modes, // full-buffer edits and the incremental line cache updates on lines that // contain tabs. @@ -492,6 +714,24 @@ static void caseAreaKeywordList(void) { wgtSetText(ta, "p.Width"); TEST_TRUE(!covPaintHasRgb(ta, COV_KW_RGB)); + // The IDE's code window uses this colorizer too: CLS, PCOPY and Enum + // are keywords, Name stays one after a dot, Variant is a type but + // plain after a dot, and a file number's '#' is no date + wgtSetText(ta, "CLS: PCOPY 1, 0"); + TEST_TRUE(covPaintHasRgb(ta, COV_KW_RGB)); + wgtSetText(ta, "Enum"); + TEST_TRUE(covPaintHasRgb(ta, COV_KW_RGB)); + wgtSetText(ta, "x.Name"); + TEST_TRUE(covPaintHasRgb(ta, COV_KW_RGB)); + wgtSetText(ta, "Variant"); + TEST_TRUE(covPaintHasRgb(ta, COV_TYPE_RGB)); + wgtSetText(ta, "p.Variant"); + TEST_TRUE(!covPaintHasRgb(ta, COV_TYPE_RGB)); + wgtSetText(ta, "x = f: y = g"); + TEST_TRUE(!covPaintHasRgb(ta, COV_NUM_RGB)); + wgtSetText(ta, "Close #f, g"); + TEST_TRUE(!covPaintHasRgb(ta, COV_NUM_RGB)); + // Every character of the literal, '#'s and words too, is a number wgtSetText(ta, "Jan 2, 2026"); int32_t digitsOnly = covPaintCountRgb(ta, COV_NUM_RGB); @@ -570,10 +810,22 @@ static void caseAreaMouse(void) { wgtTextAreaSetShowLineNumbers(ta, true); wgtSetText(ta, many); wgtTextAreaSetGutterClick(ta, covOnGutter); + ta->onEvent = wtOnEvent; + wtEventSpyReset(); wtSettle(); sCovGutterLine = -1; wtClickAt(f.win, ta->x + COV_TA_BORDER + COV_TA_PAD + 2, textY + charH + 3); TEST_EQ(sCovGutterLine, 2); + TEST_STR_EQ(wtEventSpy()->name, "GutterClick"); + TEST_EQ(wtEventSpy()->arg0, 2); + + // onEvent alone also makes the gutter clickable (the form runtime's case) + wgtTextAreaSetGutterClick(ta, NULL); + wtEventSpyReset(); + wtClickAt(f.win, ta->x + COV_TA_BORDER + COV_TA_PAD + 2, textY + 3); + TEST_EQ(wtEventSpy()->count, 1); + TEST_EQ(wtEventSpy()->arg0, 1); + wgtTextAreaSetGutterClick(ta, covOnGutter); wgtSetText(ta, "a\nb"); wtClickAt(f.win, ta->x + COV_TA_BORDER + COV_TA_PAD + 2, textY + 10 * charH + 3); TEST_EQ(sCovGutterLine, 2); @@ -1228,9 +1480,135 @@ static void caseEditRegressions(void) { } +// Image MaskColor / UseMaskColor: the BASIC colour is the key +// wgtImageSetTransparent uses. +static void caseImageMask(void) { + tCase("image: MaskColor / UseMaskColor"); + + WtFixtureT f = wtFixture("CovImgMask"); + WidgetT *img = wgtImage(f.root, NULL, 0, 0, 0); + const WgtIfaceT *iface = wgtGetIface("image"); + const WgtPropDescT *mc = wgtIfaceFindProp(iface, "MaskColor"); + const WgtPropDescT *use = wgtIfaceFindProp(iface, "UseMaskColor"); + char out[COV_TI_MAX]; + + TEST_TRUE(mc != NULL && use != NULL); + + if (!mc || !use) { + tShutdown(); + return; + } + + TEST_TRUE(wtPropApply(img, mc, "255")); + TEST_TRUE(wtPropApply(img, use, "True")); + TEST_TRUE(wtPropToString(img, mc, out, sizeof(out))); + TEST_STR_EQ(out, "255"); + TEST_TRUE(wtPropToString(img, use, out, sizeof(out))); + TEST_STR_EQ(out, "True"); + tShutdown(); +} + + +// The TextBox members C and BASIC share: SelStart / SelLength / SelText +// read and set, and Cut / Copy / Paste / SelectAll / Undo running the +// box's own keys (a password box copies nothing). +static void caseInputBasicMembers(void) { + tCase("textInput: SelStart / SelLength / SelText, Cut / Copy / Paste / SelectAll / Undo"); + + WtFixtureT f = wtFixture("CovInBas"); + WidgetT *in = wgtTextInput(f.root, COV_TI_MAX); + WidgetT *pw = wgtPasswordInput(f.root, COV_TI_MAX); + WidgetT *mask = wgtMaskedInput(f.root, "###-####"); + + wtSettle(); + wgtSetText(in, "hello world"); + TEST_EQ(wgtTextInputGetSelStart(in), 11); + TEST_EQ(wgtTextInputGetSelLength(in), 0); + wgtTextInputSetSelStart(in, 6); + wgtTextInputSetSelLength(in, 5); + TEST_EQ(wgtTextInputGetSelStart(in), 6); + TEST_EQ(wgtTextInputGetSelLength(in), 5); + TEST_STR_EQ(wgtTextInputGetSelText(in), "world"); + wgtTextInputSetSelLength(in, INT32_MAX); + TEST_EQ(wgtTextInputGetSelLength(in), 5); + + wgtTextInputCopy(in); + TEST_TRUE(covClipIs("world")); + wgtTextInputSetSelText(in, "there"); + TEST_STR_EQ(wgtGetText(in), "hello there"); + wgtTextInputUndo(in); + TEST_STR_EQ(wgtGetText(in), "hello world"); + wgtTextInputSelectAll(in); + TEST_EQ(wgtTextInputGetSelLength(in), 11); + wgtTextInputCut(in); + TEST_STR_EQ(wgtGetText(in), ""); + wgtTextInputPaste(in); + TEST_STR_EQ(wgtGetText(in), "hello world"); + + // A password box keeps its text off the clipboard + wgtSetText(pw, "secret"); + wgtTextInputSelectAll(pw); + wgtTextInputCopy(pw); + TEST_TRUE(covClipIs("hello world")); + + // A masked box fills its slots + wgtTextInputSetSelStart(mask, 0); + wgtTextInputSetSelText(mask, "5551234"); + TEST_STR_EQ(wgtGetText(mask), "555-1234"); + + // Through the BASIC interface + const WgtIfaceT *iface = wgtGetIface("textbox"); + const WgtPropDescT *p = wgtIfaceFindProp(iface, "SelText"); + char out[COV_TI_MAX]; + + wgtTextInputSetSelStart(in, 0); + wgtTextInputSetSelLength(in, 5); + TEST_TRUE(p && wtPropToString(in, p, out, sizeof(out))); + TEST_STR_EQ(out, "hello"); + TEST_TRUE(wgtIfaceFindMethod(iface, "Paste") != NULL && wgtIfaceFindMethod(iface, "Undo") != NULL); + tShutdown(); +} + + // Mouse behaviour of the single-line editor: multi-click selection, drag // auto-scroll in both directions, the selection paint runs, cursor blink // and cross-widget selection clearing. +// TextBox Mask: a live box becomes masked and plain again. +static void caseInputMask(void) { + tCase("textInput: Mask switches a live box to masked input and back"); + + WtFixtureT f = wtFixture("CovMask"); + WidgetT *in = wgtTextInput(f.root, COV_TI_MAX); + + wtSettle(); + TEST_STR_EQ(covGetStr(in, "textbox", "Mask"), ""); + wgtSetText(in, "5551234"); + covSetStr(in, "textbox", "Mask", "(###) ###"); + TEST_STR_EQ(covGetStr(in, "textbox", "Mask"), "(###) ###"); + TEST_STR_EQ(wgtGetText(in), "(555) 123"); + TEST_EQ(covGetInt(in, "textbox", "MaxLength"), 9); + + // Typing goes into the slots only + wgtSetFocused(in); + tWidgetKey(f.win, KEY_HOME, 0); + tWidgetKey(f.win, 'x', 0); + tWidgetKey(f.win, '9', 0); + TEST_STR_EQ(wgtGetText(in), "(955) 123"); + + // A password box becomes masked; PasswordChar is then "" + covSetStr(in, "textbox", "Mask", ""); + TEST_STR_EQ(covGetStr(in, "textbox", "Mask"), ""); + TEST_STR_EQ(wgtGetText(in), "(955) 123"); + TEST_EQ(covGetInt(in, "textbox", "MaxLength"), COV_TI_MAX); + covSetStr(in, "textbox", "PasswordChar", "*"); + covSetStr(in, "textbox", "Mask", "##"); + TEST_STR_EQ(covGetStr(in, "textbox", "PasswordChar"), ""); + TEST_STR_EQ(wgtGetText(in), "95"); + covSetStr(in, "textbox", "Mask", ""); + tShutdown(); +} + + static void caseInputMouse(void) { tCase("textInput: multi-click, drag auto-scroll, blink, selection paint"); @@ -1578,6 +1956,37 @@ static void caseMaskedInput(void) { } +// SpinButton RealValue, SetRealRange and SetRealStep (the C real-mode +// functions) through the BASIC interface. +static void caseSpinnerReal(void) { + tCase("spinner: RealValue, SetRealRange, SetRealStep"); + + WtFixtureT f = wtFixture("CovSpinReal"); + WidgetT *sp = wgtSpinner(f.root, 0, 10, 1); + const WgtIfaceT *iface = wgtGetIface("spinner"); + const WgtPropDescT *rv = wgtIfaceFindProp(iface, "RealValue"); + WgtValueT args[2]; + WgtValueT ret; + + wgtSpinnerSetRealMode(sp, true); + args[0] = wgtValueDouble(0.5); + args[1] = wgtValueDouble(2.5); + TEST_TRUE(wgtIfaceCallMethod(iface, sp, wgtIfaceFindMethod(iface, "SetRealRange"), args, 2, &ret)); + args[0] = wgtValueDouble(0.25); + TEST_TRUE(wgtIfaceCallMethod(iface, sp, wgtIfaceFindMethod(iface, "SetRealStep"), args, 1, &ret)); + TEST_TRUE(rv != NULL && wtPropApply(sp, rv, "9.75")); + TEST_TRUE(wgtSpinnerGetRealValue(sp) == 2.5); + wgtSpinnerSetRealValue(sp, 1.25); + + WgtValueT got; + + TEST_TRUE(rv && wgtIfaceGetProp(iface, sp, rv, &got)); + TEST_TRUE(got.type == WGT_IFACE_DOUBLE && got.f == 1.25); + TEST_TRUE(!wgtIfaceCallMethod(iface, sp, wgtIfaceFindMethod(iface, "SetRealStep"), args, 0, &ret)); + tShutdown(); +} + + // Spinner text paths: scrolling, real-mode typing rules, paste // validation and the value getters that commit pending edits. static void caseSpinnerText(void) { @@ -1719,16 +2128,25 @@ static bool covClipIs(const char *expect) { } -// Line decorator: line 2 gets a background override and a gutter marker. -static uint32_t covDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) { - (void)ctx; +// onEvent for caseAreaColorizeEvent: Colorize marks every 'k' as a +// keyword (a host writing the ByRef Colors back); counts the lines. +static void covColorizeEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + static char colors[COV_COLORIZE_MAX]; - if (lineNum == 2) { - *gutterColor = 0x00FF0000; - return 0x00303030; + (void)w; + + if (strcmp(name, "Colorize") != 0 || argc != 2) { + return; } - return 0; + snprintf(colors, sizeof(colors), "%s", args[1].s); + + for (int32_t i = 0; args[0].s[i] && i < COV_COLORIZE_MAX - 1; i++) { + colors[i] = (args[0].s[i] == 'k') ? (char)('0' + TEXT_SYNTAX_KEYWORD) : colors[i]; + } + + args[1] = wgtValueStr(colors); + sCovColorizeLines++; } @@ -1750,6 +2168,28 @@ static void *covFindMethod(const char *ifaceName, const char *methodName) { } +static const WgtPropDescT *covFindProp(const char *ifaceName, const char *propName) { + const WgtPropDescT *p = wgtIfaceFindProp(wgtGetIface(ifaceName), propName); + + TEST_TRUE(p != NULL); + return p; +} + + +static int32_t covGetInt(WidgetT *w, const char *ifaceName, const char *propName) { + const WgtPropDescT *p = covFindProp(ifaceName, propName); + + return p ? ((int32_t (*)(const WidgetT *))p->getFn)(w) : 0; +} + + +static const char *covGetStr(WidgetT *w, const char *ifaceName, const char *propName) { + const WgtPropDescT *p = covFindProp(ifaceName, propName); + + return p ? ((const char *(*)(const WidgetT *))p->getFn)(w) : ""; +} + + static void covOnChange(WidgetT *w) { (void)w; sCovChanges++; @@ -1799,6 +2239,24 @@ static bool covPaintHasRgb(WidgetT *w, uint32_t rgb) { } +static void covSetInt(WidgetT *w, const char *ifaceName, const char *propName, int32_t v) { + const WgtPropDescT *p = covFindProp(ifaceName, propName); + + if (p && p->setFn) { + ((void (*)(WidgetT *, int32_t))p->setFn)(w, v); + } +} + + +static void covSetStr(WidgetT *w, const char *ifaceName, const char *propName, const char *v) { + const WgtPropDescT *p = covFindProp(ifaceName, propName); + + if (p && p->setFn) { + ((void (*)(WidgetT *, const char *))p->setFn)(w, v); + } +} + + static void covType(WindowT *win, const char *text) { for (const char *p = text; *p; p++) { tWidgetKey(win, (int32_t)(uint8_t)*p, 0); @@ -1810,12 +2268,14 @@ void wtSectionCovText(void) { caseInputSelection(); caseInputMouse(); caseMaskedInput(); + caseInputMask(); caseAreaNavigation(); caseAreaEditing(); caseAreaMouse(); caseAreaScrollbars(); caseAreaCaretAtEdge(); caseAreaColorize(); + caseAreaBasicMembers(); caseAreaKeywordList(); caseAreaSearch(); caseChangeDestroys(); @@ -1823,4 +2283,8 @@ void wtSectionCovText(void) { caseEditRegressions(); caseSpinnerText(); caseAreaCacheLibrary(); + caseInputBasicMembers(); + caseAreaColorizeEvent(); + caseImageMask(); + caseSpinnerReal(); } diff --git a/src/test/widgets/testData.c b/src/test/widgets/testData.c index f2c2d489..9e55ae2c 100644 --- a/src/test/widgets/testData.c +++ b/src/test/widgets/testData.c @@ -36,7 +36,6 @@ #include "dvxCur.h" #include "../../libs/kpunch/sql/dvxSql.h" -#include #include #include @@ -79,6 +78,7 @@ char *strdup(const char *s); static void tdCaseCallbackDestroy(void); static void tdCaseDataAddNew(void); static void tdCaseDataDelete(void); +static void tdCaseDataIface(void); static void tdCaseDataKeyEdit(void); static void tdCaseDataLongKeys(void); static void tdCaseDataLooseKeys(void); @@ -134,19 +134,22 @@ static void tdSetProp(const char *ifaceName, WidgetT *w, const char *propN // strdup interposer. The suite links -rdynamic, so the datactrl widget // binds to this definition; while sFailStrdup is set, copying TD_OOM_TEXT -// fails as it would when out of memory. +// fails as it would when out of memory. The copy is made here rather than +// by libc's strdup (dlsym RTLD_NEXT), which would bypass the sanitizers' +// own strdup: under MemorySanitizer every copy then read as uninitialized. char *strdup(const char *s) { - static char *(*realStrdup)(const char *) = NULL; - if (sFailStrdup && strcmp(s, TD_OOM_TEXT) == 0) { return NULL; } - if (!realStrdup) { - realStrdup = (char *(*)(const char *))dlsym(RTLD_NEXT, "strdup"); + size_t len = strlen(s) + 1; + char *copy = (char *)malloc(len); + + if (copy) { + memcpy(copy, s, len); } - return realStrdup(s); + return copy; } @@ -283,6 +286,51 @@ static void tdCaseDataDelete(void) { } +// The BASIC members of the Data control (the same functions C calls): +// RecordCount, FieldCount, AbsolutePosition (moving fires Reposition), +// FieldName, GetCell, GetField and SetField. +static void tdCaseDataIface(void) { + tCase("dataCtrl: RecordCount / FieldCount / AbsolutePosition / FieldName / GetCell / GetField / SetField"); + + TdFixtureT t = tdFixture(3); + const WgtIfaceT *iface = wgtGetIface("data"); + char buf[TD_VAL_MAX]; + WgtValueT args[2]; + WgtValueT ret; + const WgtMethodDescT *m; + + tdGetProp("data", t.data, "RecordCount", buf, sizeof(buf)); + TEST_STR_EQ(buf, "3"); + tdGetProp("data", t.data, "FieldCount", buf, sizeof(buf)); + TEST_STR_EQ(buf, "3"); + tdSetProp("data", t.data, "AbsolutePosition", "2"); + TEST_EQ(tdCurrentId(t.data), 2); + tdGetProp("data", t.data, "AbsolutePosition", buf, sizeof(buf)); + TEST_STR_EQ(buf, "2"); + + args[0] = wgtValueInt(1); + m = wgtIfaceFindMethod(iface, "FieldName"); + TEST_TRUE(m != NULL && wgtIfaceCallMethod(iface, t.data, m, args, 1, &ret)); + TEST_STR_EQ(ret.s, "name"); + + args[0] = wgtValueInt(0); + args[1] = wgtValueInt(1); + m = wgtIfaceFindMethod(iface, "GetCell"); + TEST_TRUE(m != NULL && wgtIfaceCallMethod(iface, t.data, m, args, 2, &ret)); + TEST_STR_EQ(ret.s, "p00"); + + args[0] = wgtValueStr("name"); + args[1] = wgtValueStr("Zed"); + m = wgtIfaceFindMethod(iface, "SetField"); + TEST_TRUE(m != NULL && wgtIfaceCallMethod(iface, t.data, m, args, 2, &ret)); + m = wgtIfaceFindMethod(iface, "GetField"); + TEST_TRUE(m != NULL && wgtIfaceCallMethod(iface, t.data, m, args, 1, &ret)); + TEST_STR_EQ(ret.s, "Zed"); + TEST_TRUE(!wgtIfaceCallMethod(iface, t.data, m, args, 2, &ret)); + tShutdown(); +} + + // The UPDATE locates the row by the key it was loaded with (S4-025), only // changed columns are written, and a no-op or single-column row still // saves cleanly (S1-333). @@ -1909,6 +1957,7 @@ void wtSectionData(void) { tdCaseMasterDetail(); tdCaseReentrantRefresh(); tdCaseSetCurrentRow(); + tdCaseDataIface(); tdCaseNullArgs(); tdCaseGridColumns(); tdCaseGridSelect(); diff --git a/src/test/widgets/testGeneric.c b/src/test/widgets/testGeneric.c new file mode 100644 index 00000000..86a58d4d --- /dev/null +++ b/src/test/widgets/testGeneric.c @@ -0,0 +1,580 @@ +// 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. + +// testGeneric.c -- generic access on real widgets, the new class methods, +// and the canvas without its border +// +// wgtCreateByName / wgtGetProp / wgtSetProp / wgtCallMethod on loaded +// widgets (every interface is bound to its class), the dispatcher calling +// WGT_METHOD_ON_MOUSE_EVENT (hover, press, release, capture), +// WGT_METHOD_ON_KEY_UP and WGT_METHOD_ON_FOCUS before the universal +// callbacks, and CANVAS_BORDER_NONE (size, pixels, mouse coordinates, +// the BASIC BorderStyle property). + +#include "wtCommon.h" +#include "canvas/canvas.h" +#include "label/label.h" + +#include + +#define PROBE_SIZE 40 +#define LOG_MAX 256 +#define HOVER_X1 5 +#define HOVER_Y1 6 +#define HOVER_X2 7 +#define FAR_OUTSIDE 300 +#define CV_W 20 +#define CV_H 10 +#define CV_CLICK 5 +#define CV_RED 255 +#define TAB_CTRL "tabcontrol" +#define TAB_PAGE "tabpage" +#define KEY_UP_CODE 0x1E +#define LIST_ITEMS 2 +#define FIRE_BUTTON 1 +#define FIRE_SHIFT 2 +#define FIRE_X 11 +#define FIRE_Y 12 + + +// ============================================================ +// Prototypes +// ============================================================ + +static void caseCanvasBorderless(void); +static void caseCaptureAndHitWalk(void); +static void caseClassMethods(void); +static void caseGenericAccess(void); +static void caseIfaceBinding(void); +static void caseRaisesEvents(void); +static void firedMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y); +static void logAdd(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +static void onCanvasMouse(WidgetT *w, int32_t cx, int32_t cy, bool drag); +static void probeCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void probeFocus(WidgetT *w); +static void probeKeyUp(WidgetT *w, int32_t keyCode, int32_t shift); +static void probeMouseEvent(WidgetT *w, WgtMouseActionE action, int32_t button, int32_t x, int32_t y, int32_t shift); +static void probeOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +static void uniClick(WidgetT *w); +static void uniFocus(WidgetT *w); +static void uniKeyDown(WidgetT *w, int32_t keyCode, int32_t shift); +static void uniKeyUp(WidgetT *w, int32_t keyCode, int32_t shift); +static void uniMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y); +static void uniMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y); +static void uniMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y); + + +// ============================================================ +// Module state +// ============================================================ + +static char sLog[LOG_MAX]; +static bool sCapture = false; // probeOnMouse claims the drag +static bool sDestroyOn = false; // probeMouseEvent destroys the widget +static int32_t sCanvasX = -1; +static int32_t sCanvasY = -1; + +static const WidgetClassT sProbeClass = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_FOCUSABLE, + .handlers = { + [WGT_METHOD_CALC_MIN_SIZE] = (void *)probeCalcMinSize, + [WGT_METHOD_ON_MOUSE] = (void *)probeOnMouse, + [WGT_METHOD_ON_MOUSE_EVENT] = (void *)probeMouseEvent, + [WGT_METHOD_ON_KEY_UP] = (void *)probeKeyUp, + [WGT_METHOD_ON_FOCUS] = (void *)probeFocus, + } +}; + +// The probe with WCLASS_RAISES_EVENTS: its widgets' universal mouse, +// click and key callbacks come only from wgtFireEvent +static const WidgetClassT sRaisingClass = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_FOCUSABLE | WCLASS_RAISES_EVENTS, + .handlers = { + [WGT_METHOD_CALC_MIN_SIZE] = (void *)probeCalcMinSize, + [WGT_METHOD_ON_MOUSE] = (void *)probeOnMouse, + [WGT_METHOD_ON_MOUSE_EVENT] = (void *)probeMouseEvent, + [WGT_METHOD_ON_KEY_UP] = (void *)probeKeyUp, + [WGT_METHOD_ON_FOCUS] = (void *)probeFocus, + } +}; + +// A non-focusable clipping container (a box layout) that handles its own +// mouse +static const WidgetClassT sContainerClass = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_NO_HIT_RECURSE, + .handlers = { + [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox, + [WGT_METHOD_LAYOUT] = (void *)widgetLayoutBox, + [WGT_METHOD_ON_MOUSE_EVENT] = (void *)probeMouseEvent, + } +}; + + +// CANVAS_BORDER_NONE: the widget is the drawing area (no bevel, no +// offset); the fixed frame comes back on request. BorderStyle is the +// same switch for BASIC (enum None / FixedSingle). +static void caseCanvasBorderless(void) { + tCase("canvas: border-less mode paints, sizes and maps the mouse without the frame"); + + WtFixtureT f = wtFixture("CvNoBorder"); + WidgetT *cv = wgtCanvas(f.root, CV_W, CV_H); + uint32_t red = packColor(&tCtx()->display, CV_RED, 0, 0); + WgtValueT v; + char text[WT_EVENT_TEXT]; + + wgtCanvasSetMouseCallback(cv, onCanvasMouse); + wgtCanvasClear(cv, red); + wtSettle(); + TEST_EQ(wgtCanvasGetBorderStyle(cv), CANVAS_BORDER_FIXED); + TEST_EQ(cv->calcMinW, CV_W + 2 * CANVAS_BORDER_WIDTH); + TEST_TRUE(wtContentPixel(f.win, cv->x, cv->y) != red); + TEST_EQ(wtContentPixel(f.win, cv->x + CANVAS_BORDER_WIDTH, cv->y + CANVAS_BORDER_WIDTH), red); + wtClickAt(f.win, cv->x + CV_CLICK, cv->y + CV_CLICK); + TEST_EQ(sCanvasX, CV_CLICK - CANVAS_BORDER_WIDTH); + TEST_EQ(sCanvasY, CV_CLICK - CANVAS_BORDER_WIDTH); + wtSettle(); + + v = wgtValueStr("none"); + TEST_TRUE(wgtSetProp(cv, "BorderStyle", &v)); + wtSettle(); + TEST_EQ(wgtCanvasGetBorderStyle(cv), CANVAS_BORDER_NONE); + TEST_EQ(cv->contentOffX, 0); + TEST_EQ(cv->calcMinW, CV_W); + TEST_EQ(cv->calcMinH, CV_H); + TEST_EQ(wtContentPixel(f.win, cv->x, cv->y), red); + TEST_EQ(wtContentPixel(f.win, cv->x + CV_W - 1, cv->y + CV_H - 1), red); + wtClickAt(f.win, cv->x + CV_CLICK, cv->y + CV_CLICK); + TEST_EQ(sCanvasX, CV_CLICK); + TEST_EQ(sCanvasY, CV_CLICK); + wtSettle(); + + // Back to the frame, by index this time; the text form names it + v = wgtValueInt(CANVAS_BORDER_FIXED); + TEST_TRUE(wgtSetProp(cv, "BorderStyle", &v)); + TEST_TRUE(wtPropToString(cv, wgtIfaceFindProp(wgtGetIfaceOf(cv, NULL), "BorderStyle"), text, sizeof(text))); + TEST_STR_EQ(text, "FixedSingle"); + wtSettle(); + TEST_EQ(cv->calcMinW, CV_W + 2 * CANVAS_BORDER_WIDTH); + TEST_TRUE(wtContentPixel(f.win, cv->x, cv->y) != red); + tShutdown(); +} + + +// A capturing class gets moves and the release outside itself; a +// non-focusable NO_HIT_RECURSE class with the mouse method keeps the +// events of the area its children cover. +static void caseCaptureAndHitWalk(void) { + tCase("class methods: capture delivers moves and the release; the hit walk stops at the class"); + + WtFixtureT f = wtFixture("Capture"); + int32_t type = wgtRegisterClass(&sProbeClass); + int32_t cType = wgtRegisterClass(&sContainerClass); + WidgetT *w = widgetAlloc(f.root, type); + + TEST_TRUE(w != NULL); + + if (!w) { + tShutdown(); + return; + } + + w->onMouseUp = uniMouseUp; + wgtInvalidate(w); + wtSettle(); + + widgetOnMouse(f.win, w->x + HOVER_X1, w->y + HOVER_Y1, 0); + sLog[0] = '\0'; + sCapture = true; + widgetOnMouse(f.win, w->x + HOVER_X1, w->y + HOVER_Y1, MOUSE_LEFT); + widgetOnMouse(f.win, w->x + FAR_OUTSIDE, w->y + HOVER_Y1, MOUSE_LEFT); + widgetOnMouse(f.win, w->x + FAR_OUTSIDE, w->y + HOVER_Y1, 0); + sCapture = false; + TEST_STR_EQ(sLog, "cD1(5,6)s0 cM1(300,6)s0 cU1(300,6)s0 uu1 "); + TEST_PTR_EQ(sDragWidget, NULL); + + // The container's child (a label) lies under the cursor, but the + // container class has the mouse method, so it gets the event. + WidgetT *box = widgetAlloc(f.root, cType); + WidgetT *inner = box ? wgtLabel(box, "inner") : NULL; + + TEST_TRUE(inner != NULL); + + if (inner) { + wgtInvalidate(box); + wtSettle(); + TEST_PTR_EQ(widgetCallbackTarget(f.root, inner->x + 1, inner->y + 1), box); + } + + tShutdown(); + wgtUnregisterClass(type); + wgtUnregisterClass(cType); +} + + +// The dispatcher runs the class methods first: hover moves (no button), +// press / release edges for each button, key-up and focus gained. +static void caseClassMethods(void) { + tCase("class methods: mouse (hover too), key-up and focus run before the universal callbacks"); + + WtFixtureT f = wtFixture("ClassMethods"); + int32_t type = wgtRegisterClass(&sProbeClass); + WidgetT *w = widgetAlloc(f.root, type); + + TEST_TRUE(type >= 0 && w != NULL); + + if (!w) { + tShutdown(); + return; + } + + w->onMouseDown = uniMouseDown; + w->onMouseUp = uniMouseUp; + w->onMouseMove = uniMouseMove; + w->onKeyUp = uniKeyUp; + w->onFocus = uniFocus; + wgtInvalidate(w); + wtSettle(); + + // Hover: two moves with no button + sLog[0] = '\0'; + platformInjectModifiers(KEY_MOD_SHIFT); + widgetOnMouse(f.win, w->x + HOVER_X1, w->y + HOVER_Y1, 0); + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, 0); + platformInjectModifiers(0); + TEST_STR_EQ(sLog, "cM0(5,6)s3 um0 cM0(7,6)s3 um0 "); + + // Left press and release, right press and release + sLog[0] = '\0'; + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, MOUSE_LEFT); + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, 0); + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, MOUSE_RIGHT); + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, 0); + TEST_STR_EQ(sLog, "cD1(7,6)s0 ud1 cU1(7,6)s0 uu1 cD2(7,6)s0 ud2 cU2(7,6)s0 uu2 "); + + // The left press focused the probe (its ON_MOUSE): focus method first + TEST_PTR_EQ(wgtGetFocused(), w); + widgetTransferFocus(NULL); + sLog[0] = '\0'; + wgtSetFocused(w); + TEST_STR_EQ(sLog, "cF uf "); + + sLog[0] = '\0'; + widgetOnKeyUp(f.win, KEY_UP_CODE, KEY_MOD_ALT); + TEST_STR_EQ(sLog, "cK30/8 uk30 "); + + // A class method that destroys its widget: the universal callback is + // not called and nothing touches the freed widget. + sLog[0] = '\0'; + sDestroyOn = true; + widgetOnMouse(f.win, w->x + HOVER_X1, w->y + HOVER_Y1, 0); + sDestroyOn = false; + TEST_STR_EQ(sLog, "cM0(5,6)s0 "); + tShutdown(); + wgtUnregisterClass(type); +} + + +// Real widgets through the generic calls: ENUM, BOOL, INT and STRING +// properties, methods of several signatures. +static void caseGenericAccess(void) { + tCase("generic: create by name, properties and methods on loaded widgets"); + + WtFixtureT f = wtFixture("Generic"); + WidgetT *lb = wgtCreateByName("ListBox", f.root); + WidgetT *lbl = wgtCreateByName("label", f.root); + WidgetT *cb = wgtCreateByName("CheckBox", f.root); + WgtValueT v; + WgtValueT args[LIST_ITEMS]; + WgtValueT ret; + char text[WT_EVENT_TEXT]; + + TEST_TRUE(lb != NULL && lbl != NULL && cb != NULL); + + if (!lb || !lbl || !cb) { + tShutdown(); + return; + } + + args[0] = wgtValueStr("one"); + TEST_TRUE(wgtCallMethod(lb, "AddItem", args, 1, NULL)); + args[0] = wgtValueStr("two"); + TEST_TRUE(wgtCallMethod(lb, "additem", args, 1, NULL)); + TEST_TRUE(wgtCallMethod(lb, "ListCount", NULL, 0, &ret)); + TEST_EQ(ret.type, WGT_IFACE_INT); + TEST_EQ(ret.i, LIST_ITEMS); + args[0] = wgtValueInt(1); + TEST_TRUE(wgtCallMethod(lb, "List", args, 1, &ret)); + TEST_STR_EQ(ret.s, "two"); + args[0] = wgtValueBool(true); + TEST_TRUE(wgtCallMethod(lb, "SetMultiSelect", args, 1, NULL)); + args[0] = wgtValueInt(0); + args[1] = wgtValueStr("True"); + TEST_TRUE(wgtCallMethod(lb, "SetItemSelected", args, LIST_ITEMS, NULL)); + TEST_TRUE(wgtCallMethod(lb, "IsItemSelected", args, 1, &ret)); + TEST_EQ(ret.type, WGT_IFACE_BOOL); + TEST_EQ(ret.i, 1); + v = wgtValueStr("1"); + TEST_TRUE(wgtSetProp(lb, "ListIndex", &v)); + TEST_TRUE(wgtGetProp(lb, "ListIndex", &v)); + TEST_EQ(v.i, 1); + TEST_TRUE(!wgtCallMethod(lb, "List", NULL, 0, &ret)); + + v = wgtValueStr("Right"); + TEST_TRUE(wgtSetProp(lbl, "Alignment", &v)); + TEST_TRUE(wgtGetProp(lbl, "Alignment", &v)); + TEST_EQ(v.type, WGT_IFACE_ENUM); + TEST_EQ(v.i, AlignEndE); + TEST_TRUE(wtPropToString(lbl, wgtIfaceFindProp(wgtGetIfaceOf(lbl, NULL), "Alignment"), text, sizeof(text))); + TEST_STR_EQ(text, "Right"); + + v = wgtValueBool(true); + TEST_TRUE(wgtSetProp(cb, "Value", &v)); + TEST_TRUE(wgtGetProp(cb, "value", &v)); + TEST_EQ(v.i, 1); + TEST_TRUE(!wgtGetProp(cb, "NoSuchProperty", &v)); + tShutdown(); +} + + +// Every registered interface is bound to the class its create function +// makes, so wgtGetIfaceOf finds it from the widget. +static void caseIfaceBinding(void) { + tCase("generic: every interface is bound to its widget class"); + + WtFixtureT f = wtFixture("Binding"); + WidgetT *tabs = wgtCreateByName(TAB_CTRL, f.root); + + for (int32_t i = 0; i < wgtIfaceCount(); i++) { + const char *name = NULL; + const WgtIfaceT *iface = wgtIfaceAt(i, &name); + + if (iface->createSig == WGT_CREATE_PARENT_DATA) { + continue; + } + + WidgetT *w = wgtCreateByName(name, strcmp(name, TAB_PAGE) == 0 ? tabs : f.root); + const char *bound = NULL; + + TEST_TRUE(w != NULL); + + if (w) { + TEST_TRUE(wgtGetIfaceOf(w, &bound) == iface); + TEST_STR_EQ(bound, name); + } + } + + tShutdown(); +} + + +// WCLASS_RAISES_EVENTS (a BASIC UserControl): the class methods see +// every event, the universal mouse, click and key callbacks only what the +// class fires itself; the focus callbacks stay. +static void caseRaisesEvents(void) { + tCase("WCLASS_RAISES_EVENTS: universal callbacks only from wgtFireEvent"); + + WtFixtureT f = wtFixture("RaisesEvents"); + int32_t type = wgtRegisterClass(&sRaisingClass); + WidgetT *w = widgetAlloc(f.root, type); + + TEST_TRUE(type >= 0 && w != NULL); + + if (!w) { + tShutdown(); + return; + } + + w->onMouseDown = uniMouseDown; + w->onMouseUp = uniMouseUp; + w->onMouseMove = uniMouseMove; + w->onClick = uniClick; + w->onKeyDown = uniKeyDown; + w->onKeyUp = uniKeyUp; + w->onFocus = uniFocus; + wgtInvalidate(w); + wtSettle(); + + sLog[0] = '\0'; + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, 0); + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, MOUSE_LEFT); + widgetOnMouse(f.win, w->x + HOVER_X2, w->y + HOVER_Y1, 0); + TEST_STR_EQ(sLog, "cM0(7,6)s0 cD1(7,6)s0 cU1(7,6)s0 "); + + // The focus callbacks still reach the host + widgetTransferFocus(NULL); + sLog[0] = '\0'; + wgtSetFocused(w); + TEST_STR_EQ(sLog, "cF uf "); + + sLog[0] = '\0'; + widgetOnKey(f.win, KEY_UP_CODE, 0); + widgetOnKeyUp(f.win, KEY_UP_CODE, 0); + TEST_STR_EQ(sLog, "cK30/0 "); + + // What the class fires reaches the universal callbacks + sLog[0] = '\0'; + TEST_TRUE(wgtFireEvent(w, "Click", NULL, 0)); + TEST_STR_EQ(sLog, "uc "); + + // Mouse arguments come as (Button, X, Y) or VB's (Button, Shift, X, Y) + WgtValueT plain[] = { wgtValueInt(FIRE_BUTTON), wgtValueInt(FIRE_X), wgtValueInt(FIRE_Y) }; + WgtValueT vb[] = { wgtValueInt(FIRE_BUTTON), wgtValueInt(FIRE_SHIFT), wgtValueInt(FIRE_X), wgtValueInt(FIRE_Y) }; + + w->onMouseDown = firedMouseDown; + sLog[0] = '\0'; + TEST_TRUE(wgtFireEvent(w, "MouseDown", plain, DVX_ARRAY_LEN(plain))); + TEST_TRUE(wgtFireEvent(w, "MouseDown", vb, DVX_ARRAY_LEN(vb))); + TEST_STR_EQ(sLog, "fd1(11,12) fd1(11,12) "); + tShutdown(); + wgtUnregisterClass(type); +} + + +static void firedMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) { + (void)w; + logAdd("fd%d(%d,%d) ", (int)button, (int)x, (int)y); +} + + +static void logAdd(const char *fmt, ...) { + size_t len = strlen(sLog); + va_list ap; + + va_start(ap, fmt); + vsnprintf(sLog + len, sizeof(sLog) - len, fmt, ap); + va_end(ap); +} + + +static void onCanvasMouse(WidgetT *w, int32_t cx, int32_t cy, bool drag) { + (void)w; + (void)drag; + sCanvasX = cx; + sCanvasY = cy; +} + + +static void probeCalcMinSize(WidgetT *w, const BitmapFontT *font) { + (void)font; + w->calcMinW = PROBE_SIZE; + w->calcMinH = PROBE_SIZE; +} + + +static void probeFocus(WidgetT *w) { + (void)w; + logAdd("cF "); +} + + +static void probeKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) { + (void)w; + logAdd("cK%d/%d ", (int)keyCode, (int)shift); +} + + +static void probeMouseEvent(WidgetT *w, WgtMouseActionE action, int32_t button, int32_t x, int32_t y, int32_t shift) { + static const char actionChar[] = { 'D', 'U', 'M' }; + + logAdd("c%c%d(%d,%d)s%d ", actionChar[action], (int)button, (int)x, (int)y, (int)shift); + + if (sDestroyOn) { + wgtDestroy(w); + } +} + + +// Press handler: take focus like a focusable control, and the drag when +// the case asks for capture. +static void probeOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + (void)vx; + (void)vy; + sFocusedWidget = w; + + if (sCapture) { + sDragWidget = w; + } +} + + +static void uniClick(WidgetT *w) { + (void)w; + logAdd("uc "); +} + + +static void uniFocus(WidgetT *w) { + (void)w; + logAdd("uf "); +} + + +static void uniKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { + (void)w; + (void)shift; + logAdd("ukd%d ", (int)keyCode); +} + + +static void uniKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) { + (void)w; + (void)shift; + logAdd("uk%d ", (int)keyCode); +} + + +static void uniMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) { + (void)w; + (void)x; + (void)y; + logAdd("ud%d ", (int)button); +} + + +static void uniMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) { + (void)w; + (void)x; + (void)y; + logAdd("um%d ", (int)button); +} + + +static void uniMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y) { + (void)w; + (void)x; + (void)y; + logAdd("uu%d ", (int)button); +} + + +void wtSectionGeneric(void) { + caseGenericAccess(); + caseIfaceBinding(); + caseClassMethods(); + caseCaptureAndHitWalk(); + caseCanvasBorderless(); + caseRaisesEvents(); +} diff --git a/src/test/widgets/testHelpView.c b/src/test/widgets/testHelpView.c new file mode 100644 index 00000000..67616fe9 --- /dev/null +++ b/src/test/widgets/testHelpView.c @@ -0,0 +1,389 @@ +// 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. + +// testHelpView.c -- HelpView widget (BASIC HelpPane): topics from the help +// compiler's fixture (DVXTEST_HELP_HLP) built into the pane, links followed +// by mouse and keyboard, the link callback rebuilding the pane, re-wrapping +// on resize, Clear and bad topics, and the BASIC interface. + +#include "wtCommon.h" +#include "helpView/helpView.h" +#include "../../libs/kpunch/hlpfile/hlpFile.h" + +#include +#include +#include + +#define HV_WELCOME_CHILDREN 11 // fx.welcome: one widget (or picture row) per record +#define HV_NARROW_DIV 2 +#define HV_TARGET_MAX 64 + +// What the link callback saw and does +typedef struct { + char target[HV_TARGET_MAX]; + int32_t calls; + HlpFileT *showFile; // when set, the callback shows the target topic +} HvLinkSpyT; + +static HvLinkSpyT sSpy; + +// ============================================================ +// Prototypes +// ============================================================ + +// hlpfile.bas entry points (hlpFileBas.c) +void HlpClose(int32_t handle); +int32_t HlpOpen(const char *path); + +static void caseHelpViewBasic(void); +static void caseHelpViewClear(void); +static void caseHelpViewLinks(void); +static void caseHelpViewRewrap(void); +static void caseHelpViewShow(void); +static int32_t childCount(const WidgetT *w); +static WidgetT *contentBox(WidgetT *view); +static WidgetT *firstFocusable(WidgetT *root); +static WidgetT *firstParagraph(WidgetT *box); +static void onLink(WidgetT *w, const char *target); +void wtSectionHelpView(void); + + +// The BASIC face: HelpPane with ShowTopic taking a hlpfile.bas handle, +// Clear, and the Topic / LinkTopic properties. +static void caseHelpViewBasic(void) { + tCase("HelpPane interface"); + + const WgtIfaceT *iface = wgtGetIface("helpview"); + + TEST_TRUE(iface != NULL); + + if (!iface) { + return; + } + + TEST_STR_EQ(iface->basName, "HelpPane"); + TEST_EQ(iface->eventCount, 1); + TEST_STR_EQ(iface->events[0].name, "LinkClick"); + + WtFixtureT f = wtFixture("HelpPane"); + WidgetT *view = wtCreateByIface(iface, "helpview", f.root); + + TEST_TRUE(view != NULL); + + void (*showTopic)(WidgetT *w, int32_t handle, int32_t topic) = NULL; + void (*clear)(WidgetT *w) = NULL; + + for (int32_t i = 0; i < iface->methodCount; i++) { + if (strcmp(iface->methods[i].name, "ShowTopic") == 0 && iface->methods[i].sig == WGT_SIG_INT_INT) { + showTopic = (void (*)(WidgetT *, int32_t, int32_t))iface->methods[i].fn; + } else if (strcmp(iface->methods[i].name, "Clear") == 0 && iface->methods[i].sig == WGT_SIG_VOID) { + clear = (void (*)(WidgetT *))iface->methods[i].fn; + } + } + + TEST_TRUE(showTopic != NULL && clear != NULL); + + const WgtPropDescT *topicProp = wgtIfaceFindProp(iface, "Topic"); + const WgtPropDescT *linkProp = wgtIfaceFindProp(iface, "LinkTopic"); + + TEST_TRUE(topicProp && linkProp && !topicProp->setFn && !linkProp->setFn); + + // The BASIC handle of hlpfile.bas HlpOpen + int32_t h = HlpOpen(DVXTEST_HELP_HLP); + + if (view && showTopic && clear && topicProp && linkProp) { + int32_t (*getTopic)(const WidgetT *w) = (int32_t (*)(const WidgetT *))topicProp->getFn; + const char *(*getLink)(const WidgetT *w) = (const char *(*)(const WidgetT *))linkProp->getFn; + int32_t deep = hlpFileFindTopic(hlpFileBasHandle(h), "fx.deep"); + + showTopic(view, h, deep); + wtSettle(); + TEST_EQ(getTopic(view), deep); + TEST_TRUE(childCount(contentBox(view)) > 0); + TEST_STR_EQ(getLink(view), ""); + + // A bad handle shows nothing + showTopic(view, 0, deep); + TEST_EQ(getTopic(view), -1); + showTopic(view, h, deep); + clear(view); + TEST_EQ(getTopic(view), -1); + TEST_EQ(childCount(contentBox(view)), 0); + } + + HlpClose(h); + tShutdown(); +} + + +// Clear and bad topics leave an empty pane. +static void caseHelpViewClear(void) { + tCase("HelpView Clear and bad topics"); + + WtFixtureT f = wtFixture("HelpView"); + WidgetT *view = wgtHelpView(f.root); + HlpFileT *file = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(view != NULL && file != NULL); + + if (view && file) { + wgtHelpViewShowTopic(view, file, 0); + TEST_EQ(wgtHelpViewGetTopic(view), 0); + wgtHelpViewShowTopic(view, file, hlpFileTopicCount(file)); + TEST_EQ(wgtHelpViewGetTopic(view), -1); + TEST_EQ(childCount(contentBox(view)), 0); + wgtHelpViewShowTopic(view, NULL, 0); + TEST_EQ(wgtHelpViewGetTopic(view), -1); + wgtHelpViewShowTopic(view, file, 0); + wgtHelpViewClear(view); + wtSettle(); + TEST_EQ(wgtHelpViewGetTopic(view), -1); + TEST_EQ(childCount(contentBox(view)), 0); + } + + hlpFileClose(file); + tShutdown(); +} + + +// Links take the focus and follow on a click, Enter or Space: the target +// is kept (LinkTopic) and the callback runs; a callback that shows the +// target rebuilds the pane under the link that was clicked. +static void caseHelpViewLinks(void) { + tCase("HelpView links by mouse and keyboard"); + + WtFixtureT f = wtFixture("HelpView"); + WidgetT *view = wgtHelpView(f.root); + HlpFileT *file = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(view != NULL && file != NULL); + + if (!view || !file) { + hlpFileClose(file); + tShutdown(); + return; + } + + view->weight = WGT_WEIGHT_FILL; + memset(&sSpy, 0, sizeof(sSpy)); + + // No callback: following a link only records its target. fx.extra + // starts with its link (in view for a click). + int32_t extra = hlpFileFindTopic(file, "fx.extra"); + + wgtHelpViewShowTopic(view, file, extra); + wtSettle(); + + WidgetT *link = firstFocusable(view); + + TEST_TRUE(link != NULL); + + if (link) { + tWidgetClick(link); + wtSettle(); + TEST_STR_EQ(wgtHelpViewGetLinkTarget(view), "fx.welcome"); + } + + // Mouse with a callback; LinkClick (Topic) follows through onEvent + wgtHelpViewSetLinkCallback(view, onLink); + view->onEvent = wtOnEvent; + wtEventSpyReset(); + link = firstFocusable(view); + + if (link) { + tWidgetClick(link); + wtSettle(); + TEST_EQ(sSpy.calls, 1); + TEST_STR_EQ(sSpy.target, "fx.welcome"); + TEST_STR_EQ(wtEventSpy()->name, "LinkClick"); + TEST_STR_EQ(wtEventSpy()->text, "fx.welcome"); + } + + view->onEvent = NULL; + + // Keyboard: Enter and Space on the focused link (the welcome topic's + // first link is below its pictures) + wgtHelpViewShowTopic(view, file, hlpFileFindTopic(file, "fx.welcome")); + wtSettle(); + link = firstFocusable(view); + + if (link) { + wgtSetFocused(link); + tWidgetKey(f.win, KEY_ENTER, 0); + TEST_EQ(sSpy.calls, 2); + TEST_STR_EQ(sSpy.target, "fx.blocks"); + tWidgetKey(f.win, KEY_SPACE, 0); + TEST_EQ(sSpy.calls, 3); + } + + // The callback shows the target: the pane is rebuilt from inside the + // link's own handler + sSpy.showFile = file; + wgtHelpViewShowTopic(view, file, extra); + wtSettle(); + link = firstFocusable(view); + + if (link) { + tWidgetClick(link); + wtSettle(); + TEST_EQ(wgtHelpViewGetTopic(view), hlpFileFindTopic(file, "fx.welcome")); + TEST_STR_EQ(wgtHelpViewGetLinkTarget(view), "fx.welcome"); + } + + // The pane keeps copies: the file can close while it is shown + hlpFileClose(file); + wgtInvalidate(view); + wtSettle(); + TEST_TRUE(childCount(contentBox(view)) > 0); + tShutdown(); +} + + +// Text re-wraps when the pane gets narrower (measured before the new +// width is known, corrected in the layout). +static void caseHelpViewRewrap(void) { + tCase("HelpView re-wraps on resize"); + + WtFixtureT f = wtFixture("HelpView"); + WidgetT *view = wgtHelpView(f.root); + HlpFileT *file = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(view != NULL && file != NULL); + + if (view && file) { + view->weight = WGT_WEIGHT_FILL; + wgtHelpViewShowTopic(view, file, hlpFileFindTopic(file, "fx.welcome")); + wtSettle(); + + WidgetT *text = firstParagraph(contentBox(view)); + int32_t tall = text ? text->calcMinH : 0; + + TEST_TRUE(text != NULL && tall > 0); + dvxResizeWindow(tCtx(), f.win, f.win->w / HV_NARROW_DIV, f.win->h); + wtSettle(); + TEST_TRUE(text && text->calcMinH > tall); + } + + hlpFileClose(file); + tShutdown(); +} + + +// A topic becomes one widget per record (pictures that are centered or +// right-aligned sit in a row box). +static void caseHelpViewShow(void) { + tCase("HelpView shows a topic"); + + WtFixtureT f = wtFixture("HelpView"); + WidgetT *view = wgtHelpView(f.root); + HlpFileT *file = hlpFileOpen(DVXTEST_HELP_HLP); + + TEST_TRUE(view != NULL && file != NULL); + + if (view && file) { + int32_t welcome = hlpFileFindTopic(file, "fx.welcome"); + + view->weight = WGT_WEIGHT_FILL; + TEST_EQ(wgtHelpViewGetTopic(view), -1); + wgtHelpViewShowTopic(view, file, welcome); + wtSettle(); + TEST_EQ(wgtHelpViewGetTopic(view), welcome); + + // heading, 2 paragraphs, 3 pictures, heading, 2 links, heading, rule + TEST_EQ(childCount(contentBox(view)), HV_WELCOME_CHILDREN); + + // The blocks topic (lists, code, table, notes) builds too + wgtHelpViewShowTopic(view, file, hlpFileFindTopic(file, "fx.blocks")); + wtSettle(); + TEST_TRUE(childCount(contentBox(view)) > 0); + } + + hlpFileClose(file); + tShutdown(); +} + + +static int32_t childCount(const WidgetT *w) { + int32_t n = 0; + + for (const WidgetT *c = w ? w->firstChild : NULL; c; c = c->nextSibling) { + n++; + } + + return n; +} + + +// The VBox the records go in: HelpView > ScrollPane > VBox. +static WidgetT *contentBox(WidgetT *view) { + WidgetT *scroll = view ? view->firstChild : NULL; + + return scroll ? scroll->firstChild : NULL; +} + + +static WidgetT *firstFocusable(WidgetT *root) { + if (!root) { + return NULL; + } + + if (root->wclass && (root->wclass->flags & WCLASS_FOCUSABLE)) { + return root; + } + + for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { + WidgetT *hit = firstFocusable(c); + + if (hit) { + return hit; + } + } + + return NULL; +} + + +// The first paragraph: the first child after the heading. +static WidgetT *firstParagraph(WidgetT *box) { + WidgetT *heading = box ? box->firstChild : NULL; + + return heading ? heading->nextSibling : NULL; +} + + +static void onLink(WidgetT *w, const char *target) { + snprintf(sSpy.target, sizeof(sSpy.target), "%s", target); + sSpy.calls++; + + if (sSpy.showFile) { + wgtHelpViewShowTopic(w, sSpy.showFile, hlpFileFindTopic(sSpy.showFile, target)); + } +} + + +void wtSectionHelpView(void) { + caseHelpViewShow(); + caseHelpViewLinks(); + caseHelpViewRewrap(); + caseHelpViewClear(); + caseHelpViewBasic(); +} diff --git a/src/test/widgets/testProps.c b/src/test/widgets/testProps.c index 54d2e76a..81a8db24 100644 --- a/src/test/widgets/testProps.c +++ b/src/test/widgets/testProps.c @@ -39,9 +39,14 @@ #include "wtCommon.h" #include "box/box.h" +#include "comboBox/comboBox.h" #include "label/label.h" +#include "progressBar/progress.h" #include "radio/radio.h" +#include "scrollPane/scrlPane.h" #include "separator/separatr.h" +#include "splitter/splitter.h" +#include "statusBar/statBar.h" #include "wrapBox/wrapBox.h" #include @@ -54,6 +59,8 @@ #define SHORT_STR "abc" #define METHOD_INT_ARG 0 #define METHOD_STR_ARG "x" +#define BOOL_PASSES 2 // method calls with Boolean arguments: False, then True +#define PROP_TYPE_BAD 0x7F // no WGT_IFACE_* value #define TAB_PAGE_NAME "tabpage" #define TAB_CTRL_NAME "tabcontrol" #define RADIO_COUNT 3 @@ -64,30 +71,45 @@ #define WRAP_BOX_W 120 #define WRAP_CHILDREN 6 #define WRAP_KEEP 1 +#define COMBO_MAX_LEN 32 +#define COMBO_CUT_LEN 4 +#define COMBO_DEFAULT 256 +#define SP_PAD 5 +#define SP_GAP 6 +#define SP_ROWS 40 +#define SP_SCROLL 10 +#define SPLIT_POS 50 // Common events every control gets from the form runtime (dvxWgt.h). static const char *sCommonEvents[] = { "Click", "DblClick", "Change", "GotFocus", "LostFocus", NULL }; -// String setters that load a file rather than store the string: the -// getter reports what was loaded, so only the fixed-point rule applies. -static const char *sLoaderProps[] = { "Picture", NULL }; +// String setters that do not store the string as given: Picture loads a +// file (the getter reports what was loaded), PasswordChar keeps one +// character. Only the fixed-point rule applies to them. +static const char *sFixedPointProps[] = { "PasswordChar", "Picture", "SelText", NULL }; // ============================================================ // Prototypes // ============================================================ +static void caseComboBoxMaxLength(void); static void caseFrameSpacing(void); static void caseIface(int32_t idx); static void caseLabelAlign(void); static void caseLabelClearsBackground(void); static void caseOptionButtonValue(void); +static void caseProgressBarVertical(void); static void caseRadioGroupRepaint(void); static void caseRadioIndexDynamic(void); +static void caseScrollPaneGaps(void); static void caseSeparatorInBounds(void); +static void caseSplitterVertical(void); +static void caseStatusBarPanes(void); static void caseUnsupportedTypes(void); static void caseWrapBoxRelayout(void); static void checkBool(WidgetT *w, const WgtPropDescT *p, const char *label); +static void checkDouble(WidgetT *w, const WgtPropDescT *p, const char *label); static void checkEnum(WidgetT *w, const WgtPropDescT *p, const char *label); static void checkEvents(const WgtIfaceT *iface, const char *label); static void checkFixedPoint(WidgetT *w, const WgtPropDescT *p, const char *label, const char *val); @@ -95,7 +117,7 @@ static void checkInt(WidgetT *w, const WgtPropDescT *p, const char *label); static void checkMethods(WidgetT *w, const WgtIfaceT *iface, const char *label); static void checkReadOnly(WidgetT *w, const WgtPropDescT *p, const char *label); static void checkString(WidgetT *w, const WgtPropDescT *p, const char *label); -static bool isLoaderProp(const char *name); +static bool isFixedPointProp(const char *name); static void paintAlone(WidgetT *w); static uint32_t paintedAt(int32_t x, int32_t y); static bool roundTrip(WidgetT *w, const WgtPropDescT *p, const char *label, const char *in, const char *expect); @@ -105,6 +127,24 @@ static bool roundTrip(WidgetT *w, const WgtPropDescT *p, const char *label, c static uint32_t sPaintBuf[PAINT_W * PAINT_H]; +static void caseComboBoxMaxLength(void) { + tCase("combobox: MaxLength resizes the edit field and cuts the text"); + + WtFixtureT f = wtFixture("ComboMax"); + WidgetT *cb = wgtComboBox(f.root, COMBO_MAX_LEN); + + TEST_EQ(wgtComboBoxGetMaxLength(cb), COMBO_MAX_LEN); + wgtSetText(cb, "abcdefgh"); + wgtComboBoxSetMaxLength(cb, COMBO_CUT_LEN); + TEST_EQ(wgtComboBoxGetMaxLength(cb), COMBO_CUT_LEN); + TEST_STR_EQ(wgtGetText(cb), "abcd"); + wgtComboBoxSetMaxLength(cb, 0); + TEST_EQ(wgtComboBoxGetMaxLength(cb), COMBO_DEFAULT); + TEST_STR_EQ(wgtGetText(cb), "abcd"); + tShutdown(); +} + + static void caseFrameSpacing(void) { tCase("frame: Spacing property separates children"); @@ -187,6 +227,10 @@ static void caseIface(int32_t idx) { checkString(w, p, plabel); break; } + case WGT_IFACE_DOUBLE: { + checkDouble(w, p, plabel); + break; + } default: { printf(" %s: unexpected property type %d\n", plabel, p->type); TEST_TRUE(false); @@ -316,6 +360,30 @@ static void caseOptionButtonValue(void) { } +// Vertical swaps the bar's minimum size axes (the layout is redone). +static void caseProgressBarVertical(void) { + tCase("progressbar: Vertical swaps the minimum size"); + + WtFixtureT f = wtFixture("PbVert"); + WidgetT *pb = wgtProgressBar(f.root); + + wtSettle(); + + int32_t minW = pb->calcMinW; + int32_t minH = pb->calcMinH; + + TEST_TRUE(!wgtProgressBarGetVertical(pb)); + TEST_TRUE(minW > minH); + wgtProgressBarSetVertical(pb, true); + TEST_TRUE(wgtProgressBarGetVertical(pb)); + TEST_EQ(pb->calcMinW, minH); + TEST_EQ(pb->calcMinH, minW); + wgtProgressBarSetVertical(pb, false); + TEST_EQ(pb->calcMinW, minW); + tShutdown(); +} + + static void caseRadioGroupRepaint(void) { tCase("radioGroup: SetSelected repaints the old and new radio"); @@ -362,6 +430,60 @@ static void caseRadioIndexDynamic(void) { } +// Padding / Spacing through the shared libdvx helpers, and the scroll +// position (clamped by the layout). +static void caseScrollPaneGaps(void) { + tCase("scrollpane: Padding, Spacing, ScrollTop/ScrollLeft, ScrollToTop"); + + WtFixtureT f = wtFixture("SpGaps"); + WidgetT *sp = wgtScrollPane(f.root); + WidgetT *a = wgtLabel(sp, "A"); + WidgetT *b = wgtLabel(sp, "B"); + + for (int32_t i = 0; i < SP_ROWS; i++) { + wgtLabel(sp, "row"); + } + + wtSettle(); + + int32_t x0 = a->x; + int32_t y0 = a->y; + + TEST_EQ(wgtGetPaddingPx(sp), 0); + wgtSetPaddingPx(sp, SP_PAD); + wgtSetSpacingPx(sp, SP_GAP); + wtSettle(); + TEST_EQ(wgtGetPaddingPx(sp), SP_PAD); + TEST_EQ(wgtGetSpacingPx(sp), SP_GAP); + TEST_EQ(a->x, x0 + SP_PAD); + TEST_EQ(a->y, y0 + SP_PAD); + TEST_EQ(b->y, a->y + a->h + SP_GAP); + wgtSetPaddingPx(sp, -1); + TEST_EQ(wgtGetPaddingPx(sp), 0); + wgtSetPaddingPx(sp, INT32_MAX); + TEST_EQ(wgtGetPaddingPx(sp), WGT_GAP_PIXELS_MAX); + wgtSetPaddingPx(sp, 0); + wtSettle(); + TEST_EQ(a->y, y0); + + wgtScrollPaneSetScrollTop(sp, SP_SCROLL); + wtSettle(); + TEST_EQ(wgtScrollPaneGetScrollTop(sp), SP_SCROLL); + TEST_EQ(a->y, y0 - SP_SCROLL); + wgtScrollPaneSetScrollTop(sp, INT32_MAX); + wtSettle(); + TEST_TRUE(wgtScrollPaneGetScrollTop(sp) > SP_SCROLL); + TEST_TRUE(wgtScrollPaneGetScrollTop(sp) < INT32_MAX); + wgtScrollPaneSetScrollLeft(sp, SP_SCROLL); + wtSettle(); + TEST_EQ(wgtScrollPaneGetScrollLeft(sp), 0); + wgtScrollPaneScrollToTop(sp); + TEST_EQ(wgtScrollPaneGetScrollTop(sp), 0); + TEST_EQ(a->y, y0); + tShutdown(); +} + + static void caseSeparatorInBounds(void) { tCase("separator: etched pair stays inside a minimum-size widget"); @@ -382,8 +504,59 @@ static void caseSeparatorInBounds(void) { } +// Vertical = False puts the panes above and below each other; the +// position then measures down from the top. +static void caseSplitterVertical(void) { + tCase("splitter: Vertical switches the orientation at run time"); + + WtFixtureT f = wtFixture("SplVert"); + WidgetT *spl = wgtSplitter(f.root, true); + WidgetT *l = wgtLabel(spl, "L"); + WidgetT *r = wgtLabel(spl, "R"); + + wgtSplitterSetPos(spl, SPLIT_POS); + wtSettle(); + TEST_TRUE(wgtSplitterGetVertical(spl)); + TEST_EQ(l->y, r->y); + TEST_TRUE(r->x > l->x); + TEST_EQ(l->w, SPLIT_POS); + wgtSplitterSetVertical(spl, false); + wtSettle(); + TEST_TRUE(!wgtSplitterGetVertical(spl)); + TEST_EQ(l->x, r->x); + TEST_TRUE(r->y > l->y); + TEST_EQ(l->h, SPLIT_POS); + TEST_EQ(wgtSplitterGetPos(spl), SPLIT_POS); + tShutdown(); +} + + +// A StatusBar is a container (BASIC nests Labels in it as panes): the +// panes sit side by side on one row. +static void caseStatusBarPanes(void) { + tCase("statusbar: container of side-by-side panes"); + + const WgtIfaceT *iface = wgtGetIface("statusbar"); + + TEST_TRUE(iface != NULL && iface->isContainer); + + WtFixtureT f = wtFixture("SbPanes"); + WidgetT *sb = wtCreateByIface(iface, "statusbar", f.root); + WidgetT *p1 = wgtLabel(sb, "Ready"); + WidgetT *p2 = wgtLabel(sb, "Line 1"); + + p1->weight = WGT_WEIGHT_FILL; + wtSettle(); + TEST_PTR_EQ(p1->parent, sb); + TEST_EQ(p1->y, p2->y); + TEST_TRUE(p2->x > p1->x + p1->w); + TEST_TRUE(p2->x + p2->w <= sb->x + sb->w); + tShutdown(); +} + + static void caseUnsupportedTypes(void) { - tCase("float property type is refused by the string path"); + tCase("an unknown property type is refused by the string path"); WtFixtureT f = wtFixture("Float"); WidgetT *w = wtCreateByIface(wgtGetIface("label"), "label", f.root); @@ -396,7 +569,7 @@ static void caseUnsupportedTypes(void) { memset(&fake, 0, sizeof(fake)); fake.name = "Fake"; - fake.type = WGT_IFACE_FLOAT; + fake.type = PROP_TYPE_BAD; fake.getFn = (void *)wgtGetText; fake.setFn = (void *)wgtSetText; TEST_TRUE(!wtPropApply(w, &fake, "1.5")); @@ -453,6 +626,28 @@ static void checkBool(WidgetT *w, const WgtPropDescT *p, const char *label) { } +// A Double property: tiny, huge, negative and fractional values survive a +// round trip through text, then the original comes back. +static void checkDouble(WidgetT *w, const WgtPropDescT *p, const char *label) { + static const char *const values[] = { "0", "2.5", "-1.25", "1e300", "-1e300", "\"7\"" }; + char orig[PROP_BUF]; + char out[PROP_BUF]; + + TEST_TRUE(wtPropToString(w, p, orig, sizeof(orig))); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(values); i++) { + wtPropApply(w, p, values[i]); + + if (!wtPropToString(w, p, out, sizeof(out))) { + printf(" %s: unreadable after %s\n", label, values[i]); + TEST_TRUE(false); + } + } + + TEST_TRUE(wtPropApply(w, p, orig)); +} + + static void checkEnum(WidgetT *w, const WgtPropDescT *p, const char *label) { TEST_TRUE(p->enumNames != NULL); @@ -554,10 +749,15 @@ static void checkFixedPoint(WidgetT *w, const WgtPropDescT *p, const char *label } +// The extremes and small values, then the original value back so a +// property that limits another (TextBox MaxLength cuts Text) does not +// change the later properties' checks. static void checkInt(WidgetT *w, const WgtPropDescT *p, const char *label) { char minStr[PROP_BUF]; char maxStr[PROP_BUF]; + char orig[PROP_BUF]; + TEST_TRUE(wtPropToString(w, p, orig, sizeof(orig))); snprintf(minStr, sizeof(minStr), "%d", (int)INT32_MIN); snprintf(maxStr, sizeof(maxStr), "%d", (int)INT32_MAX); checkFixedPoint(w, p, label, minStr); @@ -566,6 +766,7 @@ static void checkInt(WidgetT *w, const WgtPropDescT *p, const char *label) { checkFixedPoint(w, p, label, "1"); checkFixedPoint(w, p, label, "-1"); checkFixedPoint(w, p, label, "\"7\""); + TEST_TRUE(wtPropApply(w, p, orig)); } @@ -589,108 +790,51 @@ static void checkMethods(WidgetT *w, const WgtIfaceT *iface, const char *label) } } - switch (m->sig) { - case WGT_SIG_VOID: { - ((void (*)(WidgetT *))m->fn)(w); - break; + // A dynamic method takes its arguments as they come: it must + // survive any count (here none and one) + if (m->sig == WGT_SIG_DYNAMIC) { + WgtValueT one = wgtValueStr(METHOD_STR_ARG); + WgtValueT dynRet; + + wgtIfaceCallMethod(iface, w, m, NULL, 0, &dynRet); + wgtIfaceCallMethod(iface, w, m, &one, 1, &dynRet); + wtSettle(); + continue; + } + + // Through the one marshalling path, every signature it knows; a + // signature with Boolean arguments runs with False, then True. + const WgtSigInfoT *info = wgtSigInfo(m->sig); + + if (!info) { + printf(" %s.%s: unknown method signature %d\n", label, m->name, m->sig); + TEST_TRUE(false); + continue; + } + + WgtValueT args[WGT_SIG_MAX_ARGS]; + bool hasBool = false; + + for (int32_t pass = 0; pass < BOOL_PASSES && (pass == 0 || hasBool); pass++) { + for (int32_t k = 0; k < info->argc; k++) { + hasBool |= info->argTypes[k] == WGT_IFACE_BOOL; + + if (info->argTypes[k] == WGT_IFACE_STRING) { + args[k] = wgtValueStr(METHOD_STR_ARG); + } else if (info->argTypes[k] == WGT_IFACE_BOOL) { + args[k] = wgtValueBool(pass != 0); + } else { + args[k] = wgtValueInt(METHOD_INT_ARG); + } } - case WGT_SIG_INT: { - ((void (*)(WidgetT *, int32_t))m->fn)(w, METHOD_INT_ARG); - break; - } - case WGT_SIG_BOOL: { - ((void (*)(WidgetT *, bool))m->fn)(w, false); - ((void (*)(WidgetT *, bool))m->fn)(w, true); - break; - } - case WGT_SIG_STR: { - ((void (*)(WidgetT *, const char *))m->fn)(w, METHOD_STR_ARG); - break; - } - case WGT_SIG_INT_INT: { - ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_INT_BOOL: { - ((void (*)(WidgetT *, int32_t, bool))m->fn)(w, METHOD_INT_ARG, true); - break; - } - case WGT_SIG_RET_INT: { - (void)((int32_t (*)(const WidgetT *))m->fn)(w); - break; - } - case WGT_SIG_RET_BOOL: { - (void)((bool (*)(const WidgetT *))m->fn)(w); - break; - } - case WGT_SIG_RET_BOOL_INT: { - (void)((bool (*)(const WidgetT *, int32_t))m->fn)(w, METHOD_INT_ARG); - break; - } - case WGT_SIG_INT3: { - ((void (*)(WidgetT *, int32_t, int32_t, int32_t))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_INT4: { - ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_RET_INT_INT_INT: { - (void)((int32_t (*)(const WidgetT *, int32_t, int32_t))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_INT_INT_STR: { - ((void (*)(WidgetT *, int32_t, int32_t, const char *))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_STR_ARG); - break; - } - case WGT_SIG_INT5: { - ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_INT_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_RET_STR_INT: { - (void)((const char *(*)(const WidgetT *, int32_t))m->fn)(w, METHOD_INT_ARG); - break; - } - case WGT_SIG_RET_STR_INT_INT: { - (void)((const char *(*)(const WidgetT *, int32_t, int32_t))m->fn)(w, METHOD_INT_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_STR_BOOL_BOOL: { - (void)((bool (*)(WidgetT *, const char *, bool, bool))m->fn)(w, METHOD_STR_ARG, false, true); - break; - } - case WGT_SIG_STR_STR_BOOL: { - (void)((int32_t (*)(WidgetT *, const char *, const char *, bool))m->fn)(w, METHOD_STR_ARG, METHOD_STR_ARG, false); - break; - } - case WGT_SIG_RET_STR: { - (void)((const char *(*)(const WidgetT *))m->fn)(w); - break; - } - case WGT_SIG_STR_INT: { - ((void (*)(WidgetT *, const char *, int32_t))m->fn)(w, METHOD_STR_ARG, METHOD_INT_ARG); - break; - } - case WGT_SIG_INT_STR: { - ((void (*)(WidgetT *, int32_t, const char *))m->fn)(w, METHOD_INT_ARG, METHOD_STR_ARG); - break; - } - case WGT_SIG_STR_STR: { - ((void (*)(WidgetT *, const char *, const char *))m->fn)(w, METHOD_STR_ARG, METHOD_STR_ARG); - break; - } - case WGT_SIG_STR_BOOL: { - ((void (*)(WidgetT *, const char *, bool))m->fn)(w, METHOD_STR_ARG, true); - break; - } - case WGT_SIG_RET_INT_STR: { - (void)((int32_t (*)(WidgetT *, const char *))m->fn)(w, METHOD_STR_ARG); - break; - } - default: { - printf(" %s.%s: unknown method signature %d\n", label, m->name, m->sig); - TEST_TRUE(false); - break; + + WgtValueT ret; + + TEST_TRUE(wgtIfaceCallMethod(iface, w, m, args, info->argc, &ret)); + TEST_EQ(ret.type, info->retType); + + if (ret.type == WGT_IFACE_STRING) { + TEST_TRUE(ret.s != NULL); } } @@ -722,7 +866,7 @@ static void checkString(WidgetT *w, const WgtPropDescT *p, const char *label) { longStr[LONG_STR_LEN] = '\0'; - if (isLoaderProp(p->name)) { + if (isFixedPointProp(p->name)) { checkFixedPoint(w, p, label, ""); checkFixedPoint(w, p, label, SHORT_STR); checkFixedPoint(w, p, label, longStr); @@ -737,9 +881,9 @@ static void checkString(WidgetT *w, const WgtPropDescT *p, const char *label) { } -static bool isLoaderProp(const char *name) { - for (int32_t i = 0; sLoaderProps[i]; i++) { - if (strcasecmp(sLoaderProps[i], name) == 0) { +static bool isFixedPointProp(const char *name) { + for (int32_t i = 0; sFixedPointProps[i]; i++) { + if (strcasecmp(sFixedPointProps[i], name) == 0) { return true; } } @@ -809,12 +953,17 @@ void wtSectionProps(void) { } caseUnsupportedTypes(); + caseComboBoxMaxLength(); caseFrameSpacing(); caseLabelAlign(); caseLabelClearsBackground(); caseOptionButtonValue(); + caseProgressBarVertical(); caseRadioGroupRepaint(); caseRadioIndexDynamic(); + caseScrollPaneGaps(); caseSeparatorInBounds(); + caseSplitterVertical(); + caseStatusBarPanes(); caseWrapBoxRelayout(); } diff --git a/src/test/widgets/widgetsMain.c b/src/test/widgets/widgetsMain.c index c3efeaa2..8f0fd711 100644 --- a/src/test/widgets/widgetsMain.c +++ b/src/test/widgets/widgetsMain.c @@ -38,6 +38,7 @@ int main(void) { wtSectionFocus(); wtSectionDestroy(); wtSectionHover(); + wtSectionGeneric(); wtSectionCovListView(); wtSectionCovTreeView(); wtSectionCovTerm(); @@ -45,5 +46,6 @@ int main(void) { wtSectionCovMisc(); wtSectionCovText(); wtSectionData(); + wtSectionHelpView(); return tSuiteEnd(); } diff --git a/src/test/widgets/wtCommon.c b/src/test/widgets/wtCommon.c index b543c419..bf105339 100644 --- a/src/test/widgets/wtCommon.c +++ b/src/test/widgets/wtCommon.c @@ -36,38 +36,14 @@ // Prototypes // ============================================================ -static int32_t enumIndex(const WgtPropDescT *p, const char *name); -static bool parseBool(const char *val); // Root of the current fixture window (NULL between cases) so wtSettle can // force a relayout after widgets were added without an invalidate. static WidgetT *sFixtureRoot = NULL; - -static int32_t enumIndex(const WgtPropDescT *p, const char *name) { - if (!p->enumNames || !name) { - return -1; - } - - for (int32_t en = 0; p->enumNames[en]; en++) { - if (strcasecmp(p->enumNames[en], name) == 0) { - return en; - } - } - - return -1; -} - - -// Mirrors frmParseBool: "True"/"-1" true, "False"/"0" false, else false. -static bool parseBool(const char *val) { - if (strcasecmp(val, "True") == 0 || strcmp(val, "-1") == 0) { - return true; - } - - return false; -} +// wtOnEvent's record +static WtEventSpyT sEventSpy; void wtClickAt(WindowT *win, int32_t x, int32_t y) { @@ -87,47 +63,7 @@ uint32_t wtContentPixel(const WindowT *win, int32_t x, int32_t y) { WidgetT *wtCreateByIface(const WgtIfaceT *iface, const char *name, WidgetT *parent) { - const WgtApiPrefixT *api = (const WgtApiPrefixT *)wgtGetApi(name, sizeof(WgtApiPrefixT)); - - if (!api || !iface) { - return NULL; - } - - typedef WidgetT *(*CreateParentFnT)(WidgetT *); - typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *); - typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t); - typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t); - typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t); - typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); - typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool); - typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t); - - switch (iface->createSig) { - case WGT_CREATE_PARENT_TEXT: { - return ((CreateParentTextFnT)api->create)(parent, ""); - } - case WGT_CREATE_PARENT_INT: { - return ((CreateParentIntFnT)api->create)(parent, iface->createArgs[0]); - } - case WGT_CREATE_PARENT_INT_INT: { - return ((CreateParentIntIntFnT)api->create)(parent, iface->createArgs[0], iface->createArgs[1]); - } - case WGT_CREATE_PARENT_INT_INT_INT: { - return ((CreateParentIntIntIntFnT)api->create)(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]); - } - case WGT_CREATE_PARENT_INT_BOOL: { - return ((CreateParentIntBoolFnT)api->create)(parent, iface->createArgs[0], (bool)iface->createArgs[1]); - } - case WGT_CREATE_PARENT_BOOL: { - return ((CreateParentBoolFnT)api->create)(parent, (bool)iface->createArgs[0]); - } - case WGT_CREATE_PARENT_DATA: { - return ((CreateDataFnT)api->create)(parent, NULL, 0, 0, 0); - } - default: { - return ((CreateParentFnT)api->create)(parent); - } - } + return iface ? wgtCreateByName(name, parent) : NULL; } @@ -140,6 +76,16 @@ void wtDrag(WindowT *win, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { } +const WtEventSpyT *wtEventSpy(void) { + return &sEventSpy; +} + + +void wtEventSpyReset(void) { + memset(&sEventSpy, 0, sizeof(sEventSpy)); +} + + WtFixtureT wtFixture(const char *title) { WtFixtureT f; @@ -152,8 +98,20 @@ WtFixtureT wtFixture(const char *title) { } +void wtOnEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) { + char buf[WGT_VALUE_TEXT_MAX]; + + (void)w; + sEventSpy.count++; + sEventSpy.argc = argc; + sEventSpy.arg0 = (argc > 0) ? wgtValueToInt(&args[0]) : 0; + snprintf(sEventSpy.name, sizeof(sEventSpy.name), "%s", name); + snprintf(sEventSpy.text, sizeof(sEventSpy.text), "%s", (argc > 0) ? wgtValueToText(&args[0], buf, (int32_t)sizeof(buf)) : ""); +} + + bool wtPropApply(WidgetT *w, const WgtPropDescT *p, const char *val) { - if (!w || !p || !p->setFn || !val) { + if (!val) { return false; } @@ -166,72 +124,12 @@ bool wtPropApply(WidgetT *w, const WgtPropDescT *p, const char *val) { val = buf; } - if (p->type == WGT_IFACE_ENUM) { - int32_t en = enumIndex(p, val); - - if (en < 0) { - return false; - } - - ((void (*)(WidgetT *, int32_t))p->setFn)(w, en); - return true; - } - - if (p->type == WGT_IFACE_INT) { - ((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val)); - return true; - } - - if (p->type == WGT_IFACE_BOOL) { - ((void (*)(WidgetT *, bool))p->setFn)(w, parseBool(val)); - return true; - } - - if (p->type == WGT_IFACE_STRING) { - ((void (*)(WidgetT *, const char *))p->setFn)(w, val); - return true; - } - - return false; + return wgtIfaceSetPropText(wgtGetIfaceOf(w, NULL), w, p, val); } -bool wtPropToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize) { - if (!w || !p || !p->getFn || !out || outSize <= 0) { - return false; - } - - if (p->type == WGT_IFACE_ENUM && p->enumNames) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); - - for (int32_t en = 0; p->enumNames[en]; en++) { - if (en == v) { - snprintf(out, outSize, "%s", p->enumNames[en]); - return true; - } - } - - return false; - } - - if (p->type == WGT_IFACE_INT) { - snprintf(out, outSize, "%d", (int)((int32_t (*)(const WidgetT *))p->getFn)(w)); - return true; - } - - if (p->type == WGT_IFACE_BOOL) { - snprintf(out, outSize, "%s", ((bool (*)(const WidgetT *))p->getFn)(w) ? "True" : "False"); - return true; - } - - if (p->type == WGT_IFACE_STRING) { - const char *s = ((const char *(*)(const WidgetT *))p->getFn)(w); - - snprintf(out, outSize, "%s", s ? s : ""); - return true; - } - - return false; +bool wtPropToString(WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize) { + return wgtIfaceGetPropText(wgtGetIfaceOf(w, NULL), w, p, out, outSize); } diff --git a/src/test/widgets/wtCommon.h b/src/test/widgets/wtCommon.h index 54d44a17..823641bc 100644 --- a/src/test/widgets/wtCommon.h +++ b/src/test/widgets/wtCommon.h @@ -24,10 +24,9 @@ // // Every section builds a window through wtFixture, drives it with the // dvxtest helpers and asserts on widget state. wtPropApply / -// wtPropToString mirror the form runtime's wgtApplyPropFromString / -// wgtPropValueToString (formrt.c), which cannot be linked on the host -// (formrt.c carries i386 inline asm) -- keep them in sync until the -// helpers move into libdvx. +// wtPropToString are libdvx's wgtIfaceSetPropText / wgtIfaceGetPropText +// (the .frm text forms the form runtime uses) on the widget's own +// interface. #ifndef WT_COMMON_H #define WT_COMMON_H @@ -50,6 +49,19 @@ typedef struct { WidgetT *root; } WtFixtureT; +// What wtOnEvent (a WidgetT.onEvent) saw last: the event name, its +// argument count, the first argument as an integer and as text, and how +// many events arrived since wtEventSpyReset. +#define WT_EVENT_TEXT 64 + +typedef struct { + char name[WT_EVENT_TEXT]; + char text[WT_EVENT_TEXT]; + int32_t count; + int32_t argc; + int32_t arg0; +} WtEventSpyT; + // Content-relative drag through widgetOnMouse: press at (x0,y0), move to // the midpoint, move to (x1,y1), release. void wtDrag(WindowT *win, int32_t x0, int32_t y0, int32_t x1, int32_t y1); @@ -63,14 +75,19 @@ WtFixtureT wtFixture(const char *title); // Content-relative single click through widgetOnMouse (move, press, release). void wtClickAt(WindowT *win, int32_t x, int32_t y); -// Create a widget from an interface descriptor exactly like the form runtime -// (createWidgetByIface): dispatch on createSig with the default args. +// The onEvent recorder (see WtEventSpyT). +const WtEventSpyT *wtEventSpy(void); +void wtEventSpyReset(void); +void wtOnEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc); + +// Create a widget exactly like the form runtime: libdvx wgtCreateByName +// (NULL when iface is NULL). WidgetT *wtCreateByIface(const WgtIfaceT *iface, const char *name, WidgetT *parent); // Property string helpers (see header comment). wtPropApply strips one // pair of surrounding double quotes first, as the .frm parser does. bool wtPropApply(WidgetT *w, const WgtPropDescT *p, const char *val); -bool wtPropToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); +bool wtPropToString(WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); // PC set-1 scancode for a lowercase letter (Alt+letter accelerators). int32_t wtScanForLetter(char c); @@ -89,6 +106,8 @@ void wtSectionCovTreeView(void); void wtSectionData(void); void wtSectionDestroy(void); void wtSectionFocus(void); +void wtSectionGeneric(void); +void wtSectionHelpView(void); void wtSectionHover(void); void wtSectionInput(void); void wtSectionLists(void); diff --git a/src/test/wm/wmFixes.c b/src/test/wm/wmFixes.c index 075e908d..ead64e45 100644 --- a/src/test/wm/wmFixes.c +++ b/src/test/wm/wmFixes.c @@ -99,6 +99,8 @@ #define ACC_SHIFT_F3 13 #define ACC_CTRL_SHIFT_F8 14 #define ACC_ALT_F3 15 +#define ACC_ALT_ENTER 16 +#define ACC_TEXT_MAX 32 #define APP_ID_A 3 #define APP_ID_B 4 @@ -151,6 +153,7 @@ typedef struct { static void caseAccelBiosScancodes(void); static void caseAccelModifiedKeys(void); +static void caseAccelText(void); static void caseAltTabMinimizedTop(void); static void caseCallbackAppId(void); static void caseContentDisplayNull(void); @@ -326,6 +329,66 @@ static void caseAccelModifiedKeys(void) { } +// Key text: dvxAccelParse / dvxAccelFormat round trip, the names and the +// mistakes; Alt+Enter registered once matches Alt with the main Enter +// (no ASCII byte), the keypad Enter and an Enter that kept its byte. +static void caseAccelText(void) { + static const char *const good[] = { "F2", "Ctrl+X", "Alt+Enter", "Ctrl+Shift+F12", "Del", "Backspace", "Ctrl+Alt+7", "Shift+PgDn" }; + static const char *const bad[] = { "", "Ctrl+", "Meta+X", "F13", "F0", "XY", "%", "Ctrl+Alt" }; + char text[ACC_TEXT_MAX]; + int32_t key; + int32_t mods; + + tCase("accelerators: key text, Alt+Enter from either Enter key"); + + for (int32_t i = 0; i < DVX_ARRAY_LEN(good); i++) { + TEST_TRUE(dvxAccelParse(good[i], &key, &mods)); + dvxAccelFormat(key, mods, text, sizeof(text)); + TEST_STR_EQ(text, good[i]); + } + + for (int32_t i = 0; i < DVX_ARRAY_LEN(bad); i++) { + TEST_TRUE(!dvxAccelParse(bad[i], &key, &mods)); + TEST_EQ(key, 0); + } + + TEST_TRUE(dvxAccelParse(" shift + delete ", &key, &mods)); + TEST_EQ(key, KEY_DELETE); + TEST_EQ(mods, ACCEL_SHIFT); + TEST_TRUE(dvxAccelParse("ctrl+a", &key, &mods)); + TEST_EQ(key, 'A'); + TEST_EQ(mods, ACCEL_CTRL); + TEST_TRUE(!dvxAccelParse(NULL, &key, &mods)); + dvxAccelFormat(KEY_INSERT, 0, text, sizeof(text)); + TEST_STR_EQ(text, "Ins"); + dvxAccelFormat(0, ACCEL_ALT, text, sizeof(text)); + TEST_STR_EQ(text, ""); + + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + + WindowT *win = dvxCreateWindow(tCtx(), "Text", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, false); + AccelTableT *table = dvxCreateAccelTable(); + TSpyT spy; + + TEST_TRUE(dvxAccelParse("Alt+Enter", &key, &mods)); + dvxAddAccel(table, key, mods, ACC_ALT_ENTER); + win->accelTable = table; + tSpyInit(&spy); + tSpyWindow(&spy, win, "onMenu"); + + tKey(KEY_SCAN_ENTER, 0, KEY_MOD_ALT); + tKey(KEY_SCAN_ALT_KP_ENTER, 0, KEY_MOD_ALT); + tKey(KEY_SCAN_ENTER, KEY_ENTER, KEY_MOD_ALT); + tKey(KEY_SCAN_ENTER, KEY_ENTER, 0); + TEST_EQ(tSpyCount(&spy, "Text.onMenu"), 3); + + win->accelTable = NULL; + dvxFreeAccelTable(table); + tSpyFree(&spy); + tShutdown(); +} + + static void caseAltTabMinimizedTop(void) { tCase("Alt+Tab with a minimized top window moves focus once, no spurious callbacks"); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); @@ -1162,6 +1225,7 @@ int main(void) { caseAccelBiosScancodes(); caseAccelModifiedKeys(); + caseAccelText(); caseAltTabMinimizedTop(); caseCallbackAppId(); caseContentDisplayNull(); diff --git a/src/test/wm/wmSuite2.c b/src/test/wm/wmSuite2.c index 1f91e948..79d86f0b 100644 --- a/src/test/wm/wmSuite2.c +++ b/src/test/wm/wmSuite2.c @@ -66,6 +66,8 @@ #define BIG_IMG_W 700 #define BIG_IMG_H 520 #define ALPHA_IMG_SIZE 2 +#define TOP_DOWN_BMP_W 2 // the hand-made top-down BMP in caseImageApis +#define TOP_DOWN_BMP_H 3 #define SETTLE_FRAMES 3 #define ICON_PUMP_FRAMES 60 #define TINY_DISPLAY_W 40 @@ -104,6 +106,7 @@ #define WIDE_BUTTON_COUNT 12 #define TILE_WINDOW_COUNT 5 +#define TILE_SQUARE_COUNT 4 // 2 x 2: no free slot in the last row #define CASCADE_WINDOW_COUNT 8 #define WRAP_ICON_COUNT 12 @@ -199,6 +202,7 @@ static int32_t onCursorQueryZero(WindowT *win, int32_t x, int32_t y); static void onF1Hook(void *ctx); static void onPaintCount(WindowT *win, RectT *rect); static void onQuitIdle(void *ctx); +static void onResizeSpawn(WindowT *win, int32_t w, int32_t h); static void onWidgetScroll(WidgetT *w, int32_t delta); static void popupItemCenter(int32_t itemIdx, int32_t *sx, int32_t *sy); static bool writeNativePng(char *path, int32_t pathSize, const char *name, int32_t imgW, int32_t imgH); @@ -216,6 +220,7 @@ static HotkeyProbeT sHotkeyProbe; static int32_t sPaintCount = 0; static int32_t sScrollDeltaSum = 0; static AppContextT sFailCtx; +static WindowT *sSpawned = NULL; // the window onResizeSpawn opened // ============================================================ @@ -746,6 +751,61 @@ static void caseImageApis(void) { TEST_TRUE(!dvxImageInfo(NULL, &w, &h)); TEST_TRUE(!dvxImageInfo("/nonexistent/x.png", &w, &h)); + // An image without pixels fails to load everywhere: stb_image decodes + // this 40-byte BMP (1048702 x 0, the fuzzImage reproducer), and a 0-row + // image used to come back as a valid w x 0 image. + { + static const uint8_t emptyBmp[] = { + 0x42, 0x4D, 0x35, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x7E, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00 + }; + char empty[DVX_MAX_PATH]; + int32_t outW = 0; + int32_t outH = 0; + int32_t outPitch = 0; + bool hasAlpha = false; + uint32_t keyColor = 0; + + snprintf(empty, sizeof(empty), "%s/empty.bmp", tTempDir()); + TEST_TRUE(tWriteFile(empty, emptyBmp, (int32_t)sizeof(emptyBmp))); + TEST_PTR_EQ(dvxLoadImageFromMemory(tCtx(), emptyBmp, (int32_t)sizeof(emptyBmp), &outW, &outH, &outPitch), NULL); + TEST_PTR_EQ(dvxLoadImageAlpha(tCtx(), emptyBmp, (int32_t)sizeof(emptyBmp), &outW, &outH, &outPitch, &hasAlpha, &keyColor), NULL); + TEST_PTR_EQ(dvxLoadImage(tCtx(), empty, &outW, &outH, &outPitch), NULL); + TEST_TRUE(!dvxImageInfo(empty, &w, &h)); + TEST_TRUE(!dvxSetWallpaper(tCtx(), empty)); + } + + // A top-down BMP (height stored negative, here 2 x -3 at 24 bits, rows + // padded to 8 bytes) has the size it loads at: stb_image's info call + // passes the negative height through. + { + static const uint8_t topDownBmp[] = { + 0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFD, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + char topDown[DVX_MAX_PATH]; + int32_t outW = 0; + int32_t outH = 0; + int32_t outPitch = 0; + + snprintf(topDown, sizeof(topDown), "%s/topdown.bmp", tTempDir()); + TEST_TRUE(tWriteFile(topDown, topDownBmp, (int32_t)sizeof(topDownBmp))); + TEST_TRUE(dvxImageInfo(topDown, &w, &h)); + TEST_EQ(w, TOP_DOWN_BMP_W); + TEST_EQ(h, TOP_DOWN_BMP_H); + + uint8_t *pixels = dvxLoadImage(tCtx(), topDown, &outW, &outH, &outPitch); + + TEST_TRUE(pixels != NULL); + TEST_EQ(outW, TOP_DOWN_BMP_W); + TEST_EQ(outH, TOP_DOWN_BMP_H); + dvxFreeImage(pixels); + } + // dvxLoadImageAlpha keys out transparent pixels at every depth. const int32_t bpps[] = { BPP_8, BPP_16, BPP_32 }; @@ -1574,6 +1634,30 @@ static void caseTileCascade(void) { dvxTileWindowsV(ctx); TEST_TRUE(wins[0]->h >= MIN_WINDOW_H); + // An onResize that opens a window while the grid is tiled (a BASIC + // Resize event showing a form): the new window is not one of the + // planned tiles and keeps its place. With a square count it used to + // land in an empty row and divide by zero. + tShutdown(); + tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); + ctx = tCtx(); + + for (int32_t i = 0; i < TILE_SQUARE_COUNT; i++) { + wins[i] = dvxCreateWindow(ctx, "Square", WIN_A_X + i, WIN_A_Y + i, WIN_W, WIN_H, true); + } + + sSpawned = NULL; + wins[0]->onResize = onResizeSpawn; + tPump(1); + dvxTileWindows(ctx); + TEST_TRUE(sSpawned != NULL); + + if (sSpawned) { + TEST_EQ(sSpawned->x, WIN_B_X); + TEST_EQ(sSpawned->y, WIN_B_Y); + } + + TEST_EQ(wins[TILE_SQUARE_COUNT - 1]->x, SCREEN_W / 2); tShutdown(); } @@ -2018,6 +2102,18 @@ static void onQuitIdle(void *ctx) { } +// Opens one more window the first time it is resized. +static void onResizeSpawn(WindowT *win, int32_t w, int32_t h) { + (void)win; + (void)w; + (void)h; + + if (!sSpawned) { + sSpawned = dvxCreateWindow(tCtx(), "Spawned", WIN_B_X, WIN_B_Y, WIN_W, WIN_H, true); + } +} + + static void onWidgetScroll(WidgetT *w, int32_t delta) { (void)w; sScrollDeltaSum += delta; diff --git a/src/tools/README.md b/src/tools/README.md index 3e7e1fe1..aa0323ce 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -182,7 +182,8 @@ fresh: no topic is current and no `.list`, `.note`, `.code` or `.table` block is open. 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. Source lines longer than -4094 characters are an error. `.include` paths may be absolute (`/`, +4094 characters, and lines that contain a NUL byte, are errors (in a +response file too). `.include` paths may be absolute (`/`, `\` or a drive letter) or relative to the including file, and `.image` may name a path relative to the `-i` directory, including a subdirectory (`sub/pic.png`). diff --git a/src/tools/dvxhlpc.c b/src/tools/dvxhlpc.c index 9b7a8fc3..35a4a927 100644 --- a/src/tools/dvxhlpc.c +++ b/src/tools/dvxhlpc.c @@ -42,9 +42,10 @@ #define _POSIX_C_SOURCE 200809L #include "hlpcCompile.h" -#include "../apps/kpunch/dvxhelp/hlpformat.h" +#include "../libs/kpunch/hlpfile/hlpFormat.h" #include "../libs/kpunch/libdvx/platform/dvxPlat.h" +#include #include #include #include @@ -75,16 +76,18 @@ #define INITIAL_SOURCE_CAP 16 // first allocation of the source name list #define MAX_INPUT_FILES 256 // cap on input files (incl. @filelist expansion) #define SECTION_PATH_SEP '|' // separates levels in a .section path ("Parent|Child") +#define HLPC_MSG_MAX 512 // one message line (error, warning, information) +#define HLPC_NO_FILE "" // a message outside every input file +#define HLPC_KIND_ERROR "error" +#define HLPC_KIND_WARNING "warning" // Serialized topic indices are uint16_t and HLP_NO_TOPIC (0xFFFF) marks // "no topic" in the TOC, so the last usable index is 0xFFFE. #define HLPC_MAX_TOPICS HLP_NO_TOPIC -// Trigram characters are lowercase ASCII letters and digits: 36 symbols, -// so every trigram has a slot in a 36^3 direct index. -#define TRIGRAM_DIGITS 10 -#define TRIGRAM_ALPHABET (TRIGRAM_DIGITS + 26) -#define TRIGRAM_SLOTS (TRIGRAM_ALPHABET * TRIGRAM_ALPHABET * TRIGRAM_ALPHABET) +// Trigram characters (hlpTrigramSymbol, hlpFormat.h) give every trigram a +// slot in a direct index of HLP_TRIGRAM_ALPHABET^3 entries. +#define TRIGRAM_SLOTS (HLP_TRIGRAM_ALPHABET * HLP_TRIGRAM_ALPHABET * HLP_TRIGRAM_ALPHABET) // 32-bit FNV-1a for the string table hash #define FNV_OFFSET_BASIS 2166136261u @@ -261,6 +264,10 @@ static ScratchT sScratch; static jmp_buf sOomJmp; // fatalOom's way back into hlpcCompile static bool sOomArmed = false; // sOomJmp is valid (a compile is running) +// Where messages go (hlpcSetMessageFn; NULL: stderr) +static HlpcMessageFnT sMessageFn = NULL; +static void *sMessageCtx = NULL; + // Progress callback (set by hlpcCompile, NULL for standalone) static HlpcProgressFnT sProgressFn = NULL; static void *sProgressCtx = NULL; @@ -299,6 +306,10 @@ static int compareIds(const void *a, const void *b); static int compareIndexEntries(const void *a, const void *b); static int compareTrigrams(const void *a, const void *b); static int32_t compilePasses(const char **inputFiles, int32_t inputCount, const char *outputPath); +static void diag(const char *fmt, ...); +static void diagAt(const char *kind, const char *fmt, va_list ap); +static void diagLine(const char *line); +static void diagV(const char *fmt, va_list ap); static void emitError(const char *fmt, ...); static int32_t emitHtml(const char *outputPath); static void emitTocSection(int32_t prefix, int32_t level, int32_t prefixCount, int32_t *newCount); @@ -310,6 +321,7 @@ static void flushParagraph(TopicT *topic, char *para, int32_t paraLen, ui static void flushPending(ParseStateT *ps); static void freeAll(void); static void hlpcInfo(const char *fmt, ...); +void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx); static void htmlEscapeWrite(FILE *f, const char *text); static void htmlWriteImage(FILE *f, const RecordT *rec); static void htmlWritePreformatted(FILE *f, const char *text); @@ -322,6 +334,7 @@ static void pass3SearchIndex(void); static int32_t pass4Serialize(const char *outputPath); static void progressFinish(void); static void progressStep(void); +static bool readLine(FILE *f, char *line, int32_t size, int32_t *len, bool *hasNul); #ifndef HLPC_NO_MAIN static int32_t readResponseFile(const char *path, char **inputFiles, int32_t *inputCount); #endif @@ -331,7 +344,6 @@ static void resetAll(void); static void strHashGrow(void); static uint32_t strHashString(const char *str); static int32_t strTableAdd(const char *str); -static int32_t trigramSymbol(uint8_t c); #ifndef HLPC_NO_MAIN static void usage(void); #endif @@ -571,7 +583,7 @@ static void buildSearchIndex(void) { if (!rec->data || rec->dataLen <= 0) { continue; } - if (rec->type == HLP_REC_TEXT || rec->type == HLP_REC_HEADING1 || rec->type == HLP_REC_HEADING2 || rec->type == HLP_REC_HEADING3 || rec->type == HLP_REC_LIST_ITEM || rec->type == HLP_REC_NOTE || rec->type == HLP_REC_CODE) { + if (hlpRecordSearchable(rec->type)) { bufAppend(textBuf, rec->data, rec->dataLen); bufAppend(textBuf, &space, 1); } @@ -584,15 +596,15 @@ static void buildSearchIndex(void) { // Generate trigrams of three consecutive letters/digits for (int32_t i = 0; i + 2 < textBuf->size; i++) { - int32_t sa = trigramSymbol(textBuf->data[i]); - int32_t sb = trigramSymbol(textBuf->data[i + 1]); - int32_t sc = trigramSymbol(textBuf->data[i + 2]); + int32_t sa = hlpTrigramSymbol(textBuf->data[i]); + int32_t sb = hlpTrigramSymbol(textBuf->data[i + 1]); + int32_t sc = hlpTrigramSymbol(textBuf->data[i + 2]); if (sa < 0 || sb < 0 || sc < 0) { continue; } - int32_t key = (sa * TRIGRAM_ALPHABET + sb) * TRIGRAM_ALPHABET + sc; + int32_t key = (sa * HLP_TRIGRAM_ALPHABET + sb) * HLP_TRIGRAM_ALPHABET + sc; TrigramT *tri = slots[key] ? &trigrams[slots[key] - 1] : NULL; addTrigram(&tri, textBuf->data[i], textBuf->data[i + 1], textBuf->data[i + 2], (uint16_t)t); @@ -741,7 +753,7 @@ static int32_t compilePasses(const char **inputFiles, int32_t inputCount, const sScratch.parse = NULL; if (topicCount > HLPC_MAX_TOPICS) { - emitError("too many topics (%d, max %d)", (int)topicCount, HLPC_MAX_TOPICS); + emitError("too many topics (%" PRId32 ", max %d)", topicCount, HLPC_MAX_TOPICS); } regroupTocBySections(); @@ -750,7 +762,7 @@ static int32_t compilePasses(const char **inputFiles, int32_t inputCount, const hlpcInfo(" %d topic(s), %d TOC entries, %d index entries, %d image(s)\n", topicCount, tocCount, indexCount, imageCount); if (errorCount > 0) { - fprintf(stderr, "Aborting due to %d error(s).\n", (int)errorCount); + diag("Aborting due to %" PRId32 " error(s).\n", errorCount); return 1; } @@ -782,7 +794,7 @@ static int32_t compilePasses(const char **inputFiles, int32_t inputCount, const bool swapped = platformPathSibling(bakPath, sizeof(bakPath), htmlPath, PLATFORM_BAK_EXT) && platformReplaceFile(sScratch.htmlTmp, htmlPath, bakPath); if (!swapped) { - fprintf(stderr, "error: cannot rename '%s' to '%s': %s\n", sScratch.htmlTmp, htmlPath, strerror(errno)); + diag("error: cannot rename '%s' to '%s': %s\n", sScratch.htmlTmp, htmlPath, strerror(errno)); releaseScratch(); return 1; } @@ -796,12 +808,58 @@ static int32_t compilePasses(const char **inputFiles, int32_t inputCount, const } +// One message line (a trailing line end in fmt is dropped). +static void diag(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + diagV(fmt, ap); + va_end(ap); +} + + +// An error or warning at the current file and line. +static void diagAt(const char *kind, const char *fmt, va_list ap) { + char line[HLPC_MSG_MAX]; + int32_t n = snprintf(line, sizeof(line), "%s: %s:%" PRId32 ": ", kind, currentFile ? currentFile : HLPC_NO_FILE, currentLine); + + if (n >= 0 && n < (int32_t)sizeof(line)) { + vsnprintf(line + n, sizeof(line) - (size_t)n, fmt, ap); + } + + diagLine(line); +} + + +static void diagLine(const char *line) { + if (sMessageFn) { + sMessageFn(sMessageCtx, line); + return; + } + + fprintf(stderr, "%s\n", line); +} + + +static void diagV(const char *fmt, va_list ap) { + char line[HLPC_MSG_MAX]; + size_t len; + + vsnprintf(line, sizeof(line), fmt, ap); + len = strlen(line); + + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } + + diagLine(line); +} + + + static void emitError(const char *fmt, ...) { va_list ap; va_start(ap, fmt); - fprintf(stderr, "error: %s:%d: ", currentFile ? currentFile : "", (int)currentLine); - vfprintf(stderr, fmt, ap); - fprintf(stderr, "\n"); + diagAt(HLPC_KIND_ERROR, fmt, ap); va_end(ap); errorCount++; } @@ -812,7 +870,7 @@ static void emitError(const char *fmt, ...) { // neither output. static int32_t emitHtml(const char *outputPath) { if (!platformPathSibling(sScratch.htmlTmp, sizeof(sScratch.htmlTmp), outputPath, HTML_TMP_EXT)) { - fprintf(stderr, "error: cannot name a temp file for '%s'\n", outputPath); + diag("error: cannot name a temp file for '%s'\n", outputPath); sScratch.htmlTmp[0] = '\0'; return -1; } @@ -822,7 +880,7 @@ static int32_t emitHtml(const char *outputPath) { FILE *f = sScratch.out; if (!f) { - fprintf(stderr, "error: cannot open %s for writing\n", sScratch.htmlTmp); + diag("error: cannot open %s for writing\n", sScratch.htmlTmp); sScratch.htmlTmp[0] = '\0'; return -1; } @@ -920,7 +978,7 @@ static int32_t emitHtml(const char *outputPath) { sScratch.out = NULL; if (fclose(f) != 0 || failed) { - fprintf(stderr, "error: cannot write %s: %s\n", sScratch.htmlTmp, strerror(errno)); + diag("error: cannot write %s: %s\n", sScratch.htmlTmp, strerror(errno)); remove(sScratch.htmlTmp); sScratch.htmlTmp[0] = '\0'; return -1; @@ -946,7 +1004,14 @@ static void emitTocSection(int32_t prefix, int32_t level, int32_t prefixCount, i TocEntryT *hdr = &sScratch.newToc[(*newCount)++]; memset(hdr, 0, sizeof(*hdr)); - snprintf(hdr->title, sizeof(hdr->title), "%.*s", (int)(path + len - name), name); + int32_t titleLen = (int32_t)(path + len - name); + + if (titleLen > (int32_t)sizeof(hdr->title) - 1) { + titleLen = (int32_t)sizeof(hdr->title) - 1; + } + + memcpy(hdr->title, name, (size_t)titleLen); + hdr->title[titleLen] = '\0'; hdr->topicIdx = -1; hdr->depth = level; @@ -975,9 +1040,7 @@ static void emitTocSection(int32_t prefix, int32_t level, int32_t prefixCount, i static void emitWarning(const char *fmt, ...) { va_list ap; va_start(ap, fmt); - fprintf(stderr, "warning: %s:%d: ", currentFile ? currentFile : "", (int)currentLine); - vfprintf(stderr, fmt, ap); - fprintf(stderr, "\n"); + diagAt(HLPC_KIND_WARNING, fmt, ap); va_end(ap); } @@ -985,7 +1048,7 @@ static void emitWarning(const char *fmt, ...) { // Inside a compile the compile is abandoned (hlpcCompile cleans up and // returns non-zero); only a standalone caller outside one exits. static void fatalOom(void) { - fprintf(stderr, "error: out of memory\n"); + diag("error: out of memory\n"); if (sOomArmed) { longjmp(sOomJmp, 1); @@ -1139,7 +1202,7 @@ int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *out result = compilePasses(inputFiles, inputCount, outputPath); } else { releaseScratch(); - fprintf(stderr, "Aborting: out of memory.\n"); + diag("Aborting: out of memory.\n"); } sOomArmed = false; @@ -1160,13 +1223,20 @@ static void hlpcInfo(const char *fmt, ...) { if (quietMode) { return; } + va_list ap; va_start(ap, fmt); - vfprintf(stderr, fmt, ap); + diagV(fmt, ap); va_end(ap); } +void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx) { + sMessageFn = fn; + sMessageCtx = ctx; +} + + // Writes text with HTML special characters escaped. NULL (an empty // record such as a bare .h1) writes nothing. static void htmlEscapeWrite(FILE *f, const char *text) { @@ -1663,22 +1733,28 @@ static void parseFile(const char *path, ParseStateT *ps, int32_t includeDepth) { hlpcInfo(" parsing %s\n", path); - char line[LINE_BUF_LEN]; - while (fgets(line, sizeof(line), f)) { - currentLine++; + char line[LINE_BUF_LEN]; + int32_t len; + bool hasNul; - int32_t len = strlen(line); + while (readLine(f, line, (int32_t)sizeof(line), &len, &hasNul)) { + currentLine++; // A line longer than the buffer comes back in pieces; parsing the // tail as its own line could turn text into a directive. if (len > 0 && line[len - 1] != '\n' && !feof(f)) { emitError("line exceeds %d characters", MAX_LINE_LEN - 2); - while (fgets(line, sizeof(line), f) && line[strlen(line) - 1] != '\n') { + while (readLine(f, line, (int32_t)sizeof(line), &len, &hasNul) && (len == 0 || line[len - 1] != '\n')) { // discard the rest of the line } continue; } + if (hasNul) { + emitError("NUL byte in line"); + continue; + } + // Strip trailing newline/CR while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { line[--len] = '\0'; @@ -1776,8 +1852,11 @@ static void pass3SearchIndex(void) { buildSearchIndex(); - // Sort trigrams - qsort(trigrams, trigramCount, sizeof(TrigramT), compareTrigrams); + // Sort trigrams (none, and no array, for a source without text: + // qsort's base must not be NULL even for a zero count) + if (trigramCount > 0) { + qsort(trigrams, trigramCount, sizeof(TrigramT), compareTrigrams); + } hlpcInfo(" %d trigrams\n", trigramCount); } @@ -1794,7 +1873,7 @@ static int32_t pass4Serialize(const char *outputPath) { char bakPath[DVX_MAX_PATH]; if (!platformPathSibling(tmpPath, sizeof(tmpPath), outputPath, PLATFORM_TMP_EXT) || !platformPathSibling(bakPath, sizeof(bakPath), outputPath, PLATFORM_BAK_EXT)) { - fprintf(stderr, "error: cannot name a temp file for '%s'\n", outputPath); + diag("error: cannot name a temp file for '%s'\n", outputPath); return 1; } @@ -1804,7 +1883,7 @@ static int32_t pass4Serialize(const char *outputPath) { w.failed = false; if (!w.f) { - fprintf(stderr, "error: cannot create '%s': %s\n", tmpPath, strerror(errno)); + diag("error: cannot create '%s': %s\n", tmpPath, strerror(errno)); return 1; } @@ -1832,7 +1911,7 @@ static int32_t pass4Serialize(const char *outputPath) { int32_t imgLen = 0; uint8_t *imgData = (uint8_t *)platformReadFile(img->path, &imgLen); if (!imgData) { - fprintf(stderr, "error: cannot read image '%s': %s\n", img->path, strerror(errno)); + diag("error: cannot read image '%s': %s\n", img->path, strerror(errno)); releaseScratch(); return 1; } @@ -1871,7 +1950,7 @@ static int32_t pass4Serialize(const char *outputPath) { // registered at parse time, so a miss is an internal error. int32_t imgIdx = findImage(rec->data); if (imgIdx < 0) { - fprintf(stderr, "error: image '%s' not found in references\n", rec->data); + diag("error: image '%s' not found in references\n", rec->data); w.failed = true; break; } @@ -1942,9 +2021,9 @@ static int32_t pass4Serialize(const char *outputPath) { entry.trigram[1] = trigrams[i].trigram[1]; entry.trigram[2] = trigrams[i].trigram[2]; // The serialized count is a uint8_t; clamp so a very common - // trigram (>255 postings) doesn't wrap the count and desync it + // trigram (more than HLP_POSTING_SATURATED postings) doesn't wrap the count and desync it // from the posting data written below. - int32_t pc = trigrams[i].postingCount > UINT8_MAX ? UINT8_MAX : trigrams[i].postingCount; + int32_t pc = trigrams[i].postingCount > HLP_POSTING_SATURATED ? HLP_POSTING_SATURATED : trigrams[i].postingCount; entry.postingCount = (uint8_t)pc; entry.postingOffset = postingBase + postingOff; writerPut(&w, &entry, sizeof(entry)); @@ -1953,7 +2032,7 @@ static int32_t pass4Serialize(const char *outputPath) { // Write posting lists (clamped to match the uint8_t count above). for (int32_t i = 0; i < trigramCount; i++) { - int32_t pc = trigrams[i].postingCount > UINT8_MAX ? UINT8_MAX : trigrams[i].postingCount; + int32_t pc = trigrams[i].postingCount > HLP_POSTING_SATURATED ? HLP_POSTING_SATURATED : trigrams[i].postingCount; writerPut(&w, trigrams[i].postings, sizeof(uint16_t) * pc); } } @@ -1994,14 +2073,14 @@ static int32_t pass4Serialize(const char *outputPath) { sScratch.outTmp[0] = '\0'; if (fclose(w.f) != 0 || failed) { - fprintf(stderr, "error: cannot write '%s': %s\n", tmpPath, strerror(errno)); + diag("error: cannot write '%s': %s\n", tmpPath, strerror(errno)); remove(tmpPath); return 1; } // Swap the temp file in; a failed swap keeps the previous help file. if (!platformReplaceFile(tmpPath, outputPath, bakPath)) { - fprintf(stderr, "error: cannot rename '%s' to '%s': %s\n", tmpPath, outputPath, strerror(errno)); + diag("error: cannot rename '%s' to '%s': %s\n", tmpPath, outputPath, strerror(errno)); return 1; } @@ -2026,12 +2105,53 @@ static void progressStep(void) { } +// fgets with an exact length: read one line of a text file into line (at +// most size - 1 bytes, the newline kept when it fits), NUL-terminate it +// and store its length in *len. A NUL byte in the file is not stored +// (strlen after fgets stopped there, so the newline after it was missed +// and a chunk starting with one had length 0); *hasNul reports it. +// False at end of file with nothing read. +static bool readLine(FILE *f, char *line, int32_t size, int32_t *len, bool *hasNul) { + int32_t n = 0; + bool gotByte = false; + + *hasNul = false; + + while (n < size - 1) { + int ch = getc(f); + + if (ch == EOF) { + break; + } + + gotByte = true; + + if (ch == '\0') { + *hasNul = true; + continue; + } + + line[n++] = (char)ch; + + if (ch == '\n') { + break; + } + } + + line[n] = '\0'; + *len = n; + return gotByte; +} + + #ifndef HLPC_NO_MAIN // Append the filenames listed in a response file (one per line, '#' // comments) to inputFiles. Returns 0 on success. static int32_t readResponseFile(const char *path, char **inputFiles, int32_t *inputCount) { static char respLines[MAX_INPUT_FILES][DVX_MAX_PATH]; char rline[DVX_MAX_PATH + 1]; // DVX_MAX_PATH - 2 characters plus CR, LF and NUL + int32_t rlen; + bool hasNul; int32_t lineNum = 0; FILE *rf = fopen(path, "r"); @@ -2040,13 +2160,17 @@ static int32_t readResponseFile(const char *path, char **inputFiles, int32_t *in return 1; } - while (fgets(rline, (int)sizeof(rline), rf)) { - int32_t rlen = (int32_t)strlen(rline); - + while (readLine(rf, rline, (int32_t)sizeof(rline), &rlen, &hasNul)) { lineNum++; if (rlen > 0 && rline[rlen - 1] != '\n' && !feof(rf)) { - fprintf(stderr, "error: %s:%d: path longer than %d characters\n", path, (int)lineNum, DVX_MAX_PATH - 2); + fprintf(stderr, "error: %s:%" PRId32 ": path longer than %d characters\n", path, lineNum, DVX_MAX_PATH - 2); + fclose(rf); + return 1; + } + + if (hasNul) { + fprintf(stderr, "error: %s:%" PRId32 ": NUL byte in line\n", path, lineNum); fclose(rf); return 1; } @@ -2275,19 +2399,6 @@ static int32_t strTableAdd(const char *str) { } -// Map a lowercased byte to its trigram symbol (0-9 digits, 10-35 letters), -// or -1 when it cannot be part of a trigram. -static int32_t trigramSymbol(uint8_t c) { - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'z') { - return TRIGRAM_DIGITS + (c - 'a'); - } - return -1; -} - - #ifndef HLPC_NO_MAIN static void usage(void) { fprintf(stderr, "Usage: dvxhlpc -o output.hlp [-i imagedir] [--html out.html] [--quiet] input.dhs [@filelist] [...]\n"); diff --git a/src/tools/hlpcCompile.h b/src/tools/hlpcCompile.h index 1c53805f..d090736c 100644 --- a/src/tools/hlpcCompile.h +++ b/src/tools/hlpcCompile.h @@ -55,14 +55,23 @@ typedef void (*HlpcProgressFnT)(void *ctx, int32_t current, int32_t total); // inputFiles: array of .dhs source file paths // inputCount: number of input files // outputPath: path to write the .hlp file -// imageDir: directory to search for .image references (NULL = ".") -// htmlPath: if non-NULL, also emit HTML to this path +// imgDir: directory to search for .image references (NULL = ".") +// html: if non-NULL, also emit HTML to this path // options: HLPC_STRICT or HLPC_QUIET / HLPC_LENIENT_LINKS bits // progressFn: progress callback (NULL = no progress reporting) // progressCtx: opaque context passed to progressFn // // Returns 0 on success, non-zero on error. -int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *outputPath, const char *imageDir, const char *htmlPath, uint32_t options, HlpcProgressFnT progressFn, void *progressCtx); +int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *outputPath, const char *imgDir, const char *html, uint32_t options, HlpcProgressFnT progressFn, void *progressCtx); + +// Where the compiler's messages go: errors and warnings ("error: +// file:line: text"), and without HLPC_QUIET its informational lines; one +// call per line, without the line end. NULL (the default) writes them +// to stderr. The setting stays until changed; dvx.exe callers (BASIC +// HelpCompile) set it around one compile. +typedef void (*HlpcMessageFnT)(void *ctx, const char *line); + +void hlpcSetMessageFn(HlpcMessageFnT fn, void *ctx); // Return the number of progress steps for a given input count: // inputCount (one per file in pass 1) + 3 (passes 2-4). diff --git a/src/tools/mkwgticon.c b/src/tools/mkwgticon.c index e564183c..c16f45e4 100644 --- a/src/tools/mkwgticon.c +++ b/src/tools/mkwgticon.c @@ -54,6 +54,7 @@ static void drawDbgrid(void); static void drawDropdown(void); static void drawFrame(void); static void drawHbox(void); +static void drawHelpview(void); static void drawImage(void); static void drawImgbtn(void); static void drawLabel(void); @@ -214,6 +215,22 @@ static void drawHbox(void) { } +static void drawHelpview(void) { + bmpClearBg(&canvas); + bmpFillRect(&canvas, 2, 2, 20, 20, 255, 255, 255); + bmpRect(&canvas, 2, 2, 20, 20, 128, 128, 128); + // Heading bar + bmpFillRect(&canvas, 3, 3, 18, 5, 0, 0, 128); + // Text lines + bmpHLine(&canvas, 5, 10, 14, 0, 0, 0); + bmpHLine(&canvas, 5, 12, 12, 0, 0, 0); + // Underlined link + bmpHLine(&canvas, 5, 15, 9, 0, 0, 128); + bmpHLine(&canvas, 5, 16, 9, 0, 0, 128); + bmpHLine(&canvas, 5, 19, 13, 0, 0, 0); +} + + static void drawImage(void) { bmpClearBg(&canvas); bmpFillRect(&canvas, 3, 3, 18, 18, 255, 255, 255); @@ -582,6 +599,7 @@ int main(int argc, char **argv) { {"frame", drawFrame}, {"hbox", drawHbox}, {"vbox", drawVbox}, + {"helpview", drawHelpview}, {"splitter", drawSplitter}, {"scrollpane", drawScrollpane}, {"tabctrl", drawTabctrl}, diff --git a/src/widgets/kpunch/Makefile b/src/widgets/kpunch/Makefile index f979e7af..ff561ffc 100644 --- a/src/widgets/kpunch/Makefile +++ b/src/widgets/kpunch/Makefile @@ -44,6 +44,7 @@ WIDGETS = \ dropdown:dropdown:widgetDropdown:dropdown \ imgbtn:imageButton:widgetImageButton:imgbtn \ image:image:widgetImage:image \ + helpview:helpView:widgetHelpView:helpview \ label:label:widgetLabel:label \ listbox:listBox:widgetListBox:listbox \ listview:listView:widgetListView:listview \ diff --git a/src/widgets/kpunch/ansiTerm/ansiTerm.dhs b/src/widgets/kpunch/ansiTerm/ansiTerm.dhs index f45506fa..3d188adf 100644 --- a/src/widgets/kpunch/ansiTerm/ansiTerm.dhs +++ b/src/widgets/kpunch/ansiTerm/ansiTerm.dhs @@ -102,8 +102,8 @@ Features added after the original table live in a second table so AnsiTermApiT k .table Property Type Access Description -------- ---- ------ ----------- - Cols Integer Read-only Number of columns. - Rows Integer Read-only Number of rows. + Cols Integer Read/Write Number of columns. Setting it (or Rows) resizes the terminal (1-512): screen and scrollback start empty and the modes the host set (wrap, origin, cursor visible, scroll region) are reset; palette, scrollback length and comm callbacks stay; the window is laid out again. + Rows Integer Read/Write Number of rows. Scrollback Integer Read/Write Maximum number of scrollback lines. Setting it discards the existing history. .endtable @@ -113,7 +113,11 @@ Features added after the original table live in a second table so AnsiTermApiT k Method Description ------ ----------- Clear Clear the terminal screen. + Copy Copy the selection (copySelection); returns False when nothing is selected. + Paste Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks). Poll Process any pending bytes on the attached communication channel. + ResetColors The standard CGA colors again (setPalette NULL). + SetColor index, color: one attribute color (0-15) from a BASIC color (&HBBGGRR, converted with wgtBasicColor). Write Write a string into the terminal (with ANSI escape processing). .endtable diff --git a/src/widgets/kpunch/ansiTerm/ansiterm.bhs b/src/widgets/kpunch/ansiTerm/ansiterm.bhs index 2a639c18..059aa755 100644 --- a/src/widgets/kpunch/ansiTerm/ansiterm.bhs +++ b/src/widgets/kpunch/ansiTerm/ansiterm.bhs @@ -39,8 +39,8 @@ A VT100/ANSI terminal emulator with a text-mode cell buffer, 16-color CGA palett .table Property Type Description ---------- ------- ------------------------------------------- - Cols Integer Number of character columns (read-only). - Rows Integer Number of character rows (read-only). + Cols Integer Number of character columns (1-512). Setting Cols or Rows gives the terminal a new size, starting as a new terminal: empty screen and scrollback, modes the host set reset (its colors and connection stay); set the form's AutoSize to True afterwards to fit the window to it. + Rows Integer Number of character rows (1-512). Scrollback Integer Maximum number of scrollback lines. Older lines are dropped (default 500). Setting it discards the existing history. .endtable @@ -50,7 +50,11 @@ A VT100/ANSI terminal emulator with a text-mode cell buffer, 16-color CGA palett Method Description ------ ----------- Clear Clear the terminal screen and reset the cursor to the home position. + Copy() Copy the selected text to the clipboard and clear the selection. Returns False when nothing is selected. + Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does. Poll Poll the attached communication channel for incoming bytes and feed them into the ANSI parser. + ResetColors Back to the standard 16 colors. + SetColor index%, color& The color (RGB(), as ForeColor takes it) of text attribute index (0-15, in CGA order: black, blue, green, cyan, red, magenta, brown, light gray, then their bright forms). An index outside 0-15 is ignored. Write text$ Write a string to the terminal with full ANSI escape processing. .endtable @@ -58,7 +62,7 @@ No default event. Uses common events only. .h2 Serial Communication -Use the comm.bas library to connect a Terminal to a serial link: +The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link or Telnet -- with capture, answerback and file transfers (ConnAttach; see Include Libraries). For a packet link of your own, use the comm.bas library: .code Dim link As Long @@ -82,7 +86,7 @@ Begin Terminal Terminal1 MinHeight = 400 End -Sub Form_Load () +Sub Form1_Load () Terminal1.Write "Welcome!" & Chr$(13) & Chr$(10) End Sub .endcode diff --git a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c index 424cb4a8..85130865 100644 --- a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c +++ b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c @@ -110,6 +110,11 @@ static int32_t sTypeId = -1; #define ANSI_CLIPBOARD_BUF 4096 #define ANSI_PASTE_CHUNK 64 // bytes per commWrite when pasting +// SetColor (BASIC): the channels of a DVX color (&HRRGGBB, wgtBasicColor) +#define ANSI_RGB_RED_SHIFT 16 +#define ANSI_RGB_GREEN_SHIFT 8 +#define ANSI_RGB_CHANNEL 0xFFu + typedef struct { // Fields are ordered by alignment to minimize struct padding. uint8_t *cells; @@ -217,11 +222,18 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch); static void ansiTermProcessSgr(WidgetT *w); static void ansiTermPushScreen(WidgetT *w); static void ansiTermPutChar(WidgetT *w, uint8_t ch); +static void ansiTermResetScreen(WidgetT *w); +static void ansiTermResize(WidgetT *w, int32_t cols, int32_t rows); static void ansiTermScrollDown(WidgetT *w); static void ansiTermScrollUp(WidgetT *w); static void ansiTermScrollView(WidgetT *w, int32_t delta); static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol); WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows); +static void wgtAnsiTermBasPaste(WidgetT *w); +static void wgtAnsiTermBasResetColors(WidgetT *w); +static void wgtAnsiTermBasSetColor(WidgetT *w, int32_t index, int32_t color); +static void wgtAnsiTermBasSetCols(WidgetT *w, int32_t cols); +static void wgtAnsiTermBasSetRows(WidgetT *w, int32_t rows); void wgtAnsiTermClear(WidgetT *w); static bool wgtAnsiTermCopy(WidgetT *w); static int32_t wgtAnsiTermGetCols(const WidgetT *w); @@ -606,7 +618,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { if (at->commWrite) { if (mode == 6) { char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(at->cursorRow + 1), (long)(at->cursorCol + 1)); + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->cursorRow + 1, (long)at->cursorCol + 1); at->commWrite(at->commCtx, (const uint8_t *)reply, len); } else if (mode == 255) { char reply[16]; @@ -877,17 +889,15 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { } at->parseState = PARSE_NORMAL; } else if (ch == 'c') { - at->cursorRow = 0; - at->cursorCol = 0; - at->curAttr = ANSI_DEFAULT_ATTR; - at->bold = false; + // RIS: the modes and screen of a new terminal; the old + // screen goes to the scrollback like any other clear. at->wrapMode = true; at->originMode = false; at->cursorVisible = true; at->scrollTop = 0; at->scrollBot = at->rows - 1; - ansiTermEraseDisplay(w, 2); - at->parseState = PARSE_NORMAL; + ansiTermPushScreen(w); + ansiTermResetScreen(w); } else { at->parseState = PARSE_NORMAL; } @@ -1036,6 +1046,79 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) { } +// The screen of a new terminal: every cell blank in the default +// attribute, the cursor home and the parser idle. Nothing goes to the +// scrollback (callers that clear a screen the user saw push it first). +static void ansiTermResetScreen(WidgetT *w) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + at->cursorRow = 0; + at->cursorCol = 0; + at->curAttr = ANSI_DEFAULT_ATTR; + at->bold = false; + at->parseState = PARSE_NORMAL; + // Every row dirty (this also initializes a newly allocated mask), or the + // row-skipping paint leaves stale cells until each is re-dirtied. + ansiTermDirtyAll(w); + ansiTermFillCells(w, 0, at->cols * at->rows); +} + + +// A new screen size: the screen, the scrollback and the modes the host +// set start as on a terminal made at that size (the palette, scrollback +// length and connection stay). The old screen is not pushed: it has the +// old width and the scrollback it would go to is discarded. Kept as it +// was when the memory cannot be had. +static void ansiTermResize(WidgetT *w, int32_t cols, int32_t rows) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + cols = DVX_MIN(DVX_MAX(cols, 1), ANSI_MAX_COLS); + rows = DVX_MIN(DVX_MAX(rows, 1), ANSI_MAX_ROWS); + + if (cols == at->cols && rows == at->rows) { + return; + } + + uint8_t *cells = (uint8_t *)malloc(cols * rows * 2); + uint8_t *scrollback = (uint8_t *)malloc(at->scrollbackMax * cols * 2); + uint32_t *dirtyRows = (uint32_t *)malloc(ANSI_DIRTY_WORDS(rows) * sizeof(uint32_t)); + + if (!cells || !scrollback || !dirtyRows) { + free(cells); + free(scrollback); + free(dirtyRows); + return; + } + + free(at->cells); + free(at->scrollback); + free(at->dirtyRows); + at->cells = cells; + at->scrollback = scrollback; + at->dirtyRows = dirtyRows; + at->cols = cols; + at->rows = rows; + at->scrollTop = 0; + at->scrollBot = rows - 1; + at->savedRow = 0; + at->savedCol = 0; + at->scrollbackCount = 0; + at->scrollbackHead = 0; + at->scrollPos = 0; + at->lastCursorRow = -1; + at->lastCursorCol = -1; + at->sbDirty = true; + at->cursorVisible = true; + at->wrapMode = true; + at->originMode = false; + ansiTermClearSelection(w); + ansiTermResetScreen(w); + + // A new minimum size: lay the window out again + wgtInvalidate(w); +} + + static void ansiTermScrollDown(WidgetT *w) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t cols = at->cols; @@ -1142,8 +1225,7 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { return NULL; } w->data = at; - int32_t cellCount = cols * rows; - at->cells = (uint8_t *)malloc(cellCount * 2); + at->cells = (uint8_t *)malloc(cols * rows * 2); if (!at->cells) { wgtDestroy(w); return NULL; @@ -1164,8 +1246,6 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { at->rows = rows; at->cursorVisible = true; at->wrapMode = true; - at->curAttr = ANSI_DEFAULT_ATTR; - at->parseState = PARSE_NORMAL; at->scrollBot = rows - 1; at->scrollbackMax = sbMax; at->selStartLine = -1; @@ -1176,34 +1256,65 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { at->blinkTime = platformClock(); at->cursorOn = true; at->cursorTime = platformClock(); - ansiTermDirtyAll(w); at->lastCursorRow = -1; at->lastCursorCol = -1; - for (int32_t i = 0; i < cellCount; i++) { - at->cells[i * 2] = ' '; - at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; - } + ansiTermResetScreen(w); return w; } +// BASIC Paste: the clipboard to the host, as Ctrl+V. +static void wgtAnsiTermBasPaste(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ansiTermPasteToComm(w); +} + + +// BASIC ResetColors: the standard colors again. +static void wgtAnsiTermBasResetColors(WidgetT *w) { + wgtAnsiTermSetPalette(w, NULL); +} + + +// BASIC SetColor index, color: the color (RGB(), &HBBGGRR) of text +// attribute index (0-15, the CGA order). +static void wgtAnsiTermBasSetColor(WidgetT *w, int32_t index, int32_t color) { + VALIDATE_WIDGET_VOID(w, sTypeId); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + + if (index < 0 || index >= ANSI_PALETTE_SIZE) { + return; + } + + uint32_t rgb = wgtBasicColor((uint32_t)color); + uint8_t palette[ANSITERM_PALETTE_BYTES]; + + memcpy(palette, at->palette, sizeof(palette)); + palette[index * 3] = (uint8_t)((rgb >> ANSI_RGB_RED_SHIFT) & ANSI_RGB_CHANNEL); + palette[index * 3 + 1] = (uint8_t)((rgb >> ANSI_RGB_GREEN_SHIFT) & ANSI_RGB_CHANNEL); + palette[index * 3 + 2] = (uint8_t)(rgb & ANSI_RGB_CHANNEL); + wgtAnsiTermSetPalette(w, palette); +} + + +// BASIC Cols = n +static void wgtAnsiTermBasSetCols(WidgetT *w, int32_t cols) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ansiTermResize(w, cols, ((AnsiTermDataT *)w->data)->rows); +} + + +// BASIC Rows = n +static void wgtAnsiTermBasSetRows(WidgetT *w, int32_t rows) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ansiTermResize(w, ((AnsiTermDataT *)w->data)->cols, rows); +} + + void wgtAnsiTermClear(WidgetT *w) { VALIDATE_WIDGET_VOID(w, sTypeId); - AnsiTermDataT *at = (AnsiTermDataT *)w->data; ansiTermPushScreen(w); - int32_t cellCount = at->cols * at->rows; - for (int32_t i = 0; i < cellCount; i++) { - at->cells[i * 2] = ' '; - at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; - } - at->cursorRow = 0; - at->cursorCol = 0; - at->curAttr = ANSI_DEFAULT_ATTR; - at->bold = false; - at->parseState = PARSE_NORMAL; - // Mark every row dirty or the row-skipping paint leaves stale cells - // until each happens to be re-dirtied by later output. - ansiTermDirtyAll(w); + ansiTermResetScreen(w); } @@ -1794,15 +1905,19 @@ static const AnsiTermExApiT sApiEx = { }; static const WgtPropDescT sProps[] = { - { "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, NULL, NULL }, - { "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, NULL, NULL }, + { "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, (void *)wgtAnsiTermBasSetCols, NULL }, + { "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, (void *)wgtAnsiTermBasSetRows, NULL }, { "Scrollback", WGT_IFACE_INT, (void *)wgtAnsiTermGetScrollback, (void *)wgtAnsiTermSetScrollback, NULL } }; static const WgtMethodDescT sMethods[] = { - { "Clear", WGT_SIG_VOID, (void *)wgtAnsiTermClear }, - { "Poll", WGT_SIG_VOID, (void *)wgtAnsiTermPoll }, - { "Write", WGT_SIG_STR, (void *)wgtAnsiTermWriteString } + { "Clear", WGT_SIG_VOID, (void *)wgtAnsiTermClear }, + { "Copy", WGT_SIG_RET_BOOL, (void *)wgtAnsiTermCopy }, + { "Paste", WGT_SIG_VOID, (void *)wgtAnsiTermBasPaste }, + { "Poll", WGT_SIG_VOID, (void *)wgtAnsiTermPoll }, + { "ResetColors", WGT_SIG_VOID, (void *)wgtAnsiTermBasResetColors }, + { "SetColor", WGT_SIG_INT_INT, (void *)wgtAnsiTermBasSetColor }, + { "Write", WGT_SIG_STR, (void *)wgtAnsiTermWriteString } }; static const WgtIfaceT sIface = { @@ -1822,4 +1937,5 @@ void wgtRegister(void) { wgtRegisterApi("ansiterm", &sApi); wgtRegisterApi("ansitermex", &sApiEx); wgtRegisterIface("ansiterm", &sIface); + wgtIfaceSetType("ansiterm", sTypeId); } diff --git a/src/widgets/kpunch/box/box.bhs b/src/widgets/kpunch/box/box.bhs index e48fe5ff..cb0320ce 100644 --- a/src/widgets/kpunch/box/box.bhs +++ b/src/widgets/kpunch/box/box.bhs @@ -35,9 +35,17 @@ A container with a titled border. Child controls are placed inside the frame usi .h2 Type-Specific Properties .table - Property Type Description - -------- ------ ------------------------------------------- - Caption String The title displayed in the frame border. + Property Type Description + -------- ------- ------------------------------------------- + Caption String The title displayed in the frame border. +.endtable + +.table + Property Type Description + -------- ------- ------------------------------------------- + Alignment Enum Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center or Right (the bottom). + Padding Integer Space in pixels between the border and the children; 0 (the default) means 4 pixels. + Spacing Integer Space in pixels between one child and the next; 0 (the default) means no space. .endtable No type-specific methods or events. @@ -68,6 +76,9 @@ End .toc 1 HBox .index HBox .index Horizontal Layout +.index Spacing +.index Padding +.index Alignment .h1 HBox @@ -75,7 +86,19 @@ DVX Extension -- DVX Widget: hbox (horizontal layout container) A container that arranges its children horizontally, left to right. No visible border. Use Weight on children to distribute extra space proportionally. -No type-specific properties, methods, or events. +.h2 Type-Specific Properties + +.table + Property Type Description + -------- ------- ------------------------------------------- + Alignment Enum Where the children sit along the row when they do not fill it: Left (the left, the default), Center or Right (the right). + Padding Integer Space in pixels between the edge of the box and the children; 0 (the default) means 4 pixels. + Spacing Integer Space in pixels between one child and the next; 0 (the default) means 4 pixels. +.endtable + +Alignment = Right puts a row of buttons at the right, as dialog boxes have them. + +No type-specific methods or events. Container: Yes @@ -109,7 +132,17 @@ DVX Extension -- DVX Widget: vbox (vertical layout container) A container that arranges its children vertically, top to bottom. No title or border. Use Weight on children to distribute extra space proportionally. -No type-specific properties, methods, or events. +.h2 Type-Specific Properties + +.table + Property Type Description + -------- ------- ------------------------------------------- + Alignment Enum Where the children sit along the column when they do not fill it: Left (the top, the default), Center or Right (the bottom). + Padding Integer Space in pixels between the edge of the box and the children; 0 (the default) means 4 pixels. + Spacing Integer Space in pixels between one child and the next; 0 (the default) means 4 pixels. +.endtable + +No type-specific methods or events. Container: Yes diff --git a/src/widgets/kpunch/box/widgetBox.c b/src/widgets/kpunch/box/widgetBox.c index 48569ced..396b3ad6 100644 --- a/src/widgets/kpunch/box/widgetBox.c +++ b/src/widgets/kpunch/box/widgetBox.c @@ -45,6 +45,7 @@ #include "dvxWgtP.h" #include "box.h" #include "boxMetrics.h" +#include "../label/alignNames.h" // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(box); @@ -84,6 +85,8 @@ typedef struct { // Prototypes // ============================================================ +static int32_t boxGetAlign(const WidgetT *w); +static void boxSetAlign(WidgetT *w, int32_t align); static int32_t frameBoxTop(const BitmapFontT *font); WidgetT *wgtFrame(WidgetT *parent, const char *title); WidgetT *wgtHBox(WidgetT *parent); @@ -93,6 +96,19 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, co void wgtRegister(void); +// BASIC Alignment of a VBox, HBox or Frame (Padding and Spacing are the +// shared wgtGetPaddingPx / wgtSetPaddingPx family). +static int32_t boxGetAlign(const WidgetT *w) { + return (int32_t)w->align; +} + + +static void boxSetAlign(WidgetT *w, int32_t align) { + w->align = (WidgetAlignE)align; + wgtInvalidate(w); +} + + // Offset of the groove below the widget top: half a glyph, so the title // sits centered on the top border line. static int32_t frameBoxTop(const BitmapFontT *font) { @@ -196,6 +212,8 @@ static const WidgetClassT sClassFrame = { [WGT_METHOD_GET_LAYOUT_METRICS] = (void *)widgetFrameGetLayoutMetrics, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox, [WGT_METHOD_LAYOUT] = (void *)widgetLayoutBox, + [WGT_METHOD_GET_TEXT] = (void *)widgetTextGet, + [WGT_METHOD_SET_TEXT] = (void *)widgetTextSet, } }; @@ -221,10 +239,17 @@ static const FrameCreateApiT sFrameApi = { .create = wgtFrame }; +// The same properties on every box type. +static const WgtPropDescT sBoxProps[] = { + { "Alignment", WGT_IFACE_ENUM, (void *)boxGetAlign, (void *)boxSetAlign, sAlignNames }, + { "Padding", WGT_IFACE_INT, (void *)wgtGetPaddingPx, (void *)wgtSetPaddingPx, NULL }, + { "Spacing", WGT_IFACE_INT, (void *)wgtGetSpacingPx, (void *)wgtSetSpacingPx, NULL } +}; + static const WgtIfaceT sIfaceVBox = { .basName = "VBox", - .props = NULL, - .propCount = 0, + .props = sBoxProps, + .propCount = DVX_ARRAY_LEN(sBoxProps), .methods = NULL, .methodCount = 0, .events = NULL, @@ -236,8 +261,8 @@ static const WgtIfaceT sIfaceVBox = { static const WgtIfaceT sIfaceHBox = { .basName = "HBox", - .props = NULL, - .propCount = 0, + .props = sBoxProps, + .propCount = DVX_ARRAY_LEN(sBoxProps), .methods = NULL, .methodCount = 0, .events = NULL, @@ -249,8 +274,8 @@ static const WgtIfaceT sIfaceHBox = { static const WgtIfaceT sIfaceFrame = { .basName = "Frame", - .props = NULL, - .propCount = 0, + .props = sBoxProps, + .propCount = DVX_ARRAY_LEN(sBoxProps), .methods = NULL, .methodCount = 0, .events = NULL, @@ -271,10 +296,13 @@ void wgtRegister(void) { // Per-type APIs and ifaces (for designer/toolbox) wgtRegisterApi("vbox", &sVBoxApi); wgtRegisterIface("vbox", &sIfaceVBox); + wgtIfaceSetType("vbox", sVBoxTypeId); wgtRegisterApi("hbox", &sHBoxApi); wgtRegisterIface("hbox", &sIfaceHBox); + wgtIfaceSetType("hbox", sHBoxTypeId); wgtRegisterApi("frame", &sFrameApi); wgtRegisterIface("frame", &sIfaceFrame); + wgtIfaceSetType("frame", sFrameTypeId); } diff --git a/src/widgets/kpunch/button/button.bhs b/src/widgets/kpunch/button/button.bhs index d954836b..8a897193 100644 --- a/src/widgets/kpunch/button/button.bhs +++ b/src/widgets/kpunch/button/button.bhs @@ -26,6 +26,8 @@ .index CommandButton .index Button .index Click +.index Default +.index Cancel .h1 CommandButton @@ -39,8 +41,12 @@ A push button that triggers an action when clicked. The Caption may include a '& Property Type Description -------- ------ ------------------------------------------- Caption String The text displayed on the button. Use '&' for an accelerator key. + Default Boolean True: Enter presses this button, wherever the focus is on its form (unless the focus is on another button, which Enter presses instead). + Cancel Boolean True: Escape presses this button, wherever the focus is on its form. .endtable +Default and Cancel make a form work like a dialog box: give the OK button Default = True and the Cancel button Cancel = True. If several buttons on a form have the property, the first one (in the order they were made) that is visible and enabled is pressed. + No type-specific methods or events. Default Event: Click diff --git a/src/widgets/kpunch/button/widgetButton.c b/src/widgets/kpunch/button/widgetButton.c index 40c52c08..912c75fa 100644 --- a/src/widgets/kpunch/button/widgetButton.c +++ b/src/widgets/kpunch/button/widgetButton.c @@ -60,6 +60,8 @@ static int32_t sTypeId = -1; typedef struct { const char *text; + bool isDefault; // BASIC Default: Enter on its form presses it + bool isCancel; // BASIC Cancel: Escape on its form presses it } ButtonDataT; @@ -67,12 +69,46 @@ typedef struct { // Prototypes // ============================================================ +static bool basGetCancel(const WidgetT *w); +static bool basGetDefault(const WidgetT *w); +static void basSetCancel(WidgetT *w, bool cancel); +static void basSetDefault(WidgetT *w, bool isDefault); WidgetT *wgtButton(WidgetT *parent, const char *text); void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void wgtRegister(void); +// BASIC Cancel / Default (the form runtime presses the button on Escape / +// Enter, as VB does); the flags change nothing in the widget itself. +static bool basGetCancel(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + + return ((const ButtonDataT *)w->data)->isCancel; +} + + +static bool basGetDefault(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + + return ((const ButtonDataT *)w->data)->isDefault; +} + + +static void basSetCancel(WidgetT *w, bool cancel) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + ((ButtonDataT *)w->data)->isCancel = cancel; +} + + +static void basSetDefault(WidgetT *w, bool isDefault) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + ((ButtonDataT *)w->data)->isDefault = isDefault; +} + + WidgetT *wgtButton(WidgetT *parent, const char *text) { return widgetAllocWithText(parent, sTypeId, sizeof(ButtonDataT), text); } @@ -145,10 +181,15 @@ static const ButtonApiT sApi = { .create = wgtButton }; +static const WgtPropDescT sProps[] = { + { "Cancel", WGT_IFACE_BOOL, (void *)basGetCancel, (void *)basSetCancel, NULL }, + { "Default", WGT_IFACE_BOOL, (void *)basGetDefault, (void *)basSetDefault, NULL } +}; + static const WgtIfaceT sIface = { .basName = "CommandButton", - .props = NULL, - .propCount = 0, + .props = sProps, + .propCount = DVX_ARRAY_LEN(sProps), .methods = NULL, .methodCount = 0, .events = NULL, @@ -162,4 +203,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassButton); wgtRegisterApi("button", &sApi); wgtRegisterIface("button", &sIface); + wgtIfaceSetType("button", sTypeId); } diff --git a/src/widgets/kpunch/canvas/canvas.bhs b/src/widgets/kpunch/canvas/canvas.bhs index 93105acb..8479d40d 100644 --- a/src/widgets/kpunch/canvas/canvas.bhs +++ b/src/widgets/kpunch/canvas/canvas.bhs @@ -28,6 +28,7 @@ .index Drawing .index SetPenColor .index SetPenSize +.index BorderStyle .h1 PictureBox @@ -42,6 +43,7 @@ Colors are Visual Basic RGB values, &HBBGGRR, as RGB() makes them (RGB(255, 0, 0 .table Property Type Description -------- ---- ----------- + BorderStyle Enum 0 - None: no frame, the control is exactly the size of the drawing. 1 - FixedSingle (the default): a sunken frame 2 pixels wide around the drawing. Mouse coordinates and drawing positions start inside the frame either way. ImageHeight Integer Height of the drawing in pixels (read-only). After Load it is the loaded image's height. ImageWidth Integer Width of the drawing in pixels (read-only). After Load it is the loaded image's width. .endtable @@ -59,7 +61,7 @@ Load and Save return 0 on success and -1 on failure (missing or unreadable file, DrawText x%, y%, text$ Draw text at the given position. FillCircle cx%, cy%, radius%, color% Draw a filled circle. FillRect x%, y%, w%, h%, color% Draw a filled rectangle. - GetPixel(x%, y%) Returns the color at a pixel. + GetPixel(x%, y%) Returns the color at a pixel. With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. Load(path$) Load a BMP, PNG, JPEG or GIF image; the canvas takes the image's size. Returns 0 on success, -1 on failure. Resize w%, h% Resize the canvas to new dimensions. The drawing is cleared to white. Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure. @@ -84,7 +86,7 @@ If Picture1.Save("output.bmp") <> 0 Then End If .endcode -A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight) and a Paint event: see Drawing on Forms and PictureBoxes. +A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight) and a Paint event: see Drawing on Forms and PictureBoxes. .link ctrl.common.props Common Properties, Events, and Methods .link ctrl.drawing Drawing on Forms and PictureBoxes diff --git a/src/widgets/kpunch/canvas/canvas.dhs b/src/widgets/kpunch/canvas/canvas.dhs index c92c32ad..e7f6cf27 100644 --- a/src/widgets/kpunch/canvas/canvas.dhs +++ b/src/widgets/kpunch/canvas/canvas.dhs @@ -37,12 +37,18 @@ .index wgtCanvasSave .index wgtCanvasLoad .index wgtCanvasResize +.index wgtCanvasSetBorderStyle +.index wgtCanvasGetBorderStyle +.index wgtCanvasGetDisplay +.index CANVAS_BORDER_NONE +.index CANVAS_BORDER_FIXED +.index CANVAS_BORDER_WIDTH .h2 Canvas A freeform drawing surface with a fixed-size pixel buffer stored in the display's native pixel format. Provides drawing primitives (lines, rectangles, circles, text, individual pixels; coordinates may lie anywhere, drawing is clipped to the buffer) and supports loading BMP/PNG/JPEG/GIF files and saving BMP or PNG files (via dvxLoadImage/dvxSaveImage). Mouse interaction is available via a callback. The buffer is rendered to screen as a straight blit with no per-pixel conversion. -Canvas coordinates are independent of widget position: (0,0) is the top-left of the canvas content, not the widget. The widget frames the buffer with a 2-pixel sunken bevel. +Canvas coordinates are independent of widget position: (0,0) is the top-left of the canvas content, not the widget. By default the widget frames the buffer with a sunken bevel CANVAS_BORDER_WIDTH (2) pixels wide; wgtCanvasSetBorderStyle(w, CANVAS_BORDER_NONE) drops it, so the widget is exactly the buffer's size (a drawing surface for a UserControl or a form). The frame width is the widget's contentOffX / contentOffY, so mouse coordinates are relative to the buffer either way. Header: widgets/canvas.h @@ -76,6 +82,9 @@ WidgetT *cv = wgtCanvas(parent, 320, 200); int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file: BMP when path ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error. int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error. void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white. + void wgtCanvasSetBorderStyle(w, style) CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out. + int32_t wgtCanvasGetBorderStyle(w) CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE. + bool wgtCanvasGetDisplay(w, out) Fill a DisplayT aimed at the canvas pixels (the screen's pixel format, clipped to the canvas), so any libdvx drawing call -- drawText, drawBevel, drawArrowGlyph, wmDrawScrollbarAt, rectCopyTransparent -- draws on the canvas with the context's blit ops; call wgtInvalidatePaint afterwards. False (out untouched) for a widget that is not a canvas. The BASIC DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow and PrintAccel methods use it. .endtable .h3 API Struct (wgtRegisterApi "canvas") @@ -99,15 +108,19 @@ WidgetT *cv = wgtCanvas(parent, 320, 200); getPixel wgtCanvasGetPixel drawText wgtCanvasDrawText resize wgtCanvasResize + setBorderStyle wgtCanvasSetBorderStyle + getBorderStyle wgtCanvasGetBorderStyle + getDisplay wgtCanvasGetDisplay .endtable .h3 Properties -Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers two read-only properties: +Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers these properties: .table Property Type Access Description -------- ---- ------ ----------- + BorderStyle Enum Read/Write None (0) or FixedSingle (1, the default): wgtCanvasSetBorderStyle / wgtCanvasGetBorderStyle. ImageHeight Integer Read-only Height of the canvas buffer in pixels (0 when there is no buffer). ImageWidth Integer Read-only Width of the canvas buffer in pixels (0 when there is no buffer). .endtable @@ -123,7 +136,7 @@ Drawing state (pen color, pen size) is managed by the API functions above. The B DrawText x%, y%, text$ Draw text at the given position. FillCircle cx%, cy%, radius%, color% Fill a circle. FillRect x%, y%, w%, h%, color% Fill a rectangle. - GetPixel(x%, y%) Returns the color at a pixel (&HBBGGRR). + GetPixel(x%, y%) Returns the color at a pixel (&HBBGGRR). With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. Load(path$) Load a BMP, PNG, JPEG or GIF image (the canvas takes its size). Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). Resize w%, h% Resize the canvas (the drawing is cleared to white). Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). diff --git a/src/widgets/kpunch/canvas/canvas.h b/src/widgets/kpunch/canvas/canvas.h index a7081ad1..93cbbac4 100644 --- a/src/widgets/kpunch/canvas/canvas.h +++ b/src/widgets/kpunch/canvas/canvas.h @@ -26,6 +26,13 @@ #include "dvxWgt.h" +// Border styles (setBorderStyle; BASIC PictureBox.BorderStyle). A fixed +// border is a sunken bevel CANVAS_BORDER_WIDTH pixels wide on each side; +// mouse coordinates and the drawing area start inside it either way. +#define CANVAS_BORDER_NONE 0 +#define CANVAS_BORDER_FIXED 1 // the default +#define CANVAS_BORDER_WIDTH 2 + typedef struct { WgtApiHeaderT header; WidgetT *(*create)(WidgetT *parent, int32_t w, int32_t h); @@ -43,6 +50,9 @@ typedef struct { uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y); void (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text); void (*resize)(WidgetT *w, int32_t newW, int32_t newH); + void (*setBorderStyle)(WidgetT *w, int32_t style); + int32_t (*getBorderStyle)(const WidgetT *w); + bool (*getDisplay)(const WidgetT *w, DisplayT *out); } CanvasApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -70,6 +80,9 @@ static inline const CanvasApiT *dvxCanvasApi(void) { #define wgtCanvasGetPixel(w, x, y) dvxCanvasApi()->getPixel(w, x, y) #define wgtCanvasDrawText(w, x, y, text) dvxCanvasApi()->drawText(w, x, y, text) #define wgtCanvasResize(w, newW, newH) dvxCanvasApi()->resize(w, newW, newH) +#define wgtCanvasSetBorderStyle(w, style) dvxCanvasApi()->setBorderStyle(w, style) +#define wgtCanvasGetBorderStyle(w) dvxCanvasApi()->getBorderStyle(w) +#define wgtCanvasGetDisplay(w, out) dvxCanvasApi()->getDisplay(w, out) #endif // CANVAS_IMPL diff --git a/src/widgets/kpunch/canvas/widgetCanvas.c b/src/widgets/kpunch/canvas/widgetCanvas.c index 2a461e54..53568e6a 100644 --- a/src/widgets/kpunch/canvas/widgetCanvas.c +++ b/src/widgets/kpunch/canvas/widgetCanvas.c @@ -54,7 +54,6 @@ DVX_ABI_STAMP(canvas); static int32_t sTypeId = -1; -#define CANVAS_BORDER 2 #define CANVAS_PEN_MAX 255 // largest pen diameter in pixels #define CANVAS_CLIP_MAX_PASSES 8 // Cohen-Sutherland passes (2 endpoints x 4 edges) #define CANVAS_ISQRT_TOP_BIT 62 // highest even bit a squared int32_t radius can set @@ -119,10 +118,13 @@ void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32 void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text); void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); +int32_t wgtCanvasGetBorderStyle(const WidgetT *w); +bool wgtCanvasGetDisplay(const WidgetT *w, DisplayT *out); uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y); int32_t wgtCanvasLoad(WidgetT *w, const char *path); void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH); int32_t wgtCanvasSave(WidgetT *w, const char *path); +void wgtCanvasSetBorderStyle(WidgetT *w, int32_t style); void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)); void wgtCanvasSetPenColor(WidgetT *w, uint32_t color); void wgtCanvasSetPenSize(WidgetT *w, int32_t size); @@ -185,7 +187,7 @@ static int32_t basGetImageWidth(const WidgetT *w) { static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y) { uint32_t packed = wgtCanvasGetPixel(w, x, y); - AppContextT *ctx = wgtGetContext((WidgetT *)w); + AppContextT *ctx = wgtGetContext(w); uint8_t r; uint8_t g; uint8_t b; @@ -576,8 +578,8 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { return NULL; } - wgt->contentOffX = CANVAS_BORDER; - wgt->contentOffY = CANVAS_BORDER; + wgt->contentOffX = CANVAS_BORDER_WIDTH; + wgt->contentOffY = CANVAS_BORDER_WIDTH; cd->pixelData = data; cd->ops = &ctx->blitOps; cd->canvasW = w; @@ -672,16 +674,9 @@ void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text) { return; } - DisplayT tmp = ctx->display; + DisplayT tmp; - tmp.backBuf = cd->pixelData; - tmp.width = cd->canvasW; - tmp.height = cd->canvasH; - tmp.pitch = cd->canvasPitch; - tmp.clipX = 0; - tmp.clipY = 0; - tmp.clipW = cd->canvasW; - tmp.clipH = cd->canvasH; + wgtCanvasGetDisplay(w, &tmp); drawText(&tmp, cd->ops, &ctx->font, x, y, text, cd->penColor, cd->penColor, false); wgtInvalidatePaint(w); } @@ -727,6 +722,41 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t } +// CANVAS_BORDER_FIXED when the canvas has its sunken frame, else +// CANVAS_BORDER_NONE (the frame width lives in contentOffX/Y). +int32_t wgtCanvasGetBorderStyle(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, CANVAS_BORDER_NONE); + + return w->contentOffX > 0 ? CANVAS_BORDER_FIXED : CANVAS_BORDER_NONE; +} + + +// A DisplayT aimed at the canvas pixels (the screen's pixel format, +// clipped to the canvas), so any libdvx drawing call (drawText, drawBevel, +// drawArrowGlyph, drawScrollbar ...) can draw on the canvas with the +// context's blit ops; the caller repaints with wgtInvalidatePaint. False +// (out untouched) for a widget that is not a canvas. +bool wgtCanvasGetDisplay(const WidgetT *w, DisplayT *out) { + CanvasDataT *cd = canvasData(w); + AppContextT *ctx = cd ? wgtGetContext(w) : NULL; + + if (!ctx) { + return false; + } + + *out = ctx->display; + out->backBuf = cd->pixelData; + out->width = cd->canvasW; + out->height = cd->canvasH; + out->pitch = cd->canvasPitch; + out->clipX = 0; + out->clipY = 0; + out->clipW = cd->canvasW; + out->clipH = cd->canvasH; + return true; +} + + uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) { const CanvasDataT *cd = canvasData(w); @@ -817,6 +847,24 @@ int32_t wgtCanvasSave(WidgetT *w, const char *path) { } +// Show or drop the sunken frame. The drawing area keeps its size (the +// widget grows or shrinks by the frame), and mouse coordinates stay +// relative to the drawing area through contentOffX/Y. Any value other +// than CANVAS_BORDER_NONE is the fixed frame. +void wgtCanvasSetBorderStyle(WidgetT *w, int32_t style) { + VALIDATE_WIDGET_VOID(w, sTypeId); + int32_t border = (style == CANVAS_BORDER_NONE) ? 0 : CANVAS_BORDER_WIDTH; + + if (w->contentOffX == border) { + return; + } + + w->contentOffX = border; + w->contentOffY = border; + wgtInvalidate(w); +} + + void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)) { VALIDATE_WIDGET_VOID(w, sTypeId); CanvasDataT *cd = (CanvasDataT *)w->data; @@ -896,8 +944,8 @@ void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; CanvasDataT *cd = (CanvasDataT *)w->data; - w->calcMinW = cd->canvasW + CANVAS_BORDER * 2; - w->calcMinH = cd->canvasH + CANVAS_BORDER * 2; + w->calcMinW = cd->canvasW + w->contentOffX * 2; + w->calcMinH = cd->canvasH + w->contentOffY * 2; } @@ -921,8 +969,8 @@ static void widgetCanvasOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int3 return; } - int32_t cx = vx - w->x - CANVAS_BORDER; - int32_t cy = vy - w->y - CANVAS_BORDER; + int32_t cx = vx - w->x - w->contentOffX; + int32_t cy = vy - w->y - w->contentOffY; uint32_t gen = sWidgetGen; cd->onMouse(w, cx, cy, true); @@ -946,15 +994,16 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { return; } - int32_t cx = vx - hit->x - CANVAS_BORDER; - int32_t cy = vy - hit->y - CANVAS_BORDER; + int32_t cx = vx - hit->x - hit->contentOffX; + int32_t cy = vy - hit->y - hit->contentOffY; sDragWidget = hit; cd->onMouse(hit, cx, cy, false); } -// Paint: draws a sunken bevel border then blits the canvas buffer. Because +// Paint: draws the sunken bevel border (none for CANVAS_BORDER_NONE) then +// blits the canvas buffer. Because // the canvas stores pixels in the display's native format, rectCopy is a // straight memcpy per scanline -- no per-pixel conversion needed. This makes // repaint essentially free relative to the display bandwidth. @@ -969,22 +1018,25 @@ void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma // Bevel wraps tightly around the bitmap, not the full widget. // If the layout stretched the widget beyond the bitmap, fill // the excess area with content background. - int32_t bevelW = cd->canvasW + CANVAS_BORDER * 2; - int32_t bevelH = cd->canvasH + CANVAS_BORDER * 2; + int32_t border = w->contentOffX; + int32_t bevelW = cd->canvasW + border * 2; + int32_t bevelH = cd->canvasH + border * 2; if (w->w > bevelW || w->h > bevelH) { rectFill(d, ops, w->x, w->y, w->w, w->h, colors->contentBg); } - BevelStyleT sunken; - sunken.highlight = colors->windowShadow; - sunken.shadow = colors->windowHighlight; - sunken.face = BEVEL_NO_FILL; - sunken.width = CANVAS_BORDER; - drawBevel(d, ops, w->x, w->y, bevelW, bevelH, &sunken); + if (border > 0) { + BevelStyleT sunken; + sunken.highlight = colors->windowShadow; + sunken.shadow = colors->windowHighlight; + sunken.face = BEVEL_NO_FILL; + sunken.width = border; + drawBevel(d, ops, w->x, w->y, bevelW, bevelH, &sunken); + } // Blit the canvas data inside the border - rectCopy(d, ops, w->x + CANVAS_BORDER, w->y + CANVAS_BORDER, cd->pixelData, cd->canvasPitch, 0, 0, cd->canvasW, cd->canvasH); + rectCopy(d, ops, w->x + border, w->y + border, cd->pixelData, cd->canvasPitch, 0, 0, cd->canvasW, cd->canvasH); } @@ -1020,9 +1072,16 @@ static const CanvasApiT sApi = { .setPixel = wgtCanvasSetPixel, .getPixel = wgtCanvasGetPixel, .drawText = wgtCanvasDrawText, - .resize = wgtCanvasResize + .resize = wgtCanvasResize, + .setBorderStyle = wgtCanvasSetBorderStyle, + .getBorderStyle = wgtCanvasGetBorderStyle, + .getDisplay = wgtCanvasGetDisplay }; +// BorderStyle value names, indexed by CANVAS_BORDER_NONE / _FIXED (VB's +// 0 = None, 1 = Fixed Single). +static const char *sBorderStyleNames[] = { "None", "FixedSingle", NULL }; + static const WgtMethodDescT sMethods[] = { { "Clear", WGT_SIG_INT, (void *)basClear }, { "DrawLine", WGT_SIG_INT5, (void *)basDrawLine }, @@ -1040,8 +1099,9 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtPropDescT sProps[] = { - { "ImageHeight", WGT_IFACE_INT, (void *)basGetImageHeight, NULL, NULL }, - { "ImageWidth", WGT_IFACE_INT, (void *)basGetImageWidth, NULL, NULL }, + { "BorderStyle", WGT_IFACE_ENUM, (void *)wgtCanvasGetBorderStyle, (void *)wgtCanvasSetBorderStyle, sBorderStyleNames }, + { "ImageHeight", WGT_IFACE_INT, (void *)basGetImageHeight, NULL, NULL }, + { "ImageWidth", WGT_IFACE_INT, (void *)basGetImageWidth, NULL, NULL }, }; static const WgtIfaceT sIface = { @@ -1066,4 +1126,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassCanvas); wgtRegisterApi("canvas", &sApi); wgtRegisterIface("canvas", &sIface); + wgtIfaceSetType("canvas", sTypeId); } diff --git a/src/widgets/kpunch/checkbox/widgetCheckbox.c b/src/widgets/kpunch/checkbox/widgetCheckbox.c index 73c3aa5e..3d8aea78 100644 --- a/src/widgets/kpunch/checkbox/widgetCheckbox.c +++ b/src/widgets/kpunch/checkbox/widgetCheckbox.c @@ -234,4 +234,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassCheckbox); wgtRegisterApi("checkbox", &sApi); wgtRegisterIface("checkbox", &sIface); + wgtIfaceSetType("checkbox", sTypeId); } diff --git a/src/widgets/kpunch/comboBox/comboBox.dhs b/src/widgets/kpunch/comboBox/comboBox.dhs index 273b8d3e..fef73095 100644 --- a/src/widgets/kpunch/comboBox/comboBox.dhs +++ b/src/widgets/kpunch/comboBox/comboBox.dhs @@ -32,6 +32,8 @@ .index wgtComboBoxClear .index wgtComboBoxGetSelected .index wgtComboBoxSetSelected +.index wgtComboBoxSetMaxLength +.index wgtComboBoxGetMaxLength .h2 ComboBox @@ -66,6 +68,8 @@ wgtComboBoxAddItem(cb, "Times"); void wgtComboBoxClear(w) Remove all owned items and reset the selection. const char *wgtComboBoxGetItem(w, idx) Get the text of an item by index. int32_t wgtComboBoxGetItemCount(w) Get the total number of items. + void wgtComboBoxSetMaxLength(w, maxLen) Change the maximum editable text length (0 => default 256); longer text is cut. + int32_t wgtComboBoxGetMaxLength(w) Get the maximum editable text length. .endtable .h3 API Struct (wgtRegisterApi "combobox") @@ -83,6 +87,8 @@ wgtComboBoxAddItem(cb, "Times"); clear wgtComboBoxClear getItem wgtComboBoxGetItem getItemCount wgtComboBoxGetItemCount + setMaxLength wgtComboBoxSetMaxLength + getMaxLength wgtComboBoxGetMaxLength .endtable .h3 Properties (BASIC Interface) @@ -91,6 +97,7 @@ wgtComboBoxAddItem(cb, "Times"); Property Type Access Description -------- ---- ------ ----------- ListIndex Integer Read/Write Index of the currently selected item (-1 if none). + MaxLength Integer Read/Write Most characters the edit field holds (0 = the default, 256). .endtable Editable text is accessible via the generic "Text" property. "ListCount" is available through the ListCount method. diff --git a/src/widgets/kpunch/comboBox/comboBox.h b/src/widgets/kpunch/comboBox/comboBox.h index 8e11dc63..823d439f 100644 --- a/src/widgets/kpunch/comboBox/comboBox.h +++ b/src/widgets/kpunch/comboBox/comboBox.h @@ -37,6 +37,8 @@ typedef struct { void (*clear)(WidgetT *w); const char *(*getItem)(const WidgetT *w, int32_t idx); int32_t (*getItemCount)(const WidgetT *w); + void (*setMaxLength)(WidgetT *w, int32_t maxLen); + int32_t (*getMaxLength)(const WidgetT *w); } ComboBoxApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -58,6 +60,8 @@ static inline const ComboBoxApiT *dvxComboBoxApi(void) { #define wgtComboBoxClear(w) dvxComboBoxApi()->clear(w) #define wgtComboBoxGetItem(w, idx) dvxComboBoxApi()->getItem(w, idx) #define wgtComboBoxGetItemCount(w) dvxComboBoxApi()->getItemCount(w) +#define wgtComboBoxSetMaxLength(w, maxLen) dvxComboBoxApi()->setMaxLength(w, maxLen) +#define wgtComboBoxGetMaxLength(w) dvxComboBoxApi()->getMaxLength(w) #endif // COMBOBOX_IMPL diff --git a/src/widgets/kpunch/comboBox/combobox.bhs b/src/widgets/kpunch/comboBox/combobox.bhs index fc23add5..0b809fba 100644 --- a/src/widgets/kpunch/comboBox/combobox.bhs +++ b/src/widgets/kpunch/comboBox/combobox.bhs @@ -38,6 +38,7 @@ A combination of a single-line text input and a drop-down list. The user can eit --------- ------- ------------------------------------------- Text String The text in the editable field. ListIndex Integer Index of the currently selected list item (-1 = user typed something not in the list). + MaxLength Integer Most characters the edit field holds (0 = the default, 256). Longer text is cut when it is set. .endtable .h2 Type-Specific Methods @@ -60,7 +61,7 @@ Default Event: Click Begin ComboBox Combo1 End -Sub Form_Load () +Sub Form1_Load () Combo1.AddItem "Small" Combo1.AddItem "Medium" Combo1.AddItem "Large" diff --git a/src/widgets/kpunch/comboBox/widgetComboBox.c b/src/widgets/kpunch/comboBox/widgetComboBox.c index 85ee0725..fcdb4157 100644 --- a/src/widgets/kpunch/comboBox/widgetComboBox.c +++ b/src/widgets/kpunch/comboBox/widgetComboBox.c @@ -75,9 +75,11 @@ void wgtComboBoxAddItem(WidgetT *w, const char *text); void wgtComboBoxClear(WidgetT *w); const char *wgtComboBoxGetItem(const WidgetT *w, int32_t idx); int32_t wgtComboBoxGetItemCount(const WidgetT *w); +int32_t wgtComboBoxGetMaxLength(const WidgetT *w); int32_t wgtComboBoxGetSelected(const WidgetT *w); void wgtComboBoxRemoveItem(WidgetT *w, int32_t idx); void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count); +void wgtComboBoxSetMaxLength(WidgetT *w, int32_t maxLen); void wgtComboBoxSetSelected(WidgetT *w, int32_t idx); void wgtRegister(void); void widgetComboBoxAccelActivate(WidgetT *w, WidgetT *root); @@ -162,6 +164,14 @@ int32_t wgtComboBoxGetItemCount(const WidgetT *w) { } +// The most characters the edit field holds. +int32_t wgtComboBoxGetMaxLength(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + + return ((const ComboBoxDataT *)w->data)->te.bufSize - 1; +} + + int32_t wgtComboBoxGetSelected(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, -1); const ComboBoxDataT *d = (const ComboBoxDataT *)w->data; @@ -188,6 +198,17 @@ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { // Select an item and copy its text into the edit field. An index outside // the list is stored as "no selection" and leaves the text alone. +// The edit field holds up to maxLen characters (0 = the default, +// TEXT_EDIT_DEFAULT_MAX_LEN); longer text is cut. +void wgtComboBoxSetMaxLength(WidgetT *w, int32_t maxLen) { + VALIDATE_WIDGET_VOID(w, sTypeId); + + if (textEditSingleResize(&((ComboBoxDataT *)w->data)->te, maxLen)) { + wgtInvalidatePaint(w); + } +} + + void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) { VALIDATE_WIDGET_VOID(w, sTypeId); ComboBoxDataT *d = (ComboBoxDataT *)w->data; @@ -418,11 +439,14 @@ static const ComboBoxApiT sApi = { .removeItem = wgtComboBoxRemoveItem, .clear = wgtComboBoxClear, .getItem = wgtComboBoxGetItem, - .getItemCount = wgtComboBoxGetItemCount + .getItemCount = wgtComboBoxGetItemCount, + .setMaxLength = wgtComboBoxSetMaxLength, + .getMaxLength = wgtComboBoxGetMaxLength }; static const WgtPropDescT sProps[] = { - { "ListIndex", WGT_IFACE_INT, (void *)wgtComboBoxGetSelected, (void *)wgtComboBoxSetSelected, NULL } + { "ListIndex", WGT_IFACE_INT, (void *)wgtComboBoxGetSelected, (void *)wgtComboBoxSetSelected, NULL }, + { "MaxLength", WGT_IFACE_INT, (void *)wgtComboBoxGetMaxLength, (void *)wgtComboBoxSetMaxLength, NULL } }; static const WgtMethodDescT sMethods[] = { @@ -450,4 +474,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassComboBox); wgtRegisterApi("combobox", &sApi); wgtRegisterIface("combobox", &sIface); + wgtIfaceSetType("combobox", sTypeId); } diff --git a/src/widgets/kpunch/dataCtrl/datactrl.bhs b/src/widgets/kpunch/dataCtrl/datactrl.bhs index bc1d543a..84a0c7b3 100644 --- a/src/widgets/kpunch/dataCtrl/datactrl.bhs +++ b/src/widgets/kpunch/dataCtrl/datactrl.bhs @@ -57,6 +57,9 @@ A data access control that connects to a SQLite database and provides record nav MasterSource String Name of a master Data control (for master-detail binding). MasterField String Column in the master recordset to read for the filter value. DetailField String Column in this recordset that matches the master field. + RecordCount Long The number of records (read-only). + FieldCount Integer The number of columns (read-only). + AbsolutePosition Long The current record, 0 for the first (-1 when there are none). Setting it moves there (saving pending changes first, as the Move methods do) and fires Reposition; a value out of range does nothing. .endtable .h2 Type-Specific Methods @@ -72,6 +75,10 @@ A data access control that connects to a SQLite database and provides record nav Delete (none) Delete the current record. Refresh (none) Re-query the database and reload records. Update (none) Write pending changes to the database. + GetField(col$) The current record's value of a column, as text, without a bound control ("" for an unknown column). + SetField col$, value$ Change a column of the current record; written to the database by Update (or when the record changes). + FieldName(index%) The name of a column, 0 for the first ("" past the last). + GetCell(row&, col%) The text of any cell (both from 0) without moving the current record ("" outside). .endtable .h2 Type-Specific Events @@ -99,7 +106,7 @@ Begin TextBox Text1 DataField = "title" End -Sub Form_Load () +Sub Form1_Load () Data1.Refresh End Sub .endcode diff --git a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c index 9b984491..867c97b2 100644 --- a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c +++ b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c @@ -140,6 +140,7 @@ static bool autoSave(WidgetT *w); static bool canWriteBack(const DataCtrlDataT *d); static void clearDirty(DataCtrlDataT *d); void dataCtrlAddNew(WidgetT *w); +static bool dataCtrlBasGetField(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font); static WidgetT *dataCtrlCreate(WidgetT *parent); void dataCtrlDelete(WidgetT *w); @@ -287,6 +288,21 @@ void dataCtrlAddNew(WidgetT *w) { } +// GetField(colName) for BASIC (dynamic: no fixed signature returns a +// string from a string): the current row's value of the column, as +// wgtDataCtrlGetField. +static bool dataCtrlBasGetField(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + char buf[WGT_VALUE_TEXT_MAX]; + + if (argc != 1) { + return false; + } + + *ret = wgtValueStr(dataCtrlGetField(w, wgtValueToText(&args[0], buf, (int32_t)sizeof(buf)))); + return true; +} + + static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; w->calcMinW = DATA_BTN_W * DATA_NAV_BTNS + DATA_CAPTION_MIN_W; @@ -1377,31 +1393,38 @@ static const DataCtrlApiT sApi = { }; static const WgtPropDescT sProps[] = { - { "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL }, - { "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL }, - { "KeyColumn", WGT_IFACE_STRING, (void *)dataCtrlGetKeyColumn, (void *)dataCtrlSetKeyColumn, NULL }, - { "MasterSource", WGT_IFACE_STRING, (void *)dataCtrlGetMasterSource, (void *)dataCtrlSetMasterSource, NULL }, - { "MasterField", WGT_IFACE_STRING, (void *)dataCtrlGetMasterField, (void *)dataCtrlSetMasterField, NULL }, - { "DetailField", WGT_IFACE_STRING, (void *)dataCtrlGetDetailField, (void *)dataCtrlSetDetailField, NULL }, - { "Caption", WGT_IFACE_STRING, (void *)dataCtrlGetCaption, (void *)dataCtrlSetCaption, NULL }, - { "BOF", WGT_IFACE_BOOL, (void *)dataCtrlGetBof, NULL, NULL }, - { "EOF", WGT_IFACE_BOOL, (void *)dataCtrlGetEof, NULL, NULL }, + { "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL }, + { "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL }, + { "KeyColumn", WGT_IFACE_STRING, (void *)dataCtrlGetKeyColumn, (void *)dataCtrlSetKeyColumn, NULL }, + { "MasterSource", WGT_IFACE_STRING, (void *)dataCtrlGetMasterSource, (void *)dataCtrlSetMasterSource, NULL }, + { "MasterField", WGT_IFACE_STRING, (void *)dataCtrlGetMasterField, (void *)dataCtrlSetMasterField, NULL }, + { "DetailField", WGT_IFACE_STRING, (void *)dataCtrlGetDetailField, (void *)dataCtrlSetDetailField, NULL }, + { "Caption", WGT_IFACE_STRING, (void *)dataCtrlGetCaption, (void *)dataCtrlSetCaption, NULL }, + { "BOF", WGT_IFACE_BOOL, (void *)dataCtrlGetBof, NULL, NULL }, + { "EOF", WGT_IFACE_BOOL, (void *)dataCtrlGetEof, NULL, NULL }, + { "RecordCount", WGT_IFACE_INT, (void *)dataCtrlGetRowCount, NULL, NULL }, + { "FieldCount", WGT_IFACE_INT, (void *)dataCtrlGetColCount, NULL, NULL }, + { "AbsolutePosition", WGT_IFACE_INT, (void *)dataCtrlGetCurrentRow, (void *)dataCtrlSetCurrentRow, NULL }, }; static const WgtMethodDescT sMethods[] = { - { "AddNew", WGT_SIG_VOID, (void *)dataCtrlAddNew }, - { "Delete", WGT_SIG_VOID, (void *)dataCtrlDelete }, - { "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst }, - { "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast }, - { "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext }, - { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev }, - { "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh }, - { "Update", WGT_SIG_VOID, (void *)dataCtrlUpdate }, + { "AddNew", WGT_SIG_VOID, (void *)dataCtrlAddNew }, + { "Delete", WGT_SIG_VOID, (void *)dataCtrlDelete }, + { "FieldName", WGT_SIG_RET_STR_INT, (void *)dataCtrlGetColName }, + { "GetCell", WGT_SIG_RET_STR_INT_INT, (void *)dataCtrlGetCellText }, + { "GetField", WGT_SIG_DYNAMIC, (void *)dataCtrlBasGetField }, + { "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst }, + { "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast }, + { "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext }, + { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev }, + { "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh }, + { "SetField", WGT_SIG_STR_STR, (void *)dataCtrlSetField }, + { "Update", WGT_SIG_VOID, (void *)dataCtrlUpdate }, }; static const WgtEventDescT sEvents[] = { - { "Reposition" }, - { "Validate" }, + { "Reposition", NULL }, + { "Validate", NULL }, }; static const WgtIfaceT sIface = { @@ -1423,4 +1446,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClass); wgtRegisterApi("data", &sApi); wgtRegisterIface("data", &sIface); + wgtIfaceSetType("data", sTypeId); } diff --git a/src/widgets/kpunch/dbGrid/dbgrid.bhs b/src/widgets/kpunch/dbGrid/dbgrid.bhs index 3a1f5d39..0d6d74fc 100644 --- a/src/widgets/kpunch/dbGrid/dbgrid.bhs +++ b/src/widgets/kpunch/dbGrid/dbgrid.bhs @@ -77,7 +77,7 @@ Begin DBGrid DBGrid1 GridLines = True End -Sub Form_Load () +Sub Form1_Load () Data1.Refresh End Sub diff --git a/src/widgets/kpunch/dbGrid/widgetDbGrid.c b/src/widgets/kpunch/dbGrid/widgetDbGrid.c index 4583b534..20421066 100644 --- a/src/widgets/kpunch/dbGrid/widgetDbGrid.c +++ b/src/widgets/kpunch/dbGrid/widgetDbGrid.c @@ -1206,10 +1206,12 @@ void dbGridSetSelectedRow(WidgetT *w, int32_t row) { static int dbGridSortCmp(const void *a, const void *b) { const DbGridSortKeyT *ka = (const DbGridSortKeyT *)a; const DbGridSortKeyT *kb = (const DbGridSortKeyT *)b; - int32_t cmp = strcasecmp(ka->text, kb->text) * ka->sign; + int32_t cmp = strcasecmp(ka->text, kb->text); + // Only the sign of strcasecmp is defined; negating any other value + // could overflow. if (cmp != 0) { - return cmp; + return (cmp > 0 ? 1 : -1) * ka->sign; } return (ka->row > kb->row) - (ka->row < kb->row); @@ -1398,4 +1400,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClass); wgtRegisterApi("dbgrid", &sApi); wgtRegisterIface("dbgrid", &sIface); + wgtIfaceSetType("dbgrid", sTypeId); } diff --git a/src/widgets/kpunch/dropdown/dropdown.bhs b/src/widgets/kpunch/dropdown/dropdown.bhs index e4a2439d..e4daa9a1 100644 --- a/src/widgets/kpunch/dropdown/dropdown.bhs +++ b/src/widgets/kpunch/dropdown/dropdown.bhs @@ -59,7 +59,7 @@ Default Event: Change (fires when the user picks a new item; Click fires only wh Begin DropDown DropDown1 End -Sub Form_Load () +Sub Form1_Load () DropDown1.AddItem "Small" DropDown1.AddItem "Medium" DropDown1.AddItem "Large" diff --git a/src/widgets/kpunch/dropdown/widgetDropdown.c b/src/widgets/kpunch/dropdown/widgetDropdown.c index aa959b07..622e72f0 100644 --- a/src/widgets/kpunch/dropdown/widgetDropdown.c +++ b/src/widgets/kpunch/dropdown/widgetDropdown.c @@ -410,4 +410,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassDropdown); wgtRegisterApi("dropdown", &sApi); wgtRegisterIface("dropdown", &sIface); + wgtIfaceSetType("dropdown", sTypeId); } diff --git a/src/widgets/kpunch/helpView/helpView.dhs b/src/widgets/kpunch/helpView/helpView.dhs new file mode 100644 index 00000000..75ce8fc1 --- /dev/null +++ b/src/widgets/kpunch/helpView/helpView.dhs @@ -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. +.section Technical Reference (for Developers)|Widgets +.topic widget.helpview +.title HelpView +.toc 0 HelpView +.index HelpView +.index wgtHelpView +.index wgtHelpViewShowTopic +.index wgtHelpViewClear +.index wgtHelpViewGetTopic +.index wgtHelpViewGetLinkTarget +.index wgtHelpViewSetLinkCallback +.index HelpViewLinkFnT + +.h2 HelpView + +One topic of a compiled help file, shown the way the DVX Help Viewer shows it -- the Help Viewer itself uses this widget. Headings (three levels), word-wrapped paragraphs and list items, notes (info, tip and warning), code blocks and tables (preformatted), pictures (left, centered or right), horizontal rules and links, in a scrolling pane, in the theme's colours. Text re-wraps when the widget changes width. + +Links take the keyboard focus (Tab) and are followed with a click, Enter or Space: the widget keeps the link's target topic ID (wgtHelpViewGetLinkTarget) and calls the link callback with it. The widget does not change topic by itself; the callback usually finds the topic (hlpFileFindTopic) and shows it, which may rebuild the pane from inside the link's own handler. Everything a topic shows is copied when it is shown, so the help file may be closed afterwards. + +The help file is read with the help file library (hlpfile/hlpFile.h). + +Header: widgets/helpView.h + +BASIC name: HelpPane (see the HelpPane control in the BASIC reference) + +Depends on: hlpfile.lib + +.h3 Creation + +.code +HlpFileT *file = hlpFileOpen("MYAPP.HLP"); +WidgetT *view = wgtHelpView(parent); + +view->weight = WGT_WEIGHT_FILL; +wgtHelpViewSetLinkCallback(view, onLink); +wgtHelpViewShowTopic(view, file, hlpFileStartTopic(file, NULL)); + +static void onLink(WidgetT *w, const char *target) { + wgtHelpViewShowTopic(w, file, hlpFileFindTopic(file, target)); +} +.endcode + +.h3 Macros + +.table + Macro Description + ----- ----------- + wgtHelpView(parent) Create an empty help view. + wgtHelpViewShowTopic(w, file, topic) Show topic (0 .. hlpFileTopicCount - 1) of an open help file, scrolled to the top. A NULL file or a topic out of range shows nothing. + wgtHelpViewClear(w) Show nothing. + wgtHelpViewGetTopic(w) The topic shown, -1 for none. + wgtHelpViewGetLinkTarget(w) The target topic ID of the last link followed ("" before any). + wgtHelpViewSetLinkCallback(w, fn) Call fn(w, target) when a link is followed. Signature: void (*fn)(WidgetT *w, const char *target); target is valid during the call. After it (unless it rebuilt the pane), the HelpView fires its own event LinkClick (Topic As String) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_LinkClick). +.endtable + +.h3 Events + +.table + Callback Description + -------- ----------- + link callback A link was clicked, or Enter or Space was pressed on a focused link. +.endtable diff --git a/src/widgets/kpunch/helpView/helpView.h b/src/widgets/kpunch/helpView/helpView.h new file mode 100644 index 00000000..f2acffe1 --- /dev/null +++ b/src/widgets/kpunch/helpView/helpView.h @@ -0,0 +1,72 @@ +// 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. +// helpView.h -- HelpView widget API (BASIC type HelpPane) +// +// Shows one topic of a help file (hlpfile library) the way the Help +// Viewer does: headings, word-wrapped paragraphs and lists, notes, code +// blocks and tables, images, rules and links, in a scrolling pane. Links +// take the keyboard focus (Tab) and follow on a click, Enter or Space: +// the widget remembers the link's target topic ID and calls the link +// callback, which normally shows that topic. Everything a topic shows is +// copied when it is shown, so the help file may be closed afterwards. +#ifndef HELPVIEW_H +#define HELPVIEW_H + +#include "dvxWgt.h" + +// An open help file (hlpfile library, hlpFile.h) +struct HlpFileT; + +// Link callback: w is the HelpView, target the link's target topic ID +// (valid during the call). The callback may show another topic. +typedef void (*HelpViewLinkFnT)(WidgetT *w, const char *target); + +typedef struct { + WgtApiHeaderT header; + WidgetT *(*create)(WidgetT *parent); + void (*showTopic)(WidgetT *w, struct HlpFileT *file, int32_t topic); + void (*clear)(WidgetT *w); + int32_t (*getTopic)(const WidgetT *w); + const char *(*getLinkTarget)(const WidgetT *w); + void (*setLinkCallback)(WidgetT *w, HelpViewLinkFnT fn); +} HelpViewApiT; + +// Consumer side: the accessor and convenience macros. The widget's own +// implementation defines HELPVIEW_IMPL so its functions keep their names. +#ifndef HELPVIEW_IMPL + +static inline const HelpViewApiT *dvxHelpViewApi(void) { + static const HelpViewApiT *sApi; + if (!sApi) { sApi = (const HelpViewApiT *)wgtGetApi("helpview", sizeof(HelpViewApiT)); } + return sApi; +} + +#define wgtHelpView(parent) dvxHelpViewApi()->create(parent) +#define wgtHelpViewShowTopic(w, file, topic) dvxHelpViewApi()->showTopic(w, file, topic) +#define wgtHelpViewClear(w) dvxHelpViewApi()->clear(w) +#define wgtHelpViewGetTopic(w) dvxHelpViewApi()->getTopic(w) +#define wgtHelpViewGetLinkTarget(w) dvxHelpViewApi()->getLinkTarget(w) +#define wgtHelpViewSetLinkCallback(w, fn) dvxHelpViewApi()->setLinkCallback(w, fn) + +#endif // HELPVIEW_IMPL + +#endif // HELPVIEW_H diff --git a/src/widgets/kpunch/helpView/helpview.bhs b/src/widgets/kpunch/helpView/helpview.bhs new file mode 100644 index 00000000..4182798d --- /dev/null +++ b/src/widgets/kpunch/helpView/helpview.bhs @@ -0,0 +1,90 @@ +# 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. + +.topic ctrl.helppane +.title HelpPane +.toc 1 Data Display Controls +.toc 1 HelpPane +.index HelpPane +.index ShowTopic +.index LinkClick +.index LinkTopic +.index Help topic display + +.h1 HelpPane + +VB Equivalent: (none) -- DVX Widget: helpview + +Shows one topic of a help file the way the DVX Help Viewer does: headings, paragraphs and lists wrapped to the control's width, notes, code, tables, pictures, rules and links, with a scroll bar when the topic is longer than the control. Open the help file with hlpfile.bas (HlpOpen) and give its handle to ShowTopic. With a TreeView for the table of contents and a few buttons, a HelpPane makes a complete help viewer (see Help Files under Include Libraries). + +A link is followed with a click, or with Enter or Space after Tab has moved to it: the HelpPane fires LinkClick with the ID of the topic the link names and remembers it in LinkTopic. It does not change topic by itself -- the LinkClick event procedure usually shows the topic and adds it to its Back list. Everything a topic shows is copied when it is shown, so closing the help file afterwards does not change what the HelpPane shows. + +.h2 Type-Specific Properties + +.table + Property Type R/W Description + --------- ------- --- ------------------------------------------- + Topic Integer R The topic shown (numbered from 0, as hlpfile.bas numbers them); -1 when nothing is shown. + LinkTopic String R The topic ID of the last link followed ("" before any). +.endtable + +.h2 Type-Specific Methods + +.table + Method Description + ------ ----------- + ShowTopic hlpFile&, topic& Show a topic of an open help file (an HlpOpen handle) from the top. A topic that is not in the file, or a closed handle, shows nothing. + Clear Show nothing. +.endtable + +.h2 Type-Specific Events + +.table + Event Parameters Description + --------- ----------------- ------------------------------------------- + LinkClick Topic As String A link was followed (clicked, or Enter or Space on it). Topic is the ID of the topic it names; HlpFindTopic gives its number. +.endtable + +Default Event: LinkClick + +.h2 Example + +.code +' hlpfile.bas in the project +Dim hlp As Long + +Sub Form1_Load () + hlp = HlpOpen(App.Path + "\MYAPP.HLP") + HelpPane1.ShowTopic hlp, HlpStartTopic(hlp, "") +End Sub + +Sub HelpPane1_LinkClick (Topic As String) + HelpPane1.ShowTopic hlp, HlpFindTopic(hlp, Topic) + Me.Caption = HlpTopicTitle$(hlp, HelpPane1.Topic) +End Sub + +Sub Form1_Unload () + HlpClose hlp +End Sub +.endcode + +.link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/helpView/helpview.bmp b/src/widgets/kpunch/helpView/helpview.bmp new file mode 100644 index 00000000..616ee2f6 --- /dev/null +++ b/src/widgets/kpunch/helpView/helpview.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:509095d02bb60b34e0d0dc060d08dce9db3f017e57a06aa51e6574e3f9b02b5d +size 1782 diff --git a/src/widgets/kpunch/helpView/helpview.dep b/src/widgets/kpunch/helpView/helpview.dep new file mode 100644 index 00000000..c775806d --- /dev/null +++ b/src/widgets/kpunch/helpView/helpview.dep @@ -0,0 +1 @@ +hlpfile diff --git a/src/widgets/kpunch/helpView/helpview.res b/src/widgets/kpunch/helpView/helpview.res new file mode 100644 index 00000000..f4bf0ccc --- /dev/null +++ b/src/widgets/kpunch/helpView/helpview.res @@ -0,0 +1,7 @@ +icon24 icon helpview.bmp +name text "HelpPane" +author text "Scott Duensing" +copyright text "Copyright 2026 Scott Duensing" +publisher text "Kangaroo Punch Studios" +description text "Help topic display (headings, text, links, images)" +version text "1.0" diff --git a/src/widgets/kpunch/helpView/widgetHelpView.c b/src/widgets/kpunch/helpView/widgetHelpView.c new file mode 100644 index 00000000..f4ea5c7c --- /dev/null +++ b/src/widgets/kpunch/helpView/widgetHelpView.c @@ -0,0 +1,1019 @@ +// 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. + +#define HELPVIEW_IMPL +// widgetHelpView.c -- HelpView widget (BASIC type HelpPane): one help +// topic, rendered the way the Help Viewer shows it. +// +// The HelpView owns a ScrollPane holding a VBox; showing a topic rebuilds +// the VBox from the topic's records (hlpFileForEachRecord) as widgets of +// six private classes -- HelpText (paragraphs and list items), +// HelpHeading (three levels), HelpLink (focusable; mouse, Enter or +// Space), HelpNote (info / tip / warning bar), HelpCode (code blocks and +// tables, preformatted) and HelpRule -- plus stock Image widgets +// (centered and right-aligned ones in an HBox). Colours come from the +// theme. +// +// Text wraps to the pane: the wrap width is the pane's inner width, +// measured when the HelpView is measured and checked again when it is +// laid out (a resize measures before the new width is known); a change +// re-measures the content before it is laid out. + +#include "dvxWgtP.h" +#include "dvxApp.h" +#include "dvxDraw.h" +#include "helpView.h" +#include "../../../libs/kpunch/hlpfile/hlpFile.h" +#include "../box/box.h" +#include "../image/image.h" +#include "../scrollPane/scrlPane.h" + +#include +#include +#include + +// ABI stamp checked by the loader (see DVX_ABI_VERSION). +DVX_ABI_STAMP(helpview); + +#define HELP_CONTENT_PAD 6 +#define HELP_EVT_LINKCLICK "LinkClick" // link followed (Topic As String) +#define HELP_PARA_SPACING 12 +#define HELP_HEADING1_TOP 24 +#define HELP_HEADING1_PAD 6 +#define HELP_HEADING2_TOP 16 +#define HELP_HEADING2_PAD 4 +#define HELP_HEADING2_RULE_GAP 1 // gap between heading-2 text and its underline +#define HELP_HEADING2_RULE_H (HELP_HEADING2_RULE_GAP + 1) +#define HELP_HEADING3_TOP 8 +#define HELP_LINK_PAD_X 2 +#define HELP_LINK_PAD_Y 1 +#define HELP_NOTE_PAD 6 +#define HELP_NOTE_PREFIX_W 4 +#define HELP_CODE_PAD 4 +#define HELP_RULE_H 5 // horizontal-rule widget min height +#define HELP_RULE_LINE_Y 2 // rule line offset within the widget +#define HELP_BULLET "\x07 " // list item prefix (bullet glyph + space) +#define HELP_BULLET_LEN 2 + +// Horizontal space inside the pane that text cannot use: the ScrollPane +// border on both sides, its vertical scrollbar and the content VBox +// padding. One character more is kept free when wrapping. +#define HELP_SCROLL_BORDER 2 +#define HELP_WRAP_OVERHEAD (HELP_SCROLL_BORDER * 2 + WGT_SB_W + HELP_CONTENT_PAD * 2) + +// The HelpView's own state +typedef struct { + WidgetT *scroll; // the ScrollPane (only child) + WidgetT *content; // the VBox inside it + char *linkTarget; // target of the last link followed, NULL for none + HelpViewLinkFnT onLink; + int32_t topic; // topic shown, -1 for none + int32_t wrapWidth; // pixel width text wraps at, 0 before the first layout +} HelpViewDataT; + +// Flowing text that is word-wrapped to the pane: paragraphs, list items +// and notes. +typedef struct { + char *text; // original flowing text (no newlines) + char *wrapped; // cached wrapped text (with \n), NULL = needs wrap + int32_t lineCount; // line count of wrapped text + int32_t wrapWidth; // pixel width text was wrapped at (-1 = dirty) + uint8_t noteType; // HLP_NOTE_* (only notes read it) +} HelpWrapDataT; + +typedef struct { + char *text; + int32_t level; +} HelpHeadingDataT; + +typedef struct { + char *displayText; + char *targetTopicId; +} HelpLinkDataT; + +typedef struct { + char *text; + int32_t lineCount; +} HelpCodeDataT; + +// One content record being added: the HelpView and the box records go in. +typedef struct { + HlpFileT *file; + WidgetT *view; + WidgetT *box; +} HelpBuildT; + +static int32_t sTypeId = -1; +static int32_t sHelpTextTypeId = -1; +static int32_t sHelpHeadingTypeId = -1; +static int32_t sHelpLinkTypeId = -1; +static int32_t sHelpNoteTypeId = -1; +static int32_t sHelpCodeTypeId = -1; +static int32_t sHelpRuleTypeId = -1; + +// ============================================================ +// Prototypes +// ============================================================ + +static void attachContentData(WidgetT *w, void *data, bool complete, void (*destroy)(WidgetT *w)); +static void basShowTopic(WidgetT *w, int32_t handle, int32_t topic); +static void buildImage(const HelpBuildT *b, const HlpRecordHdrT *hdr, const char *payload); +static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData); +static int32_t countLines(const char *text); +static void doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t padPixels); +static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpCodeDestroy(WidgetT *w); +static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpHeadingDestroy(WidgetT *w); +static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpLinkDestroy(WidgetT *w); +static void helpLinkFollow(WidgetT *w); +static void helpLinkOnKey(WidgetT *w, int32_t key, int32_t mod); +static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +static void helpViewCalcMinSize(WidgetT *w, const BitmapFontT *font); +static void helpViewDestroy(WidgetT *w); +static void helpViewLayout(WidgetT *w, const BitmapFontT *font); +static int32_t helpViewWrapWidth(const WidgetT *w, const BitmapFontT *font); +static void helpWrapCalcMinSize(WidgetT *w, const BitmapFontT *font, int32_t padPixels, int32_t extraH); +static void helpWrapDestroy(WidgetT *w); +static bool isFirstChild(const WidgetT *w); +static int32_t maxLineWidth(const BitmapFontT *font, const char *text); +static bool nextSiblingHasOwnBorder(const WidgetT *w); +static WidgetT *ownerView(const WidgetT *w); +static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg); +WidgetT *wgtHelpView(WidgetT *parent); +void wgtHelpViewClear(WidgetT *w); +const char *wgtHelpViewGetLinkTarget(const WidgetT *w); +int32_t wgtHelpViewGetTopic(const WidgetT *w); +void wgtHelpViewSetLinkCallback(WidgetT *w, HelpViewLinkFnT fn); +void wgtHelpViewShowTopic(WidgetT *w, HlpFileT *file, int32_t topic); +void wgtRegister(void); + +// ============================================================ +// Widget classes +// ============================================================ + +static const WidgetClassT sClassHelpView = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpViewCalcMinSize, + [WGT_METHOD_LAYOUT] = (void *)helpViewLayout, + [WGT_METHOD_DESTROY] = (void *)helpViewDestroy, + } +}; + +static const WidgetClassT sClassHelpText = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { + [WGT_METHOD_PAINT] = (void *)helpTextPaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpTextCalcMinSize, + [WGT_METHOD_DESTROY] = (void *)helpWrapDestroy, + } +}; + +static const WidgetClassT sClassHelpHeading = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { + [WGT_METHOD_PAINT] = (void *)helpHeadingPaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpHeadingCalcMinSize, + [WGT_METHOD_DESTROY] = (void *)helpHeadingDestroy, + } +}; + +static const WidgetClassT sClassHelpLink = { + .version = WGT_CLASS_VERSION, + .flags = WCLASS_FOCUSABLE, + .handlers = { + [WGT_METHOD_PAINT] = (void *)helpLinkPaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpLinkCalcMinSize, + [WGT_METHOD_ON_MOUSE] = (void *)helpLinkOnMouse, + [WGT_METHOD_ON_KEY] = (void *)helpLinkOnKey, + [WGT_METHOD_DESTROY] = (void *)helpLinkDestroy, + } +}; + +static const WidgetClassT sClassHelpNote = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { + [WGT_METHOD_PAINT] = (void *)helpNotePaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpNoteCalcMinSize, + [WGT_METHOD_DESTROY] = (void *)helpWrapDestroy, + } +}; + +static const WidgetClassT sClassHelpCode = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { + [WGT_METHOD_PAINT] = (void *)helpCodePaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpCodeCalcMinSize, + [WGT_METHOD_DESTROY] = (void *)helpCodeDestroy, + } +}; + +static const WidgetClassT sClassHelpRule = { + .version = WGT_CLASS_VERSION, + .flags = 0, + .handlers = { + [WGT_METHOD_PAINT] = (void *)helpRulePaint, + [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpRuleCalcMinSize, + } +}; + + +// Finishes adding a content widget. When the data or one of its strings +// could not be allocated (complete is false), the partial data is freed +// through destroy and the widget removed, so the paint and measure +// handlers never see missing text. +static void attachContentData(WidgetT *w, void *data, bool complete, void (*destroy)(WidgetT *w)) { + w->data = data; + + if (complete) { + return; + } + + destroy(w); + widgetAllocRollback(w); +} + + +// BASIC ShowTopic hlpFile, topic: the help file is a hlpfile.bas handle. +static void basShowTopic(WidgetT *w, int32_t handle, int32_t topic) { + wgtHelpViewShowTopic(w, hlpFileBasHandle(handle), topic); +} + + +// An image record: its picture decoded for the display, in an HBox when +// it is centered or right-aligned. +static void buildImage(const HelpBuildT *b, const HlpRecordHdrT *hdr, const char *payload) { + HlpImageRefT ref; + uint32_t size = 0; + + if (!hlpRecordImage(hdr, payload, &ref)) { + return; + } + + uint8_t *bmpData = hlpFileReadImage(b->file, &ref, &size); + + if (!bmpData) { + return; + } + + int32_t imgW = 0; + int32_t imgH = 0; + int32_t imgP = 0; + bool hasAlpha = false; + uint32_t keyColor = 0; + uint8_t *pixels = dvxLoadImageAlpha(wgtGetContext(b->view), bmpData, (int32_t)size, &imgW, &imgH, &imgP, &hasAlpha, &keyColor); + + free(bmpData); + + if (!pixels) { + return; + } + + WidgetT *parent = b->box; + + if (hdr->flags == HLP_IMG_CENTER || hdr->flags == HLP_IMG_RIGHT) { + WidgetT *row = wgtHBox(b->box); + + if (row) { + row->align = (hdr->flags == HLP_IMG_CENTER) ? AlignCenterE : AlignEndE; + parent = row; + } + } + + // wgtImage takes ownership of pixels even on failure + WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP); + + if (imgWgt && hasAlpha) { + wgtImageSetTransparent(imgWgt, true, keyColor); + } +} + + +// hlpFileForEachRecord callback that adds one record's widget(s). +static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData) { + const HelpBuildT *b = (const HelpBuildT *)userData; + + switch (hdr->type) { + case HLP_REC_TEXT: + case HLP_REC_LIST_ITEM: + case HLP_REC_NOTE: { + WidgetT *w = widgetAlloc(b->box, (hdr->type == HLP_REC_NOTE) ? sHelpNoteTypeId : sHelpTextTypeId); + + if (!w) { + break; + } + + HelpWrapDataT *wd = (HelpWrapDataT *)calloc(1, sizeof(HelpWrapDataT)); + + if (wd) { + if (hdr->type == HLP_REC_LIST_ITEM) { + size_t pLen = strlen(payload); + + wd->text = (char *)malloc(HELP_BULLET_LEN + pLen + 1); + + if (wd->text) { + memcpy(wd->text, HELP_BULLET, HELP_BULLET_LEN); + memcpy(wd->text + HELP_BULLET_LEN, payload, pLen + 1); + } + } else { + wd->text = strdup(payload); + } + + wd->wrapWidth = -1; + wd->noteType = hdr->flags; + } + + attachContentData(w, wd, wd && wd->text, helpWrapDestroy); + break; + } + + case HLP_REC_HEADING1: + case HLP_REC_HEADING2: + case HLP_REC_HEADING3: { + WidgetT *w = widgetAlloc(b->box, sHelpHeadingTypeId); + + if (!w) { + break; + } + + HelpHeadingDataT *hd = (HelpHeadingDataT *)calloc(1, sizeof(HelpHeadingDataT)); + + if (hd) { + hd->text = strdup(payload); + hd->level = hdr->type - HLP_REC_HEADING1 + 1; + } + + attachContentData(w, hd, hd && hd->text, helpHeadingDestroy); + break; + } + + case HLP_REC_LINK: { + const char *target = NULL; + const char *display = NULL; + + if (!hlpRecordLink(hdr, payload, &target, &display)) { + break; + } + + WidgetT *w = widgetAlloc(b->box, sHelpLinkTypeId); + + if (!w) { + break; + } + + HelpLinkDataT *ld = (HelpLinkDataT *)calloc(1, sizeof(HelpLinkDataT)); + + if (ld) { + ld->displayText = strdup(display); + ld->targetTopicId = strdup(target); + } + + attachContentData(w, ld, ld && ld->displayText && ld->targetTopicId, helpLinkDestroy); + break; + } + + case HLP_REC_IMAGE: + buildImage(b, hdr, payload); + break; + + case HLP_REC_HRULE: + widgetAlloc(b->box, sHelpRuleTypeId); + break; + + // Tables render as preformatted text, identical to code blocks. + case HLP_REC_TABLE: + case HLP_REC_CODE: { + WidgetT *w = widgetAlloc(b->box, sHelpCodeTypeId); + + if (!w) { + break; + } + + HelpCodeDataT *cd = (HelpCodeDataT *)calloc(1, sizeof(HelpCodeDataT)); + + if (cd) { + cd->text = strdup(payload); + cd->lineCount = countLines(cd->text); + } + + attachContentData(w, cd, cd && cd->text, helpCodeDestroy); + break; + } + } + + return true; +} + + +static int32_t countLines(const char *text) { + int32_t count = 1; + + if (!text) { + return count; + } + + for (const char *p = text; *p; p++) { + if (*p == '\n') { + count++; + } + } + + return count; +} + + +// Wraps the widget's text at the given pixel width minus padPixels and +// caches the result (wrapped, lineCount) -- only re-wraps when pixelW +// changes. +static void doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t padPixels) { + if (wd->wrapped && wd->wrapWidth == pixelW) { + return; + } + + free(wd->wrapped); + wd->wrapped = hlpWrapText(wd->text, (pixelW - padPixels) / charW); + wd->wrapWidth = pixelW; + wd->lineCount = countLines(wd->wrapped ? wd->wrapped : wd->text); +} + + +static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font) { + HelpCodeDataT *cd = (HelpCodeDataT *)w->data; + + w->calcMinW = maxLineWidth(font, cd->text) + HELP_CONTENT_PAD * 2 + HELP_CODE_PAD * 2; + w->calcMinH = cd->lineCount * font->charHeight + HELP_CODE_PAD * 2; +} + + +static void helpCodeDestroy(WidgetT *w) { + HelpCodeDataT *cd = (HelpCodeDataT *)w->data; + + if (cd) { + free(cd->text); + free(cd); + w->data = NULL; + } +} + + +static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + HelpCodeDataT *cd = (HelpCodeDataT *)w->data; + int32_t boxX = w->x + HELP_CONTENT_PAD; + int32_t boxW = w->w - HELP_CONTENT_PAD * 2; + + // Sunken background + rectFill(d, ops, boxX, w->y, boxW, w->h, colors->contentBg); + drawHLine(d, ops, boxX, w->y, boxW, colors->windowShadow); + drawHLine(d, ops, boxX, w->y + w->h - 1, boxW, colors->windowHighlight); + + paintLines(d, ops, font, boxX + HELP_CODE_PAD, w->y + HELP_CODE_PAD, cd->text, cd->lineCount, colors->contentFg); +} + + +static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) { + HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; + int32_t textH = font->charHeight; + bool first = isFirstChild(w); + + w->calcMinW = 0; + + if (hd->level == 1) { + int32_t top = first ? 0 : HELP_HEADING1_TOP; + + w->calcMinH = top + textH + HELP_HEADING1_PAD * 2; + } else if (hd->level == 2) { + int32_t top = first ? 0 : HELP_HEADING2_TOP; + int32_t extra = nextSiblingHasOwnBorder(w) ? 0 : HELP_HEADING2_RULE_H; + + w->calcMinH = top + textH + HELP_HEADING2_PAD + extra; + } else { + int32_t top = first ? 0 : HELP_HEADING3_TOP; + + w->calcMinH = top + textH; + } +} + + +static void helpHeadingDestroy(WidgetT *w) { + HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; + + if (hd) { + free(hd->text); + free(hd); + w->data = NULL; + } +} + + +static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; + int32_t textLen = (int32_t)strlen(hd->text); + bool first = isFirstChild(w); + + if (hd->level == 1) { + int32_t top = first ? 0 : HELP_HEADING1_TOP; + int32_t barY = w->y + top; + + rectFill(d, ops, w->x, barY, w->w, w->h - top, colors->activeTitleBg); + drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, barY + HELP_HEADING1_PAD, hd->text, textLen, colors->activeTitleFg, 0, false); + } else if (hd->level == 2) { + int32_t top = first ? 0 : HELP_HEADING2_TOP; + int32_t textY = w->y + top; + + drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false); + + // No underline when the next sibling paints its own top border, + // so there are not two nearly-adjacent lines. + if (!nextSiblingHasOwnBorder(w)) { + int32_t lineY = textY + font->charHeight + HELP_HEADING2_RULE_GAP; + + drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); + } + } else { + int32_t top = first ? 0 : HELP_HEADING3_TOP; + + drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, w->y + top, hd->text, textLen, colors->contentFg, 0, false); + } +} + + +static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font) { + w->calcMinW = 0; + w->calcMinH = font->charHeight + HELP_LINK_PAD_Y * 2; +} + + +static void helpLinkDestroy(WidgetT *w) { + HelpLinkDataT *ld = (HelpLinkDataT *)w->data; + + if (ld) { + free(ld->displayText); + free(ld->targetTopicId); + free(ld); + w->data = NULL; + } +} + + +// Follows a link: the HelpView keeps the target and calls its link +// callback, which may rebuild the content (destroying w), so w is not +// touched afterwards. +static void helpLinkFollow(WidgetT *w) { + HelpLinkDataT *ld = (HelpLinkDataT *)w->data; + WidgetT *view = ownerView(w); + + if (!view) { + return; + } + + HelpViewDataT *hv = (HelpViewDataT *)view->data; + char *target = strdup(ld->targetTopicId); + + if (!target) { + return; + } + + free(hv->linkTarget); + hv->linkTarget = target; + + uint32_t gen = sWidgetGen; + + if (hv->onLink) { + hv->onLink(view, target); + } + + // The callback may have rebuilt the page (freeing target with it) + if (sWidgetGen == gen) { + WgtValueT arg = wgtValueStr(target); + + wgtFireEvent(view, HELP_EVT_LINKCLICK, &arg, 1); + } +} + + +static void helpLinkOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + + if (key == KEY_ENTER || key == KEY_SPACE) { + helpLinkFollow(w); + } +} + + +static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + (void)root; + (void)vx; + (void)vy; + helpLinkFollow(w); +} + + +// Links use the active title colour, underlined. +static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + HelpLinkDataT *ld = (HelpLinkDataT *)w->data; + int32_t textLen = (int32_t)strlen(ld->displayText); + int32_t x = w->x + HELP_CONTENT_PAD + HELP_LINK_PAD_X; + int32_t y = w->y + HELP_LINK_PAD_Y; + int32_t lineW = textLen * font->charWidth; + uint32_t linkColor = colors->activeTitleBg; + + drawTextN(d, ops, font, x, y, ld->displayText, textLen, linkColor, 0, false); + drawHLine(d, ops, x, y + font->charHeight, lineW, linkColor); + + if (w == sFocusedWidget) { + drawFocusRect(d, ops, x - HELP_LINK_PAD_X, w->y, lineW + HELP_LINK_PAD_X * 2, w->h, colors->contentFg); + } +} + + +static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font) { + helpWrapCalcMinSize(w, font, HELP_NOTE_PAD * 2 + HELP_NOTE_PREFIX_W, HELP_NOTE_PAD * 2); +} + + +static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + HelpWrapDataT *wd = (HelpWrapDataT *)w->data; + uint32_t barColor = colors->windowShadow; + + if (wd->noteType == HLP_NOTE_WARNING) { + barColor = colors->activeTitleBg; + } else if (wd->noteType == HLP_NOTE_TIP) { + barColor = colors->menuHighlightBg; + } + + int32_t boxX = w->x + HELP_CONTENT_PAD; + int32_t boxW = w->w - HELP_CONTENT_PAD * 2; + int32_t textX = boxX + HELP_NOTE_PREFIX_W + HELP_NOTE_PAD; + const char *text = wd->wrapped ? wd->wrapped : wd->text; + + rectFill(d, ops, boxX, w->y, boxW, w->h, colors->windowFace); + rectFill(d, ops, boxX, w->y, HELP_NOTE_PREFIX_W, w->h, barColor); + paintLines(d, ops, font, textX, w->y + HELP_NOTE_PAD, text, wd->lineCount, colors->contentFg); +} + + +static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font) { + (void)font; + w->calcMinW = 0; + w->calcMinH = HELP_RULE_H; +} + + +static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + int32_t lineY = w->y + HELP_RULE_LINE_Y; + + (void)font; + drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); + drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY + 1, w->w - HELP_CONTENT_PAD * 2, colors->windowHighlight); +} + + +// Paragraphs and list items. +static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font) { + helpWrapCalcMinSize(w, font, 0, 0); +} + + +static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + HelpWrapDataT *wd = (HelpWrapDataT *)w->data; + const char *text = wd->wrapped ? wd->wrapped : wd->text; + + paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, text, wd->lineCount, colors->contentFg); +} + + +// The HelpView is as small as its ScrollPane allows. The wrap width is +// taken from the current size first, so the content is measured at it. +static void helpViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { + HelpViewDataT *hv = (HelpViewDataT *)w->data; + int32_t wrap = helpViewWrapWidth(w, font); + + if (wrap > 0) { + hv->wrapWidth = wrap; + } + + widgetCalcMinSizeTree(hv->scroll, font); + w->calcMinW = hv->scroll->calcMinW; + w->calcMinH = hv->scroll->calcMinH; +} + + +static void helpViewDestroy(WidgetT *w) { + HelpViewDataT *hv = (HelpViewDataT *)w->data; + + if (hv) { + free(hv->linkTarget); + free(hv); + w->data = NULL; + } +} + + +// The ScrollPane fills the HelpView. A new width (a resize is measured +// before the new size is known) re-wraps the text before the layout. +static void helpViewLayout(WidgetT *w, const BitmapFontT *font) { + HelpViewDataT *hv = (HelpViewDataT *)w->data; + WidgetT *scroll = hv->scroll; + int32_t wrap = helpViewWrapWidth(w, font); + + scroll->x = w->x; + scroll->y = w->y; + scroll->w = w->w; + scroll->h = w->h; + + if (wrap > 0 && wrap != hv->wrapWidth) { + hv->wrapWidth = wrap; + widgetCalcMinSizeTree(scroll, font); + } + + widgetLayoutChildren(scroll, font); +} + + +// The pixel width text wraps at for the HelpView's current width (0 or +// less while it has no width yet). +static int32_t helpViewWrapWidth(const WidgetT *w, const BitmapFontT *font) { + return w->w - HELP_WRAP_OVERHEAD - font->charWidth; +} + + +// Paragraphs, list items and notes: wrapped at the HelpView's wrap width +// less padPixels; extraH is added to the text height. +static void helpWrapCalcMinSize(WidgetT *w, const BitmapFontT *font, int32_t padPixels, int32_t extraH) { + HelpWrapDataT *wd = (HelpWrapDataT *)w->data; + WidgetT *view = ownerView(w); + int32_t wrap = view ? ((HelpViewDataT *)view->data)->wrapWidth : 0; + + if (wrap > 0) { + doWrap(wd, wrap, font->charWidth, padPixels); + } + + w->calcMinW = 0; + w->calcMinH = wd->lineCount * font->charHeight + extraH; +} + + +static void helpWrapDestroy(WidgetT *w) { + HelpWrapDataT *wd = (HelpWrapDataT *)w->data; + + if (wd) { + free(wd->wrapped); + free(wd->text); + free(wd); + w->data = NULL; + } +} + + +static bool isFirstChild(const WidgetT *w) { + return w->parent && w->parent->firstChild == w; +} + + +static int32_t maxLineWidth(const BitmapFontT *font, const char *text) { + int32_t maxW = 0; + int32_t lineLen = 0; + + if (!text) { + return 0; + } + + for (const char *p = text; ; p++) { + if (*p == '\n' || *p == '\0') { + int32_t w = lineLen * font->charWidth; + + if (w > maxW) { + maxW = w; + } + + lineLen = 0; + + if (*p == '\0') { + break; + } + } else { + lineLen++; + } + } + + return maxW; +} + + +// True if w's next sibling is a code or rule widget, which draws its own +// top border -- the heading then omits its bottom spacing to avoid a +// double rule. +static bool nextSiblingHasOwnBorder(const WidgetT *w) { + return w->nextSibling && (w->nextSibling->type == sHelpCodeTypeId || w->nextSibling->type == sHelpRuleTypeId); +} + + +// The HelpView a content widget belongs to, or NULL. +static WidgetT *ownerView(const WidgetT *w) { + for (WidgetT *p = w->parent; p; p = p->parent) { + if (p->type == sTypeId) { + return p; + } + } + + return NULL; +} + + +static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg) { + if (!text) { + return; + } + + const char *line = text; + + for (int32_t i = 0; i < lineCount; i++) { + const char *end = strchr(line, '\n'); + int32_t len = end ? (int32_t)(end - line) : (int32_t)strlen(line); + + if (len > 0) { + drawTextN(d, ops, font, x, y, line, len, fg, 0, false); + } + + y += font->charHeight; + line = end ? end + 1 : line + len; + } +} + + +WidgetT *wgtHelpView(WidgetT *parent) { + WidgetT *w = parent ? widgetAlloc(parent, sTypeId) : NULL; + + if (!w) { + return NULL; + } + + HelpViewDataT *hv = (HelpViewDataT *)calloc(1, sizeof(HelpViewDataT)); + + if (!hv) { + widgetAllocRollback(w); + return NULL; + } + + hv->topic = -1; + w->data = hv; + + hv->scroll = wgtScrollPane(w); + hv->content = hv->scroll ? wgtVBox(hv->scroll) : NULL; + + if (!hv->content) { + widgetDestroyChildren(w); + helpViewDestroy(w); + widgetAllocRollback(w); + return NULL; + } + + hv->scroll->weight = WGT_WEIGHT_FILL; + hv->content->spacing = wgtPixels(HELP_PARA_SPACING); + hv->content->padding = wgtPixels(HELP_CONTENT_PAD); + return w; +} + + +void wgtHelpViewClear(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTypeId); + HelpViewDataT *hv = (HelpViewDataT *)w->data; + + widgetDestroyChildren(hv->content); + hv->topic = -1; + wgtInvalidate(w); +} + + +// Target topic ID of the last link followed ("" before any). +const char *wgtHelpViewGetLinkTarget(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, ""); + HelpViewDataT *hv = (HelpViewDataT *)w->data; + + return hv->linkTarget ? hv->linkTarget : ""; +} + + +int32_t wgtHelpViewGetTopic(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, -1); + HelpViewDataT *hv = (HelpViewDataT *)w->data; + + return hv->topic; +} + + +void wgtHelpViewSetLinkCallback(WidgetT *w, HelpViewLinkFnT fn) { + VALIDATE_WIDGET_VOID(w, sTypeId); + HelpViewDataT *hv = (HelpViewDataT *)w->data; + + hv->onLink = fn; +} + + +// Shows topic of file from the top (a bad file or topic shows nothing). +// Scrolling to the top relayouts the whole window, new content included. +void wgtHelpViewShowTopic(WidgetT *w, HlpFileT *file, int32_t topic) { + VALIDATE_WIDGET_VOID(w, sTypeId); + HelpViewDataT *hv = (HelpViewDataT *)w->data; + HelpBuildT b = { file, w, hv->content }; + + widgetDestroyChildren(hv->content); + hv->topic = (topic >= 0 && topic < hlpFileTopicCount(file)) ? topic : -1; + + if (hv->topic >= 0) { + hlpFileForEachRecord(file, topic, buildRecord, &b); + } + + wgtScrollPaneScrollToTop(hv->scroll); +} + + +// ============================================================ +// DXE registration +// ============================================================ + +static const HelpViewApiT sHelpViewApi = { + .header = WGT_API_HEADER(HelpViewApiT), + .create = wgtHelpView, + .showTopic = wgtHelpViewShowTopic, + .clear = wgtHelpViewClear, + .getTopic = wgtHelpViewGetTopic, + .getLinkTarget = wgtHelpViewGetLinkTarget, + .setLinkCallback = wgtHelpViewSetLinkCallback +}; + +static const WgtPropDescT sProps[] = { + { "LinkTopic", WGT_IFACE_STRING, (void *)wgtHelpViewGetLinkTarget, NULL, NULL }, + { "Topic", WGT_IFACE_INT, (void *)wgtHelpViewGetTopic, NULL, NULL } +}; + +static const WgtMethodDescT sMethods[] = { + { "Clear", WGT_SIG_VOID, (void *)wgtHelpViewClear }, + { "ShowTopic", WGT_SIG_INT_INT, (void *)basShowTopic } +}; + +// LinkClick is fired with wgtFireEvent after the link callback. +static const WgtEventDescT sEvents[] = { + { HELP_EVT_LINKCLICK, "Topic As String" } +}; + +static const WgtIfaceT sIface = { + .basName = "HelpPane", + .props = sProps, + .propCount = DVX_ARRAY_LEN(sProps), + .methods = sMethods, + .methodCount = DVX_ARRAY_LEN(sMethods), + .events = sEvents, + .eventCount = DVX_ARRAY_LEN(sEvents), + .createSig = WGT_CREATE_PARENT, + .defaultEvent = "LinkClick" +}; + + +void wgtRegister(void) { + sTypeId = wgtRegisterClass(&sClassHelpView); + sHelpTextTypeId = wgtRegisterClass(&sClassHelpText); + sHelpHeadingTypeId = wgtRegisterClass(&sClassHelpHeading); + sHelpLinkTypeId = wgtRegisterClass(&sClassHelpLink); + sHelpNoteTypeId = wgtRegisterClass(&sClassHelpNote); + sHelpCodeTypeId = wgtRegisterClass(&sClassHelpCode); + sHelpRuleTypeId = wgtRegisterClass(&sClassHelpRule); + wgtRegisterApi("helpview", &sHelpViewApi); + wgtRegisterIface("helpview", &sIface); + wgtIfaceSetType("helpview", sTypeId); +} diff --git a/src/widgets/kpunch/image/image.bhs b/src/widgets/kpunch/image/image.bhs index 93a71bad..ce4cdc36 100644 --- a/src/widgets/kpunch/image/image.bhs +++ b/src/widgets/kpunch/image/image.bhs @@ -26,6 +26,8 @@ .index Image .index Picture .index BMP +.index MaskColor +.index UseMaskColor .h1 Image @@ -38,10 +40,12 @@ A static image display control. Loads bitmap images from file. Set the Picture p .table Property Type Description ----------- ------- ------------------------------------------- - Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE). Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent). + Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent). Stretch Boolean When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size. ImageWidth Integer Width of the loaded image in pixels (read-only). ImageHeight Integer Height of the loaded image in pixels (read-only). + MaskColor Long The transparent colour (an RGB value): with UseMaskColor True, pixels of this colour are not drawn, so the form shows through. + UseMaskColor Boolean True leaves out the pixels of MaskColor (default False). .endtable No type-specific methods or events. diff --git a/src/widgets/kpunch/image/widgetImage.c b/src/widgets/kpunch/image/widgetImage.c index ffd98237..91cc1a10 100644 --- a/src/widgets/kpunch/image/widgetImage.c +++ b/src/widgets/kpunch/image/widgetImage.c @@ -47,9 +47,11 @@ // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(image); -#define IMAGE_FIX_SHIFT 16 // 16.16 fixed point for the scaled blit -#define IMAGE_BPP8 1 -#define IMAGE_BPP16 2 +#define IMAGE_FIX_SHIFT 16 // 16.16 fixed point for the scaled blit +#define IMAGE_BPP8 1 +#define IMAGE_BPP16 2 +#define IMAGE_RED_SHIFT 16 // &HRRGGBB channels +#define IMAGE_GREEN_SHIFT 8 static int32_t sTypeId = -1; @@ -69,13 +71,17 @@ static void imagePaintScaled(DisplayT *disp, const uint8_t *src, int32_t WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int32_t pitch); WidgetT *wgtImageFromFile(WidgetT *parent, const char *path); static int32_t wgtImageGetHeight(const WidgetT *w); +static int32_t wgtImageGetMaskColor(const WidgetT *w); static const char *wgtImageGetPicture(const WidgetT *w); static bool wgtImageGetStretch(const WidgetT *w); +static bool wgtImageGetUseMaskColor(const WidgetT *w); static int32_t wgtImageGetWidth(const WidgetT *w); static void wgtImageLoadFile(WidgetT *w, const char *path); void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, int32_t pitch); +static void wgtImageSetMaskColor(WidgetT *w, int32_t color); static void wgtImageSetStretch(WidgetT *w, bool stretch); void wgtImageSetTransparent(WidgetT *w, bool hasTransparency, uint32_t keyColor); +static void wgtImageSetUseMaskColor(WidgetT *w, bool use); void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetImageDestroy(WidgetT *w); void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -211,6 +217,24 @@ static int32_t wgtImageGetHeight(const WidgetT *w) { } +// MaskColor (BASIC, &HBBGGRR): the transparent key colour +// (wgtImageSetTransparent's, unpacked from the display format). +static int32_t wgtImageGetMaskColor(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + const ImageDataT *d = (const ImageDataT *)w->data; + AppContextT *ctx = wgtGetContext(w); + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + + if (ctx) { + unpackColor(&ctx->display, d->keyColor, &r, &g, &b); + } + + return (int32_t)wgtBasicColor(((uint32_t)r << IMAGE_RED_SHIFT) | ((uint32_t)g << IMAGE_GREEN_SHIFT) | b); +} + + static const char *wgtImageGetPicture(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, ""); ImageDataT *d = (ImageDataT *)w->data; @@ -225,6 +249,13 @@ static bool wgtImageGetStretch(const WidgetT *w) { } +// UseMaskColor: whether pixels of MaskColor are left out. +static bool wgtImageGetUseMaskColor(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + return ((const ImageDataT *)w->data)->hasTransparency; +} + + static int32_t wgtImageGetWidth(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); ImageDataT *d = (ImageDataT *)w->data; @@ -253,6 +284,17 @@ void wgtImageSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t imgH, } +static void wgtImageSetMaskColor(WidgetT *w, int32_t color) { + VALIDATE_WIDGET_VOID(w, sTypeId); + AppContextT *ctx = wgtGetContext(w); + uint32_t rgb = wgtBasicColor((uint32_t)color); + + if (ctx) { + wgtImageSetTransparent(w, ((ImageDataT *)w->data)->hasTransparency, packColor(&ctx->display, (uint8_t)(rgb >> IMAGE_RED_SHIFT), (uint8_t)(rgb >> IMAGE_GREEN_SHIFT), (uint8_t)rgb)); + } +} + + // Stretch changes the min size (0x0 vs image size), so relayout. static void wgtImageSetStretch(WidgetT *w, bool stretch) { VALIDATE_WIDGET_VOID(w, sTypeId); @@ -274,6 +316,12 @@ void wgtImageSetTransparent(WidgetT *w, bool hasTransparency, uint32_t keyColor) } +static void wgtImageSetUseMaskColor(WidgetT *w, bool use) { + VALIDATE_WIDGET_VOID(w, sTypeId); + wgtImageSetTransparent(w, use, ((ImageDataT *)w->data)->keyColor); +} + + void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; ImageDataT *d = (ImageDataT *)w->data; @@ -385,10 +433,12 @@ static const ImageApiT sApi = { }; static const WgtPropDescT sProps[] = { - { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL }, - { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL }, - { "Picture", WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL }, - { "Stretch", WGT_IFACE_BOOL, (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL } + { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL }, + { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL }, + { "MaskColor", WGT_IFACE_INT, (void *)wgtImageGetMaskColor, (void *)wgtImageSetMaskColor, NULL }, + { "Picture", WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL }, + { "Stretch", WGT_IFACE_BOOL, (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL }, + { "UseMaskColor", WGT_IFACE_BOOL, (void *)wgtImageGetUseMaskColor, (void *)wgtImageSetUseMaskColor, NULL } }; static const WgtIfaceT sIface = { @@ -407,4 +457,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassImage); wgtRegisterApi("image", &sApi); wgtRegisterIface("image", &sIface); + wgtIfaceSetType("image", sTypeId); } diff --git a/src/widgets/kpunch/imageButton/imgbtn.bhs b/src/widgets/kpunch/imageButton/imgbtn.bhs index 279ed5e5..fb8c4385 100644 --- a/src/widgets/kpunch/imageButton/imgbtn.bhs +++ b/src/widgets/kpunch/imageButton/imgbtn.bhs @@ -36,7 +36,7 @@ A button that displays an image instead of text. Has the same press/release beha .table Property Type Description ----------- ------- ------------------------------------------- - Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE). Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. + Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. ImageWidth Integer Width of the loaded image in pixels (read-only). ImageHeight Integer Height of the loaded image in pixels (read-only). .endtable diff --git a/src/widgets/kpunch/imageButton/widgetImageButton.c b/src/widgets/kpunch/imageButton/widgetImageButton.c index 7dcd5c3b..bf525e57 100644 --- a/src/widgets/kpunch/imageButton/widgetImageButton.c +++ b/src/widgets/kpunch/imageButton/widgetImageButton.c @@ -262,4 +262,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassImageButton); wgtRegisterApi("imagebutton", &sApi); wgtRegisterIface("imagebutton", &sIface); + wgtIfaceSetType("imagebutton", sTypeId); } diff --git a/src/widgets/kpunch/label/widgetLabel.c b/src/widgets/kpunch/label/widgetLabel.c index 27165d5d..915b31d2 100644 --- a/src/widgets/kpunch/label/widgetLabel.c +++ b/src/widgets/kpunch/label/widgetLabel.c @@ -187,4 +187,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassLabel); wgtRegisterApi("label", &sApi); wgtRegisterIface("label", &sIface); + wgtIfaceSetType("label", sTypeId); } diff --git a/src/widgets/kpunch/listBox/listbox.bhs b/src/widgets/kpunch/listBox/listbox.bhs index 4e56d527..13146fcc 100644 --- a/src/widgets/kpunch/listBox/listbox.bhs +++ b/src/widgets/kpunch/listBox/listbox.bhs @@ -67,7 +67,7 @@ Default Event: Click .h2 Example .code -Sub Form_Load () +Sub Form1_Load () List1.AddItem "Apple" List1.AddItem "Banana" List1.AddItem "Cherry" diff --git a/src/widgets/kpunch/listBox/widgetListBox.c b/src/widgets/kpunch/listBox/widgetListBox.c index 194a82a8..5db620a4 100644 --- a/src/widgets/kpunch/listBox/widgetListBox.c +++ b/src/widgets/kpunch/listBox/widgetListBox.c @@ -789,4 +789,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassListBox); wgtRegisterApi("listbox", &sApi); wgtRegisterIface("listbox", &sIface); + wgtIfaceSetType("listbox", sTypeId); } diff --git a/src/widgets/kpunch/listView/listView.dhs b/src/widgets/kpunch/listView/listView.dhs index a3cd4381..ff529a2b 100644 --- a/src/widgets/kpunch/listView/listView.dhs +++ b/src/widgets/kpunch/listView/listView.dhs @@ -92,7 +92,7 @@ typedef enum { wgtListViewGetSelected(w) Get the index of the selected row (-1 if none). wgtListViewSetSelected(w, idx) Set the selected row by index (outside the list = none) and scroll it into view. wgtListViewSetSort(w, col, dir) Set the sort column and direction. A column outside the defined columns, or ListViewSortNoneE, clears the sort. Ignored while reorder is enabled. - wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). + wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). After it, the ListView fires its own event ColumnClick (Column As Integer, 0-based) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_ColumnClick). wgtListViewSetMultiSelect(w, multi) Enable or disable multi-selection. wgtListViewIsItemSelected(w, idx) Check if a specific row is selected. wgtListViewSetItemSelected(w, idx, selected) Select or deselect a specific row. diff --git a/src/widgets/kpunch/listView/listview.bhs b/src/widgets/kpunch/listView/listview.bhs index f786b903..136dca4c 100644 --- a/src/widgets/kpunch/listView/listview.bhs +++ b/src/widgets/kpunch/listView/listview.bhs @@ -28,6 +28,9 @@ .index Multi-Column List .index SetColumns .index SetSort +.index SetRows +.index SelectedRows +.index ColumnClick .h1 ListView @@ -53,17 +56,28 @@ A multi-column list with column headers. Supports sorting, multi-select, and dra ClearSelection Deselect all rows. GetCell$(row%, col%) Returns the text of a cell. IsItemSelected(index%) Returns True if the row is selected. + ListCount() Returns the number of rows (the same as RowCount; also read as the ListCount property). RemoveItem index% Remove a row by index. Rows after it keep their selection; removing the selected row leaves no selection. RowCount() Returns the number of rows. SelectAll Select all rows. + SelectedRows$() Returns the numbers of the selected rows (as IsItemSelected numbers them) separated by spaces, "" when none: one call instead of IsItemSelected for every row of a long list. SetCell row%, col%, text$ Set the text of a cell. - SetColumns spec$ Define columns. Pipe-delimited "Name,Width|Name,Width" (width in characters). + SetColumns spec$ Define columns. Pipe-delimited "Name,Width,Align|Name,Width": the width in characters (missing or 0 = as wide as the contents), then Left (the default), Center or Right for the column's text. SetItemSelected index%, selected Select or deselect a row. SetMultiSelect multi Enable or disable multi-select. SetReorderable reorderable Enable or disable row reordering. + SetRows text$ Replace all the rows at once: one row per line (separated by CHR$(10)), cells separated by CHR$(9) (Tab); missing cells are blank and extra ones are dropped. Much faster than AddItem and SetCell for long lists. The selection is cleared and the cursor goes to the first row. SetSort col%, dir% Sort by column. dir: 0=none, 1=ascending, 2=descending. An invalid column or direction clears the sort. Has no effect while reordering is enabled. .endtable +.h2 Type-Specific Events + +.table + Event Parameters Description + ----------- ---------------------- ------------------------------------------- + ColumnClick Column As Integer A column header was clicked (Column 0 is the first). Without a handler the list sorts itself by that column's text (the same column again reverses); a program that orders the rows itself calls SetSort -1, 0 in the handler and fills the list in its own order. +.endtable + Default Event: Click .h2 Example @@ -74,6 +88,10 @@ ListView1.AddItem "readme.txt" ListView1.SetCell 0, 1, "1024" ListView1.SetCell 0, 2, "Text" ListView1.SetSort 0, 1 + +' Many rows at once; the size column right-aligned +ListView2.SetColumns "Name,14|Size,10,Right" +ListView2.SetRows "a.txt" & Chr$(9) & "120" & Chr$(10) & "b.txt" & Chr$(9) & "4,096" .endcode .link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/listView/widgetListView.c b/src/widgets/kpunch/listView/widgetListView.c index 3b15f954..3e86eb83 100644 --- a/src/widgets/kpunch/listView/widgetListView.c +++ b/src/widgets/kpunch/listView/widgetListView.c @@ -69,16 +69,19 @@ #include "dvxWgtP.h" #include "listView.h" +#include "../label/alignNames.h" #include "../../../libs/kpunch/listhelp/listHelp.h" #include #include +#include #include "stb_ds_wrap.h" // ABI stamp checked by the loader (see DVX_ABI_VERSION). DVX_ABI_STAMP(listview); #define LISTVIEW_BORDER 2 +#define LISTVIEW_EVT_COLUMNCLICK "ColumnClick" // header click (Column As Integer, 0-based) #define LISTVIEW_MIN_COL_W 20 #define LISTVIEW_PAD 3 #define LISTVIEW_MIN_ROWS 4 @@ -86,6 +89,8 @@ DVX_ABI_STAMP(listview); #define LISTVIEW_MIN_CHARS 12 #define LISTVIEW_COL_PAD 6 #define LISTVIEW_SORT_W 10 +// " 2147483647": one row number of SelectedRows$ with its separator. +#define LISTVIEW_ROW_TEXT_MAX 16 // Header row vertical padding: font->charHeight + LISTVIEW_HEADER_VPAD. #define LISTVIEW_HEADER_VPAD 4 // Bevel width of each header button. @@ -154,7 +159,10 @@ typedef struct { // Prototypes // ============================================================ +static const char *basSelectedRows(const WidgetT *w); static void basSetColumns(WidgetT *w, const char *spec); +static void basSetRows(WidgetT *w, const char *text); +static char *cellCopy(const char *text, int32_t len); static void listViewAdoptCells(WidgetT *w); static int32_t listViewAutoColWidth(const ListViewDataT *lv, int32_t col, const BitmapFontT *font); static void listViewBuildSortIndex(ListViewDataT *lv); @@ -203,10 +211,38 @@ static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); -// BASIC wrapper: SetColumns "Name,10|Description,20" (title[,width in -// characters]; no width = auto). The new definitions are built and -// installed before the previous owned ones are freed, so lv->cols never -// dangles. +// BASIC wrapper: SelectedRows$ -- the selected rows' numbers (data rows, +// as IsItemSelected numbers them), separated by spaces, "" when none. One +// call instead of IsItemSelected for every row of a long list. The text +// stays valid until the next call. +static const char *basSelectedRows(const WidgetT *w) { + static char *text = NULL; // stb_ds array, reused + + VALIDATE_WIDGET(w, sTypeId, ""); + const ListViewDataT *lv = (const ListViewDataT *)w->data; + + arrsetlen(text, 0); + + for (int32_t r = 0; r < lv->rowCount; r++) { + if (listSelIsSelected(&lv->sel, lv->rowCount, r)) { + char num[LISTVIEW_ROW_TEXT_MAX]; + int32_t len = snprintf(num, sizeof(num), arrlen(text) > 0 ? " %ld" : "%ld", (long)r); + + for (int32_t i = 0; i < len; i++) { + arrput(text, num[i]); + } + } + } + + arrput(text, '\0'); + return text; +} + + +// BASIC wrapper: SetColumns "Name,10|Size,12,Right|Description" (title, +// then the width in characters -- missing or 0 = auto -- then Left, Center +// or Right; missing = Left). The new definitions are built and installed +// before the previous owned ones are freed, so lv->cols never dangles. static void basSetColumns(WidgetT *w, const char *spec) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; @@ -239,8 +275,16 @@ static void basSetColumns(WidgetT *w, const char *spec) { col.align = ListViewAlignLeftE; if (comma) { + char *alignText = strchr(comma + 1, ','); + *comma = '\0'; col.width = wgtChars(atoi(comma + 1)); + + for (int32_t a = 0; alignText && sAlignNames[a]; a++) { + if (strcasecmp(alignText + 1, sAlignNames[a]) == 0) { + col.align = (ListViewAlignE)a; + } + } } arrput(titles, strdup(tok)); @@ -269,6 +313,70 @@ static void basSetColumns(WidgetT *w, const char *spec) { } +// BASIC wrapper: SetRows text$ -- replace every row at once. Rows end +// at a line feed, cells are separated by tabs; a row with fewer cells than +// columns gets blank ones, extra cells are dropped; "" empties the list. +// One call instead of AddItem and SetCell for each cell of a long list. +// As with new data (wgtListViewSetData): the selection is cleared, the +// cursor stays on its row number when the new list has it (else the first +// row), and the scroll position stays. +static void basSetRows(WidgetT *w, const char *text) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ListViewDataT *lv = (ListViewDataT *)w->data; + const char *p = text ? text : ""; + + listReorderCancel(w, &lv->dragIdx, &lv->dropIdx); + widgetFreeOwnedStrings(lv->ownedCells); + + while (*p && lv->colCount > 0) { + const char *eol = strchr(p, '\n'); + const char *end = eol ? eol : p + strlen(p); + + for (int32_t c = 0; c < lv->colCount; c++) { + const char *tab = memchr(p, '\t', (size_t)(end - p)); + const char *stop = tab ? tab : end; + char *cell = cellCopy(p, (int32_t)(stop - p)); + + if (!cell) { + // Out of memory: an empty list rather than a partial row + widgetFreeOwnedStrings(lv->ownedCells); + end = p + strlen(p); + break; + } + + arrput(lv->ownedCells, cell); + p = tab ? tab + 1 : end; + } + + p = (*end && end == eol) ? eol + 1 : end; + } + + lv->nextCell = (int32_t)arrlen(lv->ownedCells); + listViewSyncOwned(w); + + if (lv->sel.cursor < 0 || lv->sel.cursor >= lv->rowCount) { + lv->sel.cursor = lv->rowCount > 0 ? 0 : -1; + } + + lv->sel.anchor = lv->sel.cursor; + listSelReset(&lv->sel, lv->rowCount); +} + + +// A heap copy of len characters of text, NUL-terminated (NULL when out of +// memory). +static char *cellCopy(const char *text, int32_t len) { + char *copy = (char *)malloc((size_t)len + 1); + + if (copy) { + memcpy(copy, text, (size_t)len); + copy[len] = '\0'; + } + + return copy; +} + + // Copy the current (application-owned, external) cell data into the internally // owned storage so the list view may rearrange it. A reorderable list view // MUST own its cells: drag-reorder rewrites the cell array in place and must @@ -1304,7 +1412,9 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) lv->onHeaderClick(hit, c, lv->sortDir); } - if (sWidgetGen == gen) { + WgtValueT arg = wgtValueInt(c); + + if (sWidgetGen == gen && wgtFireEvent(hit, LISTVIEW_EVT_COLUMNCLICK, &arg, 1)) { wgtInvalidatePaint(hit); } @@ -1675,25 +1785,33 @@ static const WgtMethodDescT sMethods[] = { { "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection }, { "GetCell", WGT_SIG_RET_STR_INT_INT, (void *)wgtListViewGetCell }, { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected }, + { "ListCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount }, { "RemoveItem", WGT_SIG_INT, (void *)wgtListViewRemoveRow }, { "RowCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount }, { "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll }, + { "SelectedRows", WGT_SIG_RET_STR, (void *)basSelectedRows }, { "SetCell", WGT_SIG_INT_INT_STR, (void *)wgtListViewSetCell }, { "SetColumns", WGT_SIG_STR, (void *)basSetColumns }, { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable }, + { "SetRows", WGT_SIG_STR, (void *)basSetRows }, { "SetSort", WGT_SIG_INT_INT, (void *)wgtListViewSetSort }, }; +// ColumnClick is fired with wgtFireEvent after the header-click callback. +static const WgtEventDescT sEvents[] = { + { LISTVIEW_EVT_COLUMNCLICK, "Column As Integer" } +}; + static const WgtIfaceT sIface = { .basName = "ListView", .props = sProps, .propCount = DVX_ARRAY_LEN(sProps), .methods = sMethods, .methodCount = DVX_ARRAY_LEN(sMethods), - .events = NULL, - .eventCount = 0, + .events = sEvents, + .eventCount = DVX_ARRAY_LEN(sEvents), .createSig = WGT_CREATE_PARENT, .defaultEvent = "Click" }; @@ -1702,4 +1820,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassListView); wgtRegisterApi("listview", &sApi); wgtRegisterIface("listview", &sIface); + wgtIfaceSetType("listview", sTypeId); } diff --git a/src/widgets/kpunch/progressBar/progress.bhs b/src/widgets/kpunch/progressBar/progress.bhs index 8c81553b..495cf7fd 100644 --- a/src/widgets/kpunch/progressBar/progress.bhs +++ b/src/widgets/kpunch/progressBar/progress.bhs @@ -29,7 +29,7 @@ VB Equivalent: ProgressBar -- DVX Widget: progressbar -A horizontal progress indicator bar. +A progress indicator bar, lying flat (the default) or standing upright. .h2 Type-Specific Properties @@ -37,6 +37,7 @@ A horizontal progress indicator bar. Property Type Description -------- ------- ------------------------------------------- Value Integer Current progress value (0-100). + Vertical Boolean True: the bar stands upright and fills from the bottom up. False (the default): it lies flat and fills from left to right. Can be set in the designer or at run time. .endtable No type-specific events or methods. No default event. diff --git a/src/widgets/kpunch/progressBar/progress.dhs b/src/widgets/kpunch/progressBar/progress.dhs index 5846a637..5f388bca 100644 --- a/src/widgets/kpunch/progressBar/progress.dhs +++ b/src/widgets/kpunch/progressBar/progress.dhs @@ -29,6 +29,8 @@ .index wgtProgressBarV .index wgtProgressBarSetValue .index wgtProgressBarGetValue +.index wgtProgressBarSetVertical +.index wgtProgressBarGetVertical .h2 ProgressBar @@ -54,6 +56,8 @@ Header: widgets/progress.h WidgetT *wgtProgressBarV(parent) Create a vertical progress bar. void wgtProgressBarSetValue(w, val) Set the progress value (0-100). Clamped to 0..maxValue. int32_t wgtProgressBarGetValue(w) Get the current progress value. + void wgtProgressBarSetVertical(w, v) Make the bar vertical (true) or horizontal (false); the layout is redone. + bool wgtProgressBarGetVertical(w) True when the bar is vertical. .endtable .h3 API Struct (wgtRegisterApi "progressbar") @@ -65,7 +69,9 @@ Header: widgets/progress.h create wgtProgressBar createV wgtProgressBarV setValue wgtProgressBarSetValue - getValue wgtProgressBarGetValue + getValue wgtProgressBarGetValue + setVertical wgtProgressBarSetVertical + getVertical wgtProgressBarGetVertical .endtable .h3 Properties (BASIC Interface) @@ -74,6 +80,7 @@ Header: widgets/progress.h Property Type Access Description -------- ---- ------ ----------- Value Integer Read/Write Current progress value (0-100). + Vertical Boolean Read/Write True: the bar stands upright and fills from the bottom. .endtable .h3 Methods diff --git a/src/widgets/kpunch/progressBar/progress.h b/src/widgets/kpunch/progressBar/progress.h index ba293363..e3c23cea 100644 --- a/src/widgets/kpunch/progressBar/progress.h +++ b/src/widgets/kpunch/progressBar/progress.h @@ -32,6 +32,8 @@ typedef struct { WidgetT *(*createV)(WidgetT *parent); void (*setValue)(WidgetT *w, int32_t value); int32_t (*getValue)(const WidgetT *w); + void (*setVertical)(WidgetT *w, bool vertical); + bool (*getVertical)(const WidgetT *w); } ProgressBarApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -48,6 +50,8 @@ static inline const ProgressBarApiT *dvxProgressBarApi(void) { #define wgtProgressBarV(parent) dvxProgressBarApi()->createV(parent) #define wgtProgressBarSetValue(w, value) dvxProgressBarApi()->setValue(w, value) #define wgtProgressBarGetValue(w) dvxProgressBarApi()->getValue(w) +#define wgtProgressBarSetVertical(w, v) dvxProgressBarApi()->setVertical(w, v) +#define wgtProgressBarGetVertical(w) dvxProgressBarApi()->getVertical(w) #endif // PROGRESS_IMPL diff --git a/src/widgets/kpunch/progressBar/widgetProgressBar.c b/src/widgets/kpunch/progressBar/widgetProgressBar.c index 40dc6146..ebda18e1 100644 --- a/src/widgets/kpunch/progressBar/widgetProgressBar.c +++ b/src/widgets/kpunch/progressBar/widgetProgressBar.c @@ -69,7 +69,9 @@ typedef struct { static WidgetT *progressBarCreate(WidgetT *parent, bool vertical); WidgetT *wgtProgressBar(WidgetT *parent); int32_t wgtProgressBarGetValue(const WidgetT *w); +bool wgtProgressBarGetVertical(const WidgetT *w); void wgtProgressBarSetValue(WidgetT *w, int32_t value); +void wgtProgressBarSetVertical(WidgetT *w, bool vertical); WidgetT *wgtProgressBarV(WidgetT *parent); void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetProgressBarPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -109,6 +111,14 @@ int32_t wgtProgressBarGetValue(const WidgetT *w) { } +bool wgtProgressBarGetVertical(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + ProgressBarDataT *d = (ProgressBarDataT *)w->data; + + return d->vertical; +} + + void wgtProgressBarSetValue(WidgetT *w, int32_t value) { VALIDATE_WIDGET_VOID(w, sTypeId); ProgressBarDataT *d = (ProgressBarDataT *)w->data; @@ -118,6 +128,17 @@ void wgtProgressBarSetValue(WidgetT *w, int32_t value) { } +// Switching orientation swaps the minimum size's axes, so the layout +// is redone (wgtInvalidate), not just the paint. +void wgtProgressBarSetVertical(WidgetT *w, bool vertical) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ProgressBarDataT *d = (ProgressBarDataT *)w->data; + + d->vertical = vertical; + wgtInvalidate(w); +} + + WidgetT *wgtProgressBarV(WidgetT *parent) { return progressBarCreate(parent, true); } @@ -138,15 +159,18 @@ static const WidgetClassT sClassProgressBar = { static const ProgressBarApiT sApi = { - .header = WGT_API_HEADER(ProgressBarApiT), - .create = wgtProgressBar, - .createV = wgtProgressBarV, - .setValue = wgtProgressBarSetValue, - .getValue = wgtProgressBarGetValue + .header = WGT_API_HEADER(ProgressBarApiT), + .create = wgtProgressBar, + .createV = wgtProgressBarV, + .setValue = wgtProgressBarSetValue, + .getValue = wgtProgressBarGetValue, + .setVertical = wgtProgressBarSetVertical, + .getVertical = wgtProgressBarGetVertical }; static const WgtPropDescT sProps[] = { - { "Value", WGT_IFACE_INT, (void *)wgtProgressBarGetValue, (void *)wgtProgressBarSetValue, NULL } + { "Value", WGT_IFACE_INT, (void *)wgtProgressBarGetValue, (void *)wgtProgressBarSetValue, NULL }, + { "Vertical", WGT_IFACE_BOOL, (void *)wgtProgressBarGetVertical, (void *)wgtProgressBarSetVertical, NULL } }; static const WgtIfaceT sIface = { @@ -226,4 +250,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassProgressBar); wgtRegisterApi("progressbar", &sApi); wgtRegisterIface("progressbar", &sIface); + wgtIfaceSetType("progressbar", sTypeId); } diff --git a/src/widgets/kpunch/radio/widgetRadio.c b/src/widgets/kpunch/radio/widgetRadio.c index 60c0569e..f7e0d7cb 100644 --- a/src/widgets/kpunch/radio/widgetRadio.c +++ b/src/widgets/kpunch/radio/widgetRadio.c @@ -51,7 +51,7 @@ DVX_ABI_STAMP(radio); // selectedIdx of a RadioGroup with no radio selected -#define RADIO_NONE -1 +#define RADIO_NONE (-1) static int32_t sRadioGroupTypeId = -1; static int32_t sRadioTypeId = -1; @@ -533,4 +533,5 @@ void wgtRegister(void) { sRadioTypeId = wgtRegisterClass(&sClassRadio); wgtRegisterApi("radio", &sApi); wgtRegisterIface("radio", &sIface); + wgtIfaceSetType("radio", sRadioTypeId); } diff --git a/src/widgets/kpunch/scrollPane/scrlPane.h b/src/widgets/kpunch/scrollPane/scrlPane.h index 82cb76d5..948dd761 100644 --- a/src/widgets/kpunch/scrollPane/scrlPane.h +++ b/src/widgets/kpunch/scrollPane/scrlPane.h @@ -32,6 +32,10 @@ typedef struct { void (*scrollToChild)(WidgetT *sp, const WidgetT *child); void (*setNoBorder)(WidgetT *w, bool noBorder); void (*scrollToTop)(WidgetT *w); + int32_t (*getScrollLeft)(const WidgetT *w); + void (*setScrollLeft)(WidgetT *w, int32_t pos); + int32_t (*getScrollTop)(const WidgetT *w); + void (*setScrollTop)(WidgetT *w, int32_t pos); } ScrollPaneApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -48,6 +52,10 @@ static inline const ScrollPaneApiT *dvxScrollPaneApi(void) { #define wgtScrollPaneScrollToChild(sp, child) dvxScrollPaneApi()->scrollToChild((sp), (child)) #define wgtScrollPaneSetNoBorder(w, noBorder) dvxScrollPaneApi()->setNoBorder((w), (noBorder)) #define wgtScrollPaneScrollToTop(w) dvxScrollPaneApi()->scrollToTop(w) +#define wgtScrollPaneGetScrollLeft(w) dvxScrollPaneApi()->getScrollLeft(w) +#define wgtScrollPaneSetScrollLeft(w, pos) dvxScrollPaneApi()->setScrollLeft((w), (pos)) +#define wgtScrollPaneGetScrollTop(w) dvxScrollPaneApi()->getScrollTop(w) +#define wgtScrollPaneSetScrollTop(w, pos) dvxScrollPaneApi()->setScrollTop((w), (pos)) #endif // SCRLPANE_IMPL diff --git a/src/widgets/kpunch/scrollPane/scrlpane.bhs b/src/widgets/kpunch/scrollPane/scrlpane.bhs index 0eba5ccd..73e9613b 100644 --- a/src/widgets/kpunch/scrollPane/scrlpane.bhs +++ b/src/widgets/kpunch/scrollPane/scrlpane.bhs @@ -35,11 +35,47 @@ A scrollable container. Place child controls inside and the ScrollPane automatic .h2 Type-Specific Properties .table - Property Type Description - -------- ------- ------------------------------------------- - NoBorder Boolean When True, suppresses the border around the scroll pane. + Property Type Description + -------- ------- ------------------------------------------- + NoBorder Boolean When True, suppresses the border around the scroll pane. + Padding Integer Space in pixels between the edge and the controls inside (0, the default, is none). + Spacing Integer Space in pixels between the controls inside, which are stacked top to bottom (0, the default, is none). + ScrollTop Integer How far the content is scrolled down, in pixels (run time). Values past the end stop at the end. + ScrollLeft Integer How far the content is scrolled to the right, in pixels (run time). Values past the end stop at the end. .endtable +.h2 Type-Specific Methods + +.table + Method Description + ------ ----------- + ScrollToTop Scroll back to the top-left corner of the content. + ScrollToControl ctrl Scroll so that a control inside the pane (the control, or its name) is in view, also one that cannot take the focus. +.endtable + +.h2 Events + +.table + Event Description + ----- ----------- + Change The user scrolled the content (the scrollbars, dragging a scroll box, the arrow, page, Home and End keys); read ScrollTop and ScrollLeft for the new position. Setting ScrollTop or ScrollLeft, or ScrollToTop, does not fire it. +.endtable + +.h2 Scrolling a Picture Bigger Than the Window + +A ScrollPane with NoBorder = True filling a form, holding a PictureBox larger than the window, gives the form scrollbars of its own: the user scrolls the picture, Change says where it is, and ScrollTop / ScrollLeft move it from code. To draw only the part in view (for a picture too large to keep whole), keep the PictureBox the size of the view, and draw from Change with ScrollTop and ScrollLeft as the offset. + +.code +Sub Form1_Load () + picBig.Resize 1200, 900 + picBig.Line (0, 0)-(1199, 899), RGB(0, 0, 255), B +End Sub + +Sub spView_Change () + lblWhere.Caption = "Top " & spView.ScrollTop & ", left " & spView.ScrollLeft +End Sub +.endcode + Container: Yes No default event. diff --git a/src/widgets/kpunch/scrollPane/scrlpane.dhs b/src/widgets/kpunch/scrollPane/scrlpane.dhs index 35cbdec3..66eceba9 100644 --- a/src/widgets/kpunch/scrollPane/scrlpane.dhs +++ b/src/widgets/kpunch/scrollPane/scrlpane.dhs @@ -28,6 +28,11 @@ .index wgtScrollPane .index wgtScrollPaneScrollToChild .index wgtScrollPaneSetNoBorder +.index wgtScrollPaneScrollToTop +.index wgtScrollPaneGetScrollTop +.index wgtScrollPaneSetScrollTop +.index wgtScrollPaneGetScrollLeft +.index wgtScrollPaneSetScrollLeft .h2 ScrollPane @@ -50,10 +55,18 @@ WidgetT *content = wgtVBox(sp); -------- ----------- WidgetT *wgtScrollPane(parent) Create a scroll pane container. void wgtScrollPaneScrollToChild(sp, child) Scroll so that the given child widget is visible. - void wgtScrollPaneScrollToTop(w) Scroll back to the top-left of the content. + void wgtScrollPaneScrollToTop(w) Scroll back to the top-left of the content and re-lay out. void wgtScrollPaneSetNoBorder(w, noBorder) When true, removes the border around the scroll pane. + int32_t wgtScrollPaneGetScrollTop(w) Vertical scroll position in pixels. + void wgtScrollPaneSetScrollTop(w, pos) Set the vertical scroll position; the layout clamps it to the content. + int32_t wgtScrollPaneGetScrollLeft(w) Horizontal scroll position in pixels. + void wgtScrollPaneSetScrollLeft(w, pos) Set the horizontal scroll position; the layout clamps it to the content. .endtable +Padding and spacing are the common WidgetT padding / spacing fields (unset = none); wgtSetPaddingPx / wgtSetSpacingPx set them in pixels. + +When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event. + .h3 API Struct (wgtRegisterApi "scrollpane") .table @@ -64,6 +77,10 @@ WidgetT *content = wgtVBox(sp); scrollToChild wgtScrollPaneScrollToChild setNoBorder wgtScrollPaneSetNoBorder scrollToTop wgtScrollPaneScrollToTop + getScrollLeft wgtScrollPaneGetScrollLeft + setScrollLeft wgtScrollPaneSetScrollLeft + getScrollTop wgtScrollPaneGetScrollTop + setScrollTop wgtScrollPaneSetScrollTop .endtable .h3 Properties (BASIC Interface) @@ -72,11 +89,19 @@ WidgetT *content = wgtVBox(sp); Property Type Access Description -------- ---- ------ ----------- NoBorder Boolean Read/Write Whether the border around the scroll pane is hidden. + Padding Integer Read/Write Pixels between the edge and the children (wgtGetPaddingPx / wgtSetPaddingPx). + Spacing Integer Read/Write Pixels between the children (wgtGetSpacingPx / wgtSetSpacingPx). + ScrollTop Integer Read/Write Vertical scroll position in pixels. + ScrollLeft Integer Read/Write Horizontal scroll position in pixels. .endtable .h3 Methods -No widget-specific methods. +.table + Method Description + ------ ----------- + ScrollToTop wgtScrollPaneScrollToTop. +.endtable .h3 Events diff --git a/src/widgets/kpunch/scrollPane/widgetScrollPane.c b/src/widgets/kpunch/scrollPane/widgetScrollPane.c index 844e20b2..0ff636fd 100644 --- a/src/widgets/kpunch/scrollPane/widgetScrollPane.c +++ b/src/widgets/kpunch/scrollPane/widgetScrollPane.c @@ -46,6 +46,12 @@ // // Scrollbars are drawn via widgetDrawScrollbarH/V and hit tested // with widgetScrollbarHitTest, the shared framework helpers (WGT_SB_W). +// +// When the user moves the scroll position (scrollbar arrows and trough, +// thumb drag, keys) the pane fires onChange (BASIC: Change); setting it +// from code (ScrollTop, ScrollLeft, ScrollToTop) does not, as with the +// other widgets' value setters, nor does scrolling a focused child into +// view. #include "dvxWgtP.h" #include "scrlPane.h" @@ -89,13 +95,18 @@ static int32_t spBorder(const WidgetT *w); static void spClampScroll(ScrollPaneDataT *sp, const SpGeomT *g); static SpGeomT spGeom(const WidgetT *w, const BitmapFontT *font); static bool spGetNoBorder(const WidgetT *w); +static void spMoved(WidgetT *w, const SpGeomT *g, int32_t oldV, int32_t oldH); static void spPlaceChildren(WidgetT *w, const BitmapFontT *font, const SpGeomT *g); -static void spScrollbarClick(WidgetT *w, int32_t orient, int32_t rel, int32_t sbLen, int32_t total, int32_t visible, int32_t lineStep); +static void spScrollbarClick(WidgetT *w, const SpGeomT *g, int32_t orient, int32_t rel, int32_t sbLen, int32_t total, int32_t visible, int32_t lineStep); static void spSetNoBorder(WidgetT *w, bool v); WidgetT *wgtScrollPane(WidgetT *parent); +int32_t wgtScrollPaneGetScrollLeft(const WidgetT *w); +int32_t wgtScrollPaneGetScrollTop(const WidgetT *w); void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child); void wgtScrollPaneScrollToTop(WidgetT *w); void wgtScrollPaneSetNoBorder(WidgetT *w, bool noBorder); +void wgtScrollPaneSetScrollLeft(WidgetT *w, int32_t pos); +void wgtScrollPaneSetScrollTop(WidgetT *w, int32_t pos); void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font); static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY); @@ -183,6 +194,24 @@ static bool spGetNoBorder(const WidgetT *w) { } +// The user moved the scroll position (it was oldV / oldH): clamp it to +// the content and, when it really moved, relayout and fire onChange last +// (the handler may destroy the pane). +static void spMoved(WidgetT *w, const SpGeomT *g, int32_t oldV, int32_t oldH) { + ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; + + spClampScroll(sp, g); + + if (sp->scrollPosV == oldV && sp->scrollPosH == oldH) { + return; + } + + // The relayout repositions the children. + wgtInvalidate(w); + widgetFireChange(w); +} + + // Vertical box placement at the virtual size, offset by the scroll // position. Surplus space (virtual height beyond the children's min // heights) is shared among weighted children; it is recomputed from g @@ -250,11 +279,12 @@ static void spPlaceChildren(WidgetT *w, const BitmapFontT *font, const SpGeomT * // Click on a scrollbar (orient SP_ORIENT_V or SP_ORIENT_H): arrows step // by a line, the trough pages, the thumb starts a drag. rel is the // click position along the bar, sbLen the bar length. -static void spScrollbarClick(WidgetT *w, int32_t orient, int32_t rel, int32_t sbLen, int32_t total, int32_t visible, int32_t lineStep) { +static void spScrollbarClick(WidgetT *w, const SpGeomT *g, int32_t orient, int32_t rel, int32_t sbLen, int32_t total, int32_t visible, int32_t lineStep) { ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; int32_t *pos = (orient == SP_ORIENT_V) ? &sp->scrollPosV : &sp->scrollPosH; int32_t pageSize = DVX_MAX(visible - lineStep, lineStep); - int32_t oldPos = *pos; + int32_t oldV = sp->scrollPosV; + int32_t oldH = sp->scrollPosH; int32_t dragOff; if (widgetScrollbarApplyHit(sbLen, rel, total, visible, lineStep, pageSize, pos, &dragOff)) { @@ -264,12 +294,7 @@ static void spScrollbarClick(WidgetT *w, int32_t orient, int32_t rel, int32_t sb return; } - if (*pos == oldPos) { - return; - } - - // The relayout repositions the children. - wgtInvalidate(w); + spMoved(w, g, oldV, oldH); } @@ -302,19 +327,31 @@ static const ScrollPaneApiT sApi = { .create = wgtScrollPane, .scrollToChild = wgtScrollPaneScrollToChild, .setNoBorder = wgtScrollPaneSetNoBorder, - .scrollToTop = wgtScrollPaneScrollToTop + .scrollToTop = wgtScrollPaneScrollToTop, + .getScrollLeft = wgtScrollPaneGetScrollLeft, + .setScrollLeft = wgtScrollPaneSetScrollLeft, + .getScrollTop = wgtScrollPaneGetScrollTop, + .setScrollTop = wgtScrollPaneSetScrollTop }; static const WgtPropDescT sProps[] = { - { "NoBorder", WGT_IFACE_BOOL, (void *)spGetNoBorder, (void *)spSetNoBorder, NULL }, + { "NoBorder", WGT_IFACE_BOOL, (void *)spGetNoBorder, (void *)spSetNoBorder, NULL }, + { "Padding", WGT_IFACE_INT, (void *)wgtGetPaddingPx, (void *)wgtSetPaddingPx, NULL }, + { "ScrollLeft", WGT_IFACE_INT, (void *)wgtScrollPaneGetScrollLeft, (void *)wgtScrollPaneSetScrollLeft, NULL }, + { "ScrollTop", WGT_IFACE_INT, (void *)wgtScrollPaneGetScrollTop, (void *)wgtScrollPaneSetScrollTop, NULL }, + { "Spacing", WGT_IFACE_INT, (void *)wgtGetSpacingPx, (void *)wgtSetSpacingPx, NULL } +}; + +static const WgtMethodDescT sMethods[] = { + { "ScrollToTop", WGT_SIG_VOID, (void *)wgtScrollPaneScrollToTop } }; static const WgtIfaceT sIface = { .basName = "ScrollPane", .props = sProps, .propCount = DVX_ARRAY_LEN(sProps), - .methods = NULL, - .methodCount = 0, + .methods = sMethods, + .methodCount = DVX_ARRAY_LEN(sMethods), .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, @@ -343,6 +380,18 @@ WidgetT *wgtScrollPane(WidgetT *parent) { } +int32_t wgtScrollPaneGetScrollLeft(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + return ((const ScrollPaneDataT *)w->data)->scrollPosH; +} + + +int32_t wgtScrollPaneGetScrollTop(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, 0); + return ((const ScrollPaneDataT *)w->data)->scrollPosV; +} + + void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) { VALIDATE_WIDGET_VOID(w, sTypeId); ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; @@ -363,6 +412,8 @@ void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) { sp->scrollPosV = childOffY + child->h - g.innerH; } + // No onChange here: the focus code that calls this goes on using the + // pane, which a handler could destroy. spClampScroll(sp, &g); } @@ -372,6 +423,7 @@ void wgtScrollPaneScrollToTop(WidgetT *w) { ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; sp->scrollPosV = 0; sp->scrollPosH = 0; + wgtInvalidate(w); } @@ -383,6 +435,22 @@ void wgtScrollPaneSetNoBorder(WidgetT *w, bool noBorder) { } +// The scroll position in pixels; the relayout clamps it to the content +// (widgetScrollPaneLayout), so reading it back gives the position used. +void wgtScrollPaneSetScrollLeft(WidgetT *w, int32_t pos) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ((ScrollPaneDataT *)w->data)->scrollPosH = DVX_MAX(pos, 0); + wgtInvalidate(w); +} + + +void wgtScrollPaneSetScrollTop(WidgetT *w, int32_t pos) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ((ScrollPaneDataT *)w->data)->scrollPosV = DVX_MAX(pos, 0); + wgtInvalidate(w); +} + + // The scroll pane reports a deliberately small min size (just enough // for the scrollbar chrome) because its whole purpose is to contain // content that doesn't fit. However, children still need their min @@ -438,14 +506,18 @@ static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mous return; } - SpGeomT g = spGeom(w, &ctx->font); - int32_t org = spBorder(w) + WGT_SB_W + sp->sbDragOff; + SpGeomT g = spGeom(w, &ctx->font); + int32_t org = spBorder(w) + WGT_SB_W + sp->sbDragOff; + int32_t oldV = sp->scrollPosV; + int32_t oldH = sp->scrollPosH; if (sp->sbDragOrient == SP_ORIENT_V && g.maxScrollV > 0) { sp->scrollPosV = widgetScrollbarThumbDragScroll(g.innerH - WGT_SB_W * 2, g.contentMinH, g.innerH, mouseY - w->y - org, g.maxScrollV); } else if (sp->sbDragOrient == SP_ORIENT_H && g.maxScrollH > 0) { sp->scrollPosH = widgetScrollbarThumbDragScroll(g.innerW - WGT_SB_W * 2, g.contentMinW, g.innerW, mouseX - w->x - org, g.maxScrollH); } + + spMoved(w, &g, oldV, oldH); } @@ -466,6 +538,8 @@ void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) { const BitmapFontT *font = &ctx->font; SpGeomT g = spGeom(w, font); + int32_t oldV = sp->scrollPosV; + int32_t oldH = sp->scrollPosH; if (key == KEY_UP) { sp->scrollPosV -= font->charHeight; @@ -487,8 +561,7 @@ void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } - // The relayout clamps the new position and repositions the children. - wgtInvalidate(w); + spMoved(w, &g, oldV, oldH); } @@ -513,13 +586,13 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy // Vertical scrollbar if (g.needVSb && vx >= vsbX && vy >= innerY && vy < innerY + g.innerH) { - spScrollbarClick(hit, SP_ORIENT_V, vy - innerY, g.innerH, g.contentMinH, g.innerH, font->charHeight); + spScrollbarClick(hit, &g, SP_ORIENT_V, vy - innerY, g.innerH, g.contentMinH, g.innerH, font->charHeight); return; } // Horizontal scrollbar if (g.needHSb && vy >= hsbY && vx >= innerX && vx < innerX + g.innerW) { - spScrollbarClick(hit, SP_ORIENT_H, vx - innerX, g.innerW, g.contentMinW, g.innerW, font->charWidth); + spScrollbarClick(hit, &g, SP_ORIENT_H, vx - innerX, g.innerW, g.contentMinW, g.innerW, font->charWidth); return; } @@ -615,4 +688,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassScrollPane); wgtRegisterApi("scrollpane", &sApi); wgtRegisterIface("scrollpane", &sIface); + wgtIfaceSetType("scrollpane", sTypeId); } diff --git a/src/widgets/kpunch/separator/widgetSeparator.c b/src/widgets/kpunch/separator/widgetSeparator.c index 8794322b..b4a58d05 100644 --- a/src/widgets/kpunch/separator/widgetSeparator.c +++ b/src/widgets/kpunch/separator/widgetSeparator.c @@ -187,4 +187,5 @@ void wgtRegister(void) { wgtRegisterApi("separator", &sApi); wgtRegisterApi("line", &sLineApi); wgtRegisterIface("line", &sIface); + wgtIfaceSetType("line", sTypeId); } diff --git a/src/widgets/kpunch/slider/widgetSlider.c b/src/widgets/kpunch/slider/widgetSlider.c index 7d525c3c..9aef2e21 100644 --- a/src/widgets/kpunch/slider/widgetSlider.c +++ b/src/widgets/kpunch/slider/widgetSlider.c @@ -397,4 +397,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSlider); wgtRegisterApi("slider", &sApi); wgtRegisterIface("slider", &sIface); + wgtIfaceSetType("slider", sTypeId); } diff --git a/src/widgets/kpunch/spacer/widgetSpacer.c b/src/widgets/kpunch/spacer/widgetSpacer.c index 3d61651d..e9da54f5 100644 --- a/src/widgets/kpunch/spacer/widgetSpacer.c +++ b/src/widgets/kpunch/spacer/widgetSpacer.c @@ -105,4 +105,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSpacer); wgtRegisterApi("spacer", &sApi); wgtRegisterIface("spacer", &sIface); + wgtIfaceSetType("spacer", sTypeId); } diff --git a/src/widgets/kpunch/spinner/spinner.bhs b/src/widgets/kpunch/spinner/spinner.bhs index d6912441..ceb4acb2 100644 --- a/src/widgets/kpunch/spinner/spinner.bhs +++ b/src/widgets/kpunch/spinner/spinner.bhs @@ -44,6 +44,7 @@ A numeric input with up/down buttons. Supports integer mode (default) and real-n Value Integer Current value (in real mode, the value truncated to an integer). RealMode Boolean True to use floating-point mode; False for integer mode. Decimals Integer Number of decimal places shown in real mode. + RealValue Double The value in real mode, with its fraction (Value is it cut to a whole number). Setting it keeps it inside the range. .endtable .h2 Type-Specific Methods @@ -53,6 +54,8 @@ A numeric input with up/down buttons. Supports integer mode (default) and real-n -------- ------------------------------ ------------------------------------------- SetRange Min As Integer, Max As Integer Set the allowed value range (used in both integer and real mode; default 0 to 100). SetStep Step As Integer Set the increment per button click. + SetRealRange Min As Double, Max As Double The range in real mode, with fractions (SetRange takes whole numbers). + SetRealStep Step As Double The increment per click in real mode (0 or less gives 0.01). .endtable Default Event: Change diff --git a/src/widgets/kpunch/spinner/widgetSpinner.c b/src/widgets/kpunch/spinner/widgetSpinner.c index 964ddbc2..11c8384f 100644 --- a/src/widgets/kpunch/spinner/widgetSpinner.c +++ b/src/widgets/kpunch/spinner/widgetSpinner.c @@ -89,6 +89,7 @@ DVX_ABI_STAMP(spinner); #define SPINNER_MIN_CHARS 6 // text area width in characters for calcMinSize #define SPINNER_PAGE_MULT 10 // Page Up/Down move this many steps #define SPINNER_ARROW_ROWS 3 // rows in each arrow triangle (1, 3, 5 px wide) +#define SPINNER_RANGE_ARGS 2 // SetRealRange min, max #define SPINNER_BTN_BEVEL 1 #define SPINNER_FOCUS_INSET 1 #define SPINNER_ARROW_UP 1 @@ -130,6 +131,8 @@ static bool ifaceGetRealMode(const WidgetT *w); static int32_t ifaceGetValue(const WidgetT *w); static void ifaceSetDecimals(WidgetT *w, int32_t v); static void ifaceSetRealMode(WidgetT *w, bool v); +static bool ifaceSetRealRange(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); +static bool ifaceSetRealStep(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret); static void ifaceSetValue(WidgetT *w, int32_t v); static bool spinnerAllowMinus(const SpinnerDataT *d); static void spinnerApplyStep(SpinnerDataT *d, int32_t direction, int32_t multiplier); @@ -192,6 +195,33 @@ static void ifaceSetRealMode(WidgetT *w, bool v) { } +// SetRealRange min, max for BASIC (no fixed signature takes Doubles): +// wgtSpinnerSetRealRange. +static bool ifaceSetRealRange(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + (void)ret; + + if (argc != SPINNER_RANGE_ARGS) { + return false; + } + + wgtSpinnerSetRealRange(w, wgtValueToFloat(&args[0]), wgtValueToFloat(&args[1])); + return true; +} + + +// SetRealStep step for BASIC: wgtSpinnerSetRealStep. +static bool ifaceSetRealStep(WidgetT *w, const WgtValueT *args, int32_t argc, WgtValueT *ret) { + (void)ret; + + if (argc != 1) { + return false; + } + + wgtSpinnerSetRealStep(w, wgtValueToFloat(&args[0])); + return true; +} + + static void ifaceSetValue(WidgetT *w, int32_t v) { wgtSpinnerSetValue(w, v); } @@ -430,14 +460,17 @@ static const SpinnerApiT sApi = { }; static const WgtPropDescT sProps[] = { - { "Value", WGT_IFACE_INT, (void *)ifaceGetValue, (void *)ifaceSetValue, NULL }, - { "RealMode", WGT_IFACE_BOOL, (void *)ifaceGetRealMode, (void *)ifaceSetRealMode, NULL }, - { "Decimals", WGT_IFACE_INT, (void *)ifaceGetDecimals, (void *)ifaceSetDecimals, NULL }, + { "Value", WGT_IFACE_INT, (void *)ifaceGetValue, (void *)ifaceSetValue, NULL }, + { "RealMode", WGT_IFACE_BOOL, (void *)ifaceGetRealMode, (void *)ifaceSetRealMode, NULL }, + { "Decimals", WGT_IFACE_INT, (void *)ifaceGetDecimals, (void *)ifaceSetDecimals, NULL }, + { "RealValue", WGT_IFACE_DOUBLE, (void *)wgtSpinnerGetRealValue, (void *)wgtSpinnerSetRealValue, NULL }, }; static const WgtMethodDescT sMethods[] = { - { "SetRange", WGT_SIG_INT_INT, (void *)wgtSpinnerSetRange }, - { "SetStep", WGT_SIG_INT, (void *)wgtSpinnerSetStep } + { "SetRange", WGT_SIG_INT_INT, (void *)wgtSpinnerSetRange }, + { "SetRealRange", WGT_SIG_DYNAMIC, (void *)ifaceSetRealRange }, + { "SetRealStep", WGT_SIG_DYNAMIC, (void *)ifaceSetRealStep }, + { "SetStep", WGT_SIG_INT, (void *)wgtSpinnerSetStep } }; static const WgtIfaceT sIface = { @@ -843,4 +876,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSpinner); wgtRegisterApi("spinner", &sApi); wgtRegisterIface("spinner", &sIface); + wgtIfaceSetType("spinner", sTypeId); } diff --git a/src/widgets/kpunch/splitter/splitter.bhs b/src/widgets/kpunch/splitter/splitter.bhs index 7088e244..35c91dd3 100644 --- a/src/widgets/kpunch/splitter/splitter.bhs +++ b/src/widgets/kpunch/splitter/splitter.bhs @@ -30,14 +30,15 @@ DVX Extension -- DVX Widget: splitter -A resizable split pane. Holds exactly two child widgets separated by a draggable divider. Default orientation is vertical (vertical divider, panes side-by-side). +A resizable split pane. Holds exactly two child widgets separated by a draggable divider. By default the divider is vertical and the panes sit side by side; set Vertical to False for a horizontal divider with one pane above the other. .h2 Type-Specific Properties .table Property Type Description -------- ------- ------------------------------------------- - Position Integer Position of the divider in pixels from the top (or left). + Position Integer Position of the divider in pixels from the left (or, when Vertical is False, from the top). + Vertical Boolean True (the default): panes side by side. False: one pane above the other. Can be set in the designer or at run time. .endtable Container: Yes diff --git a/src/widgets/kpunch/splitter/splitter.dhs b/src/widgets/kpunch/splitter/splitter.dhs index 7e833f98..978589a7 100644 --- a/src/widgets/kpunch/splitter/splitter.dhs +++ b/src/widgets/kpunch/splitter/splitter.dhs @@ -28,6 +28,8 @@ .index wgtSplitter .index wgtSplitterSetPos .index wgtSplitterGetPos +.index wgtSplitterSetVertical +.index wgtSplitterGetVertical .h2 Splitter @@ -51,6 +53,8 @@ WidgetT *right = wgtVBox(sp); WidgetT *wgtSplitter(parent, vertical) Create a splitter. vertical=true for left/right panes (vertical divider), false for top/bottom. void wgtSplitterSetPos(w, pos) Set the divider position in pixels from the leading edge. int32_t wgtSplitterGetPos(w) Get the current divider position in pixels. + void wgtSplitterSetVertical(w, vertical) Change the orientation (true = left/right panes); the position is kept and clamped to the new axis at the next layout. + bool wgtSplitterGetVertical(w) True when the panes are side by side. .endtable .h3 API Struct (wgtRegisterApi "splitter") @@ -61,7 +65,9 @@ WidgetT *right = wgtVBox(sp); header WGT_API_HEADER (ABI version and size, checked by wgtGetApi) create wgtSplitter setPos wgtSplitterSetPos - getPos wgtSplitterGetPos + getPos wgtSplitterGetPos + setVertical wgtSplitterSetVertical + getVertical wgtSplitterGetVertical .endtable .h3 Properties (BASIC Interface) @@ -70,6 +76,7 @@ WidgetT *right = wgtVBox(sp); Property Type Access Description -------- ---- ------ ----------- Position Integer Read/Write Divider position in pixels. + Vertical Boolean Read/Write True (default): panes side by side. False: panes above and below. .endtable .h3 Methods diff --git a/src/widgets/kpunch/splitter/splitter.h b/src/widgets/kpunch/splitter/splitter.h index 1b7f114b..69286cbf 100644 --- a/src/widgets/kpunch/splitter/splitter.h +++ b/src/widgets/kpunch/splitter/splitter.h @@ -31,6 +31,8 @@ typedef struct { WidgetT *(*create)(WidgetT *parent, bool vertical); void (*setPos)(WidgetT *w, int32_t pos); int32_t (*getPos)(const WidgetT *w); + void (*setVertical)(WidgetT *w, bool vertical); + bool (*getVertical)(const WidgetT *w); } SplitterApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -46,6 +48,8 @@ static inline const SplitterApiT *dvxSplitterApi(void) { #define wgtSplitter(parent, vertical) dvxSplitterApi()->create(parent, vertical) #define wgtSplitterSetPos(w, pos) dvxSplitterApi()->setPos(w, pos) #define wgtSplitterGetPos(w) dvxSplitterApi()->getPos(w) +#define wgtSplitterSetVertical(w, v) dvxSplitterApi()->setVertical(w, v) +#define wgtSplitterGetVertical(w) dvxSplitterApi()->getVertical(w) #endif // SPLITTER_IMPL diff --git a/src/widgets/kpunch/splitter/widgetSplitter.c b/src/widgets/kpunch/splitter/widgetSplitter.c index 09ce9133..a8c9c0c3 100644 --- a/src/widgets/kpunch/splitter/widgetSplitter.c +++ b/src/widgets/kpunch/splitter/widgetSplitter.c @@ -89,7 +89,9 @@ static void splitterPaneRect(const WidgetT *w, int32_t pos, bool second, in static WidgetT *spSecondChild(WidgetT *w); WidgetT *wgtSplitter(WidgetT *parent, bool vertical); int32_t wgtSplitterGetPos(const WidgetT *w); +bool wgtSplitterGetVertical(const WidgetT *w); void wgtSplitterSetPos(WidgetT *w, int32_t pos); +void wgtSplitterSetVertical(WidgetT *w, bool vertical); void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSplitterClampPos(WidgetT *w, int32_t *pos); int32_t widgetSplitterGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy); @@ -196,14 +198,17 @@ static const WidgetClassT sClassSplitter = { static const SplitterApiT sApi = { - .header = WGT_API_HEADER(SplitterApiT), - .create = wgtSplitter, - .setPos = wgtSplitterSetPos, - .getPos = wgtSplitterGetPos + .header = WGT_API_HEADER(SplitterApiT), + .create = wgtSplitter, + .setPos = wgtSplitterSetPos, + .getPos = wgtSplitterGetPos, + .setVertical = wgtSplitterSetVertical, + .getVertical = wgtSplitterGetVertical }; static const WgtPropDescT sProps[] = { - { "Position", WGT_IFACE_INT, (void *)wgtSplitterGetPos, (void *)wgtSplitterSetPos, NULL } + { "Position", WGT_IFACE_INT, (void *)wgtSplitterGetPos, (void *)wgtSplitterSetPos, NULL }, + { "Vertical", WGT_IFACE_BOOL, (void *)wgtSplitterGetVertical, (void *)wgtSplitterSetVertical, NULL } }; static const WgtIfaceT sIface = { @@ -249,6 +254,13 @@ int32_t wgtSplitterGetPos(const WidgetT *w) { } +bool wgtSplitterGetVertical(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + const SplitterDataT *d = (const SplitterDataT *)w->data; + return d->vertical; +} + + void wgtSplitterSetPos(WidgetT *w, int32_t pos) { VALIDATE_WIDGET_VOID(w, sTypeId); SplitterDataT *d = (SplitterDataT *)w->data; @@ -257,6 +269,16 @@ void wgtSplitterSetPos(WidgetT *w, int32_t pos) { } +// The divider position keeps its value; the next layout clamps it to the +// new axis (widgetSplitterLayout). +void wgtSplitterSetVertical(WidgetT *w, bool vertical) { + VALIDATE_WIDGET_VOID(w, sTypeId); + SplitterDataT *d = (SplitterDataT *)w->data; + d->vertical = vertical; + wgtInvalidate(w); +} + + void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) { // Recursively measure children for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { @@ -461,4 +483,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSplitter); wgtRegisterApi("splitter", &sApi); wgtRegisterIface("splitter", &sIface); + wgtIfaceSetType("splitter", sTypeId); } diff --git a/src/widgets/kpunch/statusBar/statbar.bhs b/src/widgets/kpunch/statusBar/statbar.bhs index ae665a58..da284c74 100644 --- a/src/widgets/kpunch/statusBar/statbar.bhs +++ b/src/widgets/kpunch/statusBar/statbar.bhs @@ -29,8 +29,24 @@ VB Equivalent: StatusBar -- DVX Widget: statusbar -A horizontal container styled as a status bar, typically placed at the bottom of a form. At the C API level it holds child widgets (such as labels), each drawn with a sunken-panel border for the classic segmented status-bar look. It is not registered as a container in the form runtime, so child controls cannot be nested inside it in .frm files. +A horizontal container styled as a status bar, typically placed at the bottom of a form. Put Labels (or other controls) inside it as its panes: they sit side by side, each drawn with a sunken-panel border for the classic segmented status-bar look. Give the pane that should take the spare room Weight = 100. Panes can be nested in the designer or the .frm, or added at run time with CreateControl(form, "Label", name$, StatusBar1). No type-specific properties, events, or methods. +Container: Yes + +.h2 Example + +.code +Begin StatusBar Status1 + Begin Label lblStatus + Caption = "Ready" + Weight = 100 + End + Begin Label lblPos + Caption = "Line 1, Col 1" + End +End +.endcode + .link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/statusBar/widgetStatusBar.c b/src/widgets/kpunch/statusBar/widgetStatusBar.c index 8d2a40b9..023ce725 100644 --- a/src/widgets/kpunch/statusBar/widgetStatusBar.c +++ b/src/widgets/kpunch/statusBar/widgetStatusBar.c @@ -142,4 +142,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassStatusBar); wgtRegisterApi("statusbar", &sApi); wgtRegisterIface("statusbar", &sIface); + wgtIfaceSetType("statusbar", sTypeId); } diff --git a/src/widgets/kpunch/tabControl/widgetTabControl.c b/src/widgets/kpunch/tabControl/widgetTabControl.c index e27c09d2..a40603a4 100644 --- a/src/widgets/kpunch/tabControl/widgetTabControl.c +++ b/src/widgets/kpunch/tabControl/widgetTabControl.c @@ -743,6 +743,8 @@ void wgtRegister(void) { sTabPageTypeId = wgtRegisterClass(&sClassTabPage); wgtRegisterApi("tabcontrol", &sApi); wgtRegisterIface("tabcontrol", &sIface); + wgtIfaceSetType("tabcontrol", sTabControlTypeId); wgtRegisterApi("tabpage", &sTabPageApi); wgtRegisterIface("tabpage", &sTabPageIface); + wgtIfaceSetType("tabpage", sTabPageTypeId); } diff --git a/src/widgets/kpunch/textInput/textInpt.h b/src/widgets/kpunch/textInput/textInpt.h index fcc93d88..4b3d4992 100644 --- a/src/widgets/kpunch/textInput/textInpt.h +++ b/src/widgets/kpunch/textInput/textInpt.h @@ -35,29 +35,46 @@ // ctx: user context pointer. typedef void (*TextColorFnT)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); +// Line markers (setLineMarker): a colour part that is not drawn. A +// marker with neither part is removed. +#define TEXTAREA_MARKER_NONE 0xFFFFFFFFu + typedef struct { WgtApiHeaderT header; - WidgetT *(*create)(WidgetT *parent, int32_t maxLen); - WidgetT *(*password)(WidgetT *parent, int32_t maxLen); - WidgetT *(*masked)(WidgetT *parent, const char *mask); - WidgetT *(*textArea)(WidgetT *parent, int32_t maxLen); - void (*setColorize)(WidgetT *w, TextColorFnT fn, void *ctx); - void (*goToLine)(WidgetT *w, int32_t line); - void (*setAutoIndent)(WidgetT *w, bool enable); - void (*setShowLineNumbers)(WidgetT *w, bool show); - void (*setCaptureTabs)(WidgetT *w, bool capture); - void (*setTabWidth)(WidgetT *w, int32_t width); - void (*setUseTabChar)(WidgetT *w, bool useChar); - bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); - int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); - bool (*replaceSelection)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward); - void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); - int32_t (*getCursorLine)(const WidgetT *w); - void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); - int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); - void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count); - void (*cursorToEnd)(WidgetT *w); - void (*setCursor)(WidgetT *w, int32_t line, int32_t col); + WidgetT *(*create)(WidgetT *parent, int32_t maxLen); + WidgetT *(*password)(WidgetT *parent, int32_t maxLen); + WidgetT *(*masked)(WidgetT *parent, const char *mask); + WidgetT *(*textArea)(WidgetT *parent, int32_t maxLen); + void (*setColorize)(WidgetT *w, TextColorFnT fn, void *ctx); + void (*goToLine)(WidgetT *w, int32_t line); + void (*setAutoIndent)(WidgetT *w, bool enable); + void (*setShowLineNumbers)(WidgetT *w, bool show); + void (*setCaptureTabs)(WidgetT *w, bool capture); + void (*setTabWidth)(WidgetT *w, int32_t width); + void (*setUseTabChar)(WidgetT *w, bool useChar); + bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); + int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); + bool (*replaceSelection)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward); + void (*setLineMarker)(WidgetT *w, int32_t line, uint32_t gutterRgb, uint32_t backRgb); + int32_t (*getCursorLine)(const WidgetT *w); + void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); + int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); + void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count); + void (*cursorToEnd)(WidgetT *w); + void (*setCursor)(WidgetT *w, int32_t line, int32_t col); + void (*clearLineMarkers)(WidgetT *w); + void (*setSyntaxMode)(WidgetT *w, const char *mode); + int32_t (*inputGetSelStart)(const WidgetT *w); + int32_t (*inputGetSelLength)(const WidgetT *w); + const char *(*inputGetSelText)(const WidgetT *w); + void (*inputSetSelStart)(WidgetT *w, int32_t start); + void (*inputSetSelLength)(WidgetT *w, int32_t length); + void (*inputSetSelText)(WidgetT *w, const char *text); + void (*inputCut)(WidgetT *w); + void (*inputCopy)(WidgetT *w); + void (*inputPaste)(WidgetT *w); + void (*inputSelectAll)(WidgetT *w); + void (*inputUndo)(WidgetT *w); } TextInputApiT; // Consumer side: the accessor and convenience macros. The widget's own @@ -84,7 +101,9 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaFindNext(w, needle, caseSens, fwd) dvxTextInputApi()->findNext(w, needle, caseSens, fwd) #define wgtTextAreaReplaceAll(w, needle, repl, caseSens) dvxTextInputApi()->replaceAll(w, needle, repl, caseSens) #define wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) dvxTextInputApi()->replaceSelection(w, needle, repl, caseSens, fwd) -#define wgtTextAreaSetLineDecorator(w, fn, ctx) dvxTextInputApi()->setLineDecorator(w, fn, ctx) +#define wgtTextAreaSetLineMarker(w, line, gutter, back) dvxTextInputApi()->setLineMarker(w, line, gutter, back) +#define wgtTextAreaClearLineMarkers(w) dvxTextInputApi()->clearLineMarkers(w) +#define wgtTextAreaSetSyntaxMode(w, mode) dvxTextInputApi()->setSyntaxMode(w, mode) #define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w) #define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn) #define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz) @@ -92,6 +111,23 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaCursorToEnd(w) dvxTextInputApi()->cursorToEnd(w) #define wgtTextAreaSetCursor(w, line, col) dvxTextInputApi()->setCursor(w, line, col) +// A single-line text box's selection (offsets from 0; SelStart is the +// cursor when nothing is selected), and its clipboard and undo commands +// (the same as its Ctrl+X / Ctrl+C / Ctrl+V / Ctrl+A / Ctrl+Z keys, +// password and mask rules included). SetSelText replaces the selection +// and fires onChange; GetSelText's text is valid until the next call. +#define wgtTextInputGetSelStart(w) dvxTextInputApi()->inputGetSelStart(w) +#define wgtTextInputGetSelLength(w) dvxTextInputApi()->inputGetSelLength(w) +#define wgtTextInputGetSelText(w) dvxTextInputApi()->inputGetSelText(w) +#define wgtTextInputSetSelStart(w, start) dvxTextInputApi()->inputSetSelStart(w, start) +#define wgtTextInputSetSelLength(w, length) dvxTextInputApi()->inputSetSelLength(w, length) +#define wgtTextInputSetSelText(w, text) dvxTextInputApi()->inputSetSelText(w, text) +#define wgtTextInputCut(w) dvxTextInputApi()->inputCut(w) +#define wgtTextInputCopy(w) dvxTextInputApi()->inputCopy(w) +#define wgtTextInputPaste(w) dvxTextInputApi()->inputPaste(w) +#define wgtTextInputSelectAll(w) dvxTextInputApi()->inputSelectAll(w) +#define wgtTextInputUndo(w) dvxTextInputApi()->inputUndo(w) + #endif // TEXTINPT_IMPL #endif // TEXTINPT_H diff --git a/src/widgets/kpunch/textInput/textinpt.bhs b/src/widgets/kpunch/textInput/textinpt.bhs index 66085f83..882f5d60 100644 --- a/src/widgets/kpunch/textInput/textinpt.bhs +++ b/src/widgets/kpunch/textInput/textinpt.bhs @@ -27,23 +27,48 @@ .index Text .index DataSource .index DataField +.index Mask +.index Masked input +.index SelText +.index SelectAll +.index Colorize .h1 TextBox -VB Equivalent: TextBox -- DVX Widget: textbox (single-line text input, max 256 chars) +VB Equivalent: TextBox -- DVX Widget: textbox (single-line text input, 256 chars unless MaxLength says otherwise) A single-line text input field. Supports data binding via DataSource and DataField properties. .h2 Type-Specific Properties .table - Property Type Description - ---------- ------ ------------------------------------------- - Text String The text content of the input field. Setting it clears the undo history. - DataSource String Name of a Data control for data binding. - DataField String Column name for data binding. + Property Type Description + ------------ ------ ------------------------------------------- + Text String The text content of the input field. Setting it clears the undo history. + MaxLength Integer The most characters the box holds (0 = the default, 256; at most 32767). A longer text is cut, and typing stops at the limit. + PasswordChar String "" for a plain box; a character makes it a password box that shows that character for each one typed and does not copy or cut its text. + Mask String "" for a plain box; an input mask makes it a masked box: # takes a digit, A a letter, * any character, and every other character of the mask is fixed text the cursor skips (Mask = "(###) ###-####" for a phone number). The text keeps the mask's shape (empty places show _), and what the box held fills its places in order. Mask = "" makes it a plain box again holding the formatted text, with its MaxLength from before. A masked box is not a password box. + DataSource String Name of a Data control for data binding. + DataField String Column name for data binding. + SelStart Integer Where the selection starts, or the cursor when nothing is selected (0 = before the first character). Setting it moves the cursor there and selects nothing. + SelLength Integer The number of characters selected (0 = none). Setting it selects that many characters from SelStart. + SelText String The selected text. Setting it replaces the selection (or inserts at the cursor), leaves the cursor after the new text and fires Change; in a masked box it fills the places from the selection's start, as pasting does. .endtable +.h2 Type-Specific Methods + +.table + Method Description + ------ ----------- + Copy Copy the selection to the clipboard (Ctrl+C); a password box copies nothing. + Cut Copy the selection to the clipboard and remove it (Ctrl+X); fires Change. A password box does nothing. + Paste Put the clipboard's text in place of the selection (Ctrl+V; line breaks are left out); fires Change. + SelectAll Select the whole text (Ctrl+A). + Undo Take back the last change (Ctrl+Z); a second Undo puts it back. +.endtable + +The methods do what the keys do, so Cut, Paste and Undo do nothing in a read-only box. An Edit menu works on whichever box has the focus with Me.ActiveControl (see Form). + Default Event: Change .h2 Example @@ -71,6 +96,17 @@ End Sub .index CursorLine .index LineNumbers .index AutoIndent +.index SelStart +.index SelLength +.index SelText +.index LineCount +.index GetLine +.index CursorCol +.index SetLineMarker +.index GutterClick +.index SetSyntaxColor +.index ReplaceSelection +.index Code editor .h1 TextArea @@ -85,6 +121,12 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv ---------- ------- --- ------------------------------------------- Text String R/W The full text content. Setting it clears the undo history. CursorLine Integer R Current cursor line number (1-based). + CursorCol Integer R Current cursor column (1 = the start of the line; a tab counts as one). + LineCount Integer R The number of lines (an empty text has one). + SelStart Integer R/W Where the selection starts, or the cursor when nothing is selected (0 = before the first character). Setting it moves the cursor there and selects nothing. + SelLength Integer R/W The number of characters selected (0 = none). Setting it selects that many characters from SelStart. + SelText String R/W The selected text. Setting it replaces the selection (or inserts at the cursor when nothing is selected), leaves the cursor after the new text and fires Change; Undo takes it back. + MaxLength Integer R/W The most characters the area holds (0 = the default, 65536; at most 262144). A smaller value cuts the text. .endtable .h2 Type-Specific Methods @@ -93,19 +135,53 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv Method Description ------ ----------- AppendText text$ Append text to the end of the buffer and invalidate the widget. + ClearLineMarkers Remove every line marker. + Copy Copy the selection to the clipboard (Ctrl+C). + CursorToEnd Put the cursor after the last character, nothing selected. + Cut Copy the selection to the clipboard and remove it (Ctrl+X); fires Change. FindNext needle$, caseSensitive, forward Search from the cursor and select the match. A forward search finds a match starting at the cursor; repeating it moves on to the next match. Returns True if found. + GetLine(line%) Returns the text of a line (1 = the first) without its line end; "" past the last line. + GetSyntaxColor(index%) Returns the colour the syntax colouring draws that kind of text in (index as for SetSyntaxColor); -1 for the area's ForeColor. GetWordAtCursor() Returns the word under the cursor. GoToLine line% Scroll to the given line and select it. + Paste Put the clipboard's text in place of the selection (Ctrl+V); fires Change. ReplaceAll needle$, replacement$, caseSensitive Replace all occurrences. Returns count of replacements. + ReplaceSelection(needle$, replacement$, caseSensitive, forward) When the selection is needle$, replace it (Change fires); then find the next needle$ in that direction, as FindNext does. Returns True when it replaced or found something (a Replace button). + SelectAll Select the whole text (Ctrl+A). SetAutoIndent enabled Enable or disable automatic indentation. SetCaptureTabs enabled When True, Tab key inserts a tab/spaces (firing Change) instead of moving focus. SetCursor line%, col% Move the cursor to a line and column (both start at 1) without selecting anything. Values past the end clamp to the text. + SetLineMarker line%, gutterColor&, backColor& Mark a line (1 = the first): a dot of gutterColor in the line-number gutter (a breakpoint) and / or the line's background in backColor (the current line). -1 leaves that part out; -1 for both removes the marker. Markers stay on their line numbers when the text changes. SetShowLineNumbers show Show or hide the line number gutter. - SetSyntaxMode mode$ Activate built-in syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC). Pass "" to disable. + SetSyntaxColor index%, color& The colour of one kind of text: 0 plain text, 1 keywords, 2 strings, 3 comments, 4 numbers, 5 operators, 6 types (and True, False, Nothing); -1 restores the default. + SetSyntaxMode mode$ Activate syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC, coloured as the DVX BASIC IDE colours it), "custom" (the program colours each line in the Colorize event). Pass "" to disable. SetTabWidth width% Set the tab stop width in characters. SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces. + Undo Take back the last change (Ctrl+Z); a second Undo puts it back. .endtable +Cut, Paste and Undo do nothing in a read-only area. + +.h2 Type-Specific Events + +.table + Event Parameters Description + ----------- ----------------------- ------------------------------------------- + GutterClick LineNumber As Integer The line-number gutter (shown by SetShowLineNumbers) was clicked beside a line: toggle a breakpoint there. A click in the gutter does not move the cursor. + Colorize LineText As String, Colors As String With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them). Characters it leaves out stay plain. It runs whenever lines are drawn, so it must be quick and must not change the area. +.endtable + +.code +TextArea1.SetSyntaxMode "custom" + +Sub TextArea1_Colorize (LineText As String, Colors As String) + Dim i As Integer + For i = 1 To Len(LineText) + If Mid$(LineText, i, 1) = "#" Then Mid$(Colors, i, 1) = CStr(vbSyntaxComment) + Next +End Sub +.endcode + Default Event: Change .h2 Example @@ -121,10 +197,17 @@ If TextArea1.FindNext("TODO", False, True) Then Print "Found at line"; TextArea1.CursorLine End If -' Replace all occurrences +'' Replace all occurrences Dim count As Integer count = TextArea1.ReplaceAll("old", "new", False) Print "Replaced"; count; "occurrences" + +' A code editor: BASIC colours, a breakpoint dot on the clicked line +TextArea1.SetSyntaxMode "bas" + +Sub TextArea1_GutterClick (LineNumber As Integer) + TextArea1.SetLineMarker LineNumber, RGB(200, 0, 0), -1 +End Sub .endcode .link ctrl.common.props Common Properties, Events, and Methods diff --git a/src/widgets/kpunch/textInput/textinpt.dhs b/src/widgets/kpunch/textInput/textinpt.dhs index b649095b..b670d085 100644 --- a/src/widgets/kpunch/textInput/textinpt.dhs +++ b/src/widgets/kpunch/textInput/textinpt.dhs @@ -34,7 +34,7 @@ .h2 TextInput / TextArea -Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization, line numbers, find/replace, and gutter decorators. +Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization (built-in BASIC and help-source colorizers, the BASIC one shared with the DVX BASIC IDE), line numbers, find/replace, line markers and gutter clicks. Header: widgets/textInpt.h @@ -60,6 +60,14 @@ Header: widgets/textInpt.h .index wgtTextAreaReplaceSelection .index wgtTextAreaCursorToEnd .index wgtTextAreaSetCursor +.index wgtTextAreaSetLineMarker +.index wgtTextAreaClearLineMarkers +.index wgtTextAreaSetSyntaxMode +.index TEXTAREA_MARKER_NONE +.index wgtTextInputGetSelStart +.index wgtTextInputSetSelText +.index wgtTextInputCut +.index wgtTextInputPaste .table Function Description @@ -75,27 +83,57 @@ Header: widgets/textInpt.h bool wgtTextAreaFindNext(w, needle, caseSens, fwd) Search from the cursor and select the match (a forward match at the cursor counts; repeating moves past it). Returns true if found. int32_t wgtTextAreaReplaceAll(w, needle, repl, caseSens) Replace all occurrences. Returns the number of replacements made. bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found. - void wgtTextAreaSetLineDecorator(w, fn, ctx) Set a gutter line decorator callback. Returns a color and receives the line number. + void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb) Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way. + void wgtTextAreaClearLineMarkers(w) Remove every line marker. + void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source) or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off. int32_t wgtTextAreaGetCursorLine(w) Get the current cursor line number. void wgtTextAreaSetGutterClickCallback(w, fn) Set a callback for gutter clicks (e.g. for breakpoint toggling). int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize) Copy the word under the cursor into buf. Returns its length. - void wgtTextAreaSetSyntaxColors(w, colors, count) Set the color palette used by the colorizer. + void wgtTextAreaSetSyntaxColors(w, colors, count) Set the color palette used by the colorizer (0xRRGGBB per TEXT_SYNTAX_ index; 0 = the default). void wgtTextAreaCursorToEnd(w) Move the cursor to the very end of the buffer (past the last character). void wgtTextAreaSetCursor(w, line, col) Move the cursor to 1-based line/column without selecting; scrolls it into view. Out-of-range values clamp to the text. + int32_t wgtTextInputGetSelStart(w) A single-line box's selection start (0-based), or its cursor when nothing is selected. + int32_t wgtTextInputGetSelLength(w) Characters selected (0 = none). + const char *wgtTextInputGetSelText(w) The selected text, valid until the next call. + void wgtTextInputSetSelStart(w, start) Cursor to offset start, nothing selected. + void wgtTextInputSetSelLength(w, length) Select length characters from SelStart (clamped to the text). + void wgtTextInputSetSelText(w, text) Replace the selection (or insert at the cursor) with text (texthelp textEditSingleReplaceSel; a masked box fills its slots); fires onChange. + void wgtTextInputCut(w), wgtTextInputCopy(w), wgtTextInputPaste(w), wgtTextInputSelectAll(w), wgtTextInputUndo(w) The box's Ctrl+X / C / V / A / Z keys, run through its own key handler (password, mask and read-only rules included). .endtable .h3 API Struct (wgtRegisterApi "textinput") -The combined "textinput" API exposes all four constructors plus the TextArea functions. The designer uses per-type APIs registered under the names "textbox" (single-line) and "textarea" (multi-line), each exposing only the header and a create slot. Every API struct starts with a header (WGT_API_HEADER: ABI version and size, checked by wgtGetApi). +The combined "textinput" API exposes all four constructors plus the TextArea functions and, appended at its end (inputGetSelStart ... inputUndo), the TextBox selection and clipboard functions. The designer uses per-type APIs registered under the names "textbox" (single-line) and "textarea" (multi-line), each exposing only the header and a create slot. Every API struct starts with a header (WGT_API_HEADER: ABI version and size, checked by wgtGetApi). .h3 Properties (BASIC Interface) -TextBox has no widget-specific properties. Text is managed via the generic "Text" property. TextArea exposes: +Text is managed via the generic "Text" property. TextBox exposes: + +.table + Property Type Access Description + -------- ---- ------ ----------- + MaxLength Integer Read/Write Characters the box holds (its buffer size less one). Setting it gives a new buffer of that size (0 = TEXT_EDIT_DEFAULT_MAX_LEN, at most TEXT_EDIT_SINGLE_MAX_LEN), the text cut to fit; a masked box keeps its mask's length. + PasswordChar String Read/Write "" for a plain box; one character makes it a password box (as wgtPasswordInput, which shows a small bullet) showing that character for each one typed. Ignored on a masked box. + Mask String Read/Write "" for a plain box; a mask makes the live box a masked input (as wgtMaskedInput) holding what fits of its text; "" makes it plain again with the formatted text and its earlier MaxLength. Replaces PasswordChar. + SelStart Integer Read/Write wgtTextInputGetSelStart / wgtTextInputSetSelStart. + SelLength Integer Read/Write wgtTextInputGetSelLength / wgtTextInputSetSelLength. + SelText String Read/Write wgtTextInputGetSelText / wgtTextInputSetSelText. +.endtable + +TextBox methods: Copy, Cut, Paste, SelectAll and Undo (wgtTextInputCopy ...). + +TextArea exposes: .table Property Type Access Description -------- ---- ------ ----------- CursorLine Integer Read-only Current cursor line number (1-based). + CursorCol Integer Read-only Current cursor column (1-based, in characters). + LineCount Integer Read-only Number of lines. + SelStart Integer Read/Write Selection start offset (0-based), or the cursor's when none; setting it moves the cursor there. + SelLength Integer Read/Write Characters selected; setting it selects from SelStart. + SelText String Read/Write The selection; setting it replaces it (textEditMultiReplaceSel), firing Change. + MaxLength Integer Read/Write Buffer capacity; setting it makes a new buffer (0 = TEXTAREA_DEFAULT_BUF, at most TEXT_EDIT_MULTI_MAX_LEN) with the text cut to fit. .endtable .h3 Methods (BASIC Interface, TextArea) @@ -104,14 +142,22 @@ TextBox has no widget-specific properties. Text is managed via the generic "Text Method Description ------ ----------- AppendText text$ Append text to the end of the buffer. + ClearLineMarkers wgtTextAreaClearLineMarkers. + Copy / Cut / Paste / SelectAll / Undo The Ctrl+C / X / V / A / Z commands (the texthelp textEditMulti* functions the key handler runs); Cut, Paste and Undo fire Change and do nothing when read-only. + CursorToEnd wgtTextAreaCursorToEnd. FindNext needle$, caseSensitive, forward Search from the cursor (a match at the cursor counts; repeating moves past it). Returns True if found. + GetLine(line%) Returns a line's text (1-based), "" past the end. + GetSyntaxColor(index%) Returns an index's colour (BASIC &HBBGGRR), -1 for the foreground. GetWordAtCursor() Returns the word under the cursor. GoToLine line% Scroll to a line and select it. ReplaceAll needle$, replacement$, caseSensitive Replace all occurrences. Returns the count. + ReplaceSelection(needle$, repl$, caseSens, fwd) wgtTextAreaReplaceSelection (WGT_SIG_STR_STR_BOOL_BOOL). SetAutoIndent enabled Enable or disable automatic indentation on newline. SetCaptureTabs enabled When True, Tab inserts whitespace instead of moving focus. SetCursor line%, col% Move the cursor to a 1-based line and column without selecting. + SetLineMarker line%, gutter&, back& wgtTextAreaSetLineMarker with BASIC colours (&HBBGGRR, -1 = TEXTAREA_MARKER_NONE). SetShowLineNumbers show Show or hide line numbers in the gutter. + SetSyntaxColor index%, color& One palette entry (BASIC colour, -1 = the default). SetSyntaxMode mode$ Activate built-in syntax highlighting (e.g. "dhs", "bas"; "" to disable). SetTabWidth width% Set the tab stop width in characters. SetUseTabChar useChar When True, Tab inserts a literal tab; when False, spaces. @@ -127,6 +173,8 @@ TextBox has no widget-specific properties. Text is managed via the generic "Text onValidate Called before committing a change. Return false to cancel. .endtable +The TextArea interface lists the event GutterClick (LineNumber As Integer, 1-based). The TextArea fires it with wgtFireEvent after its gutter-click callback, so it reaches the widget's onEvent (the form runtime's CtrlName_GutterClick); the gutter takes clicks when either the callback or onEvent is set. + .h3 Default Event "Change" for both TextBox (basName: TextBox) and TextArea (basName: TextArea). diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c index 861c4b20..fc6e4b74 100644 --- a/src/widgets/kpunch/textInput/widgetTextInput.c +++ b/src/widgets/kpunch/textInput/widgetTextInput.c @@ -66,8 +66,9 @@ // the mask layout: text shaped like the mask fills it position by // position, anything else fills the slots in order. // -// Password mode: renders bullets instead of characters (CP437 0xF9) -// and blocks copy/cut operations for security. +// Password mode: renders bullets instead of characters (CP437 0xF9; +// BASIC's PasswordChar chooses another character) and blocks copy/cut +// operations for security. #include "dvxWgtP.h" #include "textInpt.h" @@ -91,8 +92,18 @@ typedef struct { TextEditSingleT te; char *mask; // owned copy (masked mode only) InputModeE inputMode; + int32_t plainMax; // masked mode: the capacity to restore when the mask is removed + char maskChar; // password mode: shown for each character } TextInputDataT; +// One line marker of a TextArea: a gutter dot and / or a line background +// (0xRRGGBB, TEXTAREA_MARKER_NONE for neither). +typedef struct { + int32_t line; // 1-based + uint32_t gutterRgb; + uint32_t backRgb; +} TextAreaMarkerT; + typedef struct { // Core multi-line editor state (buffer, cursor, selection, scroll, // undo, line cache) is owned by textHelp via TextEditMultiT. @@ -114,11 +125,9 @@ typedef struct { void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); void *colorizeCtx; - // Line decorator callback (optional). Called for each visible line during paint. - // Returns background color override (0 = use default). Sets *gutterColor to a - // non-zero color to draw a filled circle in the gutter for breakpoints. - uint32_t (*lineDecorator)(int32_t lineNum, uint32_t *gutterColor, void *ctx); - void *lineDecoratorCtx; + // Line markers (breakpoint dots, the current line): stb_ds array, + // painted by textAreaMarkerDecorator. + TextAreaMarkerT *markers; // Gutter click callback (optional). Fired when user clicks in the gutter. void (*onGutterClick)(WidgetT *w, int32_t lineNum); @@ -164,10 +173,18 @@ typedef struct { WidgetT *(*create)(WidgetT *parent, int32_t maxLen); } TextCreateApiT; +#include "dvxDlg.h" +#include "stb_ds_wrap.h" + #include #include #define TEXTAREA_BORDER 2 +#define TEXTAREA_EVT_GUTTERCLICK "GutterClick" // line-number gutter click (LineNumber As Integer, 1-based) +#define TEXTAREA_EVT_COLORIZE "Colorize" // SetSyntaxMode "custom": the program colours each painted line +#define TEXTAREA_MODE_CUSTOM "custom" +#define TEXTAREA_COLORIZE_ARGS 2 // Colorize LineText, Colors +#define TEXTAREA_COLOR_DIGIT '0' // Colors: one digit (a TEXT_SYNTAX_* index) per character #define TEXTAREA_PAD 2 #define TEXTAREA_MIN_ROWS 4 #define TEXTAREA_MIN_COLS 20 @@ -209,6 +226,13 @@ typedef struct { // TEXT_EDIT_DEFAULT_MAX_LEN from texthelp). #define TEXTAREA_DEFAULT_BUF 65536 +// BASIC colour argument meaning "none" (a line marker part) or "the +// default" (a syntax colour). +#define BAS_COLOR_NONE (-1) + +// The colour bits of a 0xRRGGBB value. +#define TEXTAREA_RGB_MASK 0x00FFFFFFu + // sCursorBlinkOn is defined in widgetCore.c (shared state) @@ -221,8 +245,9 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *color static const char *basGetWordAtCursor(const WidgetT *w); static bool basIsTypeSuffix(char ch); static int basKeywordCmp(const void *key, const void *entry); -static void basSetSyntaxMode(WidgetT *w, const char *mode); +static void basSetLineMarker(WidgetT *w, int32_t line, int32_t gutterColor, int32_t backColor); static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); +static void eventColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static bool maskCharValid(char slot, char ch); static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod); static int32_t maskFillSlots(TextInputDataT *ti, const char *src, int32_t srcLen, int32_t *pPos); @@ -235,34 +260,78 @@ static void maskSetText(TextInputDataT *ti, const char *text); static void textAreaApplyScrollHit(WidgetT *w, int32_t orient, int32_t sbLen, int32_t relPos, int32_t total, int32_t visible, int32_t *pScroll); static void textAreaComputeGeom(WidgetT *w, const BitmapFontT *font, TextAreaGeomT *g); static inline void textAreaDirtyCache(WidgetT *w); +static void textAreaEdited(WidgetT *w, bool changed); static int32_t textAreaGetLineCount(WidgetT *w); static int32_t textAreaGetMaxLineLen(WidgetT *w); static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font); +static const TextAreaMarkerT *textAreaMarkerAt(const TextAreaDataT *ta, int32_t line); +static uint32_t textAreaMarkerDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); static void textAreaMoveCaret(WidgetT *w, int32_t off); +static uint32_t textAreaPackRgb(const DisplayT *d, uint32_t rgb); static void textAreaScrollToCursor(WidgetT *w); +static bool textAreaSelRange(const TextAreaDataT *ta, int32_t *pLo, int32_t *pHi); +static void textInputEdited(WidgetT *w, bool changed); static int32_t textInputVisibleChars(const WidgetT *w); +static const char *textOut(const char *src, int32_t len); WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask); WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen); WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen); void wgtTextAreaAppendText(WidgetT *w, const char *text); +void wgtTextAreaClearLineMarkers(WidgetT *w); +static void wgtTextAreaCopy(WidgetT *w); void wgtTextAreaCursorToEnd(WidgetT *w); +static void wgtTextAreaCut(WidgetT *w); bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward); +static int32_t wgtTextAreaGetCursorCol(const WidgetT *w); int32_t wgtTextAreaGetCursorLine(const WidgetT *w); +static const char *wgtTextAreaGetLine(const WidgetT *w, int32_t line); +static int32_t wgtTextAreaGetLineCount(const WidgetT *w); +static int32_t wgtTextAreaGetMaxLength(const WidgetT *w); +static int32_t wgtTextAreaGetSelLength(const WidgetT *w); +static int32_t wgtTextAreaGetSelStart(const WidgetT *w); +static const char *wgtTextAreaGetSelText(const WidgetT *w); +static int32_t wgtTextAreaGetSyntaxColor(const WidgetT *w, int32_t idx); int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize); void wgtTextAreaGoToLine(WidgetT *w, int32_t line); +static void wgtTextAreaPaste(WidgetT *w); int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); bool wgtTextAreaReplaceSelection(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward); +static void wgtTextAreaSelectAll(WidgetT *w); void wgtTextAreaSetAutoIndent(WidgetT *w, bool enable); void wgtTextAreaSetCaptureTabs(WidgetT *w, bool capture); void wgtTextAreaSetColorize(WidgetT *w, void (*fn)(const char *, int32_t, uint8_t *, void *), void *ctx); void wgtTextAreaSetCursor(WidgetT *w, int32_t line, int32_t col); void wgtTextAreaSetGutterClickCallback(WidgetT *w, void (*fn)(WidgetT *, int32_t)); -void wgtTextAreaSetLineDecorator(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); +void wgtTextAreaSetLineMarker(WidgetT *w, int32_t line, uint32_t gutterRgb, uint32_t backRgb); +static void wgtTextAreaSetMaxLength(WidgetT *w, int32_t maxLen); +static void wgtTextAreaSetSelLength(WidgetT *w, int32_t length); +static void wgtTextAreaSetSelStart(WidgetT *w, int32_t start); +static void wgtTextAreaSetSelText(WidgetT *w, const char *text); void wgtTextAreaSetShowLineNumbers(WidgetT *w, bool show); +static void wgtTextAreaSetSyntaxColor(WidgetT *w, int32_t idx, int32_t color); void wgtTextAreaSetSyntaxColors(WidgetT *w, const uint32_t *colors, int32_t count); +void wgtTextAreaSetSyntaxMode(WidgetT *w, const char *mode); void wgtTextAreaSetTabWidth(WidgetT *w, int32_t width); void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar); +static void wgtTextAreaUndo(WidgetT *w); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); +void wgtTextInputCopy(WidgetT *w); +void wgtTextInputCut(WidgetT *w); +static const char *wgtTextInputGetMask(const WidgetT *w); +static int32_t wgtTextInputGetMaxLength(const WidgetT *w); +static const char *wgtTextInputGetPasswordChar(const WidgetT *w); +int32_t wgtTextInputGetSelLength(const WidgetT *w); +int32_t wgtTextInputGetSelStart(const WidgetT *w); +const char *wgtTextInputGetSelText(const WidgetT *w); +void wgtTextInputPaste(WidgetT *w); +void wgtTextInputSelectAll(WidgetT *w); +static void wgtTextInputSetMask(WidgetT *w, const char *mask); +static void wgtTextInputSetMaxLength(WidgetT *w, int32_t maxLen); +static void wgtTextInputSetPasswordChar(WidgetT *w, const char *ch); +void wgtTextInputSetSelLength(WidgetT *w, int32_t length); +void wgtTextInputSetSelStart(WidgetT *w, int32_t start); +void wgtTextInputSetSelText(WidgetT *w, const char *text); +void wgtTextInputUndo(WidgetT *w); void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font); static bool widgetTextAreaClearSelection(WidgetT *w); void widgetTextAreaDestroy(WidgetT *w); @@ -283,6 +352,7 @@ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetTextInputSetText(WidgetT *w, const char *text); +void wgtRegister(void); // Returns TEXT_SYNTAX_TYPE for a BASIC type name or literal (TRUE, @@ -322,17 +392,20 @@ static uint8_t basClassifyWord(const char *word, int32_t wordLen, bool afterDot) } -// BASIC keyword colorizer (simplified version of IDE's basicColorize). -// Words are classified against the compiler's reserved-word list -// (basKeywords.h, strcmp order) with a binary search; date literals -// follow the lexer's shape rule (basDateLit.h). +// The BASIC colorizer (SetSyntaxMode "bas"; the IDE's code editor uses +// it too). Words are classified against the compiler's reserved-word +// list (basKeywords.h, strcmp order) with a binary search; date literals +// follow the lexer's shape rule (basDateLit.h). Numbers: decimal with an +// optional fraction, exponent and type suffix, or &H / &O with their +// digits; any other '&' is the concatenation operator. static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { (void)ctx; int32_t i = 0; while (i < lineLen) { - char ch = line[i]; + char ch = line[i]; + char next = (i + 1 < lineLen) ? line[i + 1] : '\0'; // Comment if (ch == '\'') { @@ -366,20 +439,57 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void continue; } - // Number - if (isdigit((unsigned char)ch) || (ch == '&' && i + 1 < lineLen && (line[i + 1] == 'H' || line[i + 1] == 'h'))) { - while (i < lineLen && (isxdigit((unsigned char)line[i]) || line[i] == '.' || line[i] == '&' || line[i] == 'H' || line[i] == 'h')) { - colors[i++] = TEXT_SYNTAX_NUMBER; + // &H / &O number: the prefix, its digits and a type suffix + if (ch == '&' && (toupper((unsigned char)next) == 'H' || toupper((unsigned char)next) == 'O')) { + int32_t start = i; + + i += 2; + + while (i < lineLen && isxdigit((unsigned char)line[i])) { + i++; } + if (i < lineLen && basIsTypeSuffix(line[i])) { + i++; + } + + memset(colors + start, TEXT_SYNTAX_NUMBER, (size_t)(i - start)); continue; } - // Identifier or keyword + // Decimal number: digits, fraction, exponent, type suffix + if (isdigit((unsigned char)ch) || (ch == '.' && isdigit((unsigned char)next))) { + int32_t start = i; + + while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) { + i++; + } + + if (i + 1 < lineLen && (toupper((unsigned char)line[i]) == 'E' || toupper((unsigned char)line[i]) == 'D') && (isdigit((unsigned char)line[i + 1]) || line[i + 1] == '+' || line[i + 1] == '-')) { + i += 2; + + while (i < lineLen && isdigit((unsigned char)line[i])) { + i++; + } + } + + if (i < lineLen && basIsTypeSuffix(line[i])) { + i++; + } + + memset(colors + start, TEXT_SYNTAX_NUMBER, (size_t)(i - start)); + continue; + } + + // Identifier or keyword, with one type suffix (Dir$, Len%) if (isalpha((unsigned char)ch) || ch == '_') { int32_t start = i; - while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || basIsTypeSuffix(line[i]))) { + while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_')) { + i++; + } + + if (i < lineLen && basIsTypeSuffix(line[i])) { i++; } @@ -396,7 +506,7 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void } // Operator - if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\') { + if (ch != '\0' && strchr("=<>+-*/\\^&", ch) != NULL) { colors[i++] = TEXT_SYNTAX_OPERATOR; continue; } @@ -424,20 +534,13 @@ static int basKeywordCmp(const void *key, const void *entry) { } -// SetSyntaxMode method: activate a built-in colorizer -static void basSetSyntaxMode(WidgetT *w, const char *mode) { - if (!mode || !*mode) { - wgtTextAreaSetColorize(w, NULL, NULL); - return; - } +// BASIC SetLineMarker line, gutterColor, backColor: colours in BASIC's +// &HBBGGRR, -1 for a part that is not drawn. +static void basSetLineMarker(WidgetT *w, int32_t line, int32_t gutterColor, int32_t backColor) { + uint32_t gutter = (gutterColor == BAS_COLOR_NONE) ? TEXTAREA_MARKER_NONE : wgtBasicColor((uint32_t)gutterColor) & TEXTAREA_RGB_MASK; + uint32_t back = (backColor == BAS_COLOR_NONE) ? TEXTAREA_MARKER_NONE : wgtBasicColor((uint32_t)backColor) & TEXTAREA_RGB_MASK; - if (strcasecmp(mode, "dhs") == 0 || strcasecmp(mode, "help") == 0) { - wgtTextAreaSetColorize(w, dhsColorize, NULL); - } else if (strcasecmp(mode, "bas") == 0 || strcasecmp(mode, "basic") == 0) { - wgtTextAreaSetColorize(w, basColorize, NULL); - } else { - wgtTextAreaSetColorize(w, NULL, NULL); - } + wgtTextAreaSetLineMarker(w, line, gutter, back); } @@ -523,6 +626,52 @@ static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void } +// SetSyntaxMode "custom": each painted line goes to the Colorize event +// (ctx is the TextArea) with Colors, one digit per character (all +// TEXT_SYNTAX_DEFAULT), which the handler changes (ByRef) to the +// TEXT_SYNTAX_* index of each character; anything else stays default. +// It runs for every painted line, so the handler must be quick. +static void eventColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { + static char *sLine = NULL; + static char *sColors = NULL; + static int32_t sCap = 0; + + if (lineLen + 1 > sCap) { + char *grownLine = (char *)realloc(sLine, (size_t)lineLen + 1); + char *grownColors = grownLine ? (char *)realloc(sColors, (size_t)lineLen + 1) : NULL; + + sLine = grownLine ? grownLine : sLine; + sColors = grownColors ? grownColors : sColors; + + if (!grownColors) { + return; + } + + sCap = lineLen + 1; + } + + memcpy(sLine, line, (size_t)lineLen); + sLine[lineLen] = '\0'; + memset(sColors, TEXTAREA_COLOR_DIGIT + TEXT_SYNTAX_DEFAULT, (size_t)lineLen); + sColors[lineLen] = '\0'; + + WgtValueT args[TEXTAREA_COLORIZE_ARGS]; + + args[0] = wgtValueStr(sLine); + args[1] = wgtValueStr(sColors); + + if (!wgtFireEvent((WidgetT *)ctx, TEXTAREA_EVT_COLORIZE, args, TEXTAREA_COLORIZE_ARGS) || args[1].type != WGT_IFACE_STRING || !args[1].s) { + return; + } + + for (int32_t i = 0; i < lineLen && args[1].s[i]; i++) { + int32_t idx = args[1].s[i] - TEXTAREA_COLOR_DIGIT; + + colors[i] = (idx > TEXT_SYNTAX_DEFAULT && idx < TEXT_SYNTAX_MAX) ? (uint8_t)idx : TEXT_SYNTAX_DEFAULT; + } +} + + static bool maskCharValid(char slot, char ch) { switch (slot) { case '#': @@ -654,12 +803,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } - if (changed && !widgetFireChange(w)) { - return; - } - - textEditScrollToCursor(*pCur, textInputVisibleChars(w), &ti->te.scrollOff); - wgtInvalidatePaint(w); + textInputEdited(w, changed); } @@ -814,6 +958,23 @@ static inline void textAreaDirtyCache(WidgetT *w) { } +// The common tail of an edit made by a method (Cut, Paste, Undo, SelText): +// the gutter width may change, Change fires (it may destroy the widget), +// then the cursor is scrolled into view and the area repainted. +static void textAreaEdited(WidgetT *w, bool changed) { + if (changed) { + textAreaDirtyCache(w); + + if (!widgetFireChange(w)) { + return; + } + } + + textAreaScrollToCursor(w); + wgtInvalidatePaint(w); +} + + static int32_t textAreaGetLineCount(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; return textEditLineCount(&ta->te.lines, ta->te.buf, ta->te.len); @@ -858,6 +1019,37 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) { } +// The marker of 1-based line, or NULL. +static const TextAreaMarkerT *textAreaMarkerAt(const TextAreaDataT *ta, int32_t line) { + for (int32_t i = 0; i < (int32_t)arrlen(ta->markers); i++) { + if (ta->markers[i].line == line) { + return &ta->markers[i]; + } + } + + return NULL; +} + + +// The paint hook's line decorator (installed while there are markers): +// the line's background colour (0 = the normal one) and gutter dot. +static uint32_t textAreaMarkerDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) { + WidgetT *w = (WidgetT *)ctx; + const DisplayT *d = &wgtGetContext(w)->display; + const TextAreaMarkerT *mk = textAreaMarkerAt((const TextAreaDataT *)w->data, lineNum); + + if (!mk) { + return 0; + } + + if (mk->gutterRgb != TEXTAREA_MARKER_NONE) { + *gutterColor = textAreaPackRgb(d, mk->gutterRgb); + } + + return (mk->backRgb != TEXTAREA_MARKER_NONE) ? textAreaPackRgb(d, mk->backRgb) : 0; +} + + // Puts the caret at buffer offset off without selecting anything, keeps // it in view and repaints. static void textAreaMoveCaret(WidgetT *w, int32_t off) { @@ -872,6 +1064,11 @@ static void textAreaMoveCaret(WidgetT *w, int32_t off) { } +static uint32_t textAreaPackRgb(const DisplayT *d, uint32_t rgb) { + return packColor(d, DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), DVX_RGB_BLUE(rgb)); +} + + // Scrolls the view so the cursor is visible under the current geometry. static void textAreaScrollToCursor(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; @@ -888,14 +1085,56 @@ static void textAreaScrollToCursor(WidgetT *w) { } +// The selection as buffer offsets lo..hi; with none, both are the cursor +// offset and the result is false. +static bool textAreaSelRange(const TextAreaDataT *ta, int32_t *pLo, int32_t *pHi) { + TextAreaDataT *mta = (TextAreaDataT *)ta; // the line cache fills lazily + + if (ta->te.selAnchor >= 0 && ta->te.selCursor >= 0 && ta->te.selAnchor != ta->te.selCursor) { + *pLo = DVX_MIN(ta->te.selAnchor, ta->te.selCursor); + *pHi = DVX_MAX(ta->te.selAnchor, ta->te.selCursor); + return true; + } + + *pLo = textEditRowColToOff(&mta->te.lines, ta->te.buf, ta->te.len, ta->te.cursorRow, ta->te.cursorCol); + *pHi = *pLo; + return false; +} + + +// The tail of a text box edit: Change when the text changed (a handler +// may destroy the widget), the cursor kept in view, a repaint. +static void textInputEdited(WidgetT *w, bool changed) { + if (changed && !widgetFireChange(w)) { + return; + } + + TextInputDataT *ti = (TextInputDataT *)w->data; + + textEditScrollToCursor(ti->te.cursorPos, textInputVisibleChars(w), &ti->te.scrollOff); + wgtInvalidatePaint(w); +} + + // Visible character columns of a single-line input (inside the pad). static int32_t textInputVisibleChars(const WidgetT *w) { return textEditVisibleChars(w->w, &wgtGetContext(w)->font); } +// A string result for BASIC: len bytes of src, valid until the next call. +static const char *textOut(const char *src, int32_t len) { + static char *sOut = NULL; // stb_ds + + arrsetlen(sOut, len + 1); + memcpy(sOut, src, (size_t)len); + sOut[len] = '\0'; + return sOut; +} + + WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask) { - if (!mask) { + if (!mask || strlen(mask) > TEXT_EDIT_SINGLE_MAX_LEN) { return NULL; } @@ -928,6 +1167,7 @@ WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen) { if (w) { TextInputDataT *ti = (TextInputDataT *)w->data; ti->inputMode = InputPasswordE; + ti->maskChar = PASSWORD_BULLET; } return w; @@ -991,6 +1231,24 @@ void wgtTextAreaAppendText(WidgetT *w, const char *text) { } +// Removes every line marker. +void wgtTextAreaClearLineMarkers(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + TextAreaDataT *ta = (TextAreaDataT *)w->data; + + arrsetlen(ta->markers, 0); + wgtInvalidatePaint(w); +} + + +// BASIC Copy: the selection to the clipboard (Ctrl+C). +static void wgtTextAreaCopy(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + textEditMultiCopy(&((TextAreaDataT *)w->data)->te); +} + + // wgtTextAreaCursorToEnd -- place the cursor at the very end of the buffer // (offset == len), past the last character. A backward search anchored // here scans every match including those on the final line, which a line- @@ -1006,6 +1264,16 @@ void wgtTextAreaCursorToEnd(WidgetT *w) { } +// BASIC Cut: the selection to the clipboard, removed (Ctrl+X). +static void wgtTextAreaCut(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + if (!w->readOnly) { + textAreaEdited(w, textEditMultiCut(&((TextAreaDataT *)w->data)->te)); + } +} + + bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) { if (!w || w->type != sTextAreaTypeId) { return false; @@ -1023,6 +1291,14 @@ bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, boo } +// BASIC CursorCol: the cursor's column, 1 = the start of the line. +static int32_t wgtTextAreaGetCursorCol(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextAreaTypeId, 1); + + return ((const TextAreaDataT *)w->data)->te.cursorCol + 1; +} + + int32_t wgtTextAreaGetCursorLine(const WidgetT *w) { if (!w || w->type != sTextAreaTypeId) { return 1; @@ -1033,6 +1309,89 @@ int32_t wgtTextAreaGetCursorLine(const WidgetT *w) { } +// BASIC GetLine(n): the text of 1-based line n without its line end, +// "" past the last line. +static const char *wgtTextAreaGetLine(const WidgetT *w, int32_t line) { + VALIDATE_WIDGET(w, sTextAreaTypeId, ""); + TextAreaDataT *ta = (TextAreaDataT *)w->data; + + if (line < 1 || line > textAreaGetLineCount((WidgetT *)w)) { + return ""; + } + + int32_t start = textEditLineStart(&ta->te.lines, ta->te.buf, ta->te.len, line - 1); + + return textOut(ta->te.buf + start, textEditLineLen(&ta->te.lines, ta->te.buf, ta->te.len, line - 1)); +} + + +// BASIC LineCount: the number of lines (an empty text has one). +static int32_t wgtTextAreaGetLineCount(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextAreaTypeId, 0); + + return textAreaGetLineCount((WidgetT *)w); +} + + +// BASIC MaxLength: the most characters the area holds. +static int32_t wgtTextAreaGetMaxLength(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextAreaTypeId, 0); + + return ((const TextAreaDataT *)w->data)->te.bufSize - 1; +} + + +// BASIC SelLength: the number of characters selected (0 = none). +static int32_t wgtTextAreaGetSelLength(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextAreaTypeId, 0); + int32_t lo; + int32_t hi; + + textAreaSelRange((const TextAreaDataT *)w->data, &lo, &hi); + return hi - lo; +} + + +// BASIC SelStart: where the selection starts, or the cursor when nothing +// is selected (0 = before the first character). +static int32_t wgtTextAreaGetSelStart(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextAreaTypeId, 0); + int32_t lo; + int32_t hi; + + textAreaSelRange((const TextAreaDataT *)w->data, &lo, &hi); + return lo; +} + + +// BASIC SelText: the selected text ("" when nothing is selected). +static const char *wgtTextAreaGetSelText(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextAreaTypeId, ""); + const TextAreaDataT *ta = (const TextAreaDataT *)w->data; + int32_t lo; + int32_t hi; + + textAreaSelRange(ta, &lo, &hi); + return textOut(ta->te.buf + lo, hi - lo); +} + + +// BASIC GetSyntaxColor(index): the colour (&HBBGGRR) the colorizer draws +// that kind of text in; -1 for the control's ForeColor. +static int32_t wgtTextAreaGetSyntaxColor(const WidgetT *w, int32_t idx) { + VALIDATE_WIDGET(w, sTextAreaTypeId, BAS_COLOR_NONE); + + if (idx < 0 || idx >= TEXT_SYNTAX_MAX) { + return BAS_COLOR_NONE; + } + + uint32_t custom = ((const TextAreaDataT *)w->data)->customSyntaxColors[idx]; + uint32_t rgb = custom ? custom : textEditDefaultSyntaxRgb((uint8_t)idx); + + return rgb ? (int32_t)wgtBasicColor(rgb) : BAS_COLOR_NONE; +} + + int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize) { if (!w || w->type != sTextAreaTypeId) { buf[0] = '\0'; @@ -1065,6 +1424,16 @@ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) { } +// BASIC Paste: the clipboard replaces the selection (Ctrl+V). +static void wgtTextAreaPaste(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + if (!w->readOnly) { + textAreaEdited(w, textEditMultiPaste(&((TextAreaDataT *)w->data)->te)); + } +} + + int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive) { if (!w || w->type != sTextAreaTypeId) { return 0; @@ -1110,6 +1479,15 @@ bool wgtTextAreaReplaceSelection(WidgetT *w, const char *needle, const char *rep } +// BASIC SelectAll: select the whole text (Ctrl+A). +static void wgtTextAreaSelectAll(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + textEditMultiSelectAll(&((TextAreaDataT *)w->data)->te); + textAreaEdited(w, false); +} + + void wgtTextAreaSetAutoIndent(WidgetT *w, bool enable) { if (!w || w->type != sTextAreaTypeId) { return; @@ -1166,14 +1544,89 @@ void wgtTextAreaSetGutterClickCallback(WidgetT *w, void (*fn)(WidgetT *, int32_t } -void wgtTextAreaSetLineDecorator(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx) { - if (!w || w->type != sTextAreaTypeId) { +// Marks 1-based line with a gutter dot and / or a background colour +// (0xRRGGBB; TEXTAREA_MARKER_NONE for a part that is not drawn). A +// marker with neither part is removed. Markers stay on their line +// numbers when the text changes. +void wgtTextAreaSetLineMarker(WidgetT *w, int32_t line, uint32_t gutterRgb, uint32_t backRgb) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + TextAreaDataT *ta = (TextAreaDataT *)w->data; + bool off = gutterRgb == TEXTAREA_MARKER_NONE && backRgb == TEXTAREA_MARKER_NONE; + int32_t idx = 0; + + while (idx < (int32_t)arrlen(ta->markers) && ta->markers[idx].line != line) { + idx++; + } + + if (off) { + if (idx < (int32_t)arrlen(ta->markers)) { + arrdel(ta->markers, idx); + } + } else { + TextAreaMarkerT mk = { line, gutterRgb, backRgb }; + + if (idx < (int32_t)arrlen(ta->markers)) { + ta->markers[idx] = mk; + } else { + arrput(ta->markers, mk); + } + } + + wgtInvalidatePaint(w); +} + + +// BASIC MaxLength = n: the area holds up to n characters (0 = the +// default, TEXTAREA_DEFAULT_BUF); a longer text is cut. +static void wgtTextAreaSetMaxLength(WidgetT *w, int32_t maxLen) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + TextAreaDataT *ta = (TextAreaDataT *)w->data; + TextEditMultiT fresh; + + if (!textEditMultiInit(&fresh, maxLen > 0 ? maxLen : TEXTAREA_DEFAULT_BUF, ta->tabWidth)) { return; } - TextAreaDataT *ta = (TextAreaDataT *)w->data; - ta->lineDecorator = fn; - ta->lineDecoratorCtx = ctx; + fresh.len = textEditCopyText(fresh.buf, fresh.bufSize, ta->te.buf); + textEditMultiFree(&ta->te); + ta->te = fresh; + textAreaDirtyCache(w); + wgtInvalidatePaint(w); +} + + +// BASIC SelLength = n: select n characters from SelStart (0 = none). +static void wgtTextAreaSetSelLength(WidgetT *w, int32_t length) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + TextAreaDataT *ta = (TextAreaDataT *)w->data; + int32_t lo; + int32_t hi; + + textAreaSelRange(ta, &lo, &hi); + hi = clampInt(lo + length, lo, ta->te.len); + textAreaMoveCaret(w, hi); + + if (hi > lo) { + ta->te.selAnchor = lo; + ta->te.selCursor = hi; + } +} + + +// BASIC SelStart = n: the cursor to offset n, nothing selected. +static void wgtTextAreaSetSelStart(WidgetT *w, int32_t start) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + textAreaMoveCaret(w, clampInt(start, 0, ((TextAreaDataT *)w->data)->te.len)); +} + + +// BASIC SelText = s: s replaces the selection (or goes in at the cursor), +// the cursor after it; fires Change. +static void wgtTextAreaSetSelText(WidgetT *w, const char *text) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + textAreaEdited(w, textEditMultiReplaceSel(&((TextAreaDataT *)w->data)->te, text ? text : "", text ? (int32_t)strlen(text) : 0)); } @@ -1188,6 +1641,18 @@ void wgtTextAreaSetShowLineNumbers(WidgetT *w, bool show) { } +// BASIC SetSyntaxColor index, color: the colour (&HBBGGRR) for one kind +// of text (TEXT_SYNTAX_* index); -1 restores the default. +static void wgtTextAreaSetSyntaxColor(WidgetT *w, int32_t idx, int32_t color) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + if (idx >= 0 && idx < TEXT_SYNTAX_MAX) { + ((TextAreaDataT *)w->data)->customSyntaxColors[idx] = (color == BAS_COLOR_NONE) ? 0 : wgtBasicColor((uint32_t)color) & TEXTAREA_RGB_MASK; + wgtInvalidatePaint(w); + } +} + + void wgtTextAreaSetSyntaxColors(WidgetT *w, const uint32_t *colors, int32_t count) { if (!w || w->type != sTextAreaTypeId) { return; @@ -1208,6 +1673,26 @@ void wgtTextAreaSetSyntaxColors(WidgetT *w, const uint32_t *colors, int32_t coun } +// Built-in colorizer by name: "bas" / "basic" (BASIC source), "dhs" / +// "help" (help source), "custom" (the Colorize event); "" or another name +// turns colouring off. The +// SetSyntaxMode method, and the IDE's code editor. +void wgtTextAreaSetSyntaxMode(WidgetT *w, const char *mode) { + TextColorFnT fn = NULL; + + if (mode && (strcasecmp(mode, "dhs") == 0 || strcasecmp(mode, "help") == 0)) { + fn = dhsColorize; + } else if (mode && (strcasecmp(mode, "bas") == 0 || strcasecmp(mode, "basic") == 0)) { + fn = basColorize; + } else if (mode && strcasecmp(mode, TEXTAREA_MODE_CUSTOM) == 0) { + fn = eventColorize; + } + + wgtTextAreaSetColorize(w, fn, w); + wgtInvalidatePaint(w); +} + + void wgtTextAreaSetTabWidth(WidgetT *w, int32_t width) { if (!w || w->type != sTextAreaTypeId) { return; @@ -1229,6 +1714,17 @@ void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar) { } +// BASIC Undo: swap with the text before the last edit (Ctrl+Z; again +// is redo). +static void wgtTextAreaUndo(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextAreaTypeId); + + if (!w->readOnly) { + textAreaEdited(w, textEditMultiUndo(&((TextAreaDataT *)w->data)->te)); + } +} + + WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { WidgetT *w = widgetAlloc(parent, sTextInputTypeId); @@ -1250,6 +1746,233 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { } +// The clipboard and undo commands run the text box's own keys, so the +// password and mask rules are the keyboard's. +void wgtTextInputCopy(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + widgetTextInputOnKey(w, KEY_CTRL_C, 0); +} + + +void wgtTextInputCut(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + widgetTextInputOnKey(w, KEY_CTRL_X, 0); +} + + +// BASIC Mask: the input mask of a masked box, "" for any other. +static const char *wgtTextInputGetMask(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextInputTypeId, ""); + const TextInputDataT *ti = (const TextInputDataT *)w->data; + + return (ti->inputMode == InputMaskedE) ? ti->mask : ""; +} + + +// BASIC MaxLength: the most characters the box holds. +static int32_t wgtTextInputGetMaxLength(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextInputTypeId, 0); + + return ((const TextInputDataT *)w->data)->te.bufSize - 1; +} + + +// BASIC PasswordChar: "" for a plain box, else the character a password +// box shows for each one typed. +static const char *wgtTextInputGetPasswordChar(const WidgetT *w) { + static char out[2]; + + VALIDATE_WIDGET(w, sTextInputTypeId, ""); + + const TextInputDataT *ti = (const TextInputDataT *)w->data; + + out[0] = (ti->inputMode == InputPasswordE) ? ti->maskChar : '\0'; + out[1] = '\0'; + return out; +} + + +int32_t wgtTextInputGetSelLength(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextInputTypeId, 0); + int32_t lo; + int32_t hi; + + textEditSingleSelRange(&((const TextInputDataT *)w->data)->te, &lo, &hi); + return hi - lo; +} + + +int32_t wgtTextInputGetSelStart(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextInputTypeId, 0); + int32_t lo; + int32_t hi; + + textEditSingleSelRange(&((const TextInputDataT *)w->data)->te, &lo, &hi); + return lo; +} + + +// The selected text, in a buffer kept until the next call. +const char *wgtTextInputGetSelText(const WidgetT *w) { + VALIDATE_WIDGET(w, sTextInputTypeId, ""); + static char *sSelText = NULL; + const TextInputDataT *ti = (const TextInputDataT *)w->data; + int32_t lo; + int32_t hi; + + textEditSingleSelRange(&ti->te, &lo, &hi); + + char *grown = (char *)realloc(sSelText, (size_t)(hi - lo) + 1); + + if (!grown) { + return ""; + } + + sSelText = grown; + memcpy(sSelText, ti->te.buf + lo, (size_t)(hi - lo)); + sSelText[hi - lo] = '\0'; + return sSelText; +} + + +void wgtTextInputPaste(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + widgetTextInputOnKey(w, KEY_CTRL_V, 0); +} + + +void wgtTextInputSelectAll(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + widgetTextInputOnKey(w, KEY_CTRL_A, 0); +} + + +// BASIC Mask = m: a masked box with input mask m ('#' a digit, 'A' a +// letter, '*' any character, others fixed), keeping what of the text +// fits the slots; "" makes it a plain box again holding the formatted +// text (its earlier MaxLength comes back). Replaces PasswordChar. +static void wgtTextInputSetMask(WidgetT *w, const char *mask) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + TextInputDataT *ti = (TextInputDataT *)w->data; + bool masked = mask && mask[0]; + int32_t cap = masked ? (int32_t)strlen(mask) : ti->plainMax; + char *copy = masked ? strdup(mask) : NULL; + TextEditSingleT fresh; + + if ((masked && (!copy || cap > TEXT_EDIT_SINGLE_MAX_LEN)) || !textEditSingleInit(&fresh, cap)) { + free(copy); + return; + } + + if (ti->inputMode != InputMaskedE) { + ti->plainMax = ti->te.bufSize - 1; + } + + TextEditSingleT prev = ti->te; + + ti->te = fresh; + free(ti->mask); + ti->mask = copy; + ti->inputMode = masked ? InputMaskedE : InputNormalE; + + if (masked) { + maskSetText(ti, prev.buf); + } else { + textEditSingleSetText(&ti->te, prev.buf); + } + + textEditSingleFree(&prev); + wgtInvalidatePaint(w); +} + + +// BASIC MaxLength = n: the box holds up to n characters (0 = the default, +// TEXT_EDIT_DEFAULT_MAX_LEN); longer text is cut. A masked box keeps its +// mask's length. +static void wgtTextInputSetMaxLength(WidgetT *w, int32_t maxLen) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + TextInputDataT *ti = (TextInputDataT *)w->data; + + if (ti->inputMode == InputMaskedE || !textEditSingleResize(&ti->te, maxLen)) { + return; + } + + wgtInvalidatePaint(w); +} + + +// BASIC PasswordChar = c: a password box showing c for each character +// (copy and cut are blocked); "" makes it a plain box again. +static void wgtTextInputSetPasswordChar(WidgetT *w, const char *ch) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + TextInputDataT *ti = (TextInputDataT *)w->data; + + if (ti->inputMode == InputMaskedE) { + return; + } + + ti->maskChar = (ch && ch[0]) ? ch[0] : '\0'; + ti->inputMode = ti->maskChar ? InputPasswordE : InputNormalE; + wgtInvalidatePaint(w); +} + + +// Select length characters from SelStart (0: none). +void wgtTextInputSetSelLength(WidgetT *w, int32_t length) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + TextEditSingleT *te = &((TextInputDataT *)w->data)->te; + int32_t lo; + int32_t hi; + + textEditSingleSelRange(te, &lo, &hi); + hi = lo + DVX_MIN(DVX_MAX(length, 0), te->len - lo); + textEditMoveCursor(lo, false, &te->cursorPos, &te->selStart, &te->selEnd); + textEditMoveCursor(hi, hi > lo, &te->cursorPos, &te->selStart, &te->selEnd); + textInputEdited(w, false); +} + + +// The cursor to offset start, nothing selected. +void wgtTextInputSetSelStart(WidgetT *w, int32_t start) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + TextEditSingleT *te = &((TextInputDataT *)w->data)->te; + + textEditMoveCursor(DVX_MAX(DVX_MIN(start, te->len), 0), false, &te->cursorPos, &te->selStart, &te->selEnd); + textInputEdited(w, false); +} + + +// text replaces the selection (or goes in at the cursor), the cursor +// after it; a masked box fills its slots from the selection's start as +// pasting does. Fires Change. +void wgtTextInputSetSelText(WidgetT *w, const char *text) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + TextInputDataT *ti = (TextInputDataT *)w->data; + int32_t textLen = text ? (int32_t)strlen(text) : 0; + bool changed; + + if (ti->inputMode == InputMaskedE) { + int32_t lo; + int32_t hi; + + textEditSingleSelRange(&ti->te, &lo, &hi); + textEditSaveUndo(ti->te.buf, ti->te.len, ti->te.cursorPos, ti->te.undoBuf, &ti->te.undoLen, &ti->te.undoCursor, ti->te.bufSize); + changed = maskFillSlots(ti, text ? text : "", textLen, &lo) > 0; + textEditMoveCursor(lo, false, &ti->te.cursorPos, &ti->te.selStart, &ti->te.selEnd); + } else { + changed = textEditSingleReplaceSel(&ti->te, text ? text : "", textLen); + } + + textInputEdited(w, changed); +} + + +void wgtTextInputUndo(WidgetT *w) { + VALIDATE_WIDGET_VOID(w, sTextInputTypeId); + widgetTextInputOnKey(w, KEY_CTRL_Z, 0); +} + + void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = font->charWidth * TEXTAREA_MIN_COLS + TEXTAREA_PAD * 2 + TEXTAREA_BORDER * 2 + WGT_SB_W; w->calcMinH = font->charHeight * TEXTAREA_MIN_ROWS + TEXTAREA_BORDER * 2; @@ -1274,6 +1997,7 @@ void widgetTextAreaDestroy(WidgetT *w) { if (ta) { textEditMultiFree(&ta->te); + arrfree(ta->markers); free(ta->rawSyntax); free(ta->expandBuf); free(ta->syntaxBuf); @@ -1344,7 +2068,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { // if the generation counter is unchanged. uint32_t gen = sWidgetGen; - widgetTextEditMultiOnKey(w, key, mod, &ta->te.lines, ta->te.buf, ta->te.bufSize, &ta->te.len, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.scrollRow, &ta->te.scrollCol, &ta->te.selAnchor, &ta->te.selCursor, ta->te.undoBuf, &ta->te.undoLen, &ta->te.undoCursor, DVX_MAX(geom.visRows, 1), DVX_MAX(geom.visCols, 1), &opts); + widgetTextEditMultiOnKey(w, key, mod, &ta->te, DVX_MAX(geom.visRows, 1), DVX_MAX(geom.visCols, 1), &opts); if (sWidgetGen != gen) { return; @@ -1413,16 +2137,19 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { } // Click on gutter -- toggle breakpoint - if (gutterW > 0 && vx < innerX && ta->onGutterClick) { + if (gutterW > 0 && vx < innerX && (ta->onGutterClick || w->onEvent)) { int32_t relY = vy - innerY; int32_t clickRow = ta->te.scrollRow + relY / font->charHeight; if (clickRow >= 0 && clickRow < totalLines) { - uint32_t gen = sWidgetGen; + uint32_t gen = sWidgetGen; + WgtValueT arg = wgtValueInt(clickRow + 1); // 1-based line number - ta->onGutterClick(w, clickRow + 1); // 1-based line number + if (ta->onGutterClick) { + ta->onGutterClick(w, arg.i); + } - if (sWidgetGen != gen) { + if (sWidgetGen != gen || !wgtFireEvent(w, TEXTAREA_EVT_GUTTERCLICK, &arg, 1)) { return; } } @@ -1483,14 +2210,11 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit break; } - int32_t drawY = textY + i * font->charHeight; - uint32_t gutterColor = 0; + int32_t drawY = textY + i * font->charHeight; + const TextAreaMarkerT *mk = textAreaMarkerAt(ta, row + 1); - if (ta->lineDecorator) { - ta->lineDecorator(row + 1, &gutterColor, ta->lineDecoratorCtx); - } - - if (gutterColor) { + if (mk && mk->gutterRgb != TEXTAREA_MARKER_NONE) { + uint32_t gutterColor = textAreaPackRgb(d, mk->gutterRgb); // Draw a filled circle of GUTTER_DOT_RADIUS as one scanline // per row: the half width of row dy is the largest hw with // hw^2 + dy^2 <= (radius + 1/2)^2 (all terms scaled by 4). @@ -1520,8 +2244,8 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit TextEditPaintHooksT hooks = { .colorize = ta->colorize, .colorizeCtx = ta->colorizeCtx, - .lineDecorator = ta->lineDecorator, - .lineDecoratorCtx = ta->lineDecoratorCtx, + .lineDecorator = arrlen(ta->markers) > 0 ? textAreaMarkerDecorator : NULL, + .lineDecoratorCtx = w, .rawSyntax = ta->rawSyntax, .expandBuf = ta->expandBuf, .syntaxBuf = ta->syntaxBuf, @@ -1706,7 +2430,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi int32_t dispLen = DVX_MAX(DVX_MIN(DVX_MIN(ti->te.len - off, textInputVisibleChars(w)), (int32_t)sizeof(dispBuf)), 0); if (ti->inputMode == InputPasswordE) { - memset(dispBuf, PASSWORD_BULLET, dispLen); + memset(dispBuf, ti->maskChar, dispLen); } else { memcpy(dispBuf, ti->te.buf + off, dispLen); } @@ -1783,13 +2507,26 @@ static const TextInputApiT sApi = { .findNext = wgtTextAreaFindNext, .replaceAll = wgtTextAreaReplaceAll, .replaceSelection = wgtTextAreaReplaceSelection, - .setLineDecorator = wgtTextAreaSetLineDecorator, + .setLineMarker = wgtTextAreaSetLineMarker, .getCursorLine = wgtTextAreaGetCursorLine, .setGutterClick = wgtTextAreaSetGutterClickCallback, .getWordAtCursor = wgtTextAreaGetWordAtCursor, .setSyntaxColors = wgtTextAreaSetSyntaxColors, .cursorToEnd = wgtTextAreaCursorToEnd, - .setCursor = wgtTextAreaSetCursor + .setCursor = wgtTextAreaSetCursor, + .clearLineMarkers = wgtTextAreaClearLineMarkers, + .setSyntaxMode = wgtTextAreaSetSyntaxMode, + .inputGetSelStart = wgtTextInputGetSelStart, + .inputGetSelLength = wgtTextInputGetSelLength, + .inputGetSelText = wgtTextInputGetSelText, + .inputSetSelStart = wgtTextInputSetSelStart, + .inputSetSelLength = wgtTextInputSetSelLength, + .inputSetSelText = wgtTextInputSetSelText, + .inputCut = wgtTextInputCut, + .inputCopy = wgtTextInputCopy, + .inputPaste = wgtTextInputPaste, + .inputSelectAll = wgtTextInputSelectAll, + .inputUndo = wgtTextInputUndo }; // Per-type APIs for the designer @@ -1806,30 +2543,72 @@ static const TextCreateApiT sTextAreaApi = { // TextArea BASIC interface static const WgtPropDescT sTextAreaProps[] = { - { "CursorLine", WGT_IFACE_INT, (void *)wgtTextAreaGetCursorLine, NULL, NULL } + { "CursorCol", WGT_IFACE_INT, (void *)wgtTextAreaGetCursorCol, NULL, NULL }, + { "CursorLine", WGT_IFACE_INT, (void *)wgtTextAreaGetCursorLine, NULL, NULL }, + { "LineCount", WGT_IFACE_INT, (void *)wgtTextAreaGetLineCount, NULL, NULL }, + { "MaxLength", WGT_IFACE_INT, (void *)wgtTextAreaGetMaxLength, (void *)wgtTextAreaSetMaxLength, NULL }, + { "SelLength", WGT_IFACE_INT, (void *)wgtTextAreaGetSelLength, (void *)wgtTextAreaSetSelLength, NULL }, + { "SelStart", WGT_IFACE_INT, (void *)wgtTextAreaGetSelStart, (void *)wgtTextAreaSetSelStart, NULL }, + { "SelText", WGT_IFACE_STRING, (void *)wgtTextAreaGetSelText, (void *)wgtTextAreaSetSelText, NULL } }; static const WgtMethodDescT sTextAreaMethods[] = { - { "AppendText", WGT_SIG_STR, (void *)wgtTextAreaAppendText }, - { "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext }, - { "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor }, - { "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine }, - { "ReplaceAll", WGT_SIG_STR_STR_BOOL, (void *)wgtTextAreaReplaceAll }, - { "SetAutoIndent", WGT_SIG_BOOL, (void *)wgtTextAreaSetAutoIndent }, - { "SetCaptureTabs", WGT_SIG_BOOL, (void *)wgtTextAreaSetCaptureTabs }, - { "SetCursor", WGT_SIG_INT_INT, (void *)wgtTextAreaSetCursor }, - { "SetShowLineNumbers", WGT_SIG_BOOL, (void *)wgtTextAreaSetShowLineNumbers }, - { "SetSyntaxMode", WGT_SIG_STR, (void *)basSetSyntaxMode }, - { "SetTabWidth", WGT_SIG_INT, (void *)wgtTextAreaSetTabWidth }, - { "SetUseTabChar", WGT_SIG_BOOL, (void *)wgtTextAreaSetUseTabChar }, + { "AppendText", WGT_SIG_STR, (void *)wgtTextAreaAppendText }, + { "ClearLineMarkers", WGT_SIG_VOID, (void *)wgtTextAreaClearLineMarkers }, + { "Copy", WGT_SIG_VOID, (void *)wgtTextAreaCopy }, + { "CursorToEnd", WGT_SIG_VOID, (void *)wgtTextAreaCursorToEnd }, + { "Cut", WGT_SIG_VOID, (void *)wgtTextAreaCut }, + { "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext }, + { "GetLine", WGT_SIG_RET_STR_INT, (void *)wgtTextAreaGetLine }, + { "GetSyntaxColor", WGT_SIG_RET_INT_INT, (void *)wgtTextAreaGetSyntaxColor }, + { "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor }, + { "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine }, + { "Paste", WGT_SIG_VOID, (void *)wgtTextAreaPaste }, + { "ReplaceAll", WGT_SIG_STR_STR_BOOL, (void *)wgtTextAreaReplaceAll }, + { "ReplaceSelection", WGT_SIG_STR_STR_BOOL_BOOL, (void *)wgtTextAreaReplaceSelection }, + { "SelectAll", WGT_SIG_VOID, (void *)wgtTextAreaSelectAll }, + { "SetAutoIndent", WGT_SIG_BOOL, (void *)wgtTextAreaSetAutoIndent }, + { "SetCaptureTabs", WGT_SIG_BOOL, (void *)wgtTextAreaSetCaptureTabs }, + { "SetCursor", WGT_SIG_INT_INT, (void *)wgtTextAreaSetCursor }, + { "SetLineMarker", WGT_SIG_INT3, (void *)basSetLineMarker }, + { "SetShowLineNumbers", WGT_SIG_BOOL, (void *)wgtTextAreaSetShowLineNumbers }, + { "SetSyntaxColor", WGT_SIG_INT_INT, (void *)wgtTextAreaSetSyntaxColor }, + { "SetSyntaxMode", WGT_SIG_STR, (void *)wgtTextAreaSetSyntaxMode }, + { "SetTabWidth", WGT_SIG_INT, (void *)wgtTextAreaSetTabWidth }, + { "SetUseTabChar", WGT_SIG_BOOL, (void *)wgtTextAreaSetUseTabChar }, + { "Undo", WGT_SIG_VOID, (void *)wgtTextAreaUndo }, +}; + +// GutterClick is fired with wgtFireEvent after the gutter-click callback. +static const WgtEventDescT sTextAreaEvents[] = { + { TEXTAREA_EVT_GUTTERCLICK, "LineNumber As Integer" }, + { TEXTAREA_EVT_COLORIZE, "LineText As String, Colors As String" } +}; + +// TextBox BASIC interface +static const WgtPropDescT sTextInputProps[] = { + { "Mask", WGT_IFACE_STRING, (void *)wgtTextInputGetMask, (void *)wgtTextInputSetMask, NULL }, + { "MaxLength", WGT_IFACE_INT, (void *)wgtTextInputGetMaxLength, (void *)wgtTextInputSetMaxLength, NULL }, + { "PasswordChar", WGT_IFACE_STRING, (void *)wgtTextInputGetPasswordChar, (void *)wgtTextInputSetPasswordChar, NULL }, + { "SelLength", WGT_IFACE_INT, (void *)wgtTextInputGetSelLength, (void *)wgtTextInputSetSelLength, NULL }, + { "SelStart", WGT_IFACE_INT, (void *)wgtTextInputGetSelStart, (void *)wgtTextInputSetSelStart, NULL }, + { "SelText", WGT_IFACE_STRING, (void *)wgtTextInputGetSelText, (void *)wgtTextInputSetSelText, NULL } +}; + +static const WgtMethodDescT sTextInputMethods[] = { + { "Copy", WGT_SIG_VOID, (void *)wgtTextInputCopy }, + { "Cut", WGT_SIG_VOID, (void *)wgtTextInputCut }, + { "Paste", WGT_SIG_VOID, (void *)wgtTextInputPaste }, + { "SelectAll", WGT_SIG_VOID, (void *)wgtTextInputSelectAll }, + { "Undo", WGT_SIG_VOID, (void *)wgtTextInputUndo } }; static const WgtIfaceT sIfaceTextInput = { .basName = "TextBox", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, + .props = sTextInputProps, + .propCount = DVX_ARRAY_LEN(sTextInputProps), + .methods = sTextInputMethods, + .methodCount = DVX_ARRAY_LEN(sTextInputMethods), .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, @@ -1843,8 +2622,8 @@ static const WgtIfaceT sIfaceTextArea = { .propCount = DVX_ARRAY_LEN(sTextAreaProps), .methods = sTextAreaMethods, .methodCount = DVX_ARRAY_LEN(sTextAreaMethods), - .events = NULL, - .eventCount = 0, + .events = sTextAreaEvents, + .eventCount = DVX_ARRAY_LEN(sTextAreaEvents), .createSig = WGT_CREATE_PARENT_INT, .createArgs = { TEXTAREA_DEFAULT_BUF }, .defaultEvent = "Change" @@ -1860,7 +2639,9 @@ void wgtRegister(void) { // Per-type APIs and ifaces (for designer/toolbox) wgtRegisterApi("textbox", &sTextInputApi); wgtRegisterIface("textbox", &sIfaceTextInput); + wgtIfaceSetType("textbox", sTextInputTypeId); wgtRegisterApi("textarea", &sTextAreaApi); wgtRegisterIface("textarea", &sIfaceTextArea); + wgtIfaceSetType("textarea", sTextAreaTypeId); } diff --git a/src/widgets/kpunch/timer/widgetTimer.c b/src/widgets/kpunch/timer/widgetTimer.c index c0be4af2..753384d0 100644 --- a/src/widgets/kpunch/timer/widgetTimer.c +++ b/src/widgets/kpunch/timer/widgetTimer.c @@ -371,7 +371,7 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtEventDescT sEvents[] = { - { "Timer" } + { "Timer", NULL } }; static const WgtIfaceT sIface = { @@ -391,4 +391,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassTimer); wgtRegisterApi("timer", &sApi); wgtRegisterIface("timer", &sIface); + wgtIfaceSetType("timer", sTypeId); } diff --git a/src/widgets/kpunch/toolbar/widgetToolbar.c b/src/widgets/kpunch/toolbar/widgetToolbar.c index 88d2abe8..b9ecdcc2 100644 --- a/src/widgets/kpunch/toolbar/widgetToolbar.c +++ b/src/widgets/kpunch/toolbar/widgetToolbar.c @@ -137,4 +137,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassToolbar); wgtRegisterApi("toolbar", &sApi); wgtRegisterIface("toolbar", &sIface); + wgtIfaceSetType("toolbar", sTypeId); } diff --git a/src/widgets/kpunch/treeView/treeview.bhs b/src/widgets/kpunch/treeView/treeview.bhs index 084edfd5..fbc41e8a 100644 --- a/src/widgets/kpunch/treeView/treeview.bhs +++ b/src/widgets/kpunch/treeView/treeview.bhs @@ -26,12 +26,23 @@ .index TreeView .index AddItem .index AddChildItem +.index SetItemTag +.index GetItemTag +.index HasChildren .h1 TreeView VB Equivalent: TreeView -- DVX Widget: treeview -A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based). +A hierarchical tree of expandable/collapsible nodes. Supports multi-select and drag-to-reorder. Items are accessed by depth-first index (0-based): an item's children follow it, before its next sibling. Adding children under an item that has none puts them right after it (index + 1, index + 2, ...), and moves the items after them along. + +.h2 Type-Specific Properties + +.table + Property Type R/W Description + --------- ------- --- ------------------------------------------- + ListIndex Integer R/W Index of the current (selected) node, -1 when none. Setting it selects that node as a click would: its parents are expanded, it is scrolled into view, and the Change event fires. +.endtable .h2 Type-Specific Methods @@ -41,16 +52,21 @@ A hierarchical tree of expandable/collapsible nodes. Supports multi-select and d AddChildItem parentIndex%, text$ Add a child node under the node at the given index. AddItem text$ Add a root-level node. Clear Remove all nodes. + GetItemTag$(index%) Returns the text SetItemTag stored on the node ("" when none). GetItemText$(index%) Returns the text of the node at the given index. + HasChildren(index%) Returns True if the node has nodes under it. IsExpanded(index%) Returns True if the node at the given index is expanded. IsItemSelected(index%) Returns True if the node at the given index is selected. ItemCount() Returns the total number of nodes (all levels). SetExpanded index%, expanded Expand or collapse the node at the given index. Collapsing a node that hides the current item moves the current item to that node. SetItemSelected index%, selected Select or deselect the node at the given index. Without multi-select this sets (or clears) the single selected node. + SetItemTag index%, tag$ Keep any text with the node (like VB's Node.Tag): a file path, a key. "" removes it. SetMultiSelect multi Enable or disable multi-select mode. Turning it off clears the multi-selection. SetReorderable reorderable Enable or disable node reordering. .endtable +The Change event fires when the current node changes (a click, the arrow keys, or ListIndex set by the program). + Default Event: Click .h2 Example diff --git a/src/widgets/kpunch/treeView/widgetTreeView.c b/src/widgets/kpunch/treeView/widgetTreeView.c index a1a05b74..ed38d489 100644 --- a/src/widgets/kpunch/treeView/widgetTreeView.c +++ b/src/widgets/kpunch/treeView/widgetTreeView.c @@ -119,6 +119,7 @@ typedef struct { typedef struct { const char *text; + char *tag; // BASIC SetItemTag text (NULL = none) WidgetT *treeView; // owning treeView, cached at creation for destroy cleanup bool expanded; bool selected; @@ -131,12 +132,17 @@ typedef struct { static void basAddChildItem(WidgetT *w, int32_t parentIdx, const char *text); static void basAddItem(WidgetT *w, const char *text); static void basClear(WidgetT *w); +static const char *basGetItemTag(const WidgetT *w, int32_t idx); static const char *basGetItemText(const WidgetT *w, int32_t idx); +static int32_t basGetListIndex(const WidgetT *w); +static bool basHasChildren(const WidgetT *w, int32_t idx); static bool basIsExpanded(const WidgetT *w, int32_t idx); static bool basIsItemSelected(const WidgetT *w, int32_t idx); static int32_t basItemCount(const WidgetT *w); static void basSetExpanded(WidgetT *w, int32_t idx, bool expanded); static void basSetItemSelected(WidgetT *w, int32_t idx, bool selected); +static void basSetItemTag(WidgetT *w, int32_t idx, const char *tag); +static void basSetListIndex(WidgetT *w, int32_t idx); static void basTreeCollect(WidgetT *parent, WidgetT **items, int32_t *count); static int32_t basTreeCountItems(WidgetT *parent); static bool basTreeIndexEnsure(WidgetT *tree); @@ -191,6 +197,7 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT * static bool widgetTreeViewReorderDrop(WidgetT *w); static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +void wgtRegister(void); // AddChildItem index%, text$ -- add child under item at depth-first index @@ -234,6 +241,20 @@ static void basClear(WidgetT *w) { } +// GetItemTag$(index%) -- the text SetItemTag stored on the item ("" when none) +static const char *basGetItemTag(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + + if (!item) { + return ""; + } + + const TreeItemDataT *ti = (const TreeItemDataT *)item->data; + + return ti->tag ? ti->tag : ""; +} + + // GetItemText$(index%) -- get item text at index static const char *basGetItemText(const WidgetT *w, int32_t idx) { WidgetT *item = basTreeItemAt((WidgetT *)w, idx); @@ -242,6 +263,32 @@ static const char *basGetItemText(const WidgetT *w, int32_t idx) { } +// ListIndex -- the index of the current (selected) item, -1 when none +static int32_t basGetListIndex(const WidgetT *w) { + const TreeViewDataT *tv = (const TreeViewDataT *)w->data; + + if (!tv->selectedItem || !basTreeIndexEnsure((WidgetT *)w)) { + return -1; + } + + for (int32_t i = 0; i < tv->indexCount; i++) { + if (tv->indexItems[i] == tv->selectedItem) { + return i; + } + } + + return -1; +} + + +// HasChildren(index%) -- True when the item at index has items under it +static bool basHasChildren(const WidgetT *w, int32_t idx) { + WidgetT *item = basTreeItemAt((WidgetT *)w, idx); + + return item && treeItemHasChildren(item); +} + + // IsExpanded(index%) -- check if item at index is expanded static bool basIsExpanded(const WidgetT *w, int32_t idx) { WidgetT *item = basTreeItemAt((WidgetT *)w, idx); @@ -284,6 +331,29 @@ static void basSetItemSelected(WidgetT *w, int32_t idx, bool selected) { } +// SetItemTag index%, tag$ -- keep any text with the item (a path, a key); +// "" removes it +static void basSetItemTag(WidgetT *w, int32_t idx, const char *tag) { + WidgetT *item = basTreeItemAt(w, idx); + + if (!item) { + return; + } + + TreeItemDataT *ti = (TreeItemDataT *)item->data; + + free(ti->tag); + ti->tag = (tag && tag[0]) ? strdup(tag) : NULL; +} + + +// ListIndex = index% -- make the item at index the current one, as a click +// would (the tree fires Change); -1 or an index past the end clears it +static void basSetListIndex(WidgetT *w, int32_t idx) { + wgtTreeViewSetSelected(w, basTreeItemAt(w, idx)); +} + + // Appends parent's tree items to items[] in depth-first order. static void basTreeCollect(WidgetT *parent, WidgetT **items, int32_t *count) { for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { @@ -1232,6 +1302,7 @@ static void widgetTreeItemDestroy(WidgetT *w) { } free((void *)ti->text); + free(ti->tag); free(w->data); w->data = NULL; } @@ -1896,20 +1967,27 @@ static const WgtMethodDescT sMethods[] = { { "AddChildItem", WGT_SIG_INT_STR, (void *)basAddChildItem }, { "AddItem", WGT_SIG_STR, (void *)basAddItem }, { "Clear", WGT_SIG_VOID, (void *)basClear }, + { "GetItemTag", WGT_SIG_RET_STR_INT, (void *)basGetItemTag }, { "GetItemText", WGT_SIG_RET_STR_INT, (void *)basGetItemText }, + { "HasChildren", WGT_SIG_RET_BOOL_INT, (void *)basHasChildren }, { "IsExpanded", WGT_SIG_RET_BOOL_INT, (void *)basIsExpanded }, { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)basIsItemSelected }, { "ItemCount", WGT_SIG_RET_INT, (void *)basItemCount }, { "SetExpanded", WGT_SIG_INT_BOOL, (void *)basSetExpanded }, { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)basSetItemSelected }, + { "SetItemTag", WGT_SIG_INT_STR, (void *)basSetItemTag }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtTreeViewSetMultiSelect }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtTreeViewSetReorderable }, }; +static const WgtPropDescT sProps[] = { + { "ListIndex", WGT_IFACE_INT, (void *)basGetListIndex, (void *)basSetListIndex, NULL } +}; + static const WgtIfaceT sIface = { .basName = "TreeView", - .props = NULL, - .propCount = 0, + .props = sProps, + .propCount = DVX_ARRAY_LEN(sProps), .methods = sMethods, .methodCount = DVX_ARRAY_LEN(sMethods), .events = NULL, @@ -1923,4 +2001,5 @@ void wgtRegister(void) { sTreeItemTypeId = wgtRegisterClass(&sClassTreeItem); wgtRegisterApi("treeview", &sApi); wgtRegisterIface("treeview", &sIface); + wgtIfaceSetType("treeview", sTreeViewTypeId); } diff --git a/src/widgets/kpunch/wgtsys.dhs b/src/widgets/kpunch/wgtsys.dhs index 42164db3..e9676c10 100644 --- a/src/widgets/kpunch/wgtsys.dhs +++ b/src/widgets/kpunch/wgtsys.dhs @@ -106,8 +106,31 @@ These callback function pointers are available on every WidgetT. Set them direct onMouseMove void (*)(WidgetT *w, int32_t button, int32_t x, int32_t y) Fires on mouse movement over the widget. onScroll void (*)(WidgetT *w, int32_t delta) Fires on mouse wheel scroll. onValidate bool (*)(WidgetT *w) Validation callback. Return false to cancel a pending write. + onEvent void (*)(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) A named event the widget fires that has no callback above (ListView ColumnClick, TextArea GutterClick, HelpView LinkClick, an event a BASIC control raises). args may be written back. .endtable +wgtFireEvent(w, name, args, argc) fires an event by name: the names above (Click, DblClick, Change, GotFocus, LostFocus, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, Scroll) go to their callback, every other name to onEvent (see dvxWgt.h, Custom Events). + +.h3 Generic Access + +.index wgtCreateByName +.index wgtGetProp +.index wgtSetProp +.index wgtCallMethod + +Every widget type can be created, read, written and called by name through its interface descriptor, with values as WgtValueT -- the way the BASIC form runtime and the designer drive widgets, and the way a C program uses a widget it has no header for (a BASIC control, for one): + +.code +WidgetT *list = wgtCreateByName("ListBox", parent); +WgtValueT item = wgtValueStr("first"); +WgtValueT count; + +wgtCallMethod(list, "AddItem", &item, 1, NULL); +wgtCallMethod(list, "ListCount", NULL, 0, &count); // count.i == 1 +.endcode + +wgtGetProp / wgtSetProp work the same way. See dvxWgt.h, Generic Widget Access. + .h3 Common Operations .index wgtInitWindow diff --git a/src/widgets/kpunch/wrapBox/widgetWrapBox.c b/src/widgets/kpunch/wrapBox/widgetWrapBox.c index 5e1e8351..9d5daff8 100644 --- a/src/widgets/kpunch/wrapBox/widgetWrapBox.c +++ b/src/widgets/kpunch/wrapBox/widgetWrapBox.c @@ -327,4 +327,5 @@ void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClass); wgtRegisterApi("wrapbox", &sApi); wgtRegisterIface("wrapbox", &sIface); + wgtIfaceSetType("wrapbox", sTypeId); }