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