From 041b9527d34dcf9ca8e08acda73d0a2f17a33c4e Mon Sep 17 00:00:00 2001 From: James Turner <zakalawe@mac.com> Date: Mon, 9 Oct 2017 22:53:26 +0200 Subject: [PATCH] Seperate aircraft cache from the model. Allows exposing aircraft data to QML (via a helper object) outside the context of the model. --- src/GUI/AircraftDetailsView.qml | 51 ++++ src/GUI/AircraftModel.cxx | 527 ++++---------------------------- src/GUI/AircraftModel.hxx | 77 +---- src/GUI/CMakeLists.txt | 5 + src/GUI/LauncherMainWindow.cxx | 21 +- src/GUI/LocalAircraftCache.cxx | 519 +++++++++++++++++++++++++++++++ src/GUI/LocalAircraftCache.hxx | 138 +++++++++ src/GUI/PathsDialog.cxx | 5 +- src/GUI/PropertyItemModel.cxx | 2 +- src/GUI/QmlAircraftInfo.cxx | 224 ++++++++++++++ src/GUI/QmlAircraftInfo.hxx | 94 ++++++ src/GUI/resources.qrc | 1 + 12 files changed, 1111 insertions(+), 553 deletions(-) create mode 100644 src/GUI/AircraftDetailsView.qml create mode 100644 src/GUI/LocalAircraftCache.cxx create mode 100644 src/GUI/LocalAircraftCache.hxx create mode 100644 src/GUI/QmlAircraftInfo.cxx create mode 100644 src/GUI/QmlAircraftInfo.hxx diff --git a/src/GUI/AircraftDetailsView.qml b/src/GUI/AircraftDetailsView.qml new file mode 100644 index 000000000..c24d84b51 --- /dev/null +++ b/src/GUI/AircraftDetailsView.qml @@ -0,0 +1,51 @@ +import QtQuick 2.0 +import FGLauncher 1.0 + +Item { + + property alias aurcradftURI: aircraft.uri + + + AircraftInfo + { + id: aircraft + } + + Column { + Text { + id: aircraftName + } + + Image { + id: preview + + + // selector overlay + + // left / right arrows + } + + Timer { + id: previewCycleTimer + } + + + Text { + id: aircraftDescription + } + + Text { + id: aircraftAuthors + } + + // info button + + // version warning! + + // ratings box + + // package size + + // install / download / update button + } // main layout column +} diff --git a/src/GUI/AircraftModel.cxx b/src/GUI/AircraftModel.cxx index c4d0ca2ed..bbaf5d932 100644 --- a/src/GUI/AircraftModel.cxx +++ b/src/GUI/AircraftModel.cxx @@ -20,11 +20,6 @@ #include "AircraftModel.hxx" -#include <QDir> -#include <QThread> -#include <QMutex> -#include <QMutexLocker> -#include <QDataStream> #include <QSettings> #include <QDebug> #include <QSharedPointer> @@ -41,317 +36,13 @@ #include <Main/globals.hxx> #include <Include/version.h> +#include "QmlAircraftInfo.hxx" + const int STANDARD_THUMBNAIL_HEIGHT = 128; const int STANDARD_THUMBNAIL_WIDTH = 172; -static quint32 CACHE_VERSION = 8; using namespace simgear::pkg; -AircraftItem::AircraftItem() -{ -} - -AircraftItem::AircraftItem(QDir dir, QString filePath) -{ - SGPropertyNode root; - readProperties(filePath.toStdString(), &root); - - if (!root.hasChild("sim")) { - throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString()); - } - - SGPropertyNode_ptr sim = root.getNode("sim"); - - path = filePath; - pathModTime = QFileInfo(path).lastModified(); - if (sim->getBoolValue("exclude-from-gui", false)) { - excluded = true; - return; - } - - description = sim->getStringValue("description"); - authors = sim->getStringValue("author"); - - if (sim->hasChild("rating")) { - SGPropertyNode_ptr ratingsNode = sim->getNode("rating"); - ratings[0] = ratingsNode->getIntValue("FDM"); - ratings[1] = ratingsNode->getIntValue("systems"); - ratings[2] = ratingsNode->getIntValue("cockpit"); - ratings[3] = ratingsNode->getIntValue("model"); - - } - - if (sim->hasChild("long-description")) { - // clean up any XML whitspace in the text. - longDescription = QString(sim->getStringValue("long-description")).simplified(); - } - - if (sim->hasChild("variant-of")) { - variantOf = sim->getStringValue("variant-of"); - } else { - isPrimary = true; - } - - if (sim->hasChild("primary-set")) { - isPrimary = sim->getBoolValue("primary-set"); - } - - if (sim->hasChild("tags")) { - SGPropertyNode_ptr tagsNode = sim->getChild("tags"); - int nChildren = tagsNode->nChildren(); - for (int i = 0; i < nChildren; i++) { - const SGPropertyNode* c = tagsNode->getChild(i); - if (strcmp(c->getName(), "tag") == 0) { - const char* tagName = c->getStringValue(); - usesHeliports |= (strcmp(tagName, "helicopter") == 0); - // could also consider vtol tag? - usesSeaports |= (strcmp(tagName, "seaplane") == 0); - usesSeaports |= (strcmp(tagName, "floats") == 0); - - needsMaintenance |= (strcmp(tagName, "needs-maintenance") == 0); - } - } // of tags iteration - } // of set-xml has tags - - if (sim->hasChild("previews")) { - SGPropertyNode_ptr previewsNode = sim->getChild("previews"); - for (auto previewNode : previewsNode->getChildren("preview")) { - // add file path as url - QString pathInXml = QString::fromStdString(previewNode->getStringValue("path")); - QString previewPath = dir.absoluteFilePath(pathInXml); - previews.append(QUrl::fromLocalFile(previewPath)); - } - } - - if (sim->hasChild("thumbnail")) { - thumbnailPath = sim->getStringValue("thumbnail"); - } else { - thumbnailPath = "thumbnail.jpg"; - } - - if (sim->hasChild("minimum-fg-version")) { - minFGVersion = sim->getStringValue("minimum-fg-version"); - } -} - -QString AircraftItem::baseName() const -{ - QString fn = QFileInfo(path).fileName(); - fn.truncate(fn.count() - 8); - return fn; -} - -void AircraftItem::fromDataStream(QDataStream& ds) -{ - ds >> path >> pathModTime >> excluded; - if (excluded) { - return; - } - - ds >> description >> longDescription >> authors >> variantOf >> isPrimary; - for (int i=0; i<4; ++i) ds >> ratings[i]; - ds >> previews; - ds >> thumbnailPath; - ds >> minFGVersion; - ds >> needsMaintenance >> usesHeliports >> usesSeaports; -} - -void AircraftItem::toDataStream(QDataStream& ds) const -{ - ds << path << pathModTime << excluded; - if (excluded) { - return; - } - - ds << description << longDescription << authors << variantOf << isPrimary; - for (int i=0; i<4; ++i) ds << ratings[i]; - ds << previews; - ds << thumbnailPath; - ds << minFGVersion; - ds << needsMaintenance << usesHeliports << usesSeaports; -} - -QPixmap AircraftItem::thumbnail(bool loadIfRequired) const -{ - if (m_thumbnail.isNull() && loadIfRequired) { - QFileInfo info(path); - QDir dir = info.dir(); - if (dir.exists(thumbnailPath)) { - m_thumbnail.load(dir.filePath(thumbnailPath)); - // resize to the standard size - if (m_thumbnail.height() > STANDARD_THUMBNAIL_HEIGHT) { - m_thumbnail = m_thumbnail.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT, Qt::SmoothTransformation); - } - } - } - - return m_thumbnail; -} - -class AircraftScanThread : public QThread -{ - Q_OBJECT -public: - AircraftScanThread(QStringList dirsToScan) : - m_dirs(dirsToScan), - m_done(false) - { - } - - ~AircraftScanThread() - { - } - - /** thread-safe access to items already scanned */ - QVector<AircraftItemPtr> items() - { - QVector<AircraftItemPtr> result; - QMutexLocker g(&m_lock); - result.swap(m_items); - g.unlock(); - return result; - } - - void setDone() - { - m_done = true; - } -Q_SIGNALS: - void addedItems(); - -protected: - virtual void run() - { - readCache(); - - Q_FOREACH(QString d, m_dirs) { - scanAircraftDir(QDir(d)); - if (m_done) { - return; - } - } - - writeCache(); - } - -private: - void readCache() - { - QSettings settings; - QByteArray cacheData = settings.value("aircraft-cache").toByteArray(); - if (!cacheData.isEmpty()) { - QDataStream ds(cacheData); - quint32 count, cacheVersion; - ds >> cacheVersion >> count; - - if (cacheVersion != CACHE_VERSION) { - return; // mis-matched cache, version, drop - } - - for (quint32 i=0; i<count; ++i) { - AircraftItemPtr item(new AircraftItem); - item->fromDataStream(ds); - - QFileInfo finfo(item->path); - if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) { - // corresponding -set.xml file still exists and is - // unmodified - m_cachedItems[item->path] = item; - } - } // of cached item iteration - } - } - - void writeCache() - { - QSettings settings; - QByteArray cacheData; - { - QDataStream ds(&cacheData, QIODevice::WriteOnly); - quint32 count = m_nextCache.count(); - ds << CACHE_VERSION << count; - - Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) { - item->toDataStream(ds); - } - } - - settings.setValue("aircraft-cache", cacheData); - } - - void scanAircraftDir(QDir path) - { - QTime t; - t.start(); - - QStringList filters; - filters << "*-set.xml"; - Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { - QDir childDir(child.absoluteFilePath()); - QMap<QString, AircraftItemPtr> baseAircraft; - QList<AircraftItemPtr> variants; - - Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) { - try { - QString absolutePath = xmlChild.absoluteFilePath(); - AircraftItemPtr item; - - if (m_cachedItems.contains(absolutePath)) { - item = m_cachedItems.value(absolutePath); - } else { - item = AircraftItemPtr(new AircraftItem(childDir, absolutePath)); - } - - m_nextCache[absolutePath] = item; - - if (item->excluded) { - continue; - } - - if (item->isPrimary) { - baseAircraft.insert(item->baseName(), item); - } else { - variants.append(item); - } - } catch (sg_exception& e) { - continue; - } - - if (m_done) { - return; - } - } // of set.xml iteration - - // bind variants to their principals - Q_FOREACH(AircraftItemPtr item, variants) { - if (!baseAircraft.contains(item->variantOf)) { - qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path; - continue; - } - - baseAircraft.value(item->variantOf)->variants.append(item); - } - - // lock mutex while we modify the items array - { - QMutexLocker g(&m_lock); - m_items+=(baseAircraft.values().toVector()); - } - - emit addedItems(); - } // of subdir iteration - } - - QMutex m_lock; - QStringList m_dirs; - QVector<AircraftItemPtr> m_items; - - QMap<QString, AircraftItemPtr > m_cachedItems; - QMap<QString, AircraftItemPtr > m_nextCache; - - bool m_done; -}; - class PackageDelegate : public simgear::pkg::Delegate { public: @@ -473,7 +164,7 @@ private: } int offset = std::distance(m_model->m_packages.begin(), it); - return m_model->index(offset + m_model->m_items.size()); + return m_model->index(offset + m_model->m_cachedLocalAircraftCount); } AircraftItemModel* m_model; @@ -482,11 +173,15 @@ private: AircraftItemModel::AircraftItemModel(QObject* pr) : QAbstractListModel(pr) { + auto cache = LocalAircraftCache::instance(); + connect(cache, &LocalAircraftCache::scanStarted, + this, &AircraftItemModel::onScanStarted); + connect(cache, &LocalAircraftCache::addedItems, + this, &AircraftItemModel::onScanAddedItems); } AircraftItemModel::~AircraftItemModel() { - abandonCurrentScan(); delete m_delegate; } @@ -506,56 +201,22 @@ void AircraftItemModel::setPackageRoot(const simgear::pkg::RootRef& root) } } -void AircraftItemModel::setPaths(QStringList paths) +void AircraftItemModel::onScanStarted() { - m_paths = paths; -} - -void AircraftItemModel::scanDirs() -{ - abandonCurrentScan(); - - const int numToRemove = m_items.size(); - if (numToRemove > 0) { + const int numToRemove = m_cachedLocalAircraftCount; + if (numToRemove > 0) { int lastRow = numToRemove - 1; beginRemoveRows(QModelIndex(), 0, lastRow); - m_items.remove(0, numToRemove); m_delegateStates.remove(0, numToRemove); - endRemoveRows(); - } - - QStringList dirs = m_paths; - - Q_FOREACH(SGPath ap, globals->get_aircraft_paths()) { - dirs << QString::fromStdString(ap.utf8Str()); - } - - SGPath rootAircraft(globals->get_fg_root()); - rootAircraft.append("Aircraft"); - dirs << QString::fromStdString(rootAircraft.utf8Str()); - - m_scanThread = new AircraftScanThread(dirs); - connect(m_scanThread, &AircraftScanThread::finished, this, - &AircraftItemModel::onScanFinished); - connect(m_scanThread, &AircraftScanThread::addedItems, - this, &AircraftItemModel::onScanResults); - m_scanThread->start(); -} - -void AircraftItemModel::abandonCurrentScan() -{ - if (m_scanThread) { - m_scanThread->setDone(); - m_scanThread->wait(1000); - delete m_scanThread; - m_scanThread = NULL; + m_cachedLocalAircraftCount = 0; + endRemoveRows(); } } void AircraftItemModel::refreshPackages() { simgear::pkg::PackageList newPkgs = m_packageRoot->allPackages(); - const int firstRow = m_items.size(); + const int firstRow = m_cachedLocalAircraftCount; const int newSize = newPkgs.size(); const int newTotalSize = firstRow + newSize; @@ -589,7 +250,7 @@ void AircraftItemModel::refreshPackages() int AircraftItemModel::rowCount(const QModelIndex& parent) const { - return m_items.size() + m_packages.size(); + return m_cachedLocalAircraftCount + m_packages.size(); } QVariant AircraftItemModel::data(const QModelIndex& index, int role) const @@ -599,8 +260,8 @@ QVariant AircraftItemModel::data(const QModelIndex& index, int role) const return m_delegateStates.at(row).variant; } - if (row >= m_items.size()) { - quint32 packageIndex = row - m_items.size(); + if (row >= m_cachedLocalAircraftCount) { + quint32 packageIndex = row - m_cachedLocalAircraftCount; const PackageRef& pkg(m_packages[packageIndex]); InstallRef ex = pkg->existingInstall(); @@ -614,7 +275,7 @@ QVariant AircraftItemModel::data(const QModelIndex& index, int role) const return dataFromPackage(pkg, m_delegateStates.at(row), role); } else { - const AircraftItemPtr item(m_items.at(row)); + const AircraftItemPtr item(LocalAircraftCache::instance()->itemAt(row)); return dataFromItem(item, m_delegateStates.at(row), role); } } @@ -694,7 +355,7 @@ QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, const DelegateSta { return 0; } else if (role == AircraftStatusRole) { - return itemAircraftStatus(item, state); + return item->status(0 /* variant is always 0 */); } else if (role == AircraftMinVersionRole) { return item->minFGVersion; } @@ -784,7 +445,7 @@ QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, const Delega } return ratings->getChild(ratingIndex)->getIntValue(); } else if (role == AircraftStatusRole) { - return packageAircraftStatus(item, state); + return QmlAircraftInfo::packageAircraftStatus(item); } else if (role == AircraftMinVersionRole) { const std::string v = item->properties()->getStringValue("minimum-fg-version"); if (!v.empty()) { @@ -860,37 +521,6 @@ QVariant AircraftItemModel::packagePreviews(PackageRef p, const DelegateState& d return result; } - -QVariant AircraftItemModel::itemAircraftStatus(AircraftItemPtr item, const DelegateState& ds) const -{ - if (item->needsMaintenance) { - return AircraftUnmaintained; - } - - if (item->minFGVersion.isEmpty()) { - return AircraftOk; - } - - const int c = simgear::strutils::compare_versions(FLIGHTGEAR_VERSION, - item->minFGVersion.toStdString(), 2); - return (c < 0) ? AircraftNeedsNewerSimulator : AircraftOk; -} - -QVariant AircraftItemModel::packageAircraftStatus(PackageRef p, const DelegateState& ds) const -{ - if (p->hasTag("needs-maintenance")) { - return AircraftUnmaintained; - } - - if (!p->properties()->hasChild("minimum-fg-version")) { - return AircraftOk; - } - - const std::string minFGVersion = p->properties()->getStringValue("minimum-fg-version"); - const int c = simgear::strutils::compare_versions(FLIGHTGEAR_VERSION, minFGVersion, 2); - return (c < 0) ? AircraftNeedsNewerSimulator : AircraftOk; -} - bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { int row = index.row(); @@ -946,23 +576,13 @@ QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const } if (uri.isLocalFile()) { - QString path = uri.toLocalFile(); - for (int row=0; row <m_items.size(); ++row) { - const AircraftItemPtr item(m_items.at(row)); - if (item->path == path) { - return index(row); - } - - // check variants too - for (int vr=0; vr < item->variants.size(); ++vr) { - if (item->variants.at(vr)->path == path) { - return index(row); - } - } + int row = LocalAircraftCache::instance()->findIndexWithUri(uri); + if (row >= 0) { + return index(row); } } else if (uri.scheme() == "package") { QString ident = uri.path(); - int rowOffset = m_items.size(); + int rowOffset = m_cachedLocalAircraftCount; PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString()); if (pkg) { @@ -991,27 +611,25 @@ void AircraftItemModel::selectVariantForAircraftURI(QUrl uri) QModelIndex modelIndex; if (uri.isLocalFile()) { - QString path = uri.toLocalFile(); - for (int row=0; row <m_items.size(); ++row) { - const AircraftItemPtr item(m_items.at(row)); - if (item->path == path) { - modelIndex = index(row); - variantIndex = 0; - break; - } + int row = LocalAircraftCache::instance()->findIndexWithUri(uri); + if (row < 0) { + return; + } - // check variants too - for (int vr=0; vr < item->variants.size(); ++vr) { - if (item->variants.at(vr)->path == path) { - modelIndex = index(row); - variantIndex = vr + 1; - break; - } + modelIndex = index(row); + // now check if we are actually selecting a variant + const AircraftItemPtr item = LocalAircraftCache::instance()->itemAt(row); + + const QString path = uri.toLocalFile(); + for (int vr=0; vr < item->variants.size(); ++vr) { + if (item->variants.at(vr)->path == path) { + variantIndex = vr + 1; + break; } } } else if (uri.scheme() == "package") { QString ident = uri.path(); - int rowOffset = m_items.size(); + int rowOffset = m_cachedLocalAircraftCount; PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString()); if (pkg) { @@ -1036,18 +654,16 @@ void AircraftItemModel::selectVariantForAircraftURI(QUrl uri) QString AircraftItemModel::nameForAircraftURI(QUrl uri) const { if (uri.isLocalFile()) { - QString path = uri.toLocalFile(); - for (int row=0; row <m_items.size(); ++row) { - const AircraftItemPtr item(m_items.at(row)); - if (item->path == path) { - return item->description; - } + AircraftItemPtr item = LocalAircraftCache::instance()->findItemWithUri(uri); + const QString path = uri.toLocalFile(); + if (item->path == path) { + return item->description; + } - // check variants too - for (int vr=0; vr < item->variants.size(); ++vr) { - if (item->variants.at(vr)->path == path) { - return item->description; - } + // check variants too + for (int vr=0; vr < item->variants.size(); ++vr) { + if (item->variants.at(vr)->path == path) { + return item->description; } } } else if (uri.scheme() == "package") { @@ -1065,32 +681,25 @@ QString AircraftItemModel::nameForAircraftURI(QUrl uri) const return QString(); } -void AircraftItemModel::onScanResults() +void AircraftItemModel::onScanAddedItems(int count) { - QVector<AircraftItemPtr> newItems = m_scanThread->items(); + QVector<AircraftItemPtr> newItems = LocalAircraftCache::instance()->newestItems(count); if (newItems.isEmpty()) return; - int firstRow = m_items.count(); - int lastRow = firstRow + newItems.count() - 1; + int firstRow = m_cachedLocalAircraftCount; + int lastRow = firstRow + count - 1; beginInsertRows(QModelIndex(), firstRow, lastRow); - m_items+=newItems; + m_cachedLocalAircraftCount += count; // default variants in all cases - for (int i=0; i< newItems.count(); ++i) { - m_delegateStates.insert(firstRow + i, DelegateState()); + for (int i=0; i< count; ++i) { + m_delegateStates.insert(firstRow + i, {}); } endInsertRows(); } -void AircraftItemModel::onScanFinished() -{ - delete m_scanThread; - m_scanThread = NULL; - emit scanCompleted(); -} - void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason) { QString msg; @@ -1120,11 +729,11 @@ void AircraftItemModel::installSucceeded(QModelIndex index) bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const { - if (index.row() < m_items.size()) { + if (index.row() < m_cachedLocalAircraftCount) { return true; // local file, always runnable } - quint32 packageIndex = index.row() - m_items.size(); + quint32 packageIndex = index.row() - m_cachedLocalAircraftCount; const PackageRef& pkg(m_packages[packageIndex]); InstallRef ex = pkg->existingInstall(); if (!ex.valid()) { @@ -1134,29 +743,6 @@ bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const return !ex->isDownloading(); } -bool AircraftItemModel::isCandidateAircraftPath(QString path) -{ - QStringList filters; - filters << "*-set.xml"; - int dirCount = 0, - setXmlCount = 0; - - QDir d(path); - Q_FOREACH(QFileInfo child, d.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { - QDir childDir(child.absoluteFilePath()); - ++dirCount; - Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) { - ++setXmlCount; - } - - if ((setXmlCount > 0) || (dirCount > 10)) { - break; - } - } - - return (setXmlCount > 0); -} - int AircraftItemModel::aircraftNeedingUpdated() const { return m_cachedUpdateCount; @@ -1176,4 +762,3 @@ void AircraftItemModel::setShowUpdateAll(bool showUpdateAll) emit aircraftNeedingUpdatedChanged(); } -#include "AircraftModel.moc" diff --git a/src/GUI/AircraftModel.hxx b/src/GUI/AircraftModel.hxx index d5fd9c8e8..a844256d7 100644 --- a/src/GUI/AircraftModel.hxx +++ b/src/GUI/AircraftModel.hxx @@ -22,13 +22,13 @@ #define FG_GUI_AIRCRAFT_MODEL #include <QAbstractListModel> -#include <QDateTime> #include <QDir> #include <QPixmap> #include <QStringList> -#include <QSharedPointer> #include <QUrl> +#include "LocalAircraftCache.hxx" + #include <simgear/package/Delegate.hxx> #include <simgear/package/Root.hxx> #include <simgear/package/Catalog.hxx> @@ -62,49 +62,10 @@ const int AircraftHasPreviewsRole = Qt::UserRole + 24; const int AircraftRatingRole = Qt::UserRole + 100; const int AircraftVariantDescriptionRole = Qt::UserRole + 200; -class AircraftScanThread; -class QDataStream; class PackageDelegate; -struct AircraftItem; -typedef QSharedPointer<AircraftItem> AircraftItemPtr; Q_DECLARE_METATYPE(simgear::pkg::PackageRef) -struct AircraftItem -{ - AircraftItem(); - - AircraftItem(QDir dir, QString filePath); - - // the file-name without -set.xml suffix - QString baseName() const; - - void fromDataStream(QDataStream& ds); - - void toDataStream(QDataStream& ds) const; - - QPixmap thumbnail(bool loadIfRequired = true) const; - - bool excluded = false; - QString path; - QString description; - QString longDescription; - QString authors; - int ratings[4] = {0, 0, 0, 0}; - QString variantOf; - QDateTime pathModTime; - QList<AircraftItemPtr> variants; - bool usesHeliports = false; - bool usesSeaports = false; - QList<QUrl> previews; - bool isPrimary = false; - QString thumbnailPath; - QString minFGVersion; - bool needsMaintenance = false; -private: - mutable QPixmap m_thumbnail; -}; - class AircraftItemModel : public QAbstractListModel { Q_OBJECT @@ -129,10 +90,6 @@ public: void setPackageRoot(const simgear::pkg::RootRef& root); - void setPaths(QStringList paths); - - void scanDirs(); - int rowCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; @@ -166,21 +123,6 @@ public: */ QString nameForAircraftURI(QUrl uri) const; - /** - * @helper to determine if a particular path is likely to contain - * aircraft or not. Checks for -set.xml files one level down in the tree. - * - */ - static bool isCandidateAircraftPath(QString path); - - enum AircraftStatus - { - AircraftOk = 0, - AircraftUnmaintained, - AircraftNeedsNewerSimulator, - AircraftNeedsOlderSimulator // won't ever occur for the moment - }; - int aircraftNeedingUpdated() const; bool showUpdateAll() const; @@ -190,17 +132,14 @@ signals: void aircraftInstallCompleted(QModelIndex index); - void scanCompleted(); - void aircraftNeedingUpdatedChanged(); public slots: void setShowUpdateAll(bool showUpdateAll); private slots: - void onScanResults(); - - void onScanFinished(); + void onScanStarted(); + void onScanAddedItems(int count); private: friend class PackageDelegate; @@ -212,11 +151,9 @@ private: struct DelegateState { quint32 variant = 0; - quint32 thumbnail = 0; }; QVariant dataFromItem(AircraftItemPtr item, const DelegateState& state, int role) const; - QVariant itemAircraftStatus(AircraftItemPtr item, const DelegateState& ds) const; QVariant dataFromPackage(const simgear::pkg::PackageRef& item, const DelegateState& state, int role) const; @@ -225,17 +162,12 @@ private: const DelegateState& state, bool download = true) const; QVariant packagePreviews(simgear::pkg::PackageRef p, const DelegateState &ds) const; - QVariant packageAircraftStatus(simgear::pkg::PackageRef p, const DelegateState &ds) const; - void abandonCurrentScan(); void refreshPackages(); void installSucceeded(QModelIndex index); void installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason); - QStringList m_paths; - AircraftScanThread* m_scanThread = nullptr; - QVector<AircraftItemPtr> m_items; PackageDelegate* m_delegate = nullptr; QVector<DelegateState> m_delegateStates; @@ -245,6 +177,7 @@ private: mutable QHash<QString, QPixmap> m_downloadedPixmapCache; int m_cachedUpdateCount = 0; + int m_cachedLocalAircraftCount = 0; bool m_showUpdateAll = true; }; diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index b4ce97216..9201136cd 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -169,6 +169,11 @@ if (HAVE_QT) FGQmlInstance.hxx FGQmlPropertyNode.cxx FGQmlPropertyNode.hxx + QmlAircraftInfo.cxx + QmlAircraftInfo.hxx + LocalAircraftCache.cxx + LocalAircraftCache.hxx + ) set_property(TARGET fgqmlui PROPERTY AUTOMOC ON) diff --git a/src/GUI/LauncherMainWindow.cxx b/src/GUI/LauncherMainWindow.cxx index ea126f7db..0ad08d1e4 100644 --- a/src/GUI/LauncherMainWindow.cxx +++ b/src/GUI/LauncherMainWindow.cxx @@ -42,6 +42,7 @@ #include "MPServersModel.h" #include "ThumbnailImageItem.hxx" #include "FlickableExtentQuery.hxx" +#include "LocalAircraftCache.hxx" #include "ui_Launcher.h" @@ -173,7 +174,10 @@ LauncherMainWindow::LauncherMainWindow() : this, &LauncherMainWindow::onAircraftInstalledCompleted); connect(m_aircraftModel, &AircraftItemModel::aircraftInstallFailed, this, &LauncherMainWindow::onAircraftInstallFailed); - connect(m_aircraftModel, &AircraftItemModel::scanCompleted, + + + connect(LocalAircraftCache::instance(), + &LocalAircraftCache::scanCompleted, this, &LauncherMainWindow::updateSelectedAircraft); AddOnsPage* addOnsPage = new AddOnsPage(NULL, globals->packageRoot()); @@ -191,9 +195,9 @@ LauncherMainWindow::LauncherMainWindow() : this, &LauncherMainWindow::delayedAircraftModelReset); QSettings settings; - m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList()); + LocalAircraftCache::instance()->setPaths(settings.value("aircraft-paths").toStringList()); + LocalAircraftCache::instance()->scanDirs(); m_aircraftModel->setPackageRoot(globals->packageRoot()); - m_aircraftModel->scanDirs(); buildSettingsSections(); buildEnvironmentSections(); @@ -864,8 +868,10 @@ void LauncherMainWindow::downloadDirChanged(QString path) QSettings settings; // re-scan the aircraft list m_aircraftModel->setPackageRoot(globals->packageRoot()); - m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList()); - m_aircraftModel->scanDirs(); + + auto aircraftCache = LocalAircraftCache::instance(); + aircraftCache->setPaths(settings.value("aircraft-paths").toStringList()); + aircraftCache->scanDirs(); emit showNoOfficialHangarChanged(); @@ -909,8 +915,9 @@ simgear::pkg::PackageRef LauncherMainWindow::packageForAircraftURI(QUrl uri) con void LauncherMainWindow::onAircraftPathsChanged() { QSettings settings; - m_aircraftModel->setPaths(settings.value("aircraft-paths").toStringList()); - m_aircraftModel->scanDirs(); + auto aircraftCache = LocalAircraftCache::instance(); + aircraftCache->setPaths(settings.value("aircraft-paths").toStringList()); + aircraftCache->scanDirs(); } void LauncherMainWindow::onChangeDataDir() diff --git a/src/GUI/LocalAircraftCache.cxx b/src/GUI/LocalAircraftCache.cxx new file mode 100644 index 000000000..8ba653ec2 --- /dev/null +++ b/src/GUI/LocalAircraftCache.cxx @@ -0,0 +1,519 @@ +// Written by James Turner, started October 2017 +// +// Copyright (C) 2017 James Turner <zakalawe@mac.com> +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include "config.h" + +#include "LocalAircraftCache.hxx" + +#include <QDir> +#include <QThread> +#include <QMutex> +#include <QMutexLocker> +#include <QDataStream> +#include <QMap> +#include <QSettings> +#include <QDebug> + +#include <Main/globals.hxx> +#include <Include/version.h> + +#include <simgear/props/props_io.hxx> +#include <simgear/structure/exception.hxx> + +static quint32 CACHE_VERSION = 8; + +const int STANDARD_THUMBNAIL_HEIGHT = 128; +const int STANDARD_THUMBNAIL_WIDTH = 172; + +AircraftItem::AircraftItem() +{ +} + +AircraftItem::AircraftItem(QDir dir, QString filePath) +{ + SGPropertyNode root; + readProperties(filePath.toStdString(), &root); + + if (!root.hasChild("sim")) { + throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString()); + } + + SGPropertyNode_ptr sim = root.getNode("sim"); + + path = filePath; + pathModTime = QFileInfo(path).lastModified(); + if (sim->getBoolValue("exclude-from-gui", false)) { + excluded = true; + return; + } + + description = sim->getStringValue("description"); + authors = sim->getStringValue("author"); + + if (sim->hasChild("rating")) { + SGPropertyNode_ptr ratingsNode = sim->getNode("rating"); + ratings[0] = ratingsNode->getIntValue("FDM"); + ratings[1] = ratingsNode->getIntValue("systems"); + ratings[2] = ratingsNode->getIntValue("cockpit"); + ratings[3] = ratingsNode->getIntValue("model"); + + } + + if (sim->hasChild("long-description")) { + // clean up any XML whitspace in the text. + longDescription = QString(sim->getStringValue("long-description")).simplified(); + } + + if (sim->hasChild("variant-of")) { + variantOf = sim->getStringValue("variant-of"); + } else { + isPrimary = true; + } + + if (sim->hasChild("primary-set")) { + isPrimary = sim->getBoolValue("primary-set"); + } + + if (sim->hasChild("tags")) { + SGPropertyNode_ptr tagsNode = sim->getChild("tags"); + int nChildren = tagsNode->nChildren(); + for (int i = 0; i < nChildren; i++) { + const SGPropertyNode* c = tagsNode->getChild(i); + if (strcmp(c->getName(), "tag") == 0) { + const char* tagName = c->getStringValue(); + usesHeliports |= (strcmp(tagName, "helicopter") == 0); + // could also consider vtol tag? + usesSeaports |= (strcmp(tagName, "seaplane") == 0); + usesSeaports |= (strcmp(tagName, "floats") == 0); + + needsMaintenance |= (strcmp(tagName, "needs-maintenance") == 0); + } + } // of tags iteration + } // of set-xml has tags + + if (sim->hasChild("previews")) { + SGPropertyNode_ptr previewsNode = sim->getChild("previews"); + for (auto previewNode : previewsNode->getChildren("preview")) { + // add file path as url + QString pathInXml = QString::fromStdString(previewNode->getStringValue("path")); + QString previewPath = dir.absoluteFilePath(pathInXml); + previews.append(QUrl::fromLocalFile(previewPath)); + } + } + + if (sim->hasChild("thumbnail")) { + thumbnailPath = sim->getStringValue("thumbnail"); + } else { + thumbnailPath = "thumbnail.jpg"; + } + + if (sim->hasChild("minimum-fg-version")) { + minFGVersion = sim->getStringValue("minimum-fg-version"); + } +} + +QString AircraftItem::baseName() const +{ + QString fn = QFileInfo(path).fileName(); + fn.truncate(fn.count() - 8); + return fn; +} + +void AircraftItem::fromDataStream(QDataStream& ds) +{ + ds >> path >> pathModTime >> excluded; + if (excluded) { + return; + } + + ds >> description >> longDescription >> authors >> variantOf >> isPrimary; + for (int i=0; i<4; ++i) ds >> ratings[i]; + ds >> previews; + ds >> thumbnailPath; + ds >> minFGVersion; + ds >> needsMaintenance >> usesHeliports >> usesSeaports; +} + +void AircraftItem::toDataStream(QDataStream& ds) const +{ + ds << path << pathModTime << excluded; + if (excluded) { + return; + } + + ds << description << longDescription << authors << variantOf << isPrimary; + for (int i=0; i<4; ++i) ds << ratings[i]; + ds << previews; + ds << thumbnailPath; + ds << minFGVersion; + ds << needsMaintenance << usesHeliports << usesSeaports; +} + +QPixmap AircraftItem::thumbnail(bool loadIfRequired) const +{ + if (m_thumbnail.isNull() && loadIfRequired) { + QFileInfo info(path); + QDir dir = info.dir(); + if (dir.exists(thumbnailPath)) { + m_thumbnail.load(dir.filePath(thumbnailPath)); + // resize to the standard size + if (m_thumbnail.height() > STANDARD_THUMBNAIL_HEIGHT) { + m_thumbnail = m_thumbnail.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT, Qt::SmoothTransformation); + } + } + } + + return m_thumbnail; +} + +QVariant AircraftItem::status(int variant) +{ + if (needsMaintenance) { + return LocalAircraftCache::AircraftUnmaintained; + } + + if (minFGVersion.isEmpty()) { + return LocalAircraftCache::AircraftOk; + } + + const int c = simgear::strutils::compare_versions(FLIGHTGEAR_VERSION, + minFGVersion.toStdString(), 2); + return (c < 0) ? LocalAircraftCache::AircraftNeedsNewerSimulator + : LocalAircraftCache::AircraftOk; + +} + +class AircraftScanThread : public QThread +{ + Q_OBJECT +public: + AircraftScanThread(QStringList dirsToScan) : + m_dirs(dirsToScan), + m_done(false) + { + } + + ~AircraftScanThread() + { + } + + /** thread-safe access to items already scanned */ + QVector<AircraftItemPtr> items() + { + QVector<AircraftItemPtr> result; + QMutexLocker g(&m_lock); + result.swap(m_items); + g.unlock(); + return result; + } + + void setDone() + { + m_done = true; + } +Q_SIGNALS: + void addedItems(); + +protected: + virtual void run() + { + readCache(); + + Q_FOREACH(QString d, m_dirs) { + scanAircraftDir(QDir(d)); + if (m_done) { + return; + } + } + + writeCache(); + } + +private: + void readCache() + { + QSettings settings; + QByteArray cacheData = settings.value("aircraft-cache").toByteArray(); + if (!cacheData.isEmpty()) { + QDataStream ds(cacheData); + quint32 count, cacheVersion; + ds >> cacheVersion >> count; + + if (cacheVersion != CACHE_VERSION) { + return; // mis-matched cache, version, drop + } + + for (quint32 i=0; i<count; ++i) { + AircraftItemPtr item(new AircraftItem); + item->fromDataStream(ds); + + QFileInfo finfo(item->path); + if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) { + // corresponding -set.xml file still exists and is + // unmodified + m_cachedItems[item->path] = item; + } + } // of cached item iteration + } + } + + void writeCache() + { + QSettings settings; + QByteArray cacheData; + { + QDataStream ds(&cacheData, QIODevice::WriteOnly); + quint32 count = m_nextCache.count(); + ds << CACHE_VERSION << count; + + Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) { + item->toDataStream(ds); + } + } + + settings.setValue("aircraft-cache", cacheData); + } + + void scanAircraftDir(QDir path) + { + QTime t; + t.start(); + + QStringList filters; + filters << "*-set.xml"; + Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { + QDir childDir(child.absoluteFilePath()); + QMap<QString, AircraftItemPtr> baseAircraft; + QList<AircraftItemPtr> variants; + + Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) { + try { + QString absolutePath = xmlChild.absoluteFilePath(); + AircraftItemPtr item; + + if (m_cachedItems.contains(absolutePath)) { + item = m_cachedItems.value(absolutePath); + } else { + item = AircraftItemPtr(new AircraftItem(childDir, absolutePath)); + } + + m_nextCache[absolutePath] = item; + + if (item->excluded) { + continue; + } + + if (item->isPrimary) { + baseAircraft.insert(item->baseName(), item); + } else { + variants.append(item); + } + } catch (sg_exception& e) { + continue; + } + + if (m_done) { + return; + } + } // of set.xml iteration + + // bind variants to their principals + Q_FOREACH(AircraftItemPtr item, variants) { + if (!baseAircraft.contains(item->variantOf)) { + qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path; + continue; + } + + baseAircraft.value(item->variantOf)->variants.append(item); + } + + // lock mutex while we modify the items array + { + QMutexLocker g(&m_lock); + m_items+=(baseAircraft.values().toVector()); + } + + emit addedItems(); + } // of subdir iteration + } + + QMutex m_lock; + QStringList m_dirs; + QVector<AircraftItemPtr> m_items; + + QMap<QString, AircraftItemPtr > m_cachedItems; + QMap<QString, AircraftItemPtr > m_nextCache; + + bool m_done; +}; + +std::unique_ptr<LocalAircraftCache> static_cacheInstance; + +LocalAircraftCache* LocalAircraftCache::instance() +{ + if (!static_cacheInstance) { + static_cacheInstance.reset(new LocalAircraftCache); + } + + return static_cacheInstance.get(); +} + +LocalAircraftCache::LocalAircraftCache() +{ + +} + +LocalAircraftCache::~LocalAircraftCache() +{ + abandonCurrentScan(); + +} + +void LocalAircraftCache::setPaths(QStringList paths) +{ + m_paths = paths; +} + +void LocalAircraftCache::scanDirs() +{ + abandonCurrentScan(); + + QStringList dirs = m_paths; + + Q_FOREACH(SGPath ap, globals->get_aircraft_paths()) { + dirs << QString::fromStdString(ap.utf8Str()); + } + + SGPath rootAircraft(globals->get_fg_root()); + rootAircraft.append("Aircraft"); + dirs << QString::fromStdString(rootAircraft.utf8Str()); + + m_scanThread = new AircraftScanThread(dirs); + connect(m_scanThread, &AircraftScanThread::finished, this, + &LocalAircraftCache::onScanFinished); + connect(m_scanThread, &AircraftScanThread::addedItems, + this, &LocalAircraftCache::onScanResults); + m_scanThread->start(); + + emit scanStarted(); +} + +int LocalAircraftCache::itemCount() const +{ + return m_items.size(); +} + +AircraftItemPtr LocalAircraftCache::itemAt(int index) const +{ + return m_items.at(index); +} + +int LocalAircraftCache::findIndexWithUri(QUrl aircraftUri) const +{ + QString path = aircraftUri.toLocalFile(); + for (int row=0; row < m_items.size(); ++row) { + const AircraftItemPtr item(m_items.at(row)); + if (item->path == path) { + return row; + } + + // check variants too + for (int vr=0; vr < item->variants.size(); ++vr) { + if (item->variants.at(vr)->path == path) { + return row; + } + } + } + + return -1; +} + +QVector<AircraftItemPtr> LocalAircraftCache::newestItems(int count) +{ + QVector<AircraftItemPtr> r; + r.reserve(count); + int total = m_items.size(); + for (int i = total - count; i < count; ++i) { + r.push_back(m_items.at(i)); + } + return r; +} + +AircraftItemPtr LocalAircraftCache::findItemWithUri(QUrl aircraftUri) const +{ + int index = findIndexWithUri(aircraftUri); + if (index >= 0) { + return m_items.at(index); + } + + return {}; +} + +void LocalAircraftCache::abandonCurrentScan() +{ + if (m_scanThread) { + m_scanThread->setDone(); + m_scanThread->wait(1000); + delete m_scanThread; + m_scanThread = NULL; + } +} + + +void LocalAircraftCache::onScanResults() +{ + QVector<AircraftItemPtr> newItems = m_scanThread->items(); + if (newItems.isEmpty()) + return; + + + m_items+=newItems; + emit addedItems(newItems.size()); +} + +void LocalAircraftCache::onScanFinished() +{ + delete m_scanThread; + m_scanThread = nullptr; + emit scanCompleted(); +} + +bool LocalAircraftCache::isCandidateAircraftPath(QString path) +{ + QStringList filters; + filters << "*-set.xml"; + int dirCount = 0, + setXmlCount = 0; + + QDir d(path); + Q_FOREACH(QFileInfo child, d.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { + QDir childDir(child.absoluteFilePath()); + ++dirCount; + Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) { + ++setXmlCount; + } + + if ((setXmlCount > 0) || (dirCount > 10)) { + break; + } + } + + return (setXmlCount > 0); +} + +#include "LocalAircraftCache.moc" diff --git a/src/GUI/LocalAircraftCache.hxx b/src/GUI/LocalAircraftCache.hxx new file mode 100644 index 000000000..594f30743 --- /dev/null +++ b/src/GUI/LocalAircraftCache.hxx @@ -0,0 +1,138 @@ +// Written by James Turner, started October 2017 + +// +// Copyright (C) 2017 James Turner <zakalawe@mac.com> +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef LOCALAIRCRAFTCACHE_HXX +#define LOCALAIRCRAFTCACHE_HXX + +#include <QObject> +#include <QPixmap> +#include <QDateTime> +#include <QUrl> +#include <QSharedPointer> +#include <QDir> +#include <QVariant> + +class QDataStream; +struct AircraftItem; +class AircraftScanThread; + +typedef QSharedPointer<AircraftItem> AircraftItemPtr; + +struct AircraftItem +{ + AircraftItem(); + + AircraftItem(QDir dir, QString filePath); + + // the file-name without -set.xml suffix + QString baseName() const; + + void fromDataStream(QDataStream& ds); + + void toDataStream(QDataStream& ds) const; + + QPixmap thumbnail(bool loadIfRequired = true) const; + + bool excluded = false; + QString path; + QString description; + QString longDescription; + QString authors; + int ratings[4] = {0, 0, 0, 0}; + QString variantOf; + QDateTime pathModTime; + QList<AircraftItemPtr> variants; + bool usesHeliports = false; + bool usesSeaports = false; + QList<QUrl> previews; + bool isPrimary = false; + QString thumbnailPath; + QString minFGVersion; + bool needsMaintenance = false; + + QVariant status(int variant); +private: + mutable QPixmap m_thumbnail; +}; + +class LocalAircraftCache : public QObject +{ + Q_OBJECT +public: + ~LocalAircraftCache(); + + static LocalAircraftCache* instance(); + + + void setPaths(QStringList paths); + + void scanDirs(); + + + /** + * @helper to determine if a particular path is likely to contain + * aircraft or not. Checks for -set.xml files one level down in the tree. + * + */ + static bool isCandidateAircraftPath(QString path); + + int itemCount() const; + + AircraftItemPtr itemAt(int index) const; + + AircraftItemPtr findItemWithUri(QUrl aircraftUri) const; + int findIndexWithUri(QUrl aircraftUri) const; + + QVector<AircraftItemPtr> newestItems(int count); + + QVariant aircraftStatus(AircraftItemPtr item) const; + + enum AircraftStatus + { + AircraftOk = 0, + AircraftUnmaintained, + AircraftNeedsNewerSimulator, + AircraftNeedsOlderSimulator // won't ever occur for the moment + }; + +signals: + + void scanStarted(); + void scanCompleted(); + + void addedItems(int count); +public slots: + +private slots: + void onScanResults(); + + void onScanFinished(); + +private: + explicit LocalAircraftCache(); + + void abandonCurrentScan(); + + QStringList m_paths; + AircraftScanThread* m_scanThread = nullptr; + QVector<AircraftItemPtr> m_items; + +}; + +#endif // LOCALAIRCRAFTCACHE_HXX diff --git a/src/GUI/PathsDialog.cxx b/src/GUI/PathsDialog.cxx index 4836106b2..d19879ebc 100644 --- a/src/GUI/PathsDialog.cxx +++ b/src/GUI/PathsDialog.cxx @@ -12,6 +12,7 @@ #include "AircraftModel.hxx" #include "InstallSceneryDialog.hxx" #include "QtLauncher.hxx" +#include "LocalAircraftCache.hxx" #include <Main/options.hxx> #include <Main/globals.hxx> @@ -137,7 +138,7 @@ void AddOnsPage::onAddAircraftPath() // to check for that case and handle it gracefully. bool pathOk = false; - if (AircraftItemModel::isCandidateAircraftPath(path)) { + if (LocalAircraftCache::isCandidateAircraftPath(path)) { m_ui->aircraftPathsList->addItem(path); pathOk = true; } else { @@ -145,7 +146,7 @@ void AddOnsPage::onAddAircraftPath() QDir d(path); if (d.exists("Aircraft")) { QString p2 = d.filePath("Aircraft"); - if (AircraftItemModel::isCandidateAircraftPath(p2)) { + if (LocalAircraftCache::isCandidateAircraftPath(p2)) { m_ui->aircraftPathsList->addItem(p2); pathOk = true; } diff --git a/src/GUI/PropertyItemModel.cxx b/src/GUI/PropertyItemModel.cxx index 354427c38..ef2e520c8 100644 --- a/src/GUI/PropertyItemModel.cxx +++ b/src/GUI/PropertyItemModel.cxx @@ -25,7 +25,7 @@ public: public slots: void setPath(QString path) { - if (_propertyPath == path.toStdString()); + if (_propertyPath == path.toStdString()) return; _propertyPath = path.toStdString(); diff --git a/src/GUI/QmlAircraftInfo.cxx b/src/GUI/QmlAircraftInfo.cxx new file mode 100644 index 000000000..c9f0781e4 --- /dev/null +++ b/src/GUI/QmlAircraftInfo.cxx @@ -0,0 +1,224 @@ +#include "QmlAircraftInfo.hxx" + +#include <QVariant> +#include <QDebug> + +#include <simgear/package/Install.hxx> + +#include <Include/version.h> + +#include "LocalAircraftCache.hxx" + +QmlAircraftInfo::QmlAircraftInfo(QObject *parent) : QObject(parent) +{ + +} + +QmlAircraftInfo::~QmlAircraftInfo() +{ + +} + +int QmlAircraftInfo::numPreviews() const +{ + return 0; +} + +int QmlAircraftInfo::numVariants() const +{ + if (_item) { + return _item->variants.size(); + } else if (_package) { + return _package->variants().size(); + } + + return 0; +} + +QString QmlAircraftInfo::name() const +{ + if (_item) { + return resolveItem()->description; + } else if (_package) { + return QString::fromStdString(_package->nameForVariant(_variant)); + } + + return {}; +} + +QString QmlAircraftInfo::description() const +{ + if (_item) { + return resolveItem()->longDescription; + } else if (_package) { + std::string longDesc = _package->getLocalisedProp("description", _variant); + return QString::fromStdString(longDesc).simplified(); + } + + return {}; +} + +QString QmlAircraftInfo::authors() const +{ + if (_item) { + return resolveItem()->authors; + } else if (_package) { + std::string authors = _package->getLocalisedProp("author", _variant); + return QString::fromStdString(authors); + } + + return {}; +} + +QVariantList QmlAircraftInfo::ratings() const +{ + if (_item) { + QVariantList result; + auto actualItem = resolveItem(); + for (int i=0; i<4; ++i) { + result << actualItem->ratings[i]; + } + return result; + } else if (_package) { + SGPropertyNode* ratings = _package->properties()->getChild("rating"); + if (!ratings) { + return {}; + } + + QVariantList result; + for (int i=0; i<4; ++i) { + result << ratings->getChild(i)->getIntValue(); + } + return result; + } + return {}; +} + +QUrl QmlAircraftInfo::thumbnail() const +{ + if (_item) { + return QUrl::fromLocalFile(resolveItem()->thumbnailPath); + } else if (_package) { + auto t = _package->thumbnailForVariant(_variant); + if (QFileInfo::exists(QString::fromStdString(t.path))) { + return QUrl::fromLocalFile(QString::fromStdString(t.path)); + } + return QUrl(QString::fromStdString(t.url)); + } + + return {}; +} + +QString QmlAircraftInfo::pathOnDisk() const +{ + if (_item) { + return resolveItem()->path; + } else if (_package) { + auto install = _package->existingInstall(); + if (install.valid()) { + return QString::fromStdString(install->primarySetPath().utf8Str()); + } + } + + return {}; +} + +QString QmlAircraftInfo::packageId() const +{ + if (_package) { + return QString::fromStdString(_package->variants()[_variant]); + } + + return {}; +} + +int QmlAircraftInfo::packageSize() const +{ + if (_package) { + return _package->fileSizeBytes(); + } + + return 0; +} + +int QmlAircraftInfo::downloadedBytes() const +{ + return 0; +} + +QVariant QmlAircraftInfo::status() const +{ + if (_item) { + return _item->status(_variant); + } else if (_package) { + return packageAircraftStatus(_package); + } + + return {}; +} + +QString QmlAircraftInfo::minimumFGVersion() const +{ + if (_item) { + return resolveItem()->minFGVersion; + } else if (_package) { + const std::string v = _package->properties()->getStringValue("minimum-fg-version"); + if (!v.empty()) { + return QString::fromStdString(v); + } + } + + return {}; +} + +AircraftItemPtr QmlAircraftInfo::resolveItem() const +{ + if (_variant > 0) { + return _item->variants.at(_variant - 1); + } + + return _item; +} + +void QmlAircraftInfo::setUri(QUrl uri) +{ + if (_uri == uri) + return; + + _uri = uri; + + + emit uriChanged(); + emit infoChanged(); +} + +void QmlAircraftInfo::requestInstallUpdate() +{ + +} + +void QmlAircraftInfo::requestUninstall() +{ + +} + +void QmlAircraftInfo::requestInstallCancel() +{ + +} + +QVariant QmlAircraftInfo::packageAircraftStatus(simgear::pkg::PackageRef p) +{ + if (p->hasTag("needs-maintenance")) { + return LocalAircraftCache::AircraftUnmaintained; + } + + if (!p->properties()->hasChild("minimum-fg-version")) { + return LocalAircraftCache::AircraftOk; + } + + const std::string minFGVersion = p->properties()->getStringValue("minimum-fg-version"); + const int c = simgear::strutils::compare_versions(FLIGHTGEAR_VERSION, minFGVersion, 2); + return (c < 0) ? LocalAircraftCache::AircraftNeedsNewerSimulator : + LocalAircraftCache::AircraftOk; +} diff --git a/src/GUI/QmlAircraftInfo.hxx b/src/GUI/QmlAircraftInfo.hxx new file mode 100644 index 000000000..8e5db27c7 --- /dev/null +++ b/src/GUI/QmlAircraftInfo.hxx @@ -0,0 +1,94 @@ +#ifndef QMLAIRCRAFTINFO_HXX +#define QMLAIRCRAFTINFO_HXX + +#include <QObject> +#include <QUrl> +#include <QSharedPointer> + +#include <simgear/package/Catalog.hxx> +#include <simgear/package/Package.hxx> + +struct AircraftItem; +typedef QSharedPointer<AircraftItem> AircraftItemPtr; + +class QmlAircraftInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QUrl uri READ uri WRITE setUri NOTIFY uriChanged) + + Q_PROPERTY(int numPreviews READ numPreviews NOTIFY infoChanged) + Q_PROPERTY(int numVariants READ numVariants NOTIFY infoChanged) + + Q_PROPERTY(QString name READ name NOTIFY infoChanged) + Q_PROPERTY(QString description READ description NOTIFY infoChanged) + Q_PROPERTY(QString authors READ authors NOTIFY infoChanged) + + Q_PROPERTY(QUrl thumbnail READ thumbnail NOTIFY infoChanged) + + Q_PROPERTY(QString pathOnDisk READ pathOnDisk NOTIFY infoChanged) + + Q_PROPERTY(QString packageId READ packageId NOTIFY infoChanged) + + Q_PROPERTY(int packageSize READ packageSize NOTIFY infoChanged) + + Q_PROPERTY(int downloadedBytes READ downloadedBytes NOTIFY downloadChanged) + + Q_PROPERTY(QVariant status READ status NOTIFY infoChanged) + + Q_PROPERTY(QString minimumFGVersion READ minimumFGVersion NOTIFY infoChanged) + + Q_INVOKABLE void requestInstallUpdate(); + + Q_INVOKABLE void requestUninstall(); + + Q_INVOKABLE void requestInstallCancel(); + + Q_PROPERTY(QVariantList ratings READ ratings NOTIFY infoChanged) + +public: + explicit QmlAircraftInfo(QObject *parent = nullptr); + virtual ~QmlAircraftInfo(); + + QUrl uri() const + { + return _uri; + } + + int numPreviews() const; + int numVariants() const; + + QString name() const; + QString description() const; + QString authors() const; + QVariantList ratings() const; + + QUrl thumbnail() const; + QString pathOnDisk() const; + + QString packageId() const; + int packageSize() const; + int downloadedBytes() const; + + QVariant status() const; + QString minimumFGVersion() const; + + static QVariant packageAircraftStatus(simgear::pkg::PackageRef p); +signals: + void uriChanged(); + void infoChanged(); + void downloadChanged(); +public slots: + + void setUri(QUrl uri); + +private: + QUrl _uri; + simgear::pkg::PackageRef _package; + AircraftItemPtr _item; + int _variant = 0; + + AircraftItemPtr resolveItem() const; +}; + +#endif // QMLAIRCRAFTINFO_HXX diff --git a/src/GUI/resources.qrc b/src/GUI/resources.qrc index 779f37fb5..947faa318 100644 --- a/src/GUI/resources.qrc +++ b/src/GUI/resources.qrc @@ -35,6 +35,7 @@ <file>Button.qml</file> <file>AircraftWarningPanel.qml</file> <file>Scrollbar.qml</file> + <file>AircraftDetailsView.qml</file> </qresource> <qresource prefix="/preview"> <file alias="close-icon">preview-close.png</file>