]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
Code cleanups, code updates and fix at least on (possible) devide-by-zero
[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("long-description")) {
94         // clean up any XML whitspace in the text.
95         longDescription = QString(sim->getStringValue("long-description")).simplified();
96     }
97
98     if (sim->hasChild("variant-of")) {
99         variantOf = sim->getStringValue("variant-of");
100     }
101
102     if (sim->hasChild("tags")) {
103         SGPropertyNode_ptr tagsNode = sim->getChild("tags");
104         int nChildren = tagsNode->nChildren();
105         for (int i = 0; i < nChildren; i++) {
106             const SGPropertyNode* c = tagsNode->getChild(i);
107             if (strcmp(c->getName(), "tag") == 0) {
108                 const char* tagName = c->getStringValue();
109                 usesHeliports |= (strcmp(tagName, "helicopter") == 0);
110                 // could also consider vtol tag?
111                 usesSeaports |= (strcmp(tagName, "seaplane") == 0);
112                 usesSeaports |= (strcmp(tagName, "floats") == 0);
113             }
114         } // of tags iteration
115     } // of set-xml has tags
116 }
117
118 QString AircraftItem::baseName() const
119 {
120     QString fn = QFileInfo(path).fileName();
121     fn.truncate(fn.count() - 8);
122     return fn;
123 }
124
125 void AircraftItem::fromDataStream(QDataStream& ds)
126 {
127     ds >> path >> pathModTime >> excluded;
128     if (excluded) {
129         return;
130     }
131
132     ds >> description >> longDescription >> authors >> variantOf;
133     for (int i=0; i<4; ++i) ds >> ratings[i];
134 }
135
136 void AircraftItem::toDataStream(QDataStream& ds) const
137 {
138     ds << path << pathModTime << excluded;
139     if (excluded) {
140         return;
141     }
142
143     ds << description << longDescription << authors << variantOf;
144     for (int i=0; i<4; ++i) ds << ratings[i];
145 }
146
147 QPixmap AircraftItem::thumbnail() const
148 {
149     if (m_thumbnail.isNull()) {
150         QFileInfo info(path);
151         QDir dir = info.dir();
152         if (dir.exists("thumbnail.jpg")) {
153             m_thumbnail.load(dir.filePath("thumbnail.jpg"));
154             // resize to the standard size
155             if (m_thumbnail.height() > STANDARD_THUMBNAIL_HEIGHT) {
156                 m_thumbnail = m_thumbnail.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
157             }
158         }
159     }
160
161     return m_thumbnail;
162 }
163
164
165 static quint32 CACHE_VERSION = 5;
166
167 class AircraftScanThread : public QThread
168 {
169     Q_OBJECT
170 public:
171     AircraftScanThread(QStringList dirsToScan) :
172         m_dirs(dirsToScan),
173         m_done(false)
174     {
175     }
176
177     ~AircraftScanThread()
178     {
179     }
180
181     /** thread-safe access to items already scanned */
182     QVector<AircraftItemPtr> items()
183     {
184         QVector<AircraftItemPtr> result;
185         QMutexLocker g(&m_lock);
186         result.swap(m_items);
187         g.unlock();
188         return result;
189     }
190
191     void setDone()
192     {
193         m_done = true;
194     }
195 Q_SIGNALS:
196     void addedItems();
197
198 protected:
199     virtual void run()
200     {
201         readCache();
202
203         Q_FOREACH(QString d, m_dirs) {
204             scanAircraftDir(QDir(d));
205             if (m_done) {
206                 return;
207             }
208         }
209
210         writeCache();
211     }
212
213 private:
214     void readCache()
215     {
216         QSettings settings;
217         QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
218         if (!cacheData.isEmpty()) {
219             QDataStream ds(cacheData);
220             quint32 count, cacheVersion;
221             ds >> cacheVersion >> count;
222
223             if (cacheVersion != CACHE_VERSION) {
224                 return; // mis-matched cache, version, drop
225             }
226
227              for (quint32 i=0; i<count; ++i) {
228                 AircraftItemPtr item(new AircraftItem);
229                 item->fromDataStream(ds);
230
231                 QFileInfo finfo(item->path);
232                 if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) {
233                     // corresponding -set.xml file still exists and is
234                     // unmodified
235                     m_cachedItems[item->path] = item;
236                 }
237             } // of cached item iteration
238         }
239     }
240
241     void writeCache()
242     {
243         QSettings settings;
244         QByteArray cacheData;
245         {
246             QDataStream ds(&cacheData, QIODevice::WriteOnly);
247             quint32 count = m_nextCache.count();
248             ds << CACHE_VERSION << count;
249
250             Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) {
251                 item->toDataStream(ds);
252             }
253         }
254
255         settings.setValue("aircraft-cache", cacheData);
256     }
257
258     void scanAircraftDir(QDir path)
259     {
260         QTime t;
261         t.start();
262
263         QStringList filters;
264         filters << "*-set.xml";
265         Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
266             QDir childDir(child.absoluteFilePath());
267             QMap<QString, AircraftItemPtr> baseAircraft;
268             QList<AircraftItemPtr> variants;
269
270             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
271                 try {
272                     QString absolutePath = xmlChild.absoluteFilePath();
273                     AircraftItemPtr item;
274
275                     if (m_cachedItems.contains(absolutePath)) {
276                         item = m_cachedItems.value(absolutePath);
277                     } else {
278                         item = AircraftItemPtr(new AircraftItem(childDir, absolutePath));
279                     }
280
281                     m_nextCache[absolutePath] = item;
282
283                     if (item->excluded) {
284                         continue;
285                     }
286
287                     if (item->variantOf.isNull()) {
288                         baseAircraft.insert(item->baseName(), item);
289                     } else {
290                         variants.append(item);
291                     }
292                 } catch (sg_exception& e) {
293                     continue;
294                 }
295
296                 if (m_done) {
297                     return;
298                 }
299             } // of set.xml iteration
300
301             // bind variants to their principals
302             Q_FOREACH(AircraftItemPtr item, variants) {
303                 if (!baseAircraft.contains(item->variantOf)) {
304                     qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
305                     continue;
306                 }
307
308                 baseAircraft.value(item->variantOf)->variants.append(item);
309             }
310
311             // lock mutex while we modify the items array
312             {
313                 QMutexLocker g(&m_lock);
314                 m_items+=(baseAircraft.values().toVector());
315             }
316
317             emit addedItems();
318         } // of subdir iteration
319     }
320
321     QMutex m_lock;
322     QStringList m_dirs;
323     QVector<AircraftItemPtr> m_items;
324
325     QMap<QString, AircraftItemPtr > m_cachedItems;
326     QMap<QString, AircraftItemPtr > m_nextCache;
327
328     bool m_done;
329 };
330
331 class PackageDelegate : public simgear::pkg::Delegate
332 {
333 public:
334     PackageDelegate(AircraftItemModel* model) :
335         m_model(model)
336     {
337         m_model->m_packageRoot->addDelegate(this);
338     }
339
340     ~PackageDelegate()
341     {
342         m_model->m_packageRoot->removeDelegate(this);
343     }
344
345 protected:
346     virtual void catalogRefreshed(CatalogRef aCatalog, StatusCode aReason)
347     {
348         if (aReason == STATUS_IN_PROGRESS) {
349             qDebug() << "doing refresh of" << QString::fromStdString(aCatalog->url());
350         } else if ((aReason == STATUS_REFRESHED) || (aReason == STATUS_SUCCESS)) {
351             m_model->refreshPackages();
352         } else {
353             qWarning() << "failed refresh of "
354                 << QString::fromStdString(aCatalog->url()) << ":" << aReason << endl;
355         }
356     }
357
358     virtual void startInstall(InstallRef aInstall)
359     {
360         QModelIndex mi(indexForPackage(aInstall->package()));
361         m_model->dataChanged(mi, mi);
362     }
363
364     virtual void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total)
365     {
366         Q_UNUSED(bytes);
367         Q_UNUSED(total);
368         QModelIndex mi(indexForPackage(aInstall->package()));
369         m_model->dataChanged(mi, mi);
370     }
371
372     virtual void finishInstall(InstallRef aInstall, StatusCode aReason)
373     {
374         QModelIndex mi(indexForPackage(aInstall->package()));
375         m_model->dataChanged(mi, mi);
376
377         if ((aReason != USER_CANCELLED) && (aReason != STATUS_SUCCESS)) {
378             m_model->installFailed(mi, aReason);
379         }
380
381         if (aReason == STATUS_SUCCESS) {
382             m_model->installSucceeded(mi);
383         }
384     }
385
386     virtual void availablePackagesChanged() Q_DECL_OVERRIDE
387     {
388         m_model->refreshPackages();
389     }
390
391     virtual void dataForThumbnail(const std::string& aThumbnailUrl,
392                                   size_t length, const uint8_t* bytes)
393     {
394         QImage img = QImage::fromData(QByteArray::fromRawData(reinterpret_cast<const char*>(bytes), length));
395         if (img.isNull()) {
396             qWarning() << "failed to load image data for URL:" <<
397                 QString::fromStdString(aThumbnailUrl);
398             return;
399         }
400
401         QPixmap pix = QPixmap::fromImage(img);
402         if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
403             pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
404         }
405         m_model->m_thumbnailPixmapCache.insert(QString::fromStdString(aThumbnailUrl),
406                                                pix);
407
408         // notify any affected items. Linear scan here avoids another map/dict
409         // structure.
410         PackageList::const_iterator it;
411         int i = 0;
412
413         for (it=m_model->m_packages.begin(); it != m_model->m_packages.end(); ++it, ++i) {
414             const string_list& urls((*it)->thumbnailUrls());
415             string_list::const_iterator cit = std::find(urls.begin(), urls.end(), aThumbnailUrl);
416             if (cit != urls.end()) {
417                 QModelIndex mi(m_model->index(i + m_model->m_items.size()));
418                 m_model->dataChanged(mi, mi);
419             }
420         } // of packages iteration
421     }
422
423 private:
424     QModelIndex indexForPackage(const PackageRef& ref) const
425     {
426         PackageList::const_iterator it = std::find(m_model->m_packages.begin(),
427                                                    m_model->m_packages.end(),
428                                                    ref);
429         if (it == m_model->m_packages.end()) {
430             return QModelIndex();
431         }
432
433         size_t offset = it - m_model->m_packages.begin();
434         return m_model->index(offset + m_model->m_items.size());
435     }
436
437     AircraftItemModel* m_model;
438 };
439
440 AircraftItemModel::AircraftItemModel(QObject* pr ) :
441     QAbstractListModel(pr),
442     m_scanThread(NULL),
443     m_showOfficialHangarMessage(false)
444 {
445 }
446
447 AircraftItemModel::~AircraftItemModel()
448 {
449     abandonCurrentScan();
450     delete m_delegate;
451 }
452
453 void AircraftItemModel::setPackageRoot(const simgear::pkg::RootRef& root)
454 {
455     if (m_packageRoot) {
456         delete m_delegate;
457         m_delegate = NULL;
458     }
459
460     m_packageRoot = root;
461
462     if (m_packageRoot) {
463         m_delegate = new PackageDelegate(this);
464         // packages may already be refreshed, so pull now
465         refreshPackages();
466     }
467 }
468
469 void AircraftItemModel::setPaths(QStringList paths)
470 {
471     m_paths = paths;
472 }
473
474 void AircraftItemModel::setOfficialHangarMessageVisible(bool vis)
475 {
476     if (m_showOfficialHangarMessage == vis) {
477         return;
478     }
479
480     m_showOfficialHangarMessage = vis;
481
482     if (vis) {
483         beginInsertRows(QModelIndex(), 0, 0);
484         m_items.prepend(AircraftItemPtr(new AircraftItem));
485         m_activeVariant.prepend(0);
486         endInsertRows();
487     } else {
488         beginRemoveRows(QModelIndex(), 0, 0);
489         m_items.removeAt(0);
490         m_activeVariant.removeAt(0);
491         endRemoveRows();
492     }
493 }
494
495 QModelIndex AircraftItemModel::officialHangarMessageIndex() const
496 {
497     if (!m_showOfficialHangarMessage) {
498         return QModelIndex();
499     }
500
501     return index(0);
502 }
503
504 void AircraftItemModel::scanDirs()
505 {
506     abandonCurrentScan();
507
508     int firstRow = (m_showOfficialHangarMessage ? 1 : 0);
509     int numToRemove = m_items.size() - firstRow;
510     int lastRow = firstRow + numToRemove - 1;
511
512     beginRemoveRows(QModelIndex(), firstRow, lastRow);
513     m_items.remove(firstRow, numToRemove);
514     m_activeVariant.remove(firstRow, numToRemove);
515     endRemoveRows();
516
517     QStringList dirs = m_paths;
518
519     Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
520         dirs << QString::fromStdString(ap);
521     }
522
523     SGPath rootAircraft(globals->get_fg_root());
524     rootAircraft.append("Aircraft");
525     dirs << QString::fromStdString(rootAircraft.str());
526
527     m_scanThread = new AircraftScanThread(dirs);
528     connect(m_scanThread, &AircraftScanThread::finished, this,
529             &AircraftItemModel::onScanFinished);
530     connect(m_scanThread, &AircraftScanThread::addedItems,
531             this, &AircraftItemModel::onScanResults);
532     m_scanThread->start();
533 }
534
535 void AircraftItemModel::abandonCurrentScan()
536 {
537     if (m_scanThread) {
538         m_scanThread->setDone();
539         m_scanThread->wait(1000);
540         delete m_scanThread;
541         m_scanThread = NULL;
542     }
543 }
544
545 void AircraftItemModel::refreshPackages()
546 {
547     simgear::pkg::PackageList newPkgs = m_packageRoot->allPackages();
548     int firstRow = m_items.size();
549     int newSize = newPkgs.size();
550
551     if (m_packages.size() != newPkgs.size()) {
552         int oldSize = m_packages.size();
553         if (newSize > oldSize) {
554             // growing
555             int firstNewRow = firstRow + oldSize;
556             int lastNewRow = firstRow + newSize - 1;
557             beginInsertRows(QModelIndex(), firstNewRow, lastNewRow);
558             m_packages = newPkgs;
559             m_packageVariant.resize(newSize);
560             endInsertRows();
561         } else {
562             // shrinking
563             int firstOldRow = firstRow + newSize;
564             int lastOldRow = firstRow + oldSize - 1;
565             beginRemoveRows(QModelIndex(), firstOldRow, lastOldRow);
566             m_packages = newPkgs;
567             m_packageVariant.resize(newSize);
568             endRemoveRows();
569         }
570     } else {
571         m_packages = newPkgs;
572     }
573
574     emit dataChanged(index(firstRow), index(firstRow + newSize - 1));
575 }
576
577 int AircraftItemModel::rowCount(const QModelIndex& parent) const
578 {
579     return m_items.size() + m_packages.size();
580 }
581
582 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
583 {
584     int row = index.row();
585     if (m_showOfficialHangarMessage) {
586         if (row == 0) {
587             if (role == AircraftPackageStatusRole) {
588                 return NoOfficialCatalogMessage;
589             }
590
591             return QVariant();
592         }
593     }
594
595     if (row >= m_items.size()) {
596         quint32 packageIndex = row - m_items.size();
597
598         if (role == AircraftVariantRole) {
599             return m_packageVariant.at(packageIndex);
600         }
601
602         const PackageRef& pkg(m_packages[packageIndex]);
603         InstallRef ex = pkg->existingInstall();
604
605         if (role == AircraftInstallPercentRole) {
606             return ex.valid() ? ex->downloadedPercent() : 0;
607         } else if (role == AircraftInstallDownloadedSizeRole) {
608             return static_cast<quint64>(ex.valid() ? ex->downloadedBytes() : 0);
609         }
610
611         quint32 variantIndex = m_packageVariant.at(packageIndex);
612         return dataFromPackage(pkg, variantIndex, role);
613     } else {
614         if (role == AircraftVariantRole) {
615             return m_activeVariant.at(row);
616         }
617
618         quint32 variantIndex = m_activeVariant.at(row);
619         const AircraftItemPtr item(m_items.at(row));
620         return dataFromItem(item, variantIndex, role);
621     }
622 }
623
624 QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, quint32 variantIndex, int role) const
625 {
626     if (role == AircraftVariantCountRole) {
627         return item->variants.count();
628     }
629
630     if (role == AircraftThumbnailCountRole) {
631         QPixmap p = item->thumbnail();
632         return p.isNull() ? 0 : 1;
633     }
634
635     if (role == AircraftThumbnailSizeRole) {
636         return item->thumbnail().size();
637     }
638
639     if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
640         int variantIndex = role - AircraftVariantDescriptionRole;
641         return item->variants.at(variantIndex)->description;
642     }
643
644     if (variantIndex) {
645         if (variantIndex <= static_cast<quint32>(item->variants.count())) {
646             // show the selected variant
647             item = item->variants.at(variantIndex - 1);
648         }
649     }
650
651     if (role == Qt::DisplayRole) {
652         if (item->description.isEmpty()) {
653             return tr("Missing description for: %1").arg(item->baseName());
654         }
655
656         return item->description;
657     } else if (role == Qt::DecorationRole) {
658         return item->thumbnail();
659     } else if (role == AircraftPathRole) {
660         return item->path;
661     } else if (role == AircraftAuthorsRole) {
662         return item->authors;
663     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
664         return item->ratings[role - AircraftRatingRole];
665     } else if (role >= AircraftThumbnailRole) {
666         return item->thumbnail();
667     } else if (role == AircraftPackageIdRole) {
668         // can we fake an ID? otherwise fall through to a null variant
669     } else if (role == AircraftPackageStatusRole) {
670         return PackageInstalled; // always the case
671     } else if (role == Qt::ToolTipRole) {
672         return item->path;
673     } else if (role == AircraftURIRole) {
674         return QUrl::fromLocalFile(item->path);
675     } else if (role == AircraftHasRatingsRole) {
676         bool have = false;
677         for (int i=0; i<4; ++i) {
678             have |= (item->ratings[i] > 0);
679         }
680         return have;
681     } else if (role == AircraftLongDescriptionRole) {
682         return item->longDescription;
683     } else if (role == AircraftIsHelicopterRole) {
684         return item->usesHeliports;
685     } else if (role == AircraftIsSeaplaneRole) {
686         return item->usesSeaports;
687     }
688
689     return QVariant();
690 }
691
692 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
693 {
694     if (role == Qt::DecorationRole) {
695         role = AircraftThumbnailRole; // use first thumbnail
696     }
697
698     if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
699         int variantIndex = role - AircraftVariantDescriptionRole;
700         QString desc = QString::fromStdString(item->nameForVariant(variantIndex));
701         if (desc.isEmpty()) {
702             desc = tr("Missing description for: %1").arg(QString::fromStdString(item->id()));
703         }
704         return desc;
705     }
706
707     if (role == Qt::DisplayRole) {
708         QString desc = QString::fromStdString(item->nameForVariant(variantIndex));
709         if (desc.isEmpty()) {
710             desc = tr("Missing description for: %1").arg(QString::fromStdString(item->id()));
711         }
712         return desc;
713     } else if (role == AircraftPathRole) {
714         InstallRef i = item->existingInstall();
715         if (i.valid()) {
716             return QString::fromStdString(i->primarySetPath().str());
717         }
718     } else if (role == AircraftPackageIdRole) {
719         return QString::fromStdString(item->variants()[variantIndex]);
720     } else if (role == AircraftPackageStatusRole) {
721         InstallRef i = item->existingInstall();
722         if (i.valid()) {
723             if (i->isDownloading()) {
724                 return PackageDownloading;
725             }
726             if (i->isQueued()) {
727                 return PackageQueued;
728             }
729             if (i->hasUpdate()) {
730                 return PackageUpdateAvailable;
731             }
732
733             return PackageInstalled;
734         } else {
735             return PackageNotInstalled;
736         }
737     } else if (role == AircraftVariantCountRole) {
738         // this value wants the number of aditional variants, i.e not
739         // including the primary. Hence the -1 term.
740         return static_cast<quint32>(item->variants().size() - 1);
741     } else if (role == AircraftThumbnailSizeRole) {
742         QPixmap pm = packageThumbnail(item, 0, false).value<QPixmap>();
743         if (pm.isNull())
744             return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
745         return pm.size();
746     } else if (role >= AircraftThumbnailRole) {
747         return packageThumbnail(item , role - AircraftThumbnailRole);
748     } else if (role == AircraftAuthorsRole) {
749         SGPropertyNode* authors = item->properties()->getChild("author");
750         if (authors) {
751             return QString::fromStdString(authors->getStringValue());
752         }
753     } else if (role == AircraftLongDescriptionRole) {
754         return QString::fromStdString(item->description()).simplified();
755     } else if (role == AircraftPackageSizeRole) {
756         return static_cast<int>(item->fileSizeBytes());
757     } else if (role == AircraftURIRole) {
758         return QUrl("package:" + QString::fromStdString(item->qualifiedVariantId(variantIndex)));
759     } else if (role == AircraftHasRatingsRole) {
760         return item->properties()->hasChild("rating");
761     } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
762         int ratingIndex = role - AircraftRatingRole;
763         SGPropertyNode* ratings = item->properties()->getChild("rating");
764         if (!ratings) {
765             return QVariant();
766         }
767         return ratings->getChild(ratingIndex)->getIntValue();
768     }
769
770     return QVariant();
771 }
772
773 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index, bool download) const
774 {
775     const string_list& thumbnails(p->thumbnailUrls());
776     if (index >= static_cast<int>(thumbnails.size())) {
777         return QVariant();
778     }
779
780     std::string thumbnailUrl = thumbnails.at(index);
781     QString urlQString(QString::fromStdString(thumbnailUrl));
782     if (m_thumbnailPixmapCache.contains(urlQString)) {
783         // cache hit, easy
784         return m_thumbnailPixmapCache.value(urlQString);
785     }
786
787 // check the on-disk store. This relies on the order of thumbnails in the
788 // results of thumbnailUrls and thumbnails corresponding
789     InstallRef ex = p->existingInstall();
790     if (ex.valid()) {
791         const string_list& thumbNames(p->thumbnails());
792         if (!thumbNames.empty()) {
793             SGPath path(ex->path());
794             path.append(p->thumbnails()[index]);
795             if (path.exists()) {
796                 QPixmap pix;
797                 pix.load(QString::fromStdString(path.str()));
798                 // resize to the standard size
799                 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
800                     pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
801                 }
802                 m_thumbnailPixmapCache[urlQString] = pix;
803                 return pix;
804             }
805         } // of have thumbnail file names
806     } // of have existing install
807
808     if (download) {
809         m_packageRoot->requestThumbnailData(thumbnailUrl);
810     }
811     return QVariant();
812 }
813
814 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
815   {
816       int row = index.row();
817       if (role == AircraftVariantRole) {
818           if (row >= m_activeVariant.size()) {
819               row -= m_activeVariant.size();
820               m_packageVariant[row] = value.toInt();
821           } else {
822               m_activeVariant[row] = value.toInt();
823           }
824
825           emit dataChanged(index, index);
826           return true;
827       }
828
829       return false;
830   }
831
832 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
833 {
834     if (uri.isEmpty()) {
835         return QModelIndex();
836     }
837
838     if (uri.isLocalFile()) {
839         QString path = uri.toLocalFile();
840         for (int row=0; row <m_items.size(); ++row) {
841             const AircraftItemPtr item(m_items.at(row));
842             if (item->path == path) {
843                 return index(row);
844             }
845
846             // check variants too
847             for (int vr=0; vr < item->variants.size(); ++vr) {
848                 if (item->variants.at(vr)->path == path) {
849                     return index(row);
850                 }
851             }
852         }
853     } else if (uri.scheme() == "package") {
854         QString ident = uri.path();
855         int rowOffset = m_items.size();
856
857         PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
858         if (pkg) {
859             for (size_t i=0; i < m_packages.size(); ++i) {
860                 if (m_packages[i] == pkg) {
861                     return index(rowOffset + i);
862                 }
863             } // of linear package scan
864         }
865     } else if (uri.scheme() == "") {
866         // Empty URI scheme (no selection), nothing to do
867     } else {
868         qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
869     }
870
871     return QModelIndex();
872 }
873
874 void AircraftItemModel::onScanResults()
875 {
876     QVector<AircraftItemPtr> newItems = m_scanThread->items();
877     if (newItems.isEmpty())
878         return;
879
880     int firstRow = m_items.count();
881     int lastRow = firstRow + newItems.count() - 1;
882     beginInsertRows(QModelIndex(), firstRow, lastRow);
883     m_items+=newItems;
884
885     // default variants in all cases
886     for (int i=0; i< newItems.count(); ++i) {
887         m_activeVariant.append(0);
888     }
889     endInsertRows();
890 }
891
892 void AircraftItemModel::onScanFinished()
893 {
894     delete m_scanThread;
895     m_scanThread = NULL;
896     emit scanCompleted();
897 }
898
899 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
900 {
901     QString msg;
902     switch (reason) {
903         case Delegate::FAIL_CHECKSUM:
904             msg = tr("Invalid package checksum"); break;
905         case Delegate::FAIL_DOWNLOAD:
906             msg = tr("Download failed"); break;
907         case Delegate::FAIL_EXTRACT:
908             msg = tr("Package could not be extracted"); break;
909         case Delegate::FAIL_FILESYSTEM:
910             msg = tr("A local file-system error occurred"); break;
911         case Delegate::FAIL_NOT_FOUND:
912             msg = tr("Package file missing from download server"); break;
913         case Delegate::FAIL_UNKNOWN:
914         default:
915             msg = tr("Unknown reason");
916     }
917
918     emit aircraftInstallFailed(index, msg);
919 }
920
921 void AircraftItemModel::installSucceeded(QModelIndex index)
922 {
923     emit aircraftInstallCompleted(index);
924 }
925
926 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
927 {
928     if (index.row() < m_items.size()) {
929         return true; // local file, always runnable
930     }
931
932     quint32 packageIndex = index.row() - m_items.size();
933     const PackageRef& pkg(m_packages[packageIndex]);
934     InstallRef ex = pkg->existingInstall();
935     if (!ex.valid()) {
936         return false; // not installed
937     }
938
939     return !ex->isDownloading();
940 }
941
942 bool AircraftItemModel::isCandidateAircraftPath(QString path)
943 {
944     QStringList filters;
945     filters << "*-set.xml";
946     int dirCount = 0,
947         setXmlCount = 0;
948
949     QDir d(path);
950     Q_FOREACH(QFileInfo child, d.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
951         QDir childDir(child.absoluteFilePath());
952         ++dirCount;
953         Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
954             ++setXmlCount;
955         }
956
957         if ((setXmlCount > 0) || (dirCount > 10)) {
958             break;
959         }
960     }
961
962     return (setXmlCount > 0);
963 }
964
965
966 #include "AircraftModel.moc"