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