Event handling now more closely matches Visual Basic.

This commit is contained in:
Scott Duensing 2026-10-21 15:59:02 -05:00
parent 3c7b93bf7a
commit 5b7fdbbde6
65 changed files with 1008 additions and 194 deletions

View file

@ -1609,7 +1609,7 @@ img { max-width: 100%; }
<li>Immediate Window -- an interactive REPL for evaluating expressions and modifying variables at runtime.</li> <li>Immediate Window -- an interactive REPL for evaluating expressions and modifying variables at runtime.</li>
<li>Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging.</li> <li>Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging.</li>
</ul> </ul>
<p>The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so the GUI remains responsive. Call DoEvents in long-running loops to keep the event loop flowing.</p> <p>The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so DVX remains responsive. The program's own events wait until the code that is running ends or calls DoEvents, as in Visual Basic, so a long-running loop that should answer its own buttons calls DoEvents now and then.</p>
<p><a href="#ide.menu.file">File Menu</a></p> <p><a href="#ide.menu.file">File Menu</a></p>
<p><a href="#ide.menu.edit">Edit Menu</a></p> <p><a href="#ide.menu.edit">Edit Menu</a></p>
<p><a href="#ide.menu.run">Run Menu</a></p> <p><a href="#ide.menu.run">Run Menu</a></p>
@ -4258,7 +4258,7 @@ END IF</code></pre>
UNLOAD FormName UNLOAD FormName
UNLOAD Me UNLOAD Me
UNLOAD formVariable</code></pre> UNLOAD formVariable</code></pre>
<p>LOAD creates the form and its controls in memory. It fires the form's Load event (Form1_Load for a form named Form1) when the form is first loaded. UNLOAD destroys the form, firing its QueryUnload event (which can cancel the close) and then its Unload event. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. UNLOAD also takes a Form variable, or anything else that gives a form (for example a form made with CreateForm and kept in an object's property), and unloads that form.</p> <p>LOAD creates the form and its controls in memory. It fires the form's Load event (Form1_Load for a form named Form1) when the form is first loaded. UNLOAD destroys the form, firing its QueryUnload event (which can cancel the close) and then its Unload event. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. UNLOAD also takes a Form variable, or anything else that gives a form (for example a form made with CreateForm and kept in an object's property), and unloads that form. Once a form is unloaded, LOAD or any use of its name (a property, a control, Show) makes a new copy of it with its Load event, as in Visual Basic, even while code that ran the UNLOAD is still running; a second UNLOAD of the same form does nothing.</p>
<h2>Showing and Hiding Forms</h2> <h2>Showing and Hiding Forms</h2>
<pre><code>FormName.Show [mode] <pre><code>FormName.Show [mode]
FormName.Hide FormName.Hide
@ -4359,7 +4359,7 @@ Sub OnCloseClick
Unload Me Unload Me
End Sub</code></pre> End Sub</code></pre>
<h2>DoEvents</h2> <h2>DoEvents</h2>
<p>Yields control to DVX so the GUI can process pending events. Call this in long-running loops to keep the UI responsive (otherwise the window will appear frozen).</p> <p>Lets your program's waiting events (a click, a key, a Timer) run now. While your code runs, DVX and the other programs keep going (they get their turn many times a second), but your program's own events wait until the code that is running ends or calls DoEvents, as in Visual Basic. A long loop that should answer a Cancel button, or let a Timer run, calls DoEvents now and then; after it, check whatever an event may have changed (a Cancel flag, a form that was closed). MsgBox, InputBox, a form shown with vbModal, SLEEP, and the statements that wait for sound or a frame run your program's events too, as do the long calls described as working &quot;as with DoEvents&quot; (FileCopy, DIR$, long SQL statements, and others).</p>
<pre><code>DOEVENTS</code></pre> <pre><code>DOEVENTS</code></pre>
<pre><code>For i = 1 To 10000 <pre><code>For i = 1 To 10000
' process data ' process data

View file

@ -3119,7 +3119,11 @@ prefsClose(h);</code></pre>
| |
+-- Every other library loads when something first needs one of +-- Every other library loads when something first needs one of
| its names: a module being opened (platformModuleOpen), or a | its names: a module being opened (platformModuleOpen), or a
| dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded) | dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded).
| platformLoadedSymbol looks a name up without loading; what
| binds to a library once it is loaded hears of each one
| through the shell's hook (platformSetLibraryLoadHook: the
| cryptography library's random numbers are seeded then)
| |
+-- dvxInit() -- video mode, input, font, colors, cursors +-- dvxInit() -- video mode, input, font, colors, cursors
| |
@ -7888,7 +7892,8 @@ void basVmSetStepLimit(BasVmT *vm, int32_t limit);</code></pre>
<p>The host provides these callbacks for PRINT, INPUT, and DoEvents statements.</p> <p>The host provides these callbacks for PRINT, INPUT, and DoEvents statements.</p>
<pre><code>void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx); <pre><code>void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx); void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);</code></pre> void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
bool basVmAtSliceEnd(const BasVmT *vm);</code></pre>
<h3>Callback Types</h3> <h3>Callback Types</h3>
<pre><code>typedef void (*BasPrintFnT)(void *ctx, const char *text, bool newline); <pre><code>typedef void (*BasPrintFnT)(void *ctx, const char *text, bool newline);
typedef bool (*BasInputFnT)(void *ctx, const char *prompt, typedef bool (*BasInputFnT)(void *ctx, const char *prompt,
@ -7898,7 +7903,7 @@ typedef bool (*BasDoEventsFnT)(void *ctx);</code></pre>
---- ----------- ---- -----------
BasPrintFnT Called for PRINT output. text is null-terminated. newline indicates line advance. BasPrintFnT Called for PRINT output. text is null-terminated. newline indicates line advance.
BasInputFnT Called for INPUT. Fill buf (up to bufSize-1 chars). Return true on success, false on cancel. BasInputFnT Called for INPUT. Fill buf (up to bufSize-1 chars). Return true on success, false on cancel.
BasDoEventsFnT Called for DoEvents. Process pending events and return. Return false to stop the program.</pre> BasDoEventsFnT Called for DoEvents and the waits that act like it (SLEEP, the sound and FRAMEWAIT waits, FileCopy, DIR$): run the program's pending events and return. Also called at each slice end of a running handler, when basVmAtSliceEnd(vm) is true: then give DVX and the other programs their turn but run none of the program's own events, which wait for DoEvents or the handler's end as in Visual Basic (basFormRtUpdate does this by itself). Return false to stop the program.</pre>
<h2>UI Callbacks</h2> <h2>UI Callbacks</h2>
<p>For form and control integration. The VM resolves all UI operations through these callbacks, keeping it independent of any specific GUI toolkit.</p> <p>For form and control integration. The VM resolves all UI operations through these callbacks, keeping it independent of any specific GUI toolkit.</p>
<pre><code>void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);</code></pre> <pre><code>void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);</code></pre>
@ -10082,7 +10087,7 @@ End Sub</code></pre>
mathx.dhs Developer help, added to the system help mathx.dhs Developer help, added to the system help
Makefile KIND = library</pre> Makefile KIND = library</pre>
<h2>How the Library Works</h2> <h2>How the Library Works</h2>
<p>DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export.</p> <p>DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Code that uses a library only once something else has loaded it looks its names up with platformLoadedSymbol (dvxPlat.h), which never loads one. Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export.</p>
<p>BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.</p> <p>BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.</p>
<p>A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length, or a length never set give an empty string). The template's tplNameXor shows both:</p> <p>A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length, or a length never set give an empty string). The template's tplNameXor shows both:</p>
<pre><code> // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING <pre><code> // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING

BIN
screenshots/86box/cpanel-cp-types.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/f1-h-dvxbas.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/hang-ended.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/hang-taskmgr.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/instadd-add-sw.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/instadd-su-what.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/lfn-fm-old.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/menus-m-design.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/sdk-sdktbx.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/shellcmd-sc-back.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/86box/sql-sqlsmk.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/ucide-ucide-ds.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/86box/ucide-ucide-tb.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/dosbox/f1-h-dvxbas.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/dosbox/player-p-wav.png (Stored with Git LFS)

Binary file not shown.

BIN
screenshots/dosbox/sql-sqlsmk.png (Stored with Git LFS)

Binary file not shown.

View file

@ -342,6 +342,7 @@ The host provides these callbacks for PRINT, INPUT, and DoEvents statements.
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx); void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx); void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx); void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
bool basVmAtSliceEnd(const BasVmT *vm);
.endcode .endcode
.h3 Callback Types .h3 Callback Types
@ -358,7 +359,7 @@ typedef bool (*BasDoEventsFnT)(void *ctx);
---- ----------- ---- -----------
BasPrintFnT Called for PRINT output. text is null-terminated. newline indicates line advance. BasPrintFnT Called for PRINT output. text is null-terminated. newline indicates line advance.
BasInputFnT Called for INPUT. Fill buf (up to bufSize-1 chars). Return true on success, false on cancel. BasInputFnT Called for INPUT. Fill buf (up to bufSize-1 chars). Return true on success, false on cancel.
BasDoEventsFnT Called for DoEvents. Process pending events and return. Return false to stop the program. BasDoEventsFnT Called for DoEvents and the waits that act like it (SLEEP, the sound and FRAMEWAIT waits, FileCopy, DIR$): run the program's pending events and return. Also called at each slice end of a running handler, when basVmAtSliceEnd(vm) is true: then give DVX and the other programs their turn but run none of the program's own events, which wait for DoEvents or the handler's end as in Visual Basic (basFormRtUpdate does this by itself). Return false to stop the program.
.endtable .endtable
.h2 UI Callbacks .h2 UI Callbacks

View file

