1 // AircraftModel.cxx - part of GUI launcher using Qt5
3 // Written by James Turner, started March 2015.
5 // Copyright (C) 2015 James Turner <zakalawe@mac.com>
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.
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.
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.
21 #include "AircraftModel.hxx"
26 #include <QMutexLocker>
27 #include <QDataStream>
30 #include <QSharedPointer>
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>
41 #include <Main/globals.hxx>
44 const int STANDARD_THUMBNAIL_HEIGHT = 128;
45 const int STANDARD_THUMBNAIL_WIDTH = 172;
47 using namespace simgear::pkg;
49 AircraftItem::AircraftItem() :
54 // oh for C++11 initialisers
55 for (int i=0; i<4; ++i) ratings[i] = 0;
58 AircraftItem::AircraftItem(QDir dir, QString filePath) :
63 for (int i=0; i<4; ++i) ratings[i] = 0;
66 readProperties(filePath.toStdString(), &root);
68 if (!root.hasChild("sim")) {
69 throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
72 SGPropertyNode_ptr sim = root.getNode("sim");
75 pathModTime = QFileInfo(path).lastModified();
76 if (sim->getBoolValue("exclude-from-gui", false)) {
81 description = sim->getStringValue("description");
82 authors = sim->getStringValue("author");
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");
93 if (sim->hasChild("variant-of")) {
94 variantOf = sim->getStringValue("variant-of");
97 if (sim->hasChild("tags")) {
98 SGPropertyNode_ptr tagsNode = sim->getChild("tags");
99 int nChildren = tagsNode->nChildren();
100 for (int i = 0; i < nChildren; i++) {
101 const SGPropertyNode* c = tagsNode->getChild(i);
102 if (strcmp(c->getName(), "tag") == 0) {
103 const char* tagName = c->getStringValue();
104 usesHeliports |= (strcmp(tagName, "helicopter") == 0);
105 // could also consider vtol tag?
106 usesSeaports |= (strcmp(tagName, "seaplane") == 0);
107 usesSeaports |= (strcmp(tagName, "floats") == 0);
109 } // of tags iteration
110 } // of set-xml has tags
113 QString AircraftItem::baseName() const
115 QString fn = QFileInfo(path).fileName();
116 fn.truncate(fn.count() - 8);
120 void AircraftItem::fromDataStream(QDataStream& ds)
122 ds >> path >> pathModTime >> excluded;
127 ds >> description >> authors >> variantOf;
128 for (int i=0; i<4; ++i) ds >> ratings[i];
131 void AircraftItem::toDataStream(QDataStream& ds) const
133 ds << path << pathModTime << excluded;
138 ds << description << authors << variantOf;
139 for (int i=0; i<4; ++i) ds << ratings[i];
142 QPixmap AircraftItem::thumbnail() const
144 if (m_thumbnail.isNull()) {
145 QFileInfo info(path);
146 QDir dir = info.dir();
147 if (dir.exists("thumbnail.jpg")) {
148 m_thumbnail.load(dir.filePath("thumbnail.jpg"));
149 // resize to the standard size
150 if (m_thumbnail.height() > STANDARD_THUMBNAIL_HEIGHT) {
151 m_thumbnail = m_thumbnail.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
160 static quint32 CACHE_VERSION = 3;
162 class AircraftScanThread : public QThread
166 AircraftScanThread(QStringList dirsToScan) :
172 ~AircraftScanThread()
176 /** thread-safe access to items already scanned */
177 QVector<AircraftItemPtr> items()
179 QVector<AircraftItemPtr> result;
180 QMutexLocker g(&m_lock);
181 result.swap(m_items);
198 Q_FOREACH(QString d, m_dirs) {
199 scanAircraftDir(QDir(d));
212 QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
213 if (!cacheData.isEmpty()) {
214 QDataStream ds(cacheData);
215 quint32 count, cacheVersion;
216 ds >> cacheVersion >> count;
218 if (cacheVersion != CACHE_VERSION) {
219 return; // mis-matched cache, version, drop
222 for (quint32 i=0; i<count; ++i) {
223 AircraftItemPtr item(new AircraftItem);
224 item->fromDataStream(ds);
226 QFileInfo finfo(item->path);
227 if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) {
228 // corresponding -set.xml file still exists and is
230 m_cachedItems[item->path] = item;
232 } // of cached item iteration
239 QByteArray cacheData;
241 QDataStream ds(&cacheData, QIODevice::WriteOnly);
242 quint32 count = m_nextCache.count();
243 ds << CACHE_VERSION << count;
245 Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) {
246 item->toDataStream(ds);
250 settings.setValue("aircraft-cache", cacheData);
253 void scanAircraftDir(QDir path)
259 filters << "*-set.xml";
260 Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
261 QDir childDir(child.absoluteFilePath());
262 QMap<QString, AircraftItemPtr> baseAircraft;
263 QList<AircraftItemPtr> variants;
265 Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
267 QString absolutePath = xmlChild.absoluteFilePath();
268 AircraftItemPtr item;
270 if (m_cachedItems.contains(absolutePath)) {
271 item = m_cachedItems.value(absolutePath);
273 item = AircraftItemPtr(new AircraftItem(childDir, absolutePath));
276 m_nextCache[absolutePath] = item;
278 if (item->excluded) {
282 if (item->variantOf.isNull()) {
283 baseAircraft.insert(item->baseName(), item);
285 variants.append(item);
287 } catch (sg_exception& e) {
294 } // of set.xml iteration
296 // bind variants to their principals
297 Q_FOREACH(AircraftItemPtr item, variants) {
298 if (!baseAircraft.contains(item->variantOf)) {
299 qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
303 baseAircraft.value(item->variantOf)->variants.append(item);
306 // lock mutex while we modify the items array
308 QMutexLocker g(&m_lock);
309 m_items+=(baseAircraft.values().toVector());
313 } // of subdir iteration
318 QVector<AircraftItemPtr> m_items;
320 QMap<QString, AircraftItemPtr > m_cachedItems;
321 QMap<QString, AircraftItemPtr > m_nextCache;
326 class PackageDelegate : public simgear::pkg::Delegate
329 PackageDelegate(AircraftItemModel* model) :
332 m_model->m_packageRoot->addDelegate(this);
337 m_model->m_packageRoot->removeDelegate(this);
341 virtual void catalogRefreshed(CatalogRef aCatalog, StatusCode aReason)
343 if (aReason == STATUS_IN_PROGRESS) {
344 qDebug() << "doing refresh of" << QString::fromStdString(aCatalog->url());
345 } else if ((aReason == STATUS_REFRESHED) || (aReason == STATUS_SUCCESS)) {
346 m_model->refreshPackages();
348 qWarning() << "failed refresh of "
349 << QString::fromStdString(aCatalog->url()) << ":" << aReason << endl;
353 virtual void startInstall(InstallRef aInstall)
355 QModelIndex mi(indexForPackage(aInstall->package()));
356 m_model->dataChanged(mi, mi);
359 virtual void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total)
363 QModelIndex mi(indexForPackage(aInstall->package()));
364 m_model->dataChanged(mi, mi);
367 virtual void finishInstall(InstallRef aInstall, StatusCode aReason)
369 QModelIndex mi(indexForPackage(aInstall->package()));
370 m_model->dataChanged(mi, mi);
372 if ((aReason != USER_CANCELLED) && (aReason != STATUS_SUCCESS)) {
373 m_model->installFailed(mi, aReason);
376 if (aReason == STATUS_SUCCESS) {
377 m_model->installSucceeded(mi);
381 virtual void availablePackagesChanged() Q_DECL_OVERRIDE
383 m_model->refreshPackages();
386 virtual void dataForThumbnail(const std::string& aThumbnailUrl,
387 size_t length, const uint8_t* bytes)
389 QImage img = QImage::fromData(QByteArray::fromRawData(reinterpret_cast<const char*>(bytes), length));
391 qWarning() << "failed to load image data for URL:" <<
392 QString::fromStdString(aThumbnailUrl);
396 QPixmap pix = QPixmap::fromImage(img);
397 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
398 pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
400 m_model->m_thumbnailPixmapCache.insert(QString::fromStdString(aThumbnailUrl),
403 // notify any affected items. Linear scan here avoids another map/dict
405 PackageList::const_iterator it;
408 for (it=m_model->m_packages.begin(); it != m_model->m_packages.end(); ++it, ++i) {
409 const string_list& urls((*it)->thumbnailUrls());
410 string_list::const_iterator cit = std::find(urls.begin(), urls.end(), aThumbnailUrl);
411 if (cit != urls.end()) {
412 QModelIndex mi(m_model->index(i + m_model->m_items.size()));
413 m_model->dataChanged(mi, mi);
415 } // of packages iteration
419 QModelIndex indexForPackage(const PackageRef& ref) const
421 PackageList::const_iterator it = std::find(m_model->m_packages.begin(),
422 m_model->m_packages.end(),
424 if (it == m_model->m_packages.end()) {
425 return QModelIndex();
428 size_t offset = it - m_model->m_packages.begin();
429 return m_model->index(offset + m_model->m_items.size());
432 AircraftItemModel* m_model;
435 AircraftItemModel::AircraftItemModel(QObject* pr, const simgear::pkg::RootRef& rootRef) :
436 QAbstractListModel(pr),
438 m_packageRoot(rootRef)
440 m_delegate = new PackageDelegate(this);
441 // packages may already be refreshed, so pull now
445 AircraftItemModel::~AircraftItemModel()
447 abandonCurrentScan();
451 void AircraftItemModel::setPaths(QStringList paths)
456 void AircraftItemModel::scanDirs()
458 abandonCurrentScan();
462 m_activeVariant.clear();
465 QStringList dirs = m_paths;
467 Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
468 dirs << QString::fromStdString(ap);
471 SGPath rootAircraft(globals->get_fg_root());
472 rootAircraft.append("Aircraft");
473 dirs << QString::fromStdString(rootAircraft.str());
475 m_scanThread = new AircraftScanThread(dirs);
476 connect(m_scanThread, &AircraftScanThread::finished, this,
477 &AircraftItemModel::onScanFinished);
478 connect(m_scanThread, &AircraftScanThread::addedItems,
479 this, &AircraftItemModel::onScanResults);
480 m_scanThread->start();
483 void AircraftItemModel::abandonCurrentScan()
486 m_scanThread->setDone();
487 m_scanThread->wait(1000);
493 void AircraftItemModel::refreshPackages()
496 m_packages = m_packageRoot->allPackages();
497 m_packageVariant.resize(m_packages.size());
501 int AircraftItemModel::rowCount(const QModelIndex& parent) const
503 return m_items.size() + m_packages.size();
506 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
508 if (index.row() >= m_items.size()) {
509 quint32 packageIndex = index.row() - m_items.size();
511 if (role == AircraftVariantRole) {
512 return m_packageVariant.at(packageIndex);
515 const PackageRef& pkg(m_packages[packageIndex]);
516 InstallRef ex = pkg->existingInstall();
518 if (role == AircraftInstallPercentRole) {
519 return ex.valid() ? ex->downloadedPercent() : 0;
520 } else if (role == AircraftInstallDownloadedSizeRole) {
521 return static_cast<quint64>(ex.valid() ? ex->downloadedBytes() : 0);
524 quint32 variantIndex = m_packageVariant.at(packageIndex);
525 return dataFromPackage(pkg, variantIndex, role);
527 if (role == AircraftVariantRole) {
528 return m_activeVariant.at(index.row());
531 quint32 variantIndex = m_activeVariant.at(index.row());
532 const AircraftItemPtr item(m_items.at(index.row()));
533 return dataFromItem(item, variantIndex, role);
537 QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, quint32 variantIndex, int role) const
539 if (role == AircraftVariantCountRole) {
540 return item->variants.count();
543 if (role == AircraftThumbnailCountRole) {
544 QPixmap p = item->thumbnail();
545 return p.isNull() ? 0 : 1;
548 if (role == AircraftThumbnailSizeRole) {
549 return item->thumbnail().size();
552 if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
553 int variantIndex = role - AircraftVariantDescriptionRole;
554 return item->variants.at(variantIndex)->description;
558 if (variantIndex <= static_cast<quint32>(item->variants.count())) {
559 // show the selected variant
560 item = item->variants.at(variantIndex - 1);
564 if (role == Qt::DisplayRole) {
565 if (item->description.isEmpty()) {
566 return tr("Missing description for: %1").arg(item->baseName());
569 return item->description;
570 } else if (role == Qt::DecorationRole) {
571 return item->thumbnail();
572 } else if (role == AircraftPathRole) {
574 } else if (role == AircraftAuthorsRole) {
575 return item->authors;
576 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
577 return item->ratings[role - AircraftRatingRole];
578 } else if (role >= AircraftThumbnailRole) {
579 return item->thumbnail();
580 } else if (role == AircraftPackageIdRole) {
581 // can we fake an ID? otherwise fall through to a null variant
582 } else if (role == AircraftPackageStatusRole) {
583 return PackageInstalled; // always the case
584 } else if (role == Qt::ToolTipRole) {
586 } else if (role == AircraftURIRole) {
587 return QUrl::fromLocalFile(item->path);
588 } else if (role == AircraftHasRatingsRole) {
590 for (int i=0; i<4; ++i) {
591 have |= (item->ratings[i] > 0);
594 } else if (role == AircraftLongDescriptionRole) {
596 return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
597 "no escape from reality. Open your eyes, like up to the skies and see. "
598 "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
599 "Litte high, little low. Anywhere the wind blows.";
601 } else if (role == AircraftIsHelicopterRole) {
602 return item->usesHeliports;
603 } else if (role == AircraftIsSeaplaneRole) {
604 return item->usesSeaports;
610 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
612 if (role == Qt::DecorationRole) {
613 role = AircraftThumbnailRole; // use first thumbnail
616 if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
617 int variantIndex = role - AircraftVariantDescriptionRole;
618 return QString::fromStdString(item->nameForVariant(variantIndex));
621 if (role == Qt::DisplayRole) {
622 return QString::fromStdString(item->nameForVariant(variantIndex));
623 } else if (role == AircraftPathRole) {
624 InstallRef i = item->existingInstall();
626 return QString::fromStdString(i->primarySetPath().str());
628 } else if (role == AircraftPackageIdRole) {
629 return QString::fromStdString(item->variants()[variantIndex]);
630 } else if (role == AircraftPackageStatusRole) {
631 InstallRef i = item->existingInstall();
633 if (i->isDownloading()) {
634 return PackageDownloading;
637 return PackageQueued;
639 if (i->hasUpdate()) {
640 return PackageUpdateAvailable;
643 return PackageInstalled;
645 return PackageNotInstalled;
647 } else if (role == AircraftVariantCountRole) {
648 // this value wants the number of aditional variants, i.e not
649 // including the primary. Hence the -1 term.
650 return static_cast<quint32>(item->variants().size() - 1);
651 } else if (role == AircraftThumbnailSizeRole) {
652 QPixmap pm = packageThumbnail(item, 0, false).value<QPixmap>();
654 return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
656 } else if (role >= AircraftThumbnailRole) {
657 return packageThumbnail(item , role - AircraftThumbnailRole);
658 } else if (role == AircraftAuthorsRole) {
659 SGPropertyNode* authors = item->properties()->getChild("author");
661 return QString::fromStdString(authors->getStringValue());
663 } else if (role == AircraftLongDescriptionRole) {
664 return QString::fromStdString(item->description());
665 } else if (role == AircraftPackageSizeRole) {
666 return static_cast<int>(item->fileSizeBytes());
667 } else if (role == AircraftURIRole) {
668 return QUrl("package:" + QString::fromStdString(item->qualifiedVariantId(variantIndex)));
669 } else if (role == AircraftHasRatingsRole) {
670 return item->properties()->hasChild("rating");
671 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
672 int ratingIndex = role - AircraftRatingRole;
673 SGPropertyNode* ratings = item->properties()->getChild("rating");
677 return ratings->getChild(ratingIndex)->getIntValue();
683 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index, bool download) const
685 const string_list& thumbnails(p->thumbnailUrls());
686 if (index >= static_cast<int>(thumbnails.size())) {
690 std::string thumbnailUrl = thumbnails.at(index);
691 QString urlQString(QString::fromStdString(thumbnailUrl));
692 if (m_thumbnailPixmapCache.contains(urlQString)) {
694 return m_thumbnailPixmapCache.value(urlQString);
697 // check the on-disk store. This relies on the order of thumbnails in the
698 // results of thumbnailUrls and thumbnails corresponding
699 InstallRef ex = p->existingInstall();
701 const string_list& thumbNames(p->thumbnails());
702 if (!thumbNames.empty()) {
703 SGPath path(ex->path());
704 path.append(p->thumbnails()[index]);
707 pix.load(QString::fromStdString(path.str()));
708 // resize to the standard size
709 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
710 pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
712 m_thumbnailPixmapCache[urlQString] = pix;
715 } // of have thumbnail file names
716 } // of have existing install
719 m_packageRoot->requestThumbnailData(thumbnailUrl);
724 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
726 int row = index.row();
727 if (role == AircraftVariantRole) {
728 if (row >= m_activeVariant.size()) {
729 row -= m_activeVariant.size();
730 m_packageVariant[row] = value.toInt();
732 m_activeVariant[row] = value.toInt();
735 emit dataChanged(index, index);
742 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
744 if (uri.isLocalFile()) {
745 QString path = uri.toLocalFile();
746 for (int row=0; row <m_items.size(); ++row) {
747 const AircraftItemPtr item(m_items.at(row));
748 if (item->path == path) {
752 } else if (uri.scheme() == "package") {
753 QString ident = uri.path();
754 PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
756 for (size_t i=0; i < m_packages.size(); ++i) {
757 if (m_packages[i] == pkg) {
758 return index(m_items.size() + i);
760 } // of linear package scan
763 qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
766 return QModelIndex();
769 void AircraftItemModel::onScanResults()
771 QVector<AircraftItemPtr> newItems = m_scanThread->items();
772 if (newItems.isEmpty())
775 int firstRow = m_items.count();
776 int lastRow = firstRow + newItems.count() - 1;
777 beginInsertRows(QModelIndex(), firstRow, lastRow);
780 // default variants in all cases
781 for (int i=0; i< newItems.count(); ++i) {
782 m_activeVariant.append(0);
787 void AircraftItemModel::onScanFinished()
791 emit scanCompleted();
794 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
796 Q_ASSERT(index.row() >= m_items.size());
800 case Delegate::FAIL_CHECKSUM:
801 msg = tr("Invalid package checksum"); break;
802 case Delegate::FAIL_DOWNLOAD:
803 msg = tr("Download failed"); break;
804 case Delegate::FAIL_EXTRACT:
805 msg = tr("Package could not be extracted"); break;
806 case Delegate::FAIL_FILESYSTEM:
807 msg = tr("A local file-system error occurred"); break;
808 case Delegate::FAIL_NOT_FOUND:
809 msg = tr("Package file missing from download server"); break;
810 case Delegate::FAIL_UNKNOWN:
812 msg = tr("Unknown reason");
815 emit aircraftInstallFailed(index, msg);
818 void AircraftItemModel::installSucceeded(QModelIndex index)
820 emit aircraftInstallCompleted(index);
823 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
825 if (index.row() < m_items.size()) {
826 return true; // local file, always runnable
829 quint32 packageIndex = index.row() - m_items.size();
830 const PackageRef& pkg(m_packages[packageIndex]);
831 InstallRef ex = pkg->existingInstall();
833 return false; // not installed
836 return !ex->isDownloading();
839 #include "AircraftModel.moc"