Merge /u/dancliff/flightgear/ branch launcher_addons_merge into next
https://sourceforge.net/p/flightgear/flightgear/merge-requests/151/
This commit is contained in:
commit
d5ce11ce55
11 changed files with 827 additions and 12 deletions
|
@ -34,6 +34,7 @@
|
|||
#include "addon_fwd.hxx"
|
||||
#include "contacts.hxx"
|
||||
#include "AddonVersion.hxx"
|
||||
#include "pointer_traits.hxx"
|
||||
|
||||
namespace flightgear
|
||||
{
|
||||
|
@ -88,6 +89,13 @@ public:
|
|||
// getMetadataFile()) and return the corresponding Addon instance.
|
||||
static Addon fromAddonDir(const SGPath& addonPath);
|
||||
|
||||
template<class T>
|
||||
static T fromAddonDir(const SGPath& addonPath)
|
||||
{
|
||||
using ptr_traits = shared_ptr_traits<T>;
|
||||
return ptr_traits::makeStrongRef(fromAddonDir(addonPath));
|
||||
}
|
||||
|
||||
std::string getId() const;
|
||||
|
||||
std::string getName() const;
|
||||
|
|
|
@ -14,9 +14,12 @@
|
|||
#include <Main/globals.hxx>
|
||||
#include <Network/HTTPClient.hxx>
|
||||
|
||||
#include "Add-ons/Addon.hxx"
|
||||
#include "Add-ons/addon_fwd.hxx"
|
||||
#include "LocalAircraftCache.hxx"
|
||||
#include "LauncherMainWindow.hxx"
|
||||
#include "CatalogListModel.hxx"
|
||||
#include "AddonsModel.hxx"
|
||||
#include "InstallSceneryDialog.hxx"
|
||||
#include "QtLauncher.hxx"
|
||||
|
||||
|
@ -31,13 +34,38 @@ AddOnsController::AddOnsController(LauncherMainWindow *parent) :
|
|||
|
||||
connect(m_catalogs, &CatalogListModel::catalogsChanged, this, &AddOnsController::onCatalogsChanged);
|
||||
|
||||
m_addonsModuleModel = new AddonsModel(this);
|
||||
connect(m_addonsModuleModel, &AddonsModel::modulesChanged, this, &AddOnsController::onAddonsChanged);
|
||||
|
||||
using namespace flightgear::addons;
|
||||
QSettings settings;
|
||||
m_sceneryPaths = settings.value("scenery-paths").toStringList();
|
||||
m_aircraftPaths = settings.value("aircraft-paths").toStringList();
|
||||
m_addonModulePaths = settings.value("addon-module-paths").toStringList();
|
||||
|
||||
int size = settings.beginReadArray("addon-modules");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
QString path = settings.value("path").toString();
|
||||
m_addonModulePaths.push_back( path );
|
||||
const SGPath addonPath(path.toStdString());
|
||||
|
||||
bool enable = settings.value("enable").toBool();
|
||||
|
||||
try {
|
||||
auto addon = Addon::fromAddonDir<AddonRef>(addonPath);
|
||||
m_addonsModuleModel->append(path, addon, enable);
|
||||
}
|
||||
catch (const sg_exception &e) {
|
||||
string msg = "Error getting add-on metadata: " + e.getFormattedMessage();
|
||||
SG_LOG(SG_GENERAL, SG_ALERT, msg);
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
qmlRegisterUncreatableType<AddOnsController>("FlightGear.Launcher", 1, 0, "AddOnsControllers", "no");
|
||||
qmlRegisterUncreatableType<CatalogListModel>("FlightGear.Launcher", 1, 0, "CatalogListModel", "no");
|
||||
qmlRegisterUncreatableType<AddonsModel>("FlightGear.Launcher", 1, 0, "AddonsModel", "no");
|
||||
}
|
||||
|
||||
QStringList AddOnsController::aircraftPaths() const
|
||||
|
@ -107,7 +135,7 @@ QString AddOnsController::addAddOnModulePath() const
|
|||
SGPath p(path.toStdString());
|
||||
bool isValid = false;
|
||||
|
||||
for (const auto& file: {"addon-config.xml", "addon-main.nas"}) {
|
||||
for (const auto& file: {"addon-metadata.xml", "addon-main.nas"}) {
|
||||
if ((p / file).exists()) {
|
||||
isValid = true;
|
||||
break;
|
||||
|
@ -120,7 +148,7 @@ QString AddOnsController::addAddOnModulePath() const
|
|||
mb.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||||
mb.setDefaultButton(QMessageBox::No);
|
||||
mb.setInformativeText(tr("Added modules should contain at least both of the following "
|
||||
"files: addon-config.xml, addon-main.nas."));
|
||||
"files: addon-metadata.xml, addon-main.nas."));
|
||||
mb.exec();
|
||||
|
||||
if (mb.result() == QMessageBox::No) {
|
||||
|
@ -128,6 +156,20 @@ QString AddOnsController::addAddOnModulePath() const
|
|||
}
|
||||
}
|
||||
|
||||
using namespace flightgear::addons;
|
||||
const SGPath addonPath(path.toStdString());
|
||||
|
||||
try {
|
||||
// when newly added, enable by default
|
||||
auto addon = Addon::fromAddonDir<AddonRef>(addonPath);
|
||||
if (!m_addonsModuleModel->append(path, addon, true)) {
|
||||
path = QString("");
|
||||
}
|
||||
} catch (const sg_exception &e) {
|
||||
string msg = "Error getting add-on metadata: " + e.getFormattedMessage();
|
||||
SG_LOG(SG_GENERAL, SG_ALERT, msg);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
|
@ -216,6 +258,12 @@ void AddOnsController::setSceneryPaths(QStringList sceneryPaths)
|
|||
emit sceneryPathsChanged(m_sceneryPaths);
|
||||
}
|
||||
|
||||
void AddOnsController::setAddons(AddonsModel* addons)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
void AddOnsController::setModulePaths(QStringList modulePaths)
|
||||
{
|
||||
if (m_addonModulePaths == modulePaths)
|
||||
|
@ -223,10 +271,22 @@ void AddOnsController::setModulePaths(QStringList modulePaths)
|
|||
|
||||
m_addonModulePaths = modulePaths;
|
||||
|
||||
m_addonsModuleModel->resetData(modulePaths);
|
||||
|
||||
QSettings settings;
|
||||
settings.setValue("addon-module-paths", m_addonModulePaths);
|
||||
int i = 0;
|
||||
settings.beginWriteArray("addon-modules");
|
||||
for (const QString& path : modulePaths) {
|
||||
if (m_addonsModuleModel->containsPath(path)) {
|
||||
settings.setArrayIndex(i++);
|
||||
settings.setValue("path", path);
|
||||
settings.setValue("enable", m_addonsModuleModel->getPathEnable(path));
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
emit modulePathsChanged(m_addonModulePaths);
|
||||
emit modulesChanged();
|
||||
}
|
||||
|
||||
void AddOnsController::officialCatalogAction(QString s)
|
||||
|
@ -268,3 +328,17 @@ void AddOnsController::onCatalogsChanged()
|
|||
emit isOfficialHangarRegisteredChanged();
|
||||
}
|
||||
|
||||
|
||||
void AddOnsController::onAddonsChanged()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
int i = 0;
|
||||
settings.beginWriteArray("addon-modules");
|
||||
for (const auto& path : m_addonModulePaths) {
|
||||
settings.setArrayIndex(i++);
|
||||
settings.setValue("path", path);
|
||||
settings.setValue("enable", m_addonsModuleModel->getPathEnable(path));
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <QStringList>
|
||||
|
||||
class CatalogListModel;
|
||||
class AddonsModel;
|
||||
class LauncherMainWindow;
|
||||
|
||||
class AddOnsController : public QObject
|
||||
|
@ -16,7 +17,7 @@ class AddOnsController : public QObject
|
|||
Q_PROPERTY(QStringList modulePaths READ modulePaths WRITE setModulePaths NOTIFY modulePathsChanged)
|
||||
|
||||
Q_PROPERTY(CatalogListModel* catalogs READ catalogs CONSTANT)
|
||||
|
||||
Q_PROPERTY(AddonsModel* modules READ modules NOTIFY modulesChanged)
|
||||
Q_PROPERTY(bool isOfficialHangarRegistered READ isOfficialHangarRegistered NOTIFY isOfficialHangarRegisteredChanged)
|
||||
Q_PROPERTY(bool showNoOfficialHangar READ showNoOfficialHangar NOTIFY showNoOfficialHangarChanged)
|
||||
|
||||
|
@ -30,6 +31,9 @@ public:
|
|||
CatalogListModel* catalogs() const
|
||||
{ return m_catalogs; }
|
||||
|
||||
AddonsModel* modules() const
|
||||
{ return m_addonsModuleModel; }
|
||||
|
||||
Q_INVOKABLE QString addAircraftPath() const;
|
||||
Q_INVOKABLE QString addSceneryPath() const;
|
||||
Q_INVOKABLE QString addAddOnModulePath() const;
|
||||
|
@ -44,10 +48,12 @@ public:
|
|||
bool showNoOfficialHangar() const;
|
||||
|
||||
Q_INVOKABLE void officialCatalogAction(QString s);
|
||||
|
||||
signals:
|
||||
void aircraftPathsChanged(QStringList aircraftPaths);
|
||||
void sceneryPathsChanged(QStringList sceneryPaths);
|
||||
void modulePathsChanged(QStringList modulePaths);
|
||||
void modulesChanged();
|
||||
|
||||
void isOfficialHangarRegisteredChanged();
|
||||
void showNoOfficialHangarChanged();
|
||||
|
@ -56,6 +62,8 @@ public slots:
|
|||
void setAircraftPaths(QStringList aircraftPaths);
|
||||
void setSceneryPaths(QStringList sceneryPaths);
|
||||
void setModulePaths(QStringList modulePaths);
|
||||
void setAddons(AddonsModel* addons);
|
||||
void onAddonsChanged(void);
|
||||
|
||||
private:
|
||||
bool shouldShowOfficialCatalogMessage() const;
|
||||
|
@ -63,6 +71,8 @@ private:
|
|||
|
||||
LauncherMainWindow* m_launcher;
|
||||
CatalogListModel* m_catalogs = nullptr;
|
||||
AddonsModel* m_addonsModuleModel = nullptr;
|
||||
|
||||
QStringList m_aircraftPaths;
|
||||
QStringList m_sceneryPaths;
|
||||
QStringList m_addonModulePaths;
|
||||
|
|
223
src/GUI/AddonsModel.cxx
Normal file
223
src/GUI/AddonsModel.cxx
Normal file
|
@ -0,0 +1,223 @@
|
|||
// AddonsModel.cxx - part of GUI launcher using Qt5
|
||||
//
|
||||
// Written by Dan Wickstrom, started February 2019.
|
||||
//
|
||||
// Copyright (C) 2019 Daniel Wickstrom <daniel.c.wickstrom@gmail.com>
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUrl>
|
||||
#include <string>
|
||||
#include <simgear/misc/strutils.hxx>
|
||||
#include <Include/version.h>
|
||||
|
||||
#include "AddonsModel.hxx"
|
||||
#include "Add-ons/AddonMetadataParser.hxx"
|
||||
|
||||
AddonsModel::AddonsModel(QObject* pr) :
|
||||
QAbstractListModel(pr)
|
||||
{
|
||||
m_roleToName[Qt::DisplayRole] = "display";
|
||||
m_nameToRole["display"] = Qt::DisplayRole;
|
||||
|
||||
int roleValue = IdRole;
|
||||
|
||||
for (auto it = m_roles.begin(); it != m_roles.end(); ++it) {
|
||||
QByteArray name = (*it).toLocal8Bit();
|
||||
m_roleToName[roleValue] = name;
|
||||
m_nameToRole[*it] = roleValue++;
|
||||
}
|
||||
}
|
||||
|
||||
AddonsModel::~AddonsModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void AddonsModel::resetData(const QStringList& ndata)
|
||||
{
|
||||
beginResetModel();
|
||||
QSet<QString> newSet = QSet<QString>::fromList(ndata);
|
||||
|
||||
for(const auto& path : m_addonsList) {
|
||||
if (!newSet.contains(path)) {
|
||||
m_addonsMap.remove(path);
|
||||
}
|
||||
}
|
||||
m_addonsList = ndata;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
int AddonsModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
return m_addonsList.size();
|
||||
}
|
||||
|
||||
|
||||
QVariant AddonsModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
auto idx = index.row();
|
||||
return get(idx, role);
|
||||
}
|
||||
|
||||
QVariant AddonsModel::get(int idx, QString role) const
|
||||
{
|
||||
int role_idx = m_nameToRole[role];
|
||||
return get(idx, role_idx);
|
||||
}
|
||||
|
||||
QVariant AddonsModel::get(int idx, int role) const
|
||||
{
|
||||
if (idx >= 0 && idx < m_addonsList.size()) {
|
||||
auto path = m_addonsList[idx];
|
||||
auto addon = m_addonsMap[path].addon;
|
||||
|
||||
if (role == Qt::DisplayRole) {
|
||||
QString name = QString::fromStdString(addon->getName());
|
||||
QString desc = QString::fromStdString(addon->getShortDescription());
|
||||
return tr("%1 - %2").arg(name).arg(desc);
|
||||
}
|
||||
else if (role == IdRole) {
|
||||
return QString::fromStdString(addon->getId());
|
||||
}
|
||||
else if (role == NameRole) {
|
||||
return QString::fromStdString(addon->getName());
|
||||
}
|
||||
else if (role == PathRole) {
|
||||
return path;
|
||||
}
|
||||
else if (role == VersionRole) {
|
||||
return QString::fromStdString(addon->getVersion()->str());
|
||||
}
|
||||
else if (role == AuthorsRole) {
|
||||
QStringList authors;
|
||||
for (auto author : addon->getAuthors()) {
|
||||
authors.push_back(QString::fromStdString(author->getName()));
|
||||
}
|
||||
return authors;
|
||||
}
|
||||
else if (role == MaintainersRole) {
|
||||
QStringList maintainers;
|
||||
for (auto maintainer : addon->getMaintainers()) {
|
||||
maintainers.push_back(QString::fromStdString(maintainer->getName()));
|
||||
}
|
||||
return maintainers;
|
||||
}
|
||||
else if (role == ShortDescriptionRole) {
|
||||
return QString::fromStdString(addon->getShortDescription());
|
||||
}
|
||||
else if (role == LongDescriptionRole) {
|
||||
return QString::fromStdString(addon->getLongDescription());
|
||||
}
|
||||
else if (role == LicenseDesignationRole) {
|
||||
return QString::fromStdString(addon->getLicenseDesignation());
|
||||
}
|
||||
else if (role == LicenseUrlRole) {
|
||||
return QUrl(QString::fromStdString(addon->getLicenseUrl()));
|
||||
}
|
||||
else if (role == TagsRole) {
|
||||
QStringList tags;
|
||||
for (auto tag : addon->getTags()) {
|
||||
tags.push_back(QString::fromStdString(tag));
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
else if (role == MinFGVersionRole) {
|
||||
return QString::fromStdString(addon->getMinFGVersionRequired());
|
||||
}
|
||||
else if (role == MaxFGVersionRole) {
|
||||
return QString::fromStdString(addon->getMaxFGVersionRequired());
|
||||
}
|
||||
else if (role == HomePageRole) {
|
||||
return QUrl(QString::fromStdString(addon->getHomePage()));
|
||||
}
|
||||
else if (role == DownloadUrlRole) {
|
||||
return QUrl(QString::fromStdString(addon->getDownloadUrl()));
|
||||
}
|
||||
else if (role == SupportUrlRole) {
|
||||
return QUrl(QString::fromStdString(addon->getSupportUrl()));
|
||||
}
|
||||
else if (role == CodeRepoUrlRole) {
|
||||
return QUrl(QString::fromStdString(addon->getCodeRepositoryUrl()));
|
||||
}
|
||||
else if (role == EnableRole) {
|
||||
return QVariant(m_addonsMap[path].enable && checkVersion(path));
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
|
||||
bool AddonsModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AddonsModel::append(QString path, flightgear::addons::AddonRef& addon, bool enable) {
|
||||
|
||||
if (!m_addonsMap.contains(path)) {
|
||||
m_addonsList.push_back(path);
|
||||
m_addonsMap[path].addon = addon;
|
||||
m_addonsMap[path].enable = enable && checkVersion(path);
|
||||
emit dataChanged(index(m_addonsList.size()-1), index(m_addonsList.size()-1));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::ItemFlags AddonsModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AddonsModel::roleNames() const
|
||||
{
|
||||
return m_roleToName;
|
||||
}
|
||||
|
||||
void AddonsModel::enable(int index, bool enable)
|
||||
{
|
||||
if ((index < 0) || (index >= m_addonsList.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto path = m_addonsList[index];
|
||||
m_addonsMap[path].enable = enable && checkVersion(path);
|
||||
|
||||
emit modulesChanged();
|
||||
}
|
||||
|
||||
bool AddonsModel::checkVersion(QString path) const
|
||||
{
|
||||
using namespace simgear;
|
||||
|
||||
// Check that the FlightGear version satisfies the add-on requirements
|
||||
std::string minFGversion = m_addonsMap[path].addon->getMinFGVersionRequired();
|
||||
if (strutils::compare_versions(FLIGHTGEAR_VERSION, minFGversion) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string maxFGversion = m_addonsMap[path].addon->getMaxFGVersionRequired();
|
||||
if (maxFGversion != "none" &&
|
||||
strutils::compare_versions(FLIGHTGEAR_VERSION, maxFGversion) > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
130
src/GUI/AddonsModel.hxx
Normal file
130
src/GUI/AddonsModel.hxx
Normal file
|
@ -0,0 +1,130 @@
|
|||
// AddonsModel.hxx - part of GUI launcher using Qt5
|
||||
//
|
||||
// Written by Dan Wickstrom, started February 2019.
|
||||
//
|
||||
// Copyright (C) 2019 Daniel Wickstrom <daniel.c.wickstrom@gmail.com>
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
#ifndef FG_GUI_METADATA_LIST_MODEL
|
||||
#define FG_GUI_METADATA_LIST_MODEL
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QPixmap>
|
||||
#include <QStringList>
|
||||
#include <set>
|
||||
#include <simgear/package/Root.hxx>
|
||||
#include <simgear/package/Catalog.hxx>
|
||||
#include "Add-ons/addon_fwd.hxx"
|
||||
#include "Add-ons/Addon.hxx"
|
||||
#include "Add-ons/AddonMetadataParser.hxx"
|
||||
|
||||
|
||||
|
||||
class AddonsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using AddonsMeta = struct { bool enable; flightgear::addons::AddonRef addon; };
|
||||
using AddonsMap = QHash<QString, AddonsMeta>;
|
||||
public:
|
||||
|
||||
AddonsModel(QObject* pr);
|
||||
|
||||
~AddonsModel();
|
||||
|
||||
int rowCount(const QModelIndex& parent) const override;
|
||||
|
||||
QVariant data(const QModelIndex& index, int role) const override;
|
||||
Q_INVOKABLE QVariant get(int index, int role) const;
|
||||
Q_INVOKABLE QVariant get(int index, QString role) const;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
bool append(QString path, flightgear::addons::AddonRef& addon, bool enable);
|
||||
|
||||
void resetData(const QStringList& ndata);
|
||||
|
||||
Q_INVOKABLE bool checkVersion(QString path) const;
|
||||
|
||||
bool getPathEnable(const QString& path) { return m_addonsMap[path].enable; }
|
||||
bool containsPath(const QString& path) { return m_addonsMap.contains(path); }
|
||||
|
||||
Q_INVOKABLE void enable(int index, bool enable);
|
||||
|
||||
// this must stay in sync with m_roles stringlist
|
||||
enum AddonRoles
|
||||
{
|
||||
IdRole = Qt::UserRole + 1,
|
||||
NameRole,
|
||||
PathRole,
|
||||
VersionRole,
|
||||
AuthorsRole,
|
||||
MaintainersRole,
|
||||
ShortDescriptionRole,
|
||||
LongDescriptionRole,
|
||||
LicenseDesignationRole,
|
||||
LicenseUrlRole,
|
||||
TagsRole,
|
||||
MinFGVersionRole,
|
||||
MaxFGVersionRole,
|
||||
HomePageRole,
|
||||
DownloadUrlRole,
|
||||
SupportUrlRole,
|
||||
CodeRepoUrlRole,
|
||||
EnableRole
|
||||
};
|
||||
|
||||
Q_ENUMS(AddonRoles)
|
||||
|
||||
signals:
|
||||
void modulesChanged();
|
||||
|
||||
private:
|
||||
// this must stay in sync with MetaRoles enum
|
||||
const QStringList m_roles = {
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"version",
|
||||
"authors",
|
||||
"maintainers",
|
||||
"short_description",
|
||||
"long_description",
|
||||
"license_designation",
|
||||
"license_url",
|
||||
"tags",
|
||||
"minversion_fg",
|
||||
"maxversion_fg",
|
||||
"homepage",
|
||||
"download_url",
|
||||
"support_url",
|
||||
"coderepo_url",
|
||||
"enable"
|
||||
};
|
||||
|
||||
QStringList m_addonsList;
|
||||
AddonsMap m_addonsMap;
|
||||
QHash<int, QByteArray> m_roleToName;
|
||||
QHash<QString, int> m_nameToRole;
|
||||
};
|
||||
|
||||
#endif // of Metadata_LIST_MODEL
|
|
@ -120,6 +120,8 @@ if (HAVE_QT)
|
|||
LauncherController.hxx
|
||||
AddOnsController.cxx
|
||||
AddOnsController.hxx
|
||||
AddonsModel.cxx
|
||||
AddonsModel.hxx
|
||||
PixmapImageItem.cxx
|
||||
PixmapImageItem.hxx
|
||||
${uic_sources}
|
||||
|
|
|
@ -292,9 +292,19 @@ void LauncherController::collectAircraftArgs()
|
|||
m_config->setArg("fg-aircraft", path);
|
||||
}
|
||||
|
||||
Q_FOREACH(QString path, settings.value("addon-module-paths").toStringList()) {
|
||||
m_config->setArg("addon", path);
|
||||
// add-on module paths
|
||||
int size = settings.beginReadArray("addon-modules");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
QString path = settings.value("path").toString();
|
||||
bool enable = settings.value("enable").toBool();
|
||||
if (enable) {
|
||||
m_config->setArg("addon", path);
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
}
|
||||
|
||||
void LauncherController::saveAircraft()
|
||||
|
|
|
@ -3,12 +3,38 @@ import FlightGear.Launcher 1.0
|
|||
import "."
|
||||
|
||||
Item {
|
||||
id: root
|
||||
Flickable {
|
||||
id: flick
|
||||
height: parent.height
|
||||
width: parent.width - scrollbar.width
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: contents.childrenRect.height
|
||||
function showDetails(index)
|
||||
{
|
||||
// set URI, start animation
|
||||
// change state
|
||||
detailsView.mdx = index;
|
||||
detailsView.visible = true
|
||||
contents.visible = false
|
||||
}
|
||||
|
||||
function goBack()
|
||||
{
|
||||
detailsView.visible = false
|
||||
contents.visible = true
|
||||
}
|
||||
|
||||
AddonsDetailsView
|
||||
{
|
||||
id: detailsView
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
onGoBack: {
|
||||
flick.goBack();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contents
|
||||
|
@ -144,7 +170,7 @@ Item {
|
|||
"know the folder(s) containing the Add-on Modules.")
|
||||
showAddButton: true
|
||||
onAdd: {
|
||||
var newPath =_addOns.addAddOnModulePath();
|
||||
var newPath = _addOns.addAddOnModulePath();
|
||||
if (newPath !== "") {
|
||||
_addOns.modulePaths.push(newPath)
|
||||
}
|
||||
|
@ -165,10 +191,10 @@ Item {
|
|||
|
||||
Repeater {
|
||||
id: addonModulesPathsRepeater
|
||||
model: _addOns.modulePaths
|
||||
delegate: PathListDelegate {
|
||||
model: _addOns.modules
|
||||
delegate: AddOnsDelegate {
|
||||
width: addonModulePathsColumn.width
|
||||
deletePromptText: qsTr("Remove the add-on module folder: '%1' from the list? (The folder contents will not be changed)").arg(modelData);
|
||||
deletePromptText: qsTr("Remove the add-on module folder: '%1' from the list? (The folder contents will not be changed)").arg(model.path);
|
||||
modelCount: _addOns.modulePaths.length
|
||||
|
||||
onPerformDelete: {
|
||||
|
@ -180,9 +206,13 @@ Item {
|
|||
onPerformMove: {
|
||||
var modifiedPaths = _addOns.modulePaths.slice()
|
||||
modifiedPaths.splice(model.index, 1);
|
||||
modifiedPaths.splice(newIndex, 0, modelData)
|
||||
modifiedPaths.splice(newIndex, 0, model.path)
|
||||
_addOns.modulePaths = modifiedPaths;
|
||||
}
|
||||
|
||||
onShowDetails: {
|
||||
flick.showDetails(detailIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
139
src/GUI/qml/AddOnsDelegate.qml
Normal file
139
src/GUI/qml/AddOnsDelegate.qml
Normal file
|
@ -0,0 +1,139 @@
|
|||
import QtQuick 2.4
|
||||
import FlightGear.Launcher 1.0
|
||||
import "."
|
||||
|
||||
Item {
|
||||
id: delegateRoot
|
||||
|
||||
signal performDelete();
|
||||
signal performMove(var newIndex);
|
||||
signal showDetails(var detailIndex);
|
||||
|
||||
property alias deletePromptText: confirmDeletePath.promptText
|
||||
property int modelCount: 0
|
||||
|
||||
height: childrenRect.height
|
||||
|
||||
function displayLabel(lbl) {
|
||||
return qsTr("%1 %2").arg(lbl).arg((_addOns.modules.checkVersion(model.path) ? "" : " (disabled due to incompatible FG version)"));
|
||||
}
|
||||
|
||||
Item {
|
||||
id: divider
|
||||
visible: model.index > 0 // divider before each item except the first
|
||||
height: Style.margin
|
||||
width: parent.width
|
||||
|
||||
Rectangle {
|
||||
color: Style.frameColor
|
||||
height: 1
|
||||
width: parent.width - Style.strutSize
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
// this is the main visible content
|
||||
Item {
|
||||
id: contentRect
|
||||
anchors.top: divider.bottom
|
||||
|
||||
height: Math.max(label.implicitHeight, pathDeleteButton.height)
|
||||
width: delegateRoot.width
|
||||
|
||||
Checkbox {
|
||||
id: chkbox
|
||||
checked: model.enable
|
||||
width: 30
|
||||
anchors.left: parent.left
|
||||
anchors.right: addonsDelegateHover.left
|
||||
anchors.rightMargin: Style.margin
|
||||
height: contentRect.height
|
||||
onCheckedChanged: {
|
||||
_addOns.modules.enable(model.index, checked)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: addonsDelegateHover
|
||||
anchors.leftMargin: Style.margin
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: chkbox.right
|
||||
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
// it's important that the button and text are children of the
|
||||
// MouseArea, so nested containsMouse logic works
|
||||
ClickableText {
|
||||
id: label
|
||||
text: delegateRoot.displayLabel(model.display)
|
||||
onClicked: {
|
||||
// open the location
|
||||
delegateRoot.showDetails(model.index)
|
||||
}
|
||||
anchors.left: addonsDelegateHover.left
|
||||
anchors.right: reorderButton.left
|
||||
anchors.rightMargin: Style.margin
|
||||
height: contentRect.height
|
||||
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
DeleteButton {
|
||||
id: pathDeleteButton
|
||||
anchors.right: parent.right
|
||||
visible: addonsDelegateHover.containsMouse
|
||||
onClicked: {
|
||||
confirmDeletePath.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
DragToReorderButton {
|
||||
id: reorderButton
|
||||
anchors.right: pathDeleteButton.left
|
||||
anchors.rightMargin: Style.margin
|
||||
visible: addonsDelegateHover.containsMouse && (canMoveDown || canMoveUp)
|
||||
|
||||
canMoveUp: model.index > 0
|
||||
canMoveDown: model.index < (delegateRoot.modelCount - 1)
|
||||
|
||||
onMoveUp: {
|
||||
if (model.index === 0)
|
||||
return;
|
||||
delegateRoot.performMove(model.index - 1)
|
||||
}
|
||||
|
||||
onMoveDown: {
|
||||
delegateRoot.performMove(model.index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
YesNoPanel {
|
||||
id: confirmDeletePath
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
yesText: qsTr("Remove")
|
||||
noText: qsTr("Cancel")
|
||||
yesIsDestructive: true
|
||||
|
||||
onAccepted: {
|
||||
delegateRoot.performDelete()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
confirmDeletePath.visible = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
187
src/GUI/qml/AddonsDetailsView.qml
Normal file
187
src/GUI/qml/AddonsDetailsView.qml
Normal file
|
@ -0,0 +1,187 @@
|
|||
import QtQuick 2.4
|
||||
import FlightGear.Launcher 1.0
|
||||
import "."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
color: "white"
|
||||
property int mdx: 0
|
||||
signal goBack()
|
||||
|
||||
MouseArea {
|
||||
// consume all mouse-clicks on the detail view
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Flickable
|
||||
{
|
||||
id: flickable
|
||||
|
||||
anchors.fill: parent
|
||||
contentWidth: parent.width
|
||||
contentHeight: content.childrenRect.height
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
function labelText(lbl, idx, key) {
|
||||
var value = _addOns.modules.get(idx, key);
|
||||
if (typeof value === 'object') {
|
||||
value = Object.keys(value).map(function(key) { return value[key]; }).join(', ');
|
||||
}
|
||||
return qsTr("%1: %2").arg(lbl).arg(value);
|
||||
}
|
||||
function getMeta(idx, key) {
|
||||
return _addOns.modules.get(idx, key);
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
id: content
|
||||
width: root.width - scrollbar.width
|
||||
height: childrenRect.height
|
||||
|
||||
Column {
|
||||
id: columnTop
|
||||
width: content.width - (Style.margin * 2)
|
||||
spacing: Style.margin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// description + authors container
|
||||
Item {
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.margin
|
||||
anchors.right: parent.right
|
||||
spacing: Style.margin
|
||||
|
||||
StyledText {
|
||||
id: addonName
|
||||
text: flickable.labelText("Module", mdx, "name");
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: flickable.getMeta(mdx, "name") != ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: addonDescription
|
||||
text: flickable.labelText("Description", mdx, "long_description");
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: flickable.getMeta(mdx, "long_description") != ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: addonId
|
||||
text: flickable.labelText("Id", mdx, "id");
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: flickable.getMeta(mdx, "id") != ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: addonVersion
|
||||
text: flickable.labelText("Version", mdx, "version");
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: flickable.getMeta(mdx, "version") != ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: minFgVersion
|
||||
text: flickable.labelText("Min FG Version", mdx, "minversion_fg");
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: flickable.getMeta(mdx, "minversion_fg") != ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: maxFgVersion
|
||||
text: flickable.labelText("Max FG Version", mdx, "maxversion_fg");
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: flickable.getMeta(mdx, "maxversion_fg") != ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: addonAuthors
|
||||
text: flickable.labelText("Authors", mdx, "authors");
|
||||
width: parent.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
visible: (flickable.getMeta(mdx, "authors") != undefined)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: addonMaintainers
|
||||
text: flickable.labelText("Maintainers", mdx, "maintainers");
|
||||
width: parent.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
visible: (flickable.getMeta(mdx, "maintainers") != undefined)
|
||||
}
|
||||
}
|
||||
|
||||
} // end of Item
|
||||
|
||||
// web-links row
|
||||
Row {
|
||||
id: weblinks
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
spacing: Style.margin
|
||||
|
||||
Weblink {
|
||||
visible: flickable.getMeta(mdx, "homepage") != ""
|
||||
label: qsTr("Website")
|
||||
link: flickable.getMeta(mdx, "homepage") || ""
|
||||
}
|
||||
|
||||
Weblink {
|
||||
visible: flickable.getMeta(mdx, "support_url") != ""
|
||||
label: qsTr("Support and issue reporting")
|
||||
link: flickable.getMeta(mdx, "support_url") || ""
|
||||
}
|
||||
|
||||
Weblink {
|
||||
visible: flickable.getMeta(mdx, "download_url") != ""
|
||||
label: qsTr("Download")
|
||||
link: flickable.getMeta(mdx, "download_url") || ""
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText {
|
||||
id: pathLabel
|
||||
text: flickable.labelText("Local file location", mdx, "path");
|
||||
onClicked: {
|
||||
// open the location
|
||||
_addOns.openDirectory(flickable.getMeta(mdx, "path"))
|
||||
}
|
||||
// anchors.top: weblinks.bottom
|
||||
anchors.left: parent.left
|
||||
width: parent.width
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
visible: flickable.getMeta(mdx, "path") != undefined
|
||||
}
|
||||
|
||||
Text {
|
||||
height: 50
|
||||
text: " "
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
width: Style.strutSize
|
||||
id: backButton
|
||||
text: "< Back"
|
||||
onClicked: root.goBack();
|
||||
}
|
||||
|
||||
} // main layout column
|
||||
} // of main item
|
||||
|
||||
} // of Flickable
|
||||
|
||||
} // of Rect
|
|
@ -85,6 +85,8 @@
|
|||
<file>qml/DeleteButton.qml</file>
|
||||
<file>qml/YesNoPanel.qml</file>
|
||||
<file>qml/PathListDelegate.qml</file>
|
||||
<file>qml/AddOnsDelegate.qml</file>
|
||||
<file>qml/AddonsDetailsView.qml</file>
|
||||
<file>qml/AddCatalogPanel.qml</file>
|
||||
<file>qml/LineEdit.qml</file>
|
||||
<file>qml/LocationAirportView.qml</file>
|
||||
|
|
Loading…
Reference in a new issue