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>
32 #include <simgear/props/props_io.hxx>
33 #include <simgear/structure/exception.hxx>
34 #include <simgear/misc/sg_path.hxx>
35 #include <simgear/package/Package.hxx>
36 #include <simgear/package/Catalog.hxx>
37 #include <simgear/package/Install.hxx>
40 #include <Main/globals.hxx>
42 using namespace simgear::pkg;
44 AircraftItem::AircraftItem()
46 // oh for C++11 initialisers
47 for (int i=0; i<4; ++i) ratings[i] = 0;
50 AircraftItem::AircraftItem(QDir dir, QString filePath)
52 for (int i=0; i<4; ++i) ratings[i] = 0;
55 readProperties(filePath.toStdString(), &root);
57 if (!root.hasChild("sim")) {
58 throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
61 SGPropertyNode_ptr sim = root.getNode("sim");
64 pathModTime = QFileInfo(path).lastModified();
66 description = sim->getStringValue("description");
67 authors = sim->getStringValue("author");
69 if (sim->hasChild("rating")) {
70 SGPropertyNode_ptr ratingsNode = sim->getNode("rating");
71 ratings[0] = ratingsNode->getIntValue("FDM");
72 ratings[1] = ratingsNode->getIntValue("systems");
73 ratings[2] = ratingsNode->getIntValue("cockpit");
74 ratings[3] = ratingsNode->getIntValue("model");
78 if (sim->hasChild("variant-of")) {
79 variantOf = sim->getStringValue("variant-of");
83 QString AircraftItem::baseName() const
85 QString fn = QFileInfo(path).fileName();
86 fn.truncate(fn.count() - 8);
90 void AircraftItem::fromDataStream(QDataStream& ds)
92 ds >> path >> description >> authors >> variantOf;
93 for (int i=0; i<4; ++i) ds >> ratings[i];
97 void AircraftItem::toDataStream(QDataStream& ds) const
99 ds << path << description << authors << variantOf;
100 for (int i=0; i<4; ++i) ds << ratings[i];
104 QPixmap AircraftItem::thumbnail() const
106 if (m_thumbnail.isNull()) {
107 QFileInfo info(path);
108 QDir dir = info.dir();
109 if (dir.exists("thumbnail.jpg")) {
110 m_thumbnail.load(dir.filePath("thumbnail.jpg"));
111 // resize to the standard size
112 if (m_thumbnail.height() > 128) {
113 m_thumbnail = m_thumbnail.scaledToHeight(128);
122 static int CACHE_VERSION = 2;
124 class AircraftScanThread : public QThread
128 AircraftScanThread(QStringList dirsToScan) :
134 ~AircraftScanThread()
138 /** thread-safe access to items already scanned */
139 QList<AircraftItem*> items()
141 QList<AircraftItem*> result;
142 QMutexLocker g(&m_lock);
143 result.swap(m_items);
160 Q_FOREACH(QString d, m_dirs) {
161 scanAircraftDir(QDir(d));
174 QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
175 if (!cacheData.isEmpty()) {
176 QDataStream ds(cacheData);
177 quint32 count, cacheVersion;
178 ds >> cacheVersion >> count;
180 if (cacheVersion != CACHE_VERSION) {
181 return; // mis-matched cache, version, drop
184 for (int i=0; i<count; ++i) {
185 AircraftItem* item = new AircraftItem;
186 item->fromDataStream(ds);
188 QFileInfo finfo(item->path);
189 if (!finfo.exists() || (finfo.lastModified() != item->pathModTime)) {
192 // corresponding -set.xml file still exists and is
194 m_cachedItems[item->path] = item;
196 } // of cached item iteration
203 QByteArray cacheData;
205 QDataStream ds(&cacheData, QIODevice::WriteOnly);
206 quint32 count = m_nextCache.count();
207 ds << CACHE_VERSION << count;
209 Q_FOREACH(AircraftItem* item, m_nextCache.values()) {
210 item->toDataStream(ds);
214 settings.setValue("aircraft-cache", cacheData);
217 void scanAircraftDir(QDir path)
223 filters << "*-set.xml";
224 Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
225 QDir childDir(child.absoluteFilePath());
226 QMap<QString, AircraftItem*> baseAircraft;
227 QList<AircraftItem*> variants;
229 Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
231 QString absolutePath = xmlChild.absoluteFilePath();
232 AircraftItem* item = NULL;
234 if (m_cachedItems.contains(absolutePath)) {
235 item = m_cachedItems.value(absolutePath);
237 item = new AircraftItem(childDir, absolutePath);
240 m_nextCache[absolutePath] = item;
242 if (item->variantOf.isNull()) {
243 baseAircraft.insert(item->baseName(), item);
245 variants.append(item);
247 } catch (sg_exception& e) {
254 } // of set.xml iteration
256 // bind variants to their principals
257 Q_FOREACH(AircraftItem* item, variants) {
258 if (!baseAircraft.contains(item->variantOf)) {
259 qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
264 baseAircraft.value(item->variantOf)->variants.append(item);
267 // lock mutex while we modify the items array
269 QMutexLocker g(&m_lock);
270 m_items.append(baseAircraft.values());
274 } // of subdir iteration
276 qDebug() << "scan of" << path << "took" << t.elapsed();
281 QList<AircraftItem*> m_items;
283 QMap<QString, AircraftItem* > m_cachedItems;
284 QMap<QString, AircraftItem* > m_nextCache;
289 AircraftItemModel::AircraftItemModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
290 QAbstractListModel(pr),
292 m_packageRoot(rootRef)
296 AircraftItemModel::~AircraftItemModel()
298 abandonCurrentScan();
301 void AircraftItemModel::setPaths(QStringList paths)
306 void AircraftItemModel::scanDirs()
308 abandonCurrentScan();
313 m_activeVariant.clear();
316 QStringList dirs = m_paths;
318 Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
319 dirs << QString::fromStdString(ap);
322 SGPath rootAircraft(globals->get_fg_root());
323 rootAircraft.append("Aircraft");
324 dirs << QString::fromStdString(rootAircraft.str());
326 m_scanThread = new AircraftScanThread(dirs);
327 connect(m_scanThread, &AircraftScanThread::finished, this,
328 &AircraftItemModel::onScanFinished);
329 connect(m_scanThread, &AircraftScanThread::addedItems,
330 this, &AircraftItemModel::onScanResults);
331 m_scanThread->start();
335 void AircraftItemModel::abandonCurrentScan()
338 m_scanThread->setDone();
339 m_scanThread->wait(1000);
345 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
347 if (role == AircraftVariantRole) {
348 return m_activeVariant.at(index.row());
351 const AircraftItem* item(m_items.at(index.row()));
352 quint32 variantIndex = m_activeVariant.at(index.row());
353 return dataFromItem(item, variantIndex, role);
356 QVariant AircraftItemModel::dataFromItem(const AircraftItem* item, quint32 variantIndex, int role) const
358 if (role == AircraftVariantCountRole) {
359 return item->variants.count();
362 if (role == AircraftThumbnailCountRole) {
363 QPixmap p = item->thumbnail();
364 return p.isNull() ? 0 : 1;
367 if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
368 int variantIndex = role - AircraftVariantDescriptionRole;
369 return item->variants.at(variantIndex)->description;
373 if (variantIndex <= item->variants.count()) {
374 // show the selected variant
375 item = item->variants.at(variantIndex - 1);
379 if (role == Qt::DisplayRole) {
380 return item->description;
381 } else if (role == Qt::DecorationRole) {
382 return item->thumbnail();
383 } else if (role == AircraftPathRole) {
385 } else if (role == AircraftAuthorsRole) {
386 return item->authors;
387 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
388 return item->ratings[role - AircraftRatingRole];
389 } else if (role >= AircraftThumbnailRole) {
390 return item->thumbnail();
391 } else if (role == AircraftPackageIdRole) {
392 // can we fake an ID? otherwise fall through to a null variant
393 } else if (role == AircraftPackageStatusRole) {
394 return PackageInstalled; // always the case
395 } else if (role == Qt::ToolTipRole) {
402 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
404 if (role == Qt::DisplayRole) {
405 return QString::fromStdString(item->description());
406 } else if (role == AircraftPathRole) {
407 // can we return the theoretical path?
408 } else if (role == AircraftPackageIdRole) {
409 return QString::fromStdString(item->id());
410 } else if (role == AircraftPackageStatusRole) {
411 bool installed = item->isInstalled();
413 InstallRef i = item->existingInstall();
414 if (i->isDownloading()) {
415 return PackageDownloading;
417 if (i->hasUpdate()) {
418 return PackageUpdateAvailable;
421 return PackageInstalled;
423 return PackageNotInstalled;
430 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
432 if (role == AircraftVariantRole) {
433 m_activeVariant[index.row()] = value.toInt();
434 emit dataChanged(index, index);
441 QModelIndex AircraftItemModel::indexOfAircraftPath(QString path) const
443 for (int row=0; row <m_items.size(); ++row) {
444 const AircraftItem* item(m_items.at(row));
445 if (item->path == path) {
450 return QModelIndex();
453 void AircraftItemModel::onScanResults()
455 QList<AircraftItem*> newItems = m_scanThread->items();
456 if (newItems.isEmpty())
459 int firstRow = m_items.count();
460 int lastRow = firstRow + newItems.count() - 1;
461 beginInsertRows(QModelIndex(), firstRow, lastRow);
462 m_items.append(newItems);
464 // default variants in all cases
465 for (int i=0; i< newItems.count(); ++i) {
466 m_activeVariant.append(0);
471 void AircraftItemModel::onScanFinished()
477 #include "AircraftModel.moc"