// 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 #include #include #include #include #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(); }