1
0
Fork 0
flightgear/src/GUI/QtLauncher.cxx
Automatic Release Builder 6b6defbead Add getting-started tips to the launcher
QML implementation of getting started tips, with a nice styled
background box. Tips are defined+positioned inline, and displayed
when their enclosing scope is active.
2021-02-04 11:49:25 +00:00

633 lines
22 KiB
C++

// QtLauncher.cxx - GUI launcher dialog using Qt5
//
// Written by James Turner, started December 2014.
//
// Copyright (C) 2014 James Turner <zakalawe@mac.com>
//
// 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 <locale.h>
// Qt
#include <QtGlobal>
#include <QString>
#include <QProgressDialog>
#include <QDir>
#include <QFileInfo>
#include <QTimer>
#include <QDebug>
#include <QSettings>
#include <QUrl>
#include <QApplication>
#include <QThread>
#include <QProcess>
#include <QTranslator>
#include <QMessageBox>
#include <QPointer>
// Simgear
#include <simgear/timing/timestamp.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/structure/subsystem_mgr.hxx>
#include <simgear/misc/sg_path.hxx>
#include <simgear/misc/strutils.hxx>
#include <simgear/package/Root.hxx>
#include <simgear/package/Catalog.hxx>
#include <simgear/package/Package.hxx>
#include <simgear/package/Install.hxx>
#include <simgear/debug/logstream.hxx>
#include <Add-ons/AddonManager.hxx>
#include <Airports/airport.hxx>
#include <Main/fg_props.hxx>
#include <Main/globals.hxx>
#include <Main/sentryIntegration.hxx>
#include <Navaids/NavDataCache.hxx>
#include <Navaids/SHPParser.hxx>
#include <Navaids/navrecord.hxx>
#include <Main/fg_init.hxx>
#include <Main/locale.hxx>
#include <Main/options.hxx>
#include <Network/HTTPClient.hxx>
#include <Viewer/WindowBuilder.hxx>
#include "LaunchConfig.hxx"
#include "LauncherMainWindow.hxx"
#include "LocalAircraftCache.hxx"
#include "PathListModel.hxx"
#include "UnitsModel.hxx"
#include "GettingStartedTip.hxx"
#if defined(SG_MAC)
#include <GUI/CocoaHelpers.h>
#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<ProgressLabel> 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<int>(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;
unsigned int numToAdd = std::min<unsigned int>(1000U, static_cast<unsigned int>(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 += 1000;
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;
};
} // 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) {
abort();
}
}
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<QApplication> static_qApp;
// 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
// QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
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);
}
qWarning() << "UI languages:" << QLocale().uiLanguages();
QTranslator* translator = new QTranslator(static_qApp.get());
// 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()) {
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 if (translator->load(QLocale(), QLatin1String("FlightGear"), QLatin1String("_"), QLatin1String(":/"))) {
// QLocale().name() looks like ' "it_IT" ' (without the outer
// quotes) when running FG on Linux with LANG=it_IT.UTF-8.
qWarning() << "Loaded translations for locale" << QLocale().name();
static_qApp->installTranslator(translator);
} else {
delete translator;
}
// 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<QuantityValue>();
qRegisterMetaTypeStreamOperators<QuantityValue>("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()
{
// 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* http = globals->add_new_subsystem<FGHTTPClient>();
// we guard against re-init in the global phase; bind and postinit
// will happen as normal
http->init();
QPointer<NaturalEarthDataLoaderThread> 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;
}
} // of namespace flightgear
#include "QtLauncher.moc"