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>
43 const int STANDARD_THUMBNAIL_HEIGHT = 128;
45 using namespace simgear::pkg;
47 AircraftItem::AircraftItem() :
50 // oh for C++11 initialisers
51 for (int i=0; i<4; ++i) ratings[i] = 0;
54 AircraftItem::AircraftItem(QDir dir, QString filePath) :
57 for (int i=0; i<4; ++i) ratings[i] = 0;
60 readProperties(filePath.toStdString(), &root);
62 if (!root.hasChild("sim")) {
63 throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
66 SGPropertyNode_ptr sim = root.getNode("sim");
69 pathModTime = QFileInfo(path).lastModified();
70 if (sim->getBoolValue("exclude-from-gui", false)) {
75 description = sim->getStringValue("description");
76 authors = sim->getStringValue("author");
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");
87 if (sim->hasChild("variant-of")) {
88 variantOf = sim->getStringValue("variant-of");
92 QString AircraftItem::baseName() const
94 QString fn = QFileInfo(path).fileName();
95 fn.truncate(fn.count() - 8);
99 void AircraftItem::fromDataStream(QDataStream& ds)
101 ds >> path >> pathModTime >> excluded;
106 ds >> description >> authors >> variantOf;
107 for (int i=0; i<4; ++i) ds >> ratings[i];
110 void AircraftItem::toDataStream(QDataStream& ds) const
112 ds << path << pathModTime << excluded;
117 ds << description << authors << variantOf;
118 for (int i=0; i<4; ++i) ds << ratings[i];
121 QPixmap AircraftItem::thumbnail() const
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);
139 static int CACHE_VERSION = 3;
141 class AircraftScanThread : public QThread
145 AircraftScanThread(QStringList dirsToScan) :
151 ~AircraftScanThread()
155 /** thread-safe access to items already scanned */
156 QVector<AircraftItemPtr> items()
158 QVector<AircraftItemPtr> result;
159 QMutexLocker g(&m_lock);
160 result.swap(m_items);
177 Q_FOREACH(QString d, m_dirs) {
178 scanAircraftDir(QDir(d));
191 QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
192 if (!cacheData.isEmpty()) {
193 QDataStream ds(cacheData);
194 quint32 count, cacheVersion;
195 ds >> cacheVersion >> count;
197 if (cacheVersion != CACHE_VERSION) {
198 return; // mis-matched cache, version, drop
201 for (int i=0; i<count; ++i) {
202 AircraftItemPtr item(new AircraftItem);
203 item->fromDataStream(ds);
205 QFileInfo finfo(item->path);
206 if (finfo.exists() && (finfo.lastModified() == item->pathModTime)) {
207 // corresponding -set.xml file still exists and is
209 m_cachedItems[item->path] = item;
211 } // of cached item iteration
218 QByteArray cacheData;
220 QDataStream ds(&cacheData, QIODevice::WriteOnly);
221 quint32 count = m_nextCache.count();
222 ds << CACHE_VERSION << count;
224 Q_FOREACH(AircraftItemPtr item, m_nextCache.values()) {
225 item->toDataStream(ds);
229 settings.setValue("aircraft-cache", cacheData);
232 void scanAircraftDir(QDir path)
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;
244 Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
246 QString absolutePath = xmlChild.absoluteFilePath();
247 AircraftItemPtr item;
249 if (m_cachedItems.contains(absolutePath)) {
250 item = m_cachedItems.value(absolutePath);
252 item = AircraftItemPtr(new AircraftItem(childDir, absolutePath));
255 m_nextCache[absolutePath] = item;
257 if (item->excluded) {
261 if (item->variantOf.isNull()) {
262 baseAircraft.insert(item->baseName(), item);
264 variants.append(item);
266 } catch (sg_exception& e) {
273 } // of set.xml iteration
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;
282 baseAircraft.value(item->variantOf)->variants.append(item);
285 // lock mutex while we modify the items array
287 QMutexLocker g(&m_lock);
288 m_items+=(baseAircraft.values().toVector());
292 } // of subdir iteration
297 QVector<AircraftItemPtr> m_items;
299 QMap<QString, AircraftItemPtr > m_cachedItems;
300 QMap<QString, AircraftItemPtr > m_nextCache;
305 class PackageDelegate : public simgear::pkg::Delegate
308 PackageDelegate(AircraftItemModel* model) :
311 m_model->m_packageRoot->addDelegate(this);
316 m_model->m_packageRoot->removeDelegate(this);
320 virtual void catalogRefreshed(CatalogRef aCatalog, StatusCode aReason)
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();
327 qWarning() << "failed refresh of "
328 << QString::fromStdString(aCatalog->url()) << ":" << aReason << endl;
332 virtual void startInstall(InstallRef aInstall)
334 QModelIndex mi(indexForPackage(aInstall->package()));
335 m_model->dataChanged(mi, mi);
338 virtual void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total)
342 QModelIndex mi(indexForPackage(aInstall->package()));
343 m_model->dataChanged(mi, mi);
346 virtual void finishInstall(InstallRef aInstall, StatusCode aReason)
348 QModelIndex mi(indexForPackage(aInstall->package()));
349 m_model->dataChanged(mi, mi);
351 if ((aReason != USER_CANCELLED) && (aReason != STATUS_SUCCESS)) {
352 m_model->installFailed(mi, aReason);
355 if (aReason == STATUS_SUCCESS) {
356 m_model->installSucceeded(mi);
360 virtual void dataForThumbnail(const std::string& aThumbnailUrl,
361 size_t length, const uint8_t* bytes)
363 QImage img = QImage::fromData(QByteArray::fromRawData(reinterpret_cast<const char*>(bytes), length));
365 qWarning() << "failed to load image data for URL:" <<
366 QString::fromStdString(aThumbnailUrl);
370 m_model->m_thumbnailPixmapCache.insert(QString::fromStdString(aThumbnailUrl),
371 QPixmap::fromImage(img));
373 // notify any affected items. Linear scan here avoids another map/dict
375 PackageList::const_iterator it;
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);
385 } // of packages iteration
389 QModelIndex indexForPackage(const PackageRef& ref) const
391 PackageList::const_iterator it = std::find(m_model->m_packages.begin(),
392 m_model->m_packages.end(),
394 if (it == m_model->m_packages.end()) {
395 return QModelIndex();
398 size_t offset = it - m_model->m_packages.begin();
399 return m_model->index(offset + m_model->m_items.size());
402 AircraftItemModel* m_model;
405 AircraftItemModel::AircraftItemModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
406 QAbstractListModel(pr),
408 m_packageRoot(rootRef)
410 m_delegate = new PackageDelegate(this);
411 // packages may already be refreshed, so pull now
415 AircraftItemModel::~AircraftItemModel()
417 abandonCurrentScan();
421 void AircraftItemModel::setPaths(QStringList paths)
426 void AircraftItemModel::scanDirs()
428 abandonCurrentScan();
432 m_activeVariant.clear();
435 QStringList dirs = m_paths;
437 Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
438 dirs << QString::fromStdString(ap);
441 SGPath rootAircraft(globals->get_fg_root());
442 rootAircraft.append("Aircraft");
443 dirs << QString::fromStdString(rootAircraft.str());
445 m_scanThread = new AircraftScanThread(dirs);
446 connect(m_scanThread, &AircraftScanThread::finished, this,
447 &AircraftItemModel::onScanFinished);
448 connect(m_scanThread, &AircraftScanThread::addedItems,
449 this, &AircraftItemModel::onScanResults);
450 m_scanThread->start();
453 void AircraftItemModel::abandonCurrentScan()
456 m_scanThread->setDone();
457 m_scanThread->wait(1000);
463 void AircraftItemModel::refreshPackages()
466 m_packages = m_packageRoot->allPackages();
467 m_packageVariant.resize(m_packages.size());
471 int AircraftItemModel::rowCount(const QModelIndex& parent) const
473 return m_items.size() + m_packages.size();
476 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
478 if (index.row() >= m_items.size()) {
479 quint32 packageIndex = index.row() - m_items.size();
481 if (role == AircraftVariantRole) {
482 return m_packageVariant.at(packageIndex);
485 const PackageRef& pkg(m_packages[packageIndex]);
486 InstallRef ex = pkg->existingInstall();
488 if (role == AircraftInstallPercentRole) {
489 return ex.valid() ? ex->downloadedPercent() : 0;
490 } else if (role == AircraftInstallDownloadedSizeRole) {
491 return static_cast<quint64>(ex.valid() ? ex->downloadedBytes() : 0);
494 quint32 variantIndex = m_packageVariant.at(packageIndex);
495 return dataFromPackage(pkg, variantIndex, role);
497 if (role == AircraftVariantRole) {
498 return m_activeVariant.at(index.row());
501 quint32 variantIndex = m_activeVariant.at(index.row());
502 const AircraftItemPtr item(m_items.at(index.row()));
503 return dataFromItem(item, variantIndex, role);
507 QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, quint32 variantIndex, int role) const
509 if (role == AircraftVariantCountRole) {
510 return item->variants.count();
513 if (role == AircraftThumbnailCountRole) {
514 QPixmap p = item->thumbnail();
515 return p.isNull() ? 0 : 1;
518 if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
519 int variantIndex = role - AircraftVariantDescriptionRole;
520 return item->variants.at(variantIndex)->description;
524 if (variantIndex <= item->variants.count()) {
525 // show the selected variant
526 item = item->variants.at(variantIndex - 1);
530 if (role == Qt::DisplayRole) {
531 return item->description;
532 } else if (role == Qt::DecorationRole) {
533 return item->thumbnail();
534 } else if (role == AircraftPathRole) {
536 } else if (role == AircraftAuthorsRole) {
537 return item->authors;
538 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
539 return item->ratings[role - AircraftRatingRole];
540 } else if (role >= AircraftThumbnailRole) {
541 return item->thumbnail();
542 } else if (role == AircraftPackageIdRole) {
543 // can we fake an ID? otherwise fall through to a null variant
544 } else if (role == AircraftPackageStatusRole) {
545 return PackageInstalled; // always the case
546 } else if (role == Qt::ToolTipRole) {
548 } else if (role == AircraftURIRole) {
549 return QUrl::fromLocalFile(item->path);
550 } else if (role == AircraftHasRatingsRole) {
552 for (int i=0; i<4; ++i) {
553 have |= (item->ratings[i] > 0);
556 } else if (role == AircraftLongDescriptionRole) {
558 return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
559 "no escape from reality. Open your eyes, like up to the skies and see. "
560 "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
561 "Litte high, little low. Anywhere the wind blows.";
568 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
570 if (role == Qt::DecorationRole) {
571 role = AircraftThumbnailRole; // use first thumbnail
574 if (role == Qt::DisplayRole) {
575 return QString::fromStdString(item->name());
576 } else if (role == AircraftPathRole) {
577 InstallRef i = item->existingInstall();
579 return QString::fromStdString(i->primarySetPath().str());
581 } else if (role == AircraftPackageIdRole) {
582 return QString::fromStdString(item->id());
583 } else if (role == AircraftPackageStatusRole) {
584 InstallRef i = item->existingInstall();
586 if (i->isDownloading()) {
587 return PackageDownloading;
590 return PackageQueued;
592 if (i->hasUpdate()) {
593 return PackageUpdateAvailable;
596 return PackageInstalled;
598 return PackageNotInstalled;
600 } else if (role >= AircraftThumbnailRole) {
601 return packageThumbnail(item , role - AircraftThumbnailRole);
602 } else if (role == AircraftAuthorsRole) {
603 SGPropertyNode* authors = item->properties()->getChild("author");
605 return QString::fromStdString(authors->getStringValue());
607 } else if (role == AircraftLongDescriptionRole) {
608 return QString::fromStdString(item->description());
609 } else if (role == AircraftPackageSizeRole) {
610 return static_cast<int>(item->fileSizeBytes());
611 } else if (role == AircraftURIRole) {
612 return QUrl("package:" + QString::fromStdString(item->qualifiedId()));
613 } else if (role == AircraftHasRatingsRole) {
614 return item->properties()->hasChild("rating");
615 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
616 int ratingIndex = role - AircraftRatingRole;
617 SGPropertyNode* ratings = item->properties()->getChild("rating");
621 return ratings->getChild(ratingIndex)->getIntValue();
627 QVariant AircraftItemModel::packageThumbnail(PackageRef p, int index) const
629 const string_list& thumbnails(p->thumbnailUrls());
630 if (index >= thumbnails.size()) {
634 std::string thumbnailUrl = thumbnails.at(index);
635 QString urlQString(QString::fromStdString(thumbnailUrl));
636 if (m_thumbnailPixmapCache.contains(urlQString)) {
638 return m_thumbnailPixmapCache.value(urlQString);
641 // check the on-disk store. This relies on the order of thumbnails in the
642 // results of thumbnailUrls and thumbnails corresponding
643 InstallRef ex = p->existingInstall();
645 const string_list& thumbNames(p->thumbnails());
646 if (!thumbNames.empty()) {
647 SGPath path(ex->path());
648 path.append(p->thumbnails()[index]);
651 pix.load(QString::fromStdString(path.str()));
652 // resize to the standard size
653 if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) {
654 pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT);
656 m_thumbnailPixmapCache[urlQString] = pix;
659 } // of have thumbnail file names
660 } // of have existing install
662 m_packageRoot->requestThumbnailData(thumbnailUrl);
666 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
668 if (role == AircraftVariantRole) {
669 m_activeVariant[index.row()] = value.toInt();
670 emit dataChanged(index, index);
677 QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const
679 if (uri.isLocalFile()) {
680 QString path = uri.toLocalFile();
681 for (int row=0; row <m_items.size(); ++row) {
682 const AircraftItemPtr item(m_items.at(row));
683 if (item->path == path) {
687 } else if (uri.scheme() == "package") {
688 QString ident = uri.path();
689 PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString());
691 for (int i=0; i < m_packages.size(); ++i) {
692 if (m_packages[i] == pkg) {
693 return index(m_items.size() + i);
695 } // of linear package scan
698 qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme();
701 return QModelIndex();
704 void AircraftItemModel::onScanResults()
706 QVector<AircraftItemPtr> newItems = m_scanThread->items();
707 if (newItems.isEmpty())
710 int firstRow = m_items.count();
711 int lastRow = firstRow + newItems.count() - 1;
712 beginInsertRows(QModelIndex(), firstRow, lastRow);
715 // default variants in all cases
716 for (int i=0; i< newItems.count(); ++i) {
717 m_activeVariant.append(0);
722 void AircraftItemModel::onScanFinished()
728 void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason)
730 Q_ASSERT(index.row() >= m_items.size());
734 case Delegate::FAIL_CHECKSUM:
735 msg = tr("Invalid package checksum"); break;
736 case Delegate::FAIL_DOWNLOAD:
737 msg = tr("Download failed"); break;
738 case Delegate::FAIL_EXTRACT:
739 msg = tr("Package could not be extracted"); break;
740 case Delegate::FAIL_FILESYSTEM:
741 msg = tr("A local file-system error occurred"); break;
742 case Delegate::FAIL_NOT_FOUND:
743 msg = tr("Package file missing from download server"); break;
744 case Delegate::FAIL_UNKNOWN:
746 msg = tr("Unknown reason");
749 emit aircraftInstallFailed(index, msg);
752 void AircraftItemModel::installSucceeded(QModelIndex index)
754 emit aircraftInstallCompleted(index);
757 bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const
759 if (index.row() < m_items.size()) {
760 return true; // local file, always runnable
763 quint32 packageIndex = index.row() - m_items.size();
764 const PackageRef& pkg(m_packages[packageIndex]);
765 InstallRef ex = pkg->existingInstall();
767 return false; // not installed
770 return !ex->isDownloading();
773 #include "AircraftModel.moc"