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