amiga-bootcamp/09_intuition/boopsi.md
Ilia Sharin 4d136b0672 doc: Intuition bootcamp — comprehensive enrichment of all subsystem articles
Transformed 8 stub articles (720 lines total) into full bootcamp-grade
references (5,100+ lines) with architecture diagrams, complete code
examples, struct field references, pitfalls, and best practices.

IDCMP (idcmp.md — 1,060 lines):
- IntuiMessage field reference with types, sizes, value ranges
- Use-case cookbook: double-click, rubber-band, multi-signal, BOOPSI
- When to use IDCMP vs alternatives (decision flowchart)
- 5 named antipatterns with WRONG/RIGHT code
- Memory safety checklist and defensive event loop template
- Cross-platform comparison table (Win32, X11, Cocoa, Qt)

Input Events (input_events.md — 850 lines):
- Event class routing and QoS analysis (priority-as-QoS hierarchy)
- Intuition consumption table (what survives the handler chain)
- lowlevel.library bypass for CD32/game input
- Latency analysis with pipeline timing budget (68000 vs 68040)
- Input strategy comparison (IDCMP vs handler vs direct hardware)
- Game input patterns: direct HW polling, CIA keyboard, hybrid handler
- Signal pattern for proper handler→application communication

Windows (windows.md — 370 lines):
- Window anatomy diagram, WA_ tag reference tables
- Refresh modes comparison (Simple/Smart/SuperBitMap)
- Window types: standard, backdrop, borderless, GimmeZeroZero
- Coordinate system, border offsets, struct Window fields
- Modification API (move, resize, title, busy pointer)

Gadgets (gadgets.md — 403 lines):
- Three-generation evolution (raw/GadTools/BOOPSI)
- Complete GadTools lifecycle with setup, creation, events, cleanup
- Runtime state updates (GadTools vs raw Intuition)
- Raw struct Gadget: GFLG_*, GACT_*, GTYP_* flag tables
- Proportional and string gadget internals

BOOPSI (boopsi.md — 505 lines):
- Class hierarchy Mermaid diagram
- Method dispatch sequence diagram
- ICA interconnection architecture (icclass, modelclass, ICTARGET_IDCMP)
- Full custom gadget class tutorial (4 steps with complete code)
- Built-in class reference table
- C++/Qt analog comparison table

Menus (menus.md — 378 lines):
- Menu hierarchy diagram (Menu → Item → Sub-Item)
- GadTools NewMenu workflow with field reference
- Event handling with multi-select chain walking
- Checkmark/mutual exclusion, sub-menus, dynamic modification
- Keyboard shortcut system (single-char and NM_COMMANDSTRING)

Requesters (requesters.md — 370 lines):
- EasyRequest return value logic table
- ASL file requester with multi-select and full tag reference
- ASL font and screenmode requesters
- Non-blocking BuildEasyRequest/SysReqHandler pattern
- Requester state persistence

IntuitionBase (intuition_base.md — 267 lines):
- Library version table (OS 1.2 through 3.2.x)
- Struct field reference with safety annotations
- ViewLord architecture diagram
- LockIBase vs Forbid/Permit guidance
- Complete library function overview (5 categories)

README index updated with enriched article descriptions.
2026-04-23 17:29:18 -04:00

16 KiB

← Home · Intuition

BOOPSI — Basic Object-Oriented Programming System for Intuition

What Is BOOPSI?

BOOPSI is AmigaOS's built-in object-oriented framework for creating reusable, interconnectable UI components. Introduced in OS 2.0 (1990), it predates Java, Qt's signal/slot system, and COM — yet implements many of the same patterns: inheritance, encapsulation, message-based dispatch, and reactive property binding.

BOOPSI is the foundation on which all modern Amiga GUI frameworks are built:

