FlightGear next
AddOnsController.cxx
Go to the documentation of this file.
2
3#include <QSettings>
4#include <QFileDialog>
5#include <QMessageBox>
6#include <QDebug>
7#include <QQmlComponent>
8#include <QDesktopServices>
9#include <QValidator>
10
11#include <simgear/package/Root.hxx>
12#include <simgear/package/Catalog.hxx>
13
14#include <Main/globals.hxx>
15#include <Main/options.hxx>
18
19#include "Add-ons/Addon.hxx"
20#include "Add-ons/addon_fwd.hxx"
23#include "CatalogListModel.hxx"
24#include "AddonsModel.hxx"
26#include "QtLauncher.hxx"
27#include "PathListModel.hxx"
28#include "LaunchConfig.hxx"
29
31
33 QObject(parent),
34 m_launcher(parent),
35 m_config(config)
36{
37 m_catalogs = new CatalogListModel(this,
38 simgear::pkg::RootRef(globals->packageRoot()));
39
40 connect(m_catalogs, &CatalogListModel::catalogsChanged, this, &AddOnsController::onCatalogsChanged);
41
42 m_addonsModuleModel = new AddonsModel(this);
43 connect(m_addonsModuleModel, &AddonsModel::modulesChanged, this, &AddOnsController::onAddonsChanged);
44
45 using namespace flightgear::addons;
46
47 m_sceneryPaths = new PathListModel(this);
48 connect(m_sceneryPaths, &PathListModel::enabledPathsChanged, [this] () {
49 m_sceneryPaths->saveToSettings("scenery-paths-v2");
51 });
52 m_sceneryPaths->loadFromSettings("scenery-paths-v2");
53
54 m_aircraftPaths = new PathListModel(this);
55 m_aircraftPaths->loadFromSettings("aircraft-paths-v2");
56
57 // sync up the aircraft cache now
58 setLocalAircraftPaths();
59
60 // watch for future changes
61 connect(m_aircraftPaths, &PathListModel::enabledPathsChanged, [this] () {
62 m_aircraftPaths->saveToSettings("aircraft-paths-v2");
63 setLocalAircraftPaths();
64 });
65
66 QSettings settings;
67 int size = settings.beginReadArray("addon-modules");
68 for (int i = 0; i < size; ++i) {
69 settings.setArrayIndex(i);
70
71 QString path = settings.value("path").toString();
72 const SGPath addonPath(path.toStdString());
73 if (!addonPath.exists()) {
74 // drop non-existing paths on load
75 continue;
76 }
77
78 m_addonModulePaths.push_back( path );
79 bool enable = settings.value("enable").toBool();
80
81 try {
82 auto addon = Addon::fromAddonDir<AddonRef>(addonPath);
83 m_addonsModuleModel->append(path, addon, enable);
84 }
85 catch (const sg_exception &e) {
86 std::string msg = "Error getting add-on metadata: " + e.getFormattedMessage();
87 SG_LOG(SG_GENERAL, SG_ALERT, msg);
88 }
89 }
90 settings.endArray();
91
92 qmlRegisterUncreatableType<AddOnsController>("FlightGear.Launcher", 1, 0, "AddOnsControllers", "no");
93 qmlRegisterUncreatableType<CatalogListModel>("FlightGear.Launcher", 1, 0, "CatalogListModel", "no");
94 qmlRegisterUncreatableType<AddonsModel>("FlightGear.Launcher", 1, 0, "AddonsModel", "no");
95 qmlRegisterUncreatableType<PathListModel>("FlightGear.Launcher", 1, 0, "PathListMode", "no");
96
97 connect(m_config, &LaunchConfig::collect,
99}
100
101void AddOnsController::setLocalAircraftPaths()
102{
103 auto aircraftCache = LocalAircraftCache::instance();
104
105 QStringList paths;
106
107 const auto commandLineAircraftPaths = flightgear::Options::sharedInstance()->valuesForOption("fg-aircraft");
108 for (const auto& arg : commandLineAircraftPaths) {
109 // inner loop becuase a single arg can define multiple paths
110 for (const auto& p : SGPath::pathsFromUtf8(arg)) {
111 paths.append(QString::fromStdString(p.utf8Str()));
112 }
113 }
114
115 paths.append(m_aircraftPaths->enabledPaths());
116 aircraftCache->setPaths(paths);
117 aircraftCache->scanDirs();
118}
119
121{
122 return m_aircraftPaths;
123}
124
126{
127 return m_sceneryPaths;
128}
129
131{
132 return m_addonModulePaths;
133}
134
136{
137 QString path = QFileDialog::getExistingDirectory(nullptr, tr("Choose aircraft folder"));
138 if (path.isEmpty()) {
139 return {};
140 }
141
142 // the user might add a directory containing an 'Aircraft' subdir. Let's attempt
143 // to check for that case and handle it gracefully.
144 bool pathOk = false;
146 pathOk = true;
147 } else {
148 // no aircraft in specified path, look for Aircraft/ subdir
149 QDir d(path);
150 if (d.exists("Aircraft")) {
151 QString p2 = d.filePath("Aircraft");
153 pathOk = true;
154 path = p2;
155 }
156 }
157 }
158
159 if (!pathOk) {
160 QMessageBox mb;
161 mb.setText(tr("No aircraft found in the folder '%1' - add anyway?").arg(path));
162 mb.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
163 mb.setDefaultButton(QMessageBox::No);
164 mb.exec();
165
166 if (mb.result() == QMessageBox::No) {
167 return {};
168 }
169
170 flightgear::addSentryBreadcrumb("User continued adding add-on with invalid path", "info");
171 }
172
173 m_aircraftPaths->appendPath(path);
174 return path;
175}
176
177
179{
180 QString path = QFileDialog::getExistingDirectory(nullptr, tr("Choose addon module folder"));
181 if (path.isEmpty()) {
182 return {};
183
184 }
185
186 // validation
187 SGPath p(path.toStdString());
188 bool isValid = false;
189
190 for (const auto& file: {"addon-metadata.xml", "addon-main.nas"}) {
191 if ((p / file).exists()) {
192 isValid = true;
193 break;
194 }
195 }
196
197 if (!isValid) {
198 QMessageBox mb;
199 mb.setText(tr("The folder '%1' doesn't appear to contain an addon module - add anyway?").arg(path));
200 mb.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
201 mb.setDefaultButton(QMessageBox::No);
202 mb.setInformativeText(tr("Added modules should contain at least both of the following "
203 "files: addon-metadata.xml, addon-main.nas."));
204 mb.exec();
205
206 if (mb.result() == QMessageBox::No) {
207 return {};
208 }
209 }
210
211 using namespace flightgear::addons;
212 const SGPath addonPath(path.toStdString());
213
214 try {
215 // when newly added, enable by default
216 auto addon = Addon::fromAddonDir<AddonRef>(addonPath);
217 if (!m_addonsModuleModel->append(path, addon, true)) {
218 path = QString("");
219 }
220 } catch (const sg_exception &e) {
221 std::string msg = "Error getting add-on metadata: " + e.getFormattedMessage();
222 SG_LOG(SG_GENERAL, SG_ALERT, msg);
223 }
224
225 return path;
226}
227
229{
230 QString path = QFileDialog::getExistingDirectory(nullptr, tr("Choose scenery folder"));
231 if (path.isEmpty()) {
232 return {};
233
234 }
235
236 // validation
237 SGPath p(path.toStdString());
238 bool isValid = false;
239
240 for (const auto& dir: {"Objects", "Terrain", "Buildings", "Roads", "Pylons", "NavData", "Airports", "Orthophotos", "vpb"}) {
241 if ((p / dir).exists()) {
242 isValid = true;
243 break;
244 }
245 }
246
247 if (!isValid) {
248 QMessageBox mb;
249 mb.setText(tr("The folder '%1' doesn't appear to contain scenery - add anyway?").arg(path));
250 mb.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
251 mb.setDefaultButton(QMessageBox::No);
252 mb.setInformativeText(tr("Added scenery should contain at least one of the following "
253 "folders: Objects, Terrain, Buildings, Roads, Pylons, NavData, Airports, Orthophotos, vpb."));
254 mb.exec();
255
256 if (mb.result() == QMessageBox::No) {
257 return {};
258 }
259 }
260
261 m_sceneryPaths->appendPath(path);
262 return path;
263}
264
266{
267 QSettings settings;
268 QString downloadDir = settings.value("download-dir").toString();
269 InstallSceneryDialog dlg(nullptr, downloadDir);
270 if (dlg.exec() == QDialog::Accepted) {
271 return dlg.sceneryPath();
272 }
273
274 return {};
275}
276
278{
279 QUrl u = QUrl::fromLocalFile(path);
280 QDesktopServices::openUrl(u);
281}
282
287
288
290{
291 if (m_addonModulePaths == modulePaths)
292 return;
293
294 m_addonModulePaths = modulePaths;
295
296 m_addonsModuleModel->resetData(modulePaths);
297
298 QSettings settings;
299 int i = 0;
300 settings.beginWriteArray("addon-modules");
301 for (const QString& path : modulePaths) {
302 if (m_addonsModuleModel->containsPath(path)) {
303 settings.setArrayIndex(i++);
304 settings.setValue("path", path);
305 settings.setValue("enable", m_addonsModuleModel->getPathEnable(path));
306 }
307 }
308 settings.endArray();
309
310 emit modulePathsChanged(m_addonModulePaths);
311 emit modulesChanged();
312}
313
315{
316 if (s == "hide") {
317 QSettings settings;
318 settings.setValue("hide-official-catalog-message", true);
319 } else if (s == "add-official") {
320 flightgear::addSentryBreadcrumb("user requested to add the default catalog", "info");
321 m_catalogs->installDefaultCatalog(false);
322 }
323
325}
326
327bool AddOnsController::shouldShowOfficialCatalogMessage() const
328{
329 QSettings settings;
330 bool showOfficialCatalogMesssage = !globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
331 if (settings.value("hide-official-catalog-message").toBool()) {
332 showOfficialCatalogMesssage = false;
333 }
334 return showOfficialCatalogMesssage;
335}
336
337
339{
340 return globals->get_subsystem<FGHTTPClient>()->isDefaultCatalogInstalled();
341}
342
344{
345 return shouldShowOfficialCatalogMessage();
346}
347
348void AddOnsController::onCatalogsChanged()
349{
352}
353
354
356{
357 QSettings settings;
358
359 int i = 0;
360 settings.beginWriteArray("addon-modules");
361 for (const auto& path : m_addonModulePaths) {
362 settings.setArrayIndex(i++);
363 settings.setValue("path", path);
364 settings.setValue("enable", m_addonsModuleModel->getPathEnable(path));
365 }
366 settings.endArray();
367}
368
370{
371 // scenery paths
372 Q_FOREACH(QString path, m_sceneryPaths->enabledPaths()) {
373 m_config->setArg("fg-scenery", path);
374 }
375
376 // aircraft paths
377 Q_FOREACH(QString path, m_aircraftPaths->enabledPaths()) {
378 m_config->setArg("fg-aircraft", path);
379 }
380
381 // add hangars as aircraft paths, so they are available for MP model searching
382 for (const auto& catRef : globals->packageRoot()->catalogs()) {
383 const auto catAircraftPath = catRef->installRoot() / "Aircraft";
384 m_config->setArg("fg-aircraft", QString::fromStdString(catAircraftPath.utf8Str()));
385 }
386
387 // add-on module paths
388 // we could query this directly from AddonsModel, but this is simpler right now
389 QSettings settings;
390 int size = settings.beginReadArray("addon-modules");
391 for (int i = 0; i < size; ++i) {
392 settings.setArrayIndex(i);
393
394 QString path = settings.value("path").toString();
395 const SGPath addonPath(path.toStdString());
396 if (!addonPath.exists()) {
397 // don't pass missing paths
398 continue;
399 }
400
401 bool enable = settings.value("enable").toBool();
402 if (enable) {
403 m_config->setArg("addon", path);
404 }
405 }
406 settings.endArray();
407}
408
410{
412 return options->isOptionSet("fg-scenery") || options->isOptionSet("fg-aircraft");
413}
#define p2(x, y)
#define p(x)
bool options(int, char **)
Definition JSBSim.cpp:568
#define i(x)
void setAddons(AddonsModel *addons)
Q_INVOKABLE QString addAircraftPath() const
Q_INVOKABLE QString addSceneryPath() const
Q_INVOKABLE QString addAddOnModulePath() const
Q_INVOKABLE void officialCatalogAction(QString s)
PathListModel * sceneryPaths
void isOfficialHangarRegisteredChanged()
Q_INVOKABLE void openDirectory(QString path)
AddOnsController(LauncherMainWindow *parent, LaunchConfig *config)
void setModulePaths(QStringList modulePaths)
void modulePathsChanged(QStringList modulePaths)
Q_INVOKABLE QString installCustomScenery()
void showNoOfficialHangarChanged()
PathListModel * aircraftPaths
static Addon fromAddonDir(const SGPath &addonPath)
Definition Addon.cxx:374
void modulesChanged()
void catalogsChanged()
void collect()
static bool isCandidateAircraftPath(QString path)
@helper to determine if a particular path is likely to contain aircraft or not.
static LocalAircraftCache * instance()
void enabledPathsChanged()
string_list valuesForOption(const std::string &key) const
return all values for a multi-valued option
Definition options.cxx:2944
static Options * sharedInstance()
Definition options.cxx:2345
FGGlobals * globals
Definition globals.cxx:142
void addSentryBreadcrumb(const std::string &, const std::string &)
void launcherSetSceneryPaths()