1
0
Fork 0

QtQuick launcher settings implementation

Moves the settings and environment pages of the launcher into QQ2,
and provides more QtQuick items to use.
This commit is contained in:
James Turner 2017-12-15 15:42:36 +00:00
parent 9a044a474b
commit 37dc418ce1
41 changed files with 2333 additions and 311 deletions

View file

@ -179,6 +179,8 @@ AircraftItemModel::AircraftItemModel(QObject* pr) :
this, &AircraftItemModel::onScanStarted);
connect(cache, &LocalAircraftCache::addedItems,
this, &AircraftItemModel::onScanAddedItems);
connect(cache, &LocalAircraftCache::cleared,
this, &AircraftItemModel::onLocalCacheCleared);
}
AircraftItemModel::~AircraftItemModel()
@ -190,7 +192,7 @@ void AircraftItemModel::setPackageRoot(const simgear::pkg::RootRef& root)
{
if (m_packageRoot) {
delete m_delegate;
m_delegate = NULL;
m_delegate = nullptr;
}
m_packageRoot = root;
@ -645,6 +647,17 @@ void AircraftItemModel::onScanAddedItems(int addedCount)
endInsertRows();
}
void AircraftItemModel::onLocalCacheCleared()
{
const int firstRow = 0;
const int lastRow = m_cachedLocalAircraftCount + 1;
beginRemoveRows(QModelIndex(), firstRow, lastRow);
m_delegateStates.remove(0, m_cachedLocalAircraftCount);
m_cachedLocalAircraftCount = 0;
endRemoveRows();
}
void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
{
QString msg;

View file

@ -128,6 +128,7 @@ public slots:
private slots:
void onScanStarted();
void onScanAddedItems(int count);
void onLocalCacheCleared();
private:
friend class PackageDelegate;

View file

@ -138,6 +138,8 @@ if (HAVE_QT)
ViewCommandLinePage.hxx
MPServersModel.cpp
MPServersModel.h
PathUrlHelper.cxx
PathUrlHelper.hxx
${uic_sources}
${qrc_sources}
${qml_sources})
@ -166,6 +168,8 @@ if (HAVE_QT)
ThumbnailImageItem.hxx
FlickableExtentQuery.cxx
FlickableExtentQuery.hxx
PopupWindowTracker.cxx
PopupWindowTracker.hxx
)
set_property(TARGET fgqmlui PROPERTY AUTOMOC ON)

View file

