]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
Disable mouse hover code for now.
[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, simgear::pkg::RootRef& rootRef) :
406     QAbstractListModel(pr),
407     m_scanThread(NULL),
408     m_packageRoot(rootRef)
409 {
410     new PackageDelegate(this);
411     // packages may already be refreshed, so pull now
412     refreshPackages();
413 }
414
415 AircraftItemModel::~AircraftItemModel()
416 {
417     abandonCurrentScan();
418 }
419
420 void AircraftItemModel::setPaths(QStringList paths)
421 {
422     m_paths = paths;
423 }
424
425 void AircraftItemModel::scanDirs()
426 {
427     abandonCurrentScan();
428
429     beginResetModel();
430     m_items.clear();
431     m_activeVariant.clear();
432     endResetModel();
433
434     QStringList dirs = m_paths;
435
436     Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
437         dirs << QString::fromStdString(ap);
438     }
439
440     SGPath rootAircraft(globals->get_fg_root());
441     rootAircraft.append("Aircraft");
442     dirs << QString::fromStdString(rootAircraft.str());
443
444     m_scanThread = new AircraftScanThread(dirs);
445     connect(m_scanThread, &AircraftScanThread::finished, this,
446             &AircraftItemModel::onScanFinished);
447     connect(m_scanThread, &AircraftScanThread::addedItems,
448             this, &AircraftItemModel::onScanResults);
449     m_scanThread->start();
450 }
451
452 void AircraftItemModel::abandonCurrentScan()
453 {
454     if (m_scanThread) {
455         m_scanThread->setDone();
456         m_scanThread->wait(1000);
457         delete m_scanThread;
458         m_scanThread = NULL;
459     }
460 }
461
462 void AircraftItemModel::refreshPackages()
463 {
464     beginResetModel();
465     m_packages = m_packageRoot->allPackages();
466     m_packageVariant.resize(m_packages.size());
467     endResetModel();
468 }
469
470 int AircraftItemModel::rowCount(const QModelIndex& parent) const
471 {
472     return m_items.size() + m_packages.size();
473 }
474
475 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
476 {
477     if (index.row() >= m_items.size()) {
478         quint32 packageIndex = index.row() - m_items.size();
479
480         if (role == AircraftVariantRole) {
481             return m_packageVariant.at(packageIndex);
482         }
483         
484         const PackageRef& pkg(m_packages[packageIndex]);
485         InstallRef ex = pkg->existingInstall();
486         
487         if (role == AircraftInstallPercentRole) {
488             return ex.valid() ? ex->downloadedPercent() : 0;
489         } else if (role == AircraftInstallDownloadedSizeRole) {
490             return static_cast<quint64>(ex.valid() ? ex->downloadedBytes() : 0);
491         }
492         
493         quint32 variantIndex = m_packageVariant.at(packageIndex);
494         return dataFromPackage(pkg, variantIndex, role);
495     } else {
496         if (role == AircraftVariantRole) {
497             return m_activeVariant.at(index.row());
498         }
499
500         quint32 variantIndex = m_activeVariant.at(index.row());
501         const AircraftItemPtr item(m_items.at(index.row()));
502         return dataFromItem(item, variantIndex, role);
503     }
504 }
505
506 QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, quint32 variantIndex, int role) const
507 {
508     if (role == AircraftVariantCountRole) {
509         return item->variants.count();
510     }
511
512     if (role == AircraftThumbnailCountRole) {
513         QPixmap p = item->thumbnail();
514         return p.isNull() ? 0 : 1;
515     }
516
517     if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
518         int variantIndex = role - AircraftVariantDescriptionRole;
519         return item->variants.at(variantIndex)->description;
520     }
521
522     if (variantIndex) {
523         if (variantIndex <= item->variants.count()) {
524             // show the selected variant
525             item = item->variants.at(variantIndex - 1);
526         }
527     }
528
529     if (role == Qt::DisplayRole) {
530         return item->description;
531     } else if (role == Qt::DecorationRole) {
532         return item->thumbnail();
533     } else if (role == AircraftPathRole) {
534         return item->path;
535     } else if (role == AircraftAuthorsRole) {
536         return item->authors;
537     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
538         return item->ratings[role - AircraftRatingRole];
539     } else if (role >= AircraftThumbnailRole) {
540         return item->thumbnail();
541     } else if (role == AircraftPackageIdRole) {
542         // can we fake an ID? otherwise fall through to a null variant
543     } else if (role == AircraftPackageStatusRole) {
544         return PackageInstalled; // always the case
545     } else if (role == Qt::ToolTipRole) {
546         return item->path;
547     } else if (role == AircraftURIRole) {
548         return QUrl::fromLocalFile(item->path);
549     } else if (role == AircraftHasRatingsRole) {
550         bool have = false;
551         for (int i=0; i<4; ++i) {
552             have |= (item->ratings[i] > 0);
553         }
554         return have;
555     } else if (role == AircraftLongDescriptionRole) {
556 #if 0
557         return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
558             "no escape from reality. Open your eyes, like up to the skies and see. "
559             "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
560             "Litte high, little low. Anywhere the wind blows.";
561 #endif
562     }
563
564     return QVariant();
565 }
566
567 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
568 {
569     if (role == Qt::DecorationRole) {
570         role = AircraftThumbnailRole; // use first thumbnail
571     }
572     
573     if (role == Qt::DisplayRole) {
574         return QString::fromStdString(item->name());
575     } else if (role == AircraftPathRole) {
576         InstallRef i = item->existingInstall();
577         if (i.valid()) {
578             return QString::fromStdString(i->primarySetPath().str());
579         }
580     } else if (role == AircraftPackageIdRole) {
581         return QString::fromStdString(item->id());
582     } else if (role == AircraftPackageStatusRole) {
583         InstallRef i = item->existingInstall();
584         if (i.valid()) {
585             if (i->isDownloading()) {
586                 return PackageDownloading;
587             }
588             if (i->isQueued()) {
589                 return PackageQueued;
590             }
591             if (i->hasUpdate()) {
592                 return PackageUpdateAvailable;
593             }
594
595             return PackageInstalled;
596         } else {
597             return PackageNotInstalled;
598         }
599     } else if (role >= AircraftThumbnailRole) {
600         return packageThumbnail(item , role - AircraftThumbnailRole);
601     } else if (role == AircraftAuthorsRole) {
602         SGPropertyNode* authors = item->properties()->getChild("author");
603         if (authors) {
604             return QString::fromStdString(authors->getStringValue());
605         }
606     } else if (role == AircraftLongDescriptionRole) {
607         return QString::fromStdString(item->description());
608     } else if (role == AircraftPackageSizeRole) {
609         return static_cast<int>(item->fileSizeBytes());
610     } else if (role == AircraftURIRole) {
611         return QUrl("package:" + QString::fromStdString(item->qualifiedId()));
612     } else if (role == AircraftHasRatingsRole) {
613         return item->properties()->hasChild("rating");
614     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
615         int ratingIndex = role - AircraftRatingRole;
616         SGPropertyNode* ratings = item->properties()->getChild("rating");
617         if (!ratings) {
618             return QVariant();
619         }
620         return ratings->getChild(ratingIndex)->getIntValue();
621     }
622
623     return QVariant();
624 }
625
626 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index) const
627 {
628     const string_list& thumbnails(p->thumbnailUrls());
629     if (index >= thumbnails.size()) {
630         return QVariant();
631     }
632     
633     std::string thumbnailUrl = thumbnails.at(index);
634     QString urlQString(QString::fromStdString(thumbnailUrl));
635     if (m_thumbnailPixmapCache.contains(urlQString)) {
636         // cache hit, easy
637         return m_thumbnailPixmapCache.value(urlQString);
638     }
639  
640 // check the on-disk store. This relies on the order of thumbnails in the
641 // results of thumbnailUrls and thumbnails corresponding
642     InstallRef ex = p->existingInstall();
643     if (ex.valid()) {
644         const string_list& thumbNames(p->thumbnails());
645         if (!thumbNames.empty()) {
646             SGPath path(ex->path());
647             path.append(p->thumbnails()[index]);
648             if (path.exists()) {
649                 QPixmap pix;
650                 pix.load(QString::fromStdString(path.str()));
651                 // resize to the standard size
652                 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
653                     pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
654                 }
655                 m_thumbnailPixmapCache[urlQString] = pix;
656                 return pix;
657             }
658         } // of have thumbnail file names
659     } // of have existing install
660     
661     m_packageRoot->requestThumbnailData(thumbnailUrl);
662     return QVariant();
663 }
664
665 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
666   {
667       if (role == AircraftVariantRole) {
668           m_activeVariant[index.row()] = value.toInt();
669           emit dataChanged(index, index);
670           return true;
671       }
672
673       return false;
674   }
675
676 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
677 {
678     if (uri.isLocalFile()) {
679         QString path = uri.toLocalFile();
680         for (int row=0; row <m_items.size(); ++row) {
681             const AircraftItemPtr item(m_items.at(row));
682             if (item->path == path) {
683                 return index(row);
684             }
685         }
686     } else if (uri.scheme() == "package") {
687         QString ident = uri.path();
688         PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
689         if (pkg) {
690             for (int i=0; i < m_packages.size(); ++i) {
691                 if (m_packages[i] == pkg) {
692                     return index(m_items.size() + i);
693                 }
694             } // of linear package scan
695         }
696     } else {
697         qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
698     }
699     
700     return QModelIndex();
701 }
702
703 void AircraftItemModel::onScanResults()
704 {
705     QVector<AircraftItemPtr> newItems = m_scanThread->items();
706     if (newItems.isEmpty())
707         return;
708
709     int firstRow = m_items.count();
710     int lastRow = firstRow + newItems.count() - 1;
711     beginInsertRows(QModelIndex(), firstRow, lastRow);
712     m_items+=newItems;
713
714     // default variants in all cases
715     for (int i=0; i< newItems.count(); ++i) {
716         m_activeVariant.append(0);
717     }
718     endInsertRows();
719 }
720
721 void AircraftItemModel::onScanFinished()
722 {
723     delete m_scanThread;
724     m_scanThread = NULL;
725 }
726
727 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
728 {
729     Q_ASSERT(index.row() >= m_items.size());
730     
731     QString msg;
732     switch (reason) {
733         case Delegate::FAIL_CHECKSUM:
734             msg = tr("Invalid package checksum"); break;
735         case Delegate::FAIL_DOWNLOAD:
736             msg = tr("Download failed"); break;
737         case Delegate::FAIL_EXTRACT:
738             msg = tr("Package could not be extracted"); break;
739         case Delegate::FAIL_FILESYSTEM:
740             msg = tr("A local file-system error occurred"); break;
741         case Delegate::FAIL_UNKNOWN:
742         default:
743             msg = tr("Unknown reason");
744     }
745     
746     quint32 packageIndex = index.row() - m_items.size();
747     const PackageRef& pkg(m_packages[packageIndex]);
748     QString packageName = QString::fromStdString(pkg->description());
749     emit aircraftInstallFailed(index, tr("Failed installation of package '%1': %2").arg(packageName).arg(msg));
750 }
751
752 void AircraftItemModel::installSucceeded(QModelIndex index)
753 {
754     qDebug() << Q_FUNC_INFO << index;
755     emit aircraftInstallCompleted(index);
756 }
757
758 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
759 {
760     if (index.row() < m_items.size()) {
761         return true; // local file, always runnable
762     }
763     
764     quint32 packageIndex = index.row() - m_items.size();
765     const PackageRef& pkg(m_packages[packageIndex]);
766     InstallRef ex = pkg->existingInstall();
767     if (!ex.valid()) {
768         return false; // not installed
769     }
770     
771     return !ex->isDownloading();
772 }
773
774 #include "AircraftModel.moc"