]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
GUI support for VIA/Discontinuity
[flightgear.git] / src / GUI / AircraftModel.cxx
1 // AircraftModel.cxx - part of GUI launcher using Qt5
2 //
3 // Written by James Turner, started March 2015.
4 //
5 // Copyright (C) 2015 James Turner <zakalawe@mac.com>
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
21 #include "AircraftModel.hxx"
22
23 #include <QDir>
24 #include <QThread>
25 #include <QMutex>
26 #include <QMutexLocker>
27 #include <QDataStream>
28 #include <QSettings>
29 #include <QDebug>
30 #include <QSharedPointer>
31
32 // Simgear
33 #include <simgear/props/props_io.hxx>
34 #include <simgear/structure/exception.hxx>
35 #include <simgear/misc/sg_path.hxx>
36 #include <simgear/package/Package.hxx>
37 #include <simgear/package/Catalog.hxx>
38 #include <simgear/package/Install.hxx>
39
40 // FlightGear
41 #include <Main/globals.hxx>
42
43
44 const int STANDARD_THUMBNAIL_HEIGHT = 128;
45 const int STANDARD_THUMBNAIL_WIDTH = 172;
46
47 using namespace simgear::pkg;
48
49 AircraftItem::AircraftItem() :
50     excluded(false),
51     usesHeliports(false),
52     usesSeaports(false)
53 {
54     // oh for C++11 initialisers
55     for (int i=0; i<4; ++i) ratings[i] = 0;
56 }
57
58 AircraftItem::AircraftItem(QDir dir, QString filePath) :
59     excluded(false),
60     usesHeliports(false),
61     usesSeaports(false)
62 {
63     for (int i=0; i<4; ++i) ratings[i] = 0;
64
65     SGPropertyNode root;
66     readProperties(filePath.toStdString(), &root);
67
68     if (!root.hasChild("sim")) {
69         throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
70     }
71
72     SGPropertyNode_ptr sim = root.getNode("sim");
73
74     path = filePath;
75     pathModTime = QFileInfo(path).lastModified();
76     if (sim->getBoolValue("exclude-from-gui", false)) {
77         excluded = true;
78         return;
79     }
80
81     description = sim->getStringValue("description");
82     authors =  sim->getStringValue("author");
83
84     if (sim->hasChild("rating")) {
85         SGPropertyNode_ptr ratingsNode = sim->getNode("rating");
86         ratings[0] = ratingsNode->getIntValue("FDM");
87         ratings[1] = ratingsNode->getIntValue("systems");
88         ratings[2] = ratingsNode->getIntValue("cockpit");
89         ratings[3] = ratingsNode->getIntValue("model");
90
91     }
92
93     if (sim->hasChild("variant-of")) {
94         variantOf = sim->getStringValue("variant-of");
95     }
96
97     if (sim->hasChild("tags")) {
98         SGPropertyNode_ptr tagsNode = sim->getChild("tags");
99         int nChildren = tagsNode->nChildren();
100         for (int i = 0; i < nChildren; i++) {
101             const SGPropertyNode* c = tagsNode->getChild(i);
102             if (strcmp(c->getName(), "tag") == 0) {
103                 const char* tagName = c->getStringValue();
104                 usesHeliports |= (strcmp(tagName, "helicopter") == 0);
105                 // could also consider vtol tag?
106                 usesSeaports |= (strcmp(tagName, "seaplane") == 0);
107                 usesSeaports |= (strcmp(tagName, "floats") == 0);
108             }
109         } // of tags iteration
110     } // of set-xml has tags
111 }
112
113 QString AircraftItem::baseName() const
114 {
115     QString fn = QFileInfo(path).fileName();
116     fn.truncate(fn.count() - 8);
117     return fn;
118 }
119
120 void AircraftItem::fromDataStream(QDataStream& ds)
121 {
122     ds >> path >> pathModTime >> excluded;
123     if (excluded) {
124         return;
125     }
126
127     ds >> description >> authors >> variantOf;
128     for (int i=0; i<4; ++i) ds >> ratings[i];
129 }
130
131 void AircraftItem::toDataStream(QDataStream& ds) const
132 {
133     ds << path << pathModTime << excluded;
134     if (excluded) {
135         return;
136     }
137
138     ds << description << authors << variantOf;
139     for (int i=0; i<4; ++i) ds << ratings[i];
140 }
141
142 QPixmap AircraftItem::thumbnail() const
143 {
144     if (m_thumbnail.isNull()) {
145         QFileInfo info(path);
146         QDir dir = info.dir();
147         if (dir.exists("thumbnail.jpg")) {
148             m_thumbnail.load(dir.filePath("thumbnail.jpg"));
149             // resize to the standard size
150             if (m_thumbnail.height() > STANDARD_THUMBNAIL_HEIGHT) {
151                 m_thumbnail = m_thumbnail.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
152             }
153         }
154     }
155
156     return m_thumbnail;
157 }
158
159
160 static quint32 CACHE_VERSION = 3;
161
162 class AircraftScanThread : public QThread
163 {
164     Q_OBJECT
165 public:
166     AircraftScanThread(QStringList dirsToScan) :
167         m_dirs(dirsToScan),
168         m_done(false)
169     {
170     }
171
172     ~AircraftScanThread()
173     {
174     }
175
176     /** thread-safe access to items already scanned */
177     QVector<AircraftItemPtr> items()
178     {
179         QVector<AircraftItemPtr> result;
180         QMutexLocker g(&m_lock);
181         result.swap(m_items);
182         g.unlock();
183         return result;
184     }
185
186     void setDone()
187     {
188         m_done = true;
189     }
190 Q_SIGNALS:
191     void addedItems();
192
193 protected:
194     virtual void run()
195     {
196         readCache();
197
198         Q_FOREACH(QString d, m_dirs) {
199             scanAircraftDir(QDir(d));
200             if (m_done) {
201                 return;
202             }
203         }
204
205         writeCache();
206     }
207
208 private:
209     void readCache()
210     {
211         QSettings settings;
212         QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
213         if (!cacheData.isEmpty()) {
214             QDataStream ds(cacheData);
215             quint32 count, cacheVersion;
216             ds >> cacheVersion >> count;
217
218             if (cacheVersion != CACHE_VERSION) {
219                 return; // mis-matched cache, version, drop
220             }
221
222              for (quint32 i=0; i<count; ++i) {
223                 AircraftItemPtr item(new AircraftItem);
224                 item->fromDataStream(ds);
225
226                 QFileInfo finfo(item->path);
227                 if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) {
228                     // corresponding -set.xml file still exists and is
229                     // unmodified
230                     m_cachedItems[item->path] = item;
231                 }
232             } // of cached item iteration
233         }
234     }
235
236     void writeCache()
237     {
238         QSettings settings;
239         QByteArray cacheData;
240         {
241             QDataStream ds(&cacheData, QIODevice::WriteOnly);
242             quint32 count = m_nextCache.count();
243             ds << CACHE_VERSION << count;
244
245             Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) {
246                 item->toDataStream(ds);
247             }
248         }
249
250         settings.setValue("aircraft-cache", cacheData);
251     }
252
253     void scanAircraftDir(QDir path)
254     {
255         QTime t;
256         t.start();
257
258         QStringList filters;
259         filters << "*-set.xml";
260         Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
261             QDir childDir(child.absoluteFilePath());
262             QMap<QString, AircraftItemPtr> baseAircraft;
263             QList<AircraftItemPtr> variants;
264
265             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
266                 try {
267                     QString absolutePath = xmlChild.absoluteFilePath();
268                     AircraftItemPtr item;
269
270                     if (m_cachedItems.contains(absolutePath)) {
271                         item = m_cachedItems.value(absolutePath);
272                     } else {
273                         item = AircraftItemPtr(new AircraftItem(childDir, absolutePath));
274                     }
275
276                     m_nextCache[absolutePath] = item;
277
278                     if (item->excluded) {
279                         continue;
280                     }
281
282                     if (item->variantOf.isNull()) {
283                         baseAircraft.insert(item->baseName(), item);
284                     } else {
285                         variants.append(item);
286                     }
287                 } catch (sg_exception& e) {
288                     continue;
289                 }
290
291                 if (m_done) {
292                     return;
293                 }
294             } // of set.xml iteration
295
296             // bind variants to their principals
297             Q_FOREACH(AircraftItemPtr item, variants) {
298                 if (!baseAircraft.contains(item->variantOf)) {
299                     qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
300                     continue;
301                 }
302
303                 baseAircraft.value(item->variantOf)->variants.append(item);
304             }
305
306             // lock mutex while we modify the items array
307             {
308                 QMutexLocker g(&m_lock);
309                 m_items+=(baseAircraft.values().toVector());
310             }
311
312             emit addedItems();
313         } // of subdir iteration
314     }
315
316     QMutex m_lock;
317     QStringList m_dirs;
318     QVector<AircraftItemPtr> m_items;
319
320     QMap<QString, AircraftItemPtr > m_cachedItems;
321     QMap<QString, AircraftItemPtr > m_nextCache;
322
323     bool m_done;
324 };
325
326 class PackageDelegate : public simgear::pkg::Delegate
327 {
328 public:
329     PackageDelegate(AircraftItemModel* model) :
330         m_model(model)
331     {
332         m_model->m_packageRoot->addDelegate(this);
333     }
334
335     ~PackageDelegate()
336     {
337         m_model->m_packageRoot->removeDelegate(this);
338     }
339
340 protected:
341     virtual void catalogRefreshed(CatalogRef aCatalog, StatusCode aReason)
342     {
343         if (aReason == STATUS_IN_PROGRESS) {
344             qDebug() << "doing refresh of" << QString::fromStdString(aCatalog->url());
345         } else if ((aReason == STATUS_REFRESHED) || (aReason == STATUS_SUCCESS)) {
346             m_model->refreshPackages();
347         } else {
348             qWarning() << "failed refresh of "
349                 << QString::fromStdString(aCatalog->url()) << ":" << aReason << endl;
350         }
351     }
352
353     virtual void startInstall(InstallRef aInstall)
354     {
355         QModelIndex mi(indexForPackage(aInstall->package()));
356         m_model->dataChanged(mi, mi);
357     }
358
359     virtual void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total)
360     {
361         Q_UNUSED(bytes);
362         Q_UNUSED(total);
363         QModelIndex mi(indexForPackage(aInstall->package()));
364         m_model->dataChanged(mi, mi);
365     }
366
367     virtual void finishInstall(InstallRef aInstall, StatusCode aReason)
368     {
369         QModelIndex mi(indexForPackage(aInstall->package()));
370         m_model->dataChanged(mi, mi);
371
372         if ((aReason != USER_CANCELLED) && (aReason != STATUS_SUCCESS)) {
373             m_model->installFailed(mi, aReason);
374         }
375
376         if (aReason == STATUS_SUCCESS) {
377             m_model->installSucceeded(mi);
378         }
379     }
380
381     virtual void availablePackagesChanged() Q_DECL_OVERRIDE
382     {
383         m_model->refreshPackages();
384     }
385
386     virtual void dataForThumbnail(const std::string& aThumbnailUrl,
387                                   size_t length, const uint8_t* bytes)
388     {
389         QImage img = QImage::fromData(QByteArray::fromRawData(reinterpret_cast<const char*>(bytes), length));
390         if (img.isNull()) {
391             qWarning() << "failed to load image data for URL:" <<
392                 QString::fromStdString(aThumbnailUrl);
393             return;
394         }
395
396         QPixmap pix = QPixmap::fromImage(img);
397         if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
398             pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
399         }
400         m_model->m_thumbnailPixmapCache.insert(QString::fromStdString(aThumbnailUrl),
401                                                pix);
402
403         // notify any affected items. Linear scan here avoids another map/dict
404         // structure.
405         PackageList::const_iterator it;
406         int i = 0;
407
408         for (it=m_model->m_packages.begin(); it != m_model->m_packages.end(); ++it, ++i) {
409             const string_list& urls((*it)->thumbnailUrls());
410             string_list::const_iterator cit = std::find(urls.begin(), urls.end(), aThumbnailUrl);
411             if (cit != urls.end()) {
412                 QModelIndex mi(m_model->index(i + m_model->m_items.size()));
413                 m_model->dataChanged(mi, mi);
414             }
415         } // of packages iteration
416     }
417
418 private:
419     QModelIndex indexForPackage(const PackageRef& ref) const
420     {
421         PackageList::const_iterator it = std::find(m_model->m_packages.begin(),
422                                                    m_model->m_packages.end(),
423                                                    ref);
424         if (it == m_model->m_packages.end()) {
425             return QModelIndex();
426         }
427
428         size_t offset = it - m_model->m_packages.begin();
429         return m_model->index(offset + m_model->m_items.size());
430     }
431
432     AircraftItemModel* m_model;
433 };
434
435 AircraftItemModel::AircraftItemModel(QObject* pr, const simgear::pkg::RootRef& rootRef) :
436     QAbstractListModel(pr),
437     m_scanThread(NULL),
438     m_packageRoot(rootRef)
439 {
440     m_delegate = new PackageDelegate(this);
441     // packages may already be refreshed, so pull now
442     refreshPackages();
443 }
444
445 AircraftItemModel::~AircraftItemModel()
446 {
447     abandonCurrentScan();
448     delete m_delegate;
449 }
450
451 void AircraftItemModel::setPaths(QStringList paths)
452 {
453     m_paths = paths;
454 }
455
456 void AircraftItemModel::scanDirs()
457 {
458     abandonCurrentScan();
459
460     beginResetModel();
461     m_items.clear();
462     m_activeVariant.clear();
463     endResetModel();
464
465     QStringList dirs = m_paths;
466
467     Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
468         dirs << QString::fromStdString(ap);
469     }
470
471     SGPath rootAircraft(globals->get_fg_root());
472     rootAircraft.append("Aircraft");
473     dirs << QString::fromStdString(rootAircraft.str());
474
475     m_scanThread = new AircraftScanThread(dirs);
476     connect(m_scanThread, &AircraftScanThread::finished, this,
477             &AircraftItemModel::onScanFinished);
478     connect(m_scanThread, &AircraftScanThread::addedItems,
479             this, &AircraftItemModel::onScanResults);
480     m_scanThread->start();
481 }
482
483 void AircraftItemModel::abandonCurrentScan()
484 {
485     if (m_scanThread) {
486         m_scanThread->setDone();
487         m_scanThread->wait(1000);
488         delete m_scanThread;
489         m_scanThread = NULL;
490     }
491 }
492
493 void AircraftItemModel::refreshPackages()
494 {
495     beginResetModel();
496     m_packages = m_packageRoot->allPackages();
497     m_packageVariant.resize(m_packages.size());
498     endResetModel();
499 }
500
501 int AircraftItemModel::rowCount(const QModelIndex& parent) const
502 {
503     return m_items.size() + m_packages.size();
504 }
505
506 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
507 {
508     if (index.row() >= m_items.size()) {
509         quint32 packageIndex = index.row() - m_items.size();
510
511         if (role == AircraftVariantRole) {
512             return m_packageVariant.at(packageIndex);
513         }
514
515         const PackageRef& pkg(m_packages[packageIndex]);
516         InstallRef ex = pkg->existingInstall();
517
518         if (role == AircraftInstallPercentRole) {
519             return ex.valid() ? ex->downloadedPercent() : 0;
520         } else if (role == AircraftInstallDownloadedSizeRole) {
521             return static_cast<quint64>(ex.valid() ? ex->downloadedBytes() : 0);
522         }
523
524         quint32 variantIndex = m_packageVariant.at(packageIndex);
525         return dataFromPackage(pkg, variantIndex, role);
526     } else {
527         if (role == AircraftVariantRole) {
528             return m_activeVariant.at(index.row());
529         }
530
531         quint32 variantIndex = m_activeVariant.at(index.row());
532         const AircraftItemPtr item(m_items.at(index.row()));
533         return dataFromItem(item, variantIndex, role);
534     }
535 }
536
537 QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, quint32 variantIndex, int role) const
538 {
539     if (role == AircraftVariantCountRole) {
540         return item->variants.count();
541     }
542
543     if (role == AircraftThumbnailCountRole) {
544         QPixmap p = item->thumbnail();
545         return p.isNull() ? 0 : 1;
546     }
547
548     if (role == AircraftThumbnailSizeRole) {
549         return item->thumbnail().size();
550     }
551
552     if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
553         int variantIndex = role - AircraftVariantDescriptionRole;
554         return item->variants.at(variantIndex)->description;
555     }
556
557     if (variantIndex) {
558         if (variantIndex <= static_cast<quint32>(item->variants.count())) {
559             // show the selected variant
560             item = item->variants.at(variantIndex - 1);
561         }
562     }
563
564     if (role == Qt::DisplayRole) {
565         if (item->description.isEmpty()) {
566             return tr("Missing description for: %1").arg(item->baseName());
567         }
568
569         return item->description;
570     } else if (role == Qt::DecorationRole) {
571         return item->thumbnail();
572     } else if (role == AircraftPathRole) {
573         return item->path;
574     } else if (role == AircraftAuthorsRole) {
575         return item->authors;
576     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
577         return item->ratings[role - AircraftRatingRole];
578     } else if (role >= AircraftThumbnailRole) {
579         return item->thumbnail();
580     } else if (role == AircraftPackageIdRole) {
581         // can we fake an ID? otherwise fall through to a null variant
582     } else if (role == AircraftPackageStatusRole) {
583         return PackageInstalled; // always the case
584     } else if (role == Qt::ToolTipRole) {
585         return item->path;
586     } else if (role == AircraftURIRole) {
587         return QUrl::fromLocalFile(item->path);
588     } else if (role == AircraftHasRatingsRole) {
589         bool have = false;
590         for (int i=0; i<4; ++i) {
591             have |= (item->ratings[i] > 0);
592         }
593         return have;
594     } else if (role == AircraftLongDescriptionRole) {
595 #if 0
596         return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
597             "no escape from reality. Open your eyes, like up to the skies and see. "
598             "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
599             "Litte high, little low. Anywhere the wind blows.";
600 #endif
601     } else if (role == AircraftIsHelicopterRole) {
602         return item->usesHeliports;
603     } else if (role == AircraftIsSeaplaneRole) {
604         return item->usesSeaports;
605     }
606
607     return QVariant();
608 }
609
610 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
611 {
612     if (role == Qt::DecorationRole) {
613         role = AircraftThumbnailRole; // use first thumbnail
614     }
615
616     if (role == Qt::DisplayRole) {
617         return QString::fromStdString(item->name());
618     } else if (role == AircraftPathRole) {
619         InstallRef i = item->existingInstall();
620         if (i.valid()) {
621             return QString::fromStdString(i->primarySetPath().str());
622         }
623     } else if (role == AircraftPackageIdRole) {
624         return QString::fromStdString(item->id());
625     } else if (role == AircraftPackageStatusRole) {
626         InstallRef i = item->existingInstall();
627         if (i.valid()) {
628             if (i->isDownloading()) {
629                 return PackageDownloading;
630             }
631             if (i->isQueued()) {
632                 return PackageQueued;
633             }
634             if (i->hasUpdate()) {
635                 return PackageUpdateAvailable;
636             }
637
638             return PackageInstalled;
639         } else {
640             return PackageNotInstalled;
641         }
642     } else if (role == AircraftThumbnailSizeRole) {
643         QPixmap pm = packageThumbnail(item, 0, false).value<QPixmap>();
644         if (pm.isNull())
645             return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
646         return pm.size();
647     } else if (role >= AircraftThumbnailRole) {
648         return packageThumbnail(item , role - AircraftThumbnailRole);
649     } else if (role == AircraftAuthorsRole) {
650         SGPropertyNode* authors = item->properties()->getChild("author");
651         if (authors) {
652             return QString::fromStdString(authors->getStringValue());
653         }
654     } else if (role == AircraftLongDescriptionRole) {
655         return QString::fromStdString(item->description());
656     } else if (role == AircraftPackageSizeRole) {
657         return static_cast<int>(item->fileSizeBytes());
658     } else if (role == AircraftURIRole) {
659         return QUrl("package:" + QString::fromStdString(item->qualifiedId()));
660     } else if (role == AircraftHasRatingsRole) {
661         return item->properties()->hasChild("rating");
662     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
663         int ratingIndex = role - AircraftRatingRole;
664         SGPropertyNode* ratings = item->properties()->getChild("rating");
665         if (!ratings) {
666             return QVariant();
667         }
668         return ratings->getChild(ratingIndex)->getIntValue();
669     }
670
671     return QVariant();
672 }
673
674 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index, bool download) const
675 {
676     const string_list& thumbnails(p->thumbnailUrls());
677     if (index >= static_cast<int>(thumbnails.size())) {
678         return QVariant();
679     }
680
681     std::string thumbnailUrl = thumbnails.at(index);
682     QString urlQString(QString::fromStdString(thumbnailUrl));
683     if (m_thumbnailPixmapCache.contains(urlQString)) {
684         // cache hit, easy
685         return m_thumbnailPixmapCache.value(urlQString);
686     }
687
688 // check the on-disk store. This relies on the order of thumbnails in the
689 // results of thumbnailUrls and thumbnails corresponding
690     InstallRef ex = p->existingInstall();
691     if (ex.valid()) {
692         const string_list& thumbNames(p->thumbnails());
693         if (!thumbNames.empty()) {
694             SGPath path(ex->path());
695             path.append(p->thumbnails()[index]);
696             if (path.exists()) {
697                 QPixmap pix;
698                 pix.load(QString::fromStdString(path.str()));
699                 // resize to the standard size
700                 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
701                     pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
702                 }
703                 m_thumbnailPixmapCache[urlQString] = pix;
704                 return pix;
705             }
706         } // of have thumbnail file names
707     } // of have existing install
708
709     if (download) {
710         m_packageRoot->requestThumbnailData(thumbnailUrl);
711     }
712     return QVariant();
713 }
714
715 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
716   {
717       if (role == AircraftVariantRole) {
718           m_activeVariant[index.row()] = value.toInt();
719           emit dataChanged(index, index);
720           return true;
721       }
722
723       return false;
724   }
725
726 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
727 {
728     if (uri.isLocalFile()) {
729         QString path = uri.toLocalFile();
730         for (int row=0; row <m_items.size(); ++row) {
731             const AircraftItemPtr item(m_items.at(row));
732             if (item->path == path) {
733                 return index(row);
734             }
735         }
736     } else if (uri.scheme() == "package") {
737         QString ident = uri.path();
738         PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
739         if (pkg) {
740             for (size_t i=0; i < m_packages.size(); ++i) {
741                 if (m_packages[i] == pkg) {
742                     return index(m_items.size() + i);
743                 }
744             } // of linear package scan
745         }
746     } else {
747         qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
748     }
749
750     return QModelIndex();
751 }
752
753 void AircraftItemModel::onScanResults()
754 {
755     QVector<AircraftItemPtr> newItems = m_scanThread->items();
756     if (newItems.isEmpty())
757         return;
758
759     int firstRow = m_items.count();
760     int lastRow = firstRow + newItems.count() - 1;
761     beginInsertRows(QModelIndex(), firstRow, lastRow);
762     m_items+=newItems;
763
764     // default variants in all cases
765     for (int i=0; i< newItems.count(); ++i) {
766         m_activeVariant.append(0);
767     }
768     endInsertRows();
769 }
770
771 void AircraftItemModel::onScanFinished()
772 {
773     delete m_scanThread;
774     m_scanThread = NULL;
775     emit scanCompleted();
776 }
777
778 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
779 {
780     Q_ASSERT(index.row() >= m_items.size());
781
782     QString msg;
783     switch (reason) {
784         case Delegate::FAIL_CHECKSUM:
785             msg = tr("Invalid package checksum"); break;
786         case Delegate::FAIL_DOWNLOAD:
787             msg = tr("Download failed"); break;
788         case Delegate::FAIL_EXTRACT:
789             msg = tr("Package could not be extracted"); break;
790         case Delegate::FAIL_FILESYSTEM:
791             msg = tr("A local file-system error occurred"); break;
792         case Delegate::FAIL_NOT_FOUND:
793             msg = tr("Package file missing from download server"); break;
794         case Delegate::FAIL_UNKNOWN:
795         default:
796             msg = tr("Unknown reason");
797     }
798
799     emit aircraftInstallFailed(index, msg);
800 }
801
802 void AircraftItemModel::installSucceeded(QModelIndex index)
803 {
804     emit aircraftInstallCompleted(index);
805 }
806
807 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
808 {
809     if (index.row() < m_items.size()) {
810         return true; // local file, always runnable
811     }
812
813     quint32 packageIndex = index.row() - m_items.size();
814     const PackageRef& pkg(m_packages[packageIndex]);
815     InstallRef ex = pkg->existingInstall();
816     if (!ex.valid()) {
817         return false; // not installed
818     }
819
820     return !ex->isDownloading();
821 }
822
823 #include "AircraftModel.moc"