]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
16307ecf9a576e59fc11751d6b240ba9501eb783
[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     Q_SLOT 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     // add the installation path since it contains default airport data,
703     // if terrasync is disabled or on first-launch
704     globals->append_fg_scenery(globals->get_fg_root() / "Scenery");
705
706 }
707
708 void QtLauncher::setInAppMode()
709 {
710   m_inAppMode = true;
711   m_ui->tabWidget->removeTab(3);
712   m_ui->tabWidget->removeTab(3);
713
714   m_ui->runButton->setText(tr("Apply"));
715   m_ui->quitButton->setText(tr("Cancel"));
716
717   disconnect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
718   connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onApply()));
719 }
720
721 void QtLauncher::restoreSettings()
722 {
723     QSettings settings;
724     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
725     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
726     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
727     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
728     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
729     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
730     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
731     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
732
733     // full paths to -set.xml files
734     m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
735
736     if (!m_recentAircraft.empty()) {
737         m_selectedAircraft = m_recentAircraft.front();
738     } else {
739         // select the default C172p
740     }
741
742     if (!m_inAppMode) {
743         setSceneryPaths();
744     }
745
746     m_ui->location->restoreSettings();
747
748     // rating filters
749     m_ui->onlyShowInstalledCheck->setChecked(settings.value("only-show-installed", false).toBool());
750     if (m_ui->onlyShowInstalledCheck->isChecked()) {
751         m_ui->ratingsFilterCheck->setEnabled(false);
752     }
753
754     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
755     int index = 0;
756     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
757         m_ratingFilters[index++] = v.toInt();
758     }
759
760     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
761     m_aircraftProxy->setRatings(m_ratingFilters);
762
763     updateSelectedAircraft();
764     maybeRestoreAircraftSelection();
765
766     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
767
768     m_ui->mpBox->setChecked(settings.value("mp-enabled").toBool());
769     m_ui->mpCallsign->setText(settings.value("mp-callsign").toString());
770     // don't restore MP server here, we do it after a refresh
771     m_doRestoreMPServer = true;
772 }
773
774 void QtLauncher::delayedAircraftModelReset()
775 {
776     QTimer::singleShot(1, this, SLOT(maybeRestoreAircraftSelection()));
777 }
778
779 void QtLauncher::maybeRestoreAircraftSelection()
780 {
781     QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
782     QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
783     if (proxyIndex.isValid()) {
784         m_ui->aircraftList->selectionModel()->setCurrentIndex(proxyIndex,
785                                                               QItemSelectionModel::ClearAndSelect);
786         m_ui->aircraftList->selectionModel()->select(proxyIndex,
787                                                      QItemSelectionModel::ClearAndSelect);
788         m_ui->aircraftList->scrollTo(proxyIndex);
789     }
790 }
791
792 void QtLauncher::saveSettings()
793 {
794     QSettings settings;
795     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
796     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
797     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
798     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
799     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
800     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
801     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
802     settings.setValue("only-show-installed", m_ui->onlyShowInstalledCheck->isChecked());
803     settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
804
805     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
806     settings.setValue("season", m_ui->seasonCombo->currentIndex());
807     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
808
809     m_ui->location->saveSettings();
810
811     settings.setValue("mp-callsign", m_ui->mpCallsign->text());
812     settings.setValue("mp-server", m_ui->mpServerCombo->currentData());
813     settings.setValue("mp-enabled", m_ui->mpBox->isChecked());
814 }
815
816 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
817 {
818     flightgear::Options* opt = flightgear::Options::sharedInstance();
819     std::string stdName(name.toStdString());
820     if (cbox->isChecked()) {
821         opt->addOption("enable-" + stdName, "");
822     } else {
823         opt->addOption("disable-" + stdName, "");
824     }
825 }
826
827 void QtLauncher::closeEvent(QCloseEvent *event)
828 {
829     qApp->exit(-1);
830 }
831
832 void QtLauncher::reject()
833 {
834     qApp->exit(-1);
835 }
836
837 void QtLauncher::onRun()
838 {
839     flightgear::Options* opt = flightgear::Options::sharedInstance();
840     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
841     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
842     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
843     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
844 //    setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
845
846     bool startPaused = m_ui->startPausedCheck->isChecked() ||
847             m_ui->location->shouldStartPaused();
848     if (startPaused) {
849         opt->addOption("enable-freeze", "");
850     }
851
852     // MSAA is more complex
853     if (!m_ui->rembrandtCheckbox->isChecked()) {
854         if (m_ui->msaaCheckbox->isChecked()) {
855             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
856             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
857         } else {
858             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
859         }
860     }
861
862     // aircraft
863     if (!m_selectedAircraft.isEmpty()) {
864         if (m_selectedAircraft.isLocalFile()) {
865             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
866             opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
867             QString setFile = setFileInfo.fileName();
868             Q_ASSERT(setFile.endsWith("-set.xml"));
869             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
870             opt->addOption("aircraft", setFile.toStdString());
871         } else if (m_selectedAircraft.scheme() == "package") {
872             QString qualifiedId = m_selectedAircraft.path();
873             // no need to set aircraft-dir, handled by the corresponding code
874             // in fgInitAircraft
875             opt->addOption("aircraft", qualifiedId.toStdString());
876         } else {
877             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
878         }
879
880       // manage aircraft history
881         if (m_recentAircraft.contains(m_selectedAircraft))
882           m_recentAircraft.removeOne(m_selectedAircraft);
883         m_recentAircraft.prepend(m_selectedAircraft);
884         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
885           m_recentAircraft.pop_back();
886     }
887
888     if (m_ui->mpBox->isChecked()) {
889         std::string callSign = m_ui->mpCallsign->text().toStdString();
890         if (!callSign.empty()) {
891             opt->addOption("callsign", callSign);
892         }
893
894         QString host = m_ui->mpServerCombo->currentData().toString();
895         int port = 5000;
896         if (host == "custom") {
897             QSettings settings;
898             host = settings.value("mp-custom-host").toString();
899             port = settings.value("mp-custom-port").toInt();
900         } else {
901             port = findMPServerPort(host.toStdString());
902         }
903         globals->get_props()->setStringValue("/sim/multiplay/txhost", host.toStdString());
904         globals->get_props()->setIntValue("/sim/multiplay/txport", port);
905     }
906
907     m_ui->location->setLocationProperties();
908
909     // time of day
910     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
911         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
912         opt->addOption("timeofday", dayval.toStdString());
913     }
914
915     if (m_ui->seasonCombo->currentIndex() != 0) {
916         QString seasonName = m_ui->seasonCombo->currentText().toLower();
917         opt->addOption("season", seasonName.toStdString());
918     }
919
920     QSettings settings;
921     QString downloadDir = settings.value("download-dir").toString();
922     if (!downloadDir.isEmpty()) {
923         QDir d(downloadDir);
924         if (!d.exists()) {
925             int result = QMessageBox::question(this, tr("Create download folder?"),
926                                   tr("The selected location for downloads does not exist. Create it?"),
927                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
928             if (result == QMessageBox::Cancel) {
929                 return;
930             }
931
932             if (result == QMessageBox::Yes) {
933                 d.mkpath(downloadDir);
934             }
935         }
936
937         opt->addOption("download-dir", downloadDir.toStdString());
938     }
939
940     // scenery paths
941     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
942         opt->addOption("fg-scenery", path.toStdString());
943     }
944
945     // aircraft paths
946     Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
947         // can't use fg-aircraft for this, as it is processed before the launcher is run
948         globals->append_aircraft_path(path.toStdString());
949     }
950
951     // additional arguments
952     ArgumentsTokenizer tk;
953     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
954         if (a.arg.startsWith("prop:")) {
955             QString v = a.arg.mid(5) + "=" + a.value;
956             opt->addOption("prop", v.toStdString());
957         } else {
958             opt->addOption(a.arg.toStdString(), a.value.toStdString());
959         }
960     }
961
962     if (settings.contains("restore-defaults-on-run")) {
963         settings.remove("restore-defaults-on-run");
964         opt->addOption("restore-defaults", "");
965     }
966
967     saveSettings();
968
969     qApp->exit(0);
970 }
971
972
973 void QtLauncher::onApply()
974 {
975     accept();
976
977     // aircraft
978     if (!m_selectedAircraft.isEmpty()) {
979         std::string aircraftPropValue,
980             aircraftDir;
981
982         if (m_selectedAircraft.isLocalFile()) {
983             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
984             QString setFile = setFileInfo.fileName();
985             Q_ASSERT(setFile.endsWith("-set.xml"));
986             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
987             aircraftDir = setFileInfo.dir().absolutePath().toStdString();
988             aircraftPropValue = setFile.toStdString();
989         } else if (m_selectedAircraft.scheme() == "package") {
990             // no need to set aircraft-dir, handled by the corresponding code
991             // in fgInitAircraft
992             aircraftPropValue = m_selectedAircraft.path().toStdString();
993         } else {
994             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
995         }
996
997         // manage aircraft history
998         if (m_recentAircraft.contains(m_selectedAircraft))
999             m_recentAircraft.removeOne(m_selectedAircraft);
1000         m_recentAircraft.prepend(m_selectedAircraft);
1001         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
1002             m_recentAircraft.pop_back();
1003
1004         globals->get_props()->setStringValue("/sim/aircraft", aircraftPropValue);
1005         globals->get_props()->setStringValue("/sim/aircraft-dir", aircraftDir);
1006     }
1007
1008     // location
1009     m_ui->location->setLocationProperties();
1010
1011     saveSettings();
1012 }
1013
1014 void QtLauncher::onQuit()
1015 {
1016     qApp->exit(-1);
1017 }
1018
1019 void QtLauncher::onToggleTerrasync(bool enabled)
1020 {
1021     if (enabled) {
1022         QSettings settings;
1023         QString downloadDir = settings.value("download-dir").toString();
1024         if (downloadDir.isEmpty()) {
1025             downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());
1026         }
1027
1028         QFileInfo info(downloadDir);
1029         if (!info.exists()) {
1030             QMessageBox msg;
1031             msg.setWindowTitle(tr("Create download folder?"));
1032             msg.setText(tr("The download folder '%1' does not exist, create it now?").arg(downloadDir));
1033             msg.addButton(QMessageBox::Yes);
1034             msg.addButton(QMessageBox::Cancel);
1035             int result = msg.exec();
1036
1037             if (result == QMessageBox::Cancel) {
1038                 m_ui->terrasyncCheck->setChecked(false);
1039                 return;
1040             }
1041
1042             QDir d(downloadDir);
1043             d.mkpath(downloadDir);
1044         }
1045     } // of is enabled
1046 }
1047
1048 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
1049 {
1050     maybeUpdateSelectedAircraft(index);
1051 }
1052
1053 void QtLauncher::onRatingsFilterToggled()
1054 {
1055     QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1056     QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
1057     if (proxyIndex.isValid()) {
1058         m_ui->aircraftList->scrollTo(proxyIndex);
1059     }
1060 }
1061
1062 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
1063 {
1064     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
1065
1066     QMessageBox msg;
1067     msg.setWindowTitle(tr("Aircraft installation failed"));
1068     msg.setText(tr("An error occurred installing the aircraft %1: %2").
1069                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
1070     msg.addButton(QMessageBox::Ok);
1071     msg.exec();
1072
1073     maybeUpdateSelectedAircraft(index);
1074 }
1075
1076 void QtLauncher::onAircraftSelected(const QModelIndex& index)
1077 {
1078     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
1079     updateSelectedAircraft();
1080 }
1081
1082 void QtLauncher::onRequestPackageInstall(const QModelIndex& index)
1083 {
1084     // also select, otherwise UI is confusing
1085     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
1086     updateSelectedAircraft();
1087
1088     QString pkg = index.data(AircraftPackageIdRole).toString();
1089     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1090     if (pref->isInstalled()) {
1091         InstallRef install = pref->existingInstall();
1092         if (install && install->hasUpdate()) {
1093             globals->packageRoot()->scheduleToUpdate(install);
1094         }
1095     } else {
1096         pref->install();
1097     }
1098 }
1099
1100 void QtLauncher::onCancelDownload(const QModelIndex& index)
1101 {
1102     QString pkg = index.data(AircraftPackageIdRole).toString();
1103     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1104     simgear::pkg::InstallRef i = pref->existingInstall();
1105     i->cancelDownload();
1106 }
1107
1108 void QtLauncher::onRestoreDefaults()
1109 {
1110     QMessageBox mbox(this);
1111     mbox.setText(tr("Restore all settings to defaults?"));
1112     mbox.setInformativeText(tr("Restoring settings to their defaults may affect available add-ons such as scenery or aircraft."));
1113     QPushButton* quitButton = mbox.addButton(tr("Restore and restart now"), QMessageBox::YesRole);
1114     mbox.addButton(QMessageBox::Cancel);
1115     mbox.setDefaultButton(QMessageBox::Cancel);
1116     mbox.setIconPixmap(QPixmap(":/app-icon-large"));
1117
1118     mbox.exec();
1119     if (mbox.clickedButton() != quitButton) {
1120         return;
1121     }
1122
1123     {
1124         QSettings settings;
1125         settings.clear();
1126         settings.setValue("restore-defaults-on-run", true);
1127     }
1128
1129     restartTheApp(QStringList());
1130 }
1131
1132 void QtLauncher::maybeUpdateSelectedAircraft(QModelIndex index)
1133 {
1134     QUrl u = index.data(AircraftURIRole).toUrl();
1135     if (u == m_selectedAircraft) {
1136         // potentially enable the run button now!
1137         updateSelectedAircraft();
1138     }
1139 }
1140
1141 void QtLauncher::updateSelectedAircraft()
1142 {
1143     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1144     if (index.isValid()) {
1145         QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
1146         m_ui->thumbnail->setPixmap(pm);
1147         m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
1148
1149         int status = index.data(AircraftPackageStatusRole).toInt();
1150         bool canRun = (status == PackageInstalled);
1151         m_ui->runButton->setEnabled(canRun);
1152
1153         LauncherAircraftType aircraftType = Airplane;
1154         if (index.data(AircraftIsHelicopterRole).toBool()) {
1155             aircraftType = Helicopter;
1156         } else if (index.data(AircraftIsSeaplaneRole).toBool()) {
1157             aircraftType = Seaplane;
1158         }
1159
1160         m_ui->location->setAircraftType(aircraftType);
1161     } else {
1162         m_ui->thumbnail->setPixmap(QPixmap());
1163         m_ui->aircraftDescription->setText("");
1164         m_ui->runButton->setEnabled(false);
1165     }
1166 }
1167
1168 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
1169 {
1170   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
1171 }
1172
1173 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
1174 {
1175     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1176     Q_ASSERT(sourceModel);
1177     return sourceModel->indexOfAircraftURI(uri);
1178 }
1179
1180 void QtLauncher::onPopupAircraftHistory()
1181 {
1182     if (m_recentAircraft.isEmpty()) {
1183         return;
1184     }
1185
1186     QMenu m;
1187     Q_FOREACH(QUrl uri, m_recentAircraft) {
1188         QModelIndex index = sourceIndexForAircraftURI(uri);
1189         if (!index.isValid()) {
1190             // not scanned yet
1191             continue;
1192         }
1193         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1194         act->setData(uri);
1195     }
1196
1197     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1198     QAction* triggered = m.exec(popupPos);
1199     if (triggered) {
1200         m_selectedAircraft = triggered->data().toUrl();
1201         QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
1202         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1203                                                               QItemSelectionModel::ClearAndSelect);
1204         m_ui->aircraftFilter->clear();
1205         updateSelectedAircraft();
1206     }
1207 }
1208
1209 void QtLauncher::onEditRatingsFilter()
1210 {
1211     EditRatingsFilterDialog dialog(this);
1212     dialog.setRatings(m_ratingFilters);
1213
1214     dialog.exec();
1215     if (dialog.result() == QDialog::Accepted) {
1216         QVariantList vl;
1217         for (int i=0; i<4; ++i) {
1218             m_ratingFilters[i] = dialog.getRating(i);
1219             vl.append(m_ratingFilters[i]);
1220         }
1221         m_aircraftProxy->setRatings(m_ratingFilters);
1222
1223         QSettings settings;
1224         settings.setValue("min-ratings", vl);
1225     }
1226 }
1227
1228 void QtLauncher::updateSettingsSummary()
1229 {
1230     QStringList summary;
1231     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1232         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1233     }
1234
1235     if (m_ui->seasonCombo->currentIndex() > 0) {
1236         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1237     }
1238
1239     if (m_ui->rembrandtCheckbox->isChecked()) {
1240         summary.append("Rembrandt enabled");
1241     } else if (m_ui->msaaCheckbox->isChecked()) {
1242         summary.append("anti-aliasing");
1243     }
1244
1245     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1246         summary.append("live weather");
1247     }
1248
1249     if (m_ui->terrasyncCheck->isChecked()) {
1250         summary.append("automatic scenery downloads");
1251     }
1252
1253     if (m_ui->startPausedCheck->isChecked()) {
1254         summary.append("paused");
1255     }
1256
1257     QString s = summary.join(", ");
1258     s[0] = s[0].toUpper();
1259     m_ui->settingsDescription->setText(s);
1260 }
1261
1262 void QtLauncher::onRembrandtToggled(bool b)
1263 {
1264     // Rembrandt and multi-sample are exclusive
1265     m_ui->msaaCheckbox->setEnabled(!b);
1266 }
1267
1268 void QtLauncher::onShowInstalledAircraftToggled(bool b)
1269 {
1270     m_ui->ratingsFilterCheck->setEnabled(!b);
1271     maybeRestoreAircraftSelection();
1272 }
1273
1274 void QtLauncher::onSubsytemIdleTimeout()
1275 {
1276     globals->get_subsystem_mgr()->update(0.0);
1277 }
1278
1279 void QtLauncher::onDownloadDirChanged()
1280 {
1281
1282     // replace existing package root
1283     globals->get_subsystem<FGHTTPClient>()->shutdown();
1284     globals->setPackageRoot(simgear::pkg::RootRef());
1285
1286     // create new root with updated download-dir value
1287     fgInitPackageRoot();
1288
1289     globals->get_subsystem<FGHTTPClient>()->init();
1290
1291     QSettings settings;
1292     // re-scan the aircraft list
1293     m_aircraftModel->setPackageRoot(globals->packageRoot());
1294     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1295     m_aircraftModel->scanDirs();
1296
1297     checkOfficialCatalogMessage();
1298
1299     // re-set scenery dirs
1300     setSceneryPaths();
1301 }
1302
1303 void QtLauncher::checkOfficialCatalogMessage()
1304 {
1305     QSettings settings;
1306     bool showOfficialCatalogMesssage = !globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
1307     if (settings.value("hide-official-catalog-message").toBool()) {
1308         showOfficialCatalogMesssage = false;
1309     }
1310
1311     m_aircraftModel->setOfficialHangarMessageVisible(showOfficialCatalogMesssage);
1312     if (showOfficialCatalogMesssage) {
1313         NoOfficialHangarMessage* messageWidget = new NoOfficialHangarMessage;
1314         connect(messageWidget, &NoOfficialHangarMessage::linkActivated,
1315                 this, &QtLauncher::onOfficialCatalogMessageLink);
1316
1317         QModelIndex index = m_aircraftProxy->mapFromSource(m_aircraftModel->officialHangarMessageIndex());
1318         m_ui->aircraftList->setIndexWidget(index, messageWidget);
1319     }
1320 }
1321
1322 void QtLauncher::onOfficialCatalogMessageLink(QUrl link)
1323 {
1324     QString s = link.toString();
1325     if (s == "action:hide") {
1326         QSettings settings;
1327         settings.setValue("hide-official-catalog-message", true);
1328     } else if (s == "action:add-official") {
1329         AddOnsPage::addDefaultCatalog(this);
1330     }
1331
1332     checkOfficialCatalogMessage();
1333 }
1334
1335 void QtLauncher::onRefreshMPServers()
1336 {
1337     if (m_mpServerRequest.get()) {
1338         return; // in-progress
1339     }
1340
1341     string url(fgGetString("/sim/multiplay/serverlist-url",
1342                            "http://liveries.flightgear.org/mpstatus/mpservers.xml"));
1343
1344     if (url.empty()) {
1345         SG_LOG(SG_IO, SG_ALERT, "do_multiplayer.refreshserverlist: no URL given");
1346         return;
1347     }
1348
1349     SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1350     m_mpServerRequest.reset(new RemoteXMLRequest(url, targetnode));
1351     m_mpServerRequest->done(this, &QtLauncher::onRefreshMPServersDone);
1352     m_mpServerRequest->fail(this, &QtLauncher::onRefreshMPServersFailed);
1353     globals->get_subsystem<FGHTTPClient>()->makeRequest(m_mpServerRequest);
1354 }
1355
1356 void QtLauncher::onRefreshMPServersDone(simgear::HTTP::Request*)
1357 {
1358     // parse the properties
1359     SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1360     m_ui->mpServerCombo->clear();
1361
1362     for (int i=0; i<targetnode->nChildren(); ++i) {
1363         SGPropertyNode* c = targetnode->getChild(i);
1364         if (c->getName() != std::string("server")) {
1365             continue;
1366         }
1367
1368         QString name = QString::fromStdString(c->getStringValue("name"));
1369         QString loc = QString::fromStdString(c->getStringValue("location"));
1370         QString host = QString::fromStdString(c->getStringValue("hostname"));
1371         m_ui->mpServerCombo->addItem(tr("%1 - %2").arg(name,loc), host);
1372     }
1373
1374     EditCustomMPServerDialog::addCustomItem(m_ui->mpServerCombo);
1375     restoreMPServerSelection();
1376
1377     m_mpServerRequest.clear();
1378 }
1379
1380 void QtLauncher::onRefreshMPServersFailed(simgear::HTTP::Request*)
1381 {
1382     qWarning() << "refreshing MP servers failed:" << QString::fromStdString(m_mpServerRequest->responseReason());
1383     m_mpServerRequest.clear();
1384     EditCustomMPServerDialog::addCustomItem(m_ui->mpServerCombo);
1385     restoreMPServerSelection();
1386 }
1387
1388 void QtLauncher::restoreMPServerSelection()
1389 {
1390     if (m_doRestoreMPServer) {
1391         QSettings settings;
1392         int index = m_ui->mpServerCombo->findData(settings.value("mp-server"));
1393         if (index >= 0) {
1394             m_ui->mpServerCombo->setCurrentIndex(index);
1395         }
1396         m_doRestoreMPServer = false;
1397     }
1398 }
1399
1400 void QtLauncher::onMPServerActivated(int index)
1401 {
1402     if (m_ui->mpServerCombo->itemData(index) == "custom") {
1403         EditCustomMPServerDialog dlg(this);
1404         dlg.exec();
1405         if (dlg.result() == QDialog::Accepted) {
1406             m_ui->mpServerCombo->setItemText(index, tr("Custom - %1").arg(dlg.hostname()));
1407         }
1408     }
1409 }
1410
1411 int QtLauncher::findMPServerPort(const std::string& host)
1412 {
1413     SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1414     for (int i=0; i<targetnode->nChildren(); ++i) {
1415         SGPropertyNode* c = targetnode->getChild(i);
1416         if (c->getName() != std::string("server")) {
1417             continue;
1418         }
1419
1420         if (c->getStringValue("hostname") == host) {
1421             return c->getIntValue("port");
1422         }
1423     }
1424
1425     return 0;
1426 }
1427
1428 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1429 {
1430     if (uri.scheme() != "package") {
1431         qWarning() << "invalid URL scheme:" << uri;
1432         return simgear::pkg::PackageRef();
1433     }
1434
1435     QString ident = uri.path();
1436     return globals->packageRoot()->getPackageById(ident.toStdString());
1437 }
1438
1439 void QtLauncher::restartTheApp(QStringList fgArgs)
1440 {
1441     // Spawn a new instance of myApplication:
1442     QProcess proc;
1443     QStringList args;
1444
1445 #if defined(Q_OS_MAC)
1446     QDir dir(qApp->applicationDirPath()); // returns the 'MacOS' dir
1447     dir.cdUp(); // up to 'contents' dir
1448     dir.cdUp(); // up to .app dir
1449     // see 'man open' for details, but '-n' ensures we launch a new instance,
1450     // and we want to pass remaining arguments to us, not open.
1451     args << "-n" << dir.absolutePath() << "--args" << "--launcher" << fgArgs;
1452     qDebug() << "args" << args;
1453     proc.startDetached("open", args);
1454 #else
1455     args << "--launcher" << fgArgs;
1456     proc.startDetached(qApp->applicationFilePath(), args);
1457 #endif
1458     qApp->exit(-1);
1459 }
1460
1461 #include "QtLauncher.moc"