]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
Disable testing hacks for package installs.
[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 #if 0
421         return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
422             "no escape from reality. Open your eyes, like up to the skies and see. "
423             "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
424             "Litte high, little low. Anywhere the wind blows.";
425 #endif
426     }
427
428     return QVariant();
429 }
430
431 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
432 {
433     if (role == Qt::DisplayRole) {
434         return QString::fromStdString(item->name());
435     } else if (role == AircraftPathRole) {
436         // can we return the theoretical path?
437     } else if (role == AircraftPackageIdRole) {
438         return QString::fromStdString(item->id());
439     } else if (role == AircraftPackageStatusRole) {
440         bool installed = item->isInstalled();
441         if (installed) {
442             InstallRef i = item->existingInstall();
443             if (i->isDownloading()) {
444                 return PackageDownloading;
445             }
446             if (i->hasUpdate()) {
447                 return PackageUpdateAvailable;
448             }
449
450             return PackageInstalled;
451         } else {
452             return PackageNotInstalled;
453         }
454     } else if (role == AircraftLongDescriptionRole) {
455         return QString::fromStdString(item->description());
456     }
457
458     return QVariant();
459 }
460
461 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
462   {
463       if (role == AircraftVariantRole) {
464           m_activeVariant[index.row()] = value.toInt();
465           emit dataChanged(index, index);
466           return true;
467       }
468
469       return false;
470   }
471
472 QModelIndex AircraftItemModel::indexOfAircraftPath(QString path) const
473 {
474     for (int row=0; row <m_items.size(); ++row) {
475         const AircraftItem* item(m_items.at(row));
476         if (item->path == path) {
477             return index(row);
478         }
479     }
480
481     return QModelIndex();
482 }
483
484 void AircraftItemModel::onScanResults()
485 {
486     QList<AircraftItem*> newItems = m_scanThread->items();
487     if (newItems.isEmpty())
488         return;
489
490     int firstRow = m_items.count();
491     int lastRow = firstRow + newItems.count() - 1;
492     beginInsertRows(QModelIndex(), firstRow, lastRow);
493     m_items.append(newItems);
494
495     // default variants in all cases
496     for (int i=0; i< newItems.count(); ++i) {
497         m_activeVariant.append(0);
498     }
499     endInsertRows();
500 }
501
502 void AircraftItemModel::onScanFinished()
503 {
504     delete m_scanThread;
505     m_scanThread = NULL;
506 }
507
508 #include "AircraftModel.moc"