toon-boom-extension-framework/docs/AC_Responder_System.md

20 KiB

AC_Responder System - Toolbar Button Click Handling

This document explains how toolbar button clicks are routed to responders in Toon Boom Harmony Premium and Storyboard Pro, and how to create custom responders to handle actions.

IDA Databases:

  • RE/ToonBoomActionManager.dll.i64 - AC_Manager and AC_Responder implementations
  • RE/ToonBoomLayout.dll.i64 - Layout responder implementations
  • RE/HarmonyPremium.exe.i64 - Application-specific responders

Overview

Toon Boom uses a Responder Chain pattern (similar to macOS/Cocoa) for handling toolbar button clicks:

  1. Toolbar XML defines button items with a responder attribute and a slot attribute
  2. When clicked, AC_Manager looks up the responder by name
  3. The responder's slot is invoked via Qt's meta-object system
  4. If the responder doesn't handle it, the action propagates up the chain

Toolbar XML Structure

From toolbars.xml:

<toolbar id="DrawingToolToolbar" text="Tools">
  <!-- Simple item with responder and slot -->
  <item 
    checkable="true" 
    icon="drawingtool/select.png" 
    id="SelectTool" 
    responder="sceneUI" 
    slot="onActionChooseSelectToolInNormalMode()" 
    text="Select" />
  
  <!-- Item with owner responder (view that owns the toolbar) -->
  <item 
    icon="library/refresh.png" 
    id="LIBRARY_REFRESH" 
    responder="owner" 
    slot="onActionFolderRefresh()" 
    text="Refresh" />
    
  <!-- Item with script execution -->
  <item 
    icon="script/createkeyframeson.png" 
    id="CREATE_KEYFRAMES_ON" 
    itemParameter="TB_CreateKeyFramesOn in TB_CreateKeyFramesOn.js" 
    responder="scriptResponder" 
    slot="onActionExecuteScript(QString)" 
    text="Create Keyframes On" />
</toolbar>

Key XML Attributes

Attribute Description
id Unique identifier for the toolbar item
responder Name of the responder to handle clicks
slot Qt slot signature to invoke
icon Path to icon image
text Display text / tooltip
checkable If "true", button toggles on/off
condition Expression for conditional visibility
itemParameter Extra parameter passed to slot
shortcut Keyboard shortcut name

Common Responder Names

Responder Description
owner The QObject that "owns" the toolbar (usually the view)
sceneUI Main scene UI responder (Harmony_SceneUI / SBoard_SceneUI)
scene Scene data responder
selection Selection responder (for cut/copy/paste)
timelineView Timeline view responder
xsheetView Xsheet view responder
onionSkinResponder Onion skin feature responder
scriptResponder Script execution responder
artLayerResponder Art layer selection responder

The Responder Chain

When a toolbar button is clicked:

┌─────────────────────────────────────────────────────────────────┐
│ 1. User clicks toolbar button                                   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. AC_ToolbarItemImpl triggers AC_ActionInfo                    │
│    - Creates AC_ActionInfo with slot name, parameters           │
│    - Gets responder name from XML attribute                     │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. AC_Manager::responder(name) looks up responder               │
│    - "owner" → toolbar's owner QObject cast to AC_Responder     │
│    - other names → registered responder by identity             │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. AC_Responder::perform(AC_ActionInfo*) is called              │
│    - Sets responder in action info                              │
│    - Invokes slot via QMetaObject::invokeMethod                 │
│    - Returns AC_Result (Handled, NotHandled, Error)             │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. If NotHandled, propagate up chain                            │
│    - Try parent responder via parentResponder()                 │
│    - Eventually reaches applicationResponder                    │
└─────────────────────────────────────────────────────────────────┘

AC_Responder Interface

The AC_Responder abstract interface defines how objects participate in the responder chain:

class AC_Responder {
public:
    virtual ~AC_Responder() = 0;
    
    // Identity
    virtual const QString& responderIdentity() const = 0;
    virtual const QString& responderDescription() const = 0;
    virtual void setResponderDescription(const QString& desc) = 0;
    
    // Chain navigation
    virtual AC_Responder* parentResponder() = 0;
    virtual AC_Responder* proxyResponder() = 0;
    
