From c584801eac53cc22917316a1134821725dfb66c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaakko=20Ker=C3=A4nen?= Date: Fri, 4 Mar 2016 19:59:19 +0200 Subject: [PATCH] Refactor|Profiles|Client|libcore: ConfigProfiles uses de::Profiles for serialization de::Profiles handles the file I/O and Info parsing. The parsed elements are given to the subclass for processing. --- doomsday/apps/client/include/configprofiles.h | 4 + doomsday/apps/client/src/configprofiles.cpp | 116 +++++++++++--- .../sdk/libcore/include/de/data/profiles.h | 31 +++- doomsday/sdk/libcore/src/data/profiles.cpp | 148 +++++++++++++++++- 4 files changed, 272 insertions(+), 27 deletions(-) diff --git a/doomsday/apps/client/include/configprofiles.h b/doomsday/apps/client/include/configprofiles.h index 0055c3ad15..881276b2e5 100644 --- a/doomsday/apps/client/include/configprofiles.h +++ b/doomsday/apps/client/include/configprofiles.h @@ -125,6 +125,10 @@ class ConfigProfiles : public de::Profiles */ void deleteProfile(de::String const &name); +protected: + AbstractProfile *profileFromInfoBlock( + de::Info::BlockElement const &block) override; + private: DENG2_PRIVATE(d) }; diff --git a/doomsday/apps/client/src/configprofiles.cpp b/doomsday/apps/client/src/configprofiles.cpp index 190397b43c..7d9a30457b 100644 --- a/doomsday/apps/client/src/configprofiles.cpp +++ b/doomsday/apps/client/src/configprofiles.cpp @@ -93,16 +93,15 @@ DENG2_PIMPL(ConfigProfiles) { typedef QMap Values; Values values; - bool readOnly = false; ///< Profile has been loaded from a read-only file, won't be serialized. ConfigProfiles &owner() { return static_cast(AbstractProfile::owner()); } - bool isReadOnly() const override + ConfigProfiles const &owner() const { - return readOnly; + return static_cast(AbstractProfile::owner()); } bool resetToDefaults() override @@ -114,6 +113,64 @@ DENG2_PIMPL(ConfigProfiles) } return false; } + + String toInfoSource() const + { + auto const &settings = owner().d->settings; + + String info; + QTextStream os(&info); + os.setCodec("UTF-8"); + + DENG2_FOR_EACH_CONST(Values, val, values) + { + DENG2_ASSERT(settings.contains(val.key())); + + Setting const &st = settings[val.key()]; + + String valueText; + switch(st.type) + { + case IntCVar: + case FloatCVar: + case StringCVar: + case ConfigVariable: + // QVariant can handle this. + valueText = val.value().toString(); + break; + } + + os << "setting \"" << st.name << "\" {\n" + << " value: " << valueText << "\n" + << "}"; + } + + return info; + } + + void initializeFromInfoBlock(ConfigProfiles const &profs, + de::Info::BlockElement const &block) + { + // Use the default settings for anything not defined in the file. + values = profs.d->defaults.values; + + // Read all the setting values from the profile block. + foreach(auto const *element, block.contentsInOrder()) + { + if(!element->isBlock()) continue; + + de::Info::BlockElement const &setBlock = element->as(); + + // Only process known settings. + if(setBlock.blockType() == "setting" && + profs.d->settings.contains(setBlock.name())) + { + values[setBlock.name()] = + profs.d->textToSettingValue(setBlock.keyValue("value").text, + setBlock.name()); + } + } + } }; Profile defaults; @@ -193,7 +250,7 @@ DENG2_PIMPL(ConfigProfiles) void fetch(String const &profileName) { Profile &prof = self.find(profileName).as(); - if(prof.readOnly) return; + if(prof.isReadOnly()) return; foreach(Setting const &st, settings.values()) { @@ -302,16 +359,6 @@ DENG2_PIMPL(ConfigProfiles) return self.persistentName().concatenateMember("profile"); } - /** - * For a persistent register, determines the file name of the Info file - * where all the profile values are written to and read from. - */ - String fileName() const - { - if(self.persistentName().isEmpty()) return ""; - return String("/home/configs/%1.dei").arg(self.persistentName()); - } - QVariant textToSettingValue(String const &text, String const &settingName) const { DENG2_ASSERT(settings.contains(settingName)); @@ -337,6 +384,7 @@ DENG2_PIMPL(ConfigProfiles) return QVariant(); } +#if 0 void loadProfilesFromInfo(File const &file, bool markReadOnly) { try @@ -365,7 +413,7 @@ DENG2_PIMPL(ConfigProfiles) LOG_VERBOSE("Reading profile '%s'") << profileName; Profile *prof = addProfile(profileName); - if(markReadOnly) prof->readOnly = true; + if(markReadOnly) prof->setReadOnly(true); // Use the default settings for anything not defined in the file. prof->values = defaults.values; @@ -391,6 +439,7 @@ DENG2_PIMPL(ConfigProfiles) << file.description() << er.asText(); } } +#endif bool addCustomProfileIfMissing() { @@ -413,11 +462,14 @@ DENG2_PIMPL(ConfigProfiles) */ void currentGameChanged(Game const &newGame) { - if(self.persistentName().isEmpty() || newGame.isNull()) return; + if(!self.isPersistent() || newGame.isNull()) return; LOG_AS("ConfigProfiles"); LOG_DEBUG("Game has been loaded, deserializing %s profiles") << self.persistentName(); + self.deserialize(); + +#if 0 self.clear(); // Read all fixed profiles from */profiles/(persistentName)/ @@ -445,15 +497,18 @@ DENG2_PIMPL(ConfigProfiles) loadProfilesFromInfo(*file, false /* modifiable */); } else +#endif + //{ + + // Settings haven't previously been created -- make sure we at least + // have the Custom profile. + if(addCustomProfileIfMissing()) { - // Settings haven't previously been created -- make sure we at least - // have the Custom profile. - if(addCustomProfileIfMissing()) - { - current = CUSTOM_PROFILE; - } + current = CUSTOM_PROFILE; } + //} + // Still nothing? addCustomProfileIfMissing(); @@ -492,7 +547,7 @@ DENG2_PIMPL(ConfigProfiles) */ void aboutToUnloadGame(Game const &gameBeingUnloaded) { - if(self.persistentName().isEmpty() || gameBeingUnloaded.isNull()) return; + if(!self.isPersistent() || gameBeingUnloaded.isNull()) return; LOG_AS("ConfigProfiles"); LOG_DEBUG("Game being unloaded, serializing %s profiles") << self.persistentName(); @@ -503,8 +558,10 @@ DENG2_PIMPL(ConfigProfiles) // Remember which profile is the current one. App::config().set(confName(), current); + self.serialize(); + // We will write one Info file with all the profiles. - String info; + /*String info; QTextStream os(&info); os.setCodec("UTF-8"); @@ -515,7 +572,7 @@ DENG2_PIMPL(ConfigProfiles) self.forAll([this, &os, &count] (AbstractProfile &prof) { Profile &profile = prof.as(); - if(profile.readOnly) return LoopContinue; + if(profile.isReadOnly()) return LoopContinue; ++count; @@ -554,6 +611,7 @@ DENG2_PIMPL(ConfigProfiles) outFile.flush(); // we're done LOG_VERBOSE("Wrote \"%s\" with %i profile%s") << fileName() << count << (count != 1? "s" : ""); + */ } }; @@ -642,3 +700,11 @@ void ConfigProfiles::deleteProfile(String const &name) delete prof; } } + +Profiles::AbstractProfile * +ConfigProfiles::profileFromInfoBlock(de::Info::BlockElement const &block) +{ + std::unique_ptr prof(new Instance::Profile); + prof->initializeFromInfoBlock(*this, block); + return prof.release(); +} diff --git a/doomsday/sdk/libcore/include/de/data/profiles.h b/doomsday/sdk/libcore/include/de/data/profiles.h index 921d1e74e0..ee9bacbce5 100644 --- a/doomsday/sdk/libcore/include/de/data/profiles.h +++ b/doomsday/sdk/libcore/include/de/data/profiles.h @@ -20,6 +20,7 @@ #define DENG2_PROFILES_H #include +#include namespace de { @@ -76,10 +77,18 @@ class DENG2_PUBLIC Profiles * * @return @c true, if the profile is read-only. */ - virtual bool isReadOnly() const = 0; + bool isReadOnly() const; + + void setReadOnly(bool readOnly); virtual bool resetToDefaults() = 0; + /** + * Serializes the contents of the profile to a text string using Info + * source syntax. + */ + virtual String toInfoSource() const = 0; + DENG2_AS_IS_METHODS() private: @@ -106,6 +115,8 @@ class DENG2_PUBLIC Profiles String persistentName() const; + bool isPersistent() const; + /** * Lists the names of all the existing profiles. */ @@ -142,6 +153,24 @@ class DENG2_PUBLIC Profiles */ void remove(AbstractProfile &profile); + /** + * Serializes all the profiles to /home/configs/(persistentName).dei. Only + * non-readonly profiles are written. Nothing happens + */ + void serialize() const; + + /** + * Deserializes all the profiles from /profiles/(persistentName).dei and + * /home/configs/(persistentName).dei. + * + * All existing profiles in the collection are deleted beforehand. + */ + void deserialize(); + +protected: + virtual AbstractProfile *profileFromInfoBlock( + de::Info::BlockElement const &block) = 0; + private: DENG2_PRIVATE(d) }; diff --git a/doomsday/sdk/libcore/src/data/profiles.cpp b/doomsday/sdk/libcore/src/data/profiles.cpp index f708ad4e0b..da3883574e 100644 --- a/doomsday/sdk/libcore/src/data/profiles.cpp +++ b/doomsday/sdk/libcore/src/data/profiles.cpp @@ -17,6 +17,11 @@ */ #include "de/Profiles" +#include "de/String" +#include "de/File" +#include "de/App" + +#include namespace de { @@ -41,6 +46,7 @@ DENG2_PIMPL(Profiles) delete profiles[profile->name()]; } profiles.insert(profile->name(), profile); + profile->setOwner(thisPublic); } void clear() @@ -48,6 +54,56 @@ DENG2_PIMPL(Profiles) qDeleteAll(profiles.values()); profiles.clear(); } + + /** + * For persistent profiles, determines the file name of the Info file + * where all the profile contents are written to and read from. + */ + String fileName() const + { + if(persistentName.isEmpty()) return ""; + return String("/home/configs/%1.dei").arg(persistentName); + } + + void loadProfilesFromInfo(File const &file, bool markReadOnly) + { + try + { + LOG_RES_VERBOSE("Reading profiles from %s") << file.description(); + + Block raw; + file >> raw; + + de::Info info; + info.parse(String::fromUtf8(raw)); + + foreach(de::Info::Element const *elem, info.root().contentsInOrder()) + { + if(!elem->isBlock()) continue; + + // There may be multiple profiles in the file. + de::Info::BlockElement const &profBlock = elem->as(); + if(profBlock.blockType() == "group" && + profBlock.name() == "profile") + { + String profileName = profBlock.keyValue("name").text; + if(profileName.isEmpty()) continue; // Name is required. + + LOG_VERBOSE("Reading profile '%s'") << profileName; + + auto *prof = self.profileFromInfoBlock(profBlock); + prof->setName(profileName); + prof->setReadOnly(markReadOnly); + add(prof); + } + } + } + catch(Error const &er) + { + LOG_RES_WARNING("Failed to load profiles from %s:\n%s") + << file.description() << er.asText(); + } + } }; Profiles::Profiles() @@ -93,6 +149,11 @@ String Profiles::persistentName() const return d->persistentName; } +bool Profiles::isPersistent() const +{ + return !d->persistentName.isEmpty(); +} + LoopResult Profiles::forAll(std::function func) { foreach(AbstractProfile *prof, d->profiles.values()) @@ -113,7 +174,6 @@ void Profiles::clear() void Profiles::add(AbstractProfile *profile) { d->add(profile); - profile->setOwner(this); } void Profiles::remove(AbstractProfile &profile) @@ -124,12 +184,88 @@ void Profiles::remove(AbstractProfile &profile) profile.setOwner(nullptr); } +void Profiles::serialize() const +{ + if(!isPersistent()) return; + + LOG_AS("Profiles"); + LOGDEV_VERBOSE("Serializing %s profiles") << d->persistentName; + + // We will write one Info file with all the profiles. + String info; + QTextStream os(&info); + os.setCodec("UTF-8"); + + os << "# Autogenerated Info file based on " << d->persistentName + << " profiles\n"; + + // Write /home/configs/(persistentName).dei with all non-readonly profiles. + int count = 0; + for(auto *prof : d->profiles) + { + if(prof->isReadOnly()) continue; + + os << "\nprofile {\n" + " name: " << prof->name() << "\n"; + for(auto line : prof->toInfoSource().split('\n')) + { + os << " " << line << "\n"; + } + os << "}\n"; + ++count; + } + + // Create the pack and update the file system. + File &outFile = App::rootFolder().replaceFile(d->fileName()); + outFile << info.toUtf8(); + outFile.flush(); // we're done + + LOG_VERBOSE("Wrote \"%s\" with %i profile%s") + << d->fileName() << count << (count != 1? "s" : ""); +} + +void Profiles::deserialize() +{ + if(!isPersistent()) return; + + LOG_AS("Profiles"); + LOGDEV_VERBOSE("Deserializing %s profiles") << d->persistentName; + + clear(); + + // Read all fixed profiles from */profiles/(persistentName)/ + FS::FoundFiles folders; + App::fileSystem().findAll("profiles" / d->persistentName, folders); + DENG2_FOR_EACH(FS::FoundFiles, i, folders) + { + if(Folder const *folder = (*i)->maybeAs()) + { + // Let's see if it contains any .dei files. + DENG2_FOR_EACH_CONST(Folder::Contents, k, folder->contents()) + { + if(k->first.fileNameExtension() == ".dei") + { + // Load this profile. + d->loadProfilesFromInfo(*k->second, true /* read-only */); + } + } + } + } + + // Read /home/configs/(persistentName).dei + if(File const *file = App::rootFolder().tryLocate(d->fileName())) + { + d->loadProfilesFromInfo(*file, false /* modifiable */); + } +} + // Profiles::AbstractProfile -------------------------------------------------- DENG2_PIMPL(Profiles::AbstractProfile) { Profiles *owner = nullptr; String name; + bool readOnly = false; Instance(Public *i) : Base(i) {} @@ -190,4 +326,14 @@ bool Profiles::AbstractProfile::setName(String const &newName) return true; } +bool Profiles::AbstractProfile::isReadOnly() const +{ + return d->readOnly; +} + +void Profiles::AbstractProfile::setReadOnly(bool readOnly) +{ + d->readOnly = readOnly; +} + } // namespace de