]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Disable mouse hover code for now.
[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     if (!qtInitDone) {
555         qtInitDone = true;
556
557         QApplication* app = new QApplication(argc, argv);
558         app->setOrganizationName("FlightGear");
559         app->setApplicationName("FlightGear");
560         app->setOrganizationDomain("flightgear.org");
561
562         // avoid double Apple menu and other weirdness if both Qt and OSG
563         // try to initialise various Cocoa structures.
564         flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
565
566         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
567         if (mods & Qt::AltModifier) {
568             qWarning() << "Alt pressed during launch";
569
570             // wipe out our settings
571             QSettings settings;
572             settings.clear();
573
574
575             Options::sharedInstance()->addOption("restore-defaults", "");
576         }
577     }
578 }
579
580 bool QtLauncher::runLauncherDialog()
581 {
582     sglog().setLogLevels( SG_ALL, SG_INFO );
583     Q_INIT_RESOURCE(resources);
584
585     // startup the nav-cache now. This pre-empts normal startup of
586     // the cache, but no harm done. (Providing scenery paths are consistent)
587
588     initNavCache();
589
590   // setup scenery paths now, especially TerraSync path for airport
591   // parking locations (after they're downloaded)
592
593     QtLauncher dlg;
594     dlg.exec();
595     if (dlg.result() != QDialog::Accepted) {
596         return false;
597     }
598
599     return true;
600 }
601
602 void QtLauncher::restoreSettings()
603 {
604     QSettings settings;
605     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
606     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
607     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
608     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
609     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
610     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
611     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
612     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
613
614     // full paths to -set.xml files
615     m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
616
617     if (!m_recentAircraft.empty()) {
618         m_selectedAircraft = m_recentAircraft.front();
619     } else {
620         // select the default C172p
621     }
622
623     updateSelectedAircraft();
624
625     // ICAO identifiers
626     m_recentAirports = settings.value("recent-airports").toStringList();
627     if (!m_recentAirports.empty()) {
628         setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString()));
629     }
630     updateAirportDescription();
631
632     // rating filters
633     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
634     int index = 0;
635     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
636         m_ratingFilters[index++] = v.toInt();
637     }
638
639     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
640     m_aircraftProxy->setRatings(m_ratingFilters);
641
642     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
643 }
644
645 void QtLauncher::saveSettings()
646 {
647     QSettings settings;
648     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
649     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
650     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
651     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
652     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
653     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
654     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
655     settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
656     settings.setValue("recent-airports", m_recentAirports);
657     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
658     settings.setValue("season", m_ui->seasonCombo->currentIndex());
659     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
660 }
661
662 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
663 {
664     flightgear::Options* opt = flightgear::Options::sharedInstance();
665     std::string stdName(name.toStdString());
666     if (cbox->isChecked()) {
667         opt->addOption("enable-" + stdName, "");
668     } else {
669         opt->addOption("disable-" + stdName, "");
670     }
671 }
672
673 void QtLauncher::onRun()
674 {
675     accept();
676
677     flightgear::Options* opt = flightgear::Options::sharedInstance();
678     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
679     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
680     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
681     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
682     setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
683
684     // MSAA is more complex
685     if (!m_ui->rembrandtCheckbox->isChecked()) {
686         if (m_ui->msaaCheckbox->isChecked()) {
687             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
688             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
689         } else {
690             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
691         }
692     }
693
694     // aircraft
695     if (!m_selectedAircraft.isEmpty()) {
696         if (m_selectedAircraft.isLocalFile()) {
697             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
698             opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
699             QString setFile = setFileInfo.fileName();
700             Q_ASSERT(setFile.endsWith("-set.xml"));
701             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
702             opt->addOption("aircraft", setFile.toStdString());
703         } else if (m_selectedAircraft.scheme() == "package") {
704             PackageRef pkg = packageForAircraftURI(m_selectedAircraft);
705             // no need to set aircraft-dir, handled by the corresponding code
706             // in fgInitAircraft
707             opt->addOption("aircraft", pkg->qualifiedId());
708         } else {
709             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
710         }
711         
712       // manage aircraft history
713         if (m_recentAircraft.contains(m_selectedAircraft))
714           m_recentAircraft.removeOne(m_selectedAircraft);
715         m_recentAircraft.prepend(m_selectedAircraft);
716         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
717           m_recentAircraft.pop_back();
718     }
719
720     // airport / location
721     if (m_selectedAirport) {
722         opt->addOption("airport", m_selectedAirport->ident());
723     }
724
725     if (m_ui->runwayRadio->isChecked()) {
726         int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
727         if ((index >= 0) && m_selectedAirport) {
728             // explicit runway choice
729             opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident());
730         }
731
732         if (m_ui->onFinalCheckbox->isChecked()) {
733             opt->addOption("glideslope", "3.0");
734             opt->addOption("offset-distance", "10.0"); // in nautical miles
735         }
736     } else if (m_ui->parkingRadio->isChecked()) {
737         // parking selection
738         opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
739     }
740
741     // time of day
742     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
743         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
744         opt->addOption("timeofday", dayval.toStdString());
745     }
746
747     if (m_ui->seasonCombo->currentIndex() != 0) {
748         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
749         opt->addOption("season", dayval.toStdString());
750     }
751
752     QSettings settings;
753     QString downloadDir = settings.value("download-dir").toString();
754     if (!downloadDir.isEmpty()) {
755         QDir d(downloadDir);
756         if (!d.exists()) {
757             int result = QMessageBox::question(this, tr("Create download folder?"),
758                                   tr("The selected location for downloads does not exist. Create it?"),
759                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
760             if (result == QMessageBox::Cancel) {
761                 return;
762             }
763
764             if (result == QMessageBox::Yes) {
765                 d.mkpath(downloadDir);
766             }
767         }
768
769         qDebug() << "Download dir is:" << downloadDir;
770         opt->addOption("download-dir", downloadDir.toStdString());
771     }
772
773     // scenery paths
774     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
775         opt->addOption("fg-scenery", path.toStdString());
776     }
777
778     // aircraft paths
779     Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
780         // can't use fg-aircraft for this, as it is processed before the launcher is run
781         globals->append_aircraft_path(path.toStdString());
782     }
783
784     // additional arguments
785     ArgumentsTokenizer tk;
786     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
787         if (a.arg.startsWith("prop:")) {
788             QString v = a.arg.mid(5) + "=" + a.value;
789             opt->addOption("prop", v.toStdString());
790         } else {
791             opt->addOption(a.arg.toStdString(), a.value.toStdString());
792         }
793     }
794
795     saveSettings();
796 }
797
798 void QtLauncher::onQuit()
799 {
800     reject();
801 }
802
803 void QtLauncher::onSearchAirports()
804 {
805     QString search = m_ui->airportEdit->text();
806     m_airportsModel->setSearch(search);
807
808     if (m_airportsModel->isSearchActive()) {
809         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
810         m_ui->locationStack->setCurrentIndex(2);
811     } else if (m_airportsModel->rowCount(QModelIndex()) == 1) {
812         QString ident = m_airportsModel->firstIdent();
813         setAirport(FGAirport::findByIdent(ident.toStdString()));
814         m_ui->locationStack->setCurrentIndex(0);
815     }
816 }
817
818 void QtLauncher::onAirportSearchComplete()
819 {
820     int numResults = m_airportsModel->rowCount(QModelIndex());
821     if (numResults == 0) {
822         m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text()));
823     } else if (numResults == 1) {
824         QString ident = m_airportsModel->firstIdent();
825         setAirport(FGAirport::findByIdent(ident.toStdString()));
826         m_ui->locationStack->setCurrentIndex(0);
827     } else {
828         m_ui->locationStack->setCurrentIndex(1);
829     }
830 }
831
832 void QtLauncher::onAirportChanged()
833 {
834     m_ui->runwayCombo->setEnabled(m_selectedAirport);
835     m_ui->parkingCombo->setEnabled(m_selectedAirport);
836     m_ui->airportDiagram->setAirport(m_selectedAirport);
837
838     m_ui->runwayRadio->setChecked(true); // default back to runway mode
839     // unelss multiplayer is enabled ?
840
841     if (!m_selectedAirport) {
842         m_ui->airportDescription->setText(QString());
843         m_ui->airportDiagram->setEnabled(false);
844         return;
845     }
846
847     m_ui->airportDiagram->setEnabled(true);
848
849     m_ui->runwayCombo->clear();
850     m_ui->runwayCombo->addItem("Automatic", -1);
851     for (unsigned int r=0; r<m_selectedAirport->numRunways(); ++r) {
852         FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r);
853         // add runway with index as data role
854         m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
855
856         m_ui->airportDiagram->addRunway(rwy);
857     }
858
859     m_ui->parkingCombo->clear();
860     FGAirportDynamics* dynamics = m_selectedAirport->getDynamics();
861     PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(
862                                                                             m_selectedAirport->guid(),
863                                                                             FGPositioned::PARKING);
864     if (parkings.empty()) {
865         m_ui->parkingCombo->setEnabled(false);
866         m_ui->parkingRadio->setEnabled(false);
867     } else {
868         m_ui->parkingCombo->setEnabled(true);
869         m_ui->parkingRadio->setEnabled(true);
870         Q_FOREACH(PositionedID parking, parkings) {
871             FGParking* park = dynamics->getParking(parking);
872             m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
873                                         static_cast<qlonglong>(parking));
874
875             m_ui->airportDiagram->addParking(park);
876         }
877     }
878 }
879
880 void QtLauncher::onAirportDiagramClicked(FGRunwayRef rwy)
881 {
882     if (rwy) {
883         m_ui->runwayRadio->setChecked(true);
884         int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident()));
885         m_ui->runwayCombo->setCurrentIndex(rwyIndex);
886     }
887     
888     updateAirportDescription();
889 }
890
891 void QtLauncher::onToggleTerrasync(bool enabled)
892 {
893     if (enabled) {
894         QSettings settings;
895         QString downloadDir = settings.value("download-dir").toString();
896         if (downloadDir.isEmpty()) {
897             downloadDir = QString::fromStdString(flightgear::defaultDownloadDir());
898         }
899
900         QFileInfo info(downloadDir);
901         if (!info.exists()) {
902             QMessageBox msg;
903             msg.setWindowTitle(tr("Create download folder?"));
904             msg.setText(tr("The download folder '%1' does not exist, create it now? "
905                            "Click 'change location' to choose another folder "
906                            "to store downloaded files").arg(downloadDir));
907             msg.addButton(QMessageBox::Yes);
908             msg.addButton(QMessageBox::Cancel);
909             msg.addButton(tr("Change location"), QMessageBox::ActionRole);
910             int result = msg.exec();
911   
912             if (result == QMessageBox::Cancel) {
913                 m_ui->terrasyncCheck->setChecked(false);
914                 return;
915             }
916
917             if (result == QMessageBox::ActionRole) {
918                 onEditPaths();
919                 return;
920             }
921
922             QDir d(downloadDir);
923             d.mkpath(downloadDir);
924         }
925     } // of is enabled
926 }
927
928 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
929 {
930     qDebug() << Q_FUNC_INFO;
931     QUrl u = index.data(AircraftURIRole).toUrl();
932     if (u == m_selectedAircraft) {
933         // potentially enable the run button now!
934         updateSelectedAircraft();
935         qDebug() << "updating selected aircraft" << index.data();
936     }
937 }
938
939 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
940 {
941     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
942     
943     QMessageBox msg;
944     msg.setWindowTitle(tr("Aircraft insallation failed"));
945     msg.setText(tr("An error occurred installing the aircraft %1: %2").
946                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
947     msg.addButton(QMessageBox::Ok);
948     msg.exec();
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     qDebug() << "request install of" << pkg;
1001     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1002     pref->install();
1003 }
1004
1005 void QtLauncher::onCancelDownload(const QModelIndex& index)
1006 {
1007     QString pkg = index.data(AircraftPackageIdRole).toString();
1008     qDebug() << "cancel download of" << pkg;
1009     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1010     simgear::pkg::InstallRef i = pref->existingInstall();
1011     i->cancelDownload();
1012 }
1013
1014 void QtLauncher::updateSelectedAircraft()
1015 {
1016     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1017     if (index.isValid()) {
1018         QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
1019         m_ui->thumbnail->setPixmap(pm);
1020         m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
1021         
1022         int status = index.data(AircraftPackageStatusRole).toInt();
1023         bool canRun = (status == PackageInstalled);
1024         m_ui->runButton->setEnabled(canRun);
1025     } else {
1026         m_ui->thumbnail->setPixmap(QPixmap());
1027         m_ui->aircraftDescription->setText("");
1028         m_ui->runButton->setEnabled(false);
1029     }
1030 }
1031
1032 void QtLauncher::onPopupAirportHistory()
1033 {
1034     if (m_recentAirports.isEmpty()) {
1035         return;
1036     }
1037
1038     QMenu m;
1039     Q_FOREACH(QString aptCode, m_recentAirports) {
1040         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
1041         QString name = QString::fromStdString(apt->name());
1042         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
1043         act->setData(aptCode);
1044     }
1045
1046     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
1047     QAction* triggered = m.exec(popupPos);
1048     if (triggered) {
1049         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
1050         setAirport(apt);
1051         m_ui->airportEdit->clear();
1052         m_ui->locationStack->setCurrentIndex(0);
1053     }
1054 }
1055
1056 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
1057 {
1058   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
1059 }
1060
1061 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
1062 {
1063     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1064     Q_ASSERT(sourceModel);
1065     return sourceModel->indexOfAircraftURI(uri);
1066 }
1067
1068 void QtLauncher::onPopupAircraftHistory()
1069 {
1070     if (m_recentAircraft.isEmpty()) {
1071         return;
1072     }
1073
1074     QMenu m;
1075     Q_FOREACH(QUrl uri, m_recentAircraft) {
1076         QModelIndex index = sourceIndexForAircraftURI(uri);
1077         if (!index.isValid()) {
1078             // not scanned yet
1079             continue;
1080         }
1081         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1082         act->setData(uri);
1083     }
1084
1085     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1086     QAction* triggered = m.exec(popupPos);
1087     if (triggered) {
1088         m_selectedAircraft = triggered->data().toUrl();
1089         QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
1090         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1091                                                               QItemSelectionModel::ClearAndSelect);
1092         m_ui->aircraftFilter->clear();
1093         updateSelectedAircraft();
1094     }
1095 }
1096
1097 void QtLauncher::setAirport(FGAirportRef ref)
1098 {
1099     if (m_selectedAirport == ref)
1100         return;
1101
1102     m_selectedAirport = ref;
1103     onAirportChanged();
1104
1105     if (ref.valid()) {
1106         // maintain the recent airport list
1107         QString icao = QString::fromStdString(ref->ident());
1108         if (m_recentAirports.contains(icao)) {
1109             // move to front
1110             m_recentAirports.removeOne(icao);
1111             m_recentAirports.push_front(icao);
1112         } else {
1113             // insert and trim list if necessary
1114             m_recentAirports.push_front(icao);
1115             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
1116                 m_recentAirports.pop_back();
1117             }
1118         }
1119     }
1120
1121     updateAirportDescription();
1122 }
1123
1124 void QtLauncher::onEditRatingsFilter()
1125 {
1126     EditRatingsFilterDialog dialog(this);
1127     dialog.setRatings(m_ratingFilters);
1128
1129     dialog.exec();
1130     if (dialog.result() == QDialog::Accepted) {
1131         QVariantList vl;
1132         for (int i=0; i<4; ++i) {
1133             m_ratingFilters[i] = dialog.getRating(i);
1134             vl.append(m_ratingFilters[i]);
1135         }
1136         m_aircraftProxy->setRatings(m_ratingFilters);
1137
1138         QSettings settings;
1139         settings.setValue("min-ratings", vl);
1140     }
1141 }
1142
1143 void QtLauncher::updateSettingsSummary()
1144 {
1145     QStringList summary;
1146     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1147         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1148     }
1149
1150     if (m_ui->seasonCombo->currentIndex() > 0) {
1151         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1152     }
1153
1154     if (m_ui->rembrandtCheckbox->isChecked()) {
1155         summary.append("Rembrandt enabled");
1156     } else if (m_ui->msaaCheckbox->isChecked()) {
1157         summary.append("anti-aliasing");
1158     }
1159
1160     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1161         summary.append("live weather");
1162     }
1163
1164     if (m_ui->terrasyncCheck->isChecked()) {
1165         summary.append("automatic scenery downloads");
1166     }
1167
1168     if (m_ui->startPausedCheck->isChecked()) {
1169         summary.append("paused");
1170     }
1171
1172     QString s = summary.join(", ");
1173     s[0] = s[0].toUpper();
1174     m_ui->settingsDescription->setText(s);
1175 }
1176
1177 void QtLauncher::onRembrandtToggled(bool b)
1178 {
1179     // Rembrandt and multi-sample are exclusive
1180     m_ui->msaaCheckbox->setEnabled(!b);
1181 }
1182
1183 void QtLauncher::onSubsytemIdleTimeout()
1184 {
1185     globals->get_subsystem_mgr()->update(0.0);
1186 }
1187
1188 void QtLauncher::onEditPaths()
1189 {
1190     PathsDialog dlg(this, globals->packageRoot());
1191     dlg.exec();
1192     if (dlg.result() == QDialog::Accepted) {
1193         // re-scan the aircraft list
1194         QSettings settings;
1195         m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1196         m_aircraftModel->scanDirs();
1197     }
1198 }
1199
1200 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1201 {
1202     if (uri.scheme() != "package") {
1203         qWarning() << "invalid URL scheme:" << uri;
1204         return simgear::pkg::PackageRef();
1205     }
1206     
1207     QString ident = uri.path();
1208     qDebug() << Q_FUNC_INFO << uri << ident;
1209     return globals->packageRoot()->getPackageById(ident.toStdString());
1210 }
1211
1212 #include "QtLauncher.moc"
1213