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
- Sound Library
+- BASIC Development Library
- BASIC Runtime Library
+
+- Connection Library
+
-- Connection Library
- Networking Library
- dvxSql -- SQL Database Interface
@@ -141,9 +150,18 @@ img { max-width: 100%; }
- File Operations Library
- Game Helpers Library
+- Help File Library
+
+
- Joystick Library
- libtasks -- Cooperative Task Switching
-- List Helper Library
+- List Helper Library
+
+
- Music Library
- Printing Library
- Serial Stack
@@ -208,6 +226,8 @@ img { max-width: 100%; }
- 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
@@ -229,6 +249,7 @@ img { max-width: 100%; }
- DataCtrl
- DbGrid
- Dropdown
+- HelpView
- ImageButton
- Display Widgets
- Image
@@ -261,6 +282,7 @@ img { max-width: 100%; }
Index
- .dep
+- .hlp file format
- /RUN=
- /SAFE
- 8-bit desktop
@@ -293,8 +315,10 @@ img { max-width: 100%; }
- audio
- audio.h
- audioClose
+- audioDeskFile
- audioDeskPlay
- audioDeskPlayFile
+- audioDeskSetFile
- audioDriverNames
- AudioDriverT
- audioDrv.h
@@ -304,14 +328,17 @@ img { max-width: 100%; }
- audioGetInfo
- AudioInfoT
- audioMidiAcquire
+- audioMidiChoices
- audioNotesAdd
- audioNotesOpen
- AudioNotesT
- audioOpen
+- audioPercentToVolume
- audioPlay
- audioPlayFile
- audioPump
- audioSampleLoad
+- audioSettingsStore
- audioStreamOpen
- audioStreamWrite
- AudioSynthDriverT
@@ -321,16 +348,34 @@ img { max-width: 100%; }
- basArrayNew
- BasArrayT
- BasArrayT
+- basBuildAddSource
+- basBuildCompile
+- basBuildMapFileLine
- bascomp
+- basCtlLoadFile
+- basDbgEval
+- basDbgExec
+- basDbgLocals
+- basdev
+- basFormRtTermBind
+- basHelpKeywordTopic
+- BASIC control file
+- BASIC Development Library
- BASIC Runtime
- basicgame template
- BasModuleT
- BasModuleT
+- basProject.h
+- basRunHostCreate
+- basRunHostSlice
+- basSourceRename
+- basSourceTokens
- basStringNew
- BasStringT
- BasStringT
- BasUdtT
- basValCopy
+- basValidateScanFrm
- basValRelease
- BasValueT
- BasValueT
@@ -358,6 +403,9 @@ img { max-width: 100%; }
- Button
- Buttons
- Canvas
+- CANVAS_BORDER_FIXED
+- CANVAS_BORDER_NONE
+- CANVAS_BORDER_WIDTH
- Cascade
- Check Boxes
- Checkbox
@@ -377,14 +425,20 @@ img { max-width: 100%; }
- ComboBox
- Common Events
- Common Properties
+- compiler.bas
- Compositing
- configDir
- conn
+- conn.bas
+- ConnAttach
- ConnConfigT
+- ConnCreate
- connect
- Connection
+- ConnPoll
- connSetWindowSize
- contentBuf
+- Control project
- Controls
- Cooperative
- Cooperative multitasking
@@ -428,6 +482,7 @@ img { max-width: 100%; }
- dbGridSetColumnHeader
- dbGridSetColumnVisible
- dbGridSetColumnWidth
+- debug.bas
- Debugging
- Desktop
- Desktop Picture
@@ -462,10 +517,13 @@ img { max-width: 100%; }
- drawBevel
- drawChar
- drawFocusRect
+- drawFocusRectDots
- drawHLine
- drawInit
- drawMaskedBitmap
+- drawScrollbar
- drawScrollbarArrow
+- drawScrollbarArrowButton
- drawTermRow
- drawText
- drawTextAccel
@@ -492,6 +550,8 @@ img { max-width: 100%; }
- DVX_DRIVER_HDR
- DVX_HOTKEY_NONE
- DVX_HUNG_APP_MS
+- dvxAccelFormat
+- dvxAccelParse
- dvxAddAccel
- dvxApp
- dvxApp.h
@@ -510,6 +570,8 @@ img { max-width: 100%; }
- dvxColorDialog
- dvxColorLabel
- dvxColorName
+- dvxColorsFromPrefs
+- dvxColorsToPrefs
- dvxComp
- dvxComp.h
- dvxContentDisplay
@@ -588,6 +650,8 @@ img { max-width: 100%; }
- dvxMemResetApp
- dvxMessageBox
- dvxMinimizeWindow
+- dvxMouseFromPrefs
+- dvxMouseToPrefs
- dvxnet
- dvxNet.h
- dvxPal.h
@@ -596,11 +660,13 @@ img { max-width: 100%; }
- dvxPlaySound
- dvxPlaySoundWait
- dvxPrefs.h
+- dvxPrefsBas.c
- dvxPromptSave
- dvxQuit
- dvxRaiseWindow
- dvxRealloc
- dvxRegisterHotkey
+- dvxRemoveAccel
- dvxRequestWindowClose
- dvxRes.h
- dvxResAppend
@@ -612,6 +678,7 @@ img { max-width: 100%; }
- dvxResizeWindow
- dvxResLoadData
- dvxResLoadIcon
+- dvxResLoadIconData
- dvxResLoadText
- dvxResOpen
- dvxResRead
@@ -662,6 +729,8 @@ img { max-width: 100%; }
- dvxUpdate
- dvxVideo
- dvxVideo.h
+- dvxWallpaperFromPrefs
+- dvxWallpaperToPrefs
- dvxWgt.h
- dvxWgtP.h
- dvxWindowScreenshot
@@ -690,12 +759,14 @@ img { max-width: 100%; }
- File Dialog
- File Transfer
- fileops
+- fileops.bas
- fileOpsJobClearAttr
- fileOpsJobCreate
- fileOpsJobRun
- fileOpsJobStep
- FileOpsJobT
- fileOpsListDir
+- fileOpsParseDate
- Files
- Flexbox
- flushRect
@@ -706,8 +777,16 @@ img { max-width: 100%; }
- Frame
- Frame Pacing
- Framebuffer
+- FrameMissed
- FRAMEWAIT
- FRAMEWAIT
+- FrmCtrlT
+- FrmMenuItemT
+- frmModelFromForm
+- frmModelLoad
+- FrmModelT
+- frmModelText
+- frmModelWrite
- Full screen
- Full screen
- Full-screen exclusive mode
@@ -750,11 +829,28 @@ img { max-width: 100%; }
- Held keys
- Help
- Help Menu
+- HelpView
+- HelpViewLinkFnT
- Hit Testing
- HKDF
+- hlpfile
+- hlpfile.bas
+- hlpFileForEachRecord
+- hlpFileOpen
+- hlpFileReadImage
+- hlpFileSearch
+- hlpFileStartTopic
+- HlpFileT
+- hlpFormat.h
+- HlpOpen
+- hlpWrapText
- HMAC
- Hot Keys
+- HotkeyAdd
+- HotkeyNext
+- HotkeyRemove
- Hotkeys
+- HotkeyText
- Image
- ImageButton
- include/net
@@ -777,6 +873,10 @@ img { max-width: 100%; }
- joyBasStick
- joyButtonPressed
- joyButtonWentDown
+- JoyChoiceE
+- joyChoiceGet
+- joyChoices
+- joyChoiceSet
- joyClose
- joyCount
- joyDeadZoneApply
@@ -787,12 +887,16 @@ img { max-width: 100%; }
- joyGetInfo
- joyGetRaw
- JoyInfoT
+- joyKbdSlotGet
+- joyKbdSlotSet
+- joyKeyIndex
- joyOpen
- joyOpenShared
- joyPump
- JoyRawT
- joyRead
- joyRescan
+- joyRescanApp
- joySetCalibration
- joySetDeadZone
- joySetEventFn
@@ -822,9 +926,14 @@ img { max-width: 100%; }
- List Boxes
- ListBox
- listhelp
+- listhelp.bas
+- listIconGrid
+- ListIconGridT
+- listIconLabelSplit
- ListItemsT
- ListPopupT
- listReorderMove
+- ListSelCreate
- ListSelT
- ListView
- ListViewColT
@@ -898,6 +1007,7 @@ img { max-width: 100%; }
- OK and Cancel
- onChange
- onClick
+- onEvent
- onKeyDown
- onKeyPress
- onMouseDown
@@ -975,12 +1085,21 @@ img { max-width: 100%; }
- platformVideoInit
- PlayTone
- PlayWave
+- pmHotkeyParse
+- pmInstalledScan
+- pmItemGetField
+- pmModel.h
+- pmModelLoad
+- pmModelSave
+- PmModelT
- poll
- Polling
- Porting
- Preferences
- Preferences
+- prefs.bas
- prefsAccelChoices
+- prefsBasHandle
- prefsChoiceIndex
- prefsChoiceIndexOfValue
- prefsChoiceLabels
@@ -1014,7 +1133,10 @@ img { max-width: 100%; }
- printRasterJob
- printTextJob
- printUiText
+- prjLoad
+- PrjStateT
- Problems
+- PROGMAN.INI
- Program Groups
- Program Help
- Program Manager
@@ -1038,6 +1160,7 @@ img { max-width: 100%; }
- Reserved keys
- resetClipRect
- Resizing Windows
+- Resource-only widget
- Resources
- Resources
- Resources
@@ -1053,6 +1176,7 @@ img { max-width: 100%; }
- rs232Write
- Run
- Run dialog
+- Run Minimized
- Running Programs
- S3M
- Safe Video Mode
@@ -1088,6 +1212,7 @@ img { max-width: 100%; }
- secRngBytes
- secRngLoadSeed
- Security
+- security.bas
- select
- Selection
- Selection Highlight
@@ -1099,61 +1224,121 @@ img { max-width: 100%; }
- Setup
- SHA-256
- Shared Library
+- shell.bas
- SHELL_APP_DESCRIPTOR
- SHELL_APP_DESCRIPTOR
- SHELL_APP_NAME_MAX
- SHELL_DESKTOP_APP
- SHELL_INI_WELCOME
- SHELL_STACK_DEFAULT
+- shellActivateApp
- shellApp.h
- ShellAppCleanupFnT
- shellAppCodeForgetTask
- shellAppFindByPath
+- ShellAppId
- shellAppInit
+- ShellAppIsHung
+- ShellAppMemory
+- ShellAppPath
- shellAppSlotCount
- ShellAppT
+- ShellArg
+- ShellArgCount
- shellAssocCommand
+- ShellAssocCommand
- shellAssocCount
+- ShellAssocCount
+- ShellAssocExt
- shellAssocGet
- shellAssocLookup
- shellAssocNormalize
+- ShellAssocNormalize
- shellAssocOpenDocument
+- ShellAssocProgram
- shellAssocRemove
+- ShellAssocRemove
- shellAssocSet
+- ShellAssocSet
+- ShellAssocValue
+- ShellChangeCount
- shellCloseAllApps
+- shellCloseApp
+- ShellCommandArgs
+- ShellCommandProgram
+- ShellConfigFile
- shellConfigPath
- shellCtrlEscFn
+- shellDesktopChangeCount
- shellDesktopUpdate
- shellEnsureConfigDir
+- ShellExitDvx
- shellForceKillApp
- shellGetApp
- shellGetSystemInfo
+- ShellGroupsLoad
- shellInfoInit
+- ShellInstanceCount
- shellIsProgram
+- ShellIsProgram
+- shellItemIcon
+- shellItemIconFile
- shellLoadApp
- shellLoadAppWithArgs
+- ShellLog
+- shellMinimizeApp
+- ShellMinimizeApp
+- ShellOpenDocument
- shellOpenHelp
+- shellOpenTaskManager
- shellPathsMatch
+- ShellPriority
+- ShellProgramCount
+- ShellProgramDescription
+- ShellProgramFree
- shellProgramIcon
- shellProgramInfo
- shellProgramListFree
+- ShellProgramName
+- ShellProgramPath
- shellProgramScan
+- ShellProgramScan
- ShellProgramT
- shellReapApp
- shellReapApps
- shellRegisterAppCleanup
- shellRegisterDesktopUpdate
- shellRegisterIdle
+- shellRequestAppClose
- shellRequestExit
- shellResolveArgs
+- ShellRun
+- shellRunCloseRequests
- shellRunCommand
+- ShellRunCommand
- shellRunDialog
+- ShellRunDialog
- shellRunExitRequest
+- ShellRunningActivate
- shellRunningAppCount
+- shellRunningAppId
+- ShellRunningClose
+- ShellRunningCount
+- ShellRunningFind
+- ShellRunningId
+- ShellRunningKill
+- ShellRunningName
+- ShellRunningPath
- shellRunProgram
+- ShellRunProgram
+- ShellSetDesktop
+- ShellSetPriority
+- ShellSetWelcome
- shellSplitCommand
+- ShellSystemInfo
- shellTaskDepthForget
- shellTaskDepthShare
+- ShellTaskManager
- shellTaskMgrOpen
- shellTaskMgrRefresh
- shellTerminateAllApps
@@ -1172,6 +1357,7 @@ img { max-width: 100%; }
- sound.bas
- SoundInfo$
- Sounds
+- SoundSettingsStore
- Spacer
- Spinner
- Splitter
@@ -1190,7 +1376,30 @@ img { max-width: 100%; }
- Submenus
- Switch To
- Switching Programs
+- SYS_VIDEO_DEFAULT_WIDTH
+- SysColorId
+- SysColorsStore
+- SysGetColor
+- SysLoadTheme
+- SysMemoryFreeKb
+- SysMemoryTotalKb
+- SysMouseButtons
+- SysMouseStore
+- SysMouseX
+- SysMouseY
+- SysSaveTheme
+- SysScreenshot
+- SysSendKey
+- SysSetColor
+- SysSetMouse
+- SysSetMousePos
+- SysSetVideoMode
+- SysSetWallpaper
+- SysShiftState
- System Menu
+- system.bas
+- SysVideoModeCount
+- SysWallpaperStore
- TabControl
- TabPage
- Tabs
@@ -1210,6 +1419,7 @@ img { max-width: 100%; }
- Text Rendering
- Text Rendering
- TextArea
+- TEXTAREA_MARKER_NONE
- textEditCopyText
- textEditDefaultSyntaxRgb
- textEditKeyEdits
@@ -1219,6 +1429,9 @@ img { max-width: 100%; }
- textEditSingleClearSelection
- textEditSingleFree
- textEditSingleInit
+- textEditSingleReplaceSel
+- textEditSingleResize
+- textEditSingleSelRange
- textEditSingleSetText
- TextEditSingleT
- textEditUndoSwap
@@ -1246,6 +1459,8 @@ img { max-width: 100%; }
- Uninstalling DVX
- Upgrading DVX
- User Guide
+- UserControl (SDK)
+- Using BASIC Controls from C
- VBE
- VBox
- VENDOR
@@ -1258,10 +1473,17 @@ img { max-width: 100%; }
- Volume
- VT100
- Wallpaper
+- WCLASS_RAISES_EVENTS
- Welcome
- Welcome Program
- WGT_API_HEADER
- WGT_API_HEADER
+- WGT_DYN_ACCESS
+- WGT_IFACE_DOUBLE
+- WGT_METHOD_ON_FOCUS
+- WGT_METHOD_ON_KEY_UP
+- WGT_METHOD_ON_MOUSE_EVENT
+- WGT_SIG_DYNAMIC
- wgtAnsiTerm
- wgtAnsiTermClear
- wgtAnsiTermCopy
@@ -1275,6 +1497,8 @@ img { max-width: 100%; }
- WgtApiPrefixT
- wgtBasicColor
- wgtButton
+- wgtCallMethod
+- wgtCallMethod
- wgtCanvas
- wgtCanvasClear
- wgtCanvasDrawLine
@@ -1282,10 +1506,13 @@ img { max-width: 100%; }
- wgtCanvasDrawText
- wgtCanvasFillCircle
- wgtCanvasFillRect
+- wgtCanvasGetBorderStyle
+- wgtCanvasGetDisplay
- wgtCanvasGetPixel
- wgtCanvasLoad
- wgtCanvasResize
- wgtCanvasSave
+- wgtCanvasSetBorderStyle
- wgtCanvasSetMouseCallback
- wgtCanvasSetPenColor
- wgtCanvasSetPenSize
@@ -1298,10 +1525,14 @@ img { max-width: 100%; }
- wgtComboBox
- wgtComboBoxAddItem
- wgtComboBoxClear
+- wgtComboBoxGetMaxLength
- wgtComboBoxGetSelected
- wgtComboBoxRemoveItem
- wgtComboBoxSetItems
+- wgtComboBoxSetMaxLength
- wgtComboBoxSetSelected
+- wgtCreateByName
+- wgtCreateByName
- wgtDataCtrl
- wgtDbGrid
- wgtDestroy
@@ -1313,19 +1544,42 @@ img { max-width: 100%; }
- wgtDropdownRemoveItem
- wgtDropdownSetItems
- wgtDropdownSetSelected
+- WgtDynIfaceT
- wgtFind
- wgtFind
- wgtFindByBasName
+- wgtFireEvent
- wgtFrame
- wgtGetApi
- wgtGetContext
- wgtGetContext
+- wgtGetDebugLayout
- wgtGetFocused
- wgtGetIface
+- wgtGetIfaceOf
+- wgtGetPaddingPx
+- wgtGetProp
+- wgtGetProp
+- wgtGetSpacingPx
- wgtGetText
- wgtGetText
- wgtHBox
+- wgtHelpView
+- wgtHelpViewClear
+- wgtHelpViewGetLinkTarget
+- wgtHelpViewGetTopic
+- wgtHelpViewSetLinkCallback
+- wgtHelpViewShowTopic
- wgtHSeparator
+- wgtIfaceCallMethod
+- wgtIfaceFindEvent
+- wgtIfaceFindMethod
+- wgtIfaceFindProp
+- wgtIfaceGetProp
+- wgtIfaceGetPropText
+- wgtIfaceSetProp
+- wgtIfaceSetPropText
+- wgtIfaceSetType
- WgtIfaceT
- wgtImage
- wgtImageButton
@@ -1368,7 +1622,9 @@ img { max-width: 100%; }
- wgtListViewSetSelected
- wgtListViewSetSort
- wgtMaskedInput
+- WgtMouseActionE
- wgtPaint
+- wgtParseBool
- wgtPasswordInput
- wgtPercent
- wgtPercent
@@ -1376,7 +1632,9 @@ img { max-width: 100%; }
- wgtPixels
- wgtProgressBar
- wgtProgressBarGetValue
+- wgtProgressBarGetVertical
- wgtProgressBarSetValue
+- wgtProgressBarSetVertical
- wgtProgressBarV
- wgtRadio
- wgtRadioGetIndex
@@ -1390,9 +1648,15 @@ img { max-width: 100%; }
- wgtRegisterClass
- wgtRegisterIface
- wgtResolveSize
+- wgtResolveTypeName
- wgtScrollPane
+- wgtScrollPaneGetScrollLeft
+- wgtScrollPaneGetScrollTop
- wgtScrollPaneScrollToChild
+- wgtScrollPaneScrollToTop
- wgtScrollPaneSetNoBorder
+- wgtScrollPaneSetScrollLeft
+- wgtScrollPaneSetScrollTop
- wgtSeparator
- wgtSetDebugLayout
- wgtSetEnabled
@@ -1400,12 +1664,18 @@ img { max-width: 100%; }
- wgtSetFocused
- wgtSetFocused
- wgtSetName
+- wgtSetPaddingPx
+- wgtSetProp
+- wgtSetProp
- wgtSetReadOnly
+- wgtSetSpacingPx
- wgtSetText
- wgtSetText
- wgtSetTooltip
- wgtSetVisible
- wgtSetVisible
+- wgtSigInfo
+- WgtSigInfoT
- wgtSlider
- wgtSliderGetValue
- wgtSliderSetValue
@@ -1423,13 +1693,16 @@ img { max-width: 100%; }
- wgtSpinnerSetValue
- wgtSplitter
- wgtSplitterGetPos
+- wgtSplitterGetVertical
- wgtSplitterSetPos
+- wgtSplitterSetVertical
- wgtStatusBar
- wgtTabControl
- wgtTabControlGetActive
- wgtTabControlSetActive
- wgtTabPage
- wgtTextArea
+- wgtTextAreaClearLineMarkers
- wgtTextAreaCursorToEnd
- wgtTextAreaFindNext
- wgtTextAreaGoToLine
@@ -1438,8 +1711,14 @@ img { max-width: 100%; }
- wgtTextAreaSetAutoIndent
- wgtTextAreaSetColorize
- wgtTextAreaSetCursor
+- wgtTextAreaSetLineMarker
- wgtTextAreaSetShowLineNumbers
+- wgtTextAreaSetSyntaxMode
- wgtTextInput
+- wgtTextInputCut
+- wgtTextInputGetSelStart
+- wgtTextInputPaste
+- wgtTextInputSetSelText
- wgtTimer
- wgtTimerIsRunning
- wgtTimerSetInterval
@@ -1458,8 +1737,19 @@ img { max-width: 100%; }
- wgtTreeViewSetSelected
- wgtUnregisterAppClasses
- wgtUnregisterClass
+- wgtUnregisterIface
- wgtUpdateCursorBlink
- wgtUpdateTimers
+- wgtValueBool
+- wgtValueDouble
+- wgtValueFloat
+- wgtValueInt
+- wgtValueStr
+- WgtValueT
+- wgtValueToBool
+- wgtValueToFloat
+- wgtValueToInt
+- wgtValueToText
- wgtVBox
- wgtVSeparator
- wgtWrapBox
@@ -1480,6 +1770,7 @@ img { max-width: 100%; }
- widgetFindByAccel
- widgetFindNextFocusable
- widgetFindPrevFocusable
+- widgetGridNavigateIndex
- widgetHitTest
- widgetIsEnabledChain
- widgetMaxItemLen
@@ -1511,6 +1802,7 @@ img { max-width: 100%; }
- wmDestroyWindow
- wmDragBegin
- wmDrawChrome
+- wmDrawScrollbarAt
- wmHitTest
- wmInit
- wmMaximize
@@ -1518,6 +1810,7 @@ img { max-width: 100%; }
- wmMinWindowSize
- wmRaiseWindow
- wmReallocContentBuf
+- wmRemoveMenu
- wmResizeBegin
- wmRestore
- wmSetFocus
@@ -1553,6 +1846,7 @@ img { max-width: 100%; }
- XTEA
- XTEA-CTR
- YMODEM
+- Your First BASIC Control
- Your First BASIC Program
- Your First DVX Program
- Your First Library
@@ -3081,7 +3375,7 @@ DVX_ABI_STAMP(mylib);
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.
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.
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.
Secure Link
@@ -8775,8 +10017,8 @@ int main(void) {
Features
- 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.
-- 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.
-- 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).
+- 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.
+- 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).
- 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).
- Status bar showing running app count plus total and used system memory.
- Single-instance: reopening the Task Manager while it is already visible raises and focuses the existing window rather than creating a new one.
@@ -8806,13 +10048,15 @@ int main(void) {
- Word boundaries. isWordChar, wordStart/wordEnd, and wordBoundaryLeft/wordBoundaryRight implement the logic for double-click word selection and Ctrl+Left/Right navigation in a uniform way.
- 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).
-
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.
Constants
Constant Value Description
-------- ----- -----------
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.
@@ -8926,10 +10170,13 @@ int main(void) {
} TextEditSingleT;
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.
@@ -9088,7 +10335,9 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
bool xferDetectFeed(d, byte) Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself.
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.
+ 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.
Files
Received names are made safe with xferSafeName and stored in the receive folder. An existing file is never replaced silently: XferOptionsT.exists decides, and with XferExistAskE the transfer pauses in XferAskingE (the other side waits) until xferAnswer. A failed or cancelled XMODEM or YMODEM download deletes its partial file. A ZMODEM download keeps it, so the same file can be continued later: the receiver continues a shorter file of the same name when the sender asks for it (resume) or when exists is XferExistResumeE.
@@ -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