]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
Launcher: Maintain aircraft selection better
[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 >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
617         int variantIndex = role - AircraftVariantDescriptionRole;
618         return QString::fromStdString(item->nameForVariant(variantIndex));
619     }
620
621     if (role == Qt::DisplayRole) {
622         return QString::fromStdString(item->nameForVariant(variantIndex));
623     } else if (role == AircraftPathRole) {
624         InstallRef i = item->existingInstall();
625         if (i.valid()) {
626             return QString::fromStdString(i->primarySetPath().str());
627         }
628     } else if (role == AircraftPackageIdRole) {
629         return QString::fromStdString(item->variants()[variantIndex]);
630     } else if (role == AircraftPackageStatusRole) {
631         InstallRef i = item->existingInstall();
632         if (i.valid()) {
633             if (i->isDownloading()) {
634                 return PackageDownloading;
635             }
636             if (i->isQueued()) {
637                 return PackageQueued;
638             }
639             if (i->hasUpdate()) {
640                 return PackageUpdateAvailable;
641             }
642
643             return PackageInstalled;
644         } else {
645             return PackageNotInstalled;
646         }
647     } else if (role == AircraftVariantCountRole) {
648         // this value wants the number of aditional variants, i.e not
649         // including the primary. Hence the -1 term.
650         return static_cast<quint32>(item->variants().size() - 1);
651     } else if (role == AircraftThumbnailSizeRole) {
652         QPixmap pm = packageThumbnail(item, 0, false).value<QPixmap>();
653         if (pm.isNull())
654             return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
655         return pm.size();
656     } else if (role >= AircraftThumbnailRole) {
657         return packageThumbnail(item , role - AircraftThumbnailRole);
658     } else if (role == AircraftAuthorsRole) {
659         SGPropertyNode* authors = item->properties()->getChild("author");
660         if (authors) {
661             return QString::fromStdString(authors->getStringValue());
662         }
663     } else if (role == AircraftLongDescriptionRole) {
664         return QString::fromStdString(item->description());
665     } else if (role == AircraftPackageSizeRole) {
666         return static_cast<int>(item->fileSizeBytes());
667     } else if (role == AircraftURIRole) {
668         return QUrl("package:" + QString::fromStdString(item->qualifiedVariantId(variantIndex)));
669     } else if (role == AircraftHasRatingsRole) {
670         return item->properties()->hasChild("rating");
671     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
672         int ratingIndex = role - AircraftRatingRole;
673         SGPropertyNode* ratings = item->properties()->getChild("rating");
674         if (!ratings) {
675             return QVariant();
676         }
677         return ratings->getChild(ratingIndex)->getIntValue();
678     }
679
680     return QVariant();
681 }
682
683 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index, bool download) const
684 {
685     const string_list& thumbnails(p->thumbnailUrls());
686     if (index >= static_cast<int>(thumbnails.size())) {
687         return QVariant();
688     }
689
690     std::string thumbnailUrl = thumbnails.at(index);
691     QString urlQString(QString::fromStdString(thumbnailUrl));
692     if (m_thumbnailPixmapCache.contains(urlQString)) {
693         // cache hit, easy
694         return m_thumbnailPixmapCache.value(urlQString);
695     }
696
697 // check the on-disk store. This relies on the order of thumbnails in the
698 // results of thumbnailUrls and thumbnails corresponding
699     InstallRef ex = p->existingInstall();
700     if (ex.valid()) {
701         const string_list& thumbNames(p->thumbnails());
702         if (!thumbNames.empty()) {
703             SGPath path(ex->path());
704             path.append(p->thumbnails()[index]);
705             if (path.exists()) {
706                 QPixmap pix;
707                 pix.load(QString::fromStdString(path.str()));
708                 // resize to the standard size
709                 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
710                     pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
711                 }
712                 m_thumbnailPixmapCache[urlQString] = pix;
713                 return pix;
714             }
715         } // of have thumbnail file names
716     } // of have existing install
717
718     if (download) {
719         m_packageRoot->requestThumbnailData(thumbnailUrl);
720     }
721     return QVariant();
722 }
723
724 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
725   {
726       int row = index.row();
727       if (role == AircraftVariantRole) {
728           if (row >= m_activeVariant.size()) {
729               row -= m_activeVariant.size();
730               m_packageVariant[row] = value.toInt();
731           } else {
732               m_activeVariant[row] = value.toInt();
733           }
734
735           emit dataChanged(index, index);
736           return true;
737       }
738
739       return false;
740   }
741
742 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
743 {
744     if (uri.isLocalFile()) {
745         QString path = uri.toLocalFile();
746         for (int row=0; row <m_items.size(); ++row) {
747             const AircraftItemPtr item(m_items.at(row));
748             if (item->path == path) {
749                 return index(row);
750             }
751         }
752     } else if (uri.scheme() == "package") {
753         QString ident = uri.path();
754         PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
755         if (pkg) {
756             for (size_t i=0; i < m_packages.size(); ++i) {
757                 if (m_packages[i] == pkg) {
758                     return index(m_items.size() + i);
759                 }
760             } // of linear package scan
761         }
762     } else {
763         qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
764     }
765
766     return QModelIndex();
767 }
768
769 void AircraftItemModel::onScanResults()
770 {
771     QVector<AircraftItemPtr> newItems = m_scanThread->items();
772     if (newItems.isEmpty())
773         return;
774
775     int firstRow = m_items.count();
776     int lastRow = firstRow + newItems.count() - 1;
777     beginInsertRows(QModelIndex(), firstRow, lastRow);
778     m_items+=newItems;
779
780     // default variants in all cases
781     for (int i=0; i< newItems.count(); ++i) {
782         m_activeVariant.append(0);
783     }
784     endInsertRows();
785 }
786
787 void AircraftItemModel::onScanFinished()
788 {
789     delete m_scanThread;
790     m_scanThread = NULL;
791     emit scanCompleted();
792 }
793
794 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
795 {
796     Q_ASSERT(index.row() >= m_items.size());
797
798     QString msg;
799     switch (reason) {
800         case Delegate::FAIL_CHECKSUM:
801             msg = tr("Invalid package checksum"); break;
802         case Delegate::FAIL_DOWNLOAD:
803             msg = tr("Download failed"); break;
804         case Delegate::FAIL_EXTRACT:
805             msg = tr("Package could not be extracted"); break;
806         case Delegate::FAIL_FILESYSTEM:
807             msg = tr("A local file-system error occurred"); break;
808         case Delegate::FAIL_NOT_FOUND:
809             msg = tr("Package file missing from download server"); break;
810         case Delegate::FAIL_UNKNOWN:
811         default:
812             msg = tr("Unknown reason");
813     }
814
815     emit aircraftInstallFailed(index, msg);
816 }
817
818 void AircraftItemModel::installSucceeded(QModelIndex index)
819 {
820     emit aircraftInstallCompleted(index);
821 }
822
823 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
824 {
825     if (index.row() < m_items.size()) {
826         return true; // local file, always runnable
827     }
828
829     quint32 packageIndex = index.row() - m_items.size();
830     const PackageRef& pkg(m_packages[packageIndex]);
831     InstallRef ex = pkg->existingInstall();
832     if (!ex.valid()) {
833         return false; // not installed
834     }
835
836     return !ex->isDownloading();
837 }
838
839 #include "AircraftModel.moc"