]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Refactor aircraft helper classes
[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/misc/sg_path.hxx>
50
51 #include "ui_Launcher.h"
52 #include "EditRatingsFilterDialog.hxx"
53 #include "AircraftItemDelegate.hxx"
54 #include "AircraftModel.hxx"
55
56 #include <Main/globals.hxx>
57 #include <Navaids/NavDataCache.hxx>
58 #include <Airports/airport.hxx>
59 #include <Airports/dynamics.hxx> // for parking
60 #include <Main/options.hxx>
61 #include <Viewer/WindowBuilder.hxx>
62
63 using namespace flightgear;
64
65 const int MAX_RECENT_AIRPORTS = 32;
66 const int MAX_RECENT_AIRCRAFT = 20;
67
68 namespace { // anonymous namespace
69
70 void initNavCache()
71 {
72     NavDataCache* cache = NavDataCache::createInstance();
73     if (cache->isRebuildRequired()) {
74         QProgressDialog rebuildProgress("Initialising navigation data, this may take several minutes",
75                                        QString() /* cancel text */,
76                                        0, 0);
77         rebuildProgress.setWindowModality(Qt::WindowModal);
78         rebuildProgress.show();
79
80         while (!cache->rebuild()) {
81             // sleep to give the rebuild thread more time
82             SGTimeStamp::sleepForMSec(50);
83             rebuildProgress.setValue(0);
84             QCoreApplication::processEvents();
85         }
86     }
87 }
88
89 class ArgumentsTokenizer
90 {
91 public:
92     class Arg
93     {
94     public:
95         explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
96
97         QString arg;
98         QString value;
99     };
100
101     QList<Arg> tokenize(QString in) const
102     {
103         int index = 0;
104         const int len = in.count();
105         QChar c, nc;
106         State state = Start;
107         QString key, value;
108         QList<Arg> result;
109
110         for (; index < len; ++index) {
111             c = in.at(index);
112             nc = index < (len - 1) ? in.at(index + 1) : QChar();
113
114             switch (state) {
115             case Start:
116                 if (c == QChar('-')) {
117                     if (nc == QChar('-')) {
118                         state = Key;
119                         key.clear();
120                         ++index;
121                     } else {
122                         // should we pemit single hyphen arguments?
123                         // choosing to fail for now
124                         return QList<Arg>();
125                     }
126                 } else if (c.isSpace()) {
127                     break;
128                 }
129                 break;
130
131             case Key:
132                 if (c == QChar('=')) {
133                     state = Value;
134                     value.clear();
135                 } else if (c.isSpace()) {
136                     state = Start;
137                     result.append(Arg(key));
138                 } else {
139                     // could check for illegal charatcers here
140                     key.append(c);
141                 }
142                 break;
143
144             case Value:
145                 if (c == QChar('"')) {
146                     state = Quoted;
147                 } else if (c.isSpace()) {
148                     state = Start;
149                     result.append(Arg(key, value));
150                 } else {
151                     value.append(c);
152                 }
153                 break;
154
155             case Quoted:
156                 if (c == QChar('\\')) {
157                     // check for escaped double-quote inside quoted value
158                     if (nc == QChar('"')) {
159                         ++index;
160                     }
161                 } else if (c == QChar('"')) {
162                     state = Value;
163                 } else {
164                     value.append(c);
165                 }
166                 break;
167             } // of state switch
168         } // of character loop
169
170         // ensure last argument isn't lost
171         if (state == Key) {
172             result.append(Arg(key));
173         } else if (state == Value) {
174             result.append(Arg(key, value));
175         }
176
177         return result;
178     }
179
180 private:
181     enum State {
182         Start = 0,
183         Key,
184         Value,
185         Quoted
186     };
187 };
188
189 } // of anonymous namespace
190
191 class AirportSearchModel : public QAbstractListModel
192 {
193     Q_OBJECT
194 public:
195     AirportSearchModel() :
196         m_searchActive(false)
197     {
198     }
199
200     void setSearch(QString t)
201     {
202         beginResetModel();
203
204         m_airports.clear();
205         m_ids.clear();
206
207         std::string term(t.toUpper().toStdString());
208         // try ICAO lookup first
209         FGAirportRef ref = FGAirport::findByIdent(term);
210         if (ref) {
211             m_ids.push_back(ref->guid());
212             m_airports.push_back(ref);
213         } else {
214             m_search.reset(new NavDataCache::ThreadedAirportSearch(term));
215             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
216             m_searchActive = true;
217         }
218
219         endResetModel();
220     }
221
222     bool isSearchActive() const
223     {
224         return m_searchActive;
225     }
226
227     virtual int rowCount(const QModelIndex&) const
228     {
229         // if empty, return 1 for special 'no matches'?
230         return m_ids.size();
231     }
232
233     virtual QVariant data(const QModelIndex& index, int role) const
234     {
235         if (!index.isValid())
236             return QVariant();
237         
238         FGAirportRef apt = m_airports[index.row()];
239         if (!apt.valid()) {
240             apt = FGPositioned::loadById<FGAirport>(m_ids[index.row()]);
241             m_airports[index.row()] = apt;
242         }
243
244         if (role == Qt::DisplayRole) {
245             QString name = QString::fromStdString(apt->name());
246             return QString("%1: %2").arg(QString::fromStdString(apt->ident())).arg(name);
247         }
248
249         if (role == Qt::EditRole) {
250             return QString::fromStdString(apt->ident());
251         }
252
253         if (role == Qt::UserRole) {
254             return static_cast<qlonglong>(m_ids[index.row()]);
255         }
256
257         return QVariant();
258     }
259
260     QString firstIdent() const
261     {
262         if (m_ids.empty())
263             return QString();
264
265         if (!m_airports.front().valid()) {
266             m_airports[0] = FGPositioned::loadById<FGAirport>(m_ids.front());
267         }
268
269         return QString::fromStdString(m_airports.front()->ident());
270     }
271
272 Q_SIGNALS:
273     void searchComplete();
274
275 private slots:
276     void onSearchResultsPoll()
277     {
278         PositionedIDVec newIds = m_search->results();
279         
280         beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
281         for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
282             m_ids.push_back(newIds[i]);
283             m_airports.push_back(FGAirportRef()); // null ref
284         }
285         endInsertRows();
286
287         if (m_search->isComplete()) {
288             m_searchActive = false;
289             m_search.reset();
290             emit searchComplete();
291         } else {
292             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
293         }
294     }
295
296 private:
297     PositionedIDVec m_ids;
298     mutable std::vector<FGAirportRef> m_airports;
299     bool m_searchActive;
300     QScopedPointer<NavDataCache::ThreadedAirportSearch> m_search;
301 };
302
303 class AircraftProxyModel : public QSortFilterProxyModel
304 {
305     Q_OBJECT
306 public:
307     AircraftProxyModel(QObject* pr) :
308         QSortFilterProxyModel(pr),
309         m_ratingsFilter(true)
310     {
311         for (int i=0; i<4; ++i) {
312             m_ratings[i] = 3;
313         }
314     }
315
316     void setRatings(int* ratings)
317     {
318         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
319         invalidate();
320     }
321
322 public slots:
323     void setRatingFilterEnabled(bool e)
324     {
325         if (e == m_ratingsFilter) {
326             return;
327         }
328
329         m_ratingsFilter = e;
330         invalidate();
331     }
332
333 protected:
334     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
335     {
336         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
337             return false;
338         }
339
340         if (m_ratingsFilter) {
341             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
342             for (int i=0; i<4; ++i) {
343                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
344                     return false;
345                 }
346             }
347         }
348
349         return true;
350     }
351
352 private:
353     bool m_ratingsFilter;
354     int m_ratings[4];
355 };
356
357 QtLauncher::QtLauncher() :
358     QDialog(),
359     m_ui(NULL)
360 {
361     m_ui.reset(new Ui::Launcher);
362     m_ui->setupUi(this);
363
364 #if QT_VERSION >= 0x050300
365     // don't require Qt 5.3
366     m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
367 #endif
368
369 #if QT_VERSION >= 0x050200
370     m_ui->aircraftFilter->setClearButtonEnabled(true);
371 #endif
372
373     for (int i=0; i<4; ++i) {
374         m_ratingFilters[i] = 3;
375     }
376
377     m_airportsModel = new AirportSearchModel;
378     m_ui->searchList->setModel(m_airportsModel);
379     connect(m_ui->searchList, &QListView::clicked,
380             this, &QtLauncher::onAirportChoiceSelected);
381     connect(m_airportsModel, &AirportSearchModel::searchComplete,
382             this, &QtLauncher::onAirportSearchComplete);
383
384     SGPath p = SGPath::documents();
385     p.append("FlightGear");
386     p.append("Aircraft");
387     m_customAircraftDir = QString::fromStdString(p.str());
388     m_ui->customAircraftDirLabel->setText(QString("Custom aircraft folder: %1").arg(m_customAircraftDir));
389
390     globals->append_aircraft_path(m_customAircraftDir.toStdString());
391
392     // create and configure the proxy model
393     m_aircraftProxy = new AircraftProxyModel(this);
394     m_aircraftProxy->setSourceModel(new AircraftItemModel(this));
395
396     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
397     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
398     m_aircraftProxy->setSortRole(Qt::DisplayRole);
399     m_aircraftProxy->setDynamicSortFilter(true);
400
401     m_ui->aircraftList->setModel(m_aircraftProxy);
402     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
403     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
404     m_ui->aircraftList->setItemDelegate(delegate);
405     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
406     connect(m_ui->aircraftList, &QListView::clicked,
407             this, &QtLauncher::onAircraftSelected);
408     connect(delegate, &AircraftItemDelegate::variantChanged,
409             this, &QtLauncher::onAircraftSelected);
410
411     connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
412             this, SLOT(updateAirportDescription()));
413     connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
414             this, SLOT(updateAirportDescription()));
415     connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
416             this, SLOT(updateAirportDescription()));
417     connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
418             this, SLOT(updateAirportDescription()));
419     connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
420             this, SLOT(updateAirportDescription()));
421
422
423     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
424     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
425     connect(m_ui->airportEdit, SIGNAL(returnPressed()),
426             this, SLOT(onSearchAirports()));
427
428     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
429             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
430
431     connect(m_ui->airportHistory, &QPushButton::clicked,
432             this, &QtLauncher::onPopupAirportHistory);
433     connect(m_ui->aircraftHistory, &QPushButton::clicked,
434           this, &QtLauncher::onPopupAircraftHistory);
435
436     restoreSettings();
437
438     connect(m_ui->openAircraftDirButton, &QPushButton::clicked,
439           this, &QtLauncher::onOpenCustomAircraftDir);
440
441     QAction* qa = new QAction(this);
442     qa->setShortcut(QKeySequence("Ctrl+Q"));
443     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
444     addAction(qa);
445
446     connect(m_ui->editRatingFilter, &QPushButton::clicked,
447             this, &QtLauncher::onEditRatingsFilter);
448     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
449             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
450
451     QIcon historyIcon(":/history-icon");
452     m_ui->aircraftHistory->setIcon(historyIcon);
453     m_ui->airportHistory->setIcon(historyIcon);
454
455     m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
456
457     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
458             this, SLOT(updateSettingsSummary()));
459     connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
460             this, SLOT(updateSettingsSummary()));
461     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
462             this, SLOT(updateSettingsSummary()));
463     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
464             this, SLOT(updateSettingsSummary()));
465     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
466             this, SLOT(updateSettingsSummary()));
467     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
468             this, SLOT(updateSettingsSummary()));
469     connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
470             this, SLOT(updateSettingsSummary()));
471
472     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
473             this, SLOT(onRembrandtToggled(bool)));
474
475     updateSettingsSummary();
476
477     connect(m_ui->addSceneryPath, &QToolButton::clicked,
478             this, &QtLauncher::onAddSceneryPath);
479     connect(m_ui->removeSceneryPath, &QToolButton::clicked,
480             this, &QtLauncher::onRemoveSceneryPath);
481 }
482
483 QtLauncher::~QtLauncher()
484 {
485     
486 }
487
488 void QtLauncher::initApp(int argc, char** argv)
489 {
490     static bool qtInitDone = false;
491     if (!qtInitDone) {
492         qtInitDone = true;
493
494         QApplication* app = new QApplication(argc, argv);
495         app->setOrganizationName("FlightGear");
496         app->setApplicationName("FlightGear");
497         app->setOrganizationDomain("flightgear.org");
498
499         // avoid double Apple menu and other weirdness if both Qt and OSG
500         // try to initialise various Cocoa structures.
501         flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
502
503         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
504         if (mods & Qt::AltModifier) {
505             qWarning() << "Alt pressed during launch";
506
507             // wipe out our settings
508             QSettings settings;
509             settings.clear();
510
511
512             Options::sharedInstance()->addOption("restore-defaults", "");
513         }
514     }
515 }
516
517 bool QtLauncher::runLauncherDialog()
518 {
519     Q_INIT_RESOURCE(resources);
520
521     // startup the nav-cache now. This pre-empts normal startup of
522     // the cache, but no harm done. (Providing scenery paths are consistent)
523
524     initNavCache();
525
526   // setup scenery paths now, especially TerraSync path for airport
527   // parking locations (after they're downloaded)
528
529     QtLauncher dlg;
530     dlg.exec();
531     if (dlg.result() != QDialog::Accepted) {
532         return false;
533     }
534
535     return true;
536 }
537
538 void QtLauncher::restoreSettings()
539 {
540     QSettings settings;
541     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
542     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
543     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
544     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
545     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
546     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
547     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
548     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
549
550     // full paths to -set.xml files
551     m_recentAircraft = settings.value("recent-aircraft").toStringList();
552
553     if (!m_recentAircraft.empty()) {
554         m_selectedAircraft = m_recentAircraft.front();
555     } else {
556         // select the default C172p
557     }
558
559     updateSelectedAircraft();
560
561     // ICAO identifiers
562     m_recentAirports = settings.value("recent-airports").toStringList();
563     if (!m_recentAirports.empty()) {
564         setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString()));
565     }
566     updateAirportDescription();
567
568     // rating filters
569     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
570     int index = 0;
571     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
572         m_ratingFilters[index++] = v.toInt();
573     }
574
575     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
576     m_aircraftProxy->setRatings(m_ratingFilters);
577
578     QStringList sceneryPaths = settings.value("scenery-paths").toStringList();
579     m_ui->sceneryPathsList->addItems(sceneryPaths);
580
581     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
582 }
583
584 void QtLauncher::saveSettings()
585 {
586     QSettings settings;
587     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
588     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
589     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
590     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
591     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
592     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
593     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
594     settings.setValue("recent-aircraft", m_recentAircraft);
595     settings.setValue("recent-airports", m_recentAirports);
596     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
597     settings.setValue("season", m_ui->seasonCombo->currentIndex());
598
599     QStringList paths;
600     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
601         paths.append(m_ui->sceneryPathsList->item(i)->text());
602     }
603
604     settings.setValue("scenery-paths", paths);
605     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
606 }
607
608 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
609 {
610     flightgear::Options* opt = flightgear::Options::sharedInstance();
611     std::string stdName(name.toStdString());
612     if (cbox->isChecked()) {
613         opt->addOption("enable-" + stdName, "");
614     } else {
615         opt->addOption("disable-" + stdName, "");
616     }
617 }
618
619 void QtLauncher::onRun()
620 {
621     accept();
622
623     flightgear::Options* opt = flightgear::Options::sharedInstance();
624     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
625     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
626     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
627     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
628     setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
629
630     // MSAA is more complex
631     if (!m_ui->rembrandtCheckbox->isChecked()) {
632         if (m_ui->msaaCheckbox->isChecked()) {
633             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
634             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
635         } else {
636             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
637         }
638     }
639
640     // aircraft
641     if (!m_selectedAircraft.isEmpty()) {
642         QFileInfo setFileInfo(m_selectedAircraft);
643         opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
644         QString setFile = setFileInfo.fileName();
645         Q_ASSERT(setFile.endsWith("-set.xml"));
646         setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
647         opt->addOption("aircraft", setFile.toStdString());
648
649       // manage aircraft history
650         if (m_recentAircraft.contains(m_selectedAircraft))
651           m_recentAircraft.removeOne(m_selectedAircraft);
652         m_recentAircraft.prepend(m_selectedAircraft);
653         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
654           m_recentAircraft.pop_back();
655     }
656
657     // airport / location
658     if (m_selectedAirport) {
659         opt->addOption("airport", m_selectedAirport->ident());
660     }
661
662     if (m_ui->runwayRadio->isChecked()) {
663         int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
664         if ((index >= 0) && m_selectedAirport) {
665             // explicit runway choice
666             opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident());
667         }
668
669         if (m_ui->onFinalCheckbox->isChecked()) {
670             opt->addOption("glideslope", "3.0");
671             opt->addOption("offset-distance", "10.0"); // in nautical miles
672         }
673     } else if (m_ui->parkingRadio->isChecked()) {
674         // parking selection
675         opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
676     }
677
678     // time of day
679     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
680         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
681         opt->addOption("timeofday", dayval.toStdString());
682     }
683
684     if (m_ui->seasonCombo->currentIndex() != 0) {
685         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
686         opt->addOption("season", dayval.toStdString());
687     }
688
689     // scenery paths
690     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
691         QString path = m_ui->sceneryPathsList->item(i)->text();
692         opt->addOption("fg-scenery", path.toStdString());
693     }
694
695     // additional arguments
696     ArgumentsTokenizer tk;
697     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
698         if (a.arg.startsWith("prop:")) {
699             QString v = a.arg.mid(5) + "=" + a.value;
700             opt->addOption("prop", v.toStdString());
701         } else {
702             opt->addOption(a.arg.toStdString(), a.value.toStdString());
703         }
704     }
705
706     saveSettings();
707 }
708
709 void QtLauncher::onQuit()
710 {
711     reject();
712 }
713
714 void QtLauncher::onSearchAirports()
715 {
716     QString search = m_ui->airportEdit->text();
717     m_airportsModel->setSearch(search);
718
719     if (m_airportsModel->isSearchActive()) {
720         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
721         m_ui->locationStack->setCurrentIndex(2);
722     } else if (m_airportsModel->rowCount(QModelIndex()) == 1) {
723         QString ident = m_airportsModel->firstIdent();
724         setAirport(FGAirport::findByIdent(ident.toStdString()));
725         m_ui->locationStack->setCurrentIndex(0);
726     }
727 }
728
729 void QtLauncher::onAirportSearchComplete()
730 {
731     int numResults = m_airportsModel->rowCount(QModelIndex());
732     if (numResults == 0) {
733         m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text()));
734     } else if (numResults == 1) {
735         QString ident = m_airportsModel->firstIdent();
736         setAirport(FGAirport::findByIdent(ident.toStdString()));
737         m_ui->locationStack->setCurrentIndex(0);
738     } else {
739         m_ui->locationStack->setCurrentIndex(1);
740     }
741 }
742
743 void QtLauncher::onAirportChanged()
744 {
745     m_ui->runwayCombo->setEnabled(m_selectedAirport);
746     m_ui->parkingCombo->setEnabled(m_selectedAirport);
747     m_ui->airportDiagram->setAirport(m_selectedAirport);
748
749     m_ui->runwayRadio->setChecked(true); // default back to runway mode
750     // unelss multiplayer is enabled ?
751
752     if (!m_selectedAirport) {
753         m_ui->airportDescription->setText(QString());
754         m_ui->airportDiagram->setEnabled(false);
755         return;
756     }
757
758     m_ui->airportDiagram->setEnabled(true);
759
760     m_ui->runwayCombo->clear();
761     m_ui->runwayCombo->addItem("Automatic", -1);
762     for (unsigned int r=0; r<m_selectedAirport->numRunways(); ++r) {
763         FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r);
764         // add runway with index as data role
765         m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
766
767         m_ui->airportDiagram->addRunway(rwy);
768     }
769
770     m_ui->parkingCombo->clear();
771     FGAirportDynamics* dynamics = m_selectedAirport->getDynamics();
772     PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(
773                                                                             m_selectedAirport->guid(),
774                                                                             FGPositioned::PARKING);
775     if (parkings.empty()) {
776         m_ui->parkingCombo->setEnabled(false);
777         m_ui->parkingRadio->setEnabled(false);
778     } else {
779         m_ui->parkingCombo->setEnabled(true);
780         m_ui->parkingRadio->setEnabled(true);
781         Q_FOREACH(PositionedID parking, parkings) {
782             FGParking* park = dynamics->getParking(parking);
783             m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
784                                         static_cast<qlonglong>(parking));
785
786             m_ui->airportDiagram->addParking(park);
787         }
788     }
789 }
790
791 void QtLauncher::updateAirportDescription()
792 {
793     if (!m_selectedAirport) {
794         m_ui->airportDescription->setText(QString("No airport selected"));
795         return;
796     }
797
798     QString ident = QString::fromStdString(m_selectedAirport->ident()),
799         name = QString::fromStdString(m_selectedAirport->name());
800     QString locationOnAirport;
801     if (m_ui->runwayRadio->isChecked()) {
802         bool onFinal = m_ui->onFinalCheckbox->isChecked();
803         QString runwayName = (m_ui->runwayCombo->currentIndex() == 0) ?
804             "active runway" :
805             QString("runway %1").arg(m_ui->runwayCombo->currentText());
806
807         if (onFinal) {
808             locationOnAirport = QString("on 10-mile final to %1").arg(runwayName);
809         } else {
810             locationOnAirport = QString("on %1").arg(runwayName);
811         }
812     } else if (m_ui->parkingRadio->isChecked()) {
813         locationOnAirport =  QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
814     }
815
816     m_ui->airportDescription->setText(QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport));
817 }
818
819 void QtLauncher::onAirportChoiceSelected(const QModelIndex& index)
820 {
821     m_ui->locationStack->setCurrentIndex(0);
822     setAirport(FGPositioned::loadById<FGAirport>(index.data(Qt::UserRole).toULongLong()));
823 }
824
825 void QtLauncher::onAircraftSelected(const QModelIndex& index)
826 {
827     m_selectedAircraft = index.data(AircraftPathRole).toString();
828     updateSelectedAircraft();
829 }
830
831 void QtLauncher::updateSelectedAircraft()
832 {
833     try {
834         QFileInfo info(m_selectedAircraft);
835         AircraftItem item(info.dir(), m_selectedAircraft);
836         m_ui->thumbnail->setPixmap(item.thumbnail());
837         m_ui->aircraftDescription->setText(item.description);
838     } catch (sg_exception& e) {
839         m_ui->thumbnail->setPixmap(QPixmap());
840         m_ui->aircraftDescription->setText("");
841     }
842 }
843
844 void QtLauncher::onPopupAirportHistory()
845 {
846     if (m_recentAirports.isEmpty()) {
847         return;
848     }
849
850     QMenu m;
851     Q_FOREACH(QString aptCode, m_recentAirports) {
852         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
853         QString name = QString::fromStdString(apt->name());
854         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
855         act->setData(aptCode);
856     }
857
858     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
859     QAction* triggered = m.exec(popupPos);
860     if (triggered) {
861         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
862         setAirport(apt);
863         m_ui->airportEdit->clear();
864         m_ui->locationStack->setCurrentIndex(0);
865     }
866 }
867
868 QModelIndex QtLauncher::proxyIndexForAircraftPath(QString path) const
869 {
870   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftPath(path));
871 }
872
873 QModelIndex QtLauncher::sourceIndexForAircraftPath(QString path) const
874 {
875     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
876     Q_ASSERT(sourceModel);
877     return sourceModel->indexOfAircraftPath(path);
878 }
879
880 void QtLauncher::onPopupAircraftHistory()
881 {
882     if (m_recentAircraft.isEmpty()) {
883         return;
884     }
885
886     QMenu m;
887     Q_FOREACH(QString path, m_recentAircraft) {
888         QModelIndex index = sourceIndexForAircraftPath(path);
889         if (!index.isValid()) {
890             // not scanned yet
891             continue;
892         }
893         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
894         act->setData(path);
895     }
896
897     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
898     QAction* triggered = m.exec(popupPos);
899     if (triggered) {
900         m_selectedAircraft = triggered->data().toString();
901         QModelIndex index = proxyIndexForAircraftPath(m_selectedAircraft);
902         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
903                                                               QItemSelectionModel::ClearAndSelect);
904         m_ui->aircraftFilter->clear();
905         updateSelectedAircraft();
906     }
907 }
908
909 void QtLauncher::setAirport(FGAirportRef ref)
910 {
911     if (m_selectedAirport == ref)
912         return;
913
914     m_selectedAirport = ref;
915     onAirportChanged();
916
917     if (ref.valid()) {
918         // maintain the recent airport list
919         QString icao = QString::fromStdString(ref->ident());
920         if (m_recentAirports.contains(icao)) {
921             // move to front
922             m_recentAirports.removeOne(icao);
923             m_recentAirports.push_front(icao);
924         } else {
925             // insert and trim list if necessary
926             m_recentAirports.push_front(icao);
927             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
928                 m_recentAirports.pop_back();
929             }
930         }
931     }
932
933     updateAirportDescription();
934 }
935
936 void QtLauncher::onOpenCustomAircraftDir()
937 {
938     QFileInfo info(m_customAircraftDir);
939     if (!info.exists()) {
940         int result = QMessageBox::question(this, "Create folder?",
941                                            "The custom aircraft folder does not exist, create it now?",
942                                            QMessageBox::Yes | QMessageBox::No,
943                                            QMessageBox::Yes);
944         if (result == QMessageBox::No) {
945             return;
946         }
947
948         QDir d(m_customAircraftDir);
949         d.mkpath(m_customAircraftDir);
950     }
951
952   QUrl u = QUrl::fromLocalFile(m_customAircraftDir);
953   QDesktopServices::openUrl(u);
954 }
955
956 void QtLauncher::onEditRatingsFilter()
957 {
958     EditRatingsFilterDialog dialog(this);
959     dialog.setRatings(m_ratingFilters);
960
961     dialog.exec();
962     if (dialog.result() == QDialog::Accepted) {
963         QVariantList vl;
964         for (int i=0; i<4; ++i) {
965             m_ratingFilters[i] = dialog.getRating(i);
966             vl.append(m_ratingFilters[i]);
967         }
968         m_aircraftProxy->setRatings(m_ratingFilters);
969
970         QSettings settings;
971         settings.setValue("min-ratings", vl);
972     }
973 }
974
975 void QtLauncher::updateSettingsSummary()
976 {
977     QStringList summary;
978     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
979         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
980     }
981
982     if (m_ui->seasonCombo->currentIndex() > 0) {
983         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
984     }
985
986     if (m_ui->rembrandtCheckbox->isChecked()) {
987         summary.append("Rembrandt enabled");
988     } else if (m_ui->msaaCheckbox->isChecked()) {
989         summary.append("anti-aliasing");
990     }
991
992     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
993         summary.append("live weather");
994     }
995
996     if (m_ui->terrasyncCheck->isChecked()) {
997         summary.append("automatic scenery downloads");
998     }
999
1000     if (m_ui->startPausedCheck->isChecked()) {
1001         summary.append("paused");
1002     }
1003
1004     QString s = summary.join(", ");
1005     s[0] = s[0].toUpper();
1006     m_ui->settingsDescription->setText(s);
1007 }
1008
1009 void QtLauncher::onAddSceneryPath()
1010 {
1011     QString path = QFileDialog::getExistingDirectory(this, tr("Choose scenery folder"));
1012     if (!path.isEmpty()) {
1013         m_ui->sceneryPathsList->addItem(path);
1014         saveSettings();
1015     }
1016 }
1017
1018 void QtLauncher::onRemoveSceneryPath()
1019 {
1020     if (m_ui->sceneryPathsList->currentItem()) {
1021         delete m_ui->sceneryPathsList->currentItem();
1022         saveSettings();
1023     }
1024 }
1025
1026 void QtLauncher::onRembrandtToggled(bool b)
1027 {
1028     // Rembrandt and multi-sample are exclusive
1029     m_ui->msaaCheckbox->setEnabled(!b);
1030 }
1031
1032 #include "QtLauncher.moc"
1033