@ -3,6 +3,9 @@
#include <Main/options.hxx>
#include <simgear/misc/sg_path.hxx>
#include <QSettings>
#include <QDebug>
static bool static_enableDownloadDirUI = true;
LaunchConfig::LaunchConfig(QObject* parent) :
@ -44,6 +47,28 @@ void LaunchConfig::setEnableDisableOption(QString name, bool value)
m_values.push_back(Arg((value ? "enable-" : "disable-") + name));
}
QVariant LaunchConfig::getValueForKey(QString group, QString key, QVariant defaultValue) const
{
QSettings settings;
settings.beginGroup(group);
auto v = settings.value(key, defaultValue);
bool convertedOk = v.convert(defaultValue.type());
if (!convertedOk) {
qWarning() << "type forcing on loaded value failed:" << key << v << v.typeName() << defaultValue;
}
// qInfo() << Q_FUNC_INFO << key << "value" << v << v.typeName() << convertedOk;
return v;
}
void LaunchConfig::setValueForKey(QString group, QString key, QVariant var)
{
QSettings settings;
settings.beginGroup(group);
// qInfo() << "saving" << key << "with value" << var << var.typeName();
settings.setValue(key, var);
settings.endGroup();
}
QString LaunchConfig::defaultDownloadDir() const
{
return QString::fromStdString(flightgear::defaultDownloadDir().utf8Str());

View file

@ -38,10 +38,15 @@ public:
Q_INVOKABLE void setEnableDisableOption(QString name, bool value);
// ensure a property is /not/ set?
// save and restore API?
Q_INVOKABLE QVariant getValueForKey(QString group, QString key, QVariant defaultValue = QVariant()) const;
Q_INVOKABLE void setValueForKey(QString group, QString key, QVariant var);
QString defaultDownloadDir() const;
bool enableDownloadDirUI() const;

View file

@ -498,38 +498,7 @@
</layout>
</widget>
<widget class="LocationWidget" name="location"/>
<widget class="QWidget" name="environmentPage">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea_2">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="environmentScrollContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>98</width>
<height>28</height>
</rect>
</property>
</widget>
</widget>
</item>
</layout>
<widget class="QQuickWidget" name="environmentPage">
</widget>
<widget class="QWidget" name="newSettingsPage">
<layout class="QVBoxLayout" name="verticalLayout">
@ -546,73 +515,7 @@
<number>4</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="settingsScrollContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>173</width>
<height>28</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_12">
<property name="font">
<font>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Settings contents</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLineEdit" name="settingsSearchEdit">
<property name="placeholderText">
<string>Search...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QQuickWidget" name="settings"/>
</item>
</layout>
</widget>

View file

@ -1,18 +1,21 @@
#include "LauncherArgumentTokenizer.hxx"
#include <QVariantMap>
#include <QJSEngine>
LauncherArgumentTokenizer::LauncherArgumentTokenizer()
{
}
QList<LauncherArgumentTokenizer::Arg> LauncherArgumentTokenizer::tokenize(QString in) const
QList<ArgumentToken> LauncherArgumentTokenizer::tokenize(QString in) const
{
int index = 0;
const int len = in.count();
QChar c, nc;
State state = Start;
QString key, value;
QList<Arg> result;
QList<ArgumentToken> result;
for (; index < len; ++index) {
c = in.at(index);
@ -28,7 +31,7 @@ QList<LauncherArgumentTokenizer::Arg> LauncherArgumentTokenizer::tokenize(QStrin
} else {
// should we pemit single hyphen arguments?
// choosing to fail for now
return QList<Arg>();
return {};
}
} else if (c == QChar('#')) {
state = Comment;
@ -44,7 +47,7 @@ QList<LauncherArgumentTokenizer::Arg> LauncherArgumentTokenizer::tokenize(QStrin
value.clear();
} else if (c.isSpace()) {
state = Start;
result.append(Arg(key));
result.append(ArgumentToken{key});
} else {
// could check for illegal charatcers here
key.append(c);
@ -56,7 +59,7 @@ QList<LauncherArgumentTokenizer::Arg> LauncherArgumentTokenizer::tokenize(QStrin
state = Quoted;
} else if (c.isSpace()) {
state = Start;
result.append(Arg(key, value));
result.append(ArgumentToken{key, value});
} else {
value.append(c);
}
@ -90,10 +93,31 @@ QList<LauncherArgumentTokenizer::Arg> LauncherArgumentTokenizer::tokenize(QStrin
// ensure last argument isn't lost
if (state == Key) {
result.append(Arg(key));
result.append(ArgumentToken{key});
} else if (state == Value) {
result.append(Arg(key, value));
result.append(ArgumentToken{key, value});
}
return result;
}
QVariantList LauncherArgumentTokenizer::tokens() const
{
QVariantList result;
Q_FOREACH(auto tk, tokenize(m_argString)) {
QVariantMap m;
m["arg"] = tk.arg;
m["value"] = tk.value;
result.append(m);
}
return result;
}
void LauncherArgumentTokenizer::setArgString(QString argString)
{
if (m_argString == argString)
return;
m_argString = argString;
emit argStringChanged(m_argString);
}

View file

@ -3,22 +3,42 @@
#include <QString>
#include <QList>
#include <QObject>
#include <QJSValue>
class LauncherArgumentTokenizer
class ArgumentToken
{
public:
explicit ArgumentToken(QString k, QString v = QString()) : arg(k), value(v) {}
QString arg;
QString value;
};
class LauncherArgumentTokenizer : public QObject
{
Q_OBJECT
Q_PROPERTY(QString argString READ argString WRITE setArgString NOTIFY argStringChanged)
Q_PROPERTY(QVariantList tokens READ tokens NOTIFY argStringChanged)
public:
LauncherArgumentTokenizer();
class Arg
Q_INVOKABLE QList<ArgumentToken> tokenize(QString in) const;
QString argString() const
{
public:
explicit Arg(QString k, QString v = QString()) : arg(k), value(v) {}
return m_argString;
}
QString arg;
QString value;
};
QVariantList tokens() const;
QList<Arg> tokenize(QString in) const;
public slots:
void setArgString(QString argString);
signals:
void argStringChanged(QString argString);
private:
enum State {
@ -28,7 +48,8 @@ private:
Quoted,
Comment
};
QString m_argString;
};
#endif // LAUNCHERARGUMENTTOKENIZER_HXX

View file

@ -46,6 +46,8 @@
#include "FlickableExtentQuery.hxx"
#include "LocalAircraftCache.hxx"
#include "QmlAircraftInfo.hxx"
#include "LauncherArgumentTokenizer.hxx"
#include "PathUrlHelper.hxx"
#include "ui_Launcher.h"
@ -166,6 +168,26 @@ LauncherMainWindow::LauncherMainWindow() :
m_ui->aircraftList->setSource(QUrl("qrc:///qml/AircraftList.qml"));
m_ui->settings->engine()->addImportPath("qrc:///");
m_ui->settings->engine()->rootContext()->setContextProperty("_launcher", this);
m_ui->settings->engine()->rootContext()->setContextProperty("_mpServers", m_serversModel);
m_ui->settings->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership);
m_ui->settings->setResizeMode(QQuickWidget::SizeRootObjectToView);
m_ui->settings->setSource(QUrl("qrc:///qml/Settings.qml"));
m_ui->environmentPage->engine()->addImportPath("qrc:///");
m_ui->environmentPage->engine()->rootContext()->setContextProperty("_launcher", this);
auto weatherScenariosModel = new flightgear::WeatherScenariosModel(this);
m_ui->environmentPage->engine()->rootContext()->setContextProperty("_weatherScenarios", weatherScenariosModel);
m_ui->environmentPage->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership);
m_ui->environmentPage->engine()->rootContext()->setContextProperty("_config", m_config);
m_ui->environmentPage->setResizeMode(QQuickWidget::SizeRootObjectToView);
m_ui->environmentPage->setSource(QUrl("qrc:///qml/Environment.qml"));
connect(m_aircraftModel, &AircraftItemModel::aircraftInstallCompleted,
this, &LauncherMainWindow::onAircraftInstalledCompleted);
connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed,
@ -191,9 +213,6 @@ LauncherMainWindow::LauncherMainWindow() :
LocalAircraftCache::instance()->scanDirs();
m_aircraftModel->setPackageRoot(globals->packageRoot());
buildSettingsSections();
buildEnvironmentSections();
m_viewCommandLinePage = new ViewCommandLinePage;
m_viewCommandLinePage->setLaunchConfig(m_config);
m_ui->stack->addWidget(m_viewCommandLinePage);
@ -208,6 +227,8 @@ void LauncherMainWindow::initQML()
QQmlPrivate::RegisterAutoParent autoparent = { 0, &launcher_autoParent };
QQmlPrivate::qmlregister(QQmlPrivate::AutoParentRegistration, &autoparent);
qmlRegisterType<LauncherArgumentTokenizer>("FlightGear.Launcher", 1, 0, "ArgumentTokenizer");
qmlRegisterType<SettingsSectionQML>("FlightGear.Launcher", 1, 0, "Section");
qmlRegisterType<SettingsCheckbox>("FlightGear.Launcher", 1, 0, "Checkbox");
qmlRegisterType<SettingsComboBox>("FlightGear.Launcher", 1, 0, "Combo");
@ -220,20 +241,15 @@ void LauncherMainWindow::initQML()
qmlRegisterUncreatableType<SettingsControl>("FlightGear.Launcher", 1, 0, "Control", "Base class");
qmlRegisterUncreatableType<LaunchConfig>("FlightGear.Launcher", 1, 0, "LaunchConfig", "Singleton API");
qmlRegisterType<PathUrlHelper>("FlightGear.Launcher", 1, 0, "PathUrlHelper");
qmlRegisterType<FlickableExtentQuery>("FlightGear.Launcher", 1, 0, "FlickableExtentQuery");
qmlRegisterType<QmlAircraftInfo>("FlightGear.Launcher", 1, 0, "AircraftInfo");
m_config = new LaunchConfig(this);
connect(m_config, &LaunchConfig::collect, this, &LauncherMainWindow::collectAircraftArgs);
m_ui->location->setLaunchConfig(m_config);
m_qmlEngine = new QQmlEngine(this);
m_qmlEngine->rootContext()->setContextProperty("_config", m_config);
m_qmlEngine->rootContext()->setContextProperty("_launcher", this);
m_qmlEngine->rootContext()->setContextProperty("_mpServers", m_serversModel);
#if defined(Q_OS_WIN)
const QString osName("win");
#elif defined(Q_OS_MAC)
@ -241,10 +257,11 @@ void LauncherMainWindow::initQML()
#else
const QString osName("unix");
#endif
m_qmlEngine->rootContext()->setContextProperty("_osName", osName);
flightgear::WeatherScenariosModel* weatherScenariosModel = new flightgear::WeatherScenariosModel(this);
m_qmlEngine->rootContext()->setContextProperty("_weatherScenarios", weatherScenariosModel);
QQmlContext* settingsContext = m_ui->settings->engine()->rootContext();
settingsContext->setContextProperty("_mpServers", m_serversModel);
settingsContext->setContextProperty("_config", m_config);
settingsContext->setContextProperty("_osName", osName);
qmlRegisterUncreatableType<LocalAircraftCache>("FlightGear.Launcher", 1, 0, "LocalAircraftCache", "Aircraft cache");
qmlRegisterUncreatableType<AircraftItemModel>("FlightGear.Launcher", 1, 0, "AircraftModel", "Built-in model");
@ -261,75 +278,11 @@ void LauncherMainWindow::initQML()
}
void LauncherMainWindow::buildSettingsSections()
{
QVBoxLayout* settingsVBox = static_cast<QVBoxLayout*>(m_ui->settingsScrollContents->layout());
QStringList sections = QStringList() << "general" << "mp" << "downloads" << "view" << "render";
Q_FOREACH (QString section, sections) {
QQmlComponent* comp = new QQmlComponent(m_qmlEngine, QUrl("qrc:///settings/" + section), this);
if (comp->isError()) {
qWarning() << "Errors parsing settings section:" << section << "\n" << comp->errorString();
} else {
SettingsSection* ss = qobject_cast<SettingsSection*>(comp->create());
if (!ss) {
qWarning() << "failed to create settings section from" << section;
} else {
ss->insertSettingsHeader();
ss->setLaunchConfig(m_config);
ss->setParent(m_ui->settingsScrollContents);
settingsVBox->addWidget(ss);
connect(ss, &SettingsSection::summaryChanged,
this, &LauncherMainWindow::updateSettingsSummary);
}
}
}
m_extraSettings = new ExtraSettingsSection(m_ui->settingsScrollContents);
m_extraSettings->setLaunchConfig(m_config);
settingsVBox->addWidget(m_extraSettings);
settingsVBox->addStretch(1);
connect(m_ui->settingsSearchEdit, &QLineEdit::textChanged,
this, &LauncherMainWindow::onSettingsSearchChanged);
}
void LauncherMainWindow::buildEnvironmentSections()
{
QVBoxLayout* settingsVBox = new QVBoxLayout;
m_ui->environmentScrollContents->setLayout(settingsVBox);
QStringList sections = QStringList() << "time" << "weather";
Q_FOREACH (QString section, sections) {
QQmlComponent* comp = new QQmlComponent(m_qmlEngine, QUrl("qrc:///environment/" + section), this);
if (comp->isError()) {
qWarning() << "Errors parsing environment section:" << section << "\n" << comp->errorString();
} else {
SettingsSection* ss = qobject_cast<SettingsSection*>(comp->create());
if (!ss) {
qWarning() << "failed to create environment section from" << section;
} else {
ss->insertSettingsHeader();
ss->setLaunchConfig(m_config);
ss->setParent(m_ui->environmentScrollContents);
settingsVBox->addWidget(ss);
connect(ss, &SettingsSection::summaryChanged,
this, &LauncherMainWindow::updateSettingsSummary);
}
}
}
settingsVBox->addStretch(1);
}
LauncherMainWindow::~LauncherMainWindow()
{
// avoid a double-free when the QQuickWidget's engine seems to try
// and delete us.
delete m_ui->aircraftList;
m_qmlEngine->collectGarbage();
delete m_qmlEngine;
// delete m_ui->aircraftList;
}
bool LauncherMainWindow::execInApp()
@ -406,6 +359,8 @@ void LauncherMainWindow::restoreSettings()
void LauncherMainWindow::saveSettings()
{
emit requestSaveState();
QSettings settings;
settings.setValue("recent-aircraft", QUrl::toStringList(m_recentAircraft));
settings.setValue("recent-location-sets", m_recentLocations);
@ -779,16 +734,7 @@ void LauncherMainWindow::onPopupLocationHistory()
void LauncherMainWindow::updateSettingsSummary()
{
QStringList summary;
Q_FOREACH(SettingsSection* ss, findChildren<SettingsSection*>()) {
QString s = ss->summary();
if (!s.isEmpty()) {
QStringList pieces = s.split(';', QString::SkipEmptyParts);
summary.append(pieces);
}
}
const QStringList summary = m_settingsSummary + m_environmentSummary;
QString s = summary.join(", ");
s[0] = s[0].toUpper();
m_ui->settingsDescription->setText(s);
@ -807,6 +753,11 @@ void LauncherMainWindow::downloadDirChanged(QString path)
return;
}
// if the default dir is passed in, map that back to the emptru string
if (path == m_config->defaultDownloadDir()) {
path.clear();;
}
auto options = flightgear::Options::sharedInstance();
if (options->valueForOption("download-dir") == path.toStdString()) {
// this works because we propogate the value from QSettings to
@ -816,7 +767,11 @@ void LauncherMainWindow::downloadDirChanged(QString path)
return;
}
options->setOption("download-dir", path.toStdString());
if (!path.isEmpty()) {
options->setOption("download-dir", path.toStdString());
} else {
options->clearOption("download-dir");
}
// replace existing package root
globals->get_subsystem<FGHTTPClient>()->shutdown();
@ -879,6 +834,32 @@ QmlAircraftInfo *LauncherMainWindow::selectedAircraftInfo() const
return m_selectedAircraftInfo;
}
bool LauncherMainWindow::matchesSearch(QString term, QStringList keywords) const
{
Q_FOREACH(QString s, keywords) {
if (s.contains(term, Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
bool LauncherMainWindow::isSearchActive() const
{
return !m_settingsSearchTerm.isEmpty();
}
QStringList LauncherMainWindow::settingsSummary() const
{
return m_settingsSummary;
}
QStringList LauncherMainWindow::environmentSummary() const
{
return m_environmentSummary;
}
void LauncherMainWindow::setSelectedAircraft(QUrl selectedAircraft)
{
if (m_selectedAircraft == selectedAircraft)
@ -889,6 +870,35 @@ void LauncherMainWindow::setSelectedAircraft(QUrl selectedAircraft)
emit selectedAircraftChanged(m_selectedAircraft);
}
void LauncherMainWindow::setSettingsSearchTerm(QString settingsSearchTerm)
{
if (m_settingsSearchTerm == settingsSearchTerm)
return;
m_settingsSearchTerm = settingsSearchTerm;
emit searchChanged();
}
void LauncherMainWindow::setSettingsSummary(QStringList settingsSummary)
{
if (m_settingsSummary == settingsSummary)
return;
m_settingsSummary = settingsSummary;
emit summaryChanged();
updateSettingsSummary();
}
void LauncherMainWindow::setEnvironmentSummary(QStringList environmentSummary)
{
if (m_environmentSummary == environmentSummary)
return;
m_environmentSummary = environmentSummary;
emit summaryChanged();
updateSettingsSummary();
}
simgear::pkg::PackageRef LauncherMainWindow::packageForAircraftURI(QUrl uri) const
{
if (uri.scheme() != "package") {
@ -946,9 +956,11 @@ void LauncherMainWindow::onChangeDataDir()
void LauncherMainWindow::onSettingsSearchChanged()
{
#if 0
Q_FOREACH(SettingsSectionQML* ss, findChildren<SettingsSectionQML*>()) {
ss->setSearchTerm(m_ui->settingsSearchEdit->text());
}
#endif
}
bool LauncherMainWindow::validateMetarString(QString metar)

View file

@ -63,7 +63,15 @@ class LauncherMainWindow : public QMainWindow
Q_PROPERTY(QUrl selectedAircraft READ selectedAircraft WRITE setSelectedAircraft NOTIFY selectedAircraftChanged)
Q_PROPERTY(QmlAircraftInfo* selectedAircraftInfo READ selectedAircraftInfo NOTIFY selectedAircraftChanged)
Q_PROPERTY(bool isSearchActive READ isSearchActive NOTIFY searchChanged)
Q_PROPERTY(QString settingsSearchTerm READ settingsSearchTerm WRITE setSettingsSearchTerm NOTIFY searchChanged)
Q_PROPERTY(QStringList settingsSummary READ settingsSummary WRITE setSettingsSummary NOTIFY summaryChanged)
Q_PROPERTY(QStringList environmentSummary READ environmentSummary WRITE setEnvironmentSummary NOTIFY summaryChanged)
public:
LauncherMainWindow();
virtual ~LauncherMainWindow();
@ -96,14 +104,43 @@ public:
Q_INVOKABLE QPointF mapToGlobal(QQuickItem* item, const QPointF& pos) const;
QmlAircraftInfo* selectedAircraftInfo() const;
Q_INVOKABLE bool matchesSearch(QString term, QStringList keywords) const;
bool isSearchActive() const;
QString settingsSearchTerm() const
{
return m_settingsSearchTerm;
}
QStringList settingsSummary() const;
QStringList environmentSummary() const;
public slots:
void setSelectedAircraft(QUrl selectedAircraft);
void setSettingsSearchTerm(QString settingsSearchTerm);
void setSettingsSummary(QStringList settingsSummary);
void setEnvironmentSummary(QStringList environmentSummary);
signals:
void showNoOfficialHangarChanged();
void selectedAircraftChanged(QUrl selectedAircraft);
void searchChanged();
void summaryChanged();
/**
* @brief requestSaveState - signal to request QML settings to save their
* state to persistent storage
*/
void requestSaveState();
protected:
virtual void closeEvent(QCloseEvent *event) override;
@ -158,8 +195,6 @@ private:
void updateLocationHistory();
bool shouldShowOfficialCatalogMessage() const;
void buildSettingsSections();
void buildEnvironmentSections();
void collectAircraftArgs();
void initQML();
@ -185,6 +220,9 @@ private:
ExtraSettingsSection* m_extraSettings = nullptr;
ViewCommandLinePage* m_viewCommandLinePage = nullptr;
QmlAircraftInfo* m_selectedAircraftInfo = nullptr;
QString m_settingsSearchTerm;
QStringList m_settingsSummary,
m_environmentSummary;
};
#endif // of LAUNCHER_MAIN_WINDOW_HXX

View file

@ -407,12 +407,19 @@ LocalAircraftCache::~LocalAircraftCache()
void LocalAircraftCache::setPaths(QStringList paths)
{
if (paths == m_paths) {
return;
}
m_items.clear();
emit cleared();
m_paths = paths;
}
void LocalAircraftCache::scanDirs()
{
abandonCurrentScan();
m_items.clear();
QStringList dirs = m_paths;
@ -504,12 +511,17 @@ void LocalAircraftCache::abandonCurrentScan()
m_scanThread->setDone();
m_scanThread->wait(1000);
m_scanThread.reset();
qWarning() << Q_FUNC_INFO << "current scan abandonded";
}
}
void LocalAircraftCache::onScanResults()
{
if (!m_scanThread) {
return;
}
QVector<AircraftItemPtr> newItems = m_scanThread->items();
if (newItems.isEmpty())
return;

View file

@ -132,6 +132,7 @@ signals:
void scanStarted();
void scanCompleted();
void cleared();
void addedItems(int count);
public slots:

View file

@ -12,6 +12,7 @@
#include "LaunchConfig.hxx"
const int IsCustomIndexRole = Qt::UserRole + 1;
const int ServerNameRole = Qt::UserRole + 2;
MPServersModel::MPServersModel(QObject* parent) :
QAbstractListModel(parent)
@ -52,6 +53,8 @@ QVariant MPServersModel::data(const QModelIndex &index, int role) const
const ServerInfo& sv(m_servers.at(row));
if (role == Qt::DisplayRole) {
return tr("%1 - %2").arg(sv.name).arg(sv.location);
} else if (role == ServerNameRole) {
return sv.name;
} else if (role == IsCustomIndexRole) {
return false;
}
@ -61,7 +64,7 @@ QVariant MPServersModel::data(const QModelIndex &index, int role) const
QHash<int, QByteArray> MPServersModel::roleNames() const
{
QHash<int, QByteArray> result;
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result[IsCustomIndexRole] = "isCustomIndex";
return result;
}

16
src/GUI/PathUrlHelper.cxx Normal file
View file

@ -0,0 +1,16 @@
#include "PathUrlHelper.hxx"
PathUrlHelper::PathUrlHelper(QObject *parent) : QObject(parent)
{
}
QString PathUrlHelper::urlToLocalFilePath(QUrl url) const
{
return url.toLocalFile();
}
QUrl PathUrlHelper::urlFromLocalFilePath(QString path) const
{
return QUrl::fromLocalFile(path);
}

21
src/GUI/PathUrlHelper.hxx Normal file
View file

@ -0,0 +1,21 @@
#ifndef PATHURLHELPER_H
#define PATHURLHELPER_H
#include <QObject>
#include <QUrl>
class PathUrlHelper : public QObject
{
Q_OBJECT
public:
explicit PathUrlHelper(QObject *parent = nullptr);
Q_INVOKABLE QString urlToLocalFilePath(QUrl url) const;
Q_INVOKABLE QUrl urlFromLocalFilePath(QString path) const;
signals:
public slots:
};
#endif // PATHURLHELPER_H

View file

@ -0,0 +1,51 @@
#include "PopupWindowTracker.hxx"
#include <QGuiApplication>
#include <QMouseEvent>
#include <QWindow>
PopupWindowTracker::PopupWindowTracker(QObject *parent) : QObject(parent)
{
}
PopupWindowTracker::~PopupWindowTracker()
{
if (m_window) {
qApp->removeEventFilter(this);
}
}
void PopupWindowTracker::setWindow(QWindow *window)
{
if (m_window == window)
return;
if (m_window) {
qApp->removeEventFilter(this);
}
m_window = window;
if (m_window) {
qApp->installEventFilter(this);
}
emit windowChanged(m_window);
}
bool PopupWindowTracker::eventFilter(QObject *watched, QEvent *event)
{
if (!m_window)
return false;
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent* me = static_cast<QMouseEvent*>(event);
QPoint windowPos = m_window->mapFromGlobal(me->globalPos());
}
// also check for app loosing focus
return false;
}

View file

@ -0,0 +1,36 @@
#ifndef POPUPWINDOWTRACKER_HXX
#define POPUPWINDOWTRACKER_HXX
#include <QObject>
class QWindow;
class PopupWindowTracker : public QObject
{
Q_OBJECT
Q_PROPERTY(QWindow* window READ window WRITE setWindow NOTIFY windowChanged)
QWindow* m_window = nullptr;
public:
explicit PopupWindowTracker(QObject *parent = nullptr);
~PopupWindowTracker();
QWindow* window() const
{
return m_window;
}
signals:
void windowChanged(QWindow* window);
public slots:
void setWindow(QWindow* window);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
};
#endif // POPUPWINDOWTRACKER_HXX

View file

@ -393,7 +393,7 @@ bool runLauncherDialog()
LaunchConfig::setEnableDownloadDirUI(false);
} else {
QSettings settings;
QString downloadDir = settings.value("downloadSettings/downloadDir").toString();
QString downloadDir = settings.value("download-dir").toString();
if (!downloadDir.isEmpty()) {
options->setOption("download-dir", downloadDir.toStdString());
}

View file

@ -0,0 +1,5 @@
import QtQuick 2.0
Item {
}

View file

@ -0,0 +1,58 @@
import QtQuick 2.0
import "."
Item {
id: root
property bool open: false
implicitWidth: label.width + gearIcon.width + Style.margin
implicitHeight: label.height
state: "closed"
states: [
State {
name: "closed"
when: !root.open
PropertyChanges { target: label; text: qsTr("Show more") }
},
State {
name: "open"
when: root.open
PropertyChanges { target: label; text: qsTr("Show less") }
}
]
Text {
id: label
anchors.right: gearIcon.left
anchors.rightMargin: Style.margin
anchors.verticalCenter: parent.verticalCenter
color: "white"
font.underline: mouse.containsMouse
}
Image {
id: gearIcon
source: "qrc:///settings-gear-white"
height: root.height - 2
fillMode: Image.PreserveAspectFit
anchors.right: parent.right
anchors.rightMargin: Style.inset
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
onClicked: open = !open
cursorShape: Qt.PointingHandCursor
}
}

View file

@ -93,7 +93,7 @@ Rectangle {
Rectangle {
border.width: 1
border.color: "#afafaf"
border.color: Style.minorFrameColor
anchors.fill: parent
}

View file

@ -6,6 +6,7 @@ Rectangle {
property string text
property string hoverText: ""
property bool enabled: true
signal clicked
@ -13,7 +14,7 @@ Rectangle {
height: buttonText.implicitHeight + (radius * 2)
radius: Style.roundRadius
color: mouse.containsMouse ? Style.activeColor : Style.themeColor
color: enabled ? (mouse.containsMouse ? Style.activeColor : Style.themeColor) : Style.disabledThemeColor
Text {
id: buttonText
@ -26,6 +27,8 @@ Rectangle {
MouseArea {
id: mouse
anchors.fill: parent
enabled: root.enabled
hoverEnabled: true
onClicked: {

View file

@ -0,0 +1,151 @@
import QtQuick 2.0
import "."
FocusScope {
id: root
property alias label: label.text
property bool enabled: true
property var value: new Date()
implicitHeight: label.implicitHeight
function daysInMonth()
{
var tempDate = new Date(year.value, month.value, 0 /* last day of preceeding month */);
return tempDate.getDate();
}
function updateCurrentDate()
{
root.value = new Date(year.value, month.value - 1, dayOfMonth.value,
hours.value, minutes.value);
}
function setDate(date)
{
year.value = date.getFullYear();
month.value = date.getMonth() + 1;
dayOfMonth.value = date.getDate();
hours.value = date.getHours();
minutes.value = date.getMinutes();
updateCurrentDate();
}
Text {
id: label
anchors.left: root.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
// color: mouseArea.containsMouse ? Style.themeColor :
// (root.enabled ? "black" : Style.inactiveThemeColor)
}
Rectangle {
id: editFrame
radius: Style.roundRadius
border.color: root.focus ? Style.themeColor : Style.minorFrameColor
border.width: 1
height: 30
// height: currentChoiceText.implicitHeight + Style.margin
anchors.left: label.right
anchors.leftMargin: Style.margin
width: editRow.childrenRect.width + Style.roundRadius * 2 + 64
// width of current item, or available space after the label
// width: Math.min(root.width - (label.width + Style.margin),
// currentChoiceText.implicitWidth + (Style.margin * 2) + upDownIcon.width);
anchors.verticalCenter: parent.verticalCenter
Row {
id: editRow
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
right: parent.right
margins: Style.margin
}
DateTimeValueEdit {
id: year
maxValue: 9999
widthString: "0000"
nextToFocus: month
fieldWidth: 4
anchors.verticalCenter: parent.verticalCenter
onCommit: updateCurrentDate();
}
Text {
text: " / "
anchors.verticalCenter: parent.verticalCenter
}
DateTimeValueEdit {
id: month
minValue: 1
maxValue: 12
widthString: "00"
nextToFocus: dayOfMonth
previousToFocus: year
anchors.verticalCenter: parent.verticalCenter
onCommit: updateCurrentDate();
}
Text {
text: " / "
anchors.verticalCenter: parent.verticalCenter
}
DateTimeValueEdit {
id: dayOfMonth
minValue: 1
maxValue: root.daysInMonth()
widthString: "00"
nextToFocus: hours
previousToFocus: month
anchors.verticalCenter: parent.verticalCenter
onCommit: updateCurrentDate();
}
// spacer here
Text {
text: " "
anchors.verticalCenter: parent.verticalCenter
}
DateTimeValueEdit {
id: hours
minValue: 0
maxValue: 23
widthString: "00"
nextToFocus: minutes
previousToFocus: dayOfMonth
anchors.verticalCenter: parent.verticalCenter
onCommit: updateCurrentDate();
}
Text {
text: " : "
anchors.verticalCenter: parent.verticalCenter
}
DateTimeValueEdit {
id: minutes
minValue: 0
maxValue: 59
widthString: "00"
anchors.verticalCenter: parent.verticalCenter
previousToFocus: hours
onCommit: updateCurrentDate();
}
} // of date elements row
}
}

View file

@ -0,0 +1,184 @@
import QtQuick 2.4
import "."
FocusScope {
id: root
property Item nextToFocus
property Item previousToFocus
property int value: 0
property int minValue: 0 // might be 1 for day-of-month
property int maxValue: 99 // max numerical value
property int fieldWidth: 2
property bool valueWraps: false
property string widthString: "00"
width: metrics.width + (input.activeFocus ? upDownArea.width : 0)
height: metrics.height
// promote Z value when focused so our icon decoration
// is on top of our sibling edits.
z: input.activeFocus ? 10 : 0
signal commit(var newValue);
onMaxValueChanged: {
// cap current value if the max is now lower
value = Math.min(value, maxValue);
}
function zeroPaddedNumber(v)
{
var s = v.toString();
while (s.length < fieldWidth) {
s = "0" + s;
}
return s;
}
readonly property string __valueString: zeroPaddedNumber(value)
function incrementValue()
{
if (input.activeFocus) {
value = Math.min(parseInt(input.text) + 1, maxValue)
input.text = __valueString
} else {
commit(Math.min(value + 1, maxValue));
}
}
function decrementValue()
{
if (input.activeFocus) {
value = Math.max(parseInt(input.text) - 1, minValue)
input.text = __valueString
} else {
commit(Math.max(value - 1, minValue));
}
}
function focusNext()
{
if (nextToFocus != undefined) {
nextToFocus.forceActiveFocus();
}
}
TextInput {
id: input
focus: true
// validate on integers
height: parent.height
width: metrics.width
maximumLength: fieldWidth
Keys.onUpPressed: {
incrementValue();
}
Keys.onDownPressed: {
decrementValue();
}
Keys.onTabPressed: { root.focusNext(); }
Keys.onBacktabPressed: {
if (previousToFocus != undefined) {
previousToFocus.focus = true;
}
}
Keys.onPressed: {
if ((event.key == Qt.Key_Colon) || (event.key == Qt.Key_Slash)) {
nextToFocus.focus = true;
event.accepted = true;
}
}
// onTextChanged: {
// if (activeFocus && (text.length == root.widthString.length)) {
// root.focusNext();
// }
// }
onActiveFocusChanged: {
if (activeFocus) {
selectAll();
} else {
commit(parseInt(text))
}
}
validator: IntValidator {
id: validator
top: root.maxValue
bottom: root.minValue
}
}
Binding {
when: !input.activeFocus
target: input
property: "text"
value: root.__valueString
}
MouseArea {
height: root.height
width: root.width
// use wheel events to adjust up/dowm
onClicked: {
input.forceActiveFocus();
}
onWheel: {
if (wheel.angleDelta > 0) {
root.incrementValue()
} else if (wheel.angleDelta < 0) {
root.decrementValue()
}
}
}
Rectangle {
id: upDownArea
color: "white"
anchors.left: input.right
anchors.verticalCenter: input.verticalCenter
height: upDownIcon.implicitHeight
visible: input.activeFocus
width: upDownIcon.implicitWidth
Image {
id: upDownIcon
// show up/down arrows
source: "qrc:///up-down-arrow"
}
MouseArea {
width: parent.width
height: parent.height / 2
onPressed: {
root.incrementValue();
}
}
MouseArea {
width: parent.width
height: parent.height / 2
anchors.bottom: parent.bottom
onPressed: {
root.decrementValue();
}
}
}
TextMetrics {
id: metrics
text: root.widthString
}
}

220
src/GUI/qml/Environment.qml Normal file
View file

@ -0,0 +1,220 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
import "."
Item {
Binding {
target: _launcher
property: "environmentSummary"
value: timeOfDay.summary().concat(weatherSettings.summary());
}
Connections {
target: _launcher
onRequestSaveState: {
timeOfDaySettings.saveState();
weatherSettings.saveState();
}
}
Flickable {
contentHeight: sectionColumn.childrenRect.height
flickableDirection: Flickable.VerticalFlick
anchors.fill: parent
Column
{
id: sectionColumn
width: parent.width
Item {
// below header margin
width: parent.width
height: Style.margin
}
Section {
id: timeOfDaySettings
title: qsTr("Time & Date")
settingGroup: "timeDate"
contents: [
SettingsComboBox {
id: timeOfDay
label: qsTr("Time of day")
description: qsTr("Select the time of day used when the simulator starts, or enter a "
+ "custom date and time.")
choices: [qsTr("Current time"), qsTr("Dawn"), qsTr("Morning"), qsTr("Noon"),
qsTr("Afternoon"), qsTr("Dusk"), qsTr("Evening"),
qsTr("Midnight"), qsTr("Custom time & date")]
defaultIndex: 0
setting: "time-of-day"
readonly property var args: ["", "dawn", "morning", "noon", "afternoon",
"dusk", "evening", "midnight"]
readonly property bool isCustom: (selectedIndex == 8)
readonly property bool isDefault: (selectedIndex == 0)
function summary()
{
if (!timeOfDay.isCustom && !timeOfDay.isDefault) {
return [choices[selectedIndex].toLowerCase()];
}
return [];
}
},
SettingsDateTimePicker
{
id: customTime
label: qsTr("Custom time & date")
hidden: !timeOfDay.isCustom
description: qsTr("Enter a date and time to begin the flight at. By default this is "
+ "in local time for the chosen starting location - use the option "
+ "below to request a time in GMT / UTC.")
setting: "custom-time"
},
SettingCheckbox {
id: customTimeIsGMT
label: qsTr("Custom time is GMT / UTC")
visible: timeOfDay.isCustom
setting: "custom-time-is-gmt"
},
SettingsComboBox {
id: season
label: qsTr("Season")
description: qsTr("Select if normal (summer) or winter textures are used for the scenery. "
+ "This does not affect other aspects of the simulation at present, "
+ "such as icing or weather simulation");
keywords: ["season", "scenery", "texture", "winter"]
choices: [qsTr("Summer (default)"), qsTr("Winter")]
defaultIndex: 0
setting: "winter-textures"
readonly property var args: ["summer", "winter"]
}
]
onApply: {
if (timeOfDay.isCustom) {
var timeString = Qt.formatDateTime(customTime.value, "yyyy:MM:dd:hh:mm:ss");
if (customTimeIsGMT.checked) {
_config.setArg("start-date-gmt", timeString)
} else {
_config.setArg("start-date-sys", timeString)
}
} else if (timeOfDay.selectedIndex > 0) {
_config.setArg("timeofday", timeOfDay.args[timeOfDay.selectedIndex])
}
if (season.selectedIndex > 0) {
_config.setArg("season", season.args[season.selectedIndex])
}
}
}
Section {
id: weatherSettings
title: qsTr("Weather")
settingGroup: "weather"
contents: [
SettingCheckbox {
id: advancedWeather
label: qsTr("Advanced weather modelling")
description: "Detailed weather simulation based on local terrain and "
+ "atmospheric simulation. Note that using advanced weather with "
+ "real-world weather data (METAR) information may not show exactly "
+ "the conditions recorded, and is not recommended for multi-player "
+ "flight since the weather simulation is not shared over the network."
setting: "aws-enables"
},
SettingCheckbox {
id: fetchMetar
label: qsTr("Real-world weather")
description: "Download real-world weather from the NOAA servers based on location."
option: "real-weather-fetch"
setting: "fetch-metar"
},
SettingsComboBox {
id: weatherScenario
enabled: !fetchMetar.checked
label: qsTr("Weather scenario")
displayRole: "name"
choices: _weatherScenarios
readonly property bool isCustomMETAR: (selectedIndex == 0);
description: _weatherScenarios.descriptionForItem(selectedIndex)
defaultIndex: 1
setting: "weather-scenario"
},
SettingLineEdit {
id: customMETAR
property bool __cachedValid: true
function revalidate() {
__cachedValid = _launcher.validateMetarString(value);
}
hidden: !weatherScenario.isCustomMETAR
enabled: !fetchMetar.checked
label: "METAR"
placeholder: "XXXX 012345Z 28035G50KT 250V300 9999 TSRA SCT022CB BKN030 13/09 Q1005"
useFullWidth: true
setting: "custom-metar"
description: __cachedValid ? qsTr("Enter a custom METAR string, e.g: '%1'").arg(placeholder)
: qsTr("The entered METAR string doesn't seem to be valid.")
onValueChanged: {
validateTimeout.restart()
}
}
]
Timer {
id: validateTimeout
interval: 200
onTriggered: customMETAR.revalidate();
}
onApply: {
if (advancedWeather.checked) {
// set description from the weather scenarios, so Local-weather
// can run the appropriate simulation
_config.setProperty("/nasal/local_weather/enabled", 1);
}
var index = weatherScenario.selectedIndex;
if (!fetchMetar.checked) {
if (weatherScenario.isCustomMETAR) {
_config.setArg("metar", customMETAR.value)
} else {
_config.setArg("metar", _weatherScenarios.metarForItem(index))
}
// either way, set the scenario name since Local-Weather keys off
// this to know what to do with the scenario + metar data
_config.setProperty("/environment/weather-scenario",
_weatherScenarios.nameForItem(index))
}
}
function summary()
{
var result = [];
if (advancedWeather.checked) result.push("advanced weather");
if (fetchMetar.checked) result.push("real-world weather");
return result;
}
}
} // of Column
} // of Flickable
}

View file

@ -0,0 +1,9 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
RenderSettings
{
}

View file

@ -1,47 +1,96 @@
import QtQuick 2.0
import QtQuick.Window 2.0
import FlightGear.Launcher 1.0
import "."
Item {
id: root
property alias label: label.text
property var choices: []
property string displayRole: ""
property var model: undefined
property string displayRole: "display"
property bool enabled: true
property int currentIndex: 0
property bool __dummy: false
implicitHeight: label.implicitHeight
Item {
Repeater {
id: internalModel
model: root.model
Item {
id: internalModelItem
// Taken from TableViewItemDelegateLoader.qml to follow QML role conventions
readonly property var text: model && model.hasOwnProperty(displayRole) ? model[displayRole] // Qml ListModel and QAbstractItemModel
: modelData && modelData.hasOwnProperty(displayRole) ? modelData[displayRole] // QObjectList / QObject
: modelData != undefined ? modelData : "" // Models without role
readonly property bool selected: root.currentIndex === model.index
readonly property QtObject modelObj: model
}
}
}
Component.onCompleted: {
// hack to force updating of currentText after internalModel
// has been populated
__dummy = !__dummy
}
function currentText()
{
var foo = __dummy; // fake propery dependency to update this
var item = internalModel.itemAt(currentIndex);
if (!item) return "";
return item.text
}
Text {
id: label
anchors.left: root.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
color: mouseArea.containsMouse ? "#68A6E1" : "black"
horizontalAlignment: Text.AlignRight
color: mouseArea.containsMouse ? Style.themeColor :
(root.enabled ? "black" : Style.inactiveThemeColor)
}
Rectangle {
id: currentChoiceFrame
radius: 4
border.color: mouseArea.containsMouse ? "#68A6E1" : "#9f9f9f"
radius: Style.roundRadius
border.color: mouseArea.containsMouse ? Style.themeColor : Style.minorFrameColor
border.width: 1
height: root.height
width: parent.width / 2
anchors.right: parent.right
anchors.rightMargin: 8
height: currentChoiceText.implicitHeight + Style.margin
anchors.left: label.right
anchors.leftMargin: Style.margin
// width of current item, or available space after the label
width: Math.min(root.width - (label.width + Style.margin),
currentChoiceText.implicitWidth + (Style.margin * 2) + upDownIcon.width);
anchors.verticalCenter: parent.verticalCenter
Text {
id: currentChoiceText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
text: choices[currentIndex][displayRole]
color: mouseArea.containsMouse ? "#68A6E1" : "#7F7F7F"
anchors.right: parent.right
anchors.margins: Style.margin
text: currentText()
color: mouseArea.containsMouse ? Style.themeColor : Style.baseTextColor
elide: Text.ElideRight
maximumLineCount: 1
}
Image {
id: upDownIcon
source: "qrc:///up-down-arrow"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.rightMargin: Style.margin
anchors.verticalCenter: parent.verticalCenter
}
}
@ -50,46 +99,68 @@ Item {
anchors.fill: parent
id: mouseArea
hoverEnabled: true
enabled: root.enabled
onClicked: {
var screenPos = _launcher.mapToGlobal(currentChoiceText, Qt.point(0, -currentChoiceText.height * currentIndex))
if (screenPos.y < 0) {
// if the popup would appear off the screen, use the first item
// position instead
screenPos = _launcher.mapToGlobal(currentChoiceText, Qt.point(0, 0))
}
popupFrame.x = screenPos.x;
popupFrame.y = screenPos.y;
popupFrame.visible = true
}
}
Rectangle {
Window {
id: popupFrame
width: currentChoiceFrame.width
anchors.left: currentChoiceFrame.left
// todo - position so current item lies on top
anchors.top: currentChoiceFrame.bottom
height: choicesColumn.childrenRect.height
modality: Qt.WindowModal
flags: Qt.Popup
height: choicesColumn.childrenRect.height + Style.margin * 2
width: choicesColumn.childrenRect.width + Style.margin * 2
visible: false
color: "white"
border.color: "#9f9f9f"
border.width: 1
Rectangle {
border.width: 1
border.color: Style.minorFrameColor
anchors.fill: parent
}
// text repeater
Column {
id: choicesColumn
spacing: Style.margin
x: Style.margin
y: Style.margin
Repeater {
model: choices
delegate: Text {
text: choices[model.index][root.displayRole]
width: popupFrame.width
height: 40
MouseArea {
anchors.fill: parent
onClicked: {
root.currentIndex = model.index
popupFrame.visible = false
}
}
id: choicesRepeater
model: root.model
delegate:
Text {
id: choiceText
readonly property bool selected: root.currentIndex === model.index
}
}
}
}
// Taken from TableViewItemDelegateLoader.qml to follow QML role conventions
text: model && model.hasOwnProperty(displayRole) ? model[displayRole] // Qml ListModel and QAbstractItemModel
: modelData && modelData.hasOwnProperty(displayRole) ? modelData[displayRole] // QObjectList / QObject
: modelData != undefined ? modelData : "" // Models without role
height: implicitHeight + Style.margin
MouseArea {
width: popupFrame.width // full width of the popup
height: parent.height
onClicked: {
root.currentIndex = model.index
popupFrame.visible = false
}
}
} // of Text delegate
} // text repeater
} // text column
} // of popup Window
}

View file

@ -1,77 +1,115 @@
import QtQuick 2.2
import "."
Rectangle {
FocusScope
{
id: root
width:frame.width
height: frame.height
// property string text
property bool active: false
signal search(string term)
radius: Style.roundRadius
width: Style.strutSize * 3
border.width: 1
border.color: (mouse.containsMouse | active) ? Style.themeColor: Style.minorFrameColor
clip: true
TextInput {
id: buttonText
anchors.left: parent.left
anchors.right: searchIcon.left
anchors.margins: Style.margin
anchors.verticalCenter: parent.verticalCenter
color: Style.baseTextColor
onTextChanged: {
searchTimer.restart();
}
onEditingFinished: {
root.search(text);
focus = false;
}
text: "Search"
}
Image {
id: searchIcon
source: "qrc:///search-icon-small"
anchors.right: parent.right
anchors.rightMargin: Style.margin
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
buttonText.forceActiveFocus();
buttonText.text = "";
}
}
property alias autoSubmitTimeout: searchTimer.interval
onActiveChanged: {
if (!active) {
// rest search text when we deactive
buttonText.text = "Search"
searchTimer.stop();
clear();
}
}
Timer {
id: searchTimer
interval: 800
onTriggered: {
if (buttonText.text.length > 2) {
root.search(buttonText.text)
function clear()
{
buttonText.text = "Search"
root.focus = false
searchTimer.stop();
root.search("");
}
Rectangle
{
id: frame
radius: Style.roundRadius
width: Style.strutSize * 3
height: Math.max(searchIcon.height, buttonText.height) + (Style.roundRadius)
border.width: 1
border.color: (mouse.containsMouse | active) ? Style.themeColor: Style.minorFrameColor
clip: true
TextInput {
id: buttonText
anchors.left: parent.left
anchors.right: searchIcon.left
anchors.margins: Style.margin
anchors.verticalCenter: parent.verticalCenter
color: Style.baseTextColor
focus: true
onTextChanged: {
searchTimer.restart();
}
onEditingFinished: {
if (text == "") {
clear();
} else {
root.search(text);
root.focus = false;
}
}
text: "Search"
}
Image {
id: searchIcon
source: clearButtonMouse.containsMouse ? "qrc:///up-down-arrow" :"qrc:///search-icon-small"
anchors.right: parent.right
anchors.rightMargin: Style.margin
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
id: mouse
anchors.left: parent.left
height: parent.height
anchors.right: clearButtonMouse.left
hoverEnabled: true
onClicked: {
root.forceActiveFocus();
buttonText.text = "";
}
}
MouseArea {
id: clearButtonMouse
anchors.right: parent.right
height: parent.height
width: searchIcon.width
hoverEnabled: true
onClicked: {
clear();
}
}
Timer {
id: searchTimer
interval: 800
onTriggered: {
if (buttonText.text.length > 2) {
root.search(buttonText.text)
} else if (buttonText.text.length == 0) {
root.search(""); // ensure we update with no search
}
}
}
}
}
} // of FocusScope

121
src/GUI/qml/Section.qml Normal file
View file

@ -0,0 +1,121 @@
import QtQuick 2.0
import "."
import FlightGear.Launcher 1.0
Item {
id: root
property alias title: headerTitle.text
property alias contents: contentBox.children
property alias showAdvanced: advancedToggle.open
property string settingGroup: ""
property string summary: ""
readonly property bool haveAdvancedSettings: anyAdvancedSettings(contents)
implicitWidth: parent.width
implicitHeight: headerRect.height + contentBox.height + (Style.margin * 2)
signal apply();
function saveState()
{
for (var i = 0; i < contents.length; i++) {
contents[i].saveState();
}
}
function anyAdvancedSettings(items)
{
for (var i = 0; i < items.length; i++) {
if (items[i].advanced == true) return true;
}
return false;
}
// we determine the initial open/close state of the advanced section
// based on whether any of the advanced settings are non-default
function anyNonDefaultAdvancedSettings(items)
{
for (var i = 0; i < items.length; i++) {
var control = items[i];
if (control.advanced == true) {
if (!control.__isDefault && !control.hidden) {
//console.info("Non-default advanced setting:" + control.label + ","
// + control.defaultValue + " != " + control.value) ;
return true;
}
}
}
return false;
}
Connections {
target: _config
onCollect: root.apply();
}
Component.onCompleted: {
// use this as a trigger to decide the initial open/close state
if (anyNonDefaultAdvancedSettings(contents)) {
showAdvanced = true;
}
}
Rectangle {
id: headerRect
width: parent.width
height: headerTitle.height + (Style.margin * 2)
color: Style.themeColor
border.width: 1
border.color: Style.frameColor
Text {
id: headerTitle
color: "white"
anchors.verticalCenter: parent.verticalCenter
font.bold: true
anchors.left: parent.left
anchors.leftMargin: Style.inset
}
AdvancedSettingsToggle
{
id: advancedToggle
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: parent.height
visible: root.haveAdvancedSettings
}
}
MouseArea {
anchors.fill: contentBox
onClicked: {
// take focus back from any active control
root.focus = true
}
}
Column {
id: contentBox
anchors.top: headerRect.bottom
anchors.topMargin: Style.margin
width: parent.width
spacing: Style.margin * 2
// this is here so SettingControl 's parent (which is us)
// can be used to find the advanced toggle state
property alias showAdvanced: advancedToggle.open
}
// bottom spacing item
Item {
height: Style.margin
width: parent.width
anchors.top: contentBox.bottom
}
}

View file

@ -0,0 +1,40 @@
import QtQuick 2.0
import "."
SettingControl {
id: root
property alias checked: toggle.checked
property alias value: toggle.checked
property bool defaultValue: false
// should we set the option, if the current value matches the default?
// this is used to suppress setting needless options to their default
// value
property bool setIfDefault: false
ToggleSwitch {
id: toggle
label: root.label
enabled: root.enabled
}
SettingDescription {
id: description
enabled: root.enabled
text: root.description
anchors.top: toggle.bottom
anchors.topMargin: Style.margin
width: root.width
}
function apply()
{
if (option == "")
return;
if (setIfDefault || (value != defaultValue)) {
_config.setEnableDisableOption(option, checked)
}
}
}

View file

@ -0,0 +1,89 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
import "."
Item {
id: root
property bool advanced: false
property string description: ""
property string label: ""
property var keywords: []
property bool enabled: true
property string option: ""
property string setting: ""
// define a new visiblity property so we can control built-in 'visible' ourselves
property bool hidden: false
implicitHeight: childrenRect.height
implicitWidth: parent.width // which is assumed to be the section
visible: {
// override so advanced items show up in searches
if (_launcher.isSearchActive && _launcher.matchesSearch(_launcher.settingsSearchTerm, keywords)) {
return true;
}
return !hidden && (!advanced || parent.showAdvanced)
}
readonly property bool __isDefault: (this.value == this.defaultValue);
Component.onCompleted: {
restoreState();
}
Connections {
target: _config
// only invoke apply if 'option' is set, otherwise we assume
// there is specialised apply code
enabled: root.option != ""
onCollect: apply();
}
Rectangle {
// this is the 'search hit highlight effect'
anchors.fill: parent
radius: Style.margin
border.color: "yellow"
border.width: 1
color: "#ffff7f"
z: -1
visible: _launcher.isSearchActive && _launcher.matchesSearch(_launcher.settingsSearchTerm, keywords)
}
function apply()
{
console.warn("Implement apply() on " + root.setting)
}
function saveState()
{
if (this.hasOwnProperty("value")) {
_config.setValueForKey("", root.setting, this.value)
} else {
console.warn("No value property on " + this);
}
}
function restoreState()
{
if (root.setting == "") {
console.warn("Missing setting key on " + label)
return;
}
if (!"value" in root) {
console.warn("No value property on " + root);
return;
}
var defaultValue = ("defaultValue" in root) ? root.defaultValue : undefined;
var rawValue = _config.getValueForKey("", root.setting, defaultValue);
// console.warn("restoring state for " + root.setting + ", got raw value " + rawValue + " with type " + typeof(rawValue))
if (rawValue != undefined) {
// root["value"] = rawValue
this.value = rawValue
}
}
}

View file

@ -0,0 +1,12 @@
import QtQuick 2.0
import "."
Text {
property bool enabled: true
color: enabled ? Style.baseTextColor : Style.disabledTextColor
// make the text slightly smaller?
wrapMode: Text.WordWrap
}

View file

@ -0,0 +1,77 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
import "."
SettingControl {
id: root
implicitHeight: childrenRect.height
readonly property string placeholder: "--option=value --prop:/sim/name=value"
property alias value: edit.text
option: "xxx" // non-empty value so apply() is called
property string defaultValue: "" // needed to type save/restore logic to string
SettingDescription {
id: description
enabled: root.enabled
anchors.top: parent.top
anchors.topMargin: Style.margin
width: parent.width
text: qsTr("Enter additional command-line arguments if any are required. " +
"See <a href=\"http://flightgear.sourceforge.net/getstart-en/getstart-enpa2.html#x5-450004.5\">here</a> " +
"for documentation on possible arguments.");
}
Rectangle {
id: editFrame
anchors.left: parent.left
anchors.margins: Style.margin
anchors.right: parent.right
anchors.top: description.bottom
height: edit.height + Style.margin
border.color: edit.activeFocus ? Style.frameColor : Style.minorFrameColor
border.width: 1
TextEdit {
id: edit
enabled: root.enabled
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.margin
height: Math.max(Style.strutSize * 4, implicitHeight)
textFormat: TextEdit.PlainText
font.family: "Courier"
selectByMouse: true
wrapMode: TextEdit.WordWrap
Text {
id: placeholder
visible: (edit.text.length == 0) && !edit.activeFocus
text: root.placeholder
color: Style.baseTextColor
}
}
}
function apply()
{
var tokens = tokenizer.tokens;
for (var i = 0; i < tokens.length; i++) {
var tk = tokens[i];
if (tk.arg.substring(0, 5) == "prop:") {
_config.setProperty(tk.arg.substring(5), tk.value);
} else {
_config.setArg(tk.arg, tk.value);
}
}
}
ArgumentTokenizer {
id: tokenizer
argString: edit.text
}
}

View file

@ -0,0 +1,82 @@
import QtQuick 2.7
import "."
SettingControl {
id: root
implicitHeight: childrenRect.height
property string placeholder: ""
property alias validation: edit.validator
property alias value: edit.text
property alias suggestedWidthString: metrics.text
readonly property int suggestedWidth: useFullWidth ? root.width
: ((metrics.width == 0) ? Style.strutSize * 4
: metrics.width)
property bool useFullWidth: false
property string defaultValue: ""
function apply()
{
if (option != "") {
_config.setArg(option, value)
}
}
TextMetrics {
id: metrics
}
Text {
id: label
text: root.label
anchors.verticalCenter: editFrame.verticalCenter
color: editFrame.activeFocus ? Style.themeColor :
(root.enabled ? "black" : Style.inactiveThemeColor)
}
Rectangle {
id: editFrame
anchors.left: label.right
anchors.margins: Style.margin
height: edit.implicitHeight + Style.margin
width: Math.min(root.width - (label.width + Style.margin * 2), Math.max(suggestedWidth, edit.implicitWidth));
radius: Style.roundRadius
border.color: edit.activeFocus ? Style.frameColor : Style.minorFrameColor
border.width: 1
clip: true
TextInput {
id: edit
enabled: root.enabled
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.margin
selectByMouse: true
Text {
id: placeholder
visible: (edit.text.length == 0) && !edit.activeFocus
text: root.placeholder
color: Style.baseTextColor
}
}
}
SettingDescription {
id: description
enabled: root.enabled
text: root.description
anchors.top: editFrame.bottom
anchors.topMargin: Style.margin
width: parent.width
}
}

View file

@ -0,0 +1,109 @@
import QtQuick 2.0
import QtQuick.Dialogs 1.0 as Dialogs
import FlightGear.Launcher 1.0 as FG
import "."
SettingControl {
id: root
implicitHeight: childrenRect.height
property alias label: label.text
property string path
property string defaultPath
property alias chooseDirectory: picker.selectFolder
property alias dialogPrompt: picker.title
property alias value: root.path // needed for save + restore
property string defaultValue: "" // needed for correct save/restore typing
readonly property bool isDefault: (path.length == 0) || (path == defaultPath)
readonly property url effectiveUrl: pathHelper.urlFromLocalFilePath(effectivePath());
function effectivePath()
{
return root.isDefault ? defaultPath : path;
}
function apply()
{
if (option != "") {
_config.setArg(option, effectivePath())
}
}
Text {
id: label
text: root.label
anchors.verticalCenter: chooseButton.verticalCenter
color: (root.enabled ? "black" : Style.inactiveThemeColor)
}
Text {
id: currentPath
text: root.isDefault ? qsTr("%1 (default)").arg(root.defaultPath) : root.path
anchors.verticalCenter: chooseButton.verticalCenter
color: (root.enabled ? "black" : Style.inactiveThemeColor)
font.italic: true
anchors.left: label.right
anchors.leftMargin: Style.margin
anchors.right: chooseButton.left
anchors.rightMargin: Style.margin
elide: Text.ElideRight
}
Button {
id: chooseButton
text: qsTr("Change")
anchors.right: defaultButton.left
anchors.rightMargin: Style.margin
enabled: root.enabled
onClicked: {
// set current value as a URL
picker.folder = effectiveUrl
picker.open();
}
}
Button {
id: defaultButton
text: qsTr("Use default")
anchors.right: parent.right
anchors.rightMargin: Style.margin
enabled: root.enabled && !root.isDefault
onClicked: {
path = defaultPath
}
}
SettingDescription {
id: description
enabled: root.enabled
text: root.description
anchors.top: chooseButton.bottom
anchors.topMargin: Style.margin
width: parent.width
}
Dialogs.FileDialog {
id: picker
onAccepted: {
path = pathHelper.urlToLocalFilePath(picker.fileUrl);
}
onRejected: {
}
}
FG.PathUrlHelper {
id: pathHelper
}
}

475
src/GUI/qml/Settings.qml Normal file
View file

@ -0,0 +1,475 @@
import QtQuick 2.0
import FlightGear.Launcher 1.0
import "."
Item {
Rectangle {
// search 'dimming' rectangle
visible: _launcher.isSearchActive
anchors.fill: parent
color: "#7f7f7f"
}
Binding {
target: _launcher
property: "settingsSummary"
value: generalSettings.summary().concat(mpSettings.summary(),
downloadSettings.summary(),
windowSettings.summary(),
renderSection.summary());
}
Connections {
target: _launcher
onRequestSaveState: {
mpSettings.saveState();
downloadSettings.saveState();
generalSettings.saveState();
renderSection.saveState();
extraArgsSection.saveState();
windowSettings.saveState();
}
}
Flickable {
contentHeight: sectionColumn.childrenRect.height
flickableDirection: Flickable.VerticalFlick
anchors.fill: parent
Column
{
id: sectionColumn
width: parent.width
Item {
// top margin
width: parent.width
height: Style.margin
}
Item {
id: header
width: parent.width
height: headerText.height
Text {
id: headerText
text: qsTr("Settings")
font.pixelSize: Style.strutSize / 2
anchors.left: parent.left
anchors.leftMargin: Style.inset
}
SearchButton {
id: search
width: Style.strutSize * 4
anchors.right: parent.right
anchors.rightMargin: Style.margin
anchors.verticalCenter: parent.verticalCenter
autoSubmitTimeout: 250
onSearch: {
_launcher.settingsSearchTerm = term
}
}
}
Item {
// below header margin
width: parent.width
height: Style.margin
}
Section {
id: generalSettings
title: qsTr("General")
settingGroup: "general"
function summary()
{
var result = [];
if (startPaused.checked) result.push("paused");
if (!showConsoleWin.hidden && showConsoleWin.checked) result.push("console");
return result;
}
contents: [
SettingCheckbox {
id: startPaused
label: qsTr("Start paused")
description: qsTr("Automatically pause the simulator when launching. This is useful "
+ "when starting in the air.")
keywords: ["pause", "freeze"]
option: "freeze"
setting: "start-paused"
},
SettingCheckbox {
id: autoCoordination
label: qsTr("Enable auto-coordination")
description: qsTr("When flying with the mouse, or a joystick lacking a rudder axis, "
+ "it's difficult to manually coordinate aileron and rudder movements during "
+ "turn. This option automatically commands the rudder to maintain zero "
+ "slip angle when banking");
advanced: true
keywords: ["input", "mouse", "control", "rudder"]
option: "auto-coordination"
setting: "auto-coordination"
},
SettingCheckbox {
id: showConsoleWin
label: qsTr("Show debugging console")
description: qsTr("Open a console window showing debug output from the application.")
advanced: true
hidden: _osName != "win"
keywords: ["console", "terminal", "log", "debug"]
setting: "console"
}
]
onApply: {
if (!showConsoleWin.hidden && showConsoleWin.checked) _config.setArg("console")
}
}
Section {
id: mpSettings
title: qsTr("Multi-player")
settingGroup: "mp"
readonly property int defaultMPPort: 5000
function summary()
{
var result = [];
if (enableMP.checked) result.push("multi-player");
return result;
}
contents: [
SettingCheckbox {
id: enableMP
label: qsTr("Connect to the multi-player network")
description: qsTr("FlightGear supporters maintain a network of servers to enable global multi-user "
+ "flight. This requires a moderately fast Internet connection to be usable. Your aircraft "
+ "will be visible to other users online, and you will see their aircraft.")
keywords: ["network", "mp", "multiplay","online"]
setting: "enabled"
onValueChanged: {
if (value) {
console.info("MP enabled, doing refresh")
_launcher.queryMPServers();
}
}
},
SettingLineEdit {
id: callSign
enabled: enableMP.checked
label: qsTr("Callsign")
description: qsTr("Enter a callsign you will use online. This is visible to all users and is " +
"how ATC services and other pilots will refer to you. " +
"(Maximum of seven characters permitted)")
placeholder: "D-FGFS"
suggestedWidthString: "MMMMMMMMMMM"
keywords: ["callsign", "handle", "name"]
// between one and seven alphanumerics, underscores and/or hypens
// spaces not permitted
validation: RegExpValidator { regExp: /[\w-]{1,7}/ }
setting: "callsign"
},
SettingsComboBox {
id: mpServer
label: qsTr("Server")
enabled: enableMP.checked
description: qsTr("Select a server close to you for better responsiveness and reduced lag when flying online.")
choices: _mpServers
readonly property bool currentIsCustom: (_mpServers.serverForIndex(selectedIndex) == "__custom__")
property string __savedServer;
keywords: ["server", "hostname"]
setting: "server"
// following three elements are to deal with the irregular way we save the
// MP server state, because the index is unstable, and the time to query online servers
// is determined by the MPServersModel. So we let that code do the state
// restoration, and emit a signal we handler here, when it wants to update the UI.
function saveState()
{
// these values match the code in MPServersModel.cpp, sorry for that
// nastyness
_config.setValueForKey("mpSettings", "mp-server", _mpServers.serverForIndex(selectedIndex) );
}
function restoreState()
{
// no-op, this is triggered by MPServersModel::restoreMPServerSelection
}
Connections
{
target: _mpServers
onRestoreIndex: { mpServer.selectedIndex = index }
}
},
SettingLineEdit {
id: mpCustomServer
enabled: enableMP.checked
label: qsTr("Custom server")
hidden: !mpServer.currentIsCustom
description: qsTr("Enter a server hostname or IP address, and optionally a port number. (Default port is 5000) For example 'localhost:5001'")
placeholder: "localhost:5001"
suggestedWidthString: "MMMMMMMMMMMMMMMMMMMMMMMMMMMMM"
setting: "custom-server"
}
]
onApply: {
if (enableMP.checked) {
if (mpServer.currentIsCustom) {
var pieces = mpCustomServer.value.split(':')
var port = defaultMPPort;
if (pieces.length > 1) {
port = pieces[1];
}
_config.setProperty("/sim/multiplay/txhost", pieces[0]);
_config.setProperty("/sim/multiplay/txport", port);
} else {
var sel = mpServer.selectedIndex
var host = _mpServers.serverForIndex(sel);
console.log("MP host is " + host)
if (host.length > 0) {
_config.setProperty("/sim/multiplay/txhost", host);
}
var port = _mpServers.portForIndex(sel);
if (port == 0) {
port = defaultMPPort;
}
_config.setProperty("/sim/multiplay/txport", port);
}
if (callSign.value.length > 0) {
_config.setArg("callsign", callSign.value)
}
}
}
}
Section {
id: downloadSettings
title: qsTr("Downloads")
width: parent.width
settingGroup: "downloads"
function summary()
{
var result = [];
if (terrasync.checked) result.push("scenery downloads");
return result;
}
contents: [
SettingCheckbox {
id: terrasync
label: "Download scenery automatically"
description: "FlightGear can automatically download scenery as needed, and check for updates to "
+ "the scenery. If you disable this option, you will need to download & install scenery "
+ "using an alternative method."
keywords: ["terrasync", "download", "scenery"]
option: "terrasync"
setting: "terrasync"
},
SettingPathChooser {
id: downloadDir
label: qsTr("Download location")
description: qsTr("FlightGear stores downloaded files (scenery and aircraft) in this location. "
+ "Depending on your settings, it may grow to a considerable size (many gigabytes). "
+ "If you change the download location, files will need to be downloaded again.")
advanced: true
chooseDirectory: true
defaultPath: _config.defaultDownloadDir
dialogPrompt: qsTr("Choose a location to store download files.")
setting: "download-dir"
// we disable this UI if the user passes --download-dir when starting the
// launcher, otherwise we coudld end up in a complete mess
enabled: _config.enableDownloadDirUI
keywords: ["download", "storage", "disk"]
onPathChanged: {
// lots of special work needs to be done immediately that this
// value is changed.
_launcher.downloadDirChanged(path);
}
}
]
onApply: {
// note we do /not/ apply the downloadDir setting here, since it's
// only permitted to occurr once, and we set it via other means;
// on startup via runLauncherDialog, and if it changes, via
// LauncherMainWindow::downloadDirChanged
}
summary: terrasync.checked ? "scenery downloads;" : ""
}
Section {
id: windowSettings
title: qsTr("View & Window")
width: parent.width
settingGroup: "window"
function summary()
{
var result = [];
if (fullscreen.checked) result.push("full-screen");
return result;
}
contents: [
SettingCheckbox {
id: fullscreen
label: qsTr("Start full-screen")
description: qsTr("Start the simulator in full-screen mode.");
setting: "fullscreen"
option: "fullscreen"
},
SettingsComboBox {
id: windowSize
enabled: !fullscreen.checked
label: qsTr("Window size")
description: qsTr("Select the initial size of the window (this has no effect in full-screen mode).")
advanced: true
choices: ["640x480", "800x600", "1024x768", "1920x1080", "2560x1600", qsTr("Custom Size") ]
defaultIndex: 2
readonly property bool isDefault: selectedIndex == defaultIndex
readonly property bool isCustom: selectedIndex == 5
keywords: ["window", "geometry", "size", "resolution"]
setting: "window-size"
},
SettingLineEdit {
id: customWindowSize
hidden: !windowSize.isCustom
label: qsTr("Custom size")
advanced: true
description: qsTr("Enter a custom window size in the form 'WWWWW x HHHHH', for example '1280 x 900'")
validation: RegExpValidator { regExp: /[\d]{1,7}[\s]*x[\s]*[\d]{1,7}$/ }
setting: "custom-size"
suggestedWidthString: "0000000 x 0000000"
}
]
onApply: {
if (windowSize.isCustom) {
_config.setArg("geometry", customWindowSize.value);
} else if (!windowSize.isDefault) {
_config.setArg("geometry", windowSize.choices[windowSize.selectedIndex]);
}
}
}
Section {
id: renderSection
title: qsTr("Rendering")
width: parent.width
settingGroup: "render"
readonly property bool rembrandt: (renderer.selectedIndex == 2)
readonly property bool alsEnabled: (renderer.selectedIndex == 1)
readonly property bool msaaEnabled: !rembrandt && (msaa.selectedIndex > 0)
function summary()
{
var result = [];
if (rembrandt) result.push("Rembrandt");
else if (alsEnabled) result.push("ALS");
if (msaaEnabled) result.push("anti-aliasing");
return result;
}
contents: [
SettingsComboBox {
id: renderer
label: qsTr("Renderer")
choices: [qsTr("Default"),
qsTr("Atmospheric Light Scattering"),
qsTr("Rembrandt")]
description: descriptions[selectedIndex]
defaultIndex: 0
setting: "renderer"
readonly property var descriptions: [
qsTr("The default renderer provides standard visuals with maximum compatibility."),
qsTr("The ALS renderer uses a sophisticated physical atmospheric model and several " +
"other effects to give realistic rendering of large distances."),
qsTr("Rembrandt is a configurable multi-pass renderer which supports shadow-maps, cinematic " +
"effects and more. However, not all aircraft appear correctly and performance will " +
"depend greatly on your system hardware.")
]
keywords: ["als", "rembrandt", "render", "shadow"]
},
SettingsComboBox {
id: msaa
label: qsTr("Anti-aliasing")
setting: "aa"
description: renderSection.rembrandt? qsTr("Anti-aliasing is disabled when Rembrandt is enabled.")
: qsTr("Anti-aliasing improves the appearance of high-contrast edges and lines. " +
"This is especially noticeable on sloping or diagonal edges. " +
"Higher settings can reduce performance.")
keywords: ["msaa", "anti", "aliasing", "multi", "sample"]
choices: [qsTr("Off"), "2x", "4x"]
enabled: !renderSection.rembrandt
property var data: [0, 2, 4];
defaultIndex: 0
}
]
onApply: {
if (msaaEnabled) {
_config.setProperty("/sim/rendering/multi-sample-buffers", 1)
_config.setProperty("/sim/rendering/multi-samples", msaa.data[msaa.selectedIndex])
}
_config.setEnableDisableOption("rembrandt", rembrandt);
if (alsEnabled) {
_config.setProperty("/sim/rendering/shaders/skydome", true);
}
}
}
Section {
id: extraArgsSection
title: qsTr("Additional Settings")
settingGroup: "extraArgs"
width: parent.width
contents: [
SettingExtraArguments {
id: extraArgs
keywords: ["property", "extra", "command", "argument"]
setting: "extra-args"
}
]
}
} // of Column
} // of Flickable
}

View file

@ -0,0 +1,33 @@
import QtQuick 2.0
import "."
SettingControl {
id: root
property alias choices: popup.model
property alias displayRole: popup.displayRole
property alias selectedIndex: popup.currentIndex
property int defaultIndex: 0
// alias for save+restore
property alias value: popup.currentIndex
property alias defaultValue: root.defaultIndex
implicitHeight: childrenRect.height
PopupChoice {
id: popup
label: root.label
enabled: root.enabled
width: root.width
}
SettingDescription {
id: description
enabled: root.enabled
text: root.description
anchors.top: popup.bottom
anchors.topMargin: Style.margin
width: root.width
}
}

View file

@ -0,0 +1,39 @@
import QtQuick 2.0
import "."
SettingControl {
id: root
// alias for save+restore
property alias value: dateTimeEdit.value
property alias defaultValue: root.now
readonly property date now: new Date()
implicitHeight: childrenRect.height
DateTimeEdit {
id: dateTimeEdit
label: root.label
enabled: root.enabled
width: root.width
}
SettingDescription {
id: description
enabled: root.enabled
text: root.description
anchors.top: dateTimeEdit.bottom
anchors.topMargin: Style.margin
width: root.width
}
function restoreState()
{
console.info("Custom restore state on SettingsDateTimePicker");
var rawValue = _config.getValueForKey("", root.setting, defaultValue);
dateTimeEdit.setDate(rawValue);
}
}

View file

@ -16,9 +16,11 @@ QtObject
readonly property string activeColor: Qt.darker(themeColor)
readonly property string inactiveThemeColor: "#9f9f9f"
readonly property string disabledThemeColor: disabledTextColor
readonly property string baseTextColor: "#3f3f3f"
readonly property int baseFontPixelSize: 12
readonly property string disabledTextColor: "#5f5f5f"
}

View file

@ -4,6 +4,7 @@ import "."
Item {
property bool checked: false
property alias label: label.text
property bool enabled: true
implicitWidth: track.width + label.width + 16
implicitHeight: label.height
@ -14,7 +15,7 @@ Item {
height: radius * 2
radius: Style.roundRadius
color: checked ? Style.frameColor : Style.minorFrameColor
color: (checked && enabled) ? Style.frameColor : Style.minorFrameColor
anchors.left: parent.left
anchors.leftMargin: Style.margin
anchors.verticalCenter: parent.verticalCenter
@ -26,7 +27,7 @@ Item {
radius: Style.roundRadius * 1.5
anchors.verticalCenter: parent.verticalCenter
color: checked ? Style.themeColor : "white"
color: (checked && enabled) ? Style.themeColor : "white"
border.width: 1
border.color: Style.inactiveThemeColor
@ -51,6 +52,8 @@ Item {
MouseArea {
anchors.fill: parent
id: mouseArea
enabled: root.enabled
hoverEnabled: true
onClicked: {
checked = !checked

View file

@ -56,6 +56,21 @@
<file>qml/Style.qml</file>
<file>qml/qmldir</file>
<file>qml/ClickableText.qml</file>
<file>qml/LauncherSettings.qml</file>
<file>qml/Section.qml</file>
<file>qml/SettingControl.qml</file>
<file>qml/SettingCheckbox.qml</file>
<file>qml/SettingsComboBox.qml</file>
<file>qml/SettingDescription.qml</file>
<file>qml/SettingLineEdit.qml</file>
<file>qml/Settings.qml</file>
<file>qml/AdvancedSettingsToggle.qml</file>
<file>qml/SettingPathChooser.qml</file>
<file>qml/SettingExtraArguments.qml</file>
<file>qml/Environment.qml</file>
<file>qml/DateTimeEdit.qml</file>
<file>qml/DateTimeValueEdit.qml</file>
<file>qml/SettingsDateTimePicker.qml</file>
</qresource>
<qresource prefix="/preview">
<file alias="close-icon">preview-close.png</file>