Bug fixes.

This commit is contained in:
Scott Duensing 2026-04-22 20:33:49 -05:00
parent 4cdcfe6b8c
commit 60d24c8c33
41 changed files with 4802 additions and 3306 deletions

View file

@ -147,465 +147,465 @@ img { max-width: 100%; }
</ul> </ul>
<h3>Index</h3> <h3>Index</h3>
<ul> <ul>
<li><a href="#ide.overview">DVX BASIC</a></li>
<li><a href="#ide.overview">IDE</a></li>
<li><a href="#ide.overview">Visual Basic</a></li>
<li><a href="#ide.overview">Development Environment</a></li>
<li><a href="#ide.menu.file">File Menu</a></li>
<li><a href="#ide.menu.file">New Project</a></li>
<li><a href="#ide.menu.file">Open Project</a></li>
<li><a href="#ide.menu.file">Save Project</a></li>
<li><a href="#ide.menu.file">Close Project</a></li>
<li><a href="#ide.menu.file">Project Properties</a></li>
<li><a href="#ide.menu.file">Add File</a></li>
<li><a href="#ide.menu.file">Save File</a></li>
<li><a href="#ide.menu.file">Save All</a></li>
<li><a href="#ide.menu.file">Remove File</a></li>
<li><a href="#ide.menu.file">Exit</a></li>
<li><a href="#ide.menu.edit">Edit Menu</a></li>
<li><a href="#ide.menu.edit">Cut</a></li>
<li><a href="#ide.menu.edit">Copy</a></li>
<li><a href="#ide.menu.edit">Paste</a></li>
<li><a href="#ide.menu.edit">Select All</a></li>
<li><a href="#ide.menu.edit">Delete</a></li>
<li><a href="#ide.menu.edit">Find</a></li>
<li><a href="#ide.menu.edit">Find Next</a></li>
<li><a href="#ide.menu.edit">Replace</a></li>
<li><a href="#ide.menu.run">Run Menu</a></li>
<li><a href="#ide.menu.run">Run</a></li>
<li><a href="#ide.menu.run">Debug</a></li>
<li><a href="#ide.menu.run">Run Without Recompile</a></li>
<li><a href="#ide.menu.run">Stop</a></li>
<li><a href="#ide.menu.run">Step Into</a></li>
<li><a href="#ide.menu.run">Step Over</a></li>
<li><a href="#ide.menu.run">Step Out</a></li>
<li><a href="#ide.menu.run">Run to Cursor</a></li>
<li><a href="#ide.menu.run">Toggle Breakpoint</a></li>
<li><a href="#ide.menu.run">Clear Output</a></li>
<li><a href="#ide.menu.run">Save on Run</a></li>
<li><a href="#ide.menu.view">View Menu</a></li>
<li><a href="#ide.menu.view">Code View</a></li>
<li><a href="#ide.menu.view">Design View</a></li>
<li><a href="#ide.menu.view">Toolbar Toggle</a></li>
<li><a href="#ide.menu.view">Status Bar Toggle</a></li>
<li><a href="#ide.menu.view">Menu Editor</a></li>
<li><a href="#ide.menu.window">Window Menu</a></li>
<li><a href="#ide.menu.tools">Tools Menu</a></li>
<li><a href="#ide.menu.tools">Preferences</a></li>
<li><a href="#ide.menu.tools">Debug Layout</a></li>
<li><a href="#ide.menu.help">Help Menu</a></li>
<li><a href="#ide.menu.help">About</a></li>
<li><a href="#ide.toolbar">Toolbar</a></li>
<li><a href="#ide.editor">Code Editor</a></li>
<li><a href="#ide.editor">Syntax Highlighting</a></li>
<li><a href="#ide.editor">Object Dropdown</a></li>
<li><a href="#ide.editor">Function Dropdown</a></li>
<li><a href="#ide.editor">Line Numbers</a></li>
<li><a href="#ide.editor">Auto-indent</a></li>
<li><a href="#ide.designer">Form Designer</a></li>
<li><a href="#ide.designer">Design Surface</a></li>
<li><a href="#ide.designer">Grid Snapping</a></li>
<li><a href="#ide.designer">Grab Handles</a></li>
<li><a href="#ide.designer">Control Placement</a></li>
<li><a href="#ide.designer">WYSIWYG</a></li>
<li><a href="#ide.project">Project System</a></li>
<li><a href="#ide.project">Project Files</a></li>
<li><a href="#ide.project">.dbp Files</a></li> <li><a href="#ide.project">.dbp Files</a></li>
<li><a href="#ide.project">Project Explorer</a></li> <li><a href="#ctrl.frm">.frm</a></li>
<li><a href="#ide.project">Source Map</a></li> <li><a href="#ide.menu.help">About</a></li>
<li><a href="#ide.properties">Properties Panel</a></li> <li><a href="#lang.func.math">ABS</a></li>
<li><a href="#ide.properties">Control Properties</a></li>
<li><a href="#ide.properties">Property List</a></li>
<li><a href="#ide.properties">Control Tree</a></li>
<li><a href="#ide.toolbox">Toolbox</a></li>
<li><a href="#ide.toolbox">Controls</a></li>
<li><a href="#ide.toolbox">Widget Palette</a></li>
<li><a href="#ide.debugger">Debugger</a></li>
<li><a href="#ide.debugger">Breakpoints</a></li>
<li><a href="#ide.debugger">Stepping</a></li>
<li><a href="#ide.debugger">Debug Mode</a></li>
<li><a href="#ide.debug.locals">Locals Window</a></li>
<li><a href="#ide.debug.locals">Variable Inspection</a></li>
<li><a href="#ide.debug.callstack">Call Stack</a></li>
<li><a href="#ide.debug.callstack">Stack Trace</a></li>
<li><a href="#ide.debug.watch">Watch Window</a></li>
<li><a href="#ide.debug.watch">Watch Expressions</a></li>
<li><a href="#ide.debug.breakpoints">Breakpoints Window</a></li>
<li><a href="#ide.immediate">Immediate Window</a></li>
<li><a href="#ide.immediate">REPL</a></li>
<li><a href="#ide.immediate">Expression Evaluation</a></li>
<li><a href="#ide.immediate">Variable Assignment</a></li>
<li><a href="#ide.output">Output Window</a></li>
<li><a href="#ide.output">PRINT Output</a></li>
<li><a href="#ide.output">Runtime Errors</a></li>
<li><a href="#ide.output">Compile Errors</a></li>
<li><a href="#ide.findreplace">Find</a></li>
<li><a href="#ide.findreplace">Replace</a></li>
<li><a href="#ide.findreplace">Find Next</a></li>
<li><a href="#ide.findreplace">Search</a></li>
<li><a href="#ide.preferences">Preferences</a></li>
<li><a href="#ide.preferences">Settings</a></li>
<li><a href="#ide.preferences">Tab Width</a></li>
<li><a href="#ide.preferences">Option Explicit</a></li>
<li><a href="#ide.shortcuts">Keyboard Shortcuts</a></li>
<li><a href="#ide.shortcuts">Hotkeys</a></li>
<li><a href="#ide.shortcuts">Accelerators</a></li> <li><a href="#ide.shortcuts">Accelerators</a></li>
<li><a href="#lang.datatypes">Data Types</a></li> <li><a href="#ide.menu.file">Add File</a></li>
<li><a href="#lang.datatypes">Integer</a></li> <li><a href="#ctrl.treeview">AddChildItem</a></li>
<li><a href="#lang.datatypes">Long</a></li> <li><a href="#ctrl.listbox">AddItem</a></li>
<li><a href="#lang.datatypes">Single</a></li> <li><a href="#ctrl.treeview">AddItem</a></li>
<li><a href="#lang.datatypes">Double</a></li> <li><a href="#ctrl.data">AddNew</a></li>
<li><a href="#lang.datatypes">String</a></li> <li><a href="#ctrl.label">Alignment</a></li>
<li><a href="#lang.datatypes">Boolean</a></li>
<li><a href="#lang.operators">Operators</a></li>
<li><a href="#lang.operators">Precedence</a></li>
<li><a href="#lang.operators">AND</a></li> <li><a href="#lang.operators">AND</a></li>
<li><a href="#lang.operators">OR</a></li> <li><a href="#ctrl.terminal">ANSI Terminal</a></li>
<li><a href="#lang.operators">NOT</a></li> <li><a href="#lang.app">App</a></li>
<li><a href="#lang.operators">XOR</a></li> <li><a href="#lang.app">App.Config</a></li>
<li><a href="#lang.operators">EQV</a></li> <li><a href="#lang.app">App.Data</a></li>
<li><a href="#lang.operators">IMP</a></li> <li><a href="#lang.app">App.Path</a></li>
<li><a href="#lang.operators">MOD</a></li> <li><a href="#lang.func.string">ASC</a></li>
<li><a href="#lang.statements">Statements</a></li> <li><a href="#lang.func.math">ATN</a></li>
<li><a href="#lang.statements">REM</a></li> <li><a href="#ide.editor">Auto-indent</a></li>
<li><a href="#ctrl.textarea">AutoIndent</a></li>
<li><a href="#ctrl.form">AutoSize</a></li>
<li><a href="#ctrl.image">BMP</a></li>
<li><a href="#lang.datatypes">Boolean</a></li>
<li><a href="#ide.debugger">Breakpoints</a></li>
<li><a href="#ide.debug.breakpoints">Breakpoints Window</a></li>
<li><a href="#ctrl.button">Button</a></li>
<li><a href="#lang.procedures">BYVAL</a></li>
<li><a href="#lang.flow">CALL</a></li>
<li><a href="#ide.debug.callstack">Call Stack</a></li>
<li><a href="#ctrl.picturebox">Canvas</a></li>
<li><a href="#ctrl.form">Caption</a></li>
<li><a href="#ctrl.label">Caption</a></li>
<li><a href="#lang.conditionals">CASE</a></li>
<li><a href="#lang.conditionals">CASE ELSE</a></li>
<li><a href="#lang.func.conversion">CBOOL</a></li>
<li><a href="#lang.func.conversion">CDBL</a></li>
<li><a href="#lang.filesystem">CHDIR</a></li>
<li><a href="#lang.filesystem">CHDRIVE</a></li>
<li><a href="#ctrl.checkbox">CheckBox</a></li>
<li><a href="#lang.func.string">CHR$</a></li>
<li><a href="#lang.func.conversion">CINT</a></li>
<li><a href="#ide.menu.run">Clear Output</a></li>
<li><a href="#ctrl.button">Click</a></li>
<li><a href="#lang.func.conversion">CLNG</a></li>
<li><a href="#lang.fileio">CLOSE</a></li>
<li><a href="#ide.menu.file">Close Project</a></li>
<li><a href="#ide.editor">Code Editor</a></li>
<li><a href="#ide.menu.view">Code View</a></li>
<li><a href="#ctrl.combobox">ComboBox</a></li>
<li><a href="#lang.runtime">comm.bas</a></li>
<li><a href="#ctrl.button">CommandButton</a></li>
<li><a href="#lang.runtime">commdlg.bas</a></li>
<li><a href="#lang.statements">Comments</a></li> <li><a href="#lang.statements">Comments</a></li>
<li><a href="#lang.declarations">DIM</a></li> <li><a href="#ctrl.common.props">Common Events</a></li>
<li><a href="#lang.declarations">REDIM</a></li> <li><a href="#ctrl.common.props">Common Methods</a></li>
<li><a href="#ctrl.common.props">Common Properties</a></li>
<li><a href="#ide.output">Compile Errors</a></li>
<li><a href="#lang.declarations">CONST</a></li> <li><a href="#lang.declarations">CONST</a></li>
<li><a href="#lang.declarations">TYPE</a></li> <li><a href="#ctrl.frame">Container</a></li>
<li><a href="#lang.declarations">END TYPE</a></li> <li><a href="#lang.forms">Control Arrays</a></li>
<li><a href="#ctrl.arrays">Control Arrays</a></li>
<li><a href="#ide.designer">Control Placement</a></li>
<li><a href="#ide.properties">Control Properties</a></li>
<li><a href="#ide.properties">Control Tree</a></li>
<li><a href="#ide.toolbox">Controls</a></li>
<li><a href="#ide.menu.edit">Copy</a></li>
<li><a href="#lang.func.math">COS</a></li>
<li><a href="#lang.forms">CreateControl</a></li>
<li><a href="#lang.forms">CreateForm</a></li>
<li><a href="#lang.func.conversion">CSNG</a></li>
<li><a href="#lang.func.conversion">CSTR</a></li>
<li><a href="#ctrl.textarea">CursorLine</a></li>
<li><a href="#ide.menu.edit">Cut</a></li>
<li><a href="#lang.io">DATA</a></li>
<li><a href="#ctrl.data">Data</a></li>
<li><a href="#ctrl.databinding">Data Binding</a></li>
<li><a href="#lang.datatypes">Data Types</a></li>
<li><a href="#ctrl.dbgrid">Data-Bound Grid</a></li>
<li><a href="#ctrl.data">Database</a></li>
<li><a href="#ctrl.data">DatabaseName</a></li>
<li><a href="#ctrl.databinding">DataField</a></li>
<li><a href="#ctrl.textbox">DataField</a></li>
<li><a href="#ctrl.databinding">DataSource</a></li>
<li><a href="#ctrl.textbox">DataSource</a></li>
<li><a href="#lang.func.misc">DATE$</a></li>
<li><a href="#ctrl.dbgrid">DBGrid</a></li>
<li><a href="#ide.menu.run">Debug</a></li>
<li><a href="#ide.menu.tools">Debug Layout</a></li>
<li><a href="#ide.debugger">Debug Mode</a></li>
<li><a href="#ide.debugger">Debugger</a></li>
<li><a href="#ctrl.spinbutton">Decimals</a></li>
<li><a href="#lang.declarations">DECLARE</a></li> <li><a href="#lang.declarations">DECLARE</a></li>
<li><a href="#lang.declarations">DECLARE LIBRARY</a></li> <li><a href="#lang.declarations">DECLARE LIBRARY</a></li>
<li><a href="#lang.declarations">SHARED</a></li> <li><a href="#lang.runtime">DECLARE LIBRARY</a></li>
<li><a href="#lang.declarations">STATIC</a></li> <li><a href="#lang.procedures">DEF FN</a></li>
<li><a href="#lang.declarations">OPTION</a></li> <li><a href="#lang.declarations">DEFDBL</a></li>
<li><a href="#lang.declarations">OPTION BASE</a></li>
<li><a href="#lang.declarations">OPTION COMPARE</a></li>
<li><a href="#lang.declarations">OPTION EXPLICIT</a></li>
<li><a href="#lang.declarations">DEFINT</a></li> <li><a href="#lang.declarations">DEFINT</a></li>
<li><a href="#lang.declarations">DEFLNG</a></li> <li><a href="#lang.declarations">DEFLNG</a></li>
<li><a href="#lang.declarations">DEFSNG</a></li> <li><a href="#lang.declarations">DEFSNG</a></li>
<li><a href="#lang.declarations">DEFDBL</a></li>
<li><a href="#lang.declarations">DEFSTR</a></li> <li><a href="#lang.declarations">DEFSTR</a></li>
<li><a href="#lang.declarations">LET</a></li> <li><a href="#ide.menu.edit">Delete</a></li>
<li><a href="#lang.declarations">SWAP</a></li> <li><a href="#ide.designer">Design Surface</a></li>
<li><a href="#lang.declarations">ERASE</a></li> <li><a href="#ide.menu.view">Design View</a></li>
<li><a href="#lang.conditionals">IF</a></li> <li><a href="#ide.overview">Development Environment</a></li>
<li><a href="#lang.conditionals">THEN</a></li> <li><a href="#lang.declarations">DIM</a></li>
<li><a href="#lang.loops">DO</a></li>
<li><a href="#lang.forms">DoEvents</a></li>
<li><a href="#lang.forms">DOEVENTS</a></li>
<li><a href="#lang.datatypes">Double</a></li>
<li><a href="#ctrl.picturebox">Drawing</a></li>
<li><a href="#ctrl.dropdown">DropDown</a></li>
<li><a href="#ide.overview">DVX BASIC</a></li>
<li><a href="#ide.menu.edit">Edit Menu</a></li>
<li><a href="#lang.conditionals">ELSE</a></li> <li><a href="#lang.conditionals">ELSE</a></li>
<li><a href="#lang.conditionals">ELSEIF</a></li> <li><a href="#lang.conditionals">ELSEIF</a></li>
<li><a href="#lang.conditionals">END IF</a></li>
<li><a href="#lang.conditionals">SELECT CASE</a></li>
<li><a href="#lang.conditionals">CASE</a></li>
<li><a href="#lang.conditionals">CASE ELSE</a></li>
<li><a href="#lang.conditionals">END SELECT</a></li>
<li><a href="#lang.loops">FOR</a></li>
<li><a href="#lang.loops">NEXT</a></li>
<li><a href="#lang.loops">STEP</a></li>
<li><a href="#lang.loops">DO</a></li>
<li><a href="#lang.loops">LOOP</a></li>
<li><a href="#lang.loops">WHILE</a></li>
<li><a href="#lang.loops">WEND</a></li>
<li><a href="#lang.loops">UNTIL</a></li>
<li><a href="#lang.loops">EXIT FOR</a></li>
<li><a href="#lang.loops">EXIT DO</a></li>
<li><a href="#lang.procedures">SUB</a></li>
<li><a href="#lang.procedures">END SUB</a></li>
<li><a href="#lang.procedures">FUNCTION</a></li>
<li><a href="#lang.procedures">END FUNCTION</a></li>
<li><a href="#lang.procedures">DEF FN</a></li>
<li><a href="#lang.procedures">BYVAL</a></li>
<li><a href="#lang.procedures">EXIT SUB</a></li>
<li><a href="#lang.procedures">EXIT FUNCTION</a></li>
<li><a href="#lang.flow">EXIT</a></li>
<li><a href="#lang.flow">CALL</a></li>
<li><a href="#lang.flow">GOTO</a></li>
<li><a href="#lang.flow">GOSUB</a></li>
<li><a href="#lang.flow">RETURN</a></li>
<li><a href="#lang.flow">ON GOTO</a></li>
<li><a href="#lang.flow">ON GOSUB</a></li>
<li><a href="#lang.io">PRINT</a></li>
<li><a href="#lang.io">INPUT</a></li>
<li><a href="#lang.io">DATA</a></li>
<li><a href="#lang.io">READ</a></li>
<li><a href="#lang.io">RESTORE</a></li>
<li><a href="#lang.io">SPC</a></li>
<li><a href="#lang.io">TAB</a></li>
<li><a href="#lang.io">PRINT USING</a></li>
<li><a href="#lang.misc">ON ERROR</a></li>
<li><a href="#lang.misc">RESUME</a></li>
<li><a href="#lang.misc">RESUME NEXT</a></li>
<li><a href="#lang.misc">ERROR</a></li>
<li><a href="#lang.misc">ERR</a></li>
<li><a href="#lang.misc">SHELL</a></li>
<li><a href="#lang.misc">SLEEP</a></li>
<li><a href="#lang.misc">RANDOMIZE</a></li>
<li><a href="#lang.misc">END</a></li> <li><a href="#lang.misc">END</a></li>
<li><a href="#lang.misc">RANDOMIZE TIMER</a></li> <li><a href="#lang.procedures">END FUNCTION</a></li>
<li><a href="#lang.fileio">OPEN</a></li> <li><a href="#lang.conditionals">END IF</a></li>
<li><a href="#lang.fileio">CLOSE</a></li> <li><a href="#lang.conditionals">END SELECT</a></li>
<li><a href="#lang.fileio">PRINT #</a></li> <li><a href="#lang.procedures">END SUB</a></li>
<li><a href="#lang.fileio">INPUT #</a></li> <li><a href="#lang.declarations">END TYPE</a></li>
<li><a href="#lang.fileio">LINE INPUT</a></li> <li><a href="#lang.func.misc">ENVIRON$</a></li>
<li><a href="#lang.fileio">WRITE #</a></li> <li><a href="#lang.func.fileio">EOF</a></li>
<li><a href="#lang.fileio">GET</a></li> <li><a href="#lang.operators">EQV</a></li>
<li><a href="#lang.fileio">PUT</a></li> <li><a href="#lang.declarations">ERASE</a></li>
<li><a href="#lang.fileio">SEEK</a></li> <li><a href="#lang.misc">ERR</a></li>
<li><a href="#lang.misc">ERROR</a></li>
<li><a href="#lang.forms">Event Handlers</a></li>
<li><a href="#ctrl.common.props">Events</a></li>
<li><a href="#ide.menu.file">Exit</a></li>
<li><a href="#lang.flow">EXIT</a></li>
<li><a href="#lang.loops">EXIT DO</a></li>
<li><a href="#lang.loops">EXIT FOR</a></li>
<li><a href="#lang.procedures">EXIT FUNCTION</a></li>
<li><a href="#lang.procedures">EXIT SUB</a></li>
<li><a href="#lang.func.math">EXP</a></li>
<li><a href="#ide.immediate">Expression Evaluation</a></li>
<li><a href="#lang.constants">False</a></li>
<li><a href="#lang.filesystem">File Attributes</a></li>
<li><a href="#ide.menu.file">File Menu</a></li>
<li><a href="#lang.filesystem">FILECOPY</a></li>
<li><a href="#ide.menu.edit">Find</a></li>
<li><a href="#ide.findreplace">Find</a></li>
<li><a href="#ide.menu.edit">Find Next</a></li>
<li><a href="#ide.findreplace">Find Next</a></li>
<li><a href="#ctrl.textarea">FindNext</a></li>
<li><a href="#lang.func.math">FIX</a></li>
<li><a href="#ctrl.wrapbox">Flow Layout</a></li>
<li><a href="#lang.loops">FOR</a></li>
<li><a href="#lang.fileio">FOR APPEND</a></li>
<li><a href="#lang.fileio">FOR BINARY</a></li>
<li><a href="#lang.fileio">FOR INPUT</a></li> <li><a href="#lang.fileio">FOR INPUT</a></li>
<li><a href="#lang.fileio">FOR OUTPUT</a></li> <li><a href="#lang.fileio">FOR OUTPUT</a></li>
<li><a href="#lang.fileio">FOR APPEND</a></li>
<li><a href="#lang.fileio">FOR RANDOM</a></li> <li><a href="#lang.fileio">FOR RANDOM</a></li>
<li><a href="#lang.fileio">FOR BINARY</a></li> <li><a href="#ctrl.form">Form</a></li>
<li><a href="#lang.filesystem">KILL</a></li> <li><a href="#ide.designer">Form Designer</a></li>
<li><a href="#lang.filesystem">NAME</a></li> <li><a href="#ctrl.frm">Form File</a></li>
<li><a href="#lang.filesystem">FILECOPY</a></li>
<li><a href="#lang.filesystem">MKDIR</a></li>
<li><a href="#lang.filesystem">RMDIR</a></li>
<li><a href="#lang.filesystem">CHDIR</a></li>
<li><a href="#lang.filesystem">CHDRIVE</a></li>
<li><a href="#lang.filesystem">GETATTR</a></li>
<li><a href="#lang.filesystem">SETATTR</a></li>
<li><a href="#lang.filesystem">File Attributes</a></li>
<li><a href="#lang.func.string">ASC</a></li>
<li><a href="#lang.func.string">CHR$</a></li>
<li><a href="#lang.func.string">FORMAT$</a></li> <li><a href="#lang.func.string">FORMAT$</a></li>
<li><a href="#ctrl.frame">Frame</a></li>
<li><a href="#lang.func.fileio">FREEFILE</a></li>
<li><a href="#ctrl.frm">FRM</a></li>
<li><a href="#lang.procedures">FUNCTION</a></li>
<li><a href="#ide.editor">Function Dropdown</a></li>
<li><a href="#lang.fileio">GET</a></li>
<li><a href="#lang.filesystem">GETATTR</a></li>
<li><a href="#lang.flow">GOSUB</a></li>
<li><a href="#lang.flow">GOTO</a></li>
<li><a href="#ctrl.textarea">GoToLine</a></li>
<li><a href="#ide.designer">Grab Handles</a></li>
<li><a href="#ide.designer">Grid Snapping</a></li>
<li><a href="#ctrl.hbox">HBox</a></li>
<li><a href="#ide.menu.help">Help Menu</a></li>
<li><a href="#lang.runtime">help.bas</a></li>
<li><a href="#lang.func.string">HEX$</a></li> <li><a href="#lang.func.string">HEX$</a></li>
<li><a href="#lang.forms">Hide</a></li>
<li><a href="#ctrl.form">Hide</a></li>
<li><a href="#ctrl.hbox">Horizontal Layout</a></li>
<li><a href="#ide.shortcuts">Hotkeys</a></li>
<li><a href="#ctrl.hscrollbar">HScrollBar</a></li>
<li><a href="#ide.overview">IDE</a></li>
<li><a href="#lang.conditionals">IF</a></li>
<li><a href="#ctrl.image">Image</a></li>
<li><a href="#ctrl.imagebutton">ImageButton</a></li>
<li><a href="#ide.immediate">Immediate Window</a></li>
<li><a href="#lang.operators">IMP</a></li>
<li><a href="#lang.runtime">Include Libraries</a></li>
<li><a href="#ctrl.arrays">Index Property</a></li>
<li><a href="#lang.ini">INI</a></li>
<li><a href="#lang.ini">IniRead</a></li>
<li><a href="#lang.ini">IniWrite</a></li>
<li><a href="#lang.io">INPUT</a></li>
<li><a href="#lang.fileio">INPUT #</a></li>
<li><a href="#lang.func.fileio">INPUT$</a></li>
<li><a href="#lang.forms">InputBox$</a></li>
<li><a href="#lang.forms">INPUTBOX$</a></li>
<li><a href="#lang.func.string">INSTR</a></li> <li><a href="#lang.func.string">INSTR</a></li>
<li><a href="#lang.func.math">INT</a></li>
<li><a href="#lang.datatypes">Integer</a></li>
<li><a href="#ctrl.timer">Interval</a></li>
<li><a href="#ide.shortcuts">Keyboard Shortcuts</a></li>
<li><a href="#lang.filesystem">KILL</a></li>
<li><a href="#ctrl.label">Label</a></li>
<li><a href="#lang.func.fileio">LBOUND</a></li>
<li><a href="#lang.func.string">LCASE$</a></li> <li><a href="#lang.func.string">LCASE$</a></li>
<li><a href="#lang.func.string">LEFT$</a></li> <li><a href="#lang.func.string">LEFT$</a></li>
<li><a href="#lang.func.string">LEN</a></li> <li><a href="#lang.func.string">LEN</a></li>
<li><a href="#lang.func.string">LTRIM$</a></li> <li><a href="#lang.declarations">LET</a></li>
<li><a href="#lang.func.string">MID$</a></li> <li><a href="#ctrl.line">Line</a></li>
<li><a href="#lang.func.string">OCT$</a></li> <li><a href="#lang.fileio">LINE INPUT</a></li>
<li><a href="#lang.func.string">RIGHT$</a></li> <li><a href="#ide.editor">Line Numbers</a></li>
<li><a href="#lang.func.string">RTRIM$</a></li> <li><a href="#ctrl.textarea">LineNumbers</a></li>
<li><a href="#lang.func.string">SPACE$</a></li> <li><a href="#ctrl.listbox">ListBox</a></li>
<li><a href="#lang.func.string">STR$</a></li> <li><a href="#ctrl.listbox">ListCount</a></li>
<li><a href="#lang.func.string">STRING$</a></li> <li><a href="#ctrl.listbox">ListIndex</a></li>
<li><a href="#lang.func.string">TRIM$</a></li> <li><a href="#ctrl.listview">ListView</a></li>
<li><a href="#lang.func.string">UCASE$</a></li>
<li><a href="#lang.func.string">VAL</a></li>
<li><a href="#lang.func.math">ABS</a></li>
<li><a href="#lang.func.math">ATN</a></li>
<li><a href="#lang.func.math">COS</a></li>
<li><a href="#lang.func.math">EXP</a></li>
<li><a href="#lang.func.math">FIX</a></li>
<li><a href="#lang.func.math">INT</a></li>
<li><a href="#lang.func.math">LOG</a></li>
<li><a href="#lang.func.math">RND</a></li>
<li><a href="#lang.func.math">SGN</a></li>
<li><a href="#lang.func.math">SIN</a></li>
<li><a href="#lang.func.math">SQR</a></li>
<li><a href="#lang.func.math">TAN</a></li>
<li><a href="#lang.func.math">TIMER</a></li>
<li><a href="#lang.func.conversion">CBOOL</a></li>
<li><a href="#lang.func.conversion">CDBL</a></li>
<li><a href="#lang.func.conversion">CINT</a></li>
<li><a href="#lang.func.conversion">CLNG</a></li>
<li><a href="#lang.func.conversion">CSNG</a></li>
<li><a href="#lang.func.conversion">CSTR</a></li>
<li><a href="#lang.func.fileio">EOF</a></li>
<li><a href="#lang.func.fileio">FREEFILE</a></li>
<li><a href="#lang.func.fileio">INPUT$</a></li>
<li><a href="#lang.func.fileio">LOC</a></li>
<li><a href="#lang.func.fileio">LOF</a></li>
<li><a href="#lang.func.fileio">LBOUND</a></li>
<li><a href="#lang.func.fileio">UBOUND</a></li>
<li><a href="#lang.func.misc">DATE$</a></li>
<li><a href="#lang.func.misc">TIME$</a></li>
<li><a href="#lang.func.misc">ENVIRON$</a></li>
<li><a href="#lang.forms">LOAD</a></li> <li><a href="#lang.forms">LOAD</a></li>
<li><a href="#lang.forms">UNLOAD</a></li> <li><a href="#ctrl.form">Load</a></li>
<li><a href="#lang.forms">Show</a></li> <li><a href="#lang.func.fileio">LOC</a></li>
<li><a href="#lang.forms">Hide</a></li> <li><a href="#ide.debug.locals">Locals Window</a></li>
<li><a href="#lang.forms">Me</a></li> <li><a href="#lang.func.fileio">LOF</a></li>
<li><a href="#lang.forms">Nothing</a></li> <li><a href="#lang.func.math">LOG</a></li>
<li><a href="#lang.forms">DoEvents</a></li> <li><a href="#lang.datatypes">Long</a></li>
<li><a href="#lang.forms">DOEVENTS</a></li> <li><a href="#lang.loops">LOOP</a></li>
<li><a href="#lang.forms">MsgBox</a></li> <li><a href="#lang.func.string">LTRIM$</a></li>
<li><a href="#lang.forms">MSGBOX</a></li>
<li><a href="#lang.forms">InputBox$</a></li>
<li><a href="#lang.forms">INPUTBOX$</a></li>
<li><a href="#lang.forms">CreateForm</a></li>
<li><a href="#lang.forms">CreateControl</a></li>
<li><a href="#lang.forms">SetEvent</a></li>
<li><a href="#lang.forms">RemoveControl</a></li>
<li><a href="#lang.forms">With</a></li>
<li><a href="#lang.forms">vbModal</a></li>
<li><a href="#lang.forms">Control Arrays</a></li>
<li><a href="#lang.forms">Event Handlers</a></li>
<li><a href="#lang.sql">SQLOpen</a></li>
<li><a href="#lang.sql">SQLClose</a></li>
<li><a href="#lang.sql">SQLExec</a></li>
<li><a href="#lang.sql">SQLAffected</a></li>
<li><a href="#lang.sql">SQLQuery</a></li>
<li><a href="#lang.sql">SQLNext</a></li>
<li><a href="#lang.sql">SQLEof</a></li>
<li><a href="#lang.sql">SQLField$</a></li>
<li><a href="#lang.sql">SQLFieldInt</a></li>
<li><a href="#lang.sql">SQLFieldDbl</a></li>
<li><a href="#lang.sql">SQLFieldCount</a></li>
<li><a href="#lang.sql">SQLFreeResult</a></li>
<li><a href="#lang.sql">SQLError$</a></li>
<li><a href="#lang.sql">SQLite</a></li>
<li><a href="#lang.app">App</a></li>
<li><a href="#lang.app">App.Path</a></li>
<li><a href="#lang.app">App.Config</a></li>
<li><a href="#lang.app">App.Data</a></li>
<li><a href="#lang.ini">IniRead</a></li>
<li><a href="#lang.ini">IniWrite</a></li>
<li><a href="#lang.ini">INI</a></li>
<li><a href="#lang.constants">vbOKOnly</a></li>
<li><a href="#lang.constants">vbOKCancel</a></li>
<li><a href="#lang.constants">vbYesNo</a></li>
<li><a href="#lang.constants">vbYesNoCancel</a></li>
<li><a href="#lang.constants">vbRetryCancel</a></li>
<li><a href="#lang.constants">vbInformation</a></li>
<li><a href="#lang.constants">vbExclamation</a></li>
<li><a href="#lang.constants">vbCritical</a></li>
<li><a href="#lang.constants">vbQuestion</a></li>
<li><a href="#lang.constants">vbOK</a></li>
<li><a href="#lang.constants">vbCancel</a></li>
<li><a href="#lang.constants">vbYes</a></li>
<li><a href="#lang.constants">vbNo</a></li>
<li><a href="#lang.constants">vbRetry</a></li>
<li><a href="#lang.constants">vbNormal</a></li>
<li><a href="#lang.constants">vbReadOnly</a></li>
<li><a href="#lang.constants">vbHidden</a></li>
<li><a href="#lang.constants">vbSystem</a></li>
<li><a href="#lang.constants">vbDirectory</a></li>
<li><a href="#lang.constants">vbArchive</a></li>
<li><a href="#lang.constants">True</a></li>
<li><a href="#lang.constants">False</a></li>
<li><a href="#lang.constants">Predefined Constants</a></li>
<li><a href="#lang.runtime">Include Libraries</a></li>
<li><a href="#lang.runtime">commdlg.bas</a></li>
<li><a href="#lang.runtime">sql.bas</a></li>
<li><a href="#lang.runtime">comm.bas</a></li>
<li><a href="#lang.runtime">resource.bas</a></li>
<li><a href="#lang.runtime">help.bas</a></li>
<li><a href="#lang.runtime">DECLARE LIBRARY</a></li>
<li><a href="#ctrl.common.props">Common Properties</a></li>
<li><a href="#ctrl.common.props">Common Events</a></li>
<li><a href="#ctrl.common.props">Common Methods</a></li>
<li><a href="#ctrl.common.props">Properties</a></li>
<li><a href="#ctrl.common.props">Events</a></li>
<li><a href="#ctrl.common.props">Methods</a></li>
<li><a href="#ctrl.databinding">Data Binding</a></li>
<li><a href="#ctrl.databinding">DataSource</a></li>
<li><a href="#ctrl.databinding">DataField</a></li>
<li><a href="#ctrl.databinding">Master-Detail</a></li> <li><a href="#ctrl.databinding">Master-Detail</a></li>
<li><a href="#lang.forms">Me</a></li>
<li><a href="#ctrl.menus">Menu</a></li> <li><a href="#ctrl.menus">Menu</a></li>
<li><a href="#ctrl.menus">Menu Bar</a></li> <li><a href="#ctrl.menus">Menu Bar</a></li>
<li><a href="#ctrl.menus">Submenu</a></li> <li><a href="#ide.menu.view">Menu Editor</a></li>
<li><a href="#ctrl.menus">Separator</a></li> <li><a href="#ctrl.common.props">Methods</a></li>
<li><a href="#ctrl.arrays">Control Arrays</a></li> <li><a href="#lang.func.string">MID$</a></li>
<li><a href="#ctrl.arrays">Index Property</a></li> <li><a href="#lang.filesystem">MKDIR</a></li>
<li><a href="#ctrl.frm">FRM</a></li> <li><a href="#lang.operators">MOD</a></li>
<li><a href="#ctrl.frm">.frm</a></li>
<li><a href="#ctrl.frm">Form File</a></li>
<li><a href="#ctrl.form">Form</a></li>
<li><a href="#ctrl.form">Window</a></li>
<li><a href="#ctrl.form">Caption</a></li>
<li><a href="#ctrl.form">AutoSize</a></li>
<li><a href="#ctrl.form">Resizable</a></li>
<li><a href="#ctrl.form">Load</a></li>
<li><a href="#ctrl.form">Unload</a></li>
<li><a href="#ctrl.form">Show</a></li>
<li><a href="#ctrl.form">Hide</a></li>
<li><a href="#ctrl.terminal">Terminal</a></li>
<li><a href="#ctrl.terminal">ANSI Terminal</a></li>
<li><a href="#ctrl.terminal">VT100</a></li>
<li><a href="#ctrl.frame">Frame</a></li>
<li><a href="#ctrl.frame">Container</a></li>
<li><a href="#ctrl.hbox">HBox</a></li>
<li><a href="#ctrl.hbox">Horizontal Layout</a></li>
<li><a href="#ctrl.vbox">VBox</a></li>
<li><a href="#ctrl.vbox">Vertical Layout</a></li>
<li><a href="#ctrl.button">CommandButton</a></li>
<li><a href="#ctrl.button">Button</a></li>
<li><a href="#ctrl.button">Click</a></li>
<li><a href="#ctrl.picturebox">PictureBox</a></li>
<li><a href="#ctrl.picturebox">Canvas</a></li>
<li><a href="#ctrl.picturebox">Drawing</a></li>
<li><a href="#ctrl.picturebox">SetPenColor</a></li>
<li><a href="#ctrl.picturebox">SetPenSize</a></li>
<li><a href="#ctrl.checkbox">CheckBox</a></li>
<li><a href="#ctrl.checkbox">Value</a></li>
<li><a href="#ctrl.combobox">ComboBox</a></li>
<li><a href="#ctrl.data">Data</a></li>
<li><a href="#ctrl.data">Database</a></li>
<li><a href="#ctrl.data">SQLite</a></li>
<li><a href="#ctrl.data">DatabaseName</a></li>
<li><a href="#ctrl.data">RecordSource</a></li>
<li><a href="#ctrl.data">MoveFirst</a></li> <li><a href="#ctrl.data">MoveFirst</a></li>
<li><a href="#ctrl.data">MoveNext</a></li> <li><a href="#ctrl.data">MoveNext</a></li>
<li><a href="#ctrl.data">AddNew</a></li> <li><a href="#lang.forms">MsgBox</a></li>
<li><a href="#ctrl.data">Reposition</a></li> <li><a href="#lang.forms">MSGBOX</a></li>
<li><a href="#ctrl.data">Validate</a></li>
<li><a href="#ctrl.dbgrid">DBGrid</a></li>
<li><a href="#ctrl.dbgrid">Data-Bound Grid</a></li>
<li><a href="#ctrl.dropdown">DropDown</a></li>
<li><a href="#ctrl.imagebutton">ImageButton</a></li>
<li><a href="#ctrl.image">Image</a></li>
<li><a href="#ctrl.image">Picture</a></li>
<li><a href="#ctrl.image">BMP</a></li>
<li><a href="#ctrl.label">Label</a></li>
<li><a href="#ctrl.label">Caption</a></li>
<li><a href="#ctrl.label">Alignment</a></li>
<li><a href="#ctrl.listbox">ListBox</a></li>
<li><a href="#ctrl.listbox">ListIndex</a></li>
<li><a href="#ctrl.listbox">ListCount</a></li>
<li><a href="#ctrl.listbox">AddItem</a></li>
<li><a href="#ctrl.listbox">RemoveItem</a></li>
<li><a href="#ctrl.listview">ListView</a></li>
<li><a href="#ctrl.listview">Multi-Column List</a></li> <li><a href="#ctrl.listview">Multi-Column List</a></li>
<li><a href="#ctrl.listview">SetColumns</a></li> <li><a href="#lang.filesystem">NAME</a></li>
<li><a href="#ctrl.listview">SetSort</a></li> <li><a href="#ide.menu.file">New Project</a></li>
<li><a href="#ctrl.progressbar">ProgressBar</a></li> <li><a href="#lang.loops">NEXT</a></li>
<li><a href="#lang.operators">NOT</a></li>
<li><a href="#lang.forms">Nothing</a></li>
<li><a href="#ide.editor">Object Dropdown</a></li>
<li><a href="#lang.func.string">OCT$</a></li>
<li><a href="#lang.misc">ON ERROR</a></li>
<li><a href="#lang.flow">ON GOSUB</a></li>
<li><a href="#lang.flow">ON GOTO</a></li>
<li><a href="#lang.fileio">OPEN</a></li>
<li><a href="#ide.menu.file">Open Project</a></li>
<li><a href="#lang.operators">Operators</a></li>
<li><a href="#lang.declarations">OPTION</a></li>
<li><a href="#lang.declarations">OPTION BASE</a></li>
<li><a href="#lang.declarations">OPTION COMPARE</a></li>
<li><a href="#ide.preferences">Option Explicit</a></li>
<li><a href="#lang.declarations">OPTION EXPLICIT</a></li>
<li><a href="#ctrl.optionbutton">OptionButton</a></li> <li><a href="#ctrl.optionbutton">OptionButton</a></li>
<li><a href="#lang.operators">OR</a></li>
<li><a href="#ide.output">Output Window</a></li>
<li><a href="#ide.menu.edit">Paste</a></li>
<li><a href="#ctrl.image">Picture</a></li>
<li><a href="#ctrl.picturebox">PictureBox</a></li>
<li><a href="#lang.operators">Precedence</a></li>
<li><a href="#lang.constants">Predefined Constants</a></li>
<li><a href="#ide.menu.tools">Preferences</a></li>
<li><a href="#ide.preferences">Preferences</a></li>
<li><a href="#lang.io">PRINT</a></li>
<li><a href="#lang.fileio">PRINT #</a></li>
<li><a href="#ide.output">PRINT Output</a></li>
<li><a href="#lang.io">PRINT USING</a></li>
<li><a href="#ctrl.progressbar">ProgressBar</a></li>
<li><a href="#ide.project">Project Explorer</a></li>
<li><a href="#ide.project">Project Files</a></li>
<li><a href="#ide.menu.file">Project Properties</a></li>
<li><a href="#ide.project">Project System</a></li>
<li><a href="#ctrl.common.props">Properties</a></li>
<li><a href="#ide.properties">Properties Panel</a></li>
<li><a href="#ide.properties">Property List</a></li>
<li><a href="#lang.fileio">PUT</a></li>
<li><a href="#ctrl.optionbutton">Radio Button</a></li> <li><a href="#ctrl.optionbutton">Radio Button</a></li>
<li><a href="#ctrl.optionbutton">SetSelected</a></li> <li><a href="#lang.misc">RANDOMIZE</a></li>
<li><a href="#ctrl.scrollpane">ScrollPane</a></li> <li><a href="#lang.misc">RANDOMIZE TIMER</a></li>
<li><a href="#lang.io">READ</a></li>
<li><a href="#ctrl.spinbutton">RealMode</a></li>
<li><a href="#ctrl.data">RecordSource</a></li>
<li><a href="#lang.declarations">REDIM</a></li>
<li><a href="#lang.statements">REM</a></li>
<li><a href="#ide.menu.file">Remove File</a></li>
<li><a href="#lang.forms">RemoveControl</a></li>
<li><a href="#ctrl.listbox">RemoveItem</a></li>
<li><a href="#ide.immediate">REPL</a></li>
<li><a href="#ide.menu.edit">Replace</a></li>
<li><a href="#ide.findreplace">Replace</a></li>
<li><a href="#ctrl.textarea">ReplaceAll</a></li>
<li><a href="#ctrl.data">Reposition</a></li>
<li><a href="#ctrl.form">Resizable</a></li>
<li><a href="#lang.runtime">resource.bas</a></li>
<li><a href="#lang.io">RESTORE</a></li>
<li><a href="#lang.misc">RESUME</a></li>
<li><a href="#lang.misc">RESUME NEXT</a></li>
<li><a href="#lang.flow">RETURN</a></li>
<li><a href="#lang.func.string">RIGHT$</a></li>
<li><a href="#lang.filesystem">RMDIR</a></li>
<li><a href="#lang.func.math">RND</a></li>
<li><a href="#lang.func.string">RTRIM$</a></li>
<li><a href="#ide.menu.run">Run</a></li>
<li><a href="#ide.menu.run">Run Menu</a></li>
<li><a href="#ide.menu.run">Run to Cursor</a></li>
<li><a href="#ide.menu.run">Run Without Recompile</a></li>
<li><a href="#ide.output">Runtime Errors</a></li>
<li><a href="#ide.menu.file">Save All</a></li>
<li><a href="#ide.menu.file">Save File</a></li>
<li><a href="#ide.menu.run">Save on Run</a></li>
<li><a href="#ide.menu.file">Save Project</a></li>
<li><a href="#ctrl.scrollpane">Scrollable Container</a></li> <li><a href="#ctrl.scrollpane">Scrollable Container</a></li>
<li><a href="#ctrl.line">Line</a></li> <li><a href="#ctrl.scrollpane">ScrollPane</a></li>
<li><a href="#ide.findreplace">Search</a></li>
<li><a href="#lang.fileio">SEEK</a></li>
<li><a href="#ide.menu.edit">Select All</a></li>
<li><a href="#lang.conditionals">SELECT CASE</a></li>
<li><a href="#ctrl.menus">Separator</a></li>
<li><a href="#ctrl.line">Separator</a></li> <li><a href="#ctrl.line">Separator</a></li>
<li><a href="#ctrl.hscrollbar">HScrollBar</a></li> <li><a href="#ctrl.tabstrip">SetActive</a></li>
<li><a href="#lang.filesystem">SETATTR</a></li>
<li><a href="#ctrl.listview">SetColumns</a></li>
<li><a href="#lang.forms">SetEvent</a></li>
<li><a href="#ctrl.picturebox">SetPenColor</a></li>
<li><a href="#ctrl.picturebox">SetPenSize</a></li>
<li><a href="#ctrl.spinbutton">SetRange</a></li>
<li><a href="#ctrl.optionbutton">SetSelected</a></li>
<li><a href="#ctrl.listview">SetSort</a></li>
<li><a href="#ctrl.spinbutton">SetStep</a></li>
<li><a href="#ide.preferences">Settings</a></li>
<li><a href="#lang.func.math">SGN</a></li>
<li><a href="#lang.declarations">SHARED</a></li>
<li><a href="#lang.misc">SHELL</a></li>
<li><a href="#lang.forms">Show</a></li>
<li><a href="#ctrl.form">Show</a></li>
<li><a href="#lang.func.math">SIN</a></li>
<li><a href="#lang.datatypes">Single</a></li>
<li><a href="#lang.misc">SLEEP</a></li>
<li><a href="#ctrl.hscrollbar">Slider</a></li> <li><a href="#ctrl.hscrollbar">Slider</a></li>
<li><a href="#ide.project">Source Map</a></li>
<li><a href="#lang.func.string">SPACE$</a></li>
<li><a href="#ctrl.spacer">Spacer</a></li> <li><a href="#ctrl.spacer">Spacer</a></li>
<li><a href="#lang.io">SPC</a></li>
<li><a href="#ctrl.spinbutton">SpinButton</a></li> <li><a href="#ctrl.spinbutton">SpinButton</a></li>
<li><a href="#ctrl.spinbutton">Spinner</a></li> <li><a href="#ctrl.spinbutton">Spinner</a></li>
<li><a href="#ctrl.spinbutton">SetRange</a></li>
<li><a href="#ctrl.spinbutton">SetStep</a></li>
<li><a href="#ctrl.spinbutton">RealMode</a></li>
<li><a href="#ctrl.spinbutton">Decimals</a></li>
<li><a href="#ctrl.splitter">Splitter</a></li>
<li><a href="#ctrl.splitter">Split Pane</a></li> <li><a href="#ctrl.splitter">Split Pane</a></li>
<li><a href="#ctrl.statusbar">StatusBar</a></li> <li><a href="#ctrl.splitter">Splitter</a></li>
<li><a href="#ctrl.tabstrip">TabStrip</a></li> <li><a href="#lang.runtime">sql.bas</a></li>
<li><a href="#ctrl.tabstrip">TabControl</a></li> <li><a href="#lang.sql">SQLAffected</a></li>
<li><a href="#ctrl.tabstrip">SetActive</a></li> <li><a href="#lang.sql">SQLClose</a></li>
<li><a href="#ctrl.textbox">TextBox</a></li> <li><a href="#lang.sql">SQLEof</a></li>
<li><a href="#ctrl.textbox">Text</a></li> <li><a href="#lang.sql">SQLError$</a></li>
<li><a href="#ctrl.textbox">DataSource</a></li> <li><a href="#lang.sql">SQLExec</a></li>
<li><a href="#ctrl.textbox">DataField</a></li> <li><a href="#lang.sql">SQLField$</a></li>
<li><a href="#ctrl.textarea">TextArea</a></li> <li><a href="#lang.sql">SQLFieldCount</a></li>
<li><a href="#ctrl.textarea">FindNext</a></li> <li><a href="#lang.sql">SQLFieldDbl</a></li>
<li><a href="#ctrl.textarea">ReplaceAll</a></li> <li><a href="#lang.sql">SQLFieldInt</a></li>
<li><a href="#ctrl.textarea">GoToLine</a></li> <li><a href="#lang.sql">SQLFreeResult</a></li>
<li><a href="#ctrl.textarea">CursorLine</a></li> <li><a href="#lang.sql">SQLite</a></li>
<li><a href="#ctrl.textarea">LineNumbers</a></li> <li><a href="#ctrl.data">SQLite</a></li>
<li><a href="#ctrl.textarea">AutoIndent</a></li> <li><a href="#lang.sql">SQLNext</a></li>
<li><a href="#ctrl.timer">Timer</a></li> <li><a href="#lang.sql">SQLOpen</a></li>
<li><a href="#ctrl.timer">Interval</a></li> <li><a href="#lang.sql">SQLQuery</a></li>
<li><a href="#lang.func.math">SQR</a></li>
<li><a href="#ide.debug.callstack">Stack Trace</a></li>
<li><a href="#ctrl.timer">Start</a></li> <li><a href="#ctrl.timer">Start</a></li>
<li><a href="#lang.statements">Statements</a></li>
<li><a href="#lang.declarations">STATIC</a></li>
<li><a href="#ide.menu.view">Status Bar Toggle</a></li>
<li><a href="#ctrl.statusbar">StatusBar</a></li>
<li><a href="#lang.loops">STEP</a></li>
<li><a href="#ide.menu.run">Step Into</a></li>
<li><a href="#ide.menu.run">Step Out</a></li>
<li><a href="#ide.menu.run">Step Over</a></li>
<li><a href="#ide.debugger">Stepping</a></li>
<li><a href="#ide.menu.run">Stop</a></li>
<li><a href="#ctrl.timer">Stop</a></li> <li><a href="#ctrl.timer">Stop</a></li>
<li><a href="#lang.func.string">STR$</a></li>
<li><a href="#lang.datatypes">String</a></li>
<li><a href="#lang.func.string">STRING$</a></li>
<li><a href="#lang.procedures">SUB</a></li>
<li><a href="#ctrl.menus">Submenu</a></li>
<li><a href="#lang.declarations">SWAP</a></li>
<li><a href="#ide.editor">Syntax Highlighting</a></li>
<li><a href="#lang.io">TAB</a></li>
<li><a href="#ide.preferences">Tab Width</a></li>
<li><a href="#ctrl.tabstrip">TabControl</a></li>
<li><a href="#ctrl.tabstrip">TabStrip</a></li>
<li><a href="#lang.func.math">TAN</a></li>
<li><a href="#ctrl.terminal">Terminal</a></li>
<li><a href="#ctrl.textbox">Text</a></li>
<li><a href="#ctrl.textarea">TextArea</a></li>
<li><a href="#ctrl.textbox">TextBox</a></li>
<li><a href="#lang.conditionals">THEN</a></li>
<li><a href="#lang.func.misc">TIME$</a></li>
<li><a href="#lang.func.math">TIMER</a></li>
<li><a href="#ctrl.timer">Timer</a></li>
<li><a href="#ide.menu.run">Toggle Breakpoint</a></li>
<li><a href="#ide.toolbar">Toolbar</a></li>
<li><a href="#ctrl.toolbar">Toolbar</a></li> <li><a href="#ctrl.toolbar">Toolbar</a></li>
<li><a href="#ide.menu.view">Toolbar Toggle</a></li>
<li><a href="#ide.toolbox">Toolbox</a></li>
<li><a href="#ide.menu.tools">Tools Menu</a></li>
<li><a href="#ctrl.treeview">TreeView</a></li> <li><a href="#ctrl.treeview">TreeView</a></li>
<li><a href="#ctrl.treeview">AddItem</a></li> <li><a href="#lang.func.string">TRIM$</a></li>
<li><a href="#ctrl.treeview">AddChildItem</a></li> <li><a href="#lang.constants">True</a></li>
<li><a href="#lang.declarations">TYPE</a></li>
<li><a href="#lang.func.fileio">UBOUND</a></li>
<li><a href="#lang.func.string">UCASE$</a></li>
<li><a href="#lang.forms">UNLOAD</a></li>
<li><a href="#ctrl.form">Unload</a></li>
<li><a href="#lang.loops">UNTIL</a></li>
<li><a href="#lang.func.string">VAL</a></li>
<li><a href="#ctrl.data">Validate</a></li>
<li><a href="#ctrl.checkbox">Value</a></li>
<li><a href="#ide.immediate">Variable Assignment</a></li>
<li><a href="#ide.debug.locals">Variable Inspection</a></li>
<li><a href="#lang.constants">vbArchive</a></li>
<li><a href="#lang.constants">vbCancel</a></li>
<li><a href="#lang.constants">vbCritical</a></li>
<li><a href="#lang.constants">vbDirectory</a></li>
<li><a href="#lang.constants">vbExclamation</a></li>
<li><a href="#lang.constants">vbHidden</a></li>
<li><a href="#lang.constants">vbInformation</a></li>
<li><a href="#lang.forms">vbModal</a></li>
<li><a href="#lang.constants">vbNo</a></li>
<li><a href="#lang.constants">vbNormal</a></li>
<li><a href="#lang.constants">vbOK</a></li>
<li><a href="#lang.constants">vbOKCancel</a></li>
<li><a href="#lang.constants">vbOKOnly</a></li>
<li><a href="#ctrl.vbox">VBox</a></li>
<li><a href="#lang.constants">vbQuestion</a></li>
<li><a href="#lang.constants">vbReadOnly</a></li>
<li><a href="#lang.constants">vbRetry</a></li>
<li><a href="#lang.constants">vbRetryCancel</a></li>
<li><a href="#lang.constants">vbSystem</a></li>
<li><a href="#lang.constants">vbYes</a></li>
<li><a href="#lang.constants">vbYesNo</a></li>
<li><a href="#lang.constants">vbYesNoCancel</a></li>
<li><a href="#ctrl.vbox">Vertical Layout</a></li>
<li><a href="#ide.menu.view">View Menu</a></li>
<li><a href="#ide.overview">Visual Basic</a></li>
<li><a href="#ctrl.terminal">VT100</a></li>
<li><a href="#ide.debug.watch">Watch Expressions</a></li>
<li><a href="#ide.debug.watch">Watch Window</a></li>
<li><a href="#lang.loops">WEND</a></li>
<li><a href="#lang.loops">WHILE</a></li>
<li><a href="#ide.toolbox">Widget Palette</a></li>
<li><a href="#ctrl.form">Window</a></li>
<li><a href="#ide.menu.window">Window Menu</a></li>
<li><a href="#lang.forms">With</a></li>
<li><a href="#ctrl.wrapbox">WrapBox</a></li> <li><a href="#ctrl.wrapbox">WrapBox</a></li>
<li><a href="#ctrl.wrapbox">Flow Layout</a></li> <li><a href="#lang.fileio">WRITE #</a></li>
<li><a href="#ide.designer">WYSIWYG</a></li>
<li><a href="#lang.operators">XOR</a></li>
</ul> </ul>
</nav> </nav>
<main> <main>

