1 // QtLauncher.cxx - GUI launcher dialog using Qt5
3 // Written by James Turner, started December 2014.
5 // Copyright (C) 2014 James Turner <zakalawe@mac.com>
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.
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.
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.
21 #include "QtLauncher.hxx"
22 #include "QtLauncher_private.hxx"
27 #include <QProgressDialog>
28 #include <QCoreApplication>
29 #include <QAbstractListModel>
38 #include <QSortFilterProxyModel>
40 #include <QDesktopServices>
43 #include <QFileDialog>
44 #include <QMessageBox>
46 #include <QApplication>
48 #include <QDoubleSpinBox>
52 #include <simgear/timing/timestamp.hxx>
53 #include <simgear/props/props_io.hxx>
54 #include <simgear/structure/exception.hxx>
55 #include <simgear/structure/subsystem_mgr.hxx>
56 #include <simgear/misc/sg_path.hxx>
57 #include <simgear/package/Catalog.hxx>
58 #include <simgear/package/Package.hxx>
59 #include <simgear/package/Install.hxx>
61 #include "ui_Launcher.h"
62 #include "ui_NoOfficialHangar.h"
64 #include "EditRatingsFilterDialog.hxx"
65 #include "AircraftItemDelegate.hxx"
66 #include "AircraftModel.hxx"
67 #include "PathsDialog.hxx"
68 #include "EditCustomMPServerDialog.hxx"
70 #include <Main/globals.hxx>
71 #include <Main/fg_props.hxx>
72 #include <Navaids/NavDataCache.hxx>
73 #include <Navaids/navrecord.hxx>
74 #include <Navaids/SHPParser.hxx>
76 #include <Main/options.hxx>
77 #include <Main/fg_init.hxx>
78 #include <Viewer/WindowBuilder.hxx>
79 #include <Network/HTTPClient.hxx>
80 #include <Network/RemoteXMLRequest.hxx>
82 using namespace flightgear;
83 using namespace simgear::pkg;
85 const int MAX_RECENT_AIRCRAFT = 20;
87 namespace { // anonymous namespace
91 QString baseLabel = QT_TR_NOOP("Initialising navigation data, this may take several minutes");
92 NavDataCache* cache = NavDataCache::createInstance();
93 if (cache->isRebuildRequired()) {
94 QProgressDialog rebuildProgress(baseLabel,
95 QString() /* cancel text */,
98 | Qt::CustomizeWindowHint
100 | Qt::WindowSystemMenuHint
101 | Qt::MSWindowsFixedSizeDialogHint);
102 rebuildProgress.setWindowModality(Qt::WindowModal);
103 rebuildProgress.show();
105 NavDataCache::RebuildPhase phase = cache->rebuild();
107 while (phase != NavDataCache::REBUILD_DONE) {
108 // sleep to give the rebuild thread more time
109 SGTimeStamp::sleepForMSec(50);
110 phase = cache->rebuild();
113 case NavDataCache::REBUILD_AIRPORTS:
114 rebuildProgress.setLabelText(QT_TR_NOOP("Loading airport data"));
117 case NavDataCache::REBUILD_FIXES:
118 rebuildProgress.setLabelText(QT_TR_NOOP("Loading waypoint data"));
121 case NavDataCache::REBUILD_NAVAIDS:
122 rebuildProgress.setLabelText(QT_TR_NOOP("Loading navigation data"));
126 case NavDataCache::REBUILD_POIS:
127 rebuildProgress.setLabelText(QT_TR_NOOP("Loading point-of-interest data"));
131 rebuildProgress.setLabelText(baseLabel);
134 if (phase == NavDataCache::REBUILD_UNKNOWN) {
135 rebuildProgress.setValue(0);
136 rebuildProgress.setMaximum(0);
138 rebuildProgress.setValue(cache->rebuildPhaseCompletionPercentage());
139 rebuildProgress.setMaximum(100);
142 QCoreApplication::processEvents();
147 class ArgumentsTokenizer
153 explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
159 QList<Arg> tokenize(QString in) const
162 const int len = in.count();
168 for (; index < len; ++index) {
170 nc = index < (len - 1) ? in.at(index + 1) : QChar();
174 if (c == QChar('-')) {
175 if (nc == QChar('-')) {
180 // should we pemit single hyphen arguments?
181 // choosing to fail for now
184 } else if (c == QChar('#')) {
187 } else if (c.isSpace()) {
193 if (c == QChar('=')) {
196 } else if (c.isSpace()) {
198 result.append(Arg(key));
200 // could check for illegal charatcers here
206 if (c == QChar('"')) {
208 } else if (c.isSpace()) {
210 result.append(Arg(key, value));
217 if (c == QChar('\\')) {
218 // check for escaped double-quote inside quoted value
219 if (nc == QChar('"')) {
222 } else if (c == QChar('"')) {
230 if ((c == QChar('\n')) || (c == QChar('\r'))) {
234 // nothing to do, eat comment chars
238 } // of character loop
240 // ensure last argument isn't lost
242 result.append(Arg(key));
243 } else if (state == Value) {
244 result.append(Arg(key, value));
260 } // of anonymous namespace
262 class AircraftProxyModel : public QSortFilterProxyModel
266 AircraftProxyModel(QObject* pr) :
267 QSortFilterProxyModel(pr),
268 m_ratingsFilter(true),
269 m_onlyShowInstalled(false)
271 for (int i=0; i<4; ++i) {
276 void setRatings(int* ratings)
278 ::memcpy(m_ratings, ratings, sizeof(int) * 4);
283 void setRatingFilterEnabled(bool e)
285 if (e == m_ratingsFilter) {
293 void setInstalledFilterEnabled(bool e)
295 if (e == m_onlyShowInstalled) {
299 m_onlyShowInstalled = e;
304 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
306 QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
307 QVariant v = index.data(AircraftPackageStatusRole);
308 AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
309 if (status == NoOfficialCatalogMessage) {
313 if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
317 if (m_onlyShowInstalled) {
318 QVariant v = index.data(AircraftPackageStatusRole);
319 AircraftItemStatus status = static_cast<AircraftItemStatus>(v.toInt());
320 if (status == PackageNotInstalled) {
325 if (!m_onlyShowInstalled && m_ratingsFilter) {
326 for (int i=0; i<4; ++i) {
327 if (m_ratings[i] > index.data(AircraftRatingRole + i).toInt()) {
337 bool m_ratingsFilter;
338 bool m_onlyShowInstalled;
342 class NoOfficialHangarMessage : public QWidget
346 NoOfficialHangarMessage() :
347 m_ui(new Ui::NoOfficialHangarMessage)
350 // proxy this signal upwards
351 connect(m_ui->label, &QLabel::linkActivated,
352 this, &NoOfficialHangarMessage::linkActivated);
356 void linkActivated(QUrl link);
359 Ui::NoOfficialHangarMessage* m_ui;
362 static void initQtResources()
364 Q_INIT_RESOURCE(resources);
370 void initApp(int& argc, char** argv)
372 static bool qtInitDone = false;
378 sglog().setLogLevels( SG_ALL, SG_INFO );
379 initQtResources(); // can't be called from a namespace
381 s_argc = argc; // QApplication only stores a reference to argc,
382 // and may crash if it is freed
383 // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication
385 QApplication* app = new QApplication(s_argc, argv);
386 app->setOrganizationName("FlightGear");
387 app->setApplicationName("FlightGear");
388 app->setOrganizationDomain("flightgear.org");
390 QSettings::setDefaultFormat(QSettings::IniFormat);
391 QSettings::setPath(QSettings::IniFormat, QSettings::UserScope,
392 QString::fromStdString(globals->get_fg_home().utf8Str()));
394 // reset numeric / collation locales as described at:
395 // http://doc.qt.io/qt-5/qcoreapplication.html#details
396 ::setlocale(LC_NUMERIC, "C");
397 ::setlocale(LC_COLLATE, "C");
399 Qt::KeyboardModifiers mods = app->queryKeyboardModifiers();
400 if (mods & (Qt::AltModifier | Qt::ShiftModifier)) {
401 qWarning() << "Alt/shift pressed during launch";
403 settings.setValue("fg-root", "!ask");
408 void loadNaturalEarthFile(const std::string& aFileName,
409 flightgear::PolyLine::Type aType,
412 SGPath path(globals->get_fg_root());
413 path.append( "Geodata" );
414 path.append(aFileName);
416 return; // silently fail for now
418 flightgear::PolyLineList lines;
419 flightgear::SHPParser::parsePolyLines(path, aType, lines, areClosed);
420 flightgear::PolyLine::bulkAddToSpatialIndex(lines);
423 void loadNaturalEarthData()
428 loadNaturalEarthFile("ne_10m_coastline.shp", flightgear::PolyLine::COASTLINE, false);
429 loadNaturalEarthFile("ne_10m_rivers_lake_centerlines.shp", flightgear::PolyLine::RIVER, false);
430 loadNaturalEarthFile("ne_10m_lakes.shp", flightgear::PolyLine::LAKE, true);
432 qDebug() << "load basic data took" << st.elapsedMSec();
436 loadNaturalEarthFile("ne_10m_urban_areas.shp", flightgear::PolyLine::URBAN, true);
438 qDebug() << "loading urban areas took:" << st.elapsedMSec();
441 bool runLauncherDialog()
443 // startup the nav-cache now. This pre-empts normal startup of
444 // the cache, but no harm done. (Providing scenery paths are consistent)
449 QString downloadDir = settings.value("download-dir").toString();
450 if (!downloadDir.isEmpty()) {
451 flightgear::Options::sharedInstance()->setOption("download-dir", downloadDir.toStdString());
456 // startup the HTTP system now since packages needs it
457 FGHTTPClient* http = globals->add_new_subsystem<FGHTTPClient>();
459 // we guard against re-init in the global phase; bind and postinit
460 // will happen as normal
463 loadNaturalEarthData();
465 // avoid double Apple menu and other weirdness if both Qt and OSG
466 // try to initialise various Cocoa structures.
467 flightgear::WindowBuilder::setPoseAsStandaloneApp(false);
472 int appResult = qApp->exec();
474 return false; // quit
477 // don't set scenery paths twice
478 globals->clear_fg_scenery();
483 bool runInAppLauncherDialog()
488 if (dlg.result() != QDialog::Accepted) {
495 } // of namespace flightgear
497 QtLauncher::QtLauncher() :
500 m_subsystemIdleTimer(NULL),
502 m_doRestoreMPServer(false)
504 m_ui.reset(new Ui::Launcher);
507 #if QT_VERSION >= 0x050300
508 // don't require Qt 5.3
509 m_ui->commandLineArgs->setPlaceholderText("--option=value --prop:/sim/name=value");
512 #if QT_VERSION >= 0x050200
513 m_ui->aircraftFilter->setClearButtonEnabled(true);
516 for (int i=0; i<4; ++i) {
517 m_ratingFilters[i] = 3;
520 m_subsystemIdleTimer = new QTimer(this);
521 m_subsystemIdleTimer->setInterval(0);
522 connect(m_subsystemIdleTimer, &QTimer::timeout,
523 this, &QtLauncher::onSubsytemIdleTimeout);
524 m_subsystemIdleTimer->start();
526 // create and configure the proxy model
527 m_aircraftProxy = new AircraftProxyModel(this);
528 connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
529 m_aircraftProxy, &AircraftProxyModel::setRatingFilterEnabled);
530 connect(m_ui->ratingsFilterCheck, &QAbstractButton::toggled,
531 this, &QtLauncher::maybeRestoreAircraftSelection);
533 connect(m_ui->onlyShowInstalledCheck, &QAbstractButton::toggled,
534 m_aircraftProxy, &AircraftProxyModel::setInstalledFilterEnabled);
535 connect(m_ui->aircraftFilter, &QLineEdit::textChanged,
536 m_aircraftProxy, &QSortFilterProxyModel::setFilterFixedString);
538 connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
539 connect(m_ui->quitButton, SIGNAL(clicked()), this, SLOT(onQuit()));
541 connect(m_ui->aircraftHistory, &QPushButton::clicked,
542 this, &QtLauncher::onPopupAircraftHistory);
544 connect(m_ui->location, &LocationWidget::descriptionChanged,
545 m_ui->locationDescription, &QLabel::setText);
547 QAction* qa = new QAction(this);
548 qa->setShortcut(QKeySequence("Ctrl+Q"));
549 connect(qa, &QAction::triggered, this, &QtLauncher::onQuit);
552 connect(m_ui->editRatingFilter, &QPushButton::clicked,
553 this, &QtLauncher::onEditRatingsFilter);
554 connect(m_ui->onlyShowInstalledCheck, &QCheckBox::toggled,
555 this, &QtLauncher::onShowInstalledAircraftToggled);
557 QIcon historyIcon(":/history-icon");
558 m_ui->aircraftHistory->setIcon(historyIcon);
560 connect(m_ui->timeOfDayCombo, SIGNAL(currentIndexChanged(int)),
561 this, SLOT(updateSettingsSummary()));
562 connect(m_ui->seasonCombo, SIGNAL(currentIndexChanged(int)),
563 this, SLOT(updateSettingsSummary()));
564 connect(m_ui->fetchRealWxrCheckbox, SIGNAL(toggled(bool)),
565 this, SLOT(updateSettingsSummary()));
566 connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
567 this, SLOT(updateSettingsSummary()));
568 connect(m_ui->terrasyncCheck, SIGNAL(toggled(bool)),
569 this, SLOT(updateSettingsSummary()));
570 connect(m_ui->startPausedCheck, SIGNAL(toggled(bool)),
571 this, SLOT(updateSettingsSummary()));
572 connect(m_ui->msaaCheckbox, SIGNAL(toggled(bool)),
573 this, SLOT(updateSettingsSummary()));
575 connect(m_ui->rembrandtCheckbox, SIGNAL(toggled(bool)),
576 this, SLOT(onRembrandtToggled(bool)));
577 connect(m_ui->terrasyncCheck, &QCheckBox::toggled,
578 this, &QtLauncher::onToggleTerrasync);
579 updateSettingsSummary();
581 connect(m_ui->mpServerCombo, SIGNAL(activated(int)),
582 this, SLOT(onMPServerActivated(int)));
584 m_aircraftModel = new AircraftItemModel(this);
585 m_aircraftProxy->setSourceModel(m_aircraftModel);
587 m_aircraftProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
588 m_aircraftProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
589 m_aircraftProxy->setSortRole(Qt::DisplayRole);
590 m_aircraftProxy->setDynamicSortFilter(true);
592 m_ui->aircraftList->setModel(m_aircraftProxy);
593 m_ui->aircraftList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
594 AircraftItemDelegate* delegate = new AircraftItemDelegate(m_ui->aircraftList);
595 m_ui->aircraftList->setItemDelegate(delegate);
596 m_ui->aircraftList->setSelectionMode(QAbstractItemView::SingleSelection);
597 connect(m_ui->aircraftList, &QListView::clicked,
598 this, &QtLauncher::onAircraftSelected);
599 connect(delegate, &AircraftItemDelegate::variantChanged,
600 this, &QtLauncher::onAircraftSelected);
601 connect(delegate, &AircraftItemDelegate::requestInstall,
602 this, &QtLauncher::onRequestPackageInstall);
603 connect(delegate, &AircraftItemDelegate::cancelDownload,
604 this, &QtLauncher::onCancelDownload);
606 connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
607 this, &QtLauncher::onAircraftInstalledCompleted);
608 connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
609 this, &QtLauncher::onAircraftInstallFailed);
610 connect(m_aircraftModel, &AircraftItemModel::scanCompleted,
611 this, &QtLauncher::updateSelectedAircraft);
612 connect(m_ui->restoreDefaultsButton, &QPushButton::clicked,
613 this, &QtLauncher::onRestoreDefaults);
616 AddOnsPage* addOnsPage = new AddOnsPage(NULL, globals->packageRoot());
617 connect(addOnsPage, &AddOnsPage::downloadDirChanged,
618 this, &QtLauncher::onDownloadDirChanged);
619 connect(addOnsPage, &AddOnsPage::sceneryPathsChanged,
620 this, &QtLauncher::setSceneryPaths);
622 m_ui->tabWidget->addTab(addOnsPage, tr("Add-ons"));
623 // after any kind of reset, try to restore selection and scroll
624 // to match the m_selectedAircraft. This needs to be delayed
625 // fractionally otherwise the scrollTo seems to be ignored,
627 connect(m_aircraftProxy, &AircraftProxyModel::modelReset,
628 this, &QtLauncher::delayedAircraftModelReset);
631 m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
632 m_aircraftModel->setPackageRoot(globals->packageRoot());
633 m_aircraftModel->scanDirs();
635 checkOfficialCatalogMessage();
638 onRefreshMPServers();
641 QtLauncher::~QtLauncher()
646 void QtLauncher::setSceneryPaths()
648 globals->clear_fg_scenery();
650 // mimic what optionss.cxx does, so we can find airport data for parking
653 // append explicit scenery paths
654 Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
655 globals->append_fg_scenery(path.toStdString());
658 // append the TerraSync path
659 QString downloadDir = settings.value("download-dir").toString();
660 if (downloadDir.isEmpty()) {
661 downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());
664 SGPath terraSyncDir(downloadDir.toStdString());
665 terraSyncDir.append("TerraSync");
666 if (terraSyncDir.exists()) {
667 globals->append_fg_scenery(terraSyncDir);
672 void QtLauncher::setInAppMode()
675 m_ui->tabWidget->removeTab(2);
676 m_ui->tabWidget->removeTab(2);
678 m_ui->runButton->setText(tr("Apply"));
679 m_ui->quitButton->setText(tr("Cancel"));
681 disconnect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onRun()));
682 connect(m_ui->runButton, SIGNAL(clicked()), this, SLOT(onApply()));
685 void QtLauncher::restoreSettings()
688 m_ui->rembrandtCheckbox->setChecked(settings.value("enable-rembrandt", false).toBool());
689 m_ui->terrasyncCheck->setChecked(settings.value("enable-terrasync", true).toBool());
690 m_ui->fullScreenCheckbox->setChecked(settings.value("start-fullscreen", false).toBool());
691 m_ui->msaaCheckbox->setChecked(settings.value("enable-msaa", false).toBool());
692 m_ui->fetchRealWxrCheckbox->setChecked(settings.value("enable-realwx", true).toBool());
693 m_ui->startPausedCheck->setChecked(settings.value("start-paused", false).toBool());
694 m_ui->timeOfDayCombo->setCurrentIndex(settings.value("timeofday", 0).toInt());
695 m_ui->seasonCombo->setCurrentIndex(settings.value("season", 0).toInt());
697 // full paths to -set.xml files
698 m_recentAircraft = QUrl::fromStringList(settings.value("recent-aircraft").toStringList());
700 if (!m_recentAircraft.empty()) {
701 m_selectedAircraft = m_recentAircraft.front();
703 // select the default C172p
710 m_ui->location->restoreSettings();
713 m_ui->onlyShowInstalledCheck->setChecked(settings.value("only-show-installed", false).toBool());
714 if (m_ui->onlyShowInstalledCheck->isChecked()) {
715 m_ui->ratingsFilterCheck->setEnabled(false);
718 m_ui->ratingsFilterCheck->setChecked(settings.value("ratings-filter", true).toBool());
720 Q_FOREACH(QVariant v, settings.value("min-ratings").toList()) {
721 m_ratingFilters[index++] = v.toInt();
724 m_aircraftProxy->setRatingFilterEnabled(m_ui->ratingsFilterCheck->isChecked());
725 m_aircraftProxy->setRatings(m_ratingFilters);
727 updateSelectedAircraft();
728 maybeRestoreAircraftSelection();
730 m_ui->commandLineArgs->setPlainText(settings.value("additional-args").toString());
732 m_ui->mpBox->setChecked(settings.value("mp-enabled").toBool());
733 m_ui->mpCallsign->setText(settings.value("mp-callsign").toString());
734 // don't restore MP server here, we do it after a refresh
735 m_doRestoreMPServer = true;
738 void QtLauncher::delayedAircraftModelReset()
740 QTimer::singleShot(1, this, SLOT(maybeRestoreAircraftSelection()));
743 void QtLauncher::maybeRestoreAircraftSelection()
745 QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
746 QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
747 if (proxyIndex.isValid()) {
748 m_ui->aircraftList->selectionModel()->setCurrentIndex(proxyIndex,
749 QItemSelectionModel::ClearAndSelect);
750 m_ui->aircraftList->selectionModel()->select(proxyIndex,
751 QItemSelectionModel::ClearAndSelect);
752 m_ui->aircraftList->scrollTo(proxyIndex);
756 void QtLauncher::saveSettings()
759 settings.setValue("enable-rembrandt", m_ui->rembrandtCheckbox->isChecked());
760 settings.setValue("enable-terrasync", m_ui->terrasyncCheck->isChecked());
761 settings.setValue("enable-msaa", m_ui->msaaCheckbox->isChecked());
762 settings.setValue("start-fullscreen", m_ui->fullScreenCheckbox->isChecked());
763 settings.setValue("enable-realwx", m_ui->fetchRealWxrCheckbox->isChecked());
764 settings.setValue("start-paused", m_ui->startPausedCheck->isChecked());
765 settings.setValue("ratings-filter", m_ui->ratingsFilterCheck->isChecked());
766 settings.setValue("only-show-installed", m_ui->onlyShowInstalledCheck->isChecked());
767 settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
769 settings.setValue("timeofday", m_ui->timeOfDayCombo->currentIndex());
770 settings.setValue("season", m_ui->seasonCombo->currentIndex());
771 settings.setValue("additional-args", m_ui->commandLineArgs->toPlainText());
773 m_ui->location->saveSettings();
775 settings.setValue("mp-callsign", m_ui->mpCallsign->text());
776 settings.setValue("mp-server", m_ui->mpServerCombo->currentData());
777 settings.setValue("mp-enabled", m_ui->mpBox->isChecked());
780 void QtLauncher::setEnableDisableOptionFromCheckbox(QCheckBox* cbox, QString name) const
782 flightgear::Options* opt = flightgear::Options::sharedInstance();
783 std::string stdName(name.toStdString());
784 if (cbox->isChecked()) {
785 opt->addOption("enable-" + stdName, "");
787 opt->addOption("disable-" + stdName, "");
791 void QtLauncher::closeEvent(QCloseEvent *event)
796 void QtLauncher::reject()
801 void QtLauncher::onRun()
803 flightgear::Options* opt = flightgear::Options::sharedInstance();
804 setEnableDisableOptionFromCheckbox(m_ui->terrasyncCheck, "terrasync");
805 setEnableDisableOptionFromCheckbox(m_ui->fetchRealWxrCheckbox, "real-weather-fetch");
806 setEnableDisableOptionFromCheckbox(m_ui->rembrandtCheckbox, "rembrandt");
807 setEnableDisableOptionFromCheckbox(m_ui->fullScreenCheckbox, "fullscreen");
808 // setEnableDisableOptionFromCheckbox(m_ui->startPausedCheck, "freeze");
810 bool startPaused = m_ui->startPausedCheck->isChecked() ||
811 m_ui->location->shouldStartPaused();
813 opt->addOption("enable-freeze", "");
816 // MSAA is more complex
817 if (!m_ui->rembrandtCheckbox->isChecked()) {
818 if (m_ui->msaaCheckbox->isChecked()) {
819 globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 1);
820 globals->get_props()->setIntValue("/sim/rendering/multi-samples", 4);
822 globals->get_props()->setIntValue("/sim/rendering/multi-sample-buffers", 0);
827 if (!m_selectedAircraft.isEmpty()) {
828 if (m_selectedAircraft.isLocalFile()) {
829 QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
830 opt->addOption("aircraft-dir", setFileInfo.dir().absolutePath().toStdString());
831 QString setFile = setFileInfo.fileName();
832 Q_ASSERT(setFile.endsWith("-set.xml"));
833 setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
834 opt->addOption("aircraft", setFile.toStdString());
835 } else if (m_selectedAircraft.scheme() == "package") {
836 QString qualifiedId = m_selectedAircraft.path();
837 // no need to set aircraft-dir, handled by the corresponding code
839 opt->addOption("aircraft", qualifiedId.toStdString());
841 qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
844 // manage aircraft history
845 if (m_recentAircraft.contains(m_selectedAircraft))
846 m_recentAircraft.removeOne(m_selectedAircraft);
847 m_recentAircraft.prepend(m_selectedAircraft);
848 if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
849 m_recentAircraft.pop_back();
852 if (m_ui->mpBox->isChecked()) {
853 opt->addOption("callsign", m_ui->mpCallsign->text().toStdString());
854 QString host = m_ui->mpServerCombo->currentData().toString();
856 if (host == "custom") {
858 host = settings.value("mp-custom-host").toString();
859 port = settings.value("mp-custom-port").toInt();
861 port = findMPServerPort(host.toStdString());
863 globals->get_props()->setStringValue("/sim/multiplay/txhost", host.toStdString());
864 globals->get_props()->setIntValue("/sim/multiplay/txport", port);
867 m_ui->location->setLocationOptions();
870 if (m_ui->timeOfDayCombo->currentIndex() != 0) {
871 QString dayval = m_ui->timeOfDayCombo->currentText().toLower();
872 opt->addOption("timeofday", dayval.toStdString());
875 if (m_ui->seasonCombo->currentIndex() != 0) {
876 QString seasonName = m_ui->seasonCombo->currentText().toLower();
877 opt->addOption("season", seasonName.toStdString());
881 QString downloadDir = settings.value("download-dir").toString();
882 if (!downloadDir.isEmpty()) {
885 int result = QMessageBox::question(this, tr("Create download folder?"),
886 tr("The selected location for downloads does not exist. Create it?"),
887 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
888 if (result == QMessageBox::Cancel) {
892 if (result == QMessageBox::Yes) {
893 d.mkpath(downloadDir);
897 opt->addOption("download-dir", downloadDir.toStdString());
901 Q_FOREACH(QString path, settings.value("scenery-paths").toStringList()) {
902 opt->addOption("fg-scenery", path.toStdString());
906 Q_FOREACH(QString path, settings.value("aircraft-paths").toStringList()) {
907 // can't use fg-aircraft for this, as it is processed before the launcher is run
908 globals->append_aircraft_path(path.toStdString());
911 // additional arguments
912 ArgumentsTokenizer tk;
913 Q_FOREACH(ArgumentsTokenizer::Arg a, tk.tokenize(m_ui->commandLineArgs->toPlainText())) {
914 if (a.arg.startsWith("prop:")) {
915 QString v = a.arg.mid(5) + "=" + a.value;
916 opt->addOption("prop", v.toStdString());
918 opt->addOption(a.arg.toStdString(), a.value.toStdString());
922 if (settings.contains("restore-defaults-on-run")) {
923 settings.remove("restore-defaults-on-run");
924 opt->addOption("restore-defaults", "");
933 void QtLauncher::onApply()
938 if (!m_selectedAircraft.isEmpty()) {
939 std::string aircraftPropValue,
942 if (m_selectedAircraft.isLocalFile()) {
943 QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
944 QString setFile = setFileInfo.fileName();
945 Q_ASSERT(setFile.endsWith("-set.xml"));
946 setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
947 aircraftDir = setFileInfo.dir().absolutePath().toStdString();
948 aircraftPropValue = setFile.toStdString();
949 } else if (m_selectedAircraft.scheme() == "package") {
950 // no need to set aircraft-dir, handled by the corresponding code
952 aircraftPropValue = m_selectedAircraft.path().toStdString();
954 qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
957 // manage aircraft history
958 if (m_recentAircraft.contains(m_selectedAircraft))
959 m_recentAircraft.removeOne(m_selectedAircraft);
960 m_recentAircraft.prepend(m_selectedAircraft);
961 if (m_recentAircraft.size() > MAX_RECENT_AIRCRAFT)
962 m_recentAircraft.pop_back();
964 globals->get_props()->setStringValue("/sim/aircraft", aircraftPropValue);
965 globals->get_props()->setStringValue("/sim/aircraft-dir", aircraftDir);
972 void QtLauncher::onQuit()
977 void QtLauncher::onToggleTerrasync(bool enabled)
981 QString downloadDir = settings.value("download-dir").toString();
982 if (downloadDir.isEmpty()) {
983 downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());
986 QFileInfo info(downloadDir);
987 if (!info.exists()) {
989 msg.setWindowTitle(tr("Create download folder?"));
990 msg.setText(tr("The download folder '%1' does not exist, create it now?").arg(downloadDir));
991 msg.addButton(QMessageBox::Yes);
992 msg.addButton(QMessageBox::Cancel);
993 int result = msg.exec();
995 if (result == QMessageBox::Cancel) {
996 m_ui->terrasyncCheck->setChecked(false);
1000 QDir d(downloadDir);
1001 d.mkpath(downloadDir);
1006 void QtLauncher::onAircraftInstalledCompleted(QModelIndex index)
1008 maybeUpdateSelectedAircraft(index);
1011 void QtLauncher::onRatingsFilterToggled()
1013 QModelIndex aircraftIndex = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1014 QModelIndex proxyIndex = m_aircraftProxy->mapFromSource(aircraftIndex);
1015 if (proxyIndex.isValid()) {
1016 m_ui->aircraftList->scrollTo(proxyIndex);
1020 void QtLauncher::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
1022 qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
1025 msg.setWindowTitle(tr("Aircraft installation failed"));
1026 msg.setText(tr("An error occurred installing the aircraft %1: %2").
1027 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
1028 msg.addButton(QMessageBox::Ok);
1031 maybeUpdateSelectedAircraft(index);
1034 void QtLauncher::onAircraftSelected(const QModelIndex& index)
1036 m_selectedAircraft = index.data(AircraftURIRole).toUrl();
1037 updateSelectedAircraft();
1040 void QtLauncher::onRequestPackageInstall(const QModelIndex& index)
1042 // also select, otherwise UI is confusing
1043 m_selectedAircraft = index.data(AircraftURIRole).toUrl();
1044 updateSelectedAircraft();
1046 QString pkg = index.data(AircraftPackageIdRole).toString();
1047 simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1048 if (pref->isInstalled()) {
1049 InstallRef install = pref->existingInstall();
1050 if (install && install->hasUpdate()) {
1051 globals->packageRoot()->scheduleToUpdate(install);
1058 void QtLauncher::onCancelDownload(const QModelIndex& index)
1060 QString pkg = index.data(AircraftPackageIdRole).toString();
1061 simgear::pkg::PackageRef pref = globals->packageRoot()->getPackageById(pkg.toStdString());
1062 simgear::pkg::InstallRef i = pref->existingInstall();
1063 i->cancelDownload();
1066 void QtLauncher::onRestoreDefaults()
1068 QMessageBox mbox(this);
1069 mbox.setText(tr("Restore all settings to defaults?"));
1070 mbox.setInformativeText(tr("Restoring settings to their defaults may affect available add-ons such as scenery or aircraft."));
1071 QPushButton* quitButton = mbox.addButton(tr("Restore and restart now"), QMessageBox::YesRole);
1072 mbox.addButton(QMessageBox::Cancel);
1073 mbox.setDefaultButton(QMessageBox::Cancel);
1074 mbox.setIconPixmap(QPixmap(":/app-icon-large"));
1077 if (mbox.clickedButton() != quitButton) {
1084 settings.setValue("restore-defaults-on-run", true);
1087 restartTheApp(QStringList());
1090 void QtLauncher::maybeUpdateSelectedAircraft(QModelIndex index)
1092 QUrl u = index.data(AircraftURIRole).toUrl();
1093 if (u == m_selectedAircraft) {
1094 // potentially enable the run button now!
1095 updateSelectedAircraft();
1099 void QtLauncher::updateSelectedAircraft()
1101 QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
1102 if (index.isValid()) {
1103 QPixmap pm = index.data(Qt::DecorationRole).value<QPixmap>();
1104 m_ui->thumbnail->setPixmap(pm);
1105 m_ui->aircraftDescription->setText(index.data(Qt::DisplayRole).toString());
1107 int status = index.data(AircraftPackageStatusRole).toInt();
1108 bool canRun = (status == PackageInstalled);
1109 m_ui->runButton->setEnabled(canRun);
1111 LauncherAircraftType aircraftType = Airplane;
1112 if (index.data(AircraftIsHelicopterRole).toBool()) {
1113 aircraftType = Helicopter;
1114 } else if (index.data(AircraftIsSeaplaneRole).toBool()) {
1115 aircraftType = Seaplane;
1118 m_ui->location->setAircraftType(aircraftType);
1120 m_ui->thumbnail->setPixmap(QPixmap());
1121 m_ui->aircraftDescription->setText("");
1122 m_ui->runButton->setEnabled(false);
1126 QModelIndex QtLauncher::proxyIndexForAircraftURI(QUrl uri) const
1128 return m_aircraftProxy->mapFromSource(sourceIndexForAircraftURI(uri));
1131 QModelIndex QtLauncher::sourceIndexForAircraftURI(QUrl uri) const
1133 AircraftItemModel* sourceModel = qobject_cast<AircraftItemModel*>(m_aircraftProxy->sourceModel());
1134 Q_ASSERT(sourceModel);
1135 return sourceModel->indexOfAircraftURI(uri);
1138 void QtLauncher::onPopupAircraftHistory()
1140 if (m_recentAircraft.isEmpty()) {
1145 Q_FOREACH(QUrl uri, m_recentAircraft) {
1146 QModelIndex index = sourceIndexForAircraftURI(uri);
1147 if (!index.isValid()) {
1151 QAction* act = m.addAction(index.data(Qt::DisplayRole).toString());
1155 QPoint popupPos = m_ui->aircraftHistory->mapToGlobal(m_ui->aircraftHistory->rect().bottomLeft());
1156 QAction* triggered = m.exec(popupPos);
1158 m_selectedAircraft = triggered->data().toUrl();
1159 QModelIndex index = proxyIndexForAircraftURI(m_selectedAircraft);
1160 m_ui->aircraftList->selectionModel()->setCurrentIndex(index,
1161 QItemSelectionModel::ClearAndSelect);
1162 m_ui->aircraftFilter->clear();
1163 updateSelectedAircraft();
1167 void QtLauncher::onEditRatingsFilter()
1169 EditRatingsFilterDialog dialog(this);
1170 dialog.setRatings(m_ratingFilters);
1173 if (dialog.result() == QDialog::Accepted) {
1175 for (int i=0; i<4; ++i) {
1176 m_ratingFilters[i] = dialog.getRating(i);
1177 vl.append(m_ratingFilters[i]);
1179 m_aircraftProxy->setRatings(m_ratingFilters);
1182 settings.setValue("min-ratings", vl);
1186 void QtLauncher::updateSettingsSummary()
1188 QStringList summary;
1189 if (m_ui->timeOfDayCombo->currentIndex() > 0) {
1190 summary.append(QString(m_ui->timeOfDayCombo->currentText().toLower()));
1193 if (m_ui->seasonCombo->currentIndex() > 0) {
1194 summary.append(QString(m_ui->seasonCombo->currentText().toLower()));
1197 if (m_ui->rembrandtCheckbox->isChecked()) {
1198 summary.append("Rembrandt enabled");
1199 } else if (m_ui->msaaCheckbox->isChecked()) {
1200 summary.append("anti-aliasing");
1203 if (m_ui->fetchRealWxrCheckbox->isChecked()) {
1204 summary.append("live weather");
1207 if (m_ui->terrasyncCheck->isChecked()) {
1208 summary.append("automatic scenery downloads");
1211 if (m_ui->startPausedCheck->isChecked()) {
1212 summary.append("paused");
1215 QString s = summary.join(", ");
1216 s[0] = s[0].toUpper();
1217 m_ui->settingsDescription->setText(s);
1220 void QtLauncher::onRembrandtToggled(bool b)
1222 // Rembrandt and multi-sample are exclusive
1223 m_ui->msaaCheckbox->setEnabled(!b);
1226 void QtLauncher::onShowInstalledAircraftToggled(bool b)
1228 m_ui->ratingsFilterCheck->setEnabled(!b);
1229 maybeRestoreAircraftSelection();
1232 void QtLauncher::onSubsytemIdleTimeout()
1234 globals->get_subsystem_mgr()->update(0.0);
1237 void QtLauncher::onDownloadDirChanged()
1240 // replace existing package root
1241 globals->get_subsystem<FGHTTPClient>()->shutdown();
1242 globals->setPackageRoot(simgear::pkg::RootRef());
1244 // create new root with updated download-dir value
1245 fgInitPackageRoot();
1247 globals->get_subsystem<FGHTTPClient>()->init();
1250 // re-scan the aircraft list
1251 m_aircraftModel->setPackageRoot(globals->packageRoot());
1252 m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList());
1253 m_aircraftModel->scanDirs();
1255 checkOfficialCatalogMessage();
1257 // re-set scenery dirs
1261 void QtLauncher::checkOfficialCatalogMessage()
1264 bool showOfficialCatalogMesssage = !globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
1265 if (settings.value("hide-official-catalog-message").toBool()) {
1266 showOfficialCatalogMesssage = false;
1269 m_aircraftModel->setOfficialHangarMessageVisible(showOfficialCatalogMesssage);
1270 if (showOfficialCatalogMesssage) {
1271 NoOfficialHangarMessage* messageWidget = new NoOfficialHangarMessage;
1272 connect(messageWidget, &NoOfficialHangarMessage::linkActivated,
1273 this, &QtLauncher::onOfficialCatalogMessageLink);
1275 QModelIndex index = m_aircraftProxy->mapFromSource(m_aircraftModel->officialHangarMessageIndex());
1276 m_ui->aircraftList->setIndexWidget(index, messageWidget);
1280 void QtLauncher::onOfficialCatalogMessageLink(QUrl link)
1282 QString s = link.toString();
1283 if (s == "action:hide") {
1285 settings.setValue("hide-official-catalog-message", true);
1286 } else if (s == "action:add-official") {
1287 AddOnsPage::addDefaultCatalog(this);
1290 checkOfficialCatalogMessage();
1293 void QtLauncher::onRefreshMPServers()
1295 if (m_mpServerRequest.get()) {
1296 return; // in-progress
1299 string url(fgGetString("/sim/multiplay/serverlist-url",
1300 "http://liveries.flightgear.org/mpstatus/mpservers.xml"));
1303 SG_LOG(SG_IO, SG_ALERT, "do_multiplayer.refreshserverlist: no URL given");
1307 SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1308 m_mpServerRequest.reset(new RemoteXMLRequest(url, targetnode));
1309 m_mpServerRequest->done(this, &QtLauncher::onRefreshMPServersDone);
1310 m_mpServerRequest->fail(this, &QtLauncher::onRefreshMPServersFailed);
1311 globals->get_subsystem<FGHTTPClient>()->makeRequest(m_mpServerRequest);
1314 void QtLauncher::onRefreshMPServersDone(simgear::HTTP::Request*)
1316 // parse the properties
1317 SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1318 m_ui->mpServerCombo->clear();
1320 for (int i=0; i<targetnode->nChildren(); ++i) {
1321 SGPropertyNode* c = targetnode->getChild(i);
1322 if (c->getName() != std::string("server")) {
1326 QString name = QString::fromStdString(c->getStringValue("name"));
1327 QString loc = QString::fromStdString(c->getStringValue("location"));
1328 QString host = QString::fromStdString(c->getStringValue("hostname"));
1329 m_ui->mpServerCombo->addItem(tr("%1 - %2").arg(name,loc), host);
1332 EditCustomMPServerDialog::addCustomItem(m_ui->mpServerCombo);
1333 restoreMPServerSelection();
1335 m_mpServerRequest.clear();
1338 void QtLauncher::onRefreshMPServersFailed(simgear::HTTP::Request*)
1340 qWarning() << "refreshing MP servers failed:" << QString::fromStdString(m_mpServerRequest->responseReason());
1341 m_mpServerRequest.clear();
1342 EditCustomMPServerDialog::addCustomItem(m_ui->mpServerCombo);
1343 restoreMPServerSelection();
1346 void QtLauncher::restoreMPServerSelection()
1348 if (m_doRestoreMPServer) {
1350 int index = m_ui->mpServerCombo->findData(settings.value("mp-server"));
1352 m_ui->mpServerCombo->setCurrentIndex(index);
1354 m_doRestoreMPServer = false;
1358 void QtLauncher::onMPServerActivated(int index)
1360 if (m_ui->mpServerCombo->itemData(index) == "custom") {
1361 EditCustomMPServerDialog dlg(this);
1363 if (dlg.result() == QDialog::Accepted) {
1364 m_ui->mpServerCombo->setItemText(index, tr("Custom - %1").arg(dlg.hostname()));
1369 int QtLauncher::findMPServerPort(const std::string& host)
1371 SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true);
1372 for (int i=0; i<targetnode->nChildren(); ++i) {
1373 SGPropertyNode* c = targetnode->getChild(i);
1374 if (c->getName() != std::string("server")) {
1378 if (c->getStringValue("hostname") == host) {
1379 return c->getIntValue("port");
1386 simgear::pkg::PackageRef QtLauncher::packageForAircraftURI(QUrl uri) const
1388 if (uri.scheme() != "package") {
1389 qWarning() << "invalid URL scheme:" << uri;
1390 return simgear::pkg::PackageRef();
1393 QString ident = uri.path();
1394 return globals->packageRoot()->getPackageById(ident.toStdString());
1397 void QtLauncher::restartTheApp(QStringList fgArgs)
1399 // Spawn a new instance of myApplication:
1403 #if defined(Q_OS_MAC)
1404 QDir dir(qApp->applicationDirPath()); // returns the 'MacOS' dir
1405 dir.cdUp(); // up to 'contents' dir
1406 dir.cdUp(); // up to .app dir
1407 // see 'man open' for details, but '-n' ensures we launch a new instance,
1408 // and we want to pass remaining arguments to us, not open.
1409 args << "-n" << dir.absolutePath() << "--args" << "--launcher" << fgArgs;
1410 qDebug() << "args" << args;
1411 proc.startDetached("open", args);
1413 args << "--launcher" << fgArgs;
1414 proc.startDetached(qApp->applicationFilePath(), args);
1419 #include "QtLauncher.moc"