]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
Hacking on the delegate height.
[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 {
46     // oh for C++11 initialisers
47     for (int i=0; i<4; ++i) ratings[i] = 0;
48 }
49
50 AircraftItem::AircraftItem(QDir dir, QString filePath)
51 {
52     for (int i=0; i<4; ++i) ratings[i] = 0;
53
54     SGPropertyNode root;
55     readProperties(filePath.toStdString(), &root);
56
57     if (!root.hasChild("sim")) {
58         throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
59     }
60
61     SGPropertyNode_ptr sim = root.getNode("sim");
62
63     path = filePath;
64     pathModTime = QFileInfo(path).lastModified();
65
66     description = sim->getStringValue("description");
67     authors =  sim->getStringValue("author");
68
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");
75
76     }
77
78     if (sim->hasChild("variant-of")) {
79         variantOf = sim->getStringValue("variant-of");
80     }
81 }
82
83 QString AircraftItem::baseName() const
84 {
85     QString fn = QFileInfo(path).fileName();
86     fn.truncate(fn.count() - 8);
87     return fn;
88 }
89
90 void AircraftItem::fromDataStream(QDataStream& ds)
91 {
92     ds >> path >> description >> authors >> variantOf;
93     for (int i=0; i<4; ++i) ds >> ratings[i];
94     ds >> pathModTime;
95 }
96
97 void AircraftItem::toDataStream(QDataStream& ds) const
98 {
99     ds << path << description << authors << variantOf;
100     for (int i=0; i<4; ++i) ds << ratings[i];
101     ds << pathModTime;
102 }
103
104 QPixmap AircraftItem::thumbnail() const
105 {
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);
114             }
115         }
116     }
117
118     return m_thumbnail;
119 }
120
121
122 static int CACHE_VERSION = 2;
123
124 class AircraftScanThread : public QThread
125 {
126     Q_OBJECT
127 public:
128     AircraftScanThread(QStringList dirsToScan) :
129         m_dirs(dirsToScan),
130         m_done(false)
131     {
132     }
133
134     ~AircraftScanThread()
135     {
136     }
137
138     /** thread-safe access to items already scanned */
139     QList<AircraftItem*> items()
140     {
141         QList<AircraftItem*> result;
142         QMutexLocker g(&m_lock);
143         result.swap(m_items);
144         g.unlock();
145         return result;
146     }
147
148     void setDone()
149     {
150         m_done = true;
151     }
152 Q_SIGNALS:
153     void addedItems();
154
155 protected:
156     virtual void run()
157     {
158         readCache();
159
160         Q_FOREACH(QString d, m_dirs) {
161             scanAircraftDir(QDir(d));
162             if (m_done) {
163                 return;
164             }
165         }
166
167         writeCache();
168     }
169
170 private:
171     void readCache()
172     {
173         QSettings settings;
174         QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
175         if (!cacheData.isEmpty()) {
176             QDataStream ds(cacheData);
177             quint32 count, cacheVersion;
178             ds >> cacheVersion >> count;
179
180             if (cacheVersion != CACHE_VERSION) {
181                 return; // mis-matched cache, version, drop
182             }
183
184              for (int i=0; i<count; ++i) {
185                 AircraftItem* item = new AircraftItem;
186                 item->fromDataStream(ds);
187
188                 QFileInfo finfo(item->path);
189                 if (!finfo.exists() || (finfo.lastModified() != item->pathModTime)) {
190                     delete item;
191                 } else {
192                     // corresponding -set.xml file still exists and is
193                     // unmodified
194                     m_cachedItems[item->path] = item;
195                 }
196             } // of cached item iteration
197         }
198     }
199
200     void writeCache()
201     {
202         QSettings settings;
203         QByteArray cacheData;
204         {
205             QDataStream ds(&cacheData, QIODevice::WriteOnly);
206             quint32 count = m_nextCache.count();
207             ds << CACHE_VERSION << count;
208
209             Q_FOREACH(AircraftItem* item, m_nextCache.values()) {
210                 item->toDataStream(ds);
211             }
212         }
213
214         settings.setValue("aircraft-cache", cacheData);
215     }
216
217     void scanAircraftDir(QDir path)
218     {
219         QTime t;
220         t.start();
221
222         QStringList filters;
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;
228
229             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
230                 try {
231                     QString absolutePath = xmlChild.absoluteFilePath();
232                     AircraftItem* item = NULL;
233
234                     if (m_cachedItems.contains(absolutePath)) {
235                         item = m_cachedItems.value(absolutePath);
236                     } else {
237                         item = new AircraftItem(childDir, absolutePath);
238                     }
239
240                     m_nextCache[absolutePath] = item;
241
242                     if (item->variantOf.isNull()) {
243                         baseAircraft.insert(item->baseName(), item);
244                     } else {
245                         variants.append(item);
246                     }
247                 } catch (sg_exception& e) {
248                     continue;
249                 }
250
251                 if (m_done) {
252                     return;
253                 }
254             } // of set.xml iteration
255
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;
260                     delete item;
261                     continue;
262                 }
263
264                 baseAircraft.value(item->variantOf)->variants.append(item);
265             }
266
267             // lock mutex while we modify the items array
268             {
269                 QMutexLocker g(&m_lock);
270                 m_items.append(baseAircraft.values());
271             }
272
273             emit addedItems();
274         } // of subdir iteration
275
276         qDebug() << "scan of" << path << "took" << t.elapsed();
277     }
278
279     QMutex m_lock;
280     QStringList m_dirs;
281     QList<AircraftItem*> m_items;
282
283     QMap<QString, AircraftItem* > m_cachedItems;
284     QMap<QString, AircraftItem* > m_nextCache;
285
286     bool m_done;
287 };
288
289 AircraftItemModel::AircraftItemModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
290     QAbstractListModel(pr),
291     m_scanThread(NULL),
292     m_packageRoot(rootRef)
293 {
294 }
295
296 AircraftItemModel::~AircraftItemModel()
297 {
298     abandonCurrentScan();
299 }
300
301 void AircraftItemModel::setPaths(QStringList paths)
302 {
303     m_paths = paths;
304 }
305
306 void AircraftItemModel::scanDirs()
307 {
308     abandonCurrentScan();
309
310     beginResetModel();
311     qDeleteAll(m_items);
312     m_items.clear();
313     m_activeVariant.clear();
314     endResetModel();
315
316     QStringList dirs = m_paths;
317
318     Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
319         dirs << QString::fromStdString(ap);
320     }
321
322     SGPath rootAircraft(globals->get_fg_root());
323     rootAircraft.append("Aircraft");
324     dirs << QString::fromStdString(rootAircraft.str());
325
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();
332
333 }
334
335 void AircraftItemModel::abandonCurrentScan()
336 {
337     if (m_scanThread) {
338         m_scanThread->setDone();
339         m_scanThread->wait(1000);
340         delete m_scanThread;
341         m_scanThread = NULL;
342     }
343 }
344
345 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
346 {
347     if (role == AircraftVariantRole) {
348         return m_activeVariant.at(index.row());
349     }
350
351     const AircraftItem* item(m_items.at(index.row()));
352     quint32 variantIndex = m_activeVariant.at(index.row());
353     return dataFromItem(item, variantIndex, role);
354 }
355
356 QVariant AircraftItemModel::dataFromItem(const AircraftItem* item, quint32 variantIndex, int role) const
357 {
358     if (role == AircraftVariantCountRole) {
359         return item->variants.count();
360     }
361
362     if (role == AircraftThumbnailCountRole) {
363         QPixmap p = item->thumbnail();
364         return p.isNull() ? 0 : 1;
365     }
366
367     if ((role >= AircraftVariantDescriptionRole) && (role < AircraftThumbnailRole)) {
368         int variantIndex = role - AircraftVariantDescriptionRole;
369         return item->variants.at(variantIndex)->description;
370     }
371
372     if (variantIndex) {
373         if (variantIndex <= item->variants.count()) {
374             // show the selected variant
375             item = item->variants.at(variantIndex - 1);
376         }
377     }
378
379     if (role == Qt::DisplayRole) {
380         return item->description;
381     } else if (role == Qt::DecorationRole) {
382         return item->thumbnail();
383     } else if (role == AircraftPathRole) {
384         return item->path;
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) {
396         return item->path;
397     } else if (role == AircraftLongDescriptionRole) {
398         return "Lorum Ipsum, etc. Is this the real life? Is this just fantasy? Caught in a land-slide, "
399             "no escape from reality. Open your eyes, like up to the skies and see. "
400             "I'm just a poor boy, I need no sympathy because I'm easy come, easy go."
401             "Litte high, little low. Anywhere the wind blows.";
402     }
403
404     return QVariant();
405 }
406
407 QVariant AircraftItemModel::dataFromPackage(const PackageRef& item, quint32 variantIndex, int role) const
408 {
409     if (role == Qt::DisplayRole) {
410         return QString::fromStdString(item->name());
411     } else if (role == AircraftPathRole) {
412         // can we return the theoretical path?
413     } else if (role == AircraftPackageIdRole) {
414         return QString::fromStdString(item->id());
415     } else if (role == AircraftPackageStatusRole) {
416         bool installed = item->isInstalled();
417         if (installed) {
418             InstallRef i = item->existingInstall();
419             if (i->isDownloading()) {
420                 return PackageDownloading;
421             }
422             if (i->hasUpdate()) {
423                 return PackageUpdateAvailable;
424             }
425
426             return PackageInstalled;
427         } else {
428             return PackageNotInstalled;
429         }
430     } else if (role == AircraftLongDescriptionRole) {
431         return QString::fromStdString(item->description());
432     }
433
434     return QVariant();
435 }
436
437 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
438   {
439       if (role == AircraftVariantRole) {
440           m_activeVariant[index.row()] = value.toInt();
441           emit dataChanged(index, index);
442           return true;
443       }
444
445       return false;
446   }
447
448 QModelIndex AircraftItemModel::indexOfAircraftPath(QString path) const
449 {
450     for (int row=0; row <m_items.size(); ++row) {
451         const AircraftItem* item(m_items.at(row));
452         if (item->path == path) {
453             return index(row);
454         }
455     }
456
457     return QModelIndex();
458 }
459
460 void AircraftItemModel::onScanResults()
461 {
462     QList<AircraftItem*> newItems = m_scanThread->items();
463     if (newItems.isEmpty())
464         return;
465
466     int firstRow = m_items.count();
467     int lastRow = firstRow + newItems.count() - 1;
468     beginInsertRows(QModelIndex(), firstRow, lastRow);
469     m_items.append(newItems);
470
471     // default variants in all cases
472     for (int i=0; i< newItems.count(); ++i) {
473         m_activeVariant.append(0);
474     }
475     endInsertRows();
476 }
477
478 void AircraftItemModel::onScanFinished()
479 {
480     delete m_scanThread;
481     m_scanThread = NULL;
482 }
483
484 #include "AircraftModel.moc"