diff --git a/src/core/StelSkyCultureMgr.cpp b/src/core/StelSkyCultureMgr.cpp index 535585e3fb35f..66139e7f5e04e 100644 --- a/src/core/StelSkyCultureMgr.cpp +++ b/src/core/StelSkyCultureMgr.cpp @@ -21,12 +21,14 @@ #include "StelFileMgr.hpp" #include "StelTranslator.hpp" #include "StelLocaleMgr.hpp" +#include "StelMainView.hpp" #include "StelApp.hpp" #include #include #include +#include #include #include #include @@ -116,20 +118,37 @@ QString convertReferenceLinks(QString text) } -QString StelSkyCultureMgr::getSkyCultureEnglishName(const QString& idFromJSON) const +QString StelSkyCultureMgr::getSkyCultureEnglishName(const QString& dir, const QString& idFromJSON, const bool reportErrorsToGUI) const { const auto skyCultureId = idFromJSON; - const QString descPath = StelFileMgr::findFile("skycultures/" + skyCultureId + "/description.md"); + constexpr char descFileName[] = "/description.md"; + const auto descPath = QFileInfo(dir).isAbsolute() ? dir + descFileName : StelFileMgr::findFile("skycultures/" + dir + descFileName); if (descPath.isEmpty()) { - qWarning() << "Can't find description for skyculture" << skyCultureId; + if (reportErrorsToGUI) + { + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Can't find file %1").arg(descPath)); + } + else + { + qWarning() << "Can't find description for skyculture" << skyCultureId; + } return idFromJSON; } QFile f(descPath); if (!f.open(QIODevice::ReadOnly)) { - qWarning().nospace() << "Failed to open sky culture description file " << descPath << ": " << f.errorString(); + if (reportErrorsToGUI) + { + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Failed to open file %1").arg(descPath)); + } + else + { + qWarning().nospace() << "Failed to open sky culture description file " << descPath << ": " << f.errorString(); + } return idFromJSON; } @@ -139,8 +158,17 @@ QString StelSkyCultureMgr::getSkyCultureEnglishName(const QString& idFromJSON) c if (line.isEmpty()) continue; if (!line.startsWith("#")) { - qWarning().nospace() << "Sky culture description file " << descPath << " at line " - << lineNum << " has wrong format (expected a top-level header, got " << line; + if (reportErrorsToGUI) + { + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Sky culture description file %1 at line %2 has wrong format " + "(expected a top-level header, got:\n%3").arg(descPath).arg(lineNum).arg(line)); + } + else + { + qWarning().nospace() << "Sky culture description file " << descPath << " at line " + << lineNum << " has wrong format (expected a top-level header, got " << line; + } return idFromJSON; } return line.mid(1).trimmed(); @@ -153,131 +181,212 @@ QString StelSkyCultureMgr::getSkyCultureEnglishName(const QString& idFromJSON) c StelSkyCultureMgr::StelSkyCultureMgr() { setObjectName("StelSkyCultureMgr"); - makeCulturesList(); } StelSkyCultureMgr::~StelSkyCultureMgr() { } -void StelSkyCultureMgr::makeCulturesList() +void StelSkyCultureMgr::addCustomCulture(const QString& dir, const bool reportErrorsToGUI) { - QSet cultureDirNames = StelFileMgr::listContents("skycultures",StelFileMgr::Directory); - for (const auto& dir : std::as_const(cultureDirNames)) + if (!addNewCulture(dir, reportErrorsToGUI)) return; + + additionalCultureDirs << dir; + + QSettings*const conf = StelApp::getInstance().getSettings(); + conf->beginGroup("skycultures"); + conf->beginWriteArray("additional_cultures"); + int i = 0; + for (const auto& dir : additionalCultureDirs) { - constexpr char indexFileName[] = "/index.json"; - const QString filePath = StelFileMgr::findFile("skycultures/" + dir + indexFileName); - if (filePath.isEmpty()) + conf->setArrayIndex(i); + conf->setValue("dir", dir); + ++i; + } + conf->endArray(); + conf->endGroup(); + + auto& culture = dirToNameEnglish[dir]; + qInfo() << "Added a new sky culture:" << culture.englishName; +} + +bool StelSkyCultureMgr::addNewCulture(const QString& dir, const bool reportErrorsToGUI) +{ + constexpr char indexFileName[] = "/index.json"; + const QString filePath = QFileInfo(dir).isAbsolute() ? dir + indexFileName : StelFileMgr::findFile("skycultures/" + dir + indexFileName); + if (filePath.isEmpty()) + { + if (reportErrorsToGUI) + { + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Failed to find %1 file in %2").arg(indexFileName, QDir::toNativeSeparators(dir))); + } + else { qCritical() << "Failed to find" << indexFileName << "file in sky culture directory" << QDir::toNativeSeparators(dir); - continue; } - QFile file(filePath); - if (!file.open(QFile::ReadOnly)) + return false; + } + QFile file(filePath); + if (!file.open(QFile::ReadOnly)) + { + if (reportErrorsToGUI) + { + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Failed to open file %1").arg(QDir::toNativeSeparators(filePath))); + } + else { qCritical() << "Failed to open" << indexFileName << "file in sky culture directory" << QDir::toNativeSeparators(dir); - continue; } - const auto jsonText = file.readAll(); - if (jsonText.isEmpty()) + return false; + } + const auto jsonText = file.readAll(); + if (jsonText.isEmpty()) + { + if (reportErrorsToGUI) { - qCritical() << "Failed to read data from" << indexFileName << "file in sky culture directory" - << QDir::toNativeSeparators(dir); - continue; + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Failed to read data from file %1").arg(QDir::toNativeSeparators(filePath))); } - QJsonParseError error; - const auto jsonDoc = QJsonDocument::fromJson(jsonText, &error); - if (error.error != QJsonParseError::NoError) + else { - qCritical().nospace() << "Failed to parse " << indexFileName << " from sky culture directory " - << QDir::toNativeSeparators(dir) << ": " << error.errorString(); - continue; + qCritical() << "Failed to read data from" << indexFileName << "file in sky culture directory" + << QDir::toNativeSeparators(dir); } - if (!jsonDoc.isObject()) + return false; + } + QJsonParseError error; + const auto jsonDoc = QJsonDocument::fromJson(jsonText, &error); + if (error.error != QJsonParseError::NoError) + { + if (reportErrorsToGUI) { - qCritical() << "Failed to find the expected JSON structure in" << indexFileName << " from sky culture directory" - << QDir::toNativeSeparators(dir); - continue; + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Failed to parse file %1").arg(QDir::toNativeSeparators(filePath))); } - const auto data = jsonDoc.object(); - - auto& culture = dirToNameEnglish[dir]; - culture.path = StelFileMgr::dirName(filePath); - const auto id = data["id"].toString(); - if(id != dir) - qWarning() << "Sky culture id" << id << "doesn't match directory name" << dir; - culture.id = id; - culture.englishName = getSkyCultureEnglishName(dir); - culture.region = data["region"].toString(); - if (culture.region.length()==0) + else { - qWarning() << "No geographic region declared in skyculture" << id << ". setting \"World\""; - culture.region = "World"; + qCritical().nospace() << "Failed to parse " << indexFileName << " from sky culture directory " + << QDir::toNativeSeparators(dir) << ": " << error.errorString(); } - if (data["constellations"].isArray()) + return false; + } + if (!jsonDoc.isObject()) + { + if (reportErrorsToGUI) { - culture.constellations = data["constellations"].toArray(); + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("Failed to find the expected JSON structure in %1").arg(QDir::toNativeSeparators(filePath))); } else { - qWarning() << "No \"constellations\" array found in JSON data in sky culture directory" - << QDir::toNativeSeparators(dir); + qCritical() << "Failed to find the expected JSON structure in" << indexFileName << " from sky culture directory" + << QDir::toNativeSeparators(dir); } + return false; + } + const auto data = jsonDoc.object(); + + auto& culture = dirToNameEnglish[dir]; + culture.path = StelFileMgr::dirName(filePath); + const auto id = data["id"].toString(); + const auto dirFileName = QFileInfo(dir).fileName(); + if(id != dirFileName) + qWarning() << "Sky culture id" << id << "doesn't match directory name" << dirFileName; + culture.id = id; + culture.englishName = getSkyCultureEnglishName(dir, id, reportErrorsToGUI); + culture.region = data["region"].toString(); + if (culture.region.length()==0) + { + qWarning() << "No geographic region declared in skyculture" << id << ". setting \"World\""; + culture.region = "World"; + } + if (data["constellations"].isArray()) + { + culture.constellations = data["constellations"].toArray(); + } + else + { + qWarning() << "No \"constellations\" array found in JSON data in sky culture directory" + << QDir::toNativeSeparators(dir); + } - culture.asterisms = data["asterisms"].toArray(); - culture.langsUseNativeNames = data["langs_use_native_names"].toArray(); + culture.asterisms = data["asterisms"].toArray(); + culture.langsUseNativeNames = data["langs_use_native_names"].toArray(); - culture.boundariesType = StelSkyCulture::BoundariesType::None; // default value if not specified in the JSON file - if (data.contains("edges") && data.contains("edges_type")) - { - const QString type = data["edges_type"].toString(); - const QString typeSimp = type.simplified().toUpper(); - static const QMap map={ - {"IAU", StelSkyCulture::BoundariesType::IAU}, - {"OWN", StelSkyCulture::BoundariesType::Own}, - {"NONE", StelSkyCulture::BoundariesType::None} - }; - if (!map.contains(typeSimp)) - qWarning().nospace() << "Unexpected edges_type value in sky culture " << dir - << ": " << type << ". Will resort to Own."; - culture.boundariesType = map.value(typeSimp, StelSkyCulture::BoundariesType::Own); - } - culture.boundaries = data["edges"].toArray(); - culture.boundariesEpoch = data["edges_epoch"].toString("J2000"); - culture.fallbackToInternationalNames = data["fallback_to_international_names"].toBool(); - culture.names = data["common_names"].toObject(); + culture.boundariesType = StelSkyCulture::BoundariesType::None; // default value if not specified in the JSON file + if (data.contains("edges") && data.contains("edges_type")) + { + const QString type = data["edges_type"].toString(); + const QString typeSimp = type.simplified().toUpper(); + static const QMap map={ + {"IAU", StelSkyCulture::BoundariesType::IAU}, + {"OWN", StelSkyCulture::BoundariesType::Own}, + {"NONE", StelSkyCulture::BoundariesType::None} + }; + if (!map.contains(typeSimp)) + qWarning().nospace() << "Unexpected edges_type value in sky culture " << dir + << ": " << type << ". Will resort to Own."; + culture.boundariesType = map.value(typeSimp, StelSkyCulture::BoundariesType::Own); + } + culture.boundaries = data["edges"].toArray(); + culture.boundariesEpoch = data["edges_epoch"].toString("J2000"); + culture.fallbackToInternationalNames = data["fallback_to_international_names"].toBool(); + culture.names = data["common_names"].toObject(); - const auto classifications = data["classification"].toArray(); - if (classifications.isEmpty()) - { - culture.classification = StelSkyCulture::INCOMPLETE; - } - else + const auto classifications = data["classification"].toArray(); + if (classifications.isEmpty()) + { + culture.classification = StelSkyCulture::INCOMPLETE; + } + else + { + static const QMap classificationMap={ + { "traditional", StelSkyCulture::TRADITIONAL}, + { "historical", StelSkyCulture::HISTORICAL}, + { "ethnographic", StelSkyCulture::ETHNOGRAPHIC}, + { "single", StelSkyCulture::SINGLE}, + { "comparative", StelSkyCulture::COMPARATIVE}, + { "personal", StelSkyCulture::PERSONAL}, + { "incomplete", StelSkyCulture::INCOMPLETE}, + }; + const auto classificationStr = classifications[0].toString(); // We'll take only the first item for now. + const auto classification=classificationMap.value(classificationStr.toLower(), StelSkyCulture::INCOMPLETE); + if (classificationMap.constFind(classificationStr.toLower()) == classificationMap.constEnd()) // not included { - static const QMap classificationMap={ - { "traditional", StelSkyCulture::TRADITIONAL}, - { "historical", StelSkyCulture::HISTORICAL}, - { "ethnographic", StelSkyCulture::ETHNOGRAPHIC}, - { "single", StelSkyCulture::SINGLE}, - { "comparative", StelSkyCulture::COMPARATIVE}, - { "personal", StelSkyCulture::PERSONAL}, - { "incomplete", StelSkyCulture::INCOMPLETE}, - }; - const auto classificationStr = classifications[0].toString(); // We'll take only the first item for now. - const auto classification=classificationMap.value(classificationStr.toLower(), StelSkyCulture::INCOMPLETE); - if (classificationMap.constFind(classificationStr.toLower()) == classificationMap.constEnd()) // not included - { - qDebug() << "Skyculture " << dir << "has UNKNOWN classification: " << classificationStr; - qDebug() << "Please edit info.ini and change to a supported value. For now, this equals 'incomplete'"; - } - culture.classification = classification; + qWarning() << "Skyculture " << dir << "has UNKNOWN classification: " << classificationStr; + qWarning() << "Please edit info.ini and change to a supported value. For now, this equals 'incomplete'"; } + culture.classification = classification; } + return true; +} + +void StelSkyCultureMgr::makeCulturesList() +{ + QSet cultureDirNames = StelFileMgr::listContents("skycultures",StelFileMgr::Directory); + cultureDirNames += additionalCultureDirs; + for (const auto& dir : std::as_const(cultureDirNames)) + addNewCulture(dir); } //! Init itself from a config file. void StelSkyCultureMgr::init() { + QSettings*const conf = StelApp::getInstance().getSettings(); + conf->beginGroup("skycultures"); + const int size = conf->beginReadArray("additional_cultures"); + for (int i = 0; i < size; i++) + { + conf->setArrayIndex(i); + additionalCultureDirs << conf->value("dir").toString(); + } + conf->endArray(); + conf->endGroup(); + + makeCulturesList(); + defaultSkyCultureID = StelApp::getInstance().getSettings()->value("localization/sky_culture", "modern").toString(); if (defaultSkyCultureID=="western") // switch to new Sky Culture ID defaultSkyCultureID = "modern"; diff --git a/src/core/StelSkyCultureMgr.hpp b/src/core/StelSkyCultureMgr.hpp index 2ffb7d4337d34..86ae247826cd9 100644 --- a/src/core/StelSkyCultureMgr.hpp +++ b/src/core/StelSkyCultureMgr.hpp @@ -147,6 +147,9 @@ class StelSkyCultureMgr : public QObject //! Gets the default sky culture name from the application's settings, //! sets that sky culture by calling setCurrentSkyCultureID(). void init(); + + bool addNewCulture(const QString& dir, bool reportErrorsToGUI = false); + void addCustomCulture(const QString& dir, bool reportErrorsToGUI); public slots: //! Get the current sky culture English name. @@ -230,7 +233,7 @@ public slots: //! Read the English name of the sky culture from description file. //! @param idFromJSON the id from \p index.json that will be used as a default name if an error occurs. - QString getSkyCultureEnglishName(const QString& idFromJSON) const; + QString getSkyCultureEnglishName(const QString& dir, const QString& idFromJSON, bool reportErrorsToGUI) const; //! Get the culture name in English associated with a specified directory. //! @param directory The directory name. //! @return The English name for the culture associated with directory. @@ -253,6 +256,7 @@ public slots: std::pair getLicenseDescription(const QString& license, const bool singleLicenseForAll) const; QMap dirToNameEnglish; + QSet additionalCultureDirs; StelSkyCulture currentSkyCulture; diff --git a/src/gui/ViewDialog.cpp b/src/gui/ViewDialog.cpp index 1aca5281a15b4..567b4557d5b31 100644 --- a/src/gui/ViewDialog.cpp +++ b/src/gui/ViewDialog.cpp @@ -32,6 +32,7 @@ #include "StelCore.hpp" #include "StelModule.hpp" #include "LandscapeMgr.hpp" +#include "StelMainView.hpp" #include "StelSkyCultureMgr.hpp" #include "StelFileMgr.hpp" #include "StelProjector.hpp" @@ -56,6 +57,8 @@ #include #include #include +#include +#include #include #include @@ -188,6 +191,7 @@ void ViewDialog::createDialogContent() // Jupiter's GRS should become property, and recheck the other "from trunk" entries. connect(ui->culturesListWidget, SIGNAL(currentTextChanged(const QString&)),&StelApp::getInstance().getSkyCultureMgr(),SLOT(setCurrentSkyCultureNameI18(QString))); connect(&StelApp::getInstance().getSkyCultureMgr(), &StelSkyCultureMgr::currentSkyCultureIDChanged, this, &ViewDialog::skyCultureChanged); + connect(ui->culturesAddNewBtn, &QPushButton::clicked, this, &ViewDialog::onAddNewCulture); // Connect and initialize checkboxes and other widgets SolarSystem* ssmgr = GETSTELMODULE(SolarSystem); @@ -1062,9 +1066,8 @@ void ViewDialog::populateToolTips() ui->rayHelpersFadeDurationDoubleSpinBox->setSuffix(seconds); } -void ViewDialog::populateLists() +void ViewDialog::updateSkyCulturesList() { - // Fill the culture list widget from the available list StelApp& app = StelApp::getInstance(); QListWidget* l = ui->culturesListWidget; l->blockSignals(true); @@ -1078,6 +1081,13 @@ void ViewDialog::populateLists() l->setCurrentItem(l->findItems(app.getSkyCultureMgr().getCurrentSkyCultureNameI18(), Qt::MatchExactly).at(0)); l->blockSignals(false); updateSkyCultureText(); +} + +void ViewDialog::populateLists() +{ + // Fill the culture list widget from the available list + StelApp& app = StelApp::getInstance(); + updateSkyCulturesList(); // populate language printing combo. (taken from DeltaT combo) StelModule* cmgr = app.getModule("ConstellationMgr"); @@ -1102,7 +1112,7 @@ void ViewDialog::populateLists() StelGui* gui = dynamic_cast(app.getGui()); // Fill the projection list - l = ui->projectionListWidget; + QListWidget* l = ui->projectionListWidget; l->blockSignals(true); l->clear(); const QStringList mappings = core->getAllProjectionTypeKeys(); @@ -1158,6 +1168,21 @@ void ViewDialog::skyCultureChanged() updateDefaultSkyCulture(); } +void ViewDialog::onAddNewCulture() +{ + const auto dir = QFileDialog::getExistingDirectory(&StelMainView::getInstance(), q_("Add new sky culture")); + if (dir.isEmpty()) return; + const auto indexPath = dir + "/index.json"; + if (!QFileInfo(indexPath).exists()) + { + QMessageBox::critical(&StelMainView::getInstance(), q_("Error loading sky culture"), + q_("The directory chosen doesn't contain a file \"index.json\"")); + return; + } + StelApp::getInstance().getSkyCultureMgr().addCustomCulture(dir, true); + updateSkyCulturesList(); +} + // fill the description text window, not the names in the sky. void ViewDialog::updateSkyCultureText() { diff --git a/src/gui/ViewDialog.hpp b/src/gui/ViewDialog.hpp index ca29bbb3e842f..3567fc770a51b 100644 --- a/src/gui/ViewDialog.hpp +++ b/src/gui/ViewDialog.hpp @@ -56,8 +56,10 @@ public slots: bool eventFilter(QObject* object, QEvent* event) override; private slots: void populateLists(); + void onAddNewCulture(); void populateToolTips(); void skyCultureChanged(); + void updateSkyCulturesList(); void changeProjection(const QString& projectionNameI18n); void projectionChanged(); void landscapeChanged(QString id,QString name); diff --git a/src/gui/viewDialog.ui b/src/gui/viewDialog.ui index c58366af67579..cdf2ade537c25 100644 --- a/src/gui/viewDialog.ui +++ b/src/gui/viewDialog.ui @@ -4944,7 +4944,14 @@ - + + + + Add new... + + + + true