diff options
Diffstat (limited to 'source/blender')
65 files changed, 3020 insertions, 336 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")); diff --git a/source/blender/blenloader/intern/versioning_userdef.c b/source/blender/blenloader/intern/versioning_userdef.c index b4890131861..4b7375bc12b 100644 --- a/source/blender/blenloader/intern/versioning_userdef.c +++ b/source/blender/blenloader/intern/versioning_userdef.c @@ -22,6 +22,7 @@ #include "DNA_windowmanager_types.h" #include "BKE_addon.h" +#include "BKE_asset_library_custom.h" #include "BKE_blender_version.h" #include "BKE_colorband.h" #include "BKE_idprop.h" @@ -98,6 +99,7 @@ static void do_versions_theme(const UserDef *userdef, bTheme *btheme) */ { /* Keep this block, even when empty. */ + btheme->space_project_settings = btheme->space_preferences; } #undef FROM_DEFAULT_V4_UCHAR @@ -678,7 +680,7 @@ void blo_do_versions_userdef(UserDef *userdef) if (!USER_VERSION_ATLEAST(292, 9)) { if (BLI_listbase_is_empty(&userdef->asset_libraries)) { - BKE_preferences_asset_library_default_add(userdef); + BKE_preferences_custom_asset_library_default_add(userdef); } } @@ -731,11 +733,11 @@ void blo_do_versions_userdef(UserDef *userdef) * since it doesn't handle translations and ignores user changes. But this was an alpha build * (experimental) feature and the name is just for display in the UI anyway. So it doesn't have * to work perfectly at all. */ - LISTBASE_FOREACH (bUserAssetLibrary *, asset_library, &userdef->asset_libraries) { + LISTBASE_FOREACH (CustomAssetLibraryDefinition *, asset_library, &userdef->asset_libraries) { /* Ignores translations, since that would depend on the current preferences (global `U`). */ if (STREQ(asset_library->name, "Default")) { - BKE_preferences_asset_library_name_set( - userdef, asset_library, BKE_PREFS_ASSET_LIBRARY_DEFAULT_NAME); + BKE_asset_library_custom_name_set( + &userdef->asset_libraries, asset_library, BKE_PREFS_ASSET_LIBRARY_DEFAULT_NAME); } } } diff --git a/source/blender/blenloader/intern/writefile.cc b/source/blender/blenloader/intern/writefile.cc index 42bc884098e..01a729732a1 100644 --- a/source/blender/blenloader/intern/writefile.cc +++ b/source/blender/blenloader/intern/writefile.cc @@ -94,6 +94,8 @@ #include "BLI_threads.h" #include "MEM_guardedalloc.h" /* MEM_freeN */ +#include "BKE_blender_project.h" +#include "BKE_blender_project.hh" #include "BKE_blender_version.h" #include "BKE_bpath.h" #include "BKE_global.h" /* for G */ @@ -921,8 +923,9 @@ static void write_userdef(BlendWriter *writer, const UserDef *userdef) BLO_write_struct(writer, bPathCompare, path_cmp); } - LISTBASE_FOREACH (const bUserAssetLibrary *, asset_library_ref, &userdef->asset_libraries) { - BLO_write_struct(writer, bUserAssetLibrary, asset_library_ref); + LISTBASE_FOREACH ( + const CustomAssetLibraryDefinition *, asset_library_ref, &userdef->asset_libraries) { + BLO_write_struct(writer, CustomAssetLibraryDefinition, asset_library_ref); } LISTBASE_FOREACH (const uiStyle *, style, &userdef->uistyles) { @@ -1438,6 +1441,11 @@ bool BLO_write_file(Main *mainvar, } } + /* Update active project information based on the new file location. */ + if (U.experimental.use_blender_projects) { + BKE_project_active_load_from_path(filepath); + } + /* actual file writing */ const bool err = write_file_handle( mainvar, &ww, nullptr, nullptr, write_flags, use_userdef, thumb); diff --git a/source/blender/editors/CMakeLists.txt b/source/blender/editors/CMakeLists.txt index 9fff8bf861c..fab403a2923 100644 --- a/source/blender/editors/CMakeLists.txt +++ b/source/blender/editors/CMakeLists.txt @@ -21,6 +21,7 @@ if(WITH_BLENDER) add_subdirectory(metaball) add_subdirectory(object) add_subdirectory(physics) + add_subdirectory(project) add_subdirectory(render) add_subdirectory(scene) add_subdirectory(sculpt_paint) @@ -37,6 +38,7 @@ if(WITH_BLENDER) add_subdirectory(space_nla) add_subdirectory(space_node) add_subdirectory(space_outliner) + add_subdirectory(space_project_settings) add_subdirectory(space_script) add_subdirectory(space_sequencer) add_subdirectory(space_spreadsheet) diff --git a/source/blender/editors/asset/ED_asset_library.h b/source/blender/editors/asset/ED_asset_library.h index cc0d97054b6..c5f526e18d5 100644 --- a/source/blender/editors/asset/ED_asset_library.h +++ b/source/blender/editors/asset/ED_asset_library.h @@ -34,6 +34,8 @@ AssetLibraryReference ED_asset_library_reference_from_enum_value(int value); const struct EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf( bool include_local_library); +struct CustomAssetLibraryDefinition *ED_asset_library_find_custom_library_from_reference( + const AssetLibraryReference *library_ref); #ifdef __cplusplus } #endif diff --git a/source/blender/editors/asset/intern/asset_indexer.cc b/source/blender/editors/asset/intern/asset_indexer.cc index cc06fa80429..2f301d55007 100644 --- a/source/blender/editors/asset/intern/asset_indexer.cc +++ b/source/blender/editors/asset/intern/asset_indexer.cc @@ -26,7 +26,6 @@ #include "BKE_asset.h" #include "BKE_asset_catalog.hh" #include "BKE_idprop.hh" -#include "BKE_preferences.h" #include "CLG_log.h" diff --git a/source/blender/editors/asset/intern/asset_library_reference.cc b/source/blender/editors/asset/intern/asset_library_reference.cc index 5096b9d653d..ce3327ff421 100644 --- a/source/blender/editors/asset/intern/asset_library_reference.cc +++ b/source/blender/editors/asset/intern/asset_library_reference.cc @@ -4,8 +4,15 @@ * \ingroup edasset */ +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" +#include "BKE_context.h" + +#include "DNA_userdef_types.h" + #include "BLI_hash.hh" +#include "ED_asset_library.h" #include "asset_library_reference.hh" namespace blender::ed::asset { @@ -18,14 +25,15 @@ AssetLibraryReferenceWrapper::AssetLibraryReferenceWrapper(const AssetLibraryRef bool operator==(const AssetLibraryReferenceWrapper &a, const AssetLibraryReferenceWrapper &b) { return (a.type == b.type) && - ((a.type == ASSET_LIBRARY_CUSTOM) ? (a.custom_library_index == b.custom_library_index) : - true); + (ELEM(a.type, ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES, ASSET_LIBRARY_CUSTOM_FROM_PROJECT) ? + (a.custom_library_index == b.custom_library_index) : + true); } uint64_t AssetLibraryReferenceWrapper::hash() const { uint64_t hash1 = DefaultHash<decltype(type)>{}(type); - if (type != ASSET_LIBRARY_CUSTOM) { + if (!ELEM(type, ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES, ASSET_LIBRARY_CUSTOM_FROM_PROJECT)) { return hash1; } @@ -34,3 +42,25 @@ uint64_t AssetLibraryReferenceWrapper::hash() const } } // namespace blender::ed::asset + +CustomAssetLibraryDefinition *ED_asset_library_find_custom_library_from_reference( + const AssetLibraryReference *library_ref) +{ + switch (library_ref->type) { + case ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES: + return BKE_asset_library_custom_find_from_index(&U.asset_libraries, + library_ref->custom_library_index); + case ASSET_LIBRARY_CUSTOM_FROM_PROJECT: { + BlenderProject *project = CTX_wm_project(); + if (project) { + return BKE_asset_library_custom_find_from_index( + BKE_project_custom_asset_libraries_get(project), library_ref->custom_library_index); + } + break; + } + case ASSET_LIBRARY_LOCAL: + return nullptr; + } + + return nullptr; +} diff --git a/source/blender/editors/asset/intern/asset_library_reference_enum.cc b/source/blender/editors/asset/intern/asset_library_reference_enum.cc index 773838a54cd..7d75dc83854 100644 --- a/source/blender/editors/asset/intern/asset_library_reference_enum.cc +++ b/source/blender/editors/asset/intern/asset_library_reference_enum.cc @@ -11,7 +11,9 @@ #include "BLI_listbase.h" -#include "BKE_preferences.h" +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" +#include "BKE_context.h" #include "DNA_userdef_types.h" @@ -23,17 +25,15 @@ int ED_asset_library_reference_to_enum_value(const AssetLibraryReference *library) { - /* Simple case: Predefined repository, just set the value. */ - if (library->type < ASSET_LIBRARY_CUSTOM) { + /* Simple case: Predefined library, just set the value. */ + if (library->type < ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES) { return library->type; } /* Note that the path isn't checked for validity here. If an invalid library path is used, the * Asset Browser can give a nice hint on what's wrong. */ - const bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_from_index( - &U, library->custom_library_index); - if (user_library) { - return ASSET_LIBRARY_CUSTOM + library->custom_library_index; + if (ED_asset_library_find_custom_library_from_reference(library)) { + return library->type + library->custom_library_index; } return ASSET_LIBRARY_LOCAL; @@ -44,32 +44,63 @@ AssetLibraryReference ED_asset_library_reference_from_enum_value(int value) AssetLibraryReference library; /* Simple case: Predefined repository, just set the value. */ - if (value < ASSET_LIBRARY_CUSTOM) { + if (value < ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES) { library.type = value; library.custom_library_index = -1; BLI_assert(ELEM(value, ASSET_LIBRARY_LOCAL)); return library; } - const bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_from_index( - &U, value - ASSET_LIBRARY_CUSTOM); + const eAssetLibraryType type = (value < ASSET_LIBRARY_CUSTOM_FROM_PROJECT) ? + ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES : + ASSET_LIBRARY_CUSTOM_FROM_PROJECT; - /* Note that there is no check if the path exists here. If an invalid library path is used, the - * Asset Browser can give a nice hint on what's wrong. */ - if (!user_library) { - library.type = ASSET_LIBRARY_LOCAL; - library.custom_library_index = -1; - } - else { - const bool is_valid = (user_library->name[0] && user_library->path[0]); - if (is_valid) { - library.custom_library_index = value - ASSET_LIBRARY_CUSTOM; - library.type = ASSET_LIBRARY_CUSTOM; + const CustomAssetLibraryDefinition *custom_library = nullptr; + + library.type = type; + library.custom_library_index = value - type; + + { + custom_library = ED_asset_library_find_custom_library_from_reference(&library); + + /* Note that there is no check if the path exists here. If an invalid library path is used, the + * Asset Browser can give a nice hint on what's wrong. */ + const bool is_valid = custom_library && (custom_library->name[0] && custom_library->path[0]); + if (!is_valid) { + library.custom_library_index = -1; } } + return library; } +static void add_custom_asset_library_enum_items( + const ListBase * /*CustomAssetLibraryDefinition*/ libraries, + const eAssetLibraryType library_type, + EnumPropertyItem **items, + int *totitem) +{ + int i; + LISTBASE_FOREACH_INDEX (CustomAssetLibraryDefinition *, custom_library, libraries, i) { + /* Note that the path itself isn't checked for validity here. If an invalid library path is + * used, the Asset Browser can give a nice hint on what's wrong. */ + const bool is_valid = (custom_library->name[0] && custom_library->path[0]); + if (!is_valid) { + continue; + } + + AssetLibraryReference library_reference; + library_reference.type = library_type; + library_reference.custom_library_index = i; + + const int enum_value = ED_asset_library_reference_to_enum_value(&library_reference); + /* Use library path as description, it's a nice hint for users. */ + EnumPropertyItem tmp = { + enum_value, custom_library->name, ICON_NONE, custom_library->name, custom_library->path}; + RNA_enum_item_add(items, totitem, &tmp); + } +} + const EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf( const bool include_local_library) { @@ -92,29 +123,21 @@ const EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf( RNA_enum_items_add(&item, &totitem, predefined_items); } - /* Add separator if needed. */ - if (!BLI_listbase_is_empty(&U.asset_libraries)) { + BlenderProject *project = CTX_wm_project(); + if (project && !BLI_listbase_is_empty(BKE_project_custom_asset_libraries_get(project))) { RNA_enum_item_add_separator(&item, &totitem); - } - int i; - LISTBASE_FOREACH_INDEX (bUserAssetLibrary *, user_library, &U.asset_libraries, i) { - /* Note that the path itself isn't checked for validity here. If an invalid library path is - * used, the Asset Browser can give a nice hint on what's wrong. */ - const bool is_valid = (user_library->name[0] && user_library->path[0]); - if (!is_valid) { - continue; - } + add_custom_asset_library_enum_items(BKE_project_custom_asset_libraries_get(project), + ASSET_LIBRARY_CUSTOM_FROM_PROJECT, + &item, + &totitem); + } - AssetLibraryReference library_reference; - library_reference.type = ASSET_LIBRARY_CUSTOM; - library_reference.custom_library_index = i; + if (!BLI_listbase_is_empty(&U.asset_libraries)) { + RNA_enum_item_add_separator(&item, &totitem); - const int enum_value = ED_asset_library_reference_to_enum_value(&library_reference); - /* Use library path as description, it's a nice hint for users. */ - EnumPropertyItem tmp = { - enum_value, user_library->name, ICON_NONE, user_library->name, user_library->path}; - RNA_enum_item_add(&item, &totitem, &tmp); + add_custom_asset_library_enum_items( + &U.asset_libraries, ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES, &item, &totitem); } RNA_enum_item_end(&item, &totitem); diff --git a/source/blender/editors/asset/intern/asset_list.cc b/source/blender/editors/asset/intern/asset_list.cc index bb72c5cc1bb..fc71882d88f 100644 --- a/source/blender/editors/asset/intern/asset_list.cc +++ b/source/blender/editors/asset/intern/asset_list.cc @@ -12,6 +12,8 @@ #include <optional> #include <string> +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" #include "BKE_context.h" #include "BLI_map.hh" @@ -20,8 +22,6 @@ #include "DNA_space_types.h" -#include "BKE_preferences.h" - #include "ED_fileselect.h" #include "WM_api.h" @@ -32,6 +32,7 @@ #include "ED_asset_handle.h" #include "ED_asset_indexer.h" +#include "ED_asset_library.h" #include "ED_asset_list.h" #include "ED_asset_list.hh" #include "asset_library_reference.hh" @@ -131,15 +132,8 @@ void AssetList::setup() { FileList *files = filelist_; - bUserAssetLibrary *user_library = nullptr; - - /* Ensure valid repository, or fall-back to local one. */ - if (library_ref_.type == ASSET_LIBRARY_CUSTOM) { - BLI_assert(library_ref_.custom_library_index >= 0); - - user_library = BKE_preferences_asset_library_find_from_index( - &U, library_ref_.custom_library_index); - } + CustomAssetLibraryDefinition *custom_library = + ED_asset_library_find_custom_library_from_reference(&library_ref_); /* Relevant bits from file_refresh(). */ /* TODO pass options properly. */ @@ -161,8 +155,18 @@ void AssetList::setup() filelist_setindexer(files, use_asset_indexer ? &file_indexer_asset : &file_indexer_noop); char path[FILE_MAXDIR] = ""; - if (user_library) { - BLI_strncpy(path, user_library->path, sizeof(path)); + if (custom_library) { + /* Project asset libraries typically use relative paths (relative to project root directory). + */ + if ((library_ref_.type == ASSET_LIBRARY_CUSTOM_FROM_PROJECT) && + BLI_path_is_rel(custom_library->path)) { + BlenderProject *project = CTX_wm_project(); + const char *project_root_path = BKE_project_root_path_get(project); + BLI_path_join(path, sizeof(path), project_root_path, custom_library->path); + } + else { + BLI_strncpy(path, custom_library->path, sizeof(path)); + } filelist_setdir(files, path); } else { @@ -377,7 +381,8 @@ std::optional<eFileSelectType> AssetListStorage::asset_library_reference_to_file const AssetLibraryReference &library_reference) { switch (library_reference.type) { - case ASSET_LIBRARY_CUSTOM: + case ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES: + case ASSET_LIBRARY_CUSTOM_FROM_PROJECT: return FILE_ASSET_LIBRARY; case ASSET_LIBRARY_LOCAL: return FILE_MAIN_ASSET; diff --git a/source/blender/editors/asset/intern/asset_ops.cc b/source/blender/editors/asset/intern/asset_ops.cc index d1c46a8259f..9b38911a993 100644 --- a/source/blender/editors/asset/intern/asset_ops.cc +++ b/source/blender/editors/asset/intern/asset_ops.cc @@ -5,12 +5,13 @@ */ #include "BKE_asset_library.hh" +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.hh" #include "BKE_bpath.h" #include "BKE_context.h" #include "BKE_global.h" #include "BKE_lib_id.h" #include "BKE_main.h" -#include "BKE_preferences.h" #include "BKE_report.h" #include "BLI_fileops.h" @@ -669,7 +670,7 @@ static void ASSET_OT_catalogs_save(struct wmOperatorType *ot) /* -------------------------------------------------------------------- */ static bool could_be_asset_bundle(const Main *bmain); -static const bUserAssetLibrary *selected_asset_library(struct wmOperator *op); +static const CustomAssetLibraryDefinition *selected_asset_library(struct wmOperator *op); static bool is_contained_in_selected_asset_library(struct wmOperator *op, const char *filepath); static bool set_filepath_for_asset_lib(const Main *bmain, struct wmOperator *op); static bool has_external_files(Main *bmain, struct ReportList *reports); @@ -691,8 +692,8 @@ static bool asset_bundle_install_poll(bContext *C) } /* Check whether this file is already located inside any asset library. */ - const struct bUserAssetLibrary *asset_lib = BKE_preferences_asset_library_containing_path( - &U, bmain->filepath); + const struct CustomAssetLibraryDefinition *asset_lib = BKE_asset_library_custom_containing_path( + &U.asset_libraries, bmain->filepath); if (asset_lib) { return false; } @@ -763,7 +764,7 @@ static int asset_bundle_install_exec(bContext *C, wmOperator *op) return operator_result; } - const bUserAssetLibrary *lib = selected_asset_library(op); + const CustomAssetLibraryDefinition *lib = selected_asset_library(op); BLI_assert_msg(lib, "If the asset library is not known, how did we get here?"); BKE_reportf(op->reports, RPT_INFO, @@ -823,18 +824,18 @@ static bool could_be_asset_bundle(const Main *bmain) return fnmatch("*_bundle.blend", bmain->filepath, FNM_CASEFOLD) == 0; } -static const bUserAssetLibrary *selected_asset_library(struct wmOperator *op) +static const CustomAssetLibraryDefinition *selected_asset_library(struct wmOperator *op) { const int enum_value = RNA_enum_get(op->ptr, "asset_library_ref"); const AssetLibraryReference lib_ref = ED_asset_library_reference_from_enum_value(enum_value); - const bUserAssetLibrary *lib = BKE_preferences_asset_library_find_from_index( - &U, lib_ref.custom_library_index); + const CustomAssetLibraryDefinition *lib = BKE_asset_library_custom_find_from_index( + &U.asset_libraries, lib_ref.custom_library_index); return lib; } static bool is_contained_in_selected_asset_library(struct wmOperator *op, const char *filepath) { - const bUserAssetLibrary *lib = selected_asset_library(op); + const CustomAssetLibraryDefinition *lib = selected_asset_library(op); if (!lib) { return false; } @@ -848,7 +849,7 @@ static bool is_contained_in_selected_asset_library(struct wmOperator *op, const static bool set_filepath_for_asset_lib(const Main *bmain, struct wmOperator *op) { /* Find the directory path of the selected asset library. */ - const bUserAssetLibrary *lib = selected_asset_library(op); + const CustomAssetLibraryDefinition *lib = selected_asset_library(op); if (lib == nullptr) { return false; } diff --git a/source/blender/editors/include/ED_project.h b/source/blender/editors/include/ED_project.h new file mode 100644 index 00000000000..6a78f964ca2 --- /dev/null +++ b/source/blender/editors/include/ED_project.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup editors + * + * Editor level functions for Blender projects. + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +struct BlenderProject; +struct Main; +struct ReportList; + +void ED_operatortypes_project(void); + +/** Sets the project name to the directory name it is located in and registers a "Project Library" + * asset library pointing to `//assets/`. */ +void ED_project_set_defaults(struct BlenderProject *project); +/** + * High level project creation (create project + load if needed + write default settings if + * needed). + * + * Initializes a new project in \a project_root_dir by creating the `.blender_project/` directory + * there. The new project will only be loaded if \a bmain represents a file within the project + * directory. + * + * \return True on success. + */ +bool ED_project_new(const struct Main *bmain, + const char *project_root_dir, + struct ReportList *reports); + +#ifdef __cplusplus +} +#endif diff --git a/source/blender/editors/include/ED_screen.h b/source/blender/editors/include/ED_screen.h index e1fd53ccdee..69ad6f8abba 100644 --- a/source/blender/editors/include/ED_screen.h +++ b/source/blender/editors/include/ED_screen.h @@ -28,6 +28,7 @@ struct Depsgraph; struct IDProperty; struct Main; struct MenuType; +struct ReportList; struct Scene; struct SpaceLink; struct WorkSpace; @@ -491,6 +492,9 @@ int ED_screen_animation_play(struct bContext *C, int sync, int mode); bScreen *ED_screen_animation_playing(const struct wmWindowManager *wm); bScreen *ED_screen_animation_no_scrub(const struct wmWindowManager *wm); +/** \return True on success. */ +bool ED_project_settings_window_show(struct bContext *C, struct ReportList *reports); + /* screen keymaps */ /* called in spacetypes.c */ void ED_operatortypes_screen(void); diff --git a/source/blender/editors/include/ED_space_api.h b/source/blender/editors/include/ED_space_api.h index 07d4f43bb2b..e36cbd25e74 100644 --- a/source/blender/editors/include/ED_space_api.h +++ b/source/blender/editors/include/ED_space_api.h @@ -46,6 +46,7 @@ void ED_spacetype_clip(void); void ED_spacetype_statusbar(void); void ED_spacetype_topbar(void); void ED_spacetype_spreadsheet(void); +void ED_spacetype_project_settings(void); /** \} */ diff --git a/source/blender/editors/interface/interface_handlers.c b/source/blender/editors/interface/interface_handlers.c index ac16d41dabe..a5c786358d6 100644 --- a/source/blender/editors/interface/interface_handlers.c +++ b/source/blender/editors/interface/interface_handlers.c @@ -676,7 +676,7 @@ static bool ui_rna_is_userdef(PointerRNA *ptr, PropertyRNA *prop) &RNA_AddonPreferences, &RNA_KeyConfigPreferences, &RNA_KeyMapItem, - &RNA_UserAssetLibrary); + &RNA_CustomAssetLibraryDefinition); } bool UI_but_is_userdef(const uiBut *but) diff --git a/source/blender/editors/interface/interface_template_search_menu.cc b/source/blender/editors/interface/interface_template_search_menu.cc index 38a7e025572..849144d9166 100644 --- a/source/blender/editors/interface/interface_template_search_menu.cc +++ b/source/blender/editors/interface/interface_template_search_menu.cc @@ -634,6 +634,7 @@ static MenuSearch_Data *menu_items_from_ui_create( SPACE_MENU_MAP(SPACE_NODE, "NODE_MT_editor_menus"); SPACE_MENU_MAP(SPACE_CONSOLE, "CONSOLE_MT_editor_menus"); SPACE_MENU_MAP(SPACE_USERPREF, "USERPREF_MT_editor_menus"); + SPACE_MENU_MAP(SPACE_PROJECT_SETTINGS, "PROJECTSETTINGS_MT_editor_menus"); SPACE_MENU_MAP(SPACE_CLIP, (((const SpaceClip *)sl)->mode == SC_MODE_TRACKING) ? "CLIP_MT_tracking_editor_menus" : diff --git a/source/blender/editors/interface/resources.c b/source/blender/editors/interface/resources.c index 93b94d42d39..2f460de4ebc 100644 --- a/source/blender/editors/interface/resources.c +++ b/source/blender/editors/interface/resources.c @@ -129,6 +129,9 @@ const uchar *UI_ThemeGetColorPtr(bTheme *btheme, int spacetype, int colorid) case SPACE_USERPREF: ts = &btheme->space_preferences; break; + case SPACE_PROJECT_SETTINGS: + ts = &btheme->space_project_settings; + break; case SPACE_CONSOLE: ts = &btheme->space_console; break; diff --git a/source/blender/editors/project/CMakeLists.txt b/source/blender/editors/project/CMakeLists.txt new file mode 100644 index 00000000000..fd85c307f06 --- /dev/null +++ b/source/blender/editors/project/CMakeLists.txt @@ -0,0 +1,28 @@ + +# SPDX-License-Identifier: GPL-2.0-or-later + +set(INC + . + ../include + ../../blenkernel + ../../blenlib + ../../blentranslation + ../../makesdna + ../../makesrna + ../../windowmanager + ../../../../intern/guardedalloc +) + +set(INC_SYS +) + +set(SRC + project.cc + project_ops.cc +) + +set(LIB + bf_blenkernel +) + +blender_add_lib(bf_editor_project "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") diff --git a/source/blender/editors/project/project.cc b/source/blender/editors/project/project.cc new file mode 100644 index 00000000000..5ecad2a9e03 --- /dev/null +++ b/source/blender/editors/project/project.cc @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edproject + */ + +#include "BLI_path_util.h" +#include "BLI_string.h" +#include "BLI_string_ref.hh" + +#include "BLT_translation.h" + +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" +#include "BKE_blender_project.hh" +#include "BKE_context.h" +#include "BKE_main.h" +#include "BKE_report.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "ED_project.h" + +using namespace blender; + +/** Name of the asset library added by default. Needs translation with `DATA_()`. */ +inline static const char *DEFAULT_ASSET_LIBRARY_NAME = N_("Project Library"); +inline static const char *DEFAULT_ASSET_LIBRARY_PATH = "//assets/"; + +void ED_project_set_defaults(BlenderProject *project) +{ + char project_root_dir[FILE_MAXDIR]; + BLI_strncpy(project_root_dir, BKE_project_root_path_get(project), sizeof(project_root_dir)); + + /* Set directory name as default project name. */ + char dirname[FILE_MAXFILE]; + BLI_path_slash_rstrip(project_root_dir); + BLI_split_file_part(project_root_dir, dirname, sizeof(dirname)); + BKE_project_name_set(project, dirname); + + ListBase *libraries = BKE_project_custom_asset_libraries_get(project); + BKE_asset_library_custom_add( + libraries, DATA_(DEFAULT_ASSET_LIBRARY_NAME), DEFAULT_ASSET_LIBRARY_PATH); +} + +bool ED_project_new(const Main *bmain, const char *project_root_dir, ReportList *reports) +{ + if (!BKE_project_create_settings_directory(project_root_dir)) { + BKE_reportf(reports, + RPT_ERROR, + "Failed to create project with unknown error. Is the directory read-only?"); + return false; + } + + std::unique_ptr<bke::BlenderProject> loaded_project = bke::BlenderProject::load_from_path( + project_root_dir); + + /* Some default settings for the project. */ + if (loaded_project) { + BlenderProject *loaded_project_c = BKE_project_c_handle(loaded_project.get()); + + ED_project_set_defaults(loaded_project_c); + /* Write defaults to the hard drive. */ + BKE_project_settings_save(loaded_project_c); + } + + BKE_reportf(reports, RPT_INFO, "Project created and loaded successfully"); + + const char *blend_path = BKE_main_blendfile_path(bmain); + const bool blend_is_in_project = blend_path[0] && + BLI_path_contains(project_root_dir, blend_path); + if (blend_is_in_project) { + bke::BlenderProject::set_active(std::move(loaded_project)); + } + else { + BKE_reportf(reports, + RPT_WARNING, + "The current file is not located inside of the new project. This means the new " + "project is not active"); + } + + return true; +} diff --git a/source/blender/editors/project/project_ops.cc b/source/blender/editors/project/project_ops.cc new file mode 100644 index 00000000000..b9398aa9e95 --- /dev/null +++ b/source/blender/editors/project/project_ops.cc @@ -0,0 +1,299 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edproject + */ + +#include <climits> + +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" +#include "BKE_context.h" +#include "BKE_main.h" +#include "BKE_report.h" + +#include "BLI_path_util.h" +#include "BLT_translation.h" + +#include "DNA_space_types.h" + +#include "RNA_access.h" +#include "RNA_define.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "ED_project.h" +#include "ED_screen.h" + +using namespace blender; + +static bool has_active_project_poll(bContext *C) +{ + const BlenderProject *active_project = CTX_wm_project(); + CTX_wm_operator_poll_msg_set(C, TIP_("No active project loaded")); + return active_project != NULL; +} + +/* -------------------------------------------------------------------- */ +/** \name New project operator + * \{ */ + +static bool new_project_poll(bContext *C) +{ + if (!U.experimental.use_blender_projects) { + CTX_wm_operator_poll_msg_set(C, "Experimental project support is not enabled"); + return false; + } + return true; +} + +static int new_project_exec(bContext *C, wmOperator *op) +{ + const Main *bmain = CTX_data_main(C); + + if (!RNA_struct_property_is_set(op->ptr, "directory")) { + BKE_report(op->reports, RPT_ERROR, "No path defined for creating a new project in"); + return OPERATOR_CANCELLED; + } + char project_root_dir[FILE_MAXDIR]; + RNA_string_get(op->ptr, "directory", project_root_dir); + + if (!ED_project_new(bmain, project_root_dir, op->reports)) { + return OPERATOR_CANCELLED; + } + + PropertyRNA *prop_open_settings = RNA_struct_find_property(op->ptr, "open_settings_after"); + if (RNA_property_is_set(op->ptr, prop_open_settings) && + RNA_property_boolean_get(op->ptr, prop_open_settings)) { + ED_project_settings_window_show(C, op->reports); + } + + WM_main_add_notifier(NC_PROJECT, NULL); + /* Update the window title. */ + WM_main_add_notifier(NC_WM | ND_DATACHANGED, NULL); + + return OPERATOR_FINISHED; +} + +static int new_project_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) +{ + const Main *bmain = CTX_data_main(C); + const char *blendfile_path = BKE_main_blendfile_path(bmain); + if (blendfile_path[0]) { + /* Open at the .blend file location if any. */ + RNA_string_set(op->ptr, "directory", blendfile_path); + } + + WM_event_add_fileselect(C, op); + return OPERATOR_RUNNING_MODAL; +} + +static void PROJECT_OT_new(wmOperatorType *ot) +{ + ot->name = "New Project"; + ot->idname = "PROJECT_OT_new"; + ot->description = "Choose a directory to use as the root of a project"; + + ot->invoke = new_project_invoke; + ot->exec = new_project_exec; + /* omit window poll so this can work in background mode */ + ot->poll = new_project_poll; + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER, + FILE_BLENDER, + FILE_OPENFILE, + WM_FILESEL_DIRECTORY, + FILE_DEFAULTDISPLAY, + FILE_SORT_DEFAULT); + + PropertyRNA *prop; + prop = RNA_def_boolean(ot->srna, + "open_settings_after", + false, + "", + "Open the project settings window after successfully creating a project"); + RNA_def_property_flag(prop, PROP_HIDDEN); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Write Project Settings Operator + * \{ */ + +static int save_settings_exec(bContext *UNUSED(C), wmOperator *UNUSED(op)) +{ + BlenderProject *active_project = CTX_wm_project(); + + if (!BKE_project_settings_save(active_project)) { + return OPERATOR_CANCELLED; + } + + return OPERATOR_FINISHED; +} + +static void PROJECT_OT_save_settings(wmOperatorType *ot) +{ + ot->name = "Save Project Settings"; + ot->idname = "PROJECT_OT_save_settings"; + ot->description = "Make the current changes to the project settings permanent"; + + ot->invoke = WM_operator_confirm; + ot->poll = has_active_project_poll; + ot->exec = save_settings_exec; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Delete project setup operator + * \{ */ + +static int delete_project_setup_exec(bContext *C, wmOperator *op) +{ + if (!BKE_project_delete_settings_directory(CTX_wm_project())) { + BKE_report(op->reports, + RPT_ERROR, + "Failed to delete project settings. Is the project directory read-only?"); + return OPERATOR_CANCELLED; + } + BKE_project_active_unset(); + + WM_main_add_notifier(NC_PROJECT, NULL); + /* Update the window title. */ + WM_event_add_notifier_ex(CTX_wm_manager(C), CTX_wm_window(C), NC_WM | ND_DATACHANGED, NULL); + + return OPERATOR_FINISHED; +} + +static void PROJECT_OT_delete_setup(wmOperatorType *ot) +{ + ot->name = "Delete Project Setup"; + ot->idname = "PROJECT_OT_delete_setup"; + ot->description = + "Remove the configuration of the current project with all settings, but keep project files " + "(such as .blend files) untouched"; + + ot->invoke = WM_operator_confirm; + ot->exec = delete_project_setup_exec; + /* omit window poll so this can work in background mode */ + ot->poll = has_active_project_poll; +} + +/** \} */ +/* -------------------------------------------------------------------- */ +/** \name Add Custom Asset Library + * \{ */ + +static int custom_asset_library_add_exec(bContext *UNUSED(C), wmOperator *op) +{ + BlenderProject *project = CTX_wm_project(); + + char path[FILE_MAXDIR]; + char dirname[FILE_MAXFILE]; + + RNA_string_get(op->ptr, "directory", path); + + BLI_path_slash_rstrip(path); + /* Always keep project paths relative for now. Adds the "//" prefix which usually denotes a path + * that's relative to the current .blend, for now use it for project relative paths as well. */ + BLI_path_rel(path, BKE_project_root_path_get(project)); + BLI_split_file_part(path, dirname, sizeof(dirname)); + + ListBase *asset_libraries = BKE_project_custom_asset_libraries_get(project); + /* NULL is a valid directory path here. A library without path will be created then. */ + BKE_asset_library_custom_add(asset_libraries, dirname, path); + BKE_project_tag_has_unsaved_changes(project); + + WM_main_add_notifier(NC_ASSET | ND_ASSET_LIBRARY, NULL); + WM_main_add_notifier(NC_PROJECT, NULL); + + return OPERATOR_FINISHED; +} + +static int custom_asset_library_add_invoke(bContext *C, + wmOperator *op, + const wmEvent *UNUSED(event)) +{ + if (!RNA_struct_property_is_set(op->ptr, "directory")) { + WM_event_add_fileselect(C, op); + return OPERATOR_RUNNING_MODAL; + } + + return custom_asset_library_add_exec(C, op); +} + +/* Similar to #PREFERENCES_OT_asset_library_add. */ +static void PROJECT_OT_custom_asset_library_add(wmOperatorType *ot) +{ + ot->name = "Add Asset Library"; + ot->idname = "PROJECT_OT_custom_asset_library_add"; + ot->description = "Register a directory to be used by the Asset Browser as source of assets"; + + ot->exec = custom_asset_library_add_exec; + ot->invoke = custom_asset_library_add_invoke; + ot->poll = has_active_project_poll; + + ot->flag = OPTYPE_INTERNAL; + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER, + FILE_SPECIAL, + FILE_OPENFILE, + WM_FILESEL_DIRECTORY, + FILE_DEFAULTDISPLAY, + FILE_SORT_DEFAULT); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Remove Custom Asset Library + * \{ */ + +static int custom_asset_library_remove_exec(bContext *UNUSED(C), wmOperator *op) +{ + const int index = RNA_int_get(op->ptr, "index"); + + BlenderProject *project = CTX_wm_project(); + ListBase *asset_libraries = BKE_project_custom_asset_libraries_get(project); + CustomAssetLibraryDefinition *library = BKE_asset_library_custom_find_from_index(asset_libraries, + index); + BKE_asset_library_custom_remove(asset_libraries, library); + BKE_project_tag_has_unsaved_changes(project); + + WM_main_add_notifier(NC_ASSET | ND_ASSET_LIBRARY, NULL); + WM_main_add_notifier(NC_PROJECT, NULL); + + return OPERATOR_FINISHED; +} + +/* Similar to #PREFERENCES_OT_asset_library_remove. */ +static void PROJECT_OT_custom_asset_library_remove(wmOperatorType *ot) +{ + ot->name = "Remove Asset Library"; + ot->idname = "PROJECT_OT_custom_asset_library_remove"; + ot->description = + "Unregister a path to a .blend file, so the Asset Browser will not attempt to show it " + "anymore"; + + ot->exec = custom_asset_library_remove_exec; + ot->poll = has_active_project_poll; + + ot->flag = OPTYPE_INTERNAL; + + RNA_def_int(ot->srna, "index", 0, 0, INT_MAX, "Index", "", 0, 1000); +} + +/** \} */ + +void ED_operatortypes_project() +{ + WM_operatortype_append(PROJECT_OT_new); + WM_operatortype_append(PROJECT_OT_save_settings); + WM_operatortype_append(PROJECT_OT_delete_setup); + WM_operatortype_append(PROJECT_OT_custom_asset_library_add); + WM_operatortype_append(PROJECT_OT_custom_asset_library_remove); +} diff --git a/source/blender/editors/screen/screen_ops.c b/source/blender/editors/screen/screen_ops.c index 6a910170e44..5d8864d1b25 100644 --- a/source/blender/editors/screen/screen_ops.c +++ b/source/blender/editors/screen/screen_ops.c @@ -5031,7 +5031,14 @@ static void SCREEN_OT_back_to_previous(struct wmOperatorType *ot) /** \name Show User Preferences Operator * \{ */ -static int userpref_show_exec(bContext *C, wmOperator *op) +/** + * Shared window opening logic for preferences and project settings. + * \return True on success. + */ +static bool settings_window_show(bContext *C, + eSpace_Type space_type, + const char *window_title, + ReportList *reports) { wmWindow *win_cur = CTX_wm_window(C); /* Use eventstate, not event from _invoke, so this can be called through exec(). */ @@ -5039,26 +5046,14 @@ static int userpref_show_exec(bContext *C, wmOperator *op) int sizex = (500 + UI_NAVIGATION_REGION_WIDTH) * UI_DPI_FAC; int sizey = 520 * UI_DPI_FAC; - PropertyRNA *prop = RNA_struct_find_property(op->ptr, "section"); - if (prop && RNA_property_is_set(op->ptr, prop)) { - /* Set active section via RNA, so it can fail properly. */ - - PointerRNA pref_ptr; - RNA_pointer_create(NULL, &RNA_Preferences, &U, &pref_ptr); - PropertyRNA *active_section_prop = RNA_struct_find_property(&pref_ptr, "active_section"); - - RNA_property_enum_set(&pref_ptr, active_section_prop, RNA_property_enum_get(op->ptr, prop)); - RNA_property_update(C, &pref_ptr, active_section_prop); - } - /* changes context! */ if (WM_window_open(C, - IFACE_("Blender Preferences"), + window_title, event->xy[0], event->xy[1], sizex, sizey, - SPACE_USERPREF, + space_type, false, false, true, @@ -5067,18 +5062,43 @@ static int userpref_show_exec(bContext *C, wmOperator *op) * So hiding in the temp window makes sense. */ ScrArea *area = CTX_wm_area(C); ARegion *region = BKE_area_find_region_type(area, RGN_TYPE_HEADER); - - region->flag |= RGN_FLAG_HIDDEN; - ED_region_visibility_change_update(C, area, region); + if (region) { + region->flag |= RGN_FLAG_HIDDEN; + ED_region_visibility_change_update(C, area, region); + } /* And also show the region with "Load & Save" buttons. */ region = BKE_area_find_region_type(area, RGN_TYPE_EXECUTE); - region->flag &= ~RGN_FLAG_HIDDEN; - ED_region_visibility_change_update(C, area, region); + if (region) { + region->flag &= ~RGN_FLAG_HIDDEN; + ED_region_visibility_change_update(C, area, region); + } + + return true; + } + + BKE_report(reports, RPT_ERROR, "Failed to open window!"); + return false; +} + +static int userpref_show_exec(bContext *C, wmOperator *op) +{ + PropertyRNA *prop = RNA_struct_find_property(op->ptr, "section"); + if (prop && RNA_property_is_set(op->ptr, prop)) { + /* Set active section via RNA, so it can fail properly. */ + + PointerRNA pref_ptr; + RNA_pointer_create(NULL, &RNA_Preferences, &U, &pref_ptr); + PropertyRNA *active_section_prop = RNA_struct_find_property(&pref_ptr, "active_section"); + + RNA_property_enum_set(&pref_ptr, active_section_prop, RNA_property_enum_get(op->ptr, prop)); + RNA_property_update(C, &pref_ptr, active_section_prop); + } + if (settings_window_show(C, SPACE_USERPREF, IFACE_("Blender Preferences"), op->reports)) { return OPERATOR_FINISHED; } - BKE_report(op->reports, RPT_ERROR, "Failed to open window!"); + return OPERATOR_CANCELLED; } @@ -5107,6 +5127,62 @@ static void SCREEN_OT_userpref_show(struct wmOperatorType *ot) /** \} */ /* -------------------------------------------------------------------- */ +/** \name Show Project Settings Operator + * \{ */ + +bool ED_project_settings_window_show(bContext *C, ReportList *reports) +{ + return settings_window_show( + C, SPACE_PROJECT_SETTINGS, IFACE_("Blender Project Settings"), reports); +} + +static int project_settings_show_exec(bContext *C, wmOperator *op) +{ + if (!ED_project_settings_window_show(C, op->reports)) { + return OPERATOR_CANCELLED; + } + + PropertyRNA *prop = RNA_struct_find_property(op->ptr, "section"); + SpaceProjectSettings *space_project = CTX_wm_space_project_settings(C); + if (space_project && prop && RNA_property_is_set(op->ptr, prop)) { + /* Set active section via RNA, so it can fail properly. */ + + bScreen *screen = CTX_wm_screen(C); + PointerRNA space_ptr; + RNA_pointer_create(&screen->id, &RNA_SpaceProjectSettings, space_project, &space_ptr); + PropertyRNA *active_section_prop = RNA_struct_find_property(&space_ptr, "active_section"); + + RNA_property_enum_set(&space_ptr, active_section_prop, RNA_property_enum_get(op->ptr, prop)); + RNA_property_update(C, &space_ptr, active_section_prop); + } + + return OPERATOR_FINISHED; +} + +static void SCREEN_OT_project_settings_show(struct wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Open Project Settings..."; + ot->description = "Edit configuration for the active Blender project"; + ot->idname = "SCREEN_OT_project_settings_show"; + + /* api callbacks */ + ot->exec = project_settings_show_exec; + ot->poll = ED_operator_screenactive_nobackground; /* Not in background as this opens a window. */ + + PropertyRNA *prop; + prop = RNA_def_enum(ot->srna, + "section", + rna_enum_project_settings_section_items, + 0, + "", + "Section to activate in the project settings"); + RNA_def_property_flag(prop, PROP_HIDDEN); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Show Drivers Editor Operator * \{ */ @@ -5694,6 +5770,7 @@ void ED_operatortypes_screen(void) WM_operatortype_append(SCREEN_OT_screenshot); WM_operatortype_append(SCREEN_OT_screenshot_area); WM_operatortype_append(SCREEN_OT_userpref_show); + WM_operatortype_append(SCREEN_OT_project_settings_show); WM_operatortype_append(SCREEN_OT_drivers_editor_show); WM_operatortype_append(SCREEN_OT_info_log_show); WM_operatortype_append(SCREEN_OT_region_blend); diff --git a/source/blender/editors/space_api/CMakeLists.txt b/source/blender/editors/space_api/CMakeLists.txt index 0024e7935d8..6e04d3493d7 100644 --- a/source/blender/editors/space_api/CMakeLists.txt +++ b/source/blender/editors/space_api/CMakeLists.txt @@ -22,6 +22,7 @@ set(SRC set(LIB bf_editor_geometry + bf_editor_project bf_editor_space_action bf_editor_space_buttons bf_editor_space_clip @@ -33,6 +34,7 @@ set(LIB bf_editor_space_nla bf_editor_space_node bf_editor_space_outliner + bf_editor_space_project_settings bf_editor_space_script bf_editor_space_sequencer bf_editor_space_spreadsheet diff --git a/source/blender/editors/space_api/spacetypes.c b/source/blender/editors/space_api/spacetypes.c index 3d964a95bc0..ffd490e3c1e 100644 --- a/source/blender/editors/space_api/spacetypes.c +++ b/source/blender/editors/space_api/spacetypes.c @@ -43,6 +43,7 @@ #include "ED_object.h" #include "ED_paint.h" #include "ED_physics.h" +#include "ED_project.h" #include "ED_render.h" #include "ED_scene.h" #include "ED_screen.h" @@ -82,9 +83,11 @@ void ED_spacetypes_init(void) ED_spacetype_statusbar(); ED_spacetype_topbar(); ED_spacetype_spreadsheet(); + ED_spacetype_project_settings(); /* Register operator types for screen and all spaces. */ ED_operatortypes_userpref(); + ED_operatortypes_project(); ED_operatortypes_workspace(); ED_operatortypes_scene(); ED_operatortypes_screen(); diff --git a/source/blender/editors/space_file/file_draw.c b/source/blender/editors/space_file/file_draw.c index 240901318b5..c3249b26fdd 100644 --- a/source/blender/editors/space_file/file_draw.c +++ b/source/blender/editors/space_file/file_draw.c @@ -1165,13 +1165,17 @@ static void file_draw_invalid_library_hint(const bContext *C, { UI_icon_draw(sx, sy - UI_UNIT_Y, ICON_INFO); + uiBlock *block = UI_block_begin(C, region, __func__, UI_EMBOSS); + const char *suggestion = TIP_( "Asset Libraries are local directories that can contain .blend files with assets inside.\n" - "Manage Asset Libraries from the File Paths section in Preferences"); + "Manage Asset Libraries from the File Paths section in the Preferences or in the Project " + "Settings."); file_draw_string_multiline( sx + UI_UNIT_X, sy, suggestion, width - UI_UNIT_X, line_height, text_col, NULL, &sy); - uiBlock *block = UI_block_begin(C, region, __func__, UI_EMBOSS); + const short but_offset_y = line_height + UI_UNIT_Y * 1.2f; + const short but_width = UI_UNIT_X * 8; uiBut *but = uiDefIconTextButO(block, UI_BTYPE_BUT, "SCREEN_OT_userpref_show", @@ -1179,13 +1183,26 @@ static void file_draw_invalid_library_hint(const bContext *C, ICON_PREFERENCES, NULL, sx + UI_UNIT_X, - sy - line_height - UI_UNIT_Y * 1.2f, - UI_UNIT_X * 8, + sy - but_offset_y, + but_width, UI_UNIT_Y, NULL); PointerRNA *but_opptr = UI_but_operator_ptr_get(but); RNA_enum_set(but_opptr, "section", USER_SECTION_FILE_PATHS); + but = uiDefButO(block, + UI_BTYPE_BUT, + "SCREEN_OT_project_settings_show", + WM_OP_INVOKE_DEFAULT, + NULL, + sx + UI_UNIT_X + but_width + UI_UNIT_X, + sy - but_offset_y, + but_width, + UI_UNIT_Y, + NULL); + but_opptr = UI_but_operator_ptr_get(but); + RNA_enum_set(but_opptr, "section", PROJECT_SETTINGS_SECTION_ASSET_LIBRARIES); + UI_block_end(C, block); UI_block_draw(C, block); } diff --git a/source/blender/editors/space_file/filelist.cc b/source/blender/editors/space_file/filelist.cc index 3257534f94d..d4a42c3f4f2 100644 --- a/source/blender/editors/space_file/filelist.cc +++ b/source/blender/editors/space_file/filelist.cc @@ -42,6 +42,8 @@ #include "BKE_asset.h" #include "BKE_asset_library.h" +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" #include "BKE_context.h" #include "BKE_global.h" #include "BKE_icons.h" @@ -49,12 +51,12 @@ #include "BKE_lib_id.h" #include "BKE_main.h" #include "BKE_main_idmap.h" -#include "BKE_preferences.h" #include "BLO_readfile.h" #include "DNA_asset_types.h" #include "DNA_space_types.h" +#include "ED_asset_library.h" #include "ED_datafiles.h" #include "ED_fileselect.h" #include "ED_screen.h" @@ -1040,12 +1042,18 @@ static bool filelist_compare_asset_libraries(const AssetLibraryReference *librar if (library_a->type != library_b->type) { return false; } - if (library_a->type == ASSET_LIBRARY_CUSTOM) { + + const bool is_custom_library = ELEM( + library_a->type, ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES, ASSET_LIBRARY_CUSTOM_FROM_PROJECT); + if (is_custom_library) { + if (library_a->custom_library_index != library_b->custom_library_index) { + return false; + } + /* Don't only check the index, also check that it's valid. */ - bUserAssetLibrary *library_ptr_a = BKE_preferences_asset_library_find_from_index( - &U, library_a->custom_library_index); - return (library_ptr_a != nullptr) && - (library_a->custom_library_index == library_b->custom_library_index); + if (!ED_asset_library_find_custom_library_from_reference(library_a)) { + return false; + } } return true; @@ -1174,6 +1182,10 @@ static int filelist_geticon_ex(const FileDirEntry *file, { const eFileSel_File_Types typeflag = (eFileSel_File_Types)file->typeflag; + if ((typeflag & FILE_TYPE_DIR) && (typeflag & FILE_TYPE_BLENDER_PROJECT)) { + return ICON_FUND; + } + if ((typeflag & FILE_TYPE_DIR) && !(ignore_libdir && (typeflag & (FILE_TYPE_BLENDERLIB | FILE_TYPE_BLENDER)))) { if (FILENAME_IS_PARENT(file->relpath)) { @@ -2955,6 +2967,12 @@ static int filelist_readjob_list_dir(const char *root, } } + if ((entry->typeflag & FILE_TYPE_DIR) && !(entry->typeflag & FILE_TYPE_BLENDER)) { + if (BKE_project_is_path_project_root(target)) { + entry->typeflag |= FILE_TYPE_BLENDER_PROJECT; + } + } + #ifndef WIN32 /* Set linux-style dot files hidden too. */ if (is_hidden_dot_filename(entry->relpath, entry)) { diff --git a/source/blender/editors/space_file/filesel.c b/source/blender/editors/space_file/filesel.c index af2c9d4e757..6dd4e37fea8 100644 --- a/source/blender/editors/space_file/filesel.c +++ b/source/blender/editors/space_file/filesel.c @@ -39,13 +39,15 @@ #include "BLT_translation.h" #include "BKE_appdir.h" +#include "BKE_asset_library_custom.h" +#include "BKE_blender_project.h" #include "BKE_context.h" #include "BKE_idtype.h" #include "BKE_main.h" -#include "BKE_preferences.h" #include "BLF_api.h" +#include "ED_asset_library.h" #include "ED_fileselect.h" #include "ED_screen.h" @@ -409,27 +411,39 @@ static void fileselect_refresh_asset_params(FileAssetSelectParams *asset_params) { AssetLibraryReference *library = &asset_params->asset_library_ref; FileSelectParams *base_params = &asset_params->base_params; - bUserAssetLibrary *user_library = NULL; + CustomAssetLibraryDefinition *custom_library = + ED_asset_library_find_custom_library_from_reference(library); - /* Ensure valid repository, or fall-back to local one. */ - if (library->type == ASSET_LIBRARY_CUSTOM) { - BLI_assert(library->custom_library_index >= 0); - - user_library = BKE_preferences_asset_library_find_from_index(&U, - library->custom_library_index); - if (!user_library) { - library->type = ASSET_LIBRARY_LOCAL; - } + /* Ensure valid asset library, or fall-back to local one. */ + if (!custom_library) { + library->type = ASSET_LIBRARY_LOCAL; } switch (library->type) { case ASSET_LIBRARY_LOCAL: base_params->dir[0] = '\0'; break; - case ASSET_LIBRARY_CUSTOM: - BLI_assert(user_library); - BLI_strncpy(base_params->dir, user_library->path, sizeof(base_params->dir)); + case ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES: + BLI_assert(custom_library); + BLI_strncpy(base_params->dir, custom_library->path, sizeof(base_params->dir)); break; + /* Project asset libraries typically use relative paths (relative to project root directory). + */ + case ASSET_LIBRARY_CUSTOM_FROM_PROJECT: { + BlenderProject *project = CTX_wm_project(); + BLI_assert(custom_library); + BLI_assert(project); + + if (BLI_path_is_rel(custom_library->path)) { + const char *project_root_path = BKE_project_root_path_get(project); + BLI_path_join( + base_params->dir, sizeof(base_params->dir), project_root_path, custom_library->path); + } + else { + BLI_strncpy(base_params->dir, custom_library->path, sizeof(base_params->dir)); + } + break; + } } base_params->type = (library->type == ASSET_LIBRARY_LOCAL) ? FILE_MAIN_ASSET : FILE_ASSET_LIBRARY; diff --git a/source/blender/editors/space_node/add_node_search.cc b/source/blender/editors/space_node/add_node_search.cc index 28e18c20f46..3751e4cb9b8 100644 --- a/source/blender/editors/space_node/add_node_search.cc +++ b/source/blender/editors/space_node/add_node_search.cc @@ -123,10 +123,11 @@ static void gather_search_items_for_all_assets(const bContext &C, Vector<AddNodeItem> &search_items) { int i; - LISTBASE_FOREACH_INDEX (const bUserAssetLibrary *, asset_library, &U.asset_libraries, i) { + LISTBASE_FOREACH_INDEX ( + const CustomAssetLibraryDefinition *, asset_library, &U.asset_libraries, i) { AssetLibraryReference library_ref{}; library_ref.custom_library_index = i; - library_ref.type = ASSET_LIBRARY_CUSTOM; + library_ref.type = ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES; /* Skip local assets to avoid duplicates when the asset is part of the local file library. */ gather_search_items_for_asset_library(C, node_tree, library_ref, r_added_assets, search_items); } diff --git a/source/blender/editors/space_node/link_drag_search.cc b/source/blender/editors/space_node/link_drag_search.cc index ffa2d377056..53e1093914e 100644 --- a/source/blender/editors/space_node/link_drag_search.cc +++ b/source/blender/editors/space_node/link_drag_search.cc @@ -243,10 +243,11 @@ static void gather_search_link_ops_for_all_assets(const bContext &C, Vector<SocketLinkOperation> &search_link_ops) { int i; - LISTBASE_FOREACH_INDEX (const bUserAssetLibrary *, asset_library, &U.asset_libraries, i) { + LISTBASE_FOREACH_INDEX ( + const CustomAssetLibraryDefinition *, asset_library, &U.asset_libraries, i) { AssetLibraryReference library_ref{}; library_ref.custom_library_index = i; - library_ref.type = ASSET_LIBRARY_CUSTOM; + library_ref.type = ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES; /* Skip local assets to avoid duplicates when the asset is part of the local file library. */ gather_search_link_ops_for_asset_library( C, node_tree, socket, library_ref, true, search_link_ops); diff --git a/source/blender/editors/space_project_settings/CMakeLists.txt b/source/blender/editors/space_project_settings/CMakeLists.txt new file mode 100644 index 00000000000..95c7bff9adc --- /dev/null +++ b/source/blender/editors/space_project_settings/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(INC + ../include + ../../blenfont + ../../blenkernel + ../../blenlib + ../../blenloader + ../../blentranslation + ../../depsgraph + ../../makesdna + ../../makesrna + ../../windowmanager + ../../../../intern/guardedalloc + + # dna_type_offsets.h + ${CMAKE_CURRENT_BINARY_DIR}/../../makesdna/intern + # RNA_prototypes.h + ${CMAKE_BINARY_DIR}/source/blender/makesrna +) + +set(SRC + space_project_settings.cc +) + +set(LIB +) + +blender_add_lib(bf_editor_space_project_settings "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") + +# RNA_prototypes.h dna_type_offsets.h + add_dependencies(bf_editor_space_project_settings bf_rna) + add_dependencies(bf_editor_space_project_settings bf_dna) diff --git a/source/blender/editors/space_project_settings/space_project_settings.cc b/source/blender/editors/space_project_settings/space_project_settings.cc new file mode 100644 index 00000000000..3c62bbaf3e4 --- /dev/null +++ b/source/blender/editors/space_project_settings/space_project_settings.cc @@ -0,0 +1,259 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_screen.h" + +#include "BLI_string.h" +#include "BLI_string_ref.hh" + +#include "BLO_read_write.h" + +#include "DNA_screen_types.h" +#include "DNA_space_types.h" + +#include "ED_screen.h" +#include "ED_space_api.h" + +#include "MEM_guardedalloc.h" + +#include "RNA_access.h" +#include "RNA_enum_types.h" + +#include "UI_interface.h" + +using namespace blender; + +static SpaceLink *project_settings_create(const ScrArea *area, const Scene *UNUSED(scene)) +{ + SpaceProjectSettings *project_settings_space = MEM_cnew<SpaceProjectSettings>( + "project settings space"); + project_settings_space->spacetype = SPACE_PROJECT_SETTINGS; + + { + /* Header. */ + ARegion *region = MEM_cnew<ARegion>("project settings header"); + BLI_addtail(&project_settings_space->regionbase, region); + region->regiontype = RGN_TYPE_HEADER; + /* Ignore preference "USER_HEADER_BOTTOM" here (always show bottom for new types). */ + region->alignment = RGN_ALIGN_BOTTOM; + } + + { + /* navigation region */ + ARegion *region = MEM_cnew<ARegion>("project settings navigation region"); + BLI_addtail(&project_settings_space->regionbase, region); + region->regiontype = RGN_TYPE_NAV_BAR; + region->alignment = RGN_ALIGN_LEFT; + + /* Use smaller size when opened in area like properties editor (same as preferences do). */ + if (area->winx && area->winx < 3.0f * UI_NAVIGATION_REGION_WIDTH * UI_DPI_FAC) { + region->sizex = UI_NARROW_NAVIGATION_REGION_WIDTH; + } + } + { + /* execution region */ + ARegion *region = MEM_cnew<ARegion>("project settings execution region"); + BLI_addtail(&project_settings_space->regionbase, region); + region->regiontype = RGN_TYPE_EXECUTE; + region->alignment = RGN_ALIGN_BOTTOM | RGN_SPLIT_PREV; + region->flag |= RGN_FLAG_DYNAMIC_SIZE | RGN_FLAG_HIDDEN; + } + + { + /* Main window. */ + ARegion *region = MEM_cnew<ARegion>("project settings main region"); + BLI_addtail(&project_settings_space->regionbase, region); + region->regiontype = RGN_TYPE_WINDOW; + } + + return reinterpret_cast<SpaceLink *>(project_settings_space); +} + +static void project_settings_free(SpaceLink *UNUSED(sl)) +{ +} + +static void project_settings_init(wmWindowManager *UNUSED(wm), ScrArea *UNUSED(area)) +{ +} + +static SpaceLink *project_settings_duplicate(SpaceLink *sl) +{ + const SpaceProjectSettings *sproject_settings_old = reinterpret_cast<SpaceProjectSettings *>(sl); + SpaceProjectSettings *sproject_settings_new = reinterpret_cast<SpaceProjectSettings *>( + MEM_dupallocN(sproject_settings_old)); + + return reinterpret_cast<SpaceLink *>(sproject_settings_new); +} + +static void project_settings_listener(const wmSpaceTypeListenerParams *params) +{ + const wmNotifier *wmn = params->notifier; + ScrArea *area = params->area; + + switch (wmn->category) { + case NC_PROJECT: + ED_area_tag_redraw(area); + break; + } +} + +static void project_settings_operatortypes(void) +{ +} + +static void project_settings_keymap(struct wmKeyConfig *UNUSED(keyconf)) +{ +} + +static void project_settings_blend_write(BlendWriter *writer, SpaceLink *sl) +{ + BLO_write_struct(writer, SpaceProjectSettings, sl); +} + +/* add handlers, stuff you only do once or on area/region changes */ +static void project_settings_main_region_init(wmWindowManager *wm, ARegion *region) +{ + /* do not use here, the properties changed in user-preferences do a system-wide refresh, + * then scroller jumps back */ + // region->v2d.flag &= ~V2D_IS_INIT; + + region->v2d.scroll = V2D_SCROLL_RIGHT | V2D_SCROLL_VERTICAL_HIDE; + + ED_region_panels_init(wm, region); +} + +static void project_settings_main_region_layout(const bContext *C, ARegion *region) +{ + SpaceProjectSettings *sproject_settings = CTX_wm_space_project_settings(C); + + char id_lower[64]; + const char *contexts[2] = {id_lower, NULL}; + + if (!CTX_wm_project()) { + /* Special context for when there is no project. UI can draw a special panel then. */ + STRNCPY(id_lower, "no_project"); + } + else { + /* Avoid duplicating identifiers, use existing RNA enum. */ + const EnumPropertyItem *items = rna_enum_project_settings_section_items; + int i = RNA_enum_from_value(items, sproject_settings->active_section); + /* Enum value not found: File is from the future. */ + if (i == -1) { + i = 0; + } + StringRefNull id = items[i].identifier; + BLI_assert(id.size() < (int64_t)sizeof(id_lower)); + STRNCPY(id_lower, id.c_str()); + BLI_str_tolower_ascii(id_lower, strlen(id_lower)); + } + + ED_region_panels_layout_ex(C, region, ®ion->type->paneltypes, contexts, NULL); +} + +static void project_settings_main_region_listener(const wmRegionListenerParams *UNUSED(params)) +{ +} + +static void project_settings_header_region_init(wmWindowManager *UNUSED(wm), ARegion *region) +{ + ED_region_header_init(region); +} + +static void project_settings_header_region_listener(const wmRegionListenerParams *UNUSED(params)) +{ +} + +/* add handlers, stuff you only do once or on area/region changes */ +static void project_settings_navigation_region_init(wmWindowManager *wm, ARegion *region) +{ + region->v2d.scroll = V2D_SCROLL_RIGHT | V2D_SCROLL_VERTICAL_HIDE; + + ED_region_panels_init(wm, region); +} + +static void project_settings_navigation_region_draw(const bContext *C, ARegion *region) +{ + ED_region_panels(C, region); +} + +static void project_settings_navigation_region_listener( + const wmRegionListenerParams *UNUSED(params)) +{ +} + +/* add handlers, stuff you only do once or on area/region changes */ +static void project_settings_execute_region_init(wmWindowManager *wm, ARegion *region) +{ + ED_region_panels_init(wm, region); + region->v2d.keepzoom |= V2D_LOCKZOOM_X | V2D_LOCKZOOM_Y; +} + +static void project_settings_execute_region_listener(const wmRegionListenerParams *UNUSED(params)) +{ +} + +void ED_spacetype_project_settings() +{ + SpaceType *st = MEM_cnew<SpaceType>("spacetype project settings"); + + st->spaceid = SPACE_PROJECT_SETTINGS; + STRNCPY(st->name, "Project Settings"); + + st->create = project_settings_create; + st->free = project_settings_free; + st->init = project_settings_init; + st->duplicate = project_settings_duplicate; + st->listener = project_settings_listener; + st->operatortypes = project_settings_operatortypes; + st->keymap = project_settings_keymap; + st->blend_write = project_settings_blend_write; + + ARegionType *art; + + /* regions: main window */ + art = MEM_cnew<ARegionType>("spacetype project settings region"); + art->regionid = RGN_TYPE_WINDOW; + art->keymapflag = ED_KEYMAP_UI; + + art->init = project_settings_main_region_init; + art->layout = project_settings_main_region_layout; + art->draw = ED_region_panels_draw; + art->listener = project_settings_main_region_listener; + BLI_addhead(&st->regiontypes, art); + + /* regions: header */ + art = MEM_cnew<ARegionType>("spacetype project settings header region"); + art->regionid = RGN_TYPE_HEADER; + art->prefsizey = HEADERY; + art->keymapflag = ED_KEYMAP_UI | ED_KEYMAP_VIEW2D | ED_KEYMAP_HEADER; + + art->listener = project_settings_header_region_listener; + art->init = project_settings_header_region_init; + art->draw = ED_region_header; + BLI_addhead(&st->regiontypes, art); + + /* regions: navigation window */ + art = MEM_cnew<ARegionType>("spacetype project settings navigation region"); + art->regionid = RGN_TYPE_NAV_BAR; + art->prefsizex = UI_NAVIGATION_REGION_WIDTH; + art->keymapflag = ED_KEYMAP_UI | ED_KEYMAP_NAVBAR; + + art->init = project_settings_navigation_region_init; + art->draw = project_settings_navigation_region_draw; + art->listener = project_settings_navigation_region_listener; + BLI_addhead(&st->regiontypes, art); + + /* regions: execution window */ + art = MEM_cnew<ARegionType>("spacetype project settings execute region"); + art->regionid = RGN_TYPE_EXECUTE; + art->prefsizey = HEADERY; + art->keymapflag = ED_KEYMAP_UI; + + art->init = project_settings_execute_region_init; + art->layout = ED_region_panels_layout; + art->draw = ED_region_panels_draw; + art->listener = project_settings_execute_region_listener; + BLI_addhead(&st->regiontypes, art); + + BKE_spacetype_register(st); +} diff --git a/source/blender/editors/space_userpref/userpref_ops.c b/source/blender/editors/space_userpref/userpref_ops.c index a7a741b3ce2..010bddfb4a7 100644 --- a/source/blender/editors/space_userpref/userpref_ops.c +++ b/source/blender/editors/space_userpref/userpref_ops.c @@ -16,9 +16,9 @@ #endif #include "BLI_path_util.h" +#include "BKE_asset_library_custom.h" #include "BKE_context.h" #include "BKE_main.h" -#include "BKE_preferences.h" #include "BKE_report.h" @@ -134,7 +134,7 @@ static int preferences_asset_library_add_exec(bContext *UNUSED(C), wmOperator *o BLI_split_file_part(path, dirname, sizeof(dirname)); /* NULL is a valid directory path here. A library without path will be created then. */ - BKE_preferences_asset_library_add(&U, dirname, path); + BKE_asset_library_custom_add(&U.asset_libraries, dirname, path); U.runtime.is_dirty = true; /* There's no dedicated notifier for the Preferences. */ @@ -156,6 +156,7 @@ static int preferences_asset_library_add_invoke(bContext *C, return preferences_asset_library_add_exec(C, op); } +/* Similar to #PROJECT_OT_custom_asset_library_add. */ static void PREFERENCES_OT_asset_library_add(wmOperatorType *ot) { ot->name = "Add Asset Library"; @@ -185,9 +186,9 @@ static void PREFERENCES_OT_asset_library_add(wmOperatorType *ot) static int preferences_asset_library_remove_exec(bContext *UNUSED(C), wmOperator *op) { const int index = RNA_int_get(op->ptr, "index"); - bUserAssetLibrary *library = BLI_findlink(&U.asset_libraries, index); + CustomAssetLibraryDefinition *library = BLI_findlink(&U.asset_libraries, index); if (library) { - BKE_preferences_asset_library_remove(&U, library); + BKE_asset_library_custom_remove(&U.asset_libraries, library); U.runtime.is_dirty = true; /* Trigger refresh for the Asset Browser. */ WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, NULL); @@ -195,6 +196,7 @@ static int preferences_asset_library_remove_exec(bContext *UNUSED(C), wmOperator return OPERATOR_FINISHED; } +/* Similar to #PROJECT_OT_custom_asset_library_add. */ static void PREFERENCES_OT_asset_library_remove(wmOperatorType *ot) { ot->name = "Remove Asset Library"; diff --git a/source/blender/editors/util/CMakeLists.txt b/source/blender/editors/util/CMakeLists.txt index 128cb897ac4..5661fac74b2 100644 --- a/source/blender/editors/util/CMakeLists.txt +++ b/source/blender/editors/util/CMakeLists.txt @@ -68,6 +68,7 @@ set(SRC ../include/ED_paint.h ../include/ED_particle.h ../include/ED_physics.h + ../include/ED_project.h ../include/ED_render.h ../include/ED_scene.h ../include/ED_screen.h diff --git a/source/blender/makesdna/DNA_asset_types.h b/source/blender/makesdna/DNA_asset_types.h index 29795519719..2bbf38b108e 100644 --- a/source/blender/makesdna/DNA_asset_types.h +++ b/source/blender/makesdna/DNA_asset_types.h @@ -2,6 +2,9 @@ /** \file * \ingroup DNA + * + * Only contains types that need writing to files or that are accessed directly via RNA (as opposed + * to being opaque types accessed via an API). */ #pragma once @@ -88,32 +91,43 @@ typedef enum eAssetLibraryType { // ASSET_LIBRARY_PROJECT = 2, /** Display assets from custom asset libraries, as defined in the preferences - * (#bUserAssetLibrary). The name will be taken from #FileSelectParams.asset_library_ref.idname - * then. - * In RNA, we add the index of the custom library to this to identify it by index. So keep - * this last! */ - ASSET_LIBRARY_CUSTOM = 100, + * (#CustomAssetLibraryDefinition). In RNA, we add the index of the custom library to this to + * identify it by index. */ + ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES = 100, + /** Same as #ASSET_LIBRARY_CUSTOM_FROM_PREFERENCES, but the library is defined for a specific + project only. */ + ASSET_LIBRARY_CUSTOM_FROM_PROJECT = 500, } eAssetLibraryType; /** - * Information to identify an asset library. May be either one of the predefined types (current - * 'Main', builtin library, project library), or a custom type as defined in the Preferences. - * - * If the type is set to #ASSET_LIBRARY_CUSTOM, `custom_library_index` must be set to identify the - * custom library. Otherwise it is not used. + * Information to identify an asset library. May be either one of the predefined types ("Current + * File" builtin library), or a custom type as defined in the Preferences/Project. */ typedef struct AssetLibraryReference { + /* If set to #ASSET_LIBRARY_CUSTOM_XXX, `custom_library_index` must be set to identify + * the custom library. Otherwise it is not used. */ short type; /* eAssetLibraryType */ char _pad1[2]; /** - * If showing a custom asset library (#ASSET_LIBRARY_CUSTOM), this is the index of the - * #bUserAssetLibrary within #UserDef.asset_libraries. + * If showing a custom asset library (#ASSET_LIBRARY_CUSTOM_XXX), this is the index of the + * #CustomAssetLibraryDefinition within its owner (preferences or project settings). * Should be ignored otherwise (but better set to -1 then, for sanity and debugging). */ int custom_library_index; } AssetLibraryReference; /** + * Custom asset libraries can be registered in the preferences or project settings. This is the + * container to hold these settings. + */ +typedef struct CustomAssetLibraryDefinition { + struct CustomAssetLibraryDefinition *next, *prev; + + char name[64]; /* MAX_NAME */ + char path[1024]; /* FILE_MAX */ +} CustomAssetLibraryDefinition; + +/** * Not part of the core design, we should try to get rid of it. Only needed to wrap FileDirEntry * into a type with PropertyGroup as base, so we can have an RNA collection of #AssetHandle's to * pass to the UI. diff --git a/source/blender/makesdna/DNA_space_types.h b/source/blender/makesdna/DNA_space_types.h index 4bb92e6fcc5..d05ba0f7e2e 100644 --- a/source/blender/makesdna/DNA_space_types.h +++ b/source/blender/makesdna/DNA_space_types.h @@ -1083,6 +1083,8 @@ typedef enum eFileSel_File_Types { FILE_TYPE_VOLUME = (1 << 19), FILE_TYPE_ASSET = (1 << 28), + /** Directory is a Blender project root directory. */ + FILE_TYPE_BLENDER_PROJECT = (1 << 29), /** An FS directory (i.e. S_ISDIR on its path is true). */ FILE_TYPE_DIR = (1 << 30), FILE_TYPE_BLENDERLIB = (1u << 31), @@ -1722,6 +1724,30 @@ typedef struct SpaceUserPref { /** \} */ /* -------------------------------------------------------------------- */ +/** \name Project Settings + * \{ */ + +typedef struct SpaceProjectSettings { + SpaceLink *next, *prev; + /** Storage of regions for inactive spaces. */ + ListBase regionbase; + char spacetype; + char link_flag; + char _pad0[6]; + /* End 'SpaceLink' header. */ + + char active_section; /* eSpaceProjectSettings_Section */ + char _pad1[7]; +} SpaceProjectSettings; + +typedef enum eSpaceProjectSettings_Section { + PROJECT_SETTINGS_SECTION_GENERAL = 0, + PROJECT_SETTINGS_SECTION_ASSET_LIBRARIES = 1, +} eSpaceProjectSettings_Section; + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Motion Tracking * \{ */ @@ -2057,9 +2083,10 @@ typedef enum eSpace_Type { SPACE_CLIP = 20, SPACE_TOPBAR = 21, SPACE_STATUSBAR = 22, - SPACE_SPREADSHEET = 23 + SPACE_SPREADSHEET = 23, + SPACE_PROJECT_SETTINGS = 24 -#define SPACE_TYPE_NUM (SPACE_SPREADSHEET + 1) +#define SPACE_TYPE_NUM (SPACE_PROJECT_SETTINGS + 1) } eSpace_Type; /* use for function args */ diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index 9d8b75450ca..5a6c7992a3e 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -487,6 +487,7 @@ typedef struct bTheme { ThemeSpace space_outliner; ThemeSpace space_node; ThemeSpace space_preferences; + ThemeSpace space_project_settings; ThemeSpace space_console; ThemeSpace space_clip; ThemeSpace space_topbar; @@ -571,13 +572,6 @@ enum { USER_MENU_TYPE_PROP = 4, }; -typedef struct bUserAssetLibrary { - struct bUserAssetLibrary *next, *prev; - - char name[64]; /* MAX_NAME */ - char path[1024]; /* FILE_MAX */ -} bUserAssetLibrary; - typedef struct SolidLight { int flag; float smooth; @@ -653,7 +647,8 @@ typedef struct UserDef_Experimental { char use_sculpt_texture_paint; char use_draw_manager_acquire_lock; char use_realtime_compositor; - char _pad[7]; + char use_blender_projects; + char _pad[6]; /** `makesdna` does not allow empty structs. */ } UserDef_Experimental; @@ -771,7 +766,7 @@ typedef struct UserDef { struct ListBase autoexec_paths; /** #bUserMenu. */ struct ListBase user_menus; - /** #bUserAssetLibrary */ + /** #CustomAssetLibraryDefinition */ struct ListBase asset_libraries; char keyconfigstr[64]; diff --git a/source/blender/makesdna/intern/dna_rename_defs.h b/source/blender/makesdna/intern/dna_rename_defs.h index 4a009a88f1a..864fbc45b13 100644 --- a/source/blender/makesdna/intern/dna_rename_defs.h +++ b/source/blender/makesdna/intern/dna_rename_defs.h @@ -41,6 +41,7 @@ DNA_STRUCT_RENAME(Lamp, Light) DNA_STRUCT_RENAME(SpaceButs, SpaceProperties) DNA_STRUCT_RENAME(SpaceIpo, SpaceGraph) DNA_STRUCT_RENAME(SpaceOops, SpaceOutliner) +DNA_STRUCT_RENAME(bUserAssetLibrary, CustomAssetLibraryDefinition) DNA_STRUCT_RENAME_ELEM(BPoint, alfa, tilt) DNA_STRUCT_RENAME_ELEM(BezTriple, alfa, tilt) DNA_STRUCT_RENAME_ELEM(Bone, curveInX, curve_in_x) diff --git a/source/blender/makesrna/RNA_enum_items.h b/source/blender/makesrna/RNA_enum_items.h index 53241c4a809..7c7b2c9b985 100644 --- a/source/blender/makesrna/RNA_enum_items.h +++ b/source/blender/makesrna/RNA_enum_items.h @@ -206,6 +206,8 @@ DEF_ENUM(rna_enum_context_mode_items) DEF_ENUM(rna_enum_preference_section_items) +DEF_ENUM(rna_enum_project_settings_section_items) + DEF_ENUM(rna_enum_attribute_type_items) DEF_ENUM(rna_enum_color_attribute_type_items) DEF_ENUM(rna_enum_attribute_type_with_auto_items) diff --git a/source/blender/makesrna/intern/CMakeLists.txt b/source/blender/makesrna/intern/CMakeLists.txt index f028b199098..4fbb246e056 100644 --- a/source/blender/makesrna/intern/CMakeLists.txt +++ b/source/blender/makesrna/intern/CMakeLists.txt @@ -20,6 +20,7 @@ set(DEFSRC rna_armature.c rna_asset.c rna_attribute.c + rna_blender_project.c rna_boid.c rna_brush.c rna_cachefile.c diff --git a/source/blender/makesrna/intern/makesrna.c b/source/blender/makesrna/intern/makesrna.c index 3592ecd84c8..95f20592152 100644 --- a/source/blender/makesrna/intern/makesrna.c +++ b/source/blender/makesrna/intern/makesrna.c @@ -4494,6 +4494,7 @@ static RNAProcessItem PROCESS_ITEMS[] = { {"rna_armature.c", "rna_armature_api.c", RNA_def_armature}, {"rna_attribute.c", NULL, RNA_def_attribute}, {"rna_asset.c", NULL, RNA_def_asset}, + {"rna_blender_project.c", NULL, RNA_def_blender_project}, {"rna_boid.c", NULL, RNA_def_boid}, {"rna_brush.c", NULL, RNA_def_brush}, {"rna_cachefile.c", NULL, RNA_def_cachefile}, diff --git a/source/blender/makesrna/intern/rna_asset.c b/source/blender/makesrna/intern/rna_asset.c index 3c7eb3d208f..7fbb6926cf1 100644 --- a/source/blender/makesrna/intern/rna_asset.c +++ b/source/blender/makesrna/intern/rna_asset.c @@ -19,6 +19,7 @@ # include "BKE_asset.h" # include "BKE_asset_library.h" +# include "BKE_asset_library_custom.h" # include "BKE_context.h" # include "BKE_idprop.h" @@ -30,6 +31,9 @@ # include "RNA_access.h" +# include "WM_api.h" +# include "WM_types.h" + static bool rna_AssetMetaData_editable_from_owner_id(const ID *owner_id, const AssetMetaData *asset_data, const char **r_info) @@ -254,6 +258,35 @@ void rna_AssetMetaData_catalog_id_update(struct bContext *C, struct PointerRNA * BKE_asset_library_refresh_catalog_simplename(asset_library, asset_data); } +static void rna_CustomAssetLibraryDefinition_name_set(PointerRNA *ptr, const char *value) +{ + CustomAssetLibraryDefinition *library = (CustomAssetLibraryDefinition *)ptr->data; + + /* We can't cleanly access the owning listbase here, but reconstructing the list from the link is + * fine. */ + ListBase asset_libraries = BLI_listbase_from_link((Link *)library); + BKE_asset_library_custom_name_set(&asset_libraries, library, value); +} + +static void rna_CustomAssetLibraryDefinition_path_set(PointerRNA *ptr, const char *value) +{ + CustomAssetLibraryDefinition *library = (CustomAssetLibraryDefinition *)ptr->data; + + char dirpath[FILE_MAX]; + BLI_strncpy(dirpath, value, sizeof(dirpath)); + if (BLI_is_file(dirpath)) { + BLI_path_parent_dir(dirpath); + } + BKE_asset_library_custom_path_set(library, dirpath); +} + +void rna_AssetLibrary_settings_update(Main *UNUSED(bmain), + Scene *UNUSED(scene), + PointerRNA *UNUSED(ptr)) +{ + WM_main_add_notifier(NC_ASSET | ND_ASSET_LIBRARY, NULL); +} + static PointerRNA rna_AssetHandle_file_data_get(PointerRNA *ptr) { AssetHandle *asset_handle = ptr->data; @@ -485,6 +518,30 @@ static void rna_def_asset_library_reference(BlenderRNA *brna) srna, "Asset Library Reference", "Identifier to refer to the asset library"); } +static void rna_def_asset_library_reference_custom(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "CustomAssetLibraryDefinition", NULL); + RNA_def_struct_clear_flag(srna, STRUCT_UNDO); + RNA_def_struct_ui_text( + srna, "Asset Library", "Settings to define a reusable library for Asset Browsers to use"); + + prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); + RNA_def_property_ui_text( + prop, "Name", "Identifier (not necessarily unique) for the asset library"); + RNA_def_property_string_funcs(prop, NULL, NULL, "rna_CustomAssetLibraryDefinition_name_set"); + RNA_def_struct_name_property(srna, prop); + RNA_def_property_update(prop, 0, "rna_AssetLibrary_settings_update"); + + prop = RNA_def_property(srna, "path", PROP_STRING, PROP_DIRPATH); + RNA_def_property_ui_text( + prop, "Path", "Path to a directory with .blend files to use as an asset library"); + RNA_def_property_string_funcs(prop, NULL, NULL, "rna_CustomAssetLibraryDefinition_path_set"); + RNA_def_property_update(prop, 0, "rna_AssetLibrary_settings_update"); +} + PropertyRNA *rna_def_asset_library_reference_common(struct StructRNA *srna, const char *get, const char *set) @@ -503,6 +560,7 @@ void RNA_def_asset(BlenderRNA *brna) rna_def_asset_tag(brna); rna_def_asset_data(brna); rna_def_asset_library_reference(brna); + rna_def_asset_library_reference_custom(brna); rna_def_asset_handle(brna); rna_def_asset_catalog_path(brna); diff --git a/source/blender/makesrna/intern/rna_blender_project.c b/source/blender/makesrna/intern/rna_blender_project.c new file mode 100644 index 00000000000..b8fc7f58bc7 --- /dev/null +++ b/source/blender/makesrna/intern/rna_blender_project.c @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup RNA + */ + +#include "RNA_define.h" + +#include "rna_internal.h" + +#ifdef RNA_RUNTIME + +# include "BKE_asset_library_custom.h" +# include "BKE_blender_project.h" + +# include "BLT_translation.h" + +# include "WM_api.h" + +static void rna_BlenderProject_update(Main *UNUSED(bmain), + Scene *UNUSED(scene), + PointerRNA *UNUSED(ptr)) +{ + /* TODO evaluate which props should send which notifiers. */ + /* Force full redraw of all windows. */ + WM_main_add_notifier(NC_WINDOW, NULL); +} + +static void rna_BlenderProject_name_get(PointerRNA *ptr, char *value) +{ + BlenderProject *project = ptr->data; + if (!project) { + value[0] = '\0'; + return; + } + + strcpy(value, BKE_project_name_get(project)); +} + +static int rna_BlenderProject_name_length(PointerRNA *ptr) +{ + BlenderProject *project = ptr->data; + if (!project) { + return 0; + } + + return strlen(BKE_project_name_get(project)); +} + +static void rna_BlenderProject_name_set(PointerRNA *ptr, const char *value) +{ + BlenderProject *project = ptr->data; + + if (!project) { + return; + } + + BKE_project_name_set(project, value); +} + +static void rna_BlenderProject_root_path_get(PointerRNA *ptr, char *value) +{ + BlenderProject *project = ptr->data; + if (!project) { + value[0] = '\0'; + return; + } + + strcpy(value, BKE_project_root_path_get(project)); +} + +static int rna_BlenderProject_root_path_length(PointerRNA *ptr) +{ + BlenderProject *project = ptr->data; + if (!project) { + return 0; + } + + return strlen(BKE_project_root_path_get(project)); +} + +static void rna_BlenderProject_root_path_set(PointerRNA *UNUSED(ptr), const char *UNUSED(value)) +{ + /* Property is not editable, see #rna_BlenderProject_root_path_editable(). */ + BLI_assert_unreachable(); +} + +static int rna_BlenderProject_root_path_editable(PointerRNA *UNUSED(ptr), const char **r_info) +{ + /* Path is never editable (setting up a project is an operation), but return a nicer disabled + * hint. */ + *r_info = N_("Project location cannot be changed, displayed for informal purposes only"); + return 0; +} + +static void rna_BlenderProject_asset_libraries_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + BlenderProject *project = ptr->data; + ListBase *asset_libraries = BKE_project_custom_asset_libraries_get(project); + rna_iterator_listbase_begin(iter, asset_libraries, NULL); +} + +static bool rna_BlenderProject_is_dirty_get(PointerRNA *ptr) +{ + const BlenderProject *project = ptr->data; + return BKE_project_has_unsaved_changes(project); +} + +#else + +void RNA_def_blender_project(BlenderRNA *brna) +{ + StructRNA *srna = RNA_def_struct(brna, "BlenderProject", NULL); + RNA_def_struct_ui_text(srna, "Blender Project", ""); + + PropertyRNA *prop; + + prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); + RNA_def_property_string_funcs(prop, + "rna_BlenderProject_name_get", + "rna_BlenderProject_name_length", + "rna_BlenderProject_name_set"); + RNA_def_property_ui_text(prop, "Name", "The identifier for the project"); + RNA_def_struct_name_property(srna, prop); + RNA_def_property_update(prop, 0, "rna_BlenderProject_update"); + + prop = RNA_def_property(srna, "root_path", PROP_STRING, PROP_NONE); + RNA_def_property_string_funcs(prop, + "rna_BlenderProject_root_path_get", + "rna_BlenderProject_root_path_length", + "rna_BlenderProject_root_path_set"); + RNA_def_property_editable_func(prop, "rna_BlenderProject_root_path_editable"); + RNA_def_property_ui_text(prop, "Location", "The location of the project on disk"); + + prop = RNA_def_property(srna, "asset_libraries", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "CustomAssetLibraryDefinition"); + RNA_def_property_collection_funcs(prop, + "rna_BlenderProject_asset_libraries_begin", + "rna_iterator_listbase_next", + "rna_iterator_listbase_end", + "rna_iterator_listbase_get", + NULL, + NULL, + NULL, + NULL); + RNA_def_property_ui_text(prop, "Asset Libraries", ""); + + prop = RNA_def_property(srna, "is_dirty", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_funcs(prop, "rna_BlenderProject_is_dirty_get", NULL); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_ui_text( + prop, + "Dirty", + "Project settings have changed since read from disk. Save the settings to keep them"); +} + +#endif diff --git a/source/blender/makesrna/intern/rna_context.c b/source/blender/makesrna/intern/rna_context.c index e723be2ab71..2b910a9eaac 100644 --- a/source/blender/makesrna/intern/rna_context.c +++ b/source/blender/makesrna/intern/rna_context.c @@ -203,6 +203,15 @@ static PointerRNA rna_Context_preferences_get(PointerRNA *UNUSED(ptr)) return newptr; } +static PointerRNA rna_Context_project_get(PointerRNA *UNUSED(ptr)) +{ + struct BlenderProject *project = CTX_wm_project(); + + PointerRNA newptr; + RNA_pointer_create(NULL, &RNA_BlenderProject, project, &newptr); + return newptr; +} + static int rna_Context_mode_get(PointerRNA *ptr) { bContext *C = (bContext *)ptr->data; @@ -338,6 +347,11 @@ void RNA_def_context(BlenderRNA *brna) RNA_def_property_struct_type(prop, "Preferences"); RNA_def_property_pointer_funcs(prop, "rna_Context_preferences_get", NULL, NULL, NULL); + prop = RNA_def_property(srna, "project", PROP_POINTER, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_struct_type(prop, "BlenderProject"); + RNA_def_property_pointer_funcs(prop, "rna_Context_project_get", NULL, NULL, NULL); + prop = RNA_def_property(srna, "mode", PROP_ENUM, PROP_NONE); RNA_def_property_enum_items(prop, rna_enum_context_mode_items); RNA_def_property_clear_flag(prop, PROP_EDITABLE); diff --git a/source/blender/makesrna/intern/rna_internal.h b/source/blender/makesrna/intern/rna_internal.h index ea829e5cd86..9b7534d081d 100644 --- a/source/blender/makesrna/intern/rna_internal.h +++ b/source/blender/makesrna/intern/rna_internal.h @@ -144,6 +144,7 @@ void RNA_def_animviz(struct BlenderRNA *brna); void RNA_def_armature(struct BlenderRNA *brna); void RNA_def_attribute(struct BlenderRNA *brna); void RNA_def_asset(struct BlenderRNA *brna); +void RNA_def_blender_project(struct BlenderRNA *brna); void RNA_def_boid(struct BlenderRNA *brna); void RNA_def_brush(struct BlenderRNA *brna); void RNA_def_cachefile(struct BlenderRNA *brna); diff --git a/source/blender/makesrna/intern/rna_screen.c b/source/blender/makesrna/intern/rna_screen.c index a65bd613ecf..2051045cd73 100644 --- a/source/blender/makesrna/intern/rna_screen.c +++ b/source/blender/makesrna/intern/rna_screen.c @@ -186,6 +186,10 @@ static const EnumPropertyItem *rna_Area_ui_type_itemf(bContext *C, continue; } + if (!U.experimental.use_blender_projects && (item_from->value == SPACE_PROJECT_SETTINGS)) { + continue; + } + SpaceType *st = item_from->identifier[0] ? BKE_spacetype_from_id(item_from->value) : NULL; int totitem_prev = totitem; if (st && st->space_subtype_item_extend != NULL) { diff --git a/source/blender/makesrna/intern/rna_space.c b/source/blender/makesrna/intern/rna_space.c index b2663b89333..2ac30c44ea8 100644 --- a/source/blender/makesrna/intern/rna_space.c +++ b/source/blender/makesrna/intern/rna_space.c @@ -171,11 +171,16 @@ const EnumPropertyItem rna_enum_space_type_items[] = { ICON_SPREADSHEET, "Spreadsheet", "Explore geometry data in a table"}, + {SPACE_PROJECT_SETTINGS, + "PROJECT_SETTINGS", + ICON_PREFERENCES, + "Project Settings", + "Edit persistent configuration settings for the active project"}, {SPACE_USERPREF, "PREFERENCES", ICON_PREFERENCES, "Preferences", - "Edit persistent configuration settings"}, + "Edit persistent configuration settings for personal use"}, {0, NULL, 0, NULL, NULL}, }; @@ -528,6 +533,12 @@ static const EnumPropertyItem rna_enum_curve_display_handle_items[] = { {0, NULL, 0, NULL, NULL}, }; +const EnumPropertyItem rna_enum_project_settings_section_items[] = { + {PROJECT_SETTINGS_SECTION_GENERAL, "GENERAL", 0, "General", ""}, + {PROJECT_SETTINGS_SECTION_ASSET_LIBRARIES, "ASSET_LIBRARIES", 0, "Asset Libraries", ""}, + {0, NULL, 0, NULL, NULL}, +}; + #ifdef RNA_RUNTIME # include "DNA_anim_types.h" @@ -549,7 +560,6 @@ static const EnumPropertyItem rna_enum_curve_display_handle_items[] = { # include "BKE_layer.h" # include "BKE_nla.h" # include "BKE_paint.h" -# include "BKE_preferences.h" # include "BKE_scene.h" # include "BKE_screen.h" # include "BKE_workspace.h" @@ -609,6 +619,8 @@ static StructRNA *rna_Space_refine(struct PointerRNA *ptr) return &RNA_SpaceConsole; case SPACE_USERPREF: return &RNA_SpacePreferences; + case SPACE_PROJECT_SETTINGS: + return &RNA_SpaceProjectSettings; case SPACE_CLIP: return &RNA_SpaceClipEditor; case SPACE_SPREADSHEET: @@ -7159,6 +7171,20 @@ static void rna_def_space_userpref(BlenderRNA *brna) RNA_def_property_ui_text(prop, "Filter", "Search term for filtering in the UI"); } +static void rna_def_space_project_settings(BlenderRNA *brna) +{ + StructRNA *srna; + + srna = RNA_def_struct(brna, "SpaceProjectSettings", "Space"); + RNA_def_struct_ui_text(srna, "Space Project Settings", "Blender project space data"); + + PropertyRNA *prop; + + prop = RNA_def_property(srna, "active_section", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_items(prop, rna_enum_project_settings_section_items); + RNA_def_property_ui_text(prop, "Active Section", "Choose the category of options to display"); +} + static void rna_def_node_tree_path(BlenderRNA *brna) { StructRNA *srna; @@ -8078,6 +8104,7 @@ void RNA_def_space(BlenderRNA *brna) rna_def_console_line(brna); rna_def_space_info(brna); rna_def_space_userpref(brna); + rna_def_space_project_settings(brna); rna_def_node_tree_path(brna); rna_def_space_node(brna); rna_def_space_clip(brna); diff --git a/source/blender/makesrna/intern/rna_userdef.c b/source/blender/makesrna/intern/rna_userdef.c index 58189ab2478..72722053a64 100644 --- a/source/blender/makesrna/intern/rna_userdef.c +++ b/source/blender/makesrna/intern/rna_userdef.c @@ -154,7 +154,6 @@ static const EnumPropertyItem rna_enum_userdef_viewport_aa_items[] = { # include "BKE_object.h" # include "BKE_paint.h" # include "BKE_pbvh.h" -# include "BKE_preferences.h" # include "BKE_screen.h" # include "DEG_depsgraph.h" @@ -308,18 +307,6 @@ static void rna_userdef_language_update(Main *UNUSED(bmain), USERDEF_TAG_DIRTY; } -static void rna_userdef_asset_library_name_set(PointerRNA *ptr, const char *value) -{ - bUserAssetLibrary *library = (bUserAssetLibrary *)ptr->data; - BKE_preferences_asset_library_name_set(&U, library, value); -} - -static void rna_userdef_asset_library_path_set(PointerRNA *ptr, const char *value) -{ - bUserAssetLibrary *library = (bUserAssetLibrary *)ptr->data; - BKE_preferences_asset_library_path_set(library, value); -} - static void rna_userdef_script_autoexec_update(Main *UNUSED(bmain), Scene *UNUSED(scene), PointerRNA *ptr) @@ -2514,6 +2501,21 @@ static void rna_def_userdef_theme_space_userpref(BlenderRNA *brna) rna_def_userdef_theme_spaces_main(srna); } +static void rna_def_userdef_theme_space_project_settings(BlenderRNA *brna) +{ + StructRNA *srna; + + /* space_userpref */ + + srna = RNA_def_struct(brna, "ThemeProjectSettings", NULL); + RNA_def_struct_sdna(srna, "ThemeSpace"); + RNA_def_struct_clear_flag(srna, STRUCT_UNDO); + RNA_def_struct_ui_text( + srna, "Theme Project Settings", "Theme settings for the Blender project settings editor"); + + rna_def_userdef_theme_spaces_main(srna); +} + static void rna_def_userdef_theme_space_console(BlenderRNA *brna) { StructRNA *srna; @@ -3854,6 +3856,8 @@ static void rna_def_userdef_themes(BlenderRNA *brna) {11, "PROPERTIES", ICON_PROPERTIES, "Properties", ""}, {12, "OUTLINER", ICON_OUTLINER, "Outliner", ""}, {14, "PREFERENCES", ICON_PREFERENCES, "Preferences", ""}, + /* TODO icon */ + {24, "PROJECT_SETTINGS", ICON_PREFERENCES, "Project Settings", ""}, {15, "INFO", ICON_INFO, "Info", ""}, {16, "FILE_BROWSER", ICON_FILEBROWSER, "File Browser", ""}, {17, "CONSOLE", ICON_CONSOLE, "Python Console", ""}, @@ -3966,6 +3970,12 @@ static void rna_def_userdef_themes(BlenderRNA *brna) RNA_def_property_struct_type(prop, "ThemePreferences"); RNA_def_property_ui_text(prop, "Preferences", ""); + prop = RNA_def_property(srna, "project_settings", PROP_POINTER, PROP_NONE); + RNA_def_property_flag(prop, PROP_NEVER_NULL); + RNA_def_property_pointer_sdna(prop, NULL, "space_project_settings"); + RNA_def_property_struct_type(prop, "ThemeProjectSettings"); + RNA_def_property_ui_text(prop, "Project Settings", ""); + prop = RNA_def_property(srna, "console", PROP_POINTER, PROP_NONE); RNA_def_property_flag(prop, PROP_NEVER_NULL); RNA_def_property_pointer_sdna(prop, NULL, "space_console"); @@ -4252,6 +4262,7 @@ static void rna_def_userdef_dothemes(BlenderRNA *brna) rna_def_userdef_theme_space_outliner(brna); rna_def_userdef_theme_space_info(brna); rna_def_userdef_theme_space_userpref(brna); + rna_def_userdef_theme_space_project_settings(brna); rna_def_userdef_theme_space_console(brna); rna_def_userdef_theme_space_clip(brna); rna_def_userdef_theme_space_topbar(brna); @@ -6051,31 +6062,6 @@ static void rna_def_userdef_keymap(BlenderRNA *brna) RNA_def_property_ui_text(prop, "Key Config", "The name of the active key configuration"); } -static void rna_def_userdef_filepaths_asset_library(BlenderRNA *brna) -{ - StructRNA *srna; - PropertyRNA *prop; - - srna = RNA_def_struct(brna, "UserAssetLibrary", NULL); - RNA_def_struct_sdna(srna, "bUserAssetLibrary"); - RNA_def_struct_clear_flag(srna, STRUCT_UNDO); - RNA_def_struct_ui_text( - srna, "Asset Library", "Settings to define a reusable library for Asset Browsers to use"); - - prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); - RNA_def_property_ui_text( - prop, "Name", "Identifier (not necessarily unique) for the asset library"); - RNA_def_property_string_funcs(prop, NULL, NULL, "rna_userdef_asset_library_name_set"); - RNA_def_struct_name_property(srna, prop); - RNA_def_property_update(prop, 0, "rna_userdef_update"); - - prop = RNA_def_property(srna, "path", PROP_STRING, PROP_DIRPATH); - RNA_def_property_ui_text( - prop, "Path", "Path to a directory with .blend files to use as an asset library"); - RNA_def_property_string_funcs(prop, NULL, NULL, "rna_userdef_asset_library_path_set"); - RNA_def_property_update(prop, 0, "rna_userdef_update"); -} - static void rna_def_userdef_filepaths(BlenderRNA *brna) { PropertyRNA *prop; @@ -6256,10 +6242,8 @@ static void rna_def_userdef_filepaths(BlenderRNA *brna) RNA_def_property_enum_items(prop, preview_type_items); RNA_def_property_ui_text(prop, "File Preview Type", "What type of blend preview to create"); - rna_def_userdef_filepaths_asset_library(brna); - prop = RNA_def_property(srna, "asset_libraries", PROP_COLLECTION, PROP_NONE); - RNA_def_property_struct_type(prop, "UserAssetLibrary"); + RNA_def_property_struct_type(prop, "CustomAssetLibraryDefinition"); RNA_def_property_ui_text(prop, "Asset Libraries", ""); } @@ -6385,6 +6369,12 @@ static void rna_def_userdef_experimental(BlenderRNA *brna) RNA_def_property_ui_text( prop, "Override Templates", "Enable library override template in the python API"); + prop = RNA_def_property(srna, "use_blender_projects", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_ui_text(prop, + "Blender Projects", + "Enable support for Blender project directories, consisting out of " + "multiple .blend files and dedicated project settings"); + prop = RNA_def_property(srna, "enable_eevee_next", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_sdna(prop, NULL, "enable_eevee_next", 1); RNA_def_property_ui_text(prop, "EEVEE Next", "Enable the new EEVEE codebase, requires restart"); diff --git a/source/blender/python/intern/bpy_rna_callback.c b/source/blender/python/intern/bpy_rna_callback.c index 4409e3ae828..f7f038d8efd 100644 --- a/source/blender/python/intern/bpy_rna_callback.c +++ b/source/blender/python/intern/bpy_rna_callback.c @@ -239,6 +239,9 @@ static eSpace_Type rna_Space_refine_reverse(StructRNA *srna) if (srna == &RNA_SpacePreferences) { return SPACE_USERPREF; } + if (srna == &RNA_SpaceProjectSettings) { + return SPACE_PROJECT_SETTINGS; + } if (srna == &RNA_SpaceClipEditor) { return SPACE_CLIP; } diff --git a/source/blender/windowmanager/WM_types.h b/source/blender/windowmanager/WM_types.h index e4050397aaa..038438ff151 100644 --- a/source/blender/windowmanager/WM_types.h +++ b/source/blender/windowmanager/WM_types.h @@ -355,6 +355,8 @@ typedef struct wmNotifier { #define NC_ASSET (27 << 24) /* Changes to the active viewer path. */ #define NC_VIEWER_PATH (28 << 24) +/* Changes related to the active project. */ +#define NC_PROJECT (29 << 24) /* data type, 256 entries is enough, it can overlap */ #define NOTE_DATA 0x00FF0000 @@ -496,6 +498,9 @@ typedef struct wmNotifier { * reloading of asset libraries & their catalogs should happen. That only happens on explicit user * action. */ #define ND_ASSET_CATALOGS (4 << 16) +/* Some settings of an asset library were changed, and UIs showing asset library information should + * redraw. */ +#define ND_ASSET_LIBRARY (5 << 16) /* subtype, 256 entries too */ #define NOTE_SUBTYPE 0x0000FF00 diff --git a/source/blender/windowmanager/intern/wm_draw.c b/source/blender/windowmanager/intern/wm_draw.c index 663a41212ba..8cd13b572d1 100644 --- a/source/blender/windowmanager/intern/wm_draw.c +++ b/source/blender/windowmanager/intern/wm_draw.c @@ -536,6 +536,7 @@ static const char *wm_area_name(ScrArea *area) SPACE_NAME(SPACE_CLIP); SPACE_NAME(SPACE_TOPBAR); SPACE_NAME(SPACE_STATUSBAR); + SPACE_NAME(SPACE_PROJECT_SETTINGS); default: return "Unknown Space"; } @@ -1344,6 +1345,9 @@ void wm_draw_update(bContext *C) BKE_image_free_unused_gpu_textures(); + /* Subscribe to messages when drawing, #ED_region_do_draw() does the same on region level. */ + wm_messages_subscribe(wm); + LISTBASE_FOREACH (wmWindow *, win, &wm->windows) { #ifdef WIN32 GHOST_TWindowState state = GHOST_GetWindowState(win->ghostwin); diff --git a/source/blender/windowmanager/intern/wm_files.c b/source/blender/windowmanager/intern/wm_files.c index b009a67efba..1d55e4369f8 100644 --- a/source/blender/windowmanager/intern/wm_files.c +++ b/source/blender/windowmanager/intern/wm_files.c @@ -62,6 +62,7 @@ #include "BKE_asset_library.h" #include "BKE_autoexec.h" #include "BKE_blender.h" +#include "BKE_blender_project.h" #include "BKE_blendfile.h" #include "BKE_callbacks.h" #include "BKE_context.h" @@ -159,7 +160,8 @@ void WM_file_tag_modified(void) bool wm_file_or_session_data_has_unsaved_changes(const Main *bmain, const wmWindowManager *wm) { return !wm->file_saved || ED_image_should_save_modified(bmain) || - BKE_asset_library_has_any_unsaved_catalogs(); + BKE_asset_library_has_any_unsaved_catalogs() || + BKE_project_has_unsaved_changes(CTX_wm_project()); } /** \} */ @@ -3150,6 +3152,16 @@ static int wm_save_as_mainfile_exec(bContext *C, wmOperator *op) return OPERATOR_CANCELLED; } + const BlenderProject *active_project = CTX_wm_project(); + if (active_project && !BKE_project_contains_path(path)) { + BKE_reportf( + op->reports, + RPT_WARNING, + "File saved outside of the active project's path (\"%s\"). Active project changed.", + BKE_project_root_path_get(active_project)); + /* Don't cancel. Otherwise there's no way to save files outside of the active project. */ + } + const int fileflags_orig = G.fileflags; int fileflags = G.fileflags; @@ -3565,6 +3577,7 @@ void wm_test_autorun_warning(bContext *C) * \{ */ static char save_images_when_file_is_closed = true; +static char save_project_settings_when_file_is_closed = true; static void wm_block_file_close_cancel(bContext *C, void *arg_block, void *UNUSED(arg_data)) { @@ -3607,6 +3620,12 @@ static void wm_block_file_close_save(bContext *C, void *arg_block, void *arg_dat } } + BlenderProject *project = CTX_wm_project(); + if (project && BKE_project_has_unsaved_changes(project) && + save_project_settings_when_file_is_closed) { + BKE_project_settings_save(project); + } + bool file_has_been_saved_before = BKE_main_blendfile_path(bmain)[0] != '\0'; if (file_has_been_saved_before) { @@ -3746,6 +3765,30 @@ static uiBlock *block_create__close_file_dialog(struct bContext *C, has_extra_checkboxes = true; } + if (BKE_project_has_unsaved_changes(CTX_wm_project())) { + /* Only the first checkbox should get extra separation. */ + if (!has_extra_checkboxes) { + uiItemS(layout); + } + + uiDefButBitC(block, + UI_BTYPE_CHECKBOX, + 1, + 0, + "Save modified project settings", + 0, + 0, + 0, + UI_UNIT_Y, + &save_project_settings_when_file_is_closed, + 0, + 0, + 0, + 0, + ""); + has_extra_checkboxes = true; + } + if (BKE_asset_library_has_any_unsaved_catalogs()) { static char save_catalogs_when_file_is_closed; diff --git a/source/blender/windowmanager/intern/wm_window.c b/source/blender/windowmanager/intern/wm_window.c index 265aa08a6b1..292d58b80d1 100644 --- a/source/blender/windowmanager/intern/wm_window.c +++ b/source/blender/windowmanager/intern/wm_window.c @@ -28,6 +28,7 @@ #include "BLT_translation.h" +#include "BKE_blender_project.h" #include "BKE_context.h" #include "BKE_global.h" #include "BKE_icons.h" @@ -42,6 +43,7 @@ #include "RNA_enum_types.h" #include "WM_api.h" +#include "WM_message.h" #include "WM_types.h" #include "wm.h" #include "wm_draw.h" @@ -450,6 +452,8 @@ void wm_window_close(bContext *C, wmWindowManager *wm, wmWindow *win) void wm_window_title(wmWindowManager *wm, wmWindow *win) { +#define MAX_PROJECT_NAME_HINT (MAX_NAME + 4) + if (WM_window_is_temp_screen(win)) { /* Nothing to do for 'temp' windows, * because #WM_window_open always sets window title. */ @@ -458,11 +462,23 @@ void wm_window_title(wmWindowManager *wm, wmWindow *win) /* this is set to 1 if you don't have startup.blend open */ const char *blendfile_path = BKE_main_blendfile_path_from_global(); if (blendfile_path[0] != '\0') { - char str[sizeof(((Main *)NULL)->filepath) + 24]; + char project_name_hint[MAX_PROJECT_NAME_HINT] = ""; + char str[sizeof(((Main *)NULL)->filepath) + sizeof(project_name_hint) + 24]; + + struct BlenderProject *project = CTX_wm_project(); + if (project) { + const char *name = BKE_project_name_get(project); + BLI_snprintf(project_name_hint, + sizeof(project_name_hint), + "%s - ", + (name && name[0]) ? name : IFACE_("Unnamed project")); + } + BLI_snprintf(str, sizeof(str), - "Blender%s [%s%s]", + "Blender%s [%s%s%s]", wm->file_saved ? "" : "*", + project_name_hint, blendfile_path, G_MAIN->recovered ? " (Recovered)" : ""); GHOST_SetTitle(win->ghostwin, str); @@ -476,6 +492,8 @@ void wm_window_title(wmWindowManager *wm, wmWindow *win) * terminate request (e.g. OS Shortcut Alt+F4, Command+Q, (...), or session end). */ GHOST_SetWindowModifiedState(win->ghostwin, (bool)!wm->file_saved); } + +#undef MAX_PROJECT_NAME_HINT } void WM_window_set_dpi(const wmWindow *win) @@ -1531,6 +1549,35 @@ void wm_window_process_events(const bContext *C) } /* -------------------------------------------------------------------- */ +/** \name WM level message bus subscribers + * \{ */ + +static void wm_msg_windows_title_update_fn(bContext *C, + wmMsgSubscribeKey *UNUSED(msg_key), + wmMsgSubscribeValue *UNUSED(msg_val)) +{ + wmWindowManager *wm = CTX_wm_manager(C); + LISTBASE_FOREACH (wmWindow *, win, &wm->windows) { + wm_window_title(wm, win); + } +} + +void wm_messages_subscribe(wmWindowManager *wm) +{ + WM_msgbus_clear_by_owner(wm->message_bus, wm); + + wmMsgSubscribeValue msg_sub_value_update_win_titles = {0}; + msg_sub_value_update_win_titles.owner = wm; + msg_sub_value_update_win_titles.notify = wm_msg_windows_title_update_fn; + + /* Update window titles on project name change. */ + WM_msg_subscribe_rna_anon_prop( + wm->message_bus, BlenderProject, name, &msg_sub_value_update_win_titles); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Ghost Init/Exit * \{ */ diff --git a/source/blender/windowmanager/wm_window.h b/source/blender/windowmanager/wm_window.h index 036a34a5140..81bb1b37384 100644 --- a/source/blender/windowmanager/wm_window.h +++ b/source/blender/windowmanager/wm_window.h @@ -23,6 +23,8 @@ void wm_ghost_init(bContext *C); void wm_ghost_init_background(void); void wm_ghost_exit(void); +void wm_messages_subscribe(wmWindowManager *wm); + /** * This one should correctly check for apple top header... * done for Cocoa: returns window contents (and not frame) max size. |