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

11 KiB
Raw Blame History

Creating and Displaying TULayoutView in Toon Boom

This document explains how to create custom views and display them in Toon Boom Harmony/Storyboard Pro using the TULayoutView system.

Overview

Toon Boom uses a hierarchical layout system:

  1. TULayoutManager - Central manager for all views and frames
  2. TULayoutArea - Metadata about a view type that can be instantiated
  3. TULayoutFrame - A window/panel containing tabbed view holders
  4. TULayoutViewHolder - Container holding 1-2 TULayoutView instances with optional splitter
  5. TULayoutView - Abstract base class for actual view content

Creating Custom Views by Subclassing TULayoutView

The recommended approach is to directly subclass TULayoutView and implement the required pure virtual methods.

Critical Discovery: How widget() Works

IMPORTANT: The widget() method (vtable slot 1) is called by TULayoutArea::add and the return value is treated as a TULayoutView*, NOT a QWidget*. The actual displayable widget is obtained later via getWidget().

This means:

  • widget() should return this (the view itself, cast to QWidget*)
  • getWidget() should return the actual QWidget for display

Required Pure Virtual Methods

When subclassing TULayoutView, you must implement these 5 pure virtual methods:

// Slot 1: Returns THIS view as the "widget" - treated as TULayoutView* by caller
virtual QWidget *widget() = 0;

// Slots 3-4: Return the actual displayable QWidget
virtual const QWidget *getWidget() const = 0;
virtual QWidget *getWidget() = 0;

// Slot 29: Called when menus need refresh
virtual void triggerMenuChanged() = 0;

// Slot 31 (protected): Marker method
virtual void isTULayoutView() = 0;

Example: Complete Custom View Implementation

// test_frame.hpp
#pragma once
#include "toon_boom_layout.hpp"

class TestView : public TULayoutView {
public:
    TestView();
    ~TestView() override;

    // Pure virtuals - MUST implement all 5
    QWidget *widget() override;
    const QWidget *getWidget() const override;
    QWidget *getWidget() override;
    void triggerMenuChanged() override {}

    // Override initiate to return this view
    TULayoutView *initiate(QWidget *parent) override;

    // Optional overrides
    QString displayName() const override;

protected:
    void isTULayoutView() override {}

private:
    QFrame *m_frame;  // The actual widget content
    QVBoxLayout *m_mainLayout;
};
// test_frame.cpp
#include "test_frame.hpp"
#include <QtWidgets/QLabel>

TestView::TestView()
    : TULayoutView() {
    // Create the frame that will hold our content
    m_frame = new QFrame();
    m_frame->setMinimumSize(400, 300);
    m_mainLayout = new QVBoxLayout(m_frame);

    QLabel *label = new QLabel("Hello from custom view!");
    label->setAlignment(Qt::AlignCenter);
    m_mainLayout->addWidget(label);
}

QWidget *TestView::widget() {
    // CRITICAL: TULayoutArea::add calls this via vtable[1] and expects
    // a TULayoutView* return value, NOT QWidget*. The returned pointer
    // is then used to call getWidget() for the actual widget.
    // So we return `this` which IS-A TULayoutView*.
    return reinterpret_cast<QWidget*>(static_cast<TULayoutView*>(this));
}

const QWidget *TestView::getWidget() const {
    return m_frame;
}

QWidget *TestView::getWidget() {
    return m_frame;
}

TULayoutView *TestView::initiate(QWidget *parent) {
    // Return this view - it's already initialized
    if (parent && m_frame) {
        m_frame->setParent(parent);
    }
    return this;
}

QString TestView::displayName() const {
    return QString("My Custom View");
}

TestView::~TestView() {
    delete m_frame;
}

Registering Views with TULayoutManager

TULayoutManager::addArea Signature

bool TULayoutManager::addArea(
    const char* typeName,           // Type identifier (e.g., "Colour", "Node View")
    const QString& displayName,     // Translated display name
    TULayoutView* view,             // View instance (your TestView*)
    bool visible,                   // Initially visible
    bool createFrame,               // Create new frame for this view
    bool docked,                    // Is docked (not floating)
    const QSize& minSize,           // Minimum size
    bool useMinSize,                // Whether to use minimum size
    bool isPlugin,                  // Is this a plugin area
    bool defaultVisible,            // Default visibility state
    bool unknown                    // Unknown flag (usually true)
);

Complete Registration Example

void showCustomView() {
    auto lm = PLUG_Services::getLayoutManager();
    if (!lm) {
        return;
    }

    // Create your custom view - it's a direct TULayoutView subclass
    TestView* myView = new TestView();

    // Register with the layout manager
    bool success = lm->addArea(
        "TestView",                    // typeName (unique ID)
        QString("My Test View"),       // displayName
        myView,                        // TULayoutView* - pass directly, no offset needed!
        true,                          // visible
        true,                          // createFrame
        true,                          // docked
        QSize(500, 400),               // minSize
        true,                          // useMinSize
        false,                         // isPlugin
        true,                          // defaultVisible
        true                           // unknown
    );

    if (success) {
        // Raise the view to show it
        auto area = lm->findArea(QString("TestView"));
        if (area) {
            lm->raiseArea(area, nullptr, false, QPoint(100, 100));
        }
    }
}

The Call Flow Explained

When you call TULayoutManager::addArea, the following happens:

  1. addArea stores your TULayoutView* in a new TULayoutArea
  2. When the view needs to be displayed, TULayoutArea::add is called
  3. add calls view->widget() (vtable slot 1) - expects TULayoutView return!*
  4. The return value is passed to TULayoutViewHolder::addView
  5. addView calls view->getWidget() to get the actual QWidget
  6. The QWidget is reparented and displayed

This is why widget() must return this - the calling code treats the return as TULayoutView* to make further virtual calls.

Opening Views at Runtime

To programmatically show a view that's already registered:

// Using TULayoutManager::raiseArea
TULayoutView* view = layoutManager->raiseArea(
    QString("TestView"),     // Area name
    targetFrame,             // TULayoutFrame* (or nullptr for current)
    true,                    // Create new instance if needed
    QPoint(0, 0)             // Position hint
);

Key Patterns

1. DPI Scaling

For views with minimum size requirements, use UT_DPI::scale():

QSize baseSize(260, 450);
QSize scaledSize = UT_DPI::scale(baseSize);

2. Common View Sizes (from HarmonyPremium analysis)

View Type Base Size
Colour 260 × 450
Coord. And Control Points 260 × 300
Layer Properties 260 × 450
Onion Skin 480 × 300
Timeline 800 × 400
Tool Properties 260 × 450
Top 300 × 400
Camera, Drawing, Node View, etc. 0 × 0 (no minimum)

3. Menu Registration

Set menus on views using TULayoutView::setMenu:

TULayoutView::setMenu(
    layoutView,
    actionManager,      // AC_Manager*
    "MENU_ID",          // Menu identifier
    MenuType::Primary   // 0 = Primary, 1 = Secondary
);

TULayoutViewHolder Usage

The TULayoutViewHolder is a QWidget container that can hold 1-2 TULayoutView instances with an optional vertical splitter.

How Views Get Into TULayoutViewHolder

When you call TULayoutManager::addArea or TULayoutManager::raiseArea, the system:

  1. Creates a TULayoutFrame if needed
  2. Creates a TULayoutViewHolder within the frame
  3. Calls TULayoutViewHolder::addView(view, splitterRatio) to add your view
  4. The view's getWidget() method is called to get the actual QWidget
  5. The widget is reparented and added to the holder's internal layout

TULayoutViewHolder::addView

bool TULayoutViewHolder::addView(
    TULayoutView* view,     // View to add
    double splitterRatio    // Ratio for splitter (default 0.5)
);

Returns: true if added successfully, false if holder is full (max 2 views)

Internal Structure (from ToonBoomLayout.dll)

TULayoutViewHolder (sizeof = 0x70 = 112 bytes)
├── QWidget base class (0x00-0x27)
├── std::vector<TULayoutView*> m_views    (+0x28, 24 bytes)
├── double m_savedSplitterRatio           (+0x40, default 0.5)
├── UI_Splitter* m_splitter               (+0x48, vertical orientation)
├── WID_VBoxLayout* m_leftLayout          (+0x50)
├── WID_VBoxLayout* m_rightLayout         (+0x58)
├── QFrame* m_leftFrame                   (+0x60)
└── QFrame* m_rightFrame                  (+0x68)

Behavior

  • 0 views: Splitter hidden, empty container
  • 1 view: View widget added directly to main layout, splitter hidden
  • 2 views: First view in left frame, second in right frame, splitter visible

Important Notes

  1. Direct Subclassing: Subclass TULayoutView directly - no need for complex multiple inheritance.

  2. widget() Returns this: The widget() method must return this (cast to QWidget*) because the calling code treats it as TULayoutView*.

  3. getWidget() Returns Content: The getWidget() method returns the actual displayable QWidget.

  4. TULayoutManager Access: Get the TULayoutManager via PLUG_Services::getLayoutManager().

  5. Memory Management: Views registered with addArea are managed by the layout system. Don't delete them manually.

Database Locations (HarmonyPremium.exe)

  • TULayoutManager::addArea import: 0x140b22668
  • TULayoutManager::raiseArea import: 0x140b22a18
  • View creation function (example): 0x1400375C0 (main session init)

Database Locations (ToonBoomLayout.dll)

  • TULayoutView vtable: 0x180056f38
  • TULayoutView constructor: 0x18002fc80
  • TULayoutViewHolder constructor: 0x180031150
  • TULayoutViewHolder::addView: 0x180031480
  • TULayoutViewHolder::removeView: 0x180031620
  • TULayoutViewHolder::nbViews: 0x180031610
  • TULayoutViewHolder::splitterRatio: 0x180031890
  • TULayoutViewHolder::updateWidgets: 0x1800319b0