]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
QtLauncher::initApp: store argc to avoid crash
[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
23 // Qt
24 #include <QProgressDialog>
25 #include <QCoreApplication>
26 #include <QAbstractListModel>
27 #include <QDir>
28 #include <QFileInfo>
29 #include <QPixmap>
30 #include <QTimer>
31 #include <QDebug>
32 #include <QCompleter>
33 #include <QListView>
34 #include <QSettings>
35 #include <QSortFilterProxyModel>
36 #include <QMenu>
37 #include <QDesktopServices>
38 #include <QUrl>
39 #include <QAction>
40 #include <QFileDialog>
41 #include <QMessageBox>
42 #include <QDateTime>
43 #include <QApplication>
44
45 // Simgear
46 #include <simgear/timing/timestamp.hxx>
47 #include <simgear/props/props_io.hxx>
48 #include <simgear/structure/exception.hxx>
49 #include <simgear/structure/subsystem_mgr.hxx>
50 #include <simgear/misc/sg_path.hxx>
51 #include <simgear/package/Catalog.hxx>
52 #include <simgear/package/Package.hxx>
53 #include <simgear/package/Install.hxx>
54
55 #include "ui_Launcher.h"
56 #include "EditRatingsFilterDialog.hxx"
57 #include "AircraftItemDelegate.hxx"
58 #include "AircraftModel.hxx"
59 #include "PathsDialog.hxx"
60
61 #include <Main/globals.hxx>
62 #include <Navaids/NavDataCache.hxx>
63 #include <Airports/airport.hxx>
64 #include <Airports/dynamics.hxx> // for parking
65 #include <Main/options.hxx>
66 #include <Main/fg_init.hxx>
67 #include <Viewer/WindowBuilder.hxx>
68 #include <Network/HTTPClient.hxx>
69
70 using namespace flightgear;
71 using namespace simgear::pkg;
72
73 const int MAX_RECENT_AIRPORTS = 32;
74 const int MAX_RECENT_AIRCRAFT = 20;
75
76 namespace { // anonymous namespace
77
78 void initNavCache()
79 {
80     QString baseLabel = QT_TR_NOOP("Initialising navigation data, this may take several minutes");
81     NavDataCache* cache = NavDataCache::createInstance();
82     if (cache->isRebuildRequired()) {
83         QProgressDialog rebuildProgress(baseLabel,
84                                        QString() /* cancel text */,
85                                        0, 100);
86         rebuildProgress.setWindowModality(Qt::WindowModal);
87         rebuildProgress.show();
88
89         NavDataCache::RebuildPhase phase = cache->rebuild();
90
91         while (phase != NavDataCache::REBUILD_DONE) {
92             // sleep to give the rebuild thread more time
93             SGTimeStamp::sleepForMSec(50);
94             phase = cache->rebuild();
95             
96             switch (phase) {
97             case NavDataCache::REBUILD_AIRPORTS:
98                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading airport data"));
99                 break;
100
101             case NavDataCache::REBUILD_FIXES:
102                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading waypoint data"));
103                 break;
104
105             case NavDataCache::REBUILD_NAVAIDS:
106                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading navigation data"));
107                 break;
108
109
110             case NavDataCache::REBUILD_POIS:
111                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading point-of-interest data"));
112                 break;
113
114             default:
115                 rebuildProgress.setLabelText(baseLabel);
116             }
117
118             if (phase == NavDataCache::REBUILD_UNKNOWN) {
119                 rebuildProgress.setValue(0);
120                 rebuildProgress.setMaximum(0);
121             } else {
122                 rebuildProgress.setValue(cache->rebuildPhaseCompletionPercentage());
123                 rebuildProgress.setMaximum(100);
124             }
125
126             QCoreApplication::processEvents();
127         }
128     }
129 }
130
131 class ArgumentsTokenizer
132 {
133 public:
134     class Arg
135     {
136     public:
137         explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
138
139         QString arg;
140         QString value;
141     };
142
143     QList<Arg> tokenize(QString in) const
144     {
145         int index = 0;
146         const int len = in.count();
147         QChar c, nc;
148         State state = Start;
149         QString key, value;
150         QList<Arg> result;
151
152         for (; index < len; ++index) {
153             c = in.at(index);
154             nc = index < (len - 1) ? in.at(index + 1) : QChar();
155
156             switch (state) {
157             case Start:
158                 if (c == QChar('-')) {
159                     if (nc == QChar('-')) {
160                         state = Key;
161                         key.clear();
162                         ++index;
163                     } else {
164                         // should we pemit single hyphen arguments?
165                         // choosing to fail for now
166                         return QList<Arg>();
167                     }
168                 } else if (c.isSpace()) {
169                     break;
170                 }
171                 break;
172
173             case Key:
174                 if (c == QChar('=')) {
175                     state = Value;
176                     value.clear();
177                 } else if (c.isSpace()) {
178                     state = Start;
179                     result.append(Arg(key));
180                 } else {
181                     // could check for illegal charatcers here
182                     key.append(c);
183                 }
184                 break;
185
186             case Value:
187                 if (c == QChar('"')) {
188                     state = Quoted;
189                 } else if (c.isSpace()) {
190                     state = Start;
191                     result.append(Arg(key, value));
192                 } else {
193                     value.append(c);
194                 }
195                 break;
196
197             case Quoted:
198                 if (c == QChar('\\')) {
199                     // check for escaped double-quote inside quoted value
200                     if (nc == QChar('"')) {
201                         ++index;
202                     }
203                 } else if (c == QChar('"')) {
204                     state = Value;
205                 } else {
206                     value.append(c);
207                 }
208                 break;
209             } // of state switch
210         } // of character loop
211
212         // ensure last argument isn't lost
213         if (state == Key) {
214             result.append(Arg(key));
215         } else if (state == Value) {
216             result.append(Arg(key, value));
217         }
218
219         return result;
220     }
221
222 private:
223     enum State {
224         Start = 0,
225         Key,
226         Value,
227         Quoted
228     };
229 };
230
231 } // of anonymous namespace
232
233 class AirportSearchModel : public QAbstractListModel
234 {
235     Q_OBJECT
236 public:
237     AirportSearchModel() :
238         m_searchActive(false)
239     {
240     }
241
242     void setSearch(QString t)
243     {
244         beginResetModel();
245
246         m_airports.clear();
247         m_ids.clear();
248
249         std::string term(t.toUpper().toStdString());
250         // try ICAO lookup first
251         FGAirportRef ref = FGAirport::findByIdent(term);
252         if (ref) {
253             m_ids.push_back(ref->guid());
254             m_airports.push_back(ref);
255         } else {
256             m_search.reset(new NavDataCache::ThreadedAirportSearch(term));
257             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
258             m_searchActive = true;
259         }
260
261         endResetModel();
262     }
263
264     bool isSearchActive() const
265     {
266         return m_searchActive;
267     }
268
269     virtual int rowCount(const QModelIndex&) const
270     {
271         // if empty, return 1 for special 'no matches'?
272         return m_ids.size();
273     }
274
275     virtual QVariant data(const QModelIndex& index, int role) const
276     {
277         if (!index.isValid())
278             return QVariant();
279         
280         FGAirportRef apt = m_airports[index.row()];
281         if (!apt.valid()) {
282             apt = FGPositioned::loadById<FGAirport>(m_ids[index.row()]);
283             m_airports[index.row()] = apt;
284         }
285
286         if (role == Qt::DisplayRole) {
287             QString name = QString::fromStdString(apt->name());
288             return QString("%1: %2").arg(QString::fromStdString(apt->ident())).arg(name);
289         }
290
291         if (role == Qt::EditRole) {
292             return QString::fromStdString(apt->ident());
293         }
294
295         if (role == Qt::UserRole) {
296             return static_cast<qlonglong>(m_ids[index.row()]);
297         }
298
299         return QVariant();
300     }
301
302     QString firstIdent() const
303     {
304         if (m_ids.empty())
305             return QString();
306
307         if (!m_airports.front().valid()) {
308             m_airports[0] = FGPositioned::loadById<FGAirport>(m_ids.front());
309         }
310
311         return QString::fromStdString(m_airports.front()->ident());
312     }
313
314 Q_SIGNALS:
315     void searchComplete();
316
317 private slots:
318     void onSearchResultsPoll()
319     {
320         PositionedIDVec newIds = m_search->results();
321         
322         beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
323         for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
324             m_ids.push_back(newIds[i]);
325             m_airports.push_back(FGAirportRef()); // null ref
326         }
327         endInsertRows();
328
329         if (m_search->isComplete()) {
330             m_searchActive = false;
331             m_search.reset();
332             emit searchComplete();
333         } else {
334             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
335         }
336     }
337
338 private:
339     PositionedIDVec m_ids;
340     mutable std::vector<FGAirportRef> m_airports;
341     bool m_searchActive;
342     QScopedPointer<NavDataCache::ThreadedAirportSearch> m_search;
343 };
344
345 class AircraftProxyModel : public QSortFilterProxyModel
346 {
347     Q_OBJECT
348 public:
349     AircraftProxyModel(QObject* pr) :
350         QSortFilterProxyModel(pr),
351         m_ratingsFilter(true)
352     {
353         for (int i=0; i<4; ++i) {
354             m_ratings[i] = 3;
355         }
356     }
357
358     void setRatings(int* ratings)
359     {
360         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
361         invalidate();
362     }
363
364 public slots:
365     void setRatingFilterEnabled(bool e)
366     {
367         if (e == m_ratingsFilter) {
368             return;
369         }
370
371         m_ratingsFilter = e;
372         invalidate();
373     }
374
375 protected:
376     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
377     {
378         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
379             return false;
380         }
381
382         if (m_ratingsFilter) {
383             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
384             for (int i=0; i<4; ++i) {
385                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
386                     return false;
387                 }
388             }
389         }
390
391         return true;
392     }
393
394 private:
395     bool m_ratingsFilter;
396     int m_ratings[4];
397 };
398
399 QtLauncher::QtLauncher() :
400     QDialog(),
401     m_ui(NULL)
402 {
403     m_ui.reset(new Ui::Launcher);
404     m_ui->setupUi(this);
405
406 #if QT_VERSION >= 0x050300
407     // don't require Qt 5.3
408     m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
409 #endif
410
411 #if QT_VERSION >= 0x050200
412     m_ui->aircraftFilter->setClearButtonEnabled(true);
413 #endif
414
415     for (int i=0; i<4; ++i) {
416         m_ratingFilters[i] = 3;
417     }
418
419     m_subsystemIdleTimer = new QTimer(this);
420     m_subsystemIdleTimer->setInterval(0);
421     connect(m_subsystemIdleTimer, &QTimer::timeout,
422             this, &QtLauncher::onSubsytemIdleTimeout);
423     m_subsystemIdleTimer->start();
424
425     m_airportsModel = new AirportSearchModel;
426     m_ui->searchList->setModel(m_airportsModel);
427     connect(m_ui->searchList, &QListView::clicked,
428             this, &QtLauncher::onAirportChoiceSelected);
429     connect(m_airportsModel, &AirportSearchModel::searchComplete,
430             this, &QtLauncher::onAirportSearchComplete);
431
432     // create and configure the proxy model
433     m_aircraftProxy = new AircraftProxyModel(this);
434     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
435             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
436     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
437             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
438
439     connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
440             this, SLOT(updateAirportDescription()));
441     connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
442             this, SLOT(updateAirportDescription()));
443     connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
444             this, SLOT(updateAirportDescription()));
445     connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
446             this, SLOT(updateAirportDescription()));
447     connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
448             this, SLOT(updateAirportDescription()));
449
450     
451     connect(m_ui->airportDiagram, &AirportDiagram::clickedRunway,
452             this, &QtLauncher::onAirportDiagramClicked);
453
454     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
455     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
456     connect(m_ui->airportEdit, SIGNAL(returnPressed()),
457             this, SLOT(onSearchAirports()));
458
459     connect(m_ui->airportHistory, &QPushButton::clicked,
460             this, &QtLauncher::onPopupAirportHistory);
461     connect(m_ui->aircraftHistory, &QPushButton::clicked,
462           this, &QtLauncher::onPopupAircraftHistory);
463
464     QAction* qa = new QAction(this);
465     qa->setShortcut(QKeySequence("Ctrl+Q"));
466     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
467     addAction(qa);
468
469     connect(m_ui->editRatingFilter, &QPushButton::clicked,
470             this, &QtLauncher::onEditRatingsFilter);
471
472     QIcon historyIcon(":/history-icon");
473     m_ui->aircraftHistory->setIcon(historyIcon);
474     m_ui->airportHistory->setIcon(historyIcon);
475
476     m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
477
478     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
479             this, SLOT(updateSettingsSummary()));
480     connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
481             this, SLOT(updateSettingsSummary()));
482     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
483             this, SLOT(updateSettingsSummary()));
484     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
485             this, SLOT(updateSettingsSummary()));
486     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
487             this, SLOT(updateSettingsSummary()));
488     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
489             this, SLOT(updateSettingsSummary()));
490     connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
491             this, SLOT(updateSettingsSummary()));
492
493     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
494             this, SLOT(onRembrandtToggled(bool)));
495     connect(m_ui->terrasyncCheck, &QCheckBox::toggled,
496             this, &QtLauncher::onToggleTerrasync);
497     updateSettingsSummary();
498
499     fgInitPackageRoot();
500     RootRef r(globals->packageRoot());
501
502     FGHTTPClient* http = new FGHTTPClient;
503     globals->add_subsystem("http", http);
504
505     // we guard against re-init in the global phase; bind and postinit
506     // will happen as normal
507     http->init();
508
509     m_aircraftModel = new AircraftItemModel(this, r);
510     m_aircraftProxy->setSourceModel(m_aircraftModel);
511
512     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
513     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
514     m_aircraftProxy->setSortRole(Qt::DisplayRole);
515     m_aircraftProxy->setDynamicSortFilter(true);
516
517     m_ui->aircraftList->setModel(m_aircraftProxy);
518     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
519     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
520     m_ui->aircraftList->setItemDelegate(delegate);
521     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
522     connect(m_ui->aircraftList, &QListView::clicked,
523             this, &QtLauncher::onAircraftSelected);
524     connect(delegate, &AircraftItemDelegate::variantChanged,
525             this, &QtLauncher::onAircraftSelected);
526     connect(delegate, &AircraftItemDelegate::requestInstall,
527             this, &QtLauncher::onRequestPackageInstall);
528     connect(delegate, &AircraftItemDelegate::cancelDownload,
529             this, &QtLauncher::onCancelDownload);
530
531     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
532             this, &QtLauncher::onAircraftInstalledCompleted);
533     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
534             this, &QtLauncher::onAircraftInstallFailed);
535     
536     connect(m_ui->pathsButton, &QPushButton::clicked,
537             this, &QtLauncher::onEditPaths);
538
539     restoreSettings();
540
541     QSettings settings;
542     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
543     m_aircraftModel->scanDirs();
544 }
545
546 QtLauncher::~QtLauncher()
547 {
548     
549 }
550
551 void QtLauncher::initApp(int& argc, char** argv)
552 {
553     static bool qtInitDone = false;
554     static int s_argc;
555     if (!qtInitDone) {
556         qtInitDone = true;
557         s_argc = argc; // QApplication only stores a reference to argc,
558         // and may crash if it is freed
559         // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication
560
561         QApplication* app = new QApplication(s_argc, argv);
562         app->setOrganizationName("FlightGear");
563         app->setApplicationName("FlightGear");
564         app->setOrganizationDomain("flightgear.org");
565
566         // avoid double Apple menu and other weirdness if both Qt and OSG
567         // try to initialise various Cocoa structures.
568         flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
569
570         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
571         if (mods & Qt::AltModifier) {
572             qWarning() << "Alt pressed during launch";
573
574             // wipe out our settings
575             QSettings settings;
576             settings.clear();
577
578
579             Options::sharedInstance()->addOption("restore-defaults", "");
580         }
581     }
582 }
583
584 bool QtLauncher::runLauncherDialog()
585 {
586     sglog().setLogLevels( SG_ALL, SG_INFO );
587     Q_INIT_RESOURCE(resources);
588
589     // startup the nav-cache now. This pre-empts normal startup of
590     // the cache, but no harm done. (Providing scenery paths are consistent)
591
592     initNavCache();
593
594   // setup scenery paths now, especially TerraSync path for airport
595   // parking locations (after they're downloaded)
596
597     QtLauncher dlg;
598     dlg.exec();
599     if (dlg.result() != QDialog::Accepted) {
600         return false;
601     }
602
603     return true;
604 }
605
606 void QtLauncher::restoreSettings()
607 {
608     QSettings settings;
609     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
610     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
611     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
612     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
613     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
614     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
615     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
616     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
617
618     // full paths to -set.xml files
619     m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
620
621     if (!m_recentAircraft.empty()) {
622         m_selectedAircraft = m_recentAircraft.front();
623     } else {
624         // select the default C172p
625     }
626
627     updateSelectedAircraft();
628
629     // ICAO identifiers
630     m_recentAirports = settings.value("recent-airports").toStringList();
631     if (!m_recentAirports.empty()) {
632         setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString()));
633     }
634     updateAirportDescription();
635
636     // rating filters
637     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
638     int index = 0;
639     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
640         m_ratingFilters[index++] = v.toInt();
641     }
642
643     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
644     m_aircraftProxy->setRatings(m_ratingFilters);
645
646     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
647 }
648
649 void QtLauncher::saveSettings()
650 {
651     QSettings settings;
652     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
653     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
654     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
655     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
656     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
657     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
658     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
659     settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
660     settings.setValue("recent-airports", m_recentAirports);
661     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
662     settings.setValue("season", m_ui->seasonCombo->currentIndex());
663     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
664 }
665
666 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
667 {
668     flightgear::Options* opt = flightgear::Options::sharedInstance();
669     std::string stdName(name.toStdString());
670     if (cbox->isChecked()) {
671         opt->addOption("enable-" + stdName, "");
672     } else {
673         opt->addOption("disable-" + stdName, "");
674     }
675 }
676
677 void QtLauncher::onRun()
678 {
679     accept();
680
681     flightgear::Options* opt = flightgear::Options::sharedInstance();
682     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
683     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
684     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
685     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
686     setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
687
688     // MSAA is more complex
689     if (!m_ui->rembrandtCheckbox->isChecked()) {
690         if (m_ui->msaaCheckbox->isChecked()) {
691             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
692             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
693         } else {
694             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
695         }
696     }
697
698     // aircraft
699     if (!m_selectedAircraft.isEmpty()) {
700         if (m_selectedAircraft.isLocalFile()) {
701             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
702             opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
703             QString setFile = setFileInfo.fileName();
704             Q_ASSERT(setFile.endsWith("-set.xml"));
705             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
706             opt->addOption("aircraft", setFile.toStdString());
707         } else if (m_selectedAircraft.scheme() == "package") {
708             PackageRef pkg = packageForAircraftURI(m_selectedAircraft);
709             // no need to set aircraft-dir, handled by the corresponding code
710             // in fgInitAircraft
711             opt->addOption("aircraft", pkg->qualifiedId());
712         } else {
713             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
714         }
715         
716       // manage aircraft history
717         if (m_recentAircraft.contains(m_selectedAircraft))
718           m_recentAircraft.removeOne(m_selectedAircraft);
719         m_recentAircraft.prepend(m_selectedAircraft);
720         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
721           m_recentAircraft.pop_back();
722     }
723
724     // airport / location
725     if (m_selectedAirport) {
726         opt->addOption("airport", m_selectedAirport->ident());
727     }
728
729     if (m_ui->runwayRadio->isChecked()) {
730         int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
731         if ((index >= 0) && m_selectedAirport) {
732             // explicit runway choice
733             opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident());
734         }
735
736         if (m_ui->onFinalCheckbox->isChecked()) {
737             opt->addOption("glideslope", "3.0");
738             opt->addOption("offset-distance", "10.0"); // in nautical miles
739         }
740     } else if (m_ui->parkingRadio->isChecked()) {
741         // parking selection
742         opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
743     }
744
745     // time of day
746     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
747         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
748         opt->addOption("timeofday", dayval.toStdString());
749     }
750
751     if (m_ui->seasonCombo->currentIndex() != 0) {
752         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
753         opt->addOption("season", dayval.toStdString());
754     }
755
756     QSettings settings;
757     QString downloadDir = settings.value("download-dir").toString();
758     if (!downloadDir.isEmpty()) {
759         QDir d(downloadDir);
760         if (!d.exists()) {
761             int result = QMessageBox::question(this, tr("Create download folder?"),
762                                   tr("The selected location for downloads does not exist. Create it?"),
763                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
764             if (result == QMessageBox::Cancel) {
765                 return;
766             }
767
768             if (result == QMessageBox::Yes) {
769                 d.mkpath(downloadDir);
770             }
771         }
772
773         qDebug() << "Download dir is:" << downloadDir;
774         opt->addOption("download-dir", downloadDir.toStdString());
775     }
776
777     // scenery paths
778     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
779         opt->addOption("fg-scenery", path.toStdString());
780     }
781
782     // aircraft paths
783     Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
784         // can't use fg-aircraft for this, as it is processed before the launcher is run
785         globals->append_aircraft_path(path.toStdString());
786     }
787
788     // additional arguments
789     ArgumentsTokenizer tk;
790     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
791         if (a.arg.startsWith("prop:")) {
792             QString v = a.arg.mid(5) + "=" + a.value;
793             opt->addOption("prop", v.toStdString());
794         } else {
795             opt->addOption(a.arg.toStdString(), a.value.toStdString());
796         }
797     }
798
799     saveSettings();
800 }
801
802 void QtLauncher::onQuit()
803 {
804     reject();
805 }
806
807 void QtLauncher::onSearchAirports()
808 {
809     QString search = m_ui->airportEdit->text();
810     m_airportsModel->setSearch(search);
811
812     if (m_airportsModel->isSearchActive()) {
813         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
814         m_ui->locationStack->setCurrentIndex(2);
815     } else if (m_airportsModel->rowCount(QModelIndex()) == 1) {
816         QString ident = m_airportsModel->firstIdent();
817         setAirport(FGAirport::findByIdent(ident.toStdString()));
818         m_ui->locationStack->setCurrentIndex(0);
819     }
820 }
821
822 void QtLauncher::onAirportSearchComplete()
823 {
824     int numResults = m_airportsModel->rowCount(QModelIndex());
825     if (numResults == 0) {
826         m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text()));
827     } else if (numResults == 1) {
828         QString ident = m_airportsModel->firstIdent();
829         setAirport(FGAirport::findByIdent(ident.toStdString()));
830         m_ui->locationStack->setCurrentIndex(0);
831     } else {
832         m_ui->locationStack->setCurrentIndex(1);
833     }
834 }
835
836 void QtLauncher::onAirportChanged()
837 {
838     m_ui->runwayCombo->setEnabled(m_selectedAirport);
839     m_ui->parkingCombo->setEnabled(m_selectedAirport);
840     m_ui->airportDiagram->setAirport(m_selectedAirport);
841
842     m_ui->runwayRadio->setChecked(true); // default back to runway mode
843     // unelss multiplayer is enabled ?
844
845     if (!m_selectedAirport) {
846         m_ui->airportDescription->setText(QString());
847         m_ui->airportDiagram->setEnabled(false);
848         return;
849     }
850
851     m_ui->airportDiagram->setEnabled(true);
852
853     m_ui->runwayCombo->clear();
854     m_ui->runwayCombo->addItem("Automatic", -1);
855     for (unsigned int r=0; r<m_selectedAirport->numRunways(); ++r) {
856         FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r);
857         // add runway with index as data role
858         m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
859
860         m_ui->airportDiagram->addRunway(rwy);
861     }
862
863     m_ui->parkingCombo->clear();
864     FGAirportDynamics* dynamics = m_selectedAirport->getDynamics();
865     PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(
866                                                                             m_selectedAirport->guid(),
867                                                                             FGPositioned::PARKING);
868     if (parkings.empty()) {
869         m_ui->parkingCombo->setEnabled(false);
870         m_ui->parkingRadio->setEnabled(false);
871     } else {
872         m_ui->parkingCombo->setEnabled(true);
873         m_ui->parkingRadio->setEnabled(true);
874         Q_FOREACH(PositionedID parking, parkings) {
875             FGParking* park = dynamics->getParking(parking);
876             m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
877                                         static_cast<qlonglong>(parking));
878
879             m_ui->airportDiagram->addParking(park);
880         }
881     }
882 }
883
884 void QtLauncher::onAirportDiagramClicked(FGRunwayRef rwy)
885 {
886     if (rwy) {
887         m_ui->runwayRadio->setChecked(true);
888         int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident()));
889         m_ui->runwayCombo->setCurrentIndex(rwyIndex);
890     }
891     
892     updateAirportDescription();
893 }
894
895 void QtLauncher::onToggleTerrasync(bool enabled)
896 {
897     if (enabled) {
898         QSettings settings;
899         QString downloadDir = settings.value("download-dir").toString();
900         if (downloadDir.isEmpty()) {
901             downloadDir = QString::fromStdString(flightgear::defaultDownloadDir());
902         }
903
904         QFileInfo info(downloadDir);
905         if (!info.exists()) {
906             QMessageBox msg;
907             msg.setWindowTitle(tr("Create download folder?"));
908             msg.setText(tr("The download folder '%1' does not exist, create it now? "
909                            "Click 'change location' to choose another folder "
910                            "to store downloaded files").arg(downloadDir));
911             msg.addButton(QMessageBox::Yes);
912             msg.addButton(QMessageBox::Cancel);
913             msg.addButton(tr("Change location"), QMessageBox::ActionRole);
914             int result = msg.exec();
915   
916             if (result == QMessageBox::Cancel) {
917                 m_ui->terrasyncCheck->setChecked(false);
918                 return;
919             }
920
921             if (result == QMessageBox::ActionRole) {
922                 onEditPaths();
923                 return;
924             }
925
926             QDir d(downloadDir);
927             d.mkpath(downloadDir);
928         }
929     } // of is enabled
930 }
931
932 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
933 {
934     maybeUpdateSelectedAircraft(index);
935 }
936
937 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
938 {
939     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
940     
941     QMessageBox msg;
942     msg.setWindowTitle(tr("Aircraft installation failed"));
943     msg.setText(tr("An error occurred installing the aircraft %1: %2").
944                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
945     msg.addButton(QMessageBox::Ok);
946     msg.exec();
947
948     maybeUpdateSelectedAircraft(index);
949 }
950
951 void QtLauncher::updateAirportDescription()
952 {
953     if (!m_selectedAirport) {
954         m_ui->airportDescription->setText(QString("No airport selected"));
955         return;
956     }
957
958     QString ident = QString::fromStdString(m_selectedAirport->ident()),
959         name = QString::fromStdString(m_selectedAirport->name());
960     QString locationOnAirport;
961     if (m_ui->runwayRadio->isChecked()) {
962         bool onFinal = m_ui->onFinalCheckbox->isChecked();
963         int comboIndex = m_ui->runwayCombo->currentIndex();
964         QString runwayName = (comboIndex == 0) ?
965             "active runway" :
966             QString("runway %1").arg(m_ui->runwayCombo->currentText());
967
968         if (onFinal) {
969             locationOnAirport = QString("on 10-mile final to %1").arg(runwayName);
970         } else {
971             locationOnAirport = QString("on %1").arg(runwayName);
972         }
973         
974         int runwayIndex = m_ui->runwayCombo->itemData(comboIndex).toInt();
975         FGRunwayRef rwy = (runwayIndex >= 0) ?
976             m_selectedAirport->getRunwayByIndex(runwayIndex) : FGRunwayRef();
977         m_ui->airportDiagram->setSelectedRunway(rwy);
978     } else if (m_ui->parkingRadio->isChecked()) {
979         locationOnAirport =  QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
980     }
981
982     m_ui->airportDescription->setText(QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport));
983 }
984
985 void QtLauncher::onAirportChoiceSelected(const QModelIndex& index)
986 {
987     m_ui->locationStack->setCurrentIndex(0);
988     setAirport(FGPositioned::loadById<FGAirport>(index.data(Qt::UserRole).toULongLong()));
989 }
990
991 void QtLauncher::onAircraftSelected(const QModelIndex& index)
992 {
993     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
994     updateSelectedAircraft();
995 }
996
997 void QtLauncher::onRequestPackageInstall(const QModelIndex& index)
998 {
999     QString pkg = index.data(AircraftPackageIdRole).toString();
1000     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1001     if (pref->isInstalled()) {
1002         InstallRef install = pref->existingInstall();
1003         if (install && install->hasUpdate()) {
1004             globals->packageRoot()->scheduleToUpdate(install);
1005         }
1006     } else {
1007         pref->install();
1008     }
1009 }
1010
1011 void QtLauncher::onCancelDownload(const QModelIndex& index)
1012 {
1013     QString pkg = index.data(AircraftPackageIdRole).toString();
1014     qDebug() << "cancel download of" << pkg;
1015     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1016     simgear::pkg::InstallRef i = pref->existingInstall();
1017     i->cancelDownload();
1018 }
1019
1020 void QtLauncher::maybeUpdateSelectedAircraft(QModelIndex index)
1021 {
1022     QUrl u = index.data(AircraftURIRole).toUrl();
1023     if (u == m_selectedAircraft) {
1024         // potentially enable the run button now!
1025         updateSelectedAircraft();
1026     }
1027 }
1028
1029 void QtLauncher::updateSelectedAircraft()
1030 {
1031     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1032     if (index.isValid()) {
1033         QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
1034         m_ui->thumbnail->setPixmap(pm);
1035         m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
1036         
1037         int status = index.data(AircraftPackageStatusRole).toInt();
1038         bool canRun = (status == PackageInstalled);
1039         m_ui->runButton->setEnabled(canRun);
1040     } else {
1041         m_ui->thumbnail->setPixmap(QPixmap());
1042         m_ui->aircraftDescription->setText("");
1043         m_ui->runButton->setEnabled(false);
1044     }
1045 }
1046
1047 void QtLauncher::onPopupAirportHistory()
1048 {
1049     if (m_recentAirports.isEmpty()) {
1050         return;
1051     }
1052
1053     QMenu m;
1054     Q_FOREACH(QString aptCode, m_recentAirports) {
1055         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
1056         QString name = QString::fromStdString(apt->name());
1057         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
1058         act->setData(aptCode);
1059     }
1060
1061     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
1062     QAction* triggered = m.exec(popupPos);
1063     if (triggered) {
1064         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
1065         setAirport(apt);
1066         m_ui->airportEdit->clear();
1067         m_ui->locationStack->setCurrentIndex(0);
1068     }
1069 }
1070
1071 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
1072 {
1073   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
1074 }
1075
1076 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
1077 {
1078     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1079     Q_ASSERT(sourceModel);
1080     return sourceModel->indexOfAircraftURI(uri);
1081 }
1082
1083 void QtLauncher::onPopupAircraftHistory()
1084 {
1085     if (m_recentAircraft.isEmpty()) {
1086         return;
1087     }
1088
1089     QMenu m;
1090     Q_FOREACH(QUrl uri, m_recentAircraft) {
1091         QModelIndex index = sourceIndexForAircraftURI(uri);
1092         if (!index.isValid()) {
1093             // not scanned yet
1094             continue;
1095         }
1096         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1097         act->setData(uri);
1098     }
1099
1100     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1101     QAction* triggered = m.exec(popupPos);
1102     if (triggered) {
1103         m_selectedAircraft = triggered->data().toUrl();
1104         QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
1105         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1106                                                               QItemSelectionModel::ClearAndSelect);
1107         m_ui->aircraftFilter->clear();
1108         updateSelectedAircraft();
1109     }
1110 }
1111
1112 void QtLauncher::setAirport(FGAirportRef ref)
1113 {
1114     if (m_selectedAirport == ref)
1115         return;
1116
1117     m_selectedAirport = ref;
1118     onAirportChanged();
1119
1120     if (ref.valid()) {
1121         // maintain the recent airport list
1122         QString icao = QString::fromStdString(ref->ident());
1123         if (m_recentAirports.contains(icao)) {
1124             // move to front
1125             m_recentAirports.removeOne(icao);
1126             m_recentAirports.push_front(icao);
1127         } else {
1128             // insert and trim list if necessary
1129             m_recentAirports.push_front(icao);
1130             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
1131                 m_recentAirports.pop_back();
1132             }
1133         }
1134     }
1135
1136     updateAirportDescription();
1137 }
1138
1139 void QtLauncher::onEditRatingsFilter()
1140 {
1141     EditRatingsFilterDialog dialog(this);
1142     dialog.setRatings(m_ratingFilters);
1143
1144     dialog.exec();
1145     if (dialog.result() == QDialog::Accepted) {
1146         QVariantList vl;
1147         for (int i=0; i<4; ++i) {
1148             m_ratingFilters[i] = dialog.getRating(i);
1149             vl.append(m_ratingFilters[i]);
1150         }
1151         m_aircraftProxy->setRatings(m_ratingFilters);
1152
1153         QSettings settings;
1154         settings.setValue("min-ratings", vl);
1155     }
1156 }
1157
1158 void QtLauncher::updateSettingsSummary()
1159 {
1160     QStringList summary;
1161     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1162         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1163     }
1164
1165     if (m_ui->seasonCombo->currentIndex() > 0) {
1166         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1167     }
1168
1169     if (m_ui->rembrandtCheckbox->isChecked()) {
1170         summary.append("Rembrandt enabled");
1171     } else if (m_ui->msaaCheckbox->isChecked()) {
1172         summary.append("anti-aliasing");
1173     }
1174
1175     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1176         summary.append("live weather");
1177     }
1178
1179     if (m_ui->terrasyncCheck->isChecked()) {
1180         summary.append("automatic scenery downloads");
1181     }
1182
1183     if (m_ui->startPausedCheck->isChecked()) {
1184         summary.append("paused");
1185     }
1186
1187     QString s = summary.join(", ");
1188     s[0] = s[0].toUpper();
1189     m_ui->settingsDescription->setText(s);
1190 }
1191
1192 void QtLauncher::onRembrandtToggled(bool b)
1193 {
1194     // Rembrandt and multi-sample are exclusive
1195     m_ui->msaaCheckbox->setEnabled(!b);
1196 }
1197
1198 void QtLauncher::onSubsytemIdleTimeout()
1199 {
1200     globals->get_subsystem_mgr()->update(0.0);
1201 }
1202
1203 void QtLauncher::onEditPaths()
1204 {
1205     PathsDialog dlg(this, globals->packageRoot());
1206     dlg.exec();
1207     if (dlg.result() == QDialog::Accepted) {
1208         // re-scan the aircraft list
1209         QSettings settings;
1210         m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1211         m_aircraftModel->scanDirs();
1212     }
1213 }
1214
1215 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1216 {
1217     if (uri.scheme() != "package") {
1218         qWarning() << "invalid URL scheme:" << uri;
1219         return simgear::pkg::PackageRef();
1220     }
1221     
1222     QString ident = uri.path();
1223     return globals->packageRoot()->getPackageById(ident.toStdString());
1224 }
1225
1226 #include "QtLauncher.moc"
1227