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>