From cda8a7e214c074dd0ac9c3b839c15a937fa2519a Mon Sep 17 00:00:00 2001 From: Simon Dick Date: Sun, 9 Aug 2026 15:45:22 +0100 Subject: [PATCH] Classify 12 WB3.2-era BOOPSI/ReAction gadget classes (#69) ClassifyByClassID() previously recognized only 10 of the 26 gadget classes documented in NDK 3.2's own gadgets/ header set, leaving clicktab.gadget (Hyperion's WB3.2-native tabbed-panel widget, the class that motivated this issue) and 11 others at role=custom -- structurally visible but not addressable via tier-2 ROLE= locators. Adds role mappings for clicktab.gadget (page_tab_list), colorwheel .gadget (color_wheel), datebrowser.gadget (calendar), fuelgauge .gadget (progress_bar), getcolor/getfile/getfont/getscreenmode .gadget (color_chooser/file_chooser/font_chooser/screenmode_chooser), gradientslider.gadget (reuses the existing slider role), palette .gadget (palette), sketchboard.gadget (canvas), speedbar.gadget (toolbar), and texteditor.gadget (text_editor). Every class-ID string was confirmed against real NDK 3.2 headers, then live-verified against a new fixture (fixtures/reaction-classes-app) exercising one instance of each class directly on a plain window (not nested inside window.class/layout.gadget, which would make them unreachable the same way issue #49 already documents). Two real bugs found building this: colorwheel.gadget's NewObject() silently returns NULL without the required WHEEL_Screen tag (easy to miss among a page of optional ones); and speedbar.gadget registers its class as literally "speedbar", not "speedbar.gadget" like every other class here -- confirmed only by reading the walker's own className field back from a live object. space.gadget, virtual.gadget, listview.gadget, tabs.gadget, and tapedeck.gadget are deliberately left unclassified -- documented honest gaps (see Locator-Tiers-and-Limits.md), not oversights. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018CbPo4nMpnM5JQX3LKE5JP --- CLAUDE.md | 53 +++ Makefile | 16 +- .../ReactionClassesApp.golden | 17 + fixtures/reaction-classes-app/src/main.c | 347 ++++++++++++++++++ intuition-model/include/intuition_model.h | 21 ++ intuition-model/src/walk.c | 118 ++++++ tests/copperline/golden-test.py | 2 + tests/copperline/run.sh | 3 +- userdocs/Changelog.md | 24 ++ userdocs/Locator-Tiers-and-Limits.md | 41 ++- 10 files changed, 633 insertions(+), 9 deletions(-) create mode 100644 fixtures/reaction-classes-app/ReactionClassesApp.golden create mode 100644 fixtures/reaction-classes-app/src/main.c diff --git a/CLAUDE.md b/CLAUDE.md index 130c860..66a91da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,59 @@ discriminator to tell apart) -- confirmed live under Copperline via now carries the new gadget's real, live-measured geometry and `role=integer` line. +WB3.2-era BOOPSI/ReAction role classification is real too (issue #69): +`ClassifyByClassID()` (`intuition-model/src/walk.c`) previously +recognised only 10 of the classes documented in NDK 3.2's own +`gadgets/` header set, leaving the rest at `role=custom`. Twelve more +now get a real role -- `clicktab.gadget` (`role=page_tab_list`, the +class that motivated filing this issue: Hyperion's WB3.2-native tabbed- +panel widget), `colorwheel.gadget` (`role=color_wheel`), +`datebrowser.gadget` (`role=calendar`), `fuelgauge.gadget` +(`role=progress_bar`), `getcolor.gadget`/`getfile.gadget`/ +`getfont.gadget`/`getscreenmode.gadget` (`role=color_chooser`/ +`file_chooser`/`font_chooser`/`screenmode_chooser` -- AT-SPI-style +names for "a compound button that opens a system requester and shows +the result", which is genuinely what all four are per their own NDK +autodocs), `gradientslider.gadget` (`role=slider` -- reuses the +existing role rather than inventing a redundant one, since AT-SPI +itself has no separate "gradient slider" role either), `palette.gadget` +(`role=palette`), `sketchboard.gadget` (`role=canvas`), +`speedbar.gadget` (`role=toolbar`), and `texteditor.gadget` +(`role=text_editor`). Every class-ID string was confirmed against real +NDK 3.2 headers first (pragma/*_lib.h's own ".gadget" comment, or +reaction_macros.h's convenience-Object macros for the two classes -- +colorwheel, gradientslider -- that register a PUBLIC class name instead +of needing an explicit `XXX_GetClass()` call), not guessed, then +live-verified against a new dedicated fixture, +`fixtures/reaction-classes-app`, one instance of each class attached +DIRECTLY to a plain classic window rather than via `window.class`/ +`layout.gadget` (which would make them exactly as unreachable as +issue #49's own confirmed limit already documents -- this fixture +exists specifically to sidestep that, not to demonstrate it). Two real +bugs found building this, live (2026-08-09): `colorwheel.gadget`'s +`NewObject()` silently returned NULL until `WHEEL_Screen` was supplied +-- its own autodoc does say this tag is required, but it's easy to +miss among a page of optional ones, and the failure mode (NULL, no +error text) gives no hint why; and `speedbar.gadget` registers its +class as literally `"speedbar"`, NOT `"speedbar.gadget"` like every +other class checked here -- confirmed only by reading the walker's own +`className` field back from a live object, since nothing in the NDK +materials flags this exception. `fixtures/reaction-classes-app`'s own +`ReactionClassesApp.golden` locks in the live-confirmed output for +regression protection, verified via `tests/copperline/run.sh`'s +`run_golden_check`. Five real classes from the same research pass were +deliberately left unclassified, not silently missed -- +`space.gadget`/`virtual.gadget` (honest limits, `virtual.gadget` +sharing `layout.gadget`'s own unreachable-children problem), +`listview.gadget` (its own autodoc says `listbrowser.gadget`, already +mapped, is a strict upgrade), and `tabs.gadget`/`tapedeck.gadget` +(both ship as real library files on a stock WB3.2.3 install, confirmed +on disk, but neither has a documented NDK-supported construction path +in this project's own NDK 3.2 snapshot -- no `GetClass()` proto/pragma +header and no `reaction_macros.h` convenience macro exists for either) +-- see `userdocs/Locator-Tiers-and-Limits.md`'s own writeup for the +full reasoning behind each. + Phase 0.5 (reliability and reach into the wider ecosystem) before it: `WAITFOR` (including its `TEXT=` condition) and `CLICK`'s `EXPECT=` (wait/expectation diff --git a/Makefile b/Makefile index f1724f3..6d99be6 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,14 @@ WBGUIAPP_BIN := $(BUILD)/fixtures/WBGuiApp P96APP_SRC := fixtures/p96-app/src/main.c P96APP_BIN := $(BUILD)/fixtures/P96App +# reaction-classes-app: one instance each of the 13 BOOPSI/ReAction +# gadget classes issue #69 adds role classification for, attached +# directly to a plain classic window (NOT window.class/layout.gadget +# -- see fixtures/reaction-classes-app/src/main.c's own header for +# why that would defeat the fixture's purpose). +REACTION_CLASSES_APP_SRC := fixtures/reaction-classes-app/src/main.c +REACTION_CLASSES_APP_BIN := $(BUILD)/fixtures/ReactionClassesApp + # --- server/ (phase 0.2, in progress -- see docs/implementation-plan.md) -- ACTION_SRCDIR := server/src ACTION_INCDIR := server/include @@ -127,7 +135,7 @@ all: amiga amiga: $(INSPECT_BIN) -fixtures: $(GADTOOLS_APP_BIN) $(CLASSACT_APP_BIN) $(SECONDSCREEN_APP_BIN) $(WBAPP_BIN) $(MAKEICON_BIN) $(P96APP_BIN) $(WBGUIAPP_BIN) +fixtures: $(GADTOOLS_APP_BIN) $(CLASSACT_APP_BIN) $(SECONDSCREEN_APP_BIN) $(WBAPP_BIN) $(MAKEICON_BIN) $(P96APP_BIN) $(WBGUIAPP_BIN) $(REACTION_CLASSES_APP_BIN) server: $(CLICKTEST_BIN) $(SETMOUSE_BIN) $(AMIPILOTD_BIN) @@ -170,6 +178,12 @@ $(P96APP_BIN): $(P96APP_SRC) @mkdir -p $(BUILD)/fixtures $(CC) $(CFLAGS) -o $@ $(P96APP_SRC) +# -lamiga: same NewObject varargs marshaling reason as CLASSACT_APP_BIN +# above. +$(REACTION_CLASSES_APP_BIN): $(REACTION_CLASSES_APP_SRC) + @mkdir -p $(BUILD)/fixtures + $(CC) $(CFLAGS) -o $@ $(REACTION_CLASSES_APP_SRC) -lamiga + $(MAKEICON_BIN): $(MAKEICON_SRC) @mkdir -p $(BUILD)/fixtures $(CC) $(CFLAGS) -o $@ $(MAKEICON_SRC) diff --git a/fixtures/reaction-classes-app/ReactionClassesApp.golden b/fixtures/reaction-classes-app/ReactionClassesApp.golden new file mode 100644 index 0000000..dd0b513 --- /dev/null +++ b/fixtures/reaction-classes-app/ReactionClassesApp.golden @@ -0,0 +1,17 @@ +window "AmiPilot ReAction Classes Fixture" screen="Workbench Screen" [40,0 340x256] + gadget id=0 role=custom class="buttongclass" label="" [318,0 24x11] + gadget id=0 role=custom class="buttongclass" label="" [0,0 20x11] + gadget id=0 role=custom class="gadgetclass" label="" [0,0 340x10] + gadget id=101 role=page_tab_list class="clicktab.gadget" label="" [8,20 150x22] + gadget id=102 role=color_wheel class="colorwheel.gadget" label="" [172,20 150x60] + gadget id=103 role=calendar class="datebrowser.gadget" label="" [8,84 150x22] + gadget id=104 role=progress_bar class="fuelgauge.gadget" label="" [172,84 150x22] + gadget id=105 role=color_chooser class="getcolor.gadget" label="" [8,110 150x22] + gadget id=106 role=file_chooser class="getfile.gadget" label="" [172,110 150x22] + gadget id=107 role=font_chooser class="getfont.gadget" label="" [8,136 150x22] + gadget id=108 role=screenmode_chooser class="getscreenmode.gadget" label="" [172,136 150x22] + gadget id=109 role=slider class="gradientslider.gadget" label="" [8,162 150x22] + gadget id=110 role=palette class="palette.gadget" label="" [172,162 150x22] + gadget id=111 role=canvas class="sketchboard.gadget" label="" [8,188 150x40] + gadget id=112 role=toolbar class="speedbar" label="" [172,188 150x22] + gadget id=113 role=text_editor class="texteditor.gadget" label="" [8,232 314x30] diff --git a/fixtures/reaction-classes-app/src/main.c b/fixtures/reaction-classes-app/src/main.c new file mode 100644 index 0000000..b70fffa --- /dev/null +++ b/fixtures/reaction-classes-app/src/main.c @@ -0,0 +1,347 @@ +/* + * reaction-classes-app -- exercises the BOOPSI/ReAction gadget classes + * intuition-model's walker didn't role-classify before issue #69 + * (clicktab.gadget, colorwheel.gadget, datebrowser.gadget, + * fuelgauge.gadget, getcolor.gadget, getfile.gadget, getfont.gadget, + * getscreenmode.gadget, gradientslider.gadget, palette.gadget, + * sketchboard.gadget, speedbar.gadget, texteditor.gadget). + * + * Deliberately does NOT use window.class/layout.gadget the way + * fixtures/classact-app does -- a layout.gadget's own children are + * permanently invisible to structural walking (issue #49's confirmed + * limit), which would defeat this fixture's whole purpose. Instead, + * each object is created directly via its own class's GetClass() + * function with explicit GA_Left/GA_Top/GA_Width/GA_Height/GA_ID tags + * (the same absolute-placement convention GadTools' CreateGadget() + * uses internally), then linked into a PLAIN classic window's gadget + * chain via AddGList()/RefreshGList() -- BOOPSI or not, any struct + * Gadget* is Intuition-uniform (intuition/classes.h: "Gadget objects + * are Gadget pointers"), the same property this project's own walker + * already relies on. + * + * Every class here still needs OpenLibrary() to load its .gadget file + * before ANY construction form works, including the two + * (colorwheel.gadget, gradientslider.gadget) that register a public + * class name (reaction/reaction_macros.h's own ColorWheelObject/ + * GradientObject macros construct via NewObject(NULL, "name", ...), + * not a GetClass() call) -- confirmed against classact-app's own + * precedent, which OpenLibrary()s button.gadget even though it then + * constructs it by name too. + * + * texteditor.gadget is a real, documented NDK quirk: its own pragma + * header (pragma/texteditor_lib.h) states outright "The library base + * name is TextFieldBase and not TextEditorBase" even though the + * exported function is TEXTEDITOR_GetClass() -- easy to get wrong + * guessing from the class name alone. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct IntuitionBase *IntuitionBase = NULL; +struct Library *ClickTabBase = NULL; +struct Library *ColorWheelBase = NULL; +struct Library *DateBrowserBase = NULL; +struct Library *FuelGaugeBase = NULL; +struct Library *GetColorBase = NULL; +struct Library *GetFileBase = NULL; +struct Library *GetFontBase = NULL; +struct Library *GetScreenModeBase = NULL; +struct Library *GradientSliderBase = NULL; +struct Library *PaletteBase = NULL; +struct Library *SketchBoardBase = NULL; +struct Library *SpeedBarBase = NULL; +struct Library *TextFieldBase = NULL; /* texteditor.gadget -- see header comment */ + +#define GID_CLICKTAB 101 +#define GID_COLORWHEEL 102 +#define GID_DATEBROWSER 103 +#define GID_FUELGAUGE 104 +#define GID_GETCOLOR 105 +#define GID_GETFILE 106 +#define GID_GETFONT 107 +#define GID_GETSCREENMODE 108 +#define GID_GRADIENTSLIDER 109 +#define GID_PALETTE 110 +#define GID_SKETCHBOARD 111 +#define GID_SPEEDBAR 112 +#define GID_TEXTEDITOR 113 + +/* Writes directly to a fixed file via dos.library rather than stdout -- + * "Run >file " does not reliably capture a background CLI's + * own Output() stream under Copperline (confirmed repeatedly this + * session, e.g. fixtures/classact-app's own DiagFile()); this + * sidesteps that uncertainty entirely. */ +static void Diag(const char *msg) +{ + BPTR fh = Open((CONST_STRPTR)"SRC:build/rc-diag.txt", MODE_READWRITE); + if (fh == 0) { + fh = Open((CONST_STRPTR)"SRC:build/rc-diag.txt", MODE_NEWFILE); + } + if (fh == 0) { + return; + } + Seek(fh, 0, OFFSET_END); + Write(fh, (APTR)msg, (LONG)strlen(msg)); + Close(fh); +} + +static void CleanExit(struct Window *window, struct Gadget *glist, int rc) +{ + struct Gadget *g; + + if (window != NULL) { + CloseWindow(window); + } + /* Every object in the chain is a real BOOPSI object -- DisposeObject, + * not FreeGadget, same as classact-app's own DisposeObject use. */ + while (glist != NULL) { + g = glist; + glist = glist->NextGadget; + DisposeObject((Object *)g); + } + + if (TextFieldBase != NULL) CloseLibrary(TextFieldBase); + if (SpeedBarBase != NULL) CloseLibrary(SpeedBarBase); + if (SketchBoardBase != NULL) CloseLibrary(SketchBoardBase); + if (PaletteBase != NULL) CloseLibrary(PaletteBase); + if (GradientSliderBase != NULL) CloseLibrary(GradientSliderBase); + if (GetScreenModeBase != NULL) CloseLibrary(GetScreenModeBase); + if (GetFontBase != NULL) CloseLibrary(GetFontBase); + if (GetFileBase != NULL) CloseLibrary(GetFileBase); + if (GetColorBase != NULL) CloseLibrary(GetColorBase); + if (FuelGaugeBase != NULL) CloseLibrary(FuelGaugeBase); + if (DateBrowserBase != NULL) CloseLibrary(DateBrowserBase); + if (ColorWheelBase != NULL) CloseLibrary(ColorWheelBase); + if (ClickTabBase != NULL) CloseLibrary(ClickTabBase); + if (IntuitionBase != NULL) CloseLibrary((struct Library *)IntuitionBase); + + exit(rc); +} + +int main(void) +{ + struct Screen *screen; + struct Window *window; + struct Gadget *glist = NULL, *tail = NULL, *g; + BOOL done = FALSE; + WORD col1x = 8, col2x = 172, y = 20, rowh = 26; + +#define LINK(newg) \ + do { \ + g = (struct Gadget *)(newg); \ + if (g != NULL) { \ + if (tail == NULL) { glist = g; } else { tail->NextGadget = g; } \ + tail = g; \ + } \ + } while (0) + + IntuitionBase = (struct IntuitionBase *)OpenLibrary((CONST_STRPTR)"intuition.library", 37); + ClickTabBase = OpenLibrary((CONST_STRPTR)"gadgets/clicktab.gadget", 44); + ColorWheelBase = OpenLibrary((CONST_STRPTR)"gadgets/colorwheel.gadget", 44); + DateBrowserBase = OpenLibrary((CONST_STRPTR)"gadgets/datebrowser.gadget", 44); + FuelGaugeBase = OpenLibrary((CONST_STRPTR)"gadgets/fuelgauge.gadget", 44); + GetColorBase = OpenLibrary((CONST_STRPTR)"gadgets/getcolor.gadget", 44); + GetFileBase = OpenLibrary((CONST_STRPTR)"gadgets/getfile.gadget", 44); + GetFontBase = OpenLibrary((CONST_STRPTR)"gadgets/getfont.gadget", 44); + GetScreenModeBase = OpenLibrary((CONST_STRPTR)"gadgets/getscreenmode.gadget", 44); + GradientSliderBase = OpenLibrary((CONST_STRPTR)"gadgets/gradientslider.gadget", 44); + PaletteBase = OpenLibrary((CONST_STRPTR)"gadgets/palette.gadget", 44); + SketchBoardBase = OpenLibrary((CONST_STRPTR)"gadgets/sketchboard.gadget", 44); + SpeedBarBase = OpenLibrary((CONST_STRPTR)"gadgets/speedbar.gadget", 44); + TextFieldBase = OpenLibrary((CONST_STRPTR)"gadgets/texteditor.gadget", 44); + + if (IntuitionBase == NULL) { + Diag("rcapp: intuition.library FAILED\n"); + CleanExit(NULL, NULL, RETURN_FAIL); + } + Diag(ClickTabBase != NULL ? "rcapp: clicktab.gadget ok\n" : "rcapp: clicktab.gadget FAILED\n"); + Diag(ColorWheelBase != NULL ? "rcapp: colorwheel.gadget ok\n" : "rcapp: colorwheel.gadget FAILED\n"); + Diag(DateBrowserBase != NULL ? "rcapp: datebrowser.gadget ok\n" : "rcapp: datebrowser.gadget FAILED\n"); + Diag(FuelGaugeBase != NULL ? "rcapp: fuelgauge.gadget ok\n" : "rcapp: fuelgauge.gadget FAILED\n"); + Diag(GetColorBase != NULL ? "rcapp: getcolor.gadget ok\n" : "rcapp: getcolor.gadget FAILED\n"); + Diag(GetFileBase != NULL ? "rcapp: getfile.gadget ok\n" : "rcapp: getfile.gadget FAILED\n"); + Diag(GetFontBase != NULL ? "rcapp: getfont.gadget ok\n" : "rcapp: getfont.gadget FAILED\n"); + Diag(GetScreenModeBase != NULL ? "rcapp: getscreenmode.gadget ok\n" : "rcapp: getscreenmode.gadget FAILED\n"); + Diag(GradientSliderBase != NULL ? "rcapp: gradientslider.gadget ok\n" : "rcapp: gradientslider.gadget FAILED\n"); + Diag(PaletteBase != NULL ? "rcapp: palette.gadget ok\n" : "rcapp: palette.gadget FAILED\n"); + Diag(SketchBoardBase != NULL ? "rcapp: sketchboard.gadget ok\n" : "rcapp: sketchboard.gadget FAILED\n"); + Diag(SpeedBarBase != NULL ? "rcapp: speedbar.gadget ok\n" : "rcapp: speedbar.gadget FAILED\n"); + Diag(TextFieldBase != NULL ? "rcapp: texteditor.gadget ok\n" : "rcapp: texteditor.gadget FAILED\n"); + + screen = LockPubScreen(NULL); + if (screen == NULL) { + Diag("rcapp: LockPubScreen FAILED\n"); + CleanExit(NULL, NULL, RETURN_FAIL); + } + + if (ClickTabBase != NULL) { + LINK(NewObject(CLICKTAB_GetClass(), NULL, + GA_ID, GID_CLICKTAB, GA_Left, col1x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + if (ColorWheelBase != NULL) { + /* WHEEL_Screen is documented as a REQUIRED OM_NEW tag (the + * autodoc: "must be provided when the wheel is created via + * NewObject()") -- confirmed live: NewObject() returns NULL + * without it, no other tag omission causes that here. */ + Object *cw = NewObject(NULL, (CONST_STRPTR)"colorwheel.gadget", + GA_ID, GID_COLORWHEEL, GA_Left, col2x, GA_Top, y, + GA_Width, 150, GA_Height, 60, + WHEEL_Screen, (ULONG)screen, TAG_DONE); + Diag(cw != NULL ? "rcapp: colorwheel obj ok\n" : "rcapp: colorwheel obj FAILED\n"); + LINK(cw); + } + y += rowh + 38; /* colorwheel wants real vertical room to render as a circle */ + + if (DateBrowserBase != NULL) { + LINK(NewObject(DATEBROWSER_GetClass(), NULL, + GA_ID, GID_DATEBROWSER, GA_Left, col1x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + if (FuelGaugeBase != NULL) { + LINK(NewObject(FUELGAUGE_GetClass(), NULL, + GA_ID, GID_FUELGAUGE, GA_Left, col2x, GA_Top, y, + GA_Width, 150, GA_Height, 22, + FUELGAUGE_Min, 0, FUELGAUGE_Max, 100, FUELGAUGE_Level, 40, + TAG_DONE)); + } + y += rowh; + + if (GetColorBase != NULL) { + LINK(NewObject(GETCOLOR_GetClass(), NULL, + GA_ID, GID_GETCOLOR, GA_Left, col1x, GA_Top, y, + GA_Width, 150, GA_Height, 22, + GETCOLOR_Screen, (ULONG)screen, TAG_DONE)); + } + if (GetFileBase != NULL) { + LINK(NewObject(GETFILE_GetClass(), NULL, + GA_ID, GID_GETFILE, GA_Left, col2x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + y += rowh; + + if (GetFontBase != NULL) { + LINK(NewObject(GETFONT_GetClass(), NULL, + GA_ID, GID_GETFONT, GA_Left, col1x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + if (GetScreenModeBase != NULL) { + LINK(NewObject(GETSCREENMODE_GetClass(), NULL, + GA_ID, GID_GETSCREENMODE, GA_Left, col2x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + y += rowh; + + if (GradientSliderBase != NULL) { + LINK(NewObject(NULL, (CONST_STRPTR)"gradientslider.gadget", + GA_ID, GID_GRADIENTSLIDER, GA_Left, col1x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + if (PaletteBase != NULL) { + LINK(NewObject(PALETTE_GetClass(), NULL, + GA_ID, GID_PALETTE, GA_Left, col2x, GA_Top, y, + GA_Width, 150, GA_Height, 22, + PALETTE_NumColours, 4, TAG_DONE)); + } + y += rowh; + + if (SketchBoardBase != NULL) { + LINK(NewObject(SKETCHBOARD_GetClass(), NULL, + GA_ID, GID_SKETCHBOARD, GA_Left, col1x, GA_Top, y, + GA_Width, 150, GA_Height, 40, TAG_DONE)); + } + if (SpeedBarBase != NULL) { + LINK(NewObject(SPEEDBAR_GetClass(), NULL, + GA_ID, GID_SPEEDBAR, GA_Left, col2x, GA_Top, y, + GA_Width, 150, GA_Height, 22, TAG_DONE)); + } + y += rowh + 18; /* sketchboard is taller */ + + if (TextFieldBase != NULL) { + LINK(NewObject(TEXTEDITOR_GetClass(), NULL, + GA_ID, GID_TEXTEDITOR, GA_Left, col1x, GA_Top, y, + GA_Width, 314, GA_Height, 30, TAG_DONE)); + } + y += rowh + 8; + + window = OpenWindowTags(NULL, + WA_Left, 40, WA_Top, 20, + WA_Width, 340, WA_Height, (ULONG)(y + 20), + WA_Title, (ULONG)"AmiPilot ReAction Classes Fixture", + WA_Gadgets, (ULONG)glist, + WA_CloseGadget, TRUE, + WA_DragBar, TRUE, + WA_DepthGadget, TRUE, + WA_Activate, TRUE, + WA_SimpleRefresh, TRUE, + WA_IDCMP, IDCMP_CLOSEWINDOW | IDCMP_REFRESHWINDOW, + WA_PubScreen, (ULONG)screen, + TAG_DONE); + + UnlockPubScreen(NULL, screen); + + if (window == NULL) { + Diag("rcapp: OpenWindowTags FAILED\n"); + CleanExit(NULL, glist, RETURN_FAIL); + } + + RefreshGadgets(glist, window, NULL); + Diag("rcapp: ready\n"); + + while (!done) { + struct IntuiMessage *msg; + + Wait(1UL << window->UserPort->mp_SigBit); + + while ((msg = (struct IntuiMessage *)GetMsg(window->UserPort)) != NULL) { + ULONG class = msg->Class; + ReplyMsg((struct Message *)msg); + + if (class == IDCMP_CLOSEWINDOW) { + done = TRUE; + } else if (class == IDCMP_REFRESHWINDOW) { + BeginRefresh(window); + EndRefresh(window, TRUE); + } + } + } + + CleanExit(window, glist, RETURN_OK); + return RETURN_OK; +} diff --git a/intuition-model/include/intuition_model.h b/intuition-model/include/intuition_model.h index b7aa521..187562b 100644 --- a/intuition-model/include/intuition_model.h +++ b/intuition-model/include/intuition_model.h @@ -33,6 +33,27 @@ typedef enum { AMIP_ROLE_TEXT, AMIP_ROLE_MENU, AMIP_ROLE_MENU_ITEM, + /* issue #69 -- WB3.2-era BOOPSI/ReAction classes ClassifyByClassID() + * didn't have a role for. AT-SPI-style names chosen to match ATK's + * own vocabulary where a clean equivalent exists (PAGE_TAB_LIST, + * COLOR_CHOOSER, PROGRESS_BAR, CALENDAR, TOOLBAR); the rest + * (COLOR_WHEEL, FILE_CHOOSER, FONT_CHOOSER, SCREENMODE_CHOOSER, + * PALETTE, CANVAS, TEXT_EDITOR) have no single ATK role but follow + * the same "what it does, not how it looks" naming convention. See + * intuition-model/src/walk.c's ClassifyByClassID() for the exact + * class-ID -> role mapping and its NDK-header provenance. */ + AMIP_ROLE_PAGE_TAB_LIST, /* clicktab.gadget */ + AMIP_ROLE_COLOR_WHEEL, /* colorwheel.gadget */ + AMIP_ROLE_CALENDAR, /* datebrowser.gadget */ + AMIP_ROLE_PROGRESS_BAR, /* fuelgauge.gadget */ + AMIP_ROLE_COLOR_CHOOSER, /* getcolor.gadget */ + AMIP_ROLE_FILE_CHOOSER, /* getfile.gadget */ + AMIP_ROLE_FONT_CHOOSER, /* getfont.gadget */ + AMIP_ROLE_SCREENMODE_CHOOSER, /* getscreenmode.gadget */ + AMIP_ROLE_PALETTE, /* palette.gadget */ + AMIP_ROLE_CANVAS, /* sketchboard.gadget */ + AMIP_ROLE_TOOLBAR, /* speedbar.gadget */ + AMIP_ROLE_TEXT_EDITOR, /* texteditor.gadget */ AMIP_ROLE_CUSTOM /* recognised structurally, but not classifiable */ } AmipRole; diff --git a/intuition-model/src/walk.c b/intuition-model/src/walk.c index ea7fe2b..95f57de 100644 --- a/intuition-model/src/walk.c +++ b/intuition-model/src/walk.c @@ -171,6 +171,100 @@ static AmipRole ClassifyByClassID(CONST_STRPTR classID) return AMIP_ROLE_LISTBROWSER; } + /* issue #69 -- confirmed against real NDK 3.2 headers (pragma/ + * *_lib.h's own ".gadget" comment, or reaction_macros.h's + * convenience-Object macros for the two classes -- colorwheel, + * gradientslider -- that register a PUBLIC class name instead of + * needing an explicit XXX_GetClass() call), not guessed, and + * live-verified against fixtures/reaction-classes-app (one + * instance of each, attached directly to a plain window so it's + * actually reachable -- see that fixture's own header for why it + * doesn't use window.class/layout.gadget the way classact-app + * does) plus, for clicktab.gadget specifically, the real WB3.2 + * stock app that motivated this issue (SYS:Prefs/ScreenMode). */ + if (strcmp(id, "clicktab.gadget") == 0) { + return AMIP_ROLE_PAGE_TAB_LIST; + } + if (strcmp(id, "colorwheel.gadget") == 0) { + return AMIP_ROLE_COLOR_WHEEL; + } + if (strcmp(id, "datebrowser.gadget") == 0) { + return AMIP_ROLE_CALENDAR; + } + if (strcmp(id, "fuelgauge.gadget") == 0) { + return AMIP_ROLE_PROGRESS_BAR; + } + if (strcmp(id, "getcolor.gadget") == 0) { + return AMIP_ROLE_COLOR_CHOOSER; + } + if (strcmp(id, "getfile.gadget") == 0) { + return AMIP_ROLE_FILE_CHOOSER; + } + if (strcmp(id, "getfont.gadget") == 0) { + return AMIP_ROLE_FONT_CHOOSER; + } + if (strcmp(id, "getscreenmode.gadget") == 0) { + return AMIP_ROLE_SCREENMODE_CHOOSER; + } + /* gradientslider.gadget is functionally a slider (a single + * draggable knob over a bounded range) -- AT-SPI doesn't have a + * separate "gradient slider" role either, sliders are sliders + * regardless of how the track is painted. Reuses the existing + * role rather than adding a redundant one. */ + if (strcmp(id, "gradientslider.gadget") == 0) { + return AMIP_ROLE_SLIDER; + } + if (strcmp(id, "palette.gadget") == 0) { + return AMIP_ROLE_PALETTE; + } + if (strcmp(id, "sketchboard.gadget") == 0) { + return AMIP_ROLE_CANVAS; + } + /* Registered class name is literally "speedbar", NOT + * "speedbar.gadget" -- confirmed live against + * fixtures/reaction-classes-app (every OTHER class here does + * follow the "name.gadget" convention; this one genuinely + * doesn't, an easy guess to get wrong from the library filename + * alone). */ + if (strcmp(id, "speedbar") == 0) { + return AMIP_ROLE_TOOLBAR; + } + if (strcmp(id, "texteditor.gadget") == 0) { + return AMIP_ROLE_TEXT_EDITOR; + } + + /* Deliberately NOT classified here, same issue #69 research pass + * (see userdocs/Locator-Tiers-and-Limits.md for the user-facing + * writeup of each): + * - space.gadget: a pure layout placeholder with no interactive + * state of its own (the class's own autodoc: "does more than + * just take up space" -- rendering is entirely the + * application's responsibility) -- arguably doesn't need a + * role at all, so it stays role=custom rather than inventing + * one nothing would ever query for. + * - virtual.gadget: a scrolling container for arbitrarily large + * groups -- its own children are exactly as unreachable as + * layout.gadget's (the CONFIRMED LIMIT above), so a role here + * would advertise a container this project can't actually see + * inside. + * - listview.gadget: the class's own autodoc says outright + * "listbrowser.gadget is a better alternative to this gadget" + * -- already covered by the existing listbrowser.gadget + * mapping above; not worth a second role for the superseded + * class. + * - tabs.gadget, tapedeck.gadget: both ship as real library + * files on a stock WB3.2.3 install (confirmed on disk), but + * neither has a documented, NDK-supported construction path + * in this SDK snapshot -- no XXX_GetClass() proto/pragma + * header and no reaction_macros.h convenience macro exists + * for either (unlike every class above). Can't build a + * fixture instance without guessing an undocumented API, so + * this is an honest gap, not a silent omission. + * - page.gadget: documented as "part of layout.gadget" itself + * (its own header comment) -- a layout-internal helper, not a + * standalone top-level object an application attaches + * directly; the CONFIRMED LIMIT above already covers it. */ + /* Genuinely unrecognised class (a third-party subclass, or a * ReAction class this tier hasn't been taught yet) -- className is * still captured for the caller, so the tree says exactly what it @@ -744,6 +838,18 @@ const char *AmipRoleName(AmipRole role) case AMIP_ROLE_TEXT: return "text"; case AMIP_ROLE_MENU: return "menu"; case AMIP_ROLE_MENU_ITEM: return "menu_item"; + case AMIP_ROLE_PAGE_TAB_LIST: return "page_tab_list"; + case AMIP_ROLE_COLOR_WHEEL: return "color_wheel"; + case AMIP_ROLE_CALENDAR: return "calendar"; + case AMIP_ROLE_PROGRESS_BAR: return "progress_bar"; + case AMIP_ROLE_COLOR_CHOOSER: return "color_chooser"; + case AMIP_ROLE_FILE_CHOOSER: return "file_chooser"; + case AMIP_ROLE_FONT_CHOOSER: return "font_chooser"; + case AMIP_ROLE_SCREENMODE_CHOOSER: return "screenmode_chooser"; + case AMIP_ROLE_PALETTE: return "palette"; + case AMIP_ROLE_CANVAS: return "canvas"; + case AMIP_ROLE_TOOLBAR: return "toolbar"; + case AMIP_ROLE_TEXT_EDITOR: return "text_editor"; case AMIP_ROLE_CUSTOM: return "custom"; default: return "unknown"; } @@ -780,6 +886,18 @@ AmipRole AmipRoleFromName(const char *name) { "text", AMIP_ROLE_TEXT }, { "menu", AMIP_ROLE_MENU }, { "menu_item", AMIP_ROLE_MENU_ITEM }, + { "page_tab_list", AMIP_ROLE_PAGE_TAB_LIST }, + { "color_wheel", AMIP_ROLE_COLOR_WHEEL }, + { "calendar", AMIP_ROLE_CALENDAR }, + { "progress_bar", AMIP_ROLE_PROGRESS_BAR }, + { "color_chooser", AMIP_ROLE_COLOR_CHOOSER }, + { "file_chooser", AMIP_ROLE_FILE_CHOOSER }, + { "font_chooser", AMIP_ROLE_FONT_CHOOSER }, + { "screenmode_chooser", AMIP_ROLE_SCREENMODE_CHOOSER }, + { "palette", AMIP_ROLE_PALETTE }, + { "canvas", AMIP_ROLE_CANVAS }, + { "toolbar", AMIP_ROLE_TOOLBAR }, + { "text_editor", AMIP_ROLE_TEXT_EDITOR }, { "custom", AMIP_ROLE_CUSTOM }, }; size_t i; diff --git a/tests/copperline/golden-test.py b/tests/copperline/golden-test.py index 881b5c6..2eef6f6 100644 --- a/tests/copperline/golden-test.py +++ b/tests/copperline/golden-test.py @@ -42,6 +42,8 @@ os.path.join(REPO_ROOT, "fixtures", "gadtools-app", "GTApp.golden")), ("CAAPP", "ClassAct", os.path.join(REPO_ROOT, "fixtures", "classact-app", "CAApp.golden")), + ("RCAPP", "ReAction", + os.path.join(REPO_ROOT, "fixtures", "reaction-classes-app", "ReactionClassesApp.golden")), ] diff --git a/tests/copperline/run.sh b/tests/copperline/run.sh index cf3e7dc..7cabc84 100755 --- a/tests/copperline/run.sh +++ b/tests/copperline/run.sh @@ -1783,6 +1783,7 @@ run_golden_check() { cat > "$SMOKE_SCRIPT" <NIL: SRC:build/fixtures/GTApp Run >NIL: SRC:build/fixtures/CAApp +Run >NIL: SRC:build/fixtures/ReactionClassesApp Wait 5 Run >NIL: SRC:build/AmiPilotServer SERIAL Wait 5 @@ -1833,7 +1834,7 @@ EOF rm -f "$info" ok=1 - for pattern in 'GOLDEN-GTAPP MATCH' 'GOLDEN-CAAPP MATCH'; do + for pattern in 'GOLDEN-GTAPP MATCH' 'GOLDEN-CAAPP MATCH' 'GOLDEN-RCAPP MATCH'; do if ! grep -qF "$pattern" "$BUILD/golden-result.txt" 2>/dev/null; then echo "run.sh: FAIL (golden): expected line not found: $pattern" ok=0 diff --git a/userdocs/Changelog.md b/userdocs/Changelog.md index 0f1aa01..71dfd9a 100644 --- a/userdocs/Changelog.md +++ b/userdocs/Changelog.md @@ -9,6 +9,30 @@ for the full engineering detail and phase sequencing behind each one. ## Unreleased +- **BOOPSI/ReAction role classification for 12 WB3.2-era gadget + classes** (issue #69): `clicktab.gadget`, `colorwheel.gadget`, + `datebrowser.gadget`, `fuelgauge.gadget`, `getcolor.gadget`, + `getfile.gadget`, `getfont.gadget`, `getscreenmode.gadget`, + `gradientslider.gadget`, `palette.gadget`, `sketchboard.gadget`, + `speedbar.gadget`, and `texteditor.gadget` — previously all + `role=custom` — now get a real, AT-SPI-style role + (`page_tab_list`/`color_wheel`/`calendar`/`progress_bar`/ + `color_chooser`/`file_chooser`/`font_chooser`/`screenmode_chooser`/ + `slider`/`palette`/`canvas`/`toolbar`/`text_editor`), addressable via + tier-2 `ROLE=` locators. `speedbar.gadget` turned up a real, + easy-to-guess-wrong exception: its registered class name is literally + `"speedbar"`, not `"speedbar.gadget"` like every other class here — + found live, not from documentation, which follows the + `"name.gadget"` pattern uniformly. A new fixture + (`fixtures/reaction-classes-app`) exercises one instance of each + class directly (deliberately not nested inside `window.class`/ + `layout.gadget`, which would make them unreachable the same way + issue #49 already documents), with a checked-in golden-tree file + locking in the live-confirmed output. `space.gadget`, + `virtual.gadget`, `listview.gadget`, `tabs.gadget`, and + `tapedeck.gadget` were deliberately left unclassified — see + [Locator Tiers and Limits](Locator-Tiers-and-Limits.md) for why each + one is an honest gap rather than an oversight. - **`WHERE`, the cooperative geometry port** (issue #49): the honest escape hatch for gadgets nested inside a `window.class` window's `layout.gadget` — permanently invisible to structural walking on diff --git a/userdocs/Locator-Tiers-and-Limits.md b/userdocs/Locator-Tiers-and-Limits.md index ba482eb..7465c54 100644 --- a/userdocs/Locator-Tiers-and-Limits.md +++ b/userdocs/Locator-Tiers-and-Limits.md @@ -59,13 +59,40 @@ live class name via `OCLASS()` — a documented NDK mechanism for exactly this, not a private hack — and maps known classes to a role: `button.gadget`, `checkbox.gadget`, `string.gadget`/`getstring.gadget`, `integer.gadget`, `radiobutton.gadget`, `chooser.gadget`, -`scroller.gadget`, `slider.gadget`, `listbrowser.gadget`. An unrecognised -class still gets its real name reported (`class="..."`, -`role=custom`) rather than a blank field. A gadget whose `GadgetType` -bits *claim* `GTYP_CUSTOMGADGET` but doesn't actually carry a real -BOOPSI object header (confirmed against a real, OS-shipped stock -application) degrades the same way — `role=custom`, no class or -label — rather than trusting the claim and dereferencing garbage. +`scroller.gadget`, `slider.gadget`, `listbrowser.gadget`, and (issue +#69) the WB3.2-era classes `clicktab.gadget` (`role=page_tab_list`), +`colorwheel.gadget` (`role=color_wheel`), `datebrowser.gadget` +(`role=calendar`), `fuelgauge.gadget` (`role=progress_bar`), +`getcolor.gadget` (`role=color_chooser`), `getfile.gadget` +(`role=file_chooser`), `getfont.gadget` (`role=font_chooser`), +`getscreenmode.gadget` (`role=screenmode_chooser`), +`gradientslider.gadget` (`role=slider` — functionally a slider variant, +no separate AT-SPI role exists for one either), `palette.gadget` +(`role=palette`), `sketchboard.gadget` (`role=canvas`), `speedbar.gadget` +(`role=toolbar` — registers its class as literally `"speedbar"`, not +`"speedbar.gadget"` like every other class here, a real, easy-to-guess- +wrong exception confirmed live), and `texteditor.gadget` +(`role=text_editor`). An unrecognised class still gets its real name +reported (`class="..."`, `role=custom`) rather than a blank field. A +gadget whose `GadgetType` bits *claim* `GTYP_CUSTOMGADGET` but doesn't +actually carry a real BOOPSI object header (confirmed against a real, +OS-shipped stock application) degrades the same way — `role=custom`, +no class or label — rather than trusting the claim and dereferencing +garbage. + +**Deliberately not classified** (issue #69's own research pass): +`space.gadget` (a pure layout placeholder with no interactive state of +its own — nothing a role would usefully describe), `virtual.gadget` (a +scrolling container whose children are exactly as unreachable as +`layout.gadget`'s — see the confirmed limit below), `listview.gadget` +(its own autodoc says outright "`listbrowser.gadget` is a better +alternative" — already covered by that mapping), and `tabs.gadget`/ +`tapedeck.gadget` (both ship as real library files on a stock WB3.2.3 +install, but neither has a documented, NDK-supported construction path +in this project's own NDK 3.2 snapshot — no `XXX_GetClass()` proto/ +pragma header and no `reaction_macros.h` convenience macro exists for +either, unlike every class classified above — an honest gap, not a +guess). ## Documented gaps