graph TB
    BOOPSI["BOOPSI<br/>(rootclass)"] --> GADGET["gadgetclass"]
    BOOPSI --> IMAGE["imageclass"]
    BOOPSI --> IC["icclass / modelclass"]
    GADGET --> PROP["propgclass<br/>(sliders)"]
    GADGET --> STR["strgclass<br/>(strings)"]
    GADGET --> BUTTON["buttongclass"]
    GADGET --> FRBUT["frbuttongclass"]
    GADGET --> GROUP["groupgclass"]
    GADGET --> CUSTOM["Your Custom Class"]
    IMAGE --> FRAME["frameiclass"]
    IMAGE --> FILL["fillrectclass"]
    IMAGE --> SYSI["sysiclass<br/>(system images)"]
    IC --> MODEL["modelclass"]

    style BOOPSI fill:#e8f4fd,stroke:#2196f3,color:#333
    style CUSTOM fill:#fff3e0,stroke:#ff9800,color:#333

Why BOOPSI Matters

Problem Before BOOPSI With BOOPSI
Custom gadgets Rewrite rendering, hit-testing, state from scratch Inherit from gadgetclass — get all behavior free
Gadget communication Application must manually shuttle values between gadgets ICA interconnection — gadgets notify each other directly
Consistent look Every app draws gadgets differently Inherit standard imagery and behavior
Extensibility Requires source code modification Subclass and override only what you need

Core Concepts

1. Classes, Objects, and Methods

Concept Amiga BOOPSI C++ Analog Qt Analog
Class struct IClass class definition QObject subclass
Object Object * (opaque) Class instance QObject *
Method DoMethod(obj, MethodID, ...) obj->method() QMetaObject::invokeMethod()
Attribute SetAttrs(obj, TAG_ID, value) obj->setX(val) obj->setProperty("x", val)
Superclass DoSuperMethod(cl, obj, msg) Base class call QObject::event() chain
Instance data INST_DATA(cl, obj) this->member d_ptr (Pimpl)
Interconnection ICA_TARGET + ICA_MAP Observer pattern connect(signal, slot)

2. The Dispatcher

Every class has a single dispatcher function — the equivalent of a virtual method table compressed into a switch statement:

ULONG MyDispatcher(struct IClass *cl, Object *obj, Msg msg)
{
    switch (msg->MethodID)
    {
        case OM_NEW:     return MyNew(cl, obj, (struct opSet *)msg);
        case OM_DISPOSE: return MyDispose(cl, obj, msg);
        case OM_SET:     return MySet(cl, obj, (struct opSet *)msg);
        case OM_GET:     return MyGet(cl, obj, (struct opGet *)msg);
        case OM_UPDATE:  return MySet(cl, obj, (struct opSet *)msg);
        default:         return DoSuperMethodA(cl, obj, msg);
    }
}

3. Method Dispatch Flow

sequenceDiagram
    participant App as Application
    participant Obj as Object
    participant Disp as Dispatcher
    participant Super as Superclass

    App->>Obj: DoMethod(obj, OM_SET, tags)
    Obj->>Disp: Dispatcher(cl, obj, msg)
    Disp->>Disp: Handle known attributes
    Disp->>Super: DoSuperMethodA(cl, obj, msg)
    Super-->>Disp: Return value
    Disp-->>App: Return value

Standard Methods

Object Lifecycle

Method Message Struct When Called Your Job
OM_NEW struct opSet Object creation (NewObject()) Allocate resources, parse initial tags, call super first
OM_DISPOSE Msg (base) Object destruction (DisposeObject()) Free your resources, then call super

Attribute Access

Method Message Struct When Called Your Job
OM_SET struct opSet SetAttrs() / SetGadgetAttrs() Parse tag list, update internal state, notify if changed
OM_GET struct opGet GetAttr() Check opg_AttrID, store value in *opg_Storage
OM_UPDATE struct opUpdate ICA notification from another object Same as OM_SET, but check opu_Flags for interim updates
OM_NOTIFY struct opUpdate Internal — trigger notifications to ICA targets Called by your OM_SET handler when attributes change

Gadget-Specific Methods

