Generated reference › API — ICoreEssentials/UI/Signals
kind: generated#api#icoreessentials-ui-signals

API — ICoreEssentials/UI/Signals

The public contract of 2 header(s) under ICoreEssentials/UI/Signals — 6 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.

ICoreMainThread.h#

ICoreEssentials/UI/Signals/ICoreMainThread.h

ICoreMainThread#

ICoreMainThread.h:49 · class · 6 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();

    // LEND the platform's event loop for up to `ms` milliseconds, so queued
    // work, timers and toolkit callbacks get their turn, then return. It is
    // ICoreApplication::tick(waitMs) reachable without the instance -- the same
    // "the loop is lent, not entered" contract, for a caller that has no way to
    // hold the application object.
    //
    // ⚠ WHAT IT IS FOR, AND WHY A SLEEP IS NOT IT. The thing a caller is
    // usually waiting for -- an ICoreUiTimer's debounce, a queued callback, a
    // child process's completion -- needs a LOOP TURN, not wall-clock. A
    // std::this_thread::sleep_for of the same duration delivers none of them,
    // on EITHER backend, and turns a real wait into a race the caller wins on a
    // fast machine and loses on a slow one.
    //
    // `ms <= 0` drains what is already pending and returns without waiting,
    // matching tick(0).
    //
    // ⚠ MAIN THREAD ONLY -- it is the loop's own thread that can pump it. Off
    // that thread, and before the application object exists, there is no loop
    // to lend: it then SLEEPS for the remaining time rather than returning
    // instantly, because the duration is the part such a caller can observe and
    // a busy return would spin them. That is the same family as post()'s
    // no-loop-yet fallback above, and it is the only case where this call does
    // not do what its name says.
    //
    // ⚠ Added 2026-08-22 for a caller that had no portable spelling at all: a
    // test harness whose waits were raw toolkit event loops, which HANG on a
    // backend whose toolkit application object does not exist -- stopping the
    // regression suite dead at two separate cases, on the same mechanism.
    static void pumpFor(int ms);

    // 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#

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();
    }

};