1 // QtLauncher.cxx - GUI launcher dialog using Qt5
2 //
3 // Written by James Turner, started December 2014.
4 //
5 // Copyright (C) 2014 James Turner <zakalawe@mac.com>
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 
21 #include "config.h"
22 
23 #include "QtLauncher.hxx"
24 
25 #include <locale.h>
26 
27 // Qt
28 #include <QApplication>
29 #include <QDebug>
30 #include <QDir>
31 #include <QFileInfo>
32 #include <QMessageBox>
33 #include <QOffscreenSurface>
34 #include <QOpenGLContext>
35 #include <QPointer>
36 #include <QProcess>
37 #include <QProgressDialog>
38 #include <QSettings>
39 #include <QString>
40 #include <QThread>
41 #include <QTimer>
42 #include <QTranslator>
43 #include <QUrl>
44 #include <QtGlobal>
45 
46 // Simgear
47 #include <simgear/timing/timestamp.hxx>
48 #include <simgear/props/props_io.hxx>
49 #include <simgear/structure/exception.hxx>
50 #include <simgear/structure/subsystem_mgr.hxx>
51 #include <simgear/misc/sg_path.hxx>
52 #include <simgear/misc/strutils.hxx>
53 #include <simgear/package/Root.hxx>
54 #include <simgear/package/Catalog.hxx>
55 #include <simgear/package/Package.hxx>
56 #include <simgear/package/Install.hxx>
57 #include <simgear/debug/logstream.hxx>
58 
59 #include <Add-ons/AddonManager.hxx>
60 #include <Airports/airport.hxx>
61 #include <Main/fg_props.hxx>
62 #include <Main/globals.hxx>
63 #include <Main/sentryIntegration.hxx>
64 #include <Navaids/NavDataCache.hxx>
65 #include <Navaids/SHPParser.hxx>
66 #include <Navaids/navrecord.hxx>
67 
68 
69 #include <Main/fg_init.hxx>
70 #include <Main/locale.hxx>
71 #include <Main/options.hxx>
72 #include <Network/HTTPClient.hxx>
73 #include <Viewer/WindowBuilder.hxx>
74 
75 #include "LaunchConfig.hxx"
76 #include "LauncherMainWindow.hxx"
77 #include "LocalAircraftCache.hxx"
78 #include "PathListModel.hxx"
79 #include "UnitsModel.hxx"
80 #include "GettingStartedTip.hxx"
81 
82 #if defined(SG_MAC)
83 #include <GUI/CocoaHelpers.h>
84 #endif
85 
86 using namespace flightgear;
87 using namespace simgear::pkg;
88 using std::string;
89 
90 namespace { // anonymous namespace
91 
92 struct ProgressLabel {
93     NavDataCache::RebuildPhase phase;
94     const char* label;
95 };
96 
97 static std::initializer_list<ProgressLabel> progressStrings = {
98     {NavDataCache::REBUILD_READING_APT_DAT_FILES, QT_TRANSLATE_NOOP("initNavCache","Reading airport data")},
99     {NavDataCache::REBUILD_LOADING_AIRPORTS, QT_TRANSLATE_NOOP("initNavCache","Loading airports")},
100     {NavDataCache::REBUILD_FIXES, QT_TRANSLATE_NOOP("initNavCache","Loading waypoint data")},
101     {NavDataCache::REBUILD_NAVAIDS, QT_TRANSLATE_NOOP("initNavCache","Loading navigation data")},
102     {NavDataCache::REBUILD_POIS, QT_TRANSLATE_NOOP("initNavCache","Loading point-of-interest data")}
103 };
104 
initNavCache()105 bool initNavCache()
106 {
107     const char* baseLabelKey = QT_TRANSLATE_NOOP("initNavCache", "Initialising navigation data, this may take several minutes");
108     QString baseLabel= qApp->translate("initNavCache", baseLabelKey);
109 
110     const auto wflags = Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::MSWindowsFixedSizeDialogHint;
111 
112     if (NavDataCache::isAnotherProcessRebuilding()) {
113         const char* waitForOtherMsg = QT_TRANSLATE_NOOP("initNavCache", "Another copy of FlightGear is creating the navigation database. Waiting for it to finish.");
114         QString m = qApp->translate("initNavCache", waitForOtherMsg);
115 
116         addSentryBreadcrumb("Launcher: showing wait for other process NavCache rebuild dialog", "info");
117         QProgressDialog waitForRebuild(m,
118                                        QString() /* cancel text */,
119                                        0, 0, Q_NULLPTR,
120                                        wflags);
121         waitForRebuild.setWindowModality(Qt::WindowModal);
122         waitForRebuild.setMinimumWidth(600);
123         waitForRebuild.setAutoReset(false);
124         waitForRebuild.setAutoClose(false);
125         waitForRebuild.show();
126 
127         QTimer updateTimer;
128         updateTimer.setInterval(500);
129         bool rebuildIsDone = false;
130 
131         QObject::connect(&updateTimer, &QTimer::timeout, [&waitForRebuild, &rebuildIsDone]() {
132             if (!NavDataCache::isAnotherProcessRebuilding()) {
133                 waitForRebuild.done(0);
134                 rebuildIsDone = true;
135                 return;
136             }
137         });
138 
139         updateTimer.start(); // timer won't actually run until we process events
140         waitForRebuild.exec();
141         updateTimer.stop();
142 
143         if (!rebuildIsDone) {
144             flightgear::addSentryBreadcrumb("Launcher wait on other process nav-cache rebuild abandoned by user", "info");
145             return false;
146         }
147 
148         addSentryBreadcrumb("Launcher: done waiting for other process NavCache rebuild dialog", "info");
149     }
150 
151     NavDataCache* cache = NavDataCache::createInstance();
152     if (cache->isRebuildRequired()) {
153         QProgressDialog rebuildProgress(baseLabel,
154                                         QString() /* cancel text */,
155                                         0, 100, Q_NULLPTR,
156                                         wflags);
157         rebuildProgress.setWindowModality(Qt::WindowModal);
158         rebuildProgress.setMinimumWidth(600);
159         rebuildProgress.setAutoReset(false);
160         rebuildProgress.setAutoClose(false);
161         rebuildProgress.show();
162 
163         QTimer updateTimer;
164         updateTimer.setInterval(100);
165 
166         bool didComplete = false;
167 
168         QObject::connect(&updateTimer, &QTimer::timeout, [&cache, &rebuildProgress, &baseLabel, &didComplete]() {
169             auto phase = cache->rebuild();
170             if (phase == NavDataCache::REBUILD_DONE) {
171                 rebuildProgress.done(0);
172                 didComplete = true;
173                 return;
174             }
175 
176             auto it = std::find_if(progressStrings.begin(), progressStrings.end(), [phase]
177                                    (const ProgressLabel& l) { return l.phase == phase; });
178             if (it == progressStrings.end()) {
179                 rebuildProgress.setLabelText(baseLabel);
180             } else {
181                 QString trans = qApp->translate("initNavCache", it->label);
182                 rebuildProgress.setLabelText(trans);
183             }
184 
185             if (phase == NavDataCache::REBUILD_UNKNOWN) {
186                 rebuildProgress.setValue(0);
187                 rebuildProgress.setMaximum(0);
188             } else {
189                 rebuildProgress.setValue(static_cast<int>(cache->rebuildPhaseCompletionPercentage()));
190                 rebuildProgress.setMaximum(100);
191             }
192         });
193 
194         updateTimer.start(); // timer won't actually run until we process events
195         rebuildProgress.exec();
196         updateTimer.stop();
197 
198         if (!didComplete) {
199             flightgear::addSentryBreadcrumb("Launcher nav-cache rebuild abandoned by user", "info");
200             return false;
201         }
202 
203         flightgear::addSentryBreadcrumb("Launcher nav-cache rebuild complete", "info");
204     }
205 
206     return true;
207 }
208 
209 class NaturalEarthDataLoaderThread : public QThread
210 {
211     Q_OBJECT
212 public:
213 
NaturalEarthDataLoaderThread()214     NaturalEarthDataLoaderThread() :
215         m_lineInsertCount(0)
216     {
217         connect(this, &QThread::finished, this, &NaturalEarthDataLoaderThread::onFinished);
218     }
219 
abandon()220     void abandon()
221     {
222         m_abandoned = true;
223     }
224 protected:
run()225     void run() override
226     {
227         loadNaturalEarthFile("ne_10m_coastline.shp", flightgear::PolyLine::COASTLINE, false);
228         loadNaturalEarthFile("ne_10m_rivers_lake_centerlines.shp", flightgear::PolyLine::RIVER, false);
229         loadNaturalEarthFile("ne_10m_lakes.shp", flightgear::PolyLine::LAKE, true);
230         loadNaturalEarthFile("ne_10m_urban_areas.shp", flightgear::PolyLine::URBAN, true);
231     }
232 
233 private:
onFinished()234     Q_SLOT void onFinished()
235     {
236         if (m_abandoned)
237             return;
238 
239 
240         flightgear::PolyLine::bulkAddToSpatialIndex(m_parsedLines.begin(), m_parsedLines.end());
241         deleteLater(); // commit suicide
242     }
243 
loadNaturalEarthFile(const std::string & aFileName,flightgear::PolyLine::Type aType,bool areClosed)244     void loadNaturalEarthFile(const std::string& aFileName,
245                               flightgear::PolyLine::Type aType,
246                               bool areClosed)
247     {
248         SGPath path(globals->get_fg_root());
249         path.append( "Geodata" );
250         path.append(aFileName);
251         if (!path.exists())
252             return; // silently fail for now
253 
254         flightgear::SHPParser::parsePolyLines(path, aType, m_parsedLines, areClosed);
255     }
256 
257     flightgear::PolyLineList m_parsedLines;
258     unsigned int m_lineInsertCount;
259     bool m_abandoned = false;
260 };
261 
262 enum class OpenGLStatus
263 {
264     OpenGL21,
265     Unknown,
266     GDIGeneric,
267     Intel14
268 };
269 
checkForWorkingOpenGL()270 OpenGLStatus checkForWorkingOpenGL()
271 {
272     // request an OpenGL comptability profile, version 2.1
273     // anything lower and we'll crash
274     QSurfaceFormat fmt;
275     fmt.setProfile(QSurfaceFormat::CompatibilityProfile);
276     fmt.setMajorVersion(2);
277     fmt.setMinorVersion(1);
278 
279     QOpenGLContext ctx;
280     ctx.setFormat(fmt);
281     if (!ctx.create()) {
282         return OpenGLStatus::Unknown;
283     }
284 
285     // from here on, we need to ensure orderly cleanup or some drivers
286     // crash. So we can't early return.
287 
288     OpenGLStatus result = OpenGLStatus::Unknown;
289     QOffscreenSurface offSurface;
290     offSurface.setFormat(ctx.format()); // ensure it's compatible
291     offSurface.create();
292 
293     if (ctx.makeCurrent(&offSurface)) {
294         result = OpenGLStatus::OpenGL21;
295         std::string renderer = (char*)glGetString(GL_RENDERER);
296         if (renderer == "GDI Generic") {
297             flightgear::addSentryBreadcrumb("Detected GDI generic renderer", "info");
298             result = OpenGLStatus::GDIGeneric;
299         } else if (simgear::strutils::starts_with(renderer, "Intel")) {
300             if (ctx.format().majorVersion() < 2) {
301                 flightgear::addSentryBreadcrumb("Detected Intel < 2.1 renderer", "info");
302                 result = OpenGLStatus::Intel14;
303             }
304         } else if (simgear::strutils::starts_with(renderer, "S3 Graphics")) {
305             if (ctx.format().majorVersion() < 2) {
306                 flightgear::addSentryBreadcrumb("Detected S2 < 2.1 renderer", "info");
307                 result = OpenGLStatus::Unknown;
308             }
309         }
310 
311         // ensure the context is no longer current on the offscreen
312         ctx.doneCurrent();
313     }
314 
315     offSurface.destroy();
316     return result;
317 }
318 
319 } // of anonymous namespace
320 
initQtResources()321 static void initQtResources()
322 {
323     Q_INIT_RESOURCE(resources);
324 #if defined(HAVE_QRC_TRANSLATIONS)
325     Q_INIT_RESOURCE(translations);
326 #endif
327 }
328 
simgearMessageOutput(QtMsgType type,const QMessageLogContext & context,const QString & msg)329 static void simgearMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
330 {
331     sgDebugPriority mappedPriority = SG_WARN;
332     switch (type) {
333     case QtDebugMsg:    mappedPriority = SG_DEBUG; break;
334 #if QT_VERSION >= 0x050500
335     case QtInfoMsg:     mappedPriority = SG_INFO; break;
336 #endif
337     case QtWarningMsg:  mappedPriority = SG_WARN; break;
338     case QtCriticalMsg: mappedPriority = SG_ALERT; break;
339     case QtFatalMsg:    mappedPriority = SG_POPUP; break;
340     }
341 
342     static const char* nullFile = "";
343     const char* file = context.file ? context.file : nullFile;
344     const auto s = msg.toStdString();
345     // important we copy the file name here, since QMessageLogContext doesn't
346     sglog().logCopyingFilename(SG_GUI, mappedPriority, file, context.line, s);
347     if (type == QtFatalMsg) {
348         // if we abort() here, we get a thread crash which prevents
349         // us actually seeing the error
350         // we'll attempt to continue so we do the SG_POPUP dialog, maybe it works
351         // if it crashes we're no worse off than calling abort() here
352     }
353 }
354 
355 namespace flightgear
356 {
357 
358 // making this a unique ptr ensures the QApplication will be deleted
359 // event if we forget to call shutdownQtApp. Cleanly destroying this is
360 // important so QPA resources, in particular the XCB thread, are exited
361 // cleanly on quit. However, at present, the official policy is that static
362 // destruction is too late to call this, hence why we have shutdownQtApp()
363 
364 static std::unique_ptr<QApplication> static_qApp;
365 
366 
367 // becuase QTranslator::load (find_translation, internally) doesn't handle the
368 // sciprt part of a language code like: zh-Hans-CN, use this code borrowed from
369 // Qt Creator to do the search manually.
selectUITranslation()370 void selectUITranslation()
371 {
372     QStringList uiLanguages = QLocale::system().uiLanguages();
373     //qWarning() << "UI languages:" << uiLanguages;
374 
375     for (QString locale : qAsConst(uiLanguages)) {
376         // remove script if it exists, eg zh-Hans-CN -> zh-CN
377         locale = QLocale(locale).name();
378         locale.replace('-', '_');
379 
380         QTranslator* translator = new QTranslator;
381         ;
382 
383         if (translator->load("FlightGear_" + locale, QLatin1String(":/"))) {
384             static_qApp->installTranslator(translator);
385             return;
386         }
387 
388         delete translator;
389     }
390 }
391 
392 // Only requires FGGlobals to be initialized if 'doInitQSettings' is true.
393 // Safe to call several times.
initApp(int & argc,char ** argv,bool doInitQSettings)394 void initApp(int& argc, char** argv, bool doInitQSettings)
395 {
396     static bool qtInitDone = false;
397     static int s_argc;
398 
399     if (!qtInitDone) {
400         qtInitDone = true;
401 
402         // Disable Qt 5.15 warnings about obsolete Connections/onFoo: syntax
403         // we cannot use the new syntax
404         // as long as we have to support Qt 5.9
405         qputenv("QT_LOGGING_RULES", "qt.qml.connections.warning=false");
406 
407         initQtResources(); // can't be called from a namespace
408 
409         s_argc = argc; // QApplication only stores a reference to argc,
410         // and may crash if it is freed
411         // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication
412 
413         // log to simgear instead of the console from Qt, so we go to
414         // whichever log locations SimGear has configured
415         qInstallMessageHandler(simgearMessageOutput);
416 
417         // ensure we use desktop OpenGL, don't even fall back to ANGLE, since
418         // this gets into a knot on Optimus setups (since we export the magic
419         // Optimus / AMD symbols in main.cxx).
420         QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
421 
422 		// becuase on Windows, Qt only supports integer scaling factors,
423 		// forceibly enabling HighDpiScaling is controversial.
424 		// leave things unset here, so users can use env var
425 		// QT_AUTO_SCREEN_SCALE_FACTOR=1 to enable it at runtime
426 
427 #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
428 #if !defined (SG_WINDOWS)
429     QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
430 #endif
431 #endif
432         static_qApp.reset(new QApplication(s_argc, argv));
433         static_qApp->setOrganizationName("FlightGear");
434         static_qApp->setApplicationName("FlightGear");
435         static_qApp->setOrganizationDomain("flightgear.org");
436 
437 #if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
438         static_qApp->setDesktopFileName(
439           QStringLiteral("org.flightgear.FlightGear.desktop"));
440 #endif
441         QTranslator* fallbackTranslator = new QTranslator(static_qApp.get());
442         if (!fallbackTranslator->load(QLatin1String(":/FlightGear_en_US.qm"))) {
443             qWarning() << "Failed to load default (en) translations";
444             delete fallbackTranslator;
445         } else {
446             static_qApp->installTranslator(fallbackTranslator);
447         }
448 
449         // check for --langauge=xx option and prefer that over QLocale
450         // detection of the locale if it exists
451         auto lang = simgear::strutils::replace(
452             Options::getArgValue(argc, argv, "--language"),
453             "-",
454             "_");
455         if (!lang.empty()) {
456             QTranslator* translator = new QTranslator(static_qApp.get());
457             QString localeFile = "FlightGear_" + QString::fromStdString(lang);
458             if (translator->load(localeFile, QLatin1String(":/"))) {
459                 qInfo() << "Loaded translations based on --language from:" << localeFile;
460                 static_qApp->installTranslator(translator);
461             } else {
462                 qInfo() << "--langauge was set, but no translations found at:" << localeFile;
463                 delete translator;
464             }
465         } else {
466             selectUITranslation();
467         }
468 
469         // temporary code: only enable getting-started tips for English, to give translators time to
470         // catch up
471         if (!lang.empty()) {
472             GettingStartedTip::setGlobalTipsEnabled(simgear::strutils::starts_with(lang, "en"));
473         } else {
474             QLocale currentLocale;
475             GettingStartedTip::setGlobalTipsEnabled(currentLocale.name().startsWith("en"));
476         }
477 
478         // reset numeric / collation locales as described at:
479         // http://doc.qt.io/qt-5/qcoreapplication.html#details
480         ::setlocale(LC_NUMERIC, "C");
481         ::setlocale(LC_COLLATE, "C");
482 
483 
484 #if defined(SG_MAC)
485         if (cocoaIsRunningTranslocated()) {
486             addSentryBreadcrumb("did show translocation warning", "info");
487             const char* titleKey = QT_TRANSLATE_NOOP("macTranslationWarning", "Application running from download location");
488             const char* key = QT_TRANSLATE_NOOP("macTranslationWarning", "FlightGear is running from the download image. For better performance and to avoid potential problems, "
489                                                 "please copy FlightGear to some other location, such as your desktop or Applications folder.");
490             QString msg = qApp->translate("macTranslationWarning", key);
491             QString title = qApp->translate("macTranslationWarning", titleKey);
492 
493             QMessageBox::warning(nullptr, title, msg);
494         }
495 #endif
496     }
497 
498     if (doInitQSettings) {
499         initQSettings();
500     }
501 }
502 
shutdownQtApp()503 void shutdownQtApp()
504 {
505 	// restore default message handler, otherwise Qt logging on
506 	// shutdown crashes once sglog is killed
507 	qInstallMessageHandler(nullptr);
508     static_qApp.reset();
509 }
510 
511 // Requires FGGlobals to be initialized. Safe to call several times.
initQSettings()512 void initQSettings()
513 {
514     static bool qSettingsInitDone = false;
515 
516     if (!qSettingsInitDone) {
517         qRegisterMetaType<QuantityValue>();
518         qRegisterMetaTypeStreamOperators<QuantityValue>("QuantityValue");
519 
520 
521         qSettingsInitDone = true;
522         string fgHome = globals->get_fg_home().utf8Str();
523 
524         QSettings::setDefaultFormat(QSettings::IniFormat);
525         QSettings::setPath(QSettings::IniFormat, QSettings::UserScope,
526                            QString::fromStdString(fgHome));
527     }
528 }
529 
checkKeyboardModifiersForSettingFGRoot()530 bool checkKeyboardModifiersForSettingFGRoot()
531 {
532     initQSettings();
533 #if defined(Q_OS_WIN)
534     const auto altState = GetAsyncKeyState(VK_MENU);
535     const auto shiftState = GetAsyncKeyState(VK_SHIFT);
536     if ((altState < 0) || (shiftState < 0))
537 #else
538     Qt::KeyboardModifiers mods = qApp->queryKeyboardModifiers();
539     if (mods & (Qt::AltModifier | Qt::ShiftModifier))
540 #endif
541     {
542         qWarning() << "Alt/shift pressed during launch";
543         return true;
544     }
545 
546     return false;
547 }
548 
restartTheApp()549 void restartTheApp()
550 {
551     QStringList fgArgs;
552 
553     // Spawn a new instance of myApplication:
554     QProcess proc;
555     QStringList args;
556 
557 	// ensure we release whatever mutex/lock file we have in home,
558 	// so the new instance runs in writeable mode
559 	fgShutdownHome();
560 
561 #if defined(Q_OS_MAC)
562     QDir dir(qApp->applicationDirPath()); // returns the 'MacOS' dir
563     dir.cdUp(); // up to 'contents' dir
564     dir.cdUp(); // up to .app dir
565     // see 'man open' for details, but '-n' ensures we launch a new instance,
566     // and we want to pass remaining arguments to us, not open.
567     args << "-n" << dir.absolutePath() << "--args" << "--launcher" << fgArgs;
568     qDebug() << "args" << args;
569     proc.startDetached("open", args);
570 #else
571     args << "--launcher" << fgArgs;
572     proc.startDetached(qApp->applicationFilePath(), args);
573 #endif
574     qApp->exit(-1);
575 }
576 
startLaunchOnExit(const std::vector<std::string> & originalCommandLine)577 void startLaunchOnExit(const std::vector<std::string>& originalCommandLine)
578 {
579     QStringList fgArgs;
580     for (const auto& arg : originalCommandLine) {
581         fgArgs.append(QString::fromStdString(arg));
582     }
583 
584     QProcess proc;
585 #if defined(Q_OS_MAC)
586     QDir dir(qApp->applicationDirPath()); // returns the 'MacOS' dir
587     dir.cdUp();                           // up to 'contents' dir
588     dir.cdUp();                           // up to .app dir
589 
590     QStringList args;
591     // see 'man open' for details, but '-n' ensures we launch a new instance,
592     // and we want to pass remaining arguments to us, not open.
593     args << "-n" << dir.absolutePath() << "--args" << fgArgs;
594     qDebug() << "args" << args;
595     proc.startDetached("open", args);
596 #else
597     proc.startDetached(qApp->applicationFilePath(), fgArgs);
598 #endif
599 }
600 
launcherSetSceneryPaths()601 void launcherSetSceneryPaths()
602 {
603     globals->clear_fg_scenery();
604 
605     // process path sthe user supplied on the existing command line
606     const auto commandLineSceneryPaths = flightgear::Options::sharedInstance()->valuesForOption("fg-scenery");
607     for (const auto& arg : commandLineSceneryPaths) {
608         // each arg can be multiple paths
609         globals->append_fg_scenery(SGPath::pathsFromUtf8(arg));
610     }
611 
612 // mimic what options.cxx does, so we can find airport data for parking
613 // positions
614     QSettings settings;
615     // append explicit scenery paths
616     Q_FOREACH(QString path, PathListModel::readEnabledPaths("scenery-paths-v2")) {
617         globals->append_fg_scenery(path.toStdString());
618     }
619 
620     // append the TerraSync path
621     QString downloadDir = settings.value("download-dir").toString();
622     if (downloadDir.isEmpty()) {
623         downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());
624     }
625 
626     SGPath terraSyncDir(downloadDir.toStdString());
627     terraSyncDir.append("TerraSync");
628     if (terraSyncDir.exists()) {
629         globals->append_fg_scenery(terraSyncDir);
630     }
631 
632     // add the installation path since it contains default airport data,
633     // if terrasync is disabled or on first-launch
634     const SGPath rootScenery = globals->get_fg_root() / "Scenery";
635     if (rootScenery.exists()) {
636         globals->append_fg_scenery(rootScenery);
637     }
638 }
639 
runLauncherDialog()640 bool runLauncherDialog()
641 {
642     auto glCheckResult = checkForWorkingOpenGL();
643     if (glCheckResult != OpenGLStatus::OpenGL21) {
644         QMessageBox::critical(nullptr, "Failed to find graphics drivers",
645                               "This computer is missing suitable graphics drivers (OpenGL) to run FlightGear. "
646                               "Please download and install drivers from your graphics card vendor.");
647         return false;
648     }
649 
650     // Used for NavDataCache initialization: needed to find the apt.dat files
651     launcherSetSceneryPaths();
652     // startup the nav-cache now. This pre-empts normal startup of
653     // the cache, but no harm done. (Providing scenery paths are consistent)
654 
655     bool ok = initNavCache();
656     if (!ok) {
657         return false;
658     }
659 
660     auto options = flightgear::Options::sharedInstance();
661     if (options->isOptionSet("download-dir")) {
662         // user set download-dir on command line, don't mess with it in the
663         // launcher GUI. We'll disable the UI.
664         LaunchConfig::setEnableDownloadDirUI(false);
665     } else {
666         QSettings settings;
667         QString downloadDir = settings.value("download-dir").toString();
668         if (!downloadDir.isEmpty()) {
669             options->setOption("download-dir", downloadDir.toStdString());
670         }
671     }
672 
673     fgInitPackageRoot();
674 
675     // setup package language
676     auto lang = options->valueForOption("language");
677     if (lang.empty()) {
678         const auto langName = QLocale::languageToString(QLocale{}.language());
679         lang = langName.toStdString();
680     }
681 
682     // we will re-do this later, but we want to access translated strings
683     // from within the launcher
684     globals->get_locale()->selectLanguage(lang);
685     globals->packageRoot()->setLocale(globals->get_locale()->getPreferredLanguage());
686 
687     // startup the HTTP system now since packages needs it
688     FGHTTPClient* http = globals->add_new_subsystem<FGHTTPClient>();
689 
690     // we guard against re-init in the global phase; bind and postinit
691     // will happen as normal
692     http->init();
693 
694     QPointer<NaturalEarthDataLoaderThread> naturalEarthLoader = new NaturalEarthDataLoaderThread;
695     naturalEarthLoader->start();
696 
697     // avoid double Apple menu and other weirdness if both Qt and OSG
698     // try to initialise various Cocoa structures.
699     flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
700 
701     LauncherMainWindow dlg(false);
702     if (options->isOptionSet("enable-fullscreen")) {
703         dlg.showFullScreen();
704     } else {
705         dlg.show();
706     }
707 
708     int appResult = qApp->exec();
709     if (appResult <= 0) {
710         return false; // quit
711     }
712 
713     // avoid crashes / NavCache races if the loader is still running after
714     // the launcher exits
715     if (naturalEarthLoader) {
716         naturalEarthLoader->abandon();
717     }
718 
719     // avoid a race-y crash on the locale, if a scan thread is
720     // still running: this reset will cancel any running scan
721     LocalAircraftCache::reset();
722 
723     // don't set scenery paths twice
724     globals->clear_fg_scenery();
725     globals->get_locale()->clear();
726 
727     return true;
728 }
729 
runInAppLauncherDialog()730 bool runInAppLauncherDialog()
731 {
732     LauncherMainWindow dlg(true);
733     bool accepted = dlg.execInApp();
734     if (!accepted) {
735         return false;
736     }
737 
738     return true;
739 }
740 
741 static const char* static_lockFileDialog_Title =
742     QT_TRANSLATE_NOOP("LockFileDialog", "Multiple copies of FlightGear running");
743 static const char* static_lockFileDialog_Text =
744     QT_TRANSLATE_NOOP("LockFileDialog",
745                       "FlightGear has detected another copy is already running. "
746                       "This copy will run in read-only mode, so downloads will not be possible, "
747                       "and settings will not be saved.");
748 static const char* static_lockFileDialog_Info =
749     QT_TRANSLATE_NOOP("LockFileDialog",
750                       "If you are sure another copy is not running on this computer, "
751                       "you can choose to reset the lock file, and run this copy as normal. "
752                        "Alternatively, you can close this copy of the software.");
753 
showLockFileDialog()754 LockFileDialogResult showLockFileDialog()
755 {
756     flightgear::addSentryBreadcrumb("showing lock-file dialog", "info");
757 
758     QString title = qApp->translate("LockFileDialog", static_lockFileDialog_Title);
759     QString text = qApp->translate("LockFileDialog", static_lockFileDialog_Text);
760     QString infoText = qApp->translate("LockFileDialog", static_lockFileDialog_Info);
761 
762     QMessageBox mb;
763     mb.setIconPixmap(QPixmap(":/app-icon-large"));
764     mb.setWindowTitle(title);
765     mb.setText(text);
766     mb.setInformativeText(infoText);
767     mb.addButton(QMessageBox::Ok);
768     mb.setDefaultButton(QMessageBox::Ok);
769     mb.addButton(QMessageBox::Reset);
770     mb.addButton(QMessageBox::Close);
771 
772     int r = mb.exec();
773     if (r == QMessageBox::Reset)
774         return LockFileReset;
775     if (r == QMessageBox::Close)
776         return LockFileQuit;
777     return LockFileContinue;
778 }
779 
780 } // of namespace flightgear
781 
782 #include "QtLauncher.moc"
783