Support saving project settings to .blender_project/settings.json

This commit is contained in:
Julian Eisel 2022-10-06 15:51:10 +02:00
parent 3ab935c8a9
commit cd15fbbed6
8 changed files with 176 additions and 22 deletions

View File

@ -19,8 +19,8 @@ class PROJECTSETTINGS_HT_header(Header):
# Show '*' to let users know the settings have been modified.
# TODO, wrong operator
layout.operator(
"wm.save_userpref",
text=iface_("Save Project Settings") + (" *" if is_dirty else ""),
"wm.save_project_settings",
text=iface_("Save Settings") + (" *" if is_dirty else ""),
translate=False,
)

View File

@ -36,6 +36,8 @@ void BKE_project_active_unset(void);
*/
BlenderProject *BKE_project_active_load_from_path(const char *path) ATTR_NONNULL();
bool BKE_project_settings_save(const BlenderProject *project) ATTR_NONNULL();
const char *BKE_project_root_path_get(const BlenderProject *project) ATTR_WARN_UNUSED_RESULT
ATTR_NONNULL();
const char *BKE_project_name_get(const BlenderProject *project) ATTR_WARN_UNUSED_RESULT

View File

@ -10,6 +10,10 @@
#include "BLI_string_ref.hh"
namespace blender::io::serialize {
class DictionaryValue;
}
namespace blender::bke {
class ProjectSettings;
@ -60,16 +64,27 @@ class ProjectSettings {
/**
* Read project settings from the given \a project_path, which may be either a project root
* directory or the .blender_project directory.
* Both Unix and Windows style slashes are allowed.
* Both Unix and Windows style slashes are allowed. Path is expected to be normalized.
* \return The read project settings or null in case of failure.
*/
static auto load_from_disk [[nodiscard]] (StringRef project_path)
-> std::unique_ptr<ProjectSettings>;
/**
* Write project settings to the given \a project_path, which may be either a project root
* directory or the .blender_project directory. The .blender_project directory must exist.
* Both Unix and Windows style slashes are allowed. Path is expected to be normalized.
* \return True on success. If the .blender_project directory doesn't exist, that's treated as
* failure.
*/
auto save_to_disk(StringRef project_path) const -> bool;
explicit ProjectSettings(StringRef project_root_path);
auto project_root_path [[nodiscard]] () const -> StringRefNull;
auto project_name [[nodiscard]] () const -> StringRefNull;
private:
auto to_dictionary() const -> std::unique_ptr<io::serialize::DictionaryValue>;
};
} // namespace blender::bke

View File