@ -393,7 +393,8 @@ typedef struct {
static CommSecLinkApiT sCommApi; static CommSecLinkApiT sCommApi;
// The printing library's job functions (printerBind) // The printing library's job functions (printerResolve: looked up the
// first time a program prints)
typedef struct { typedef struct {
__typeof__(printUiOpenPort) *open; __typeof__(printUiOpenPort) *open;
__typeof__(printJobWrite) *write; __typeof__(printJobWrite) *write;
@ -405,7 +406,8 @@ static PrintApiT sPrintApi;
// The music, joystick, game, and shell libraries' functions behind BEEP, // The music, joystick, game, and shell libraries' functions behind BEEP,
// SOUND, PLAY, STICK, STRIG, FRAMEWAIT, and SHELL (deviceBind; a group // SOUND, PLAY, STICK, STRIG, FRAMEWAIT, and SHELL (deviceBind; a group
// stays NULL when its library is not loaded) // stays NULL when its library is not installed). The game library's are
// looked up at the first FRAMEWAIT (deviceFrameStart).
typedef struct { typedef struct {
__typeof__(musicBasBeep) *beep; __typeof__(musicBasBeep) *beep;
__typeof__(musicBasBusy) *busy; __typeof__(musicBasBusy) *busy;
@ -423,6 +425,29 @@ typedef struct {
static DeviceApiT sDeviceApi; static DeviceApiT sDeviceApi;
// The libraries whose BASIC functions need the GUI context: each gets it
// (its setContext) for a program at the program's start when the library
// is loaded, else at the program's first call into it (basExternResolve,
// printerResolve), so a program that never uses one does not load it.
// For printing it also marks the program as one that has made no printer
// choice yet. rt->ctxLibsBound has a bit for each.
typedef void (*CtxLibSetFnT)(AppContextT *ctx);
typedef struct {
const char *lib; // its DECLARE LIBRARY name
const char *setContext;
} CtxLibT;
#define CTX_LIB_GAME 0
#define CTX_LIB_PRINT 1
static const CtxLibT sCtxLibs[] = {
[CTX_LIB_GAME] = { "game", DVX_SYM("gameBasSetContext") },
[CTX_LIB_PRINT] = { "print", DVX_SYM("printBasSetContext") },
};
#define CTX_LIB_COUNT ((int32_t)(sizeof(sCtxLibs) / sizeof(sCtxLibs[0])))
static bool sCommApiResolved = false; static bool sCommApiResolved = false;
#define COMM_BITS_PER_BYTE 8 #define COMM_BITS_PER_BYTE 8
@ -956,6 +981,7 @@ static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len);
static BasValueT contentBoxProp(BasFormRtT *rt, BasFormT *form, const char *propName, const BasValueT *value); static BasValueT contentBoxProp(BasFormRtT *rt, BasFormT *form, const char *propName, const BasValueT *value);
static const char *ctrlDisplayName(const BasControlT *ctrl); static const char *ctrlDisplayName(const BasControlT *ctrl);
static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt); static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt);
static void ctxLibBind(BasFormRtT *rt, int32_t idx);
static BasFormRtT *curRt(void); static BasFormRtT *curRt(void);
static void curRtForget(const BasFormRtT *rt); static void curRtForget(const BasFormRtT *rt);
static BasFormRtT *curRtSet(BasFormRtT *rt); static BasFormRtT *curRtSet(BasFormRtT *rt);
@ -965,7 +991,7 @@ static void detachFormForUnload(BasFormRtT *rt, BasFormT *form);
static void deviceBeep(void *ctx); static void deviceBeep(void *ctx);
static void deviceBind(BasFormRtT *rt); static void deviceBind(BasFormRtT *rt);
static bool deviceFrameBusy(void *ctx); static bool deviceFrameBusy(void *ctx);
static void deviceFrameStart(void *ctx, int32_t hz); static bool deviceFrameStart(void *ctx, int32_t hz);
static bool devicePlay(void *ctx, const char *macro); static bool devicePlay(void *ctx, const char *macro);
static int32_t devicePlayCount(void *ctx); static int32_t devicePlayCount(void *ctx);
static bool deviceShellBusy(void *ctx); static bool deviceShellBusy(void *ctx);
@ -1054,7 +1080,6 @@ static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level);
static void frmLoadOnMenuEnd(void *userData); static void frmLoadOnMenuEnd(void *userData);
static void frmLoadOnMenuProp(void *userData, const char *key, const char *value); static void frmLoadOnMenuProp(void *userData, const char *key, const char *value);
static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, const char *menuName); static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, const char *menuName);
static void gameBind(BasFormRtT *rt);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName); static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
@ -1116,6 +1141,7 @@ static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu);
static void printerBind(BasFormRtT *rt); static void printerBind(BasFormRtT *rt);
static bool printerClose(void *ctx, void *job); static bool printerClose(void *ctx, void *job);
static void *printerOpen(void *ctx, int32_t port); static void *printerOpen(void *ctx, int32_t port);
static bool printerResolve(BasFormRtT *rt);
static bool printerWrite(void *ctx, void *job, const char *data, int32_t len); static bool printerWrite(void *ctx, void *job, const char *data, int32_t len);
int32_t PrintForm(const char *formName); int32_t PrintForm(const char *formName);
int32_t PrintFormDialog(const char *formName); int32_t PrintFormDialog(const char *formName);
@ -1141,6 +1167,7 @@ static void rtEventEnter(BasFormRtT *rt);
static void rtEventLeave(BasFormRtT *rt); static void rtEventLeave(BasFormRtT *rt);
static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name); static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name);
static bool rtPump(BasFormRtT *rt); static bool rtPump(BasFormRtT *rt);
static bool rtSliceTurn(BasFormRtT *rt);
static BasVmResultE runSliced(BasFormRtT *rt); static BasVmResultE runSliced(BasFormRtT *rt);
static void runtimeErrorEnd(BasFormRtT *rt, const char *summary, const char *details); static void runtimeErrorEnd(BasFormRtT *rt, const char *summary, const char *details);
static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc); static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
@ -1465,14 +1492,24 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
} }
// dlsym(NULL) loads the library that has the function when it is not
// loaded yet; one of sCtxLibs then gets the program's GUI context.
void *basExternResolve(void *ctx, const char *libName, const char *funcName) { void *basExternResolve(void *ctx, const char *libName, const char *funcName) {
(void)ctx;
(void)libName;
char mangledName[BAS_MANGLED_NAME_LEN]; char mangledName[BAS_MANGLED_NAME_LEN];
snprintf(mangledName, sizeof(mangledName), DVX_SYM_PREFIX "%s", funcName); BasFormRtT *rt = curRt();
void *fn;
return dlsym(NULL, mangledName); (void)ctx;
snprintf(mangledName, sizeof(mangledName), DVX_SYM_PREFIX "%s", funcName);
fn = dlsym(NULL, mangledName);
for (int32_t i = 0; fn && rt && libName && i < CTX_LIB_COUNT; i++) {
if (strcasecmp(libName, sCtxLibs[i].lib) == 0) {
ctxLibBind(rt, i);
}
}
return fn;
} }
@ -1570,9 +1607,12 @@ void basFormRtBindVm(BasFormRtT *rt) {
basVmSetUiCallbacks(rt->vm, &ui); basVmSetUiCallbacks(rt->vm, &ui);
printerBind(rt); printerBind(rt);
gameBind(rt);
deviceBind(rt); deviceBind(rt);
basFormRtScreenBind(rt); basFormRtScreenBind(rt);
for (int32_t i = 0; i < CTX_LIB_COUNT; i++) {
ctxLibBind(rt, i);
}
} }
@ -2598,11 +2638,14 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
} }
// A form whose Unload is committed but deferred (a handler is still // A form whose Unload is committed but deferred (a handler is still
// on the stack) keeps answering to its name until the frees flush: // on the stack, or events wait) answers to its name for another
// 'Unload X' compiles to a Load-then-Unload pair, and re-parsing // Unload only: 'Unload X' compiles to a Load-then-Unload pair, and
// the cached .frm here would resurrect the form and unload it a // re-parsing the cached .frm here would resurrect the form and unload
// second time. Unload on the returned form is a no-op (unloading). // it a second time (Unload on the returned form is a no-op). Load or
for (int32_t i = 0; !sNewFormCopy && i < (int32_t)arrlen(rt->pendingUnload); i++) { // a reference gets a new form, as in VB, not the one going away.
bool forUnload = rt->vm && basVmFormLookupForUnload(rt->vm);
for (int32_t i = 0; forUnload && !sNewFormCopy && i < (int32_t)arrlen(rt->pendingUnload); i++) {
if (strcasecmp(rt->pendingUnload[i]->name, formName) == 0) { if (strcasecmp(rt->pendingUnload[i]->name, formName) == 0) {
return rt->pendingUnload[i]; return rt->pendingUnload[i];
} }
@ -3754,10 +3797,16 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
// The waiting events of every runtime of this task run (rt's, and those // The waiting events of every runtime of this task run (rt's, and those
// of programs it runs), then a frame. // of programs it runs), then a frame. At a slice end of the program's
// code (the host's DoEvents callback, not DoEvents itself) only DVX and
// the other programs get their turn (rtSliceTurn).
bool basFormRtUpdate(BasFormRtT *rt) { bool basFormRtUpdate(BasFormRtT *rt) {
uint32_t task = basFormRtTaskId(); uint32_t task = basFormRtTaskId();
if (rt->vm && basVmAtSliceEnd(rt->vm)) {
return rtSliceTurn(rt);
}
for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) { for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) {
if (sRuntimes[i]->ownTask == task) { if (sRuntimes[i]->ownTask == task) {
queueDrain(sRuntimes[i]); queueDrain(sRuntimes[i]);
@ -5044,6 +5093,24 @@ static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt) {
} }
// sCtxLibs[idx] gets the program's GUI context once, if it is loaded
// (platformLoadedSymbol never loads it).
static void ctxLibBind(BasFormRtT *rt, int32_t idx) {
CtxLibSetFnT setContext;
if (rt->ctxLibsBound & (1u << idx)) {
return;
}
setContext = (CtxLibSetFnT)platformLoadedSymbol(sCtxLibs[idx].setContext);
if (setContext) {
setContext(rt->ctx);
rt->ctxLibsBound |= 1u << idx;
}
}
// The runtime current on this task, or NULL. // The runtime current on this task, or NULL.
static BasFormRtT *curRt(void) { static BasFormRtT *curRt(void) {
uint32_t task = basFormRtTaskId(); uint32_t task = basFormRtTaskId();
@ -5185,8 +5252,6 @@ static void deviceBind(BasFormRtT *rt) {
sDeviceApi.sound = (__typeof__(sDeviceApi.sound))dlsym(NULL, DVX_SYM("musicBasSound")); sDeviceApi.sound = (__typeof__(sDeviceApi.sound))dlsym(NULL, DVX_SYM("musicBasSound"));
sDeviceApi.stick = (__typeof__(sDeviceApi.stick))dlsym(NULL, DVX_SYM("joyBasStick")); sDeviceApi.stick = (__typeof__(sDeviceApi.stick))dlsym(NULL, DVX_SYM("joyBasStick"));
sDeviceApi.strig = (__typeof__(sDeviceApi.strig))dlsym(NULL, DVX_SYM("joyBasStrig")); sDeviceApi.strig = (__typeof__(sDeviceApi.strig))dlsym(NULL, DVX_SYM("joyBasStrig"));
sDeviceApi.frameInit = (__typeof__(sDeviceApi.frameInit))dlsym(NULL, DVX_SYM("gameFrameInit"));
sDeviceApi.frameNext = (__typeof__(sDeviceApi.frameNext))dlsym(NULL, DVX_SYM("gameFrameNext"));
sDeviceApi.dosFor = (__typeof__(sDeviceApi.dosFor))dlsym(NULL, DVX_SYM("shellRunDosFor")); sDeviceApi.dosFor = (__typeof__(sDeviceApi.dosFor))dlsym(NULL, DVX_SYM("shellRunDosFor"));
sDeviceApi.dosDone = (__typeof__(sDeviceApi.dosDone))dlsym(NULL, DVX_SYM("shellRunDosDone")); sDeviceApi.dosDone = (__typeof__(sDeviceApi.dosDone))dlsym(NULL, DVX_SYM("shellRunDosDone"));
sDeviceApi.dosSplit = (__typeof__(sDeviceApi.dosSplit))dlsym(NULL, DVX_SYM("shellSplitCommand")); sDeviceApi.dosSplit = (__typeof__(sDeviceApi.dosSplit))dlsym(NULL, DVX_SYM("shellSplitCommand"));
@ -5205,10 +5270,8 @@ static void deviceBind(BasFormRtT *rt) {
cb.strig = deviceStrig; cb.strig = deviceStrig;
} }
if (sDeviceApi.frameInit && sDeviceApi.frameNext) {
cb.frameStart = deviceFrameStart; cb.frameStart = deviceFrameStart;
cb.frameBusy = deviceFrameBusy; cb.frameBusy = deviceFrameBusy;
}
if (sDeviceApi.dosFor && sDeviceApi.dosDone && sDeviceApi.dosSplit) { if (sDeviceApi.dosFor && sDeviceApi.dosDone && sDeviceApi.dosSplit) {
cb.shellStart = deviceShellStart; cb.shellStart = deviceShellStart;
@ -5229,16 +5292,28 @@ static bool deviceFrameBusy(void *ctx) {
// The frame clock starts over when the program asks for another rate. // The frame clock starts over when the program asks for another rate.
static void deviceFrameStart(void *ctx, int32_t hz) { // The game library's pacer is looked up (the library loaded) at the
// first FRAMEWAIT; false without it (Device unavailable).
static bool deviceFrameStart(void *ctx, int32_t hz) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
uint32_t missed = 0; uint32_t missed = 0;
if (!sDeviceApi.frameInit || !sDeviceApi.frameNext) {
sDeviceApi.frameInit = (__typeof__(sDeviceApi.frameInit))dlsym(NULL, DVX_SYM("gameFrameInit"));
sDeviceApi.frameNext = (__typeof__(sDeviceApi.frameNext))dlsym(NULL, DVX_SYM("gameFrameNext"));
}
if (!sDeviceApi.frameInit || !sDeviceApi.frameNext) {
return false;
}
if (rt->frameHz != hz) { if (rt->frameHz != hz) {
sDeviceApi.frameInit(&rt->framePacer, (uint32_t)hz); sDeviceApi.frameInit(&rt->framePacer, (uint32_t)hz);
rt->frameHz = hz; rt->frameHz = hz;
} }
rt->frameDueUs = sDeviceApi.frameNext(&rt->framePacer, &missed); rt->frameDueUs = sDeviceApi.frameNext(&rt->framePacer, &missed);
return true;
} }
@ -6065,9 +6140,9 @@ static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *even
return; return;
} }
// Re-entrancy guard: reject only same-event re-entry (runSubLoop // Re-entrancy guard: reject only same-event re-entry (DoEvents or a
// pumps events during long handlers; without this, a lingering // wait in a handler runs the program's events; without this, a
// mouse-up can re-deliver Click on top of itself). But allow // lingering mouse-up can re-deliver Click on top of itself). But allow
// different events on the same control (e.g. LostFocus while Click // different events on the same control (e.g. LostFocus while Click
// is still running) -- those aren't re-entrant, they're interleaved. // is still running) -- those aren't re-entrant, they're interleaved.
if (ctrl->eventFiring) { if (ctrl->eventFiring) {
@ -7010,17 +7085,6 @@ static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, cons
} }
// The game library's BASIC functions (game.bas) get the GUI context, when
// the library is loaded.
static void gameBind(BasFormRtT *rt) {
__typeof__(gameBasSetContext) *setContext = (__typeof__(setContext))dlsym(NULL, DVX_SYM("gameBasSetContext"));
if (setContext) {
setContext(rt->ctx);
}
}
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) { static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) {
const BasPropDescT *pd = basFormRtFindCommonProp(propName); const BasPropDescT *pd = basFormRtFindCommonProp(propName);
WidgetT *w = ctrl->widget; WidgetT *w = ctrl->widget;
@ -8458,25 +8522,10 @@ static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu) {
} }
// The printing library's functions, looked up by name so the runtime // LPRINT and OPEN "LPT1:" reach the printing library through these; its
// works without it (LPRINT and OPEN "LPT1:" then raise "Device // functions are looked up when the program first prints (printerResolve),
// unavailable"). It also gets the GUI context for print.bas. // so a program that never prints does not load it.
static void printerBind(BasFormRtT *rt) { static void printerBind(BasFormRtT *rt) {
__typeof__(printBasSetContext) *setContext = (__typeof__(setContext))dlsym(NULL, DVX_SYM("printBasSetContext"));
sPrintApi.open = (__typeof__(sPrintApi.open))dlsym(NULL, DVX_SYM("printUiOpenPort"));
sPrintApi.write = (__typeof__(sPrintApi.write))dlsym(NULL, DVX_SYM("printJobWrite"));
sPrintApi.close = (__typeof__(sPrintApi.close))dlsym(NULL, DVX_SYM("printJobClose"));
sPrintApi.window = (__typeof__(sPrintApi.window))dlsym(NULL, DVX_SYM("printUiWindow"));
if (setContext) {
setContext(rt->ctx);
}
if (!sPrintApi.open || !sPrintApi.write || !sPrintApi.close) {
return;
}
BasPrinterCallbacksT cb; BasPrinterCallbacksT cb;
cb.open = printerOpen; cb.open = printerOpen;
@ -8496,10 +8545,35 @@ static bool printerClose(void *ctx, void *job) {
static void *printerOpen(void *ctx, int32_t port) { static void *printerOpen(void *ctx, int32_t port) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
if (!printerResolve(rt)) {
return NULL;
}
return sPrintApi.open(rt->ctx, port == BAS_PRINTER_DEFAULT ? PRINT_PORT_DEFAULT : port); return sPrintApi.open(rt->ctx, port == BAS_PRINTER_DEFAULT ? PRINT_PORT_DEFAULT : port);
} }
// The printing library's functions, looked up by name (the library loaded)
// the first time a program prints, so the runtime works without it (LPRINT
// and OPEN "LPT1:" then raise "Device unavailable"). The program's
// print.bas gets the GUI context then if it has not yet.
static bool printerResolve(BasFormRtT *rt) {
if (!sPrintApi.open) {
sPrintApi.open = (__typeof__(sPrintApi.open))dlsym(NULL, DVX_SYM("printUiOpenPort"));
sPrintApi.write = (__typeof__(sPrintApi.write))dlsym(NULL, DVX_SYM("printJobWrite"));
sPrintApi.close = (__typeof__(sPrintApi.close))dlsym(NULL, DVX_SYM("printJobClose"));
sPrintApi.window = (__typeof__(sPrintApi.window))dlsym(NULL, DVX_SYM("printUiWindow"));
}
if (!sPrintApi.open || !sPrintApi.write || !sPrintApi.close) {
return false;
}
ctxLibBind(rt, CTX_LIB_PRINT);
return true;
}
static bool printerWrite(void *ctx, void *job, const char *data, int32_t len) { static bool printerWrite(void *ctx, void *job, const char *data, int32_t len) {
(void)ctx; (void)ctx;
return sPrintApi.write((PrintJobT *)job, data, len); return sPrintApi.write((PrintJobT *)job, data, len);
@ -8530,7 +8604,7 @@ static int32_t printFormWindow(const char *formName, bool askFirst) {
} }
} }
if (!form || !form->window || !sPrintApi.window) { if (!form || !form->window || !printerResolve(rt) || !sPrintApi.window) {
return 0; return 0;
} }
@ -8597,19 +8671,33 @@ static void queueDrain(BasFormRtT *rt) {
// Keep an event for the program's own task, its arguments copied; a newer // Keep an event for the program's own task, its arguments copied; a newer
// state (sQueueLatest) replaces one of the same object still waiting. The // state (sQueueLatest) replaces the same object's newest waiting event when
// that is the same state, so each object's events keep their order. The
// first to wait takes the queue's eventDepth bracket. True (it will run). // first to wait takes the queue's eventDepth bracket. True (it will run).
static bool queueEvent(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasQueuedKindE kind) { static bool queueEvent(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasQueuedKindE kind) {
BasQueuedEventT *slot = NULL; BasQueuedEventT *slot = NULL;
for (int32_t i = 0; !slot && i < DVX_ARRAY_LEN(sQueueLatest); i++) { for (int32_t i = 0; !slot && i < DVX_ARRAY_LEN(sQueueLatest); i++) {
for (int32_t j = (int32_t)arrlen(rt->queued) - 1; j >= 0 && strcasecmp(eventName, sQueueLatest[i]) == 0; j--) { if (strcasecmp(eventName, sQueueLatest[i]) != 0) {
continue;
}
// The newest waiting event of the same object is replaced when it
// is this kind; anything else of that object waits in between (a
// drag's MouseMove goes after its MouseDown, not in place of the
// hover move before it)
for (int32_t j = (int32_t)arrlen(rt->queued) - 1; j >= 0; j--) {
BasQueuedEventT *ev = &rt->queued[j]; BasQueuedEventT *ev = &rt->queued[j];
if (ev->form == form && ev->obj == obj && strcasecmp(ev->eventName, eventName) == 0 && strcasecmp(ev->ctrlName, ctrlName) == 0) { if (ev->form != form || ev->obj != obj || strcasecmp(ev->ctrlName, ctrlName) != 0) {
slot = ev; continue;
break;
} }
if (strcasecmp(ev->eventName, eventName) == 0) {
slot = ev;
}
break;
} }
} }
@ -9126,6 +9214,21 @@ static bool rtPump(BasFormRtT *rt) {
} }
// A slice end of the program's code: DVX and the other programs get their
// turn, but none of the program's own events run (as in VB, they wait for
// DoEvents, a wait, or the code's end). On its own task that is a yield
// (the shell's frames go on); on another (an Unload the shell fired) a
// frame, in which the program's events only queue.
static bool rtSliceTurn(BasFormRtT *rt) {
if (queueForeign(rt, NULL)) {
return dvxUpdate(rt->ctx);
}
basFormRtYield();
return rt->ctx->running;
}
// Runs the VM in step slices, giving DVX a turn between them, until the // Runs the VM in step slices, giving DVX a turn between them, until the
// program halts or fails (an error is shown). Returns the last result. // program halts or fails (an error is shown). Returns the last result.
static BasVmResultE runSliced(BasFormRtT *rt) { static BasVmResultE runSliced(BasFormRtT *rt) {
@ -9143,7 +9246,7 @@ static BasVmResultE runSliced(BasFormRtT *rt) {
curRtSet(prevRt); curRtSet(prevRt);
if (result == BAS_VM_STEP_LIMIT) { if (result == BAS_VM_STEP_LIMIT) {
if (!basFormRtUpdate(rt)) { if (!rtSliceTurn(rt)) {
break; break;
} }
} else if (result != BAS_VM_HALTED && result != BAS_VM_OK) { } else if (result != BAS_VM_HALTED && result != BAS_VM_OK) {

View file

@ -455,10 +455,10 @@ typedef struct BasControlT {
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES]; BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
int32_t eventOverrideCount; int32_t eventOverrideCount;
// Re-entrancy guard: set while an event handler for this control // Re-entrancy guard: set while an event handler for this control
// is running. runSubLoop pumps events during long-running handlers // is running. DoEvents (or a wait that runs events) in a handler
// (painting, draw loops), and without this guard a lingering // runs the program's waiting events, and without this guard a
// mouse-up or repaint cycle can re-fire the same Click on top of // lingering mouse-up or repaint cycle can re-fire the same Click on
// itself -- an unbounded recursion. firingEventName records which // top of itself -- an unbounded recursion. firingEventName records which
// event is in flight so different-event delivery (e.g. LostFocus // event is in flight so different-event delivery (e.g. LostFocus
// while Click is still running) is allowed through. // while Click is still running) is allowed through.
bool eventFiring; bool eventFiring;
@ -679,6 +679,9 @@ typedef struct {
int32_t screenPointer; int32_t screenPointer;
// The Printer object's pages and document (NULL until it is used). // The Printer object's pages and document (NULL until it is used).
BasPrinterT *printer; BasPrinterT *printer;
// The libraries (a bit for each of formrt.c's sCtxLibs) that have
// this program's GUI context (ctxLibBind)
uint32_t ctxLibsBound;
// A program's handlers run on its own task (the one that made the // A program's handlers run on its own task (the one that made the
// runtime; BAS_RT_ANY_TASK: any). An event another task dispatched // runtime; BAS_RT_ANY_TASK: any). An event another task dispatched
// (the shell's frame takes the input), and a form's window closed // (the shell's frame takes the input), and a form's window closed

View file

@ -51,7 +51,7 @@ The DVX BASIC IDE is modeled after Visual Basic 3.0. It consists of several floa
.item Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging. .item Debug Windows -- Locals, Call Stack, Watch, and Breakpoints windows that appear automatically when debugging.
.endlist .endlist
The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so the GUI remains responsive. Call DoEvents in long-running loops to keep the event loop flowing. The IDE compiles BASIC source into bytecode and runs it in an integrated virtual machine. Programs execute in cooperative slices, yielding to DVX between slices so DVX remains responsive. The program's own events wait until the code that is running ends or calls DoEvents, as in Visual Basic, so a long-running loop that should answer its own buttons calls DoEvents now and then.
.link ide.menu.file File Menu .link ide.menu.file File Menu
.link ide.menu.edit Edit Menu .link ide.menu.edit Edit Menu

View file

@ -3226,7 +3226,7 @@ UNLOAD Me
UNLOAD formVariable UNLOAD formVariable
.endcode .endcode
LOAD creates the form and its controls in memory. It fires the form's Load event (Form1_Load for a form named Form1) when the form is first loaded. UNLOAD destroys the form, firing its QueryUnload event (which can cancel the close) and then its Unload event. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. UNLOAD also takes a Form variable, or anything else that gives a form (for example a form made with CreateForm and kept in an object's property), and unloads that form. LOAD creates the form and its controls in memory. It fires the form's Load event (Form1_Load for a form named Form1) when the form is first loaded. UNLOAD destroys the form, firing its QueryUnload event (which can cancel the close) and then its Unload event. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running. UNLOAD also takes a Form variable, or anything else that gives a form (for example a form made with CreateForm and kept in an object's property), and unloads that form. Once a form is unloaded, LOAD or any use of its name (a property, a control, Show) makes a new copy of it with its Load event, as in Visual Basic, even while code that ran the UNLOAD is still running; a second UNLOAD of the same form does nothing.
.h2 Showing and Hiding Forms .h2 Showing and Hiding Forms
@ -3414,7 +3414,7 @@ End Sub
.h2 DoEvents .h2 DoEvents
Yields control to DVX so the GUI can process pending events. Call this in long-running loops to keep the UI responsive (otherwise the window will appear frozen). Lets your program's waiting events (a click, a key, a Timer) run now. While your code runs, DVX and the other programs keep going (they get their turn many times a second), but your program's own events wait until the code that is running ends or calls DoEvents, as in Visual Basic. A long loop that should answer a Cancel button, or let a Timer run, calls DoEvents now and then; after it, check whatever an event may have changed (a Cancel flag, a form that was closed). MsgBox, InputBox, a form shown with vbModal, SLEEP, and the statements that wait for sound or a frame run your program's events too, as do the long calls described as working "as with DoEvents" (FileCopy, DIR$, long SQL statements, and others).
.code .code
DOEVENTS DOEVENTS

View file

@ -934,6 +934,11 @@ static BasValueT arrValue(BasArrayT *arr) {
} }
bool basVmAtSliceEnd(const BasVmT *vm) {
return vm->sliceTurn;
}
bool basVmCallMember(BasVmT *vm, BasObjectT *obj, const char *name, int32_t op, const BasValueT *args, int32_t argc, BasValueT *result) { bool basVmCallMember(BasVmT *vm, BasObjectT *obj, const char *name, int32_t op, const BasValueT *args, int32_t argc, BasValueT *result) {
if (result) { if (result) {
*result = basValEmpty(); *result = basValEmpty();
@ -1201,6 +1206,11 @@ void basVmForgetForm(BasVmT *vm, void *formRef, BasValueT *formVars, int32_t for
} }
bool basVmFormLookupForUnload(const BasVmT *vm) {
return vm->unloadLookup;
}
int32_t basVmGetCurrentLine(const BasVmT *vm) { int32_t basVmGetCurrentLine(const BasVmT *vm) {
return vm->currentLine; return vm->currentLine;
} }
@ -3521,7 +3531,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
if (vm->ui.loadForm) { if (vm->ui.loadForm) {
BasValueT sv = basValToString(nameVal); BasValueT sv = basValToString(nameVal);
vm->unloadLookup = operandFits(vm, (int32_t)sizeof(uint8_t)) && vm->module->code[vm->pc] == OP_UNLOAD_FORM;
formRef = vm->ui.loadForm(vm->ui.ctx, sv.strVal->data); formRef = vm->ui.loadForm(vm->ui.ctx, sv.strVal->data);
vm->unloadLookup = false;
basValRelease(&sv); basValRelease(&sv);
} }
@ -7163,7 +7176,10 @@ static BasVmResultE execDevice(BasVmT *vm, uint8_t op) {
return BAS_VM_ERROR; return BAS_VM_ERROR;
} }
d->frameStart(d->ctx, hz); if (!d->frameStart(d->ctx, hz)) {
runtimeError(vm, BAS_ERR_DEVICE_UNAVAILABLE, errorText(BAS_ERR_DEVICE_UNAVAILABLE));
return BAS_VM_ERROR;
}
if (!waitWhile(vm, d->frameBusy)) { if (!waitWhile(vm, d->frameBusy)) {
vm->running = false; vm->running = false;
@ -13992,8 +14008,8 @@ fastResume:
// Executes instructions until callDepth drops back to savedCallDepth // Executes instructions until callDepth drops back to savedCallDepth
// (meaning the subroutine returned). Periodically yields via the // (meaning the subroutine returned). Periodically yields via the
// doEvents callback to keep the GUI responsive during long-running // doEvents callback (a slice-end turn, basVmAtSliceEnd) so DVX keeps
// event handlers. // running during long event handlers.
static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) { static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) {
int32_t stepsSinceYield = 0; int32_t stepsSinceYield = 0;
int32_t slices = 0; // a run limit's count int32_t slices = 0; // a run limit's count
@ -14050,8 +14066,10 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
return subLoopFail(vm, savedPc, savedCallDepth, savedRunning); return subLoopFail(vm, savedPc, savedCallDepth, savedRunning);
} }
// Yield periodically to keep the GUI responsive; code with a run // Yield periodically so DVX and the other programs keep running
// limit that has run too long is stopped (ON ERROR cannot trap it) // (the host runs none of this program's events then: they wait for
// DoEvents or the handler's end, as in VB); code with a run limit
// that has run too long is stopped (ON ERROR cannot trap it)
if (stepsSinceYield >= BAS_VM_DEFAULT_STEP_SLICE) { if (stepsSinceYield >= BAS_VM_DEFAULT_STEP_SLICE) {
stepsSinceYield = 0; stepsSinceYield = 0;
@ -14059,7 +14077,13 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
return subLoopFail(vm, savedPc, savedCallDepth, savedRunning); return subLoopFail(vm, savedPc, savedCallDepth, savedRunning);
} }
if (!basVmDoEvents(vm)) { vm->sliceTurn = true;
bool goOn = basVmDoEvents(vm);
vm->sliceTurn = false;
if (!goOn) {
vm->running = false; vm->running = false;
break; break;
} }

View file

@ -126,9 +126,13 @@ typedef void (*BasPrintFnT)(void *ctx, const char *text, bool newline);
// success, false on cancel/error. // success, false on cancel/error.
typedef bool (*BasInputFnT)(void *ctx, const char *prompt, char *buf, int32_t bufSize); typedef bool (*BasInputFnT)(void *ctx, const char *prompt, char *buf, int32_t bufSize);
// DoEvents callback: called for DoEvents statement. // DoEvents callback: called for the DoEvents statement and the waits that
// The host should process pending events and return. Returns // act like it (SLEEP, the sound and FRAMEWAIT waits, FileCopy, DIR$), when
// true to continue execution, false to stop the program. // the host should run the program's waiting events, and at each slice end
// of a running handler (basVmAtSliceEnd then true), when it should give
// DVX and the other programs their turn but run none of the program's
// own events: as in VB, those wait for DoEvents or the code's end.
// Returns true to continue execution, false to stop the program.
typedef bool (*BasDoEventsFnT)(void *ctx); typedef bool (*BasDoEventsFnT)(void *ctx);
// Breakpoint callback: called when a breakpoint fires inside a // Breakpoint callback: called when a breakpoint fires inside a
@ -322,14 +326,15 @@ typedef struct {
// BEEP, SOUND and PLAY the VM keeps the host's events running while // BEEP, SOUND and PLAY the VM keeps the host's events running while
// soundBusy says the statement must still wait (foreground music); after // soundBusy says the statement must still wait (foreground music); after
// frameStart(hz) (the host's frame clock moves on to its next frame at hz // frameStart(hz) (the host's frame clock moves on to its next frame at hz
// frames a second) likewise while frameBusy says that frame has not // frames a second; false when the host has no frame clock) likewise while
// begun. SHELL hands its command line to shellStart (the host runs it // frameBusy says that frame has not begun. SHELL hands its command line
// with DVX stepped aside: false when it cannot), keeps the host's events // to shellStart (the host runs it with DVX stepped aside: false when it
// running while shellBusy, and returns shellResult (the exit code, -1 when // cannot), keeps the host's events running while shellBusy, and returns
// it did not run). Without beep / sound / play the statements do nothing // shellResult (the exit code, -1 when it did not run). Without beep /
// and PLAY(n) is 0, without stick / strig they read 0, FRAMEWAIT without // sound / play the statements do nothing and PLAY(n) is 0, without stick /
// frameStart raises "Device unavailable", and SHELL without shellStart // strig they read 0, FRAMEWAIT without frameStart (or when it says false)
// runs the command with system() (BASIC outside DVX: the runner, BASCOMP). // raises "Device unavailable", and SHELL without shellStart runs the
// command with system() (BASIC outside DVX: the runner, BASCOMP).
// QBasic's ranges: SOUND frequency (Hz) and duration (clock ticks, 18.2 // QBasic's ranges: SOUND frequency (Hz) and duration (clock ticks, 18.2
// a second), STICK(0..3), STRIG(0..7) // a second), STICK(0..3), STRIG(0..7)
@ -347,7 +352,7 @@ typedef struct {
bool (*soundBusy)(void *ctx); bool (*soundBusy)(void *ctx);
int32_t (*stick)(void *ctx, int32_t n); int32_t (*stick)(void *ctx, int32_t n);
int32_t (*strig)(void *ctx, int32_t n); int32_t (*strig)(void *ctx, int32_t n);
void (*frameStart)(void *ctx, int32_t hz); bool (*frameStart)(void *ctx, int32_t hz);
bool (*frameBusy)(void *ctx); bool (*frameBusy)(void *ctx);
bool (*shellStart)(void *ctx, const char *command); bool (*shellStart)(void *ctx, const char *command);
bool (*shellBusy)(void *ctx); bool (*shellBusy)(void *ctx);
@ -883,6 +888,14 @@ typedef struct {
struct BasScreenT *screen; struct BasScreenT *screen;
bool stopRequested; bool stopRequested;
// The DoEvents callback is running for a slice end of the program's
// code, not for DoEvents or a wait (basVmAtSliceEnd)
bool sliceTurn;
// OP_LOAD_FORM is asking the host for the form an Unload follows
// (basVmFormLookupForUnload)
bool unloadLookup;
// External library callbacks (set by host) // External library callbacks (set by host)
BasExternCallbacksT ext; BasExternCallbacksT ext;
@ -975,6 +988,15 @@ bool basVmSetCommand(BasVmT *vm, const char *args);
// App.User's folder is made when the program first asks for it. // App.User's folder is made when the program first asks for it.
void basVmSetAppPaths(BasVmT *vm, const char *path, const char *user, const char *data); void basVmSetAppPaths(BasVmT *vm, const char *path, const char *user, const char *data);
// True while the DoEvents callback runs for a slice end of the program's
// code rather than for DoEvents or a wait (see BasDoEventsFnT).
bool basVmAtSliceEnd(const BasVmT *vm);
// True while the host's loadForm runs for an Unload ('Unload X' compiles to
// OP_LOAD_FORM then OP_UNLOAD_FORM): a form whose Unload is already under
// way is the one to give then, where Load or a reference gets a new one.
bool basVmFormLookupForUnload(const BasVmT *vm);
// Give the host a turn: show what changed on the program screen, then // Give the host a turn: show what changed on the program screen, then
// the DoEvents callback. False when the program must stop (the host said // the DoEvents callback. False when the program must stop (the host said
// so, or basVmRequestStop). // so, or basVmRequestStop).

View file

@ -242,7 +242,7 @@ static BasModuleT *compileSource(const char *name, const char *source);
static void deviceBeepStub(void *ctx); static void deviceBeepStub(void *ctx);
static bool deviceDoEventsStub(void *ctx); static bool deviceDoEventsStub(void *ctx);
static bool deviceFrameBusyStub(void *ctx); static bool deviceFrameBusyStub(void *ctx);
static void deviceFrameStartStub(void *ctx, int32_t hz); static bool deviceFrameStartStub(void *ctx, int32_t hz);
static int32_t devicePlayCountStub(void *ctx); static int32_t devicePlayCountStub(void *ctx);
static bool devicePlayStub(void *ctx, const char *macro); static bool devicePlayStub(void *ctx, const char *macro);
static bool deviceSoundBusyStub(void *ctx); static bool deviceSoundBusyStub(void *ctx);
@ -372,6 +372,7 @@ static int32_t sDeviceSoundWaits = 0;
static int32_t sDeviceFrameWaits = 0; static int32_t sDeviceFrameWaits = 0;
static int32_t sDeviceEvents = 0; static int32_t sDeviceEvents = 0;
static int32_t sDeviceEventsLeft = 0; static int32_t sDeviceEventsLeft = 0;
static bool sDeviceNoClock = false; // frameStart says the host has no frame clock
static const BasDeviceCallbacksT sDeviceStubs = { static const BasDeviceCallbacksT sDeviceStubs = {
.beep = deviceBeepStub, .beep = deviceBeepStub,
@ -463,9 +464,10 @@ static bool deviceFrameBusyStub(void *ctx) {
} }
static void deviceFrameStartStub(void *ctx, int32_t hz) { static bool deviceFrameStartStub(void *ctx, int32_t hz) {
(void)ctx; (void)ctx;
snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "frame %ld;", (long)hz); snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "frame %ld;", (long)hz);
return !sDeviceNoClock;
} }
@ -1870,6 +1872,12 @@ static void testDeviceStatements(void) {
TEST_RUNTIME_ERROR("device-strig-range", "PRINT STRIG(-1)\n", "Illegal function call"); TEST_RUNTIME_ERROR("device-strig-range", "PRINT STRIG(-1)\n", "Illegal function call");
TEST_RUNTIME_ERROR("device-play-bad", "PLAY \"CXD\"\n", "PLAY string"); TEST_RUNTIME_ERROR("device-play-bad", "PLAY \"CXD\"\n", "PLAY string");
TEST_RUNTIME_ERROR("device-frame-zero", "FRAMEWAIT 0\n", "Illegal function call"); TEST_RUNTIME_ERROR("device-frame-zero", "FRAMEWAIT 0\n", "Illegal function call");
// A host whose frame clock is missing (the game library is not
// installed) says so at the FRAMEWAIT.
sDeviceNoClock = true;
TEST_RUNTIME_ERROR("device-frame-noclock", "FRAMEWAIT 60\n", "Device unavailable");
sDeviceNoClock = false;
TEST_EQ("device-play-trapped", "ON ERROR GOTO h\nPLAY \"X\"\nEND\nh:\nPRINT ERR\nRESUME NEXT\n", "5 \n"); TEST_EQ("device-play-trapped", "ON ERROR GOTO h\nPLAY \"X\"\nEND\nh:\nPRINT ERR\nRESUME NEXT\n", "5 \n");
// Closing the program while it waits stops it there. // Closing the program while it waits stops it there.

View file

@ -557,6 +557,7 @@ SUB RefreshInstalled
ShowRows lvInstalled, rows ShowRows lvInstalled, rows
lblStatus.Caption = CStr(count) & IIF(count = 1, " package installed", " packages installed") lblStatus.Caption = CStr(count) & IIF(count = 1, " package installed", " packages installed")
ShellLog "Software: " & lblStatus.Caption
END SUB END SUB

View file

@ -498,6 +498,7 @@ SUB ShowTour(BYVAL page AS INTEGER)
lblPage.Caption = "Page " + LTRIM$(STR$(page)) + " of " + LTRIM$(STR$(TOUR_PAGES)) lblPage.Caption = "Page " + LTRIM$(STR$(page)) + " of " + LTRIM$(STR$(TOUR_PAGES))
cmdPrev.Enabled = page > 1 cmdPrev.Enabled = page > 1
cmdNext.Enabled = page < TOUR_PAGES cmdNext.Enabled = page < TOUR_PAGES
ShellLog "Welcome: tour page " + LTRIM$(STR$(page))
END SUB END SUB

View file

@ -92,11 +92,22 @@ DVX_ABI_STAMP(dvxshell);
// Random-seed file carried across runs for the cryptography library's RNG // Random-seed file carried across runs for the cryptography library's RNG
#define SHELL_SEED_NAME "SECLINK.RND" // in the user folder #define SHELL_SEED_NAME "SECLINK.RND" // in the user folder
// The shell task's priority: high, so the UI stays responsive even when
// app tasks are CPU-hungry (11 credits a round against a NORMAL app's 6,
// roughly twice the turns of any single app).
#define SHELL_TASK_PRIORITY TS_PRIORITY_HIGH
// Exit DVX: how long an app may take to close a window it was asked to // Exit DVX: how long an app may take to close a window it was asked to
// close before it is reported as not responding, and the frames a main-loop // close before it is reported as not responding, and the frames a main-loop
// app gets to act on the request before a kept window counts as a refusal. // app gets to act on the request before a kept window counts as a refusal.
// A kept window is a refusal only after QUIET frames in a row with no
// dialog open: an app can have none open for a moment between two (a
// BASIC program's slice may end between its save prompt and its Save As),
// and the shell can take a whole round of its turns (its priority + 1)
// before the app's next one.
#define SHELL_EXIT_GRACE_MS 5000 #define SHELL_EXIT_GRACE_MS 5000
#define SHELL_EXIT_SETTLE_FRAMES 4 #define SHELL_EXIT_SETTLE_FRAMES 4
#define SHELL_EXIT_QUIET_FRAMES (SHELL_TASK_PRIORITY + 2)
#define SHELL_EXIT_TITLE "Exit DVX" #define SHELL_EXIT_TITLE "Exit DVX"
#define SHELL_CLOSE_TITLE "Close Program" #define SHELL_CLOSE_TITLE "Close Program"
@ -201,8 +212,8 @@ static int32_t *sTaskDepth = NULL; // stb_ds, indexed by task id
static uint32_t sRunningTask = 0; // task whose context was last restored static uint32_t sRunningTask = 0; // task whose context was last restored
static int32_t sDepthBase = 0; // dispatchDepth when that task was switched in static int32_t sDepthBase = 0; // dispatchDepth when that task was switched in
// Serial-library RNG entry points, resolved at run time (the shell does // The cryptography library's RNG calls, found at run time once the
// not link against the library, and the host tests have none). // library is loaded (the shell does not link against it; entropyBind).
// secRngAddEntropy has exactly the DvxEntropyHookT signature and is // secRngAddEntropy has exactly the DvxEntropyHookT signature and is
// registered directly. // registered directly.
typedef int (*RngSeedFileFnT)(const char *path); typedef int (*RngSeedFileFnT)(const char *path);
@ -240,7 +251,9 @@ static void compactDesktopFns(void);
static void compactIdleHandlers(void); static void compactIdleHandlers(void);
static void ctrlEscHandler(void *ctx); static void ctrlEscHandler(void *ctx);
static void desktopEnded(int32_t appId); static void desktopEnded(int32_t appId);
static bool entropyBind(RngSeedFileFnT loadSeed, DvxEntropyHookT addEntropy, RngSeedFileFnT saveSeed);
static void entropyInit(void); static void entropyInit(void);
static void entropyLibraryLoaded(void *handle);
static void entropyShutdown(void); static void entropyShutdown(void);
static ShellAppT *exitAppRunning(int32_t appId); static ShellAppT *exitAppRunning(int32_t appId);
static bool exitAskApp(AppContextT *ctx, int32_t appId, bool exiting); static bool exitAskApp(AppContextT *ctx, int32_t appId, bool exiting);
@ -392,27 +405,49 @@ static void desktopEnded(int32_t appId) {
} }
// When the cryptography library is loaded: feed it input timings and restore the // Feed the cryptography library input timings and restore the seed saved
// seed saved by the previous run (secRngLoadSeed rewrites the file at once). // by the previous run (secRngLoadSeed rewrites the file at once). False
static void entropyInit(void) { // when these are not the library's calls (it is not loaded).
RngSeedFileFnT loadSeed = (RngSeedFileFnT)dlsym(NULL, DVX_SYM("secRngLoadSeed")); static bool entropyBind(RngSeedFileFnT loadSeed, DvxEntropyHookT addEntropy, RngSeedFileFnT saveSeed) {
DvxEntropyHookT addEntropy = (DvxEntropyHookT)dlsym(NULL, DVX_SYM("secRngAddEntropy")); if (!loadSeed || !addEntropy || !saveSeed) {
return false;
sRngSaveSeed = (RngSeedFileFnT)dlsym(NULL, DVX_SYM("secRngSaveSeed"));
platformUserPath(sSeedPath, sizeof(sSeedPath), SHELL_SEED_NAME);
if (addEntropy) {
dvxSetInputEntropyHook(addEntropy);
} }
if (loadSeed && loadSeed(sSeedPath) != SEC_SUCCESS) { platformSetLibraryLoadHook(NULL);
sRngSaveSeed = saveSeed;
dvxSetInputEntropyHook(addEntropy);
if (loadSeed(sSeedPath) != SEC_SUCCESS) {
dvxLog("Shell: no usable random seed in %s (a new one is written at shutdown)", sSeedPath); dvxLog("Shell: no usable random seed in %s (a new one is written at shutdown)", sSeedPath);
} }
return true;
}
// The cryptography library is bound now when it is loaded already, else
// the moment DVX loads it (a program needs it): looking it up here would
// load it at every start.
static void entropyInit(void) {
platformUserPath(sSeedPath, sizeof(sSeedPath), SHELL_SEED_NAME);
if (!entropyBind((RngSeedFileFnT)platformLoadedSymbol(DVX_SYM("secRngLoadSeed")), (DvxEntropyHookT)platformLoadedSymbol(DVX_SYM("secRngAddEntropy")), (RngSeedFileFnT)platformLoadedSymbol(DVX_SYM("secRngSaveSeed")))) {
platformSetLibraryLoadHook(entropyLibraryLoaded);
}
} }
// Detach the input hook, then save a fresh seed for the next run. static void entropyLibraryLoaded(void *handle) {
if (entropyBind((RngSeedFileFnT)dlsym(handle, DVX_SYM("secRngLoadSeed")), (DvxEntropyHookT)dlsym(handle, DVX_SYM("secRngAddEntropy")), (RngSeedFileFnT)dlsym(handle, DVX_SYM("secRngSaveSeed")))) {
dvxLog("Shell: random numbers seeded as the cryptography library loaded");
}
}
// Detach the input hook, then save a fresh seed for the next run (when
// the cryptography library was loaded).
static void entropyShutdown(void) { static void entropyShutdown(void) {
platformSetLibraryLoadHook(NULL);
dvxSetInputEntropyHook(NULL); dvxSetInputEntropyHook(NULL);
if (sRngSaveSeed && sRngSaveSeed(sSeedPath) != SEC_SUCCESS) { if (sRngSaveSeed && sRngSaveSeed(sSeedPath) != SEC_SUCCESS) {
@ -478,12 +513,13 @@ static bool exitAskApp(AppContextT *ctx, int32_t appId, bool exiting) {
// Pump the shell until the window asked to close is gone. A kept window // Pump the shell until the window asked to close is gone. A kept window
// is a refusal once the app has shown something (its save prompt) and no // is a refusal once the app has shown something (its save prompt) and no
// dialog is open any more; an app that shows nothing and keeps the window // dialog has been open for SHELL_EXIT_QUIET_FRAMES; an app that shows
// for SHELL_EXIT_GRACE_MS is stuck. created is ctx->windowsCreated from // nothing and keeps the window for SHELL_EXIT_GRACE_MS is stuck. created
// before the request. // is ctx->windowsCreated from before the request.
static ExitCloseE exitAwaitClose(AppContextT *ctx, int32_t appId, int32_t winId, uint32_t created) { static ExitCloseE exitAwaitClose(AppContextT *ctx, int32_t appId, int32_t winId, uint32_t created) {
PlatformTicksT start = platformClock(); PlatformTicksT start = platformClock();
PlatformTicksT grace = PLATFORM_MS_TO_TICKS(SHELL_EXIT_GRACE_MS); PlatformTicksT grace = PLATFORM_MS_TO_TICKS(SHELL_EXIT_GRACE_MS);
int32_t quietFrames = 0;
for (int32_t frame = 0; ; frame++) { for (int32_t frame = 0; ; frame++) {
if (!exitAppRunning(appId) || exitWindowGone(ctx, winId)) { if (!exitAppRunning(appId) || exitWindowGone(ctx, winId)) {
@ -491,14 +527,16 @@ static ExitCloseE exitAwaitClose(AppContextT *ctx, int32_t appId, int32_t winId,
} }
if (frame >= SHELL_EXIT_SETTLE_FRAMES) { if (frame >= SHELL_EXIT_SETTLE_FRAMES) {
if (ctx->windowsCreated != created) { if (ctx->windowsCreated == created) {
if (!ctx->modalWindow) { if (platformClock() - start >= grace) {
return ExitCloseStuckE;
}
} else if (ctx->modalWindow) {
quietFrames = 0;
} else if (++quietFrames >= SHELL_EXIT_QUIET_FRAMES) {
dvxLog("Shell: exit cancelled by '%s'", exitAppRunning(appId)->name); dvxLog("Shell: exit cancelled by '%s'", exitAppRunning(appId)->name);
return ExitCloseRefusedE; return ExitCloseRefusedE;
} }
} else if (platformClock() - start >= grace) {
return ExitCloseStuckE;
}
} }
shellFrame(ctx); shellFrame(ctx);
@ -797,11 +835,7 @@ int shellMain(int argc, char *argv[]) {
return 1; return 1;
} }
// Shell task (task 0) gets high priority so the UI remains responsive tsSetPriority(TS_MAIN_TASK_ID, SHELL_TASK_PRIORITY);
// even when app tasks are CPU-hungry. With HIGH priority (11 credits
// per epoch) vs app tasks at NORMAL (6 credits), the shell gets
// roughly twice as many scheduling turns as any single app.
tsSetPriority(TS_MAIN_TASK_ID, TS_PRIORITY_HIGH);
// Initialize app slot table // Initialize app slot table
shellAppInit(); shellAppInit();

View file

@ -470,7 +470,14 @@ static void saveBegin(void) {
if (!sSave.f) { if (!sSave.f) {
saveEnd(false); saveEnd(false);
return;
} }
// A buffer of one step's chunk, flushed by each saveStep: one write to
// the disk a step, not a default buffer's worth (16 KB, a third of a
// second on a 486) in whichever step fills it. Unbuffered would be
// worse: the C library writes such a file a byte at a time.
setvbuf(sSave.f, NULL, _IOFBF, SHELL_PROG_CACHE_SAVE_CHUNK);
} }
@ -505,7 +512,7 @@ static void saveStep(void) {
n = SHELL_PROG_CACHE_SAVE_CHUNK; n = SHELL_PROG_CACHE_SAVE_CHUNK;
} }
if (fwrite(sSave.data + sSave.pos, 1, n, sSave.f) != n) { if (fwrite(sSave.data + sSave.pos, 1, n, sSave.f) != n || fflush(sSave.f) != 0) {
saveEnd(false); saveEnd(false);
return; return;
} }

View file

@ -617,7 +617,11 @@ DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all
| |
+-- Every other library loads when something first needs one of +-- Every other library loads when something first needs one of
| its names: a module being opened (platformModuleOpen), or a | its names: a module being opened (platformModuleOpen), or a
| dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded) | dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded).
| platformLoadedSymbol looks a name up without loading; what
| binds to a library once it is loaded hears of each one
| through the shell's hook (platformSetLibraryLoadHook: the
| cryptography library's random numbers are seeded then)
| |
+-- dvxInit() -- video mode, input, font, colors, cursors +-- dvxInit() -- video mode, input, font, colors, cursors
| |

View file

@ -1104,6 +1104,20 @@ typedef void *(*PlatformSymbolResolverFnT)(const char *name);
void platformSetModuleOpen(PlatformModuleOpenFnT fn); void platformSetModuleOpen(PlatformModuleOpenFnT fn);
void platformSetSymbolResolver(PlatformSymbolResolverFnT fn); void platformSetSymbolResolver(PlatformSymbolResolverFnT fn);
// The address of name in dvx.exe or a module already loaded, as
// dlsym(NULL, name) finds it, but never loading a library for it
// (dlsym(NULL) loads the library that exports a name not loaded yet).
// For code that binds to a library only once something has loaded it.
void *platformLoadedSymbol(const char *name);
// A library DVX loaded after it started (one a program needed), ready
// for use: the loader calls platformLibraryLoaded with its handle, which
// passes it to the hook (the shell's, for what binds to a library only
// once it is loaded; dlsym with the handle never loads anything).
typedef void (*PlatformLibraryLoadFnT)(void *handle);
void platformLibraryLoaded(void *handle);
void platformSetLibraryLoadHook(PlatformLibraryLoadFnT fn);
// Register platform and C runtime symbols with the dynamic module // Register platform and C runtime symbols with the dynamic module
// loader so that DXE modules can resolve them at load time. On DOS // loader so that DXE modules can resolve them at load time. On DOS
// this calls dlregsym() with the full DJGPP libc/libm/libgcc/platform // this calls dlregsym() with the full DJGPP libc/libm/libgcc/platform

View file

@ -1966,6 +1966,19 @@ const char *platformLineEnding(void) {
} }
// dlsym(NULL) with DJGPP's last resort (the loader's, which loads the
// library behind a name) out of the way for the one lookup.
void *platformLoadedSymbol(const char *name) {
void *(*resolver)(const char *) = _dlsymresolver;
void *sym;
_dlsymresolver = NULL;
sym = dlsym(NULL, name);
_dlsymresolver = resolver;
return sym;
}
// Pin a code or data range into physical memory so an interrupt handler // Pin a code or data range into physical memory so an interrupt handler
// can touch it without faulting under a paging DPMI host. addr is a // can touch it without faulting under a paging DPMI host. addr is a
// near pointer in this program's CS (isCode) or DS space. // near pointer in this program's CS (isCode) or DS space.
@ -3592,8 +3605,8 @@ static void videoError(const char *fmt, ...) {
// loader's own start-up ones, platformRegisterDxeExports, // loader's own start-up ones, platformRegisterDxeExports,
// platformRegisterSymOverrides, platformUserInit, platformSetLogPath, // platformRegisterSymOverrides, platformUserInit, platformSetLogPath,
// platformSetLogMemory, platformBootNoteAdd, platformSetReadOnly, // platformSetLogMemory, platformBootNoteAdd, platformSetReadOnly,
// platformSetModuleOpen, platformSetSymbolResolver, and // platformSetModuleOpen, platformSetSymbolResolver,
// platformDxeExports, are not exported) // platformLibraryLoaded, and platformDxeExports, are not exported)
// 2. C runtime (libc, libm, libgcc, DJGPP internals) // 2. C runtime (libc, libm, libgcc, DJGPP internals)
// 3. DXE3 runtime (dlsym/dlclose/dlerror/dlregsym; not dlopen: a module // 3. DXE3 runtime (dlsym/dlclose/dlerror/dlregsym; not dlopen: a module
// opens one with platformModuleOpen, which loads what it needs) // opens one with platformModuleOpen, which loads what it needs)
@ -3723,6 +3736,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformKindNameOf) DXE_EXPORT(platformKindNameOf)
DXE_EXPORT(platformKindReadDir) DXE_EXPORT(platformKindReadDir)
DXE_EXPORT(platformLineEnding) DXE_EXPORT(platformLineEnding)
DXE_EXPORT(platformLoadedSymbol)
DXE_EXPORT(platformLockRegion) DXE_EXPORT(platformLockRegion)
DXE_EXPORT(platformLogCrashDetail) DXE_EXPORT(platformLogCrashDetail)
DXE_EXPORT(platformLogMemory) DXE_EXPORT(platformLogMemory)
@ -3756,6 +3770,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformReadFile) DXE_EXPORT(platformReadFile)
DXE_EXPORT(platformReplaceFile) DXE_EXPORT(platformReplaceFile)
DXE_EXPORT(platformSetAppExitHook) DXE_EXPORT(platformSetAppExitHook)
DXE_EXPORT(platformSetLibraryLoadHook)
DXE_EXPORT(platformShortPath) DXE_EXPORT(platformShortPath)
DXE_EXPORT(platformShutdownHookAdd) DXE_EXPORT(platformShutdownHookAdd)
DXE_EXPORT(platformShutdownHookRemove) DXE_EXPORT(platformShutdownHookRemove)

View file

@ -56,6 +56,7 @@
#include <ctype.h> #include <ctype.h>
#include <dlfcn.h>
#include <setjmp.h> #include <setjmp.h>
#include <signal.h> #include <signal.h>
#include <stdio.h> #include <stdio.h>
@ -669,6 +670,12 @@ const char *platformLineEnding(void) {
} }
// The host's dynamic linker never loads a library for a lookup.
void *platformLoadedSymbol(const char *name) {
return dlsym(RTLD_DEFAULT, name);
}
// No paging concerns in a host process: nothing to pin. // No paging concerns in a host process: nothing to pin.
int32_t platformLockRegion(const void *addr, uint32_t size, bool isCode) { int32_t platformLockRegion(const void *addr, uint32_t size, bool isCode) {
(void)addr; (void)addr;

View file

@ -250,6 +250,7 @@ bool platformKindFind(char *out, int32_t outSize, DvxKindE ki
const char *platformKindName(DvxKindE kind); const char *platformKindName(DvxKindE kind);
bool platformKindNameOf(DvxKindE kind, const char *path, char *out, int32_t outSize); bool platformKindNameOf(DvxKindE kind, const char *path, char *out, int32_t outSize);
char **platformKindReadDir(DvxKindE kind); char **platformKindReadDir(DvxKindE kind);
void platformLibraryLoaded(void *handle);
const char *platformLogMemory(void); const char *platformLogMemory(void);
bool platformLongNameChar(char c); bool platformLongNameChar(char c);
int32_t platformMkdirRecursive(const char *path); int32_t platformMkdirRecursive(const char *path);
@ -274,6 +275,7 @@ char *platformPathTidy(char *path);
char *platformReadFile(const char *path, int32_t *outLen); char *platformReadFile(const char *path, int32_t *outLen);
bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath); bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath);
void platformSetAppExitHook(PlatformAppExitFnT fn); void platformSetAppExitHook(PlatformAppExitFnT fn);
void platformSetLibraryLoadHook(PlatformLibraryLoadFnT fn);
void platformSetLogMemory(void); void platformSetLogMemory(void);
void platformSetLogPath(const char *path); void platformSetLogPath(const char *path);
void platformSetModuleOpen(PlatformModuleOpenFnT fn); void platformSetModuleOpen(PlatformModuleOpenFnT fn);
@ -341,6 +343,9 @@ static PlatformAppExitFnT sAppExitHook = NULL;
// The loader's module opening (platformModuleOpen), set at its start // The loader's module opening (platformModuleOpen), set at its start
static PlatformModuleOpenFnT sModuleOpen = NULL; static PlatformModuleOpenFnT sModuleOpen = NULL;
// What hears of each library loaded after the start (platformLibraryLoaded)
static PlatformLibraryLoadFnT sLibraryLoadHook = NULL;
// Every modifier-specific scancode run the keyboard path folds. // Every modifier-specific scancode run the keyboard path folds.
static const ScanFoldT sScanFolds[] = { static const ScanFoldT sScanFolds[] = {
{ FKEY_SCAN_SHIFT_F1, FKEY_LOW_COUNT, KEY_SCAN_F1, KEY_MOD_SHIFT, 0, KEY_MOD_CTRL | KEY_MOD_ALT }, { FKEY_SCAN_SHIFT_F1, FKEY_LOW_COUNT, KEY_SCAN_F1, KEY_MOD_SHIFT, 0, KEY_MOD_CTRL | KEY_MOD_ALT },
@ -1430,6 +1435,13 @@ char **platformKindReadDir(DvxKindE kind) {
} }
void platformLibraryLoaded(void *handle) {
if (sLibraryLoadHook) {
sLibraryLoadHook(handle);
}
}
const char *platformLogMemory(void) { const char *platformLogMemory(void) {
return sLogMem ? sLogMem : ""; return sLogMem ? sLogMem : "";
} }
@ -1853,6 +1865,11 @@ void platformSetAppExitHook(PlatformAppExitFnT fn) {
} }
void platformSetLibraryLoadHook(PlatformLibraryLoadFnT fn) {
sLibraryLoadHook = fn;
}
void platformSetLogMemory(void) { void platformSetLogMemory(void) {
if (!sLogMem) { if (!sLogMem) {
sLogMem = (char *)calloc(1, PLATFORM_LOG_MEM_MAX); sLogMem = (char *)calloc(1, PLATFORM_LOG_MEM_MAX);

View file

@ -301,7 +301,7 @@ The build makes mathx.lib and mathx.hlp (a preview of the help). The project hol
.h2 How the Library Works .h2 How the Library Works
DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export. DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Code that uses a library only once something else has loaded it looks its names up with platformLoadedSymbol (dvxPlat.h), which never loads one. Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export.
BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *. BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.

View file

@ -888,8 +888,10 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
// steal global widget focus from the active window. The widget that // steal global widget focus from the active window. The widget that
// held focus when the window last blurred gets it back (so GotFocus // held focus when the window last blurred gets it back (so GotFocus
// pairs with the LostFocus the blur fired); otherwise the first // pairs with the LostFocus the blur fired); otherwise the first
// focusable widget does. // focusable widget does. A hidden window (a BASIC form during its
if (!sFocusedWidget && win->focused) { // Load, focused but not shown yet) waits until it is shown, so the
// choice is made on what its program set up, as in VB.
if (!sFocusedWidget && win->focused && win->visible) {
WidgetT *target = win->lastFocusWidget; WidgetT *target = win->lastFocusWidget;
win->lastFocusWidget = NULL; win->lastFocusWidget = NULL;

View file

@ -885,6 +885,12 @@ static bool libLoad(ModuleT *lib) {
loadBasicControls((CtlLoadFnT)dlsym(lib->handle, DVX_SYM(CTL_LOAD_SYMBOL))); loadBasicControls((CtlLoadFnT)dlsym(lib->handle, DVX_SYM(CTL_LOAD_SYMBOL)));
} }
// What binds to a library once it is loaded (the shell's hook), with
// its memory DVX's too
if (sStarted) {
platformLibraryLoaded(lib->handle);
}
dvxMemSetApp(prevApp); dvxMemSetApp(prevApp);
if (!sStarted) { if (!sStarted) {

View file

@ -346,7 +346,7 @@ Begin Form ParDemo
Begin Timer tmrCheck Begin Timer tmrCheck
Interval = 200 Interval = 200
Enabled = True Enabled = False
End End
End End
@ -577,6 +577,9 @@ SUB ParDemo_Load
ParCopy.Show ParCopy.Show
SET copyB = NEW ParCopy SET copyB = NEW ParCopy
copyB.Show copyB.Show
' The checks start once every window is up
tmrCheck.Enabled = True
END SUB END SUB

View file

@ -55,7 +55,7 @@ End
' and a Data control whose record set arrives a part at a time (the first ' and a Data control whose record set arrives a part at a time (the first
' DATA_FIRST rows at Refresh, the rest while the program waits), walked ' DATA_FIRST rows at Refresh, the rest while the program waits), walked
' with a Do Until EOF loop. The window title reports the outcome for the ' with a Do Until EOF loop. The window title reports the outcome for the
' script once every step has run: "SQL PASS 3 rows 3.25 long data 64/1000 ' script once every step has run: "SQL PASS 3 rows 3.25 long data 1000
' loop 1000" or "SQL FAIL <what>". ' loop 1000" or "SQL FAIL <what>".
OPTION EXPLICIT OPTION EXPLICIT
@ -216,8 +216,16 @@ SUB tmrStart_Timer
datBig.MoveLast datBig.MoveLast
last = datBig.GetField("x") last = datBig.GetField("x")
ok = (first = DATA_FIRST AND datBig.RecordCount = BIG_ROWS AND loops = BIG_ROWS AND last = LTRIM$(STR$(BIG_ROWS))) ' Refresh gave at least the first DATA_FIRST rows at once (any number
' up to all of them by the next line: the rest come between the
' program's turns, and one may end right after Refresh)
ok = (first >= DATA_FIRST AND datBig.RecordCount = BIG_ROWS AND loops = BIG_ROWS AND last = LTRIM$(STR$(BIG_ROWS)))
IF ok THEN
Report ok, sums + " long data " + LTRIM$(STR$(datBig.RecordCount)) + " loop " + LTRIM$(STR$(loops))
ELSE
Report ok, sums + " long data " + LTRIM$(STR$(first)) + "/" + LTRIM$(STR$(datBig.RecordCount)) + " loop " + LTRIM$(STR$(loops)) Report ok, sums + " long data " + LTRIM$(STR$(first)) + "/" + LTRIM$(STR$(datBig.RecordCount)) + " loop " + LTRIM$(STR$(loops))
END IF
END SUB END SUB

View file

@ -29,6 +29,9 @@
// GAMETEST: raw down|up <scan> ext <0|1> rep <0|1> mods <hex> // GAMETEST: raw down|up <scan> ext <0|1> rep <0|1> mods <hex>
// GAMETEST: held LShift=<0|1> RCtrl=.. Up=.. A=.. when that set changes // GAMETEST: held LShift=<0|1> RCtrl=.. Up=.. A=.. when that set changes
// GAMETEST: key <hex> mods <hex> onKey (F1, Alt+X, ... reach it) // GAMETEST: key <hex> mods <hex> onKey (F1, Alt+X, ... reach it)
// F9 starts the timing over (the script's own steps, which run in the idle
// time a game shares, are done by then):
// GAMETEST: timing starts over
// F10 (which a game window receives, instead of opening a menu) ends the // F10 (which a game window receives, instead of opening a menu) ends the
// run: the frame, timer and BIOS tick statistics are logged and checked // run: the frame, timer and BIOS tick statistics are logged and checked
// ("GAMETEST: timing PASS" or "... FAIL <what>"), and the program ends // ("GAMETEST: timing PASS" or "... FAIL <what>"), and the program ends
@ -84,6 +87,7 @@ SHELL_APP_DESCRIPTOR(
static DxeAppContextT *sCtx = NULL; static DxeAppContextT *sCtx = NULL;
static volatile uint32_t sTicks = 0; // timerTick calls (interrupt time) static volatile uint32_t sTicks = 0; // timerTick calls (interrupt time)
static volatile bool sStop = false; // F10 static volatile bool sStop = false; // F10
static volatile bool sRestart = false; // F9
static volatile bool sClosed = false; // the window was closed static volatile bool sClosed = false; // the window was closed
@ -139,7 +143,9 @@ static void onKey(WindowT *win, int32_t key, int32_t mod) {
(void)win; (void)win;
dvxLog("GAMETEST: key %lX mods %lX", (unsigned long)key, (unsigned long)mod); dvxLog("GAMETEST: key %lX mods %lX", (unsigned long)key, (unsigned long)mod);
if (key == KEY_F10) { if (key == KEY_F9) {
sRestart = true;
} else if (key == KEY_F10) {
sStop = true; sStop = true;
} }
} }
@ -228,6 +234,20 @@ int32_t appMain(DxeAppContextT *ctx) {
while (!sStop && !sClosed) { while (!sStop && !sClosed) {
gameFrameWait(&pacer); gameFrameWait(&pacer);
if (sRestart) {
sRestart = false;
dvxLog("GAMETEST: timing starts over");
platformTimerStats(&st);
bios0 = st.biosTicks;
minUs = UINT64_MAX;
maxUs = 0;
startUs = platformMicros();
lastUs = startUs;
sTicks = 0;
gameFrameInit(&pacer, GAME_FRAME_HZ);
continue;
}
uint64_t now = platformMicros(); uint64_t now = platformMicros();
uint64_t dt = now - lastUs; uint64_t dt = now - lastUs;

View file

@ -32,6 +32,11 @@
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame and every shot has them
# all; the first program started (Notepad, a BASIC program) loads the BASIC
# run time first, which DVX leaves on disk until it is needed, and the
# icons wait while it loads
WAIT 60
LAUNCH apps/kpunch/notepad/notepad.app LAUNCH apps/kpunch/notepad/notepad.app
WAITWIN "Untitled - Notepad" WAITWIN "Untitled - Notepad"

View file

@ -30,7 +30,9 @@
# through the keyboard hook, not the BIOS buffer). F1 and Alt+X must # through the keyboard hook, not the BIOS buffer). F1 and Alt+X must
# reach the game (no help window, no menu); F10 ends its run, when it # reach the game (no help window, no menu); F10 ends its run, when it
# checks the timer calls and the BIOS ticks against the clock and then # checks the timer calls and the BIOS ticks against the clock and then
# calls exit(), which must end only that program. The DOS time must # calls exit(), which must end only that program. F9 first starts the
# timing over, so the ten seconds it checks are the game's alone and not
# the steps above (this script runs in the idle time the game shares). The DOS time must
# keep pace with the host's while the fast timer runs (# CLOCK below). # keep pace with the host's while the fast timer runs (# CLOCK below).
# Last, TEST\GAMEKEY.APP (fixtures/gamekey) shows GameKeyDown from BASIC # Last, TEST\GAMEKEY.APP (fixtures/gamekey) shows GameKeyDown from BASIC
# in its caption. The game loop and the event dispatch run in either # in its caption. The game loop and the event dispatch run in either
@ -65,6 +67,8 @@ WAITLOG "GAMETEST: key 12D mods"
WAITMS 1000 WAITMS 1000
ASSERTNOWIN "*Help*" ASSERTNOWIN "*Help*"
ASSERTWIN "Game Test" ASSERTWIN "Game Test"
KEY F9
WAITLOG "GAMETEST: timing starts over"
WAITMS 10000 WAITMS 10000
CLOCKMARK CLOCKMARK
KEY F10 KEY F10

View file

@ -43,6 +43,13 @@
# NOFILE: SYSTEM/DVXHLPC.EXE # NOFILE: SYSTEM/DVXHLPC.EXE
# NOFILE: APPS/KPUNCH/HELPEDIT # NOFILE: APPS/KPUNCH/HELPEDIT
# FILE: APPS/KPUNCH/DVXHELP/DVXHELP.APP # FILE: APPS/KPUNCH/DVXHELP/DVXHELP.APP
# Libraries load when first used: a BASIC program starting loads none it
# does not call (printing, games, and serial ports), and the cryptography
# library comes in with Software's package library, not at the start.
# NOLOG: Loading: LIBS\kpunch\print\print.lib
# NOLOG: Loading: LIBS\kpunch\game\game.lib
# NOLOG: Loading: LIBS\kpunch\serial\serial.lib
# LOG: Shell: random numbers seeded as the cryptography library loaded
# FILE: SYSTEM/DVXPKG.EXE # FILE: SYSTEM/DVXPKG.EXE
# LOG: DVX Shell ready. # LOG: DVX Shell ready.
TIMEOUT 300 TIMEOUT 300

View file

@ -37,6 +37,10 @@
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH TEST/LFNSMK.APP LAUNCH TEST/LFNSMK.APP
WAITWIN "LFN *" WAITWIN "LFN *"
WAIT 10 WAIT 10

View file

@ -33,6 +33,10 @@
TIMEOUT 90 TIMEOUT 90
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH apps/kpunch/recorder/recorder.app LAUNCH apps/kpunch/recorder/recorder.app
WAITWIN "New recording - Sound Recorder" WAITWIN "New recording - Sound Recorder"
WAIT 10 WAIT 10

View file

@ -35,6 +35,10 @@
TIMEOUT 180 TIMEOUT 180
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH TEST/NETSMK.APP LAUNCH TEST/NETSMK.APP
WAITLOG "dvxnet: packet driver" WAITLOG "dvxnet: packet driver"
WAITLOG "dvxnet: address" WAITLOG "dvxnet: address"

View file

@ -32,6 +32,10 @@
TIMEOUT 60 TIMEOUT 60
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH TEST/PATHSMK.APP LAUNCH TEST/PATHSMK.APP
WAITWIN "PATHS *" WAITWIN "PATHS *"
WAIT 10 WAIT 10

View file

@ -39,6 +39,10 @@
TIMEOUT 90 TIMEOUT 90
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH apps/kpunch/recorder/recorder.app LAUNCH apps/kpunch/recorder/recorder.app
WAITWIN "New recording - Sound Recorder" WAITWIN "New recording - Sound Recorder"
WAIT 10 WAIT 10

View file

@ -32,6 +32,10 @@
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH apps/sdktest/sdkApp/sdkApp.app LAUNCH apps/sdktest/sdkApp/sdkApp.app
WAITWIN "SdkApp" WAITWIN "SdkApp"

View file

@ -41,6 +41,10 @@
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH apps/sdktest/sdkGame/sdkGame.app LAUNCH apps/sdktest/sdkGame/sdkGame.app
WAITLOG "sdkGame: started" WAITLOG "sdkGame: started"
WAITWIN "SdkGame - Score 0 - Time *" WAITWIN "SdkGame - Score 0 - Time *"

View file

@ -36,6 +36,10 @@
TIMEOUT 60 TIMEOUT 60
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH TEST/SHELLRUN.APP LAUNCH TEST/SHELLRUN.APP
WAITLOG "ShellRun: ShellRunDosWait returned 0" WAITLOG "ShellRun: ShellRunDosWait returned 0"
WAITLOG "ShellRun: SHELL returned" WAITLOG "ShellRun: SHELL returned"

View file

@ -22,7 +22,10 @@
# software.scr -- Software installs a package opened with it, with the # software.scr -- Software installs a package opened with it, with the
# package it needs from the same folder (a theme that needs another), then # package it needs from the same folder (a theme that needs another), then
# removes the needed one, which takes the other with it. # removes the needed one, which takes the other with it. The shots wait
# for Software's own lines: the install, and the installed list's count
# (made before its window is shown, which is waited for too: the install
# message, also "Software", has closed by then).
# PACKAGE: swspace DOWNLOAD/SPACE.DPK # PACKAGE: swspace DOWNLOAD/SPACE.DPK
# PACKAGE: swbase DOWNLOAD/BASE.DPK # PACKAGE: swbase DOWNLOAD/BASE.DPK
# LOG: Software: installed from # LOG: Software: installed from
@ -30,16 +33,22 @@
TIMEOUT 180 TIMEOUT 180
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH apps/kpunch/software/software.app DOWNLOAD\SPACE.DPK LAUNCH apps/kpunch/software/software.app DOWNLOAD\SPACE.DPK
WAITWIN "Install Software" WAITWIN "Install Software"
WAIT 10 WAIT 10
SHOT sw-plan SHOT sw-plan
KEY Enter KEY Enter
WAIT 60
SHOT sw-done
KEY Enter
WAITLOG "Software: installed from" WAITLOG "Software: installed from"
WAIT 20 WAIT 20
SHOT sw-done
KEY Enter
WAITLOG "Software: 2 packages installed"
WAITWIN "Software"
WAIT 10
SHOT sw-list SHOT sw-list
# Remove... with Base Theme selected (the first row) # Remove... with Base Theme selected (the first row)
KEY Alt+R KEY Alt+R
@ -47,6 +56,7 @@ WAIT 30
SHOT sw-needs SHOT sw-needs
KEY Enter KEY Enter
WAITLOG "Software: removed acme.base" WAITLOG "Software: removed acme.base"
WAIT 20 WAITLOG "Software: 0 packages installed"
WAIT 10
SHOT sw-empty SHOT sw-empty
QUIT QUIT

View file

@ -27,17 +27,22 @@
# SQLFieldDbl (a DECLARE LIBRARY function returning DOUBLE). Then a long # SQLFieldDbl (a DECLARE LIBRARY function returning DOUBLE). Then a long
# statement, during which the program's Timer events run (and are refused # statement, during which the program's Timer events run (and are refused
# the busy database), and a Data control that reads 1000 records a part # the busy database), and a Data control that reads 1000 records a part
# at a time (64 at Refresh) and walks them with Do Until EOF. Its window # at a time (at least 64 at Refresh, any number more by the next line when
# title reports "SQL PASS 3 rows 3.25 long data 64/1000 loop 1000" only # the program's turn ended between them) and walks them with Do Until EOF.
# when every step worked. # Its window title reports "SQL PASS 3 rows 3.25 long data 1000 loop 1000" only
# when every step worked; the shot waits for it (the steps run from a
# Timer, so the title passes through earlier stages first).
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH TEST/SQLSMK.APP LAUNCH TEST/SQLSMK.APP
WAITWIN "SQL *" WAITWIN "SQL PASS 3 rows 3.25 long data 1000 loop 1000"
WAIT 10 WAIT 10
SHOT sqlsmk SHOT sqlsmk
ASSERTWIN "SQL PASS 3 rows 3.25 long data 64/1000 loop 1000"
KEY Alt+F4 KEY Alt+F4
WAITNOWIN "SQL *" WAITNOWIN "SQL *"
QUIT QUIT

View file

@ -34,6 +34,10 @@
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
# Program Manager's icons come in a few a frame (the first start has no
# icon cache yet, and they come slower once a program runs); the shots
# have them all, so they are in before the program starts
WAIT 60
LAUNCH TEST/UCDEMO.APP LAUNCH TEST/UCDEMO.APP
WAITWIN "UCDEMO *" WAITWIN "UCDEMO *"
WAITNOWIN "UCDEMO RUN" WAITNOWIN "UCDEMO RUN"

View file

@ -26,7 +26,8 @@
# DVX.INI). Take the tour shows the tour's pages in place of the tiles # DVX.INI). Take the tour shows the tour's pages in place of the tiles
# (Next, End the tour), Read the User Guide shows the DVX help, F1 the # (Next, End the tour), Read the User Guide shows the DVX help, F1 the
# Welcome help, Close (with "Don't show this again" ticked, the default) # Welcome help, Close (with "Don't show this again" ticked, the default)
# records that it is not shown again. # records that it is not shown again. A tour page logs "Welcome: tour page
# N" once it is drawn, which the shots wait for.
TIMEOUT 120 TIMEOUT 120
WAITWIN "Program Manager" WAITWIN "Program Manager"
MOVE 639 479 MOVE 639 479
@ -35,10 +36,12 @@ WAITWIN "Welcome to DVX"
WAIT 20 WAIT 20
SHOT wl-open SHOT wl-open
KEY Alt+T KEY Alt+T
WAIT 20 WAITLOG "Welcome: tour page 1"
WAIT 10
SHOT wl-tour SHOT wl-tour
KEY Alt+N KEY Alt+N
WAIT 20 WAITLOG "Welcome: tour page 2"
WAIT 10
SHOT wl-tour2 SHOT wl-tour2
KEY Alt+E KEY Alt+E
WAIT 20 WAIT 20

View file

@ -72,6 +72,7 @@
#define FORM_W 400 #define FORM_W 400
#define FORM_H 300 #define FORM_H 300
#define TIMER_INTERVAL_MS 100 #define TIMER_INTERVAL_MS 100
#define QUEUE_DRAG_DX 3 // caseQueuedOrder: the drag's move, in pixels
#define TIMER_TICKS 3 #define TIMER_TICKS 3
#define RESET_CYCLES 50 #define RESET_CYCLES 50
#define FRM_TEXT_MAX 16384 #define FRM_TEXT_MAX 16384
@ -392,6 +393,7 @@ static void caseDynamicControl(void);
static void caseEditorMembers(void); static void caseEditorMembers(void);
static void caseEventLoopExits(void); static void caseEventLoopExits(void);
static void caseEvents(void); static void caseEvents(void);
static void caseEventsWaitForCode(void);
static void caseExternCalls(void); static void caseExternCalls(void);
static void caseExternLive(void); static void caseExternLive(void);
static void caseExternMarshal(void); static void caseExternMarshal(void);
@ -436,6 +438,7 @@ static void caseProgramScreenModes(void);
static void casePropEdges(void); static void casePropEdges(void);
static void casePropTables(void); static void casePropTables(void);
static void caseQueryUnloadCancel(void); static void caseQueryUnloadCancel(void);
static void caseQueuedOrder(void);
static void caseRefValidation(void); static void caseRefValidation(void);
static void caseRemoveContainerFromHandler(void); static void caseRemoveContainerFromHandler(void);
static void caseResetCycles(void); static void caseResetCycles(void);
@ -457,6 +460,7 @@ static void caseTimers(void);
static void caseToolbarClearFromHandler(void); static void caseToolbarClearFromHandler(void);
static void caseTwoRuntimes(void); static void caseTwoRuntimes(void);
static void caseUnloadDeferred(void); static void caseUnloadDeferred(void);
static void caseUnloadThenLoad(void);
static void caseValidateCancel(void); static void caseValidateCancel(void);
static void caseVisibleKeepsModal(void); static void caseVisibleKeepsModal(void);
static void caseWidgetParity(void); static void caseWidgetParity(void);
@ -489,6 +493,7 @@ static void parseOnFormProp(void *userData, const char *key, const char *
static void parseOnMenuBegin(void *userData, const char *name, int32_t level); static void parseOnMenuBegin(void *userData, const char *name, int32_t level);
static void parseOnMenuEnd(void *userData); static void parseOnMenuEnd(void *userData);
static void parseOnMenuProp(void *userData, const char *key, const char *value); static void parseOnMenuProp(void *userData, const char *key, const char *value);
static bool pumpRtEvents(void *ctx);
static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize); static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize);
static int32_t screenPixel(const BasScreenT *scr, int32_t x, int32_t y); static int32_t screenPixel(const BasScreenT *scr, int32_t x, int32_t y);
static const char *serReadText(int32_t com); static const char *serReadText(int32_t com);
@ -3964,6 +3969,95 @@ static void caseEvents(void) {
} }
// A program's own events wait while its code runs, as in VB: a Timer that
// ticked during a long handler runs after it, not at one of its slice
// ends; DoEvents in the handler runs it there.
static void caseEventsWaitForCode(void) {
tCase("a program's events wait for DoEvents or the handler's end");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
const char *frm =
"Begin Form Form1\n"
" Caption = \"W\"\n"
" Width = 400\n"
" Height = 300\n"
" Begin Timer Tmr1\n"
" Interval = 100\n"
" Enabled = False\n"
" End\n"
"End\n"
"\n"
"Sub Tmr1_Timer\n"
" Print \"Timer\"\n"
"End Sub\n"
"Sub Form1_Busy\n"
" Dim i As Long\n"
" Dim x As Long\n"
" Print \"Start\"\n"
" For i = 1 To 100000\n"
" x = x + 1\n"
" Next\n"
" Print \"End\"\n"
"End Sub\n"
"Sub Form1_BusyDo\n"
" Dim i As Long\n"
" Dim x As Long\n"
" Print \"Start\"\n"
" For i = 1 To 100000\n"
" x = x + 1\n"
" If i = 50000 Then DoEvents\n"
" Next\n"
" Print \"End\"\n"
"End Sub\n";
FrtAppT app;
if (!expectBuild(&app, "", &frm, 1)) {
tShutdown();
return;
}
frtStart(&app, NULL);
BasFormT *form = frtForm(&app, "Form1");
BasControlT *tmr = form ? frtCtrl(form, "Tmr1") : NULL;
uint32_t own = app.rt->ownTask;
TEST_TRUE(tmr != NULL);
if (!tmr) {
frtDestroy(&app);
tShutdown();
return;
}
basVmSetDoEventsCallback(app.vm, pumpRtEvents, &app);
wgtTimerStart(tmr->widget);
// A tick from another task waits; the long handler's slice ends do
// not run it, its end does
app.rt->ownTask = own + 1;
tAdvanceClock(TIMER_INTERVAL_MS);
app.rt->ownTask = own;
frtOutClear(&app);
TEST_TRUE(frtFire(&app, "Form1", "Busy"));
TEST_STR_EQ(frtOut(&app), "Start\nEnd\n");
TEST_TRUE(basFormRtUpdate(app.rt));
TEST_STR_EQ(frtOut(&app), "Start\nEnd\nTimer\n");
// DoEvents runs it where it is
app.rt->ownTask = own + 1;
tAdvanceClock(TIMER_INTERVAL_MS);
app.rt->ownTask = own;
frtOutClear(&app);
TEST_TRUE(frtFire(&app, "Form1", "BusyDo"));
TEST_STR_EQ(frtOut(&app), "Start\nTimer\nEnd\n");
wgtTimerStop(tmr->widget);
frtDestroy(&app);
tShutdown();
}
static void caseExternCalls(void) { static void caseExternCalls(void) {
tCase("DECLARE LIBRARY through basExternResolve/basExternCall"); tCase("DECLARE LIBRARY through basExternResolve/basExternCall");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
@ -7482,17 +7576,18 @@ static void casePendingUnloadDrain(void) {
int32_t sx = 0; int32_t sx = 0;
int32_t sy = 0; int32_t sy = 0;
// The handler unloads Form2 (deferred), re-Loads it (must get the // The handler unloads Form2 (deferred), Loads it again (a new Form2,
// pending form back, not a resurrected copy), then unloads its own // as in VB; the one going away stays out of reach), then unloads its
// form; the tail print proves the handler finished on live memory // own form; the tail print proves the handler finished on live memory,
// and the drain then frees both forms. // and the drain then frees both unloaded forms, leaving the new Form2.
tWidgetCenter(go->widget, &sx, &sy); tWidgetCenter(go->widget, &sx, &sy);
tClick(sx, sy); tClick(sx, sy);
tPump(1); tPump(1);
outContains(&app, "tail"); outContains(&app, "tail");
TEST_PTR_EQ(frtForm(&app, "Form1"), NULL); TEST_PTR_EQ(frtForm(&app, "Form1"), NULL);
TEST_PTR_EQ(frtForm(&app, "Form2"), NULL); TEST_TRUE(frtForm(&app, "Form2") != NULL);
TEST_EQ((int32_t)arrlen(app.rt->forms), 0); TEST_EQ((int32_t)arrlen(app.rt->forms), 1);
TEST_EQ((int32_t)arrlen(app.rt->pendingUnload), 0);
TEST_TRUE(!app.rt->terminated); TEST_TRUE(!app.rt->terminated);
frtDestroy(&app); frtDestroy(&app);
@ -8457,6 +8552,71 @@ static void caseQueryUnloadCancel(void) {
} }
// Events another task dispatched wait for the program's turn in the order
// they came: a newer MouseMove replaces only the control's newest waiting
// event, so a drag's move runs after its MouseDown, not before it.
static void caseQueuedOrder(void) {
tCase("waiting events keep their order: a drag's move after its MouseDown");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
const char *frm =
"Begin Form Form1\n"
" Caption = \"Q\"\n"
" Width = 400\n"
" Height = 300\n"
" Begin Label Lbl1\n"
" Caption = \"Label\"\n"
" End\n"
"End\n"
"\n"
"Sub Lbl1_MouseDown(Button As Integer, X As Integer, Y As Integer)\n"
" Print \"Down=\"; Button\n"
"End Sub\n"
"Sub Lbl1_MouseMove(Button As Integer, X As Integer, Y As Integer)\n"
" Print \"Move=\"; Button\n"
"End Sub\n";
FrtAppT app;
if (!expectBuild(&app, "", &frm, 1)) {
tShutdown();
return;
}
frtStart(&app, NULL);
BasFormT *form = frtForm(&app, "Form1");
BasControlT *lbl = form ? frtCtrl(form, "Lbl1") : NULL;
uint32_t own = app.rt->ownTask;
int32_t sx = 0;
int32_t sy = 0;
TEST_TRUE(lbl != NULL);
if (!lbl) {
frtDestroy(&app);
tShutdown();
return;
}
// Hover, press, and drag while the program's task is another: all wait
tWidgetCenter(lbl->widget, &sx, &sy);
app.rt->ownTask = own + 1;
tMouseMove(sx, sy);
tMouseDown(sx, sy, MOUSE_LEFT);
tMouseMove(sx + QUEUE_DRAG_DX, sy);
tPump(1);
app.rt->ownTask = own;
frtOutClear(&app);
TEST_TRUE(basFormRtUpdate(app.rt));
TEST_STR_EQ(frtOut(&app), "Move=0 \nDown=1 \nMove=1 \n");
tMouseUp(sx + QUEUE_DRAG_DX, sy, MOUSE_LEFT);
tPump(1);
frtDestroy(&app);
tShutdown();
}
static void caseRefValidation(void) { static void caseRefValidation(void) {
tCase("stale refs, CreateControl parents, menu/form overrides, size props"); tCase("stale refs, CreateControl parents, menu/form overrides, size props");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
@ -10513,6 +10673,60 @@ static void caseUnloadDeferred(void) {
} }
// A form unloaded inside a handler goes when the handler is done; until
// then another Unload of it does nothing, but Load or a reference makes a
// new one, as in VB (it used to give the form going away, which Show
// refused: Terminal's New Session did not open when its Session Settings
// had just been closed).
static void caseUnloadThenLoad(void) {
tCase("a form unloaded in a handler: Load or Show there gets a new one");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
const char *frm1 =
"Begin Form Form1\n"
" Caption = \"One\"\n"
"End\n"
"\n"
"Sub Form1_Again\n"
" Load Form2\n"
" Unload Form2\n"
" Unload Form2\n"
" Form2.Show\n"
" Print \"shown=\"; Form2.Visible\n"
"End Sub\n";
const char *frm2 =
"Begin Form Form2\n"
" Caption = \"Two\"\n"
"End\n"
"\n"
"Sub Form2_Load\n"
" Print \"load\"\n"
"End Sub\n"
"Sub Form2_Unload\n"
" Print \"unload\"\n"
"End Sub\n";
const char *frms[2] = { frm1, frm2 };
FrtAppT app;
if (!expectBuild(&app, "", frms, 2)) {
tShutdown();
return;
}
frtStart(&app, "Form1");
frtOutClear(&app);
TEST_TRUE(frtFire(&app, "Form1", "Again"));
TEST_STR_EQ(frtOut(&app), "unload\nload\nshown=True \n");
tPump(2);
TEST_TRUE(frtForm(&app, "Form2") != NULL);
TEST_TRUE(tFindWindow("Two") != NULL);
TEST_EQ((int32_t)arrlen(app.rt->pendingUnload), 0);
frtDestroy(&app);
tShutdown();
}
static void caseValidateCancel(void) { static void caseValidateCancel(void) {
tCase("Validate cancel reaches the widget as a refused write"); tCase("Validate cancel reaches the widget as a refused write");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP); tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
@ -11331,6 +11545,13 @@ static void parseOnMenuProp(void *userData, const char *key, const char *value)
} }
// The hosts' DoEvents (the stub's, the IDE's): the program's waiting
// events, then a frame (basFormRtUpdate).
static bool pumpRtEvents(void *ctx) {
return basFormRtUpdate(((FrtAppT *)ctx)->rt);
}
static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize) { static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize) {
// A RichEdit's selection is run-time state: an empty one at the start // A RichEdit's selection is run-time state: an empty one at the start
// reads the style and paragraph of plain text; its RTF is the empty // reads the style and paragraph of plain text; its RTF is the empty
@ -11729,6 +11950,7 @@ int main(void) {
caseAppCommandLine(); caseAppCommandLine();
caseMenus(); caseMenus();
caseEvents(); caseEvents();
caseEventsWaitForCode();
caseToolbarClearFromHandler(); caseToolbarClearFromHandler();
caseRemoveContainerFromHandler(); caseRemoveContainerFromHandler();
caseVisibleKeepsModal(); caseVisibleKeepsModal();
@ -11736,12 +11958,14 @@ int main(void) {
caseLoadFormsYield(); caseLoadFormsYield();
caseSetEvent(); caseSetEvent();
caseQueryUnloadCancel(); caseQueryUnloadCancel();
caseQueuedOrder();
caseValidateCancel(); caseValidateCancel();
casePopupMenu(); casePopupMenu();
casePropTables(); casePropTables();
caseCommonMethods(); caseCommonMethods();
caseFormShowHide(); caseFormShowHide();
caseUnloadDeferred(); caseUnloadDeferred();
caseUnloadThenLoad();
caseFormVars(); caseFormVars();
caseFormInstances(); caseFormInstances();
caseWithEvents(); caseWithEvents();

View file

@ -156,6 +156,7 @@
#define FIXTURE(name) DVXTEST_FIXTURES_DIR "/" name #define FIXTURE(name) DVXTEST_FIXTURES_DIR "/" name
#define HELP_QUOTE_STR "\"" // DVX_ARG_QUOTE as a string literal #define HELP_QUOTE_STR "\"" // DVX_ARG_QUOTE as a string literal
#define FIX_SAVE FIXTURE("fixSave.so") #define FIX_SAVE FIXTURE("fixSave.so")
#define FIX_SAVE_AS FIXTURE("fixSaveAs.so")
#define FIX_STUCK FIXTURE("fixStuck.so") #define FIX_STUCK FIXTURE("fixStuck.so")
#define APPS_LOG "appsTest.log" // the DVX log (fixProcessInit) #define APPS_LOG "appsTest.log" // the DVX log (fixProcessInit)
#define EXIT_NOTES_MAX 256 #define EXIT_NOTES_MAX 256
@ -424,6 +425,7 @@ static void caseExitCleanApps(void);
static void caseExitDemoPrompt(void); static void caseExitDemoPrompt(void);
static void caseExitDirtyPrompts(void); static void caseExitDirtyPrompts(void);
static void caseExitStuckApp(void); static void caseExitStuckApp(void);
static void caseExitTwoDialogs(void);
static void caseHelpBadFiles(void); static void caseHelpBadFiles(void);
static void caseHelpF1Reuses(void); static void caseHelpF1Reuses(void);
static void caseHelpKeyboard(void); static void caseHelpKeyboard(void);
@ -2314,6 +2316,34 @@ static void caseExitStuckApp(void) {
} }
// An app that answers the close request on its own task with two dialogs
// in a row (a BASIC program's save prompt, then its Save As) has a turn
// with neither open between them: that gap is not a refusal, and the exit
// completes once the second is answered.
static void caseExitTwoDialogs(void) {
tCase("Exit DVX waits through the gap between an app's two dialogs");
fixShellUp(SCREEN_BPP);
fixDialogsOn("Yes");
int32_t pm = loadApp(APP_PROGMAN);
int32_t doc = loadApp(FIX_SAVE_AS);
fixPumpShell(TASK_FRAMES);
TEST_TRUE(pm > 0 && doc > 0);
TEST_EQ(fixAppWindowCount(doc), 1);
sExitNotes[0] = '\0';
fixDialogs = 0;
TEST_TRUE(shellCloseAllApps(tCtx(), pm));
TEST_EQ(fixDialogs, 2);
TEST_STR_EQ(sExitNotes, "saved ");
TEST_PTR_EQ(shellGetApp(doc), NULL);
fixDialogsOff();
fixShellDown();
}
static void caseHelpBadFiles(void) { static void caseHelpBadFiles(void) {
tCase("dvxhelp rejects corrupt help files"); tCase("dvxhelp rejects corrupt help files");
fixShellUp(SCREEN_BPP); fixShellUp(SCREEN_BPP);
@ -5016,6 +5046,7 @@ int main(void) {
RUN_CASE(caseExitDirtyPrompts); RUN_CASE(caseExitDirtyPrompts);
RUN_CASE(caseExitDemoPrompt); RUN_CASE(caseExitDemoPrompt);
RUN_CASE(caseExitStuckApp); RUN_CASE(caseExitStuckApp);
RUN_CASE(caseExitTwoDialogs);
return tSuiteEnd(); return tSuiteEnd();
} }

View file

@ -0,0 +1,109 @@
// 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.
// fixSaveAs.c -- shell suite fixture: a document app whose close request
// is answered on its own task (a main loop), as a BASIC program answers
// one: "Save changes?" (Yes/No/Cancel); on Yes a turn with no dialog open
// (where a BASIC program's slice ends), then a second question standing in
// for Save As, then the window closes. Each outcome is reported through
// fixExitNote when the host defines it.
#include "shellApp.h"
#include "dvxDlg.h"
#include <stdbool.h>
#define FIX_WIN_X 80
#define FIX_WIN_Y 80
#define FIX_WIN_W 200
#define FIX_WIN_H 100
extern void fixExitNote(const char *event) __attribute__((weak));
int32_t appMain(DxeAppContextT *ctx);
static void note(const char *event);
static void onClose(WindowT *win);
static bool sCloseAsked = false;
SHELL_APP_DESCRIPTOR(
.name = "Fix Save As",
.hasMainLoop = true,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
);
static void note(const char *event) {
if (fixExitNote) {
fixExitNote(event);
}
}
// The request is answered on this program's task (appMain's loop).
static void onClose(WindowT *win) {
(void)win;
sCloseAsked = true;
}
int32_t appMain(DxeAppContextT *ctx) {
AppContextT *ac = ctx->shellCtx;
WindowT *win = dvxCreateWindow(ac, "Fix Save As", FIX_WIN_X, FIX_WIN_Y, FIX_WIN_W, FIX_WIN_H, false);
if (!win) {
return -1;
}
win->onClose = onClose;
while (true) {
int32_t answer;
tsYield();
if (!sCloseAsked) {
continue;
}
sCloseAsked = false;
answer = dvxMessageBox(ac, "Fix Save As", "Save changes?", MB_YESNOCANCEL | MB_ICONQUESTION);
if (answer == ID_CANCEL) {
note("cancelled");
continue;
}
if (answer == ID_YES) {
tsYield();
dvxMessageBox(ac, "Fix Save As", "Save as FIX.TXT?", MB_YESNO | MB_ICONQUESTION);
note("saved");
} else {
note("discarded");
}
dvxDestroyWindow(ac, win);
return 0;
}
}

View file

@ -126,6 +126,8 @@ static void testDxeWrite(const char *path, uint32_t init, int32_t cut);
static void testFolders(void); static void testFolders(void);
static void testInfo(void); static void testInfo(void);
static void testInstalledCount(void); static void testInstalledCount(void);
static void testLibraryHook(void *handle);
static void testLibraryLoad(void);
static void testMouseClamp(void); static void testMouseClamp(void);
static void testOutOfMemory(void); static void testOutOfMemory(void);
static void testPalette(void); static void testPalette(void);
@ -142,6 +144,10 @@ static void testVideoInitFailure(void);
static char sLastLog[PLAT_LOG_MAX]; static char sLastLog[PLAT_LOG_MAX];
static int32_t sLogCalls = 0; static int32_t sLogCalls = 0;
// What testLibraryHook was told (platformLibraryLoaded)
static void *sHookHandle = NULL;
static int32_t sHookCalls = 0;
static void bankRun(uint8_t *vram, const uint8_t *src, uint32_t offset, uint32_t bytes, uint32_t gran, uint32_t *winStart, int32_t *moves); static void bankRun(uint8_t *vram, const uint8_t *src, uint32_t offset, uint32_t bytes, uint32_t gran, uint32_t *winStart, int32_t *moves);
static void checkOneSignal(int32_t sig); static void checkOneSignal(int32_t sig);
@ -735,6 +741,35 @@ static void testInstalledCount(void) {
} }
static void testLibraryHook(void *handle) {
sHookHandle = handle;
sHookCalls++;
}
// The loader's news of a library loaded after the start reaches the hook
// with the library's handle (nothing when no hook is set), and a name
// already in the process is found without loading anything.
static void testLibraryLoad(void) {
int32_t marker = 0;
sHookHandle = NULL;
sHookCalls = 0;
platformLibraryLoaded(&marker);
TEST_EQ(sHookCalls, 0);
platformSetLibraryLoadHook(testLibraryHook);
platformLibraryLoaded(&marker);
TEST_EQ(sHookCalls, 1);
TEST_TRUE(sHookHandle == &marker);
platformSetLibraryLoadHook(NULL);
platformLibraryLoaded(&marker);
TEST_EQ(sHookCalls, 1);
TEST_TRUE(platformLoadedSymbol(DVX_SYM("testPlatform")) == (void *)testPlatform);
TEST_PTR_NULL(platformLoadedSymbol(DVX_SYM("noSuchNameAnywhere")));
}
static void testMouseClamp(void) { static void testMouseClamp(void) {
int32_t x = 0; int32_t x = 0;
int32_t y = 0; int32_t y = 0;
@ -869,6 +904,7 @@ void testPlatform(void) {
testFolders(); testFolders();
testDirEntries(); testDirEntries();
testInstalledCount(); testInstalledCount();
testLibraryLoad();
for (int32_t i = 0; i < DVX_ARRAY_LEN(sBpps); i++) { for (int32_t i = 0; i < DVX_ARRAY_LEN(sBpps); i++) {
testPng(sBpps[i]); testPng(sBpps[i]);

View file

@ -59,6 +59,7 @@ typedef struct {
static FocusFixtureT build(const char *title); static FocusFixtureT build(const char *title);
static void caseDisableDropsFocus(void); static void caseDisableDropsFocus(void);
static void caseHiddenFocusedNoKeys(void); static void caseHiddenFocusedNoKeys(void);
static void caseHiddenWindowWaits(void);
static void caseRadioArrowFiresFocus(void); static void caseRadioArrowFiresFocus(void);
static void caseRestoreOnWindowFocus(void); static void caseRestoreOnWindowFocus(void);
static void caseSetVisibleDropsFocus(void); static void caseSetVisibleDropsFocus(void);
@ -154,6 +155,40 @@ static void caseHiddenFocusedNoKeys(void) {
} }
// A window that holds the window manager's focus while it is hidden (a
// BASIC form during its Load) gets no widget focus from a paint until it
// is shown, and then the first widget that can take it then, as in VB
// (Media Player's slider, enabled at the end of its Load).
static void caseHiddenWindowWaits(void) {
tCase("focus: a hidden window takes widget focus only once shown");
// The section's window, focused first; this one takes the focus from it
build("Waits");
WindowT *win = dvxCreateWindow(tCtx(), "Hidden", OTHER_X, OTHER_Y, OTHER_W, OTHER_H, false);
WidgetT *root = wgtInitWindow(tCtx(), win);
WidgetT *first = wgtButton(root, "First");
WidgetT *second = wgtButton(root, "Second");
TEST_TRUE(second != NULL);
win->visible = false;
wgtSetEnabled(first, false);
dvxInvalidateWindow(tCtx(), win);
tPump(WT_SETTLE);
TEST_TRUE(win->focused);
TEST_PTR_EQ(wgtGetFocused(), NULL);
wgtSetEnabled(first, true);
dvxShowWindow(tCtx(), win);
tPump(WT_SETTLE);
TEST_PTR_EQ(wgtGetFocused(), first);
dvxDestroyWindow(tCtx(), win);
tPump(WT_SETTLE);
teardown();
}
// Window blur remembers the focused widget; the next paint of the // Window blur remembers the focused widget; the next paint of the
// re-focused window hands focus back and fires onFocus. // re-focused window hands focus back and fires onFocus.
static void caseRadioArrowFiresFocus(void) { static void caseRadioArrowFiresFocus(void) {
@ -321,4 +356,5 @@ void wtSectionFocus(void) {
caseSetVisibleDropsFocus(); caseSetVisibleDropsFocus();
caseDisableDropsFocus(); caseDisableDropsFocus();
caseRestoreOnWindowFocus(); caseRestoreOnWindowFocus();
caseHiddenWindowWaits();
} }