Method When Called Your Job
GM_RENDER Intuition needs gadget redrawn Draw the gadget using the provided GadgetInfo
GM_HITTEST Mouse click — is it inside your gadget? Return GMR_GADGETHIT or 0
GM_GOACTIVE Gadget becomes active (clicked) Begin interaction; return GMR_MEACTIVE to stay active
GM_HANDLEINPUT Mouse/key events while active Process input; return GMR_MEACTIVE, GMR_NOREUSE, or GMR_REUSE
GM_GOINACTIVE Gadget deactivated Clean up interaction state

Creating Objects

Using Built-in Classes

/* Create a proportional gadget (slider) */
Object *slider = NewObject(NULL, "propgclass",
    GA_ID,        1,
    GA_Left,      20,
    GA_Top,       40,
    GA_Width,     200,
    GA_Height,    16,
    GA_RelVerify, TRUE,
    PGA_Freedom,  FREEHORIZ,
    PGA_Total,    100,
    PGA_Top,      0,
    PGA_Visible,  10,
    TAG_DONE);

/* Add to window */
AddGadget(win, (struct Gadget *)slider, -1);
RefreshGList((struct Gadget *)slider, win, NULL, 1);

/* Read current position */
LONG pos;
GetAttr(PGA_Top, slider, (ULONG *)&pos);

/* Update position */
SetGadgetAttrs((struct Gadget *)slider, win, NULL,
    PGA_Top, 50,
    TAG_DONE);

/* Destroy */
RemoveGadget(win, (struct Gadget *)slider);
DisposeObject(slider);

Using a Class Pointer

/* When you have a Class * instead of a name */
struct IClass *myClass = MakeMyClass();
Object *obj = NewObject(myClass, NULL,
    MY_Attribute, value,
    TAG_DONE);

ICA — Interconnection Architecture

The most powerful BOOPSI feature: reactive data binding between objects without application intervention.

Direct Connection (icclass)

graph LR
    SLIDER["Slider<br/>(propgclass)"] -->|"PGA_Top changes"| IC["icclass<br/>(mapper)"]
    IC -->|"Mapped attribute"| READOUT["Number Display<br/>(strgclass)"]

    style IC fill:#e8f4fd,stroke:#2196f3,color:#333
/* Map slider position to string gadget value */
struct TagItem sliderToReadout[] = {
    { PGA_Top, STRINGA_LongVal },
    { TAG_DONE, 0 }
};

/* Create interconnection */
SetAttrs(slider,
    ICA_TARGET, readout,          /* Target object */
    ICA_MAP,    sliderToReadout,  /* Attribute mapping */
    TAG_DONE);

/* Now when slider moves, readout updates automatically! */

Broadcast (modelclass)

For one-to-many notifications:

/* Create a model (data source) */
Object *model = NewObject(NULL, "modelclass",
    ICA_TARGET, ICTARGET_IDCMP,  /* Also notify application */
    TAG_DONE);

/* Add multiple views */
DoMethod(model, OM_ADDMEMBER, slider);
DoMethod(model, OM_ADDMEMBER, readout);
DoMethod(model, OM_ADDMEMBER, gauge);

/* Update the model — all views update automatically */
SetAttrs(model, MY_VALUE, 42, TAG_DONE);

ICA → IDCMP Bridge

When ICA_TARGET is ICTARGET_IDCMP, attribute changes generate IDCMP_IDCMPUPDATE messages:

SetAttrs(slider,
    ICA_TARGET, ICTARGET_IDCMP,
    TAG_DONE);

/* In event loop: */
case IDCMP_IDCMPUPDATE:
{
    struct TagItem *tags = (struct TagItem *)msg->IAddress;
    LONG value = GetTagData(PGA_Top, 0, tags);
    UpdateApplication(value);
    break;
}

Writing a Custom Class

Step 1: Define Instance Data

struct MyGadgetData {
    LONG  value;
    LONG  minVal;
    LONG  maxVal;
    UWORD fgPen;
    UWORD bgPen;
    BOOL  active;
};