    // First responder status (keyboard focus)
    virtual bool acceptsFirstResponder() = 0;
    virtual bool becomeFirstResponder() = 0;
    virtual bool resignFirstResponder() = 0;
    
    // Selection responder status
    virtual bool acceptsSelectionResponder() = 0;
    virtual bool becomeSelectionResponder() = 0;
    virtual bool resignSelectionResponder() = 0;
    
    // Action handling
    virtual AC_Result perform(AC_ActionInfo* info) = 0;
    virtual AC_Result performDownToChildren(AC_ActionInfo* info) = 0;
    virtual bool shouldReceiveMessages() const = 0;
    virtual bool handleShortcuts() const = 0;
    
    // Event handling
    virtual AC_Result handleEvent(QEvent* event) = 0;
    
    // Manager access
    virtual AC_Manager* actionManager() const = 0;
};

AC_ResponderBase Helper Class

The framework provides AC_ResponderBase, a concrete helper class that implements the AC_Responder interface with sensible defaults. This makes it easy to create custom responders without implementing every virtual method:

// From ac_manager.hpp
class AC_ResponderBase : public AC_Responder {
public:
    AC_ResponderBase(const QString& identity, AC_Manager* manager = nullptr,
                     AC_Responder* parent = nullptr);
    virtual ~AC_ResponderBase() = default;
    
    // Identity - stored internally
    const QString& responderIdentity() const override;
    const QString& responderDescription() const override;
    void setResponderDescription(const QString& desc) override;
    
    // Chain - returns parent passed to constructor
    AC_Responder* parentResponder() override;
    AC_Responder* proxyResponder() override;  // returns nullptr
    
    // First responder - all return false by default
    bool acceptsFirstResponder() override;
    bool becomeFirstResponder() override;
    bool resignFirstResponder() override;
    
    // Selection responder - all return false by default
    bool acceptsSelectionResponder() override;
    bool becomeSelectionResponder() override;
    bool resignSelectionResponder() override;
    
    // Action handling - return NotHandled by default
    AC_Result perform(AC_ActionInfo* info) override;
    AC_Result performDownToChildren(AC_ActionInfo* info) override;
    
    // Message handling - return true by default
    bool shouldReceiveMessages() const override;
    bool handleShortcuts() const override;
    
    // Event handling - returns NotHandled
    AC_Result handleEvent(QEvent* event) override;
    
    // Manager access
    AC_Manager* actionManager() const override;
    void setActionManager(AC_Manager* manager);

protected:
    QString m_identity;
    QString m_description;
    AC_Manager* m_manager;
    AC_Responder* m_parentResponder;
};

Creating a Custom Responder

Method 1: Using AC_ResponderBase with QObject

The simplest approach is to create a class that inherits from both QObject (for Qt slots) and AC_ResponderBase (for responder functionality):

#include <toon_boom/ac_manager.hpp>

class MyResponder : public QObject, public AC_ResponderBase {
    Q_OBJECT
    
public:
    MyResponder(const QString& identity, AC_Manager* manager, QObject* parent = nullptr)
        : QObject(parent)
        , AC_ResponderBase(identity, manager)
    {
        // Register with AC_Manager
        manager->registerResponder(this, nullptr);
    }
    
    ~MyResponder() {
        if (AC_Manager* mgr = actionManager()) {
            mgr->unregisterResponder(this);
        }
    }

public slots:
    // Slot that matches toolbar XML slot signature
    void onActionMyCustomAction() {
        qDebug() << "My custom action triggered!";
    }
    
    void onActionWithParameter(const QString& param) {
        qDebug() << "Action with parameter:" << param;
    }
    
    // Validate slot - called before action to update enabled/checked state
    void onActionMyCustomActionValidate(AC_ActionInfo* info) {
        // Enable based on some condition
        info->setEnabled(canPerformAction());
        // Checkable state is controlled by an additional (currently-opaque) vfunc
        // on the `AC_ActionInfo*` implementation, not by a public `setChecked(...)` API.
    }
    
private:
    bool canPerformAction() const { return true; }
    bool isActionActive() const { return false; }
};

Method 2: Widget-Based Responder

For a widget that also acts as a responder:

#include <toon_boom/ac_manager.hpp>
#include <QWidget>

