1
/***************************************************************************
2
* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
***************************************************************************/
24
#include "PreCompiled.h"
28
# include <string_view>
32
#include <boost/filesystem.hpp>
35
#include "PreferencePackManager.h"
36
#include "App/Metadata.h"
37
#include "Base/Parameter.h"
38
#include "Base/Interpreter.h"
39
#include "Base/Console.h"
40
#include "DockWindowManager.h"
41
#include "ToolBarManager.h"
43
#include <App/Application.h>
45
#include <ctime> // For generating a timestamped filename
49
using namespace xercesc;
50
namespace fs = boost::filesystem;
52
PreferencePack::PreferencePack(const fs::path& path, const App::Metadata& metadata) :
53
_path(path), _metadata(metadata)
55
if (!fs::exists(_path)) {
56
throw std::runtime_error{ "Cannot access " + path.string() };
59
auto qssPaths = QDir::searchPaths(QString::fromUtf8("qss"));
60
auto cssPaths = QDir::searchPaths(QString::fromUtf8("css"));
61
auto overlayPaths = QDir::searchPaths(QString::fromUtf8("overlay"));
63
qssPaths.append(QString::fromStdString(_path.string()));
64
cssPaths.append(QString::fromStdString(_path.string()));
65
overlayPaths.append(QString::fromStdString(_path.string() + "/overlay"));
67
QDir::setSearchPaths(QString::fromUtf8("qss"), qssPaths);
68
QDir::setSearchPaths(QString::fromUtf8("css"), cssPaths);
69
QDir::setSearchPaths(QString::fromUtf8("overlay"), overlayPaths);
72
std::string PreferencePack::name() const
74
return _metadata.name();
77
bool PreferencePack::apply() const
79
// Run the pre.FCMacro, if it exists: if it raises an exception, abort the process
80
auto preMacroPath = _path / "pre.FCMacro";
81
if (fs::exists(preMacroPath)) {
83
Base::Interpreter().runFile(preMacroPath.string().c_str(), false);
86
Base::Console().Message("PreferencePack application aborted by the preferencePack's pre.FCMacro");
91
// Back up the old config file
92
auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
93
auto backupFile = savedPreferencePacksDirectory / "user.cfg.backup";
95
fs::remove(backupFile);
98
App::GetApplication().GetUserParameter().SaveDocument(backupFile.string().c_str());
100
// Apply the config settings
101
applyConfigChanges();
103
// Run the Post.FCMacro, if it exists
104
auto postMacroPath = _path / "post.FCMacro";
105
if (fs::exists(postMacroPath)) {
107
Base::Interpreter().runFile(postMacroPath.string().c_str(), false);
110
Base::Console().Message("PreferencePack application reverted by the preferencePack's post.FCMacro");
111
App::GetApplication().GetUserParameter().LoadDocument(backupFile.string().c_str());
120
App::Metadata Gui::PreferencePack::metadata() const
125
void PreferencePack::applyConfigChanges() const
127
auto configFile = _path / (_metadata.name() + ".cfg");
128
if (fs::exists(configFile)) {
129
auto newParameters = ParameterManager::Create();
130
newParameters->LoadDocument(configFile.string().c_str());
131
auto baseAppGroup = App::GetApplication().GetUserParameter().GetGroup("BaseApp");
132
newParameters->GetGroup("BaseApp")->insertTo(baseAppGroup);
136
PreferencePackManager::PreferencePackManager()
138
auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
139
auto savedPath = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
140
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui" / "PreferencePacks";
141
_preferencePackPaths.push_back(resourcePath);
142
_preferencePackPaths.push_back(modPath);
143
_preferencePackPaths.push_back(savedPath);
150
void PreferencePackManager::rescan()
152
std::lock_guard<std::mutex> lock(_mutex);
153
_preferencePacks.clear();
154
for (const auto& path : _preferencePackPaths) {
155
if (fs::exists(path) && fs::is_directory(path)) {
156
FindPreferencePacksInPackage(path);
157
for (const auto& mod : fs::directory_iterator(path)) {
158
if (fs::is_directory(mod)) {
159
FindPreferencePacksInPackage(mod);
166
void Gui::PreferencePackManager::AddPackToMetadata(const std::string &packName) const
168
std::lock_guard<std::mutex> lock(_mutex);
169
auto savedPreferencePacksDirectory =
170
fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
171
fs::path preferencePackDirectory(savedPreferencePacksDirectory / packName);
172
if (fs::exists(preferencePackDirectory) && !fs::is_directory(preferencePackDirectory))
173
throw std::runtime_error("Cannot create " + savedPreferencePacksDirectory.string()
174
+ ": file with that name exists already");
176
if (!fs::exists(preferencePackDirectory)) fs::create_directories(preferencePackDirectory);
178
// Create or update the saved user preferencePacks package.xml metadata file
179
std::unique_ptr<App::Metadata> metadata;
180
if (fs::exists(savedPreferencePacksDirectory / "package.xml")) {
181
metadata = std::make_unique<App::Metadata>(savedPreferencePacksDirectory / "package.xml");
184
metadata = std::make_unique<App::Metadata>();
185
metadata->setName("User-Saved Preference Packs");
186
std::stringstream str;
187
str << "Generated automatically -- edits may be lost when saving new preference packs. To "
188
<< "distribute one or more of these packs:\n"
189
<< " 1) copy the entire SavedPreferencePacks directory to a convenient location,\n"
190
<< " 2) rename the directory (usually to the name of the preference pack you are "
191
<< "distributing),\n"
192
<< " 3) delete any subfolders containing packs you don't want to distribute,\n"
193
<< " 4) use git to initialize the directory as a git repository,\n"
194
<< " 5) push it to a remote git host,\n"
195
<< " 6) activate Developer Mode in the Addon Manager,\n"
196
<< " 7) use Developer Tools in the Addon Manager to update the metadata file,\n"
197
<< " 8) add, commit, and push the updated package.xml file,\n"
198
<< " 9) add your remote host to the custom repositories list in the Addon Manager"
200
<< " 10) use the Addon Manager to install your preference pack locally for testing.";
201
metadata->setDescription(str.str());
202
metadata->addLicense(App::Meta::License("All Rights Reserved", fs::path()));
204
for (const auto &item : metadata->content()) {
205
if (item.first == "preferencepack") {
206
if (item.second.name() == packName) {
207
// A pack with this name exists already, bail out
212
App::Metadata newPreferencePackMetadata;
213
newPreferencePackMetadata.setName(packName);
215
metadata->addContentItem("preferencepack", newPreferencePackMetadata);
216
metadata->write(savedPreferencePacksDirectory / "package.xml");
219
void Gui::PreferencePackManager::importConfig(const std::string& packName,
220
const boost::filesystem::path& path)
222
AddPackToMetadata(packName);
224
auto savedPreferencePacksDirectory =
225
fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
226
auto cfgFilename = savedPreferencePacksDirectory / packName / (packName + ".cfg");
227
fs::copy_file(path, cfgFilename, fs::copy_option::overwrite_if_exists);
231
void Gui::PreferencePackManager::FindPreferencePacksInPackage(const fs::path &mod)
234
TryFindPreferencePacksInPackage(mod);
236
catch (const std::exception& e) {
237
Base::Console().Error("%s\n", e.what());
240
// Failed to read the metadata, or to create the preferencePack based on it...
241
auto packageMetadataFile = mod / "package.xml";
242
Base::Console().Error("Failed to read %s\n", packageMetadataFile.string().c_str());
246
void PreferencePackManager::TryFindPreferencePacksInPackage(const boost::filesystem::path& mod)
248
auto packageMetadataFile = mod / "package.xml";
249
static const auto modDirectory = fs::path(App::Application::getUserAppDataDir()) / "Mod" / "SavedPreferencePacks";
250
static const auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui" / "PreferencePacks";
252
if (fs::exists(packageMetadataFile) && fs::is_regular_file(packageMetadataFile)) {
253
App::Metadata metadata(packageMetadataFile);
254
auto content = metadata.content();
255
auto basename = mod.filename().string();
256
if (mod == modDirectory)
257
basename = "##USER_SAVED##";
258
else if (mod == resourcePath)
259
basename = "##BUILT_IN##";
260
for (const auto& item : content) {
261
if (item.first == "preferencepack") {
262
if (isVisible(basename, item.second.name())) {
263
PreferencePack newPreferencePack(mod / item.second.name(), item.second);
264
_preferencePacks.insert(std::make_pair(newPreferencePack.name(), newPreferencePack));
271
std::vector<std::string> PreferencePackManager::preferencePackNames() const
273
std::lock_guard<std::mutex> lock(_mutex);
274
std::vector<std::string> names;
275
for (const auto& preferencePack : _preferencePacks)
276
names.push_back(preferencePack.first);
280
std::map<std::string, PreferencePack> Gui::PreferencePackManager::preferencePacks() const
282
return _preferencePacks;
285
bool PreferencePackManager::apply(const std::string& preferencePackName) const
287
std::lock_guard<std::mutex> lock(_mutex);
288
if (auto preferencePack = _preferencePacks.find(preferencePackName); preferencePack != _preferencePacks.end()) {
289
BackupCurrentConfig();
290
bool wasApplied = preferencePack->second.apply();
292
// If the visibility state of the dock windows was changed we have to manually reload their state
293
Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance();
294
pDockMgr->loadState();
296
// Same goes for toolbars:
297
Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance();
298
pToolbarMgr->restoreState();
300
// TODO: Are there other things that have to be manually triggered?
305
throw std::runtime_error("No such Preference Pack: " + preferencePackName);
309
std::string findUnusedName(const std::string &basename, ParameterGrp::handle parent)
313
std::ostringstream nameToTest;
314
nameToTest << basename << "_" << i;
315
if (!parent->HasGroup(nameToTest.str().c_str()))
316
return nameToTest.str();
321
bool PreferencePackManager::isVisible(const std::string& addonName, const std::string& preferencePackName) const
323
if (addonName.empty() || preferencePackName.empty())
326
auto pref = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General/HiddenPreferencePacks");
327
auto hiddenPacks = pref->GetGroups();
328
auto hiddenPack = std::find_if(hiddenPacks.begin(), hiddenPacks.end(), [addonName, preferencePackName](ParameterGrp::handle handle) {
329
return (handle->GetASCII("addonName", "") == addonName) && (handle->GetASCII("preferencePackName", "") == preferencePackName);
331
if (hiddenPack == hiddenPacks.end())
337
void PreferencePackManager::toggleVisibility(const std::string& addonName, const std::string& preferencePackName)
339
if (preferencePackName.empty())
341
auto pref = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General/HiddenPreferencePacks");
342
auto hiddenPacks = pref->GetGroups();
343
auto hiddenPack = std::find_if(hiddenPacks.begin(), hiddenPacks.end(), [addonName,preferencePackName](ParameterGrp::handle handle) {
344
return (handle->GetASCII("addonName", "") == addonName) && (handle->GetASCII("preferencePackName", "") == preferencePackName);
346
if (hiddenPack == hiddenPacks.end()) {
347
auto name = findUnusedName("PreferencePack", pref);
348
auto group = pref->GetGroup(name.c_str());
349
group->SetASCII("addonName", addonName.c_str());
350
group->SetASCII("preferencePackName", preferencePackName.c_str());
353
auto groupName = (*hiddenPack)->GetGroupName();
354
hiddenPacks.clear(); // To decrement the reference count of the group we are about the remove...
355
pref->RemoveGrp(groupName);
360
void Gui::PreferencePackManager::deleteUserPack(const std::string& name)
364
auto savedPreferencePacksDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
365
auto savedPath = savedPreferencePacksDirectory / name;
366
std::unique_ptr<App::Metadata> metadata;
367
if (fs::exists(savedPreferencePacksDirectory / "package.xml")) {
368
metadata = std::make_unique<App::Metadata>(savedPreferencePacksDirectory / "package.xml");
371
throw std::runtime_error("Lost the user-saved preference packs metadata file!");
373
metadata->removeContentItem("preferencepack", name);
374
metadata->write(savedPreferencePacksDirectory / "package.xml");
375
if (fs::exists(savedPath))
376
fs::remove_all(savedPath);
380
void copyTemplateParameters(Base::Reference<ParameterGrp> templateGroup, const std::string& path, Base::Reference<ParameterGrp> outputGroup)
382
auto userParameterHandle = App::GetApplication().GetParameterGroupByPath(path.c_str());
384
// Ensure that the DockWindowManager has saved its current state:
385
Gui::DockWindowManager* pDockMgr = Gui::DockWindowManager::instance();
386
pDockMgr->saveState();
388
// Do the same for ToolBars
389
Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance();
390
pToolbarMgr->saveState();
392
auto boolMap = templateGroup->GetBoolMap();
393
for (const auto& kv : boolMap) {
394
auto currentValue = userParameterHandle->GetBool(kv.first.c_str(), kv.second);
395
outputGroup->SetBool(kv.first.c_str(), currentValue);
398
auto intMap = templateGroup->GetIntMap();
399
for (const auto& kv : intMap) {
400
auto currentValue = userParameterHandle->GetInt(kv.first.c_str(), kv.second);
401
outputGroup->SetInt(kv.first.c_str(), currentValue);
404
auto uintMap = templateGroup->GetUnsignedMap();
405
for (const auto& kv : uintMap) {
406
auto currentValue = userParameterHandle->GetUnsigned(kv.first.c_str(), kv.second);
407
outputGroup->SetUnsigned(kv.first.c_str(), currentValue);
410
auto floatMap = templateGroup->GetFloatMap();
411
for (const auto& kv : floatMap) {
412
auto currentValue = userParameterHandle->GetFloat(kv.first.c_str(), kv.second);
413
outputGroup->SetFloat(kv.first.c_str(), currentValue);
416
auto asciiMap = templateGroup->GetASCIIMap();
417
for (const auto& kv : asciiMap) {
418
auto currentValue = userParameterHandle->GetASCII(kv.first.c_str(), kv.second.c_str());
419
outputGroup->SetASCII(kv.first.c_str(), currentValue.c_str());
423
auto templateSubgroups = templateGroup->GetGroups();
424
for (auto& templateSubgroup : templateSubgroups) {
425
std::string sgName = templateSubgroup->GetGroupName();
426
auto outputSubgroupHandle = outputGroup->GetGroup(sgName.c_str());
427
copyTemplateParameters(templateSubgroup, path + "/" + sgName, outputSubgroupHandle);
431
void copyTemplateParameters(/*const*/ ParameterManager& templateParameterManager, ParameterManager& outputParameterManager)
433
auto groups = templateParameterManager.GetGroups();
434
for (auto& group : groups) {
435
std::string name = group->GetGroupName();
436
auto groupHandle = outputParameterManager.GetGroup(name.c_str());
437
copyTemplateParameters(group, "User parameter:" + name, groupHandle);
441
void PreferencePackManager::save(const std::string& name, const std::vector<TemplateFile>& templates)
443
if (templates.empty())
446
AddPackToMetadata(name);
448
// Create the config file
449
auto outputParameterManager = ParameterManager::Create();
450
outputParameterManager->CreateDocument();
451
for (const auto& t : templates) {
452
auto templateParameterManager = ParameterManager::Create();
453
templateParameterManager->LoadDocument(t.path.string().c_str());
454
copyTemplateParameters(*templateParameterManager, *outputParameterManager);
456
auto savedPreferencePacksDirectory =
457
fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks";
458
auto cfgFilename = savedPreferencePacksDirectory / name / (name + ".cfg");
459
outputParameterManager->SaveDocument(cfgFilename.string().c_str());
462
// Needed until we support only C++20 and above and can use std::string's built-in ends_with()
463
bool fc_ends_with(std::string_view str, std::string_view suffix)
465
return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
468
std::vector<fs::path> scanForTemplateFolders(const std::string& groupName, const fs::path& entry)
470
// From this location, find the folder(s) called "PreferencePackTemplates"
471
std::vector<fs::path> templateFolders;
472
if (fs::exists(entry)) {
473
if (fs::is_directory(entry)) {
474
if (entry.filename() == "PreferencePackTemplates" ||
475
entry.filename() == "preference_pack_templates") {
476
templateFolders.push_back(entry);
479
std::string subgroupName = groupName + "/" + entry.filename().string();
480
for (const auto& subentry : fs::directory_iterator(entry)) {
481
auto contents = scanForTemplateFolders(subgroupName, subentry);
482
std::copy(contents.begin(), contents.end(), std::back_inserter(templateFolders));
487
return templateFolders;
490
std::vector<PreferencePackManager::TemplateFile> scanForTemplateFiles(const std::string& groupName, const fs::path& entry)
492
auto templateFolders = scanForTemplateFolders(groupName, entry);
494
std::vector<PreferencePackManager::TemplateFile> templateFiles;
495
for (const auto& templateDir : templateFolders) {
496
if (!fs::exists(templateDir) || !fs::is_directory(templateDir))
498
for (const auto& entry : fs::directory_iterator(templateDir)) {
499
if (entry.path().extension() == ".cfg") {
500
auto name = entry.path().filename().stem().string();
501
std::replace(name.begin(), name.end(), '_', ' ');
502
// Make sure we don't insert the same thing twice...
503
if (std::find_if(templateFiles.begin(), templateFiles.end(), [groupName, name](const auto &rhs)->bool {
504
return groupName == rhs.group && name == rhs.name;
505
} ) != templateFiles.end())
507
templateFiles.push_back({ groupName, name, entry });
511
return templateFiles;
514
std::vector<PreferencePackManager::TemplateFile> PreferencePackManager::templateFiles(bool rescan)
516
std::lock_guard<std::mutex> lock(_mutex);
517
if (!_templateFiles.empty() && !rescan)
518
return _templateFiles;
520
// Locate all of the template files available on this system
521
// Template files end in ".cfg" -- They are located in:
522
// * $INSTALL_DIR/data/Gui/PreferencePackTemplates/(Appearance|Behavior)/*
523
// * $DATA_DIR/Mod/**/PreferencePackTemplates/(Appearance|Behavior)/*
524
// (alternate spellings are provided for packages using CamelCase and snake_case, and both major English dialects)
526
auto resourcePath = fs::path(App::Application::getResourceDir()) / "Gui";
527
auto modPath = fs::path(App::Application::getUserAppDataDir()) / "Mod";
529
std::string group = "Built-In";
530
if (fs::exists(resourcePath) && fs::is_directory(resourcePath)) {
531
const auto localFiles = scanForTemplateFiles(group, resourcePath);
532
std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
535
if (fs::exists(modPath) && fs::is_directory(modPath)) {
536
for (const auto& mod : fs::directory_iterator(modPath)) {
537
group = mod.path().filename().string();
538
const auto localFiles = scanForTemplateFiles(group, mod);
539
std::copy(localFiles.begin(), localFiles.end(), std::back_inserter(_templateFiles));
543
return _templateFiles;
546
void Gui::PreferencePackManager::BackupCurrentConfig() const
548
auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups";
549
fs::create_directories(backupDirectory);
551
// Create a timestamped filename:
552
auto time = std::time(nullptr);
553
std::ostringstream timestampStream;
554
timestampStream << "user." << time << ".cfg";
555
auto filename = backupDirectory / timestampStream.str();
557
// Save the current config:
558
App::GetApplication().GetUserParameter().SaveDocument(filename.string().c_str());
561
void Gui::PreferencePackManager::DeleteOldBackups() const
563
constexpr auto oneWeek = 60.0 * 60.0 * 24.0 * 7.0;
564
const auto now = std::time(nullptr);
565
auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups";
566
if (fs::exists(backupDirectory) && fs::is_directory(backupDirectory)) {
567
for (const auto& backup : fs::directory_iterator(backupDirectory)) {
568
if (std::difftime(now, fs::last_write_time(backup)) > oneWeek) {
578
std::vector<boost::filesystem::path> Gui::PreferencePackManager::configBackups() const
580
std::vector<boost::filesystem::path> results;
581
auto backupDirectory = fs::path(App::Application::getUserAppDataDir()) / "SavedPreferencePacks" / "Backups";
582
if (fs::exists(backupDirectory) && fs::is_directory(backupDirectory)) {
583
for (const auto& backup : fs::directory_iterator(backupDirectory)) {
584
results.push_back(backup);