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