# 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`:
```xml
```
### 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:
```cpp
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:
```cpp
// 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):
```cpp
#include
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:
```cpp
#include
#include
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:
```cpp
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
```cpp
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 (`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`:
```cpp
// 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:
```cpp
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)
```xml
```
### Step 2: Create and Register Responder
```cpp
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
```cpp
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 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:
```cpp
// 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(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** (`Validate(AC_ActionInfo*)`) for state updates
5. **Reference your responder** by identity in the toolbar XML `responder` attribute