Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'source/blender/blenkernel')
-rw-r--r--source/blender/blenkernel/BKE_asset_library_custom.h69
-rw-r--r--source/blender/blenkernel/BKE_blender_project.h75
-rw-r--r--source/blender/blenkernel/BKE_blender_project.hh204
-rw-r--r--source/blender/blenkernel/BKE_context.h3
-rw-r--r--source/blender/blenkernel/BKE_preferences.h46
-rw-r--r--source/blender/blenkernel/CMakeLists.txt8
-rw-r--r--source/blender/blenkernel/intern/asset_catalog_test.cc8
-rw-r--r--source/blender/blenkernel/intern/asset_library.cc6
-rw-r--r--source/blender/blenkernel/intern/asset_library_custom.cc112
-rw-r--r--source/blender/blenkernel/intern/blender_project.cc311
-rw-r--r--source/blender/blenkernel/intern/blender_project_settings.cc318
-rw-r--r--source/blender/blenkernel/intern/blender_project_test.cc325
-rw-r--r--source/blender/blenkernel/intern/blendfile.c14
-rw-r--r--source/blender/blenkernel/intern/context.c15
-rw-r--r--source/blender/blenkernel/intern/preferences.c91
15 files changed, 1466 insertions, 139 deletions
diff --git a/source/blender/blenkernel/BKE_asset_library_custom.h b/source/blender/blenkernel/BKE_asset_library_custom.h
new file mode 100644
index 00000000000..738729ce957
--- /dev/null
+++ b/source/blender/blenkernel/BKE_asset_library_custom.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ *
+ * API to manage a list of #CustomAssetLibraryDefinition items.
+ */
+
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "BLI_compiler_attrs.h"
+#include "BLI_utildefines.h"
+
+struct CustomAssetLibraryDefinition;
+struct ListBase;
+
+struct CustomAssetLibraryDefinition *BKE_asset_library_custom_add(
+ struct ListBase *custom_libraries,
+ const char *name CPP_ARG_DEFAULT(nullptr),
+ const char *path CPP_ARG_DEFAULT(nullptr)) ATTR_NONNULL(1);
+/**
+ * Unlink and free a library preference member.
+ * \note Free's \a library itself.
+ */
+void BKE_asset_library_custom_remove(struct ListBase *custom_libraries,
+ struct CustomAssetLibraryDefinition *library) ATTR_NONNULL();
+
+void BKE_asset_library_custom_name_set(struct ListBase *custom_libraries,
+ struct CustomAssetLibraryDefinition *library,
+ const char *name) ATTR_NONNULL();
+
+/**
+ * Set the library path, ensuring it is pointing to a directory.
+ * Single blend files can only act as "Current File" library; libraries on disk
+ * should always be directories. Blindly sets the path without additional checks. The asset system
+ * can ignore libraries that it can't resolve to a valid location. If the path does not exist,
+ * that's fine; it can created as directory if necessary later.
+ */
+void BKE_asset_library_custom_path_set(struct CustomAssetLibraryDefinition *library,
+ const char *path) ATTR_NONNULL();
+
+struct CustomAssetLibraryDefinition *BKE_asset_library_custom_find_from_index(
+ const struct ListBase *custom_libraries, int index) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
+struct CustomAssetLibraryDefinition *BKE_asset_library_custom_find_from_name(
+ const struct ListBase *custom_libraries, const char *name)
+ ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
+
+/**
+ * Return the #CustomAssetLibraryDefinition that contains the given file/directory path. The given
+ * path can be the library's top-level directory, or any path inside that directory.
+ *
+ * When more than one asset libraries match, the first matching one is returned (no smartness when
+ * there nested asset libraries).
+ *
+ * Return NULL when no such asset library is found. */
+struct CustomAssetLibraryDefinition *BKE_asset_library_custom_containing_path(
+ const struct ListBase *custom_libraries, const char *path)
+ ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
+
+int BKE_asset_library_custom_get_index(
+ const struct ListBase /*#CustomAssetLibraryDefinition*/ *custom_libraries,
+ const struct CustomAssetLibraryDefinition *library) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
+#ifdef __cplusplus
+}
+#endif
diff --git a/source/blender/blenkernel/BKE_blender_project.h b/source/blender/blenkernel/BKE_blender_project.h
new file mode 100644
index 00000000000..4851e249884
--- /dev/null
+++ b/source/blender/blenkernel/BKE_blender_project.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ */
+
+#pragma once
+
+#include "DNA_space_types.h"
+
+#include "BLI_compiler_attrs.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* C-handle for #bke::BlenderProject. */
+typedef struct BlenderProject BlenderProject;
+
+/** See #bke::ProjectSettings::create_settings_directory(). */
+bool BKE_project_create_settings_directory(const char *project_root_path) ATTR_NONNULL();
+/** See #bke::ProjectSettings::delete_settings_directory(). */
+bool BKE_project_delete_settings_directory(BlenderProject *project) ATTR_NONNULL();
+
+BlenderProject *BKE_project_active_get(void) ATTR_WARN_UNUSED_RESULT;
+/**
+ * \note: When unsetting an active project, the previously active one will be destroyed, so
+ * pointers may dangle.
+ */
+void BKE_project_active_unset(void);
+/**
+ * Check if \a path references a project root directory. Will return false for paths pointing into
+ * the project root directory.
+ */
+bool BKE_project_is_path_project_root(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
+/**
+ * Check if \a path points to or into a project root path (i.e. if one of the ancestors of the
+ * referenced file/directory is a project root directory).
+ */
+bool BKE_project_contains_path(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
+
+/**
+ * Attempt to load and activate a project based on the given path. If the path doesn't lead
+ * into a project, the active project is unset. Note that the project will be unset on any
+ * failure when loading the project.
+ *
+ * \note: When setting an active project, the previously active one will be destroyed, so
+ * pointers may dangle.
+ */
+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();
+/**
+ * \param name The new name to set, expected to be 0 terminated.
+ */
+void BKE_project_name_set(const BlenderProject *project_handle, const char *name) ATTR_NONNULL();
+const char *BKE_project_name_get(const BlenderProject *project) ATTR_WARN_UNUSED_RESULT
+ ATTR_NONNULL();
+ListBase *BKE_project_custom_asset_libraries_get(const BlenderProject *project)
+ ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
+void BKE_project_tag_has_unsaved_changes(const BlenderProject *project) ATTR_NONNULL();
+/**
+ * Check if the project is marked as having unsaved changes. For convenience this allows passing
+ * null as the project (returns false then), so a call like
+ * `BKE_project_has_unsaved_changes(CTX_wm_project())` can be done without having to null-check the
+ * project first.
+ */
+bool BKE_project_has_unsaved_changes(const BlenderProject *project) ATTR_WARN_UNUSED_RESULT;
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/source/blender/blenkernel/BKE_blender_project.hh b/source/blender/blenkernel/BKE_blender_project.hh
new file mode 100644
index 00000000000..185860674c6
--- /dev/null
+++ b/source/blender/blenkernel/BKE_blender_project.hh
@@ -0,0 +1,204 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ */
+
+#pragma once
+
+#include <memory>
+
+#include "BLI_listbase.h"
+#include "BLI_string_ref.hh"
+#include "BLI_utility_mixins.hh"
+
+struct BlenderProject;
+
+namespace blender::io::serialize {
+class DictionaryValue;
+}
+
+namespace blender::bke {
+
+class ProjectSettings;
+struct CustomAssetLibraries;
+
+/**
+ * Entry point / API for core Blender project management.
+ *
+ * Responsibilities:
+ * - Own and give access to the active project.
+ * - Manage the .blender_project/ directory.
+ * - Store and manage (including reading & writing) of the .blender_project/settings.json file. The
+ * implementation of this can be found in the internal #ProjectSettings class.
+ * - Tag for unsaved changes as needed.
+ */
+class BlenderProject {
+ friend class ProjectSettings;
+
+ /* Path to the project root using native slashes plus a trailing slash. */
+ std::string root_path_;
+ std::unique_ptr<ProjectSettings> settings_;
+
+ public:
+ inline static const StringRefNull SETTINGS_DIRNAME = ".blender_project";
+ inline static const StringRefNull SETTINGS_FILENAME = "settings.json";
+
+ public:
+ static auto get_active [[nodiscard]] () -> BlenderProject *;
+ static auto set_active(std::unique_ptr<BlenderProject> settings) -> BlenderProject *;
+
+ /**
+ * Read project settings from the given \a path, which may point to some directory or file inside
+ * of the project directory. Both Unix and Windows style slashes are allowed. Path is expected to
+ * be normalized.
+ *
+ * Attempt to read project data from the given \a project_path, which may be either a project
+ * root directory or the .blender_project directory, and load it into runtime data. Letting the
+ * returned #unique_pointer run out of scope cleanly destructs the runtime project data.
+ *
+ * \note Does NOT set the loaded project active.
+ *
+ * \return The loaded project or null on failure.
+ */
+ static auto load_from_path(StringRef project_path) -> std::unique_ptr<BlenderProject>;
+
+ /**
+ * Initializes a blender project by creating a .blender_project directory at the given \a
+ * project_root_path.
+ * Both Unix and Windows style slashes are allowed.
+ *
+ * \return True if the settings directory was created, or already existed. False on failure.
+ */
+ static auto create_settings_directory(StringRef project_root_path) -> bool;
+ /**
+ * Remove the .blender_project directory with all of its contents at the given \a
+ * project_root_path. If this is the path of the active project, it is marked as having changed
+ * but it is not unloaded. Runtime project data is still valid at this point.
+ *
+ * \return True on success.
+ */
+ static auto delete_settings_directory(StringRef project_root_path) -> bool;
+
+ /**
+ * Check if the directory given by \a path contains a .blender_project directory and should thus
+ * be considered a project root directory.
+ */
+ static auto path_is_project_root(StringRef path) -> bool;
+
+ /**
+ * Check if \a path points into a project and return the root directory path of that project (the
+ * one containing the .blender_project directory). Walks "upwards" through the path and returns
+ * the first project found, so if a project is nested inside another one, the nested project is
+ * used.
+ * Both Unix and Windows style slashes are allowed.
+ *
+ * \return The project root path or an empty path if not found. The referenced string points into
+ * the input \a path, so slashes are not converted in the returned value.
+ */
+ static auto project_root_path_find_from_path [[nodiscard]] (StringRef path) -> StringRef;
+
+ /* --- Non-static member functions. --- */
+
+ BlenderProject(StringRef project_root_path, std::unique_ptr<ProjectSettings> settings);
+
+ /**
+ * Version of the static #delete_settings_directory() that deletes the settings directory of this
+ * project. Always tags as having unsaved changes after successful deletion.
+ */
+ auto delete_settings_directory() -> bool;
+
+ auto root_path [[nodiscard]] () const -> StringRefNull;
+ auto get_settings [[nodiscard]] () const -> ProjectSettings &;
+
+ private:
+ static auto active_project_ptr() -> std::unique_ptr<BlenderProject> &;
+ /**
+ * Get the project root path from a path that is either already the project root, or the
+ * .blender_project directory. Returns the path with native slashes plus a trailing slash.
+ */
+ static auto project_path_to_native_project_root_path(StringRef project_path) -> std::string;
+ /**
+ * Get the .blender_project directory path from a project root path. Returns the path with native
+ * slashes plus a trailing slash. Assumes the path already ends with a native trailing slash.
+ */
+ static auto project_root_path_to_settings_path(StringRef project_root_path) -> std::string;
+ /**
+ * Returns the path with native slashes.
+ * Assumes the path already ends with a native trailing slash.
+ */
+ static auto project_root_path_to_settings_filepath(StringRef project_root_path) -> std::string;
+};
+
+/**
+ * Runtime representation of the project settings (`.blender_project/settings.json`) with IO
+ * functionality.
+ */
+class ProjectSettings {
+ std::string project_name_;
+ std::unique_ptr<CustomAssetLibraries> asset_libraries_;
+
+ bool has_unsaved_changes_ = false;
+
+ public:
+ /**
+ * 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. 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>;
+ /**
+ * Read project settings from the given \a path, which may point to some directory or file inside
+ * of the project directory. 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_path [[nodiscard]] (StringRef path) -> std::unique_ptr<ProjectSettings>;
+
+ /** Explicit constructor and destructor needed to manage the CustomAssetLibraries unique_ptr. */
+ ProjectSettings();
+ /* Implementation defaulted. */
+ ~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) -> bool;
+
+ void project_name(StringRef new_name);
+ auto project_name [[nodiscard]] () const -> StringRefNull;
+ auto asset_library_definitions() const -> const ListBase &;
+ auto asset_library_definitions() -> ListBase &;
+ /**
+ * Forcefully tag the project settings for having unsaved changes. This needs to be done if
+ * project settings data is modified directly by external code, not via a project settings API
+ * function. The API functions set the tag for all changes they manage.
+ */
+ void tag_has_unsaved_changes();
+ /**
+ * Returns true if there were any changes done to the settings that have not been written to
+ * disk yet. Project settings API functions that change data set this, however when external
+ * code modifies project settings data it may have to manually set the tag, see
+ * #tag_has_unsaved_changes().
+ */
+ auto has_unsaved_changes [[nodiscard]] () const -> bool;
+
+ private:
+ auto to_dictionary() const -> std::unique_ptr<io::serialize::DictionaryValue>;
+};
+
+} // namespace blender::bke
+
+inline ::BlenderProject *BKE_project_c_handle(blender::bke::BlenderProject *project)
+{
+ return reinterpret_cast<::BlenderProject *>(project);
+}
diff --git a/source/blender/blenkernel/BKE_context.h b/source/blender/blenkernel/BKE_context.h
index e1406e63ce1..512982adb7c 100644
--- a/source/blender/blenkernel/BKE_context.h
+++ b/source/blender/blenkernel/BKE_context.h
@@ -22,6 +22,7 @@ extern "C" {
struct ARegion;
struct Base;
+struct BlenderProject;
struct CacheFile;
struct Collection;
struct Depsgraph;
@@ -173,6 +174,7 @@ struct ARegion *CTX_wm_menu(const bContext *C);
struct wmGizmoGroup *CTX_wm_gizmo_group(const bContext *C);
struct wmMsgBus *CTX_wm_message_bus(const bContext *C);
struct ReportList *CTX_wm_reports(const bContext *C);
+struct BlenderProject *CTX_wm_project(void);
struct View3D *CTX_wm_view3d(const bContext *C);
struct RegionView3D *CTX_wm_region_view3d(const bContext *C);
@@ -192,6 +194,7 @@ struct SpaceUserPref *CTX_wm_space_userpref(const bContext *C);
struct SpaceClip *CTX_wm_space_clip(const bContext *C);
struct SpaceTopBar *CTX_wm_space_topbar(const bContext *C);
struct SpaceSpreadsheet *CTX_wm_space_spreadsheet(const bContext *C);
+struct SpaceProjectSettings *CTX_wm_space_project_settings(const bContext *C);
void CTX_wm_manager_set(bContext *C, struct wmWindowManager *wm);
void CTX_wm_window_set(bContext *C, struct wmWindow *win);
diff --git a/source/blender/blenkernel/BKE_preferences.h b/source/blender/blenkernel/BKE_preferences.h
index 9d33848b3d1..93119af0710 100644
--- a/source/blender/blenkernel/BKE_preferences.h
+++ b/source/blender/blenkernel/BKE_preferences.h
@@ -13,55 +13,11 @@ extern "C" {
#include "BLI_compiler_attrs.h"
struct UserDef;
-struct bUserAssetLibrary;
/** Name of the asset library added by default. Needs translation with `DATA_()` still. */
#define BKE_PREFS_ASSET_LIBRARY_DEFAULT_NAME N_("User Library")
-struct bUserAssetLibrary *BKE_preferences_asset_library_add(struct UserDef *userdef,
- const char *name,
- const char *path) ATTR_NONNULL(1);
-/**
- * Unlink and free a library preference member.
- * \note Free's \a library itself.
- */
-void BKE_preferences_asset_library_remove(struct UserDef *userdef,
- struct bUserAssetLibrary *library) ATTR_NONNULL();
-
-void BKE_preferences_asset_library_name_set(struct UserDef *userdef,
- struct bUserAssetLibrary *library,
- const char *name) ATTR_NONNULL();
-
-/**
- * Set the library path, ensuring it is pointing to a directory.
- * Single blend files can only act as "Current File" library; libraries on disk
- * should always be directories. If the path does not exist, that's fine; it can
- * created as directory if necessary later.
- */
-void BKE_preferences_asset_library_path_set(struct bUserAssetLibrary *library, const char *path)
- ATTR_NONNULL();
-
-struct bUserAssetLibrary *BKE_preferences_asset_library_find_from_index(
- const struct UserDef *userdef, int index) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
-struct bUserAssetLibrary *BKE_preferences_asset_library_find_from_name(
- const struct UserDef *userdef, const char *name) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
-
-/**
- * Return the bUserAssetLibrary that contains the given file/directory path. The given path can be
- * the library's top-level directory, or any path inside that directory.
- *
- * When more than one asset libraries match, the first matching one is returned (no smartness when
- * there nested asset libraries).
- *
- * Return NULL when no such asset library is found. */
-struct bUserAssetLibrary *BKE_preferences_asset_library_containing_path(
- const struct UserDef *userdef, const char *path) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
-
-int BKE_preferences_asset_library_get_index(const struct UserDef *userdef,
- const struct bUserAssetLibrary *library)
- ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT;
-
-void BKE_preferences_asset_library_default_add(struct UserDef *userdef) ATTR_NONNULL();
+void BKE_preferences_custom_asset_library_default_add(struct UserDef *userdef) ATTR_NONNULL();
#ifdef __cplusplus
}
diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt
index 7d43fa7e6af..b93aef01eb6 100644
--- a/source/blender/blenkernel/CMakeLists.txt
+++ b/source/blender/blenkernel/CMakeLists.txt
@@ -74,6 +74,7 @@ set(SRC
intern/asset_catalog.cc
intern/asset_catalog_path.cc
intern/asset_library.cc
+ intern/asset_library_custom.cc
intern/asset_library_service.cc
intern/attribute.cc
intern/attribute_access.cc
@@ -81,6 +82,8 @@ set(SRC
intern/autoexec.c
intern/blender.c
intern/blender_copybuffer.c
+ intern/blender_project.cc
+ intern/blender_project_settings.cc
intern/blender_undo.c
intern/blender_user_menu.c
intern/blendfile.c
@@ -324,12 +327,15 @@ set(SRC
BKE_asset_catalog_path.hh
BKE_asset_library.h
BKE_asset_library.hh
+ BKE_asset_library_custom.h
BKE_attribute.h
BKE_attribute.hh
BKE_attribute_math.hh
BKE_autoexec.h
BKE_blender.h
BKE_blender_copybuffer.h
+ BKE_blender_project.h
+ BKE_blender_project.hh
BKE_blender_undo.h
BKE_blender_user_menu.h
BKE_blender_version.h
@@ -835,6 +841,7 @@ if(WITH_GTESTS)
intern/asset_library_service_test.cc
intern/asset_library_test.cc
intern/asset_test.cc
+ intern/blender_project_test.cc
intern/bpath_test.cc
intern/cryptomatte_test.cc
intern/curves_geometry_test.cc
@@ -851,6 +858,7 @@ if(WITH_GTESTS)
)
set(TEST_INC
../editors/include
+ ../blenloader/tests
)
include(GTestTesting)
blender_add_test_lib(bf_blenkernel_tests "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB}")
diff --git a/source/blender/blenkernel/intern/asset_catalog_test.cc b/source/blender/blenkernel/intern/asset_catalog_test.cc
index ee2dd652b61..ee8b9e28117 100644
--- a/source/blender/blenkernel/intern/asset_catalog_test.cc
+++ b/source/blender/blenkernel/intern/asset_catalog_test.cc
@@ -3,7 +3,7 @@
#include "BKE_appdir.h"
#include "BKE_asset_catalog.hh"
-#include "BKE_preferences.h"
+#include "BKE_asset_library_custom.h"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
@@ -214,8 +214,8 @@ class AssetCatalogTest : public testing::Test {
BLI_path_slash_native(cdf_in_subdir.data());
/* Set up a temporary asset library for testing. */
- bUserAssetLibrary *asset_lib_pref = BKE_preferences_asset_library_add(
- &U, "Test", registered_asset_lib.c_str());
+ CustomAssetLibraryDefinition *asset_lib_pref = BKE_asset_library_custom_add(
+ &U.asset_libraries, "Test", registered_asset_lib.c_str());
ASSERT_NE(nullptr, asset_lib_pref);
ASSERT_TRUE(BLI_dir_create_recursive(asset_lib_subdir.c_str()));
@@ -266,7 +266,7 @@ class AssetCatalogTest : public testing::Test {
/* Test that the "red herring" CDF has not been touched. */
EXPECT_EQ(0, BLI_file_size(cdf_in_subdir.c_str()));
- BKE_preferences_asset_library_remove(&U, asset_lib_pref);
+ BKE_asset_library_custom_remove(&U.asset_libraries, asset_lib_pref);
}
};
diff --git a/source/blender/blenkernel/intern/asset_library.cc b/source/blender/blenkernel/intern/asset_library.cc
index b8420af1168..6a667d572a0 100644
--- a/source/blender/blenkernel/intern/asset_library.cc
+++ b/source/blender/blenkernel/intern/asset_library.cc
@@ -7,8 +7,8 @@
#include <memory>
#include "BKE_asset_library.hh"
+#include "BKE_asset_library_custom.h"
#include "BKE_main.h"
-#include "BKE_preferences.h"
#include "BLI_fileops.h"
#include "BLI_path_util.h"
@@ -53,8 +53,8 @@ bool BKE_asset_library_has_any_unsaved_catalogs()
bool BKE_asset_library_find_suitable_root_path_from_path(const char *input_path,
char *r_library_path)
{
- if (bUserAssetLibrary *preferences_lib = BKE_preferences_asset_library_containing_path(
- &U, input_path)) {
+ if (CustomAssetLibraryDefinition *preferences_lib = BKE_asset_library_custom_containing_path(
+ &U.asset_libraries, input_path)) {
BLI_strncpy(r_library_path, preferences_lib->path, FILE_MAXDIR);
return true;
}
diff --git a/source/blender/blenkernel/intern/asset_library_custom.cc b/source/blender/blenkernel/intern/asset_library_custom.cc
new file mode 100644
index 00000000000..4e9cb2ae50d
--- /dev/null
+++ b/source/blender/blenkernel/intern/asset_library_custom.cc
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ */
+
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ */
+
+#include <string.h>
+
+#include "MEM_guardedalloc.h"
+
+#include "BLI_fileops.h"
+#include "BLI_listbase.h"
+#include "BLI_path_util.h"
+#include "BLI_string.h"
+#include "BLI_string_utf8.h"
+#include "BLI_string_utils.h"
+
+#include "BKE_appdir.h"
+
+#include "BLT_translation.h"
+
+#include "DNA_asset_types.h"
+#include "DNA_userdef_types.h"
+
+#include "BKE_asset_library_custom.h"
+
+using namespace blender;
+
+/* -------------------------------------------------------------------- */
+/** \name Asset Libraries
+ * \{ */
+
+CustomAssetLibraryDefinition *BKE_asset_library_custom_add(ListBase *custom_libraries,
+ const char *name,
+ const char *path)
+{
+ CustomAssetLibraryDefinition *library = MEM_cnew<CustomAssetLibraryDefinition>(
+ "CustomAssetLibraryDefinition");
+
+ BLI_addtail(custom_libraries, library);
+
+ if (name) {
+ BKE_asset_library_custom_name_set(custom_libraries, library, name);
+ }
+ if (path) {
+ BLI_strncpy(library->path, path, sizeof(library->path));
+ }
+
+ return library;
+}
+
+void BKE_asset_library_custom_remove(ListBase *custom_libraries,
+ CustomAssetLibraryDefinition *library)
+{
+ BLI_freelinkN(custom_libraries, library);
+}
+
+void BKE_asset_library_custom_name_set(ListBase *custom_libraries,
+ CustomAssetLibraryDefinition *library,
+ const char *name)
+{
+ BLI_strncpy_utf8(library->name, name, sizeof(library->name));
+ BLI_uniquename(custom_libraries,
+ library,
+ name,
+ '.',
+ offsetof(CustomAssetLibraryDefinition, name),
+ sizeof(library->name));
+}
+
+void BKE_asset_library_custom_path_set(CustomAssetLibraryDefinition *library, const char *path)
+{
+ BLI_strncpy(library->path, path, sizeof(library->path));
+}
+
+CustomAssetLibraryDefinition *BKE_asset_library_custom_find_from_index(
+ const ListBase *custom_libraries, int index)
+{
+ return static_cast<CustomAssetLibraryDefinition *>(BLI_findlink(custom_libraries, index));
+}
+
+CustomAssetLibraryDefinition *BKE_asset_library_custom_find_from_name(
+ const ListBase *custom_libraries, const char *name)
+{
+ return static_cast<CustomAssetLibraryDefinition *>(
+ BLI_findstring(custom_libraries, name, offsetof(CustomAssetLibraryDefinition, name)));
+}
+
+CustomAssetLibraryDefinition *BKE_asset_library_custom_containing_path(
+ const ListBase *custom_libraries, const char *path)
+{
+ LISTBASE_FOREACH (CustomAssetLibraryDefinition *, asset_lib_pref, custom_libraries) {
+ if (BLI_path_contains(asset_lib_pref->path, path)) {
+ return asset_lib_pref;
+ }
+ }
+ return NULL;
+}
+
+int BKE_asset_library_custom_get_index(const ListBase *custom_libraries,
+ const CustomAssetLibraryDefinition *library)
+{
+ return BLI_findindex(custom_libraries, library);
+}
+
+/** \} */
diff --git a/source/blender/blenkernel/intern/blender_project.cc b/source/blender/blenkernel/intern/blender_project.cc
new file mode 100644
index 00000000000..b54b8f47e32
--- /dev/null
+++ b/source/blender/blenkernel/intern/blender_project.cc
@@ -0,0 +1,311 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ */
+
+#include "BKE_asset_library_custom.h"
+
+#include "BLI_path_util.h"
+#include "BLI_string.h"
+
+#include "BLI_fileops.h"
+
+#include "BKE_blender_project.h"
+#include "BKE_blender_project.hh"
+
+namespace blender::bke {
+
+BlenderProject::BlenderProject(const StringRef project_root_path,
+ std::unique_ptr<ProjectSettings> settings)
+ : settings_(std::move(settings))
+{
+ root_path_ = BlenderProject::project_path_to_native_project_root_path(project_root_path);
+ BLI_assert(root_path_.back() == SEP);
+}
+
+/* ---------------------------------------------------------------------- */
+/** \name Active project management (static storage)
+ * \{ */
+
+/* Construct on First Use idiom. */
+std::unique_ptr<BlenderProject> &BlenderProject::active_project_ptr()
+{
+ static std::unique_ptr<BlenderProject> active_;
+ return active_;
+}
+
+BlenderProject *BlenderProject::set_active(std::unique_ptr<BlenderProject> project)
+{
+ std::unique_ptr<BlenderProject> &active = active_project_ptr();
+ if (project) {
+ active = std::move(project);
+ }
+ else {
+ active = nullptr;
+ }
+
+ return active.get();
+}
+
+BlenderProject *BlenderProject::get_active()
+{
+ std::unique_ptr<BlenderProject> &active = active_project_ptr();
+ return active.get();
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Project and project settings management.
+ * \{ */
+
+std::unique_ptr<BlenderProject> BlenderProject::load_from_path(StringRef project_path)
+{
+ const StringRef project_root_path = project_root_path_find_from_path(project_path);
+
+ std::unique_ptr<bke::ProjectSettings> project_settings = bke::ProjectSettings::load_from_path(
+ project_root_path);
+ if (!project_settings) {
+ return nullptr;
+ }
+
+ return std::make_unique<BlenderProject>(project_root_path, std::move(project_settings));
+}
+
+bool BlenderProject::create_settings_directory(StringRef project_path)
+{
+ std::string project_root_path = project_path_to_native_project_root_path(project_path);
+ std::string settings_path = project_root_path_to_settings_path(project_root_path);
+
+ return BLI_dir_create_recursive(settings_path.c_str());
+}
+
+bool BlenderProject::delete_settings_directory(StringRef project_path)
+{
+ std::string project_root_path = project_path_to_native_project_root_path(project_path);
+ std::string settings_path = project_root_path_to_settings_path(project_root_path);
+
+ /* Returns 0 on success. */
+ if (BLI_delete(settings_path.c_str(), true, true)) {
+ return false;
+ }
+
+ BlenderProject *active_project = get_active();
+ if (active_project &&
+ BLI_path_cmp_normalized(project_root_path.c_str(), active_project->root_path().c_str())) {
+ active_project->settings_->tag_has_unsaved_changes();
+ }
+ return true;
+}
+
+bool BlenderProject::delete_settings_directory()
+{
+ if (!delete_settings_directory(root_path_)) {
+ return false;
+ }
+
+ settings_->tag_has_unsaved_changes();
+ return true;
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Simple getters & setters
+ * \{ */
+
+StringRefNull BlenderProject::root_path() const
+{
+ return root_path_;
+}
+
+ProjectSettings &BlenderProject::get_settings() const
+{
+ BLI_assert(settings_ != nullptr);
+ return *settings_;
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name Path stuff
+ * \{ */
+
+StringRef BlenderProject::project_root_path_find_from_path(StringRef path)
+{
+ /* There are two versions of the path used here: One copy that is converted to native slashes,
+ * and the unmodified original path from the input. */
+
+ std::string path_native = path;
+ BLI_path_slash_native(path_native.data());
+
+ StringRef cur_path = path;
+
+ while (cur_path.size()) {
+ std::string cur_path_native = StringRef(path_native.c_str(), cur_path.size());
+ if (path_is_project_root(cur_path_native)) {
+ return path.substr(0, cur_path.size());
+ }
+
+ /* Walk "up the path" (check the parent next). */
+ const int64_t pos_last_slash = cur_path_native.find_last_of(SEP);
+ if (pos_last_slash == StringRef::not_found) {
+ break;
+ }
+ cur_path = cur_path.substr(0, pos_last_slash);
+ }
+
+ return "";
+}
+
+static StringRef path_strip_trailing_native_slash(StringRef path)
+{
+ const int64_t pos_before_trailing_slash = path.find_last_not_of(SEP);
+ return (pos_before_trailing_slash == StringRef::not_found) ?
+ path :
+ path.substr(0, pos_before_trailing_slash + 1);
+}
+
+bool BlenderProject::path_is_project_root(StringRef path)
+{
+ path = path_strip_trailing_native_slash(path);
+ return BLI_exists(std::string(path + SEP_STR + SETTINGS_DIRNAME).c_str());
+}
+
+std::string BlenderProject::project_path_to_native_project_root_path(StringRef project_path)
+{
+ std::string project_path_native = project_path;
+ BLI_path_slash_native(project_path_native.data());
+
+ const StringRef path_no_trailing_slashes = path_strip_trailing_native_slash(project_path_native);
+ if (path_no_trailing_slashes.endswith(SETTINGS_DIRNAME)) {
+ return StringRef(path_no_trailing_slashes).drop_suffix(SETTINGS_DIRNAME.size());
+ }
+
+ return std::string(path_no_trailing_slashes) + SEP;
+}
+
+std::string BlenderProject::project_root_path_to_settings_path(StringRef project_root_path)
+{
+ BLI_assert(project_root_path.back() == SEP);
+ return project_root_path + SETTINGS_DIRNAME + SEP;
+}
+
+std::string BlenderProject::project_root_path_to_settings_filepath(StringRef project_root_path)
+{
+ BLI_assert(project_root_path.back() == SEP);
+ return project_root_path_to_settings_path(project_root_path) + SETTINGS_FILENAME;
+}
+
+/** \} */
+
+} // namespace blender::bke
+
+/* ---------------------------------------------------------------------- */
+/** \name C-API
+ * \{ */
+
+using namespace blender;
+
+bool BKE_project_create_settings_directory(const char *project_root_path)
+{
+ return bke::BlenderProject::create_settings_directory(project_root_path);
+}
+
+bool BKE_project_delete_settings_directory(BlenderProject *project_handle)
+{
+ bke::BlenderProject *project = reinterpret_cast<bke::BlenderProject *>(project_handle);
+ return project->delete_settings_directory();
+}
+
+BlenderProject *BKE_project_active_get(void)
+{
+ return reinterpret_cast<BlenderProject *>(bke::BlenderProject::get_active());
+}
+
+void BKE_project_active_unset(void)
+{
+ bke::BlenderProject::set_active(nullptr);
+}
+
+bool BKE_project_is_path_project_root(const char *path)
+{
+ return bke::BlenderProject::path_is_project_root(path);
+}
+
+bool BKE_project_contains_path(const char *path)
+{
+ const StringRef found_root_path = bke::BlenderProject::project_root_path_find_from_path(path);
+ return !found_root_path.is_empty();
+}
+
+BlenderProject *BKE_project_active_load_from_path(const char *path)
+{
+ /* Project should be unset if the path doesn't contain a project root. Unset in the beginning so
+ * early exiting behaves correctly. */
+ BKE_project_active_unset();
+
+ std::unique_ptr<bke::BlenderProject> project = bke::BlenderProject::load_from_path(path);
+
+ return reinterpret_cast<::BlenderProject *>(bke::BlenderProject::set_active(std::move(project)));
+}
+
+bool BKE_project_settings_save(const BlenderProject *project_handle)
+{
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ bke::ProjectSettings &settings = project->get_settings();
+ return settings.save_to_disk(project->root_path());
+}
+
+const char *BKE_project_root_path_get(const BlenderProject *project_handle)
+{
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ return project->root_path().c_str();
+}
+
+void BKE_project_name_set(const BlenderProject *project_handle, const char *name)
+{
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ project->get_settings().project_name(name);
+}
+
+const char *BKE_project_name_get(const BlenderProject *project_handle)
+{
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ return project->get_settings().project_name().c_str();
+}
+
+ListBase *BKE_project_custom_asset_libraries_get(const BlenderProject *project_handle)
+{
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ bke::ProjectSettings &settings = project->get_settings();
+ return &settings.asset_library_definitions();
+}
+
+void BKE_project_tag_has_unsaved_changes(const BlenderProject *project_handle)
+{
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ bke::ProjectSettings &settings = project->get_settings();
+ settings.tag_has_unsaved_changes();
+}
+
+bool BKE_project_has_unsaved_changes(const BlenderProject *project_handle)
+{
+ if (!project_handle) {
+ return false;
+ }
+
+ const bke::BlenderProject *project = reinterpret_cast<const bke::BlenderProject *>(
+ project_handle);
+ const bke::ProjectSettings &settings = project->get_settings();
+ return settings.has_unsaved_changes();
+}
+
+/** \} */
diff --git a/source/blender/blenkernel/intern/blender_project_settings.cc b/source/blender/blenkernel/intern/blender_project_settings.cc
new file mode 100644
index 00000000000..9c55d050283
--- /dev/null
+++ b/source/blender/blenkernel/intern/blender_project_settings.cc
@@ -0,0 +1,318 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup bke
+ */
+
+#include <fstream>
+#include <memory>
+#include <string>
+
+#include "BLI_fileops.h"
+#include "BLI_listbase.h"
+#include "BLI_serialize.hh"
+
+#include "BKE_asset_library_custom.h"
+
+#include "DNA_asset_types.h"
+
+#include "BKE_blender_project.hh"
+
+namespace serialize = blender::io::serialize;
+
+namespace blender::bke {
+
+/* ---------------------------------------------------------------------- */
+
+struct CustomAssetLibraries : NonCopyable {
+ ListBase asset_libraries = {nullptr, nullptr}; /* CustomAssetLibraryDefinition */
+
+ CustomAssetLibraries() = default;
+ CustomAssetLibraries(ListBase asset_libraries);
+ CustomAssetLibraries(CustomAssetLibraries &&);
+ ~CustomAssetLibraries();
+ auto operator=(CustomAssetLibraries &&) -> CustomAssetLibraries &;
+};
+
+CustomAssetLibraries::CustomAssetLibraries(ListBase asset_libraries)
+ : asset_libraries(asset_libraries)
+{
+}
+
+CustomAssetLibraries::CustomAssetLibraries(CustomAssetLibraries &&other)
+{
+ *this = std::move(other);
+}
+
+CustomAssetLibraries &CustomAssetLibraries::operator=(CustomAssetLibraries &&other)
+{
+ asset_libraries = other.asset_libraries;
+ BLI_listbase_clear(&other.asset_libraries);
+ return *this;
+}
+
+CustomAssetLibraries::~CustomAssetLibraries()
+{
+ LISTBASE_FOREACH_MUTABLE (CustomAssetLibraryDefinition *, library, &asset_libraries) {
+ BKE_asset_library_custom_remove(&asset_libraries, library);
+ }
+}
+
+/* ---------------------------------------------------------------------- */
+/** \name settings.json Reading (Deserializing)
+ * \{ */
+
+struct ExtractedSettings {
+ std::string project_name;
+ ListBase asset_libraries = {nullptr, nullptr}; /* CustomAssetLibraryDefinition */
+};
+
+static std::unique_ptr<serialize::Value> read_settings_file(StringRef settings_filepath)
+{
+ std::ifstream is;
+ is.open(settings_filepath);
+ if (is.fail()) {
+ return nullptr;
+ }
+
+ serialize::JsonFormatter formatter;
+ /* Will not be a dictionary in case of error (corrupted file). */
+ std::unique_ptr<serialize::Value> deserialized_values = formatter.deserialize(is);
+ is.close();
+
+ if (deserialized_values->type() != serialize::eValueType::Dictionary) {
+ return nullptr;
+ }
+
+ return deserialized_values;
+}
+
+static std::unique_ptr<ExtractedSettings> extract_settings(
+ const serialize::DictionaryValue &dictionary)
+{
+ using namespace serialize;
+
+ std::unique_ptr extracted_settings = std::make_unique<ExtractedSettings>();
+
+ const DictionaryValue::Lookup attributes = dictionary.create_lookup();
+
+ /* "project": */ {
+ const DictionaryValue::LookupValue *project_value = attributes.lookup_ptr("project");
+ BLI_assert(project_value != nullptr);
+
+ const DictionaryValue *project_dict = (*project_value)->as_dictionary_value();
+ const StringValue *project_name_value =
+ project_dict->create_lookup().lookup("name")->as_string_value();
+ if (project_name_value) {
+ extracted_settings->project_name = project_name_value->value();
+ }
+ }
+ /* "asset_libraries": */ {
+ const DictionaryValue::LookupValue *asset_libraries_value = attributes.lookup_ptr(
+ "asset_libraries");
+ if (asset_libraries_value) {
+ const ArrayValue *asset_libraries_array = (*asset_libraries_value)->as_array_value();
+ if (!asset_libraries_array) {
+ throw std::runtime_error(
+ "Unexpected asset_library format in settings.json, expected array");
+ }
+
+ for (const ArrayValue::Item &element : asset_libraries_array->elements()) {
+ const DictionaryValue *object_value = element->as_dictionary_value();
+ if (!object_value) {
+ throw std::runtime_error(
+ "Unexpected asset_library entry in settings.json, expected dictionary entries only");
+ }
+ const DictionaryValue::Lookup element_lookup = object_value->create_lookup();
+ const DictionaryValue::LookupValue *name_value = element_lookup.lookup_ptr("name");
+ if (name_value && (*name_value)->type() != eValueType::String) {
+ throw std::runtime_error(
+ "Unexpected asset_library entry in settings.json, expected name to be string");
+ }
+ const DictionaryValue::LookupValue *path_value = element_lookup.lookup_ptr("path");
+ if (path_value && (*path_value)->type() != eValueType::String) {
+ throw std::runtime_error(
+ "Unexpected asset_library entry in settings.json, expected path to be string");
+ }
+
+ /* TODO this isn't really extracting, should be creating data from the settings be a
+ * separate step? */
+ CustomAssetLibraryDefinition *library = BKE_asset_library_custom_add(
+ &extracted_settings->asset_libraries);
+ /* Name or path may not be set, this is fine. */
+ if (name_value) {
+ std::string name = (*name_value)->as_string_value()->value();
+ BKE_asset_library_custom_name_set(
+ &extracted_settings->asset_libraries, library, name.c_str());
+ }
+ if (path_value) {
+ std::string path = (*path_value)->as_string_value()->value();
+ BKE_asset_library_custom_path_set(library, path.c_str());
+ }
+ }
+ }
+ }
+
+ return extracted_settings;
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+/** \name settings.json Writing (Serializing)
+ * \{ */
+
+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();
+
+ /* "project": */ {
+ 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));
+ }
+ /* "asset_libraries": */ {
+ if (!BLI_listbase_is_empty(&asset_libraries_->asset_libraries)) {
+ std::unique_ptr<ArrayValue> asset_libs_array = std::make_unique<ArrayValue>();
+ ArrayValue::Items &asset_libs_elements = asset_libs_array->elements();
+ LISTBASE_FOREACH (
+ const CustomAssetLibraryDefinition *, library, &asset_libraries_->asset_libraries) {
+ std::unique_ptr<DictionaryValue> library_dict = std::make_unique<DictionaryValue>();
+ DictionaryValue::Items &library_attributes = library_dict->elements();
+
+ library_attributes.append_as("name", new StringValue(library->name));
+ library_attributes.append_as("path", new StringValue(library->path));
+ asset_libs_elements.append_as(std::move(library_dict));
+ }
+ root_attributes.append_as("asset_libraries", std::move(asset_libs_array));
+ }
+ }
+
+ 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();
+}
+
+/** \} */
+
+/* ---------------------------------------------------------------------- */
+
+std::unique_ptr<ProjectSettings> ProjectSettings::load_from_disk(StringRef project_path)
+{
+ const std::string project_root_path = BlenderProject::project_path_to_native_project_root_path(
+ project_path);
+
+ if (!BLI_exists(project_root_path.c_str())) {
+ return nullptr;
+ }
+ if (!BlenderProject::path_is_project_root(project_root_path.c_str())) {
+ return nullptr;
+ }
+
+ const std::string settings_filepath = BlenderProject::project_root_path_to_settings_filepath(
+ project_root_path);
+ std::unique_ptr<serialize::Value> values = read_settings_file(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>();
+ if (extracted_settings) {
+ loaded_settings->project_name_ = extracted_settings->project_name;
+ /* Moves ownership. */
+ loaded_settings->asset_libraries_ = std::make_unique<CustomAssetLibraries>(
+ extracted_settings->asset_libraries);
+ }
+
+ return loaded_settings;
+}
+
+std::unique_ptr<ProjectSettings> ProjectSettings::load_from_path(StringRef path)
+{
+ StringRef project_root = BlenderProject::project_root_path_find_from_path(path);
+ if (project_root.is_empty()) {
+ return nullptr;
+ }
+
+ return bke::ProjectSettings::load_from_disk(project_root);
+}
+
+bool ProjectSettings::save_to_disk(StringRef project_path)
+{
+ const std::string project_root_path = BlenderProject::project_path_to_native_project_root_path(
+ project_path);
+
+ if (!BLI_exists(project_root_path.c_str())) {
+ return false;
+ }
+ if (!BlenderProject::path_is_project_root(project_root_path.c_str())) {
+ return false;
+ }
+
+ const std::string settings_filepath = BlenderProject::project_root_path_to_settings_filepath(
+ project_root_path);
+ std::unique_ptr settings_as_dict = to_dictionary();
+ write_settings_file(settings_filepath, std::move(settings_as_dict));
+
+ has_unsaved_changes_ = false;
+
+ return true;
+}
+
+/* ---------------------------------------------------------------------- */
+
+ProjectSettings::ProjectSettings() : asset_libraries_(std::make_unique<CustomAssetLibraries>())
+{
+}
+
+ProjectSettings::~ProjectSettings() = default;
+
+void ProjectSettings::project_name(StringRef new_name)
+{
+ project_name_ = new_name;
+ has_unsaved_changes_ = true;
+}
+
+StringRefNull ProjectSettings::project_name() const
+{
+ return project_name_;
+}
+
+const ListBase &ProjectSettings::asset_library_definitions() const
+{
+ return asset_libraries_->asset_libraries;
+}
+
+ListBase &ProjectSettings::asset_library_definitions()
+{
+ return asset_libraries_->asset_libraries;
+}
+
+void ProjectSettings::tag_has_unsaved_changes()
+{
+ has_unsaved_changes_ = true;
+}
+
+bool ProjectSettings::has_unsaved_changes() const
+{
+ return has_unsaved_changes_;
+}
+
+} // namespace blender::bke
diff --git a/source/blender/blenkernel/intern/blender_project_test.cc b/source/blender/blenkernel/intern/blender_project_test.cc
new file mode 100644
index 00000000000..e4daf062d8c
--- /dev/null
+++ b/source/blender/blenkernel/intern/blender_project_test.cc
@@ -0,0 +1,325 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright 2022 Blender Foundation. All rights reserved. */
+
+#include "BKE_appdir.h"
+#include "BKE_blender_project.h"
+#include "BKE_blender_project.hh"
+#include "BKE_main.h"
+
+#include "BLI_fileops.h"
+#include "BLI_function_ref.hh"
+#include "BLI_path_util.h"
+#include "BLI_vector.hh"
+
+#include "BLO_readfile.h"
+
+#include "blendfile_loading_base_test.h"
+
+#include "testing/testing.h"
+
+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 {
+ std::string project_path_;
+ /* Path with OS preferred slashes ('/' on Unix, '\' on Windows). Important for some file
+ * operations. */
+ std::string native_project_path_;
+ std::string base_path_;
+
+ ProjectDirectoryRAIIWrapper(StringRefNull base_path, StringRefNull relative_project_path)
+ {
+ BLI_assert_msg(ELEM(base_path.back(), SEP, ALTSEP),
+ "Expected base_path to have trailing slash");
+ std::string project_path = base_path + relative_project_path;
+
+ 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. */
+ if (BLI_exists(native_project_path_.c_str())) {
+ throw std::runtime_error("Can't execute test, temporary path '" + project_path +
+ "' already exists");
+ }
+
+ BLI_dir_create_recursive(native_project_path_.c_str());
+ if (!BLI_exists(native_project_path_.c_str())) {
+ throw std::runtime_error("Can't execute test, failed to create path '" +
+ native_project_path_ + "'");
+ }
+
+ base_path_ = base_path;
+ project_path_ = project_path;
+ BLI_assert(StringRef{&project_path_[base_path.size()]} == relative_project_path);
+ }
+
+ ~ProjectDirectoryRAIIWrapper()
+ {
+ if (!project_path_.empty()) {
+ /* Cut the path off at the first slash after the base path, so we delete the directory
+ * created for the test. */
+ const size_t first_slash_pos = native_project_path_.find_first_of(SEP, base_path_.size());
+ std::string path_to_delete = native_project_path_;
+ if (first_slash_pos != std::string::npos) {
+ path_to_delete.erase(first_slash_pos);
+ }
+ BLI_delete(path_to_delete.c_str(), true, true);
+ BLI_assert(!BLI_exists(native_project_path_.c_str()));
+ }
+ }
+ };
+
+ public:
+ /* Run the test on multiple paths or variations of the same path. Useful to test things like
+ * unicode paths, with or without trailing slash, non-native slashes, etc. The callback gets both
+ * the unmodified path (possibly with non-native slashes), and the path converted to native
+ * slashes passed. Call functions under test with the former, and use the latter to check the
+ * results with BLI_fileops.h functions */
+ void test_foreach_project_path(FunctionRef<void(StringRefNull /* project_path */,
+ StringRefNull /* project_path_native */)> fn)
+ {
+ const Vector<StringRefNull> subpaths = {
+ "temporary-project-root",
+ "test-temporary-unicode-dir-новый/temporary-project-root",
+ /* Same but with trailing slash. */
+ "test-temporary-unicode-dir-новый/temporary-project-root/",
+ /* Windows style slash. */
+ "test-temporary-unicode-dir-новый\\temporary-project-root",
+ /* Windows style trailing slash. */
+ "test-temporary-unicode-dir-новый\\temporary-project-root\\",
+ };
+
+ BKE_tempdir_init("");
+
+ const std::string tempdir = BKE_tempdir_session();
+ for (StringRefNull subpath : subpaths) {
+ ProjectDirectoryRAIIWrapper temp_project_path(tempdir, subpath);
+ fn(temp_project_path.project_path_, temp_project_path.native_project_path_);
+ }
+ }
+
+ void TearDown() override
+ {
+ BKE_project_active_unset();
+ }
+};
+
+TEST_F(ProjectTest, settings_create)
+{
+ test_foreach_project_path([](StringRefNull project_path, StringRefNull project_path_native) {
+ if (!BlenderProject::create_settings_directory(project_path)) {
+ /* Not a regular test failure, this may fail if there is a permission issue for example. */
+ FAIL() << "Failed to create project directory in '" << project_path
+ << "', check permissions";
+ }
+ std::string project_settings_dir = project_path_native + SEP_STR +
+ BlenderProject::SETTINGS_DIRNAME;
+ EXPECT_TRUE(BLI_exists(project_settings_dir.c_str()) &&
+ BLI_is_dir(project_settings_dir.c_str()))
+ << project_settings_dir + " was not created";
+ });
+}
+
+/* Load the project by pointing to the project root directory (as opposed to the .blender_project
+ * directory). */
+TEST_F(ProjectTest, load_from_project_root_path)
+{
+ test_foreach_project_path([](StringRefNull project_path, StringRefNull project_path_native) {
+ BlenderProject::create_settings_directory(project_path);
+
+ std::unique_ptr project = BlenderProject::load_from_path(project_path);
+ EXPECT_NE(project, nullptr);
+ EXPECT_EQ(project->root_path(), project_path_native);
+ EXPECT_EQ(project->get_settings().project_name(), "");
+ });
+}
+
+/* Load the project by pointing to the .blender_project directory (as opposed to the project root
+ * directory). */
+TEST_F(ProjectTest, load_from_project_settings_path)
+{
+ test_foreach_project_path([](StringRefNull project_path, StringRefNull project_path_native) {
+ BlenderProject::create_settings_directory(project_path);
+
+ std::unique_ptr project = BlenderProject::load_from_path(
+ project_path + (ELEM(project_path.back(), SEP, ALTSEP) ? "" : SEP_STR) +
+ BlenderProject::SETTINGS_DIRNAME);
+ EXPECT_NE(project, nullptr);
+ EXPECT_EQ(project->root_path(), project_path_native);
+ EXPECT_EQ(project->get_settings().project_name(), "");
+ });
+}
+
+static void project_settings_match_expected_from_svn(const ProjectSettings &project_settings)
+{
+ EXPECT_EQ(project_settings.project_name(), "Ružena");
+
+ const ListBase &asset_libraries = project_settings.asset_library_definitions();
+ CustomAssetLibraryDefinition *first = (CustomAssetLibraryDefinition *)asset_libraries.first;
+ EXPECT_STREQ(first->name, "Lorem Ipsum");
+ EXPECT_STREQ(first->path, "assets");
+ EXPECT_EQ(first->next, asset_libraries.last);
+ CustomAssetLibraryDefinition *last = (CustomAssetLibraryDefinition *)asset_libraries.last;
+ EXPECT_EQ(last->prev, asset_libraries.first);
+ EXPECT_STREQ(last->name, "Материалы");
+ EXPECT_STREQ(last->path, "новый\\assets");
+}
+
+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);
+ project_settings_match_expected_from_svn(*from_project_settings);
+}
+
+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) {
+ BlenderProject::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);
+ project_settings_match_expected_from_svn(*written_settings);
+ });
+}
+
+TEST_F(ProjectTest, settings_read_change_write)
+{
+ SVNFiles svn_files{};
+ std::unique_ptr from_project_settings = ProjectSettings::load_from_disk(svn_files.project_root);
+
+ EXPECT_FALSE(from_project_settings->has_unsaved_changes());
+
+ /* 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) {
+ BlenderProject::create_settings_directory(to_project_path);
+
+ from_project_settings->project_name("новый");
+ EXPECT_TRUE(from_project_settings->has_unsaved_changes());
+
+ if (!from_project_settings->save_to_disk(to_project_path)) {
+ FAIL();
+ }
+ EXPECT_FALSE(from_project_settings->has_unsaved_changes());
+
+ /* 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(), "новый");
+ EXPECT_FALSE(from_project_settings->has_unsaved_changes());
+ });
+}
+
+TEST_F(ProjectTest, project_root_path_find_from_path)
+{
+ /* Test the temporarily created directories with their various path formats. */
+ test_foreach_project_path([](StringRefNull project_path, StringRefNull /*project_path_native*/) {
+ /* First test without a .blender_project directory present. */
+ EXPECT_EQ(BlenderProject::project_root_path_find_from_path(project_path), "");
+
+ BlenderProject::create_settings_directory(project_path);
+ EXPECT_EQ(BlenderProject::project_root_path_find_from_path(project_path), project_path);
+ });
+
+ SVNFiles svn_files{};
+
+ /* Test the prepared project directory from the libs SVN repository. */
+ EXPECT_EQ(BlenderProject::project_root_path_find_from_path(svn_files.project_root +
+ "/some_project_file.blend"),
+ svn_files.project_root);
+ EXPECT_EQ(BlenderProject::project_root_path_find_from_path(
+ svn_files.project_root +
+ "/unicode_subdirectory_новый/another_subdirectory/some_nested_project_file.blend"),
+ svn_files.project_root);
+}
+
+class BlendfileProjectLoadingTest : public BlendfileLoadingBaseTest {
+};
+
+/* Test if loading the blend file loads the project data as expected. */
+TEST_F(BlendfileProjectLoadingTest, load_blend_file)
+{
+ EXPECT_EQ(BKE_project_active_get(), nullptr);
+
+ if (!blendfile_load("blender_project/the_project/some_project_file.blend")) {
+ FAIL();
+ }
+
+ ::BlenderProject *svn_project = BKE_project_active_load_from_path(bfile->main->filepath);
+ EXPECT_NE(svn_project, nullptr);
+ EXPECT_EQ(BKE_project_active_get(), svn_project);
+ EXPECT_STREQ("Ružena", BKE_project_name_get(svn_project));
+ /* Note: The project above will be freed once a different active project is set. So get the path
+ * for future comparisons. */
+ std::string svn_project_path = BKE_project_root_path_get(svn_project);
+
+ blendfile_free();
+
+ /* Check if loading a different .blend updates the project properly */
+ if (!blendfile_load("blender_project/the_project/unicode_subdirectory_новый/"
+ "another_subdirectory/some_nested_project_file.blend")) {
+ FAIL();
+ }
+ ::BlenderProject *svn_project_from_nested = BKE_project_active_load_from_path(
+ bfile->main->filepath);
+ EXPECT_NE(svn_project_from_nested, nullptr);
+ EXPECT_EQ(BKE_project_active_get(), svn_project_from_nested);
+ EXPECT_STREQ(svn_project_path.c_str(), BKE_project_root_path_get(svn_project_from_nested));
+ EXPECT_STREQ("Ružena", BKE_project_name_get(svn_project_from_nested));
+ blendfile_free();
+
+ /* Check if loading a .blend that's not in the project unsets the project properly. */
+ if (!blendfile_load("blender_project/not_a_project_file.blend")) {
+ FAIL();
+ }
+ BKE_project_active_load_from_path(bfile->main->filepath);
+ EXPECT_EQ(BKE_project_active_get(), nullptr);
+}
+
+TEST_F(ProjectTest, project_load_and_delete)
+{
+ test_foreach_project_path([](StringRefNull project_path, StringRefNull project_path_native) {
+ BlenderProject::create_settings_directory(project_path);
+
+ ::BlenderProject *project = BKE_project_active_load_from_path(project_path.c_str());
+ EXPECT_NE(project, nullptr);
+ EXPECT_FALSE(BKE_project_has_unsaved_changes(project));
+
+ std::string project_settings_dir = project_path_native + SEP_STR +
+ BlenderProject::SETTINGS_DIRNAME;
+
+ EXPECT_TRUE(BLI_exists(project_settings_dir.c_str()));
+ if (!BKE_project_delete_settings_directory(project)) {
+ FAIL();
+ }
+ /* Runtime project should still exist, but with unsaved changes. */
+ EXPECT_NE(project, nullptr);
+ EXPECT_TRUE(BKE_project_has_unsaved_changes(project));
+ EXPECT_FALSE(BLI_exists(project_settings_dir.c_str()));
+ });
+}
+
+} // namespace blender::bke::tests
diff --git a/source/blender/blenkernel/intern/blendfile.c b/source/blender/blenkernel/intern/blendfile.c
index 8b0d3f2e92e..f625f06d4d0 100644
--- a/source/blender/blenkernel/intern/blendfile.c
+++ b/source/blender/blenkernel/intern/blendfile.c
@@ -30,6 +30,7 @@
#include "BKE_addon.h"
#include "BKE_appdir.h"
#include "BKE_blender.h"
+#include "BKE_blender_project.h"
#include "BKE_blender_version.h"
#include "BKE_blendfile.h"
#include "BKE_bpath.h"
@@ -445,6 +446,16 @@ static void setup_app_blend_file_data(bContext *C,
}
}
+static void setup_app_project_data(BlendFileData *bfd, const struct BlendFileReadParams *params)
+{
+ if (!U.experimental.use_blender_projects) {
+ return;
+ }
+ if ((params->skip_flags & BLO_READ_SKIP_DATA) == 0) {
+ BKE_project_active_load_from_path(bfd->main->filepath);
+ }
+}
+
static void handle_subversion_warning(Main *main, BlendFileReadReport *reports)
{
if (main->minversionfile > BLENDER_FILE_VERSION ||
@@ -471,6 +482,7 @@ void BKE_blendfile_read_setup_ex(bContext *C,
BLO_update_defaults_startup_blend(bfd->main, startup_app_template);
}
}
+ setup_app_project_data(bfd, params);
setup_app_blend_file_data(C, bfd, params, reports);
BLO_blendfiledata_free(bfd);
}
@@ -682,7 +694,7 @@ UserDef *BKE_blendfile_userdef_from_defaults(void)
/* Default studio light. */
BKE_studiolight_default(userdef->light_param, userdef->light_ambient);
- BKE_preferences_asset_library_default_add(userdef);
+ BKE_preferences_custom_asset_library_default_add(userdef);
return userdef;
}
diff --git a/source/blender/blenkernel/intern/context.c b/source/blender/blenkernel/intern/context.c
index 1d6092849cc..ed6f55cc8f9 100644
--- a/source/blender/blenkernel/intern/context.c
+++ b/source/blender/blenkernel/intern/context.c
@@ -30,6 +30,7 @@
#include "BLT_translation.h"
+#include "BKE_blender_project.h"
#include "BKE_context.h"
#include "BKE_layer.h"
#include "BKE_main.h"
@@ -782,6 +783,11 @@ struct ReportList *CTX_wm_reports(const bContext *C)
return NULL;
}
+BlenderProject *CTX_wm_project(void)
+{
+ return BKE_project_active_get();
+}
+
View3D *CTX_wm_view3d(const bContext *C)
{
ScrArea *area = CTX_wm_area(C);
@@ -948,6 +954,15 @@ struct SpaceSpreadsheet *CTX_wm_space_spreadsheet(const bContext *C)
return NULL;
}
+SpaceProjectSettings *CTX_wm_space_project_settings(const bContext *C)
+{
+ ScrArea *area = CTX_wm_area(C);
+ if (area && area->spacetype == SPACE_PROJECT_SETTINGS) {
+ return area->spacedata.first;
+ }
+ return NULL;
+}
+
void CTX_wm_manager_set(bContext *C, wmWindowManager *wm)
{
C->wm.manager = wm;
diff --git a/source/blender/blenkernel/intern/preferences.c b/source/blender/blenkernel/intern/preferences.c
index dd76f9eddc1..fa894529f97 100644
--- a/source/blender/blenkernel/intern/preferences.c
+++ b/source/blender/blenkernel/intern/preferences.c
@@ -2,26 +2,17 @@
/** \file
* \ingroup bke
- *
- * User defined asset library API.
*/
-#include <string.h>
-
-#include "MEM_guardedalloc.h"
-
-#include "BLI_fileops.h"
-#include "BLI_listbase.h"
#include "BLI_path_util.h"
-#include "BLI_string.h"
-#include "BLI_string_utf8.h"
-#include "BLI_string_utils.h"
#include "BKE_appdir.h"
+#include "BKE_asset_library_custom.h"
#include "BKE_preferences.h"
#include "BLT_translation.h"
+#include "DNA_asset_types.h"
#include "DNA_userdef_types.h"
#define U BLI_STATIC_ASSERT(false, "Global 'U' not allowed, only use arguments passed in!")
@@ -30,79 +21,7 @@
/** \name Asset Libraries
* \{ */
-bUserAssetLibrary *BKE_preferences_asset_library_add(UserDef *userdef,
- const char *name,
- const char *path)
-{
- bUserAssetLibrary *library = MEM_callocN(sizeof(*library), "bUserAssetLibrary");
-
- BLI_addtail(&userdef->asset_libraries, library);
-
- if (name) {
- BKE_preferences_asset_library_name_set(userdef, library, name);
- }
- if (path) {
- BLI_strncpy(library->path, path, sizeof(library->path));
- }
-
- return library;
-}
-
-void BKE_preferences_asset_library_remove(UserDef *userdef, bUserAssetLibrary *library)
-{
- BLI_freelinkN(&userdef->asset_libraries, library);
-}
-
-void BKE_preferences_asset_library_name_set(UserDef *userdef,
- bUserAssetLibrary *library,
- const char *name)
-{
- BLI_strncpy_utf8(library->name, name, sizeof(library->name));
- BLI_uniquename(&userdef->asset_libraries,
- library,
- name,
- '.',
- offsetof(bUserAssetLibrary, name),
- sizeof(library->name));
-}
-
-void BKE_preferences_asset_library_path_set(bUserAssetLibrary *library, const char *path)
-{
- BLI_strncpy(library->path, path, sizeof(library->path));
- if (BLI_is_file(library->path)) {
- BLI_path_parent_dir(library->path);
- }
-}
-
-bUserAssetLibrary *BKE_preferences_asset_library_find_from_index(const UserDef *userdef, int index)
-{
- return BLI_findlink(&userdef->asset_libraries, index);
-}
-
-bUserAssetLibrary *BKE_preferences_asset_library_find_from_name(const UserDef *userdef,
- const char *name)
-{
- return BLI_findstring(&userdef->asset_libraries, name, offsetof(bUserAssetLibrary, name));
-}
-
-bUserAssetLibrary *BKE_preferences_asset_library_containing_path(const UserDef *userdef,
- const char *path)
-{
- LISTBASE_FOREACH (bUserAssetLibrary *, asset_lib_pref, &userdef->asset_libraries) {
- if (BLI_path_contains(asset_lib_pref->path, path)) {
- return asset_lib_pref;
- }
- }
- return NULL;
-}
-
-int BKE_preferences_asset_library_get_index(const UserDef *userdef,
- const bUserAssetLibrary *library)
-{
- return BLI_findindex(&userdef->asset_libraries, library);
-}
-
-void BKE_preferences_asset_library_default_add(UserDef *userdef)
+void BKE_preferences_custom_asset_library_default_add(UserDef *userdef)
{
char documents_path[FILE_MAXDIR];
@@ -111,8 +30,8 @@ void BKE_preferences_asset_library_default_add(UserDef *userdef)
return;
}
- bUserAssetLibrary *library = BKE_preferences_asset_library_add(
- userdef, DATA_(BKE_PREFS_ASSET_LIBRARY_DEFAULT_NAME), NULL);
+ CustomAssetLibraryDefinition *library = BKE_asset_library_custom_add(
+ &userdef->asset_libraries, DATA_(BKE_PREFS_ASSET_LIBRARY_DEFAULT_NAME), NULL);
/* Add new "Default" library under '[doc_path]/Blender/Assets'. */
BLI_path_join(library->path, sizeof(library->path), documents_path, N_("Blender"), N_("Assets"));