# Signal Architecture Kindred Create uses two signal systems side by side: **fastsignals** for domain and application events, and **Qt signals** for UI framework integration. This page explains why both exist, where each is used, and the patterns for working with them. ## Why two signal systems Qt signals require the `Q_OBJECT` macro, MOC preprocessing, and a `QObject` base class. This makes them the right tool for widget-to-widget communication but a poor fit for domain classes like `FileOrigin`, `OriginManager`, and `App::Document` that are not `QObject` subclasses and should not depend on the Qt meta-object system. fastsignals is a header-only C++ library that provides type-safe signals with lambda support, RAII connection management, and no build-tool preprocessing. It lives at `src/3rdParty/FastSignals/` and is linked into the Gui module via CMake. | | Qt signals | fastsignals | |---|-----------|-------------| | **Requires** | `Q_OBJECT`, MOC, `QObject` base | Nothing (header-only) | | **Dispatch** | Event loop (can be queued) | Synchronous, immediate | | **Connection** | `QObject::connect()` | `signal.connect(lambda)` | | **Lifetime** | Tied to `QObject` parent-child tree | `scoped_connection` RAII | | **Thread model** | Can queue across threads | Fires on emitter's thread | | **Slot types** | Slots, lambdas, `std::function` | Lambdas, `std::function`, function pointers | ## Where each is used ### fastsignals — domain events These are events about application state that multiple independent listeners may care about. The emitting class is not a `QObject`. **FileOrigin** (`src/Gui/FileOrigin.h`): ```cpp fastsignals::signal signalConnectionStateChanged; ``` **OriginManager** (`src/Gui/OriginManager.h`): ```cpp fastsignals::signal signalOriginRegistered; fastsignals::signal signalOriginUnregistered; fastsignals::signal signalCurrentOriginChanged; fastsignals::signal signalDocumentOriginChanged; ``` **Gui::Document** (`src/Gui/Document.h`): ```cpp mutable fastsignals::signal signalNewObject; mutable fastsignals::signal signalDeletedObject; mutable fastsignals::signal signalChangedObject; // ~9 more document-level signals ``` **Gui::Application** (`src/Gui/Application.h`): ```cpp fastsignals::signal signalNewDocument; fastsignals::signal signalDeleteDocument; fastsignals::signal signalNewObject; // ~7 more application-level signals ``` ### Qt signals — UI interaction These are events between Qt widgets, actions, and framework components where `QObject` is already the base class and the Qt event loop handles dispatch. **OriginSelectorWidget** (a `QToolButton` subclass): ```cpp // QActionGroup::triggered → onOriginActionTriggered connect(m_originActions, &QActionGroup::triggered, this, &OriginSelectorWidget::onOriginActionTriggered); // QAction::triggered → onManageOriginsClicked connect(m_manageAction, &QAction::triggered, this, &OriginSelectorWidget::onManageOriginsClicked); ``` ### The boundary The pattern is consistent: **fastsignals for observable state changes in domain classes, Qt signals for UI framework plumbing**. A typical flow crosses the boundary once: ``` OriginManager emits signalCurrentOriginChanged (fastsignals) → OriginSelectorWidget::onCurrentOriginChanged (lambda listener) → updates QToolButton text/icon (Qt API calls) ``` ## fastsignals API ### Declaring a signal Signals are public member variables with a template signature: ```cpp fastsignals::signal signalSomethingHappened; ``` Use `mutable` if the signal needs to fire from `const` methods (as `Gui::Document` does): ```cpp mutable fastsignals::signal signalReadOnlyEvent; ``` Name signals with the `signal` prefix by convention. ### Emitting Call the signal like a function: ```cpp signalSomethingHappened("hello"); ``` All connected slots execute **synchronously**, in connection order, before the call returns. There is no event loop queuing. ### Connecting `signal.connect()` accepts any callable and returns a connection object: ```cpp auto conn = mySignal.connect([](const std::string& s) { Base::Console().log("Got: %s\n", s.c_str()); }); ``` ### Connection types | Type | RAII | Copyable | Use case | |------|------|----------|----------| | `fastsignals::connection` | No | Yes | Long-lived, manually managed | | `fastsignals::scoped_connection` | Yes | No (move only) | Member variable, automatic cleanup | | `fastsignals::advanced_connection` | No | No | Temporary blocking via `shared_connection_block` | `scoped_connection` is the standard choice for class members. It disconnects automatically when destroyed. ### Disconnecting ```cpp conn.disconnect(); // Explicit // or let scoped_connection destructor handle it ``` ## Connection patterns ### Pattern 1: Scoped member connections The most common pattern. Store `scoped_connection` as a class member and connect in the constructor or an `attach()` method. ```cpp class MyListener { fastsignals::scoped_connection m_conn; public: MyListener(OriginManager* mgr) { m_conn = mgr->signalOriginRegistered.connect( [this](const std::string& id) { onRegistered(id); } ); } // Destructor auto-disconnects via m_conn ~MyListener() = default; private: void onRegistered(const std::string& id) { /* ... */ } }; ``` ### Pattern 2: Explicit disconnect in destructor `OriginSelectorWidget` disconnects explicitly before destruction to prevent any signal delivery during teardown: ```cpp OriginSelectorWidget::~OriginSelectorWidget() { disconnectSignals(); // m_conn*.disconnect() } ``` This is defensive — `scoped_connection` would disconnect on its own, but explicit disconnection avoids edge cases where a signal fires between member destruction order. ### Pattern 3: DocumentObserver base class `Gui::DocumentObserver` wraps ~10 document signals behind virtual methods: ```cpp class DocumentObserver { using Connection = fastsignals::scoped_connection; Connection connectDocumentCreatedObject; Connection connectDocumentDeletedObject; // ... void attachDocument(Document* doc); // connects all void detachDocument(); // disconnects all }; ``` Subclasses override `slotCreatedObject()`, `slotDeletedObject()`, etc. This pattern avoids repeating connection boilerplate in every document listener. ### Pattern 4: Temporary blocking `advanced_connection` supports blocking a slot without disconnecting: ```cpp auto conn = signal.connect(handler, fastsignals::advanced_tag()); fastsignals::shared_connection_block block(conn, true); // blocked signal(); // handler does NOT execute block.unblock(); signal(); // handler executes ``` Use this to prevent recursive signal handling. ## Thread safety ### What is thread-safe - **Emitting** a signal from any thread (internal spin mutex protects the slot list). - **Connecting and disconnecting** from any thread, even while slots are executing on another thread. ### What is not thread-safe - **Accessing the same `connection` object** from multiple threads. Protect with your own mutex or keep connection objects thread-local. - **Slot execution context.** Slots run on the emitter's thread. If a fastsignal fires on a background thread and the slot touches Qt widgets, you must marshal to the main thread: ```cpp mgr->signalOriginRegistered.connect([this](const std::string& id) { QMetaObject::invokeMethod(this, [this, id]() { // Now on the main thread — safe to update UI updateUI(id); }, Qt::QueuedConnection); }); ``` In practice, all origin and document signals in Kindred Create fire on the main thread, so this marshalling is not currently needed. It would become necessary if background workers emitted signals. ## Performance - **Emission:** O(n) where n = connected slots. No allocation, no event loop overhead. - **Connection:** O(1) with spin mutex. - **Memory:** Each signal stores a shared pointer to a slot vector. Each `scoped_connection` is ~16 bytes. - fastsignals is rarely a bottleneck. Profile before optimising signal infrastructure. ## Adding a new signal 1. Declare the signal as a public member (or `mutable` if emitting from const methods). 2. Name it with the `signal` prefix. 3. Emit it at the appropriate point in your code. 4. Listeners store `scoped_connection` members and connect via lambdas. 5. Document the signal's signature and when it fires. Do not create a fastsignal for single-listener scenarios — a direct method call is simpler. ## Common mistakes **Dangling `this` capture.** If a lambda captures `this` and the object is destroyed before the connection, the next emission crashes. Always store the connection as a `scoped_connection` member so it disconnects on destruction. **Assuming queued dispatch.** fastsignals are synchronous. A slot that blocks will block the emitter. Keep slots fast or offload work to a background thread. **Forgetting `mutable`.** If you need to emit from a `const` method, the signal member must be `mutable`. Otherwise the compiler rejects the call. **Copying `scoped_connection`.** It is move-only. Use `std::move()` when putting connections into containers: ```cpp std::vector conns; conns.push_back(std::move(conn)); // OK ``` ## See also - [OriginManager](../reference/cpp-origin-manager.md) — signal catalog for origin lifecycle events - [FileOrigin Interface](../reference/cpp-file-origin.md) — `signalConnectionStateChanged` - [OriginSelectorWidget](../reference/cpp-origin-selector-widget.md) — listener patterns in practice - `src/3rdParty/FastSignals/` — library source and headers