How to make a custom event loop in Cocoa?

25 Views Asked by At

I need to implement a custom Cocoa main loop using metal-cpp (the source code is also available here). I use an extended version of metal-cpp, so all called below methods were implemented in my version of metal-cpp.

I've create a custom app delegate:

class AppDelegate: public NS::ApplicationDelegate
{
    public:
        AppDelegate(const char* app_name);
        ~AppDelegate();
        
        NS::Menu* createMenuBar();
        virtual void applicationWillFinishLaunching(NS::Notification* notification_ptr) override;
        virtual void applicationDidFinishLaunching(NS::Notification* notification_ptr) override;
        virtual bool applicationShouldTerminateAfterLastWindowClosed(NS::Application* sender_ptr) override;

    private:
        const char* m_app_name;
};

AppDelegate::AppDelegate(const char* app_name):
    m_app_name(app_name)
{
}

AppDelegate::~AppDelegate()
{
}

NS::Menu* AppDelegate::createMenuBar()
{
    // Menu creation
}

void AppDelegate::applicationWillFinishLaunching(NS::Notification* notification_ptr)
{
    NS::Menu* menu_ptr = createMenuBar();
    NS::Application* app_ptr = reinterpret_cast<NS::Application*>(notification_ptr->object());
    app_ptr->setMainMenu(menu_ptr);
    app_ptr->setActivationPolicy(NS::ActivationPolicy::ActivationPolicyRegular);
}

void AppDelegate::applicationDidFinishLaunching(NS::Notification* notification)
{
    NS::Application* application = reinterpret_cast< NS::Application* >( notification->object() );
    application->activateIgnoringOtherApps( true );
}

bool AppDelegate::applicationShouldTerminateAfterLastWindowClosed(NS::Application* sender_ptr)
{
    return false;
}

And a custom application class:

struct AppConfig {
    std::string appName;
};

class App
{
    public:
        App(AppConfig config);
        virtual ~App() {}

        virtual Result      start() = 0;
        virtual Result      update() = 0;
        virtual bool        isFinished() const = 0;
        virtual Result      stop() = 0;
        const AppConfig&    configuration() const;

    protected:
        AppConfig   config;
};

App::App(AppConfig config):
    config(std::move(config))
{
}

const AppConfig& App::configuration() const {
    return config;
}

For macOS the App is implemented by DarwinApp:

class DarwinApp: public App
{
    public:
        DarwinApp(AppConfig config);
        ~DarwinApp();

        Result  start() override;
        Result  update() override;
        bool    isFinished() const override;
        Result  stop() override;
    private:
        bool    m_is_finished;
};

DarwinApp::DarwinApp(AppConfig config):
    App(std::move(config)),
    m_is_finished(true)
{
}

DarwinApp::~DarwinApp()
{
}

Result DarwinApp::start()
{
    auto autorelease_pool = NS::AutoreleasePool::alloc()->init();

    AppDelegate app_delegate(config.appName.c_str());
    NS::Application* application = NS::Application::sharedApplication();
    application->setDelegate(&app_delegate);
    application->finishLaunching();

    autorelease_pool->release();

    m_is_finished = false;

    return b3::SUCCESS;
}

Result DarwinApp::update()
{
    NS::Application* application = NS::Application::sharedApplication();

    NS::String* defaultRunLoopMode = NS::String::string(NS::DefaultRunLoopMode, NS::StringEncoding::ASCIIStringEncoding);

    NS::Event* event = application->nextEvent(NS::EventMask::AnyEventMask, nil, defaultRunLoopMode, true);

    if (event)
    {
        application->sendEvent(event);
        event->release();
    }

    usleep(10000);

    return SUCCESS;
}

bool DarwinApp::isFinished() const
{
    return m_is_finished;
}

Result DarwinApp::stop()
{
    m_is_finished = true;
    return SUCCESS;
}

This solution is based on that answer.

Finally, I implement an event loop:

int main() {
    AppConfig config = {};
    config.appName = "Tim Cook, add C++ support!";
    Application app(config);
    std::cout << "App: initialized" << std::endl;
    app.start();
    std::cout << "App: started" << std::endl;
    do
    {
        app.update();
    } while (!app.isFinished());
    return 0;
}

It works, of course, BUT macOS marks such app as Not responding and it doesn't build app menu (if to call a simple NSApplication instance run() the menu shows normally, everything works).

What's a right way to make the app responsible instead of usleep()?

Or is there a some C++ example of a custom main loop?

P.S. I don't remember already this is which iteration of the code refactoring. I've re-tried a lot of approaches, they all lead me to Not responsible or to blocking run() and stop() doesn't help. I know to stop() worked you need to send a custom UI event, unfortunately, in my case it just blocked me on postEvent(). In short, everywhere I meet some problems. This solution at least runs a custom main loop, so I try to fix Not responsible error just.

P.P.S. Please, don't send me to GLFW sources, I am researching it for few days already. As much as I could understand it and implement using metal-cpp led me to infinite postEvent in the app delegate.

0

There are 0 best solutions below