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