diff --git a/sources/main.cpp b/sources/main.cpp index eede1bfb8..e723d0d43 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -19,6 +19,7 @@ #include "machine_info.h" #include "qet.h" #include "qetapp.h" +#include "qetmessagebox.h" #include "qetproject.h" #include "singleapplication.h" #include "utils/qetsettings.h" @@ -249,6 +250,11 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto // runs on a background thread referencing the project and races the // process exit (intermittent segfault in QET::writeToFile). QETProject::setBackupEnabled(false); + // Answer message boxes instead of showing them: opening a project + // saved by an older QElectroTech raises a warning from + // QETProject::readProjectXml(), and with nobody able to dismiss it + // QDialog::exec() would spin its event loop forever. + QET::QetMessageBox::setNonInteractive(true); return CLIExport::run(export_app.arguments()); } } diff --git a/sources/qetmessagebox.cpp b/sources/qetmessagebox.cpp index 31d6e41a8..2731baf00 100644 --- a/sources/qetmessagebox.cpp +++ b/sources/qetmessagebox.cpp @@ -17,6 +17,84 @@ */ #include "qetmessagebox.h" +#include + +namespace { + bool g_non_interactive = false; + + /** + @brief autoAnswer + Report a message box on stderr and pick an answer, for use when there + is no user to click anything. @see QET::QetMessageBox::setNonInteractive + @param severity : short word naming the kind of box, for the log line + @param title + @param text + @param buttons : the buttons the caller offered + @param defaultButton : the caller's preferred answer, may be NoButton + @return the button to report as pressed + */ + QMessageBox::StandardButton autoAnswer( + const char *severity, + const QString &title, + const QString &text, + QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) + { + //Honour the caller's own default when it named one. + if (defaultButton != QMessageBox::NoButton + && (buttons & defaultButton)) { + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: answered with the caller's default button)\n"; + return defaultButton; + } + + //Otherwise prefer a "carry on" answer over one that cancels, so a + //batch run completes rather than silently doing nothing. + static const QMessageBox::StandardButton preference[] = { + QMessageBox::Ok, QMessageBox::Open, QMessageBox::Yes, + QMessageBox::Save, QMessageBox::Apply, QMessageBox::YesToAll, + QMessageBox::Retry, QMessageBox::Ignore, QMessageBox::Close + }; + for (auto candidate : preference) { + if (buttons & candidate) { + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: continuing)\n"; + return candidate; + } + } + + //Nothing affirmative on offer -- fall back to whatever is set. + for (int bit = QMessageBox::Ok; bit <= QMessageBox::RestoreDefaults; bit <<= 1) { + auto candidate = static_cast(bit); + if (buttons & candidate) { + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: answered automatically)\n"; + return candidate; + } + } + + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: no button offered)\n"; + return QMessageBox::NoButton; + } +} + +/** + @brief QET::QetMessageBox::setNonInteractive + @param non_interactive +*/ +void QET::QetMessageBox::setNonInteractive(bool non_interactive) { + g_non_interactive = non_interactive; +} + +/** + @brief QET::QetMessageBox::isNonInteractive + @return true when message boxes are answered without a user +*/ +bool QET::QetMessageBox::isNonInteractive() { + return g_non_interactive; +} + /** @see Documentation Qt pour QMessageBox::critical */ @@ -27,6 +105,9 @@ QMessageBox::StandardButton QET::QetMessageBox::critical ( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Critical", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Critical, @@ -59,6 +140,9 @@ QMessageBox::StandardButton QET::QetMessageBox::information( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Information", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Information, @@ -91,6 +175,9 @@ QMessageBox::StandardButton QET::QetMessageBox::question ( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Question", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Question, @@ -123,6 +210,9 @@ QMessageBox::StandardButton QET::QetMessageBox::warning ( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Warning", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Warning, diff --git a/sources/qetmessagebox.h b/sources/qetmessagebox.h index 60238595a..6e838f4e4 100644 --- a/sources/qetmessagebox.h +++ b/sources/qetmessagebox.h @@ -27,6 +27,28 @@ namespace QET { Qt:Sheet flag, thus enabling a better MacOS integration. */ namespace QetMessageBox { + /** + Enable non-interactive mode. + + In non-interactive mode the functions below never construct a + dialog. They write the message to stderr and return an answer + immediately, so a headless run cannot block on a modal box that + nobody is there to dismiss. + + This is needed because these are reachable from the command-line + tools: opening a project written by an older QElectroTech raises + a warning from QETProject::readProjectXml(), and with no display + to click it, QDialog::exec() spins its event loop forever. + + The answer is chosen as: the caller's defaultButton when it gave + one, otherwise the first "carry on" button among those offered + (Ok, Open, Yes, Save, Apply...), otherwise the first button set. + So the two warnings above resolve to Open and the project loads, + which is what a batch invocation wants. + */ + void setNonInteractive(bool non_interactive); + bool isNonInteractive(); + QMessageBox::StandardButton critical ( QWidget *, const QString &,