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() :
52 // oh for C++11 initialisers
53 for (int i=0; i<4; ++i) ratings[i] = 0;
56 AircraftItem::AircraftItem(QDir dir, QString filePath) :
59 for (int i=0; i<4; ++i) ratings[i] = 0;
62 readProperties(filePath.toStdString(), &root);
64 if (!root.hasChild("sim")) {
65 throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
68 SGPropertyNode_ptr sim = root.getNode("sim");
71 pathModTime = QFileInfo(path).lastModified();
72 if (sim->getBoolValue("exclude-from-gui", false)) {
77 description = sim->getStringValue("description");
78 authors = sim->getStringValue("author");
80 if (sim->hasChild("rating")) {
81 SGPropertyNode_ptr ratingsNode = sim->getNode("rating");
82 ratings[0] = ratingsNode->getIntValue("FDM");
83 ratings[1] = ratingsNode->getIntValue("systems");
84 ratings[2] = ratingsNode->getIntValue("cockpit");
85 ratings[3] = ratingsNode->getIntValue("model");
89 if (sim->hasChild("variant-of")) {
90 variantOf = sim->getStringValue("variant-of");
94 QString AircraftItem::baseName() const
96 QString fn = QFileInfo(path).fileName();
97 fn.truncate(fn.count() - 8);
101 void AircraftItem::fromDataStream(QDataStream& ds)
103 ds >> path >> pathModTime >> excluded;
108 ds >> description >> authors >> variantOf;
109 for (int i=0; i<4; ++i) ds >> ratings[i];
112 void AircraftItem::toDataStream(QDataStream& ds) const
114 ds << path << pathModTime << excluded;
119 ds << description << authors << variantOf;
120 for (int i=0; i<4; ++i) ds << ratings[i];
123 QPixmap AircraftItem::thumbnail() const
125 if (m_thumbnail.isNull()) {
126 QFileInfo info(path);
127 QDir dir = info.dir();
128 if (dir.exists("thumbnail.jpg")) {
129 m_thumbnail.load(dir.filePath("thumbnail.jpg"));
130 // resize to the standard size
131 if (m_thumbnail.height() > STANDARD_THUMBNAIL_HEIGHT) {
132 m_thumbnail = m_thumbnail.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
141 static int CACHE_VERSION = 3;
143 class AircraftScanThread : public QThread
147 AircraftScanThread(QStringList dirsToScan) :
153 ~AircraftScanThread()
157 /** thread-safe access to items already scanned */
158 QVector<AircraftItemPtr> items()
160 QVector<AircraftItemPtr> result;
161 QMutexLocker g(&m_lock);
162 result.swap(m_items);
179 Q_FOREACH(QString d, m_dirs) {
180 scanAircraftDir(QDir(d));
193 QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
194 if (!cacheData.isEmpty()) {
195 QDataStream ds(cacheData);
196 quint32 count, cacheVersion;
197 ds >> cacheVersion >> count;
199 if (cacheVersion != CACHE_VERSION) {
200 return; // mis-matched cache, version, drop
203 for (int i=0; i<count; ++i) {
204 AircraftItemPtr item(new AircraftItem);
205 item->fromDataStream(ds);
207 QFileInfo finfo(item->path);
208 if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) {
209 // corresponding -set.xml file still exists and is
211 m_cachedItems[item->path] = item;
213 } // of cached item iteration
220 QByteArray cacheData;
222 QDataStream ds(&cacheData, QIODevice::WriteOnly);
223 quint32 count = m_nextCache.count();
224 ds << CACHE_VERSION << count;
226 Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) {
227 item->toDataStream(ds);
231 settings.setValue("aircraft-cache", cacheData);
234 void scanAircraftDir(QDir path)
240 filters << "*-set.xml";
241 Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
242 QDir childDir(child.absoluteFilePath());
243 QMap<QString, AircraftItemPtr> baseAircraft;
244 QList<AircraftItemPtr> variants;
246 Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
248 QString absolutePath = xmlChild.absoluteFilePath();
249 AircraftItemPtr item;
251 if (m_cachedItems.contains(absolutePath)) {
252 item = m_cachedItems.value(absolutePath);
254 item = AircraftItemPtr(new AircraftItem(childDir, absolutePath));
257 m_nextCache[absolutePath] = item;
259 if (item->excluded) {
263 if (item->variantOf.isNull()) {
264 baseAircraft.insert(item->baseName(), item);
266 variants.append(item);
268 } catch (sg_exception& e) {
275 } // of set.xml iteration
277 // bind variants to their principals
278 Q_FOREACH(AircraftItemPtr item, variants) {
279 if (!baseAircraft.contains(item->variantOf)) {
280 qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
284 baseAircraft.value(item->variantOf)->variants.append(item);
287 // lock mutex while we modify the items array
289 QMutexLocker g(&m_lock);
290 m_items+=(baseAircraft.values().toVector());
294 } // of subdir iteration
299 QVector<AircraftItemPtr> m_items;
301 QMap<QString, AircraftItemPtr > m_cachedItems;
302 QMap<QString, AircraftItemPtr > m_nextCache;
307 class PackageDelegate : public simgear::pkg::Delegate
310 PackageDelegate(AircraftItemModel* model) :
313 m_model->m_packageRoot->addDelegate(this);
318 m_model->m_packageRoot->removeDelegate(this);
322 virtual void catalogRefreshed(CatalogRef aCatalog, StatusCode aReason)
324 if (aReason == STATUS_IN_PROGRESS) {
325 qDebug() << "doing refresh of" << QString::fromStdString(aCatalog->url());
326 } else if ((aReason == STATUS_REFRESHED) || (aReason == STATUS_SUCCESS)) {
327 m_model->refreshPackages();
329 qWarning() << "failed refresh of "
330 << QString::fromStdString(aCatalog->url()) << ":" << aReason << endl;
334 virtual void startInstall(InstallRef aInstall)
336 QModelIndex mi(indexForPackage(aInstall->package()));
337 m_model->dataChanged(mi, mi);
340 virtual void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total)
344 QModelIndex mi(indexForPackage(aInstall->package()));
345 m_model->dataChanged(mi, mi);
348 virtual void finishInstall(InstallRef aInstall, StatusCode aReason)
350 QModelIndex mi(indexForPackage(aInstall->package()));
351 m_model->dataChanged(mi, mi);
353 if ((aReason != USER_CANCELLED) && (aReason != STATUS_SUCCESS)) {
354 m_model->installFailed(mi, aReason);
357 if (aReason == STATUS_SUCCESS) {
358 m_model->installSucceeded(mi);
362 virtual void dataForThumbnail(const std::string& aThumbnailUrl,
363 size_t length, const uint8_t* bytes)
365 QImage img = QImage::fromData(QByteArray::fromRawData(reinterpret_cast<const char*>(bytes), length));
367 qWarning() << "failed to load image data for URL:" <<
368 QString::fromStdString(aThumbnailUrl);
372 QPixmap pix = QPixmap::fromImage(img);
373 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
374 pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
376 m_model->m_thumbnailPixmapCache.insert(QString::fromStdString(aThumbnailUrl),
379 // notify any affected items. Linear scan here avoids another map/dict
381 PackageList::const_iterator it;
384 for (it=m_model->m_packages.begin(); it != m_model->m_packages.end(); ++it, ++i) {
385 const string_list& urls((*it)->thumbnailUrls());
386 string_list::const_iterator cit = std::find(urls.begin(), urls.end(), aThumbnailUrl);
387 if (cit != urls.end()) {
388 QModelIndex mi(m_model->index(i + m_model->m_items.size()));
389 m_model->dataChanged(mi, mi);
391 } // of packages iteration
395 QModelIndex indexForPackage(const PackageRef& ref) const
397 PackageList::const_iterator it = std::find(m_model->m_packages.begin(),
398 m_model->m_packages.end(),
400 if (it == m_model->m_packages.end()) {
401 return QModelIndex();
404 size_t offset = it - m_model->m_packages.begin();
405 return m_model->index(offset + m_model->m_items.size());
408 AircraftItemModel* m_model;
411 AircraftItemModel::AircraftItemModel(QObject* pr, const simgear::pkg::RootRef& rootRef) :
412 QAbstractListModel(pr),
414 m_packageRoot(rootRef)
416 m_delegate = new PackageDelegate(this);
417 // packages may already be refreshed, so pull now
421 AircraftItemModel::~AircraftItemModel()
423 abandonCurrentScan();
427 void AircraftItemModel::setPaths(QStringList paths)
432 void AircraftItemModel::scanDirs()
434 abandonCurrentScan();
438 m_activeVariant.clear();
441 QStringList dirs = m_paths;
443 Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
444 dirs << QString::fromStdString(ap);
447 SGPath rootAircraft(globals->get_fg_root());
448 rootAircraft.append("Aircraft");
449 dirs << QString::fromStdString(rootAircraft.str());
451 m_scanThread = new AircraftScanThread(dirs);
452 connect(m_scanThread, &AircraftScanThread::finished, this,
453 &AircraftItemModel::onScanFinished);
454 connect(m_scanThread, &AircraftScanThread::addedItems,
455 this, &AircraftItemModel::onScanResults);
456 m_scanThread->start();
459 void AircraftItemModel::abandonCurrentScan()
462 m_scanThread->setDone();
463 m_scanThread->wait(1000);
469 void AircraftItemModel::refreshPackages()
472 m_packages = m_packageRoot->allPackages();
473 m_packageVariant.resize(m_packages.size());
477 int AircraftItemModel::rowCount(const QModelIndex& parent) const
479 return m_items.size() + m_packages.size();
482 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
484 if (index.row() >= m_items.size()) {
485 quint32 packageIndex = index.row() - m_items.size();
487 if (role == AircraftVariantRole) {
488 return m_packageVariant.at(packageIndex);
491 const PackageRef& pkg(m_packages[packageIndex]);
492 InstallRef ex = pkg->existingInstall();
494 if (role == AircraftInstallPercentRole) {
495 return ex.valid() ? ex->downloadedPercent() : 0;
496 } else if (role == AircraftInstallDownloadedSizeRole) {
497 return static_cast<quint64>(ex.valid() ? ex->downloadedBytes() : 0);
500 quint32 variantIndex = m_packageVariant.at(packageIndex);
501 return dataFromPackage(pkg, variantIndex, role);
503 if (role == AircraftVariantRole) {
504 return m_activeVariant.at(index.row());
507 quint32 variantIndex = m_activeVariant.at(index.row());
508 const AircraftItemPtr item(m_items.at(index.row()));
509 return dataFromItem(item, variantIndex, role);
513 QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, quint32 variantIndex, int role) const
515 if (role == AircraftVariantCountRole) {
516 return item->variants.count();
519 if (role == AircraftThumbnailCountRole) {
520 QPixmap p = item->thumbnail();
521 return p.isNull() ? 0 : 1;
524 if (role == AircraftThumbnailSizeRole) {
525 return item->thumbnail().size();
528 if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
529 int variantIndex = role - AircraftVariantDescriptionRole;
530 return item->variants.at(variantIndex)->description;
534 if (variantIndex <= item->variants.count()) {
535 // show the selected variant
536 item = item->variants.at(variantIndex - 1);
540 if (role == Qt::DisplayRole) {
541 if (item->description.isEmpty()) {
542 return tr("Missing description for: %1").arg(item->baseName());
545 return item->description;
546 } else if (role == Qt::DecorationRole) {
547 return item->thumbnail();
548 } else if (role == AircraftPathRole) {
550 } else if (role == AircraftAuthorsRole) {
551 return item->authors;
552 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
553 return item->ratings[role - AircraftRatingRole];
554 } else if (role >= AircraftThumbnailRole) {
555 return item->thumbnail();
556 } else if (role == AircraftPackageIdRole) {
557 // can we fake an ID? otherwise fall through to a null variant
558 } else if (role == AircraftPackageStatusRole) {
559 return PackageInstalled; // always the case
560 } else if (role == Qt::ToolTipRole) {
562 } else if (role == AircraftURIRole) {
563 return QUrl::fromLocalFile(item->path);
564 } else if (role == AircraftHasRatingsRole) {
566 for (int i=0; i<4; ++i) {
567 have |= (item->ratings[i] > 0);
570 } else if (role == AircraftLongDescriptionRole) {
572 return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
573 "no escape from reality. Open your eyes, like up to the skies and see. "
574 "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
575 "Litte high, little low. Anywhere the wind blows.";
582 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
584 if (role == Qt::DecorationRole) {
585 role = AircraftThumbnailRole; // use first thumbnail
588 if (role == Qt::DisplayRole) {
589 return QString::fromStdString(item->name());
590 } else if (role == AircraftPathRole) {
591 InstallRef i = item->existingInstall();
593 return QString::fromStdString(i->primarySetPath().str());
595 } else if (role == AircraftPackageIdRole) {
596 return QString::fromStdString(item->id());
597 } else if (role == AircraftPackageStatusRole) {
598 InstallRef i = item->existingInstall();
600 if (i->isDownloading()) {
601 return PackageDownloading;
604 return PackageQueued;
606 if (i->hasUpdate()) {
607 return PackageUpdateAvailable;
610 return PackageInstalled;
612 return PackageNotInstalled;
614 } else if (role == AircraftThumbnailSizeRole) {
615 QPixmap pm = packageThumbnail(item, 0, false).value<QPixmap>();
617 return QSize(STANDARD_THUMBNAIL_WIDTH, STANDARD_THUMBNAIL_HEIGHT);
619 } else if (role >= AircraftThumbnailRole) {
620 return packageThumbnail(item , role - AircraftThumbnailRole);
621 } else if (role == AircraftAuthorsRole) {
622 SGPropertyNode* authors = item->properties()->getChild("author");
624 return QString::fromStdString(authors->getStringValue());
626 } else if (role == AircraftLongDescriptionRole) {
627 return QString::fromStdString(item->description());
628 } else if (role == AircraftPackageSizeRole) {
629 return static_cast<int>(item->fileSizeBytes());
630 } else if (role == AircraftURIRole) {
631 return QUrl("package:" + QString::fromStdString(item->qualifiedId()));
632 } else if (role == AircraftHasRatingsRole) {
633 return item->properties()->hasChild("rating");
634 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
635 int ratingIndex = role - AircraftRatingRole;
636 SGPropertyNode* ratings = item->properties()->getChild("rating");
640 return ratings->getChild(ratingIndex)->getIntValue();
646 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index, bool download) const
648 const string_list& thumbnails(p->thumbnailUrls());
649 if (index >= thumbnails.size()) {
653 std::string thumbnailUrl = thumbnails.at(index);
654 QString urlQString(QString::fromStdString(thumbnailUrl));
655 if (m_thumbnailPixmapCache.contains(urlQString)) {
657 return m_thumbnailPixmapCache.value(urlQString);
660 // check the on-disk store. This relies on the order of thumbnails in the
661 // results of thumbnailUrls and thumbnails corresponding
662 InstallRef ex = p->existingInstall();
664 const string_list& thumbNames(p->thumbnails());
665 if (!thumbNames.empty()) {
666 SGPath path(ex->path());
667 path.append(p->thumbnails()[index]);
670 pix.load(QString::fromStdString(path.str()));
671 // resize to the standard size
672 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
673 pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
675 m_thumbnailPixmapCache[urlQString] = pix;
678 } // of have thumbnail file names
679 } // of have existing install
682 m_packageRoot->requestThumbnailData(thumbnailUrl);
687 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
689 if (role == AircraftVariantRole) {
690 m_activeVariant[index.row()] = value.toInt();
691 emit dataChanged(index, index);
698 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
700 if (uri.isLocalFile()) {
701 QString path = uri.toLocalFile();
702 for (int row=0; row <m_items.size(); ++row) {
703 const AircraftItemPtr item(m_items.at(row));
704 if (item->path == path) {
708 } else if (uri.scheme() == "package") {
709 QString ident = uri.path();
710 PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
712 for (int i=0; i < m_packages.size(); ++i) {
713 if (m_packages[i] == pkg) {
714 return index(m_items.size() + i);
716 } // of linear package scan
719 qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
722 return QModelIndex();
725 void AircraftItemModel::onScanResults()
727 QVector<AircraftItemPtr> newItems = m_scanThread->items();
728 if (newItems.isEmpty())
731 int firstRow = m_items.count();
732 int lastRow = firstRow + newItems.count() - 1;
733 beginInsertRows(QModelIndex(), firstRow, lastRow);
736 // default variants in all cases
737 for (int i=0; i< newItems.count(); ++i) {
738 m_activeVariant.append(0);
743 void AircraftItemModel::onScanFinished()
749 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
751 Q_ASSERT(index.row() >= m_items.size());
755 case Delegate::FAIL_CHECKSUM:
756 msg = tr("Invalid package checksum"); break;
757 case Delegate::FAIL_DOWNLOAD:
758 msg = tr("Download failed"); break;
759 case Delegate::FAIL_EXTRACT:
760 msg = tr("Package could not be extracted"); break;
761 case Delegate::FAIL_FILESYSTEM:
762 msg = tr("A local file-system error occurred"); break;
763 case Delegate::FAIL_NOT_FOUND:
764 msg = tr("Package file missing from download server"); break;
765 case Delegate::FAIL_UNKNOWN:
767 msg = tr("Unknown reason");
770 emit aircraftInstallFailed(index, msg);
773 void AircraftItemModel::installSucceeded(QModelIndex index)
775 emit aircraftInstallCompleted(index);
778 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
780 if (index.row() < m_items.size()) {
781 return true; // local file, always runnable
784 quint32 packageIndex = index.row() - m_items.size();
785 const PackageRef& pkg(m_packages[packageIndex]);
786 InstallRef ex = pkg->existingInstall();
788 return false; // not installed
791 return !ex->isDownloading();
794 #include "AircraftModel.moc"