ThreadWithProgressWindow: disabled by default? bad idea?

My plugin will let users drag and drop a file. I want to process it in a way that might take a minute.

At first I was struggling to figure out how to let a user cancel this process or prevent things from getting messed up dragging another file (interrupting the current processing loop).

Then I found ThreadWithProgressWindow, which seemed to be an answer to my problem.

But I found warnings in the forum about how it shouldn’t be used?

And then I noticed that JUCE_MODAL_LOOPS_PERMITTED=0 is the default as seen here. Out of the box, Xcode says No member named 'runThread' for my object.

I am able to make it compile with JUCE_MODAL_LOOPS_PERMITTED=1 with a custom “Preprocessor Definition”, but unsure if this is unsupported or if I’ll see issues on particular OS’s.

Otherwise, what would people recommend as a design pattern for this?

EDIT: I have noticed in trying this out, while the modal loop is happening, the OS X finder window freezes (spinny ball). This isn’t ideal.

So in theory, a fix is pretty easy! Instead of calling runThread you call ThreadWithProgressWindow::launchThread. This method will exit immediately and everything will just work exactly how you expect it to work with two exceptions:

  1. I’m not sure how exactly you are implementing the run method in ThreadWindow (essentially what should happen in background). But this method will not longer be called on the message thread. This sounds like it won’t be a problem, but it can be very quickly. I suspect your are more on the UI side with that code – normally you don’t have too worry to much about thread issues on the UI side since everything is just called on the message thread. It is worth to run that code with thread sanatizer on. This will assert you possible race conditions. Also the JUCE code has a tone of asserts preventing you to do work that should be done on the message thread. But that won’t protect you against changing e.g. ValueTrees that you are using in your own code base that are working fine as long as they are only every used in the message thread.
  2. Second problem is the live time of ThreadWindow. You can’t declare it on the stack anymore. When the instance goes out of scope, the dtor is called and stops your newly started background thread. Similar to the FileChooser, I recommend putting a std::unique_ptr<ThreadWindow> into your component. This way has two benefits: first your background action is canceled automatically if the component is destroyed and second the background thread is automatically canceled when the action is started again on top of the already running action. But FYI: both cases probably won’t come up since you are still creating a modal component that implicitly prevents user interactions to the rest of your UI.
2 Likes

OK, wow! That was super helpful. If I try it with Finder or with my DAW, neither freeze while waiting for completion.

But now things are a little more complex. As the thread is working, I want to set a few variables on subcomponent children of PluginEditor (component that originally kicks off the thread) because otherwise there was no point in doing the work, because user can’t access it in UI.

If I could give my ThreadWithProgressWindow subclass the instance of my PluginEditor, then I suppose the window could write state to editor itself, but I, a) can’t do that without a circular import, and b) seems like a bad idea from a multithreading perspective (?).

Any ideas on how I might structure this?

I’m not an expert at C++, so maybe this is a silly question.

(Another option is keeping all the state returned from the thread on the ThreadWithProgressWindow instance, then accessing state through it. But then subcomponents of the PluginEditor have same problem of needing parent instance, causing a circular import)

If you are not an expert in C++, that’s totally fine – and by that I wouldn’t worry too much about a circular import for now. Xcode creates include guards automatically and if still in doubt, add #pragma once on the very top of the header file and you’ll be alright. You can fix that circular import later when you have the time (and patients).
Otherwise: the circular import in general is caused by bad abstraction and modularisation. Try to remove as much out of the PluginEditor as possible and put that into dedicated classes. E.g. inherit from juce::Component to condense your window into sub parts or (also very useful) design attachment classes similar to e.g. juces SliderParameterAttachment that bundles logic into a different class.

You can safely pass any kind of component related reference to your thread window class. Let’s say for a moment the thread loads five files (0%-20% … 80%) and after each file you want to trigger a method in your PluginEditor with the loaded data. Here is how you do it:

// Member is 'PluginEditor& editor'
// load data for single file as 'auto data'

  MessageManager::getInstance()->callAsync([safeEditor = juce::Component::SafePointer(&editor), data = std::move(data) /* move data into callback for faster performance */] {
    if (safeThis) // Make sure the editor is still alive. There are no more race conditions that can occur.
      auto& mySafeEditorInstanceToCallStuffOn = *safeThis;
  });

If your ‘data’ does not offer a move constructor, just put ‘data’ instead of 'data = std::move(data). Without moving this will copy – so might be relatively slow depending on the size. But that would be copied as part of the background threads actions, so no worries here.

Ah, just a thought about the circular import: you are most likely able to remove the import of “ThreadWindow” from your plugin editor header since you only need it for the unique_ptr declaration in the header. Change that to std::unique_ptr<ThreadWithProgressWindow> and re-add the import to “ThreadWindow” to the actual compilation unit (=cpp file).

As far as modularisation goes, this will be cleaner anyway.