|
2 | 2 |
|
3 | 3 | // geant4 |
4 | 4 | #include "G4UImanager.hh" |
| 5 | +#include "G4coutDestination.hh" |
5 | 6 |
|
6 | 7 | GUI_Session::GUI_Session(const std::shared_ptr<GOptions>& gopt, GBoard* b) : |
7 | 8 | GBase(gopt, GBOARD_LOGGER), |
8 | 9 | board(b) { |
9 | | - // Route Geant4 UI output to this session instance, so we can forward it to the GUI board. |
| 10 | + // Route Geant4 UI output to this session instance so we can forward it to the GUI board. |
| 11 | + // SetCoutDestination updates the per-thread stream buffer but does NOT update |
| 12 | + // masterG4coutDestination, which G4UIQt set to itself in its constructor. |
| 13 | + // Worker threads use G4MasterForwardcoutDestination -> masterG4coutDestination, so |
| 14 | + // we must update it here to prevent G4UIQt::ReceiveG4cerr from being called on a |
| 15 | + // background thread (which would trigger an NSAlert from a non-main thread on macOS). |
10 | 16 | G4UImanager::GetUIpointer()->SetCoutDestination(this); |
| 17 | + G4coutDestination::masterG4coutDestination = this; |
11 | 18 |
|
12 | 19 | log->info(1, SFUNCTION_NAME, " g4 dialog : GUI_Session created"); |
13 | 20 | } |
14 | 21 |
|
15 | 22 | G4int GUI_Session::ReceiveG4cout(const G4String& coutString) { |
16 | 23 | // See header for API docs. |
17 | | - if (board) { |
18 | | - QString fullQString = QString::fromStdString(coutString); |
19 | | - |
20 | | - // Split into lines so that the board gets "log-like" incremental entries. |
21 | | - // KeepEmptyParts preserves blank lines, while avoiding QRegularExpression |
22 | | - // compatibility issues with Unicode line separator escapes. |
23 | | - fullQString.replace("\r\n", "\n"); |
24 | | - fullQString.replace('\r', '\n'); |
25 | | - fullQString.replace(QChar(0x2028), '\n'); |
26 | | - QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts); |
27 | | - |
28 | | - for (const QString& line : lines) { |
29 | | - // Convert ANSI attributes (if present) into HTML for rich-text display. |
30 | | - QString htmlLine = ansiToHtml(line); |
31 | | - board->appendLog(htmlLine); |
32 | | - } |
33 | | - } |
| 24 | + if (!board) return 0; |
| 25 | + |
| 26 | + QString fullQString = QString::fromStdString(coutString); |
| 27 | + // Split into lines so that the board gets "log-like" incremental entries. |
| 28 | + fullQString.replace("\r\n", "\n"); |
| 29 | + fullQString.replace('\r', '\n'); |
| 30 | + fullQString.replace(QChar(0x2028), '\n'); |
| 31 | + QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts); |
| 32 | + |
| 33 | + // Convert ANSI to HTML on the calling thread (no Qt object access needed). |
| 34 | + QStringList htmlLines; |
| 35 | + htmlLines.reserve(lines.size()); |
| 36 | + for (const QString& line : lines) { htmlLines << ansiToHtml(line); } |
| 37 | + |
| 38 | + // Post widget update to the main thread. Qt::AutoConnection calls directly |
| 39 | + // when already on the main thread, and queues the call otherwise, preventing |
| 40 | + // NSWindow/NSAlert operations from a Geant4 worker thread on macOS. |
| 41 | + auto* b = board; |
| 42 | + QMetaObject::invokeMethod(b, [b, htmlLines]() { |
| 43 | + for (const QString& htmlLine : htmlLines) { b->appendLog(htmlLine); } |
| 44 | + }); |
34 | 45 | return 0; |
35 | 46 | } |
36 | 47 |
|
37 | 48 |
|
38 | 49 | G4int GUI_Session::ReceiveG4cerr(const G4String& cerrString) { |
39 | 50 | // See header for API docs. |
40 | | - if (board) { |
41 | | - QString fullQString = QString::fromStdString(cerrString); |
42 | | - |
43 | | - // Use the same line normalization as stdout. |
44 | | - fullQString.replace("\r\n", "\n"); |
45 | | - fullQString.replace('\r', '\n'); |
46 | | - fullQString.replace(QChar(0x2028), '\n'); |
47 | | - QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts); |
48 | | - |
49 | | - for (const QString& line : lines) { |
50 | | - QString htmlLine = ansiToHtml(line); |
51 | | - board->appendLog(htmlLine); |
52 | | - } |
53 | | - } |
| 51 | + if (!board) return 0; |
| 52 | + |
| 53 | + QString fullQString = QString::fromStdString(cerrString); |
| 54 | + fullQString.replace("\r\n", "\n"); |
| 55 | + fullQString.replace('\r', '\n'); |
| 56 | + fullQString.replace(QChar(0x2028), '\n'); |
| 57 | + QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts); |
| 58 | + |
| 59 | + QStringList htmlLines; |
| 60 | + htmlLines.reserve(lines.size()); |
| 61 | + for (const QString& line : lines) { htmlLines << ansiToHtml(line); } |
| 62 | + |
| 63 | + auto* b = board; |
| 64 | + QMetaObject::invokeMethod(b, [b, htmlLines]() { |
| 65 | + for (const QString& htmlLine : htmlLines) { b->appendLog(htmlLine); } |
| 66 | + }); |
54 | 67 | return 0; |
55 | 68 | } |
56 | 69 |
|
@@ -211,8 +224,12 @@ GUI_Session::~GUI_Session() { |
211 | 224 | // See header for API docs. |
212 | 225 | // Detach Geant4 cout/cerr from our GUI session (avoid dangling callback). |
213 | 226 | if (auto* UIM = G4UImanager::GetUIpointer()) { |
214 | | - // If available in your G4 version, prefer checking the current destination: |
215 | | - // if (UIM->GetCoutDestination() == gui_session.get()) { ... } |
216 | 227 | UIM->SetCoutDestination(nullptr); |
217 | 228 | } |
| 229 | + // Also clear masterG4coutDestination, which we claimed in the constructor. |
| 230 | + // Worker threads read this pointer at call time, so nulling it here prevents |
| 231 | + // any in-flight forwarding from reaching the already-destroyed session. |
| 232 | + if (G4coutDestination::masterG4coutDestination == this) { |
| 233 | + G4coutDestination::masterG4coutDestination = nullptr; |
| 234 | + } |
218 | 235 | } |
0 commit comments