btzy / nativefiledialog-extended

Cross platform (Windows, Mac, Linux) native file dialog library with C and C++ bindings, based on mlabbe/nativefiledialog.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Open an asynchronous File Dialog

dropTableUsers42 opened this issue · comments

Opening the file dialog (or folder picker) in main thread blocks the main thread.
Currently my application's rendering, and input (handled by glfw) is done on the main thread, and it gets blocked if a file dialog is opened

Now on Windows and Linux, I'm able to open the file dialog asynchronously using std::async (I have to call the NFD init and quit functions from the new thread only)

                        result = std::async(std::launch::async, 
                                                                 [](NFD::UniquePath& outpath) 
                                                                 { 
                                                                         // Init and Uninit in the same thread as Dialog creation 
                                                                         NFD::Init(); 
                                                                         auto ret = NFD::PickFolder(outpath, nullptr, "Open Project"); 
                                                                         NFD::Quit(); 
                                                                         return ret; 
                                                                 }, 
                                                                 std::reference_wrapper(outpath));

However, this does not work for AppKit.

Is there any way add an async API to open dialogs?

Hi,

I think it isn't possible on macOS to have the file dialog run on a non-main thread. There's this StackOverflow question about it, which seems to also say that it isn't possible. #46 seems to assume that it's impossible to open the file dialog from a non-main thread, maybe you can try asking them about it.

Actually, I'm not sure if what you're doing is guaranteed to work on Windows and Linux. For Windows, it seems to be possible to create windows in threads, but it seems like there's some additional setup that needs to be done for that to work.

Hi, I've managed to make async call work on macOS, will you accept a PR for it? @btzy

Sample code to make NFD_OpenDialogN can be called from none main thread:

nfdresult_t NFD_OpenDialogN() {
    __block nfdresult_t result = NFD_CANCEL;

    if ([NSThread isMainThread]) {
        NSOpenPanel* dialog = [NSOpenPanel openPanel];
        NSModalResponse resp = [dialog runModal];
        // result = ...
    } else {
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        dispatch_async(dispatch_get_main_queue(), ^{
            NSOpenPanel* dialog = [NSOpenPanel openPanel];
            [dialog beginWithCompletionHandler:^(NSModalResponse resp) {
                // result = ...
                dispatch_semaphore_signal(semaphore);
            }];
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }

    return result
}

The idea is from Rusty File Dialog, which supports async on both windows / Linux / macOS.

I think there're two separate things in the code above:

  1. The code is async in the sense that it does not block the main thread. This is because beginWithCompletionHandler returns immediately without waiting for the dialog to complete. When the dialog is complete (i.e. the user pressed "OK" or "Cancel"), we will get a callback via the completion handler, and it will be called on an arbitrary thread. This allows the main thread to do other things, and is usually what we mean when we talk about async file dialogs.
  2. The dialog can be invoked from threads other than the main thread. This is because the code does dispatch_async(dispatch_get_main_queue(), ...). This will only work if the main thread is running a regular macOS event loop, but won't work if it is some console program which doesn't use the regular event loop (since there main thread won't poll the queue in that case).

I find your proposed solution kind of hacky for what the OP wants to do: They want to open a file dialog asynchronously, but instead the program performs a delicate dance between threads, by spawning a new thread which then invokes the main thread again to start the task (which will only start if they are using a regular macOS event loop). We should just have the main thread call beginWithCompletionHandler directly without spawning the secondary thread.

I think we can design a more proper async API at some point - something like OpenDialogAsync(..., void (*handler)(void*, nfdresult_t, char*), void* context) (exact API up to discussion). But we should properly understand how opening a non-model dialog works on Windows and on Linux first. For Linux I believe it is possible to not have the callback be invoked on a separate thread at all (using some kind of epoll-ish mechanism, where we have an fd that will trigger when the dialog is complete), so there's a question of whether we want to support that in some way too (I think probably not though, since such mechanisms are quite OS-specific).

I find your proposed solution kind of hacky
I think we can design a more proper async API at some point

I agree, will you consider implementing an async API?

a OpenDialogAsync API may not be enough, it's better to provide a boo wait(int timeout) API that allow the caller to check if the dialog is finish and return control to the caller (similay to the approch used by portable-file-dialogs).

In fact, I'm going to make a wrapper like that in my application.

Can you explain why a bool wait(int timeout) API is preferable? It seems worse than an OpenDialogAsync API to me for at least three reasons:

  • Calling that function freezes up your application for however long you specify to wait for.
  • The only way to properly wait for the dialog to be completed is to keep calling wait() in a tight loop, which takes up CPU cycles.
  • It is easy to implement your wait API on top of OpenDialogAsync, but the other way around can only be done with busy waiting.

The wait API seems to me to be like an inferior attempt at coroutines.

I'm using it in an imgui app (glfw+opengl3), which will run my code in every frame (event loop).

To check async result in a loop without blocking the loop forever, the try+wait API maybe useful.

I'm using it in an imgui app (glfw+opengl3), which will run my code in every frame (event loop).

In this case, aren't you going to pass a timeout of zero (i.e. it becomes more like a poll() API)?

Yes, will pass it with 0.

The wait+timeout api is more like what a c++ future provides, it's more flexible for general use case.

I'm still not really convinced that wait+timeout will be useful for file dialogs, and I'm more of the opinion that those who want async file dialogs will mostly use the callback API. I think this design decision will also be affected by how the platform-specific APIs work - if most OSes provide a wait+timeout API, then I would be more inclined to implement it in NFD; however if most of them provide a callback API, then I would gravitate toward something like OpenDialogAsync.

I'm OK with a callback api. It's not hard to implement the check api myself on top of callback.