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