476 lines
19 KiB
C++
476 lines
19 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
|
|
* *
|
|
* This file is part of the FreeCAD CAx development system. *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Library General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU Library General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Library General Public *
|
|
* License along with this library; see the file COPYING.LIB. If not, *
|
|
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
* Suite 330, Boston, MA 02111-1307, USA *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
|
|
#include "PreCompiled.h"
|
|
|
|
#ifndef _PreComp_
|
|
# include <memory>
|
|
# include <string_view>
|
|
# include <mutex>
|
|
#endif
|
|
|
|
#include <boost/filesystem.hpp>
|
|
#include <QDir>
|
|
|
|
#include "PreferencePackManager.h"
|
|
#include "App/Metadata.h"
|
|
#include "Base/Parameter.h"
|
|
#include "Base/Interpreter.h"
|
|
#include "Base/Console.h"
|
|
#include "DockWindowManager.h"
|
|
#include "ToolBarManager.h"
|
|
|
|
#include <App/Application.h>
|
|
|
|
#include <ctime> // For generating a timestamped filename
|
|
|
|
|
|
using namespace Gui;
|
|
using namespace xercesc;
|
|
namespace fs = boost::filesystem;
|
|
|
|
PreferencePack::PreferencePack(const fs::path& path, const App::Metadata& metadata) :
|
|
_path(path), _metadata(metadata)
|
|
{
|
|
if (!fs::exists(_path)) {
|
|
throw std::runtime_error{ "Cannot access " + path.string() };
|
|
}
|
|
|
|
auto qssPaths = QDir::searchPaths(QString::fromUtf8("qss"));
|
|
auto cssPaths = QDir::searchPaths(QString::fromUtf8("css"));
|
|
|
|
qssPaths.append(QString::fromStdString(_path.string()));
|
|
cssPaths.append(QString::fromStdString(_path.string()));
|
|
|
|
QDir::setSearchPaths(QString::fromUtf8("qss"), qssPaths);
|
|
QDir::setSearchPaths(QString::fromUtf8("css"), cssPaths);
|
|
}
|
|
|
|
std::string PreferencePack::name() const
|
|
{
|
|
return _metadata.name();
|
|
}
|
|
|
|
bool PreferencePack::apply() const
|
|
{
|
|
// Run the pre.FCMacro, if it exists: if it raises an exception, abort the process
|
|
auto preMacroPath = _path / "pre.FCMacro";
|
|
if (fs::exists(preMacroPath)) {
|
|
try {
|
|
Base::Interpreter().runFile(preMacroPath.string().c_str(), false);
|
|
}
|
|
catch (...) {
|
|
Base::Console().Message("PreferencePack application aborted by the preferencePack's pre.FCMacro");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Back up the old config file
|
|
auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
|
|
auto backupFile = savedPreferencePacksDirectory / "user.cfg.backup";
|
|
try {
|
|
fs::remove(backupFile);
|
|
}
|
|
catch (...) {}
|
|
App::GetApplication().GetUserParameter().SaveDocument(backupFile.string().c_str());
|
|
|
|
// Apply the config settings
|
|
applyConfigChanges();
|
|
|
|
// Run the Post.FCMacro, if it exists
|
|
auto postMacroPath = _path / "post.FCMacro";
|
|
if (fs::exists(postMacroPath)) {
|
|
try {
|
|
Base::Interpreter().runFile(postMacroPath.string().c_str(), false);
|
|
}
|
|
catch (...) {
|
|
Base::Console().Message("PreferencePack application reverted by the preferencePack's post.FCMacro");
|
|
App::GetApplication().GetUserParameter().LoadDocument(backupFile.string().c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
PreferencePack::Type PreferencePack::type() const
|
|
{
|
|
auto typeList = _metadata["type"];
|
|
if (typeList.empty())
|
|
return Type::Combination;
|
|
|
|
auto typeString = typeList.front().contents;
|
|
if (typeString == "appearance")
|
|
return Type::Appearance;
|
|
else if (typeString == "behavior" || typeString == "behaviour")
|
|
return Type::Behavior;
|
|
else
|
|
return Type::Combination;
|
|
}
|
|
|
|
void PreferencePack::applyConfigChanges() const
|
|
{
|
|
auto configFile = _path / (_metadata.name() + ".cfg");
|
|
if (fs::exists(configFile)) {
|
|
ParameterManager newParameters;
|
|
newParameters.LoadDocument(configFile.string().c_str());
|
|
auto baseAppGroup = App::GetApplication().GetUserParameter().GetGroup("BaseApp");
|
|
newParameters.GetGroup("BaseApp")->insertTo(baseAppGroup);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
PreferencePackManager::PreferencePackManager()
|
|
{
|
|
auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
|
|
auto savedPath = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
|
|
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui" / "PreferencePacks";
|
|
_preferencePackPaths.push_back(resourcePath);
|
|
_preferencePackPaths.push_back(modPath);
|
|
_preferencePackPaths.push_back(savedPath);
|
|
rescan();
|
|
|
|
// Housekeeping:
|
|
DeleteOldBackups();
|
|
}
|
|
|
|
void PreferencePackManager::rescan()
|
|
{
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
for (const auto& path : _preferencePackPaths) {
|
|
if (fs::exists(path) && fs::is_directory(path)) {
|
|
FindPreferencePacksInPackage(path);
|
|
for (const auto& mod : fs::directory_iterator(path)) {
|
|
if (fs::is_directory(mod)) {
|
|
FindPreferencePacksInPackage(mod);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Gui::PreferencePackManager::FindPreferencePacksInPackage(const fs::path& mod)
|
|
{
|
|
auto packageMetadataFile = mod / "package.xml";
|
|
if (fs::exists(packageMetadataFile) && fs::is_regular_file(packageMetadataFile)) {
|
|
try {
|
|
App::Metadata metadata(packageMetadataFile);
|
|
auto content = metadata.content();
|
|
for (const auto& item : content) {
|
|
if (item.first == "preferencepack") {
|
|
PreferencePack newPreferencePack(mod / item.second.name(), item.second);
|
|
_preferencePacks.insert(std::make_pair(newPreferencePack.name(), newPreferencePack));
|
|
}
|
|
}
|
|
}
|
|
catch (...) {
|
|
// Failed to read the metadata, or to create the preferencePack based on it...
|
|
Base::Console().Error(("Failed to read " + packageMetadataFile.string()).c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> PreferencePackManager::preferencePackNames(PreferencePack::Type type) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
std::vector<std::string> names;
|
|
for (const auto& preferencePack : _preferencePacks)
|
|
if (preferencePack.second.type() == type)
|
|
names.push_back(preferencePack.first);
|
|
return names;
|
|
}
|
|
|
|
bool PreferencePackManager::apply(const std::string& preferencePackName) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
if (auto preferencePack = _preferencePacks.find(preferencePackName); preferencePack != _preferencePacks.end()) {
|
|
BackupCurrentConfig();
|
|
bool wasApplied = preferencePack->second.apply();
|
|
if (wasApplied) {
|
|
// If the visibility state of the dock windows was changed we have to manually reload their state
|
|
Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance();
|
|
pDockMgr->loadState();
|
|
|
|
// Same goes for toolbars:
|
|
Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance();
|
|
pToolbarMgr->restoreState();
|
|
|
|
// TODO: Are there other things that have to be manually triggered?
|
|
}
|
|
return wasApplied;
|
|
}
|
|
else {
|
|
throw std::runtime_error("No such Preference Pack: " + preferencePackName);
|
|
}
|
|
}
|
|
|
|
void copyTemplateParameters(Base::Reference<ParameterGrp> templateGroup, const std::string& path, Base::Reference<ParameterGrp> outputGroup)
|
|
{
|
|
auto userParameterHandle = App::GetApplication().GetParameterGroupByPath(path.c_str());
|
|
|
|
// Ensure that the DockWindowManager has saved its current state:
|
|
Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance();
|
|
pDockMgr->saveState();
|
|
|
|
// Do the same for ToolBars
|
|
Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance();
|
|
pToolbarMgr->saveState();
|
|
|
|
auto boolMap = templateGroup->GetBoolMap();
|
|
for (const auto& kv : boolMap) {
|
|
auto currentValue = userParameterHandle->GetBool(kv.first.c_str(), kv.second);
|
|
outputGroup->SetBool(kv.first.c_str(), currentValue);
|
|
}
|
|
|
|
auto intMap = templateGroup->GetIntMap();
|
|
for (const auto& kv : intMap) {
|
|
auto currentValue = userParameterHandle->GetInt(kv.first.c_str(), kv.second);
|
|
outputGroup->SetInt(kv.first.c_str(), currentValue);
|
|
}
|
|
|
|
auto uintMap = templateGroup->GetUnsignedMap();
|
|
for (const auto& kv : uintMap) {
|
|
auto currentValue = userParameterHandle->GetUnsigned(kv.first.c_str(), kv.second);
|
|
outputGroup->SetUnsigned(kv.first.c_str(), currentValue);
|
|
}
|
|
|
|
auto floatMap = templateGroup->GetFloatMap();
|
|
for (const auto& kv : floatMap) {
|
|
auto currentValue = userParameterHandle->GetFloat(kv.first.c_str(), kv.second);
|
|
outputGroup->SetFloat(kv.first.c_str(), currentValue);
|
|
}
|
|
|
|
auto asciiMap = templateGroup->GetASCIIMap();
|
|
for (const auto& kv : asciiMap) {
|
|
auto currentValue = userParameterHandle->GetASCII(kv.first.c_str(), kv.second.c_str());
|
|
outputGroup->SetASCII(kv.first.c_str(), currentValue.c_str());
|
|
}
|
|
|
|
// Recurse...
|
|
auto templateSubgroups = templateGroup->GetGroups();
|
|
for (auto& templateSubgroup : templateSubgroups) {
|
|
std::string sgName = templateSubgroup->GetGroupName();
|
|
auto outputSubgroupHandle = outputGroup->GetGroup(sgName.c_str());
|
|
copyTemplateParameters(templateSubgroup, path + "/" + sgName, outputSubgroupHandle);
|
|
}
|
|
}
|
|
|
|
void copyTemplateParameters(/*const*/ ParameterManager& templateParameterManager, ParameterManager& outputParameterManager)
|
|
{
|
|
auto groups = templateParameterManager.GetGroups();
|
|
for (auto& group : groups) {
|
|
std::string name = group->GetGroupName();
|
|
auto groupHandle = outputParameterManager.GetGroup(name.c_str());
|
|
copyTemplateParameters(group, "User parameter:" + name, groupHandle);
|
|
}
|
|
}
|
|
|
|
void PreferencePackManager::save(const std::string& name, const std::vector<TemplateFile>& templates)
|
|
{
|
|
if (templates.empty())
|
|
return;
|
|
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
|
|
fs::path preferencePackDirectory(savedPreferencePacksDirectory / name);
|
|
if (fs::exists(preferencePackDirectory) && !fs::is_directory(preferencePackDirectory))
|
|
throw std::runtime_error("Cannot create " + savedPreferencePacksDirectory.string() + ": file with that name exists already");
|
|
|
|
if (!fs::exists(preferencePackDirectory))
|
|
fs::create_directories(preferencePackDirectory);
|
|
|
|
// Create or update the saved user preferencePacks package.xml metadata file
|
|
std::unique_ptr<App::Metadata> metadata;
|
|
if (fs::exists(savedPreferencePacksDirectory / "package.xml")) {
|
|
metadata = std::make_unique<App::Metadata>(savedPreferencePacksDirectory / "package.xml");
|
|
}
|
|
else {
|
|
// Create and set all of the required metadata to make it easier for PreferencePack authors to copy this
|
|
// file into their preferencePack distributions.
|
|
metadata = std::make_unique<App::Metadata>();
|
|
metadata->setName("User-Saved PreferencePacks");
|
|
metadata->setDescription("Generated automatically -- edits may be lost when saving new preferencePacks");
|
|
metadata->setVersion(1);
|
|
metadata->addMaintainer(App::Meta::Contact("No Maintainer", "email@freecadweb.org"));
|
|
metadata->addLicense(App::Meta::License("(Unspecified)", "(Unspecified)"));
|
|
metadata->addUrl(App::Meta::Url("https://github.com/FreeCAD/FreeCAD", App::Meta::UrlType::repository));
|
|
}
|
|
App::Metadata newPreferencePackMetadata;
|
|
newPreferencePackMetadata.setName(name);
|
|
newPreferencePackMetadata.setVersion(1);
|
|
|
|
auto templateType = templates.front().type;
|
|
for (const auto& t : templates) {
|
|
if (t.type != templateType) {
|
|
templateType = PreferencePack::Type::Combination;
|
|
break;
|
|
}
|
|
}
|
|
std::string typeString;
|
|
switch (templateType) {
|
|
case PreferencePack::Type::Appearance: typeString = "appearance"; break;
|
|
case PreferencePack::Type::Behavior: typeString = "behavior"; break;
|
|
case PreferencePack::Type::Combination: typeString = "combination"; break;
|
|
}
|
|
newPreferencePackMetadata.addGenericMetadata("type", App::Meta::GenericMetadata(typeString));
|
|
|
|
metadata->addContentItem("preferencepack", newPreferencePackMetadata);
|
|
metadata->write(savedPreferencePacksDirectory / "package.xml");
|
|
|
|
// Create the config file
|
|
ParameterManager outputParameterManager;
|
|
outputParameterManager.CreateDocument();
|
|
for (const auto& t : templates) {
|
|
ParameterManager templateParameterManager;
|
|
templateParameterManager.LoadDocument(t.path.string().c_str());
|
|
copyTemplateParameters(templateParameterManager, outputParameterManager);
|
|
}
|
|
auto cfgFilename = savedPreferencePacksDirectory / name / (name + ".cfg");
|
|
outputParameterManager.SaveDocument(cfgFilename.string().c_str());
|
|
}
|
|
|
|
// Needed until we support only C++20 and above and can use std::string's built-in ends_with()
|
|
bool fc_ends_with(std::string_view str, std::string_view suffix)
|
|
{
|
|
return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
|
|
}
|
|
|
|
std::vector<fs::path> scanForTemplateFolders(const std::string& groupName, const fs::path& entry)
|
|
{
|
|
// From this location, find the folder(s) called "PreferencePackTemplates"
|
|
std::vector<fs::path> templateFolders;
|
|
if (fs::exists(entry)) {
|
|
if (fs::is_directory(entry)) {
|
|
if (entry.filename() == "PreferencePackTemplates" ||
|
|
entry.filename() == "preference_pack_templates") {
|
|
templateFolders.push_back(entry);
|
|
}
|
|
else {
|
|
std::string subgroupName = groupName + "/" + entry.filename().string();
|
|
for (const auto& subentry : fs::directory_iterator(entry)) {
|
|
auto contents = scanForTemplateFolders(subgroupName, subentry);
|
|
std::copy(contents.begin(), contents.end(), std::back_inserter(templateFolders));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return templateFolders;
|
|
}
|
|
|
|
std::vector<PreferencePackManager::TemplateFile> scanForTemplateFiles(const std::string& groupName, const fs::path& entry)
|
|
{
|
|
auto templateFolders = scanForTemplateFolders(groupName, entry);
|
|
|
|
std::vector<PreferencePackManager::TemplateFile> templateFiles;
|
|
for (const auto& dir : templateFolders) {
|
|
auto templateDirs = std::vector<std::pair<fs::path, PreferencePack::Type>>({
|
|
std::make_pair(dir / "Appearance", PreferencePack::Type::Appearance),
|
|
std::make_pair(dir / "appearance", PreferencePack::Type::Appearance),
|
|
std::make_pair(dir / "Behavior", PreferencePack::Type::Behavior),
|
|
std::make_pair(dir / "behavior", PreferencePack::Type::Behavior),
|
|
std::make_pair(dir / "Behaviour", PreferencePack::Type::Behavior),
|
|
std::make_pair(dir / "behaviour", PreferencePack::Type::Behavior) });
|
|
for (const auto& templateDir : templateDirs) {
|
|
if (!fs::exists(templateDir.first) || !fs::is_directory(templateDir.first))
|
|
continue;
|
|
for (const auto& entry : fs::directory_iterator(templateDir.first)) {
|
|
if (entry.path().extension() == ".cfg") {
|
|
auto name = entry.path().filename().stem().string();
|
|
std::replace(name.begin(), name.end(), '_', ' ');
|
|
// Make sure we don't insert the same thing twice...
|
|
if (std::find_if(templateFiles.begin(), templateFiles.end(), [groupName, name](const auto &rhs)->bool {
|
|
return groupName == rhs.group && name == rhs.name;
|
|
} ) != templateFiles.end())
|
|
continue;
|
|
templateFiles.push_back({ groupName, name, entry, templateDir.second });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return templateFiles;
|
|
}
|
|
|
|
std::vector<PreferencePackManager::TemplateFile> PreferencePackManager::templateFiles(bool rescan)
|
|
{
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|
if (!_templateFiles.empty() && !rescan)
|
|
return _templateFiles;
|
|
|
|
// Locate all of the template files available on this system
|
|
// Template files end in ".cfg" -- They are located in:
|
|
// * $INSTALL_DIR/data/Gui/PreferencePackTemplates/(Appearance|Behavior)/*
|
|
// * $DATA_DIR/Mod/**/PreferencePackTemplates/(Appearance|Behavior)/*
|
|
// (alternate spellings are provided for packages using CamelCase and snake_case, and both major English dialects)
|
|
|
|
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui";
|
|
auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
|
|
|
|
std::string group = "Built-In";
|
|
if (fs::exists(resourcePath) && fs::is_directory(resourcePath)) {
|
|
const auto localFiles = scanForTemplateFiles(group, resourcePath);
|
|
std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
|
|
}
|
|
|
|
if (fs::exists(modPath) && fs::is_directory(modPath)) {
|
|
for (const auto& mod : fs::directory_iterator(modPath)) {
|
|
group = mod.path().filename().string();
|
|
const auto localFiles = scanForTemplateFiles(group, mod);
|
|
std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
|
|
}
|
|
}
|
|
|
|
return _templateFiles;
|
|
}
|
|
|
|
void Gui::PreferencePackManager::BackupCurrentConfig() const
|
|
{
|
|
auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups";
|
|
fs::create_directories(backupDirectory);
|
|
|
|
// Create a timestamped filename:
|
|
auto time = std::time(nullptr);
|
|
std::ostringstream timestampStream;
|
|
timestampStream << "user." << time << ".cfg";
|
|
auto filename = backupDirectory / timestampStream.str();
|
|
|
|
// Save the current config:
|
|
App::GetApplication().GetUserParameter().SaveDocument(filename.string().c_str());
|
|
}
|
|
|
|
void Gui::PreferencePackManager::DeleteOldBackups() const
|
|
{
|
|
constexpr auto oneWeek = 60.0 * 60.0 * 24.0 * 7.0;
|
|
const auto now = std::time(nullptr);
|
|
auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups";
|
|
if (fs::exists(backupDirectory) && fs::is_directory(backupDirectory)) {
|
|
for (const auto& backup : fs::directory_iterator(backupDirectory)) {
|
|
if (std::difftime(now, fs::last_write_time(backup)) > oneWeek) {
|
|
try {
|
|
fs::remove(backup);
|
|
}
|
|
catch (...) {}
|
|
}
|
|
}
|
|
}
|
|
} |