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() :
47 // oh for C++11 initialisers
48 for (int i=0; i<4; ++i) ratings[i] = 0;
51 AircraftItem::AircraftItem(QDir dir, QString filePath) :
54 for (int i=0; i<4; ++i) ratings[i] = 0;
57 readProperties(filePath.toStdString(), &root);
59 if (!root.hasChild("sim")) {
60 throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
63 SGPropertyNode_ptr sim = root.getNode("sim");
66 pathModTime = QFileInfo(path).lastModified();
67 if (sim->getBoolValue("exclude-from-gui", false)) {
72 description = sim->getStringValue("description");
73 authors = sim->getStringValue("author");
75 if (sim->hasChild("rating")) {
76 SGPropertyNode_ptr ratingsNode = sim->getNode("rating");
77 ratings[0] = ratingsNode->getIntValue("FDM");
78 ratings[1] = ratingsNode->getIntValue("systems");
79 ratings[2] = ratingsNode->getIntValue("cockpit");
80 ratings[3] = ratingsNode->getIntValue("model");
84 if (sim->hasChild("variant-of")) {
85 variantOf = sim->getStringValue("variant-of");
89 QString AircraftItem::baseName() const
91 QString fn = QFileInfo(path).fileName();
92 fn.truncate(fn.count() - 8);
96 void AircraftItem::fromDataStream(QDataStream& ds)
98 ds >> path >> pathModTime >> excluded;
103 ds >> description >> authors >> variantOf;
104 for (int i=0; i<4; ++i) ds >> ratings[i];
107 void AircraftItem::toDataStream(QDataStream& ds) const
109 ds << path << pathModTime << excluded;
114 ds << description << authors << variantOf;
115 for (int i=0; i<4; ++i) ds << ratings[i];
118 QPixmap AircraftItem::thumbnail() const
120 if (m_thumbnail.isNull()) {
121 QFileInfo info(path);
122 QDir dir = info.dir();
123 if (dir.exists("thumbnail.jpg")) {
124 m_thumbnail.load(dir.filePath("thumbnail.jpg"));
125 // resize to the standard size
126 if (m_thumbnail.height() > 128) {
127 m_thumbnail = m_thumbnail.scaledToHeight(128);
136 static int CACHE_VERSION = 3;
138 class AircraftScanThread : public QThread
142 AircraftScanThread(QStringList dirsToScan) :
148 ~AircraftScanThread()
152 /** thread-safe access to items already scanned */
153 QList<AircraftItem*> items()
155 QList<AircraftItem*> result;
156 QMutexLocker g(&m_lock);
157 result.swap(m_items);
174 Q_FOREACH(QString d, m_dirs) {
175 scanAircraftDir(QDir(d));
188 QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
189 if (!cacheData.isEmpty()) {
190 QDataStream ds(cacheData);
191 quint32 count, cacheVersion;
192 ds >> cacheVersion >> count;
194 if (cacheVersion != CACHE_VERSION) {
195 return; // mis-matched cache, version, drop
198 for (int i=0; i<count; ++i) {
199 AircraftItem* item = new AircraftItem;
200 item->fromDataStream(ds);
202 QFileInfo finfo(item->path);
203 if (!finfo.exists() || (finfo.lastModified() != item->pathModTime)) {
206 // corresponding -set.xml file still exists and is
208 m_cachedItems[item->path] = item;
210 } // of cached item iteration
217 QByteArray cacheData;
219 QDataStream ds(&cacheData, QIODevice::WriteOnly);
220 quint32 count = m_nextCache.count();
221 ds << CACHE_VERSION << count;
223 Q_FOREACH(AircraftItem* item, m_nextCache.values()) {
224 item->toDataStream(ds);
228 settings.setValue("aircraft-cache", cacheData);
231 void scanAircraftDir(QDir path)
237 filters << "*-set.xml";
238 Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
239 QDir childDir(child.absoluteFilePath());
240 QMap<QString, AircraftItem*> baseAircraft;
241 QList<AircraftItem*> variants;
243 Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
245 QString absolutePath = xmlChild.absoluteFilePath();
246 AircraftItem* item = NULL;
248 if (m_cachedItems.contains(absolutePath)) {
249 item = m_cachedItems.value(absolutePath);
251 item = new AircraftItem(childDir, absolutePath);
254 m_nextCache[absolutePath] = item;
256 if (item->excluded) {
260 if (item->variantOf.isNull()) {
261 baseAircraft.insert(item->baseName(), item);
263 variants.append(item);
265 } catch (sg_exception& e) {
272 } // of set.xml iteration
274 // bind variants to their principals
275 Q_FOREACH(AircraftItem* item, variants) {
276 if (!baseAircraft.contains(item->variantOf)) {
277 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.append(baseAircraft.values());
292 } // of subdir iteration
297 QList<AircraftItem*> m_items;
299 QMap<QString, AircraftItem* > m_cachedItems;
300 QMap<QString, AircraftItem* > m_nextCache;
305 AircraftItemModel::AircraftItemModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
306 QAbstractListModel(pr),
308 m_packageRoot(rootRef)
312 AircraftItemModel::~AircraftItemModel()
314 abandonCurrentScan();
317 void AircraftItemModel::setPaths(QStringList paths)
322 void AircraftItemModel::scanDirs()
324 abandonCurrentScan();
329 m_activeVariant.clear();
332 QStringList dirs = m_paths;
334 Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
335 dirs << QString::fromStdString(ap);
338 SGPath rootAircraft(globals->get_fg_root());
339 rootAircraft.append("Aircraft");
340 dirs << QString::fromStdString(rootAircraft.str());
342 m_scanThread = new AircraftScanThread(dirs);
343 connect(m_scanThread, &AircraftScanThread::finished, this,
344 &AircraftItemModel::onScanFinished);
345 connect(m_scanThread, &AircraftScanThread::addedItems,
346 this, &AircraftItemModel::onScanResults);
347 m_scanThread->start();
351 void AircraftItemModel::abandonCurrentScan()
354 m_scanThread->setDone();
355 m_scanThread->wait(1000);
361 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
363 if (role == AircraftVariantRole) {
364 return m_activeVariant.at(index.row());
367 const AircraftItem* item(m_items.at(index.row()));
368 quint32 variantIndex = m_activeVariant.at(index.row());
369 return dataFromItem(item, variantIndex, role);
372 QVariant AircraftItemModel::dataFromItem(const AircraftItem* item, quint32 variantIndex, int role) const
374 if (role == AircraftVariantCountRole) {
375 return item->variants.count();
378 if (role == AircraftThumbnailCountRole) {
379 QPixmap p = item->thumbnail();
380 return p.isNull() ? 0 : 1;
383 if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
384 int variantIndex = role - AircraftVariantDescriptionRole;
385 return item->variants.at(variantIndex)->description;
389 if (variantIndex <= item->variants.count()) {
390 // show the selected variant
391 item = item->variants.at(variantIndex - 1);
395 if (role == Qt::DisplayRole) {
396 return item->description;
397 } else if (role == Qt::DecorationRole) {
398 return item->thumbnail();
399 } else if (role == AircraftPathRole) {
401 } else if (role == AircraftAuthorsRole) {
402 return item->authors;
403 } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
404 return item->ratings[role - AircraftRatingRole];
405 } else if (role >= AircraftThumbnailRole) {
406 return item->thumbnail();
407 } else if (role == AircraftPackageIdRole) {
408 // can we fake an ID? otherwise fall through to a null variant
409 } else if (role == AircraftPackageStatusRole) {
410 return PackageInstalled; // always the case
411 } else if (role == Qt::ToolTipRole) {
413 } else if (role == AircraftHasRatingsRole) {
415 for (int i=0; i<4; ++i) {
416 have |= (item->ratings[i] > 0);
419 } else if (role == AircraftLongDescriptionRole) {
420 return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
421 "no escape from reality. Open your eyes, like up to the skies and see. "
422 "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
423 "Litte high, little low. Anywhere the wind blows.";
429 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
431 if (role == Qt::DisplayRole) {
432 return QString::fromStdString(item->name());
433 } else if (role == AircraftPathRole) {
434 // can we return the theoretical path?
435 } else if (role == AircraftPackageIdRole) {
436 return QString::fromStdString(item->id());
437 } else if (role == AircraftPackageStatusRole) {
438 bool installed = item->isInstalled();
440 InstallRef i = item->existingInstall();
441 if (i->isDownloading()) {
442 return PackageDownloading;
444 if (i->hasUpdate()) {
445 return PackageUpdateAvailable;
448 return PackageInstalled;
450 return PackageNotInstalled;
452 } else if (role == AircraftLongDescriptionRole) {
453 return QString::fromStdString(item->description());
459 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
461 if (role == AircraftVariantRole) {
462 m_activeVariant[index.row()] = value.toInt();
463 emit dataChanged(index, index);
470 QModelIndex AircraftItemModel::indexOfAircraftPath(QString path) const
472 for (int row=0; row <m_items.size(); ++row) {
473 const AircraftItem* item(m_items.at(row));
474 if (item->path == path) {
479 return QModelIndex();
482 void AircraftItemModel::onScanResults()
484 QList<AircraftItem*> newItems = m_scanThread->items();
485 if (newItems.isEmpty())
488 int firstRow = m_items.count();
489 int lastRow = firstRow + newItems.count() - 1;
490 beginInsertRows(QModelIndex(), firstRow, lastRow);
491 m_items.append(newItems);
493 // default variants in all cases
494 for (int i=0; i< newItems.count(); ++i) {
495 m_activeVariant.append(0);
500 void AircraftItemModel::onScanFinished()
506 #include "AircraftModel.moc"