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