// QtLauncher.cxx - GUI launcher dialog using Qt5 // // Written by James Turner, started December 2014. // // Copyright (C) 2014 James Turner // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #include "config.h" #include "QtLauncher.hxx" #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Simgear #include #include #include #include #include #include #include #include #include #include #include #include #include #include
#include
#include
#include #include #include #include
#include
#include
#include #include #include "LaunchConfig.hxx" #include "LauncherMainWindow.hxx" #include "LocalAircraftCache.hxx" #include "PathListModel.hxx" #include "UnitsModel.hxx" #include "GettingStartedTip.hxx" #include "SetupRootDialog.hxx" #if defined(SG_MAC) #include #endif using namespace flightgear; using namespace simgear::pkg; using std::string; namespace { // anonymous namespace struct ProgressLabel { NavDataCache::RebuildPhase phase; const char* label; }; static std::initializer_list progressStrings = { {NavDataCache::REBUILD_READING_APT_DAT_FILES, QT_TRANSLATE_NOOP("initNavCache","Reading airport data")}, {NavDataCache::REBUILD_LOADING_AIRPORTS, QT_TRANSLATE_NOOP("initNavCache","Loading airports")}, {NavDataCache::REBUILD_FIXES, QT_TRANSLATE_NOOP("initNavCache","Loading waypoint data")}, {NavDataCache::REBUILD_NAVAIDS, QT_TRANSLATE_NOOP("initNavCache","Loading navigation data")}, {NavDataCache::REBUILD_POIS, QT_TRANSLATE_NOOP("initNavCache","Loading point-of-interest data")} }; void initNavCache() { const char* baseLabelKey = QT_TRANSLATE_NOOP("initNavCache", "Initialising navigation data, this may take several minutes"); QString baseLabel= qApp->translate("initNavCache", baseLabelKey); const auto wflags = Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::MSWindowsFixedSizeDialogHint; if (NavDataCache::isAnotherProcessRebuilding()) { const char* waitForOtherMsg = QT_TRANSLATE_NOOP("initNavCache", "Another copy of FlightGear is creating the navigation database. Waiting for it to finish."); QString m = qApp->translate("initNavCache", waitForOtherMsg); addSentryBreadcrumb("Launcher: showing wait for other process NavCache rebuild dialog", "info"); QProgressDialog waitForRebuild(m, QString() /* cancel text */, 0, 0, Q_NULLPTR, wflags); waitForRebuild.setWindowModality(Qt::WindowModal); waitForRebuild.setMinimumWidth(600); waitForRebuild.setAutoReset(false); waitForRebuild.setAutoClose(false); waitForRebuild.show(); QTimer updateTimer; updateTimer.setInterval(500); QObject::connect(&updateTimer, &QTimer::timeout, [&waitForRebuild]() { if (!NavDataCache::isAnotherProcessRebuilding()) { waitForRebuild.done(0); return; } }); updateTimer.start(); // timer won't actually run until we process events waitForRebuild.exec(); updateTimer.stop(); addSentryBreadcrumb("Launcher: done waiting for other process NavCache rebuild dialog", "info"); } NavDataCache* cache = NavDataCache::createInstance(); if (cache->isRebuildRequired()) { QProgressDialog rebuildProgress(baseLabel, QString() /* cancel text */, 0, 100, Q_NULLPTR, wflags); rebuildProgress.setWindowModality(Qt::WindowModal); rebuildProgress.setMinimumWidth(600); rebuildProgress.setAutoReset(false); rebuildProgress.setAutoClose(false); rebuildProgress.show(); QTimer updateTimer; updateTimer.setInterval(100); QObject::connect(&updateTimer, &QTimer::timeout, [&cache, &rebuildProgress, &baseLabel]() { auto phase = cache->rebuild(); if (phase == NavDataCache::REBUILD_DONE) { rebuildProgress.done(0); return; } auto it = std::find_if(progressStrings.begin(), progressStrings.end(), [phase] (const ProgressLabel& l) { return l.phase == phase; }); if (it == progressStrings.end()) { rebuildProgress.setLabelText(baseLabel); } else { QString trans = qApp->translate("initNavCache", it->label); rebuildProgress.setLabelText(trans); } if (phase == NavDataCache::REBUILD_UNKNOWN) { rebuildProgress.setValue(0); rebuildProgress.setMaximum(0); } else { rebuildProgress.setValue(static_cast(cache->rebuildPhaseCompletionPercentage())); rebuildProgress.setMaximum(100); } }); updateTimer.start(); // timer won't actually run until we process events rebuildProgress.exec(); updateTimer.stop(); flightgear::addSentryBreadcrumb("Launcher nav-cache rebuild complete", "info"); } } class NaturalEarthDataLoaderThread : public QThread { Q_OBJECT public: NaturalEarthDataLoaderThread() : m_lineInsertCount(0) { connect(this, &QThread::finished, this, &NaturalEarthDataLoaderThread::onFinished); } void abandon() { m_abandoned = true; } protected: void run() override { loadNaturalEarthFile("ne_10m_coastline.shp", flightgear::PolyLine::COASTLINE, false); loadNaturalEarthFile("ne_10m_rivers_lake_centerlines.shp", flightgear::PolyLine::RIVER, false); loadNaturalEarthFile("ne_10m_lakes.shp", flightgear::PolyLine::LAKE, true); loadNaturalEarthFile("ne_10m_urban_areas.shp", flightgear::PolyLine::URBAN, true); } private: Q_SLOT void onFinished() { if (m_abandoned) return; flightgear::PolyLineList::const_iterator begin = m_parsedLines.begin() + m_lineInsertCount; // add lines in batches of 100; this is a tradeoff to ensure we don't lock // the database for too long. (Which triggers SQLITE_BUSY) const int lineInsertBatchSize = 100U; unsigned int numToAdd = std::min(100U, static_cast(m_parsedLines.size()) - m_lineInsertCount); flightgear::PolyLineList::const_iterator end = begin + numToAdd; flightgear::PolyLine::bulkAddToSpatialIndex(begin, end); if (end == m_parsedLines.end()) { deleteLater(); // commit suicide } else { m_lineInsertCount += lineInsertBatchSize; QTimer::singleShot(50, this, SLOT(onFinished())); } } void loadNaturalEarthFile(const std::string& aFileName, flightgear::PolyLine::Type aType, bool areClosed) { SGPath path(globals->get_fg_root()); path.append( "Geodata" ); path.append(aFileName); if (!path.exists()) return; // silently fail for now flightgear::PolyLineList lines; flightgear::SHPParser::parsePolyLines(path, aType, m_parsedLines, areClosed); } flightgear::PolyLineList m_parsedLines; unsigned int m_lineInsertCount; bool m_abandoned = false; }; enum class OpenGLStatus { OpenGL21, Unknown, GDIGeneric, Intel14 }; OpenGLStatus checkForWorkingOpenGL() { // request an OpenGL comptability profile, version 2.1 // anything lower and we'll crash QSurfaceFormat fmt; fmt.setProfile(QSurfaceFormat::CompatibilityProfile); fmt.setMajorVersion(2); fmt.setMinorVersion(1); QOpenGLContext ctx; ctx.setFormat(fmt); if (!ctx.create()) { return OpenGLStatus::Unknown; } // from here on, we need to ensure orderly cleanup or some drivers // crash. So we can't early return. OpenGLStatus result = OpenGLStatus::Unknown; QOffscreenSurface offSurface; offSurface.setFormat(ctx.format()); // ensure it's compatible offSurface.create(); if (ctx.makeCurrent(&offSurface)) { result = OpenGLStatus::OpenGL21; std::string renderer = (char*)glGetString(GL_RENDERER); if (renderer == "GDI Generic") { flightgear::addSentryBreadcrumb("Detected GDI generic renderer", "info"); result = OpenGLStatus::GDIGeneric; } else if (simgear::strutils::starts_with(renderer, "Intel")) { if (ctx.format().majorVersion() < 2) { flightgear::addSentryBreadcrumb("Detected Intel < 2.1 renderer", "info"); result = OpenGLStatus::Intel14; } } // ensure the context is no longer current on the offscreen ctx.doneCurrent(); } offSurface.destroy(); return result; } } // of anonymous namespace static void initQtResources() { Q_INIT_RESOURCE(resources); #if defined(HAVE_QRC_TRANSLATIONS) Q_INIT_RESOURCE(translations); #endif } static void simgearMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { sgDebugPriority mappedPriority = SG_WARN; switch (type) { case QtDebugMsg: mappedPriority = SG_DEBUG; break; case QtInfoMsg: mappedPriority = SG_INFO; break; case QtWarningMsg: mappedPriority = SG_WARN; break; case QtCriticalMsg: mappedPriority = SG_ALERT; break; case QtFatalMsg: mappedPriority = SG_POPUP; break; } static const char* nullFile = ""; const char* file = context.file ? context.file : nullFile; const auto s = msg.toStdString(); // important we copy the file name here, since QMessageLogContext doesn't sglog().logCopyingFilename(SG_GUI, mappedPriority, file, context.line, "" /*function*/, s); if (type == QtFatalMsg) { // if we abort() here, we get a thread crash which prevents // us actually seeing the error // we'll attempt to continue so we do the SG_POPUP dialog, maybe it works // if it crashes we're no worse off than calling abort() here } } namespace flightgear { // making this a unique ptr ensures the QApplication will be deleted // event if we forget to call shutdownQtApp. Cleanly destroying this is // important so QPA resources, in particular the XCB thread, are exited // cleanly on quit. However, at present, the official policy is that static // destruction is too late to call this, hence why we have shutdownQtApp() static std::unique_ptr static_qApp; // becuase QTranslator::load (find_translation, internally) doesn't handle the // sciprt part of a language code like: zh-Hans-CN, use this code borrowed from // Qt Creator to do the search manually. void selectUITranslation() { QStringList uiLanguages = QLocale::system().uiLanguages(); //qWarning() << "UI languages:" << uiLanguages; for (QString locale : qAsConst(uiLanguages)) { // remove script if it exists, eg zh-Hans-CN -> zh-CN locale = QLocale(locale).name(); locale.replace('-', '_'); QTranslator* translator = new QTranslator; ; if (translator->load("FlightGear_" + locale, QLatin1String(":/"))) { static_qApp->installTranslator(translator); return; } delete translator; } } // Only requires FGGlobals to be initialized if 'doInitQSettings' is true. // Safe to call several times. void initApp(int& argc, char** argv, bool doInitQSettings) { static bool qtInitDone = false; static int s_argc; if (!qtInitDone) { qtInitDone = true; // Disable Qt 5.15 warnings about obsolete Connections/onFoo: syntax // we cannot use the new syntax // as long as we have to support Qt 5.9 qputenv("QT_LOGGING_RULES", "qt.qml.connections.warning=false"); initQtResources(); // can't be called from a namespace s_argc = argc; // QApplication only stores a reference to argc, // and may crash if it is freed // http://doc.qt.io/qt-5/qguiapplication.html#QGuiApplication // log to simgear instead of the console from Qt, so we go to // whichever log locations SimGear has configured qInstallMessageHandler(simgearMessageOutput); // ensure we use desktop OpenGL, don't even fall back to ANGLE, since // this gets into a knot on Optimus setups (since we export the magic // Optimus / AMD symbols in main.cxx). QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); // becuase on Windows, Qt only supports integer scaling factors, // forceibly enabling HighDpiScaling is controversial. // leave things unset here, so users can use env var // QT_AUTO_SCREEN_SCALE_FACTOR=1 to enable it at runtime #if !defined (SG_WINDOWS) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif static_qApp.reset(new QApplication(s_argc, argv)); static_qApp->setOrganizationName("FlightGear"); static_qApp->setApplicationName("FlightGear"); static_qApp->setOrganizationDomain("flightgear.org"); static_qApp->setDesktopFileName( QStringLiteral("org.flightgear.FlightGear.desktop")); QTranslator* fallbackTranslator = new QTranslator(static_qApp.get()); if (!fallbackTranslator->load(QLatin1String(":/FlightGear_en_US.qm"))) { qWarning() << "Failed to load default (en) translations"; delete fallbackTranslator; } else { static_qApp->installTranslator(fallbackTranslator); } // check for --langauge=xx option and prefer that over QLocale // detection of the locale if it exists auto lang = simgear::strutils::replace( Options::getArgValue(argc, argv, "--language"), "-", "_"); if (!lang.empty()) { QTranslator* translator = new QTranslator(static_qApp.get()); QString localeFile = "FlightGear_" + QString::fromStdString(lang); if (translator->load(localeFile, QLatin1String(":/"))) { qInfo() << "Loaded translations based on --language from:" << localeFile; static_qApp->installTranslator(translator); } else { qInfo() << "--langauge was set, but no translations found at:" << localeFile; delete translator; } } else { selectUITranslation(); } // reset numeric / collation locales as described at: // http://doc.qt.io/qt-5/qcoreapplication.html#details ::setlocale(LC_NUMERIC, "C"); ::setlocale(LC_COLLATE, "C"); #if defined(SG_MAC) if (cocoaIsRunningTranslocated()) { addSentryBreadcrumb("did show translocation warning", "info"); const char* titleKey = QT_TRANSLATE_NOOP("macTranslationWarning", "Application running from download location"); const char* key = QT_TRANSLATE_NOOP("macTranslationWarning", "FlightGear is running from the download image. For better performance and to avoid potential problems, " "please copy FlightGear to some other location, such as your desktop or Applications folder."); QString msg = qApp->translate("macTranslationWarning", key); QString title = qApp->translate("macTranslationWarning", titleKey); QMessageBox::warning(nullptr, title, msg); } #endif } if (doInitQSettings) { initQSettings(); } } void shutdownQtApp() { // restore default message handler, otherwise Qt logging on // shutdown crashes once sglog is killed qInstallMessageHandler(nullptr); static_qApp.reset(); } // Requires FGGlobals to be initialized. Safe to call several times. void initQSettings() { static bool qSettingsInitDone = false; if (!qSettingsInitDone) { qRegisterMetaType(); qRegisterMetaTypeStreamOperators("QuantityValue"); qSettingsInitDone = true; string fgHome = globals->get_fg_home().utf8Str(); QSettings::setDefaultFormat(QSettings::IniFormat); QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QString::fromStdString(fgHome)); } } bool checkKeyboardModifiersForSettingFGRoot() { initQSettings(); #if defined(Q_OS_WIN) const auto altState = GetAsyncKeyState(VK_MENU); const auto shiftState = GetAsyncKeyState(VK_SHIFT); if ((altState < 0) || (shiftState < 0)) #else Qt::KeyboardModifiers mods = qApp->queryKeyboardModifiers(); if (mods & (Qt::AltModifier | Qt::ShiftModifier)) #endif { qWarning() << "Alt/shift pressed during launch"; return true; } return false; } void restartTheApp() { QStringList fgArgs; // Spawn a new instance of myApplication: QProcess proc; QStringList args; // ensure we release whatever mutex/lock file we have in home, // so the new instance runs in writeable mode fgShutdownHome(); #if defined(Q_OS_MAC) QDir dir(qApp->applicationDirPath()); // returns the 'MacOS' dir dir.cdUp(); // up to 'contents' dir dir.cdUp(); // up to .app dir // see 'man open' for details, but '-n' ensures we launch a new instance, // and we want to pass remaining arguments to us, not open. args << "-n" << dir.absolutePath() << "--args" << "--launcher" << fgArgs; qDebug() << "args" << args; proc.startDetached("open", args); #else args << "--launcher" << fgArgs; proc.startDetached(qApp->applicationFilePath(), args); #endif qApp->exit(-1); } void launcherSetSceneryPaths() { globals->clear_fg_scenery(); // mimic what options.cxx does, so we can find airport data for parking // positions QSettings settings; // append explicit scenery paths Q_FOREACH(QString path, PathListModel::readEnabledPaths("scenery-paths-v2")) { globals->append_fg_scenery(path.toStdString()); } // append the TerraSync path QString downloadDir = settings.value("download-dir").toString(); if (downloadDir.isEmpty()) { downloadDir = QString::fromStdString(flightgear::defaultDownloadDir().utf8Str()); } SGPath terraSyncDir(downloadDir.toStdString()); terraSyncDir.append("TerraSync"); if (terraSyncDir.exists()) { globals->append_fg_scenery(terraSyncDir); } // add the installation path since it contains default airport data, // if terrasync is disabled or on first-launch const SGPath rootScenery = globals->get_fg_root() / "Scenery"; if (rootScenery.exists()) { globals->append_fg_scenery(rootScenery); } } bool runLauncherDialog() { auto glCheckResult = checkForWorkingOpenGL(); if (glCheckResult != OpenGLStatus::OpenGL21) { QMessageBox::critical(nullptr, "Failed to find graphics drivers", "This computer is missing suitable graphics drivers (OpenGL) to run FlightGear. " "Please download and install drivers from your graphics card vendor."); return false; } // Used for NavDataCache initialization: needed to find the apt.dat files launcherSetSceneryPaths(); // startup the nav-cache now. This pre-empts normal startup of // the cache, but no harm done. (Providing scenery paths are consistent) initNavCache(); auto options = flightgear::Options::sharedInstance(); if (options->isOptionSet("download-dir")) { // user set download-dir on command line, don't mess with it in the // launcher GUI. We'll disable the UI. LaunchConfig::setEnableDownloadDirUI(false); } else { QSettings settings; QString downloadDir = settings.value("download-dir").toString(); if (!downloadDir.isEmpty()) { options->setOption("download-dir", downloadDir.toStdString()); } } fgInitPackageRoot(); // setup package language auto lang = options->valueForOption("language"); if (lang.empty()) { const auto langName = QLocale::languageToString(QLocale{}.language()); lang = langName.toStdString(); } // we will re-do this later, but we want to access translated strings // from within the launcher globals->get_locale()->selectLanguage(lang); globals->packageRoot()->setLocale(globals->get_locale()->getPreferredLanguage()); // startup the HTTP system now since packages needs it FGHTTPClient::getOrCreate(); QPointer naturalEarthLoader = new NaturalEarthDataLoaderThread; naturalEarthLoader->start(); // avoid double Apple menu and other weirdness if both Qt and OSG // try to initialise various Cocoa structures. flightgear::WindowBuilder::setPoseAsStandaloneApp(false); LauncherMainWindow dlg(false); if (options->isOptionSet("enable-fullscreen")) { dlg.showFullScreen(); } else { dlg.show(); } int appResult = qApp->exec(); if (appResult <= 0) { return false; // quit } // avoid crashes / NavCache races if the loader is still running after // the launcher exits if (naturalEarthLoader) { naturalEarthLoader->abandon(); } // avoid a race-y crash on the locale, if a scan thread is // still running: this reset will cancel any running scan LocalAircraftCache::reset(); // don't set scenery paths twice globals->clear_fg_scenery(); globals->get_locale()->clear(); return true; } bool runInAppLauncherDialog() { LauncherMainWindow dlg(true); bool accepted = dlg.execInApp(); if (!accepted) { return false; } return true; } static const char* static_lockFileDialog_Title = QT_TRANSLATE_NOOP("LockFileDialog", "Multiple copies of FlightGear running"); static const char* static_lockFileDialog_Text = QT_TRANSLATE_NOOP("LockFileDialog", "FlightGear has detected another copy is already running. " "This copy will run in read-only mode, so downloads will not be possible, " "and settings will not be saved."); static const char* static_lockFileDialog_Info = QT_TRANSLATE_NOOP("LockFileDialog", "If you are sure another copy is not running on this computer, " "you can choose to reset the lock file, and run this copy as normal. " "Alternatively, you can close this copy of the software."); LockFileDialogResult showLockFileDialog() { flightgear::addSentryBreadcrumb("showing lock-file dialog", "info"); QString title = qApp->translate("LockFileDialog", static_lockFileDialog_Title); QString text = qApp->translate("LockFileDialog", static_lockFileDialog_Text); QString infoText = qApp->translate("LockFileDialog", static_lockFileDialog_Info); QMessageBox mb; mb.setIconPixmap(QPixmap(":/app-icon-large")); mb.setWindowTitle(title); mb.setText(text); mb.setInformativeText(infoText); mb.addButton(QMessageBox::Ok); mb.setDefaultButton(QMessageBox::Ok); mb.addButton(QMessageBox::Reset); mb.addButton(QMessageBox::Close); int r = mb.exec(); if (r == QMessageBox::Reset) return LockFileReset; if (r == QMessageBox::Close) return LockFileQuit; return LockFileContinue; } bool showSetupRootDialog(bool usingDefaultRoot) { return SetupRootDialog::runDialog(usingDefaultRoot); } SetupRootResult restoreUserSelectedRoot(SGPath& path) { return SetupRootDialog::restoreUserSelectedRoot(path); } } // of namespace flightgear #include "QtLauncher.moc"