App: Invoke signalBeforeRecompute() on the GUI thread

Historically, `App::Document::recompute()` ran entirely on the **main**
(GUI) thread and directly emitted `signalBeforeRecompute()`.

* Add-ons like **Assembly3** and others depend on that signal for
setup/teardown hooks before any recompute work begins.

* After offloading `recompute()` into a background worker thread to keep
the UI responsive, calling `signalBeforeRecompute()` directly from the
worker would break thread-affinity rules and silently break
compatibility with those add-ons.

**Solution**

1. **Introduce a generic hook** (`PreRecomputeHook`) in
**App::Document**:

* A `std::function<void()>` that, if set, is invoked at the very
start of `recompute()`.

* Core code stays Qt-free—only knows to call a callback if one
exists.

2. **Wire up the hook in `Gui::Document`**:

* In the GUI wrapper’s constructor, install a hook that calls
`callSignalBeforeRecompute()`.

* `callSignalBeforeRecompute()` uses `QMetaObject::invokeMethod(...,
Qt::BlockingQueuedConnection)` to enqueue `signalBeforeRecompute()` on
the GUI thread and **block** the worker until it completes.

    * If already on the GUI thread, it simply calls the signal directly.

3. **Maintain add-on compatibility**:

* From the add-on’s perspective nothing changes—they still receive
`signalBeforeRecompute()` on the main thread before any recompute work.

* Internally, the recompute body now runs on a worker thread,
improving UI responsiveness without breaking existing hooks.

**Result**

* **Recompute** remains fully backward-compatible for add-ons like
Assembly3.

* **UI thread** still handles all GUI-related signaling.

* **Worker thread** performs the actual heavy lifting, unblocked only
once the GUI is primed and all pre-recompute signals have been
delivered.
This commit is contained in:
Joao Matos
2025-05-18 13:23:08 +01:00
committed by Benjamin Nauck
parent 6cd828d986
commit 1f5e0f6494
5 changed files with 39 additions and 1 deletions

View File

@@ -39,6 +39,7 @@
# include <QOpenGLWidget>
# include <QTextStream>
# include <QTimer>
# include <QThread>
# include <QStatusBar>
# include <Inventor/actions/SoSearchAction.h>
# include <Inventor/nodes/SoSeparator.h>
@@ -505,6 +506,8 @@ Document::Document(App::Document* pcDocument,Application * app)
(std::bind(&Gui::Document::slotTransactionRemove, this, sp::_1, sp::_2));
//NOLINTEND
pcDocument->setPreRecomputeHook([this] { callSignalBeforeRecompute(); });
// pointer to the python class
// NOTE: As this Python object doesn't get returned to the interpreter we
// mustn't increment it (Werner Jan-12-2006)
@@ -1193,6 +1196,25 @@ void Document::slotTouchedObject(const App::DocumentObject &Obj)
}
}
// helper that guarantees signalBeforeRecompute call is executed in the GUI thread and
// that the worker waits until it finishes
void Document::callSignalBeforeRecompute()
{
auto invokeSignalBeforeRecompute = [this]{
// this runs in the GUI thread
this->getDocument()->signalBeforeRecompute(*this->getDocument());
};
if (QThread::currentThread() == qApp->thread()) {
// already on GUI thread no hop, just call it
invokeSignalBeforeRecompute();
} else {
// hop to GUI and *block* until it returns
QMetaObject::invokeMethod(qApp, std::move(invokeSignalBeforeRecompute),
Qt::BlockingQueuedConnection);
}
}
void Document::addViewProvider(Gui::ViewProviderDocumentObject* vp)
{
// Hint: The undo/redo first adds the view provider to the Gui