]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Fix a clang unused constant warning
[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_aircraftModel = new AircraftItemModel(this, r);
415     m_aircraftProxy->setSourceModel(m_aircraftModel);
416
417     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
418     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
419     m_aircraftProxy->setSortRole(Qt::DisplayRole);
420     m_aircraftProxy->setDynamicSortFilter(true);
421
422     m_ui->aircraftList->setModel(m_aircraftProxy);
423     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
424     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
425     m_ui->aircraftList->setItemDelegate(delegate);
426     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
427     connect(m_ui->aircraftList, &QListView::clicked,
428             this, &QtLauncher::onAircraftSelected);
429     connect(delegate, &AircraftItemDelegate::variantChanged,
430             this, &QtLauncher::onAircraftSelected);
431
432     connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
433             this, SLOT(updateAirportDescription()));
434     connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
435             this, SLOT(updateAirportDescription()));
436     connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
437             this, SLOT(updateAirportDescription()));
438     connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
439             this, SLOT(updateAirportDescription()));
440     connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
441             this, SLOT(updateAirportDescription()));
442
443
444     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
445     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
446     connect(m_ui->airportEdit, SIGNAL(returnPressed()),
447             this, SLOT(onSearchAirports()));
448
449     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
450             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
451
452     connect(m_ui->airportHistory, &QPushButton::clicked,
453             this, &QtLauncher::onPopupAirportHistory);
454     connect(m_ui->aircraftHistory, &QPushButton::clicked,
455           this, &QtLauncher::onPopupAircraftHistory);
456
457     restoreSettings();
458
459     connect(m_ui->openAircraftDirButton, &QPushButton::clicked,
460           this, &QtLauncher::onOpenCustomAircraftDir);
461
462     QAction* qa = new QAction(this);
463     qa->setShortcut(QKeySequence("Ctrl+Q"));
464     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
465     addAction(qa);
466
467     connect(m_ui->editRatingFilter, &QPushButton::clicked,
468             this, &QtLauncher::onEditRatingsFilter);
469     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
470             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
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
496     updateSettingsSummary();
497
498     connect(m_ui->addSceneryPath, &QToolButton::clicked,
499             this, &QtLauncher::onAddSceneryPath);
500     connect(m_ui->removeSceneryPath, &QToolButton::clicked,
501             this, &QtLauncher::onRemoveSceneryPath);
502
503     connect(m_ui->addAircraftPath, &QToolButton::clicked,
504             this, &QtLauncher::onAddAircraftPath);
505     connect(m_ui->removeAircraftPath, &QToolButton::clicked,
506             this, &QtLauncher::onRemoveAircraftPath);
507
508     QSettings settings;
509     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
510     m_aircraftModel->scanDirs();
511 }
512
513 QtLauncher::~QtLauncher()
514 {
515     
516 }
517
518 void QtLauncher::initApp(int argc, char** argv)
519 {
520     static bool qtInitDone = false;
521     if (!qtInitDone) {
522         qtInitDone = true;
523
524         QApplication* app = new QApplication(argc, argv);
525         app->setOrganizationName("FlightGear");
526         app->setApplicationName("FlightGear");
527         app->setOrganizationDomain("flightgear.org");
528
529         // avoid double Apple menu and other weirdness if both Qt and OSG
530         // try to initialise various Cocoa structures.
531         flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
532
533         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
534         if (mods & Qt::AltModifier) {
535             qWarning() << "Alt pressed during launch";
536
537             // wipe out our settings
538             QSettings settings;
539             settings.clear();
540
541
542             Options::sharedInstance()->addOption("restore-defaults", "");
543         }
544     }
545 }
546
547 bool QtLauncher::runLauncherDialog()
548 {
549     Q_INIT_RESOURCE(resources);
550
551     // startup the nav-cache now. This pre-empts normal startup of
552     // the cache, but no harm done. (Providing scenery paths are consistent)
553
554     initNavCache();
555
556   // setup scenery paths now, especially TerraSync path for airport
557   // parking locations (after they're downloaded)
558
559     QtLauncher dlg;
560     dlg.exec();
561     if (dlg.result() != QDialog::Accepted) {
562         return false;
563     }
564
565     return true;
566 }
567
568 void QtLauncher::restoreSettings()
569 {
570     QSettings settings;
571     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
572     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
573     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
574     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
575     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
576     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
577     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
578     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
579
580     // full paths to -set.xml files
581     m_recentAircraft = settings.value("recent-aircraft").toStringList();
582
583     if (!m_recentAircraft.empty()) {
584         m_selectedAircraft = m_recentAircraft.front();
585     } else {
586         // select the default C172p
587     }
588
589     updateSelectedAircraft();
590
591     // ICAO identifiers
592     m_recentAirports = settings.value("recent-airports").toStringList();
593     if (!m_recentAirports.empty()) {
594         setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString()));
595     }
596     updateAirportDescription();
597
598     // rating filters
599     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
600     int index = 0;
601     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
602         m_ratingFilters[index++] = v.toInt();
603     }
604
605     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
606     m_aircraftProxy->setRatings(m_ratingFilters);
607
608     QStringList sceneryPaths = settings.value("scenery-paths").toStringList();
609     m_ui->sceneryPathsList->addItems(sceneryPaths);
610
611     QStringList aircraftPaths = settings.value("aircraft-paths").toStringList();
612     m_ui->aircraftPathsList->addItems(aircraftPaths);
613
614     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
615 }
616
617 void QtLauncher::saveSettings()
618 {
619     QSettings settings;
620     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
621     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
622     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
623     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
624     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
625     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
626     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
627     settings.setValue("recent-aircraft", m_recentAircraft);
628     settings.setValue("recent-airports", m_recentAirports);
629     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
630     settings.setValue("season", m_ui->seasonCombo->currentIndex());
631
632     QStringList paths;
633     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
634         paths.append(m_ui->sceneryPathsList->item(i)->text());
635     }
636
637     settings.setValue("scenery-paths", paths);
638     paths.clear();
639
640     for (int i=0; i<m_ui->aircraftPathsList->count(); ++i) {
641         paths.append(m_ui->aircraftPathsList->item(i)->text());
642     }
643
644     settings.setValue("aircraft-paths", paths);
645     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
646 }
647
648 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
649 {
650     flightgear::Options* opt = flightgear::Options::sharedInstance();
651     std::string stdName(name.toStdString());
652     if (cbox->isChecked()) {
653         opt->addOption("enable-" + stdName, "");
654     } else {
655         opt->addOption("disable-" + stdName, "");
656     }
657 }
658
659 void QtLauncher::onRun()
660 {
661     accept();
662
663     flightgear::Options* opt = flightgear::Options::sharedInstance();
664     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
665     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
666     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
667     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
668     setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
669
670     // MSAA is more complex
671     if (!m_ui->rembrandtCheckbox->isChecked()) {
672         if (m_ui->msaaCheckbox->isChecked()) {
673             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
674             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
675         } else {
676             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
677         }
678     }
679
680     // aircraft
681     if (!m_selectedAircraft.isEmpty()) {
682         QFileInfo setFileInfo(m_selectedAircraft);
683         opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
684         QString setFile = setFileInfo.fileName();
685         Q_ASSERT(setFile.endsWith("-set.xml"));
686         setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
687         opt->addOption("aircraft", setFile.toStdString());
688
689       // manage aircraft history
690         if (m_recentAircraft.contains(m_selectedAircraft))
691           m_recentAircraft.removeOne(m_selectedAircraft);
692         m_recentAircraft.prepend(m_selectedAircraft);
693         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
694           m_recentAircraft.pop_back();
695     }
696
697     // airport / location
698     if (m_selectedAirport) {
699         opt->addOption("airport", m_selectedAirport->ident());
700     }
701
702     if (m_ui->runwayRadio->isChecked()) {
703         int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
704         if ((index >= 0) && m_selectedAirport) {
705             // explicit runway choice
706             opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident());
707         }
708
709         if (m_ui->onFinalCheckbox->isChecked()) {
710             opt->addOption("glideslope", "3.0");
711             opt->addOption("offset-distance", "10.0"); // in nautical miles
712         }
713     } else if (m_ui->parkingRadio->isChecked()) {
714         // parking selection
715         opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
716     }
717
718     // time of day
719     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
720         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
721         opt->addOption("timeofday", dayval.toStdString());
722     }
723
724     if (m_ui->seasonCombo->currentIndex() != 0) {
725         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
726         opt->addOption("season", dayval.toStdString());
727     }
728
729     // scenery paths
730     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
731         QString path = m_ui->sceneryPathsList->item(i)->text();
732         opt->addOption("fg-scenery", path.toStdString());
733     }
734
735     // aircraft paths
736     for (int i=0; i<m_ui->aircraftPathsList->count(); ++i) {
737         QString path = m_ui->aircraftPathsList->item(i)->text();
738         opt->addOption("fg-aircraft", path.toStdString());
739     }
740
741     // additional arguments
742     ArgumentsTokenizer tk;
743     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
744         if (a.arg.startsWith("prop:")) {
745             QString v = a.arg.mid(5) + "=" + a.value;
746             opt->addOption("prop", v.toStdString());
747         } else {
748             opt->addOption(a.arg.toStdString(), a.value.toStdString());
749         }
750     }
751
752     saveSettings();
753 }
754
755 void QtLauncher::onQuit()
756 {
757     reject();
758 }
759
760 void QtLauncher::onSearchAirports()
761 {
762     QString search = m_ui->airportEdit->text();
763     m_airportsModel->setSearch(search);
764
765     if (m_airportsModel->isSearchActive()) {
766         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
767         m_ui->locationStack->setCurrentIndex(2);
768     } else if (m_airportsModel->rowCount(QModelIndex()) == 1) {
769         QString ident = m_airportsModel->firstIdent();
770         setAirport(FGAirport::findByIdent(ident.toStdString()));
771         m_ui->locationStack->setCurrentIndex(0);
772     }
773 }
774
775 void QtLauncher::onAirportSearchComplete()
776 {
777     int numResults = m_airportsModel->rowCount(QModelIndex());
778     if (numResults == 0) {
779         m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text()));
780     } else if (numResults == 1) {
781         QString ident = m_airportsModel->firstIdent();
782         setAirport(FGAirport::findByIdent(ident.toStdString()));
783         m_ui->locationStack->setCurrentIndex(0);
784     } else {
785         m_ui->locationStack->setCurrentIndex(1);
786     }
787 }
788
789 void QtLauncher::onAirportChanged()
790 {
791     m_ui->runwayCombo->setEnabled(m_selectedAirport);
792     m_ui->parkingCombo->setEnabled(m_selectedAirport);
793     m_ui->airportDiagram->setAirport(m_selectedAirport);
794
795     m_ui->runwayRadio->setChecked(true); // default back to runway mode
796     // unelss multiplayer is enabled ?
797
798     if (!m_selectedAirport) {
799         m_ui->airportDescription->setText(QString());
800         m_ui->airportDiagram->setEnabled(false);
801         return;
802     }
803
804     m_ui->airportDiagram->setEnabled(true);
805
806     m_ui->runwayCombo->clear();
807     m_ui->runwayCombo->addItem("Automatic", -1);
808     for (unsigned int r=0; r<m_selectedAirport->numRunways(); ++r) {
809         FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r);
810         // add runway with index as data role
811         m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
812
813         m_ui->airportDiagram->addRunway(rwy);
814     }
815
816     m_ui->parkingCombo->clear();
817     FGAirportDynamics* dynamics = m_selectedAirport->getDynamics();
818     PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(
819                                                                             m_selectedAirport->guid(),
820                                                                             FGPositioned::PARKING);
821     if (parkings.empty()) {
822         m_ui->parkingCombo->setEnabled(false);
823         m_ui->parkingRadio->setEnabled(false);
824     } else {
825         m_ui->parkingCombo->setEnabled(true);
826         m_ui->parkingRadio->setEnabled(true);
827         Q_FOREACH(PositionedID parking, parkings) {
828             FGParking* park = dynamics->getParking(parking);
829             m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
830                                         static_cast<qlonglong>(parking));
831
832             m_ui->airportDiagram->addParking(park);
833         }
834     }
835 }
836
837 void QtLauncher::updateAirportDescription()
838 {
839     if (!m_selectedAirport) {
840         m_ui->airportDescription->setText(QString("No airport selected"));
841         return;
842     }
843
844     QString ident = QString::fromStdString(m_selectedAirport->ident()),
845         name = QString::fromStdString(m_selectedAirport->name());
846     QString locationOnAirport;
847     if (m_ui->runwayRadio->isChecked()) {
848         bool onFinal = m_ui->onFinalCheckbox->isChecked();
849         QString runwayName = (m_ui->runwayCombo->currentIndex() == 0) ?
850             "active runway" :
851             QString("runway %1").arg(m_ui->runwayCombo->currentText());
852
853         if (onFinal) {
854             locationOnAirport = QString("on 10-mile final to %1").arg(runwayName);
855         } else {
856             locationOnAirport = QString("on %1").arg(runwayName);
857         }
858     } else if (m_ui->parkingRadio->isChecked()) {
859         locationOnAirport =  QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
860     }
861
862     m_ui->airportDescription->setText(QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport));
863 }
864
865 void QtLauncher::onAirportChoiceSelected(const QModelIndex& index)
866 {
867     m_ui->locationStack->setCurrentIndex(0);
868     setAirport(FGPositioned::loadById<FGAirport>(index.data(Qt::UserRole).toULongLong()));
869 }
870
871 void QtLauncher::onAircraftSelected(const QModelIndex& index)
872 {
873     m_selectedAircraft = index.data(AircraftPathRole).toString();
874     updateSelectedAircraft();
875 }
876
877 void QtLauncher::updateSelectedAircraft()
878 {
879     try {
880         QFileInfo info(m_selectedAircraft);
881         AircraftItem item(info.dir(), m_selectedAircraft);
882         m_ui->thumbnail->setPixmap(item.thumbnail());
883         m_ui->aircraftDescription->setText(item.description);
884     } catch (sg_exception& e) {
885         m_ui->thumbnail->setPixmap(QPixmap());
886         m_ui->aircraftDescription->setText("");
887     }
888 }
889
890 void QtLauncher::onPopupAirportHistory()
891 {
892     if (m_recentAirports.isEmpty()) {
893         return;
894     }
895
896     QMenu m;
897     Q_FOREACH(QString aptCode, m_recentAirports) {
898         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
899         QString name = QString::fromStdString(apt->name());
900         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
901         act->setData(aptCode);
902     }
903
904     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
905     QAction* triggered = m.exec(popupPos);
906     if (triggered) {
907         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
908         setAirport(apt);
909         m_ui->airportEdit->clear();
910         m_ui->locationStack->setCurrentIndex(0);
911     }
912 }
913
914 QModelIndex QtLauncher::proxyIndexForAircraftPath(QString path) const
915 {
916   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftPath(path));
917 }
918
919 QModelIndex QtLauncher::sourceIndexForAircraftPath(QString path) const
920 {
921     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
922     Q_ASSERT(sourceModel);
923     return sourceModel->indexOfAircraftPath(path);
924 }
925
926 void QtLauncher::onPopupAircraftHistory()
927 {
928     if (m_recentAircraft.isEmpty()) {
929         return;
930     }
931
932     QMenu m;
933     Q_FOREACH(QString path, m_recentAircraft) {
934         QModelIndex index = sourceIndexForAircraftPath(path);
935         if (!index.isValid()) {
936             // not scanned yet
937             continue;
938         }
939         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
940         act->setData(path);
941     }
942
943     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
944     QAction* triggered = m.exec(popupPos);
945     if (triggered) {
946         m_selectedAircraft = triggered->data().toString();
947         QModelIndex index = proxyIndexForAircraftPath(m_selectedAircraft);
948         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
949                                                               QItemSelectionModel::ClearAndSelect);
950         m_ui->aircraftFilter->clear();
951         updateSelectedAircraft();
952     }
953 }
954
955 void QtLauncher::setAirport(FGAirportRef ref)
956 {
957     if (m_selectedAirport == ref)
958         return;
959
960     m_selectedAirport = ref;
961     onAirportChanged();
962
963     if (ref.valid()) {
964         // maintain the recent airport list
965         QString icao = QString::fromStdString(ref->ident());
966         if (m_recentAirports.contains(icao)) {
967             // move to front
968             m_recentAirports.removeOne(icao);
969             m_recentAirports.push_front(icao);
970         } else {
971             // insert and trim list if necessary
972             m_recentAirports.push_front(icao);
973             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
974                 m_recentAirports.pop_back();
975             }
976         }
977     }
978
979     updateAirportDescription();
980 }
981
982 void QtLauncher::onOpenCustomAircraftDir()
983 {
984     QFileInfo info(m_customAircraftDir);
985     if (!info.exists()) {
986         int result = QMessageBox::question(this, "Create folder?",
987                                            "The custom aircraft folder does not exist, create it now?",
988                                            QMessageBox::Yes | QMessageBox::No,
989                                            QMessageBox::Yes);
990         if (result == QMessageBox::No) {
991             return;
992         }
993
994         QDir d(m_customAircraftDir);
995         d.mkpath(m_customAircraftDir);
996     }
997
998   QUrl u = QUrl::fromLocalFile(m_customAircraftDir);
999   QDesktopServices::openUrl(u);
1000 }
1001
1002 void QtLauncher::onEditRatingsFilter()
1003 {
1004     EditRatingsFilterDialog dialog(this);
1005     dialog.setRatings(m_ratingFilters);
1006
1007     dialog.exec();
1008     if (dialog.result() == QDialog::Accepted) {
1009         QVariantList vl;
1010         for (int i=0; i<4; ++i) {
1011             m_ratingFilters[i] = dialog.getRating(i);
1012             vl.append(m_ratingFilters[i]);
1013         }
1014         m_aircraftProxy->setRatings(m_ratingFilters);
1015
1016         QSettings settings;
1017         settings.setValue("min-ratings", vl);
1018     }
1019 }
1020
1021 void QtLauncher::updateSettingsSummary()
1022 {
1023     QStringList summary;
1024     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1025         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1026     }
1027
1028     if (m_ui->seasonCombo->currentIndex() > 0) {
1029         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1030     }
1031
1032     if (m_ui->rembrandtCheckbox->isChecked()) {
1033         summary.append("Rembrandt enabled");
1034     } else if (m_ui->msaaCheckbox->isChecked()) {
1035         summary.append("anti-aliasing");
1036     }
1037
1038     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1039         summary.append("live weather");
1040     }
1041
1042     if (m_ui->terrasyncCheck->isChecked()) {
1043         summary.append("automatic scenery downloads");
1044     }
1045
1046     if (m_ui->startPausedCheck->isChecked()) {
1047         summary.append("paused");
1048     }
1049
1050     QString s = summary.join(", ");
1051     s[0] = s[0].toUpper();
1052     m_ui->settingsDescription->setText(s);
1053 }
1054
1055 void QtLauncher::onAddSceneryPath()
1056 {
1057     QString path = QFileDialog::getExistingDirectory(this, tr("Choose scenery folder"));
1058     if (!path.isEmpty()) {
1059         m_ui->sceneryPathsList->addItem(path);
1060         saveSettings();
1061     }
1062 }
1063
1064 void QtLauncher::onRemoveSceneryPath()
1065 {
1066     if (m_ui->sceneryPathsList->currentItem()) {
1067         delete m_ui->sceneryPathsList->currentItem();
1068         saveSettings();
1069     }
1070 }
1071
1072 void QtLauncher::onAddAircraftPath()
1073 {
1074     QString path = QFileDialog::getExistingDirectory(this, tr("Choose aircraft folder"));
1075     if (!path.isEmpty()) {
1076         m_ui->aircraftPathsList->addItem(path);
1077         saveSettings();
1078
1079         // re-scan the aircraft list
1080         QSettings settings;
1081         m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1082         m_aircraftModel->scanDirs();
1083     }
1084 }
1085
1086 void QtLauncher::onRemoveAircraftPath()
1087 {
1088     if (m_ui->aircraftPathsList->currentItem()) {
1089         delete m_ui->aircraftPathsList->currentItem();
1090         saveSettings();
1091     }
1092 }
1093
1094 void QtLauncher::onRembrandtToggled(bool b)
1095 {
1096     // Rembrandt and multi-sample are exclusive
1097     m_ui->msaaCheckbox->setEnabled(!b);
1098 }
1099
1100 void QtLauncher::onSubsytemIdleTimeout()
1101 {
1102     globals->get_subsystem_mgr()->update(0.0);
1103 }
1104
1105 #include "QtLauncher.moc"
1106