]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Fix some coastline issues.
[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 #include "QtLauncher_private.hxx"
23
24 // Qt
25 #include <QProgressDialog>
26 #include <QCoreApplication>
27 #include <QAbstractListModel>
28 #include <QDir>
29 #include <QFileInfo>
30 #include <QPixmap>
31 #include <QTimer>
32 #include <QDebug>
33 #include <QCompleter>
34 #include <QListView>
35 #include <QSettings>
36 #include <QSortFilterProxyModel>
37 #include <QMenu>
38 #include <QDesktopServices>
39 #include <QUrl>
40 #include <QAction>
41 #include <QFileDialog>
42 #include <QMessageBox>
43 #include <QDateTime>
44 #include <QApplication>
45 #include <QSpinBox>
46 #include <QDoubleSpinBox>
47
48 // Simgear
49 #include <simgear/timing/timestamp.hxx>
50 #include <simgear/props/props_io.hxx>
51 #include <simgear/structure/exception.hxx>
52 #include <simgear/structure/subsystem_mgr.hxx>
53 #include <simgear/misc/sg_path.hxx>
54 #include <simgear/package/Catalog.hxx>
55 #include <simgear/package/Package.hxx>
56 #include <simgear/package/Install.hxx>
57
58 #include "ui_Launcher.h"
59 #include "EditRatingsFilterDialog.hxx"
60 #include "AircraftItemDelegate.hxx"
61 #include "AircraftModel.hxx"
62 #include "PathsDialog.hxx"
63
64 #include <Main/globals.hxx>
65 #include <Navaids/NavDataCache.hxx>
66 #include <Navaids/navrecord.hxx>
67 #include <Navaids/SHPParser.hxx>
68
69 #include <Main/options.hxx>
70 #include <Main/fg_init.hxx>
71 #include <Viewer/WindowBuilder.hxx>
72 #include <Network/HTTPClient.hxx>
73
74 using namespace flightgear;
75 using namespace simgear::pkg;
76
77 const int MAX_RECENT_AIRCRAFT = 20;
78
79 namespace { // anonymous namespace
80
81 void initNavCache()
82 {
83     QString baseLabel = QT_TR_NOOP("Initialising navigation data, this may take several minutes");
84     NavDataCache* cache = NavDataCache::createInstance();
85     if (cache->isRebuildRequired()) {
86         QProgressDialog rebuildProgress(baseLabel,
87                                        QString() /* cancel text */,
88                                        0, 100);
89         rebuildProgress.setWindowModality(Qt::WindowModal);
90         rebuildProgress.show();
91
92         NavDataCache::RebuildPhase phase = cache->rebuild();
93
94         while (phase != NavDataCache::REBUILD_DONE) {
95             // sleep to give the rebuild thread more time
96             SGTimeStamp::sleepForMSec(50);
97             phase = cache->rebuild();
98
99             switch (phase) {
100             case NavDataCache::REBUILD_AIRPORTS:
101                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading airport data"));
102                 break;
103
104             case NavDataCache::REBUILD_FIXES:
105                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading waypoint data"));
106                 break;
107
108             case NavDataCache::REBUILD_NAVAIDS:
109                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading navigation data"));
110                 break;
111
112
113             case NavDataCache::REBUILD_POIS:
114                 rebuildProgress.setLabelText(QT_TR_NOOP("Loading point-of-interest data"));
115                 break;
116
117             default:
118                 rebuildProgress.setLabelText(baseLabel);
119             }
120
121             if (phase == NavDataCache::REBUILD_UNKNOWN) {
122                 rebuildProgress.setValue(0);
123                 rebuildProgress.setMaximum(0);
124             } else {
125                 rebuildProgress.setValue(cache->rebuildPhaseCompletionPercentage());
126                 rebuildProgress.setMaximum(100);
127             }
128
129             QCoreApplication::processEvents();
130         }
131     }
132 }
133
134 class ArgumentsTokenizer
135 {
136 public:
137     class Arg
138     {
139     public:
140         explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
141
142         QString arg;
143         QString value;
144     };
145
146     QList<Arg> tokenize(QString in) const
147     {
148         int index = 0;
149         const int len = in.count();
150         QChar c, nc;
151         State state = Start;
152         QString key, value;
153         QList<Arg> result;
154
155         for (; index < len; ++index) {
156             c = in.at(index);
157             nc = index < (len - 1) ? in.at(index + 1) : QChar();
158
159             switch (state) {
160             case Start:
161                 if (c == QChar('-')) {
162                     if (nc == QChar('-')) {
163                         state = Key;
164                         key.clear();
165                         ++index;
166                     } else {
167                         // should we pemit single hyphen arguments?
168                         // choosing to fail for now
169                         return QList<Arg>();
170                     }
171                 } else if (c == QChar('#')) {
172                     state = Comment;
173                     break;
174                 } else if (c.isSpace()) {
175                     break;
176                 }
177                 break;
178
179             case Key:
180                 if (c == QChar('=')) {
181                     state = Value;
182                     value.clear();
183                 } else if (c.isSpace()) {
184                     state = Start;
185                     result.append(Arg(key));
186                 } else {
187                     // could check for illegal charatcers here
188                     key.append(c);
189                 }
190                 break;
191
192             case Value:
193                 if (c == QChar('"')) {
194                     state = Quoted;
195                 } else if (c.isSpace()) {
196                     state = Start;
197                     result.append(Arg(key, value));
198                 } else {
199                     value.append(c);
200                 }
201                 break;
202
203             case Quoted:
204                 if (c == QChar('\\')) {
205                     // check for escaped double-quote inside quoted value
206                     if (nc == QChar('"')) {
207                         ++index;
208                     }
209                 } else if (c == QChar('"')) {
210                     state = Value;
211                 } else {
212                     value.append(c);
213                 }
214                 break;
215
216             case Comment:
217                 if ((c == QChar('\n')) || (c == QChar('\r'))) {
218                     state = Start;
219                     break;
220                 } else {
221                     // nothing to do, eat comment chars
222                 }
223                 break;
224             } // of state switch
225         } // of character loop
226
227         // ensure last argument isn't lost
228         if (state == Key) {
229             result.append(Arg(key));
230         } else if (state == Value) {
231             result.append(Arg(key, value));
232         }
233
234         return result;
235     }
236
237 private:
238     enum State {
239         Start = 0,
240         Key,
241         Value,
242         Quoted,
243         Comment
244     };
245 };
246
247 } // of anonymous namespace
248
249 class AircraftProxyModel : public QSortFilterProxyModel
250 {
251     Q_OBJECT
252 public:
253     AircraftProxyModel(QObject* pr) :
254         QSortFilterProxyModel(pr),
255         m_ratingsFilter(true)
256     {
257         for (int i=0; i<4; ++i) {
258             m_ratings[i] = 3;
259         }
260     }
261
262     void setRatings(int* ratings)
263     {
264         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
265         invalidate();
266     }
267
268 public slots:
269     void setRatingFilterEnabled(bool e)
270     {
271         if (e == m_ratingsFilter) {
272             return;
273         }
274
275         m_ratingsFilter = e;
276         invalidate();
277     }
278
279 protected:
280     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
281     {
282         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
283             return false;
284         }
285
286         if (m_ratingsFilter) {
287             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
288             for (int i=0; i<4; ++i) {
289                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
290                     return false;
291                 }
292             }
293         }
294
295         return true;
296     }
297
298 private:
299     bool m_ratingsFilter;
300     int m_ratings[4];
301 };
302
303 static void initQtResources()
304 {
305     Q_INIT_RESOURCE(resources);
306 }
307
308 namespace flightgear
309 {
310
311 void initApp(int& argc, char** argv)
312 {
313     sglog().setLogLevels( SG_ALL, SG_INFO );
314     initQtResources(); // can't be called from a namespace
315
316     static bool qtInitDone = false;
317     static int s_argc;
318
319     if (!qtInitDone) {
320         qtInitDone = true;
321         s_argc = argc; // QApplication only stores a reference to argc,
322         // and may crash if it is freed
323         // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication
324
325         QApplication* app = new QApplication(s_argc, argv);
326         app->setOrganizationName("FlightGear");
327         app->setApplicationName("FlightGear");
328         app->setOrganizationDomain("flightgear.org");
329
330         // avoid double Apple menu and other weirdness if both Qt and OSG
331         // try to initialise various Cocoa structures.
332         flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
333
334         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
335         if (mods & Qt::AltModifier) {
336             qWarning() << "Alt pressed during launch";
337
338             // wipe out our settings
339             QSettings settings;
340             settings.clear();
341
342
343             Options::sharedInstance()->addOption("restore-defaults", "");
344         }
345     }
346 }
347
348 void loadNaturalEarthFile(const std::string& aFileName,
349                           flightgear::PolyLine::Type aType,
350                           bool areClosed)
351 {
352     SGPath path(globals->get_fg_root());
353     path.append( "Geodata" );
354     path.append(aFileName);
355     if (!path.exists())
356         return; // silently fail for now
357
358     flightgear::PolyLineList lines;
359     flightgear::SHPParser::parsePolyLines(path, aType, lines, areClosed);
360     flightgear::PolyLineList::iterator it;
361     for (it=lines.begin(); it != lines.end(); ++it) {
362         (*it)->addToSpatialIndex();
363     }
364 }
365
366 void loadNaturalEarthData()
367 {
368     SGTimeStamp st;
369     st.stamp();
370
371     loadNaturalEarthFile("ne_10m_coastline.shp", flightgear::PolyLine::COASTLINE, false);
372     loadNaturalEarthFile("ne_10m_rivers_lake_centerlines.shp", flightgear::PolyLine::RIVER, false);
373     loadNaturalEarthFile("ne_10m_lakes.shp", flightgear::PolyLine::LAKE, true);
374
375     qDebug() << "load basic data took" << st.elapsedMSec();
376
377
378     st.stamp();
379     loadNaturalEarthFile("ne_10m_urban_areas.shp", flightgear::PolyLine::URBAN, true);
380
381     qDebug() << "loading urban areas took:" << st.elapsedMSec();
382 }
383
384 bool runLauncherDialog()
385 {
386     // startup the nav-cache now. This pre-empts normal startup of
387     // the cache, but no harm done. (Providing scenery paths are consistent)
388
389     initNavCache();
390
391     fgInitPackageRoot();
392
393     // startup the HTTP system now since packages needs it
394     FGHTTPClient* http = new FGHTTPClient;
395     globals->add_subsystem("http", http);
396     // we guard against re-init in the global phase; bind and postinit
397     // will happen as normal
398     http->init();
399
400     loadNaturalEarthData();
401
402     // setup scenery paths now, especially TerraSync path for airport
403     // parking locations (after they're downloaded)
404
405     QtLauncher dlg;
406     dlg.exec();
407     if (dlg.result() != QDialog::Accepted) {
408         return false;
409     }
410
411     return true;
412 }
413
414 bool runInAppLauncherDialog()
415 {
416     QtLauncher dlg;
417     dlg.setInAppMode();
418     dlg.exec();
419     if (dlg.result() != QDialog::Accepted) {
420         return false;
421     }
422
423     return true;
424 }
425
426 } // of namespace flightgear
427
428 QtLauncher::QtLauncher() :
429     QDialog(),
430     m_ui(NULL),
431     m_subsystemIdleTimer(NULL),
432     m_inAppMode(false)
433 {
434     m_ui.reset(new Ui::Launcher);
435     m_ui->setupUi(this);
436
437 #if QT_VERSION >= 0x050300
438     // don't require Qt 5.3
439     m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
440 #endif
441
442 #if QT_VERSION >= 0x050200
443     m_ui->aircraftFilter->setClearButtonEnabled(true);
444 #endif
445
446     for (int i=0; i<4; ++i) {
447         m_ratingFilters[i] = 3;
448     }
449
450     m_subsystemIdleTimer = new QTimer(this);
451     m_subsystemIdleTimer->setInterval(0);
452     connect(m_subsystemIdleTimer, &QTimer::timeout,
453             this, &QtLauncher::onSubsytemIdleTimeout);
454     m_subsystemIdleTimer->start();
455
456     // create and configure the proxy model
457     m_aircraftProxy = new AircraftProxyModel(this);
458     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
459             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
460     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
461             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
462
463     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
464     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
465
466     connect(m_ui->aircraftHistory, &QPushButton::clicked,
467           this, &QtLauncher::onPopupAircraftHistory);
468
469     connect(m_ui->location, &LocationWidget::descriptionChanged,
470             m_ui->locationDescription, &QLabel::setText);
471
472     QAction* qa = new QAction(this);
473     qa->setShortcut(QKeySequence("Ctrl+Q"));
474     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
475     addAction(qa);
476
477     connect(m_ui->editRatingFilter, &QPushButton::clicked,
478             this, &QtLauncher::onEditRatingsFilter);
479
480     QIcon historyIcon(":/history-icon");
481     m_ui->aircraftHistory->setIcon(historyIcon);
482
483     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
484             this, SLOT(updateSettingsSummary()));
485     connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
486             this, SLOT(updateSettingsSummary()));
487     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
488             this, SLOT(updateSettingsSummary()));
489     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
490             this, SLOT(updateSettingsSummary()));
491     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
492             this, SLOT(updateSettingsSummary()));
493     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
494             this, SLOT(updateSettingsSummary()));
495     connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
496             this, SLOT(updateSettingsSummary()));
497
498     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
499             this, SLOT(onRembrandtToggled(bool)));
500     connect(m_ui->terrasyncCheck, &QCheckBox::toggled,
501             this, &QtLauncher::onToggleTerrasync);
502     updateSettingsSummary();
503
504     m_aircraftModel = new AircraftItemModel(this, RootRef(globals->packageRoot()));
505     m_aircraftProxy->setSourceModel(m_aircraftModel);
506
507     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
508     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
509     m_aircraftProxy->setSortRole(Qt::DisplayRole);
510     m_aircraftProxy->setDynamicSortFilter(true);
511
512     m_ui->aircraftList->setModel(m_aircraftProxy);
513     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
514     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
515     m_ui->aircraftList->setItemDelegate(delegate);
516     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
517     connect(m_ui->aircraftList, &QListView::clicked,
518             this, &QtLauncher::onAircraftSelected);
519     connect(delegate, &AircraftItemDelegate::variantChanged,
520             this, &QtLauncher::onAircraftSelected);
521     connect(delegate, &AircraftItemDelegate::requestInstall,
522             this, &QtLauncher::onRequestPackageInstall);
523     connect(delegate, &AircraftItemDelegate::cancelDownload,
524             this, &QtLauncher::onCancelDownload);
525
526     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
527             this, &QtLauncher::onAircraftInstalledCompleted);
528     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
529             this, &QtLauncher::onAircraftInstallFailed);
530     connect(m_aircraftModel, &AircraftItemModel::scanCompleted,
531             this, &QtLauncher::updateSelectedAircraft);
532     connect(m_ui->pathsButton, &QPushButton::clicked,
533             this, &QtLauncher::onEditPaths);
534
535     restoreSettings();
536
537     QSettings settings;
538     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
539     m_aircraftModel->scanDirs();
540 }
541
542 QtLauncher::~QtLauncher()
543 {
544
545 }
546
547 void QtLauncher::setInAppMode()
548 {
549   m_inAppMode = true;
550   m_ui->tabWidget->removeTab(2);
551   m_ui->runButton->setText(tr("Apply"));
552   m_ui->quitButton->setText(tr("Cancel"));
553
554   disconnect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
555   connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onApply()));
556 }
557
558 void QtLauncher::restoreSettings()
559 {
560     QSettings settings;
561     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
562     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
563     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
564     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
565     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
566     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
567     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
568     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
569
570     // full paths to -set.xml files
571     m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
572
573     if (!m_recentAircraft.empty()) {
574         m_selectedAircraft = m_recentAircraft.front();
575     } else {
576         // select the default C172p
577     }
578
579     updateSelectedAircraft();
580     m_ui->location->restoreSettings();
581
582     // rating filters
583     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
584     int index = 0;
585     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
586         m_ratingFilters[index++] = v.toInt();
587     }
588
589     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
590     m_aircraftProxy->setRatings(m_ratingFilters);
591
592     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
593 }
594
595 void QtLauncher::saveSettings()
596 {
597     QSettings settings;
598     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
599     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
600     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
601     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
602     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
603     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
604     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
605     settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
606
607     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
608     settings.setValue("season", m_ui->seasonCombo->currentIndex());
609     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
610
611     m_ui->location->saveSettings();
612     qDebug() << "saving settings";
613 }
614
615 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
616 {
617     flightgear::Options* opt = flightgear::Options::sharedInstance();
618     std::string stdName(name.toStdString());
619     if (cbox->isChecked()) {
620         opt->addOption("enable-" + stdName, "");
621     } else {
622         opt->addOption("disable-" + stdName, "");
623     }
624 }
625
626 void QtLauncher::onRun()
627 {
628     accept();
629
630     flightgear::Options* opt = flightgear::Options::sharedInstance();
631     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
632     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
633     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
634     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
635 //    setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
636
637     bool startPaused = m_ui->startPausedCheck->isChecked() ||
638             m_ui->location->shouldStartPaused();
639     if (startPaused) {
640         qDebug() << "will start paused";
641         opt->addOption("enable-freeze", "");
642     }
643
644     // MSAA is more complex
645     if (!m_ui->rembrandtCheckbox->isChecked()) {
646         if (m_ui->msaaCheckbox->isChecked()) {
647             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
648             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
649         } else {
650             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
651         }
652     }
653
654     // aircraft
655     if (!m_selectedAircraft.isEmpty()) {
656         if (m_selectedAircraft.isLocalFile()) {
657             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
658             opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
659             QString setFile = setFileInfo.fileName();
660             Q_ASSERT(setFile.endsWith("-set.xml"));
661             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
662             opt->addOption("aircraft", setFile.toStdString());
663         } else if (m_selectedAircraft.scheme() == "package") {
664             PackageRef pkg = packageForAircraftURI(m_selectedAircraft);
665             // no need to set aircraft-dir, handled by the corresponding code
666             // in fgInitAircraft
667             opt->addOption("aircraft", pkg->qualifiedId());
668         } else {
669             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
670         }
671
672       // manage aircraft history
673         if (m_recentAircraft.contains(m_selectedAircraft))
674           m_recentAircraft.removeOne(m_selectedAircraft);
675         m_recentAircraft.prepend(m_selectedAircraft);
676         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
677           m_recentAircraft.pop_back();
678     }
679
680     m_ui->location->setLocationOptions();
681
682     // time of day
683     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
684         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
685         opt->addOption("timeofday", dayval.toStdString());
686     }
687
688     if (m_ui->seasonCombo->currentIndex() != 0) {
689         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
690         opt->addOption("season", dayval.toStdString());
691     }
692
693     QSettings settings;
694     QString downloadDir = settings.value("download-dir").toString();
695     if (!downloadDir.isEmpty()) {
696         QDir d(downloadDir);
697         if (!d.exists()) {
698             int result = QMessageBox::question(this, tr("Create download folder?"),
699                                   tr("The selected location for downloads does not exist. Create it?"),
700                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
701             if (result == QMessageBox::Cancel) {
702                 return;
703             }
704
705             if (result == QMessageBox::Yes) {
706                 d.mkpath(downloadDir);
707             }
708         }
709
710         qDebug() << "Download dir is:" << downloadDir;
711         opt->addOption("download-dir", downloadDir.toStdString());
712     }
713
714     // scenery paths
715     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
716         opt->addOption("fg-scenery", path.toStdString());
717     }
718
719     // aircraft paths
720     Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
721         // can't use fg-aircraft for this, as it is processed before the launcher is run
722         globals->append_aircraft_path(path.toStdString());
723     }
724
725     // additional arguments
726     ArgumentsTokenizer tk;
727     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
728         if (a.arg.startsWith("prop:")) {
729             QString v = a.arg.mid(5) + "=" + a.value;
730             opt->addOption("prop", v.toStdString());
731         } else {
732             opt->addOption(a.arg.toStdString(), a.value.toStdString());
733         }
734     }
735
736     saveSettings();
737 }
738
739
740 void QtLauncher::onApply()
741 {
742     accept();
743
744     // aircraft
745     if (!m_selectedAircraft.isEmpty()) {
746         std::string aircraftPropValue,
747             aircraftDir;
748
749         if (m_selectedAircraft.isLocalFile()) {
750             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
751             QString setFile = setFileInfo.fileName();
752             Q_ASSERT(setFile.endsWith("-set.xml"));
753             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
754             aircraftDir = setFileInfo.dir().absolutePath().toStdString();
755             aircraftPropValue = setFile.toStdString();
756         } else if (m_selectedAircraft.scheme() == "package") {
757             PackageRef pkg = packageForAircraftURI(m_selectedAircraft);
758             // no need to set aircraft-dir, handled by the corresponding code
759             // in fgInitAircraft
760             aircraftPropValue = pkg->qualifiedId();
761         } else {
762             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
763         }
764
765         // manage aircraft history
766         if (m_recentAircraft.contains(m_selectedAircraft))
767             m_recentAircraft.removeOne(m_selectedAircraft);
768         m_recentAircraft.prepend(m_selectedAircraft);
769         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
770             m_recentAircraft.pop_back();
771
772         globals->get_props()->setStringValue("/sim/aircraft", aircraftPropValue);
773         globals->get_props()->setStringValue("/sim/aircraft-dir", aircraftDir);
774     }
775
776
777     saveSettings();
778 }
779
780 void QtLauncher::onQuit()
781 {
782     reject();
783 }
784
785 void QtLauncher::onToggleTerrasync(bool enabled)
786 {
787     if (enabled) {
788         QSettings settings;
789         QString downloadDir = settings.value("download-dir").toString();
790         if (downloadDir.isEmpty()) {
791             downloadDir = QString::fromStdString(flightgear::defaultDownloadDir());
792         }
793
794         QFileInfo info(downloadDir);
795         if (!info.exists()) {
796             QMessageBox msg;
797             msg.setWindowTitle(tr("Create download folder?"));
798             msg.setText(tr("The download folder '%1' does not exist, create it now? "
799                            "Click 'change location' to choose another folder "
800                            "to store downloaded files").arg(downloadDir));
801             msg.addButton(QMessageBox::Yes);
802             msg.addButton(QMessageBox::Cancel);
803             msg.addButton(tr("Change location"), QMessageBox::ActionRole);
804             int result = msg.exec();
805
806             if (result == QMessageBox::Cancel) {
807                 m_ui->terrasyncCheck->setChecked(false);
808                 return;
809             }
810
811             if (result == QMessageBox::ActionRole) {
812                 onEditPaths();
813                 return;
814             }
815
816             QDir d(downloadDir);
817             d.mkpath(downloadDir);
818         }
819     } // of is enabled
820 }
821
822 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
823 {
824     maybeUpdateSelectedAircraft(index);
825 }
826
827 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
828 {
829     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
830
831     QMessageBox msg;
832     msg.setWindowTitle(tr("Aircraft installation failed"));
833     msg.setText(tr("An error occurred installing the aircraft %1: %2").
834                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
835     msg.addButton(QMessageBox::Ok);
836     msg.exec();
837
838     maybeUpdateSelectedAircraft(index);
839 }
840
841 void QtLauncher::onAircraftSelected(const QModelIndex& index)
842 {
843     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
844     updateSelectedAircraft();
845 }
846
847 void QtLauncher::onRequestPackageInstall(const QModelIndex& index)
848 {
849     QString pkg = index.data(AircraftPackageIdRole).toString();
850     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
851     if (pref->isInstalled()) {
852         InstallRef install = pref->existingInstall();
853         if (install && install->hasUpdate()) {
854             globals->packageRoot()->scheduleToUpdate(install);
855         }
856     } else {
857         pref->install();
858     }
859 }
860
861 void QtLauncher::onCancelDownload(const QModelIndex& index)
862 {
863     QString pkg = index.data(AircraftPackageIdRole).toString();
864     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
865     simgear::pkg::InstallRef i = pref->existingInstall();
866     i->cancelDownload();
867 }
868
869 void QtLauncher::maybeUpdateSelectedAircraft(QModelIndex index)
870 {
871     QUrl u = index.data(AircraftURIRole).toUrl();
872     if (u == m_selectedAircraft) {
873         // potentially enable the run button now!
874         updateSelectedAircraft();
875     }
876 }
877
878 void QtLauncher::updateSelectedAircraft()
879 {
880     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
881     if (index.isValid()) {
882         QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
883         m_ui->thumbnail->setPixmap(pm);
884         m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
885
886         int status = index.data(AircraftPackageStatusRole).toInt();
887         bool canRun = (status == PackageInstalled);
888         m_ui->runButton->setEnabled(canRun);
889
890         LauncherAircraftType aircraftType = Airplane;
891         if (index.data(AircraftIsHelicopterRole).toBool()) {
892             aircraftType = Helicopter;
893         } else if (index.data(AircraftIsSeaplaneRole).toBool()) {
894             aircraftType = Seaplane;
895         }
896
897         m_ui->location->setAircraftType(aircraftType);
898     } else {
899         m_ui->thumbnail->setPixmap(QPixmap());
900         m_ui->aircraftDescription->setText("");
901         m_ui->runButton->setEnabled(false);
902     }
903 }
904
905 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
906 {
907   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
908 }
909
910 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
911 {
912     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
913     Q_ASSERT(sourceModel);
914     return sourceModel->indexOfAircraftURI(uri);
915 }
916
917 void QtLauncher::onPopupAircraftHistory()
918 {
919     if (m_recentAircraft.isEmpty()) {
920         return;
921     }
922
923     QMenu m;
924     Q_FOREACH(QUrl uri, m_recentAircraft) {
925         QModelIndex index = sourceIndexForAircraftURI(uri);
926         if (!index.isValid()) {
927             // not scanned yet
928             continue;
929         }
930         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
931         act->setData(uri);
932     }
933
934     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
935     QAction* triggered = m.exec(popupPos);
936     if (triggered) {
937         m_selectedAircraft = triggered->data().toUrl();
938         QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
939         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
940                                                               QItemSelectionModel::ClearAndSelect);
941         m_ui->aircraftFilter->clear();
942         updateSelectedAircraft();
943     }
944 }
945
946 void QtLauncher::onEditRatingsFilter()
947 {
948     EditRatingsFilterDialog dialog(this);
949     dialog.setRatings(m_ratingFilters);
950
951     dialog.exec();
952     if (dialog.result() == QDialog::Accepted) {
953         QVariantList vl;
954         for (int i=0; i<4; ++i) {
955             m_ratingFilters[i] = dialog.getRating(i);
956             vl.append(m_ratingFilters[i]);
957         }
958         m_aircraftProxy->setRatings(m_ratingFilters);
959
960         QSettings settings;
961         settings.setValue("min-ratings", vl);
962     }
963 }
964
965 void QtLauncher::updateSettingsSummary()
966 {
967     QStringList summary;
968     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
969         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
970     }
971
972     if (m_ui->seasonCombo->currentIndex() > 0) {
973         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
974     }
975
976     if (m_ui->rembrandtCheckbox->isChecked()) {
977         summary.append("Rembrandt enabled");
978     } else if (m_ui->msaaCheckbox->isChecked()) {
979         summary.append("anti-aliasing");
980     }
981
982     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
983         summary.append("live weather");
984     }
985
986     if (m_ui->terrasyncCheck->isChecked()) {
987         summary.append("automatic scenery downloads");
988     }
989
990     if (m_ui->startPausedCheck->isChecked()) {
991         summary.append("paused");
992     }
993
994     QString s = summary.join(", ");
995     s[0] = s[0].toUpper();
996     m_ui->settingsDescription->setText(s);
997 }
998
999 void QtLauncher::onRembrandtToggled(bool b)
1000 {
1001     // Rembrandt and multi-sample are exclusive
1002     m_ui->msaaCheckbox->setEnabled(!b);
1003 }
1004
1005 void QtLauncher::onSubsytemIdleTimeout()
1006 {
1007     globals->get_subsystem_mgr()->update(0.0);
1008 }
1009
1010 void QtLauncher::onEditPaths()
1011 {
1012     PathsDialog dlg(this, globals->packageRoot());
1013     dlg.exec();
1014     if (dlg.result() == QDialog::Accepted) {
1015         // re-scan the aircraft list
1016         QSettings settings;
1017         m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1018         m_aircraftModel->scanDirs();
1019     }
1020 }
1021
1022 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1023 {
1024     if (uri.scheme() != "package") {
1025         qWarning() << "invalid URL scheme:" << uri;
1026         return simgear::pkg::PackageRef();
1027     }
1028
1029     QString ident = uri.path();
1030     return globals->packageRoot()->getPackageById(ident.toStdString());
1031 }
1032
1033
1034 #include "QtLauncher.moc"