]> git.mxchange.org Git - flightgear.git/blob - src/GUI/QtLauncher.cxx
Launcher: fix slot name for old-style syntax
[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         m_onlyShowInstalled(false)
257     {
258         for (int i=0; i<4; ++i) {
259             m_ratings[i] = 3;
260         }
261     }
262
263     void setRatings(int* ratings)
264     {
265         ::memcpy(m_ratings, ratings, sizeof(int) * 4);
266         invalidate();
267     }
268
269 public slots:
270     void setRatingFilterEnabled(bool e)
271     {
272         if (e == m_ratingsFilter) {
273             return;
274         }
275
276         m_ratingsFilter = e;
277         invalidate();
278     }
279
280     void setInstalledFilterEnabled(bool e)
281     {
282         if (e == m_onlyShowInstalled) {
283             return;
284         }
285
286         m_onlyShowInstalled = e;
287         invalidate();
288     }
289
290 protected:
291     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
292     {
293         if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
294             return false;
295         }
296
297         if (m_onlyShowInstalled) {
298             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
299             QVariant v = index.data(AircraftPackageStatusRole);
300             AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
301             if (status == PackageNotInstalled) {
302                 return false;
303             }
304         }
305
306         if (!m_onlyShowInstalled && m_ratingsFilter) {
307             QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
308             for (int i=0; i<4; ++i) {
309                 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
310                     return false;
311                 }
312             }
313         }
314
315         return true;
316     }
317
318 private:
319     bool m_ratingsFilter;
320     bool m_onlyShowInstalled;
321     int m_ratings[4];
322 };
323
324 static void initQtResources()
325 {
326     Q_INIT_RESOURCE(resources);
327 }
328
329 namespace flightgear
330 {
331
332 void initApp(int& argc, char** argv)
333 {
334     sglog().setLogLevels( SG_ALL, SG_INFO );
335     initQtResources(); // can't be called from a namespace
336
337     static bool qtInitDone = false;
338     static int s_argc;
339
340     if (!qtInitDone) {
341         qtInitDone = true;
342         s_argc = argc; // QApplication only stores a reference to argc,
343         // and may crash if it is freed
344         // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication
345
346         QApplication* app = new QApplication(s_argc, argv);
347         app->setOrganizationName("FlightGear");
348         app->setApplicationName("FlightGear");
349         app->setOrganizationDomain("flightgear.org");
350
351         // avoid double Apple menu and other weirdness if both Qt and OSG
352         // try to initialise various Cocoa structures.
353         flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
354
355         Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
356         if (mods & Qt::AltModifier) {
357             qWarning() << "Alt pressed during launch";
358
359             // wipe out our settings
360             QSettings settings;
361             settings.clear();
362
363
364             Options::sharedInstance()->addOption("restore-defaults", "");
365         }
366     }
367 }
368
369 void loadNaturalEarthFile(const std::string& aFileName,
370                           flightgear::PolyLine::Type aType,
371                           bool areClosed)
372 {
373     SGPath path(globals->get_fg_root());
374     path.append( "Geodata" );
375     path.append(aFileName);
376     if (!path.exists())
377         return; // silently fail for now
378
379     flightgear::PolyLineList lines;
380     flightgear::SHPParser::parsePolyLines(path, aType, lines, areClosed);
381     flightgear::PolyLine::bulkAddToSpatialIndex(lines);
382 }
383
384 void loadNaturalEarthData()
385 {
386     SGTimeStamp st;
387     st.stamp();
388
389     loadNaturalEarthFile("ne_10m_coastline.shp", flightgear::PolyLine::COASTLINE, false);
390     loadNaturalEarthFile("ne_10m_rivers_lake_centerlines.shp", flightgear::PolyLine::RIVER, false);
391     loadNaturalEarthFile("ne_10m_lakes.shp", flightgear::PolyLine::LAKE, true);
392
393     qDebug() << "load basic data took" << st.elapsedMSec();
394
395
396     st.stamp();
397     loadNaturalEarthFile("ne_10m_urban_areas.shp", flightgear::PolyLine::URBAN, true);
398
399     qDebug() << "loading urban areas took:" << st.elapsedMSec();
400 }
401
402 bool runLauncherDialog()
403 {
404     // startup the nav-cache now. This pre-empts normal startup of
405     // the cache, but no harm done. (Providing scenery paths are consistent)
406
407     initNavCache();
408
409     fgInitPackageRoot();
410
411     // startup the HTTP system now since packages needs it
412     FGHTTPClient* http = new FGHTTPClient;
413     globals->add_subsystem("http", http);
414     // we guard against re-init in the global phase; bind and postinit
415     // will happen as normal
416     http->init();
417
418     loadNaturalEarthData();
419
420     QtLauncher dlg;
421     dlg.exec();
422
423     if (dlg.result() != QDialog::Accepted) {
424         return false;
425     }
426
427     // don't set scenery paths twice
428     globals->clear_fg_scenery();
429
430     return true;
431 }
432
433 bool runInAppLauncherDialog()
434 {
435     QtLauncher dlg;
436     dlg.setInAppMode();
437     dlg.exec();
438     if (dlg.result() != QDialog::Accepted) {
439         return false;
440     }
441
442     return true;
443 }
444
445 } // of namespace flightgear
446
447 QtLauncher::QtLauncher() :
448     QDialog(),
449     m_ui(NULL),
450     m_subsystemIdleTimer(NULL),
451     m_inAppMode(false)
452 {
453     m_ui.reset(new Ui::Launcher);
454     m_ui->setupUi(this);
455
456 #if QT_VERSION >= 0x050300
457     // don't require Qt 5.3
458     m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
459 #endif
460
461 #if QT_VERSION >= 0x050200
462     m_ui->aircraftFilter->setClearButtonEnabled(true);
463 #endif
464
465     for (int i=0; i<4; ++i) {
466         m_ratingFilters[i] = 3;
467     }
468
469     m_subsystemIdleTimer = new QTimer(this);
470     m_subsystemIdleTimer->setInterval(0);
471     connect(m_subsystemIdleTimer, &QTimer::timeout,
472             this, &QtLauncher::onSubsytemIdleTimeout);
473     m_subsystemIdleTimer->start();
474
475     // create and configure the proxy model
476     m_aircraftProxy = new AircraftProxyModel(this);
477     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
478             m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
479     connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
480             this, &QtLauncher::maybeRestoreAircraftSelection);
481
482     connect(m_ui->onlyShowInstalledCheck, &QAbstractButton::toggled,
483             m_aircraftProxy, &AircraftProxyModel::setInstalledFilterEnabled);
484     connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
485             m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
486
487     connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
488     connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
489
490     connect(m_ui->aircraftHistory, &QPushButton::clicked,
491           this, &QtLauncher::onPopupAircraftHistory);
492
493     connect(m_ui->location, &LocationWidget::descriptionChanged,
494             m_ui->locationDescription, &QLabel::setText);
495
496     QAction* qa = new QAction(this);
497     qa->setShortcut(QKeySequence("Ctrl+Q"));
498     connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
499     addAction(qa);
500
501     connect(m_ui->editRatingFilter, &QPushButton::clicked,
502             this, &QtLauncher::onEditRatingsFilter);
503     connect(m_ui->onlyShowInstalledCheck, &QCheckBox::toggled,
504             this, &QtLauncher::onShowInstalledAircraftToggled);
505
506     QIcon historyIcon(":/history-icon");
507     m_ui->aircraftHistory->setIcon(historyIcon);
508
509     connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
510             this, SLOT(updateSettingsSummary()));
511     connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
512             this, SLOT(updateSettingsSummary()));
513     connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
514             this, SLOT(updateSettingsSummary()));
515     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
516             this, SLOT(updateSettingsSummary()));
517     connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
518             this, SLOT(updateSettingsSummary()));
519     connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
520             this, SLOT(updateSettingsSummary()));
521     connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
522             this, SLOT(updateSettingsSummary()));
523
524     connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
525             this, SLOT(onRembrandtToggled(bool)));
526     connect(m_ui->terrasyncCheck, &QCheckBox::toggled,
527             this, &QtLauncher::onToggleTerrasync);
528     updateSettingsSummary();
529
530     m_aircraftModel = new AircraftItemModel(this, RootRef(globals->packageRoot()));
531     m_aircraftProxy->setSourceModel(m_aircraftModel);
532
533     m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
534     m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
535     m_aircraftProxy->setSortRole(Qt::DisplayRole);
536     m_aircraftProxy->setDynamicSortFilter(true);
537
538     m_ui->aircraftList->setModel(m_aircraftProxy);
539     m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
540     AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
541     m_ui->aircraftList->setItemDelegate(delegate);
542     m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
543     connect(m_ui->aircraftList, &QListView::clicked,
544             this, &QtLauncher::onAircraftSelected);
545     connect(delegate, &AircraftItemDelegate::variantChanged,
546             this, &QtLauncher::onAircraftSelected);
547     connect(delegate, &AircraftItemDelegate::requestInstall,
548             this, &QtLauncher::onRequestPackageInstall);
549     connect(delegate, &AircraftItemDelegate::cancelDownload,
550             this, &QtLauncher::onCancelDownload);
551
552     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
553             this, &QtLauncher::onAircraftInstalledCompleted);
554     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
555             this, &QtLauncher::onAircraftInstallFailed);
556     connect(m_aircraftModel, &AircraftItemModel::scanCompleted,
557             this, &QtLauncher::updateSelectedAircraft);
558     connect(m_ui->pathsButton, &QPushButton::clicked,
559             this, &QtLauncher::onEditPaths);
560
561     // after any kind of reset, try to restore selection and scroll
562     // to match the m_selectedAircraft. This needs to be delayed
563     // fractionally otherwise the scrollTo seems to be ignored,
564     // unfortunately.
565     connect(m_aircraftProxy, &AircraftProxyModel::modelReset,
566             this, &QtLauncher::delayedAircraftModelReset);
567
568     restoreSettings();
569
570     QSettings settings;
571     m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
572     m_aircraftModel->scanDirs();
573 }
574
575 QtLauncher::~QtLauncher()
576 {
577
578 }
579
580 void QtLauncher::setSceneryPaths()
581 {
582     globals->clear_fg_scenery();
583
584 // mimic what optionss.cxx does, so we can find airport data for parking
585 // positions
586     QSettings settings;
587     // append explicit scenery paths
588     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
589         globals->append_fg_scenery(path.toStdString());
590     }
591
592     // append the TerraSync path
593     QString downloadDir = settings.value("download-dir").toString();
594     if (downloadDir.isEmpty()) {
595         downloadDir = QString::fromStdString(flightgear::defaultDownloadDir());
596     }
597
598     SGPath terraSyncDir(downloadDir.toStdString());
599     terraSyncDir.append("TerraSync");
600     if (terraSyncDir.exists()) {
601         globals->append_fg_scenery(terraSyncDir.str());
602     }
603
604 }
605
606 void QtLauncher::setInAppMode()
607 {
608   m_inAppMode = true;
609   m_ui->tabWidget->removeTab(2);
610   m_ui->runButton->setText(tr("Apply"));
611   m_ui->quitButton->setText(tr("Cancel"));
612
613   disconnect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
614   connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onApply()));
615 }
616
617 void QtLauncher::restoreSettings()
618 {
619     QSettings settings;
620     m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
621     m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
622     m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
623     m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
624     m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
625     m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
626     m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
627     m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
628
629     // full paths to -set.xml files
630     m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
631
632     if (!m_recentAircraft.empty()) {
633         m_selectedAircraft = m_recentAircraft.front();
634     } else {
635         // select the default C172p
636     }
637
638     if (!m_inAppMode) {
639         setSceneryPaths();
640     }
641
642     m_ui->location->restoreSettings();
643
644     // rating filters
645     m_ui->onlyShowInstalledCheck->setChecked(settings.value("only-show-installed", false).toBool());
646     if (m_ui->onlyShowInstalledCheck->isChecked()) {
647         m_ui->ratingsFilterCheck->setEnabled(false);
648     }
649
650     m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
651     int index = 0;
652     Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
653         m_ratingFilters[index++] = v.toInt();
654     }
655
656     m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
657     m_aircraftProxy->setRatings(m_ratingFilters);
658
659     updateSelectedAircraft();
660     maybeRestoreAircraftSelection();
661
662     m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
663 }
664
665 void QtLauncher::delayedAircraftModelReset()
666 {
667     QTimer::singleShot(1, this, SLOT(maybeRestoreAircraftSelection()));
668 }
669
670 void QtLauncher::maybeRestoreAircraftSelection()
671 {
672     QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
673     QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
674     if (proxyIndex.isValid()) {
675         m_ui->aircraftList->selectionModel()->setCurrentIndex(proxyIndex,
676                                                               QItemSelectionModel::ClearAndSelect);
677         m_ui->aircraftList->selectionModel()->select(proxyIndex,
678                                                      QItemSelectionModel::ClearAndSelect);
679         m_ui->aircraftList->scrollTo(proxyIndex);
680     }
681 }
682
683 void QtLauncher::saveSettings()
684 {
685     QSettings settings;
686     settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
687     settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
688     settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
689     settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
690     settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
691     settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
692     settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
693     settings.setValue("only-show-installed", m_ui->onlyShowInstalledCheck->isChecked());
694     settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
695
696     settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
697     settings.setValue("season", m_ui->seasonCombo->currentIndex());
698     settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
699
700     m_ui->location->saveSettings();
701 }
702
703 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
704 {
705     flightgear::Options* opt = flightgear::Options::sharedInstance();
706     std::string stdName(name.toStdString());
707     if (cbox->isChecked()) {
708         opt->addOption("enable-" + stdName, "");
709     } else {
710         opt->addOption("disable-" + stdName, "");
711     }
712 }
713
714 void QtLauncher::onRun()
715 {
716     accept();
717
718     flightgear::Options* opt = flightgear::Options::sharedInstance();
719     setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
720     setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
721     setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
722     setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
723 //    setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
724
725     bool startPaused = m_ui->startPausedCheck->isChecked() ||
726             m_ui->location->shouldStartPaused();
727     if (startPaused) {
728         opt->addOption("enable-freeze", "");
729     }
730
731     // MSAA is more complex
732     if (!m_ui->rembrandtCheckbox->isChecked()) {
733         if (m_ui->msaaCheckbox->isChecked()) {
734             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
735             globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
736         } else {
737             globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
738         }
739     }
740
741     // aircraft
742     if (!m_selectedAircraft.isEmpty()) {
743         if (m_selectedAircraft.isLocalFile()) {
744             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
745             opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
746             QString setFile = setFileInfo.fileName();
747             Q_ASSERT(setFile.endsWith("-set.xml"));
748             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
749             opt->addOption("aircraft", setFile.toStdString());
750         } else if (m_selectedAircraft.scheme() == "package") {
751             QString qualifiedId = m_selectedAircraft.path();
752             // no need to set aircraft-dir, handled by the corresponding code
753             // in fgInitAircraft
754             opt->addOption("aircraft", qualifiedId.toStdString());
755         } else {
756             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
757         }
758
759       // manage aircraft history
760         if (m_recentAircraft.contains(m_selectedAircraft))
761           m_recentAircraft.removeOne(m_selectedAircraft);
762         m_recentAircraft.prepend(m_selectedAircraft);
763         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
764           m_recentAircraft.pop_back();
765     }
766
767     m_ui->location->setLocationOptions();
768
769     // time of day
770     if (m_ui->timeOfDayCombo->currentIndex() != 0) {
771         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
772         opt->addOption("timeofday", dayval.toStdString());
773     }
774
775     if (m_ui->seasonCombo->currentIndex() != 0) {
776         QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
777         opt->addOption("season", dayval.toStdString());
778     }
779
780     QSettings settings;
781     QString downloadDir = settings.value("download-dir").toString();
782     if (!downloadDir.isEmpty()) {
783         QDir d(downloadDir);
784         if (!d.exists()) {
785             int result = QMessageBox::question(this, tr("Create download folder?"),
786                                   tr("The selected location for downloads does not exist. Create it?"),
787                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
788             if (result == QMessageBox::Cancel) {
789                 return;
790             }
791
792             if (result == QMessageBox::Yes) {
793                 d.mkpath(downloadDir);
794             }
795         }
796
797         opt->addOption("download-dir", downloadDir.toStdString());
798     }
799
800     // scenery paths
801     Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
802         opt->addOption("fg-scenery", path.toStdString());
803     }
804
805     // aircraft paths
806     Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
807         // can't use fg-aircraft for this, as it is processed before the launcher is run
808         globals->append_aircraft_path(path.toStdString());
809     }
810
811     // additional arguments
812     ArgumentsTokenizer tk;
813     Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
814         if (a.arg.startsWith("prop:")) {
815             QString v = a.arg.mid(5) + "=" + a.value;
816             opt->addOption("prop", v.toStdString());
817         } else {
818             opt->addOption(a.arg.toStdString(), a.value.toStdString());
819         }
820     }
821
822     saveSettings();
823 }
824
825
826 void QtLauncher::onApply()
827 {
828     accept();
829
830     // aircraft
831     if (!m_selectedAircraft.isEmpty()) {
832         std::string aircraftPropValue,
833             aircraftDir;
834
835         if (m_selectedAircraft.isLocalFile()) {
836             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
837             QString setFile = setFileInfo.fileName();
838             Q_ASSERT(setFile.endsWith("-set.xml"));
839             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
840             aircraftDir = setFileInfo.dir().absolutePath().toStdString();
841             aircraftPropValue = setFile.toStdString();
842         } else if (m_selectedAircraft.scheme() == "package") {
843             // no need to set aircraft-dir, handled by the corresponding code
844             // in fgInitAircraft
845             aircraftPropValue = m_selectedAircraft.path().toStdString();
846         } else {
847             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
848         }
849
850         // manage aircraft history
851         if (m_recentAircraft.contains(m_selectedAircraft))
852             m_recentAircraft.removeOne(m_selectedAircraft);
853         m_recentAircraft.prepend(m_selectedAircraft);
854         if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
855             m_recentAircraft.pop_back();
856
857         globals->get_props()->setStringValue("/sim/aircraft", aircraftPropValue);
858         globals->get_props()->setStringValue("/sim/aircraft-dir", aircraftDir);
859     }
860
861
862     saveSettings();
863 }
864
865 void QtLauncher::onQuit()
866 {
867     reject();
868 }
869
870 void QtLauncher::onToggleTerrasync(bool enabled)
871 {
872     if (enabled) {
873         QSettings settings;
874         QString downloadDir = settings.value("download-dir").toString();
875         if (downloadDir.isEmpty()) {
876             downloadDir = QString::fromStdString(flightgear::defaultDownloadDir());
877         }
878
879         QFileInfo info(downloadDir);
880         if (!info.exists()) {
881             QMessageBox msg;
882             msg.setWindowTitle(tr("Create download folder?"));
883             msg.setText(tr("The download folder '%1' does not exist, create it now? "
884                            "Click 'change location' to choose another folder "
885                            "to store downloaded files").arg(downloadDir));
886             msg.addButton(QMessageBox::Yes);
887             msg.addButton(QMessageBox::Cancel);
888             msg.addButton(tr("Change location"), QMessageBox::ActionRole);
889             int result = msg.exec();
890
891             if (result == QMessageBox::Cancel) {
892                 m_ui->terrasyncCheck->setChecked(false);
893                 return;
894             }
895
896             if (result == QMessageBox::ActionRole) {
897                 onEditPaths();
898                 return;
899             }
900
901             QDir d(downloadDir);
902             d.mkpath(downloadDir);
903         }
904     } // of is enabled
905 }
906
907 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
908 {
909     maybeUpdateSelectedAircraft(index);
910 }
911
912 void QtLauncher::onRatingsFilterToggled()
913 {
914     QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
915     QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
916     if (proxyIndex.isValid()) {
917         m_ui->aircraftList->scrollTo(proxyIndex);
918     }
919 }
920
921 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
922 {
923     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
924
925     QMessageBox msg;
926     msg.setWindowTitle(tr("Aircraft installation failed"));
927     msg.setText(tr("An error occurred installing the aircraft %1: %2").
928                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
929     msg.addButton(QMessageBox::Ok);
930     msg.exec();
931
932     maybeUpdateSelectedAircraft(index);
933 }
934
935 void QtLauncher::onAircraftSelected(const QModelIndex& index)
936 {
937     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
938     updateSelectedAircraft();
939 }
940
941 void QtLauncher::onRequestPackageInstall(const QModelIndex& index)
942 {
943     // also select, otherwise UI is confusing
944     m_selectedAircraft = index.data(AircraftURIRole).toUrl();
945     updateSelectedAircraft();
946
947     QString pkg = index.data(AircraftPackageIdRole).toString();
948     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
949     if (pref->isInstalled()) {
950         InstallRef install = pref->existingInstall();
951         if (install && install->hasUpdate()) {
952             globals->packageRoot()->scheduleToUpdate(install);
953         }
954     } else {
955         pref->install();
956     }
957 }
958
959 void QtLauncher::onCancelDownload(const QModelIndex& index)
960 {
961     QString pkg = index.data(AircraftPackageIdRole).toString();
962     simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
963     simgear::pkg::InstallRef i = pref->existingInstall();
964     i->cancelDownload();
965 }
966
967 void QtLauncher::maybeUpdateSelectedAircraft(QModelIndex index)
968 {
969     QUrl u = index.data(AircraftURIRole).toUrl();
970     if (u == m_selectedAircraft) {
971         // potentially enable the run button now!
972         updateSelectedAircraft();
973     }
974 }
975
976 void QtLauncher::updateSelectedAircraft()
977 {
978     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
979     if (index.isValid()) {
980         QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
981         m_ui->thumbnail->setPixmap(pm);
982         m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
983
984         int status = index.data(AircraftPackageStatusRole).toInt();
985         bool canRun = (status == PackageInstalled);
986         m_ui->runButton->setEnabled(canRun);
987
988         LauncherAircraftType aircraftType = Airplane;
989         if (index.data(AircraftIsHelicopterRole).toBool()) {
990             aircraftType = Helicopter;
991         } else if (index.data(AircraftIsSeaplaneRole).toBool()) {
992             aircraftType = Seaplane;
993         }
994
995         m_ui->location->setAircraftType(aircraftType);
996     } else {
997         m_ui->thumbnail->setPixmap(QPixmap());
998         m_ui->aircraftDescription->setText("");
999         m_ui->runButton->setEnabled(false);
1000     }
1001 }
1002
1003 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
1004 {
1005   return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
1006 }
1007
1008 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
1009 {
1010     AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1011     Q_ASSERT(sourceModel);
1012     return sourceModel->indexOfAircraftURI(uri);
1013 }
1014
1015 void QtLauncher::onPopupAircraftHistory()
1016 {
1017     if (m_recentAircraft.isEmpty()) {
1018         return;
1019     }
1020
1021     QMenu m;
1022     Q_FOREACH(QUrl uri, m_recentAircraft) {
1023         QModelIndex index = sourceIndexForAircraftURI(uri);
1024         if (!index.isValid()) {
1025             // not scanned yet
1026             continue;
1027         }
1028         QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1029         act->setData(uri);
1030     }
1031
1032     QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1033     QAction* triggered = m.exec(popupPos);
1034     if (triggered) {
1035         m_selectedAircraft = triggered->data().toUrl();
1036         QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
1037         m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1038                                                               QItemSelectionModel::ClearAndSelect);
1039         m_ui->aircraftFilter->clear();
1040         updateSelectedAircraft();
1041     }
1042 }
1043
1044 void QtLauncher::onEditRatingsFilter()
1045 {
1046     EditRatingsFilterDialog dialog(this);
1047     dialog.setRatings(m_ratingFilters);
1048
1049     dialog.exec();
1050     if (dialog.result() == QDialog::Accepted) {
1051         QVariantList vl;
1052         for (int i=0; i<4; ++i) {
1053             m_ratingFilters[i] = dialog.getRating(i);
1054             vl.append(m_ratingFilters[i]);
1055         }
1056         m_aircraftProxy->setRatings(m_ratingFilters);
1057
1058         QSettings settings;
1059         settings.setValue("min-ratings", vl);
1060     }
1061 }
1062
1063 void QtLauncher::updateSettingsSummary()
1064 {
1065     QStringList summary;
1066     if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1067         summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1068     }
1069
1070     if (m_ui->seasonCombo->currentIndex() > 0) {
1071         summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1072     }
1073
1074     if (m_ui->rembrandtCheckbox->isChecked()) {
1075         summary.append("Rembrandt enabled");
1076     } else if (m_ui->msaaCheckbox->isChecked()) {
1077         summary.append("anti-aliasing");
1078     }
1079
1080     if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1081         summary.append("live weather");
1082     }
1083
1084     if (m_ui->terrasyncCheck->isChecked()) {
1085         summary.append("automatic scenery downloads");
1086     }
1087
1088     if (m_ui->startPausedCheck->isChecked()) {
1089         summary.append("paused");
1090     }
1091
1092     QString s = summary.join(", ");
1093     s[0] = s[0].toUpper();
1094     m_ui->settingsDescription->setText(s);
1095 }
1096
1097 void QtLauncher::onRembrandtToggled(bool b)
1098 {
1099     // Rembrandt and multi-sample are exclusive
1100     m_ui->msaaCheckbox->setEnabled(!b);
1101 }
1102
1103 void QtLauncher::onShowInstalledAircraftToggled(bool b)
1104 {
1105     m_ui->ratingsFilterCheck->setEnabled(!b);
1106     maybeRestoreAircraftSelection();
1107 }
1108
1109 void QtLauncher::onSubsytemIdleTimeout()
1110 {
1111     globals->get_subsystem_mgr()->update(0.0);
1112 }
1113
1114 void QtLauncher::onEditPaths()
1115 {
1116     PathsDialog dlg(this, globals->packageRoot());
1117     dlg.exec();
1118     if (dlg.result() == QDialog::Accepted) {
1119         // re-scan the aircraft list
1120         QSettings settings;
1121         m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1122         m_aircraftModel->scanDirs();
1123
1124         // re-set scenery dirs
1125         setSceneryPaths();
1126     }
1127 }
1128
1129 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1130 {
1131     if (uri.scheme() != "package") {
1132         qWarning() << "invalid URL scheme:" << uri;
1133         return simgear::pkg::PackageRef();
1134     }
1135
1136     QString ident = uri.path();
1137     return globals->packageRoot()->getPackageById(ident.toStdString());
1138 }
1139
1140
1141 #include "QtLauncher.moc"