API — ICoreEssentials/UI/Signals
The public contract of 3 header(s) under src/ICoreEssentials/UI/Signals — 7 class/struct definition(s), 28 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
| Header | Defines | Declarations | Bases |
|---|---|---|---|
ICoreMainThread.h | ICoreMainThread | 5 | — |
ICoreSignal.h | SlotBase, Slot, ICoreSignalConnection, ICoreSignalScope, ICoreSignal | 22 | SlotBase |
ICoreSignalObjectBinding.h | ScopeHolder | 1 | public ICoreObject |
ICoreMainThread.h#
src/ICoreEssentials/UI/Signals/ICoreMainThread.h
ICoreMainThread#
ICoreMainThread.h:19 · class · 5 declaration(s)
The one door to the GUI thread.
class ICoreMainThread {
public:
ICoreMainThread() = delete;
// Run `fn` on the GUI thread, queued behind whatever the event loop is
// already doing.
static void post(std::function<void()> fn);
// Run `fn` NOW when already on the GUI thread, queue it otherwise --
// Qt::AutoConnection's dispatch, where post() above is always-queued.
// Pick deliberately: a caller escaping a destructor or paint context
// needs post()'s returns-first guarantee; a caller that just wants the
// work done soonest on the right thread wants this.
static void runNowOrPost(std::function<void()> fn);
static bool isMainThread();
// QApplication::quit(), behind the boundary. ONLY the fallback for
// processes that never construct an icore::Application -- under the SDK
// entry point quit() acts on a loop that is not running and does nothing,
// so route through icore::Application::requestQuit() first and call this
// when there is no live instance.
static void quitApplicationLoopFallback();
};
};
ICoreSignal.h#
src/ICoreEssentials/UI/Signals/ICoreSignal.h
The project's replacement for Qt signals/slots at every call site OUTSIDE the wrapper layer. Client classes hold public ICoreSignal<...> members where they used to declare
signals:sections, and receivers subscribe with connect() where they used to call QObject::connect. No moc, no Q_OBJECT, no QObject base required on either side.Naming: the sync emit is operator(), the queued emit is post(). There is deliberately no method named
emit-- that identifier is a Qt macro in any TU that sees a Qt header, which in this project is most of them.Threading: connect/disconnect/emit are safe from any thread. Sync emission runs the slots on the calling thread against a snapshot taken under the lock, so a slot may connect or disconnect (even itself) during delivery. post() delivers the snapshot on the GUI thread via ICoreMainThread -- this
SlotBase#
ICoreSignal.h:40 · struct · 1 declaration(s)
struct SlotBase {
public:
std::atomic<bool> alive { true };
virtual ~SlotBase() = default;
};
};
Slot#
ICoreSignal.h:46 · struct · bases SlotBase · 1 declaration(s)
struct Slot : SlotBase {
public:
std::function<void(Args...)> fn;
};
};
ICoreSignalConnection#
ICoreSignal.h:54 · class · pImpl · 6 declaration(s)
Handle to one registration.
class ICoreSignalConnection {
public:
ICoreSignalConnection();
~ICoreSignalConnection();
// Copyable, and every copy severs the SAME slot -- that is the contract,
// not an accident, and it survives the Impl because the Impl holds the same
// weak_ptr. Written out because a unique_ptr<Impl> deletes the implicit
// copy.
ICoreSignalConnection(const ICoreSignalConnection& other);
ICoreSignalConnection& operator=(const ICoreSignalConnection& other);
void disconnect();
[[nodiscard]] bool connected() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSignalScope#
ICoreSignal.h:86 · class · pImpl · 6 declaration(s)
Collects connections and severs them all on destruction.
class ICoreSignalScope {
public:
ICoreSignalScope();
ICoreSignalScope(const ICoreSignalScope&) = delete;
ICoreSignalScope& operator=(const ICoreSignalScope&) = delete;
// Severs every connection it collected.
~ICoreSignalScope();
// const, with the state behind it, so a receiver can subscribe from a
// const member function -- much of this codebase builds its children from
// const methods, and what a scope holds is bookkeeping about the owner's
// subscriptions rather than part of the owner's logical value.
//
// ⚠ The `mutable` these two used to need is GONE, and not by oversight:
// operator-> on a const unique_ptr yields a NON-const Impl&, so state
// behind an Impl is already writable from a const member. The constness of
// the interface is unchanged; only the keyword that used to buy it is.
void add(ICoreSignalConnection connection) const;
void disconnectAll() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSignal#
ICoreSignal.h:114 · class · 8 declaration(s)
class ICoreSignal {
public:
ICoreSignal() = default;
ICoreSignal(const ICoreSignal&) = delete;
ICoreSignal& operator=(const ICoreSignal&) = delete;
// Bare connect exists for slots whose lifetime provably exceeds the
// signal's (a static, or the signal's own owner). Everything else should
// pass a scope -- an unscoped lambda capturing `this` is exactly the
// dangling QObject::connect this class exists to retire.
ICoreSignalConnection connect(std::function<void(Args...)> fn) {
auto slot = std::make_shared<Slot>();
slot->fn = std::move(fn);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_slots.push_back(slot);
}
return ICoreSignalConnection(slot);
}
ICoreSignalConnection connect(const ICoreSignalScope& owner, std::function<void(Args...)> fn) {
ICoreSignalConnection connection = connect(std::move(fn));
owner.add(connection);
return connection;
}
// Synchronous emit on the calling thread.
void operator()(Args... args) {
for (const auto& slot : snapshot()) {
if (slot->alive.load()) {
slot->fn(args...);
}
}
}
// Queued emit on the GUI thread. Arguments are copied into the hop, so
// they must be value types (everything crossing this boundary is).
//
// NOTE the local is `delivery`, not `slots`: most TUs see Qt's keyword
// macros, which #define `signals`, `slots` and `emit` away -- none of the
// three may be used as an identifier anywhere in the wrapper layer.
void post(Args... args) {
auto delivery = snapshot();
auto packed = std::make_tuple(std::move(args)...);
ICoreMainThread::post([delivery = std::move(delivery), packed = std::move(packed)]() mutable {
std::apply(
[&delivery](auto&... unpacked) {
for (const auto& slot : delivery) {
if (slot->alive.load()) {
slot->fn(unpacked...);
}
}
},
packed);
});
}
void disconnectAll() {
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& slot : m_slots) {
slot->alive.store(false);
}
m_slots.clear();
}
};
ICoreSignalObjectBinding.h#
src/ICoreEssentials/UI/Signals/ICoreSignalObjectBinding.h
Bind an ICoreSignal subscription to the lifetime of a toolkit object.
The normal way to own a subscription is an ICoreSignalScope member on the receiver. This exists for the cases where there is no such member to reach: a template helper handed an arbitrary
target*, or a receiver whose type is not ours to edit. It is what the toolkit's own three-argument connect (sender, signal, CONTEXT, slot) was doing -- severing when the context dies.WRAPPER ZONE ONLY: it names a toolkit object, which is the whole point. Prefer a plain ICoreSignalScope member wherever the receiver has one.
ScopeHolder#
ICoreSignalObjectBinding.h:26 · class · bases public ICoreObject · 1 declaration(s)
Holds one scope and nothing else.
class ScopeHolder : public ICoreObject {
public:
explicit ScopeHolder(QObject* parent);
ICoreSignalScope scope;
};
};