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