"Fossies" - the Fresh Open Source Software Archive

Member "flightgear-2020.3.8/src/GUI/LauncherController.cxx" (24 Mar 2021, 32054 Bytes) of package /linux/privat/flightgear-2020.3.8.tar.bz2:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "LauncherController.cxx" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 2020.3.7_vs_2020.3.8.

    1 #include "LauncherController.hxx"
    2 
    3 // Qt headers
    4 #include <QDebug>
    5 #include <QDesktopServices>
    6 #include <QFileDialog>
    7 #include <QJSEngine>
    8 #include <QMessageBox>
    9 #include <QNetworkAccessManager>
   10 #include <QNetworkDiskCache>
   11 #include <QPushButton>
   12 #include <QQmlComponent>
   13 #include <QQuickWindow>
   14 #include <QSettings>
   15 
   16 // simgear headers
   17 #include <simgear/package/Install.hxx>
   18 #include <simgear/environment/metar.hxx>
   19 #include <simgear/structure/exception.hxx>
   20 
   21 // FlightGear headers
   22 #include <Network/HTTPClient.hxx>
   23 #include <Main/globals.hxx>
   24 #include <Airports/airport.hxx>
   25 #include <Main/options.hxx>
   26 #include <Main/fg_init.hxx>
   27 #include <Main/fg_props.hxx>
   28 #include "version.h"
   29 #include <Main/sentryIntegration.hxx>
   30 
   31 #include "AircraftModel.hxx"
   32 #include "AircraftSearchFilterModel.hxx"
   33 #include "AirportDiagram.hxx"
   34 #include "CarrierDiagram.hxx"
   35 #include "CarriersLocationModel.hxx"
   36 #include "DefaultAircraftLocator.hxx"
   37 #include "FlightPlanController.hxx"
   38 #include "HoverArea.hxx"
   39 #include "LaunchConfig.hxx"
   40 #include "LauncherArgumentTokenizer.hxx"
   41 #include "LauncherNotificationsController.hxx"
   42 #include "LocationController.hxx"
   43 #include "MPServersModel.h"
   44 #include "ModelDataExtractor.hxx"
   45 #include "NavaidDiagram.hxx"
   46 #include "NavaidSearchModel.hxx"
   47 #include "PathUrlHelper.hxx"
   48 #include "PixmapImageItem.hxx"
   49 #include "PreviewImageItem.hxx"
   50 #include "QmlAircraftInfo.hxx"
   51 #include "QmlPositioned.hxx"
   52 #include "QmlRadioButtonHelper.hxx"
   53 #include "QtLauncher.hxx"
   54 #include "RecentAircraftModel.hxx"
   55 #include "RecentLocationsModel.hxx"
   56 #include "RouteDiagram.hxx"
   57 #include "SetupRootDialog.hxx"
   58 #include "StackController.hxx"
   59 #include "ThumbnailImageItem.hxx"
   60 #include "UnitsModel.hxx"
   61 #include "UpdateChecker.hxx"
   62 #include "GettingStartedTipsController.hxx"
   63 #include "GettingStartedTip.hxx"
   64 #include "TipBackgroundBox.hxx"
   65 #include "GettingStartedScope.hxx"
   66 
   67 using namespace simgear::pkg;
   68 
   69 LauncherController::LauncherController(QObject *parent, QWindow* window) :
   70     QObject(parent),
   71     m_window(window)
   72 {
   73     m_serversModel = new MPServersModel(this);
   74     m_location = new LocationController(this);
   75     m_locationHistory = new RecentLocationsModel(this);
   76     m_selectedAircraftInfo = new QmlAircraftInfo(this);
   77 
   78     m_config = new LaunchConfig(this);
   79     connect(m_config, &LaunchConfig::collect, this, &LauncherController::collectAircraftArgs);
   80     connect(m_config, &LaunchConfig::save, this, &LauncherController::saveAircraft);
   81     connect(m_config, &LaunchConfig::restore, this, &LauncherController::restoreAircraft);
   82 
   83     m_flightPlan = new FlightPlanController(this, m_config);
   84 
   85     m_location->setLaunchConfig(m_config);
   86     connect(m_location, &LocationController::descriptionChanged,
   87             this, &LauncherController::summaryChanged);
   88 
   89     initQML();
   90 
   91     m_aircraftModel = new AircraftItemModel(this);
   92     m_installedAircraftModel = new AircraftProxyModel(this, m_aircraftModel);
   93     m_installedAircraftModel->setInstalledFilterEnabled(true);
   94 
   95     m_aircraftWithUpdatesModel = new AircraftProxyModel(this, m_aircraftModel);
   96     m_aircraftWithUpdatesModel->setInstalledFilterEnabled(true);
   97     m_aircraftWithUpdatesModel->setHaveUpdateFilterEnabled(true);
   98 
   99     m_browseAircraftModel = new AircraftProxyModel(this, m_aircraftModel);
  100     m_browseAircraftModel->setRatingFilterEnabled(true);
  101 
  102     m_aircraftSearchModel = new AircraftProxyModel(this, m_aircraftModel);
  103 
  104     m_favouriteAircraftModel = new AircraftProxyModel(this, m_aircraftModel);
  105     m_favouriteAircraftModel->setShowFavourites(true);
  106 
  107     m_aircraftHistory = new RecentAircraftModel(m_aircraftModel, this);
  108 
  109     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
  110             this, &LauncherController::onAircraftInstalledCompleted);
  111     connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
  112             this, &LauncherController::onAircraftInstallFailed);
  113 
  114     connect(LocalAircraftCache::instance(),
  115             &LocalAircraftCache::scanCompleted,
  116             this, &LauncherController::updateSelectedAircraft);
  117 
  118     QSettings settings;
  119     m_aircraftModel->setPackageRoot(globals->packageRoot());
  120 
  121     m_aircraftGridMode = settings.value("aircraftGridMode").toBool();
  122 
  123     m_subsystemIdleTimer = new QTimer(this);
  124     m_subsystemIdleTimer->setInterval(5);
  125     connect(m_subsystemIdleTimer, &QTimer::timeout, []()
  126        {globals->get_subsystem_mgr()->update(0.0);});
  127     m_subsystemIdleTimer->start();
  128 
  129     QRect winRect= settings.value("window-geometry").toRect();
  130 
  131     if (winRect.isValid()) {
  132         m_window->setGeometry(winRect);
  133     } else {
  134         m_window->setWidth(600);
  135         m_window->setHeight(800);
  136     }
  137 
  138     if (settings.contains("window-state")) {
  139         const auto ws = static_cast<Qt::WindowState>(settings.value("window-state").toInt());
  140         m_window->setWindowState(ws);
  141     }
  142 
  143     // count launches; we use this to trigger first-run and periodic notices
  144     // in the UI.
  145     m_launchCount = settings.value("launch-count", 0).toInt();
  146     settings.setValue("launch-count", m_launchCount + 1);
  147 
  148     std::ostringstream os;
  149     string_list versionParts = simgear::strutils::split(FLIGHTGEAR_VERSION, ".");
  150     if (versionParts.size() >= 2) {
  151         // build a setting key like launch-count-2020-2
  152         QString versionedCountKey = QString::fromStdString("launch-count-" + versionParts.at(0) + "-" + versionParts.at(1));
  153         m_versionLaunchCount = settings.value(versionedCountKey, 0).toInt();
  154         settings.setValue(versionedCountKey, m_versionLaunchCount + 1);
  155     }
  156 
  157     QTimer::singleShot(2000, this, &LauncherController::checkForOldDownloadDir);
  158 }
  159 
  160 void LauncherController::initQML()
  161 {
  162     qmlRegisterUncreatableType<LauncherController>("FlightGear.Launcher", 1, 0, "LauncherController", "no");
  163     qmlRegisterUncreatableType<LocationController>("FlightGear.Launcher", 1, 0, "LocationController", "no");
  164     qmlRegisterUncreatableType<FlightPlanController>("FlightGear.Launcher", 1, 0, "FlightPlanController", "no");
  165     qmlRegisterUncreatableType<UpdateChecker>("FlightGear.Launcher", 1, 0, "UpdateChecker", "for enums");
  166 
  167     qmlRegisterType<LauncherArgumentTokenizer>("FlightGear.Launcher", 1, 0, "ArgumentTokenizer");
  168     qmlRegisterUncreatableType<QAbstractItemModel>("FlightGear.Launcher", 1, 0, "QAIM", "no");
  169     qmlRegisterUncreatableType<AircraftProxyModel>("FlightGear.Launcher", 1, 0, "AircraftProxyModel", "no");
  170     qmlRegisterUncreatableType<RecentAircraftModel>("FlightGear.Launcher", 1, 0, "RecentAircraftModel", "no");
  171     qmlRegisterUncreatableType<RecentLocationsModel>("FlightGear.Launcher", 1, 0, "RecentLocationsModel", "no");
  172     qmlRegisterUncreatableType<LaunchConfig>("FlightGear.Launcher", 1, 0, "LaunchConfig", "Singleton API");
  173     qmlRegisterUncreatableType<MPServersModel>("FlightGear.Launcher", 1, 0, "MPServers", "Singleton API");
  174 
  175     qmlRegisterType<NavaidSearchModel>("FlightGear", 1, 0, "NavaidSearch");
  176     qmlRegisterType<CarriersLocationModel>("FlightGear", 1, 0, "CarriersModel");
  177 
  178     qmlRegisterUncreatableType<Units>("FlightGear", 1, 0, "Units", "Only for enum");
  179     qmlRegisterType<UnitsModel>("FlightGear", 1, 0, "UnitsModel");
  180 
  181     qmlRegisterType<FileDialogWrapper>("FlightGear", 1, 0, "FileDialog");
  182     qmlRegisterType<QmlAircraftInfo>("FlightGear.Launcher", 1, 0, "AircraftInfo");
  183 
  184     qmlRegisterUncreatableType<LocalAircraftCache>("FlightGear.Launcher", 1, 0, "LocalAircraftCache", "Aircraft cache");
  185     qmlRegisterUncreatableType<AircraftItemModel>("FlightGear.Launcher", 1, 0, "AircraftModel", "Built-in model");
  186     qmlRegisterType<ThumbnailImageItem>("FlightGear.Launcher", 1, 0, "ThumbnailImage");
  187     qmlRegisterType<PreviewImageItem>("FlightGear.Launcher", 1, 0, "PreviewImage");
  188 
  189     qmlRegisterType<QmlPositioned>("FlightGear", 1, 0, "Positioned");
  190     // this is a Q_GADGET, but we need to register it for use in return types, etc
  191     qRegisterMetaType<QmlGeod>();
  192 
  193     qmlRegisterType<PixmapImageItem>("FlightGear", 1, 0, "PixmapImage");
  194     qmlRegisterType<AirportDiagram>("FlightGear", 1, 0, "AirportDiagram");
  195     qmlRegisterType<CarrierDiagram>("FlightGear", 1, 0, "CarrierDiagram");
  196     qmlRegisterType<NavaidDiagram>("FlightGear", 1, 0, "NavaidDiagram");
  197     qmlRegisterType<RouteDiagram>("FlightGear", 1, 0, "RouteDiagram");
  198     qmlRegisterType<QmlRadioButtonGroup>("FlightGear", 1, 0, "RadioButtonGroup");
  199     qmlRegisterType<HoverArea>("FlightGear", 1, 0, "HoverArea");
  200     qmlRegisterType<StackController>("FlightGear", 1, 0, "StackController");
  201 
  202     qmlRegisterType<ModelDataExtractor>("FlightGear", 1, 0, "ModelDataExtractor");
  203 
  204     qmlRegisterSingletonType(QUrl("qrc:/qml/OverlayShared.qml"), "FlightGear", 1, 0, "OverlayShared");
  205 
  206     qmlRegisterType<GettingStartedScope>("FlightGear", 1, 0, "GettingStartedScope");
  207     qmlRegisterType<GettingStartedTipsController>("FlightGear", 1, 0, "GettingStartedController");
  208     qmlRegisterType<GettingStartedTip>("FlightGear", 1, 0, "GettingStartedTip");
  209     qmlRegisterType<TipBackgroundBox>("FlightGear", 1, 0, "TipBackgroundBox");
  210 
  211     QNetworkDiskCache* diskCache = new QNetworkDiskCache(this);
  212     SGPath cachePath = globals->get_fg_home() / "PreviewsCache";
  213     diskCache->setCacheDirectory(QString::fromStdString(cachePath.utf8Str()));
  214 
  215     QNetworkAccessManager* netAccess = new QNetworkAccessManager(this);
  216     netAccess->setCache(diskCache);
  217     PreviewImageItem::setGlobalNetworkAccess(netAccess);
  218 }
  219 
  220 void LauncherController::setInAppMode()
  221 {
  222     m_inAppMode = true;
  223     m_keepRunningInAppMode = true;
  224     m_appModeResult = true;
  225     emit inAppChanged();
  226 }
  227 
  228 bool LauncherController::keepRunningInAppMode() const
  229 {
  230     return m_keepRunningInAppMode;
  231 }
  232 
  233 bool LauncherController::inAppResult() const
  234 {
  235     return m_appModeResult;
  236 }
  237 
  238 void LauncherController::initialRestoreSettings()
  239 {
  240     m_selectedAircraft = m_aircraftHistory->mostRecent();
  241     if (m_selectedAircraft.isEmpty()) {
  242         // select the default aircraft specified in defaults.xml
  243         flightgear::DefaultAircraftLocator da;
  244         if (da.foundPath().exists()) {
  245             m_selectedAircraft = QUrl::fromLocalFile(QString::fromStdString(da.foundPath().utf8Str()));
  246             qDebug() << "Restored default aircraft:" << m_selectedAircraft;
  247         } else {
  248             qWarning() << "Completely failed to find the default aircraft";
  249         }
  250     }
  251 
  252     m_location->restoreSearchHistory();
  253     QVariantMap currentLocation = m_locationHistory->mostRecent();
  254     if (currentLocation.isEmpty()) {
  255         // use the default
  256         std::string defaultAirport = flightgear::defaultAirportICAO();
  257         FGAirportRef apt = FGAirport::findByIdent(defaultAirport);
  258         if (apt) {
  259             currentLocation["location-id"] = static_cast<qlonglong>(apt->guid());
  260             currentLocation["location-apt-runway"] = "ACTIVE";
  261         } // otherwise we failed to find the default airport in the nav-db :(
  262     }
  263     m_location->restoreLocation(currentLocation);
  264 
  265     emit selectedAircraftChanged(m_selectedAircraft);
  266 
  267     updateSelectedAircraft();
  268     m_serversModel->requestRestore();
  269     m_aircraftState = m_config->getValueForKey("", "selected-aircraft-state", QString()).toString();
  270     emit selectedAircraftStateChanged();
  271 
  272     emit summaryChanged();
  273  }
  274 
  275 void LauncherController::saveSettings()
  276 {
  277     QSettings settings;
  278     settings.setValue("window-geometry", m_window->geometry());
  279     if (m_window->windowState() != Qt::WindowNoState) {
  280         settings.setValue("window-state", m_window->windowState());
  281     }
  282 
  283     m_config->saveConfigToINI();
  284     m_aircraftHistory->saveToSettings();
  285     m_locationHistory->saveToSettings();
  286 }
  287 
  288 void LauncherController::collectAircraftArgs()
  289 {
  290     if (m_skipAircraftFromArgs)
  291         return;
  292 
  293     // aircraft
  294     if (!m_selectedAircraft.isEmpty()) {
  295         if (m_selectedAircraft.isLocalFile()) {
  296             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
  297             m_config->setArg("aircraft-dir", setFileInfo.dir().absolutePath());
  298             QString setFile = setFileInfo.fileName();
  299             Q_ASSERT(setFile.endsWith("-set.xml"));
  300             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
  301             m_config->setArg("aircraft", setFile);
  302         } else if (m_selectedAircraft.scheme() == "package") {
  303             // no need to set aircraft-dir, handled by the corresponding code
  304             // in fgInitAircraft
  305             m_config->setArg("aircraft", m_selectedAircraft.path());
  306         } else {
  307             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
  308         }
  309     }
  310 
  311     if (m_selectedAircraftInfo->hasStates() && !m_aircraftState.isEmpty()) {
  312         QString state = m_aircraftState;
  313         if ((m_aircraftState == "auto") && !m_selectedAircraftInfo->haveExplicitAutoState()) {
  314             state = selectAircraftStateAutomatically();
  315 #if QT_VERSION >= 0x050600
  316             qInfo() << "doing launcher auto state selection, picked:" + state;
  317 #endif
  318         }
  319 
  320         if (!state.isEmpty()) {
  321             m_config->setArg("state", state);
  322         }
  323     }
  324 }
  325 
  326 void LauncherController::saveAircraft()
  327 {
  328     m_config->setValueForKey("", "selected-aircraft", m_selectedAircraft);
  329     if (!m_aircraftState.isEmpty()) {
  330         m_config->setValueForKey("", "selected-aircraft-state", m_aircraftState);
  331     }
  332 }
  333 
  334 void LauncherController::restoreAircraft()
  335 {
  336     m_selectedAircraft = m_config->getValueForKey("", "selected-aircraft", QUrl()).toUrl();
  337     m_aircraftState = m_config->getValueForKey("", "selected-aircraft-state", QString()).toString();
  338     emit selectedAircraftChanged(m_selectedAircraft);
  339     updateSelectedAircraft();
  340     emit selectedAircraftStateChanged();
  341 }
  342 
  343 void LauncherController::doRun()
  344 {
  345     flightgear::addSentryBreadcrumb("Launcher: fly!", "info");
  346     flightgear::Options* opt = flightgear::Options::sharedInstance();
  347     m_config->reset();
  348     m_config->collect();
  349 
  350     m_aircraftHistory->insert(m_selectedAircraft);
  351 
  352     QVariant locSet = m_location->saveLocation();
  353     m_locationHistory->insert(locSet);
  354 
  355     // aircraft paths
  356     QSettings settings;
  357     QString downloadDir = settings.value("downloadSettings/downloadDir").toString();
  358     if (!downloadDir.isEmpty()) {
  359         QDir d(downloadDir);
  360         if (!d.exists()) {
  361             int result = QMessageBox::question(nullptr, tr("Create download folder?"),
  362                                   tr("The selected location for downloads does not exist. (%1) Create it?").arg(downloadDir),
  363                                                QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
  364             if (result == QMessageBox::Cancel) {
  365                 return;
  366             }
  367 
  368             if (result == QMessageBox::Yes) {
  369                 d.mkpath(downloadDir);
  370             }
  371         }
  372     }
  373 
  374     if (settings.contains("restore-defaults-on-run")) {
  375         settings.remove("restore-defaults-on-run");
  376         opt->addOption("restore-defaults", "");
  377     }
  378 
  379     m_config->applyToOptions();
  380     saveSettings();
  381 }
  382 
  383 void LauncherController::doApply()
  384 {
  385     // aircraft
  386     if (!m_selectedAircraft.isEmpty()) {
  387         std::string aircraftPropValue,
  388             aircraftDir;
  389 
  390         if (m_selectedAircraft.isLocalFile()) {
  391             QFileInfo setFileInfo(m_selectedAircraft.toLocalFile());
  392             QString setFile = setFileInfo.fileName();
  393             Q_ASSERT(setFile.endsWith("-set.xml"));
  394             setFile.truncate(setFile.count() - 8); // drop the '-set.xml' portion
  395             aircraftDir = setFileInfo.dir().absolutePath().toStdString();
  396             aircraftPropValue = setFile.toStdString();
  397         } else if (m_selectedAircraft.scheme() == "package") {
  398             // no need to set aircraft-dir, handled by the corresponding code
  399             // in fgInitAircraft
  400             aircraftPropValue = m_selectedAircraft.path().toStdString();
  401         } else {
  402             qWarning() << "unsupported aircraft launch URL" << m_selectedAircraft;
  403         }
  404 
  405         m_aircraftHistory->insert(m_selectedAircraft);
  406         globals->get_props()->setStringValue("/sim/aircraft", aircraftPropValue);
  407         globals->get_props()->setStringValue("/sim/aircraft-dir", aircraftDir);
  408     }
  409 
  410     m_location->setLocationProperties();
  411     saveSettings();
  412 }
  413 
  414 
  415 QString LauncherController::selectAircraftStateAutomatically()
  416 {
  417     if (!m_selectedAircraftInfo)
  418         return {};
  419 
  420     if (m_location->isAirborneLocation() && m_selectedAircraftInfo->hasState("cruise")) {
  421         const double altitudeFt = m_location->altitude().convertToUnit(Units::FeetMSL).value;
  422         if (altitudeFt > 6000) {
  423             return "cruise";
  424         }
  425     }
  426 
  427     if (m_location->isCarrier() && m_location->isAirborneLocation() && m_selectedAircraftInfo->hasState("carrier-approach")) {
  428         return "carrier-approach";
  429     }
  430 
  431     if (m_location->isAirborneLocation() && m_selectedAircraftInfo->hasState("approach")) {
  432         return "approach";
  433     }
  434 
  435     if (m_location->isParkedLocation()) {
  436         if (m_selectedAircraftInfo->hasState("parked")) {
  437             return "parked";
  438         }
  439         if (m_selectedAircraftInfo->hasState("parking")) {
  440             return "parking";
  441         }
  442     }
  443 
  444     if (m_location->isCarrier() && m_selectedAircraftInfo->hasState("carrier-take-off")) {
  445         return "carrier-take-off";
  446     }
  447 
  448     if (m_selectedAircraftInfo->hasState("take-off")) {
  449         return "take-off";
  450     }
  451 
  452     return {}; // failed to compute, give up
  453 }
  454 
  455 void LauncherController::maybeUpdateSelectedAircraft(QModelIndex index)
  456 {
  457     QUrl u = index.data(AircraftURIRole).toUrl();
  458     if (u == m_selectedAircraft) {
  459         // potentially enable the run button now!
  460         updateSelectedAircraft();
  461     }
  462 }
  463 
  464 void LauncherController::updateSelectedAircraft()
  465 {
  466     m_selectedAircraftInfo->setUri(m_selectedAircraft);
  467     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
  468     if (index.isValid()) {
  469         // we have to default to unknown here, until we have an explicit
  470         // way to determine if it's a regular aircraft or not
  471         m_aircraftType = Unknown;
  472         if (index.data(AircraftIsHelicopterRole).toBool()) {
  473             m_aircraftType = Helicopter;
  474         } else if (index.data(AircraftIsSeaplaneRole).toBool()) {
  475             m_aircraftType = Seaplane;
  476         }
  477     }
  478 
  479     if (!m_aircraftState.isEmpty()) {
  480       if (!m_selectedAircraftInfo->hasState(m_aircraftState)) {
  481         m_aircraftState.clear();
  482         emit selectedAircraftStateChanged();
  483       }
  484     }
  485 
  486     emit canFlyChanged();
  487 }
  488 
  489 bool LauncherController::canFly() const
  490 {
  491     QModelIndex index = m_aircraftModel->indexOfAircraftURI(m_selectedAircraft);
  492     if (!index.isValid()) {
  493         return false;
  494     }
  495 
  496     int status = index.data(AircraftPackageStatusRole).toInt();
  497     bool canRun = (status == LocalAircraftCache::PackageInstalled);
  498     return canRun;
  499 }
  500 
  501 void LauncherController::downloadDirChanged(QString path)
  502 {
  503     // this can get run if the UI is disabled, just bail out before doing
  504     // anything permanent.
  505     if (!m_config->enableDownloadDirUI()) {
  506         return;
  507     }
  508 
  509     // if the default dir is passed in, map that back to the empty string
  510     if (path == m_config->defaultDownloadDir()) {
  511         path.clear();
  512     }
  513 
  514     auto options = flightgear::Options::sharedInstance();
  515     if (options->valueForOption("download-dir") == path.toStdString()) {
  516         // this works because we propogate the value from QSettings to
  517         // the flightgear::Options object in runLauncherDialog()
  518         // so the options object always contains our current idea of this
  519         // value
  520         return;
  521     }
  522 
  523     if (!path.isEmpty()) {
  524         options->setOption("download-dir", path.toStdString());
  525     } else {
  526         options->clearOption("download-dir");
  527     }
  528 
  529     m_config->setValueForKey("", "download-dir", path);
  530     saveSettings();
  531     flightgear::restartTheApp();
  532 }
  533 
  534 QmlAircraftInfo *LauncherController::selectedAircraftInfo() const
  535 {
  536     return m_selectedAircraftInfo;
  537 }
  538 
  539 void LauncherController::restoreLocation(QVariant var)
  540 {
  541     m_location->restoreLocation(var.toMap());
  542 }
  543 
  544 QUrl LauncherController::selectedAircraft() const
  545 {
  546     return m_selectedAircraft;
  547 }
  548 
  549 bool LauncherController::matchesSearch(QString term, QStringList keywords) const
  550 {
  551     Q_FOREACH(QString s, keywords) {
  552         if (s.contains(term, Qt::CaseInsensitive)) {
  553             return true;
  554         }
  555     }
  556 
  557     return false;
  558 }
  559 
  560 bool LauncherController::isSearchActive() const
  561 {
  562     return !m_settingsSearchTerm.isEmpty();
  563 }
  564 
  565 QStringList LauncherController::settingsSummary() const
  566 {
  567     return m_settingsSummary;
  568 }
  569 
  570 QStringList LauncherController::environmentSummary() const
  571 {
  572     return m_environmentSummary;
  573 }
  574 
  575 
  576 void LauncherController::setSelectedAircraft(QUrl selectedAircraft)
  577 {
  578     if (m_selectedAircraft == selectedAircraft)
  579         return;
  580 
  581     m_selectedAircraft = selectedAircraft;
  582     m_aircraftState.clear();
  583 
  584     updateSelectedAircraft();
  585     emit selectedAircraftChanged(m_selectedAircraft);
  586     emit selectedAircraftStateChanged();
  587 }
  588 
  589 void LauncherController::setSettingsSearchTerm(QString settingsSearchTerm)
  590 {
  591     if (m_settingsSearchTerm == settingsSearchTerm)
  592         return;
  593 
  594     m_settingsSearchTerm = settingsSearchTerm;
  595     emit searchChanged();
  596 }
  597 
  598 void LauncherController::setSettingsSummary(QStringList settingsSummary)
  599 {
  600     if (m_settingsSummary == settingsSummary)
  601         return;
  602 
  603     m_settingsSummary = settingsSummary;
  604     emit summaryChanged();
  605 }
  606 
  607 void LauncherController::setEnvironmentSummary(QStringList environmentSummary)
  608 {
  609     if (m_environmentSummary == environmentSummary)
  610         return;
  611 
  612     m_environmentSummary = environmentSummary;
  613     emit summaryChanged();
  614 }
  615 
  616 void LauncherController::fly()
  617 {
  618     // avoid duplicate calls to fly, if the user's system is slow, and they
  619     // generate multiple clicks / events before the qApp->exit() fires.
  620     // without this, we can apply options, etc twice which breaks.
  621     if (m_flyRequested)
  622         return;
  623     m_flyRequested = true;
  624 
  625     if (m_inAppMode) {
  626         doApply();
  627         m_keepRunningInAppMode = false;
  628         m_appModeResult = true;
  629     } else {
  630         doRun();
  631         qApp->exit(1);
  632     }
  633 }
  634 
  635 void LauncherController::quit()
  636 {
  637     if (m_inAppMode) {
  638         m_keepRunningInAppMode = false;
  639         m_appModeResult = false;
  640     } else {
  641         saveSettings();
  642         qApp->exit(0);
  643     }
  644 }
  645 
  646 QStringList LauncherController::combinedSummary() const
  647 {
  648     return m_settingsSummary + m_environmentSummary;
  649 }
  650 
  651 simgear::pkg::PackageRef LauncherController::packageForAircraftURI(QUrl uri) const
  652 {
  653     if (uri.scheme() != "package") {
  654         qWarning() << "invalid URL scheme:" << uri;
  655         return simgear::pkg::PackageRef();
  656     }
  657 
  658     QString ident = uri.path();
  659     return globals->packageRoot()->getPackageById(ident.toStdString());
  660 }
  661 
  662 bool LauncherController::validateMetarString(QString metar)
  663 {
  664     if (metar.isEmpty()) {
  665         return true;
  666     }
  667 
  668     try {
  669         std::string s = metar.toStdString();
  670         SGMetar theMetar(s);
  671     } catch (sg_io_exception&) {
  672         return false;
  673     }
  674 
  675     return true;
  676 }
  677 
  678 void LauncherController::requestInstallUpdate(QUrl aircraftUri)
  679 {
  680     simgear::pkg::PackageRef pref = packageForAircraftURI(aircraftUri);
  681     if (pref) {
  682         if (pref->isInstalled()) {
  683             InstallRef install = pref->existingInstall();
  684             if (install->hasUpdate()) {
  685                 globals->packageRoot()->scheduleToUpdate(install);
  686             }
  687         } else {
  688             pref->install();
  689         }
  690     }
  691 }
  692 
  693 void LauncherController::requestUninstall(QUrl aircraftUri)
  694 {
  695     simgear::pkg::PackageRef pref = packageForAircraftURI(aircraftUri);
  696     if (pref) {
  697         simgear::pkg::InstallRef i = pref->existingInstall();
  698         if (i) {
  699             i->uninstall();
  700         }
  701     }
  702 }
  703 
  704 void LauncherController::requestInstallCancel(QUrl aircraftUri)
  705 {
  706     simgear::pkg::PackageRef pref = packageForAircraftURI(aircraftUri);
  707     if (pref) {
  708         simgear::pkg::InstallRef i = pref->existingInstall();
  709         if (i) {
  710             i->cancelDownload();
  711         }
  712     }
  713 }
  714 
  715 void LauncherController::requestUpdateAllAircraft()
  716 {
  717     const auto pkgRoot = globals->packageRoot();
  718     const PackageList& toBeUpdated = pkgRoot->packagesNeedingUpdate();
  719     std::for_each(toBeUpdated.begin(), toBeUpdated.end(), [pkgRoot](PackageRef pkg) {
  720         const auto ins = pkg->install();
  721         if (!pkgRoot->isInstallQueued(ins)) {
  722             pkgRoot->scheduleToUpdate(ins);
  723         }
  724     });
  725 }
  726 
  727 void LauncherController::queryMPServers()
  728 {
  729     m_serversModel->refresh();
  730 }
  731 
  732 QString LauncherController::versionString() const
  733 {
  734     return FLIGHTGEAR_VERSION;
  735 }
  736 
  737 RecentAircraftModel *LauncherController::aircraftHistory()
  738 {
  739     return m_aircraftHistory;
  740 }
  741 
  742 RecentLocationsModel *LauncherController::locationHistory()
  743 {
  744     return m_locationHistory;
  745 }
  746 
  747 void LauncherController::launchUrl(QUrl url)
  748 {
  749     QDesktopServices::openUrl(url);
  750 }
  751 
  752 QVariantList LauncherController::defaultSplashUrls() const
  753 {
  754     QVariantList urls;
  755 
  756     for (auto path : flightgear::defaultSplashScreenPaths()) {
  757         QUrl url = QUrl::fromLocalFile(QString::fromStdString(path));
  758         urls.append(url);
  759     }
  760 
  761     return urls;
  762 }
  763 
  764 QVariant LauncherController::loadUISetting(QString name, QVariant defaultValue) const
  765 {
  766     QSettings settings;
  767     if (!settings.contains(name))
  768         return defaultValue;
  769     return settings.value(name);
  770 }
  771 
  772 void LauncherController::saveUISetting(QString name, QVariant value) const
  773 {
  774     QSettings settings;
  775     settings.setValue(name, value);
  776 }
  777 
  778 void LauncherController::onAircraftInstalledCompleted(QModelIndex index)
  779 {
  780     maybeUpdateSelectedAircraft(index);
  781 }
  782 
  783 void LauncherController::onAircraftInstallFailed(QModelIndex index, QString errorMessage)
  784 {
  785     qWarning() << Q_FUNC_INFO << index.data(AircraftURIRole) << errorMessage;
  786 
  787     QMessageBox msg;
  788     msg.setWindowTitle(tr("Aircraft installation failed"));
  789     msg.setText(tr("An error occurred installing the aircraft %1: %2").
  790                 arg(index.data(Qt::DisplayRole).toString()).arg(errorMessage));
  791     msg.addButton(QMessageBox::Ok);
  792     msg.exec();
  793 
  794     maybeUpdateSelectedAircraft(index);
  795 }
  796 
  797 
  798 QPointF LauncherController::mapToGlobal(QQuickItem *item, const QPointF &pos) const
  799 {
  800     QPointF scenePos = item->mapToScene(pos);
  801     QQuickWindow* win = item->window();
  802     return win->mapToGlobal(scenePos.toPoint());
  803 }
  804 
  805 void LauncherController::requestRestoreDefaults()
  806 {
  807     QMessageBox mbox;
  808     mbox.setText(tr("Restore all settings to defaults?"));
  809     mbox.setInformativeText(tr("Restoring settings to their defaults may affect available add-ons such as scenery or aircraft."));
  810     QPushButton* quitButton = mbox.addButton(tr("Restore and restart now"), QMessageBox::YesRole);
  811     mbox.addButton(QMessageBox::Cancel);
  812     mbox.setDefaultButton(QMessageBox::Cancel);
  813     mbox.setIconPixmap(QPixmap(":/app-icon-large"));
  814 
  815     mbox.exec();
  816     if (mbox.clickedButton() != quitButton) {
  817         return;
  818     }
  819 
  820     {
  821         QSettings settings;
  822         settings.clear();
  823         settings.setValue("restore-defaults-on-run", true);
  824     }
  825 
  826     flightgear::restartTheApp();
  827 }
  828 
  829 void LauncherController::requestChangeDataPath()
  830 {
  831     QString currentLocText;
  832     QSettings settings;
  833     QString root = settings.value(SetupRootDialog::rootPathKey()).toString();
  834     if (root.isNull()) {
  835         currentLocText = tr("Currently the built-in data files are being used");
  836     }
  837     else {
  838         currentLocText = tr("Currently using location: %1").arg(root);
  839     }
  840 
  841     QMessageBox mbox;
  842     mbox.setText(tr("Change the data files used by FlightGear?"));
  843     mbox.setInformativeText(tr("FlightGear requires additional files to operate. "
  844         "(Also called the base package, or fg-data) "
  845         "You can restart FlightGear and choose a "
  846         "different data files location, or restore the default setting. %1").arg(currentLocText));
  847     QPushButton* quitButton = mbox.addButton(tr("Restart FlightGear now"), QMessageBox::YesRole);
  848     mbox.addButton(QMessageBox::Cancel);
  849     mbox.setDefaultButton(QMessageBox::Cancel);
  850     mbox.setIconPixmap(QPixmap(":/app-icon-large"));
  851 
  852     mbox.exec();
  853     if (mbox.clickedButton() != quitButton) {
  854         return;
  855     }
  856 
  857     SetupRootDialog::askRootOnNextLaunch();
  858     flightgear::restartTheApp();
  859 }
  860 
  861 void LauncherController::openConfig()
  862 {
  863     QString file = QFileDialog::getOpenFileName(nullptr, tr("Choose a saved configuration"),
  864        {}, "*.fglaunch");
  865     if (file.isEmpty())
  866         return;
  867 
  868     m_config->loadConfigFromFile(file);
  869 }
  870 
  871 void LauncherController::saveConfigAs()
  872 {
  873     QString file = QFileDialog::getSaveFileName(nullptr, tr("Save the current configuration"),
  874        {}, "*.fglaunch");
  875     if (file.isEmpty())
  876         return;
  877     if (!file.endsWith(".fglaunch")) {
  878         file += ".fglaunch";
  879     }
  880 
  881     m_config->saveConfigToFile(file);
  882 }
  883 
  884 void LauncherController::setAircraftGridMode(bool aircraftGridMode)
  885 {
  886     if (m_aircraftGridMode == aircraftGridMode)
  887         return;
  888 
  889     QSettings settings;
  890     settings.setValue("aircraftGridMode", aircraftGridMode);
  891     m_aircraftGridMode = aircraftGridMode;
  892     emit aircraftGridModeChanged(m_aircraftGridMode);
  893 }
  894 
  895 void LauncherController::resetGettingStartedTips()
  896 {
  897     {
  898         QSettings settings;
  899         settings.beginGroup("GettingStarted-DontShow");
  900         settings.remove(""); // remove all keys in the current group
  901         settings.endGroup();
  902     } // ensure settings are written, before we emit the signal
  903 
  904     emit didResetGettingStartedTips();
  905 }
  906 
  907 void LauncherController::setMinWindowSize(QSize sz)
  908 {
  909     if (sz == m_minWindowSize)
  910         return;
  911 
  912     m_window->setMinimumSize(sz);
  913     emit minWindowSizeChanged();
  914 }
  915 
  916 QUrl LauncherController::flyIconUrl() const
  917 {
  918     if (m_aircraftType == Helicopter) {
  919         return QUrl{"qrc:///svg/toolbox-fly-heli"};
  920     } else if (m_selectedAircraftInfo) {
  921         if (m_selectedAircraftInfo->hasTag("spaceship")) {
  922             return QUrl{"qrc:///svg/toolbox-fly-alt"};
  923         }
  924     }
  925 
  926     return QUrl{"qrc:///svg/toolbox-fly"};
  927 }
  928 
  929 QString LauncherController::flyButtonLabel() const
  930 {
  931     if (m_aircraftType == Helicopter) {
  932         return tr("Fly!", "For a helicopter");
  933     } else if (m_selectedAircraftInfo) {
  934         if (m_selectedAircraftInfo->hasTag("spaceship")) {
  935             return tr("Fly!", "For a spaceship");
  936         }
  937     }
  938 
  939     return tr("Fly!");
  940 }
  941 
  942 QUrl LauncherController::urlToDataPath(QString relPath) const
  943 {
  944     QString absFilePath = QString::fromStdString(globals->get_fg_root().utf8Str());
  945     if (!relPath.startsWith("/")) {
  946         relPath.prepend("/");
  947     }
  948     return QUrl::fromLocalFile(absFilePath + relPath);
  949 }
  950 
  951 void LauncherController::checkForOldDownloadDir()
  952 {
  953 #if defined(Q_OS_WIN)
  954     auto options = flightgear::Options::sharedInstance();
  955     if (options->valueForOption("download-dir") != std::string{}) {
  956         return; // if we're using a custom value, nothing to do
  957     }
  958 
  959     if (haveOldWindowsDownloadDir()) {
  960         // the notifications logic handles 'don't show again' lgoic internally,
  961         // so we can always trigger this check
  962         auto nc = LauncherNotificationsController::instance();
  963         QJSValue args = nc->jsEngine()->newObject();
  964 
  965         const auto oldPath = SGPath::documents() / "FlightGear";
  966         const auto newPath = flightgear::defaultDownloadDir();
  967 
  968         const QUrl oldLocURI = QUrl::fromLocalFile(QString::fromStdString(oldPath.utf8Str()));
  969         const QUrl newLocURI = QUrl::fromLocalFile(QString::fromStdString(newPath.utf8Str()));
  970 
  971         args.setProperty("oldLocation", oldLocURI.toString());
  972         args.setProperty("newLocation", newLocURI.toString());
  973         args.setProperty("persistent-dismiss", true);
  974 
  975         nc->postNotification("have-old-downloads-location", QUrl{"qrc:///qml/DownloadsInDocumentsWarning.qml"}, args);
  976     }
  977 #endif
  978 }
  979 
  980 bool LauncherController::haveOldWindowsDownloadDir() const
  981 {
  982     const SGPath p = SGPath::documents() / "FlightGear";
  983     if ((p / "TerraSync").exists() || (p / "Aircraft").exists()) {
  984         return true;
  985     }
  986 
  987     // tex-cache dir is created by default, so check if it's populated
  988     simgear::Dir texCacheDir(p / "TextureCache");
  989     return (texCacheDir.exists() && !texCacheDir.isEmpty());
  990 }