Wholesale copy of all Kindred Create additions that don't conflict with upstream FreeCAD code: - kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides) - src/Mod/Create/ (Kindred Create workbench) - src/Gui/ Kindred source files (FileOrigin, OriginManager, OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext) - src/Gui/Icons/ (Kindred branding and silo icons) - src/Gui/PreferencePacks/KindredCreate/ - src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/) - package/ (rattler-build recipe) - docs/ (architecture, guides, specifications) - .gitea/ (CI workflows, issue templates) - mods/silo, mods/ztools submodules - .gitmodules (Kindred submodule URLs) - resources/ (kindred-create.desktop, kindred-create.xml) - banner-logo-light.png, CONTRIBUTING.md
10 KiB
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):
fastsignals::signal<void(ConnectionState)> signalConnectionStateChanged;
OriginManager (src/Gui/OriginManager.h):
fastsignals::signal<void(const std::string&)> signalOriginRegistered;
fastsignals::signal<void(const std::string&)> signalOriginUnregistered;
fastsignals::signal<void(const std::string&)> signalCurrentOriginChanged;
fastsignals::signal<void(App::Document*, const std::string&)> signalDocumentOriginChanged;
Gui::Document (src/Gui/Document.h):
mutable fastsignals::signal<void(const ViewProviderDocumentObject&)> signalNewObject;
mutable fastsignals::signal<void(const ViewProviderDocumentObject&)> signalDeletedObject;
mutable fastsignals::signal<void(const ViewProviderDocumentObject&,
const App::Property&)> signalChangedObject;
// ~9 more document-level signals
Gui::Application (src/Gui/Application.h):
fastsignals::signal<void(const Gui::Document&, bool)> signalNewDocument;
fastsignals::signal<void(const Gui::Document&)> signalDeleteDocument;
fastsignals::signal<void(const Gui::ViewProvider&)> 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):
// 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:
fastsignals::signal<void(const std::string&)> signalSomethingHappened;
Use mutable if the signal needs to fire from const methods (as Gui::Document does):
mutable fastsignals::signal<void(int)> signalReadOnlyEvent;
Name signals with the signal prefix by convention.
Emitting
Call the signal like a function:
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:
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
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.
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:
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:
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:
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
connectionobject 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:
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_connectionis ~16 bytes. - fastsignals is rarely a bottleneck. Profile before optimising signal infrastructure.
Adding a new signal
- Declare the signal as a public member (or
mutableif emitting from const methods). - Name it with the
signalprefix. - Emit it at the appropriate point in your code.
- Listeners store
scoped_connectionmembers and connect via lambdas. - 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:
std::vector<fastsignals::scoped_connection> conns;
conns.push_back(std::move(conn)); // OK
See also
- OriginManager — signal catalog for origin lifecycle events
- FileOrigin Interface —
signalConnectionStateChanged - OriginSelectorWidget — listener patterns in practice
src/3rdParty/FastSignals/— library source and headers