Merged in f5soh/librepilot/LP-575_fedora_package (pull request #491)
[librepilot.git] / ground / gcs / src / app / main.cpp
blobb5a42b1ca0fc20cb97c22737dab9cd6b1cb72a68
1 /**
2 ******************************************************************************
4 * @file main.cpp
5 * @author The LibrePilot Team http://www.librepilot.org Copyright (C) 2017.
6 * The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
7 * Parts by Nokia Corporation (qt-info@nokia.com) Copyright (C) 2009.
8 * @brief
9 * @see The GNU Public License (GPL) Version 3
10 * @defgroup
11 * @{
13 *****************************************************************************/
15 * This program is free software; you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License as published by
17 * the Free Software Foundation; either version 3 of the License, or
18 * (at your option) any later version.
20 * This program is distributed in the hope that it will be useful, but
21 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
22 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 * for more details.
25 * You should have received a copy of the GNU General Public License along
26 * with this program; if not, write to the Free Software Foundation, Inc.,
27 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
31 The GCS application name is defined in the top level makefile - GCS_BIG_NAME / GCS_SMALL_NAME, and
32 set for the build in ../../gcs.pri and ./app.pro
34 The GCS locale is set to the system locale by default unless the "hidden" setting General/Locale has a value.
35 The user can not change General/Locale from the Options dialog.
37 The GCS language will default to the GCS locale unless the General/OverrideLanguage has a value.
38 The user can change General/OverrideLanguage to any available language from the Options dialog.
40 Both General/Locale and General/OverrideLanguage can be set from the command line or through the the factory defaults file.
42 The -D option is used to permanently set a user setting.
44 The -reset switch will clear all the user settings and will trigger a reload of the factory defaults.
46 You can combine it with the -config-file=<file> command line argument to quickly switch between multiple settings files.
48 [code]
49 gcs -reset -config-file ./MyGCS.xml
50 [/code]
52 Relative paths are relative to <install dir>/share/$(GCS_SMALL_NAME)/configurations/
54 The specified file will be used to load the factory defaults from but only when the user settings are empty.
55 If the user settings are not empty the file will not be used.
56 This switch is useful on the 1st run when the user settings are empty or in combination with -reset.
59 Quickly switch configurations
61 [code]
62 gcs -reset -config-file <relative or absolute path>
63 [/code]
65 Configuring GCS from installer
67 The -D option is used to permanently set a user setting.
69 If the user chooses to start GCS at the end of the installer:
71 [code]
72 gcs -D General/OverrideLanguage=de
73 [/code]
75 If the user chooses not to start GCS at the end of the installer, you still need to configure GCS.
76 In that case you can use -exit-after-config
78 [code]
79 gcs -D General/OverrideLanguage=de -exit-after-config
80 [/code]
84 #include "qtsingleapplication.h"
85 #include "gcssplashscreen.h"
86 #include "utils/pathutils.h"
87 #include "utils/settingsutils.h"
88 #include "utils/filelogger.h"
90 #include <extensionsystem/pluginmanager.h>
91 #include <extensionsystem/pluginspec.h>
92 #include <extensionsystem/iplugin.h>
94 #include <QtCore/QDir>
95 #include <QtCore/QElapsedTimer>
96 #include <QtCore/QTextStream>
97 #include <QtCore/QFileInfo>
98 #include <QtCore/QDebug>
99 #include <QtCore/QTimer>
100 #include <QtCore/QLibraryInfo>
101 #include <QtCore/QTranslator>
102 #include <QtCore/QSettings>
104 #include <QMessageBox>
105 #include <QApplication>
106 #include <QSurfaceFormat>
108 typedef QList<ExtensionSystem::PluginSpec *> PluginSpecSet;
109 typedef QMap<QString, bool> AppOptions;
110 typedef QMap<QString, QString> AppOptionValues;
112 const int OptionIndent = 4;
113 const int DescriptionIndent = 24;
115 const QLatin1String APP_NAME(GCS_BIG_NAME);
116 const QLatin1String ORG_NAME(ORG_BIG_NAME);
118 const QLatin1String CORE_PLUGIN_NAME("Core");
120 const char *DEFAULT_CONFIG_FILENAME = "default.xml";
122 const char *fixedOptionsC = " [OPTION]... [FILE]...\n"
123 "Options:\n"
124 " -help Display this help\n"
125 " -version Display application version\n"
126 " -no-splash Don't display splash screen\n"
127 " -log <file> Log to specified file\n"
128 " -client Attempt to connect to already running instance\n"
129 " -D <key>=<value> Permanently set a user setting, e.g: -D General/OverrideLanguage=de\n"
130 " -reset Reset user settings to factory defaults.\n"
131 " -config-file <file> Specify alternate factory defaults settings file (used with -reset)\n"
132 " -exit-after-config Exit after manipulating configuration settings\n";
134 const QLatin1String HELP1_OPTION("-h");
135 const QLatin1String HELP2_OPTION("-help");
136 const QLatin1String HELP3_OPTION("/h");
137 const QLatin1String HELP4_OPTION("--help");
138 const QLatin1String VERSION_OPTION("-version");
139 const QLatin1String NO_SPLASH_OPTION("-no-splash");
140 const QLatin1String CLIENT_OPTION("-client");
141 const QLatin1String CONFIG_OPTION("-D");
142 const QLatin1String RESET_OPTION("-reset");
143 const QLatin1String CONFIG_FILE_OPTION("-config-file");
144 const QLatin1String EXIT_AFTER_CONFIG_OPTION("-exit-after-config");
145 const QLatin1String LOG_FILE_OPTION("-log");
147 // Helpers for displaying messages. Note that there is no console on Windows.
148 void displayHelpText(QString t)
150 #ifdef Q_OS_WIN
151 // No console on Windows. (???)
152 // TODO there is a console on windows and popups are not always desired
153 t.replace(QLatin1Char('&'), QLatin1String("&amp;"));
154 t.replace(QLatin1Char('<'), QLatin1String("&lt;"));
155 t.replace(QLatin1Char('>'), QLatin1String("&gt;"));
156 t.insert(0, QLatin1String("<html><pre>"));
157 t.append(QLatin1String("</pre></html>"));
158 QMessageBox::information(0, APP_NAME, t);
159 #else
160 qWarning("%s", qPrintable(t));
161 #endif
164 void displayError(const QString &t)
166 #ifdef Q_OS_WIN
167 // No console on Windows. (???)
168 // TODO there is a console on windows and popups are not always desired
169 QMessageBox::critical(0, APP_NAME, t);
170 #else
171 qCritical("%s", qPrintable(t));
172 #endif
175 void printVersion(const ExtensionSystem::PluginSpec *corePlugin, const ExtensionSystem::PluginManager &pm)
177 QString version;
178 QTextStream str(&version);
180 str << '\n' << APP_NAME << ' ' << corePlugin->version() << " based on Qt " << qVersion() << "\n\n";
181 pm.formatPluginVersions(str);
182 str << '\n' << corePlugin->copyright() << '\n';
183 displayHelpText(version);
186 void printHelp(const QString &appExecName, const ExtensionSystem::PluginManager &pm)
188 QString help;
189 QTextStream str(&help);
191 str << "Usage: " << appExecName << fixedOptionsC;
192 ExtensionSystem::PluginManager::formatOptions(str, OptionIndent, DescriptionIndent);
193 pm.formatPluginOptions(str, OptionIndent, DescriptionIndent);
194 displayHelpText(help);
197 inline QString msgCoreLoadFailure(const QString &reason)
199 return QCoreApplication::translate("Application", "Failed to load core plug-in, reason is: %1").arg(reason);
202 inline QString msgSendArgumentFailed()
204 return QCoreApplication::translate("Application",
205 "Unable to send command line arguments to the already running instance. It appears to be not responding.");
208 inline QString msgLogfileOpenFailed(const QString &fileName)
210 return QCoreApplication::translate("Application", "Failed to open log file %1").arg(fileName);
213 // Prepare a remote argument: If it is a relative file, add the current directory
214 // since the the central instance might be running in a different directory.
215 inline QString prepareRemoteArgument(const QString &arg)
217 QFileInfo fi(arg);
219 if (!fi.exists()) {
220 return arg;
222 if (fi.isRelative()) {
223 return fi.absoluteFilePath();
225 return arg;
228 // Send the arguments to an already running instance of application
229 bool sendArguments(SharedTools::QtSingleApplication &app, const QStringList &arguments)
231 if (!arguments.empty()) {
232 // Send off arguments
233 const QStringList::const_iterator acend = arguments.constEnd();
234 for (QStringList::const_iterator it = arguments.constBegin(); it != acend; ++it) {
235 if (!app.sendMessage(prepareRemoteArgument(*it))) {
236 displayError(msgSendArgumentFailed());
237 return false;
241 // Special empty argument means: Show and raise (the slot just needs to be triggered)
242 if (!app.sendMessage(QString())) {
243 displayError(msgSendArgumentFailed());
244 return false;
246 return true;
249 void systemInit()
251 #ifdef Q_OS_MAC
252 // increase the number of file that can be opened in application
253 struct rlimit rl;
254 getrlimit(RLIMIT_NOFILE, &rl);
255 rl.rlim_cur = rl.rlim_max;
256 setrlimit(RLIMIT_NOFILE, &rl);
257 #endif
259 QApplication::setAttribute(Qt::AA_X11InitThreads, true);
261 // protect QQuickWidget from native widgets like GLC ModelView
262 // TODO revisit this...
263 QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true);
265 #ifdef Q_OS_WIN
266 #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
267 // see https://doc-snapshots.qt.io/qt5-5.6/highdpi.html
268 qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "1");
269 #else
270 // see http://doc.qt.io/qt-5/highdpi.html
271 qputenv("QT_DEVICE_PIXEL_RATIO", "auto");
272 #endif
273 #endif
275 // Force "basic" render loop
276 // Only Mac uses "threaded" by default and that mode currently does not work well with OSGViewport
277 qputenv("QSG_RENDER_LOOP", "basic");
279 // disable vsync
280 QSurfaceFormat format = QSurfaceFormat::defaultFormat();
281 format.setSwapInterval(0);
282 QSurfaceFormat::setDefaultFormat(format);
284 // see https://bugreports.qt.io/browse/QTBUG-40332
285 int timeout = std::numeric_limits<int>::max();
286 qputenv("QT_BEARER_POLL_TIMEOUT", QString::number(timeout).toLatin1());
289 static FileLogger *logger = NULL;
291 void mainMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
293 logger->log(type, context, msg);
296 Q_DECLARE_METATYPE(QtMsgType)
298 void logInit(QString &fileName)
300 qRegisterMetaType<QtMsgType>();
301 logger = new FileLogger(fileName);
302 if (!logger->start()) {
303 delete logger;
304 logger = NULL;
305 displayError(msgLogfileOpenFailed(fileName));
306 return;
308 qInstallMessageHandler(mainMessageOutput);
311 void logDeinit()
313 if (!logger) {
314 return;
316 qInstallMessageHandler(0);
317 delete logger;
318 logger = NULL;
321 AppOptions options()
323 AppOptions appOptions;
325 appOptions.insert(HELP1_OPTION, false);
326 appOptions.insert(HELP2_OPTION, false);
327 appOptions.insert(HELP3_OPTION, false);
328 appOptions.insert(HELP4_OPTION, false);
329 appOptions.insert(VERSION_OPTION, false);
330 appOptions.insert(NO_SPLASH_OPTION, false);
331 appOptions.insert(LOG_FILE_OPTION, true);
332 appOptions.insert(CLIENT_OPTION, false);
333 appOptions.insert(CONFIG_OPTION, true);
334 appOptions.insert(RESET_OPTION, false);
335 appOptions.insert(CONFIG_FILE_OPTION, true);
336 appOptions.insert(EXIT_AFTER_CONFIG_OPTION, false);
337 return appOptions;
340 AppOptionValues parseCommandLine(SharedTools::QtSingleApplication &app,
341 ExtensionSystem::PluginManager &pluginManager, QString &errorMessage)
343 AppOptionValues appOptionValues;
344 const QStringList arguments = app.arguments();
346 if (arguments.size() > 1) {
347 AppOptions appOptions = options();
348 pluginManager.parseOptions(arguments, appOptions, &appOptionValues, &errorMessage);
350 return appOptionValues;
353 void loadTranslators(QString language, QTranslator &translator, QTranslator &qtTranslator)
355 const QString &creatorTrPath = Utils::GetDataPath() + QLatin1String("translations");
357 if (translator.load(QLatin1String("gcs_") + language, creatorTrPath)) {
358 // Install gcs_xx.qm translation file
359 QCoreApplication::installTranslator(&translator);
361 const QString &qtTrPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
362 const QString &qtTrFile = QLatin1String("qt_") + language;
363 // Binary installer puts Qt tr files into creatorTrPath
364 if (qtTranslator.load(qtTrFile, qtTrPath) || qtTranslator.load(qtTrFile, creatorTrPath)) {
365 // Install main qt_xx.qm translation file
366 QCoreApplication::installTranslator(&qtTranslator);
368 } else {
369 // unload(), no gcs translation found
370 translator.load(QString());
374 int runApplication(int argc, char * *argv)
376 QElapsedTimer timer;
378 timer.start();
380 // create application
381 SharedTools::QtSingleApplication app(APP_NAME, argc, argv);
383 QCoreApplication::setApplicationName(APP_NAME);
384 QCoreApplication::setOrganizationName(ORG_NAME);
386 // initialize the plugin manager
387 ExtensionSystem::PluginManager pluginManager;
388 pluginManager.setFileExtension(QLatin1String("pluginspec"));
389 pluginManager.setPluginPaths(Utils::GetPluginPaths());
391 // parse command line
392 qDebug() << "main - command line" << app.arguments();
393 QString errorMessage;
394 AppOptionValues appOptionValues = parseCommandLine(app, pluginManager, errorMessage);
395 if (!errorMessage.isEmpty()) {
396 // this will display two popups : one error popup + one usage string popup
397 // TODO merge two popups into one.
398 displayError(errorMessage);
399 printHelp(QFileInfo(app.applicationFilePath()).baseName(), pluginManager);
400 return -1;
403 // start logging to file if requested
404 if (appOptionValues.contains(LOG_FILE_OPTION)) {
405 QString logFileName = appOptionValues.value(LOG_FILE_OPTION);
406 logInit(logFileName);
407 // relog command line arguments for the benefit of the file logger...
408 qDebug() << "main - command line" << app.arguments();
411 // init settings
412 Utils::initSettings(appOptionValues.value(CONFIG_FILE_OPTION));
414 // load user settings
415 QSettings settings;
416 qDebug() << "main - loading user settings from" << settings.fileName();
418 // need to reset user settings?
419 if (settings.allKeys().isEmpty() || appOptionValues.contains(RESET_OPTION)) {
420 qDebug() << "main - resetting user settings";
421 Utils::resetToFactoryDefaults(settings);
423 Utils::mergeFactoryDefaults(settings);
425 // override settings with command line provided values
426 // take notice that the overridden values will be saved in the user settings and will continue to be effective
427 // in subsequent GCS runs
428 Utils::overrideSettings(settings, argc, argv);
430 // initialize GCS locale
431 // use the value defined by the General/Locale setting or default to system Locale.
432 // the General/Locale setting is not available in the Options dialog, it is a hidden setting but can still be changed:
433 // - through the command line
434 // - editing the factory defaults XML file before 1st launch
435 // - editing the user XML file
436 QString localeName = settings.value("General/Locale", QLocale::system().name()).toString();
437 QLocale::setDefault(localeName);
439 // some debugging
440 qDebug() << "main - system locale:" << QLocale::system().name();
441 qDebug() << "main - GCS locale:" << QLocale().name();
443 // load translation file
444 // the language used is defined by the General/OverrideLanguage setting (defaults to GCS locale)
445 // if the translation file for the given language is not found, GCS will default to built in English.
446 QString language = settings.value("General/OverrideLanguage", localeName).toString();
447 qDebug() << "main - language:" << language;
448 QTranslator translator;
449 QTranslator qtTranslator;
450 loadTranslators(language, translator, qtTranslator);
452 app.setProperty("qtc_locale", localeName); // Do we need this?
454 if (appOptionValues.contains(EXIT_AFTER_CONFIG_OPTION)) {
455 qDebug() << "main - exiting after config!";
456 return 0;
459 // open the splash screen
460 GCSSplashScreen *splash = 0;
461 if (!appOptionValues.contains(NO_SPLASH_OPTION)) {
462 splash = new GCSSplashScreen();
463 // show splash
464 splash->showProgressMessage(QObject::tr("Application starting..."));
465 splash->show();
466 // connect to track progress of plugin manager
467 QObject::connect(&pluginManager, SIGNAL(pluginAboutToBeLoaded(ExtensionSystem::PluginSpec *)), splash,
468 SLOT(showPluginLoadingProgress(ExtensionSystem::PluginSpec *)));
471 // find and load core plugin
472 const PluginSpecSet plugins = pluginManager.plugins();
473 ExtensionSystem::PluginSpec *coreplugin = 0;
474 foreach(ExtensionSystem::PluginSpec * spec, plugins) {
475 if (spec->name() == CORE_PLUGIN_NAME) {
476 coreplugin = spec;
477 break;
480 if (!coreplugin) {
481 QString nativePaths = QDir::toNativeSeparators(Utils::GetPluginPaths().join(QLatin1String(",")));
482 const QString reason = QCoreApplication::translate("Application", "Could not find '%1.pluginspec' in %2")
483 .arg(CORE_PLUGIN_NAME).arg(nativePaths);
484 displayError(msgCoreLoadFailure(reason));
485 return 1;
487 if (coreplugin->hasError()) {
488 displayError(msgCoreLoadFailure(coreplugin->errorString()));
489 return 1;
492 if (appOptionValues.contains(VERSION_OPTION)) {
493 printVersion(coreplugin, pluginManager);
494 return 0;
496 if (appOptionValues.contains(HELP1_OPTION) || appOptionValues.contains(HELP2_OPTION)
497 || appOptionValues.contains(HELP3_OPTION) || appOptionValues.contains(HELP4_OPTION)) {
498 printHelp(QFileInfo(app.applicationFilePath()).baseName(), pluginManager);
499 return 0;
502 const bool isFirstInstance = !app.isRunning();
503 if (!isFirstInstance && appOptionValues.contains(CLIENT_OPTION)) {
504 return sendArguments(app, pluginManager.arguments()) ? 0 : -1;
507 pluginManager.loadPlugins();
509 if (coreplugin->hasError()) {
510 displayError(msgCoreLoadFailure(coreplugin->errorString()));
511 return 1;
514 QStringList errors;
515 foreach(ExtensionSystem::PluginSpec * p, pluginManager.plugins()) {
516 if (p->hasError()) {
517 errors.append(p->errorString());
520 if (!errors.isEmpty()) {
521 QMessageBox::warning(0,
522 QCoreApplication::translate("Application", "%1 - Plugin loader messages").arg(GCS_BIG_NAME),
523 errors.join(QString::fromLatin1("\n\n")));
526 if (isFirstInstance) {
527 // Set up lock and remote arguments for the first instance only.
528 // Silently fallback to unconnected instances for any subsequent instances.
529 app.initialize();
530 QObject::connect(&app, SIGNAL(messageReceived(QString)), coreplugin->plugin(), SLOT(remoteArgument(QString)));
532 QObject::connect(&app, SIGNAL(fileOpenRequest(QString)), coreplugin->plugin(), SLOT(remoteArgument(QString)));
534 // Do this after the event loop has started
535 QTimer::singleShot(100, &pluginManager, SLOT(startTests()));
537 if (splash) {
538 // close and delete splash
539 splash->close();
540 delete splash;
543 qDebug() << "main - starting GCS took" << timer.elapsed() << "ms";
544 int ret = app.exec();
545 qDebug() << "main - GCS ran for" << timer.elapsed() << "ms";
547 return ret;
550 int main(int argc, char * *argv)
552 // low level init
553 systemInit();
555 int ret = runApplication(argc, argv);
557 // close log file if needed
558 logDeinit();
560 return ret;