]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Launcher support for aircraft variants.
[flightgear.git] / src / GUI / QtLauncher.cxx
1 #include "QtLauncher.hxx"
2
3 // Qt
4 #include <QProgressDialog>
5 #include <QCoreApplication>
6 #include <QAbstractListModel>
7 #include <QDir>
8 #include <QFileInfo>
9 #include <QPixmap>
10 #include <QTimer>
11 #include <QDebug>
12 #include <QCompleter>
13 #include <QThread>
14 #include <QMutex>
15 #include <QMutexLocker>
16 #include <QListView>
17 #include <QSettings>
18 #include <QPainter>
19 #include <QSortFilterProxyModel>
20 #include <QMenu>
21 #include <QDesktopServices>
22 #include <QUrl>
23 #include <QAction>
24 #include <QStyledItemDelegate>
25 #include <QLinearGradient>
26 #include <QFileDialog>
27 #include <QMessageBox>
28
29 // Simgear
30 #include <simgear/timing/timestamp.hxx>
31 #include <simgear/props/props_io.hxx>
32 #include <simgear/structure/exception.hxx>
33 #include <simgear/misc/sg_path.hxx>
34
35 #include "ui_Launcher.h"
36 #include "EditRatingsFilterDialog.hxx"
37
38 #include <Main/globals.hxx>
39 #include <Navaids/NavDataCache.hxx>
40 #include <Airports/airport.hxx>
41 #include <Airports/dynamics.hxx> // for parking
42 #include <Main/options.hxx>
43
44 using namespace flightgear;
45
46 const int MAX_RECENT_AIRPORTS = 32;
47 const int MAX_RECENT_AIRCRAFT = 20;
48
49 namespace { // anonymous namespace
50
51 const int AircraftPathRole = Qt::UserRole + 1;
52 const int AircraftAuthorsRole = Qt::UserRole + 2;
53 const int AircraftVariantRole = Qt::UserRole + 3;
54 const int AircraftVariantCountRole = Qt::UserRole + 4;
55 const int AircraftRatingRole = Qt::UserRole + 100;
56 const int AircraftVariantDescriptionRole = Qt::UserRole + 200;
57
58 void initNavCache()
59 {
60     NavDataCache* cache = NavDataCache::instance();
61     if (cache->isRebuildRequired()) {
62         QProgressDialog rebuildProgress("Initialising navigation data, this may take several minutes",
63                                        QString() /* cancel text */,
64                                        0, 0);
65         rebuildProgress.setWindowModality(Qt::WindowModal);
66         rebuildProgress.show();
67
68         while (!cache->rebuild()) {
69             // sleep to give the rebuild thread more time
70             SGTimeStamp::sleepForMSec(50);
71             rebuildProgress.setValue(0);
72             QCoreApplication::processEvents();
73         }
74     }
75 }
76
77 struct AircraftItem
78 {
79     AircraftItem()
80     {
81         // oh for C++11 initialisers
82         for (int i=0; i<4; ++i) ratings[i] = 0;
83     }
84
85     AircraftItem(QDir dir, QString filePath)
86     {
87         for (int i=0; i<4; ++i) ratings[i] = 0;
88
89         SGPropertyNode root;
90         readProperties(filePath.toStdString(), &root);
91
92         if (!root.hasChild("sim")) {
93             throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
94         }
95
96         SGPropertyNode_ptr sim = root.getNode("sim");
97
98         path = filePath;
99         description = sim->getStringValue("description");
100         authors =  sim->getStringValue("author");
101
102         if (sim->hasChild("rating")) {
103             parseRatings(sim->getNode("rating"));
104         }
105
106         if (sim->hasChild("variant-of")) {
107             variantOf = sim->getStringValue("variant-of");
108         }
109
110         if (dir.exists("thumbnail.jpg")) {
111             thumbnail.load(dir.filePath("thumbnail.jpg"));
112             // resize to the standard size
113             if (thumbnail.height() > 128) {
114                 thumbnail = thumbnail.scaledToHeight(128);
115             }
116         }
117
118     }
119
120     // the file-name without -set.xml suffix
121     QString baseName() const
122     {
123         QString fn = QFileInfo(path).fileName();
124         fn.truncate(fn.count() - 8);
125         return fn;
126     }
127
128     QString path;
129     QPixmap thumbnail;
130     QString description;
131     QString authors;
132     int ratings[4];
133     QString variantOf;
134
135     QList<AircraftItem*> variants;
136 private:
137     void parseRatings(SGPropertyNode_ptr ratingsNode)
138     {
139         ratings[0] = ratingsNode->getIntValue("FDM");
140         ratings[1] = ratingsNode->getIntValue("systems");
141         ratings[2] = ratingsNode->getIntValue("cockpit");
142         ratings[3] = ratingsNode->getIntValue("model");
143     }
144 };
145
146 class AircraftScanThread : public QThread
147 {
148     Q_OBJECT
149 public:
150     AircraftScanThread(QStringList dirsToScan) :
151         m_dirs(dirsToScan),
152         m_done(false)
153     {
154
155     }
156
157     /** thread-safe access to items already scanned */
158     QList<AircraftItem*> items()
159     {
160         QList<AircraftItem*> result;
161         QMutexLocker g(&m_lock);
162         result.swap(m_items);
163         g.unlock();
164         return result;
165     }
166
167     void setDone()
168     {
169         m_done = true;
170     }
171 Q_SIGNALS:
172     void addedItems();
173
174 protected:
175     virtual void run()
176     {
177         Q_FOREACH(QString d, m_dirs) {
178             scanAircraftDir(QDir(d));
179             if (m_done) {
180                 return;
181             }
182         }
183     }
184
185 private:
186     void scanAircraftDir(QDir path)
187     {
188         QStringList filters;
189         filters << "*-set.xml";
190         Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
191             QDir childDir(child.absoluteFilePath());
192             QMap<QString, AircraftItem*> baseAircraft;
193             QList<AircraftItem*> variants;
194
195             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
196                 try {
197                     AircraftItem* item = new AircraftItem(childDir, xmlChild.absoluteFilePath());
198                     if (item->variantOf.isNull()) {
199                         baseAircraft.insert(item->baseName(), item);
200                     } else {
201                         variants.append(item);
202                     }
203                 } catch (sg_exception& e) {
204                     continue;
205                 }
206
207                 if (m_done) {
208                     return;
209                 }
210             } // of set.xml iteration
211
212             // bind variants to their principals
213             Q_FOREACH(AircraftItem* item, variants) {
214                 if (!baseAircraft.contains(item->variantOf)) {
215                     qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
216                     delete item;
217                     continue;
218                 }
219
220                 baseAircraft.value(item->variantOf)->variants.append(item);
221             }
222
223             // lock mutex whil we modify the items array
224             {
225                 QMutexLocker g(&m_lock);
226                 m_items.append(baseAircraft.values());
227             }
228
229             emit addedItems();
230         } // of subdir iteration
231     }
232
233     QMutex m_lock;
234     QStringList m_dirs;
235     QList<AircraftItem*> m_items;
236     bool m_done;
237 };
238
239 class AircraftItemModel : public QAbstractListModel
240 {
241     Q_OBJECT
242 public:
243     AircraftItemModel(QObject* pr) :
244         QAbstractListModel(pr)
245     {
246         QStringList dirs;
247         Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
248             dirs << QString::fromStdString(ap);
249         }
250
251         SGPath rootAircraft(globals->get_fg_root());
252         rootAircraft.append("Aircraft");
253         dirs << QString::fromStdString(rootAircraft.str());
254
255         m_scanThread = new AircraftScanThread(dirs);
256         connect(m_scanThread, &AircraftScanThread::finished, this,
257                 &AircraftItemModel::onScanFinished);
258         connect(m_scanThread, &AircraftScanThread::addedItems,
259                 this, &AircraftItemModel::onScanResults);
260         m_scanThread->start();
261     }
262
263     ~AircraftItemModel()
264     {
265         if (m_scanThread) {
266             m_scanThread->setDone();
267             m_scanThread->wait(1000);
268             delete m_scanThread;
269         }
270     }
271
272     virtual int rowCount(const QModelIndex& parent) const
273     {
274         return m_items.size();
275     }
276
277     virtual QVariant data(const QModelIndex& index, int role) const
278     {
279         if (role == AircraftVariantRole) {
280             return m_activeVariant.at(index.row());
281         }
282
283         const AircraftItem* item(m_items.at(index.row()));
284
285         if (role == AircraftVariantCountRole) {
286             return item->variants.count();
287         }
288
289         if (role >= AircraftVariantDescriptionRole) {
290             int variantIndex = role - AircraftVariantDescriptionRole;
291             return item->variants.at(variantIndex)->description;
292         }
293
294         quint32 variantIndex = m_activeVariant.at(index.row());
295         if (variantIndex) {
296             if (variantIndex < item->variants.count()) {
297                 // show the selected variant
298                 item = item->variants.at(variantIndex);
299             }
300         }
301
302         if (role == Qt::DisplayRole) {
303             return item->description;
304         } else if (role == Qt::DecorationRole) {
305             return item->thumbnail;
306         } else if (role == AircraftPathRole) {
307             return item->path;
308         } else if (role == AircraftAuthorsRole) {
309             return item->authors;
310         } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
311             return item->ratings[role - AircraftRatingRole];
312         } else if (role == Qt::ToolTipRole) {
313             return item->path;
314         }
315
316         return QVariant();
317     }
318
319     virtual bool setData(const QModelIndex &index, const QVariant &value, int role)
320     {
321         if (role == AircraftVariantRole) {
322             m_activeVariant[index.row()] = value.toInt();
323             emit dataChanged(index, index);
324             return true;
325         }
326
327         return false;
328     }
329
330   QModelIndex indexOfAircraftPath(QString path) const
331   {
332       for (int row=0; row <m_items.size(); ++row) {
333           const AircraftItem* item(m_items.at(row));
334           if (item->path == path) {
335               return index(row);
336           }
337       }
338
339       return QModelIndex();
340   }
341
342 private slots:
343     void onScanResults()
344     {
345         QList<AircraftItem*> newItems = m_scanThread->items();
346         if (newItems.isEmpty())
347             return;
348
349         int firstRow = m_items.count();
350         int lastRow = firstRow + newItems.count() - 1;
351         beginInsertRows(QModelIndex(), firstRow, lastRow);
352         m_items.append(newItems);
353
354         // default variants in all cases
355         for (int i=0; i< newItems.count(); ++i) {
356             m_activeVariant.append(0);
357         }
358         endInsertRows();
359     }
360
361     void onScanFinished()
362     {
363         delete m_scanThread;
364         m_scanThread = NULL;
365     }
366
367 private:
368     AircraftScanThread* m_scanThread;
369     QList<AircraftItem*> m_items;
370     QList<quint32> m_activeVariant;
371 };
372
373 class AircraftItemDelegate : public QStyledItemDelegate
374 {
375     Q_OBJECT
376 public:
377     static const int MARGIN = 4;
378     static const int ARROW_SIZE = 20;
379
380     AircraftItemDelegate(QListView* view) :
381         m_view(view)
382     {
383         view->viewport()->installEventFilter(this);
384     }
385
386     virtual void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
387     {
388         // selection feedback rendering
389         if (option.state & QStyle::State_Selected) {
390             QLinearGradient grad(option.rect.topLeft(), option.rect.bottomLeft());
391             grad.setColorAt(0.0, QColor(152, 163, 180));
392             grad.setColorAt(1.0, QColor(90, 107, 131));
393
394             QBrush backgroundBrush(grad);
395             painter->fillRect(option.rect, backgroundBrush);
396
397             painter->setPen(QColor(90, 107, 131));
398             painter->drawLine(option.rect.topLeft(), option.rect.topRight());
399
400         }
401
402         QRect contentRect = option.rect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
403
404         QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
405         painter->drawPixmap(contentRect.topLeft(), thumbnail);
406
407         // draw 1px frame
408         painter->setPen(QColor(0x7f, 0x7f, 0x7f));
409         painter->setBrush(Qt::NoBrush);
410         painter->drawRect(contentRect.left(), contentRect.top(), thumbnail.width(), thumbnail.height());
411
412         int variantCount = index.data(AircraftVariantCountRole).toInt();
413         int currentVariant =index.data(AircraftVariantRole).toInt();
414         QString description = index.data(Qt::DisplayRole).toString();
415         contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
416
417         painter->setPen(Qt::black);
418         QFont f;
419         f.setPointSize(18);
420         painter->setFont(f);
421
422         QRect descriptionRect = contentRect.adjusted(ARROW_SIZE, 0, -ARROW_SIZE, 0),
423             actualBounds;
424
425         if (variantCount > 0) {
426             bool canLeft = (currentVariant > 0);
427             bool canRight =  (currentVariant < (variantCount - 1));
428
429             QRect leftArrowRect = leftCycleArrowRect(option.rect, index);
430             painter->fillRect(leftArrowRect, canLeft ? Qt::black : Qt::gray);
431
432             QRect rightArrowRect = rightCycleArrowRect(option.rect, index);
433             painter->fillRect(rightArrowRect, canRight ? Qt::black : Qt::gray);
434         }
435
436         painter->drawText(descriptionRect, Qt::TextWordWrap, description, &actualBounds);
437
438         QString authors = index.data(AircraftAuthorsRole).toString();
439
440         f.setPointSize(12);
441         painter->setFont(f);
442
443         QRect authorsRect = descriptionRect;
444         authorsRect.moveTop(actualBounds.bottom() + MARGIN);
445         painter->drawText(authorsRect, Qt::TextWordWrap,
446                           QString("by: %1").arg(authors),
447                           &actualBounds);
448
449         QRect r = contentRect;
450         r.setWidth(contentRect.width() / 2);
451         r.moveTop(actualBounds.bottom() + MARGIN);
452         r.setHeight(24);
453
454         drawRating(painter, "Flight model:", r, index.data(AircraftRatingRole).toInt());
455         r.moveTop(r.bottom());
456         drawRating(painter, "Systems:", r, index.data(AircraftRatingRole + 1).toInt());
457
458         r.moveTop(actualBounds.bottom() + MARGIN);
459         r.moveLeft(r.right());
460         drawRating(painter, "Cockpit:", r, index.data(AircraftRatingRole + 2).toInt());
461         r.moveTop(r.bottom());
462         drawRating(painter, "Exterior model:", r, index.data(AircraftRatingRole + 3).toInt());
463     }
464
465     virtual QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
466     {
467         return QSize(500, 128 + (MARGIN * 2));
468     }
469
470     virtual bool eventFilter( QObject*, QEvent* event )
471     {
472         if ( event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease )
473         {
474             QMouseEvent* me = static_cast< QMouseEvent* >( event );
475             QModelIndex index = m_view->indexAt( me->pos() );
476             int variantCount = index.data(AircraftVariantCountRole).toInt();
477             int variantIndex = index.data(AircraftVariantRole).toInt();
478
479             if ( (event->type() == QEvent::MouseButtonRelease) && (variantCount > 0) )
480             {
481                 QRect vr = m_view->visualRect(index);
482                 QRect leftCycleRect = leftCycleArrowRect(vr, index),
483                     rightCycleRect = rightCycleArrowRect(vr, index);
484
485                 if ((variantIndex > 0) && leftCycleRect.contains(me->pos())) {
486                     m_view->model()->setData(index, variantIndex - 1, AircraftVariantRole);
487                     emit variantChanged(index);
488                     return true;
489                 } else if ((variantIndex < (variantCount - 1)) && rightCycleRect.contains(me->pos())) {
490                     m_view->model()->setData(index, variantIndex + 1, AircraftVariantRole);
491                     emit variantChanged(index);
492                     return true;
493                 }
494             }
495         } // of mouse button press or release
496         
497         return false;
498     }
499
500 Q_SIGNALS:
501     void variantChanged(const QModelIndex& index);
502
503 private:
504     QRect leftCycleArrowRect(const QRect& visualRect, const QModelIndex& index) const
505     {
506         QRect contentRect = visualRect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
507         QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
508         contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
509
510         QRect r = contentRect;
511         r.setRight(r.left() + ARROW_SIZE);
512         r.setBottom(r.top() + ARROW_SIZE);
513         return r;
514
515     }
516
517     QRect rightCycleArrowRect(const QRect& visualRect, const QModelIndex& index) const
518     {
519         QRect contentRect = visualRect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
520         QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
521         contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
522
523         QRect r = contentRect;
524         r.setLeft(r.right() - ARROW_SIZE);
525         r.setBottom(r.top() + ARROW_SIZE);
526         return r;
527
528     }
529
530     void drawRating(QPainter* painter, QString label, const QRect& box, int value) const
531     {
532         const int DOT_SIZE = 10;
533         const int DOT_MARGIN = 4;
534
535         QRect dotBox = box;
536         dotBox.setLeft(box.right() - (DOT_MARGIN * 6 + DOT_SIZE * 5));
537
538         painter->setPen(Qt::black);
539         QRect textBox = box;
540         textBox.setRight(dotBox.left() - DOT_MARGIN);
541         painter->drawText(textBox, Qt::AlignVCenter | Qt::AlignRight, label);
542
543         painter->setPen(Qt::NoPen);
544         QRect dot(dotBox.left() + DOT_MARGIN,
545                   dotBox.center().y() - (DOT_SIZE / 2),
546                   DOT_SIZE,
547                   DOT_SIZE);
548         for (int i=0; i<5; ++i) {
549             painter->setBrush((i < value) ? QColor(0x3f, 0x3f, 0x3f) : QColor(0xaf, 0xaf, 0xaf));
550             painter->drawEllipse(dot);
551             dot.moveLeft(dot.right() + DOT_MARGIN);
552         }
553     }
554
555     QListView* m_view;
556 };
557
558 class ArgumentsTokenizer
559 {
560 public:
561     class Arg
562     {
563     public:
564         explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
565
566         QString arg;
567         QString value;
568     };
569
570     QList<Arg> tokenize(QString in) const
571     {
572         int index = 0;
573         const int len = in.count();
574         QChar c, nc;
575         State state = Start;
576         QString key, value;
577         QList<Arg> result;
578
579         for (; index < len; ++index) {
580             c = in.at(index);
581             nc = index < (len - 1) ? in.at(index + 1) : QChar();
582
583             switch (state) {
584             case Start:
585                 if (c == QChar('-')) {
586                     if (nc == QChar('-')) {
587                         state = Key;
588                         key.clear();
589                         ++index;
590                     } else {
591                         // should we pemit single hyphen arguments?
592                         // choosing to fail for now
593                         return QList<Arg>();
594                     }
595                 } else if (c.isSpace()) {
596                     break;
597                 }
598                 break;
599
600             case Key:
601                 if (c == QChar('=')) {
602                     state = Value;
603                     value.clear();
604                 } else if (c.isSpace()) {
605                     state = Start;
606                     result.append(Arg(key));
607                 } else {
608                     // could check for illegal charatcers here
609                     key.append(c);
610                 }
611                 break;
612
613             case Value:
614                 if (c == QChar('"')) {
615                     state = Quoted;
616                 } else if (c.isSpace()) {
617                     state = Start;
618                     result.append(Arg(key, value));
619                 } else {
620                     value.append(c);
621                 }
622                 break;
623
624             case Quoted:
625                 if (c == QChar('\\')) {
626                     // check for escaped double-quote inside quoted value
627                     if (nc == QChar('"')) {
628                         ++index;
629                     }
630                 } else if (c == QChar('"')) {
631                     state = Value;
632                 } else {
633                     value.append(c);
634                 }
635                 break;
636             } // of state switch
637         } // of character loop
638
639         // ensure last argument isn't lost
640         if (state == Key) {
641             result.append(Arg(key));
642         } else if (state == Value) {
643             result.append(Arg(key, value));
644         }
645
646         return result;
647     }
648
649 private:
650     enum State {
651         Start = 0,
652         Key,
653         Value,
654         Quoted
655     };
656 };
657
658 } // of anonymous namespace
659
660 class AirportSearchModel : public QAbstractListModel
661 {
662     Q_OBJECT
663 public:
664     AirportSearchModel() :
665         m_searchActive(false)
666     {
667     }
668
669     void setSearch(QString t)
670     {
671         beginResetModel();
672
673         m_airports.clear();
674         m_ids.clear();
675
676         std::string term(t.toUpper().toStdString());
677         // try ICAO lookup first
678         FGAirportRef ref = FGAirport::findByIdent(term);
679         if (ref) {
680             m_ids.push_back(ref->guid());
681             m_airports.push_back(ref);
682         } else {
683             m_search.reset(new NavDataCache::ThreadedAirportSearch(term));
684             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
685             m_searchActive = true;
686         }
687
688         endResetModel();
689     }
690
691     bool isSearchActive() const
692     {
693         return m_searchActive;
694     }
695
696     virtual int rowCount(const QModelIndex&) const
697     {
698         // if empty, return 1 for special 'no matches'?
699         return m_ids.size();
700     }
701
702     virtual QVariant data(const QModelIndex& index, int role) const
703     {
704         if (!index.isValid())
705             return QVariant();
706         
707         FGAirportRef apt = m_airports[index.row()];
708         if (!apt.valid()) {
709             apt = FGPositioned::loadById<FGAirport>(m_ids[index.row()]);
710             m_airports[index.row()] = apt;
711         }
712
713         if (role == Qt::DisplayRole) {
714             QString name = QString::fromStdString(apt->name());
715             return QString("%1: %2").arg(QString::fromStdString(apt->ident())).arg(name);
716         }
717
718         if (role == Qt::EditRole) {
719             return QString::fromStdString(apt->ident());
720         }
721
722         if (role == Qt::UserRole) {
723             return static_cast<qlonglong>(m_ids[index.row()]);
724         }
725
726         return QVariant();
727     }
728
729     QString firstIdent() const
730     {
731         if (m_ids.empty())
732             return QString();
733
734         if (!m_airports.front().valid()) {
735             m_airports[0] = FGPositioned::loadById<FGAirport>(m_ids.front());
736         }
737
738         return QString::fromStdString(m_airports.front()->ident());
739     }
740
741 Q_SIGNALS:
742     void searchComplete();
743
744 private slots:
745     void onSearchResultsPoll()
746     {
747         PositionedIDVec newIds = m_search->results();
748         
749         beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
750         for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
751             m_ids.push_back(newIds[i]);
752             m_airports.push_back(FGAirportRef()); // null ref
753         }
754         endInsertRows();
755
756         if (m_search->isComplete()) {
757             m_searchActive = false;
758             m_search.reset();
759             emit searchComplete();
760         } else {
761             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
762         }
763     }
764
765 private:
766     PositionedIDVec m_ids;
767     mutable std::vector<FGAirportRef> m_airports;
768     bool m_searchActive;
769     QScopedPointer<NavDataCache::ThreadedAirportSearch> m_search;
770 };
771
772 class AircraftProxyModel : public QSortFilterProxyModel
773 {
774     Q_OBJECT
775 public:
776     AircraftProxyModel(QObject* pr) :
777         QSortFilterProxyModel(pr),
778         m_ratingsFilter(true)
779     {
780         for (int i=0; i<4; ++i) {
781             m_ratings[i] = 3;
782         }
783     }
784
785     void setRatings(int* ratings)
786     {
787         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
788         invalidate();
789     }
790
791 public slots:
792     void setRatingFilterEnabled(bool e)
793     {
794         if (e == m_ratingsFilter) {
795             return;
796         }
797
798         m_ratingsFilter = e;
799         invalidate();
800     }
801
802 protected:
803     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
804     {
805         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
806             return false;
807         }
808
809         if (m_ratingsFilter) {
810             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
811             for (int i=0; i<4; ++i) {
812                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
813                     return false;
814                 }
815             }
816         }
817
818         return true;
819     }
820
821 private:
822     bool m_ratingsFilter;
823     int m_ratings[4];
824 };
825
826 QtLauncher::QtLauncher() :
827     QDialog(),
828     m_ui(NULL)
829 {
830     m_ui.reset(new Ui::Launcher);
831     m_ui->setupUi(this);
832
833 #if QT_VERSION >= 0x050300
834     // don't require Qt 5.3
835     m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
836 #endif
837
838 #if QT_VERSION >= 0x050200
839     m_ui->aircraftFilter->setClearButtonEnabled(true);
840 #endif
841
842     for (int i=0; i<4; ++i) {
843         m_ratingFilters[i] = 3;
844     }
845
846     m_airportsModel = new AirportSearchModel;
847     m_ui->searchList->setModel(m_airportsModel);
848     connect(m_ui->searchList, &QListView::clicked,
849             this, &QtLauncher::onAirportChoiceSelected);
850     connect(m_airportsModel, &AirportSearchModel::searchComplete,
851             this, &QtLauncher::onAirportSearchComplete);
852
853     SGPath p = SGPath::documents();
854     p.append("FlightGear");
855     p.append("Aircraft");
856     m_customAircraftDir = QString::fromStdString(p.str());
857     m_ui->customAircraftDirLabel->setText(QString("Custom aircraft folder: %1").arg(m_customAircraftDir));
858
859     globals->append_aircraft_path(m_customAircraftDir.toStdString());
860
861     // create and configure the proxy model
862     m_aircraftProxy = new AircraftProxyModel(this);
863     m_aircraftProxy->setSourceModel(new AircraftItemModel(this));
864
865     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
866     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
867     m_aircraftProxy->setSortRole(Qt::DisplayRole);
868     m_aircraftProxy->setDynamicSortFilter(true);
869
870     m_ui->aircraftList->setModel(m_aircraftProxy);
871     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
872     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
873     m_ui->aircraftList->setItemDelegate(delegate);
874     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
875     connect(m_ui->aircraftList, &QListView::clicked,
876             this, &QtLauncher::onAircraftSelected);
877     connect(delegate, &AircraftItemDelegate::variantChanged,
878             this, &QtLauncher::onAircraftSelected);
879
880     connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
881             this, SLOT(updateAirportDescription()));
882     connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
883             this, SLOT(updateAirportDescription()));
884     connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
885             this, SLOT(updateAirportDescription()));
886     connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
887             this, SLOT(updateAirportDescription()));
888     connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
889             this, SLOT(updateAirportDescription()));
890
891
892     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
893     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
894     connect(m_ui->airportEdit, SIGNAL(returnPressed()),
895             this, SLOT(onSearchAirports()));
896
897     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
898             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
899
900     connect(m_ui->airportHistory, &QPushButton::clicked,
901             this, &QtLauncher::onPopupAirportHistory);
902     connect(m_ui->aircraftHistory, &QPushButton::clicked,
903           this, &QtLauncher::onPopupAircraftHistory);
904
905     restoreSettings();
906
907     connect(m_ui->openAircraftDirButton, &QPushButton::clicked,
908           this, &QtLauncher::onOpenCustomAircraftDir);
909
910     QAction* qa = new QAction(this);
911     qa->setShortcut(QKeySequence("Ctrl+Q"));
912     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
913     addAction(qa);
914
915     connect(m_ui->editRatingFilter, &QPushButton::clicked,
916             this, &QtLauncher::onEditRatingsFilter);
917     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
918             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
919
920     QIcon historyIcon(":/history-icon");
921     m_ui->aircraftHistory->setIcon(historyIcon);
922     m_ui->airportHistory->setIcon(historyIcon);
923
924     m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
925
926     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
927             this, SLOT(updateSettingsSummary()));
928     connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
929             this, SLOT(updateSettingsSummary()));
930     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
931             this, SLOT(updateSettingsSummary()));
932     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
933             this, SLOT(updateSettingsSummary()));
934     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
935             this, SLOT(updateSettingsSummary()));
936     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
937             this, SLOT(updateSettingsSummary()));
938     connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
939             this, SLOT(updateSettingsSummary()));
940
941     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
942             this, SLOT(onRembrandtToggled(bool)));
943
944     updateSettingsSummary();
945
946     connect(m_ui->addSceneryPath, &QToolButton::clicked,
947             this, &QtLauncher::onAddSceneryPath);
948     connect(m_ui->removeSceneryPath, &QToolButton::clicked,
949             this, &QtLauncher::onRemoveSceneryPath);
950 }
951
952 QtLauncher::~QtLauncher()
953 {
954     
955 }
956
957 bool QtLauncher::runLauncherDialog()
958 {
959      Q_INIT_RESOURCE(resources);
960
961     // startup the nav-cache now. This pre-empts normal startup of
962     // the cache, but no harm done. (Providing scenery paths are consistent)
963
964     initNavCache();
965
966   // setup scenery paths now, especially TerraSync path for airport
967   // parking locations (after they're downloaded)
968
969     QtLauncher dlg;
970     dlg.exec();
971     if (dlg.result() != QDialog::Accepted) {
972         return false;
973     }
974
975     return true;
976 }
977
978 void QtLauncher::restoreSettings()
979 {
980     QSettings settings;
981     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
982     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
983     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
984     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
985     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
986     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
987     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
988     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
989
990     // full paths to -set.xml files
991     m_recentAircraft = settings.value("recent-aircraft").toStringList();
992
993     if (!m_recentAircraft.empty()) {
994         m_selectedAircraft = m_recentAircraft.front();
995     } else {
996         // select the default C172p
997     }
998
999     updateSelectedAircraft();
1000
1001     // ICAO identifiers
1002     m_recentAirports = settings.value("recent-airports").toStringList();
1003     if (!m_recentAirports.empty()) {
1004         setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString()));
1005     }
1006     updateAirportDescription();
1007
1008     // rating filters
1009     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
1010     int index = 0;
1011     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
1012         m_ratingFilters[index++] = v.toInt();
1013     }
1014
1015     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
1016     m_aircraftProxy->setRatings(m_ratingFilters);
1017
1018     QStringList sceneryPaths = settings.value("scenery-paths").toStringList();
1019     m_ui->sceneryPathsList->addItems(sceneryPaths);
1020
1021     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
1022 }
1023
1024 void QtLauncher::saveSettings()
1025 {
1026     QSettings settings;
1027     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
1028     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
1029     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
1030     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
1031     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
1032     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
1033     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
1034     settings.setValue("recent-aircraft", m_recentAircraft);
1035     settings.setValue("recent-airports", m_recentAirports);
1036     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
1037     settings.setValue("season", m_ui->seasonCombo->currentIndex());
1038
1039     QStringList paths;
1040     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
1041         paths.append(m_ui->sceneryPathsList->item(i)->text());
1042     }
1043
1044     settings.setValue("scenery-paths", paths);
1045     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
1046 }
1047
1048 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
1049 {
1050     flightgear::Options* opt = flightgear::Options::sharedInstance();
1051     std::string stdName(name.toStdString());
1052     if (cbox->isChecked()) {
1053         opt->addOption("enable-" + stdName, "");
1054     } else {
1055         opt->addOption("disable-" + stdName, "");
1056     }
1057 }
1058
1059 void QtLauncher::onRun()
1060 {
1061     accept();
1062
1063     flightgear::Options* opt = flightgear::Options::sharedInstance();
1064     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
1065     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
1066     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
1067     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
1068     setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
1069
1070     // MSAA is more complex
1071     if (!m_ui->rembrandtCheckbox->isChecked()) {
1072         if (m_ui->msaaCheckbox->isChecked()) {
1073             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
1074             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
1075         } else {
1076             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
1077         }
1078     }
1079
1080     // aircraft
1081     if (!m_selectedAircraft.isEmpty()) {
1082         QFileInfo setFileInfo(m_selectedAircraft);
1083         opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
1084         QString setFile = setFileInfo.fileName();
1085         Q_ASSERT(setFile.endsWith("-set.xml"));
1086         setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
1087         opt->addOption("aircraft", setFile.toStdString());
1088
1089       // manage aircraft history
1090         if (m_recentAircraft.contains(m_selectedAircraft))
1091           m_recentAircraft.removeOne(m_selectedAircraft);
1092         m_recentAircraft.prepend(m_selectedAircraft);
1093         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
1094           m_recentAircraft.pop_back();
1095     }
1096
1097     // airport / location
1098     if (m_selectedAirport) {
1099         opt->addOption("airport", m_selectedAirport->ident());
1100     }
1101
1102     if (m_ui->runwayRadio->isChecked()) {
1103         int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
1104         if ((index >= 0) && m_selectedAirport) {
1105             // explicit runway choice
1106             opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident());
1107         }
1108
1109         if (m_ui->onFinalCheckbox->isChecked()) {
1110             opt->addOption("glideslope", "3.0");
1111             opt->addOption("offset-distance", "10.0"); // in nautical miles
1112         }
1113     } else if (m_ui->parkingRadio->isChecked()) {
1114         // parking selection
1115         opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
1116     }
1117
1118     // time of day
1119     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
1120         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
1121         opt->addOption("timeofday", dayval.toStdString());
1122     }
1123
1124     if (m_ui->seasonCombo->currentIndex() != 0) {
1125         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
1126         opt->addOption("season", dayval.toStdString());
1127     }
1128
1129     // scenery paths
1130     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
1131         QString path = m_ui->sceneryPathsList->item(i)->text();
1132         opt->addOption("fg-scenery", path.toStdString());
1133     }
1134
1135     // additional arguments
1136     ArgumentsTokenizer tk;
1137     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
1138         if (a.arg.startsWith("prop:")) {
1139             QString v = a.arg.mid(5) + "=" + a.value;
1140             opt->addOption("prop", v.toStdString());
1141         } else {
1142             opt->addOption(a.arg.toStdString(), a.value.toStdString());
1143         }
1144     }
1145
1146     saveSettings();
1147 }
1148
1149 void QtLauncher::onQuit()
1150 {
1151     reject();
1152 }
1153
1154 void QtLauncher::onSearchAirports()
1155 {
1156     QString search = m_ui->airportEdit->text();
1157     m_airportsModel->setSearch(search);
1158
1159     if (m_airportsModel->isSearchActive()) {
1160         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
1161         m_ui->locationStack->setCurrentIndex(2);
1162     } else if (m_airportsModel->rowCount(QModelIndex()) == 1) {
1163         QString ident = m_airportsModel->firstIdent();
1164         setAirport(FGAirport::findByIdent(ident.toStdString()));
1165         m_ui->locationStack->setCurrentIndex(0);
1166     }
1167 }
1168
1169 void QtLauncher::onAirportSearchComplete()
1170 {
1171     int numResults = m_airportsModel->rowCount(QModelIndex());
1172     if (numResults == 0) {
1173         m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text()));
1174     } else if (numResults == 1) {
1175         QString ident = m_airportsModel->firstIdent();
1176         setAirport(FGAirport::findByIdent(ident.toStdString()));
1177         m_ui->locationStack->setCurrentIndex(0);
1178     } else {
1179         m_ui->locationStack->setCurrentIndex(1);
1180     }
1181 }
1182
1183 void QtLauncher::onAirportChanged()
1184 {
1185     m_ui->runwayCombo->setEnabled(m_selectedAirport);
1186     m_ui->parkingCombo->setEnabled(m_selectedAirport);
1187     m_ui->airportDiagram->setAirport(m_selectedAirport);
1188
1189     m_ui->runwayRadio->setChecked(true); // default back to runway mode
1190     // unelss multiplayer is enabled ?
1191
1192     if (!m_selectedAirport) {
1193         m_ui->airportDescription->setText(QString());
1194         m_ui->airportDiagram->setEnabled(false);
1195         return;
1196     }
1197
1198     m_ui->airportDiagram->setEnabled(true);
1199
1200     m_ui->runwayCombo->clear();
1201     m_ui->runwayCombo->addItem("Automatic", -1);
1202     for (unsigned int r=0; r<m_selectedAirport->numRunways(); ++r) {
1203         FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r);
1204         // add runway with index as data role
1205         m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
1206
1207         m_ui->airportDiagram->addRunway(rwy);
1208     }
1209
1210     m_ui->parkingCombo->clear();
1211     FGAirportDynamics* dynamics = m_selectedAirport->getDynamics();
1212     PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(
1213                                                                             m_selectedAirport->guid(),
1214                                                                             FGPositioned::PARKING);
1215     if (parkings.empty()) {
1216         m_ui->parkingCombo->setEnabled(false);
1217         m_ui->parkingRadio->setEnabled(false);
1218     } else {
1219         m_ui->parkingCombo->setEnabled(true);
1220         m_ui->parkingRadio->setEnabled(true);
1221         Q_FOREACH(PositionedID parking, parkings) {
1222             FGParking* park = dynamics->getParking(parking);
1223             m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
1224                                         static_cast<qlonglong>(parking));
1225
1226             m_ui->airportDiagram->addParking(park);
1227         }
1228     }
1229 }
1230
1231 void QtLauncher::updateAirportDescription()
1232 {
1233     if (!m_selectedAirport) {
1234         m_ui->airportDescription->setText(QString("No airport selected"));
1235         return;
1236     }
1237
1238     QString ident = QString::fromStdString(m_selectedAirport->ident()),
1239         name = QString::fromStdString(m_selectedAirport->name());
1240     QString locationOnAirport;
1241     if (m_ui->runwayRadio->isChecked()) {
1242         bool onFinal = m_ui->onFinalCheckbox->isChecked();
1243         QString runwayName = (m_ui->runwayCombo->currentIndex() == 0) ?
1244             "active runway" :
1245             QString("runway %1").arg(m_ui->runwayCombo->currentText());
1246
1247         if (onFinal) {
1248             locationOnAirport = QString("on 10-mile final to %1").arg(runwayName);
1249         } else {
1250             locationOnAirport = QString("on %1").arg(runwayName);
1251         }
1252     } else if (m_ui->parkingRadio->isChecked()) {
1253         locationOnAirport =  QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
1254     }
1255
1256     m_ui->airportDescription->setText(QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport));
1257 }
1258
1259 void QtLauncher::onAirportChoiceSelected(const QModelIndex& index)
1260 {
1261     m_ui->locationStack->setCurrentIndex(0);
1262     setAirport(FGPositioned::loadById<FGAirport>(index.data(Qt::UserRole).toULongLong()));
1263 }
1264
1265 void QtLauncher::onAircraftSelected(const QModelIndex& index)
1266 {
1267     m_selectedAircraft = index.data(AircraftPathRole).toString();
1268     updateSelectedAircraft();
1269 }
1270
1271 void QtLauncher::updateSelectedAircraft()
1272 {
1273     try {
1274         QFileInfo info(m_selectedAircraft);
1275         AircraftItem item(info.dir(), m_selectedAircraft);
1276         m_ui->thumbnail->setPixmap(item.thumbnail);
1277         m_ui->aircraftDescription->setText(item.description);
1278     } catch (sg_exception& e) {
1279         m_ui->thumbnail->setPixmap(QPixmap());
1280         m_ui->aircraftDescription->setText("");
1281     }
1282 }
1283
1284 void QtLauncher::onPopupAirportHistory()
1285 {
1286     if (m_recentAirports.isEmpty()) {
1287         return;
1288     }
1289
1290     QMenu m;
1291     Q_FOREACH(QString aptCode, m_recentAirports) {
1292         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
1293         QString name = QString::fromStdString(apt->name());
1294         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
1295         act->setData(aptCode);
1296     }
1297
1298     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
1299     QAction* triggered = m.exec(popupPos);
1300     if (triggered) {
1301         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
1302         setAirport(apt);
1303         m_ui->airportEdit->clear();
1304         m_ui->locationStack->setCurrentIndex(0);
1305     }
1306 }
1307
1308 QModelIndex QtLauncher::proxyIndexForAircraftPath(QString path) const
1309 {
1310   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftPath(path));
1311 }
1312
1313 QModelIndex QtLauncher::sourceIndexForAircraftPath(QString path) const
1314 {
1315     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1316     Q_ASSERT(sourceModel);
1317     return sourceModel->indexOfAircraftPath(path);
1318 }
1319
1320 void QtLauncher::onPopupAircraftHistory()
1321 {
1322     if (m_recentAircraft.isEmpty()) {
1323         return;
1324     }
1325
1326     QMenu m;
1327     Q_FOREACH(QString path, m_recentAircraft) {
1328         QModelIndex index = sourceIndexForAircraftPath(path);
1329         if (!index.isValid()) {
1330             // not scanned yet
1331             continue;
1332         }
1333         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1334         act->setData(path);
1335     }
1336
1337     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1338     QAction* triggered = m.exec(popupPos);
1339     if (triggered) {
1340         m_selectedAircraft = triggered->data().toString();
1341         QModelIndex index = proxyIndexForAircraftPath(m_selectedAircraft);
1342         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1343                                                               QItemSelectionModel::ClearAndSelect);
1344         m_ui->aircraftFilter->clear();
1345         updateSelectedAircraft();
1346     }
1347 }
1348
1349 void QtLauncher::setAirport(FGAirportRef ref)
1350 {
1351     if (m_selectedAirport == ref)
1352         return;
1353
1354     m_selectedAirport = ref;
1355     onAirportChanged();
1356
1357     if (ref.valid()) {
1358         // maintain the recent airport list
1359         QString icao = QString::fromStdString(ref->ident());
1360         if (m_recentAirports.contains(icao)) {
1361             // move to front
1362             m_recentAirports.removeOne(icao);
1363             m_recentAirports.push_front(icao);
1364         } else {
1365             // insert and trim list if necessary
1366             m_recentAirports.push_front(icao);
1367             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
1368                 m_recentAirports.pop_back();
1369             }
1370         }
1371     }
1372
1373     updateAirportDescription();
1374 }
1375
1376 void QtLauncher::onOpenCustomAircraftDir()
1377 {
1378     QFileInfo info(m_customAircraftDir);
1379     if (!info.exists()) {
1380         int result = QMessageBox::question(this, "Create folder?",
1381                                            "The custom aircraft folder does not exist, create it now?",
1382                                            QMessageBox::Yes | QMessageBox::No,
1383                                            QMessageBox::Yes);
1384         if (result == QMessageBox::No) {
1385             return;
1386         }
1387
1388         QDir d(m_customAircraftDir);
1389         d.mkpath(m_customAircraftDir);
1390     }
1391
1392   QUrl u = QUrl::fromLocalFile(m_customAircraftDir);
1393   QDesktopServices::openUrl(u);
1394 }
1395
1396 void QtLauncher::onEditRatingsFilter()
1397 {
1398     EditRatingsFilterDialog dialog(this);
1399     dialog.setRatings(m_ratingFilters);
1400
1401     dialog.exec();
1402     if (dialog.result() == QDialog::Accepted) {
1403         QVariantList vl;
1404         for (int i=0; i<4; ++i) {
1405             m_ratingFilters[i] = dialog.getRating(i);
1406             vl.append(m_ratingFilters[i]);
1407         }
1408         m_aircraftProxy->setRatings(m_ratingFilters);
1409
1410         QSettings settings;
1411         settings.setValue("min-ratings", vl);
1412     }
1413 }
1414
1415 void QtLauncher::updateSettingsSummary()
1416 {
1417     QStringList summary;
1418     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1419         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1420     }
1421
1422     if (m_ui->seasonCombo->currentIndex() > 0) {
1423         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1424     }
1425
1426     if (m_ui->rembrandtCheckbox->isChecked()) {
1427         summary.append("Rembrandt enabled");
1428     } else if (m_ui->msaaCheckbox->isChecked()) {
1429         summary.append("anti-aliasing");
1430     }
1431
1432     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1433         summary.append("live weather");
1434     }
1435
1436     if (m_ui->terrasyncCheck->isChecked()) {
1437         summary.append("automatic scenery downloads");
1438     }
1439
1440     if (m_ui->startPausedCheck->isChecked()) {
1441         summary.append("paused");
1442     }
1443
1444     QString s = summary.join(", ");
1445     s[0] = s[0].toUpper();
1446     m_ui->settingsDescription->setText(s);
1447 }
1448
1449 void QtLauncher::onAddSceneryPath()
1450 {
1451     QString path = QFileDialog::getExistingDirectory(this, tr("Choose scenery folder"));
1452     if (!path.isEmpty()) {
1453         m_ui->sceneryPathsList->addItem(path);
1454         saveSettings();
1455     }
1456 }
1457
1458 void QtLauncher::onRemoveSceneryPath()
1459 {
1460     if (m_ui->sceneryPathsList->currentItem()) {
1461         delete m_ui->sceneryPathsList->currentItem();
1462         saveSettings();
1463     }
1464 }
1465
1466 void QtLauncher::onRembrandtToggled(bool b)
1467 {
1468     // Rembrandt and multi-sample are exclusive
1469     m_ui->msaaCheckbox->setEnabled(!b);
1470 }
1471
1472 #include "QtLauncher.moc"
1473