More BASIC widget debugging.
This commit is contained in:
parent
4600f3e631
commit
4cdcfe6b8c
22 changed files with 1221 additions and 126 deletions
6
Makefile
6
Makefile
|
|
@ -107,7 +107,7 @@ SDKDIR = bin/sdk
|
|||
|
||||
deploy-sdk:
|
||||
@echo "Building SDK..."
|
||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo
|
||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
|
||||
@# Core headers (libdvx public API)
|
||||
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
|
||||
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
|
||||
|
|
@ -141,6 +141,10 @@ deploy-sdk:
|
|||
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
|
||||
@# BASIC sample: widshow (widget showcase)
|
||||
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
|
||||
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
|
||||
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
|
||||
@# README
|
||||
@printf '%s\n' \
|
||||
'DVX SDK' \
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ img { max-width: 100%; }
|
|||
<li><a href="#lang.func.string">LEN</a></li>
|
||||
<li><a href="#lang.func.string">LTRIM$</a></li>
|
||||
<li><a href="#lang.func.string">MID$</a></li>
|
||||
<li><a href="#lang.func.string">OCT$</a></li>
|
||||
<li><a href="#lang.func.string">RIGHT$</a></li>
|
||||
<li><a href="#lang.func.string">RTRIM$</a></li>
|
||||
<li><a href="#lang.func.string">SPACE$</a></li>
|
||||
|
|
@ -397,6 +398,7 @@ img { max-width: 100%; }
|
|||
<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>
|
||||
|
|
@ -856,6 +858,14 @@ img { max-width: 100%; }
|
|||
<p>File list -- relative paths of all .bas and .frm files in the project. Each entry tracks whether it is a form file.</p>
|
||||
<h2>How Projects are Compiled</h2>
|
||||
<p>When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.</p>
|
||||
<h2>Compile-Time Validation</h2>
|
||||
<p>Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:</p>
|
||||
<ul>
|
||||
<li>Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).</li>
|
||||
<li>Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.</li>
|
||||
</ul>
|
||||
<p>Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.</p>
|
||||
<p>These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.</p>
|
||||
<h2>Project Operations</h2>
|
||||
<pre> Operation Description
|
||||
--------- -----------
|
||||
|
|
@ -1203,7 +1213,9 @@ name$ = "Hello" ' String</code></pre>
|
|||
<pre> Form Example Description
|
||||
---- ------- -----------
|
||||
Decimal integer 42 Values -32768..32767 are Integer; larger are Long
|
||||
Hex integer &HFF, &H1234 Hexadecimal literal
|
||||
Hex integer &HFF, &H1234 Hexadecimal literal (0-9, A-F)
|
||||
Octal integer &O10, &O77 Octal literal (0-7)
|
||||
Binary integer &B1010, &B11111111 Binary literal (0-1)
|
||||
Integer suffix 42% Force Integer type
|
||||
Long suffix 42&, &HFF& Force Long type
|
||||
Floating-point 3.14, 1.5E10, 2.5D3 Any number containing a decimal point or exponent is Double by default
|
||||
|
|
@ -1514,7 +1526,7 @@ END SUB</code></pre>
|
|||
<pre><code>Sub Greet(ByVal name As String)
|
||||
Print "Hello, " & name
|
||||
End Sub</code></pre>
|
||||
<p>Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:</p>
|
||||
<p>Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:</p>
|
||||
<pre><code>Greet "World"
|
||||
Greet("World")
|
||||
Call Greet("World")</code></pre>
|
||||
|
|
@ -1806,6 +1818,7 @@ WEND</code></pre>
|
|||
LTRIM$(s$) String Removes leading spaces from s$
|
||||
MID$(s$, start) String Substring from start (1-based) to end of string
|
||||
MID$(s$, start, length) String Substring of length characters starting at start
|
||||
OCT$(n) String Octal representation of n (no leading &O)
|
||||
RIGHT$(s$, n) String Rightmost n characters of s$
|
||||
RTRIM$(s$) String Removes trailing spaces from s$
|
||||
SPACE$(n) String String of n spaces
|
||||
|
|
@ -1866,6 +1879,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background</code></pre>
|
|||
<h1>Conversion Functions</h1>
|
||||
<pre> Function Returns Description
|
||||
-------- ------- -----------
|
||||
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
|
||||
CDBL(n) Double Converts n to Double
|
||||
CINT(n) Integer Converts n to Integer (rounds half away from zero)
|
||||
CLNG(n) Long Converts n to Long
|
||||
|
|
@ -2233,7 +2247,8 @@ End If</code></pre>
|
|||
Enabled Boolean R/W Whether the control accepts user input.
|
||||
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.</pre>
|
||||
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
|
||||
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.</pre>
|
||||
<h2>Common Events</h2>
|
||||
<p>These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.</p>
|
||||
<pre> Event Parameters Description
|
||||
|
|
@ -2863,7 +2878,7 @@ End Sub</code></pre>
|
|||
List(index%) Return the text of the item at the given index.
|
||||
ListCount() Return the number of items in the list.
|
||||
RemoveItem index% Remove the item at the given index.</pre>
|
||||
<p>Default Event: Click</p>
|
||||
<p>Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup)</p>
|
||||
<h2>Example</h2>
|
||||
<pre><code>Begin DropDown DropDown1
|
||||
End
|
||||
|
|
@ -2875,7 +2890,7 @@ Sub Form_Load ()
|
|||
DropDown1.ListIndex = 1
|
||||
End Sub
|
||||
|
||||
Sub DropDown1_Click ()
|
||||
Sub DropDown1_Change ()
|
||||
Label1.Caption = "Picked: " & DropDown1.List(DropDown1.ListIndex)
|
||||
End Sub</code></pre>
|
||||
<p><a href="#ctrl.common.props">Common Properties, Events, and Methods</a></p>
|
||||
|
|
|
|||
|
|
@ -3271,6 +3271,7 @@ prefsClose(h);</code></pre>
|
|||
--------- -----------
|
||||
name Widget type name (e.g. "button", "listbox")
|
||||
api Pointer to the widget's API struct</pre>
|
||||
<blockquote><strong>Warning:</strong> The create function must be the first field of the API struct. BASIC's form runtime instantiates widgets by dereferencing the api pointer as a pointer-to-function-pointer (see createWidgetByIface), so whichever field is first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it.</blockquote>
|
||||
<h3>wgtGetApi</h3>
|
||||
<pre><code>const void *wgtGetApi(const char *name);</code></pre>
|
||||
<p>Retrieve a registered widget API struct by name.</p>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ C_APPS = progman clock dvxdemo cpanel dvxhelp
|
|||
BASCOMP = ../../../bin/host/bascomp
|
||||
# BASIC apps: each is a .dbp project in its own directory.
|
||||
# BASIC-only notepad, imgview, etc. replace the old C versions.
|
||||
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo
|
||||
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
|
||||
|
||||
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||
|
||||
|
|
@ -120,6 +120,11 @@ basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
|
|||
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
|
||||
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
|
||||
|
||||
widshow: $(BINDIR)/kpunch/widshow/widshow.app
|
||||
|
||||
$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic
|
||||
$(BASCOMP) widshow/widshow.dbp -o $@ -release
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
|
|
@ -133,6 +138,7 @@ $(BINDIR)/kpunch/notepad: ; mkdir -p $@
|
|||
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
|
||||
|
||||
# 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
|
||||
|
|
@ -154,4 +160,5 @@ clean:
|
|||
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||
rm -f $(BINDIR)/kpunch/resedit/resedit.app
|
||||
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
rm -f $(BINDIR)/kpunch/widshow/widshow.app
|
||||
$(MAKE) -C dvxbasic clean
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
|
|||
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
|
||||
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
|
||||
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
|
||||
.endtable
|
||||
|
||||
.h2 Common Events
|
||||
|
|
|
|||
|
|
@ -1168,13 +1168,7 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
|
|||
BasFormRtT *rt = (BasFormRtT *)ctx;
|
||||
BasFormT *form = (BasFormT *)formRef;
|
||||
|
||||
dvxLog("[FC] findCtrl: form=%p '%s' looking for '%s'",
|
||||
(void *)form,
|
||||
form ? form->name : "(null)",
|
||||
ctrlName ? ctrlName : "(null)");
|
||||
|
||||
if (!form) {
|
||||
dvxLog("[FC] NULL FORM");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -2184,13 +2178,6 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
|
|||
(void)ctx;
|
||||
BasControlT *ctrl = (BasControlT *)ctrlRef;
|
||||
|
||||
dvxLog("[SE] SetEvent: ctrl=%p '%s' type='%s' event='%s' -> handler='%s'",
|
||||
(void *)ctrl,
|
||||
ctrl ? ctrl->name : "(null)",
|
||||
ctrl ? ctrl->typeName : "(null)",
|
||||
eventName ? eventName : "(null)",
|
||||
handlerName ? handlerName : "(null)");
|
||||
|
||||
if (!ctrl || !eventName || !handlerName) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -2208,9 +2195,6 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
|
|||
BasEventOverrideT *ov = &ctrl->eventOverrides[ctrl->eventOverrideCount++];
|
||||
snprintf(ov->eventName, BAS_MAX_CTRL_NAME, "%s", eventName);
|
||||
snprintf(ov->handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName);
|
||||
dvxLog("[SE] added (count=%d)", (int)ctrl->eventOverrideCount);
|
||||
} else {
|
||||
dvxLog("[SE] NO SLOT (count=%d)", (int)ctrl->eventOverrideCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2345,7 +2329,6 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
|
|||
// strdup their text internally).
|
||||
if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
|
||||
BasStringT *s = basValFormatString(value);
|
||||
dvxLog("[SP] setProp Caption: ctrl='%s' text='%s'", ctrl->name, s ? s->data : "(null)");
|
||||
wgtSetText(ctrl->widget, s->data);
|
||||
basStringUnref(s);
|
||||
return;
|
||||
|
|
@ -2453,6 +2436,7 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
|
|||
}
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
|
||||
free(form->controls[i]->tooltip);
|
||||
free(form->controls[i]);
|
||||
}
|
||||
|
||||
|
|
@ -2548,20 +2532,11 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char
|
|||
// the SUB is module-global (no BEGINFORM scope) or the owning form
|
||||
// isn't currently loaded -- callers should fall back to ctrl->form.
|
||||
static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) {
|
||||
if (!rt || !proc) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dvxLog("[OF] resolveOwningForm: proc='%s' formName='%s' formCount=%d",
|
||||
proc->name, proc->formName, (int)arrlen(rt->forms));
|
||||
|
||||
if (!proc->formName[0]) {
|
||||
if (!rt || !proc || !proc->formName[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
|
||||
dvxLog("[OF] form[%d] name='%s' varCount=%d",
|
||||
(int)i, rt->forms[i]->name, (int)rt->forms[i]->formVarCount);
|
||||
if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) {
|
||||
return rt->forms[i];
|
||||
}
|
||||
|
|
@ -3000,14 +2975,6 @@ WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *p
|
|||
|
||||
|
||||
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) {
|
||||
dvxLog("[FE] fireCtrlEvent: ctrl=%p '%s' type='%s' event='%s' eventFiring=%d overrideCount=%d",
|
||||
(void *)ctrl,
|
||||
ctrl ? ctrl->name : "(null)",
|
||||
ctrl ? ctrl->typeName : "(null)",
|
||||
eventName ? eventName : "(null)",
|
||||
ctrl ? (int)ctrl->eventFiring : -1,
|
||||
ctrl ? (int)ctrl->eventOverrideCount : -1);
|
||||
|
||||
// Re-entrancy guard: reject only same-event re-entry (runSubLoop
|
||||
// pumps events during long handlers; without this, a lingering
|
||||
// mouse-up can re-deliver Click on top of itself). But allow
|
||||
|
|
@ -3018,7 +2985,6 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
|
|||
strcasecmp(ctrl->firingEventName, eventName) == 0);
|
||||
|
||||
if (sameEvent) {
|
||||
dvxLog(" SUPPRESSED (same event already firing)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -3049,27 +3015,18 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
|
|||
|
||||
// Check for event override (SetEvent)
|
||||
for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) {
|
||||
dvxLog("[FE] override[%d] event='%s' handler='%s'",
|
||||
(int)i,
|
||||
ctrl->eventOverrides[i].eventName,
|
||||
ctrl->eventOverrides[i].handlerName);
|
||||
if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Override found: call the named SUB directly
|
||||
if (!rt->vm || !rt->module) {
|
||||
dvxLog("[FE] NO VM/MODULE");
|
||||
return;
|
||||
}
|
||||
|
||||
const BasProcEntryT *proc = basModuleFindProc(rt->module, ctrl->eventOverrides[i].handlerName);
|
||||
|
||||
if (!proc || proc->isFunction) {
|
||||
dvxLog("[FE] PROC NOT FOUND or IS FUNCTION: '%s' proc=%p isFunction=%d",
|
||||
ctrl->eventOverrides[i].handlerName,
|
||||
(void *)proc,
|
||||
proc ? (int)proc->isFunction : -1);
|
||||
free(allArgs);
|
||||
return;
|
||||
}
|
||||
|
|
@ -3100,24 +3057,15 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
|
|||
snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : "");
|
||||
}
|
||||
|
||||
dvxLog(" -> calling SUB '%s' at addr=%d", ctrl->eventOverrides[i].handlerName, (int)proc->codeAddr);
|
||||
|
||||
rt->vm->errorMsg[0] = '\0';
|
||||
rt->vm->errorNumber = 0;
|
||||
|
||||
bool subOk;
|
||||
if (finalArgCount > 0 && finalArgs) {
|
||||
subOk = basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount);
|
||||
basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount);
|
||||
} else {
|
||||
subOk = basVmCallSub(rt->vm, proc->codeAddr);
|
||||
basVmCallSub(rt->vm, proc->codeAddr);
|
||||
}
|
||||
|
||||
dvxLog(" <- SUB '%s' returned ok=%d errNum=%d errMsg='%s'",
|
||||
ctrl->eventOverrides[i].handlerName,
|
||||
(int)subOk,
|
||||
(int)rt->vm->errorNumber,
|
||||
rt->vm->errorMsg);
|
||||
|
||||
if (claimedGuard) {
|
||||
ctrl->eventFiring = false;
|
||||
ctrl->firingEventName[0] = '\0';
|
||||
|
|
@ -3131,9 +3079,6 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
|
|||
}
|
||||
|
||||
// No override -- fall back to naming convention (CtrlName_EventName).
|
||||
dvxLog("[FE] no override matched, trying naming convention '%s_%s'",
|
||||
ctrl->name, eventName);
|
||||
|
||||
bool claimedGuard = !ctrl->eventFiring;
|
||||
|
||||
if (claimedGuard) {
|
||||
|
|
@ -3141,16 +3086,12 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
|
|||
snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : "");
|
||||
}
|
||||
|
||||
bool fired;
|
||||
|
||||
if (finalArgCount > 0 && finalArgs) {
|
||||
fired = basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount);
|
||||
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount);
|
||||
} else {
|
||||
fired = basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName);
|
||||
basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName);
|
||||
}
|
||||
|
||||
dvxLog("[FE] naming-convention result: fired=%d", (int)fired);
|
||||
|
||||
if (claimedGuard) {
|
||||
ctrl->eventFiring = false;
|
||||
ctrl->firingEventName[0] = '\0';
|
||||
|
|
@ -3180,6 +3121,20 @@ static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char
|
|||
const char *wgtTypeName = resolveTypeName(typeName);
|
||||
bool isCtrlContainer = false;
|
||||
|
||||
if (!wgtTypeName) {
|
||||
// Unknown widget type in .frm -- catch typos ("Spinner" instead
|
||||
// of "SpinButton") that would otherwise silently drop the
|
||||
// control and surface later as a misleading "control not
|
||||
// found" runtime error. The IDE validator blocks these at
|
||||
// compile time; this is the safety net for bascomp standalone.
|
||||
basFormRtRuntimeError(sFormRt,
|
||||
"Unknown widget type in form",
|
||||
"Form: %s\nControl: %s\nType: %s\nNot a recognized widget type (check spelling; DVX uses VB6-style names, e.g. SpinButton not Spinner).",
|
||||
ctx->form->name,
|
||||
name ? name : "(null)",
|
||||
typeName ? typeName : "(null)");
|
||||
}
|
||||
|
||||
if (wgtTypeName) {
|
||||
WidgetT *parent = ctx->parentStack[ctx->nestDepth - 1];
|
||||
WidgetT *widget = createWidget(wgtTypeName, parent);
|
||||
|
|
@ -3456,6 +3411,10 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha
|
|||
return basValLong((int32_t)ctrl->widget->fgColor);
|
||||
}
|
||||
|
||||
if (strcasecmp(propName, "ToolTipText") == 0) {
|
||||
return basValStringFromC(ctrl->tooltip ? ctrl->tooltip : "");
|
||||
}
|
||||
|
||||
*handled = false;
|
||||
return zeroValue();
|
||||
}
|
||||
|
|
@ -3727,11 +3686,6 @@ static void onWidgetBlur(WidgetT *w) {
|
|||
static void onWidgetChange(WidgetT *w) {
|
||||
BasControlT *ctrl = (BasControlT *)w->userData;
|
||||
|
||||
dvxLog("[OC] onWidgetChange: w=%p ctrl=%p name='%s' type='%s'",
|
||||
(void *)w, (void *)ctrl,
|
||||
ctrl ? ctrl->name : "(null)",
|
||||
ctrl ? ctrl->typeName : "(null)");
|
||||
|
||||
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -3757,13 +3711,6 @@ static void onWidgetChange(WidgetT *w) {
|
|||
static void onWidgetClick(WidgetT *w) {
|
||||
BasControlT *ctrl = (BasControlT *)w->userData;
|
||||
|
||||
dvxLog("[OK] onWidgetClick: w=%p ctrl=%p name='%s' type='%s' form=%p vm=%p",
|
||||
(void *)w, (void *)ctrl,
|
||||
ctrl ? ctrl->name : "(null)",
|
||||
ctrl ? ctrl->typeName : "(null)",
|
||||
ctrl ? (void *)ctrl->form : NULL,
|
||||
ctrl && ctrl->form ? (void *)ctrl->form->vm : NULL);
|
||||
|
||||
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -4662,6 +4609,15 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
|
|||
return true;
|
||||
}
|
||||
|
||||
if (strcasecmp(propName, "ToolTipText") == 0) {
|
||||
BasStringT *s = basValFormatString(value);
|
||||
free(ctrl->tooltip);
|
||||
ctrl->tooltip = (s->len > 0) ? strdup(s->data) : NULL;
|
||||
wgtSetTooltip(ctrl->widget, ctrl->tooltip);
|
||||
basStringUnref(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ typedef struct BasControlT {
|
|||
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
|
||||
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
|
||||
char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1
|
||||
char *tooltip; // heap-owned tooltip text (NULL = none); wgtSetTooltip references this buffer, so it must outlive the widget
|
||||
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
|
||||
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
|
||||
int32_t eventOverrideCount;
|
||||
|
|
|
|||
|
|
@ -1446,7 +1446,16 @@ typedef struct {
|
|||
|
||||
|
||||
typedef struct {
|
||||
IdeCtrlMapEntryT *entries; // stb_ds dynamic array
|
||||
char ctrlName[BAS_MAX_CTRL_NAME]; // control that has the bad type
|
||||
char typeName[BAS_MAX_CTRL_NAME]; // the unrecognised type name
|
||||
char formName[BAS_MAX_CTRL_NAME]; // enclosing form (for error message)
|
||||
} IdeBadTypeT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
IdeCtrlMapEntryT *entries; // stb_ds: known controls
|
||||
IdeBadTypeT *badTypes; // stb_ds: unknown widget types encountered
|
||||
char currentForm[BAS_MAX_CTRL_NAME]; // most recent Begin Form
|
||||
} IdeValidatorCtxT;
|
||||
|
||||
|
||||
|
|
@ -1457,6 +1466,7 @@ static bool ideValidator_onFormBegin(void *ud, const char *name) {
|
|||
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
|
||||
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form");
|
||||
arrput(v->entries, e);
|
||||
snprintf(v->currentForm, BAS_MAX_CTRL_NAME, "%s", name ? name : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1468,6 +1478,24 @@ static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char
|
|||
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
|
||||
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
|
||||
arrput(v->entries, e);
|
||||
|
||||
// Also flag unknown widget types. wgtFindByBasName returns NULL
|
||||
// for anything not registered by a .wgt DXE loaded by the IDE.
|
||||
// Skip internal structural types (they're handled by layout code,
|
||||
// not widget DXEs).
|
||||
if (typeName && typeName[0]) {
|
||||
bool structural = (strcasecmp(typeName, "Form") == 0 ||
|
||||
strcasecmp(typeName, "Menu") == 0);
|
||||
|
||||
if (!structural && !wgtFindByBasName(typeName)) {
|
||||
IdeBadTypeT bad;
|
||||
memset(&bad, 0, sizeof(bad));
|
||||
snprintf(bad.ctrlName, BAS_MAX_CTRL_NAME, "%s", name ? name : "?");
|
||||
snprintf(bad.typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
|
||||
snprintf(bad.formName, BAS_MAX_CTRL_NAME, "%s", v->currentForm);
|
||||
arrput(v->badTypes, bad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1524,9 +1552,14 @@ static bool ideValidator_isCommonProp(const char *propName) {
|
|||
strcasecmp(propName, "Weight") == 0 ||
|
||||
strcasecmp(propName, "Visible") == 0 ||
|
||||
strcasecmp(propName, "Enabled") == 0 ||
|
||||
strcasecmp(propName, "ReadOnly") == 0 ||
|
||||
strcasecmp(propName, "TabIndex") == 0 ||
|
||||
strcasecmp(propName, "BackColor") == 0 ||
|
||||
strcasecmp(propName, "ForeColor") == 0 ||
|
||||
strcasecmp(propName, "Caption") == 0 ||
|
||||
strcasecmp(propName, "Text") == 0 ||
|
||||
strcasecmp(propName, "HelpTopic") == 0 ||
|
||||
strcasecmp(propName, "ToolTipText") == 0 ||
|
||||
strcasecmp(propName, "DataSource") == 0 ||
|
||||
strcasecmp(propName, "DataField") == 0 ||
|
||||
strcasecmp(propName, "ListCount") == 0;
|
||||
|
|
@ -1906,6 +1939,40 @@ static bool compileProject(void) {
|
|||
memset(&validatorCtx, 0, sizeof(validatorCtx));
|
||||
ideBuildCtrlMap(&validatorCtx);
|
||||
|
||||
// Reject unknown widget types up front -- the .frm parse records
|
||||
// any `Begin <Type> <Name>` whose type isn't registered by a .wgt
|
||||
// DXE. Those would silently drop the control at runtime and
|
||||
// produce a misleading "control not found" error on first access.
|
||||
if (arrlen(validatorCtx.badTypes) > 0) {
|
||||
int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n");
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - 256; i++) {
|
||||
IdeBadTypeT *b = &validatorCtx.badTypes[i];
|
||||
n += snprintf(sOutputBuf + n, IDE_MAX_OUTPUT - n,
|
||||
" Form '%s' control '%s': type '%s' is not registered.\n",
|
||||
b->formName, b->ctrlName, b->typeName);
|
||||
}
|
||||
|
||||
n += snprintf(sOutputBuf + n, IDE_MAX_OUTPUT - n,
|
||||
"\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n");
|
||||
sOutputLen = n;
|
||||
setOutputText(sOutputBuf);
|
||||
showOutputWindow();
|
||||
|
||||
if (sOutWin) {
|
||||
dvxRaiseWindow(sAc, sOutWin);
|
||||
}
|
||||
|
||||
setStatus("Compilation failed: unknown widget type.");
|
||||
dvxSetBusy(sAc, false);
|
||||
basParserFree(parser);
|
||||
free(parser);
|
||||
free(concatBuf);
|
||||
arrfree(validatorCtx.entries);
|
||||
arrfree(validatorCtx.badTypes);
|
||||
return false;
|
||||
}
|
||||
|
||||
BasCtrlValidatorT validator;
|
||||
validator.lookupCtrlType = ideValidator_lookupCtrlType;
|
||||
validator.isMethodValid = ideValidator_isMethodValid;
|
||||
|
|
@ -1980,11 +2047,13 @@ static bool compileProject(void) {
|
|||
free(parser);
|
||||
free(concatBuf);
|
||||
arrfree(validatorCtx.entries);
|
||||
arrfree(validatorCtx.badTypes);
|
||||
return false;
|
||||
}
|
||||
|
||||
free(concatBuf);
|
||||
arrfree(validatorCtx.entries);
|
||||
arrfree(validatorCtx.badTypes);
|
||||
|
||||
BasModuleT *mod = basParserBuildModule(parser);
|
||||
basParserFree(parser);
|
||||
|
|
|
|||
|
|
@ -451,6 +451,18 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi
|
|||
|
||||
When the project is compiled, all source files are concatenated into a single source stream. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form.
|
||||
|
||||
.h2 Compile-Time Validation
|
||||
|
||||
Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:
|
||||
|
||||
.list
|
||||
.item Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).
|
||||
.item Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name.
|
||||
.item Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.
|
||||
.endlist
|
||||
|
||||
These checks use the same metadata the Properties panel and Toolbox read from each widget DXE, so property and method names always match the current widget set without any separate list to maintain.
|
||||
|
||||
.h2 Project Operations
|
||||
|
||||
.table
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ name$ = "Hello" ' String
|
|||
Form Example Description
|
||||
---- ------- -----------
|
||||
Decimal integer 42 Values -32768..32767 are Integer; larger are Long
|
||||
Hex integer &HFF, &H1234 Hexadecimal literal
|
||||
Hex integer &HFF, &H1234 Hexadecimal literal (0-9, A-F)
|
||||
Octal integer &O10, &O77 Octal literal (0-7)
|
||||
Binary integer &B1010, &B11111111 Binary literal (0-1)
|
||||
Integer suffix 42% Force Integer type
|
||||
Long suffix 42&, &HFF& Force Long type
|
||||
Floating-point 3.14, 1.5E10, 2.5D3 Any number containing a decimal point or exponent is Double by default
|
||||
|
|
@ -669,7 +671,7 @@ Sub Greet(ByVal name As String)
|
|||
End Sub
|
||||
.endcode
|
||||
|
||||
Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
|
||||
Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
|
||||
|
||||
.code
|
||||
Greet "World"
|
||||
|
|
@ -1253,6 +1255,7 @@ bytes = FILELEN(filename$)
|
|||
.index LEN
|
||||
.index LTRIM$
|
||||
.index MID$
|
||||
.index OCT$
|
||||
.index RIGHT$
|
||||
.index RTRIM$
|
||||
.index SPACE$
|
||||
|
|
@ -1279,6 +1282,7 @@ bytes = FILELEN(filename$)
|
|||
LTRIM$(s$) String Removes leading spaces from s$
|
||||
MID$(s$, start) String Substring from start (1-based) to end of string
|
||||
MID$(s$, start, length) String Substring of length characters starting at start
|
||||
OCT$(n) String Octal representation of n (no leading &O)
|
||||
RIGHT$(s$, n) String Rightmost n characters of s$
|
||||
RTRIM$(s$) String Removes trailing spaces from s$
|
||||
SPACE$(n) String String of n spaces
|
||||
|
|
@ -1387,6 +1391,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
|
|||
.topic lang.func.conversion
|
||||
.title Conversion Functions
|
||||
.toc 2 Conversion Functions
|
||||
.index CBOOL
|
||||
.index CDBL
|
||||
.index CINT
|
||||
.index CLNG
|
||||
|
|
@ -1398,6 +1403,7 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
|
|||
.table
|
||||
Function Returns Description
|
||||
-------- ------- -----------
|
||||
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
|
||||
CDBL(n) Double Converts n to Double
|
||||
CINT(n) Integer Converts n to Integer (rounds half away from zero)
|
||||
CLNG(n) Long Converts n to Long
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
#include "runtime/vm.h"
|
||||
#include "runtime/values.h"
|
||||
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -381,6 +383,329 @@ static void testRuntimeError(const char *name, const char *source, const char *n
|
|||
#define TEST_RUNTIME_ERROR(n, s, e) testRuntimeError((n), (s), (e))
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Widget-level event-dispatch harness
|
||||
// ============================================================
|
||||
//
|
||||
// formrt.c depends on the full DVX widget/window stack and can't be
|
||||
// linked into the host test binary. This harness mirrors the dispatch
|
||||
// semantics of fireCtrlEvent / basFormRtFireEvent using stand-alone
|
||||
// mini-structs and a bare minimum of VM calls.
|
||||
//
|
||||
// IMPORTANT: if you change the dispatch rules in formrt.c (event
|
||||
// override table, re-entrancy guard, form-scope-vars binding), update
|
||||
// the matching code below. The tests are re-implementation tests,
|
||||
// not link-through tests.
|
||||
|
||||
#define MINI_MAX_NAME 32
|
||||
#define MINI_MAX_OVERRIDES 16
|
||||
|
||||
typedef struct {
|
||||
char eventName[MINI_MAX_NAME];
|
||||
char handlerName[MINI_MAX_NAME];
|
||||
} MiniOverrideT;
|
||||
|
||||
|
||||
typedef struct MiniCtrlT {
|
||||
char name[MINI_MAX_NAME];
|
||||
char typeName[MINI_MAX_NAME];
|
||||
struct MiniFormT *form;
|
||||
MiniOverrideT overrides[MINI_MAX_OVERRIDES];
|
||||
int32_t overrideCount;
|
||||
bool eventFiring;
|
||||
char firingEventName[MINI_MAX_NAME];
|
||||
} MiniCtrlT;
|
||||
|
||||
|
||||
typedef struct MiniFormT {
|
||||
char name[MINI_MAX_NAME];
|
||||
MiniCtrlT *ctrls; // stb_ds array
|
||||
BasValueT *formVars;
|
||||
int32_t formVarCount;
|
||||
} MiniFormT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
BasVmT *vm;
|
||||
BasModuleT *module;
|
||||
MiniFormT **forms; // stb_ds array of heap-alloc'd forms
|
||||
MiniFormT *currentForm;
|
||||
} MiniRtT;
|
||||
|
||||
|
||||
static MiniFormT *miniCreateForm(MiniRtT *rt, const char *name, int32_t formVarCount) {
|
||||
MiniFormT *f = (MiniFormT *)calloc(1, sizeof(MiniFormT));
|
||||
snprintf(f->name, MINI_MAX_NAME, "%s", name);
|
||||
f->ctrls = NULL;
|
||||
|
||||
if (formVarCount > 0) {
|
||||
f->formVars = (BasValueT *)calloc(formVarCount, sizeof(BasValueT));
|
||||
f->formVarCount = formVarCount;
|
||||
}
|
||||
|
||||
arrput(rt->forms, f);
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName) {
|
||||
MiniCtrlT c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
snprintf(c.name, MINI_MAX_NAME, "%s", name);
|
||||
snprintf(c.typeName, MINI_MAX_NAME, "%s", typeName ? typeName : "");
|
||||
c.form = f;
|
||||
arrput(f->ctrls, c);
|
||||
return &f->ctrls[arrlen(f->ctrls) - 1];
|
||||
}
|
||||
|
||||
|
||||
static void miniSetEvent(MiniCtrlT *ctrl, const char *eventName, const char *handlerName) {
|
||||
// Match formrt's SetEvent: update existing or append
|
||||
for (int32_t i = 0; i < ctrl->overrideCount; i++) {
|
||||
if (strcasecmp(ctrl->overrides[i].eventName, eventName) == 0) {
|
||||
snprintf(ctrl->overrides[i].handlerName, MINI_MAX_NAME, "%s", handlerName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctrl->overrideCount < MINI_MAX_OVERRIDES) {
|
||||
MiniOverrideT *o = &ctrl->overrides[ctrl->overrideCount++];
|
||||
snprintf(o->eventName, MINI_MAX_NAME, "%s", eventName);
|
||||
snprintf(o->handlerName, MINI_MAX_NAME, "%s", handlerName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const BasProcEntryT *miniFindProc(BasModuleT *mod, const char *name) {
|
||||
if (!mod || !mod->procs || !name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < mod->procCount; i++) {
|
||||
if (strcasecmp(mod->procs[i].name, name) == 0) {
|
||||
return &mod->procs[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static MiniFormT *miniResolveOwningForm(MiniRtT *rt, const BasProcEntryT *proc) {
|
||||
if (!rt || !proc || !proc->formName[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
|
||||
if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) {
|
||||
return rt->forms[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Mirror of formrt.c's fireCtrlEvent. Re-entrancy guard, override
|
||||
// table, form-scope vars binding -- kept in sync by copy.
|
||||
static void miniFireCtrlEvent(MiniRtT *rt, MiniCtrlT *ctrl, const char *eventName) {
|
||||
if (!rt || !ctrl || !eventName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctrl->eventFiring) {
|
||||
bool sameEvent = (ctrl->firingEventName[0] &&
|
||||
strcasecmp(ctrl->firingEventName, eventName) == 0);
|
||||
if (sameEvent) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Event-override path (SetEvent)
|
||||
for (int32_t i = 0; i < ctrl->overrideCount; i++) {
|
||||
if (strcasecmp(ctrl->overrides[i].eventName, eventName) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!rt->vm || !rt->module) {
|
||||
return;
|
||||
}
|
||||
|
||||
const BasProcEntryT *proc = miniFindProc(rt->module, ctrl->overrides[i].handlerName);
|
||||
|
||||
if (!proc || proc->isFunction) {
|
||||
return;
|
||||
}
|
||||
|
||||
MiniFormT *prevForm = rt->currentForm;
|
||||
BasValueT *prevVars = rt->vm->currentFormVars;
|
||||
int32_t prevVarCount = rt->vm->currentFormVarCount;
|
||||
|
||||
MiniFormT *owningForm = miniResolveOwningForm(rt, proc);
|
||||
MiniFormT *varsForm = owningForm ? owningForm : ctrl->form;
|
||||
|
||||
rt->currentForm = ctrl->form;
|
||||
basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount);
|
||||
|
||||
bool claimedGuard = !ctrl->eventFiring;
|
||||
|
||||
if (claimedGuard) {
|
||||
ctrl->eventFiring = true;
|
||||
snprintf(ctrl->firingEventName, MINI_MAX_NAME, "%s", eventName);
|
||||
}
|
||||
|
||||
rt->vm->errorMsg[0] = '\0';
|
||||
rt->vm->errorNumber = 0;
|
||||
basVmCallSub(rt->vm, proc->codeAddr);
|
||||
|
||||
if (claimedGuard) {
|
||||
ctrl->eventFiring = false;
|
||||
ctrl->firingEventName[0] = '\0';
|
||||
}
|
||||
|
||||
rt->currentForm = prevForm;
|
||||
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Naming-convention fallback: CtrlName_EventName
|
||||
char handlerName[MINI_MAX_NAME * 2];
|
||||
snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrl->name, eventName);
|
||||
|
||||
const BasProcEntryT *proc = miniFindProc(rt->module, handlerName);
|
||||
|
||||
if (!proc || proc->isFunction) {
|
||||
return;
|
||||
}
|
||||
|
||||
MiniFormT *prevForm = rt->currentForm;
|
||||
BasValueT *prevVars = rt->vm->currentFormVars;
|
||||
int32_t prevVarCount = rt->vm->currentFormVarCount;
|
||||
|
||||
MiniFormT *owningForm = miniResolveOwningForm(rt, proc);
|
||||
MiniFormT *varsForm = owningForm ? owningForm : ctrl->form;
|
||||
|
||||
rt->currentForm = ctrl->form;
|
||||
basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount);
|
||||
|
||||
bool claimedGuard = !ctrl->eventFiring;
|
||||
|
||||
if (claimedGuard) {
|
||||
ctrl->eventFiring = true;
|
||||
snprintf(ctrl->firingEventName, MINI_MAX_NAME, "%s", eventName);
|
||||
}
|
||||
|
||||
basVmCallSub(rt->vm, proc->codeAddr);
|
||||
|
||||
if (claimedGuard) {
|
||||
ctrl->eventFiring = false;
|
||||
ctrl->firingEventName[0] = '\0';
|
||||
}
|
||||
|
||||
rt->currentForm = prevForm;
|
||||
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
|
||||
}
|
||||
|
||||
|
||||
// Context for an event-dispatch test: what to build and how to fire.
|
||||
typedef struct {
|
||||
const char *source; // BASIC source
|
||||
const char *formName; // "" if ctrl has no owning BEGINFORM
|
||||
int32_t formVarCount; // >0 if the module has form-scope vars
|
||||
const char *ctrlName; // control to fire events on
|
||||
const char *ctrlType; // widget type, e.g. "CommandButton" / "Timer"
|
||||
// Array of (eventName, handlerName) for SetEvent. Terminate with NULL.
|
||||
const char *overrides[2 * 8];
|
||||
// Array of event names to fire in order. Terminate with NULL.
|
||||
const char *fires[8];
|
||||
} TestDispatchT;
|
||||
|
||||
|
||||
static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected) {
|
||||
int32_t len = (int32_t)strlen(d->source);
|
||||
|
||||
BasParserT parser;
|
||||
basParserInit(&parser, d->source, len);
|
||||
|
||||
if (!basParse(&parser)) {
|
||||
char detail[256];
|
||||
snprintf(detail, sizeof(detail), "compile error: %s", parser.error);
|
||||
reportFail(name, detail);
|
||||
basParserFree(&parser);
|
||||
return;
|
||||
}
|
||||
|
||||
BasModuleT *mod = basParserBuildModule(&parser);
|
||||
basParserFree(&parser);
|
||||
|
||||
if (!mod) {
|
||||
reportFail(name, "module build failed");
|
||||
return;
|
||||
}
|
||||
|
||||
BasVmT *vm = basVmCreate();
|
||||
basVmLoadModule(vm, mod);
|
||||
|
||||
CaptureT cap;
|
||||
captureReset(&cap);
|
||||
basVmSetPrintCallback(vm, capturePrint, &cap);
|
||||
|
||||
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
|
||||
vm->callDepth = 1;
|
||||
|
||||
// Run module-level init (populates globals, runs BEGINFORM init block).
|
||||
basVmRun(vm);
|
||||
|
||||
// Build the mini runtime + form + control.
|
||||
MiniRtT rt;
|
||||
memset(&rt, 0, sizeof(rt));
|
||||
rt.vm = vm;
|
||||
rt.module = mod;
|
||||
|
||||
MiniFormT *form = miniCreateForm(&rt, d->formName[0] ? d->formName : "_NoForm", d->formVarCount);
|
||||
MiniCtrlT *ctrl = miniAddCtrl(form, d->ctrlName, d->ctrlType);
|
||||
|
||||
// Wire overrides.
|
||||
for (int32_t i = 0; d->overrides[i] && d->overrides[i + 1]; i += 2) {
|
||||
miniSetEvent(ctrl, d->overrides[i], d->overrides[i + 1]);
|
||||
}
|
||||
|
||||
// Fire the sequence of events.
|
||||
for (int32_t i = 0; d->fires[i]; i++) {
|
||||
miniFireCtrlEvent(&rt, ctrl, d->fires[i]);
|
||||
}
|
||||
|
||||
if (strcmp(cap.buf, expected) == 0) {
|
||||
reportPass(name);
|
||||
} else {
|
||||
reportFailDiff(name, expected, cap.buf);
|
||||
}
|
||||
|
||||
// Teardown.
|
||||
for (int32_t i = 0; i < (int32_t)arrlen(rt.forms); i++) {
|
||||
MiniFormT *f = rt.forms[i];
|
||||
if (f->formVars) {
|
||||
for (int32_t j = 0; j < f->formVarCount; j++) {
|
||||
basValRelease(&f->formVars[j]);
|
||||
}
|
||||
free(f->formVars);
|
||||
}
|
||||
arrfree(f->ctrls);
|
||||
free(f);
|
||||
}
|
||||
arrfree(rt.forms);
|
||||
|
||||
basVmDestroy(vm);
|
||||
basModuleFree(mod);
|
||||
}
|
||||
|
||||
|
||||
// Helper macros to keep tests compact. `overrides` and `fires` are
|
||||
// comma lists terminated with NULL/NULL.
|
||||
#define DISPATCH_OVR(...) { __VA_ARGS__, NULL, NULL }
|
||||
#define DISPATCH_FIRE(...) { __VA_ARGS__, NULL }
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Test cases
|
||||
// ============================================================
|
||||
|
|
@ -2043,6 +2368,241 @@ int main(void) {
|
|||
"driver",
|
||||
"ok\ncaught\nok\n");
|
||||
|
||||
// ============================================================
|
||||
// Widget-level event dispatch (mirrors formrt.c fireCtrlEvent)
|
||||
// ============================================================
|
||||
|
||||
// SetEvent override: handler name differs from Ctrl_Event convention
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source = "SUB handleIt\n PRINT \"clicked\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.overrides = DISPATCH_OVR("Click", "handleIt"),
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-override", &d, "clicked\n");
|
||||
}
|
||||
|
||||
// Naming-convention fallback: no override, CtrlName_EventName SUB
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source = "SUB btn1_Click\n PRINT \"fallback\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-naming-convention", &d, "fallback\n");
|
||||
}
|
||||
|
||||
// Override wins over naming convention
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"SUB btn1_Click\n PRINT \"convention\"\nEND SUB\n"
|
||||
"SUB doOverride\n PRINT \"override\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.overrides = DISPATCH_OVR("Click", "doOverride"),
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-override-beats-naming", &d, "override\n");
|
||||
}
|
||||
|
||||
// Handler missing: silently skipped, no crash
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source = "SUB unrelated\n PRINT \"never\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-no-handler", &d, "");
|
||||
}
|
||||
|
||||
// Multiple events on same control
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"SUB btn1_Click\n PRINT \"click\"\nEND SUB\n"
|
||||
"SUB btn1_GotFocus\n PRINT \"focus\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("GotFocus", "Click", "GotFocus"),
|
||||
};
|
||||
testDispatchEq("dispatch-multiple-events", &d, "focus\nclick\nfocus\n");
|
||||
}
|
||||
|
||||
// Globals accumulate across events (real apps' common pattern)
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"DIM counter AS INTEGER\n"
|
||||
"counter = 0\n"
|
||||
"SUB btn1_Click\n"
|
||||
" counter = counter + 1\n"
|
||||
" PRINT counter\n"
|
||||
"END SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click", "Click", "Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-global-counter", &d, "1 \n2 \n3 \n");
|
||||
}
|
||||
|
||||
// Form-scope vars accessed via SUB declared in BEGINFORM
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"BEGINFORM \"MyForm\"\n"
|
||||
"DIM hits AS INTEGER\n"
|
||||
"SUB btn1_Click\n"
|
||||
" hits = hits + 1\n"
|
||||
" PRINT hits\n"
|
||||
"END SUB\n"
|
||||
"ENDFORM\n",
|
||||
.formName = "MyForm",
|
||||
.formVarCount = 1,
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click", "Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-form-scope-vars", &d, "1 \n2 \n");
|
||||
}
|
||||
|
||||
// ON ERROR inside the handler traps the error and the handler is
|
||||
// reusable on the next fire (inErrorHandler must reset cleanly).
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"SUB btn1_Click\n"
|
||||
" ON ERROR GOTO h\n"
|
||||
" DIM a AS INTEGER\n DIM b AS INTEGER\n"
|
||||
" a = 10\n b = 0\n"
|
||||
" PRINT a \\ b\n"
|
||||
" EXIT SUB\n"
|
||||
" h:\n"
|
||||
" PRINT \"caught\"\n"
|
||||
"END SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click", "Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-on-error-reusable", &d, "caught\ncaught\n");
|
||||
}
|
||||
|
||||
// Handler that recurses into the same SUB via a method-ish call.
|
||||
// The re-entrancy guard rejects same-event re-entry. Outer run
|
||||
// produces one PRINT; inner attempt is swallowed.
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"DIM depth AS INTEGER\n"
|
||||
"depth = 0\n"
|
||||
"SUB btn1_Click\n"
|
||||
" depth = depth + 1\n"
|
||||
" PRINT depth\n"
|
||||
"END SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-guard-single-fire", &d, "1 \n");
|
||||
}
|
||||
|
||||
// SetEvent override twice on the same event name: second wins.
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"SUB first\n PRINT \"first\"\nEND SUB\n"
|
||||
"SUB second\n PRINT \"second\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.overrides = DISPATCH_OVR("Click", "first", "Click", "second"),
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-override-replace", &d, "second\n");
|
||||
}
|
||||
|
||||
// Timer-like pattern: onChange fires "Timer" (mapped in formrt by
|
||||
// ctrl->typeName == "Timer"); here we exercise the SetEvent path
|
||||
// with a Timer-typed control.
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"DIM ticks AS INTEGER\n"
|
||||
"ticks = 0\n"
|
||||
"SUB onTick\n"
|
||||
" ticks = ticks + 1\n"
|
||||
" PRINT ticks\n"
|
||||
"END SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "t1",
|
||||
.ctrlType = "Timer",
|
||||
.overrides = DISPATCH_OVR("Timer", "onTick"),
|
||||
.fires = DISPATCH_FIRE("Timer", "Timer", "Timer"),
|
||||
};
|
||||
testDispatchEq("dispatch-timer-override", &d, "1 \n2 \n3 \n");
|
||||
}
|
||||
|
||||
// Menu-like dispatch via naming convention (proxies have typeName "")
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source = "SUB mnuFoo_Click\n PRINT \"menu\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "mnuFoo",
|
||||
.ctrlType = "Menu",
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-menu-click", &d, "menu\n");
|
||||
}
|
||||
|
||||
// Handler calls into another SUB. Verifies nested SUB calls don't
|
||||
// interact badly with the event guard.
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"SUB say(s AS STRING)\n PRINT s\nEND SUB\n"
|
||||
"SUB btn1_Click\n"
|
||||
" say \"a\"\n say \"b\"\n say \"c\"\n"
|
||||
"END SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.fires = DISPATCH_FIRE("Click"),
|
||||
};
|
||||
testDispatchEq("dispatch-handler-nested-calls", &d, "a\nb\nc\n");
|
||||
}
|
||||
|
||||
// Different events on the same control during a single sequence:
|
||||
// GotFocus, Click, LostFocus. Guard lets each event through.
|
||||
{
|
||||
TestDispatchT d = {
|
||||
.source =
|
||||
"SUB onFocus\n PRINT \"focus\"\nEND SUB\n"
|
||||
"SUB onClick\n PRINT \"click\"\nEND SUB\n"
|
||||
"SUB onBlur\n PRINT \"blur\"\nEND SUB\n",
|
||||
.formName = "",
|
||||
.ctrlName = "btn1",
|
||||
.ctrlType = "CommandButton",
|
||||
.overrides = DISPATCH_OVR(
|
||||
"GotFocus", "onFocus",
|
||||
"Click", "onClick",
|
||||
"LostFocus", "onBlur"),
|
||||
.fires = DISPATCH_FIRE("GotFocus", "Click", "LostFocus"),
|
||||
};
|
||||
testDispatchEq("dispatch-focus-click-blur", &d, "focus\nclick\nblur\n");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Operator precedence edge cases
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1020,11 +1020,9 @@ static void navigateToTopic(int32_t topicIdx) {
|
|||
}
|
||||
|
||||
if (topicIdx == sCurrentTopic) {
|
||||
dvxLog("[WRAP] navigateToTopic: SKIPPED (same topic %d)", (int)topicIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
dvxLog("[WRAP] navigateToTopic: %d -> %d", (int)sCurrentTopic, (int)topicIdx);
|
||||
historyPush(topicIdx);
|
||||
sCurrentTopic = topicIdx;
|
||||
buildContentWidgets();
|
||||
|
|
@ -1039,7 +1037,6 @@ static void navigateToTopic(int32_t topicIdx) {
|
|||
// Sync TOC tree selection
|
||||
if (sTocTree) {
|
||||
WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx);
|
||||
dvxLog("[WRAP] treeSync: topicIdx=%d item=%p", (int)topicIdx, (void *)item);
|
||||
|
||||
if (item) {
|
||||
// Suppress onChange to avoid re-entering navigateToTopic
|
||||
|
|
|
|||
BIN
src/apps/kpunch/widshow/ICON32.BMP
(Stored with Git LFS)
Normal file
BIN
src/apps/kpunch/widshow/ICON32.BMP
(Stored with Git LFS)
Normal file
Binary file not shown.
14
src/apps/kpunch/widshow/widshow.dbp
Normal file
14
src/apps/kpunch/widshow/widshow.dbp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Project]
|
||||
Name = Widget Showcase
|
||||
Author = Scott Duensing
|
||||
Publisher = Kangaroo Punch Studios
|
||||
Copyright = Copyright 2026 Scott Duensing
|
||||
Description = Tour of DVX BASIC widgets not covered by BASIC Demo
|
||||
Icon = ICON32.BMP
|
||||
|
||||
[Forms]
|
||||
File0 = widshow.frm
|
||||
Count = 1
|
||||
|
||||
[Settings]
|
||||
StartupForm = WidgetShow
|
||||
396
src/apps/kpunch/widshow/widshow.frm
Normal file
396
src/apps/kpunch/widshow/widshow.frm
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
VERSION DVX 1.00
|
||||
|
||||
Begin Form WidgetShow
|
||||
Caption = "Widget Showcase"
|
||||
Layout = VBox
|
||||
AutoSize = False
|
||||
Resizable = True
|
||||
Centered = True
|
||||
Width = 620
|
||||
Height = 440
|
||||
|
||||
Begin TabStrip tabs
|
||||
Weight = 1
|
||||
|
||||
Begin TabPage pgText
|
||||
Caption = "&Text"
|
||||
|
||||
Begin Label lbl1
|
||||
Caption = "TextBox echoes into a Label on the right."
|
||||
Weight = 0
|
||||
End
|
||||
Begin HBox rowInput
|
||||
Weight = 0
|
||||
Begin Label lblPrompt
|
||||
Caption = "Type:"
|
||||
Weight = 0
|
||||
End
|
||||
Begin TextBox txtInput
|
||||
Weight = 1
|
||||
ToolTipText = "A single-line editable text field. Fires Change on every keystroke."
|
||||
End
|
||||
Begin Label lblEcho
|
||||
Caption = "(echo)"
|
||||
Weight = 1
|
||||
ToolTipText = "A static Label. Its Caption property is being driven by the TextBox."
|
||||
End
|
||||
End
|
||||
Begin Spacer spc1
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
|
||||
Begin TabPage pgPick
|
||||
Caption = "&Picks"
|
||||
|
||||
Begin Label lbl2
|
||||
Caption = "ComboBox (typeable), DropDown, SpinButton, CheckBox."
|
||||
Weight = 0
|
||||
End
|
||||
Begin HBox rowPick
|
||||
Weight = 0
|
||||
Begin ComboBox cboSize
|
||||
Weight = 1
|
||||
ToolTipText = "ComboBox: pick from the list OR type your own value."
|
||||
End
|
||||
Begin DropDown ddColor
|
||||
Weight = 1
|
||||
ToolTipText = "DropDown: read-only list. Pick one of the options."
|
||||
End
|
||||
Begin SpinButton spnQty
|
||||
Weight = 0
|
||||
ToolTipText = "SpinButton: numeric input with up/down arrows."
|
||||
End
|
||||
Begin CheckBox chkLoud
|
||||
Caption = "Loud"
|
||||
Weight = 0
|
||||
ToolTipText = "CheckBox: toggle a boolean flag."
|
||||
End
|
||||
End
|
||||
|
||||
Begin Label lbl3
|
||||
Caption = "OptionButtons form a mutually exclusive group."
|
||||
Weight = 0
|
||||
End
|
||||
Begin HBox rowRadio
|
||||
Weight = 0
|
||||
Begin OptionButton rdoSmall
|
||||
Caption = "Small"
|
||||
Weight = 1
|
||||
ToolTipText = "OptionButton: picking one clears siblings in the same container."
|
||||
End
|
||||
Begin OptionButton rdoMed
|
||||
Caption = "Medium"
|
||||
Weight = 1
|
||||
ToolTipText = "OptionButton: picking one clears siblings in the same container."
|
||||
End
|
||||
Begin OptionButton rdoLarge
|
||||
Caption = "Large"
|
||||
Weight = 1
|
||||
ToolTipText = "OptionButton: picking one clears siblings in the same container."
|
||||
End
|
||||
End
|
||||
Begin Spacer spc2
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
|
||||
Begin TabPage pgRange
|
||||
Caption = "&Range"
|
||||
|
||||
Begin Label lbl4
|
||||
Caption = "HScrollBar drives a ProgressBar."
|
||||
Weight = 0
|
||||
End
|
||||
Begin HBox rowRange
|
||||
Weight = 0
|
||||
Begin HScrollBar sldVal
|
||||
Weight = 2
|
||||
ToolTipText = "HScrollBar: drag the thumb; fires Change with the new value."
|
||||
End
|
||||
Begin ProgressBar prgVal
|
||||
Weight = 2
|
||||
ToolTipText = "ProgressBar: visual-only indicator; set Value 0-100."
|
||||
End
|
||||
Begin Label lblVal
|
||||
Caption = "0"
|
||||
Weight = 0
|
||||
End
|
||||
End
|
||||
Begin Spacer spc3
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
|
||||
Begin TabPage pgLists
|
||||
Caption = "&Lists"
|
||||
|
||||
Begin Label lbl5
|
||||
Caption = "ListBox, ListView, and TreeView."
|
||||
Weight = 0
|
||||
End
|
||||
Begin HBox rowLists
|
||||
Weight = 1
|
||||
Begin VBox colList
|
||||
Weight = 1
|
||||
Begin ListBox lstItems
|
||||
Weight = 1
|
||||
ToolTipText = "ListBox: simple vertical list of strings."
|
||||
End
|
||||
Begin HBox rowLB
|
||||
Weight = 0
|
||||
Begin CommandButton btnAdd
|
||||
Caption = "Add"
|
||||
Weight = 1
|
||||
ToolTipText = "Append a new item to the list."
|
||||
End
|
||||
Begin CommandButton btnDel
|
||||
Caption = "Del"
|
||||
Weight = 1
|
||||
ToolTipText = "Remove the selected item."
|
||||
End
|
||||
End
|
||||
End
|
||||
Begin ListView lvFiles
|
||||
Weight = 1
|
||||
ToolTipText = "ListView: multi-column table with selectable rows."
|
||||
End
|
||||
Begin TreeView tvTree
|
||||
Weight = 1
|
||||
ToolTipText = "TreeView: hierarchical list with collapsible nodes."
|
||||
End
|
||||
End
|
||||
End
|
||||
|
||||
Begin TabPage pgAbout
|
||||
Caption = "&About"
|
||||
|
||||
Begin Label lblAbout1
|
||||
Caption = "DVX BASIC Widget Showcase"
|
||||
Weight = 0
|
||||
End
|
||||
Begin Label lblAbout2
|
||||
Caption = "Exercises the standard widgets"
|
||||
Weight = 0
|
||||
End
|
||||
Begin Label lblAbout3
|
||||
Caption = "available to BASIC programs."
|
||||
Weight = 0
|
||||
End
|
||||
Begin Label lblAbout4
|
||||
Caption = " "
|
||||
Weight = 0
|
||||
End
|
||||
Begin Label lblAbout5
|
||||
Caption = "Hover any widget for a tooltip."
|
||||
Weight = 0
|
||||
End
|
||||
Begin Label lblAbout6
|
||||
Caption = "The status bar below reflects the"
|
||||
Weight = 0
|
||||
End
|
||||
Begin Label lblAbout7
|
||||
Caption = "last event fired by the widget."
|
||||
Weight = 0
|
||||
End
|
||||
Begin Spacer spc4
|
||||
Weight = 1
|
||||
End
|
||||
End
|
||||
End
|
||||
|
||||
Begin StatusBar sbar
|
||||
Caption = "Ready."
|
||||
Weight = 0
|
||||
End
|
||||
End
|
||||
|
||||
|
||||
' The MIT License (MIT)
|
||||
'
|
||||
' Copyright (C) 2026 Scott Duensing
|
||||
'
|
||||
' Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
' of this software and associated documentation files (the "Software"), to
|
||||
' deal in the Software without restriction, including without limitation the
|
||||
' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
' sell copies of the Software, and to permit persons to whom the Software is
|
||||
' furnished to do so, subject to the following conditions:
|
||||
'
|
||||
' The above copyright notice and this permission notice shall be included in
|
||||
' all copies or substantial portions of the Software.
|
||||
'
|
||||
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
' IN THE SOFTWARE.
|
||||
|
||||
|
||||
OPTION EXPLICIT
|
||||
|
||||
DIM addCount AS INTEGER
|
||||
|
||||
|
||||
SUB setStatus(msg AS STRING)
|
||||
sbar.Caption = msg
|
||||
END SUB
|
||||
|
||||
|
||||
Load WidgetShow
|
||||
WidgetShow.Show
|
||||
|
||||
cboSize.AddItem "Small"
|
||||
cboSize.AddItem "Medium"
|
||||
cboSize.AddItem "Large"
|
||||
|
||||
ddColor.AddItem "Red"
|
||||
ddColor.AddItem "Green"
|
||||
ddColor.AddItem "Blue"
|
||||
|
||||
spnQty.SetRange 0, 99
|
||||
spnQty.Value = 5
|
||||
|
||||
lstItems.AddItem "Apple"
|
||||
lstItems.AddItem "Banana"
|
||||
lstItems.AddItem "Cherry"
|
||||
addCount = 0
|
||||
|
||||
lvFiles.SetColumns "Name,12|Size,8|Type,8"
|
||||
lvFiles.AddItem "readme.txt"
|
||||
lvFiles.SetCell 0, 1, "1024"
|
||||
lvFiles.SetCell 0, 2, "Text"
|
||||
lvFiles.AddItem "app.exe"
|
||||
lvFiles.SetCell 1, 1, "65536"
|
||||
lvFiles.SetCell 1, 2, "EXE"
|
||||
lvFiles.AddItem "data.dat"
|
||||
lvFiles.SetCell 2, 1, "8192"
|
||||
lvFiles.SetCell 2, 2, "Binary"
|
||||
|
||||
tvTree.AddItem "Animals"
|
||||
tvTree.AddChildItem 0, "Cat"
|
||||
tvTree.AddChildItem 0, "Dog"
|
||||
tvTree.AddChildItem 0, "Fox"
|
||||
tvTree.AddItem "Plants"
|
||||
tvTree.AddChildItem 4, "Oak"
|
||||
tvTree.AddChildItem 4, "Pine"
|
||||
tvTree.SetExpanded 0, -1
|
||||
tvTree.SetExpanded 4, -1
|
||||
|
||||
setStatus "Ready. Hover widgets for descriptions."
|
||||
|
||||
|
||||
' ============================================================
|
||||
' Event handlers
|
||||
' ============================================================
|
||||
|
||||
SUB tabs_Click
|
||||
setStatus "Tab: " + STR$(tabs.GetActive())
|
||||
END SUB
|
||||
|
||||
|
||||
SUB txtInput_Change
|
||||
lblEcho.Caption = txtInput.Text
|
||||
setStatus "Typed: " + txtInput.Text
|
||||
END SUB
|
||||
|
||||
|
||||
SUB cboSize_Change
|
||||
setStatus "Size: " + cboSize.Text
|
||||
END SUB
|
||||
|
||||
|
||||
SUB ddColor_Change
|
||||
setStatus "Color: " + ddColor.List(ddColor.ListIndex)
|
||||
END SUB
|
||||
|
||||
|
||||
SUB spnQty_Change
|
||||
setStatus "Quantity: " + STR$(spnQty.Value)
|
||||
END SUB
|
||||
|
||||
|
||||
SUB chkLoud_Click
|
||||
IF chkLoud.Value THEN
|
||||
setStatus "LOUD MODE ON"
|
||||
ELSE
|
||||
setStatus "quiet"
|
||||
END IF
|
||||
END SUB
|
||||
|
||||
|
||||
SUB rdoSmall_Click
|
||||
setStatus "Selected Small"
|
||||
END SUB
|
||||
|
||||
|
||||
SUB rdoMed_Click
|
||||
setStatus "Selected Medium"
|
||||
END SUB
|
||||
|
||||
|
||||
SUB rdoLarge_Click
|
||||
setStatus "Selected Large"
|
||||
END SUB
|
||||
|
||||
|
||||
SUB btnAdd_Click
|
||||
addCount = addCount + 1
|
||||
lstItems.AddItem "Item " + STR$(addCount)
|
||||
setStatus "Added; " + STR$(lstItems.ListCount()) + " entries."
|
||||
END SUB
|
||||
|
||||
|
||||
SUB btnDel_Click
|
||||
IF lstItems.ListIndex >= 0 THEN
|
||||
lstItems.RemoveItem lstItems.ListIndex
|
||||
setStatus "Removed; " + STR$(lstItems.ListCount()) + " entries."
|
||||
ELSE
|
||||
setStatus "Nothing selected."
|
||||
END IF
|
||||
END SUB
|
||||
|
||||
|
||||
SUB lstItems_Click
|
||||
IF lstItems.ListIndex >= 0 THEN
|
||||
setStatus "ListBox: " + lstItems.List(lstItems.ListIndex)
|
||||
END IF
|
||||
END SUB
|
||||
|
||||
|
||||
SUB lvFiles_Click
|
||||
DIM i AS INTEGER
|
||||
FOR i = 0 TO lvFiles.RowCount() - 1
|
||||
IF lvFiles.IsItemSelected(i) THEN
|
||||
setStatus "ListView: " + lvFiles.GetCell(i, 0) + " (" + lvFiles.GetCell(i, 1) + " bytes)"
|
||||
EXIT SUB
|
||||
END IF
|
||||
NEXT i
|
||||
END SUB
|
||||
|
||||
|
||||
SUB tvTree_Click
|
||||
DIM i AS INTEGER
|
||||
FOR i = 0 TO tvTree.ItemCount() - 1
|
||||
IF tvTree.IsItemSelected(i) THEN
|
||||
setStatus "Tree: " + tvTree.GetItemText(i)
|
||||
EXIT SUB
|
||||
END IF
|
||||
NEXT i
|
||||
END SUB
|
||||
|
||||
|
||||
SUB sldVal_Change
|
||||
DIM v AS INTEGER
|
||||
v = sldVal.Value
|
||||
prgVal.Value = v
|
||||
lblVal.Caption = STR$(v)
|
||||
setStatus "Slider: " + STR$(v) + "%"
|
||||
END SUB
|
||||
|
||||
|
||||
SUB WidgetShow_Unload
|
||||
END
|
||||
END SUB
|
||||
|
|
@ -3623,6 +3623,10 @@ Register a widget API struct under a name. Each widget DXE registers its API dur
|
|||
api Pointer to the widget's API struct
|
||||
.endtable
|
||||
|
||||
.note warning
|
||||
The create function must be the first field of the API struct. BASIC's form runtime instantiates widgets by dereferencing the api pointer as a pointer-to-function-pointer (see createWidgetByIface), so whichever field is first is what gets invoked. If the struct has multiple constructor variants, the one matching createSig must come first; put helper constructors, index accessors, and other entry points after it.
|
||||
.endnote
|
||||
|
||||
.h3 wgtGetApi
|
||||
|
||||
.code
|
||||
|
|
|
|||
|
|
@ -636,11 +636,15 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
|||
(void)orient;
|
||||
(void)value;
|
||||
|
||||
// Repaint with new scroll position -- dvxInvalidateWindow calls onPaint
|
||||
// Repaint with new scroll position. PAINT_FULL is required so
|
||||
// widgetOnPaint re-lays out children at the new root x/y offset;
|
||||
// otherwise children keep their old positions and the content
|
||||
// does not visibly scroll.
|
||||
if (win->widgetRoot) {
|
||||
AppContextT *ctx = wgtGetContext(win->widgetRoot);
|
||||
|
||||
if (ctx) {
|
||||
win->paintNeeded = PAINT_FULL;
|
||||
dvxInvalidateWindow(ctx, win);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ A read-only drop-down list. Unlike ComboBox, the user cannot type free text; the
|
|||
RemoveItem index% Remove the item at the given index.
|
||||
.endtable
|
||||
|
||||
Default Event: Click
|
||||
Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup)
|
||||
|
||||
.h2 Example
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ Sub Form_Load ()
|
|||
DropDown1.ListIndex = 1
|
||||
End Sub
|
||||
|
||||
Sub DropDown1_Click ()
|
||||
Sub DropDown1_Change ()
|
||||
Label1.Caption = "Picked: " & DropDown1.List(DropDown1.ListIndex)
|
||||
End Sub
|
||||
.endcode
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@ static const WgtIfaceT sIface = {
|
|||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT,
|
||||
.defaultEvent = "Click"
|
||||
.defaultEvent = "Change"
|
||||
};
|
||||
|
||||
void wgtRegister(void) {
|
||||
|
|
|
|||
|
|
@ -173,14 +173,18 @@ static const WidgetClassT sClassRadio = {
|
|||
}
|
||||
};
|
||||
|
||||
// The `create` function MUST be the first field of this struct:
|
||||
// createWidgetByIface dereferences the api pointer as a function
|
||||
// pointer to call the widget's create function, so whatever field
|
||||
// is first is what gets invoked when BASIC instantiates the widget.
|
||||
static const struct {
|
||||
WidgetT *(*group)(WidgetT *parent);
|
||||
WidgetT *(*create)(WidgetT *parent, const char *text);
|
||||
WidgetT *(*group)(WidgetT *parent);
|
||||
void (*groupSetSelected)(WidgetT *group, int32_t index);
|
||||
int32_t (*getIndex)(const WidgetT *w);
|
||||
} sApi = {
|
||||
.group = wgtRadioGroup,
|
||||
.create = wgtRadio,
|
||||
.group = wgtRadioGroup,
|
||||
.groupSetSelected = wgtRadioGroupSetSelected,
|
||||
.getIndex = wgtRadioGetIndex
|
||||
};
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ typedef struct {
|
|||
} TabControlDataT;
|
||||
|
||||
typedef struct {
|
||||
const char *title;
|
||||
char *title;
|
||||
} TabPageDataT;
|
||||
|
||||
#define TAB_ARROW_W 16
|
||||
|
|
@ -99,6 +99,8 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod);
|
|||
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
||||
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||
void widgetTabPageAccelActivate(WidgetT *w, WidgetT *root);
|
||||
void widgetTabPageDestroy(WidgetT *w);
|
||||
void widgetTabPageSetText(WidgetT *w, const char *text);
|
||||
|
||||
|
||||
// tabClosePopup -- close any open dropdown/combobox popup
|
||||
|
|
@ -225,15 +227,37 @@ WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
|
|||
|
||||
if (w) {
|
||||
TabPageDataT *d = calloc(1, sizeof(TabPageDataT));
|
||||
d->title = title;
|
||||
d->title = strdup(title ? title : "");
|
||||
w->data = d;
|
||||
w->accelKey = accelParse(title);
|
||||
w->accelKey = accelParse(d->title);
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
void widgetTabPageDestroy(WidgetT *w) {
|
||||
TabPageDataT *d = (TabPageDataT *)w->data;
|
||||
|
||||
if (d) {
|
||||
free(d->title);
|
||||
free(d);
|
||||
w->data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void widgetTabPageSetText(WidgetT *w, const char *text) {
|
||||
TabPageDataT *d = (TabPageDataT *)w->data;
|
||||
|
||||
if (d) {
|
||||
free(d->title);
|
||||
d->title = strdup(text ? text : "");
|
||||
w->accelKey = accelParse(d->title);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Min size: tab header height + the maximum min size across ALL pages
|
||||
// (not just the active one). This ensures the tab control reserves
|
||||
// enough space for the largest page, preventing resize flicker when
|
||||
|
|
@ -648,6 +672,8 @@ static const WidgetClassT sClassTabPage = {
|
|||
[WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetTabPageAccelActivate,
|
||||
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox,
|
||||
[WGT_METHOD_LAYOUT] = (void *)widgetLayoutBox,
|
||||
[WGT_METHOD_SET_TEXT] = (void *)widgetTabPageSetText,
|
||||
[WGT_METHOD_DESTROY] = (void *)widgetTabPageDestroy,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -693,9 +719,33 @@ static const WgtIfaceT sIface = {
|
|||
.defaultEvent = "Click"
|
||||
};
|
||||
|
||||
// TabPage: authored in .frm as Begin TabPage name ... End inside a
|
||||
// TabStrip. Caption sets the title string. The page is a container
|
||||
// so children nest inside it (one page per Begin/End block).
|
||||
static const struct {
|
||||
WidgetT *(*create)(WidgetT *parent, const char *title);
|
||||
} sTabPageApi = {
|
||||
.create = wgtTabPage
|
||||
};
|
||||
|
||||
static const WgtIfaceT sTabPageIface = {
|
||||
.basName = "TabPage",
|
||||
.props = NULL,
|
||||
.propCount = 0,
|
||||
.methods = NULL,
|
||||
.methodCount = 0,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT_TEXT,
|
||||
.isContainer = true,
|
||||
.defaultEvent = "Click"
|
||||
};
|
||||
|
||||
void wgtRegister(void) {
|
||||
sTabControlTypeId = wgtRegisterClass(&sClassTabControl);
|
||||
sTabPageTypeId = wgtRegisterClass(&sClassTabPage);
|
||||
wgtRegisterApi("tabcontrol", &sApi);
|
||||
wgtRegisterIface("tabcontrol", &sIface);
|
||||
wgtRegisterApi("tabpage", &sTabPageApi);
|
||||
wgtRegisterIface("tabpage", &sTabPageIface);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,12 +181,6 @@ void wgtTimerStop(WidgetT *w) {
|
|||
|
||||
|
||||
void wgtUpdateTimers(void) {
|
||||
static int32_t sUpdateCount = 0;
|
||||
sUpdateCount++;
|
||||
if (sUpdateCount <= 3 || sUpdateCount % 100 == 0) {
|
||||
dvxLog("[T] wgtUpdateTimers call #%d activeCount=%d",
|
||||
(int)sUpdateCount, (int)arrlen(sActiveTimers));
|
||||
}
|
||||
clock_t now = clock();
|
||||
|
||||
// Iterate backwards so arrdel doesn't skip entries
|
||||
|
|
@ -203,9 +197,6 @@ void wgtUpdateTimers(void) {
|
|||
clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000;
|
||||
|
||||
if (elapsed >= interval) {
|
||||
dvxLog("[T] timer tick: w=%p onChange=%p intervalMs=%d elapsed=%ld interval=%ld",
|
||||
(void *)w, (void *)w->onChange, (int)d->intervalMs,
|
||||
(long)elapsed, (long)interval);
|
||||
d->lastFire = now;
|
||||
|
||||
if (w->onChange) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue