]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
Explicit AppKit includes for Mac.
[flightgear.git] / src / GUI / AircraftModel.cxx
1 // AircraftModel.cxx - part of GUI launcher using Qt5
2 //
3 // Written by James Turner, started March 2015.
4 //
5 // Copyright (C) 2015 James Turner <zakalawe@mac.com>
6 //
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.
11 //
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.
16 //
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.
20
21 #include "AircraftModel.hxx"
22
23 #include <QDir>
24 #include <QThread>
25 #include <QMutex>
26 #include <QMutexLocker>
27 #include <QDataStream>
28 #include <QSettings>
29 #include <QDebug>
30
31 // Simgear
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>
38
39 // FlightGear
40 #include <Main/globals.hxx>
41
42 using namespace simgear::pkg;
43
44 AircraftItem::AircraftItem() :
45     excluded(false)
46 {
47     // oh for C++11 initialisers
48     for (int i=0; i<4; ++i) ratings[i] = 0;
49 }
50
51 AircraftItem::AircraftItem(QDir dir, QString filePath) :
52     excluded(false)
53 {
54     for (int i=0; i<4; ++i) ratings[i] = 0;
55
56     SGPropertyNode root;
57     readProperties(filePath.toStdString(), &root);
58
59     if (!root.hasChild("sim")) {
60         throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
61     }
62
63     SGPropertyNode_ptr sim = root.getNode("sim");
64
65     path = filePath;
66     pathModTime = QFileInfo(path).lastModified();
67     if (sim->getBoolValue("exclude-from-gui", false)) {
68         excluded = true;
69         return;
70     }
71
72     description = sim->getStringValue("description");
73     authors =  sim->getStringValue("author");
74
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");
81
82     }
83
84     if (sim->hasChild("variant-of")) {
85         variantOf = sim->getStringValue("variant-of");
86     }
87 }
88
89 QString AircraftItem::baseName() const
90 {
91     QString fn = QFileInfo(path).fileName();
92     fn.truncate(fn.count() - 8);
93     return fn;
94 }
95
96 void AircraftItem::fromDataStream(QDataStream& ds)
97 {
98     ds >> path >> pathModTime >> excluded;
99     if (excluded) {
100         return;
101     }
102
103     ds >> description >> authors >> variantOf;
104     for (int i=0; i<4; ++i) ds >> ratings[i];
105 }
106
107 void AircraftItem::toDataStream(QDataStream& ds) const
108 {
109     ds << path << pathModTime << excluded;
110     if (excluded) {
111         return;
112     }
113
114     ds << description << authors << variantOf;
115     for (int i=0; i<4; ++i) ds << ratings[i];
116 }
117
118 QPixmap AircraftItem::thumbnail() const
119 {
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);
128             }
129         }
130     }
131
132     return m_thumbnail;
133 }
134
135
136 static int CACHE_VERSION = 3;
137
138 class AircraftScanThread : public QThread
139 {
140     Q_OBJECT
141 public:
142     AircraftScanThread(QStringList dirsToScan) :
143         m_dirs(dirsToScan),
144         m_done(false)
145     {
146     }
147
148     ~AircraftScanThread()
149     {
150     }
151
152     /** thread-safe access to items already scanned */
153     QList<AircraftItem*> items()
154     {
155         QList<AircraftItem*> result;
156         QMutexLocker g(&m_lock);
157         result.swap(m_items);
158         g.unlock();
159         return result;
160     }
161
162     void setDone()
163     {
164         m_done = true;
165     }
166 Q_SIGNALS:
167     void addedItems();
168
169 protected:
170     virtual void run()
171     {
172         readCache();
173
174         Q_FOREACH(QString d, m_dirs) {
175             scanAircraftDir(QDir(d));
176             if (m_done) {
177                 return;
178             }
179         }
180
181         writeCache();
182     }
183
184 private:
185     void readCache()
186     {
187         QSettings settings;
188         QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
189         if (!cacheData.isEmpty()) {
190             QDataStream ds(cacheData);
191             quint32 count, cacheVersion;
192             ds >> cacheVersion >> count;
193
194             if (cacheVersion != CACHE_VERSION) {
195                 return; // mis-matched cache, version, drop
196             }
197
198              for (int i=0; i<count; ++i) {
199                 AircraftItem* item = new AircraftItem;
200                 item->fromDataStream(ds);
201
202                 QFileInfo finfo(item->path);
203                 if (!finfo.exists() || (finfo.lastModified() != item->pathModTime)) {
204                     delete item;
205                 } else {
206                     // corresponding -set.xml file still exists and is
207                     // unmodified
208                     m_cachedItems[item->path] = item;
209                 }
210             } // of cached item iteration
211         }
212     }
213
214     void writeCache()
215     {
216         QSettings settings;
217         QByteArray cacheData;
218         {
219             QDataStream ds(&cacheData, QIODevice::WriteOnly);
220             quint32 count = m_nextCache.count();
221             ds << CACHE_VERSION << count;
222
223             Q_FOREACH(AircraftItem* item, m_nextCache.values()) {
224                 item->toDataStream(ds);
225             }
226         }
227
228         settings.setValue("aircraft-cache", cacheData);
229     }
230
231     void scanAircraftDir(QDir path)
232     {
233         QTime t;
234         t.start();
235
236         QStringList filters;
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;
242
243             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
244                 try {
245                     QString absolutePath = xmlChild.absoluteFilePath();
246                     AircraftItem* item = NULL;
247
248                     if (m_cachedItems.contains(absolutePath)) {
249                         item = m_cachedItems.value(absolutePath);
250                     } else {
251                         item = new AircraftItem(childDir, absolutePath);
252                     }
253
254                     m_nextCache[absolutePath] = item;
255
256                     if (item->excluded) {
257                         continue;
258                     }
259
260                     if (item->variantOf.isNull()) {
261                         baseAircraft.insert(item->baseName(), item);
262                     } else {
263                         variants.append(item);
264                     }
265                 } catch (sg_exception& e) {
266                     continue;
267                 }
268
269                 if (m_done) {
270                     return;
271                 }
272             } // of set.xml iteration
273
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;
278                     delete item;
279                     continue;
280                 }
281
282                 baseAircraft.value(item->variantOf)->variants.append(item);
283             }
284
285             // lock mutex while we modify the items array
286             {
287                 QMutexLocker g(&m_lock);
288                 m_items.append(baseAircraft.values());
289             }
290
291             emit addedItems();
292         } // of subdir iteration
293     }
294
295     QMutex m_lock;
296     QStringList m_dirs;
297     QList<AircraftItem*> m_items;
298
299     QMap<QString, AircraftItem* > m_cachedItems;
300     QMap<QString, AircraftItem* > m_nextCache;
301
302     bool m_done;
303 };
304
305 AircraftItemModel::AircraftItemModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
306     QAbstractListModel(pr),
307     m_scanThread(NULL),
308     m_packageRoot(rootRef)
309 {
310 }
311
312 AircraftItemModel::~AircraftItemModel()
313 {
314     abandonCurrentScan();
315 }
316
317 void AircraftItemModel::setPaths(QStringList paths)
318 {
319     m_paths = paths;
320 }
321
322 void AircraftItemModel::scanDirs()
323 {
324     abandonCurrentScan();
325
326     beginResetModel();
327     qDeleteAll(m_items);
328     m_items.clear();
329     m_activeVariant.clear();
330     endResetModel();
331
332     QStringList dirs = m_paths;
333
334     Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
335         dirs << QString::fromStdString(ap);
336     }
337
338     SGPath rootAircraft(globals->get_fg_root());
339     rootAircraft.append("Aircraft");
340     dirs << QString::fromStdString(rootAircraft.str());
341
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();
348
349 }
350
351 void AircraftItemModel::abandonCurrentScan()
352 {
353     if (m_scanThread) {
354         m_scanThread->setDone();
355         m_scanThread->wait(1000);
356         delete m_scanThread;
357         m_scanThread = NULL;
358     }
359 }
360
361 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
362 {
363     if (role == AircraftVariantRole) {
364         return m_activeVariant.at(index.row());
365     }
366
367     const AircraftItem* item(m_items.at(index.row()));
368     quint32 variantIndex = m_activeVariant.at(index.row());
369     return dataFromItem(item, variantIndex, role);
370 }
371
372 QVariant AircraftItemModel::dataFromItem(const AircraftItem* item, quint32 variantIndex, int role) const
373 {
374     if (role == AircraftVariantCountRole) {
375         return item->variants.count();
376     }
377
378     if (role == AircraftThumbnailCountRole) {
379         QPixmap p = item->thumbnail();
380         return p.isNull() ? 0 : 1;
381     }
382
383     if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
384         int variantIndex = role - AircraftVariantDescriptionRole;
385         return item->variants.at(variantIndex)->description;
386     }
387
388     if (variantIndex) {
389         if (variantIndex <= item->variants.count()) {
390             // show the selected variant
391             item = item->variants.at(variantIndex - 1);
392         }
393     }
394
395     if (role == Qt::DisplayRole) {
396         return item->description;
397     } else if (role == Qt::DecorationRole) {
398         return item->thumbnail();
399     } else if (role == AircraftPathRole) {
400         return item->path;
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) {
412         return item->path;
413     } else if (role == AircraftHasRatingsRole) {
414         bool have = false;
415         for (int i=0; i<4; ++i) {
416             have |= (item->ratings[i] > 0);
417         }
418         return have;
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.";
424     }
425
426     return QVariant();
427 }
428
429 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
430 {
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();
439         if (installed) {
440             InstallRef i = item->existingInstall();
441             if (i->isDownloading()) {
442                 return PackageDownloading;
443             }
444             if (i->hasUpdate()) {
445                 return PackageUpdateAvailable;
446             }
447
448             return PackageInstalled;
449         } else {
450             return PackageNotInstalled;
451         }
452     } else if (role == AircraftLongDescriptionRole) {
453         return QString::fromStdString(item->description());
454     }
455
456     return QVariant();
457 }
458
459 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
460   {
461       if (role == AircraftVariantRole) {
462           m_activeVariant[index.row()] = value.toInt();
463           emit dataChanged(index, index);
464           return true;
465       }
466
467       return false;
468   }
469
470 QModelIndex AircraftItemModel::indexOfAircraftPath(QString path) const
471 {
472     for (int row=0; row <m_items.size(); ++row) {
473         const AircraftItem* item(m_items.at(row));
474         if (item->path == path) {
475             return index(row);
476         }
477     }
478
479     return QModelIndex();
480 }
481
482 void AircraftItemModel::onScanResults()
483 {
484     QList<AircraftItem*> newItems = m_scanThread->items();
485     if (newItems.isEmpty())
486         return;
487
488     int firstRow = m_items.count();
489     int lastRow = firstRow + newItems.count() - 1;
490     beginInsertRows(QModelIndex(), firstRow, lastRow);
491     m_items.append(newItems);
492
493     // default variants in all cases
494     for (int i=0; i< newItems.count(); ++i) {
495         m_activeVariant.append(0);
496     }
497     endInsertRows();
498 }
499
500 void AircraftItemModel::onScanFinished()
501 {
502     delete m_scanThread;
503     m_scanThread = NULL;
504 }
505
506 #include "AircraftModel.moc"