View file

@ -36,18 +36,18 @@ img { max-width: 100%; }
</ul> </ul>
<h3>Index</h3> <h3>Index</h3>
<ul> <ul>
<li><a href="#help.overview">DVX Help</a></li>
<li><a href="#help.overview">Help Viewer</a></li>
<li><a href="#help.format">.dhs</a></li> <li><a href="#help.format">.dhs</a></li>
<li><a href="#help.format">Source Format</a></li>
<li><a href="#help.format">Directives</a></li>
<li><a href="#help.compiler">dvxhlpc</a></li>
<li><a href="#help.compiler">Compiler</a></li> <li><a href="#help.compiler">Compiler</a></li>
<li><a href="#help.integration">F1</a></li>
<li><a href="#help.integration">Context Help</a></li> <li><a href="#help.integration">Context Help</a></li>
<li><a href="#help.format">Directives</a></li>
<li><a href="#help.overview">DVX Help</a></li>
<li><a href="#help.compiler">dvxhlpc</a></li>
<li><a href="#help.integration">F1</a></li>
<li><a href="#help.overview">Help Viewer</a></li>
<li><a href="#help.integration">helpFile</a></li> <li><a href="#help.integration">helpFile</a></li>
<li><a href="#help.integration">helpTopic</a></li> <li><a href="#help.integration">helpTopic</a></li>
<li><a href="#help.integration">shellLoadAppWithArgs</a></li> <li><a href="#help.integration">shellLoadAppWithArgs</a></li>
<li><a href="#help.format">Source Format</a></li>
</ul> </ul>
</nav> </nav>
<main> <main>