Step 2: Implement the Dispatcher

ULONG MyGadgetDispatcher(struct IClass *cl, Object *obj, Msg msg)
{
    struct MyGadgetData *data;

    switch (msg->MethodID)
    {
        case OM_NEW:
        {
            /* Let superclass create the object first */
            Object *newObj = (Object *)DoSuperMethodA(cl, obj, msg);
            if (!newObj) return 0;

            data = INST_DATA(cl, newObj);
            data->value  = 0;
            data->minVal = 0;
            data->maxVal = 100;
            data->fgPen  = 1;
            data->bgPen  = 0;
            data->active = FALSE;

            /* Parse initial tags */
            struct TagItem *tags = ((struct opSet *)msg)->ops_AttrList;
            struct TagItem *tag;
            while ((tag = NextTagItem(&tags)))
            {
                switch (tag->ti_Tag)
                {
                    case MYGA_Value:  data->value  = tag->ti_Data; break;
                    case MYGA_Min:    data->minVal = tag->ti_Data; break;
                    case MYGA_Max:    data->maxVal = tag->ti_Data; break;
                }
            }

            return (ULONG)newObj;
        }

        case OM_SET:
        case OM_UPDATE:
        {
            data = INST_DATA(cl, obj);
            struct TagItem *tags = ((struct opSet *)msg)->ops_AttrList;
            struct TagItem *tag;
            BOOL refresh = FALSE;

            while ((tag = NextTagItem(&tags)))
            {
                switch (tag->ti_Tag)
                {
                    case MYGA_Value:
                        if (data->value != tag->ti_Data)
                        {
                            data->value = tag->ti_Data;
                            refresh = TRUE;
                        }
                        break;
                }
            }

            /* Propagate to superclass */
            DoSuperMethodA(cl, obj, msg);

            /* Notify ICA targets of changes */
            if (refresh)
            {
                struct TagItem notify[] = {
                    { MYGA_Value, data->value },
                    { TAG_DONE, 0 }
                };
                DoSuperMethod(cl, obj, OM_NOTIFY, notify,
                    ((struct opSet *)msg)->ops_GInfo, 0);
            }

            return refresh;
        }

        case OM_GET:
        {
            data = INST_DATA(cl, obj);
            struct opGet *opg = (struct opGet *)msg;

            switch (opg->opg_AttrID)
            {
                case MYGA_Value:
                    *opg->opg_Storage = data->value;
                    return TRUE;
                default:
                    return DoSuperMethodA(cl, obj, msg);
            }
        }

        case GM_RENDER:
        {
            data = INST_DATA(cl, obj);
            struct gpRender *gpr = (struct gpRender *)msg;
            struct RastPort *rp = gpr->gpr_RPort;
            struct Gadget *g = (struct Gadget *)obj;

            /* Draw the gadget */
            SetAPen(rp, data->bgPen);
            RectFill(rp, g->LeftEdge, g->TopEdge,
                     g->LeftEdge + g->Width - 1,
                     g->TopEdge + g->Height - 1);

            /* Draw value bar */
            LONG barWidth = (data->value - data->minVal) *
                            g->Width / (data->maxVal - data->minVal);
            SetAPen(rp, data->fgPen);
            RectFill(rp, g->LeftEdge, g->TopEdge,
                     g->LeftEdge + barWidth - 1,
                     g->TopEdge + g->Height - 1);

            return TRUE;
        }

        case GM_HITTEST:
        {
            /* Simple rectangular hit test — already handled by gadgetclass */
            return GMR_GADGETHIT;
        }

        default:
            return DoSuperMethodA(cl, obj, msg);
    }
}

Step 3: Register the Class

struct IClass *MyGadgetClass = NULL;