class MyCustomWidget : public QWidget, public AC_ResponderBase {
    Q_OBJECT
    
public:
    MyCustomWidget(const QString& identity, AC_Manager* manager, QWidget* parent = nullptr)
        : QWidget(parent)
        , AC_ResponderBase(identity, manager)
    {
        // Register with AC_Manager, passing 'this' as the associated widget
        manager->registerResponder(this, this);
    }
    
    ~MyCustomWidget() {
        if (AC_Manager* mgr = actionManager()) {
            mgr->unregisterResponder(this);
        }
    }

    AC_Result perform(AC_ActionInfo* info) override {
        if (!info) {
            return AC_Result::NotHandled;
        }

        // IDA-verified: internal widget responders call `info->invokeOnQObject(widget)`.
        // This uses Qt's metaobject (`QMetaObject::indexOfSlot`), so your class must
        // have `Q_OBJECT` (and be built with moc/automoc) for the slots to be found.
        // If it returns NotHandled, they propagate to parentResponder().
        AC_Result result = info->invokeOnQObject(this);
        if (result == AC_Result::NotHandled) {
            if (AC_Responder* parent = parentResponder()) {
                return parent->perform(info);
            }
        }
        return result;
    }

public slots:
    void onActionWidgetAction() {
        qDebug() << "Widget action triggered!";
    }
    
    void onActionWidgetActionValidate(AC_ActionInfo* info) {
        info->setEnabled(isEnabled());
    }
};

Method 3: Implementing AC_Responder Directly

For full control over the responder interface:

class MyFullResponder : public QObject, public AC_Responder {
    Q_OBJECT
    
public:
    MyFullResponder(const QString& identity, AC_Manager* manager)
        : m_identity(identity), m_manager(manager)
    {
        manager->registerResponder(this, nullptr);
    }
    
    // AC_Responder interface - implement all methods
    const QString& responderIdentity() const override { return m_identity; }
    const QString& responderDescription() const override { return m_description; }
    void setResponderDescription(const QString& desc) override { m_description = desc; }
    
    AC_Responder* parentResponder() override { return nullptr; }
    AC_Responder* proxyResponder() override { return nullptr; }
    
    bool acceptsFirstResponder() override { return false; }
    bool becomeFirstResponder() override { return false; }
    bool resignFirstResponder() override { return false; }
    
    bool acceptsSelectionResponder() override { return false; }
    bool becomeSelectionResponder() override { return false; }
    bool resignSelectionResponder() override { return false; }
    
    AC_Result perform(AC_ActionInfo* info) override {
        if (!info) {
            return AC_Result::NotHandled;
        }
        return info->invokeOnQObject(this);
    }
    
    AC_Result performDownToChildren(AC_ActionInfo* info) override {
        return AC_Result::NotHandled;
    }
    
    bool shouldReceiveMessages() const override { return true; }
    bool handleShortcuts() const override { return true; }
    
    AC_Result handleEvent(QEvent*) override { return AC_Result::NotHandled; }
    
    AC_Manager* actionManager() const override { return m_manager; }

public slots:
    void onActionDoSomething() {
        // Handle the action
    }

private:
    QString m_identity;
    QString m_description;
    AC_Manager* m_manager;
};

AC_ActionInfo Structure

When an action is triggered or validated, Toon Boom passes an AC_ActionInfo* to responders.

Important ABI correction (IDA-verified): AC_ActionInfo is not a QObject. RTTI in ToonBoomActionManager.dll shows:

  • AC_ActionInfoImpl derives from AC_ActionInfo
  • AC_ActionInfo derives from AC_ActionData

So, “action info” methods like setEnabled(bool) are actually exported as AC_ActionData::setEnabled(bool) and are inherited by AC_ActionInfo.

Important return-value detail (IDA-verified): action invocation/validation helpers return:

  • 0 = handled/success
  • 1 = not handled / slot not found
class AC_ActionData {
public:
  bool isValidation() const;
  void setEnabled(bool enabled);
  void setVisible(bool visible);
};

class AC_ActionInfo : public AC_ActionData {
  // Most fields/methods are opaque; Toon Boom provides concrete impls.
};

Validation Pattern

