]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Fix a problem for MSVC
[flightgear.git] / src / GUI / QtLauncher.cxx
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 "QtLauncher.hxx"
22 #include "QtLauncher_private.hxx"
23
24 #include <locale.h>
25
26 // Qt
27 #include <QProgressDialog>
28 #include <QCoreApplication>
29 #include <QAbstractListModel>
30 #include <QDir>
31 #include <QFileInfo>
32 #include <QPixmap>
33 #include <QTimer>
34 #include <QDebug>
35 #include <QCompleter>
36 #include <QListView>
37 #include <QSettings>
38 #include <QSortFilterProxyModel>
39 #include <QMenu>
40 #include <QDesktopServices>
41 #include <QUrl>
42 #include <QAction>
43 #include <QFileDialog>
44 #include <QMessageBox>
45 #include <QDateTime>
46 #include <QApplication>
47 #include <QSpinBox>
48 #include <QDoubleSpinBox>
49 #include <QProcess>
50 #include <QThread>
51
52 // Simgear
53 #include <simgear/timing/timestamp.hxx>
54 #include <simgear/props/props_io.hxx>
55 #include <simgear/structure/exception.hxx>
56 #include <simgear/structure/subsystem_mgr.hxx>
57 #include <simgear/misc/sg_path.hxx>
58 #include <simgear/package/Catalog.hxx>
59 #include <simgear/package/Package.hxx>
60 #include <simgear/package/Install.hxx>
61
62 #include "ui_Launcher.h"
63 #include "ui_NoOfficialHangar.h"
64
65 #include "EditRatingsFilterDialog.hxx"
66 #include "AircraftItemDelegate.hxx"
67 #include "AircraftModel.hxx"
68 #include "PathsDialog.hxx"
69 #include "EditCustomMPServerDialog.hxx"
70
71 #include <Main/globals.hxx>
72 #include <Main/fg_props.hxx>
73 #include <Navaids/NavDataCache.hxx>
74 #include <Navaids/navrecord.hxx>
75 #include <Navaids/SHPParser.hxx>
76
77 #include <Main/options.hxx>
78 #include <Main/fg_init.hxx>
79 #include <Viewer/WindowBuilder.hxx>
80 #include <Network/HTTPClient.hxx>
81 #include <Network/RemoteXMLRequest.hxx>
82
83 using namespace flightgear;
84 using namespace simgear::pkg;
85
86 const int MAX_RECENT_AIRCRAFT = 20;
87
88 namespace { // anonymous namespace
89
90 void initNavCache()
91 {
92     QString baseLabel = QT_TR_NOOP("Initialising navigation data, this may take several minutes");
93     NavDataCache* cache = NavDataCache::createInstance();
94     if (cache->isRebuildRequired()) {
95         QProgressDialog rebuildProgress(baseLabel,
96                                        QString() /* cancel text */,
97                                        0, 100, Q_NULLPTR,
98                                        Qt::Dialog
99                                            | Qt::CustomizeWindowHint
100                                            | Qt::WindowTitleHint
101                                            | Qt::WindowSystemMenuHint
102                                            | Qt::MSWindowsFixedSizeDialogHint);
103         rebuildProgress.setWindowModality(Qt::WindowModal);
104         rebuildProgress.show();
105
106         NavDataCache::RebuildPhase phase = cache->rebuild();
107
108         while (phase != NavDataCache::REBUILD_DONE) {
109             // sleep to give the rebuild thread more time
110             SGTimeStamp::sleepForMSec(50);
111             phase = cache->rebuild();
112
113             switch (phase) {
114             case NavDataCache::REBUILD_AIRPORTS:
115                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading airport data"));
116                 break;
117
118             case NavDataCache::REBUILD_FIXES:
119                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading waypoint data"));
120                 break;
121
122             case NavDataCache::REBUILD_NAVAIDS:
123                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading navigation data"));
124                 break;
125
126
127             case NavDataCache::REBUILD_POIS:
128                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading point-of-interest data"));
129                 break;
130
131             default:
132                 rebuildProgress.setLabelText(baseLabel);
133             }
134
135             if (phase == NavDataCache::REBUILD_UNKNOWN) {
136                 rebuildProgress.setValue(0);
137                 rebuildProgress.setMaximum(0);
138             } else {
139                 rebuildProgress.setValue(cache->rebuildPhaseCompletionPercentage());
140                 rebuildProgress.setMaximum(100);
141             }
142
143             QCoreApplication::processEvents();
144         }
145     }
146 }
147
148 class ArgumentsTokenizer
149 {
150 public:
151     class Arg
152     {
153     public:
154         explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
155
156         QString arg;
157         QString value;
158     };
159
160     QList<Arg> tokenize(QString in) const
161     {
162         int index = 0;
163         const int len = in.count();
164         QChar c, nc;
165         State state = Start;
166         QString key, value;
167         QList<Arg> result;
168
169         for (; index < len; ++index) {
170             c = in.at(index);
171             nc = index < (len - 1) ? in.at(index + 1) : QChar();
172
173             switch (state) {
174             case Start:
175                 if (c == QChar('-')) {
176                     if (nc == QChar('-')) {
177                         state = Key;
178                         key.clear();
179                         ++index;
180                     } else {
181                         // should we pemit single hyphen arguments?
182                         // choosing to fail for now
183                         return QList<Arg>();
184                     }
185                 } else if (c == QChar('#')) {
186                     state = Comment;
187                     break;
188                 } else if (c.isSpace()) {
189                     break;
190                 }
191                 break;
192
193             case Key:
194                 if (c == QChar('=')) {
195                     state = Value;
196                     value.clear();
197                 } else if (c.isSpace()) {
198                     state = Start;
199                     result.append(Arg(key));
200                 } else {
201                     // could check for illegal charatcers here
202                     key.append(c);
203                 }
204                 break;
205
206             case Value:
207                 if (c == QChar('"')) {
208                     state = Quoted;
209                 } else if (c.isSpace()) {
210                     state = Start;
211                     result.append(Arg(key, value));
212                 } else {
213                     value.append(c);
214                 }
215                 break;
216
217             case Quoted:
218                 if (c == QChar('\\')) {
219                     // check for escaped double-quote inside quoted value
220                     if (nc == QChar('"')) {
221                         ++index;
222                     }
223                 } else if (c == QChar('"')) {
224                     state = Value;
225                 } else {
226                     value.append(c);
227                 }
228                 break;
229
230             case Comment:
231                 if ((c == QChar('\n')) || (c == QChar('\r'))) {
232                     state = Start;
233                     break;
234                 } else {
235                     // nothing to do, eat comment chars
236                 }
237                 break;
238             } // of state switch
239         } // of character loop
240
241         // ensure last argument isn't lost
242         if (state == Key) {
243             result.append(Arg(key));
244         } else if (state == Value) {
245             result.append(Arg(key, value));
246         }
247
248         return result;
249     }
250
251 private:
252     enum State {
253         Start = 0,
254         Key,
255         Value,
256         Quoted,
257         Comment
258     };
259 };
260
261 class NaturalEarthDataLoaderThread : public QThread
262 {
263     Q_OBJECT
264 public:
265
266     NaturalEarthDataLoaderThread() :
267         m_lineInsertCount(0)
268     {
269         connect(this, &QThread::finished, this, &NaturalEarthDataLoaderThread::onFinished);
270     }
271
272 protected:
273     virtual void run() Q_DECL_OVERRIDE
274     {
275         SGTimeStamp st;
276         st.stamp();
277
278         loadNaturalEarthFile("ne_10m_coastline.shp", flightgear::PolyLine::COASTLINE, false);
279         loadNaturalEarthFile("ne_10m_rivers_lake_centerlines.shp", flightgear::PolyLine::RIVER, false);
280         loadNaturalEarthFile("ne_10m_lakes.shp", flightgear::PolyLine::LAKE, true);
281
282         qDebug() << "load basic data took" << st.elapsedMSec();
283
284         st.stamp();
285         loadNaturalEarthFile("ne_10m_urban_areas.shp", flightgear::PolyLine::URBAN, true);
286
287         qDebug() << "loading urban areas took:" << st.elapsedMSec();
288     }
289
290 private:
291     void onFinished()
292     {
293         flightgear::PolyLineList::const_iterator begin = m_parsedLines.begin() + m_lineInsertCount;
294         unsigned int numToAdd = std::min<unsigned long>(1000UL, m_parsedLines.size() - m_lineInsertCount);
295         flightgear::PolyLineList::const_iterator end = begin + numToAdd;
296         flightgear::PolyLine::bulkAddToSpatialIndex(begin, end);
297
298         if (end == m_parsedLines.end()) {
299             deleteLater(); // commit suicide
300         } else {
301             m_lineInsertCount += 1000;
302             QTimer::singleShot(50, this, SLOT(onFinished()));
303         }
304     }
305
306     void loadNaturalEarthFile(const std::string& aFileName,
307                               flightgear::PolyLine::Type aType,
308                               bool areClosed)
309     {
310         SGPath path(globals->get_fg_root());
311         path.append( "Geodata" );
312         path.append(aFileName);
313         if (!path.exists())
314             return; // silently fail for now
315
316         flightgear::PolyLineList lines;
317         flightgear::SHPParser::parsePolyLines(path, aType, m_parsedLines, areClosed);
318     }
319     
320     flightgear::PolyLineList m_parsedLines;
321     unsigned int m_lineInsertCount;
322 };
323
324 } // of anonymous namespace
325
326 class AircraftProxyModel : public QSortFilterProxyModel
327 {
328     Q_OBJECT
329 public:
330     AircraftProxyModel(QObject* pr) :
331         QSortFilterProxyModel(pr),
332         m_ratingsFilter(true),
333         m_onlyShowInstalled(false)
334     {
335         for (int i=0; i<4; ++i) {
336             m_ratings[i] = 3;
337         }
338     }
339
340     void setRatings(int* ratings)
341     {
342         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
343         invalidate();
344     }
345
346 public slots:
347     void setRatingFilterEnabled(bool e)
348     {
349         if (e == m_ratingsFilter) {
350             return;
351         }
352
353         m_ratingsFilter = e;
354         invalidate();
355     }
356
357     void setInstalledFilterEnabled(bool e)
358     {
359         if (e == m_onlyShowInstalled) {
360             return;
361         }
362
363         m_onlyShowInstalled = e;
364         invalidate();
365     }
366
367 protected:
368     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
369     {
370         QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
371         QVariant v = index.data(AircraftPackageStatusRole);
372         AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
373         if (status == NoOfficialCatalogMessage) {
374             return true;
375         }
376
377         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
378             return false;
379         }
380
381         if (m_onlyShowInstalled) {
382             QVariant v = index.data(AircraftPackageStatusRole);
383             AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
384             if (status == PackageNotInstalled) {
385                 return false;
386             }
387         }
388
389         if (!m_onlyShowInstalled && m_ratingsFilter) {
390             for (int i=0; i<4; ++i) {
391                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
392                     return false;
393                 }
394             }
395         }
396
397         return true;
398     }
399
400 private:
401     bool m_ratingsFilter;
402     bool m_onlyShowInstalled;
403     int m_ratings[4];
404 };
405
406 class NoOfficialHangarMessage : public QWidget
407 {
408     Q_OBJECT
409 public:
410     NoOfficialHangarMessage() :
411         m_ui(new Ui::NoOfficialHangarMessage)
412     {
413         m_ui->setupUi(this);
414         // proxy this signal upwards
415         connect(m_ui->label, &QLabel::linkActivated,
416                 this, &NoOfficialHangarMessage::linkActivated);
417     }
418
419 Q_SIGNALS:
420     void linkActivated(QUrl link);
421
422 private:
423     Ui::NoOfficialHangarMessage* m_ui;
424 };
425
426 static void initQtResources()
427 {
428     Q_INIT_RESOURCE(resources);
429 }
430
431 namespace flightgear
432 {
433
434 void initApp(int& argc, char** argv)
435 {
436     static bool qtInitDone = false;
437     static int s_argc;
438
439     if (!qtInitDone) {
440         qtInitDone = true;
441
442         sglog().setLogLevels( SG_ALL, SG_INFO );
443         initQtResources(); // can't be called from a namespace
444
445         s_argc = argc; // QApplication only stores a reference to argc,
446         // and may crash if it is freed
447         // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication
448
449         QApplication* app = new QApplication(s_argc, argv);
450         app->setOrganizationName("FlightGear");
451         app->setApplicationName("FlightGear");
452         app->setOrganizationDomain("flightgear.org");
453
454         QSettings::setDefaultFormat(QSettings::IniFormat);
455         QSettings::setPath(QSettings::IniFormat, QSettings::UserScope,
456                            QString::fromStdString(globals->get_fg_home().utf8Str()));
457
458         // reset numeric / collation locales as described at:
459         // http://doc.qt.io/qt-5/qcoreapplication.html#details
460         ::setlocale(LC_NUMERIC, "C");
461         ::setlocale(LC_COLLATE, "C");
462
463         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
464         if (mods & (Qt::AltModifier | Qt::ShiftModifier)) {
465             qWarning() << "Alt/shift pressed during launch";
466             QSettings settings;
467             settings.setValue("fg-root", "!ask");
468         }
469     }
470 }
471
472 bool runLauncherDialog()
473 {
474     // startup the nav-cache now. This pre-empts normal startup of
475     // the cache, but no harm done. (Providing scenery paths are consistent)
476
477     initNavCache();
478
479     QSettings settings;
480     QString downloadDir = settings.value("download-dir").toString();
481     if (!downloadDir.isEmpty()) {
482         flightgear::Options::sharedInstance()->setOption("download-dir", downloadDir.toStdString());
483     }
484
485     fgInitPackageRoot();
486
487     // startup the HTTP system now since packages needs it
488     FGHTTPClient* http = globals->add_new_subsystem<FGHTTPClient>();
489
490     // we guard against re-init in the global phase; bind and postinit
491     // will happen as normal
492     http->init();
493
494     NaturalEarthDataLoaderThread* naturalEarthLoader = new NaturalEarthDataLoaderThread;
495     naturalEarthLoader->start();
496
497     // avoid double Apple menu and other weirdness if both Qt and OSG
498     // try to initialise various Cocoa structures.
499     flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
500
501     QtLauncher dlg;
502     dlg.show();
503
504     int appResult = qApp->exec();
505     if (appResult < 0) {
506         return false; // quit
507     }
508
509     // don't set scenery paths twice
510     globals->clear_fg_scenery();
511
512     return true;
513 }
514
515 bool runInAppLauncherDialog()
516 {
517     QtLauncher dlg;
518     dlg.setInAppMode();
519     dlg.exec();
520     if (dlg.result() != QDialog::Accepted) {
521         return false;
522     }
523
524     return true;
525 }
526
527 } // of namespace flightgear
528
529 QtLauncher::QtLauncher() :
530     QDialog(),
531     m_ui(NULL),
532     m_subsystemIdleTimer(NULL),
533     m_inAppMode(false),
534     m_doRestoreMPServer(false)
535 {
536     m_ui.reset(new Ui::Launcher);
537     m_ui->setupUi(this);
538
539 #if QT_VERSION >= 0x050300
540     // don't require Qt 5.3
541     m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
542 #endif
543
544 #if QT_VERSION >= 0x050200
545     m_ui->aircraftFilter->setClearButtonEnabled(true);
546 #endif
547
548     for (int i=0; i<4; ++i) {
549         m_ratingFilters[i] = 3;
550     }
551
552     m_subsystemIdleTimer = new QTimer(this);
553     m_subsystemIdleTimer->setInterval(0);
554     connect(m_subsystemIdleTimer, &QTimer::timeout,
555             this, &QtLauncher::onSubsytemIdleTimeout);
556     m_subsystemIdleTimer->start();
557
558     // create and configure the proxy model
559     m_aircraftProxy = new AircraftProxyModel(this);
560     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
561             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
562     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
563             this, &QtLauncher::maybeRestoreAircraftSelection);
564
565     connect(m_ui->onlyShowInstalledCheck, &QAbstractButton::toggled,
566             m_aircraftProxy, &AircraftProxyModel::setInstalledFilterEnabled);
567     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
568             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
569
570     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
571     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
572
573     connect(m_ui->aircraftHistory, &QPushButton::clicked,
574           this, &QtLauncher::onPopupAircraftHistory);
575
576     connect(m_ui->location, &LocationWidget::descriptionChanged,
577             m_ui->locationDescription, &QLabel::setText);
578
579     QAction* qa = new QAction(this);
580     qa->setShortcut(QKeySequence("Ctrl+Q"));
581     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
582     addAction(qa);
583
584     connect(m_ui->editRatingFilter, &QPushButton::clicked,
585             this, &QtLauncher::onEditRatingsFilter);
586     connect(m_ui->onlyShowInstalledCheck, &QCheckBox::toggled,
587             this, &QtLauncher::onShowInstalledAircraftToggled);
588
589     QIcon historyIcon(":/history-icon");
590     m_ui->aircraftHistory->setIcon(historyIcon);
591
592     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
593             this, SLOT(updateSettingsSummary()));
594     connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
595             this, SLOT(updateSettingsSummary()));
596     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
597             this, SLOT(updateSettingsSummary()));
598     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
599             this, SLOT(updateSettingsSummary()));
600     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
601             this, SLOT(updateSettingsSummary()));
602     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
603             this, SLOT(updateSettingsSummary()));
604     connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
605             this, SLOT(updateSettingsSummary()));
606
607     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
608             this, SLOT(onRembrandtToggled(bool)));
609     connect(m_ui->terrasyncCheck, &QCheckBox::toggled,
610             this, &QtLauncher::onToggleTerrasync);
611     updateSettingsSummary();
612
613     connect(m_ui->mpServerCombo, SIGNAL(activated(int)),
614             this, SLOT(onMPServerActivated(int)));
615
616     m_aircraftModel = new AircraftItemModel(this);
617     m_aircraftProxy->setSourceModel(m_aircraftModel);
618
619     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
620     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
621     m_aircraftProxy->setSortRole(Qt::DisplayRole);
622     m_aircraftProxy->setDynamicSortFilter(true);
623
624     m_ui->aircraftList->setModel(m_aircraftProxy);
625     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
626     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
627     m_ui->aircraftList->setItemDelegate(delegate);
628     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
629     connect(m_ui->aircraftList, &QListView::clicked,
630             this, &QtLauncher::onAircraftSelected);
631     connect(delegate, &AircraftItemDelegate::variantChanged,
632             this, &QtLauncher::onAircraftSelected);
633     connect(delegate, &AircraftItemDelegate::requestInstall,
634             this, &QtLauncher::onRequestPackageInstall);
635     connect(delegate, &AircraftItemDelegate::cancelDownload,
636             this, &QtLauncher::onCancelDownload);
637
638     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
639             this, &QtLauncher::onAircraftInstalledCompleted);
640     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
641             this, &QtLauncher::onAircraftInstallFailed);
642     connect(m_aircraftModel, &AircraftItemModel::scanCompleted,
643             this, &QtLauncher::updateSelectedAircraft);
644     connect(m_ui->restoreDefaultsButton, &QPushButton::clicked,
645             this, &QtLauncher::onRestoreDefaults);
646
647
648     AddOnsPage* addOnsPage = new AddOnsPage(NULL, globals->packageRoot());
649     connect(addOnsPage, &AddOnsPage::downloadDirChanged,
650             this, &QtLauncher::onDownloadDirChanged);
651     connect(addOnsPage, &AddOnsPage::sceneryPathsChanged,
652             this, &QtLauncher::setSceneryPaths);
653
654     m_ui->tabWidget->addTab(addOnsPage, tr("Add-ons"));
655     // after any kind of reset, try to restore selection and scroll
656     // to match the m_selectedAircraft. This needs to be delayed
657     // fractionally otherwise the scrollTo seems to be ignored,
658     // unfortunately.
659     connect(m_aircraftProxy, &AircraftProxyModel::modelReset,
660             this, &QtLauncher::delayedAircraftModelReset);
661
662     QSettings settings;
663     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
664     m_aircraftModel->setPackageRoot(globals->packageRoot());
665     m_aircraftModel->scanDirs();
666
667     checkOfficialCatalogMessage();
668     restoreSettings();
669
670     onRefreshMPServers();
671 }
672
673 QtLauncher::~QtLauncher()
674 {
675
676 }
677
678 void QtLauncher::setSceneryPaths()
679 {
680     globals->clear_fg_scenery();
681
682 // mimic what optionss.cxx does, so we can find airport data for parking
683 // positions
684     QSettings settings;
685     // append explicit scenery paths
686     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
687         globals->append_fg_scenery(path.toStdString());
688     }
689
690     // append the TerraSync path
691     QString downloadDir = settings.value("download-dir").toString();
692     if (downloadDir.isEmpty()) {
693         downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());
694     }
695
696     SGPath terraSyncDir(downloadDir.toStdString());
697     terraSyncDir.append("TerraSync");
698     if (terraSyncDir.exists()) {
699         globals->append_fg_scenery(terraSyncDir);
700     }
701
702 }
703
704 void QtLauncher::setInAppMode()
705 {
706   m_inAppMode = true;
707   m_ui->tabWidget->removeTab(2);
708   m_ui->tabWidget->removeTab(2);
709
710   m_ui->runButton->setText(tr("Apply"));
711   m_ui->quitButton->setText(tr("Cancel"));
712
713   disconnect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
714   connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onApply()));
715 }
716
717 void QtLauncher::restoreSettings()
718 {
719     QSettings settings;
720     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
721     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
722     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
723     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
724     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
725     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
726     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
727     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
728
729     // full paths to -set.xml files
730     m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
731
732     if (!m_recentAircraft.empty()) {
733         m_selectedAircraft = m_recentAircraft.front();
734     } else {
735         // select the default C172p
736     }
737
738     if (!m_inAppMode) {
739         setSceneryPaths();
740     }
741
742     m_ui->location->restoreSettings();
743
744     // rating filters
745     m_ui->onlyShowInstalledCheck->setChecked(settings.value("only-show-installed", false).toBool());
746     if (m_ui->onlyShowInstalledCheck->isChecked()) {
747         m_ui->ratingsFilterCheck->setEnabled(false);
748     }
749
750     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
751     int index = 0;
752     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
753         m_ratingFilters[index++] = v.toInt();
754     }
755
756     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
757     m_aircraftProxy->setRatings(m_ratingFilters);
758
759     updateSelectedAircraft();
760     maybeRestoreAircraftSelection();
761
762     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
763
764     m_ui->mpBox->setChecked(settings.value("mp-enabled").toBool());
765     m_ui->mpCallsign->setText(settings.value("mp-callsign").toString());
766     // don't restore MP server here, we do it after a refresh
767     m_doRestoreMPServer = true;
768 }
769
770 void QtLauncher::delayedAircraftModelReset()
771 {
772     QTimer::singleShot(1, this, SLOT(maybeRestoreAircraftSelection()));
773 }
774
775 void QtLauncher::maybeRestoreAircraftSelection()
776 {
777     QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
778     QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
779     if (proxyIndex.isValid()) {
780         m_ui->aircraftList->selectionModel()->setCurrentIndex(proxyIndex,
781                                                               QItemSelectionModel::ClearAndSelect);
782         m_ui->aircraftList->selectionModel()->select(proxyIndex,
783                                                      QItemSelectionModel::ClearAndSelect);
784         m_ui->aircraftList->scrollTo(proxyIndex);
785     }
786 }
787
788 void QtLauncher::saveSettings()
789 {
790     QSettings settings;
791     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
792     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
793     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
794     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
795     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
796     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
797     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
798     settings.setValue("only-show-installed", m_ui->onlyShowInstalledCheck->isChecked());
799     settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
800
801     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
802     settings.setValue("season", m_ui->seasonCombo->currentIndex());
803     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
804
805     m_ui->location->saveSettings();
806
807     settings.setValue("mp-callsign", m_ui->mpCallsign->text());
808     settings.setValue("mp-server", m_ui->mpServerCombo->currentData());
809     settings.setValue("mp-enabled", m_ui->mpBox->isChecked());
810 }
811
812 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
813 {
814     flightgear::Options* opt = flightgear::Options::sharedInstance();
815     std::string stdName(name.toStdString());
816     if (cbox->isChecked()) {
817         opt->addOption("enable-" + stdName, "");
818     } else {
819         opt->addOption("disable-" + stdName, "");
820     }
821 }
822
823 void QtLauncher::closeEvent(QCloseEvent *event)
824 {
825     qApp->exit(-1);
826 }
827
828 void QtLauncher::reject()
829 {
830     qApp->exit(-1);
831 }
832
833 void QtLauncher::onRun()
834 {
835     flightgear::Options* opt = flightgear::Options::sharedInstance();
836     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
837     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
838     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
839     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
840 //    setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
841
842     bool startPaused = m_ui->startPausedCheck->isChecked() ||
843             m_ui->location->shouldStartPaused();
844     if (startPaused) {
845         opt->addOption("enable-freeze", "");
846     }
847
848     // MSAA is more complex
849     if (!m_ui->rembrandtCheckbox->isChecked()) {
850         if (m_ui->msaaCheckbox->isChecked()) {
851             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
852             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
853         } else {
854             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
855         }
856     }
857
858     // aircraft
859     if (!m_selectedAircraft.isEmpty()) {
860         if (m_selectedAircraft.isLocalFile()) {
861             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
862             opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
863             QString setFile = setFileInfo.fileName();
864             Q_ASSERT(setFile.endsWith("-set.xml"));
865             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
866             opt->addOption("aircraft", setFile.toStdString());
867         } else if (m_selectedAircraft.scheme() == "package") {
868             QString qualifiedId = m_selectedAircraft.path();
869             // no need to set aircraft-dir, handled by the corresponding code
870             // in fgInitAircraft
871             opt->addOption("aircraft", qualifiedId.toStdString());
872         } else {
873             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
874         }
875
876       // manage aircraft history
877         if (m_recentAircraft.contains(m_selectedAircraft))
878           m_recentAircraft.removeOne(m_selectedAircraft);
879         m_recentAircraft.prepend(m_selectedAircraft);
880         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
881           m_recentAircraft.pop_back();
882     }
883
884     if (m_ui->mpBox->isChecked()) {
885         opt->addOption("callsign", m_ui->mpCallsign->text().toStdString());
886         QString host = m_ui->mpServerCombo->currentData().toString();
887         int port = 5000;
888         if (host == "custom") {
889             QSettings settings;
890             host = settings.value("mp-custom-host").toString();
891             port = settings.value("mp-custom-port").toInt();
892         } else {
893             port = findMPServerPort(host.toStdString());
894         }
895         globals->get_props()->setStringValue("/sim/multiplay/txhost", host.toStdString());
896         globals->get_props()->setIntValue("/sim/multiplay/txport", port);
897     }
898
899     m_ui->location->setLocationOptions();
900
901     // time of day
902     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
903         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
904         opt->addOption("timeofday", dayval.toStdString());
905     }
906
907     if (m_ui->seasonCombo->currentIndex() != 0) {
908         QString seasonName = m_ui->seasonCombo->currentText().toLower();
909         opt->addOption("season", seasonName.toStdString());
910     }
911
912     QSettings settings;
913     QString downloadDir = settings.value("download-dir").toString();
914     if (!downloadDir.isEmpty()) {
915         QDir d(downloadDir);
916         if (!d.exists()) {
917             int result = QMessageBox::question(this, tr("Create download folder?"),
918                                   tr("The selected location for downloads does not exist. Create it?"),
919                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
920             if (result == QMessageBox::Cancel) {
921                 return;
922             }
923
924             if (result == QMessageBox::Yes) {
925                 d.mkpath(downloadDir);
926             }
927         }
928
929         opt->addOption("download-dir", downloadDir.toStdString());
930     }
931
932     // scenery paths
933     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
934         opt->addOption("fg-scenery", path.toStdString());
935     }
936
937     // aircraft paths
938     Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
939         // can't use fg-aircraft for this, as it is processed before the launcher is run
940         globals->append_aircraft_path(path.toStdString());
941     }
942
943     // additional arguments
944     ArgumentsTokenizer tk;
945     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
946         if (a.arg.startsWith("prop:")) {
947             QString v = a.arg.mid(5) + "=" + a.value;
948             opt->addOption("prop", v.toStdString());
949         } else {
950             opt->addOption(a.arg.toStdString(), a.value.toStdString());
951         }
952     }
953
954     if (settings.contains("restore-defaults-on-run")) {
955         settings.remove("restore-defaults-on-run");
956         opt->addOption("restore-defaults", "");
957     }
958
959     saveSettings();
960
961     qApp->exit(0);
962 }
963
964
965 void QtLauncher::onApply()
966 {
967     accept();
968
969     // aircraft
970     if (!m_selectedAircraft.isEmpty()) {
971         std::string aircraftPropValue,
972             aircraftDir;
973
974         if (m_selectedAircraft.isLocalFile()) {
975             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
976             QString setFile = setFileInfo.fileName();
977             Q_ASSERT(setFile.endsWith("-set.xml"));
978             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
979             aircraftDir = setFileInfo.dir().absolutePath().toStdString();
980             aircraftPropValue = setFile.toStdString();
981         } else if (m_selectedAircraft.scheme() == "package") {
982             // no need to set aircraft-dir, handled by the corresponding code
983             // in fgInitAircraft
984             aircraftPropValue = m_selectedAircraft.path().toStdString();
985         } else {
986             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
987         }
988
989         // manage aircraft history
990         if (m_recentAircraft.contains(m_selectedAircraft))
991             m_recentAircraft.removeOne(m_selectedAircraft);
992         m_recentAircraft.prepend(m_selectedAircraft);
993         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
994             m_recentAircraft.pop_back();
995
996         globals->get_props()->setStringValue("/sim/aircraft", aircraftPropValue);
997         globals->get_props()->setStringValue("/sim/aircraft-dir", aircraftDir);
998     }
999
1000
1001     saveSettings();
1002 }
1003
1004 void QtLauncher::onQuit()
1005 {
1006     qApp->exit(-1);
1007 }
1008
1009 void QtLauncher::onToggleTerrasync(bool enabled)
1010 {
1011     if (enabled) {
1012         QSettings settings;
1013         QString downloadDir = settings.value("download-dir").toString();
1014         if (downloadDir.isEmpty()) {
1015             downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());
1016         }
1017
1018         QFileInfo info(downloadDir);
1019         if (!info.exists()) {
1020             QMessageBox msg;
1021             msg.setWindowTitle(tr("Create download folder?"));
1022             msg.setText(tr("The download folder '%1' does not exist, create it now?").arg(downloadDir));
1023             msg.addButton(QMessageBox::Yes);
1024             msg.addButton(QMessageBox::Cancel);
1025             int result = msg.exec();
1026
1027             if (result == QMessageBox::Cancel) {
1028                 m_ui->terrasyncCheck->setChecked(false);
1029                 return;
1030             }
1031
1032             QDir d(downloadDir);
1033             d.mkpath(downloadDir);
1034         }
1035     } // of is enabled
1036 }
1037
1038 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
1039 {
1040     maybeUpdateSelectedAircraft(index);
1041 }
1042
1043 void QtLauncher::onRatingsFilterToggled()
1044 {
1045     QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1046     QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
1047     if (proxyIndex.isValid()) {
1048         m_ui->aircraftList->scrollTo(proxyIndex);
1049     }
1050 }
1051
1052 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
1053 {
1054     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
1055
1056     QMessageBox msg;
1057     msg.setWindowTitle(tr("Aircraft installation failed"));
1058     msg.setText(tr("An error occurred installing the aircraft %1: %2").
1059                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
1060     msg.addButton(QMessageBox::Ok);
1061     msg.exec();
1062
1063     maybeUpdateSelectedAircraft(index);
1064 }
1065
1066 void QtLauncher::onAircraftSelected(const QModelIndex& index)
1067 {
1068     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
1069     updateSelectedAircraft();
1070 }
1071
1072 void QtLauncher::onRequestPackageInstall(const QModelIndex& index)
1073 {
1074     // also select, otherwise UI is confusing
1075     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
1076     updateSelectedAircraft();
1077
1078     QString pkg = index.data(AircraftPackageIdRole).toString();
1079     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1080     if (pref->isInstalled()) {
1081         InstallRef install = pref->existingInstall();
1082         if (install && install->hasUpdate()) {
1083             globals->packageRoot()->scheduleToUpdate(install);
1084         }
1085     } else {
1086         pref->install();
1087     }
1088 }
1089
1090 void QtLauncher::onCancelDownload(const QModelIndex& index)
1091 {
1092     QString pkg = index.data(AircraftPackageIdRole).toString();
1093     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1094     simgear::pkg::InstallRef i = pref->existingInstall();
1095     i->cancelDownload();
1096 }
1097
1098 void QtLauncher::onRestoreDefaults()
1099 {
1100     QMessageBox mbox(this);
1101     mbox.setText(tr("Restore all settings to defaults?"));
1102     mbox.setInformativeText(tr("Restoring settings to their defaults may affect available add-ons such as scenery or aircraft."));
1103     QPushButton* quitButton = mbox.addButton(tr("Restore and restart now"), QMessageBox::YesRole);
1104     mbox.addButton(QMessageBox::Cancel);
1105     mbox.setDefaultButton(QMessageBox::Cancel);
1106     mbox.setIconPixmap(QPixmap(":/app-icon-large"));
1107
1108     mbox.exec();
1109     if (mbox.clickedButton() != quitButton) {
1110         return;
1111     }
1112
1113     {
1114         QSettings settings;
1115         settings.clear();
1116         settings.setValue("restore-defaults-on-run", true);
1117     }
1118
1119     restartTheApp(QStringList());
1120 }
1121
1122 void QtLauncher::maybeUpdateSelectedAircraft(QModelIndex index)
1123 {
1124     QUrl u = index.data(AircraftURIRole).toUrl();
1125     if (u == m_selectedAircraft) {
1126         // potentially enable the run button now!
1127         updateSelectedAircraft();
1128     }
1129 }
1130
1131 void QtLauncher::updateSelectedAircraft()
1132 {
1133     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1134     if (index.isValid()) {
1135         QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
1136         m_ui->thumbnail->setPixmap(pm);
1137         m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
1138
1139         int status = index.data(AircraftPackageStatusRole).toInt();
1140         bool canRun = (status == PackageInstalled);
1141         m_ui->runButton->setEnabled(canRun);
1142
1143         LauncherAircraftType aircraftType = Airplane;
1144         if (index.data(AircraftIsHelicopterRole).toBool()) {
1145             aircraftType = Helicopter;
1146         } else if (index.data(AircraftIsSeaplaneRole).toBool()) {
1147             aircraftType = Seaplane;
1148         }
1149
1150         m_ui->location->setAircraftType(aircraftType);
1151     } else {
1152         m_ui->thumbnail->setPixmap(QPixmap());
1153         m_ui->aircraftDescription->setText("");
1154         m_ui->runButton->setEnabled(false);
1155     }
1156 }
1157
1158 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
1159 {
1160   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
1161 }
1162
1163 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
1164 {
1165     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1166     Q_ASSERT(sourceModel);
1167     return sourceModel->indexOfAircraftURI(uri);
1168 }
1169
1170 void QtLauncher::onPopupAircraftHistory()
1171 {
1172     if (m_recentAircraft.isEmpty()) {
1173         return;
1174     }
1175
1176     QMenu m;
1177     Q_FOREACH(QUrl uri, m_recentAircraft) {
1178         QModelIndex index = sourceIndexForAircraftURI(uri);
1179         if (!index.isValid()) {
1180             // not scanned yet
1181             continue;
1182         }
1183         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1184         act->setData(uri);
1185     }
1186
1187     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1188     QAction* triggered = m.exec(popupPos);
1189     if (triggered) {
1190         m_selectedAircraft = triggered->data().toUrl();
1191         QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
1192         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1193                                                               QItemSelectionModel::ClearAndSelect);
1194         m_ui->aircraftFilter->clear();
1195         updateSelectedAircraft();
1196     }
1197 }
1198
1199 void QtLauncher::onEditRatingsFilter()
1200 {
1201     EditRatingsFilterDialog dialog(this);
1202     dialog.setRatings(m_ratingFilters);
1203
1204     dialog.exec();
1205     if (dialog.result() == QDialog::Accepted) {
1206         QVariantList vl;
1207         for (int i=0; i<4; ++i) {
1208             m_ratingFilters[i] = dialog.getRating(i);
1209             vl.append(m_ratingFilters[i]);
1210         }
1211         m_aircraftProxy->setRatings(m_ratingFilters);
1212
1213         QSettings settings;
1214         settings.setValue("min-ratings", vl);
1215     }
1216 }
1217
1218 void QtLauncher::updateSettingsSummary()
1219 {
1220     QStringList summary;
1221     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1222         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1223     }
1224
1225     if (m_ui->seasonCombo->currentIndex() > 0) {
1226         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1227     }
1228
1229     if (m_ui->rembrandtCheckbox->isChecked()) {
1230         summary.append("Rembrandt enabled");
1231     } else if (m_ui->msaaCheckbox->isChecked()) {
1232         summary.append("anti-aliasing");
1233     }
1234
1235     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1236         summary.append("live weather");
1237     }
1238
1239     if (m_ui->terrasyncCheck->isChecked()) {
1240         summary.append("automatic scenery downloads");
1241     }
1242
1243     if (m_ui->startPausedCheck->isChecked()) {
1244         summary.append("paused");
1245     }
1246
1247     QString s = summary.join(", ");
1248     s[0] = s[0].toUpper();
1249     m_ui->settingsDescription->setText(s);
1250 }
1251
1252 void QtLauncher::onRembrandtToggled(bool b)
1253 {
1254     // Rembrandt and multi-sample are exclusive
1255     m_ui->msaaCheckbox->setEnabled(!b);
1256 }
1257
1258 void QtLauncher::onShowInstalledAircraftToggled(bool b)
1259 {
1260     m_ui->ratingsFilterCheck->setEnabled(!b);
1261     maybeRestoreAircraftSelection();
1262 }
1263
1264 void QtLauncher::onSubsytemIdleTimeout()
1265 {
1266     globals->get_subsystem_mgr()->update(0.0);
1267 }
1268
1269 void QtLauncher::onDownloadDirChanged()
1270 {
1271
1272     // replace existing package root
1273     globals->get_subsystem<FGHTTPClient>()->shutdown();
1274     globals->setPackageRoot(simgear::pkg::RootRef());
1275
1276     // create new root with updated download-dir value
1277     fgInitPackageRoot();
1278
1279     globals->get_subsystem<FGHTTPClient>()->init();
1280
1281     QSettings settings;
1282     // re-scan the aircraft list
1283     m_aircraftModel->setPackageRoot(globals->packageRoot());
1284     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1285     m_aircraftModel->scanDirs();
1286
1287     checkOfficialCatalogMessage();
1288
1289     // re-set scenery dirs
1290     setSceneryPaths();
1291 }
1292
1293 void QtLauncher::checkOfficialCatalogMessage()
1294 {
1295     QSettings settings;
1296     bool showOfficialCatalogMesssage = !globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
1297     if (settings.value("hide-official-catalog-message").toBool()) {
1298         showOfficialCatalogMesssage = false;
1299     }
1300
1301     m_aircraftModel->setOfficialHangarMessageVisible(showOfficialCatalogMesssage);
1302     if (showOfficialCatalogMesssage) {
1303         NoOfficialHangarMessage* messageWidget = new NoOfficialHangarMessage;
1304         connect(messageWidget, &NoOfficialHangarMessage::linkActivated,
1305                 this, &QtLauncher::onOfficialCatalogMessageLink);
1306
1307         QModelIndex index = m_aircraftProxy->mapFromSource(m_aircraftModel->officialHangarMessageIndex());
1308         m_ui->aircraftList->setIndexWidget(index, messageWidget);
1309     }
1310 }
1311
1312 void QtLauncher::onOfficialCatalogMessageLink(QUrl link)
1313 {
1314     QString s = link.toString();
1315     if (s == "action:hide") {
1316         QSettings settings;
1317         settings.setValue("hide-official-catalog-message", true);
1318     } else if (s == "action:add-official") {
1319         AddOnsPage::addDefaultCatalog(this);
1320     }
1321
1322     checkOfficialCatalogMessage();
1323 }
1324
1325 void QtLauncher::onRefreshMPServers()
1326 {
1327     if (m_mpServerRequest.get()) {
1328         return; // in-progress
1329     }
1330
1331     string url(fgGetString("/sim/multiplay/serverlist-url",
1332                            "http://liveries.flightgear.org/mpstatus/mpservers.xml"));
1333
1334     if (url.empty()) {
1335         SG_LOG(SG_IO, SG_ALERT, "do_multiplayer.refreshserverlist: no URL given");
1336         return;
1337     }
1338
1339     SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1340     m_mpServerRequest.reset(new RemoteXMLRequest(url, targetnode));
1341     m_mpServerRequest->done(this, &QtLauncher::onRefreshMPServersDone);
1342     m_mpServerRequest->fail(this, &QtLauncher::onRefreshMPServersFailed);
1343     globals->get_subsystem<FGHTTPClient>()->makeRequest(m_mpServerRequest);
1344 }
1345
1346 void QtLauncher::onRefreshMPServersDone(simgear::HTTP::Request*)
1347 {
1348     // parse the properties
1349     SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1350     m_ui->mpServerCombo->clear();
1351
1352     for (int i=0; i<targetnode->nChildren(); ++i) {
1353         SGPropertyNode* c = targetnode->getChild(i);
1354         if (c->getName() != std::string("server")) {
1355             continue;
1356         }
1357
1358         QString name = QString::fromStdString(c->getStringValue("name"));
1359         QString loc = QString::fromStdString(c->getStringValue("location"));
1360         QString host = QString::fromStdString(c->getStringValue("hostname"));
1361         m_ui->mpServerCombo->addItem(tr("%1 - %2").arg(name,loc), host);
1362     }
1363
1364     EditCustomMPServerDialog::addCustomItem(m_ui->mpServerCombo);
1365     restoreMPServerSelection();
1366
1367     m_mpServerRequest.clear();
1368 }
1369
1370 void QtLauncher::onRefreshMPServersFailed(simgear::HTTP::Request*)
1371 {
1372     qWarning() << "refreshing MP servers failed:" << QString::fromStdString(m_mpServerRequest->responseReason());
1373     m_mpServerRequest.clear();
1374     EditCustomMPServerDialog::addCustomItem(m_ui->mpServerCombo);
1375     restoreMPServerSelection();
1376 }
1377
1378 void QtLauncher::restoreMPServerSelection()
1379 {
1380     if (m_doRestoreMPServer) {
1381         QSettings settings;
1382         int index = m_ui->mpServerCombo->findData(settings.value("mp-server"));
1383         if (index >= 0) {
1384             m_ui->mpServerCombo->setCurrentIndex(index);
1385         }
1386         m_doRestoreMPServer = false;
1387     }
1388 }
1389
1390 void QtLauncher::onMPServerActivated(int index)
1391 {
1392     if (m_ui->mpServerCombo->itemData(index) == "custom") {
1393         EditCustomMPServerDialog dlg(this);
1394         dlg.exec();
1395         if (dlg.result() == QDialog::Accepted) {
1396             m_ui->mpServerCombo->setItemText(index, tr("Custom - %1").arg(dlg.hostname()));
1397         }
1398     }
1399 }
1400
1401 int QtLauncher::findMPServerPort(const std::string& host)
1402 {
1403     SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1404     for (int i=0; i<targetnode->nChildren(); ++i) {
1405         SGPropertyNode* c = targetnode->getChild(i);
1406         if (c->getName() != std::string("server")) {
1407             continue;
1408         }
1409
1410         if (c->getStringValue("hostname") == host) {
1411             return c->getIntValue("port");
1412         }
1413     }
1414
1415     return 0;
1416 }
1417
1418 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1419 {
1420     if (uri.scheme() != "package") {
1421         qWarning() << "invalid URL scheme:" << uri;
1422         return simgear::pkg::PackageRef();
1423     }
1424
1425     QString ident = uri.path();
1426     return globals->packageRoot()->getPackageById(ident.toStdString());
1427 }
1428
1429 void QtLauncher::restartTheApp(QStringList fgArgs)
1430 {
1431     // Spawn a new instance of myApplication:
1432     QProcess proc;
1433     QStringList args;
1434
1435 #if defined(Q_OS_MAC)
1436     QDir dir(qApp->applicationDirPath()); // returns the 'MacOS' dir
1437     dir.cdUp(); // up to 'contents' dir
1438     dir.cdUp(); // up to .app dir
1439     // see 'man open' for details, but '-n' ensures we launch a new instance,
1440     // and we want to pass remaining arguments to us, not open.
1441     args << "-n" << dir.absolutePath() << "--args" << "--launcher" << fgArgs;
1442     qDebug() << "args" << args;
1443     proc.startDetached("open", args);
1444 #else
1445     args << "--launcher" << fgArgs;
1446     proc.startDetached(qApp->applicationFilePath(), args);
1447 #endif
1448     qApp->exit(-1);
1449 }
1450
1451 #include "QtLauncher.moc"