]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Only set AUTOMOC for fglauncher target.
[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
28 // Simgear
29 #include <simgear/timing/timestamp.hxx>
30 #include <simgear/props/props_io.hxx>
31 #include <simgear/structure/exception.hxx>
32 #include <simgear/misc/sg_path.hxx>
33
34 #include "ui_Launcher.h"
35 #include "EditRatingsFilterDialog.hxx"
36
37 #include <Main/globals.hxx>
38 #include <Navaids/NavDataCache.hxx>
39 #include <Airports/airport.hxx>
40 #include <Airports/dynamics.hxx> // for parking
41 #include <Main/options.hxx>
42
43 using namespace flightgear;
44
45 const int MAX_RECENT_AIRPORTS = 32;
46 const int MAX_RECENT_AIRCRAFT = 20;
47
48 namespace { // anonymous namespace
49
50 const int AircraftPathRole = Qt::UserRole + 1;
51 const int AircraftAuthorsRole = Qt::UserRole + 2;
52 const int AircraftRatingRole = Qt::UserRole + 100;
53
54 void initNavCache()
55 {
56     NavDataCache* cache = NavDataCache::instance();
57     if (cache->isRebuildRequired()) {
58         QProgressDialog rebuildProgress("Initialising navigation data, this may take several minutes",
59                                        QString() /* cancel text */,
60                                        0, 0);
61         rebuildProgress.setWindowModality(Qt::WindowModal);
62         rebuildProgress.show();
63
64         while (!cache->rebuild()) {
65             // sleep to give the rebuild thread more time
66             SGTimeStamp::sleepForMSec(50);
67             rebuildProgress.setValue(0);
68             QCoreApplication::processEvents();
69         }
70     }
71 }
72
73 struct AircraftItem
74 {
75     AircraftItem() {
76         // oh for C++11 initialisers
77         for (int i=0; i<4; ++i) ratings[i] = 0;
78     }
79
80     AircraftItem(QDir dir, QString filePath)
81     {
82         for (int i=0; i<4; ++i) ratings[i] = 0;
83
84         SGPropertyNode root;
85         readProperties(filePath.toStdString(), &root);
86
87         if (!root.hasChild("sim")) {
88             throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
89         }
90
91         SGPropertyNode_ptr sim = root.getNode("sim");
92
93         path = filePath;
94         description = sim->getStringValue("description");
95         authors =  sim->getStringValue("author");
96
97         if (sim->hasChild("rating")) {
98             parseRatings(sim->getNode("rating"));
99         }
100
101         if (dir.exists("thumbnail.jpg")) {
102             thumbnail.load(dir.filePath("thumbnail.jpg"));
103             // resize to the standard size
104             if (thumbnail.height() > 128) {
105                 thumbnail = thumbnail.scaledToHeight(128);
106             }
107         }
108
109     }
110
111     QString path;
112     QPixmap thumbnail;
113     QString description;
114     QString authors;
115     int ratings[4];
116
117 private:
118     void parseRatings(SGPropertyNode_ptr ratingsNode)
119     {
120         ratings[0] = ratingsNode->getIntValue("FDM");
121         ratings[1] = ratingsNode->getIntValue("systems");
122         ratings[2] = ratingsNode->getIntValue("cockpit");
123         ratings[3] = ratingsNode->getIntValue("model");
124     }
125 };
126
127 class AircraftScanThread : public QThread
128 {
129     Q_OBJECT
130 public:
131     AircraftScanThread(QStringList dirsToScan) :
132         m_dirs(dirsToScan),
133         m_done(false)
134     {
135
136     }
137
138     /** thread-safe access to items already scanned */
139     QList<AircraftItem> items()
140     {
141         QList<AircraftItem> result;
142         QMutexLocker g(&m_lock);
143         result.swap(m_items);
144         g.unlock();
145         return result;
146     }
147
148     void setDone()
149     {
150         m_done = true;
151     }
152 Q_SIGNALS:
153     void addedItems();
154
155 protected:
156     virtual void run()
157     {
158         Q_FOREACH(QString d, m_dirs) {
159             scanAircraftDir(QDir(d));
160             if (m_done) {
161                 return;
162             }
163         }
164     }
165
166 private:
167     void scanAircraftDir(QDir path)
168     {
169         QStringList filters;
170         filters << "*-set.xml";
171         Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
172             QDir childDir(child.absoluteFilePath());
173             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
174                 try {
175                     AircraftItem item(childDir, xmlChild.absoluteFilePath());
176                     // lock mutex whil we modify the items array
177                     {
178                         QMutexLocker g(&m_lock);
179                         m_items.append(item);
180                     }
181                 } catch (sg_exception& e) {
182                     continue;
183                 }
184
185                 if (m_done) {
186                     return;
187                 }
188             } // of set.xml iteration
189
190             emit addedItems();
191         } // of subdir iteration
192     }
193
194     QMutex m_lock;
195     QStringList m_dirs;
196     QList<AircraftItem> m_items;
197     bool m_done;
198 };
199
200 class AircraftItemModel : public QAbstractListModel
201 {
202     Q_OBJECT
203 public:
204     AircraftItemModel(QObject* pr) :
205         QAbstractListModel(pr)
206     {
207         QStringList dirs;
208         Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
209             dirs << QString::fromStdString(ap);
210         }
211
212         SGPath rootAircraft(globals->get_fg_root());
213         rootAircraft.append("Aircraft");
214         dirs << QString::fromStdString(rootAircraft.str());
215
216         m_scanThread = new AircraftScanThread(dirs);
217         connect(m_scanThread, &AircraftScanThread::finished, this,
218                 &AircraftItemModel::onScanFinished);
219         connect(m_scanThread, &AircraftScanThread::addedItems,
220                 this, &AircraftItemModel::onScanResults);
221         m_scanThread->start();
222     }
223
224     ~AircraftItemModel()
225     {
226         if (m_scanThread) {
227             m_scanThread->setDone();
228             m_scanThread->wait(1000);
229             delete m_scanThread;
230         }
231     }
232
233     virtual int rowCount(const QModelIndex& parent) const
234     {
235         return m_items.size();
236     }
237
238     virtual QVariant data(const QModelIndex& index, int role) const
239     {
240         const AircraftItem& item(m_items.at(index.row()));
241         if (role == Qt::DisplayRole) {
242             return item.description;
243         } else if (role == Qt::DecorationRole) {
244             return item.thumbnail;
245         } else if (role == AircraftPathRole) {
246             return item.path;
247         } else if (role == AircraftAuthorsRole) {
248             return item.authors;
249         } else if (role >= AircraftRatingRole) {
250             return item.ratings[role - AircraftRatingRole];
251         } else if (role == Qt::ToolTipRole) {
252             return item.path;
253         }
254
255         return QVariant();
256     }
257
258   QModelIndex indexOfAircraftPath(QString path) const
259   {
260       for (int row=0; row <m_items.size(); ++row) {
261           const AircraftItem& item(m_items.at(row));
262           if (item.path == path) {
263               return index(row);
264           }
265       }
266
267       return QModelIndex();
268   }
269
270 private slots:
271     void onScanResults()
272     {
273         QList<AircraftItem> newItems = m_scanThread->items();
274         if (newItems.isEmpty())
275             return;
276
277         int firstRow = m_items.count();
278         int lastRow = firstRow + newItems.count() - 1;
279         beginInsertRows(QModelIndex(), firstRow, lastRow);
280         m_items.append(newItems);
281         endInsertRows();
282     }
283
284     void onScanFinished()
285     {
286         delete m_scanThread;
287         m_scanThread = NULL;
288     }
289
290 private:
291     AircraftScanThread* m_scanThread;
292     QList<AircraftItem> m_items;
293 };
294
295 class AircraftItemDelegate : public QStyledItemDelegate
296 {
297 public:
298     static const int MARGIN = 4;
299
300     virtual void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
301     {
302         // selection feedback rendering
303         if (option.state & QStyle::State_Selected) {
304             QLinearGradient grad(option.rect.topLeft(), option.rect.bottomLeft());
305             grad.setColorAt(0.0, QColor(152, 163, 180));
306             grad.setColorAt(1.0, QColor(90, 107, 131));
307
308             QBrush backgroundBrush(grad);
309             painter->fillRect(option.rect, backgroundBrush);
310
311             painter->setPen(QColor(90, 107, 131));
312             painter->drawLine(option.rect.topLeft(), option.rect.topRight());
313
314         }
315
316         QRect contentRect = option.rect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
317
318         QPixmap thumbnail = index.data(Qt::DecorationRole).value<QPixmap>();
319         painter->drawPixmap(contentRect.topLeft(), thumbnail);
320
321         // draw 1px frame
322         painter->setPen(QColor(0x7f, 0x7f, 0x7f));
323         painter->setBrush(Qt::NoBrush);
324         painter->drawRect(contentRect.left(), contentRect.top(), thumbnail.width(), thumbnail.height());
325
326         QString description = index.data(Qt::DisplayRole).toString();
327         contentRect.setLeft(contentRect.left() + MARGIN + thumbnail.width());
328
329         painter->setPen(Qt::black);
330         QFont f;
331         f.setPointSize(18);
332         painter->setFont(f);
333
334         QRect actualBounds;
335         painter->drawText(contentRect, Qt::TextWordWrap, description, &actualBounds);
336
337         QString authors = index.data(AircraftAuthorsRole).toString();
338
339         f.setPointSize(12);
340         painter->setFont(f);
341
342         QRect authorsRect = contentRect;
343         authorsRect.moveTop(actualBounds.bottom() + MARGIN);
344         painter->drawText(authorsRect, Qt::TextWordWrap,
345                           QString("by: %1").arg(authors),
346                           &actualBounds);
347
348         QRect r = contentRect;
349         r.setWidth(contentRect.width() / 2);
350         r.moveTop(actualBounds.bottom() + MARGIN);
351         r.setHeight(24);
352
353         drawRating(painter, "Flight model:", r, index.data(AircraftRatingRole).toInt());
354         r.moveTop(r.bottom());
355         drawRating(painter, "Systems:", r, index.data(AircraftRatingRole + 1).toInt());
356
357         r.moveTop(actualBounds.bottom() + MARGIN);
358         r.moveLeft(r.right());
359         drawRating(painter, "Cockpit:", r, index.data(AircraftRatingRole + 2).toInt());
360         r.moveTop(r.bottom());
361         drawRating(painter, "Exterior model:", r, index.data(AircraftRatingRole + 3).toInt());
362
363
364     }
365
366     virtual QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
367     {
368         return QSize(500, 128 + (MARGIN * 2));
369     }
370
371 private:
372     void drawRating(QPainter* painter, QString label, const QRect& box, int value) const
373     {
374         const int DOT_SIZE = 10;
375         const int DOT_MARGIN = 4;
376
377         QRect dotBox = box;
378         dotBox.setLeft(box.right() - (DOT_MARGIN * 6 + DOT_SIZE * 5));
379
380         painter->setPen(Qt::black);
381         QRect textBox = box;
382         textBox.setRight(dotBox.left() - DOT_MARGIN);
383         painter->drawText(textBox, Qt::AlignVCenter | Qt::AlignRight, label);
384
385         painter->setPen(Qt::NoPen);
386         QRect dot(dotBox.left() + DOT_MARGIN,
387                   dotBox.center().y() - (DOT_SIZE / 2),
388                   DOT_SIZE,
389                   DOT_SIZE);
390         for (int i=0; i<5; ++i) {
391             painter->setBrush((i < value) ? QColor(0x3f, 0x3f, 0x3f) : QColor(0xaf, 0xaf, 0xaf));
392             painter->drawEllipse(dot);
393             dot.moveLeft(dot.right() + DOT_MARGIN);
394         }
395     }
396 };
397
398 } // of anonymous namespace
399
400 class AirportSearchModel : public QAbstractListModel
401 {
402     Q_OBJECT
403 public:
404     AirportSearchModel() :
405         m_searchActive(false)
406     {
407     }
408
409     void setSearch(QString t)
410     {
411         beginResetModel();
412
413         m_airports.clear();
414         m_ids.clear();
415
416         std::string term(t.toUpper().toStdString());
417         // try ICAO lookup first
418         FGAirportRef ref = FGAirport::findByIdent(term);
419         if (ref) {
420             m_ids.push_back(ref->guid());
421             m_airports.push_back(ref);
422         } else {
423             m_search.reset(new NavDataCache::ThreadedAirportSearch(term));
424             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
425             m_searchActive = true;
426         }
427
428         endResetModel();
429     }
430
431     bool isSearchActive() const
432     {
433         return m_searchActive;
434     }
435
436     virtual int rowCount(const QModelIndex&) const
437     {
438         // if empty, return 1 for special 'no matches'?
439         return m_ids.size();
440     }
441
442     virtual QVariant data(const QModelIndex& index, int role) const
443     {
444         if (!index.isValid())
445             return QVariant();
446         
447         FGAirportRef apt = m_airports[index.row()];
448         if (!apt.valid()) {
449             apt = FGPositioned::loadById<FGAirport>(m_ids[index.row()]);
450             m_airports[index.row()] = apt;
451         }
452
453         if (role == Qt::DisplayRole) {
454             QString name = QString::fromStdString(apt->name());
455             return QString("%1: %2").arg(QString::fromStdString(apt->ident())).arg(name);
456         }
457
458         if (role == Qt::EditRole) {
459             return QString::fromStdString(apt->ident());
460         }
461
462         if (role == Qt::UserRole) {
463             return static_cast<qlonglong>(m_ids[index.row()]);
464         }
465
466         return QVariant();
467     }
468
469     QString firstIdent() const
470     {
471         if (m_ids.empty())
472             return QString();
473
474         if (!m_airports.front().valid()) {
475             m_airports[0] = FGPositioned::loadById<FGAirport>(m_ids.front());
476         }
477
478         return QString::fromStdString(m_airports.front()->ident());
479     }
480
481 Q_SIGNALS:
482     void searchComplete();
483
484 private slots:
485     void onSearchResultsPoll()
486     {
487         PositionedIDVec newIds = m_search->results();
488         
489         beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
490         for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
491             m_ids.push_back(newIds[i]);
492             m_airports.push_back(FGAirportRef()); // null ref
493         }
494         endInsertRows();
495
496         if (m_search->isComplete()) {
497             m_searchActive = false;
498             m_search.reset();
499             emit searchComplete();
500         } else {
501             QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
502         }
503     }
504
505 private:
506     PositionedIDVec m_ids;
507     mutable std::vector<FGAirportRef> m_airports;
508     bool m_searchActive;
509     QScopedPointer<NavDataCache::ThreadedAirportSearch> m_search;
510 };
511
512 class AircraftProxyModel : public QSortFilterProxyModel
513 {
514     Q_OBJECT
515 public:
516     AircraftProxyModel(QObject* pr) :
517         QSortFilterProxyModel(pr),
518         m_ratingsFilter(true)
519     {
520         for (int i=0; i<4; ++i) {
521             m_ratings[i] = 3;
522         }
523     }
524
525     void setRatings(int* ratings)
526     {
527         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
528         invalidate();
529     }
530
531 public slots:
532     void setRatingFilterEnabled(bool e)
533     {
534         if (e == m_ratingsFilter) {
535             return;
536         }
537
538         m_ratingsFilter = e;
539         invalidate();
540     }
541
542 protected:
543     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
544     {
545         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
546             return false;
547         }
548
549         if (m_ratingsFilter) {
550             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
551             for (int i=0; i<4; ++i) {
552                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
553                     return false;
554                 }
555             }
556         }
557
558         return true;
559     }
560
561 private:
562     bool m_ratingsFilter;
563     int m_ratings[4];
564 };
565
566 QtLauncher::QtLauncher() :
567     QDialog(),
568     m_ui(NULL)
569 {
570     m_ui.reset(new Ui::Launcher);
571     m_ui->setupUi(this);
572
573     for (int i=0; i<4; ++i) {
574         m_ratingFilters[i] = 3;
575     }
576
577     m_airportsModel = new AirportSearchModel;
578     m_ui->searchList->setModel(m_airportsModel);
579     connect(m_ui->searchList, &QListView::clicked,
580             this, &QtLauncher::onAirportChoiceSelected);
581     connect(m_airportsModel, &AirportSearchModel::searchComplete,
582             this, &QtLauncher::onAirportSearchComplete);
583
584     SGPath p = SGPath::documents();
585     p.append("FlightGear");
586     p.append("Aircraft");
587     m_customAircraftDir = QString::fromStdString(p.str());
588     m_ui->customAircraftDirLabel->setText(QString("Custom aircraft folder: %1").arg(m_customAircraftDir));
589
590     globals->append_aircraft_path(m_customAircraftDir.toStdString());
591
592     // create and configure the proxy model
593     m_aircraftProxy = new AircraftProxyModel(this);
594     m_aircraftProxy->setSourceModel(new AircraftItemModel(this));
595
596     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
597     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
598     m_aircraftProxy->setSortRole(Qt::DisplayRole);
599     m_aircraftProxy->setDynamicSortFilter(true);
600
601     m_ui->aircraftList->setModel(m_aircraftProxy);
602     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
603     m_ui->aircraftList->setItemDelegate(new AircraftItemDelegate);
604     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
605     connect(m_ui->aircraftList, &QListView::clicked,
606             this, &QtLauncher::onAircraftSelected);
607
608     connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
609             this, SLOT(updateAirportDescription()));
610     connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
611             this, SLOT(updateAirportDescription()));
612     connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
613             this, SLOT(updateAirportDescription()));
614     connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
615             this, SLOT(updateAirportDescription()));
616     connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
617             this, SLOT(updateAirportDescription()));
618
619
620     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
621     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
622     connect(m_ui->airportEdit, SIGNAL(returnPressed()),
623             this, SLOT(onSearchAirports()));
624
625     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
626             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
627
628     connect(m_ui->airportHistory, &QPushButton::clicked,
629             this, &QtLauncher::onPopupAirportHistory);
630     connect(m_ui->aircraftHistory, &QPushButton::clicked,
631           this, &QtLauncher::onPopupAircraftHistory);
632
633     restoreSettings();
634
635     connect(m_ui->openAircraftDirButton, &QPushButton::clicked,
636           this, &QtLauncher::onOpenCustomAircraftDir);
637
638     QAction* qa = new QAction(this);
639     qa->setShortcut(QKeySequence("Ctrl+Q"));
640     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
641     addAction(qa);
642
643     connect(m_ui->editRatingFilter, &QPushButton::clicked,
644             this, &QtLauncher::onEditRatingsFilter);
645     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
646             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
647
648     QIcon historyIcon(":/history-icon");
649     m_ui->aircraftHistory->setIcon(historyIcon);
650     m_ui->airportHistory->setIcon(historyIcon);
651
652     m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
653
654     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
655             this, SLOT(updateSettingsSummary()));
656     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
657             this, SLOT(updateSettingsSummary()));
658     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
659             this, SLOT(updateSettingsSummary()));
660     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
661             this, SLOT(updateSettingsSummary()));
662     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
663             this, SLOT(updateSettingsSummary()));
664
665     updateSettingsSummary();
666
667     connect(m_ui->addSceneryPath, &QToolButton::clicked,
668             this, &QtLauncher::onAddSceneryPath);
669     connect(m_ui->removeSceneryPath, &QToolButton::clicked,
670             this, &QtLauncher::onRemoveSceneryPath);
671 }
672
673 QtLauncher::~QtLauncher()
674 {
675     
676 }
677
678 bool QtLauncher::runLauncherDialog()
679 {
680      Q_INIT_RESOURCE(resources);
681
682     // startup the nav-cache now. This pre-empts normal startup of
683     // the cache, but no harm done. (Providing scenery paths are consistent)
684
685     initNavCache();
686
687   // setup scenery paths now, especially TerraSync path for airport
688   // parking locations (after they're downloaded)
689
690     QtLauncher dlg;
691     dlg.exec();
692     if (dlg.result() != QDialog::Accepted) {
693         return false;
694     }
695
696     return true;
697 }
698
699 void QtLauncher::restoreSettings()
700 {
701     QSettings settings;
702     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
703     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
704     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
705     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
706     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
707     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
708     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
709
710     // full paths to -set.xml files
711     m_recentAircraft = settings.value("recent-aircraft").toStringList();
712
713     if (!m_recentAircraft.empty()) {
714         m_selectedAircraft = m_recentAircraft.front();
715     } else {
716         // select the default C172p
717     }
718
719     updateSelectedAircraft();
720
721     // ICAO identifiers
722     m_recentAirports = settings.value("recent-airports").toStringList();
723     if (!m_recentAirports.empty()) {
724         setAirport(FGAirport::findByIdent(m_recentAirports.front().toStdString()));
725     }
726     updateAirportDescription();
727
728     // rating filters
729     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
730     int index = 0;
731     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
732         m_ratingFilters[index++] = v.toInt();
733     }
734
735     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
736     m_aircraftProxy->setRatings(m_ratingFilters);
737
738     QStringList sceneryPaths = settings.value("scenery-paths").toStringList();
739     m_ui->sceneryPathsList->addItems(sceneryPaths);
740 }
741
742 void QtLauncher::saveSettings()
743 {
744     QSettings settings;
745     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
746     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
747     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
748     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
749     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
750     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
751     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
752     settings.setValue("recent-aircraft", m_recentAircraft);
753     settings.setValue("recent-airports", m_recentAirports);
754     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
755
756     QStringList paths;
757     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
758         paths.append(m_ui->sceneryPathsList->item(i)->text());
759     }
760
761     settings.setValue("scenery-paths", paths);
762 }
763
764 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
765 {
766     flightgear::Options* opt = flightgear::Options::sharedInstance();
767     std::string stdName(name.toStdString());
768     if (cbox->isChecked()) {
769         opt->addOption("enable-" + stdName, "");
770     } else {
771         opt->addOption("disable-" + stdName, "");
772     }
773 }
774
775 void QtLauncher::onRun()
776 {
777     accept();
778
779     flightgear::Options* opt = flightgear::Options::sharedInstance();
780     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
781     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
782     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
783     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
784     setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
785
786     // aircraft
787     if (!m_selectedAircraft.isEmpty()) {
788         QFileInfo setFileInfo(m_selectedAircraft);
789         opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
790         QString setFile = setFileInfo.fileName();
791         Q_ASSERT(setFile.endsWith("-set.xml"));
792         setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
793         opt->addOption("aircraft", setFile.toStdString());
794
795       // manage aircraft history
796         if (m_recentAircraft.contains(m_selectedAircraft))
797           m_recentAircraft.removeOne(m_selectedAircraft);
798         m_recentAircraft.prepend(m_selectedAircraft);
799         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
800           m_recentAircraft.pop_back();
801     }
802
803     // airport / location
804     if (m_selectedAirport) {
805         opt->addOption("airport", m_selectedAirport->ident());
806     }
807
808     if (m_ui->runwayRadio->isChecked()) {
809         int index = m_ui->runwayCombo->currentData().toInt();
810         if (index >= 0) {
811             // explicit runway choice
812             opt->addOption("runway", m_selectedAirport->getRunwayByIndex(index)->ident());
813         }
814
815         if (m_ui->onFinalCheckbox->isChecked()) {
816             opt->addOption("glideslope", "3.0");
817             opt->addOption("offset-distance", "10.0"); // in nautical miles
818         }
819     } else if (m_ui->parkingRadio->isChecked()) {
820         // parking selection
821         opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
822     }
823
824     // time of day
825     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
826         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
827         opt->addOption("timeofday", dayval.toStdString());
828     }
829
830     // scenery paths
831     for (int i=0; i<m_ui->sceneryPathsList->count(); ++i) {
832         QString path = m_ui->sceneryPathsList->item(i)->text();
833         opt->addOption("fg-scenery", path.toStdString());
834     }
835
836     saveSettings();
837 }
838
839 void QtLauncher::onQuit()
840 {
841     reject();
842 }
843
844 void QtLauncher::onSearchAirports()
845 {
846     QString search = m_ui->airportEdit->text();
847     m_airportsModel->setSearch(search);
848
849     if (m_airportsModel->isSearchActive()) {
850         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
851         m_ui->locationStack->setCurrentIndex(2);
852     } else if (m_airportsModel->rowCount(QModelIndex()) == 1) {
853         QString ident = m_airportsModel->firstIdent();
854         setAirport(FGAirport::findByIdent(ident.toStdString()));
855         m_ui->locationStack->setCurrentIndex(0);
856     }
857 }
858
859 void QtLauncher::onAirportSearchComplete()
860 {
861     int numResults = m_airportsModel->rowCount(QModelIndex());
862     if (numResults == 0) {
863         m_ui->searchStatusText->setText(QString("No matching airports for '%1'").arg(m_ui->airportEdit->text()));
864     } else if (numResults == 1) {
865         QString ident = m_airportsModel->firstIdent();
866         setAirport(FGAirport::findByIdent(ident.toStdString()));
867         m_ui->locationStack->setCurrentIndex(0);
868     } else {
869         m_ui->locationStack->setCurrentIndex(1);
870     }
871 }
872
873 void QtLauncher::onAirportChanged()
874 {
875     m_ui->runwayCombo->setEnabled(m_selectedAirport);
876     m_ui->parkingCombo->setEnabled(m_selectedAirport);
877     m_ui->airportDiagram->setAirport(m_selectedAirport);
878
879     m_ui->runwayRadio->setChecked(true); // default back to runway mode
880     // unelss multiplayer is enabled ?
881
882     if (!m_selectedAirport) {
883         m_ui->airportDescription->setText(QString());
884         m_ui->airportDiagram->setEnabled(false);
885         return;
886     }
887
888     m_ui->airportDiagram->setEnabled(true);
889
890     m_ui->runwayCombo->clear();
891     m_ui->runwayCombo->addItem("Automatic", -1);
892     for (unsigned int r=0; r<m_selectedAirport->numRunways(); ++r) {
893         FGRunwayRef rwy = m_selectedAirport->getRunwayByIndex(r);
894         // add runway with index as data role
895         m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
896
897         m_ui->airportDiagram->addRunway(rwy);
898     }
899
900     m_ui->parkingCombo->clear();
901     FGAirportDynamics* dynamics = m_selectedAirport->getDynamics();
902     PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(
903                                                                             m_selectedAirport->guid(),
904                                                                             FGPositioned::PARKING);
905     if (parkings.empty()) {
906         m_ui->parkingCombo->setEnabled(false);
907         m_ui->parkingRadio->setEnabled(false);
908     } else {
909         m_ui->parkingCombo->setEnabled(true);
910         m_ui->parkingRadio->setEnabled(true);
911         Q_FOREACH(PositionedID parking, parkings) {
912             FGParking* park = dynamics->getParking(parking);
913             m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
914                                         static_cast<qlonglong>(parking));
915
916             m_ui->airportDiagram->addParking(park);
917         }
918     }
919 }
920
921 void QtLauncher::updateAirportDescription()
922 {
923     if (!m_selectedAirport) {
924         m_ui->airportDescription->setText(QString("No airport selected"));
925         return;
926     }
927
928     QString ident = QString::fromStdString(m_selectedAirport->ident()),
929         name = QString::fromStdString(m_selectedAirport->name());
930     QString locationOnAirport;
931     if (m_ui->runwayRadio->isChecked()) {
932         bool onFinal = m_ui->onFinalCheckbox->isChecked();
933         QString runwayName = (m_ui->runwayCombo->currentIndex() == 0) ?
934             "active runway" :
935             QString("runway %1").arg(m_ui->runwayCombo->currentText());
936
937         if (onFinal) {
938             locationOnAirport = QString("on 10-mile final to %1").arg(runwayName);
939         } else {
940             locationOnAirport = QString("on %1").arg(runwayName);
941         }
942     } else if (m_ui->parkingRadio->isChecked()) {
943         locationOnAirport =  QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
944     }
945
946     m_ui->airportDescription->setText(QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport));
947 }
948
949 void QtLauncher::onAirportChoiceSelected(const QModelIndex& index)
950 {
951     m_ui->locationStack->setCurrentIndex(0);
952     setAirport(FGPositioned::loadById<FGAirport>(index.data(Qt::UserRole).toULongLong()));
953 }
954
955 void QtLauncher::onAircraftSelected(const QModelIndex& index)
956 {
957     m_selectedAircraft = index.data(AircraftPathRole).toString();
958     updateSelectedAircraft();
959 }
960
961 void QtLauncher::updateSelectedAircraft()
962 {
963     try {
964         QFileInfo info(m_selectedAircraft);
965         AircraftItem item(info.dir(), m_selectedAircraft);
966         m_ui->thumbnail->setPixmap(item.thumbnail);
967         m_ui->aircraftDescription->setText(item.description);
968     } catch (sg_exception& e) {
969         m_ui->thumbnail->setPixmap(QPixmap());
970         m_ui->aircraftDescription->setText("");
971     }
972 }
973
974 void QtLauncher::onPopupAirportHistory()
975 {
976     if (m_recentAirports.isEmpty()) {
977         return;
978     }
979
980     QMenu m;
981     Q_FOREACH(QString aptCode, m_recentAirports) {
982         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
983         QString name = QString::fromStdString(apt->name());
984         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
985         act->setData(aptCode);
986     }
987
988     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
989     QAction* triggered = m.exec(popupPos);
990     if (triggered) {
991         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
992         setAirport(apt);
993         m_ui->airportEdit->clear();
994         m_ui->locationStack->setCurrentIndex(0);
995     }
996 }
997
998 QModelIndex QtLauncher::proxyIndexForAircraftPath(QString path) const
999 {
1000   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftPath(path));
1001 }
1002
1003 QModelIndex QtLauncher::sourceIndexForAircraftPath(QString path) const
1004 {
1005     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1006     Q_ASSERT(sourceModel);
1007     return sourceModel->indexOfAircraftPath(path);
1008 }
1009
1010 void QtLauncher::onPopupAircraftHistory()
1011 {
1012     if (m_recentAircraft.isEmpty()) {
1013         return;
1014     }
1015
1016     QMenu m;
1017     Q_FOREACH(QString path, m_recentAircraft) {
1018         QModelIndex index = sourceIndexForAircraftPath(path);
1019         if (!index.isValid()) {
1020             // not scanned yet
1021             continue;
1022         }
1023         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1024         act->setData(path);
1025     }
1026
1027     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1028     QAction* triggered = m.exec(popupPos);
1029     if (triggered) {
1030         m_selectedAircraft = triggered->data().toString();
1031         QModelIndex index = proxyIndexForAircraftPath(m_selectedAircraft);
1032         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1033                                                               QItemSelectionModel::ClearAndSelect);
1034         m_ui->aircraftFilter->clear();
1035         updateSelectedAircraft();
1036     }
1037 }
1038
1039 void QtLauncher::setAirport(FGAirportRef ref)
1040 {
1041     if (m_selectedAirport == ref)
1042         return;
1043
1044     m_selectedAirport = ref;
1045     onAirportChanged();
1046
1047     if (ref.valid()) {
1048         // maintain the recent airport list
1049         QString icao = QString::fromStdString(ref->ident());
1050         if (m_recentAirports.contains(icao)) {
1051             // move to front
1052             m_recentAirports.removeOne(icao);
1053             m_recentAirports.push_front(icao);
1054         } else {
1055             // insert and trim list if necessary
1056             m_recentAirports.push_front(icao);
1057             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
1058                 m_recentAirports.pop_back();
1059             }
1060         }
1061     }
1062
1063     updateAirportDescription();
1064 }
1065
1066 void QtLauncher::onOpenCustomAircraftDir()
1067 {
1068   QUrl u = QUrl::fromLocalFile(m_customAircraftDir);
1069   QDesktopServices::openUrl(u);
1070 }
1071
1072 void QtLauncher::onEditRatingsFilter()
1073 {
1074     EditRatingsFilterDialog dialog(this);
1075     dialog.setRatings(m_ratingFilters);
1076
1077     dialog.exec();
1078     if (dialog.result() == QDialog::Accepted) {
1079         QVariantList vl;
1080         for (int i=0; i<4; ++i) {
1081             m_ratingFilters[i] = dialog.getRating(i);
1082             vl.append(m_ratingFilters[i]);
1083         }
1084         m_aircraftProxy->setRatings(m_ratingFilters);
1085
1086         QSettings settings;
1087         settings.setValue("min-ratings", vl);
1088     }
1089 }
1090
1091 void QtLauncher::updateSettingsSummary()
1092 {
1093     QStringList summary;
1094     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1095         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1096     }
1097
1098     if (m_ui->rembrandtCheckbox->isChecked()) {
1099         summary.append("Rembrandt enabled");
1100     }
1101
1102     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1103         summary.append("live weather");
1104     }
1105
1106     if (m_ui->terrasyncCheck->isChecked()) {
1107         summary.append("automatic scenery downloads");
1108     }
1109
1110     if (m_ui->startPausedCheck->isChecked()) {
1111         summary.append("paused");
1112     }
1113
1114     QString s = summary.join(", ");
1115     s[0] = s[0].toUpper();
1116     m_ui->settingsDescription->setText(s);
1117 }
1118
1119 void QtLauncher::onAddSceneryPath()
1120 {
1121     QString path = QFileDialog::getExistingDirectory(this, tr("Choose scenery folder"));
1122     if (!path.isEmpty()) {
1123         m_ui->sceneryPathsList->addItem(path);
1124         saveSettings();
1125     }
1126 }
1127
1128 void QtLauncher::onRemoveSceneryPath()
1129 {
1130     if (m_ui->sceneryPathsList->currentItem()) {
1131         delete m_ui->sceneryPathsList->currentItem();
1132         saveSettings();
1133     }
1134 }
1135
1136 #include "QtLauncher.moc"
1137