From 1e858767b1da725f8447adaca57b150c8387d210 Mon Sep 17 00:00:00 2001 From: Dan Wickstrom Date: Sun, 3 Mar 2019 12:59:48 -0500 Subject: [PATCH] Modifications to launcher to load Add-on modules and view related metadata. Each module can also be selectively enabled with a checkbox. Fixes also submitted by Florent Rougon. --- src/Add-ons/Addon.hxx | 8 ++ src/GUI/AddOnsController.cxx | 82 ++++++++++- src/GUI/AddOnsController.hxx | 12 +- src/GUI/AddonsModel.cxx | 223 ++++++++++++++++++++++++++++++ src/GUI/AddonsModel.hxx | 130 +++++++++++++++++ src/GUI/CMakeLists.txt | 2 + src/GUI/LauncherController.cxx | 14 +- src/GUI/qml/AddOns.qml | 40 +++++- src/GUI/qml/AddOnsDelegate.qml | 139 +++++++++++++++++++ src/GUI/qml/AddonsDetailsView.qml | 187 +++++++++++++++++++++++++ src/GUI/resources.qrc | 2 + 11 files changed, 827 insertions(+), 12 deletions(-) create mode 100644 src/GUI/AddonsModel.cxx create mode 100644 src/GUI/AddonsModel.hxx create mode 100644 src/GUI/qml/AddOnsDelegate.qml create mode 100644 src/GUI/qml/AddonsDetailsView.qml diff --git a/src/Add-ons/Addon.hxx b/src/Add-ons/Addon.hxx index f95d2b75e..8983283a1 100644 --- a/src/Add-ons/Addon.hxx +++ b/src/Add-ons/Addon.hxx @@ -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 + static T fromAddonDir(const SGPath& addonPath) + { + using ptr_traits = shared_ptr_traits; + return ptr_traits::makeStrongRef(fromAddonDir(addonPath)); + } + std::string getId() const; std::string getName() const; diff --git a/src/GUI/AddOnsController.cxx b/src/GUI/AddOnsController.cxx index f6496286f..43d9988f7 100644 --- a/src/GUI/AddOnsController.cxx +++ b/src/GUI/AddOnsController.cxx @@ -14,9 +14,12 @@ #include
#include +#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(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("FlightGear.Launcher", 1, 0, "AddOnsControllers", "no"); qmlRegisterUncreatableType("FlightGear.Launcher", 1, 0, "CatalogListModel", "no"); + qmlRegisterUncreatableType("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(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(); +} diff --git a/src/GUI/AddOnsController.hxx b/src/GUI/AddOnsController.hxx index bec917a28..fab9119a8 100644 --- a/src/GUI/AddOnsController.hxx +++ b/src/GUI/AddOnsController.hxx @@ -5,6 +5,7 @@ #include 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; diff --git a/src/GUI/AddonsModel.cxx b/src/GUI/AddonsModel.cxx new file mode 100644 index 000000000..1d605051d --- /dev/null +++ b/src/GUI/AddonsModel.cxx @@ -0,0 +1,223 @@ +// AddonsModel.cxx - part of GUI launcher using Qt5 +// +// Written by Dan Wickstrom, started February 2019. +// +// Copyright (C) 2019 Daniel Wickstrom +// +// 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 +#include +#include +#include +#include + +#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 newSet = QSet::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 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; +} \ No newline at end of file diff --git a/src/GUI/AddonsModel.hxx b/src/GUI/AddonsModel.hxx new file mode 100644 index 000000000..e72184f34 --- /dev/null +++ b/src/GUI/AddonsModel.hxx @@ -0,0 +1,130 @@ +// AddonsModel.hxx - part of GUI launcher using Qt5 +// +// Written by Dan Wickstrom, started February 2019. +// +// Copyright (C) 2019 Daniel Wickstrom +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#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; +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 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 m_roleToName; + QHash m_nameToRole; +}; + +#endif // of Metadata_LIST_MODEL diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index 5f4c20472..9715df6b9 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -120,6 +120,8 @@ if (HAVE_QT) LauncherController.hxx AddOnsController.cxx AddOnsController.hxx + AddonsModel.cxx + AddonsModel.hxx PixmapImageItem.cxx PixmapImageItem.hxx ${uic_sources} diff --git a/src/GUI/LauncherController.cxx b/src/GUI/LauncherController.cxx index 901ebeb3e..b7615a41a 100644 --- a/src/GUI/LauncherController.cxx +++ b/src/GUI/LauncherController.cxx @@ -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() diff --git a/src/GUI/qml/AddOns.qml b/src/GUI/qml/AddOns.qml index 741f4f5d0..cf32c38a5 100644 --- a/src/GUI/qml/AddOns.qml +++ b/src/GUI/qml/AddOns.qml @@ -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) + } } } diff --git a/src/GUI/qml/AddOnsDelegate.qml b/src/GUI/qml/AddOnsDelegate.qml new file mode 100644 index 000000000..2839b9d19 --- /dev/null +++ b/src/GUI/qml/AddOnsDelegate.qml @@ -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 + } + } + + } +} + diff --git a/src/GUI/qml/AddonsDetailsView.qml b/src/GUI/qml/AddonsDetailsView.qml new file mode 100644 index 000000000..990d5a3da --- /dev/null +++ b/src/GUI/qml/AddonsDetailsView.qml @@ -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 diff --git a/src/GUI/resources.qrc b/src/GUI/resources.qrc index 30c954827..3a7f3f820 100644 --- a/src/GUI/resources.qrc +++ b/src/GUI/resources.qrc @@ -85,6 +85,8 @@ qml/DeleteButton.qml qml/YesNoPanel.qml qml/PathListDelegate.qml + qml/AddOnsDelegate.qml + qml/AddonsDetailsView.qml qml/AddCatalogPanel.qml qml/LineEdit.qml qml/LocationAirportView.qml