File diff suppressed because it is too large Load diff

View file

@ -142,10 +142,15 @@ $(BINDIR)/kpunch/widshow: ; mkdir -p $@
# Header dependencies # Header dependencies
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
# Every widget public header. Apps that pull in widget APIs (e.g. dvxdemo)
# must rebuild when these change, otherwise the struct-layout contract
# between the widget DXE's sApi and the app's cast to RadioApiT/etc.
# silently drifts and produces late-binding crashes.
WIDGET_H = $(wildcard ../../widgets/kpunch/*/*.h)
$(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h $(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h
$(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h $(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
$(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h $(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(WIDGET_H)
clean: clean:
rm -f $(OBJDIR)/*.o rm -f $(OBJDIR)/*.o

View file

@ -162,7 +162,6 @@ End
' IN THE SOFTWARE. ' IN THE SOFTWARE.
OPTION EXPLICIT OPTION EXPLICIT
TYPE PointT TYPE PointT
@ -185,22 +184,6 @@ dynCount = 0
timerWin = 0 timerWin = 0
tickCount = 0 tickCount = 0
' ============================================================
' OutArea helpers
' ============================================================
SUB Say(s AS STRING)
OutArea.AppendText s + CHR$(10)
END SUB
SUB Header(title AS STRING)
Say ""
Say "--- " + title + " ---"
END SUB
Load BasicDemo Load BasicDemo
BasicDemo.Show BasicDemo.Show
@ -212,9 +195,16 @@ Say "Check the Demos menu for graphics, dynamic UI, and timer demos."
Say "" Say ""
' ============================================================ SUB Say(s AS STRING)
' Menu handlers OutArea.AppendText s + CHR$(10)
' ============================================================ END SUB
SUB Header(title AS STRING)
Say ""
Say "--- " + title + " ---"
END SUB
SUB mnuClear_Click SUB mnuClear_Click
OutArea.Text = "" OutArea.Text = ""
@ -266,12 +256,8 @@ SUB mnuAbout_Click
MsgBox msg, vbOKOnly, "About" MsgBox msg, vbOKOnly, "About"
END SUB END SUB
' ============================================================
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
' ============================================================
SUB btnTypes_Click SUB btnTypes_Click
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
Header "Types" Header "Types"
DIM i AS INTEGER DIM i AS INTEGER
@ -302,12 +288,8 @@ SUB btnTypes_Click
LblStatus.Caption = "Types demo complete." LblStatus.Caption = "Types demo complete."
END SUB END SUB
' ============================================================
' Math: integer + float operators, built-in functions
' ============================================================
SUB btnMath_Click SUB btnMath_Click
' Math: integer + float operators, built-in functions
Header "Math" Header "Math"
DIM a AS INTEGER DIM a AS INTEGER
@ -342,12 +324,8 @@ SUB btnMath_Click
LblStatus.Caption = "Math demo complete." LblStatus.Caption = "Math demo complete."
END SUB END SUB
' ============================================================
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
' ============================================================
SUB btnStrings_Click SUB btnStrings_Click
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
Header "Strings" Header "Strings"
DIM s AS STRING DIM s AS STRING
@ -371,12 +349,8 @@ SUB btnStrings_Click
LblStatus.Caption = "Strings demo complete." LblStatus.Caption = "Strings demo complete."
END SUB END SUB
' ============================================================
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
' ============================================================
SUB btnArrays_Click SUB btnArrays_Click
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
Header "Arrays" Header "Arrays"
' 1D array ' 1D array
@ -430,12 +404,8 @@ SUB btnArrays_Click
LblStatus.Caption = "Arrays demo complete." LblStatus.Caption = "Arrays demo complete."
END SUB END SUB
' ============================================================
' DATA / READ / RESTORE
' ============================================================
SUB btnData_Click SUB btnData_Click
' DATA / READ / RESTORE
Header "DATA / READ / RESTORE" Header "DATA / READ / RESTORE"
DATA "Red", 255, 0, 0 DATA "Red", 255, 0, 0
@ -465,12 +435,8 @@ SUB btnData_Click
LblStatus.Caption = "DATA/READ demo complete." LblStatus.Caption = "DATA/READ demo complete."
END SUB END SUB
' ============================================================
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
' ============================================================
SUB btnFlow_Click SUB btnFlow_Click
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
Header "Control Flow" Header "Control Flow"
' FOR with STEP ' FOR with STEP
@ -538,12 +504,8 @@ SUB btnFlow_Click
RETURN RETURN
END SUB END SUB
' ============================================================
' User-Defined Type
' ============================================================
SUB btnUdt_Click SUB btnUdt_Click
' User-Defined Type
Header "User-Defined Type" Header "User-Defined Type"
DIM p AS PointT DIM p AS PointT
@ -566,11 +528,6 @@ SUB btnUdt_Click
LblStatus.Caption = "UDT demo complete." LblStatus.Caption = "UDT demo complete."
END SUB END SUB
' ============================================================
' Optional parameters (DVX extension)
' ============================================================
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
IF greeting = "" THEN IF greeting = "" THEN
greeting = "Hello" greeting = "Hello"
@ -589,12 +546,8 @@ SUB btnOpt_Click
LblStatus.Caption = "Optional params demo complete." LblStatus.Caption = "Optional params demo complete."
END SUB END SUB
' ============================================================
' ON ERROR GOTO
' ============================================================
SUB btnError_Click SUB btnError_Click
' ON ERROR GOTO
Header "ON ERROR GOTO" Header "ON ERROR GOTO"
ON ERROR GOTO handler ON ERROR GOTO handler
@ -613,12 +566,8 @@ SUB btnError_Click
LblStatus.Caption = "Error handler ran successfully." LblStatus.Caption = "Error handler ran successfully."
END SUB END SUB
' ============================================================
' PRINT USING / FORMAT$
' ============================================================
SUB btnFormat_Click SUB btnFormat_Click
' PRINT USING / FORMAT$
Header "Formatting" Header "Formatting"
Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00") Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00")
@ -629,12 +578,8 @@ SUB btnFormat_Click
LblStatus.Caption = "Format demo complete." LblStatus.Caption = "Format demo complete."
END SUB END SUB
' ============================================================
' File I/O
' ============================================================
SUB btnFileIO_Click SUB btnFileIO_Click
' File I/O
Header "File I/O" Header "File I/O"
DIM path AS STRING DIM path AS STRING
@ -664,12 +609,8 @@ SUB btnFileIO_Click
LblStatus.Caption = "File I/O demo complete." LblStatus.Caption = "File I/O demo complete."
END SUB END SUB
' ============================================================
' System: App object, environment, current directory
' ============================================================
SUB btnSystem_Click SUB btnSystem_Click
' System: App object, environment, current directory
Header "System / App" Header "System / App"
Say "App.Path = " + App.Path Say "App.Path = " + App.Path
@ -683,12 +624,8 @@ SUB btnSystem_Click
LblStatus.Caption = "System demo complete." LblStatus.Caption = "System demo complete."
END SUB END SUB
' ============================================================
' INI read/write
' ============================================================
SUB btnIni_Click SUB btnIni_Click
' INI read/write
Header "INI Read/Write" Header "INI Read/Write"
DIM path AS STRING DIM path AS STRING
@ -710,11 +647,6 @@ SUB btnIni_Click
LblStatus.Caption = "INI demo complete." LblStatus.Caption = "INI demo complete."
END SUB END SUB
' ============================================================
' Dialog demos (spawns the real dialogs)
' ============================================================
SUB btnDialogs_Click SUB btnDialogs_Click
Header "Dialogs" Header "Dialogs"
@ -750,11 +682,6 @@ SUB btnClear_Click
mnuClear_Click mnuClear_Click
END SUB END SUB
' ============================================================
' Graphics demo (opens a second form with Canvas)
' ============================================================
SUB mnuGraphics_Click SUB mnuGraphics_Click
IF gfxWin <> 0 THEN IF gfxWin <> 0 THEN
EXIT SUB EXIT SUB
@ -766,7 +693,7 @@ SUB mnuGraphics_Click
gfxWin = frm gfxWin = frm
DIM cv AS LONG DIM cv AS LONG
SET cv = CreateControl(frm, "Canvas", "GfxCanvas") SET cv = CreateControl(frm, "PictureBox", "GfxCanvas")
GfxCanvas.Width = 340 GfxCanvas.Width = 340
GfxCanvas.Height = 260 GfxCanvas.Height = 260
GfxCanvas.Weight = 1 GfxCanvas.Weight = 1
@ -866,11 +793,6 @@ SUB GraphicsForm_Unload
gfxWin = 0 gfxWin = 0
END SUB END SUB
' ============================================================
' Dynamic form demo
' ============================================================
SUB mnuDynamic_Click SUB mnuDynamic_Click
IF dynForm <> 0 THEN IF dynForm <> 0 THEN
EXIT SUB EXIT SUB
@ -922,11 +844,6 @@ SUB DynForm_Unload
dynCount = 0 dynCount = 0
END SUB END SUB
' ============================================================
' Timer demo
' ============================================================
SUB mnuTimer_Click SUB mnuTimer_Click
IF timerWin <> 0 THEN IF timerWin <> 0 THEN
EXIT SUB EXIT SUB

View file

@ -996,9 +996,14 @@ static void scanThemes(void) {
nameLen = (int32_t)sizeof(entry.name) - 1; nameLen = (int32_t)sizeof(entry.name) - 1;
} }
// entry.name is the extension-stripped label shown in the
// dropdown. entry.path must use the ORIGINAL filename (with
// extension) so dvxLoadTheme can open it -- previously we
// used entry.name here, which pointed at a path missing
// .THEME and silently failed.
memcpy(entry.name, names[i], nameLen); memcpy(entry.name, names[i], nameLen);
entry.name[nameLen] = '\0'; entry.name[nameLen] = '\0';
snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, entry.name); snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, names[i]);
arrput(sThemeEntries, entry); arrput(sThemeEntries, entry);
} }

View file

@ -202,6 +202,7 @@ static void parseKill(BasParserT *p);
static void parseLineInput(BasParserT *p); static void parseLineInput(BasParserT *p);
static void parseMkDir(BasParserT *p); static void parseMkDir(BasParserT *p);
static void parseModule(BasParserT *p); static void parseModule(BasParserT *p);
static void prescanSignatures(BasParserT *p);
static void parseMulExpr(BasParserT *p); static void parseMulExpr(BasParserT *p);
static void parseName(BasParserT *p); static void parseName(BasParserT *p);
static void parseNotExpr(BasParserT *p); static void parseNotExpr(BasParserT *p);
@ -2911,6 +2912,12 @@ static void parseFunction(BasParserT *p) {
funcSym->paramOptional[i] = paramOptional[i]; funcSym->paramOptional[i] = paramOptional[i];
} }
// Record the owning form -- see parseSub for the rationale.
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
strncpy(funcSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
funcSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
}
// Backpatch any forward-reference calls to this function // Backpatch any forward-reference calls to this function
patchCallAddrs(p, funcSym); patchCallAddrs(p, funcSym);
@ -3370,7 +3377,193 @@ static void parseMkDir(BasParserT *p) {
} }
// Walk the token stream from current position, find every
// top-level SUB/FUNCTION declaration, extract the signature
// (name, params, return type), and register it in the symbol
// table. Does not emit code. Saves and restores lexer position
// so the main parse pass starts from the same point. This gives
// VB-style forward visibility: call sites that appear earlier in
// the source than the SUB definition still resolve to the right
// paramCount / types.
static void prescanSignatures(BasParserT *p) {
BasLexerT savedLex = p->lex;
bool savedErr = p->hasError;
int32_t savedErrLn = p->errorLine;
char savedErrMsg[BAS_PARSE_ERR_SCRATCH];
snprintf(savedErrMsg, sizeof(savedErrMsg), "%s", p->error);
while (!check(p, TOK_EOF)) {
// "END SUB" / "END FUNCTION" consume the END and the following
// keyword as separate tokens; skip END so the next-iteration
// SUB/FUNCTION check doesn't misinterpret it as a declaration.
if (check(p, TOK_END)) {
advance(p);
continue;
}
bool isFn = check(p, TOK_FUNCTION);
bool isSub = check(p, TOK_SUB);
if (!isFn && !isSub) {
advance(p);
continue;
}
advance(p); // consume SUB / FUNCTION
if (!check(p, TOK_IDENT)) {
continue;
}
char name[BAS_MAX_TOKEN_LEN];
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
// Param list
int32_t paramCount = 0;
int32_t requiredCount = 0;
uint8_t paramTypes[BAS_MAX_PARAMS] = {0};
bool paramByVal[BAS_MAX_PARAMS] = {0};
bool paramOptional[BAS_MAX_PARAMS] = {0};
if (check(p, TOK_LPAREN)) {
advance(p);
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !check(p, TOK_NEWLINE)) {
if (paramCount > 0) {
if (!check(p, TOK_COMMA)) {
break;
}
advance(p);
}
bool optional = false;
if (check(p, TOK_OPTIONAL)) {
optional = true;
advance(p);
}
bool byVal = false;
if (check(p, TOK_BYVAL)) {
byVal = true;
advance(p);
}
if (!check(p, TOK_IDENT)) {
break;
}
char paramName[BAS_MAX_TOKEN_LEN];
strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
uint8_t pdt = suffixToType(paramName);
if (check(p, TOK_AS)) {
advance(p);
// resolveTypeName sets hasError on miss; we clear
// at function end so one broken signature doesn't
// stop us from discovering the rest.
pdt = resolveTypeName(p);
if (p->hasError) {
break;
}
}
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;
paramByVal[paramCount] = byVal;
paramOptional[paramCount] = optional;
}
if (!optional) {
requiredCount = paramCount + 1;
}
paramCount++;
}
if (check(p, TOK_RPAREN)) {
advance(p);
}
}
// FUNCTION return type (AS clause; or suffix on name)
uint8_t returnType = suffixToType(name);
if (isFn && check(p, TOK_AS)) {
advance(p);
returnType = resolveTypeName(p);
}
// Register / update the symbol. If a call site already
// created a forward-ref stub, update it in place.
BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
if (sym == NULL) {
bool savedLocal = p->sym.inLocalScope;
p->sym.inLocalScope = false;
sym = basSymTabAdd(&p->sym, name,
isFn ? SYM_FUNCTION : SYM_SUB,
returnType);
p->sym.inLocalScope = savedLocal;
}
if (sym != NULL) {
sym->scope = SCOPE_GLOBAL;
sym->dataType = returnType;
sym->paramCount = paramCount;
sym->requiredParams = requiredCount;
for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
sym->paramTypes[i] = paramTypes[i];
sym->paramByVal[i] = paramByVal[i];
sym->paramOptional[i] = paramOptional[i];
}
// basSymTabAdd defaults isDefined=true; clear it so
// call sites that encounter this symbol before the real
// body is parsed register themselves as forward-refs
// (patchAddrs). The real parseSub/parseFunction pass
// sets isDefined=true and fills in codeAddr, at which
// point patchCallAddrs backpatches the forward refs.
sym->isDefined = false;
sym->codeAddr = 0;
}
// Best-effort: clear any scan error so we continue to the
// next declaration. The main parse pass will re-surface
// real errors with full location info.
if (p->hasError) {
p->hasError = false;
p->errorLine = 0;
p->error[0] = '\0';
}
}
p->lex = savedLex;
p->hasError = savedErr;
p->errorLine = savedErrLn;
snprintf(p->error, sizeof(p->error), "%s", savedErrMsg);
}
static void parseModule(BasParserT *p) { static void parseModule(BasParserT *p) {
// VB semantics: all SUB/FUNCTION declarations are visible from
// anywhere in the module regardless of source order. Do a
// pre-scan that walks the token stream, extracts every
// SUB/FUNCTION signature, and registers it in the symbol table.
// Call sites encountered later (including module-level code
// that precedes the SUB definition in source) can then validate
// argument count / types against the real signature instead of
// falling back to a paramCount=0 placeholder.
prescanSignatures(p);
skipNewlines(p); skipNewlines(p);
while (!p->hasError && !check(p, TOK_EOF)) { while (!p->hasError && !check(p, TOK_EOF)) {
@ -5843,6 +6036,16 @@ static void parseSub(BasParserT *p) {
subSym->paramOptional[i] = paramOptional[i]; subSym->paramOptional[i] = paramOptional[i];
} }
// Record the owning form so fireCtrlEvent can bind the SUB's
// form-scope variables at call time. Prescan adds SUB symbols
// before the BEGINFORM directive is consumed, so the formName
// wasn't populated by basSymTabAdd; set it here once we know we
// are inside a form scope.
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
strncpy(subSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
subSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
}
// Backpatch any forward-reference calls to this sub // Backpatch any forward-reference calls to this sub
patchCallAddrs(p, subSym); patchCallAddrs(p, subSym);

View file

@ -34,7 +34,14 @@
#include "dvxRes.h" #include "dvxRes.h"
#include "dvxWm.h" #include "dvxWm.h"
#include "shellApp.h" #include "shellApp.h"
#include "box/box.h" // Widget headers below publish inline accessor macros over
// dvx*Api() -> wgtGetApi(...) lookups, so they're lightweight
// compile-time type-safety over a runtime-dynamic dispatch -- no
// link-time dependency on the widget implementation. AnsiTerm,
// DataCtrl, and DbGrid need direct typed calls because their C
// APIs take callback function pointers, peer widget pointers, or
// string-returning-from-string signatures that don't fit the
// basic-callable iface-method dispatch.
#include "ansiTerm/ansiTerm.h" #include "ansiTerm/ansiTerm.h"
#include "dataCtrl/dataCtrl.h" #include "dataCtrl/dataCtrl.h"
#include "dbGrid/dbGrid.h" #include "dbGrid/dbGrid.h"
@ -60,6 +67,16 @@
// Module-level form runtime pointer for onFormClose callback // Module-level form runtime pointer for onFormClose callback
static BasFormRtT *sFormRt = NULL; static BasFormRtT *sFormRt = NULL;
// True while a basFormRtLoadFrm is in progress. The .frm parser calls
// basFormRtLoadForm via onFormBegin to create the bare form before it
// starts firing onCtrlBegin callbacks; if we let basFormRtLoadForm run
// the form's init-code during that window the init fires against a
// control list that is still empty (and then crashes with
// "unknown control: cboSize" etc.). basFormRtLoadFrm sets this true
// around frmParse so the nested LoadForm skips init; the outer
// caller runs init (or the event loop) after parsing completes.
static bool sLoadingFrm = false;
// ============================================================ // ============================================================
// Module-scope declarations // Module-scope declarations
@ -177,6 +194,7 @@ typedef struct {
bool checked; bool checked;
bool radioCheck; bool radioCheck;
bool enabled; bool enabled;
bool visible; // level==0: true = menu bar entry, false = popup-only
} BasFrmMenuItemT; } BasFrmMenuItemT;
@ -241,6 +259,7 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char
static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc); static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc);
int32_t basPromptSave(const char *title); int32_t basPromptSave(const char *title);
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name);
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
void CommClose(int32_t handle); void CommClose(int32_t handle);
void CommDetach(int32_t handle); void CommDetach(int32_t handle);
@ -592,7 +611,8 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
if (!ctrl) { if (!ctrl) {
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
"Method call on unknown control", "Method call on unknown control",
"Method: %s\nControl was not found on any loaded form.", "Control: %s\nMethod: %s\nControl was not found on any loaded form.",
(rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?",
methodName ? methodName : "?"); methodName ? methodName : "?");
return zeroValue(); return zeroValue();
} }
@ -765,6 +785,24 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
basStringUnref(s); basStringUnref(s);
} }
return zeroValue(); 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();
} }
} }
} }
@ -1131,6 +1169,12 @@ void basFormRtDestroy(BasFormRtT *rt) {
arrfree(form->menuIdMap); arrfree(form->menuIdMap);
for (int32_t j = 0; j < form->popupMenuCount; j++) {
wmFreeMenu(form->popupMenus[j].menu);
}
arrfree(form->popupMenus);
if (form->window) { if (form->window) {
dvxDestroyWindow(rt->ctx, form->window); dvxDestroyWindow(rt->ctx, form->window);
form->window = NULL; form->window = NULL;
@ -1168,6 +1212,12 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef; BasFormT *form = (BasFormT *)formRef;
// 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);
}
if (!form) { if (!form) {
return NULL; return NULL;
} }
@ -1184,6 +1234,8 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
} }
} }
// (local miss is expected for cross-form access; don't spam the log)
// Search menu items on the current form // Search menu items on the current form
for (int32_t i = 0; i < form->menuIdMapCount; i++) { for (int32_t i = 0; i < form->menuIdMapCount; i++) {
if (strcasecmp(form->menuIdMap[i].name, ctrlName) == 0) { if (strcasecmp(form->menuIdMap[i].name, ctrlName) == 0) {
@ -1243,9 +1295,14 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index) { void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index) {
(void)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef; BasFormT *form = (BasFormT *)formRef;
if (rt && ctrlName) {
snprintf(rt->lastLookupName, sizeof(rt->lastLookupName),
"%s(%d)", ctrlName, (int)index);
}
if (!form) { if (!form) {
return NULL; return NULL;
} }
@ -1385,7 +1442,8 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
if (!ctrl) { if (!ctrl) {
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
"Read of unknown control", "Read of unknown control",
"Property: %s\nControl was not found on any loaded form.", "Control: %s\nProperty: %s\nControl was not found on any loaded form.",
(rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?",
propName ? propName : "?"); propName ? propName : "?");
return zeroValue(); return zeroValue();
} }
@ -1667,12 +1725,11 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
} }
// Check the .frm cache for reload after unload. // Check the .frm cache for reload after unload.
// Use a static guard to prevent recursion: basFormRtLoadFrm calls // sLoadingFrm (module-scope) prevents recursion: basFormRtLoadFrm
// basFormRtLoadForm for the "Begin Form" line, which would re-enter // calls basFormRtLoadForm via frmLoad_onFormBegin, which would
// this function. The guard lets the recursive call fall through to // re-enter this function. The guard lets the recursive call fall
// bare form creation, which basFormRtLoadFrm then populates. // through to bare form creation, which basFormRtLoadFrm then
static bool sLoadingFrm = false; // populates.
if (!sLoadingFrm) { if (!sLoadingFrm) {
for (int32_t i = 0; i < rt->frmCacheCount; i++) { for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) { if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
@ -1786,7 +1843,19 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
cbs.onCtrlEnd = frmLoad_onCtrlEnd; cbs.onCtrlEnd = frmLoad_onCtrlEnd;
cbs.onCtrlProp = frmLoad_onCtrlProp; cbs.onCtrlProp = frmLoad_onCtrlProp;
if (!frmParse(source, sourceLen, &cbs)) { // Set the guard so nested basFormRtLoadForm (fired from
// frmLoad_onFormBegin) skips the form's init code -- running it
// here would fire against an empty control list. The outer
// caller (IDE runModule or basFormRtLoadAllForms) runs module-
// level / init code after parsing finishes. Save+restore so
// nested basFormRtLoadForm -> basFormRtLoadFrm reentry still
// terminates the inner guard correctly.
bool savedLoadingFrm = sLoadingFrm;
sLoadingFrm = true;
bool parsed = frmParse(source, sourceLen, &cbs);
sLoadingFrm = savedLoadingFrm;
if (!parsed) {
arrfree(ctx.menuItems); arrfree(ctx.menuItems);
return NULL; return NULL;
} }
@ -1801,59 +1870,93 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout); form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout);
} }
// Build menu bar from accumulated menu items // 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.
int32_t menuCount = (int32_t)arrlen(menuItems); int32_t menuCount = (int32_t)arrlen(menuItems);
if (menuCount > 0 && form->window) { if (menuCount > 0 && form->window) {
MenuBarT *bar = wmAddMenuBar(form->window); MenuBarT *bar = NULL;
bool haveVisibleTop = false;
if (bar) { for (int32_t i = 0; i < menuCount; i++) {
#define MENU_ID_BASE 10000 if (menuItems[i].level == 0 && menuItems[i].visible) {
MenuT *menuStack[16]; haveVisibleTop = true;
memset(menuStack, 0, sizeof(menuStack)); break;
for (int32_t i = 0; i < menuCount; i++) {
BasFrmMenuItemT *mi = &menuItems[i];
bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
// Top-level menu header
menuStack[0] = wmAddMenu(bar, mi->caption);
} else if (isSep && mi->level > 0 && menuStack[mi->level - 1]) {
// Separator
wmAddMenuSeparator(menuStack[mi->level - 1]);
} else if (isSubParent && mi->level > 0 && menuStack[mi->level - 1]) {
// Submenu parent
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (mi->level > 0 && menuStack[mi->level - 1]) {
// Regular menu item
int32_t id = MENU_ID_BASE + i;
if (mi->radioCheck) {
wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
} else if (mi->checked) {
wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
} else {
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
}
if (!mi->enabled) {
wmMenuItemSetEnabled(bar, id, false);
}
// Store ID-to-name mapping for event dispatch
BasMenuIdMapT map;
memset(&map, 0, sizeof(map));
map.id = id;
snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
arrput(form->menuIdMap, map);
form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
}
} }
form->window->onMenu = onFormMenu;
} }
if (haveVisibleTop) {
bar = wmAddMenuBar(form->window);
}
#define MENU_ID_BASE 10000
MenuT *menuStack[16];
memset(menuStack, 0, sizeof(menuStack));
bool topIsPopup = false;
MenuT *curTopPopup = NULL;
for (int32_t i = 0; i < menuCount; i++) {
BasFrmMenuItemT *mi = &menuItems[i];
bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
if (mi->visible) {
// Top-level menu header on the bar
menuStack[0] = bar ? wmAddMenu(bar, mi->caption) : NULL;
topIsPopup = false;
curTopPopup = NULL;
} else {
// Popup menu: allocate standalone, store by name.
curTopPopup = wmCreateMenu();
menuStack[0] = curTopPopup;
topIsPopup = true;
if (curTopPopup) {
BasFrmPopupMenuT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
entry.menu = curTopPopup;
arrput(form->popupMenus, entry);
form->popupMenuCount = (int32_t)arrlen(form->popupMenus);
}
}
} else if (isSep && menuStack[mi->level - 1]) {
wmAddMenuSeparator(menuStack[mi->level - 1]);
} else if (isSubParent && menuStack[mi->level - 1]) {
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (menuStack[mi->level - 1]) {
int32_t id = MENU_ID_BASE + i;
if (mi->radioCheck) {
wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
} else if (mi->checked) {
wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
} else {
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
}
if (!mi->enabled && bar && !topIsPopup) {
wmMenuItemSetEnabled(bar, id, false);
}
// 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_CTRL_NAME, "%s", mi->name);
arrput(form->menuIdMap, map);
form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
}
}
form->window->onMenu = onFormMenu;
} }
arrfree(menuItems); arrfree(menuItems);
@ -2021,6 +2124,16 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const cha
// control type, missing control reference, unknown method, etc. // control type, missing control reference, unknown method, etc.
// Silent no-ops here would hide these failures from the developer. // Silent no-ops here would hide these failures from the developer.
void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...) { 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.
if (rt && rt->terminated) {
return;
}
char details[512]; char details[512];
va_list ap; va_list ap;
@ -2028,54 +2141,94 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
vsnprintf(details, sizeof(details), detailFmt, ap); vsnprintf(details, sizeof(details), detailFmt, ap);
va_end(ap); va_end(ap);
// Log with a blank line and indent so it stands out among normal // Log the error only when we're also going to show the modal
// log traffic. // dialog. Hosts that suppress the dialog (the IDE) do their own
dvxLog(""); // reporting via the Output pane and editor navigation, and don't
dvxLog("BASIC RUNTIME ERROR: %s", summary ? summary : "(no summary)"); // 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. // Split details on newlines and indent each one.
const char *p = details; const char *p = details;
while (*p) { while (*p) {
const char *nl = strchr(p, '\n'); const char *nl = strchr(p, '\n');
int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p); int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p);
char line[256]; char line[256];
if (len >= (int32_t)sizeof(line)) { if (len >= (int32_t)sizeof(line)) {
len = sizeof(line) - 1; len = sizeof(line) - 1;
}
memcpy(line, p, len);
line[len] = '\0';
dvxLog(" %s", line);
if (!nl) {
break;
}
p = nl + 1;
} }
memcpy(line, p, len);
line[len] = '\0';
dvxLog(" %s", line);
if (!nl) {
break;
}
p = nl + 1;
} }
if (rt && rt->ctx) { // 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;
// Format: "Summary\nDetails". No embedded "on line N:" so the
// host (IDE) can prefix its own proc/line header without
// 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);
// Snapshot PC and source line at the error site so the
// debugger can locate the owning SUB and line. runSubLoop
// restores vm->pc to the caller's saved PC on the way out (to
// keep RESUME semantics clean for ON ERROR), and
// vm->currentLine keeps being stomped by OP_LINE as other
// queued event handlers run. Both would otherwise be wrong
// by the time the IDE reads them after the loop exits.
//
// Prefer vm->currentOpPc over vm->pc: basVmStep advances
// vm->pc past the opcode AND its operands before dispatching
// the handler, so by the time we get here it can point at
// the first byte of the NEXT proc, which makes the proc
// lookup land on the wrong handler. currentOpPc is the PC
// 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[640]; char boxMsg[640];
snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s", snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s",
summary ? summary : "Runtime error", summary ? summary : "Runtime error",
details); details);
dvxMessageBox(rt->ctx, "BASIC Runtime Error", boxMsg, MB_OK | MB_ICONERROR); dvxMessageBox(rt->ctx, "BASIC Runtime Error", boxMsg, MB_OK | MB_ICONERROR);
} }
// Halt the VM so execution stops here rather than stumbling forward
// with a corrupted state. Without this, a single missing control
// can trigger a cascade of follow-on errors as subsequent property
// accesses and method calls fail too. The `terminated` flag tells
// basFormRtEventLoop to exit at its next pump -- otherwise the main
// window would stay open with a dead VM behind it.
if (rt) {
if (rt->vm) {
rt->vm->running = false;
}
rt->terminated = true;
}
} }
@ -2157,10 +2310,16 @@ void basFormRtRunSimple(BasFormRtT *rt) {
} else if (result == BAS_VM_YIELDED) { } else if (result == BAS_VM_YIELDED) {
// DoEvents returned, continue // DoEvents returned, continue
} else if (result == BAS_VM_ERROR) { } else if (result == BAS_VM_ERROR) {
const char *errMsg = basVmGetError(vm); // basFormRtRuntimeError already surfaces its own error
char buf[512]; // dialog and sets rt->terminated, so only show this generic
snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error"); // box for VM-internal errors (division by zero, type
dvxMessageBox(rt->ctx, "Error", buf, 0); // mismatch, etc.) that didn't come through that path.
if (!rt->terminated) {
const char *errMsg = basVmGetError(vm);
char buf[512];
snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error");
dvxMessageBox(rt->ctx, "Error", buf, 0);
}
break; break;
} else { } else {
break; break;
@ -2206,7 +2365,8 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
if (!ctrl) { if (!ctrl) {
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
"Assignment to unknown control", "Assignment to unknown control",
"Property: %s\nControl was not found on any loaded form.", "Control: %s\nProperty: %s\nControl was not found on any loaded form.",
(rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?",
propName ? propName : "?"); propName ? propName : "?");
return; return;
} }
@ -2313,9 +2473,29 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
return; return;
} }
if (strcasecmp(propName, "ContextMenu") == 0) {
BasStringT *s = basValFormatString(value);
MenuT *m = NULL;
if (s->len > 0) {
BasFrmPopupMenuT *pm = findPopupMenu(frm, s->data);
if (pm) {
m = pm->menu;
}
}
if (win) {
win->contextMenu = m;
}
basStringUnref(s);
return;
}
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
"Unknown form property", "Unknown form property",
"Form: %s\nProperty: %s\nValid: Caption, Visible, Width, Height, Left, Top, Resizable, AutoSize, Centered.", "Form: %s\nProperty: %s\nValid: Caption, Visible, Width, Height, Left, Top, Resizable, AutoSize, Centered, ContextMenu.",
frm->name, propName ? propName : "?"); frm->name, propName ? propName : "?");
return; return;
} }
@ -2443,6 +2623,14 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
arrfree(form->controls); arrfree(form->controls);
form->controls = NULL; form->controls = NULL;
for (int32_t i = 0; i < form->popupMenuCount; i++) {
wmFreeMenu(form->popupMenus[i].menu);
}
arrfree(form->popupMenus);
form->popupMenus = NULL;
form->popupMenuCount = 0;
if (form->window) { if (form->window) {
if (rt->ctx->modalWindow == form->window) { if (rt->ctx->modalWindow == form->window) {
rt->ctx->modalWindow = NULL; rt->ctx->modalWindow = NULL;
@ -2555,6 +2743,40 @@ int32_t basPromptSave(const char *title) {
} }
// findPopupMenu -- look up a named popup menu on a form. Returns
// NULL if no such menu exists.
static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name) {
if (!form || !name) {
return NULL;
}
for (int32_t i = 0; i < form->popupMenuCount; i++) {
if (strcasecmp(form->popupMenus[i].name, name) == 0) {
return &form->popupMenus[i];
}
}
return NULL;
}
// 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 = 10000 - 1; // MENU_ID_BASE - 1
for (int32_t i = 0; i < form->menuIdMapCount; i++) {
if (form->menuIdMap[i].id > maxId) {
maxId = form->menuIdMap[i].id;
}
}
return maxId + 1;
}
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) { static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) {
if (strcasecmp(methodName, "SetFocus") == 0) { if (strcasecmp(methodName, "SetFocus") == 0) {
wgtSetFocused(ctrl->widget); wgtSetFocused(ctrl->widget);
@ -2584,6 +2806,341 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue(); return zeroValue();
} }
// ========================================================
// Popup / context menu methods
// ========================================================
//
// Any control (and the synthetic form control) can show, build,
// or tear down popup menus owned by its form. Menus are named
// by string and stored on the form; IDs are shared with the
// menu-bar items so nameClick handlers fire regardless of which
// surface the user picked from.
BasFormT *menuForm = ctrl ? ctrl->form : NULL;
if (strcasecmp(methodName, "PopupMenu") == 0) {
// PopupMenu name$ [, x%, y%]
if (!menuForm || !menuForm->window || argc < 1) {
return zeroValue();
}
BasStringT *s = basValFormatString(args[0]);
BasFrmPopupMenuT *pm = findPopupMenu(menuForm, s->data);
basStringUnref(s);
if (!pm || !pm->menu) {
return zeroValue();
}
int32_t screenX = sFormRt ? sFormRt->ctx->mouseX : 0;
int32_t screenY = sFormRt ? sFormRt->ctx->mouseY : 0;
if (argc >= 3) {
// Explicit coords are relative to the control if we have
// one with a widget; form-level PopupMenu treats them as
// client-area coords translated through the window.
int32_t x = (int32_t)basValToNumber(args[1]);
int32_t y = (int32_t)basValToNumber(args[2]);
if (ctrl && ctrl->widget) {
screenX = ctrl->widget->x + x;
screenY = ctrl->widget->y + y;
} else if (menuForm->window) {
screenX = menuForm->window->x + menuForm->window->contentX + x;
screenY = menuForm->window->y + menuForm->window->contentY + y;
}
}
if (sFormRt && sFormRt->ctx) {
dvxShowContextMenu(sFormRt->ctx, menuForm->window, pm->menu, screenX, screenY);
}
return zeroValue();
}
if (strcasecmp(methodName, "CreateMenu") == 0) {
// CreateMenu name$ -- allocate an empty named popup menu.
// No-op if a menu with that name already exists.
if (!menuForm || argc < 1) {
return zeroValue();
}
BasStringT *s = basValFormatString(args[0]);
if (!findPopupMenu(menuForm, s->data)) {
BasFrmPopupMenuT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", s->data);
entry.menu = wmCreateMenu();
arrput(menuForm->popupMenus, entry);
menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus);
}
basStringUnref(s);
return zeroValue();
}
if (strcasecmp(methodName, "AddMenuItem") == 0) {
// AddMenuItem parentMenu$, itemName$, caption$
if (!menuForm || argc < 3) {
return zeroValue();
}
BasStringT *parent = basValFormatString(args[0]);
BasStringT *itemN = basValFormatString(args[1]);
BasStringT *capN = basValFormatString(args[2]);
BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data);
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_CTRL_NAME, "%s", itemN->data);
arrput(menuForm->menuIdMap, map);
menuForm->menuIdMapCount = (int32_t)arrlen(menuForm->menuIdMap);
if (menuForm->window) {
menuForm->window->onMenu = onFormMenu;
}
}
basStringUnref(parent);
basStringUnref(itemN);
basStringUnref(capN);
return zeroValue();
}
if (strcasecmp(methodName, "AddMenuSeparator") == 0) {
// AddMenuSeparator parentMenu$
if (!menuForm || argc < 1) {
return zeroValue();
}
BasStringT *parent = basValFormatString(args[0]);
BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data);
if (pm && pm->menu) {
wmAddMenuSeparator(pm->menu);
}
basStringUnref(parent);
return zeroValue();
}
if (strcasecmp(methodName, "AddSubMenu") == 0) {
// AddSubMenu parentMenu$, childName$, caption$
// The new submenu can have AddMenuItem etc. called on it
// via `childName`.
if (!menuForm || argc < 3) {
return zeroValue();
}
BasStringT *parent = basValFormatString(args[0]);
BasStringT *childN = basValFormatString(args[1]);
BasStringT *capN = basValFormatString(args[2]);
BasFrmPopupMenuT *parentPm = findPopupMenu(menuForm, parent->data);
if (parentPm && parentPm->menu && !findPopupMenu(menuForm, childN->data)) {
MenuT *sub = wmAddSubMenu(parentPm->menu, capN->data);
if (sub) {
BasFrmPopupMenuT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.name, BAS_MAX_CTRL_NAME, "%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);
}
}
basStringUnref(parent);
basStringUnref(childN);
basStringUnref(capN);
return zeroValue();
}
// ========================================================
// Toolbar runtime methods
// ========================================================
//
// Toolbars hold buttons, separators, and other children laid
// out horizontally. These convenience methods create the new
// widget, register it on the form by name (so BASIC code can
// do tbNew.Enabled = False afterwards), and tuck it into the
// toolbar. Equivalent manual construction uses
// CreateControl(form, "ImageButton", "tbNew", toolbar) -- these
// just shorten the common case.
bool isToolbar = (ctrl && strcasecmp(ctrl->typeName, "Toolbar") == 0);
if (isToolbar && strcasecmp(methodName, "AddButton") == 0) {
// AddButton name$, iconPath$
//
// Routed entirely through the generic runtime-creation +
// property-set path so formrt doesn't link against the
// ImageButton widget directly. CreateControl("ImageButton")
// resolves the widget via wgtFindByBasName at call time, and
// setProp "Picture" dispatches to whichever setter the
// ImageButton DXE registered in its iface.
if (!menuForm || !ctrl->widget || argc < 2) {
return zeroValue();
}
BasStringT *nameS = basValFormatString(args[0]);
BasStringT *pathS = basValFormatString(args[1]);
BasControlT *btn = (BasControlT *)basFormRtCreateCtrlEx(sFormRt, menuForm, "ImageButton", nameS->data, ctrl);
if (btn) {
BasValueT v = basValString(pathS);
basFormRtSetProp(sFormRt, btn, "Picture", v);
basValRelease(&v);
}
basStringUnref(nameS);
basStringUnref(pathS);
return zeroValue();
}
if (isToolbar && strcasecmp(methodName, "AddTextButton") == 0) {
// AddTextButton name$, caption$
if (!menuForm || !ctrl->widget || argc < 2) {
return zeroValue();
}
BasStringT *nameS = basValFormatString(args[0]);
BasStringT *capS = basValFormatString(args[1]);
BasControlT *btn = (BasControlT *)basFormRtCreateCtrlEx(sFormRt, menuForm, "CommandButton", nameS->data, ctrl);
if (btn) {
BasValueT v = basValString(capS);
basFormRtSetProp(sFormRt, btn, "Caption", v);
basValRelease(&v);
}
basStringUnref(nameS);
basStringUnref(capS);
return zeroValue();
}
if (isToolbar && strcasecmp(methodName, "AddSeparator") == 0) {
// "Line" resolves to the separator widget via the iface
// registry. The separator widget auto-orients to its parent
// (vertical inside a horizontal container, horizontal inside
// a vertical one), so we don't need a distinct VLine basName.
if (menuForm && ctrl->widget) {
basFormRtCreateCtrlEx(sFormRt, menuForm, "Line", "", ctrl);
wgtInvalidate(ctrl->widget);
}
return zeroValue();
}
if (isToolbar && strcasecmp(methodName, "Clear") == 0) {
// Destroy every child. Also remove their BasControlT
// entries from the form so stale name lookups don't linger.
if (!menuForm || !ctrl->widget) {
return zeroValue();
}
WidgetT *child = ctrl->widget->firstChild;
while (child) {
WidgetT *next = child->nextSibling;
// Remove matching BasControlT from the form's control
// list so its name becomes unknown again.
for (int32_t i = 0; i < (int32_t)arrlen(menuForm->controls); i++) {
if (menuForm->controls[i]->widget == child) {
free(menuForm->controls[i]->tooltip);
free(menuForm->controls[i]);
arrdel(menuForm->controls, i);
break;
}
}
wgtDestroy(child);
child = next;
}
wgtInvalidate(ctrl->widget);
return zeroValue();
}
if (isToolbar && strcasecmp(methodName, "ButtonCount") == 0) {
int32_t count = 0;
if (ctrl->widget) {
for (WidgetT *c = ctrl->widget->firstChild; c; c = c->nextSibling) {
count++;
}
}
return basValLong(count);
}
if (strcasecmp(methodName, "DestroyMenu") == 0) {
// DestroyMenu name$ -- free a top-level popup menu and
// remove its entry. Submenus are freed recursively as
// part of their owning parent; calling DestroyMenu on a
// submenu name only removes the lookup entry.
if (!menuForm || argc < 1) {
return zeroValue();
}
BasStringT *s = basValFormatString(args[0]);
int32_t foundIdx = -1;
for (int32_t i = 0; i < menuForm->popupMenuCount; i++) {
if (strcasecmp(menuForm->popupMenus[i].name, s->data) == 0) {
foundIdx = i;
break;
}
}
if (foundIdx >= 0) {
MenuT *m = menuForm->popupMenus[foundIdx].menu;
// Only free if this is a root (not a submenu whose
// parent also has a pointer). Heuristic: check if any
// other popup entry contains m as a descendant. Safe
// alternative: if m appears elsewhere in the list, treat
// the first occurrence as the root. For now we free
// only if no other popup has the same MenuT*, which
// handles the common case where a submenu is registered
// under a separate name but still owned by its parent.
bool isSubmenu = false;
for (int32_t i = 0; i < menuForm->popupMenuCount; i++) {
if (i != foundIdx && menuForm->popupMenus[i].menu == m) {
isSubmenu = true;
break;
}
}
if (!isSubmenu) {
wmFreeMenu(m);
}
arrdel(menuForm->popupMenus, foundIdx);
menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus);
}
basStringUnref(s);
return zeroValue();
}
// Unknown method: raise a loud runtime error (visible MessageBox + // Unknown method: raise a loud runtime error (visible MessageBox +
// DVX.LOG entry). Silent no-ops here used to hide typos in method // DVX.LOG entry). Silent no-ops here used to hide typos in method
// names. This is a runtime safety net -- the parser should reject // names. This is a runtime safety net -- the parser should reject
@ -3108,6 +3665,12 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) { static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
dvxLog("onCtrlBegin: type='%s' name='%s' form=%s nestDepth=%d",
typeName ? typeName : "?",
name ? name : "?",
(ctx->form && ctx->form->name[0]) ? ctx->form->name : "(null)",
(int)ctx->nestDepth);
if (!ctx->form || ctx->nestDepth <= 0) { if (!ctx->form || ctx->nestDepth <= 0) {
return; return;
} }
@ -3316,6 +3879,7 @@ static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level)
snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", name); snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", name);
mi.level = level; mi.level = level;
mi.enabled = true; mi.enabled = true;
mi.visible = true;
arrput(ctx->menuItems, mi); arrput(ctx->menuItems, mi);
ctx->curMenuItemIdx = (int32_t)arrlen(ctx->menuItems) - 1; ctx->curMenuItemIdx = (int32_t)arrlen(ctx->menuItems) - 1;
ctx->current = NULL; ctx->current = NULL;
@ -3352,6 +3916,10 @@ static void frmLoad_onMenuProp(void *userData, const char *key, const char *valu
} else if (strcasecmp(key, "Enabled") == 0) { } else if (strcasecmp(key, "Enabled") == 0) {
// Default-true: anything not "False" enables the item. // Default-true: anything not "False" enables the item.
mip->enabled = (strcasecmp(text, "False") != 0); mip->enabled = (strcasecmp(text, "False") != 0);
} else if (strcasecmp(key, "Visible") == 0) {
// Top-level only: Visible=False marks the menu as a popup
// (not part of the menu bar). Ignored on nested items.
mip->visible = (strcasecmp(text, "False") != 0);
} }
} }
@ -4618,6 +5186,31 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
return true; return true;
} }
if (strcasecmp(propName, "ContextMenu") == 0) {
// Set the auto-right-click menu. Empty string clears it.
BasStringT *s = basValFormatString(value);
MenuT *m = NULL;
if (s->len > 0 && ctrl->form) {
BasFrmPopupMenuT *pm = findPopupMenu(ctrl->form, s->data);
if (pm) {
m = pm->menu;
}
}
if (ctrl->widget) {
ctrl->widget->contextMenu = m;
} else if (ctrl->form && ctrl->form->window && ctrl == &ctrl->form->formCtrl) {
// Form-level ContextMenu: attach to the window so clicks
// outside any child widget still pop up the menu.
ctrl->form->window->contextMenu = m;
}
basStringUnref(s);
return true;
}
return false; return false;
} }

View file

@ -62,6 +62,12 @@ typedef struct {
BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored) BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored)
} BasMenuIdMapT; } BasMenuIdMapT;
// Named popup / context menu owned by a form.
typedef struct {
char name[BAS_MAX_CTRL_NAME];
MenuT *menu; // standalone wmCreateMenu allocation, owned
} BasFrmPopupMenuT;
// ============================================================ // ============================================================
// Control instance (a widget on a form) // Control instance (a widget on a form)
// ============================================================ // ============================================================
@ -129,6 +135,13 @@ typedef struct BasFormT {
// Menu ID to name mapping (for event dispatch) // Menu ID to name mapping (for event dispatch)
BasMenuIdMapT *menuIdMap; BasMenuIdMapT *menuIdMap;
int32_t menuIdMapCount; int32_t menuIdMapCount;
// Named popup (context) menus. Allocated standalone via
// wmCreateMenu() either from .frm `Begin Menu` with Visible=False
// or at runtime via CreateMenu/AddMenuItem BASIC calls. Click
// dispatch reuses menuIdMap above so Foo_Click-style handlers
// work the same as for menu-bar items. Freed on form unload.
BasFrmPopupMenuT *popupMenus; // stb_ds array
int32_t popupMenuCount;
// Synthetic control entry for the form itself, so that // Synthetic control entry for the form itself, so that
// FormName.Property works through the same getProp/setProp path. // FormName.Property works through the same getProp/setProp path.
BasControlT formCtrl; BasControlT formCtrl;
@ -167,6 +180,21 @@ typedef struct {
// loop exits at the next pump so the app doesn't stumble forward // loop exits at the next pump so the app doesn't stumble forward
// with a halted VM. // with a halted VM.
bool terminated; bool terminated;
// Set true by hosts that want to handle runtime-error reporting
// themselves (the IDE shows the error in the output pane and
// navigates the editor to the line). When true,
// basFormRtRuntimeError still logs and halts but skips the modal
// MessageBox -- this removes a stray second OK press the IDE users
// were seeing when the dialog's event pump raced with other
// queued events.
bool suppressErrorDialog;
// Name of the most recent basFormRtFindCtrl lookup. The VM's
// OP_FIND_CTRL opcode discards the name string after the lookup,
// so when a subsequent OP_CALL_METHOD / OP_SET_PROP sees a NULL
// ctrl we'd otherwise have no way to tell the user WHICH control
// was missing. This is the last requested name; if the lookup
// succeeded this value is still the name of that control.
char lastLookupName[BAS_MAX_CTRL_NAME];
} BasFormRtT; } BasFormRtT;
// ============================================================ // ============================================================

View file

@ -616,6 +616,7 @@ static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level
snprintf(mi.name, DSGN_MAX_NAME, "%s", name); snprintf(mi.name, DSGN_MAX_NAME, "%s", name);
mi.level = level; mi.level = level;
mi.enabled = true; mi.enabled = true;
mi.visible = true;
arrput(ctx->form->menuItems, mi); arrput(ctx->form->menuItems, mi);
ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1; ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1;
ctx->current = NULL; ctx->current = NULL;
@ -653,6 +654,8 @@ static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *val
mip->radioCheck = frmParseBool(val); mip->radioCheck = frmParseBool(val);
} else if (strcasecmp(key, "Enabled") == 0) { } else if (strcasecmp(key, "Enabled") == 0) {
mip->enabled = frmParseBool(val); mip->enabled = frmParseBool(val);
} else if (strcasecmp(key, "Visible") == 0) {
mip->visible = (strcasecmp(val, "False") != 0);
} }
} }
@ -1038,6 +1041,17 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n"); pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n");
} }
// Visible is only meaningful on top-level menus -- False
// marks it as a popup (not on the menu bar). Never
// serialize the default True.
if (mi->level == 0 && !mi->visible) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Visible = False\n");
}
curLevel = mi->level + 1; curLevel = mi->level + 1;
} }

View file

@ -70,6 +70,7 @@ typedef struct {
bool checked; bool checked;
bool radioCheck; // true = radio bullet instead of checkmark bool radioCheck; // true = radio bullet instead of checkmark
bool enabled; // default true bool enabled; // default true
bool visible; // default true; false on a top-level item = popup-only (not on menu bar)
} DsgnMenuItemT; } DsgnMenuItemT;
// ============================================================ // ============================================================

View file

@ -603,6 +603,11 @@ static int32_t sOutputLen = 0;
// swaps directly between buffers with no splicing needed. // swaps directly between buffers with no splicing needed.
static char *sGeneralBuf = NULL; // (General) section: module-level code static char *sGeneralBuf = NULL; // (General) section: module-level code
static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
// Cached copy of the source last passed to parseProcs (a .bas file's
// contents, or a .frm's extracted code section). updateDropdowns
// scans this so sProcTable.lineNum is in the same line-number space
// prjMapLine returns -- getFullSource() packs blank lines and diverges.
static char *sParsedSource = NULL;
static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none) static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none) static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit
@ -625,7 +630,8 @@ static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
typedef struct { typedef struct {
char objName[64]; char objName[64];
char evtName[64]; char evtName[64];
int32_t lineNum; int32_t lineNum; // line of the SUB / FUNCTION declaration
int32_t endLineNum; // line of the END SUB / END FUNCTION
} IdeProcEntryT; } IdeProcEntryT;
static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array
@ -1529,11 +1535,22 @@ static const char *ideValidator_lookupCtrlType(void *ctx, const char *ctrlName)
// Methods that exist on every widget via callCommonMethod() in formrt.c. // Methods that exist on every widget via callCommonMethod() in formrt.c.
static bool ideValidator_isCommonMethod(const char *methodName) { static bool ideValidator_isCommonMethod(const char *methodName) {
return strcasecmp(methodName, "SetFocus") == 0 || return strcasecmp(methodName, "SetFocus") == 0 ||
strcasecmp(methodName, "Refresh") == 0 || strcasecmp(methodName, "Refresh") == 0 ||
strcasecmp(methodName, "SetReadOnly") == 0 || strcasecmp(methodName, "SetReadOnly") == 0 ||
strcasecmp(methodName, "SetEnabled") == 0 || strcasecmp(methodName, "SetEnabled") == 0 ||
strcasecmp(methodName, "SetVisible") == 0; strcasecmp(methodName, "SetVisible") == 0 ||
strcasecmp(methodName, "PopupMenu") == 0 ||
strcasecmp(methodName, "CreateMenu") == 0 ||
strcasecmp(methodName, "AddMenuItem") == 0 ||
strcasecmp(methodName, "AddMenuSeparator") == 0 ||
strcasecmp(methodName, "AddSubMenu") == 0 ||
strcasecmp(methodName, "DestroyMenu") == 0 ||
strcasecmp(methodName, "AddButton") == 0 ||
strcasecmp(methodName, "AddTextButton") == 0 ||
strcasecmp(methodName, "AddSeparator") == 0 ||
strcasecmp(methodName, "Clear") == 0 ||
strcasecmp(methodName, "ButtonCount") == 0;
} }
@ -1560,6 +1577,7 @@ static bool ideValidator_isCommonProp(const char *propName) {
strcasecmp(propName, "Text") == 0 || strcasecmp(propName, "Text") == 0 ||
strcasecmp(propName, "HelpTopic") == 0 || strcasecmp(propName, "HelpTopic") == 0 ||
strcasecmp(propName, "ToolTipText") == 0 || strcasecmp(propName, "ToolTipText") == 0 ||
strcasecmp(propName, "ContextMenu") == 0 ||
strcasecmp(propName, "DataSource") == 0 || strcasecmp(propName, "DataSource") == 0 ||
strcasecmp(propName, "DataField") == 0 || strcasecmp(propName, "DataField") == 0 ||
strcasecmp(propName, "ListCount") == 0; strcasecmp(propName, "ListCount") == 0;
@ -1620,25 +1638,28 @@ static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char
// Form-level properties // Form-level properties
if (strcasecmp(wgtType, "Form") == 0) { if (strcasecmp(wgtType, "Form") == 0) {
return strcasecmp(propName, "Name") == 0 || return strcasecmp(propName, "Name") == 0 ||
strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Caption") == 0 ||
strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "Width") == 0 ||
strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "Height") == 0 ||
strcasecmp(propName, "Left") == 0 || strcasecmp(propName, "Left") == 0 ||
strcasecmp(propName, "Top") == 0 || strcasecmp(propName, "Top") == 0 ||
strcasecmp(propName, "Visible") == 0 || strcasecmp(propName, "Visible") == 0 ||
strcasecmp(propName, "Resizable") == 0 || strcasecmp(propName, "Resizable") == 0 ||
strcasecmp(propName, "AutoSize") == 0 || strcasecmp(propName, "AutoSize") == 0 ||
strcasecmp(propName, "Centered") == 0 || strcasecmp(propName, "Centered") == 0 ||
strcasecmp(propName, "Layout") == 0; strcasecmp(propName, "ContextMenu") == 0 ||
strcasecmp(propName, "Layout") == 0;
} }
// Menu items // Menu items
if (strcasecmp(wgtType, "Menu") == 0) { if (strcasecmp(wgtType, "Menu") == 0) {
return strcasecmp(propName, "Name") == 0 || return strcasecmp(propName, "Name") == 0 ||
strcasecmp(propName, "Checked") == 0 || strcasecmp(propName, "Checked") == 0 ||
strcasecmp(propName, "Enabled") == 0 || strcasecmp(propName, "Enabled") == 0 ||
strcasecmp(propName, "Caption") == 0; strcasecmp(propName, "Visible") == 0 || // top-level: False = popup
strcasecmp(propName, "RadioCheck") == 0 ||
strcasecmp(propName, "Caption") == 0;
} }
if (ideValidator_isCommonProp(propName)) { if (ideValidator_isCommonProp(propName)) {
@ -2135,30 +2156,36 @@ static void debugNavigateToLine(int32_t concatLine) {
int32_t fileIdx = -1; int32_t fileIdx = -1;
int32_t localLine = concatLine; int32_t localLine = concatLine;
// Map concatenated line to file and file-local line (code section) // Map concatenated line to file and file-local line (code section).
// prjMapLine already returns a line relative to the code section
// (startLine in the source map is recorded AFTER the injected
// BEGINFORM directive), so no further adjustment is needed. The
// compile-error path uses the result as-is and this path should too.
if (sProject.sourceMapCount > 0) { if (sProject.sourceMapCount > 0) {
prjMapLine(&sProject, concatLine, &fileIdx, &localLine); prjMapLine(&sProject, concatLine, &fileIdx, &localLine);
// For .frm files, subtract the injected BEGINFORM line
if (fileIdx >= 0 && fileIdx < sProject.fileCount &&
sProject.files[fileIdx].isForm) {
localLine--;
}
} }
// Find which procedure we're in using the VM's PC and the compiled // Find which procedure we're in using the VM's PC and the compiled
// module's proc table, then build a dot-separated name for navigateToCodeLine. // module's proc table, then build a dot-separated name for navigateToCodeLine.
// When a runtime error has halted the program, vm->pc has already
// been restored by runSubLoop to the caller's saved PC (outside the
// event handler that actually faulted), so use the snapshot
// captured in vm->errorPc instead. Without this, the proc lookup
// lands on whatever sub owns the saved PC -- typically "(General)"
// or the wrong handler -- and the editor jumps to the wrong place.
const char *procName = NULL; const char *procName = NULL;
char procBuf[128]; char procBuf[128];
if (sVm && sDbgModule) { if (sVm && sDbgModule) {
const char *compiledName = NULL; const char *compiledName = NULL;
int32_t bestAddr = -1; int32_t bestAddr = -1;
int32_t lookupPc = (sVm->errorMsg[0] && sVm->errorPc > 0)
? sVm->errorPc : sVm->pc;
for (int32_t i = 0; i < sDbgModule->procCount; i++) { for (int32_t i = 0; i < sDbgModule->procCount; i++) {
int32_t addr = sDbgModule->procs[i].codeAddr; int32_t addr = sDbgModule->procs[i].codeAddr;
if (addr <= sVm->pc && addr > bestAddr) { if (addr <= lookupPc && addr > bestAddr) {
bestAddr = addr; bestAddr = addr;
compiledName = sDbgModule->procs[i].name; compiledName = sDbgModule->procs[i].name;
} }
@ -3313,6 +3340,9 @@ static void freeProcBufs(void) {
free(sGeneralBuf); free(sGeneralBuf);
sGeneralBuf = NULL; sGeneralBuf = NULL;
free(sParsedSource);
sParsedSource = NULL;
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) { for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
free(sProcBufs[i]); free(sProcBufs[i]);
} }
@ -3747,6 +3777,8 @@ static void handleRunCmd(int32_t cmd) {
case CMD_OUTPUT_TO_LOG: case CMD_OUTPUT_TO_LOG:
if (sWin && sWin->menuBar) { if (sWin && sWin->menuBar) {
sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG); sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG);
prefsSetBool(sPrefs, "run", "outputToLog", sOutputToLog);
prefsSave(sPrefs);
} }
break; break;
@ -3914,10 +3946,9 @@ static void handleWindowCmd(int32_t cmd) {
case CMD_WIN_TOOLBOX: case CMD_WIN_TOOLBOX:
if (!sToolboxWin) { if (!sToolboxWin) {
sToolboxWin = tbxCreate(sAc, &sDesigner); sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom());
if (sToolboxWin) { if (sToolboxWin) {
sToolboxWin->y = toolbarBottom();
sToolboxWin->onMenu = onMenu; sToolboxWin->onMenu = onMenu;
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
} }
@ -4759,7 +4790,11 @@ static void loadFormCodeIntoEditor(void) {
// form runtime for execution. // form runtime for execution.
static void loadFrmFiles(BasFormRtT *rt) { static void loadFrmFiles(BasFormRtT *rt) {
dvxLog("loadFrmFiles: fileCount=%d", (int)sProject.fileCount);
for (int32_t i = 0; i < sProject.fileCount; i++) { for (int32_t i = 0; i < sProject.fileCount; i++) {
dvxLog(" file[%d] path=%s isForm=%d", (int)i, sProject.files[i].path, (int)sProject.files[i].isForm);
if (!sProject.files[i].isForm) { if (!sProject.files[i].isForm) {
continue; continue;
} }
@ -4770,6 +4805,8 @@ static void loadFrmFiles(BasFormRtT *rt) {
int32_t bytesRead = 0; int32_t bytesRead = 0;
char *frmBuf = platformReadFile(fullPath, &bytesRead); char *frmBuf = platformReadFile(fullPath, &bytesRead);
dvxLog(" loadFrmFiles: fullPath=%s bytes=%d frmBuf=%s", fullPath, (int)bytesRead, frmBuf ? "ok" : "null");
if (!frmBuf) { if (!frmBuf) {
continue; continue;
} }
@ -4782,6 +4819,9 @@ static void loadFrmFiles(BasFormRtT *rt) {
BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead); BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
free(frmBuf); free(frmBuf);
dvxLog(" loadFrmFiles: basFormRtLoadFrm returned form='%s'",
form && form->name[0] ? form->name : "(null)");
// Cache the form object name in the project file entry // Cache the form object name in the project file entry
if (form && form->name[0]) { if (form && form->name[0]) {
snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name); snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name);
@ -7035,6 +7075,9 @@ static void openProject(void) {
static void parseProcs(const char *source) { static void parseProcs(const char *source) {
freeProcBufs(); freeProcBufs();
free(sParsedSource);
sParsedSource = source ? strdup(source) : NULL;
if (!source) { if (!source) {
sGeneralBuf = strdup(""); sGeneralBuf = strdup("");
return; return;
@ -7597,6 +7640,16 @@ static void runModule(BasModuleT *mod) {
// Create form runtime (bridges UI opcodes to DVX widgets) // Create form runtime (bridges UI opcodes to DVX widgets)
BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod); BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);
// 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.
if (formRt) {
formRt->suppressErrorDialog = true;
}
sVm = vm; sVm = vm;
sDbgFormRt = formRt; sDbgFormRt = formRt;
sDbgModule = mod; sDbgModule = mod;
@ -7673,18 +7726,18 @@ static void runModule(BasModuleT *mod) {
break; break;
} }
// Runtime error — navigate to error line // Module-level runtime error. For VM-internal errors (e.g.
int32_t pos = sOutputLen; // division by zero) there's no vm->errorLine set, so fall
int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm)); // back to vm->currentLine -- but copy it into errorLine so
sOutputLen += n; // the shared post-loop handler below can format and navigate
setOutputText(sOutputBuf); // 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->currentLine > 0 && sEditor) { if (vm->errorLine == 0 && vm->currentLine > 0) {
wgtTextAreaGoToLine(sEditor, vm->currentLine); vm->errorLine = vm->currentLine;
if (sCodeWin && !sCodeWin->visible) {
dvxShowWindow(sAc, sCodeWin);
}
} }
break; break;
@ -7720,6 +7773,84 @@ static void runModule(BasModuleT *mod) {
} }
} }
// A runtime error fired from a host callback (e.g. setting a
// property on an unknown control) halts execution by writing
// vm->errorMsg and setting vm->ended. basVmRun surfaces that as
// BAS_VM_ERROR for module-level code, but errors raised inside an
// event handler just break the loop above. Either way, report
// the message and navigate the editor to the offending line so
// the user can fix it. debugNavigateToLine depends on sVm and
// sDbgModule, so it has to run BEFORE the cleanup block below.
// 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;
if (hadRuntimeError) {
// Map the concatenated line to a file / proc-local line the
// same way compile errors do, so the Output message matches
// what the editor is actually showing.
int32_t errFileIdx = -1;
int32_t errLocalLine = errorLine;
const char *errFile = "";
if (errorLine > 0 && sProject.fileCount > 0 && sProject.sourceMapCount > 0) {
prjMapLine(&sProject, errorLine, &errFileIdx, &errLocalLine);
if (errFileIdx >= 0 && errFileIdx < sProject.fileCount) {
errFile = sProject.files[errFileIdx].path;
}
}
// Activate the file (populates sProcTable for that file), then
// find the proc whose body actually contains errLocalLine.
// A simple "last proc with lineNum <= errLocalLine" match is
// wrong when the error is module-level code between procs:
// it lands on whichever proc just finished instead of
// correctly reporting that the error is outside any sub.
// Use the proc's endLineNum to gate membership.
char procName[128] = {0};
int32_t procLine = errLocalLine;
if (errFileIdx >= 0) {
activateFile(errFileIdx, ViewCodeE);
int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
if (errLocalLine >= sProcTable[i].lineNum &&
errLocalLine <= sProcTable[i].endLineNum) {
snprintf(procName, sizeof(procName), "%s.%s",
sProcTable[i].objName, sProcTable[i].evtName);
procLine = errLocalLine - sProcTable[i].lineNum + 1;
break;
}
}
navigateToCodeLine(errFileIdx, errLocalLine, procName[0] ? procName : NULL, false);
}
int32_t pos = sOutputLen;
int32_t n;
if (procName[0]) {
n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos,
"\nRUNTIME ERROR:\n%s line %d: %s\n",
procName, (int)procLine, vm->errorMsg);
} else if (errFile[0]) {
n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos,
"\nRUNTIME ERROR:\n%s line %d: %s\n",
errFile, (int)errLocalLine, vm->errorMsg);
} else {
n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos,
"\nRUNTIME ERROR:\nline %d: %s\n",
(int)errLocalLine, vm->errorMsg);
}
sOutputLen += n;
}
sVm = NULL; sVm = NULL;
sDbgFormRt = NULL; sDbgFormRt = NULL;
sDbgModule = NULL; sDbgModule = NULL;
@ -7739,14 +7870,28 @@ static void runModule(BasModuleT *mod) {
updateProjectMenuState(); updateProjectMenuState();
setOutputText(sOutputBuf); setOutputText(sOutputBuf);
setStatus("Done."); setStatus(hadRuntimeError ? "Runtime error." : "Done.");
// Restore IDE windows // Restore IDE windows. On a runtime error we want the code
if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); } // window on top so the user lands on the offending line; don't
if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); } // pop the form designer / toolbox / project back up, since that
if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); } // would cover the editor (dvxShowWindow raises). Output window
if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); } // stays up too so the error text is visible.
if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); } if (hadRuntimeError) {
if (sCodeWin && !sCodeWin->visible) {
dvxShowWindow(sAc, sCodeWin);
}
if (sCodeWin) {
dvxRaiseWindow(sAc, sCodeWin);
}
} else {
if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); }
if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); }
if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); }
if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); }
if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); }
}
// Repaint to clear destroyed runtime forms and restore designer // Repaint to clear destroyed runtime forms and restore designer
dvxUpdate(sAc); dvxUpdate(sAc);
@ -8863,10 +9008,9 @@ static void switchToDesign(void) {
// Create toolbox and properties windows // Create toolbox and properties windows
if (!sToolboxWin) { if (!sToolboxWin) {
sToolboxWin = tbxCreate(sAc, &sDesigner); sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom());
if (sToolboxWin) { if (sToolboxWin) {
sToolboxWin->y = toolbarBottom();
sToolboxWin->onMenu = onMenu; sToolboxWin->onMenu = onMenu;
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
} }
@ -9273,8 +9417,16 @@ static void updateDropdowns(void) {
return; return;
} }
// Scan the reassembled full source // Scan the ORIGINAL parsed source (a .bas file or a .frm's code
const char *src = getFullSource(); // section) so line numbers match what prjMapLine returns.
// getFullSource() packs blank lines between procs, so its line
// numbering diverges from the file and breaks runtime error
// navigation and the navigateToCodeLine line translation.
const char *src = sParsedSource;
if (!src) {
src = getFullSource();
}
if (!src) { if (!src) {
return; return;
@ -9306,15 +9458,19 @@ static void updateDropdowns(void) {
procName[nameLen] = '\0'; procName[nameLen] = '\0';
// Find End Sub / End Function // Find End Sub / End Function and record its line.
const char *endTag = isSub ? "END SUB" : "END FUNCTION"; const char *endTag = isSub ? "END SUB" : "END FUNCTION";
int32_t endTagLen = isSub ? 7 : 12; int32_t endTagLen = isSub ? 7 : 12;
const char *scan = pos; const char *scan = pos;
int32_t scanLine = lineNum; // line of the Sub/Function declaration
int32_t endLineNum = 0;
while (*scan) { while (*scan) {
const char *sl = dvxSkipWs(scan); const char *sl = dvxSkipWs(scan);
if (strncasecmp(sl, endTag, endTagLen) == 0) { if (strncasecmp(sl, endTag, endTagLen) == 0) {
endLineNum = scanLine;
// Advance past the End line // Advance past the End line
while (*scan && *scan != '\n') { while (*scan && *scan != '\n') {
scan++; scan++;
@ -9322,6 +9478,7 @@ static void updateDropdowns(void) {
if (*scan == '\n') { if (*scan == '\n') {
scan++; scan++;
scanLine++;
} }
break; break;
@ -9333,12 +9490,14 @@ static void updateDropdowns(void) {
if (*scan == '\n') { if (*scan == '\n') {
scan++; scan++;
scanLine++;
} }
} }
IdeProcEntryT entry; IdeProcEntryT entry;
memset(&entry, 0, sizeof(entry)); memset(&entry, 0, sizeof(entry));
entry.lineNum = lineNum; entry.lineNum = lineNum;
entry.endLineNum = endLineNum > 0 ? endLineNum : scanLine;
// Match proc name against known objects: form name, // Match proc name against known objects: form name,
// controls, menu items. Try each as a prefix followed // controls, menu items. Try each as a prefix followed
@ -9813,6 +9972,9 @@ int32_t appMain(DxeAppContextT *ctx) {
if (sWin && sWin->menuBar) { if (sWin && sWin->menuBar) {
bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true); bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true);
wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun); wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
sOutputToLog = prefsGetBool(sPrefs, "run", "outputToLog", false);
wmMenuItemSetChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG, sOutputToLog);
} }
// Load recent files list and populate menu // Load recent files list and populate menu

View file

@ -70,6 +70,7 @@ typedef struct {
WidgetT *checkedCb; WidgetT *checkedCb;
WidgetT *radioCheckCb; WidgetT *radioCheckCb;
WidgetT *enabledCb; WidgetT *enabledCb;
WidgetT *popupCb; // Top-level popup (hidden from menu bar)
WidgetT *listBox; WidgetT *listBox;
} MnuEdStateT; } MnuEdStateT;
@ -169,6 +170,13 @@ static void applyFields(void) {
mi->checked = wgtCheckboxIsChecked(sMed.checkedCb); mi->checked = wgtCheckboxIsChecked(sMed.checkedCb);
mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb); mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb);
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb); mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
// Popup checkbox only meaningful on top-level items; for nested
// items `visible` stays true (the field is ignored there).
if (mi->level == 0 && sMed.popupCb) {
mi->visible = !wgtCheckboxIsChecked(sMed.popupCb);
} else {
mi->visible = true;
}
} }
@ -198,6 +206,10 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, false); wgtCheckboxSetChecked(sMed.checkedCb, false);
wgtCheckboxSetChecked(sMed.radioCheckCb, false); wgtCheckboxSetChecked(sMed.radioCheckCb, false);
wgtCheckboxSetChecked(sMed.enabledCb, true); wgtCheckboxSetChecked(sMed.enabledCb, true);
if (sMed.popupCb) {
wgtCheckboxSetChecked(sMed.popupCb, false);
wgtSetEnabled(sMed.popupCb, false);
}
sMed.nameAutoGen = true; // new blank item -- auto-gen eligible sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
return; return;
} }
@ -209,6 +221,11 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked); wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck); wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled); wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
if (sMed.popupCb) {
// Popup (Visible=False) only applies to top-level menus.
wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible);
wgtSetEnabled(sMed.popupCb, mi->level == 0);
}
} }
@ -230,6 +247,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
DsgnMenuItemT mi; DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi)); memset(&mi, 0, sizeof(mi));
mi.enabled = true; mi.enabled = true;
mi.visible = true;
arrput(sMed.items, mi); arrput(sMed.items, mi);
} }
@ -271,6 +289,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
sMed.checkedCb = wgtCheckbox(chkRow, "Checked"); sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck"); sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled"); sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
sMed.popupCb = wgtCheckbox(chkRow, "Popup");
wgtCheckboxSetChecked(sMed.enabledCb, true); wgtCheckboxSetChecked(sMed.enabledCb, true);
// Listbox // Listbox
@ -444,6 +463,7 @@ static void onInsert(WidgetT *w) {
DsgnMenuItemT mi; DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi)); memset(&mi, 0, sizeof(mi));
mi.enabled = true; mi.enabled = true;
mi.visible = true;
// Insert after the current item's subtree, at the same level // Insert after the current item's subtree, at the same level
int32_t insertAt; int32_t insertAt;
@ -583,6 +603,7 @@ static void onNext(WidgetT *w) {
DsgnMenuItemT mi; DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi)); memset(&mi, 0, sizeof(mi));
mi.enabled = true; mi.enabled = true;
mi.visible = true;
if (count > 0) { if (count > 0) {
mi.level = sMed.items[count - 1].level; mi.level = sMed.items[count - 1].level;

View file

@ -95,11 +95,11 @@ static void onToolClick(WidgetT *w) {
} }
WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds) { WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
sDs = ds; sDs = ds;
arrsetlen(sTbxTools, 0); arrsetlen(sTbxTools, 0);
WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, 30, TBX_WIN_W, TBX_WIN_H, false); WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, y, TBX_WIN_W, TBX_WIN_H, false);
if (!win) { if (!win) {
return NULL; return NULL;

View file

@ -27,8 +27,12 @@
#include "ideDesigner.h" #include "ideDesigner.h"
// Create the toolbox floating window. Returns the WindowT pointer. // Create the toolbox floating window at the given y coordinate.
WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds); // Caller owns y so the window is painted at its final location
// during the asynchronous icon-loading dvxUpdate cycles; moving
// the window after creation would leave stale paint at the old
// position on the backbuffer. Returns the WindowT pointer.
WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y);
// Destroy the toolbox window. // Destroy the toolbox window.
void tbxDestroy(AppContextT *ctx, WindowT *win); void tbxDestroy(AppContextT *ctx, WindowT *win);

View file

@ -327,6 +327,8 @@ void basVmReset(BasVmT *vm) {
vm->errorNumber = 0; vm->errorNumber = 0;
vm->errorPc = 0; vm->errorPc = 0;
vm->errorNextPc = 0; vm->errorNextPc = 0;
vm->errorLine = 0;
vm->currentOpPc = 0;
vm->inErrorHandler = false; vm->inErrorHandler = false;
vm->errorMsg[0] = '\0'; vm->errorMsg[0] = '\0';
} }
@ -391,6 +393,17 @@ BasVmResultE basVmRun(BasVmT *vm) {
} }
} }
// Host callbacks (property setters, method dispatch, etc.) can
// abort execution by writing an error message and clearing
// vm->running. The step itself returns OK because the VM did
// nothing wrong; the failure happened on the host side. If the
// host left an error message, surface it as BAS_VM_ERROR so the
// caller (IDE, stub) can navigate to the line and stop the
// program instead of treating the halt as a normal END.
if (vm->errorMsg[0] != '\0') {
return BAS_VM_ERROR;
}
return BAS_VM_HALTED; return BAS_VM_HALTED;
} }
@ -467,7 +480,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_HALTED; return BAS_VM_HALTED;
} }
uint8_t op = vm->module->code[vm->pc++]; vm->currentOpPc = vm->pc;
uint8_t op = vm->module->code[vm->pc++];
switch (op) { switch (op) {
case OP_NOP: case OP_NOP:

View file

@ -389,6 +389,7 @@ typedef struct {
int32_t stepLimit; // max steps per basVmRun (0 = unlimited) int32_t stepLimit; // max steps per basVmRun (0 = unlimited)
int32_t stepCount; // steps executed in last basVmRun int32_t stepCount; // steps executed in last basVmRun
int32_t currentLine; // source line from last OP_LINE (debugger) int32_t currentLine; // source line from last OP_LINE (debugger)
int32_t currentOpPc; // PC of the instruction currently executing (set at start of each basVmStep; use this, not vm->pc, for "which SUB are we in" lookups from host callbacks, since vm->pc has already advanced past the opcode + operands)
// Debug state // Debug state
int32_t *breakpoints; // sorted line numbers (host-managed) int32_t *breakpoints; // sorted line numbers (host-managed)
@ -428,6 +429,7 @@ typedef struct {
int32_t errorNumber; // current Err number int32_t errorNumber; // current Err number
int32_t errorPc; // PC of the instruction that caused the error (for RESUME) int32_t errorPc; // PC of the instruction that caused the error (for RESUME)
int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT) int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT)
int32_t errorLine; // source line where the error was raised (frozen; debuggers use this because currentLine keeps advancing while the error dialog pumps events)
bool inErrorHandler; // true when executing error handler code bool inErrorHandler; // true when executing error handler code
char errorMsg[BAS_VM_ERROR_MSG_LEN]; // current error description char errorMsg[BAS_VM_ERROR_MSG_LEN]; // current error description

View file

@ -703,7 +703,11 @@ static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinH = top + textH + HELP_HEADING1_PAD * 2; w->calcMinH = top + textH + HELP_HEADING1_PAD * 2;
} else if (hd->level == 2) { } else if (hd->level == 2) {
int32_t top = first ? 0 : HELP_HEADING2_TOP; int32_t top = first ? 0 : HELP_HEADING2_TOP;
w->calcMinH = top + textH + HELP_HEADING2_PAD + 2; bool nextHasBorder = (w->nextSibling &&
(w->nextSibling->type == sHelpCodeTypeId ||
w->nextSibling->type == sHelpRuleTypeId));
int32_t extra = nextHasBorder ? 0 : 2;
w->calcMinH = top + textH + HELP_HEADING2_PAD + extra;
} else { } else {
int32_t top = first ? 0 : HELP_HEADING3_TOP; int32_t top = first ? 0 : HELP_HEADING3_TOP;
w->calcMinH = top + textH; w->calcMinH = top + textH;
@ -738,8 +742,19 @@ static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
int32_t top = first ? 0 : HELP_HEADING2_TOP; int32_t top = first ? 0 : HELP_HEADING2_TOP;
int32_t textY = w->y + top; int32_t textY = w->y + top;
drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false); drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false);
int32_t lineY = textY + font->charHeight + 1;
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); // 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.
bool nextHasBorder = (w->nextSibling &&
(w->nextSibling->type == sHelpCodeTypeId ||
w->nextSibling->type == sHelpRuleTypeId));
if (!nextHasBorder) {
int32_t lineY = textY + font->charHeight + 1;
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow);
}
} else { } else {
int32_t top = first ? 0 : HELP_HEADING3_TOP; int32_t top = first ? 0 : HELP_HEADING3_TOP;
int32_t textY = w->y + top; int32_t textY = w->y + top;

View file

@ -132,6 +132,7 @@ static int32_t sAppCount = 0;
int32_t appMain(DxeAppContextT *ctx); int32_t appMain(DxeAppContextT *ctx);
void appShutdown(void); void appShutdown(void);
static int32_t appEntryCmpName(const void *a, const void *b);
static void buildPmWindow(void); static void buildPmWindow(void);
static void desktopUpdate(void); static void desktopUpdate(void);
static void onAppButtonClick(WidgetT *w); static void onAppButtonClick(WidgetT *w);
@ -166,6 +167,14 @@ void appShutdown(void) {
} }
// qsort comparator for AppEntryT -- case-insensitive on display name.
static int32_t appEntryCmpName(const void *a, const void *b) {
const AppEntryT *ea = (const AppEntryT *)a;
const AppEntryT *eb = (const AppEntryT *)b;
return strcasecmp(ea->name, eb->name);
}
// Build the main Program Manager window with app buttons, menus, and status bar. // Build the main Program Manager window with app buttons, menus, and status bar.
// Window is centered horizontally and placed in the upper quarter vertically // Window is centered horizontally and placed in the upper quarter vertically
// so spawned app windows don't hide behind it. // so spawned app windows don't hide behind it.
@ -435,6 +444,11 @@ static void scanAppsDir(void) {
sAppFiles = NULL; sAppFiles = NULL;
sAppCount = 0; sAppCount = 0;
scanAppsDirRecurse("apps"); scanAppsDirRecurse("apps");
if (sAppCount > 1) {
qsort(sAppFiles, sAppCount, sizeof(AppEntryT), (int (*)(const void *, const void *))appEntryCmpName);
}
dvxLog("Progman: found %ld app(s)", (long)sAppCount); dvxLog("Progman: found %ld app(s)", (long)sAppCount);
} }

View file

@ -106,12 +106,8 @@ ResEdit.Show
ResList.SetColumns "Name,20|Type,8|Size,12" ResList.SetColumns "Name,20|Type,8|Size,12"
LblStatus.Caption = "Ready. Use File > Open to load a DXE file." LblStatus.Caption = "Ready. Use File > Open to load a DXE file."
' ============================================================
' Type name helper
' ============================================================
FUNCTION TypeName$(t AS LONG) FUNCTION TypeName$(t AS LONG)
' Type name helper
IF t = RES_TYPE_ICON THEN IF t = RES_TYPE_ICON THEN
TypeName$ = "Icon" TypeName$ = "Icon"
ELSEIF t = RES_TYPE_TEXT THEN ELSEIF t = RES_TYPE_TEXT THEN
@ -123,12 +119,8 @@ FUNCTION TypeName$(t AS LONG)
END IF END IF
END FUNCTION END FUNCTION
' ============================================================
' Format a byte size for display
' ============================================================
FUNCTION FormatSize$(sz AS LONG) FUNCTION FormatSize$(sz AS LONG)
' Format a byte size for display
IF sz < 1024 THEN IF sz < 1024 THEN
FormatSize$ = STR$(sz) + " B" FormatSize$ = STR$(sz) + " B"
ELSE ELSE
@ -136,12 +128,8 @@ FUNCTION FormatSize$(sz AS LONG)
END IF END IF
END FUNCTION END FUNCTION
' ============================================================
' Refresh the resource list from the open handle
' ============================================================
SUB RefreshList SUB RefreshList
' Refresh the resource list from the open handle
ResList.Clear ResList.Clear
IF resHandle = 0 THEN IF resHandle = 0 THEN
@ -161,12 +149,8 @@ SUB RefreshList
LblStatus.Caption = filePath + " - " + STR$(n) + " resource(s)" LblStatus.Caption = filePath + " - " + STR$(n) + " resource(s)"
END SUB END SUB
' ============================================================
' Close the current file
' ============================================================
SUB CloseFile SUB CloseFile
' Close the current file
IF resHandle <> 0 THEN IF resHandle <> 0 THEN
ResClose resHandle ResClose resHandle
resHandle = 0 resHandle = 0
@ -183,12 +167,8 @@ SUB CloseFile
LblStatus.Caption = "No file loaded." LblStatus.Caption = "No file loaded."
END SUB END SUB
' ============================================================
' Reopen the file (after modification) and refresh
' ============================================================
SUB ReopenAndRefresh SUB ReopenAndRefresh
' Reopen the file (after modification) and refresh
DIM path AS STRING DIM path AS STRING
path = filePath path = filePath
@ -210,12 +190,8 @@ SUB ReopenAndRefresh
END IF END IF
END SUB END SUB
' ============================================================
' Enable/disable selection-dependent menus
' ============================================================
SUB UpdateMenuState SUB UpdateMenuState
' Enable/disable selection-dependent menus
DIM hasSel AS INTEGER DIM hasSel AS INTEGER
hasSel = (ResList.ListIndex >= 0) hasSel = (ResList.ListIndex >= 0)
mnuExtract.Enabled = hasSel mnuExtract.Enabled = hasSel
@ -223,11 +199,6 @@ SUB UpdateMenuState
mnuEditText.Enabled = hasSel mnuEditText.Enabled = hasSel
END SUB END SUB
' ============================================================
' Menu handlers
' ============================================================
SUB mnuOpen_Click SUB mnuOpen_Click
DIM path AS STRING DIM path AS STRING
path = basFileOpen("Open DXE File", "Applications (*.app)|Widget Modules (*.wgt)|Libraries (*.lib)|All Files (*.*)") path = basFileOpen("Open DXE File", "Applications (*.app)|Widget Modules (*.wgt)|Libraries (*.lib)|All Files (*.*)")
@ -429,11 +400,6 @@ SUB mnuRemove_Click
END IF END IF
END SUB END SUB
' ============================================================
' ListView selection change
' ============================================================
SUB ResList_Click SUB ResList_Click
UpdateMenuState UpdateMenuState
END SUB END SUB

View file

@ -200,8 +200,11 @@ Begin Form WidgetShow
End End
Begin StatusBar sbar Begin StatusBar sbar
Caption = "Ready."
Weight = 0 Weight = 0
Begin Label lblStatus
Caption = "Ready."
Weight = 1
End
End End
End End
@ -233,12 +236,6 @@ OPTION EXPLICIT
DIM addCount AS INTEGER DIM addCount AS INTEGER
SUB setStatus(msg AS STRING)
sbar.Caption = msg
END SUB
Load WidgetShow Load WidgetShow
WidgetShow.Show WidgetShow.Show
@ -279,12 +276,13 @@ tvTree.AddChildItem 4, "Pine"
tvTree.SetExpanded 0, -1 tvTree.SetExpanded 0, -1
tvTree.SetExpanded 4, -1 tvTree.SetExpanded 4, -1
setStatus "Ready. Hover widgets for descriptions." lblStatus.Caption = "Ready. Hover widgets for descriptions."
' ============================================================ SUB setStatus(msg AS STRING)
' Event handlers lblStatus.Caption = msg
' ============================================================ END SUB
SUB tabs_Click SUB tabs_Click
setStatus "Tab: " + STR$(tabs.GetActive()) setStatus "Tab: " + STR$(tabs.GetActive())
@ -353,14 +351,14 @@ SUB btnDel_Click
END SUB END SUB
SUB lstItems_Click SUB lstItems_Change
IF lstItems.ListIndex >= 0 THEN IF lstItems.ListIndex >= 0 THEN
setStatus "ListBox: " + lstItems.List(lstItems.ListIndex) setStatus "ListBox: " + lstItems.List(lstItems.ListIndex)
END IF END IF
END SUB END SUB
SUB lvFiles_Click SUB lvFiles_Change
DIM i AS INTEGER DIM i AS INTEGER
FOR i = 0 TO lvFiles.RowCount() - 1 FOR i = 0 TO lvFiles.RowCount() - 1
IF lvFiles.IsItemSelected(i) THEN IF lvFiles.IsItemSelected(i) THEN
@ -371,7 +369,7 @@ SUB lvFiles_Click
END SUB END SUB
SUB tvTree_Click SUB tvTree_Change
DIM i AS INTEGER DIM i AS INTEGER
FOR i = 0 TO tvTree.ItemCount() - 1 FOR i = 0 TO tvTree.ItemCount() - 1
IF tvTree.IsItemSelected(i) THEN IF tvTree.IsItemSelected(i) THEN

View file

@ -217,9 +217,6 @@ int shellMain(int argc, char *argv[]) {
// roughly twice as many scheduling turns as any single app. // roughly twice as many scheduling turns as any single app.
tsSetPriority(0, TS_PRIORITY_HIGH); tsSetPriority(0, TS_PRIORITY_HIGH);
// Gather system information (CPU, memory, drives, etc.)
shellInfoInit(&sCtx);
// Initialize app slot table // Initialize app slot table
shellAppInit(); shellAppInit();
@ -266,6 +263,10 @@ int shellMain(int argc, char *argv[]) {
platformVideoEnumModes(logVideoMode, NULL); platformVideoEnumModes(logVideoMode, NULL);
dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch); dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);
// Gather system information (CPU, memory, video, drives, etc.)
// after dvxInit so the display fields have real values to report.
shellInfoInit(&sCtx);
// Apply mouse preferences // Apply mouse preferences
const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", MOUSE_WHEEL_DIR_DEFAULT); const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", MOUSE_WHEEL_DIR_DEFAULT);
int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1; int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1;

View file

@ -2105,6 +2105,11 @@ static void invalidateAllWindows(AppContextT *ctx) {
// between top-level menus. Position is clamped to screen edges so the // between top-level menus. Position is clamped to screen edges so the
// popup doesn't go off-screen. // popup doesn't go off-screen.
void dvxShowContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
openContextMenu(ctx, win, menu, screenX, screenY);
}
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) { static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
if (!menu || menu->itemCount <= 0) { if (!menu || menu->itemCount <= 0) {
return; return;
@ -3917,6 +3922,15 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
} }
// Clear the modal pointer if it was this window. Callers that open
// modal dialogs are supposed to restore the previous modal before
// destroying the dialog window, but a crash / longjmp / missed
// cleanup can leave modalWindow pointing at freed memory -- every
// subsequent click is then dropped by the modal gate.
if (ctx->modalWindow == win) {
ctx->modalWindow = NULL;
}
wmDestroyWindow(&ctx->stack, win); wmDestroyWindow(&ctx->stack, win);
// Dirty icon area again with the updated count (one fewer icon) // Dirty icon area again with the updated count (one fewer icon)
@ -4209,6 +4223,13 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
// Call the window's paint callback to update the content buffer // Call the window's paint callback to update the content buffer
// before marking the screen dirty. This means raw-paint apps only // before marking the screen dirty. This means raw-paint apps only
// need to call dvxInvalidateWindow -- onPaint fires automatically. // need to call dvxInvalidateWindow -- onPaint fires automatically.
// Set PAINT_FULL for widget-managed windows: widgetOnPaint only
// relays out + background-clears when paintNeeded >= PAINT_FULL,
// and callers invalidating "the whole window" (theme changes,
// scroll, color-scheme swaps) need that full repaint rather than
// the dirty-widget-only partial repaint the default would give.
win->paintNeeded = PAINT_FULL;
if (win->onPaint) { if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));

View file

@ -238,6 +238,14 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win);
// Raise a window to the top of the z-order and give it focus. // Raise a window to the top of the z-order and give it focus.
void dvxRaiseWindow(AppContextT *ctx, WindowT *win); void dvxRaiseWindow(AppContextT *ctx, WindowT *win);
// Display a context menu at a screen position. `menu` must be a
// standalone MenuT allocated with wmCreateMenu() and populated with
// wmAddMenuItem() / wmAddMenuSeparator() / wmAddSubMenu(). `win`
// supplies the onMenu dispatch target; menu-item clicks fire
// win->onMenu(win, id) exactly like menu-bar items. Position is
// clamped to screen edges.
void dvxShowContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
// Resize a window to exactly fit its widget tree's computed minimum size // Resize a window to exactly fit its widget tree's computed minimum size
// (plus chrome). Used for dialog boxes and other fixed-layout windows // (plus chrome). Used for dialog boxes and other fixed-layout windows
// where the window should shrink-wrap its content. // where the window should shrink-wrap its content.

View file

@ -634,6 +634,8 @@ const void *wgtGetApi(const char *name);
#define WGT_SIG_RET_STR 18 // const char *fn(const WidgetT *) #define WGT_SIG_RET_STR 18 // const char *fn(const WidgetT *)
#define WGT_SIG_STR_INT 19 // void fn(WidgetT *, const char *, int32_t) #define WGT_SIG_STR_INT 19 // void fn(WidgetT *, const char *, int32_t)
#define WGT_SIG_INT_STR 20 // void fn(WidgetT *, int32_t, const char *) #define WGT_SIG_INT_STR 20 // void fn(WidgetT *, int32_t, const char *)
#define WGT_SIG_STR_STR 21 // void fn(WidgetT *, const char *, const char *)
#define WGT_SIG_STR_BOOL 22 // void fn(WidgetT *, const char *, bool)
// Property descriptor // Property descriptor
typedef struct { typedef struct {

View file

@ -636,15 +636,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
(void)orient; (void)orient;
(void)value; (void)value;
// Repaint with new scroll position. PAINT_FULL is required so // Repaint with new scroll position. dvxInvalidateWindow sets
// widgetOnPaint re-lays out children at the new root x/y offset; // PAINT_FULL so widgetOnPaint re-lays out children at the new
// otherwise children keep their old positions and the content // root x/y offset.
// does not visibly scroll.
if (win->widgetRoot) { if (win->widgetRoot) {
AppContextT *ctx = wgtGetContext(win->widgetRoot); AppContextT *ctx = wgtGetContext(win->widgetRoot);
if (ctx) { if (ctx) {
win->paintNeeded = PAINT_FULL;
dvxInvalidateWindow(ctx, win); dvxInvalidateWindow(ctx, win);
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -51,6 +51,8 @@ void clearOtherSelections(WidgetT *except);
// ============================================================ // ============================================================
bool isWordChar(char c); bool isWordChar(char c);
int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
int32_t wordBoundaryLeft(const char *buf, int32_t pos); int32_t wordBoundaryLeft(const char *buf, int32_t pos);
int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
int32_t wordEnd(const char *buf, int32_t len, int32_t pos); int32_t wordEnd(const char *buf, int32_t len, int32_t pos);
@ -62,6 +64,240 @@ int32_t wordStart(const char *buf, int32_t pos);
void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
// ============================================================
// Multi-line line-offset cache
// ============================================================
//
// Library-owned cache of line-start offsets and tab-expanded visual
// lengths for a text buffer. Widget owns the backing buffer; the
// cache is a derived index that must be invalidated (dirty) or
// incrementally updated whenever the buffer mutates.
//
// Contract: every mutator call on the cache takes the current buf
// and len so any fallback-to-rebuild path has what it needs. If the
// cache's tabWidth is changed via textEditLineCacheSetTabWidth it
// automatically dirties itself, since visual-length entries depend
// on tab stops.
typedef struct TextEditLineCacheT {
int32_t *lineOffsets; // [lineCount+1], last entry = buffer length sentinel
int32_t *lineVisLens; // [lineCount], tab-expanded visual column counts
int32_t lineOffsetCap;
int32_t lineVisLenCap;
int32_t cachedLines; // -1 = dirty, otherwise >= 1
int32_t cachedMaxLL; // -1 = unknown
int32_t tabWidth; // tab stop width used to compute lineVisLens
} TextEditLineCacheT;
void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth);
void textEditLineCacheFree(TextEditLineCacheT *lc);
void textEditLineCacheDirty(TextEditLineCacheT *lc);
void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len);
void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen);
void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen);
int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len);
int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len);
void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col);
int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col);
// ============================================================
// Syntax coloring palette
// ============================================================
//
// Indices returned by a widget-supplied colorize callback. The palette
// has hard-coded defaults; any non-zero entry in a caller-supplied
// customColors[TEXT_SYNTAX_MAX] overrides the default.
#define TEXT_SYNTAX_DEFAULT 0
#define TEXT_SYNTAX_KEYWORD 1
#define TEXT_SYNTAX_STRING 2
#define TEXT_SYNTAX_COMMENT 3
#define TEXT_SYNTAX_NUMBER 4
#define TEXT_SYNTAX_OPERATOR 5
#define TEXT_SYNTAX_TYPE 6
#define TEXT_SYNTAX_MAX 7
uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
// ============================================================
// Scroll adjustment
// ============================================================
//
// Adjust scrollRow/scrollCol so the cursor stays inside the visible
// window [visRows x visCols]. Visual column accounts for tab width.
void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol);
// ============================================================
// Multi-line paint
// ============================================================
//
// Renders the text content of a multi-line editor: per-line tab
// expansion, optional syntax coloring, selection highlight, cursor,
// and optional per-line background overrides. All widget chrome
// (border, gutter, scrollbars, focus rect) stays with the caller.
//
// The three scratch buffers (rawSyntax, expandBuf, syntaxBuf) are
// caller-owned so the caller controls the truncation policy for
// very long lines. Lines longer than scratchLen are drawn up to
// that limit and truncated cleanly.
typedef struct TextEditPaintHooksT {
void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
void *colorizeCtx;
uint32_t (*lineDecorator)(int32_t lineNum, uint32_t *gutterColor, void *ctx);
void *lineDecoratorCtx;
uint8_t *rawSyntax;
char *expandBuf;
uint8_t *syntaxBuf;
int32_t scratchLen;
const uint32_t *customSyntaxColors;
} TextEditPaintHooksT;
void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks);
// ============================================================
// Multi-line mouse click and drag
// ============================================================
//
// Mouse handling for the text content area. Caller is responsible
// for all chrome hit-tests (scrollbar, gutter) before calling.
//
// widgetTextEditMultiMouseClick places the cursor at the click
// position, handling single (cursor + drag anchor), double (select
// word), and triple (select line) clicks. Caller gets back updated
// cursor/desired/sel state.
//
// widgetTextEditMultiDragUpdateArea is called during a drag to
// auto-scroll the view and extend the selection.
void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor);
// ============================================================
// Multi-line key handler
// ============================================================
//
// Options bundle: bundles the small set of mode flags the key
// handler needs to know about. Avoids a 20-argument entry point.
typedef struct TextEditMultiOptionsT {
bool autoIndent;
bool captureTabs;
bool useTabChar;
int32_t tabWidth;
bool readOnly;
} TextEditMultiOptionsT;
// Runs a single keystroke through the multi-line editing engine.
// Handles clipboard (Ctrl+A/C/V/X), undo (Ctrl+Z), navigation
// (arrows, Ctrl-arrows, Home/End, Ctrl-Home/Ctrl-End, PgUp/PgDn),
// editing (Enter with auto-indent, Backspace, Delete, Tab,
// printable characters), and shift-based selection extension.
//
// The caller owns buf/undoBuf storage; buf must have room for
// bufSize - 1 characters plus terminator. The library invalidates
// the cache when it knows edits span newlines; incremental
// single-byte updates go through the cache's notify paths.
//
// Fires w->onChange for edits that mutate the buffer. Calls
// wgtInvalidatePaint(w) before returning for any keystroke that
// visibly changes state.
void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts);
// ============================================================
// Text-grid scrollbars
// ============================================================
//
// Scrollbar helpers for widgets that scroll a text grid (lines x
// columns) rather than pixels. Unlike the general-purpose bevel
// scrollbars, these pass `total`/`visible`/`scroll` in logical
// units (lines or columns). Layout is fixed: arrow button at each
// end sized `thick` x `thick`, remainder is the thumb track.
//
// widgetTextScrollbarHitTest returns one of the TEXT_SB_HIT_*
// values. TEXT_SB_HIT_THUMB sets *pDragOff to the click offset
// from the thumb origin, for use in subsequent drag updates.
//
// widgetTextScrollbarDragToScroll converts a mouse coordinate
// during an active thumb drag to a clamped scroll value.
#define TEXT_SB_HIT_NONE 0
#define TEXT_SB_HIT_UP 1
#define TEXT_SB_HIT_DOWN 2
#define TEXT_SB_HIT_PAGE_UP 3
#define TEXT_SB_HIT_PAGE_DOWN 4
#define TEXT_SB_HIT_THUMB 5
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
// ============================================================
// Multi-line editor state aggregate
// ============================================================
//
// Groups the per-editor state (buffer, cursor, selection, scroll,
// undo slot, line cache) into a single struct. Consumers embed this
// in their widget data; the library operates on the individual
// fields via pointer parameters (see widgetTextEditMulti* calls).
//
// Allocation policy: caller owns the buf/undoBuf storage. Use
// textEditMultiInit to allocate them (returns false if malloc
// fails) and textEditMultiFree to release.
typedef struct TextEditMultiT {
char *buf;
int32_t bufSize;
int32_t len;
int32_t cursorRow;
int32_t cursorCol;
int32_t desiredCol;
int32_t scrollRow;
int32_t scrollCol;
int32_t selAnchor;
int32_t selCursor;
char *undoBuf;
int32_t undoLen;
int32_t undoCursor;
TextEditLineCacheT lines;
} TextEditMultiT;
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
void textEditMultiFree(TextEditMultiT *te);
// ============================================================
// Multi-line text operations
// ============================================================
//
// Pure text operations: no widget chrome, no scrollbar math (beyond
// visRows for centering). The caller invalidates paint / ensures
// visibility using its own conventions after these return.
//
// textEditFindNext: searches forward/backward from the current
// cursor position. If found, selects the match and moves the cursor
// to the match's start (forward) or end (backward). Returns true
// on match, false on no-match (no wrap-around).
//
// textEditReplaceAll: replaces every occurrence of needle with
// replacement. Records a single undo snapshot before any change.
// Returns the number of replacements made.
//
// textEditGoToLine: places cursor at the start of the given
// 1-based line, selects the entire line, and scrolls so the line
// sits ~1/4 from the top of the visible area (with clamping).
//
// textEditGetWordAtCursor: writes the word under the cursor into
// the caller's buffer. Returns the length written (0 if no word).
bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize);
void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor);
int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
// ============================================================ // ============================================================
// Single-line text editing engine // Single-line text editing engine
// ============================================================ // ============================================================

View file

@ -689,6 +689,12 @@ int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *out
pass2Wrap(); pass2Wrap();
progressStep(); progressStep();
// Sort index entries alphabetically. Done once here so both the
// HTML sidebar and the binary index section see the same order.
if (indexCount > 1) {
qsort(indexEntries, indexCount, sizeof(IndexEntryT), compareIndexEntries);
}
// HTML output (uses wrapped text, before binary passes) // HTML output (uses wrapped text, before binary passes)
if (htmlPath) { if (htmlPath) {
if (emitHtml(htmlPath) == 0) { if (emitHtml(htmlPath) == 0) {
@ -1523,8 +1529,7 @@ static int pass5Serialize(const char *outputPath) {
offset += sizeof(entry); offset += sizeof(entry);
} }
// --- 4. Keyword index entries (sorted) --- // --- 4. Keyword index entries (sorted in compile() after pass2) ---
qsort(indexEntries, indexCount, sizeof(IndexEntryT), compareIndexEntries);
hdr.indexOffset = offset; hdr.indexOffset = offset;
hdr.indexCount = indexCount; hdr.indexCount = indexCount;
for (int32_t i = 0; i < indexCount; i++) { for (int32_t i = 0; i < indexCount; i++) {

View file

@ -96,7 +96,7 @@ define WIDGET_RULES
$(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subst :, ,$1)).c $$(WIDGET_DEPS_H) | $(OBJDIR) $(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subst :, ,$1)).c $$(WIDGET_DEPS_H) | $(OBJDIR)
$$(CC) $$(CFLAGS) -c -o $$@ $$< $$(CC) $$(CFLAGS) -c -o $$@ $$<
$(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(WGTDIR)/$(word 1,$(subst :, ,$1)) $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o $$(wildcard $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res) $$(wildcard $(word 2,$(subst :, ,$1))/*.bmp) | $(WGTDIR)/$(word 1,$(subst :, ,$1))
$$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe -U $$< $$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe -U $$<
mv $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe $$@ mv $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe $$@
@if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \ @if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \

View file

@ -1045,7 +1045,11 @@ static const WgtMethodDescT sMethods[] = {
}; };
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
.basName = "Canvas", // VB-compatible name is "PictureBox" -- matches what the rest of
// the docs (ideguide, canvas.bhs) and sample .frm files (iconed)
// already use. Prior "Canvas" basName caused silent frm-load
// drops whenever an app asked for a PictureBox.
.basName = "PictureBox",
.props = NULL, .props = NULL,
.propCount = 0, .propCount = 0,
.methods = sMethods, .methods = sMethods,

View file

@ -99,8 +99,13 @@ void widgetCheckboxAccelActivate(WidgetT *w, WidgetT *root) {
// relative to each other regardless of font size. // relative to each other regardless of font size.
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) { void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
CheckboxDataT *d = (CheckboxDataT *)w->data; CheckboxDataT *d = (CheckboxDataT *)w->data;
// +1 on the width: widgetCheckboxPaint draws the focus rect from
// labelX-1 with width labelW+2, whose rightmost column lands one
// pixel past the widget's claimed right edge. Without the +1,
// rectFill in the next paint doesn't clear that column and the
// stale dot survives after focus moves away.
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
textWidthAccel(font, d->text); textWidthAccel(font, d->text) + 1;
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
} }

View file

@ -35,6 +35,7 @@ typedef struct {
void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header); void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width); void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
int32_t (*getSelectedRow)(const WidgetT *w); int32_t (*getSelectedRow)(const WidgetT *w);
void (*setSelectedRow)(WidgetT *w, int32_t row);
} DbGridApiT; } DbGridApiT;
static inline const DbGridApiT *dvxDbGridApi(void) { static inline const DbGridApiT *dvxDbGridApi(void) {
@ -48,5 +49,6 @@ static inline const DbGridApiT *dvxDbGridApi(void) {
#define wgtDbGridSetColumnHeader(w, field, hdr) dvxDbGridApi()->setColumnHeader(w, field, hdr) #define wgtDbGridSetColumnHeader(w, field, hdr) dvxDbGridApi()->setColumnHeader(w, field, hdr)
#define wgtDbGridSetColumnWidth(w, field, width) dvxDbGridApi()->setColumnWidth(w, field, width) #define wgtDbGridSetColumnWidth(w, field, width) dvxDbGridApi()->setColumnWidth(w, field, width)
#define wgtDbGridGetSelectedRow(w) dvxDbGridApi()->getSelectedRow(w) #define wgtDbGridGetSelectedRow(w) dvxDbGridApi()->getSelectedRow(w)
#define wgtDbGridSetSelectedRow(w, row) dvxDbGridApi()->setSelectedRow(w, row)
#endif // DBGRID_H #endif // DBGRID_H

View file

@ -145,6 +145,7 @@ static void dbGridDestroy(WidgetT *w);
static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my); static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my);
static bool dbGridGetGridLines(const WidgetT *w); static bool dbGridGetGridLines(const WidgetT *w);
int32_t dbGridGetSelectedRow(const WidgetT *w); int32_t dbGridGetSelectedRow(const WidgetT *w);
void dbGridSetSelectedRow(WidgetT *w, int32_t row);
static void dbGridOnDragEnd(WidgetT *w); static void dbGridOnDragEnd(WidgetT *w);
static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod); static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod);
@ -384,6 +385,40 @@ int32_t dbGridGetSelectedRow(const WidgetT *w) {
} }
// Programmatically select a data-row (same effect as a user click).
// Clamps to the current row range, syncs the bound Data control's
// cursor (which cascades to any controls bound to that Data via
// the normal Reposition path), and repaints. -1 deselects.
void dbGridSetSelectedRow(WidgetT *w, int32_t row) {
if (!w || !w->data) {
return;
}
DbGridDataT *d = (DbGridDataT *)w->data;
int32_t rowCount = getDataRowCount(d);
if (row < -1) {
row = -1;
}
if (row >= rowCount) {
row = rowCount - 1;
}
if (d->selectedRow == row) {
return;
}
d->selectedRow = row;
if (d->dataWidget && row >= 0) {
wgtDataCtrlSetCurrentRow(d->dataWidget, row);
}
wgtInvalidatePaint(w);
}
static void dbGridOnDragEnd(WidgetT *w) { static void dbGridOnDragEnd(WidgetT *w) {
DbGridDataT *d = (DbGridDataT *)w->data; DbGridDataT *d = (DbGridDataT *)w->data;
d->sbDragOrient = -2; d->sbDragOrient = -2;
@ -1191,6 +1226,7 @@ static const struct {
void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header); void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width); void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
int32_t (*getSelectedRow)(const WidgetT *w); int32_t (*getSelectedRow)(const WidgetT *w);
void (*setSelectedRow)(WidgetT *w, int32_t row);
} sApi = { } sApi = {
.create = dbGridCreate, .create = dbGridCreate,
.setDataWidget = dbGridSetDataWidget, .setDataWidget = dbGridSetDataWidget,
@ -1199,14 +1235,19 @@ static const struct {
.setColumnHeader = dbGridSetColumnHeader, .setColumnHeader = dbGridSetColumnHeader,
.setColumnWidth = dbGridSetColumnWidth, .setColumnWidth = dbGridSetColumnWidth,
.getSelectedRow = dbGridGetSelectedRow, .getSelectedRow = dbGridGetSelectedRow,
.setSelectedRow = dbGridSetSelectedRow,
}; };
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
{ "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL }, { "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL },
{ "SelectedRow", WGT_IFACE_INT, (void *)dbGridGetSelectedRow, (void *)dbGridSetSelectedRow, NULL },
}; };
static const WgtMethodDescT sMethods[] = { static const WgtMethodDescT sMethods[] = {
{ "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh }, { "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh },
{ "SetColumnVisible", WGT_SIG_STR_BOOL, (void *)dbGridSetColumnVisible },
{ "SetColumnHeader", WGT_SIG_STR_STR, (void *)dbGridSetColumnHeader },
{ "SetColumnWidth", WGT_SIG_STR_INT, (void *)dbGridSetColumnWidth },
}; };
static const WgtEventDescT sEvents[] = { static const WgtEventDescT sEvents[] = {
@ -1217,11 +1258,11 @@ static const WgtEventDescT sEvents[] = {
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
.basName = "DBGrid", .basName = "DBGrid",
.props = sProps, .props = sProps,
.propCount = 1, .propCount = sizeof(sProps) / sizeof(sProps[0]),
.methods = sMethods, .methods = sMethods,
.methodCount = 1, .methodCount = sizeof(sMethods) / sizeof(sMethods[0]),
.events = sEvents, .events = sEvents,
.eventCount = 2, .eventCount = sizeof(sEvents) / sizeof(sEvents[0]),
.createSig = WGT_CREATE_PARENT, .createSig = WGT_CREATE_PARENT,
.isContainer = false, .isContainer = false,
.defaultEvent = "DblClick", .defaultEvent = "DblClick",

View file

@ -26,9 +26,14 @@
#include "../../../libs/kpunch/libdvx/dvxWgt.h" #include "../../../libs/kpunch/libdvx/dvxWgt.h"
// Field order mirrors widgetRadio.c's sApi. `create` must be first
// because the BASIC form runtime invokes widget constructors by
// dereferencing the api pointer as a pointer-to-function-pointer
// (see createWidgetByIface). If the order is changed here, update
// widgetRadio.c to match.
typedef struct { typedef struct {
WidgetT *(*group)(WidgetT *parent);
WidgetT *(*create)(WidgetT *parent, const char *text); WidgetT *(*create)(WidgetT *parent, const char *text);
WidgetT *(*group)(WidgetT *parent);
void (*groupSetSelected)(WidgetT *w, int32_t idx); void (*groupSetSelected)(WidgetT *w, int32_t idx);
int32_t (*getIndex)(const WidgetT *w); int32_t (*getIndex)(const WidgetT *w);
} RadioApiT; } RadioApiT;

View file

@ -55,6 +55,12 @@ static int32_t sRadioTypeId = -1;
typedef struct { typedef struct {
const char *text; const char *text;
int32_t index; int32_t index;
// `selected` is the source of truth for .frm-loaded OptionButtons,
// whose parent is a plain layout container rather than a
// RadioGroup. When the parent IS a RadioGroup, the group's
// selectedIdx still wins (the group view is canonical and the C
// API callers like dvxdemo keep using it).
bool selected;
} RadioDataT; } RadioDataT;
typedef struct { typedef struct {
@ -229,8 +235,13 @@ void widgetRadioAccelActivate(WidgetT *w, WidgetT *root) {
// column, and the indicator + label layout is visually consistent. // column, and the indicator + label layout is visually consistent.
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) { void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
RadioDataT *d = (RadioDataT *)w->data; RadioDataT *d = (RadioDataT *)w->data;
// +1 on the width: widgetRadioPaint draws the focus rect from
// labelX-1 with width labelW+2, whose rightmost column lands at
// w->x + (BOX+GAP+labelW). Without the +1 that column is one
// pixel past the widget's claimed right edge, so the rectFill
// in the next paint doesn't clear it and the focus dot survives.
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
textWidthAccel(font, d->text); textWidthAccel(font, d->text) + 1;
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
} }
@ -244,72 +255,85 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// //
// Key codes use DOS BIOS scancode convention: high byte 0x01 flag // Key codes use DOS BIOS scancode convention: high byte 0x01 flag
// ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left. // ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left.
// Selects `target` within `parent` -- parent may be a RadioGroup (C
// API flow, updates group selectedIdx) or a plain layout container
// (.frm-loaded OptionButton flow, updates each RadioDataT.selected).
static void selectRadio(WidgetT *parent, WidgetT *target) {
if (!parent || !target) {
return;
}
RadioDataT *td = (RadioDataT *)target->data;
if (parent->type == sRadioGroupTypeId) {
RadioGroupDataT *gd = (RadioGroupDataT *)parent->data;
invalidateOldSelection(parent, gd->selectedIdx);
gd->selectedIdx = td ? td->index : 0;
} else {
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type == sRadioTypeId && c != target) {
RadioDataT *sd = (RadioDataT *)c->data;
if (sd && sd->selected) {
sd->selected = false;
wgtInvalidatePaint(c);
}
}
}
if (td) {
td->selected = true;
}
}
if (parent->type == sRadioGroupTypeId && parent->onChange) {
parent->onChange(parent);
} else if (parent->type != sRadioGroupTypeId && target->onChange) {
target->onChange(target);
}
}
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) { void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod; (void)mod;
RadioDataT *rd = (RadioDataT *)w->data;
if (!w->parent) {
return;
}
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
// Select this radio selectRadio(w->parent, w);
if (w->parent && w->parent->type == sRadioGroupTypeId) {
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
gd->selectedIdx = rd->index;
if (w->parent->onChange) {
w->parent->onChange(w->parent);
}
}
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
} else if (key == KEY_DOWN || key == KEY_RIGHT) { } else if (key == KEY_DOWN || key == KEY_RIGHT) {
// Down or Right -- next radio in group WidgetT *next = NULL;
if (w->parent && w->parent->type == sRadioGroupTypeId) {
WidgetT *next = NULL;
for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) { for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
if (s->type == sRadioTypeId && s->visible && s->enabled) { if (s->type == sRadioTypeId && s->visible && s->enabled) {
next = s; next = s;
break; break;
}
}
if (next) {
RadioDataT *nd = (RadioDataT *)next->data;
RadioGroupDataT *gd = (RadioGroupDataT *)next->parent->data;
invalidateOldSelection(w->parent, gd->selectedIdx);
sFocusedWidget = next;
gd->selectedIdx = nd->index;
if (next->parent->onChange) {
next->parent->onChange(next->parent);
}
wgtInvalidatePaint(next);
} }
} }
if (next) {
wgtInvalidatePaint(w); // erase focus rect on widget we are leaving
sFocusedWidget = next;
selectRadio(w->parent, next);
wgtInvalidatePaint(next);
}
} else if (key == KEY_UP || key == KEY_LEFT) { } else if (key == KEY_UP || key == KEY_LEFT) {
// Up or Left -- previous radio in group WidgetT *prev = NULL;
if (w->parent && w->parent->type == sRadioGroupTypeId) {
WidgetT *prev = NULL;
for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) { for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
if (s->type == sRadioTypeId && s->visible && s->enabled) { if (s->type == sRadioTypeId && s->visible && s->enabled) {
prev = s; prev = s;
}
} }
}
if (prev) { if (prev) {
RadioDataT *pd = (RadioDataT *)prev->data; wgtInvalidatePaint(w); // erase focus rect on widget we are leaving
RadioGroupDataT *gd = (RadioGroupDataT *)prev->parent->data; sFocusedWidget = prev;
invalidateOldSelection(w->parent, gd->selectedIdx); selectRadio(w->parent, prev);
sFocusedWidget = prev; wgtInvalidatePaint(prev);
gd->selectedIdx = pd->index;
if (prev->parent->onChange) {
prev->parent->onChange(prev->parent);
}
wgtInvalidatePaint(prev);
}
} }
} }
} }
@ -321,8 +345,9 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vy; (void)vy;
sFocusedWidget = w; sFocusedWidget = w;
RadioDataT *rd = (RadioDataT *)w->data;
if (w->parent && w->parent->type == sRadioGroupTypeId) { if (w->parent && w->parent->type == sRadioGroupTypeId) {
RadioDataT *rd = (RadioDataT *)w->data;
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data; RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
if (gd->selectedIdx != rd->index) { if (gd->selectedIdx != rd->index) {
@ -334,7 +359,34 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
if (w->parent->onChange) { if (w->parent->onChange) {
w->parent->onChange(w->parent); w->parent->onChange(w->parent);
} }
} else if (w->parent) {
// .frm-loaded OptionButton: no enclosing RadioGroup. Walk
// sibling OptionButtons, clear theirs, mark this one selected.
for (WidgetT *c = w->parent->firstChild; c; c = c->nextSibling) {
if (c->type == sRadioTypeId && c != w) {
RadioDataT *sd = (RadioDataT *)c->data;
if (sd && sd->selected) {
sd->selected = false;
wgtInvalidatePaint(c);
}
}
}
if (rd) {
rd->selected = true;
}
if (w->onChange) {
w->onChange(w);
}
} }
// Always repaint the clicked radio: its selection dot or focus rect
// may need to appear. The mouse dispatcher invalidates the previous
// focus owner, but nothing invalidates the newly-focused widget
// unless we do it here.
wgtInvalidatePaint(w);
} }
@ -389,19 +441,29 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
drawHLine(d, ops, bx + right, boxY + i, 1, sh); drawHLine(d, ops, bx + right, boxY + i, 1, sh);
} }
// Draw filled diamond if selected // Draw filled diamond if selected. Two sources of truth: the
// RadioGroup's selectedIdx (when parent is a RadioGroup, via C
// API or wgtRadioGroupSetSelected), or this widget's own
// rd->selected bit (set by mouse/key handlers when the radio is
// just a child of a layout container, as happens for
// .frm-loaded OptionButtons).
bool isSelected = false;
if (w->parent && w->parent->type == sRadioGroupTypeId) { if (w->parent && w->parent->type == sRadioGroupTypeId) {
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data; RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
isSelected = (gd->selectedIdx == rd->index);
} else {
isSelected = rd->selected;
}
if (gd->selectedIdx == rd->index) { if (isSelected) {
uint32_t dotFg = w->enabled ? fg : colors->windowShadow; uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
static const int32_t dotW[] = {2, 4, 6, 6, 4, 2}; static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
for (int32_t i = 0; i < 6; i++) { for (int32_t i = 0; i < 6; i++) {
int32_t dw = dotW[i]; int32_t dw = dotW[i];
drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg); drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
}
} }
} }

View file

@ -67,6 +67,15 @@ WidgetT *wgtHSeparator(WidgetT *parent) {
if (w) { if (w) {
SeparatorDataT *d = calloc(1, sizeof(SeparatorDataT)); SeparatorDataT *d = calloc(1, sizeof(SeparatorDataT));
// Auto-orient to the parent: horizontal container -> vertical
// divider, vertical container -> horizontal divider. This
// lets a single "Line" basName work correctly in both
// toolbars and menus without requiring callers to pick
// between HSeparator and VSeparator by widget type.
if (parent && widgetIsHorizContainer(parent->type)) {
d->vertical = true;
}
w->data = d; w->data = d;
} }

View file

@ -41,7 +41,11 @@
#include "dvxWgtP.h" #include "dvxWgtP.h"
#define TOOLBAR_PAD 2 // Tight padding keeps the status bar visually a single-line strip
// (VB-style). The per-child sunken bevel already adds 1px of visual
// inset around each panel, so additional container padding just makes
// the bar look chunky.
#define TOOLBAR_PAD 1
#define TOOLBAR_GAP 2 #define TOOLBAR_GAP 2
static int32_t sTypeId = -1; static int32_t sTypeId = -1;
@ -87,7 +91,8 @@ static const WgtIfaceT sIface = {
.methodCount = 0, .methodCount = 0,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT .createSig = WGT_CREATE_PARENT,
.isContainer = true
}; };
void wgtRegister(void) { void wgtRegister(void) {

View file

@ -1,5 +1,7 @@
icon24 icon tabctrl.bmp icon24 icon tabctrl.bmp
name text "TabControl" name text "TabStrip"
icon24-2 icon tabctrl.bmp
name-2 text "TabPage"
author text "Scott Duensing" author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing" copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios" publisher text "Kangaroo Punch Studios"

File diff suppressed because it is too large Load diff

View file

@ -854,7 +854,24 @@ bool wgtTreeItemIsSelected(const WidgetT *w) {
VALIDATE_WIDGET(w, sTreeItemTypeId, false); VALIDATE_WIDGET(w, sTreeItemTypeId, false);
TreeItemDataT *ti = (TreeItemDataT *)w->data; TreeItemDataT *ti = (TreeItemDataT *)w->data;
return ti->selected;
// ti->selected tracks multi-select state (Ctrl-click, shift-click
// range). In single-select mode, the current selection lives on
// the TreeView's tv->selectedItem pointer instead -- walk up to
// the owning TreeView and compare, so callers see the expected
// answer for both modes.
if (ti->selected) {
return true;
}
for (WidgetT *p = w->parent; p; p = p->parent) {
if (p->type == sTreeViewTypeId) {
TreeViewDataT *tv = (TreeViewDataT *)p->data;
return tv && tv->selectedItem == w;
}
}
return false;
} }
@ -923,17 +940,26 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
TreeViewDataT *tv = (TreeViewDataT *)w->data; TreeViewDataT *tv = (TreeViewDataT *)w->data;
// Expand all ancestors so the item is visible in the tree // Expand all ancestors so the item is visible in the tree.
// Invalidate cached dims so the scroll-into-view below sees the
// correct post-expansion totalH / item Y coordinate.
if (item) { if (item) {
bool expandedAny = false;
for (WidgetT *p = item->parent; p && p != w; p = p->parent) { for (WidgetT *p = item->parent; p && p != w; p = p->parent) {
if (p->type == sTreeItemTypeId) { if (p->type == sTreeItemTypeId) {
TreeItemDataT *pti = (TreeItemDataT *)p->data; TreeItemDataT *pti = (TreeItemDataT *)p->data;
if (pti && !pti->expanded) { if (pti && !pti->expanded) {
pti->expanded = true; pti->expanded = true;
expandedAny = true;
} }
} }
} }
if (expandedAny) {
tv->dimsValid = false;
}
} }
setSelectedItem(w, item); setSelectedItem(w, item);
@ -945,7 +971,37 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
tv->anchorItem = item; tv->anchorItem = item;
} }
wgtInvalidatePaint(w); // Scroll so the selected item is inside the visible band. Manual
// expand+click goes through widgetTreeViewOnMouse / OnKey which
// have their own scroll-into-view logic; programmatic callers
// (e.g. dvxhelp syncing the TOC from navigateToTopic) need this.
if (item) {
AppContextT *ctx = wgtGetContext(w);
if (ctx) {
const BitmapFontT *font = &ctx->font;
int32_t itemY = treeItemYPos(w, item, font);
if (itemY >= 0) {
int32_t totalH;
int32_t totalW;
int32_t innerH;
int32_t innerW;
bool needVSb;
bool needHSb;
treeCalcScrollbarNeeds(w, font, &totalH, &totalW, &innerH, &innerW, &needVSb, &needHSb);
if (itemY < tv->scrollPos) {
tv->scrollPos = itemY;
} else if (itemY + font->charHeight > tv->scrollPos + innerH) {
tv->scrollPos = itemY + font->charHeight - innerH;
}
}
}
}
wgtInvalidate(w);
} }
@ -997,10 +1053,15 @@ static void widgetTreeViewDestroy(WidgetT *w) {
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) { void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
TreeViewDataT *tv = (TreeViewDataT *)w->data; TreeViewDataT *tv = (TreeViewDataT *)w->data;
// Auto-select first item if nothing is selected // Auto-select first item if nothing is selected. This runs at
// layout time (not user action), so set tv->selectedItem directly
// instead of going through setSelectedItem -- the latter fires
// onChange as if the user clicked, which triggers callers (e.g.
// dvxhelp's TOC sync) to re-navigate to the first tree item and
// clobber any programmatic selection the host is about to make.
if (!tv->selectedItem) { if (!tv->selectedItem) {
WidgetT *first = firstVisibleItem(w); WidgetT *first = firstVisibleItem(w);
setSelectedItem(w, first); tv->selectedItem = first;
if (tv->multiSelect && first) { if (tv->multiSelect && first) {
((TreeItemDataT *)first->data)->selected = true; ((TreeItemDataT *)first->data)->selected = true;