DVX_GUI/src/test/dialogs/dialogsTest.c
2026-10-13 13:49:12 -05:00

2047 lines
65 KiB
C

// 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.
// dialogsTest.c -- T4: modal dialogs driven through tDialogScript
//
// Every blocking dialog (message/input/int/choice/file) runs its own
// nested dvxUpdate loop, so each case installs an idle script that
// finds the modal window and drives it with queued input. A script
// may pump nested frames (tClick, tType, ...) from inside the idle
// callback; the busy stack below keeps a script from re-entering
// itself for the SAME modal while still letting it answer a nested
// dialog (confirmation boxes opened by the file dialog).
#include "dvxtest.h"
#include "dvxWm.h"
#include "dvxWgtP.h"
#include "box/box.h"
#include "button/button.h"
#include "canvas/canvas.h"
#include "dropdown/dropdown.h"
#include "listBox/listBox.h"
#include "slider/slider.h"
#include "spinner/spinner.h"
#include "textInput/textInpt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/stat.h>
#define SCREEN_W 640
#define SCREEN_H 480
#define SCREEN_W_LARGE 800
#define SCREEN_H_LARGE 600
#define SCREEN_BPP 32
#define SETTLE_FRAMES 3
#define SOUND_LOG_MAX 16
#define CENTER_TOLERANCE 1
#define BUSY_STACK_MAX 8
#define TEXT_MAX_LEN 64
#define SMALL_BUF_LEN 4
#define OUT_PATH_LEN 512
#define CUSTOM_LABEL_W 60
// Message box geometry mirrored from dvxDialog.c (not exported)
#define MSG_PADDING 8
#define MSG_MAX_WIDTH 320
#define MSG_MAX_WIDTH_FRAME (MSG_MAX_WIDTH + CHROME_TOTAL_SIDE * 2)
#define IB_WIDTH_FRAME (300 + CHROME_TOTAL_SIDE * 2) // dvxDialog.c IB_DIALOG_WIDTH + chrome
#define BUTTON_HEIGHT 24
#define LISTBOX_BORDER 2
#define SPINNER_BORDER 2
#define SPINNER_BTN_W 14
// Spinner fixture
#define SPIN_DEFAULT 5
#define SPIN_MIN 0
#define SPIN_MAX 10
#define SPIN_STEP 2
// Choice fixture
#define CHOICE_COUNT 4
#define CHOICE_DEFAULT 1
#define BUTTON_BOX_COUNT 4 // caseButtonBox's labels
#define BUTTON_BOX_ALL 2 // index of "Yes to All"
// Fixture tree entries: "..", "[sub]", a.txt, b.txt, c.bmp
#define FIXTURE_ENTRIES_ALL 5
#define FIXTURE_ENTRIES_TXT 4
#define FIXTURE_ENTRIES_BMP 3
#define FIXTURE_IDX_PARENT 0
#define FIXTURE_IDX_SUB 1
#define FIXTURE_IDX_ATXT 2
#define FIXTURE_SUB_ENTRIES 3 // "..", "[deep]", d.txt
// Color dialog fixture: the root's children are the palette, one row
// (label, slider, number box) per channel, the preview; a basic color's
// cell and where it sits relative to the palette's center.
#define COLOR_ROW_RED 1
#define COLOR_ROW_GREEN 2
#define COLOR_ROW_BLUE 3
#define COLOR_IDX_PREVIEW 4
#define COLOR_IDX_SLIDER 1
#define COLOR_IDX_SPINNER 2
#define COLOR_START 0x804020
#define COLOR_UNCHANGED 0x123456
#define COLOR_SWATCH_DX (-60) // cell (1, 1) center (36, 27) minus the palette center (96, 54)
#define COLOR_SWATCH_DY (-27)
#define COLOR_SWATCH_RGB 0xFFFF00 // basic color at (1, 1)
#define COLOR_GREEN_TEXT "200"
#define COLOR_KEYS_RGB 0xFFC820 // red End -> 255, green typed 200, blue from COLOR_START
#define COLOR_CHANNEL_MAX 255
#define COLOR_SHIFT_R 16
#define COLOR_SHIFT_G 8
#define COLOR_MASK 0xFF
#define BPP_8 8
#define BPP_15 15
#define BPP_16 16
#define SPIN_OVERRANGE_TEXT "999"
#define SPIN_UNDERRANGE_TEXT "-55"
#define BACKSPACE_CLEAR_COUNT 4
#define OTHER_WIN_X 20
#define OTHER_WIN_Y 20
#define OTHER_WIN_W 240
#define OTHER_WIN_H 160
// The runner bakes its own golden directory into libdvxtest.a, so the
// crop name walks from there to this suite's golden/ (see NEEDS in the
// T4 report: a per-suite golden dir setter).
#define GOLDEN_MSGBOX "msgBoxInfo32"
#define DBLCLICK_GAP_MS 10
#define SINGLE_CLICK_GAP_MS (MOUSE_DBLCLICK_DEFAULT_MS + 1)
// ============================================================
// Types
// ============================================================
typedef struct {
int32_t flags;
const char *labels[3];
int32_t ids[3];
int32_t count;
} ButtonSetT;
typedef struct {
const char *button; // caption to click on the outer dialog (NULL = none)
const char *nestedButton; // caption to click on any nested dialog
const char *typeText; // typed into the focused widget first (NULL = none)
bool pressEnter; // press Enter instead of clicking
bool pressEsc; // press Escape instead of clicking
bool closeGadget; // click the title-bar close gadget
bool abandon; // clear ctx->running
char title[MAX_TITLE_LEN];
int32_t calls;
int32_t winX;
int32_t winY;
int32_t winW;
int32_t winH;
int32_t contentH;
WindowT *modalSeen;
WindowT *prevModalAfterNested;
int32_t nestedResult;
char captured[DVX_MAX_PATH];
int32_t capturedInt;
int32_t capturedInt2;
const char *goldenName;
} ScriptT;
typedef struct {
WindowT *win;
WidgetT *btn;
WidgetT *input;
} OtherWindowT;
typedef enum {
ColorOpOkE, // OK as opened
ColorOpCancelE,
ColorOpEscE,
ColorOpSwatchE, // click a basic color, then OK
ColorOpKeysE // End on the focused Red slider, type into the Green box, Enter
} ColorOpE;
typedef struct {
ColorOpE op;
uint32_t start; // the color the dialog was opened with
int32_t calls;
bool previewOk; // the preview showed start
bool redFocused; // the Red slider had the focus
int32_t greenAfter; // Green slider after the typed number (keys op)
} ColorScriptT;
// caseCustom's onOk state: refuses the first `refuse` OKs, then copies
// the input's text.
typedef struct {
WidgetT *input;
int32_t calls;
int32_t refuse;
char text[TEXT_MAX_LEN];
} CustomOkT;
typedef enum {
FileOpCancelE,
FileOpEscE,
FileOpCloseE,
FileOpDblClickSubE,
FileOpTypePathE,
FileOpFilterNextE,
FileOpClickFileOpenE,
FileOpTypeNameE,
FileOpDblClickFileE,
FileOpSelectDirOpenE,
FileOpEnterInNameE,
FileOpDriveE
} FileOpE;
typedef struct {
FileOpE op;
bool acted; // step 0 done; next idle call records and cancels
const char *nestedButton;
const char *text;
char pathAfter[DVX_MAX_PATH];
char nameAfter[DVX_MAX_PATH];
int32_t itemsAfter;
int32_t itemsBefore;
int32_t driveIdx; // FileOpDriveE: the drive to pick (-1: none)
int32_t driveItems; // drive dropdown: its items, first label,
int32_t driveBefore; // and the selection before / after
int32_t driveAfter;
char driveLabel[MAX_TITLE_LEN];
bool stillModalAfter;
bool nestedSeen;
char nestedTitle[MAX_TITLE_LEN];
} FileScriptT;
// ============================================================
// Prototypes
// ============================================================
static void caseAbandon(void);
static void caseButtonBox(void);
static void caseChoice(void);
static void caseColorDialog(void);
static void caseCustom(void);
static void caseErrorInfoBox(void);
static void caseFileDialog(void);
static void caseGeometry(int32_t w, int32_t h);
static void caseGolden(void);
static void caseInputBox(void);
static void caseIntInputBox(void);
static void caseMessageBoxMatrix(void);
static void caseModality(void);
static void caseNested(void);
static void caseNestedSameType(void);
static void casePromptSave(void);
static void caseSounds(void);
static void caseWrapping(void);
static WidgetT *childAt(WidgetT *parent, int32_t idx);
static void clickCloseGadget(WindowT *win);
static void clickListRow(WidgetT *lb, int32_t row, bool dbl);
static void clickSpinnerArrow(WidgetT *sp, bool up);
static void colorScript(AppContextT *ctx, WindowT *modal, void *userCtx);
static void customEnd(WidgetT *w);
static bool customOk(WindowT *dlg, void *userCtx);
static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx);
static void fileSelectRow(WidgetT *lb, int32_t row);
static void fixtureTree(char *dir, int32_t dirSize);
static void nestedSameTypeScript(AppContextT *ctx, WindowT *modal, void *userCtx);
static void nestedScript(AppContextT *ctx, WindowT *modal, void *userCtx);
static OtherWindowT otherWindow(const char *title);
static bool scriptEnter(WindowT *modal);
static void scriptInit(ScriptT *s, const char *button);
static void scriptLeave(void);
static void scriptRecord(ScriptT *s, WindowT *modal);
static void soundRecord(DvxSoundE event, bool wait);
static void spinInit(ScriptT *s, const char *button);
static void spinScript(AppContextT *ctx, WindowT *modal, void *userCtx);
static void uniformScript(AppContextT *ctx, WindowT *modal, void *userCtx);
// ============================================================
// Module state
// ============================================================
static WindowT *sBusy[BUSY_STACK_MAX];
static int32_t sBusyDepth = 0;
static const ButtonSetT sButtonSets[] = {
{ MB_OK, { "OK", NULL, NULL }, { ID_OK, 0, 0 }, 1 },
{ MB_OKCANCEL, { "OK", "Cancel", NULL }, { ID_OK, ID_CANCEL, 0 }, 2 },
{ MB_YESNO, { "Yes", "No", NULL }, { ID_YES, ID_NO, 0 }, 2 },
{ MB_YESNOCANCEL, { "Yes", "No", "Cancel" }, { ID_YES, ID_NO, ID_CANCEL }, 3 },
{ MB_RETRYCANCEL, { "Retry", "Cancel", NULL }, { ID_RETRY, ID_CANCEL, 0 }, 2 },
};
static const int32_t sIcons[] = { 0, MB_ICONINFO, MB_ICONWARNING, MB_ICONERROR, MB_ICONQUESTION };
// The desktop sound of a message box with each of sIcons
static const DvxSoundE sIconSounds[] = { SoundDefaultE, SoundInfoE, SoundWarningE, SoundErrorE, SoundQuestionE };
// Desktop sounds asked for (the recording sound hook)
static DvxSoundE sSoundLog[SOUND_LOG_MAX];
static bool sSoundWait[SOUND_LOG_MAX];
static int32_t sSoundCount = 0;
static const char *sChoices[CHOICE_COUNT] = { "Alpha", "Beta", "Gamma", "Delta" };
static const FileFilterT sFiltersTxt[] = { { "Text Files (*.txt)" }, { "All Files (*.*)" } };
static const FileFilterT sFiltersMulti[] = { { "Text and Images (*.txt;*.bmp)" }, { "Bitmaps (*.bmp)" } };
// ============================================================
// Cases
// ============================================================
static void caseAbandon(void) {
tCase("abandon mid-loop leaves no state behind");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
scriptInit(&s, NULL);
s.abandon = true;
tDialogScript(uniformScript, &s);
int32_t result = dvxMessageBox(tCtx(), "Abandon", "Going away", MB_OKCANCEL);
TEST_EQ(result, ID_CANCEL);
TEST_TRUE(!tCtx()->running);
TEST_PTR_EQ(tCtx()->modalWindow, NULL);
tCtx()->running = true;
tPump(1);
TEST_PTR_EQ(tFindWindow("Abandon"), NULL);
TEST_EQ(tCtx()->stack.count, 0);
// A fresh dialog must work normally.
scriptInit(&s, "OK");
result = dvxMessageBox(tCtx(), "After", "Back again", MB_OKCANCEL);
TEST_EQ(result, ID_OK);
// Abandon a file dialog too (it owns heap entries).
char dir[DVX_MAX_PATH];
char out[OUT_PATH_LEN];
fixtureTree(dir, sizeof(dir));
scriptInit(&s, NULL);
s.abandon = true;
TEST_TRUE(!dvxFileDialog(tCtx(), "Gone", FD_OPEN, dir, sFiltersTxt, 2, out, sizeof(out)));
tCtx()->running = true;
tPump(1);
TEST_PTR_EQ(tFindWindow("Gone"), NULL);
tShutdown();
}
// Caller's buttons: the index of the one clicked, Enter the first,
// Escape and the close gadget the last; bad arguments give -1.
static void caseButtonBox(void) {
static const char *const labels[BUTTON_BOX_COUNT] = { "&Yes", "&No", "Yes to &All", "&Cancel" };
tCase("button box");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
scriptInit(&s, "Yes to All");
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "Replace it?", MB_ICONQUESTION, labels, BUTTON_BOX_COUNT), BUTTON_BOX_ALL);
TEST_STR_EQ(s.title, "Ask");
scriptInit(&s, NULL);
s.pressEnter = true;
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "Replace it?", 0, labels, BUTTON_BOX_COUNT), 0);
scriptInit(&s, NULL);
s.pressEsc = true;
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "Replace it?", 0, labels, BUTTON_BOX_COUNT), BUTTON_BOX_COUNT - 1);
scriptInit(&s, NULL);
s.closeGadget = true;
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "Replace it?", 0, labels, BUTTON_BOX_COUNT), BUTTON_BOX_COUNT - 1);
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "m", 0, labels, 0), -1);
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "m", 0, labels, DVX_BUTTON_BOX_MAX + 1), -1);
TEST_EQ(dvxButtonBox(tCtx(), "Ask", NULL, 0, labels, 1), -1);
TEST_EQ(dvxButtonBox(tCtx(), "Ask", "m", 0, NULL, 1), -1);
TEST_EQ(dvxButtonBox(NULL, "Ask", "m", 0, labels, 1), -1);
tDialogScript(NULL, NULL);
tPump(1);
TEST_PTR_EQ(tFindWindow("Ask"), NULL);
tShutdown();
}
static void caseChoice(void) {
tCase("choice dialog");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
int32_t idx = -1;
// Default index is highlighted and returned by OK.
scriptInit(&s, "OK");
TEST_TRUE(dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, CHOICE_DEFAULT, &idx));
TEST_EQ(idx, CHOICE_DEFAULT);
TEST_EQ(s.capturedInt, CHOICE_DEFAULT);
// Keyboard: Down twice from the default then OK.
scriptInit(&s, "OK");
s.capturedInt2 = 2;
idx = -1;
TEST_TRUE(dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, CHOICE_DEFAULT, &idx));
TEST_EQ(idx, CHOICE_DEFAULT + 2);
// Enter on the list accepts the highlighted item.
scriptInit(&s, NULL);
s.pressEnter = true;
idx = -1;
TEST_TRUE(dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, 0, &idx));
TEST_EQ(idx, 0);
// Double-click on the last row accepts it.
scriptInit(&s, NULL);
s.capturedInt2 = -(CHOICE_COUNT - 1) - 1;
idx = -1;
TEST_TRUE(dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, 0, &idx));
TEST_EQ(idx, CHOICE_COUNT - 1);
// Cancel, Escape and the close gadget all reject.
idx = 7;
scriptInit(&s, "Cancel");
TEST_TRUE(!dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, CHOICE_DEFAULT, &idx));
TEST_EQ(idx, 7);
scriptInit(&s, NULL);
s.pressEsc = true;
TEST_TRUE(!dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, CHOICE_DEFAULT, &idx));
scriptInit(&s, NULL);
s.closeGadget = true;
TEST_TRUE(!dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, CHOICE_DEFAULT, &idx));
TEST_EQ(idx, 7);
// defaultIdx -1: the list box highlights its first item anyway.
scriptInit(&s, "OK");
TEST_TRUE(dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, -1, &idx));
TEST_EQ(idx, 0);
TEST_EQ(s.capturedInt, 0);
// Bad arguments
TEST_TRUE(!dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, 0, 0, &idx));
TEST_TRUE(!dvxChoiceDialog(tCtx(), "Pick", "Pick one:", NULL, CHOICE_COUNT, 0, &idx));
TEST_TRUE(!dvxChoiceDialog(tCtx(), "Pick", "Pick one:", sChoices, CHOICE_COUNT, 0, NULL));
tPump(1);
TEST_PTR_EQ(tFindWindow("Pick"), NULL);
tShutdown();
}
// Custom dialogs: onOk may refuse (the dialog stays up and is answered
// again), sees the live widgets when it accepts, and is never called on
// Escape, Cancel or close; NULL onOk accepts at once.
static void caseColorDialog(void) {
tCase("color dialog: palette, sliders, number boxes, OK/Cancel at 8/15/16/32 bpp");
const int32_t bpps[] = { BPP_8, BPP_15, BPP_16, SCREEN_BPP };
for (int32_t i = 0; i < DVX_ARRAY_LEN(bpps); i++) {
tInit(SCREEN_W, SCREEN_H, bpps[i]);
ColorScriptT cs;
uint32_t rgb = COLOR_START;
memset(&cs, 0, sizeof(cs));
cs.op = ColorOpOkE;
cs.start = rgb;
tDialogScript(colorScript, &cs);
TEST_TRUE(dvxColorDialog(tCtx(), NULL, &rgb));
TEST_EQ(rgb, COLOR_START);
TEST_TRUE(cs.previewOk);
TEST_TRUE(cs.redFocused);
tShutdown();
}
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ColorScriptT cs;
uint32_t rgb = COLOR_START;
// A basic color picked with the mouse.
memset(&cs, 0, sizeof(cs));
cs.op = ColorOpSwatchE;
cs.start = rgb;
tDialogScript(colorScript, &cs);
TEST_TRUE(dvxColorDialog(tCtx(), "Pick", &rgb));
TEST_EQ(rgb, COLOR_SWATCH_RGB);
// Keyboard: End on the Red slider, a typed Green value, Enter.
rgb = COLOR_START;
memset(&cs, 0, sizeof(cs));
cs.op = ColorOpKeysE;
cs.start = rgb;
tDialogScript(colorScript, &cs);
TEST_TRUE(dvxColorDialog(tCtx(), "Pick", &rgb));
TEST_EQ(rgb, COLOR_KEYS_RGB);
TEST_EQ(cs.greenAfter, (COLOR_KEYS_RGB >> COLOR_SHIFT_G) & COLOR_MASK);
// Cancel and Escape leave the color alone.
rgb = COLOR_UNCHANGED;
memset(&cs, 0, sizeof(cs));
cs.op = ColorOpCancelE;
cs.start = rgb;
tDialogScript(colorScript, &cs);
TEST_TRUE(!dvxColorDialog(tCtx(), "Pick", &rgb));
memset(&cs, 0, sizeof(cs));
cs.op = ColorOpEscE;
cs.start = rgb;
tDialogScript(colorScript, &cs);
TEST_TRUE(!dvxColorDialog(tCtx(), "Pick", &rgb));
TEST_EQ(rgb, COLOR_UNCHANGED);
TEST_TRUE(!dvxColorDialog(tCtx(), "Pick", NULL));
TEST_TRUE(!dvxColorDialog(NULL, "Pick", &rgb));
tDialogScript(NULL, NULL);
tPump(1);
TEST_PTR_EQ(tFindWindow("Pick"), NULL);
tShutdown();
}
static void caseCustom(void) {
tCase("custom dialog");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
CustomOkT ok;
for (int32_t pass = 0; pass < 4; pass++) {
WindowT *dlg = dvxDialogCreate(tCtx(), "Custom", IB_WIDTH_FRAME);
TEST_TRUE(dlg != NULL);
if (!dlg) {
break;
}
memset(&ok, 0, sizeof(ok));
ok.input = dvxDialogAddField(dlg, "Name:", CUSTOM_LABEL_W, TEXT_MAX_LEN - 1, "value");
ok.refuse = 1;
TEST_TRUE(ok.input != NULL && tFindByText(dlg->widgetRoot, "Name:") == ok.input->parent->firstChild);
dvxDialogAddOkCancel(dlg, NULL, customOk, &ok);
switch (pass) {
case 0:
scriptInit(&s, "OK");
s.nestedButton = "OK";
TEST_TRUE(dvxDialogRun(tCtx(), dlg));
TEST_EQ(ok.calls, 2);
TEST_STR_EQ(ok.text, "value");
TEST_STR_EQ(s.title, "Custom");
break;
case 1:
scriptInit(&s, NULL);
s.pressEsc = true;
TEST_TRUE(!dvxDialogRun(tCtx(), dlg));
TEST_EQ(ok.calls, 0);
break;
case 2:
scriptInit(&s, "Cancel");
TEST_TRUE(!dvxDialogRun(tCtx(), dlg));
TEST_EQ(ok.calls, 0);
break;
default:
scriptInit(&s, NULL);
s.closeGadget = true;
TEST_TRUE(!dvxDialogRun(tCtx(), dlg));
TEST_EQ(ok.calls, 0);
break;
}
}
// No onOk: OK accepts on the first press.
WindowT *plain = dvxDialogCreate(tCtx(), NULL, IB_WIDTH_FRAME);
TEST_TRUE(plain != NULL);
dvxDialogAddOkCancel(plain, "&Go", NULL, NULL);
scriptInit(&s, "Go");
TEST_TRUE(dvxDialogRun(tCtx(), plain));
// dvxDialogEnd from the caller's own button: accepting runs onOk,
// ending unaccepted does not.
for (int32_t accept = 0; accept < 2; accept++) {
WindowT *own = dvxDialogCreate(tCtx(), "Own", IB_WIDTH_FRAME);
TEST_TRUE(own != NULL);
if (!own) {
break;
}
memset(&ok, 0, sizeof(ok));
ok.input = dvxDialogAddField(own, "Name:", CUSTOM_LABEL_W, TEXT_MAX_LEN - 1, "own");
WidgetT *endBtn = wgtButton(own->widgetRoot, accept ? "&End" : "&Drop");
endBtn->userData = accept ? (void *)&ok : NULL;
endBtn->onClick = customEnd;
dvxDialogAddOkCancel(own, NULL, customOk, &ok);
scriptInit(&s, accept ? "End" : "Drop");
TEST_EQ(dvxDialogRun(tCtx(), own), accept == 1);
TEST_EQ(ok.calls, accept);
TEST_STR_EQ(ok.text, accept ? "own" : "");
}
dvxDialogEnd(NULL, true);
// Bad arguments are inert.
TEST_PTR_EQ(dvxDialogCreate(NULL, "x", IB_WIDTH_FRAME), NULL);
TEST_PTR_EQ(dvxDialogAddField(NULL, "x", CUSTOM_LABEL_W, TEXT_MAX_LEN - 1, NULL), NULL);
TEST_TRUE(!dvxDialogRun(tCtx(), NULL));
dvxDialogAddOkCancel(NULL, NULL, NULL, NULL);
tDialogScript(NULL, NULL);
tShutdown();
}
static void caseErrorInfoBox(void) {
tCase("error/info box titles");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
scriptInit(&s, "OK");
TEST_EQ(dvxErrorBox(tCtx(), NULL, "Boom"), ID_OK);
TEST_STR_EQ(s.title, "Error");
scriptInit(&s, "OK");
TEST_EQ(dvxErrorBox(tCtx(), "Custom", "Boom"), ID_OK);
TEST_STR_EQ(s.title, "Custom");
scriptInit(&s, "OK");
TEST_EQ(dvxInfoBox(tCtx(), NULL, "FYI"), ID_OK);
TEST_STR_EQ(s.title, "Information");
scriptInit(&s, "OK");
TEST_EQ(dvxInfoBox(tCtx(), "Note", "FYI"), ID_OK);
TEST_STR_EQ(s.title, "Note");
// Only one button exists; Cancel is absent.
scriptInit(&s, "Cancel");
s.nestedButton = "OK";
TEST_EQ(dvxInfoBox(tCtx(), "Note", "FYI"), ID_OK);
// NULL message / ctx are rejected without a window.
TEST_EQ(dvxMessageBox(tCtx(), "X", NULL, MB_OK), ID_CANCEL);
TEST_EQ(dvxMessageBox(NULL, "X", "m", MB_OK), ID_CANCEL);
tShutdown();
}
static void caseFileDialog(void) {
tCase("file dialog");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
char dir[DVX_MAX_PATH];
char sub[DVX_MAX_PATH];
char expect[OUT_PATH_LEN];
char out[OUT_PATH_LEN];
FileScriptT f;
char real[PATH_MAX];
fixtureTree(dir, sizeof(dir));
if (!realpath(dir, real)) {
snprintf(real, sizeof(real), "%s", dir);
}
snprintf(sub, sizeof(sub), "%s/sub", real);
tDialogScript(fileScript, &f);
// Cancel / Escape / close gadget reject and leave outPath alone.
memset(&f, 0, sizeof(f));
f.op = FileOpCancelE;
strcpy(out, "untouched");
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(out, "untouched");
TEST_EQ(f.itemsBefore, FIXTURE_ENTRIES_ALL);
TEST_STR_EQ(f.pathAfter, real);
memset(&f, 0, sizeof(f));
f.op = FileOpEscE;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
memset(&f, 0, sizeof(f));
f.op = FileOpCloseE;
TEST_TRUE(!dvxFileDialog(tCtx(), NULL, FD_OPEN, dir, NULL, 0, out, sizeof(out)));
tPump(1);
TEST_PTR_EQ(tFindWindow("Open"), NULL);
// Default titles
memset(&f, 0, sizeof(f));
f.op = FileOpCancelE;
TEST_TRUE(!dvxFileDialog(tCtx(), NULL, FD_SAVE, dir, NULL, 0, out, sizeof(out)));
TEST_PTR_EQ(tFindWindow("Save As"), NULL);
// Double-click on "[sub]" navigates; the dialog stays up.
memset(&f, 0, sizeof(f));
f.op = FileOpDblClickSubE;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(f.pathAfter, sub);
TEST_TRUE(f.stillModalAfter);
TEST_EQ(f.itemsAfter, FIXTURE_SUB_ENTRIES);
// Typing a path + Enter in the path input navigates.
memset(&f, 0, sizeof(f));
f.op = FileOpTypePathE;
f.text = sub;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(f.pathAfter, sub);
TEST_TRUE(f.stillModalAfter);
// Drives: the host has one, "/", selected for the folder; picking it
// goes to its root, picking none does nothing.
memset(&f, 0, sizeof(f));
f.op = FileOpDriveE;
f.driveIdx = 0;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_EQ(f.driveItems, 1);
TEST_STR_EQ(f.driveLabel, "/");
TEST_EQ(f.driveBefore, 0);
TEST_EQ(f.driveAfter, 0);
TEST_STR_EQ(f.pathAfter, "/");
TEST_TRUE(f.stillModalAfter);
memset(&f, 0, sizeof(f));
f.op = FileOpDriveE;
f.driveIdx = -1;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(f.pathAfter, real);
TEST_EQ(f.driveAfter, -1);
// Filters: single glob, then switching to the next filter.
memset(&f, 0, sizeof(f));
f.op = FileOpFilterNextE;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, sFiltersTxt, 2, out, sizeof(out)));
TEST_EQ(f.itemsBefore, FIXTURE_ENTRIES_TXT);
TEST_EQ(f.itemsAfter, FIXTURE_ENTRIES_ALL);
memset(&f, 0, sizeof(f));
f.op = FileOpFilterNextE;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, sFiltersMulti, 2, out, sizeof(out)));
TEST_EQ(f.itemsBefore, FIXTURE_ENTRIES_ALL);
TEST_EQ(f.itemsAfter, FIXTURE_ENTRIES_BMP);
// Open: click a.txt (name input follows), then Open.
memset(&f, 0, sizeof(f));
f.op = FileOpClickFileOpenE;
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/a.txt", dir);
TEST_STR_EQ(out, expect);
TEST_STR_EQ(f.nameAfter, "a.txt");
// Double-click on a file accepts it.
memset(&f, 0, sizeof(f));
f.op = FileOpDblClickFileE;
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(out, expect);
// Enter in the filename input is the default button.
memset(&f, 0, sizeof(f));
f.op = FileOpEnterInNameE;
f.text = "b.txt";
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/b.txt", dir);
TEST_STR_EQ(out, expect);
// Open of a missing file asks to create it: Yes accepts, No stays.
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "zz.txt";
f.nestedButton = "Yes";
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/zz.txt", dir);
TEST_STR_EQ(out, expect);
TEST_STR_EQ(f.nestedTitle, "Confirm Create");
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "zz.txt";
f.nestedButton = "No";
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_TRUE(f.stillModalAfter);
TEST_TRUE(f.nestedSeen);
// Save over an existing file asks to replace it.
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "a.txt";
f.nestedButton = "Yes";
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, NULL, 0, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/a.txt", dir);
TEST_STR_EQ(out, expect);
TEST_STR_EQ(f.nestedTitle, "Confirm Save");
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "a.txt";
f.nestedButton = "No";
TEST_TRUE(!dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, NULL, 0, out, sizeof(out)));
TEST_TRUE(f.stillModalAfter);
// Save of a new name without extension appends the filter's extension.
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "fresh";
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, sFiltersTxt, 2, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/fresh.txt", dir);
TEST_STR_EQ(out, expect);
TEST_TRUE(!f.nestedSeen);
// Save with a wildcard filter leaves the name alone.
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "plain";
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, &sFiltersTxt[1], 1, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/plain", dir);
TEST_STR_EQ(out, expect);
// A name inside a missing folder: error box, then the dialog is
// still up.
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "bad/name";
f.nestedButton = "OK";
TEST_TRUE(!dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(f.nestedTitle, "Invalid Path");
TEST_TRUE(f.stillModalAfter);
// A name with a folder part navigates there and accepts the bare
// name in it (it used to be rejected as an invalid filename).
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "sub/new.txt";
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, NULL, 0, out, sizeof(out)));
snprintf(expect, sizeof(expect), "%s/sub/new.txt", dir);
TEST_STR_EQ(out, expect);
TEST_TRUE(!f.nestedSeen);
// An absolute path in the name box works the same way.
char absName[DVX_MAX_PATH];
snprintf(absName, sizeof(absName), "%s/sub/abs.txt", dir);
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = absName;
out[0] = '\0';
TEST_TRUE(dvxFileDialog(tCtx(), "Save It", FD_SAVE, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(out, absName);
// Selecting a directory and pressing Open navigates, never accepts.
memset(&f, 0, sizeof(f));
f.op = FileOpSelectDirOpenE;
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(f.pathAfter, sub);
TEST_TRUE(f.stillModalAfter);
// Typing a directory name and pressing Open navigates too.
memset(&f, 0, sizeof(f));
f.op = FileOpTypeNameE;
f.text = "sub";
TEST_TRUE(!dvxFileDialog(tCtx(), "Open It", FD_OPEN, dir, NULL, 0, out, sizeof(out)));
TEST_STR_EQ(f.pathAfter, sub);
TEST_TRUE(f.stillModalAfter);
TEST_TRUE(!f.nestedSeen);
// Bad arguments
TEST_TRUE(!dvxFileDialog(tCtx(), "X", FD_OPEN, dir, NULL, 0, NULL, 0));
TEST_TRUE(!dvxFileDialog(tCtx(), "X", FD_OPEN, dir, NULL, 0, out, 0));
tPump(1);
TEST_PTR_EQ(tFindWindow("Open It"), NULL);
TEST_PTR_EQ(tFindWindow("Save It"), NULL);
TEST_PTR_EQ(tCtx()->modalWindow, NULL);
tShutdown();
}
static void caseGeometry(int32_t w, int32_t h) {
tCase("dialogs are centered");
tInit(w, h, SCREEN_BPP);
ScriptT s;
char dir[DVX_MAX_PATH];
char out[OUT_PATH_LEN];
char buf[TEXT_MAX_LEN];
int32_t val = 0;
int32_t idx = 0;
fixtureTree(dir, sizeof(dir));
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Geo", "Centered?", MB_OK | MB_ICONINFO);
TEST_TRUE(abs(s.winX + s.winW / 2 - w / 2) <= CENTER_TOLERANCE);
TEST_TRUE(abs(s.winY + s.winH / 2 - h / 2) <= CENTER_TOLERANCE);
scriptInit(&s, "Cancel");
dvxInputBox(tCtx(), "Geo", "Prompt", NULL, buf, sizeof(buf));
TEST_TRUE(abs(s.winX + s.winW / 2 - w / 2) <= CENTER_TOLERANCE);
TEST_TRUE(abs(s.winY + s.winH / 2 - h / 2) <= CENTER_TOLERANCE);
scriptInit(&s, "Cancel");
dvxIntInputBox(tCtx(), "Geo", "Prompt", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val);
TEST_TRUE(abs(s.winX + s.winW / 2 - w / 2) <= CENTER_TOLERANCE);
TEST_TRUE(abs(s.winY + s.winH / 2 - h / 2) <= CENTER_TOLERANCE);
scriptInit(&s, "Cancel");
dvxChoiceDialog(tCtx(), "Geo", "Prompt", sChoices, CHOICE_COUNT, 0, &idx);
TEST_TRUE(abs(s.winX + s.winW / 2 - w / 2) <= CENTER_TOLERANCE);
TEST_TRUE(abs(s.winY + s.winH / 2 - h / 2) <= CENTER_TOLERANCE);
scriptInit(&s, "Cancel");
dvxFileDialog(tCtx(), "Geo", FD_OPEN, dir, NULL, 0, out, sizeof(out));
TEST_TRUE(abs(s.winX + s.winW / 2 - w / 2) <= CENTER_TOLERANCE);
TEST_TRUE(abs(s.winY + s.winH / 2 - h / 2) <= CENTER_TOLERANCE);
tShutdown();
}
static void caseGolden(void) {
tCase("message box golden crop (32bpp)");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
scriptInit(&s, "OK");
s.goldenName = GOLDEN_MSGBOX;
TEST_EQ(dvxMessageBox(tCtx(), "Golden", "Golden crop of a message box.", MB_OKCANCEL | MB_ICONINFO), ID_OK);
TEST_TRUE(s.calls >= 1);
tShutdown();
}
static void caseInputBox(void) {
tCase("input box");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
char buf[TEXT_MAX_LEN];
char small[SMALL_BUF_LEN];
// Default text is returned untouched by OK.
scriptInit(&s, "OK");
strcpy(buf, "old");
TEST_TRUE(dvxInputBox(tCtx(), "Ask", "Name?", "default", buf, sizeof(buf)));
TEST_STR_EQ(buf, "default");
TEST_STR_EQ(s.captured, "default");
// No default: empty string on OK.
scriptInit(&s, "OK");
strcpy(buf, "old");
TEST_TRUE(dvxInputBox(tCtx(), "Ask", "Name?", NULL, buf, sizeof(buf)));
TEST_STR_EQ(buf, "");
// Typed text (the input has focus) is appended and accepted by OK.
scriptInit(&s, "OK");
s.typeText = " Jr.";
TEST_TRUE(dvxInputBox(tCtx(), "Ask", "Name?", "Scott", buf, sizeof(buf)));
TEST_STR_EQ(buf, "Scott Jr.");
// Enter accepts, Escape cancels, close gadget cancels.
scriptInit(&s, NULL);
s.typeText = "typed";
s.pressEnter = true;
TEST_TRUE(dvxInputBox(tCtx(), "Ask", "Name?", NULL, buf, sizeof(buf)));
TEST_STR_EQ(buf, "typed");
scriptInit(&s, NULL);
s.typeText = "lost";
s.pressEsc = true;
strcpy(buf, "kept");
TEST_TRUE(!dvxInputBox(tCtx(), "Ask", "Name?", NULL, buf, sizeof(buf)));
TEST_STR_EQ(buf, "kept");
scriptInit(&s, "Cancel");
TEST_TRUE(!dvxInputBox(tCtx(), "Ask", "Name?", "x", buf, sizeof(buf)));
TEST_STR_EQ(buf, "kept");
scriptInit(&s, NULL);
s.closeGadget = true;
TEST_TRUE(!dvxInputBox(tCtx(), "Ask", "Name?", "x", buf, sizeof(buf)));
TEST_STR_EQ(buf, "kept");
// Buffer bound: result is truncated and terminated.
scriptInit(&s, "OK");
memset(small, 'Z', sizeof(small));
TEST_TRUE(dvxInputBox(tCtx(), "Ask", "Name?", "longer than four", small, sizeof(small)));
TEST_STR_EQ(small, "lon");
// Default title when NULL
scriptInit(&s, "Cancel");
dvxInputBox(tCtx(), NULL, "Name?", NULL, buf, sizeof(buf));
TEST_STR_EQ(s.title, "Input");
// Bad arguments
TEST_TRUE(!dvxInputBox(tCtx(), "Ask", "Name?", NULL, NULL, 0));
TEST_TRUE(!dvxInputBox(tCtx(), "Ask", "Name?", NULL, buf, 0));
tPump(1);
TEST_PTR_EQ(tFindWindow("Ask"), NULL);
tShutdown();
}
static void caseIntInputBox(void) {
tCase("integer input box");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
int32_t val = -1;
// Default value through OK.
spinInit(&s, "OK");
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, SPIN_DEFAULT);
// Up arrow twice (step 2), then the value is clamped at max on the third.
spinInit(&s, "OK");
s.capturedInt = 3;
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, SPIN_MAX);
spinInit(&s, "OK");
s.capturedInt = 1;
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, SPIN_DEFAULT + SPIN_STEP);
// Down arrows clamp at min.
spinInit(&s, "OK");
s.capturedInt = -4;
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, SPIN_MIN);
// Keyboard Up/Down on the focused spinner.
spinInit(&s, "OK");
s.capturedInt2 = 1;
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, SPIN_DEFAULT + SPIN_STEP);
// Typed out-of-range values are clamped on Enter (commit + default OK).
spinInit(&s, NULL);
s.typeText = SPIN_OVERRANGE_TEXT;
s.pressEnter = true;
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, SPIN_MAX);
spinInit(&s, "OK");
s.typeText = SPIN_UNDERRANGE_TEXT;
TEST_TRUE(dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, -SPIN_MAX, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, -SPIN_MAX);
// Cancel / Escape / close leave the output alone.
val = 42;
spinInit(&s, "Cancel");
TEST_TRUE(!dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
spinInit(&s, NULL);
s.pressEsc = true;
TEST_TRUE(!dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
spinInit(&s, NULL);
s.closeGadget = true;
TEST_TRUE(!dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, &val));
TEST_EQ(val, 42);
TEST_TRUE(!dvxIntInputBox(tCtx(), "Int", "How many?", SPIN_DEFAULT, SPIN_MIN, SPIN_MAX, SPIN_STEP, NULL));
tPump(1);
TEST_PTR_EQ(tFindWindow("Int"), NULL);
tShutdown();
}
static void caseMessageBoxMatrix(void) {
tCase("message box button sets x icons");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
int32_t setCount = DVX_ARRAY_LEN(sButtonSets);
int32_t iconCount = DVX_ARRAY_LEN(sIcons);
for (int32_t si = 0; si < setCount; si++) {
const ButtonSetT *set = &sButtonSets[si];
for (int32_t ii = 0; ii < iconCount; ii++) {
int32_t flags = set->flags | sIcons[ii];
for (int32_t b = 0; b < set->count; b++) {
scriptInit(&s, set->labels[b]);
TEST_EQ(dvxMessageBox(tCtx(), "Matrix", "Pick a button", flags), set->ids[b]);
TEST_EQ(s.calls, 1);
TEST_STR_EQ(s.title, "Matrix");
}
// Enter activates the first (default) button.
scriptInit(&s, NULL);
s.pressEnter = true;
TEST_EQ(dvxMessageBox(tCtx(), "Matrix", "Enter", flags), set->ids[0]);
// Escape activates the last button (OK / Cancel / No).
scriptInit(&s, NULL);
s.pressEsc = true;
TEST_EQ(dvxMessageBox(tCtx(), "Matrix", "Escape", flags), set->ids[set->count - 1]);
// The close gadget always reports cancel.
scriptInit(&s, NULL);
s.closeGadget = true;
TEST_EQ(dvxMessageBox(tCtx(), "Matrix", "Close", flags), ID_CANCEL);
}
}
// Unknown button nibble falls back to a lone OK.
scriptInit(&s, "OK");
TEST_EQ(dvxMessageBox(tCtx(), "Matrix", "Odd", MB_BUTTON_MASK), ID_OK);
tPump(1);
TEST_PTR_EQ(tFindWindow("Matrix"), NULL);
TEST_EQ(tCtx()->stack.count, 0);
tShutdown();
}
static void caseModality(void) {
tCase("modal input gate and focus return");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
OtherWindowT o = otherWindow("Other");
TSpyT spy;
ScriptT s;
tSpyInit(&spy);
tSpyWidget(&spy, o.btn, "onClick");
tSpyWindow(&spy, o.win, "onFocus");
tSpyWindow(&spy, o.win, "onBlur");
TEST_TRUE(o.win->focused);
scriptInit(&s, "OK");
s.modalSeen = o.win; // script pokes this window while the modal is up
TEST_EQ(dvxMessageBox(tCtx(), "Modal", "Wait", MB_OK), ID_OK);
// Nothing reached the other window: no click, no typed text, no raise.
TEST_EQ(tSpyCount(&spy, "onClick"), 0);
TEST_STR_EQ(wgtGetText(o.input), "");
TEST_EQ(s.capturedInt, 1); // modal stayed on top after the clicks
TEST_EQ(s.capturedInt2, 1); // modal stayed focused after the clicks
// Focus came back to the previous window once the dialog closed.
tPump(1);
TEST_TRUE(o.win->focused);
TEST_EQ(tSpyCount(&spy, "onBlur"), 1);
TEST_EQ(tSpyCount(&spy, "onFocus"), 1);
TEST_PTR_EQ(tCtx()->modalWindow, NULL);
// With the dialog gone, the same clicks work again.
int32_t sx = 0;
int32_t sy = 0;
tWidgetCenter(o.btn, &sx, &sy);
tClick(sx, sy);
TEST_EQ(tSpyCount(&spy, "onClick"), 1);
tSpyFree(&spy);
tShutdown();
}
static void caseNested(void) {
tCase("nested dialog restores the previous modal");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
char buf[TEXT_MAX_LEN];
scriptInit(&s, "OK");
tDialogScript(nestedScript, &s);
TEST_TRUE(dvxInputBox(tCtx(), "Outer", "Name?", "outer", buf, sizeof(buf)));
TEST_STR_EQ(buf, "outer");
TEST_EQ(s.nestedResult, ID_YES);
TEST_PTR_EQ(s.prevModalAfterNested, s.modalSeen);
TEST_PTR_EQ(tCtx()->modalWindow, NULL);
tPump(1);
TEST_PTR_EQ(tFindWindow("Outer"), NULL);
TEST_PTR_EQ(tFindWindow("Inner"), NULL);
tShutdown();
}
// Two message boxes alive at once (a second dvxMessageBox while the
// first one's loop runs) keep separate state: the inner answer must not
// end or overwrite the outer box.
static void caseNestedSameType(void) {
tCase("nested message boxes keep separate state");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
scriptInit(&s, NULL);
tDialogScript(nestedSameTypeScript, &s);
int32_t outer = dvxMessageBox(tCtx(), "Outer", "Outer text", MB_OKCANCEL);
TEST_EQ(s.nestedResult, ID_YES);
TEST_EQ(outer, ID_CANCEL);
TEST_EQ(s.calls, 2);
TEST_PTR_EQ(tCtx()->modalWindow, NULL);
tPump(1);
TEST_PTR_EQ(tFindWindow("Outer"), NULL);
tShutdown();
}
static void casePromptSave(void) {
tCase("prompt save");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
scriptInit(&s, "Yes");
TEST_EQ(dvxPromptSave(tCtx(), "Doc"), DVX_SAVE_YES);
TEST_STR_EQ(s.title, "Doc");
scriptInit(&s, "No");
TEST_EQ(dvxPromptSave(tCtx(), "Doc"), DVX_SAVE_NO);
scriptInit(&s, "Cancel");
TEST_EQ(dvxPromptSave(tCtx(), NULL), DVX_SAVE_CANCEL);
TEST_STR_EQ(s.title, "Save");
scriptInit(&s, NULL);
s.pressEsc = true;
TEST_EQ(dvxPromptSave(tCtx(), "Doc"), DVX_SAVE_CANCEL);
scriptInit(&s, NULL);
s.pressEnter = true;
TEST_EQ(dvxPromptSave(tCtx(), "Doc"), DVX_SAVE_YES);
scriptInit(&s, NULL);
s.closeGadget = true;
TEST_EQ(dvxPromptSave(tCtx(), "Doc"), DVX_SAVE_CANCEL);
tShutdown();
}
// Message boxes play their icon's sound (dialogs no window sounds);
// windows play open (a frame later) and close; the API calls reach the
// hook.
static void caseSounds(void) {
tCase("desktop sounds: message boxes, windows, dvxBeep");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
dvxSetSoundHook(soundRecord);
ScriptT s;
for (int32_t i = 0; i < DVX_ARRAY_LEN(sIcons); i++) {
sSoundCount = 0;
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Sounds", "Listen", MB_OK | sIcons[i]);
tPump(1);
TEST_EQ(sSoundCount, 1);
TEST_EQ(sSoundLog[0], sIconSounds[i]);
TEST_TRUE(!sSoundWait[0]);
}
sSoundCount = 0;
WindowT *win = dvxCreateWindow(tCtx(), "Plain", OTHER_WIN_X, OTHER_WIN_Y, OTHER_WIN_W, OTHER_WIN_H, false);
TEST_EQ(sSoundCount, 0);
tPump(1);
TEST_EQ(sSoundCount, 1);
TEST_EQ(sSoundLog[0], SoundWindowOpenE);
tPump(1);
TEST_EQ(sSoundCount, 1);
dvxDestroyWindow(tCtx(), win);
TEST_EQ(sSoundCount, 2);
TEST_EQ(sSoundLog[1], SoundWindowCloseE);
// Opened and closed in one frame: only the close is heard.
sSoundCount = 0;
win = dvxCreateWindow(tCtx(), "Brief", OTHER_WIN_X, OTHER_WIN_Y, OTHER_WIN_W, OTHER_WIN_H, false);
dvxDestroyWindow(tCtx(), win);
tPump(1);
TEST_EQ(sSoundCount, 1);
TEST_EQ(sSoundLog[0], SoundWindowCloseE);
sSoundCount = 0;
dvxBeep();
dvxPlaySoundWait(SoundExitE);
TEST_EQ(sSoundCount, 2);
TEST_EQ(sSoundLog[0], SoundDefaultE);
TEST_EQ(sSoundLog[1], SoundExitE);
TEST_TRUE(sSoundWait[1]);
// Without a hook nothing happens.
dvxSetSoundHook(NULL);
sSoundCount = 0;
dvxBeep();
TEST_EQ(sSoundCount, 0);
tShutdown();
}
static void caseWrapping(void) {
tCase("long message wrapping");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
ScriptT s;
int32_t lineH = tCtx()->font.charHeight;
int32_t baseH = 0;
// One line: contentH = lineH + 2*pad + button + 3*pad
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Wrap", "short", MB_OK);
baseH = s.contentH;
TEST_EQ(baseH, lineH + MSG_PADDING * 2 + BUTTON_HEIGHT + MSG_PADDING * 3);
// 30 + 30 + 10 characters with spaces: greedy wrap at 38 columns -> 3 lines.
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Wrap", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccc", MB_OK);
TEST_EQ((s.contentH - baseH) / lineH + 1, 3);
// Explicit newlines count as lines.
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Wrap", "one\ntwo\nthree\nfour", MB_OK);
TEST_EQ((s.contentH - baseH) / lineH + 1, 4);
// With an icon the message area is at least the icon area tall.
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Wrap", "short", MB_OK | MB_ICONWARNING);
TEST_TRUE(s.contentH > baseH);
// A 93-character word is broken at the 38-column limit (3 lines)
// and never widens the box.
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Wrap", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", MB_OK);
TEST_EQ((s.contentH - baseH) / lineH + 1, 3);
TEST_EQ(s.winW, MSG_MAX_WIDTH_FRAME);
// A line that ends exactly on a space fits whole: 20 + 1 + 17 = 38
// columns, then a 37-character word -> 2 lines (not 3).
scriptInit(&s, "OK");
dvxMessageBox(tCtx(), "Wrap", "aaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbb ccccccccccccccccccccccccccccccccccccc", MB_OK);
TEST_EQ((s.contentH - baseH) / lineH + 1, 2);
// Input prompts wrap too: '\n' starts a new label line and a long
// prompt never widens the dialog past the wrap width.
char buf[TEXT_MAX_LEN];
scriptInit(&s, "Cancel");
dvxInputBox(tCtx(), "Prompt", "first\nsecond", NULL, buf, sizeof(buf));
TEST_TRUE(s.winW <= IB_WIDTH_FRAME);
int32_t twoLineH = s.contentH;
scriptInit(&s, "Cancel");
dvxInputBox(tCtx(), "Prompt", "first", NULL, buf, sizeof(buf));
TEST_TRUE(twoLineH > s.contentH);
scriptInit(&s, "Cancel");
dvxInputBox(tCtx(), "Prompt", "a long prompt that goes on and on well beyond the width of any sensible input dialog box", NULL, buf, sizeof(buf));
TEST_TRUE(s.winW <= IB_WIDTH_FRAME);
tShutdown();
}
// ============================================================
// Helpers
// ============================================================
static WidgetT *childAt(WidgetT *parent, int32_t idx) {
WidgetT *c = parent ? parent->firstChild : NULL;
while (c && idx > 0) {
c = c->nextSibling;
idx--;
}
return c;
}
// Find the close gadget by scanning the title bar with the WM hit test.
// The gadget is Motif-style: one click opens the system menu, a
// double-click closes the window.
static void clickCloseGadget(WindowT *win) {
int32_t y = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT / 2;
for (int32_t x = win->x; x < win->x + win->w; x++) {
int32_t part = HIT_NONE;
int32_t idx = wmHitTest(&tCtx()->stack, x, y, &part);
if (idx >= 0 && tCtx()->stack.windows[idx] == win && part == HIT_CLOSE) {
tAdvanceClock(SINGLE_CLICK_GAP_MS);
tClick(x, y);
tAdvanceClock(DBLCLICK_GAP_MS);
tClick(x, y);
return;
}
}
TEST_FAIL("close gadget not found");
}
// Click (or double-click, with a clock gap so the second press counts)
// on the given visible row of a list box.
static void clickListRow(WidgetT *lb, int32_t row, bool dbl) {
int32_t cx = 0;
int32_t cy = 0;
tWidgetCenter(lb, &cx, &cy);
int32_t x = cx - lb->w / 4;
int32_t y = cy - lb->h / 2 + LISTBOX_BORDER + row * tCtx()->font.charHeight + tCtx()->font.charHeight / 2;
tAdvanceClock(SINGLE_CLICK_GAP_MS);
tClick(x, y);
if (dbl) {
tAdvanceClock(DBLCLICK_GAP_MS);
tClick(x, y);
}
}
static void clickSpinnerArrow(WidgetT *sp, bool up) {
int32_t cx = 0;
int32_t cy = 0;
tWidgetCenter(sp, &cx, &cy);
int32_t x = cx + sp->w / 2 - SPINNER_BORDER - SPINNER_BTN_W / 2;
int32_t y = up ? cy - sp->h / 4 : cy + sp->h / 4;
tAdvanceClock(SINGLE_CLICK_GAP_MS);
tClick(x, y);
}
// Color dialog script: checks the opening state, then acts per op.
static void colorScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
ColorScriptT *cs = (ColorScriptT *)userCtx;
if (!scriptEnter(modal)) {
return;
}
cs->calls++;
if (cs->calls > 1) {
scriptLeave();
return;
}
WidgetT *root = modal->widgetRoot;
WidgetT *palette = childAt(root, 0);
WidgetT *redRow = childAt(root, COLOR_ROW_RED);
WidgetT *greenRow = childAt(root, COLOR_ROW_GREEN);
WidgetT *preview = childAt(root, COLOR_IDX_PREVIEW);
uint8_t r = (uint8_t)((cs->start >> COLOR_SHIFT_R) & COLOR_MASK);
uint8_t g = (uint8_t)((cs->start >> COLOR_SHIFT_G) & COLOR_MASK);
uint8_t b = (uint8_t)(cs->start & COLOR_MASK);
int32_t sx = 0;
int32_t sy = 0;
cs->previewOk = wgtCanvasGetPixel(preview, 0, 0) == packColor(&ctx->display, r, g, b);
cs->redFocused = wgtGetFocused() == childAt(redRow, COLOR_IDX_SLIDER);
switch (cs->op) {
case ColorOpOkE:
TEST_TRUE(tDialogClickButton(modal, "OK"));
break;
case ColorOpCancelE:
TEST_TRUE(tDialogClickButton(modal, "Cancel"));
break;
case ColorOpEscE:
tKeyEsc();
break;
case ColorOpSwatchE:
tWidgetCenter(palette, &sx, &sy);
tAdvanceClock(SINGLE_CLICK_GAP_MS);
tClick(sx + COLOR_SWATCH_DX, sy + COLOR_SWATCH_DY);
TEST_TRUE(tDialogClickButton(modal, "OK"));
break;
case ColorOpKeysE: {
WidgetT *greenBox = childAt(greenRow, COLOR_IDX_SPINNER);
tKey(KEY_END & ~KEY_EXT_FLAG, 0, 0);
TEST_EQ(wgtSpinnerGetValue(childAt(redRow, COLOR_IDX_SPINNER)), COLOR_CHANNEL_MAX);
wgtSetFocused(greenBox);
tKey(KEY_END & ~KEY_EXT_FLAG, 0, 0);
for (int32_t i = 0; i < BACKSPACE_CLEAR_COUNT; i++) {
tKeyBackspace();
}
tType(COLOR_GREEN_TEXT);
cs->greenAfter = wgtSliderGetValue(childAt(greenRow, COLOR_IDX_SLIDER));
tKeyEnter();
break;
}
}
scriptLeave();
}
// A button of the caller's own: ends the dialog, accepted when its
// userData is set.
static void customEnd(WidgetT *w) {
dvxDialogEnd(w->window, w->userData != NULL);
}
static bool customOk(WindowT *dlg, void *userCtx) {
CustomOkT *ok = (CustomOkT *)userCtx;
(void)dlg;
ok->calls++;
if (ok->calls <= ok->refuse) {
return false;
}
snprintf(ok->text, sizeof(ok->text), "%s", wgtGetText(ok->input));
return true;
}
// File dialog script. Layout: pathRow(label,input,label,drive
// dropdown), list, [filterRow(label,dropdown)], nameRow(label,input),
// buttons. Nested confirmation boxes (different title) are answered
// with nestedButton.
static void fileScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
FileScriptT *f = (FileScriptT *)userCtx;
(void)ctx;
if (!scriptEnter(modal)) {
return;
}
bool outer = strcmp(modal->title, "Open It") == 0 || strcmp(modal->title, "Save It") == 0 || strcmp(modal->title, "Open") == 0 || strcmp(modal->title, "Save As") == 0;
if (!outer) {
f->nestedSeen = true;
snprintf(f->nestedTitle, sizeof(f->nestedTitle), "%s", modal->title);
TEST_TRUE(tDialogClickButton(modal, f->nestedButton ? f->nestedButton : "OK"));
scriptLeave();
return;
}
WidgetT *root = modal->widgetRoot;
WidgetT *pathInput = childAt(childAt(root, 0), 1);
WidgetT *driveDd = childAt(childAt(root, 0), 3);
WidgetT *list = childAt(root, 1);
bool hasFilter = childAt(root, 4) != NULL;
WidgetT *filterDd = hasFilter ? childAt(childAt(root, 2), 1) : NULL;
WidgetT *nameInput = childAt(childAt(root, hasFilter ? 3 : 2), 1);
WidgetT *okBtn = childAt(childAt(root, hasFilter ? 4 : 3), 0);
int32_t sx = 0;
int32_t sy = 0;
TEST_TRUE(pathInput && driveDd && list && nameInput && okBtn);
if (!pathInput || !driveDd || !list || !nameInput || !okBtn) {
scriptLeave();
return;
}
// Step 1: the dialog survived the action. The modal loop only
// notices 'done' after this frame, so the state is captured on the
// NEXT idle call rather than right after the action.
if (f->acted) {
f->stillModalAfter = true;
f->itemsAfter = dvxListBoxApi()->getItemCount(list);
f->driveAfter = wgtDropdownGetSelected(driveDd);
snprintf(f->pathAfter, sizeof(f->pathAfter), "%s", wgtGetText(pathInput));
tDialogClickButton(modal, "Cancel");
scriptLeave();
return;
}
f->acted = true;
f->itemsBefore = dvxListBoxApi()->getItemCount(list);
f->driveItems = wgtDropdownGetItemCount(driveDd);
f->driveBefore = wgtDropdownGetSelected(driveDd);
snprintf(f->driveLabel, sizeof(f->driveLabel), "%s", wgtDropdownGetItem(driveDd, 0));
switch (f->op) {
case FileOpCancelE:
snprintf(f->pathAfter, sizeof(f->pathAfter), "%s", wgtGetText(pathInput));
tDialogClickButton(modal, "Cancel");
break;
case FileOpEscE:
tKeyEsc();
break;
case FileOpCloseE:
clickCloseGadget(modal);
break;
case FileOpDblClickSubE:
clickListRow(list, FIXTURE_IDX_SUB, true);
break;
case FileOpTypePathE:
tWidgetCenter(pathInput, &sx, &sy);
tClick(sx, sy);
wgtSetText(pathInput, "");
tType(f->text);
tKeyEnter();
break;
case FileOpFilterNextE:
wgtSetFocused(filterDd);
tKey(KEY_SCAN_DOWN, 0, 0);
break;
case FileOpClickFileOpenE:
fileSelectRow(list, FIXTURE_IDX_ATXT);
snprintf(f->nameAfter, sizeof(f->nameAfter), "%s", wgtGetText(nameInput));
tWidgetCenter(okBtn, &sx, &sy);
tClick(sx, sy);
break;
case FileOpDblClickFileE:
clickListRow(list, FIXTURE_IDX_ATXT, true);
break;
case FileOpTypeNameE:
tWidgetCenter(nameInput, &sx, &sy);
tClick(sx, sy);
tType(f->text);
tWidgetCenter(okBtn, &sx, &sy);
tClick(sx, sy);
break;
case FileOpEnterInNameE:
tWidgetCenter(nameInput, &sx, &sy);
tClick(sx, sy);
tType(f->text);
tKeyEnter();
break;
case FileOpSelectDirOpenE:
fileSelectRow(list, FIXTURE_IDX_SUB);
TEST_STR_EQ(wgtGetText(nameInput), "");
tWidgetCenter(okBtn, &sx, &sy);
tClick(sx, sy);
break;
case FileOpDriveE:
wgtSetFocused(driveDd);
wgtDropdownSetSelected(driveDd, f->driveIdx);
driveDd->onChange(driveDd);
break;
}
scriptLeave();
}
// Single-click a row with the double-click window expired first.
static void fileSelectRow(WidgetT *lb, int32_t row) {
clickListRow(lb, row, false);
TEST_EQ(wgtListBoxGetSelected(lb), row);
}
// Fixture: dir/{a.txt, b.txt, c.bmp, sub/{d.txt, deep/}}
static void fixtureTree(char *dir, int32_t dirSize) {
char path[DVX_MAX_PATH * 2];
snprintf(dir, dirSize, "%s/tree", tTempDir());
mkdir(dir, 0700);
snprintf(path, sizeof(path), "%s/sub", dir);
mkdir(path, 0700);
snprintf(path, sizeof(path), "%s/sub/deep", dir);
mkdir(path, 0700);
snprintf(path, sizeof(path), "%s/a.txt", dir);
tWriteFile(path, "a", 1);
snprintf(path, sizeof(path), "%s/b.txt", dir);
tWriteFile(path, "b", 1);
snprintf(path, sizeof(path), "%s/c.bmp", dir);
tWriteFile(path, "c", 1);
snprintf(path, sizeof(path), "%s/sub/d.txt", dir);
tWriteFile(path, "d", 1);
}
// Outer message box: open a second message box from the idle script,
// answer it Yes, then cancel the outer one.
static void nestedSameTypeScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
ScriptT *s = (ScriptT *)userCtx;
if (!scriptEnter(modal)) {
return;
}
if (strcmp(modal->title, "Inner") == 0) {
tDialogClickButton(modal, "Yes");
scriptLeave();
return;
}
// First visit opens the inner box; the outer box must still be up
// on a later frame, where it is cancelled.
s->calls++;
if (s->calls == 1) {
s->nestedResult = dvxMessageBox(ctx, "Inner", "Inner text", MB_YESNO);
TEST_PTR_EQ(ctx->modalWindow, modal);
} else {
tDialogClickButton(modal, "Cancel");
}
scriptLeave();
}
// Outer input box: open a nested message box from the idle script,
// answer it, and record which modal was restored afterwards.
static void nestedScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
ScriptT *s = (ScriptT *)userCtx;
if (!scriptEnter(modal)) {
return;
}
if (strcmp(modal->title, "Inner") == 0) {
TEST_PTR_EQ(ctx->modalWindow, modal);
TEST_TRUE(modal->focused);
tDialogClickButton(modal, "Yes");
scriptLeave();
return;
}
scriptRecord(s, modal);
if (s->calls == 1) {
s->nestedResult = dvxMessageBox(ctx, "Inner", "Nested?", MB_YESNO | MB_ICONQUESTION);
s->prevModalAfterNested = ctx->modalWindow;
TEST_TRUE(modal->focused);
tDialogClickButton(modal, s->button);
}
scriptLeave();
}
static OtherWindowT otherWindow(const char *title) {
OtherWindowT o;
memset(&o, 0, sizeof(o));
o.win = dvxCreateWindow(tCtx(), title, OTHER_WIN_X, OTHER_WIN_Y, OTHER_WIN_W, OTHER_WIN_H, false);
WidgetT *root = wgtInitWindow(tCtx(), o.win);
WidgetT *box = wgtVBox(root);
o.btn = wgtButton(box, "Press");
o.input = wgtTextInput(box, TEXT_MAX_LEN);
wgtSetName(o.btn, "press");
wgtSetName(o.input, "input");
wgtInvalidate(root);
tPump(SETTLE_FRAMES);
return o;
}
// Re-entrancy guard keyed by modal window: the same script may be
// invoked again for the same modal from a nested pump; refuse that, but
// let it run for a different (nested) modal.
static bool scriptEnter(WindowT *modal) {
for (int32_t i = 0; i < sBusyDepth; i++) {
if (sBusy[i] == modal) {
return false;
}
}
if (sBusyDepth >= BUSY_STACK_MAX) {
return false;
}
sBusy[sBusyDepth++] = modal;
return true;
}
static void scriptInit(ScriptT *s, const char *button) {
memset(s, 0, sizeof(*s));
s->button = button;
tDialogScript(uniformScript, s);
}
static void scriptLeave(void) {
if (sBusyDepth > 0) {
sBusyDepth--;
}
}
static void scriptRecord(ScriptT *s, WindowT *modal) {
s->calls++;
snprintf(s->title, sizeof(s->title), "%s", modal->title);
s->winX = modal->x;
s->winY = modal->y;
s->winW = modal->w;
s->winH = modal->h;
s->contentH = modal->contentH;
s->modalSeen = modal;
}
static void soundRecord(DvxSoundE event, bool wait) {
if (sSoundCount < SOUND_LOG_MAX) {
sSoundLog[sSoundCount] = event;
sSoundWait[sSoundCount] = wait;
sSoundCount++;
}
}
static void spinInit(ScriptT *s, const char *button) {
scriptInit(s, button);
tDialogScript(spinScript, s);
}
// Int input box: layout label, spinner, buttons. capturedInt = arrow
// clicks (+up / -down), capturedInt2 = keyboard Up presses, typeText =
// replace the text after End + backspaces.
static void spinScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
ScriptT *s = (ScriptT *)userCtx;
(void)ctx;
if (!scriptEnter(modal)) {
return;
}
scriptRecord(s, modal);
if (s->calls > 1) {
scriptLeave();
return;
}
WidgetT *spinner = childAt(modal->widgetRoot, 1);
TEST_PTR_EQ(wgtGetFocused(), spinner);
for (int32_t i = 0; i < s->capturedInt; i++) {
clickSpinnerArrow(spinner, true);
}
for (int32_t i = 0; i > s->capturedInt; i--) {
clickSpinnerArrow(spinner, false);
}
for (int32_t i = 0; i < s->capturedInt2; i++) {
tKey(KEY_SCAN_UP, 0, 0);
}
if (s->typeText) {
wgtSetFocused(spinner);
tKey(KEY_END & ~KEY_EXT_FLAG, 0, 0);
for (int32_t i = 0; i < BACKSPACE_CLEAR_COUNT; i++) {
tKeyBackspace();
}
tType(s->typeText);
}
if (s->pressEnter) {
tKeyEnter();
} else if (s->pressEsc) {
tKeyEsc();
} else if (s->closeGadget) {
clickCloseGadget(modal);
} else if (s->button) {
TEST_TRUE(tDialogClickButton(modal, s->button));
}
scriptLeave();
}
// Generic script for message/input/choice boxes and geometry checks.
// Nested dialogs (a different title than the first one seen) get
// nestedButton. For the choice dialog capturedInt2 > 0 presses Down
// that many times, < 0 double-clicks row (-capturedInt2 - 1).
static void uniformScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
ScriptT *s = (ScriptT *)userCtx;
if (!scriptEnter(modal)) {
return;
}
if (s->calls > 0 && s->modalSeen != modal && s->nestedButton) {
tDialogClickButton(modal, s->nestedButton);
scriptLeave();
return;
}
if (s->calls > 0 && s->modalSeen == modal) {
// Second visit to the same dialog (e.g. OK refused): fall back.
s->calls++;
if (s->nestedButton) {
tDialogClickButton(modal, s->nestedButton);
}
scriptLeave();
return;
}
// caseModality passes the other window in modalSeen before the
// first call; poke it before recording the modal.
WindowT *other = s->modalSeen;
scriptRecord(s, modal);
if (other) {
int32_t sx = 0;
int32_t sy = 0;
tWidgetCenter(tFind(other, "press"), &sx, &sy);
tClick(sx, sy);
tWidgetCenter(tFind(other, "input"), &sx, &sy);
tClick(sx, sy);
tType("nope");
tClick(other->x + other->w / 2, other->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT / 2);
tAltTab();
s->capturedInt = (ctx->stack.windows[ctx->stack.count - 1] == modal) ? 1 : 0;
s->capturedInt2 = modal->focused ? 1 : 0;
}
if (s->goldenName) {
// Park the cursor off the dialog so the crop holds only chrome,
// icon, text and buttons.
tMouseMove(0, 0);
tGoldenRect(s->goldenName, modal->x, modal->y, modal->w, modal->h);
}
// Choice dialog: record the highlighted row and optionally move it.
WidgetT *focus = wgtGetFocused();
if (focus && strcmp(modal->title, "Pick") == 0) {
s->capturedInt = wgtListBoxGetSelected(focus);
for (int32_t i = 0; i < s->capturedInt2; i++) {
tKey(KEY_SCAN_DOWN, 0, 0);
}
if (s->capturedInt2 < 0) {
clickListRow(focus, -s->capturedInt2 - 1, true);
}
}
// Input box: the text input owns focus; capture its default text.
if (focus && strcmp(modal->title, "Ask") == 0) {
snprintf(s->captured, sizeof(s->captured), "%s", wgtGetText(focus));
}
if (s->typeText) {
tType(s->typeText);
}
if (s->abandon) {
ctx->running = false;
} else if (s->pressEnter) {
tKeyEnter();
} else if (s->pressEsc) {
tKeyEsc();
} else if (s->closeGadget) {
clickCloseGadget(modal);
} else if (s->button) {
bool found = tDialogClickButton(modal, s->button);
if (!found && s->nestedButton) {
tDialogClickButton(modal, s->nestedButton);
}
}
scriptLeave();
}
int main(void) {
platformSetLogPath("dialogsTest.log");
tSuiteBegin("T4 dialogs");
tSetGoldenDir(DIALOGS_GOLDEN_DIR);
int32_t loaded = tProcessInit(DVXTEST_WIDGET_DIR);
printf("widgets loaded: %ld\n", (long)loaded);
TEST_TRUE(loaded > 0);
caseMessageBoxMatrix();
caseErrorInfoBox();
caseInputBox();
caseIntInputBox();
caseButtonBox();
caseChoice();
caseColorDialog();
caseCustom();
casePromptSave();
caseFileDialog();
caseModality();
caseSounds();
caseNested();
caseNestedSameType();
caseAbandon();
caseGeometry(SCREEN_W, SCREEN_H);
caseGeometry(SCREEN_W_LARGE, SCREEN_H_LARGE);
caseWrapping();
caseGolden();
return tSuiteEnd();
}