// AircraftModel.cxx - part of GUI launcher using Qt5 // // Written by James Turner, started March 2015. // // Copyright (C) 2015 James Turner // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #include "AircraftModel.hxx" #include #include #include // Simgear #include #include #include #include #include #include // FlightGear #include
#include #include "QmlAircraftInfo.hxx" const int STANDARD_THUMBNAIL_HEIGHT = 128; using namespace simgear::pkg; class PackageDelegate : public simgear::pkg::Delegate { public: PackageDelegate(AircraftItemModel* model) : m_model(model) { m_model->m_packageRoot->addDelegate(this); } ~PackageDelegate() override { m_model->m_packageRoot->removeDelegate(this); } protected: void catalogRefreshed(CatalogRef aCatalog, StatusCode aReason) override { if (aReason == STATUS_IN_PROGRESS) { // nothing to do } else if ((aReason == STATUS_REFRESHED) || (aReason == STATUS_SUCCESS)) { m_model->refreshPackages(); } else { qWarning() << "failed refresh of" << QString::fromStdString(aCatalog->url()) << ":" << aReason << endl; } } void startInstall(InstallRef aInstall) override { QModelIndex mi(indexForPackage(aInstall->package())); m_model->dataChanged(mi, mi); } void installProgress(InstallRef aInstall, unsigned int bytes, unsigned int total) override { Q_UNUSED(bytes); Q_UNUSED(total); QModelIndex mi(indexForPackage(aInstall->package())); m_model->dataChanged(mi, mi); } void finishInstall(InstallRef aInstall, StatusCode aReason) override { QModelIndex mi(indexForPackage(aInstall->package())); m_model->dataChanged(mi, mi); if ((aReason != USER_CANCELLED) && (aReason != STATUS_SUCCESS)) { m_model->installFailed(mi, aReason); } if (aReason == STATUS_SUCCESS) { m_model->installSucceeded(mi); } } void availablePackagesChanged() override { m_model->refreshPackages(); } void installStatusChanged(InstallRef aInstall, StatusCode aReason) override { Q_UNUSED(aReason); QModelIndex mi(indexForPackage(aInstall->package())); m_model->dataChanged(mi, mi); } void finishUninstall(const PackageRef& pkg) override { QModelIndex mi(indexForPackage(pkg)); m_model->dataChanged(mi, mi); } virtual void dataForThumbnail(const std::string& aThumbnailUrl, size_t length, const uint8_t* bytes) override { QImage img = QImage::fromData(QByteArray::fromRawData(reinterpret_cast(bytes), length)); if (img.isNull()) { qWarning() << "failed to load image data for URL:" << QString::fromStdString(aThumbnailUrl); return; } QPixmap pix = QPixmap::fromImage(img); if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) { pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT, Qt::SmoothTransformation); } QString url = QString::fromStdString(aThumbnailUrl); m_model->m_downloadedPixmapCache.insert(url, pix); // notify any affected items. Linear scan here avoids another map/dict structure. for (auto pkg : m_model->m_packages) { const size_t variantCount = pkg->variants().size(); bool notifyChanged = false; for (size_t v=0; v < variantCount; ++v) { const Package::Thumbnail& thumb(pkg->thumbnailForVariant(v)); if (thumb.url == aThumbnailUrl) { notifyChanged = true; } } if (notifyChanged) { QModelIndex mi = indexForPackage(pkg); m_model->dataChanged(mi, mi); } } // of packages iteration } private: QModelIndex indexForPackage(const PackageRef& ref) const { auto it = std::find(m_model->m_packages.begin(), m_model->m_packages.end(), ref); if (it == m_model->m_packages.end()) { return QModelIndex(); } int offset = std::distance(m_model->m_packages.begin(), it); return m_model->index(offset + m_model->m_cachedLocalAircraftCount); } AircraftItemModel* m_model; }; AircraftItemModel::AircraftItemModel(QObject* pr) : QAbstractListModel(pr) { auto cache = LocalAircraftCache::instance(); connect(cache, &LocalAircraftCache::scanStarted, this, &AircraftItemModel::onScanStarted); connect(cache, &LocalAircraftCache::addedItems, this, &AircraftItemModel::onScanAddedItems); connect(cache, &LocalAircraftCache::cleared, this, &AircraftItemModel::onLocalCacheCleared); } AircraftItemModel::~AircraftItemModel() { delete m_delegate; } void AircraftItemModel::setPackageRoot(const simgear::pkg::RootRef& root) { if (m_packageRoot) { delete m_delegate; m_delegate = nullptr; } m_packageRoot = root; if (m_packageRoot) { m_delegate = new PackageDelegate(this); // packages may already be refreshed, so pull now refreshPackages(); } } void AircraftItemModel::onScanStarted() { const int numToRemove = m_cachedLocalAircraftCount; if (numToRemove > 0) { int lastRow = numToRemove - 1; beginRemoveRows(QModelIndex(), 0, lastRow); m_delegateStates.remove(0, numToRemove); m_cachedLocalAircraftCount = 0; endRemoveRows(); } } void AircraftItemModel::refreshPackages() { simgear::pkg::PackageList newPkgs = m_packageRoot->allPackages(); const int firstRow = m_cachedLocalAircraftCount; const int newSize = static_cast(newPkgs.size()); const int newTotalSize = firstRow + newSize; if (m_packages.size() != newPkgs.size()) { const int oldSize = static_cast(m_packages.size()); if (newSize > oldSize) { // growing int firstNewRow = firstRow + oldSize; int lastNewRow = firstRow + newSize - 1; beginInsertRows(QModelIndex(), firstNewRow, lastNewRow); m_packages = newPkgs; m_delegateStates.resize(newTotalSize); endInsertRows(); } else { // shrinking int firstOldRow = firstRow + newSize; int lastOldRow = firstRow + oldSize - 1; beginRemoveRows(QModelIndex(), firstOldRow, lastOldRow); m_packages = newPkgs; m_delegateStates.resize(newTotalSize); endRemoveRows(); } } else { m_packages = newPkgs; } emit dataChanged(index(firstRow), index(firstRow + newSize - 1)); emit contentsChanged(); } int AircraftItemModel::rowCount(const QModelIndex&) const { return m_cachedLocalAircraftCount + static_cast(m_packages.size()); } QVariant AircraftItemModel::data(const QModelIndex& index, int role) const { int row = index.row(); if (role == AircraftVariantRole) { return m_delegateStates.at(row).variant; } if (row >= m_cachedLocalAircraftCount) { quint32 packageIndex = row - m_cachedLocalAircraftCount; const PackageRef& pkg(m_packages[packageIndex]); InstallRef ex = pkg->existingInstall(); if (role == AircraftInstallPercentRole) { return ex.valid() ? ex->downloadedPercent() : 0; } else if (role == AircraftInstallDownloadedSizeRole) { return static_cast(ex.valid() ? ex->downloadedBytes() : 0); } else if (role == AircraftPackageRefRole ) { return QVariant::fromValue(pkg); } return dataFromPackage(pkg, m_delegateStates.at(row), role); } else { const AircraftItemPtr item(LocalAircraftCache::instance()->itemAt(row)); return dataFromItem(item, m_delegateStates.at(row), role); } } QVariant AircraftItemModel::dataFromItem(AircraftItemPtr item, const DelegateState& state, int role) const { if (role == AircraftVariantCountRole) { return item->variants.count(); } if (role >= AircraftVariantDescriptionRole) { int variantIndex = role - AircraftVariantDescriptionRole; if (variantIndex == 0) { return item->description; } Q_ASSERT(variantIndex < item->variants.size()); return item->variants.at(variantIndex)->description; } if (state.variant) { if (state.variant <= static_cast(item->variants.count())) { // show the selected variant item = item->variants.at(state.variant - 1); } } if (role == Qt::DisplayRole) { if (item->description.isEmpty()) { return tr("Missing description for: %1").arg(item->baseName()); } return item->description; } else if (role == Qt::DecorationRole) { return item->thumbnail(); } else if (role == AircraftPathRole) { return item->path; } else if (role == AircraftAuthorsRole) { return item->authors; } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) { return item->ratings[role - AircraftRatingRole]; } else if (role == AircraftThumbnailRole) { return item->thumbnail(); } else if (role == AircraftPackageIdRole) { // can we fake an ID? otherwise fall through to a null variant } else if (role == AircraftPackageStatusRole) { return LocalAircraftCache::PackageInstalled; // always the case } else if (role == Qt::ToolTipRole) { return item->path; } else if (role == AircraftURIRole) { return QUrl::fromLocalFile(item->path); } else if (role == AircraftHasRatingsRole) { bool have = false; for (int i=0; i<4; ++i) { have |= (item->ratings[i] > 0); } return have; } else if (role == AircraftLongDescriptionRole) { return item->longDescription; } else if (role == AircraftIsHelicopterRole) { return item->usesHeliports; } else if (role == AircraftIsSeaplaneRole) { return item->usesSeaports; } else if ((role == AircraftInstallDownloadedSizeRole) || (role == AircraftPackageSizeRole)) { return 0; } else if (role == AircraftStatusRole) { return item->status(0 /* variant is always 0 */); } else if (role == AircraftMinVersionRole) { return item->minFGVersion; } return QVariant(); } QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, const DelegateState& state, int role) const { if (role == Qt::DecorationRole) { role = AircraftThumbnailRole; } if (role >= AircraftVariantDescriptionRole) { int variantIndex = role - AircraftVariantDescriptionRole; QString desc = QString::fromStdString(item->nameForVariant(variantIndex)); if (desc.isEmpty()) { desc = tr("Missing description for: %1").arg(QString::fromStdString(item->id())); } return desc; } if (role == Qt::DisplayRole) { QString desc = QString::fromStdString(item->nameForVariant(state.variant)); if (desc.isEmpty()) { desc = tr("Missing description for: %1").arg(QString::fromStdString(item->id())); } return desc; } else if (role == AircraftPathRole) { InstallRef i = item->existingInstall(); if (i.valid()) { return QString::fromStdString(i->primarySetPath().utf8Str()); } } else if (role == AircraftPackageIdRole) { return QString::fromStdString(item->variants()[state.variant]); } else if (role == AircraftPackageStatusRole) { InstallRef i = item->existingInstall(); if (i.valid()) { if (i->isDownloading()) { return LocalAircraftCache::PackageDownloading; } if (i->isQueued()) { return LocalAircraftCache::PackageQueued; } if (i->hasUpdate()) { return LocalAircraftCache::PackageUpdateAvailable; } return LocalAircraftCache::PackageInstalled; } else { return LocalAircraftCache::PackageNotInstalled; } } else if (role == AircraftVariantCountRole) { // this value wants the number of aditional variants, i.e not // including the primary. Hence the -1 term. return static_cast(item->variants().size() - 1); } else if (role == AircraftThumbnailRole) { return packageThumbnail(item, state); } else if (role == AircraftAuthorsRole) { std::string authors = item->getLocalisedProp("author", state.variant); if (!authors.empty()) { return QString::fromStdString(authors); } } else if (role == AircraftLongDescriptionRole) { std::string longDesc = item->getLocalisedProp("description", state.variant); return QString::fromStdString(longDesc).simplified(); } else if (role == AircraftPackageSizeRole) { return static_cast(item->fileSizeBytes()); } else if (role == AircraftURIRole) { return QUrl("package:" + QString::fromStdString(item->qualifiedVariantId(state.variant))); } else if (role == AircraftHasRatingsRole) { return item->properties()->hasChild("rating"); } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) { int ratingIndex = role - AircraftRatingRole; SGPropertyNode* ratings = item->properties()->getChild("rating"); if (!ratings) { return 0; } return ratings->getChild(ratingIndex)->getIntValue(); } else if (role == AircraftStatusRole) { return QmlAircraftInfo::packageAircraftStatus(item); } else if (role == AircraftMinVersionRole) { const std::string v = item->properties()->getStringValue("minimum-fg-version"); if (!v.empty()) { return QString::fromStdString(v); } } return QVariant(); } QVariant AircraftItemModel::packageThumbnail(PackageRef p, const DelegateState& ds, bool download) const { const Package::Thumbnail& thumb(p->thumbnailForVariant(ds.variant)); if (thumb.url.empty()) { return QVariant(); } QString urlQString(QString::fromStdString(thumb.url)); if (m_downloadedPixmapCache.contains(urlQString)) { // cache hit, easy return m_downloadedPixmapCache.value(urlQString); } // check the on-disk store. InstallRef ex = p->existingInstall(); if (ex.valid()) { SGPath thumbPath = ex->path() / thumb.path; if (thumbPath.exists()) { QPixmap pix; pix.load(QString::fromStdString(thumbPath.utf8Str())); // resize to the standard size if (pix.height() > STANDARD_THUMBNAIL_HEIGHT) { pix = pix.scaledToHeight(STANDARD_THUMBNAIL_HEIGHT); } m_downloadedPixmapCache[urlQString] = pix; return pix; } } // of have existing install if (download) { m_packageRoot->requestThumbnailData(thumb.url); } return QVariant(); } bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { int row = index.row(); quint32 newValue = value.toUInt(); if (role == AircraftVariantRole) { if (m_delegateStates[row].variant == newValue) { return true; } m_delegateStates[row].variant = newValue; emit dataChanged(index, index); return true; } return false; } QHash AircraftItemModel::roleNames() const { QHash result = QAbstractListModel::roleNames(); result[Qt::DisplayRole] = "title"; result[AircraftURIRole] = "uri"; result[AircraftPackageIdRole] = "package"; result[AircraftAuthorsRole] = "authors"; result[AircraftVariantCountRole] = "variantCount"; result[AircraftLongDescriptionRole] = "description"; result[AircraftThumbnailRole] = "thumbnail"; result[AircraftPackageSizeRole] = "packageSizeBytes"; result[AircraftPackageStatusRole] = "packageStatus"; result[AircraftInstallDownloadedSizeRole] = "downloadedBytes"; result[AircraftVariantRole] = "activeVariant"; result[AircraftStatusRole] = "aircraftStatus"; result[AircraftMinVersionRole] = "requiredFGVersion"; result[AircraftHasRatingsRole] = "hasRatings"; result[AircraftRatingRole] = "ratingFDM"; result[AircraftRatingRole + 1] = "ratingSystems"; result[AircraftRatingRole + 2] = "ratingCockpit"; result[AircraftRatingRole + 3] = "ratingExterior"; return result; } QModelIndex AircraftItemModel::indexOfAircraftURI(QUrl uri) const { if (uri.isEmpty()) { return QModelIndex(); } if (uri.isLocalFile()) { int row = LocalAircraftCache::instance()->findIndexWithUri(uri); if (row >= 0) { return index(row); } } else if (uri.scheme() == "package") { QString ident = uri.path(); int rowOffset = m_cachedLocalAircraftCount; PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString()); if (pkg) { for (size_t i=0; i < m_packages.size(); ++i) { if (m_packages[i] == pkg) { return index(rowOffset + i); } } // of linear package scan } } else if (uri.scheme() == "") { // Empty URI scheme (no selection), nothing to do } else { qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme(); } return QModelIndex(); } void AircraftItemModel::selectVariantForAircraftURI(QUrl uri) { if (uri.isEmpty()) { return; } int variantIndex = 0; QModelIndex modelIndex; if (uri.isLocalFile()) { int row = LocalAircraftCache::instance()->findIndexWithUri(uri); if (row < 0) { return; } modelIndex = index(row); // now check if we are actually selecting a variant const AircraftItemPtr item = LocalAircraftCache::instance()->itemAt(row); const QString path = uri.toLocalFile(); for (int vr=0; vr < item->variants.size(); ++vr) { if (item->variants.at(vr)->path == path) { variantIndex = vr + 1; break; } } } else if (uri.scheme() == "package") { QString ident = uri.path(); int rowOffset = m_cachedLocalAircraftCount; PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString()); if (pkg) { for (size_t i=0; i < m_packages.size(); ++i) { if (m_packages[i] == pkg) { modelIndex = index(rowOffset + static_cast(i)); variantIndex = pkg->indexOfVariant(ident.toStdString()); break; } } // of linear package scan } } else { qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme(); return; } if (modelIndex.isValid()) { setData(modelIndex, variantIndex, AircraftVariantRole); } } QString AircraftItemModel::nameForAircraftURI(QUrl uri) const { if (uri.isLocalFile()) { AircraftItemPtr item = LocalAircraftCache::instance()->findItemWithUri(uri); if (!item) { return {}; } const QString path = uri.toLocalFile(); if (item->path == path) { return item->description; } // check variants too for (int vr=0; vr < item->variants.size(); ++vr) { auto variant = item->variants.at(vr); if (variant->path == path) { return variant->description; } } } else if (uri.scheme() == "package") { QString ident = uri.path(); PackageRef pkg = m_packageRoot->getPackageById(ident.toStdString()); if (pkg) { int variantIndex = pkg->indexOfVariant(ident.toStdString()); return QString::fromStdString(pkg->nameForVariant(variantIndex)); } } else { qWarning() << "Unknown aircraft URI scheme" << uri << uri.scheme(); } return {}; } void AircraftItemModel::onScanAddedItems(int addedCount) { Q_UNUSED(addedCount); const auto items = LocalAircraftCache::instance()->allItems(); const int newItemCount = items.size() - m_cachedLocalAircraftCount; const int firstRow = m_cachedLocalAircraftCount; const int lastRow = firstRow + newItemCount - 1; beginInsertRows(QModelIndex(), firstRow, lastRow); m_delegateStates.insert(m_cachedLocalAircraftCount, newItemCount, {}); m_cachedLocalAircraftCount += newItemCount; endInsertRows(); emit contentsChanged(); } void AircraftItemModel::onLocalCacheCleared() { const int firstRow = 0; const int lastRow = m_cachedLocalAircraftCount + 1; beginRemoveRows(QModelIndex(), firstRow, lastRow); m_delegateStates.remove(0, m_cachedLocalAircraftCount); m_cachedLocalAircraftCount = 0; endRemoveRows(); } void AircraftItemModel::installFailed(QModelIndex index, simgear::pkg::Delegate::StatusCode reason) { QString msg; switch (reason) { case Delegate::FAIL_CHECKSUM: msg = tr("Invalid package checksum"); break; case Delegate::FAIL_DOWNLOAD: msg = tr("Download failed"); break; case Delegate::FAIL_EXTRACT: msg = tr("Package could not be extracted"); break; case Delegate::FAIL_FILESYSTEM: msg = tr("A local file-system error occurred"); break; case Delegate::FAIL_NOT_FOUND: msg = tr("Package file missing from download server"); break; case Delegate::FAIL_UNKNOWN: default: msg = tr("Unknown reason"); } emit aircraftInstallFailed(index, msg); } void AircraftItemModel::installSucceeded(QModelIndex index) { emit aircraftInstallCompleted(index); } bool AircraftItemModel::isIndexRunnable(const QModelIndex& index) const { if (index.row() < m_cachedLocalAircraftCount) { return true; // local file, always runnable } quint32 packageIndex = index.row() - m_cachedLocalAircraftCount; const PackageRef& pkg(m_packages[packageIndex]); InstallRef ex = pkg->existingInstall(); if (!ex.valid()) { return false; // not installed } return !ex->isDownloading(); }