Merge pull request #7 from parched/OP-1879_remove_openpilot_hardcoding
[librepilot.git] / ground / openpilotgcs / src / app / main.cpp
blob02338ba9eb4f0518ea742d16012a0cdc607d2621
1 /**
2 ******************************************************************************
4 * @file main.cpp
5 * @author The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
6 * Parts by Nokia Corporation (qt-info@nokia.com) Copyright (C) 2009.
7 * @brief
8 * @see The GNU Public License (GPL) Version 3
9 * @defgroup
10 * @{
12 *****************************************************************************/
14 * This program is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; either version 3 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful, but
20 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
21 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
22 * for more details.
24 * You should have received a copy of the GNU General Public License along
25 * with this program; if not, write to the Free Software Foundation, Inc.,
26 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
30 The GCS locale is set to the system locale by default unless the "hidden" setting General/Locale has a value.
31 The user can not change General/Locale from the Options dialog.
33 The GCS language will default to the GCS locale unless the General/OverrideLanguage has a value.
34 The user can change General/OverrideLanguage to any available language from the Options dialog.
36 Both General/Locale and General/OverrideLanguage can be set from the command line or through the the factory defaults file.
38 The -D option is used to permanently set a user setting.
40 The -reset switch will clear all the user settings and will trigger a reload of the factory defaults.
42 You can combine it with the -config-file=<file> command line argument to quickly switch between multiple settings files.
44 [code]
45 openpilotgcs -reset -config-file ./MyOpenPilotGCS.xml
46 [/code]
48 Relative paths are relative to <install dir>/share/openpilotgcs/default_configurations/
50 The specified file will be used to load the factory defaults from but only when the user settings are empty.
51 If the user settings are not empty the file will not be used.
52 This switch is useful on the 1st run when the user settings are empty or in combination with -reset.
55 Quickly switch configurations
57 [code]
58 openpilotgcs -reset -config-file <relative or absolute path>
59 [/code]
61 Configuring GCS from installer
63 The -D option is used to permanently set a user setting.
65 If the user chooses to start GCS at the end of the installer:
67 [code]
68 openpilotgcs -D General/OverrideLanguage=de
69 [/code]
71 If the user chooses not to start GCS at the end of the installer, you still need to configure GCS.
72 In that case you can use -exit-after-config
74 [code]
75 openpilotgcs -D General/OverrideLanguage=de -exit-after-config
76 [/code]
80 #include "qtsingleapplication.h"
81 #include "utils/xmlconfig.h"
82 #include "utils/pathutils.h"
83 #include "gcssplashscreen.h"
85 #include <extensionsystem/pluginmanager.h>
86 #include <extensionsystem/pluginspec.h>
87 #include <extensionsystem/iplugin.h>
89 #include <QtCore/QDir>
90 #include <QtCore/QFile>
91 #include <QtCore/QElapsedTimer>
92 #include <QtCore/QTextStream>
93 #include <QtCore/QFileInfo>
94 #include <QtCore/QDebug>
95 #include <QtCore/QTimer>
96 #include <QtCore/QLibraryInfo>
97 #include <QtCore/QTranslator>
98 #include <QtCore/QSettings>
99 #include <QtCore/QVariant>
101 #include <QMessageBox>
102 #include <QtWidgets/QApplication>
103 #include <QMainWindow>
104 #include <QSplashScreen>
106 namespace {
107 typedef QList<ExtensionSystem::PluginSpec *> PluginSpecSet;
108 typedef QMap<QString, bool> AppOptions;
109 typedef QMap<QString, QString> AppOptionValues;
111 const int OptionIndent = 4;
112 const int DescriptionIndent = 24;
114 const QLatin1String APP_NAME(GCS_NAME);
115 const QLatin1String ORG_NAME("OpenPilot");
117 const QLatin1String CORE_PLUGIN_NAME("Core");
119 const char *DEFAULT_CONFIG_FILENAME = "OpenPilotGCS.xml";
121 const char *fixedOptionsC = " [OPTION]... [FILE]...\n"
122 "Options:\n"
123 " -help Display this help\n"
124 " -version Display application version\n"
125 " -no-splash Don't display splash screen\n"
126 " -log <file> Log to specified file\n"
127 " -client Attempt to connect to already running instance\n"
128 " -D <key>=<value> Permanently set a user setting, e.g: -D General/OverrideLanguage=de\n"
129 " -reset Reset user settings to factory defaults.\n"
130 " -config-file <file> Specify alternate factory defaults settings file (used with -reset)\n"
131 " -exit-after-config Exit after manipulating configuration settings\n";
133 const QLatin1String HELP1_OPTION("-h");
134 const QLatin1String HELP2_OPTION("-help");
135 const QLatin1String HELP3_OPTION("/h");
136 const QLatin1String HELP4_OPTION("--help");
137 const QLatin1String VERSION_OPTION("-version");
138 const QLatin1String NO_SPLASH_OPTION("-no-splash");
139 const QLatin1String CLIENT_OPTION("-client");
140 const QLatin1String CONFIG_OPTION("-D");
141 const QLatin1String RESET_OPTION("-reset");
142 const QLatin1String CONFIG_FILE_OPTION("-config-file");
143 const QLatin1String EXIT_AFTER_CONFIG_OPTION("-exit-after-config");
144 const QLatin1String LOG_FILE_OPTION("-log");
146 // Helpers for displaying messages. Note that there is no console on Windows.
147 void displayHelpText(QString t)
149 #ifdef Q_OS_WIN
150 // No console on Windows. (???)
151 // TODO there is a console on windows and popups are not always desired
152 t.replace(QLatin1Char('&'), QLatin1String("&amp;"));
153 t.replace(QLatin1Char('<'), QLatin1String("&lt;"));
154 t.replace(QLatin1Char('>'), QLatin1String("&gt;"));
155 t.insert(0, QLatin1String("<html><pre>"));
156 t.append(QLatin1String("</pre></html>"));
157 QMessageBox::information(0, APP_NAME, t);
158 #else
159 qWarning("%s", qPrintable(t));
160 #endif
163 void displayError(const QString &t)
165 #ifdef Q_OS_WIN
166 // No console on Windows. (???)
167 // TODO there is a console on windows and popups are not always desired
168 QMessageBox::critical(0, APP_NAME, t);
169 #else
170 qCritical("%s", qPrintable(t));
171 #endif
174 void printVersion(const ExtensionSystem::PluginSpec *corePlugin, const ExtensionSystem::PluginManager &pm)
176 QString version;
177 QTextStream str(&version);
179 str << '\n' << APP_NAME << ' ' << corePlugin->version() << " based on Qt " << qVersion() << "\n\n";
180 pm.formatPluginVersions(str);
181 str << '\n' << corePlugin->copyright() << '\n';
182 displayHelpText(version);
185 void printHelp(const QString &appExecName, const ExtensionSystem::PluginManager &pm)
187 QString help;
188 QTextStream str(&help);
190 str << "Usage: " << appExecName << fixedOptionsC;
191 ExtensionSystem::PluginManager::formatOptions(str, OptionIndent, DescriptionIndent);
192 pm.formatPluginOptions(str, OptionIndent, DescriptionIndent);
193 displayHelpText(help);
196 inline QString msgCoreLoadFailure(const QString &reason)
198 return QCoreApplication::translate("Application", "Failed to load core plug-in, reason is: %1").arg(reason);
201 inline QString msgSendArgumentFailed()
203 return QCoreApplication::translate("Application",
204 "Unable to send command line arguments to the already running instance. It appears to be not responding.");
207 // Prepare a remote argument: If it is a relative file, add the current directory
208 // since the the central instance might be running in a different directory.
209 inline QString prepareRemoteArgument(const QString &arg)
211 QFileInfo fi(arg);
213 if (!fi.exists()) {
214 return arg;
216 if (fi.isRelative()) {
217 return fi.absoluteFilePath();
219 return arg;
222 // Send the arguments to an already running instance of application
223 bool sendArguments(SharedTools::QtSingleApplication &app, const QStringList &arguments)
225 if (!arguments.empty()) {
226 // Send off arguments
227 const QStringList::const_iterator acend = arguments.constEnd();
228 for (QStringList::const_iterator it = arguments.constBegin(); it != acend; ++it) {
229 if (!app.sendMessage(prepareRemoteArgument(*it))) {
230 displayError(msgSendArgumentFailed());
231 return false;
235 // Special empty argument means: Show and raise (the slot just needs to be triggered)
236 if (!app.sendMessage(QString())) {
237 displayError(msgSendArgumentFailed());
238 return false;
240 return true;
243 void systemInit()
245 #ifdef Q_OS_MAC
246 // increase the number of file that can be opened in application
247 struct rlimit rl;
248 getrlimit(RLIMIT_NOFILE, &rl);
249 rl.rlim_cur = rl.rlim_max;
250 setrlimit(RLIMIT_NOFILE, &rl);
251 QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true);
252 #endif
253 #ifdef Q_OS_LINUX
254 QApplication::setAttribute(Qt::AA_X11InitThreads, true);
255 #endif
258 static QTextStream *logStream;
260 void mainMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
262 Q_UNUSED(context);
264 QTextStream &out = *logStream;
266 // logStream << QTime::currentTime().toString("hh:mm:ss.zzz ");
268 switch (type) {
269 case QtDebugMsg:
270 out << "DBG: ";
271 break;
272 case QtWarningMsg:
273 out << "WRN: ";
274 break;
275 case QtCriticalMsg:
276 out << "CRT: ";
277 break;
278 case QtFatalMsg:
279 out << "FTL: ";
280 break;
283 out << msg << '\n';
284 out.flush();
287 void logInit(QString fileName)
289 QFile *file = new QFile(fileName);
291 if (file->open(QIODevice::WriteOnly | QIODevice::Text)) {
292 logStream = new QTextStream(file);
293 qInstallMessageHandler(mainMessageOutput);
294 } else {
295 // TODO error popup
299 inline QStringList getPluginPaths()
301 QStringList rc;
303 QString pluginPath = QApplication::applicationDirPath();
304 pluginPath += QLatin1Char('/');
305 pluginPath += QLatin1String(PLUGIN_REL_PATH);
306 rc.push_back(pluginPath);
308 return rc;
311 AppOptions options()
313 AppOptions appOptions;
315 appOptions.insert(HELP1_OPTION, false);
316 appOptions.insert(HELP2_OPTION, false);
317 appOptions.insert(HELP3_OPTION, false);
318 appOptions.insert(HELP4_OPTION, false);
319 appOptions.insert(VERSION_OPTION, false);
320 appOptions.insert(NO_SPLASH_OPTION, false);
321 appOptions.insert(LOG_FILE_OPTION, true);
322 appOptions.insert(CLIENT_OPTION, false);
323 appOptions.insert(CONFIG_OPTION, true);
324 appOptions.insert(RESET_OPTION, false);
325 appOptions.insert(CONFIG_FILE_OPTION, true);
326 appOptions.insert(EXIT_AFTER_CONFIG_OPTION, false);
327 return appOptions;
330 AppOptionValues parseCommandLine(SharedTools::QtSingleApplication &app,
331 ExtensionSystem::PluginManager &pluginManager, QString &errorMessage)
333 AppOptionValues appOptionValues;
334 const QStringList arguments = app.arguments();
336 if (arguments.size() > 1) {
337 AppOptions appOptions = options();
338 pluginManager.parseOptions(arguments, appOptions, &appOptionValues, &errorMessage);
340 return appOptionValues;
343 void loadFactoryDefaults(QSettings &settings, AppOptionValues &appOptionValues)
345 QDir directory(Utils::PathUtils().GetDataPath() + QString("default_configurations"));
347 qDebug() << "Looking for factory defaults configuration files in:" << directory.absolutePath();
349 QString fileName;
351 // check if command line option -config-file contains a file name
352 QString commandLine = appOptionValues.value(CONFIG_FILE_OPTION);
353 if (!commandLine.isEmpty()) {
354 QFileInfo fi(commandLine);
355 if (fi.isRelative()) {
356 // file name specified on command line has a relative path
357 commandLine = directory.absolutePath() + QDir::separator() + commandLine;
359 if (QFile::exists(commandLine)) {
360 fileName = commandLine;
361 qDebug() << "Configuration file" << fileName << "specified on command line will be loaded.";
362 } else {
363 qWarning() << "Configuration file" << commandLine << "specified on command line does not exist.";
367 if (fileName.isEmpty()) {
368 // check default file
369 if (QFile::exists(directory.absolutePath() + QDir::separator() + DEFAULT_CONFIG_FILENAME)) {
370 // use default file name
371 fileName = directory.absolutePath() + QDir::separator() + DEFAULT_CONFIG_FILENAME;
372 qDebug() << "Default configuration file" << fileName << "will be loaded.";
373 } else {
374 qWarning() << "No default configuration file found in" << directory.absolutePath();
378 if (fileName.isEmpty()) {
379 // TODO should we exit violently?
380 qCritical() << "No default configuration file found!";
381 return;
384 // create settings from file
385 QSettings qs(fileName, XmlConfig::XmlSettingsFormat);
387 // transfer loaded settings to application settings
388 QStringList keys = qs.allKeys();
389 foreach(QString key, keys) {
390 settings.setValue(key, qs.value(key));
393 qDebug() << "Configuration file" << fileName << "was loaded.";
396 void overrideSettings(QSettings &settings, int argc, char * *argv)
398 // Options like -D My/setting=test
399 QRegExp rx("([^=]+)=(.*)");
401 for (int i = 0; i < argc; ++i) {
402 if (CONFIG_OPTION == QString(argv[i])) {
403 if (rx.indexIn(argv[++i]) > -1) {
404 QString key = rx.cap(1);
405 QString value = rx.cap(2);
406 qDebug() << "User setting" << key << "set to value" << value;
407 settings.setValue(key, value);
412 settings.sync();
415 void loadTranslators(QString language, QTranslator &translator, QTranslator &qtTranslator)
417 const QString &creatorTrPath = Utils::PathUtils().GetDataPath() + QLatin1String("translations");
419 if (translator.load(QLatin1String("openpilotgcs_") + language, creatorTrPath)) {
420 const QString &qtTrPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
421 const QString &qtTrFile = QLatin1String("qt_") + language;
422 // Binary installer puts Qt tr files into creatorTrPath
423 if (qtTranslator.load(qtTrFile, qtTrPath) || qtTranslator.load(qtTrFile, creatorTrPath)) {
424 QCoreApplication::installTranslator(&translator);
425 QCoreApplication::installTranslator(&qtTranslator);
426 } else {
427 // unload()
428 translator.load(QString());
432 } // namespace anonymous
434 int main(int argc, char * *argv)
436 QElapsedTimer timer;
438 timer.start();
440 // low level init
441 systemInit();
443 // create application
444 SharedTools::QtSingleApplication app(APP_NAME, argc, argv);
446 QCoreApplication::setApplicationName(APP_NAME);
447 QCoreApplication::setOrganizationName(ORG_NAME);
448 QSettings::setDefaultFormat(XmlConfig::XmlSettingsFormat);
450 // initialize the plugin manager
451 ExtensionSystem::PluginManager pluginManager;
452 pluginManager.setFileExtension(QLatin1String("pluginspec"));
453 pluginManager.setPluginPaths(getPluginPaths());
455 // parse command line
456 qDebug() << "Command line" << app.arguments();
457 QString errorMessage;
458 AppOptionValues appOptionValues = parseCommandLine(app, pluginManager, errorMessage);
459 if (!errorMessage.isEmpty()) {
460 // this will display two popups : one error popup + one usage string popup
461 // TODO merge two popups into one.
462 displayError(errorMessage);
463 printHelp(QFileInfo(app.applicationFilePath()).baseName(), pluginManager);
464 return -1;
467 if (appOptionValues.contains(LOG_FILE_OPTION)) {
468 QString logFileName = appOptionValues.value(LOG_FILE_OPTION);
469 logInit(logFileName);
472 // load user settings
473 // Must be done before any QSettings class is created
474 // keep this in sync with the MainWindow ctor in coreplugin/mainwindow.cpp
475 QString settingsPath = Utils::PathUtils().GetDataPath();
476 qDebug() << "Loading system settings from" << settingsPath;
477 QSettings::setPath(XmlConfig::XmlSettingsFormat, QSettings::SystemScope, settingsPath);
478 QSettings settings;
479 qDebug() << "Loading user settings from" << settings.fileName();
481 // need to reset all user settings?
482 if (appOptionValues.contains(RESET_OPTION)) {
483 qDebug() << "Resetting user settings!";
484 settings.clear();
487 // check if we have user settings
488 if (!settings.allKeys().count()) {
489 // no user settings, load the factory defaults
490 qDebug() << "No user settings found, loading factory defaults...";
491 loadFactoryDefaults(settings, appOptionValues);
494 // override settings with command line provided values
495 // take notice that the overridden values will be saved in the user settings and will continue to be effective
496 // in subsequent GCS runs
497 overrideSettings(settings, argc, argv);
499 // initialize GCS locale
500 // use the value defined by the General/Locale setting or default to system Locale.
501 // the General/Locale setting is not available in the Options dialog, it is a hidden setting but can still be changed:
502 // - through the command line
503 // - editing the factory defaults XML file before 1st launch
504 // - editing the user XML file
505 QString localeName = settings.value("General/Locale", QLocale::system().name()).toString();
506 QLocale::setDefault(localeName);
508 // some debugging
509 qDebug() << "main - system locale:" << QLocale::system().name();
510 qDebug() << "main - GCS locale:" << QLocale().name();
512 // load translation file
513 // the language used is defined by the General/OverrideLanguage setting (defaults to GCS locale)
514 // if the translation file for the given language is not found, GCS will default to built in English.
515 QString language = settings.value("General/OverrideLanguage", localeName).toString();
516 qDebug() << "main - language:" << language;
517 QTranslator translator;
518 QTranslator qtTranslator;
519 loadTranslators(language, translator, qtTranslator);
521 app.setProperty("qtc_locale", localeName); // Do we need this?
523 if (appOptionValues.contains(EXIT_AFTER_CONFIG_OPTION)) {
524 qDebug() << "main - exiting after config!";
525 return 0;
528 // open the splash screen
529 GCSSplashScreen *splash = 0;
530 if (!appOptionValues.contains(NO_SPLASH_OPTION)) {
531 splash = new GCSSplashScreen();
532 // show splash
533 splash->showProgressMessage(QObject::tr("Application starting..."));
534 splash->show();
535 // connect to track progress of plugin manager
536 QObject::connect(&pluginManager, SIGNAL(pluginAboutToBeLoaded(ExtensionSystem::PluginSpec *)), splash,
537 SLOT(showPluginLoadingProgress(ExtensionSystem::PluginSpec *)));
540 // find and load core plugin
541 const PluginSpecSet plugins = pluginManager.plugins();
542 ExtensionSystem::PluginSpec *coreplugin = 0;
543 foreach(ExtensionSystem::PluginSpec * spec, plugins) {
544 if (spec->name() == CORE_PLUGIN_NAME) {
545 coreplugin = spec;
546 break;
549 if (!coreplugin) {
550 QString nativePaths = QDir::toNativeSeparators(getPluginPaths().join(QLatin1String(",")));
551 const QString reason = QCoreApplication::translate("Application", "Could not find 'Core.pluginspec' in %1").arg(
552 nativePaths);
553 displayError(msgCoreLoadFailure(reason));
554 return 1;
556 if (coreplugin->hasError()) {
557 displayError(msgCoreLoadFailure(coreplugin->errorString()));
558 return 1;
561 if (appOptionValues.contains(VERSION_OPTION)) {
562 printVersion(coreplugin, pluginManager);
563 return 0;
565 if (appOptionValues.contains(HELP1_OPTION) || appOptionValues.contains(HELP2_OPTION)
566 || appOptionValues.contains(HELP3_OPTION) || appOptionValues.contains(HELP4_OPTION)) {
567 printHelp(QFileInfo(app.applicationFilePath()).baseName(), pluginManager);
568 return 0;
571 const bool isFirstInstance = !app.isRunning();
572 if (!isFirstInstance && appOptionValues.contains(CLIENT_OPTION)) {
573 return sendArguments(app, pluginManager.arguments()) ? 0 : -1;
576 pluginManager.loadPlugins();
578 if (coreplugin->hasError()) {
579 displayError(msgCoreLoadFailure(coreplugin->errorString()));
580 return 1;
584 QStringList errors;
585 foreach(ExtensionSystem::PluginSpec * p, pluginManager.plugins()) {
586 if (p->hasError()) {
587 errors.append(p->errorString());
590 if (!errors.isEmpty()) {
591 QMessageBox::warning(0,
592 QCoreApplication::translate("Application", "OpenPilot GCS - Plugin loader messages"),
593 errors.join(QString::fromLatin1("\n\n")));
597 if (isFirstInstance) {
598 // Set up lock and remote arguments for the first instance only.
599 // Silently fallback to unconnected instances for any subsequent instances.
600 app.initialize();
601 QObject::connect(&app, SIGNAL(messageReceived(QString)), coreplugin->plugin(), SLOT(remoteArgument(QString)));
603 QObject::connect(&app, SIGNAL(fileOpenRequest(QString)), coreplugin->plugin(), SLOT(remoteArgument(QString)));
605 // Do this after the event loop has started
606 QTimer::singleShot(100, &pluginManager, SLOT(startTests()));
608 if (splash) {
609 // close and delete splash
610 splash->close();
611 delete splash;
614 qDebug() << "main - main took" << timer.elapsed() << "ms";
616 int ret = app.exec();
618 qDebug() << "main - GCS ran for" << timer.elapsed() << "ms";
620 return ret;