Before displaying a menu or when the UI updates, Toon Boom calls validation methods:

  1. For each toolbar item, Toon Boom tries to validate via a derived validate slot name (<slotName>Validate).
  2. If the validate slot exists, it is invoked and is expected to update state (typically via info->setEnabled(...), and optionally a checked/visible-like vfunc).
  3. If the validate slot does not exist, Toon Boom can fall back to enabling the action if the action slot exists.
  4. If neither validate nor action slot exists on the resolved responder, the action is disabled.

Example from TULayoutManager:

// Address: 0x7ffa0be52e60
void TULayoutManager::onActionFullscreenValidate(AC_ActionInfo* info) {
    // 1) Calls AC_ActionInfo vfunc @ +0x38 with a byte flag from `this+0xC0`
    //    (used to update a check/visible-like state on the action).
    // 2) Always enables the action:
    info->setEnabled(true); // AC_ActionData::setEnabled
}

See also: docs/AC_Toolbar_ButtonEnablement.md:1

Registration with AC_Manager

Responders must be registered to be found by name:

AC_Manager* manager = PLUG_Services::getActionManager();

// Register a responder
bool success = manager->registerResponder(myResponder, myWidget);

// Unregister when done
manager->unregisterResponder(myResponder);

// Find a responder by identity
AC_Responder* resp = manager->responder("myResponderIdentity");

// Get responder for a widget
AC_Responder* widgetResp = manager->responderForWidget(someWidget);

Example: Adding a Custom Toolbar Button

Step 1: Define in XML (or load programmatically)

<toolbar id="MyToolbar" text="My Tools">
  <item 
    id="MyAction" 
    icon="my/icon.png"
    responder="myCustomResponder" 
    slot="onActionMyAction()" 
    text="My Action" />
</toolbar>

Step 2: Create and Register Responder

class MyToolResponder : public QObject, public AC_ResponderBase {
    Q_OBJECT
    
public:
    MyToolResponder(AC_Manager* manager)
        : QObject()
        , AC_ResponderBase("myCustomResponder", manager)
    {
        manager->registerResponder(this, nullptr);
    }
    
    ~MyToolResponder() {
        if (AC_Manager* mgr = actionManager()) {
            mgr->unregisterResponder(this);
        }
    }

public slots:
    void onActionMyAction() {
        qDebug() << "My custom toolbar button clicked!";
        // Do your custom action here
    }
    
    void onActionMyActionValidate(AC_ActionInfo* info) {
        info->setEnabled(true);
    }
};

Step 3: Initialize

void initializeMyToolbar() {
    AC_Manager* manager = PLUG_Services::getActionManager();
    
    // Create and register responder
    MyToolResponder* responder = new MyToolResponder(manager);
    
    // Load toolbar XML
    QDomDocument doc;
    doc.setContent(myToolbarXmlString);
    QList<QString> ids;
    manager->loadToolbars(doc.documentElement(), ids);
    
    // Show the toolbar
    TULayoutManager* layoutManager = getLayoutManager();
    layoutManager->showToolbar("MyToolbar", true);
}

Owner Responder Pattern

When responder="owner" is used, the toolbar's owner QObject is used:

// In AC_ToolbarImpl, owner is stored at offset +0xE0
void AC_ToolbarImpl::setOwner(QObject* owner) {
    m_owner = owner;
    connect(owner, &QObject::destroyed, this, &AC_ToolbarImpl::ownerDestroyed);
}

// When resolving "owner" responder:
if (responderName == "owner") {
    QObject* owner = toolbar->owner();
    return qobject_cast<AC_Responder*>(owner);
}

This pattern is commonly used for view-specific toolbars where the view itself handles the actions.

Key Addresses

ToonBoomActionManager.dll

Symbol Address Description
AC_ManagerImpl::registerResponder (via vtable) Registers responder
AC_ManagerImpl::unregisterResponder (via vtable) Unregisters responder
AC_ManagerImpl::responder (via vtable) Finds responder by name

Summary

To respond to toolbar button clicks:

  1. Create a class that inherits from QObject and AC_ResponderBase (or implement AC_Responder directly)
  2. Register with AC_Manager::registerResponder(responder, widget)
  3. Define slots matching the toolbar XML slot attribute signatures
  4. Optionally define validate slots (<slotName>Validate(AC_ActionInfo*)) for state updates
  5. Reference your responder by identity in the toolbar XML responder attribute