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:
parent
9a044a474b
commit
37dc418ce1
41 changed files with 2333 additions and 311 deletions
|
@ -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;
|
||||
|
|
|
@ -128,6 +128,7 @@ public slots:
|
|||
private slots:
|
||||
void onScanStarted();
|
||||
void onScanAddedItems(int count);
|
||||
void onLocalCacheCleared();
|
||||
|
||||
private:
|
||||
friend class PackageDelegate;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -132,6 +132,7 @@ signals:
|
|||
void scanStarted();
|
||||
void scanCompleted();
|
||||
|
||||
void cleared();
|
||||
void addedItems(int count);
|
||||
public slots:
|
||||
|
||||
|
|
|
@ -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
16
src/GUI/PathUrlHelper.cxx
Normal 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
21
src/GUI/PathUrlHelper.hxx
Normal 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
|
51
src/GUI/PopupWindowTracker.cxx
Normal file
51
src/GUI/PopupWindowTracker.cxx
Normal 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;
|
||||
}
|
36
src/GUI/PopupWindowTracker.hxx
Normal file
36
src/GUI/PopupWindowTracker.hxx
Normal 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
|
|
@ -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());
|
||||
}
|
||||
|
|
5
src/GUI/SettingsComboBox.qml
Normal file
5
src/GUI/SettingsComboBox.qml
Normal file
|
@ -0,0 +1,5 @@
|
|||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
|
||||
}
|
58
src/GUI/qml/AdvancedSettingsToggle.qml
Normal file
58
src/GUI/qml/AdvancedSettingsToggle.qml
Normal 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
|
||||
}
|
||||
}
|
|
@ -93,7 +93,7 @@ Rectangle {
|
|||
|
||||
Rectangle {
|
||||
border.width: 1
|
||||
border.color: "#afafaf"
|
||||
border.color: Style.minorFrameColor
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
151
src/GUI/qml/DateTimeEdit.qml
Normal file
151
src/GUI/qml/DateTimeEdit.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
184
src/GUI/qml/DateTimeValueEdit.qml
Normal file
184
src/GUI/qml/DateTimeValueEdit.qml
Normal 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
220
src/GUI/qml/Environment.qml
Normal 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
|
||||
}
|
9
src/GUI/qml/LauncherSettings.qml
Normal file
9
src/GUI/qml/LauncherSettings.qml
Normal file
|
@ -0,0 +1,9 @@
|
|||
import QtQuick 2.0
|
||||
import FlightGear.Launcher 1.0
|
||||
|
||||
|
||||
RenderSettings
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
121
src/GUI/qml/Section.qml
Normal 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
|
||||
}
|
||||
}
|
40
src/GUI/qml/SettingCheckbox.qml
Normal file
40
src/GUI/qml/SettingCheckbox.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
89
src/GUI/qml/SettingControl.qml
Normal file
89
src/GUI/qml/SettingControl.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
12
src/GUI/qml/SettingDescription.qml
Normal file
12
src/GUI/qml/SettingDescription.qml
Normal 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
|
||||
}
|
77
src/GUI/qml/SettingExtraArguments.qml
Normal file
77
src/GUI/qml/SettingExtraArguments.qml
Normal 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
|
||||
}
|
||||
|
||||
}
|
82
src/GUI/qml/SettingLineEdit.qml
Normal file
82
src/GUI/qml/SettingLineEdit.qml
Normal 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
|
||||
}
|
||||
}
|
109
src/GUI/qml/SettingPathChooser.qml
Normal file
109
src/GUI/qml/SettingPathChooser.qml
Normal 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
475
src/GUI/qml/Settings.qml
Normal 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
|
||||
}
|
33
src/GUI/qml/SettingsComboBox.qml
Normal file
33
src/GUI/qml/SettingsComboBox.qml
Normal 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
|
||||
}
|
||||
}
|
39
src/GUI/qml/SettingsDateTimePicker.qml
Normal file
39
src/GUI/qml/SettingsDateTimePicker.qml
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue