diff options
Diffstat (limited to 'source/blender/blenkernel')
-rw-r--r-- | source/blender/blenkernel/BKE_asset_library_custom.h | 69 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_blender_project.h | 75 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_blender_project.hh | 204 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_context.h | 3 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_preferences.h | 46 | ||||
-rw-r--r-- | source/blender/blenkernel/CMakeLists.txt | 8 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/asset_catalog_test.cc | 8 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/asset_library.cc | 6 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/asset_library_custom.cc | 112 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/blender_project.cc | 311 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/blender_project_settings.cc | 318 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/blender_project_test.cc | 325 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/blendfile.c | 14 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/context.c | 15 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/preferences.c | 91 |
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")); |