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