@ -152,36 +152,55 @@ static std::unique_ptr<ExtractedSettings> extract_settings(
return extracted_settings;
}
std::unique_ptr<ProjectSettings> ProjectSettings::load_from_disk(StringRef project_path)
struct ResolvedPaths {
std::string settings_filepath;
std::string project_root_path;
};
/**
* Returned paths can be assumed to use native slashes.
*/
static ResolvedPaths resolve_paths_from_project_path(StringRef project_path)
{
std::string project_path_native = project_path;
BLI_path_slash_native(project_path_native.data());
if (!BLI_exists(project_path_native.c_str())) {
return nullptr;
}
StringRef project_root_path = project_path_native;
ResolvedPaths resolved_paths{};
const StringRef path_no_trailing_slashes = path_strip_trailing_native_slash(project_path_native);
if (path_no_trailing_slashes.endswith(SETTINGS_DIRNAME)) {
project_root_path = StringRef(project_path_native).drop_suffix(SETTINGS_DIRNAME.size() + 1);
if (path_no_trailing_slashes.endswith(ProjectSettings::SETTINGS_DIRNAME)) {
resolved_paths.project_root_path =
StringRef(path_no_trailing_slashes).drop_suffix(ProjectSettings::SETTINGS_DIRNAME.size());
}
else {
resolved_paths.project_root_path = std::string(path_no_trailing_slashes) + SEP;
}
resolved_paths.settings_filepath = resolved_paths.project_root_path +
ProjectSettings::SETTINGS_DIRNAME + SEP +
ProjectSettings::SETTINGS_FILENAME;
if (!path_contains_project_settings(project_root_path)) {
return resolved_paths;
}
std::unique_ptr<ProjectSettings> ProjectSettings::load_from_disk(StringRef project_path)
{
ResolvedPaths paths = resolve_paths_from_project_path(project_path);
if (!BLI_exists(paths.project_root_path.c_str())) {
return nullptr;
}
if (!path_contains_project_settings(paths.project_root_path.c_str())) {
return nullptr;
}
std::string settings_filepath = project_path_native + SEP + SETTINGS_DIRNAME + SEP +
SETTINGS_FILENAME;
std::unique_ptr<serialize::Value> values = read_settings_file(settings_filepath);
std::unique_ptr<serialize::Value> values = read_settings_file(paths.settings_filepath);
std::unique_ptr<ExtractedSettings> extracted_settings = nullptr;
if (values) {
BLI_assert(values->as_dictionary_value() != nullptr);
extracted_settings = extract_settings(*values->as_dictionary_value());
}
std::unique_ptr loaded_settings = std::make_unique<ProjectSettings>(project_root_path);
std::unique_ptr loaded_settings = std::make_unique<ProjectSettings>(paths.project_root_path);
if (extracted_settings) {
loaded_settings->project_name_ = extracted_settings->project_name;
}
@ -189,6 +208,50 @@ std::unique_ptr<ProjectSettings> ProjectSettings::load_from_disk(StringRef proje
return loaded_settings;
}
std::unique_ptr<serialize::DictionaryValue> ProjectSettings::to_dictionary() const
{
using namespace serialize;
std::unique_ptr<DictionaryValue> root = std::make_unique<DictionaryValue>();
DictionaryValue::Items &root_attributes = root->elements();
std::unique_ptr<DictionaryValue> project_dict = std::make_unique<DictionaryValue>();
DictionaryValue::Items &project_attributes = project_dict->elements();
project_attributes.append_as("name", new StringValue(project_name_));
root_attributes.append_as("project", std::move(project_dict));
return root;
}
static void write_settings_file(StringRef settings_filepath,
std::unique_ptr<serialize::DictionaryValue> dictionary)
{
using namespace serialize;
JsonFormatter formatter;
std::ofstream os;
os.open(settings_filepath, std::ios::out | std::ios::trunc);
formatter.serialize(os, *dictionary);
os.close();
}
bool ProjectSettings::save_to_disk(StringRef project_path) const
{
ResolvedPaths paths = resolve_paths_from_project_path(project_path);
if (!BLI_exists(paths.project_root_path.c_str())) {
return false;
}
if (!path_contains_project_settings(paths.project_root_path.c_str())) {
return false;
}
std::unique_ptr settings_as_dict = to_dictionary();
write_settings_file(paths.settings_filepath, std::move(settings_as_dict));
return true;
}
StringRefNull ProjectSettings::project_root_path() const
{
return project_root_path_;
@ -241,6 +304,14 @@ BlenderProject *BKE_project_active_load_from_path(const char *path)
return BKE_project_active_get();
}
bool BKE_project_settings_save(const BlenderProject *project_handle)
{
const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
project_handle);
const bke::ProjectSettings &settings = project->get_settings();
return settings.save_to_disk(settings.project_root_path());
}
const char *BKE_project_root_path_get(const BlenderProject *project_handle)
{
const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(

View File

@ -19,6 +19,12 @@
namespace blender::bke::tests {
struct SVNFiles {
const std::string svn_root = blender::tests::flags_test_asset_dir();
const std::string project_root_rel = "blender_project/the_project";
const std::string project_root = svn_root + "/blender_project/the_project";
};
class ProjectTest : public testing::Test {
/* RAII helper to delete the created directories reliably after testing or on errors. */
struct ProjectDirectoryRAIIWrapper {
@ -36,6 +42,9 @@ class ProjectTest : public testing::Test {
native_project_path_ = project_path;
BLI_path_slash_native(native_project_path_.data());
if (native_project_path_.back() != SEP) {
native_project_path_ += SEP;
}
/** Assert would be preferable but that would only run in debug builds, and #ASSERT_TRUE()
* doesn't support printing a message. */
@ -139,18 +148,42 @@ TEST_F(ProjectTest, settings_load_from_project_settings_path)
ProjectSettings::create_settings_directory(project_path);
std::unique_ptr project_settings = ProjectSettings::load_from_disk(
project_path + SEP_STR + ProjectSettings::SETTINGS_DIRNAME);
project_path + (ELEM(project_path.back(), SEP, ALTSEP) ? "" : SEP_STR) +
ProjectSettings::SETTINGS_DIRNAME);
EXPECT_NE(project_settings, nullptr);
EXPECT_EQ(project_settings->project_root_path(), project_path_native);
EXPECT_EQ(project_settings->project_name(), "");
});
}
struct SVNFiles {
const std::string svn_root = blender::tests::flags_test_asset_dir();
const std::string project_root_rel = "blender_project/the_project";
const std::string project_root = svn_root + "/blender_project/the_project";
};
TEST_F(ProjectTest, settings_json_read)
{
SVNFiles svn_files{};
std::unique_ptr from_project_settings = ProjectSettings::load_from_disk(svn_files.project_root);
EXPECT_NE(from_project_settings, nullptr);
EXPECT_EQ(from_project_settings->project_name(), "Ružena");
}
TEST_F(ProjectTest, settings_json_write)
{
SVNFiles svn_files{};
std::unique_ptr from_project_settings = ProjectSettings::load_from_disk(svn_files.project_root);
/* Take the settings read from the SVN files and write it to /tmp/ projects. */
test_foreach_project_path(
[&from_project_settings](StringRefNull to_project_path, StringRefNull) {
ProjectSettings::create_settings_directory(to_project_path);
if (!from_project_settings->save_to_disk(to_project_path)) {
FAIL();
}
/* Now check if the settings written to disk match the expectations. */
std::unique_ptr written_settings = ProjectSettings::load_from_disk(to_project_path);
EXPECT_NE(written_settings, nullptr);
EXPECT_EQ(written_settings->project_name(), "Ružena");
});
}
TEST_F(ProjectTest, project_root_path_find_from_path)
{

View File

@ -2312,6 +2312,37 @@ void WM_OT_read_factory_userpref(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Write Project Settings Operator
* \{ */
/* Only save the prefs block. operator entry */
static int wm_save_project_settings_exec(bContext *UNUSED(C), wmOperator *UNUSED(op))
{
BlenderProject *active_project = CTX_wm_project();
if (!active_project) {
return OPERATOR_CANCELLED;
}
if (!BKE_project_settings_save(active_project)) {
return OPERATOR_CANCELLED;
}
return OPERATOR_FINISHED;
}
void WM_OT_save_project_settings(wmOperatorType *ot)
{
ot->name = "Save Project Settings";
ot->idname = "WM_OT_save_project_settings";
ot->description = "Make the current changes to the project settings permanent";
ot->invoke = WM_operator_confirm;
ot->exec = wm_save_project_settings_exec;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Read File History Operator
* \{ */

View File

@ -3742,6 +3742,7 @@ void wm_operatortypes_register(void)
WM_operatortype_append(WM_OT_read_factory_settings);
WM_operatortype_append(WM_OT_save_homefile);
WM_operatortype_append(WM_OT_save_userpref);
WM_operatortype_append(WM_OT_save_project_settings);
WM_operatortype_append(WM_OT_read_userpref);
WM_operatortype_append(WM_OT_read_factory_userpref);
WM_operatortype_append(WM_OT_window_fullscreen_toggle);

View File

@ -90,6 +90,7 @@ bool wm_file_or_session_data_has_unsaved_changes(const Main *bmain, const wmWind
void WM_OT_save_homefile(struct wmOperatorType *ot);
void WM_OT_save_userpref(struct wmOperatorType *ot);
void WM_OT_read_userpref(struct wmOperatorType *ot);
void WM_OT_save_project_settings(struct wmOperatorType *ot);
void WM_OT_read_factory_userpref(struct wmOperatorType *ot);
void WM_OT_read_history(struct wmOperatorType *ot);
void WM_OT_read_homefile(struct wmOperatorType *ot);