struct IClass *InitMyGadgetClass(void)
{
    MyGadgetClass = MakeClass(
        NULL,                          /* Public name (NULL = private) */
        "gadgetclass",                 /* Superclass name */
        NULL,                          /* Superclass pointer (alt.) */
        sizeof(struct MyGadgetData),   /* Instance data size */
        0                              /* Flags */
    );

    if (MyGadgetClass)
        MyGadgetClass->cl_Dispatcher.h_Entry = (HOOKFUNC)MyGadgetDispatcher;

    return MyGadgetClass;
}

/* For a public class (usable by other programs): */
struct IClass *pubClass = MakeClass(
    "mygadget.class",     /* Public name */
    "gadgetclass",
    NULL,
    sizeof(struct MyGadgetData),
    0
);
AddClass(pubClass);       /* Register with Intuition */

Step 4: Use Your Class

struct IClass *cl = InitMyGadgetClass();

Object *gauge = NewObject(cl, NULL,
    GA_Left,    20,
    GA_Top,     60,
    GA_Width,   200,
    GA_Height,  20,
    MYGA_Value, 50,
    MYGA_Min,   0,
    MYGA_Max,   100,
    TAG_DONE);

/* Connect slider to gauge via ICA */
SetAttrs(slider,
    ICA_TARGET, gauge,
    ICA_MAP,    sliderToGauge,
    TAG_DONE);

Built-in Class Hierarchy

Class Superclass Purpose
rootclass Base of all BOOPSI objects
imageclass rootclass Base for all images
frameiclass imageclass Recessed/raised 3D frames
sysiclass imageclass System images (arrows, checkmarks)
fillrectclass imageclass Filled rectangles with patterns
gadgetclass rootclass Base for all gadgets
propgclass gadgetclass Proportional (slider) gadgets
strgclass gadgetclass String input gadgets
buttongclass gadgetclass Push buttons
frbuttongclass gadgetclass Framed push buttons
groupgclass gadgetclass Container for gadget groups
icclass rootclass Interconnection (1-to-1 binding)
modelclass icclass Interconnection (1-to-many broadcast)

Pitfalls

1. Forgetting DoSuperMethodA

If you handle OM_NEW without calling DoSuperMethodA() first, the object is never properly allocated. If you handle OM_DISPOSE without calling super last, the base class never frees its memory.

2. OM_SET Without OM_NOTIFY

If you update internal state in OM_SET but don't call OM_NOTIFY, ICA connections are silently broken — connected objects never update.

3. Wrong INST_DATA Timing

In OM_NEW, you must call DoSuperMethodA() before INST_DATA(). The superclass allocates the memory that INST_DATA() points to.

4. Modifying Gadget Attrs Without Window

SetGadgetAttrs() requires a window pointer to trigger a visual refresh. SetAttrs() alone updates internal state but doesn't redraw.

5. ICA Map Memory Management

The ICA_MAP tag list must persist for the lifetime of the connection. If you pass a stack-allocated array, it becomes garbage when the function returns.


Best Practices

  1. Always call DoSuperMethodA() for methods you partially handle — let the superclass do its job
  2. Use NextTagItem() to iterate tag lists — it handles TAG_SKIP, TAG_MORE, etc.
  3. Send OM_NOTIFY whenever a settable attribute changes — this drives ICA
  4. Keep dispatch fast — complex rendering should be deferred to GM_RENDER
  5. Use ICA_TARGET = ICTARGET_IDCMP when the application needs to know about changes
  6. Allocate ICA_MAP arrays statically or in instance data — not on the stack
  7. Private classes (NULL name) are simpler and sufficient for most applications
  8. Use modelclass for MVC patterns — it's the Amiga's built-in observer

References

  • NDK 3.9: intuition/classes.h, intuition/classusr.h, intuition/gadgetclass.h, intuition/imageclass.h, intuition/icclass.h
  • ADCD 2.1: NewObject(), DisposeObject(), SetAttrs(), GetAttr(), DoMethod()
  • AmigaOS Reference Manual (RKRM): Libraries, Chapter 8 — BOOPSI
  • See also: Gadgets, MUI Framework