diff options
Diffstat (limited to 'source/blender/editors/asset')
-rw-r--r-- | source/blender/editors/asset/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_catalog.h | 2 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_catalog.hh | 25 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_filter.h | 14 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_handle.h | 6 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_indexer.h | 50 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_library.h | 20 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_list.h | 26 | ||||
-rw-r--r-- | source/blender/editors/asset/ED_asset_temp_id_consumer.h | 5 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_catalog.cc | 23 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_filter.cc | 12 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_handle.cc | 6 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_indexer.cc | 781 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_library_reference_enum.cc | 46 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_list.cc | 36 | ||||
-rw-r--r-- | source/blender/editors/asset/intern/asset_ops.cc | 330 |
16 files changed, 1290 insertions, 94 deletions
diff --git a/source/blender/editors/asset/CMakeLists.txt b/source/blender/editors/asset/CMakeLists.txt index b9ef8e82bba..2391f4af14d 100644 --- a/source/blender/editors/asset/CMakeLists.txt +++ b/source/blender/editors/asset/CMakeLists.txt @@ -24,6 +24,7 @@ set(INC ../../makesdna ../../makesrna ../../windowmanager + ../../../../intern/clog ../../../../intern/guardedalloc ) @@ -34,6 +35,7 @@ set(SRC intern/asset_catalog.cc intern/asset_filter.cc intern/asset_handle.cc + intern/asset_indexer.cc intern/asset_library_reference.cc intern/asset_library_reference_enum.cc intern/asset_list.cc diff --git a/source/blender/editors/asset/ED_asset_catalog.h b/source/blender/editors/asset/ED_asset_catalog.h index 451ec0d5984..be99de01173 100644 --- a/source/blender/editors/asset/ED_asset_catalog.h +++ b/source/blender/editors/asset/ED_asset_catalog.h @@ -16,6 +16,8 @@ /** \file * \ingroup edasset + * + * Supplement for `ED_asset_catalog.hh`. Part of the same API but usable in C. */ #pragma once diff --git a/source/blender/editors/asset/ED_asset_catalog.hh b/source/blender/editors/asset/ED_asset_catalog.hh index 8da8fc0d6c9..d4df15561d0 100644 --- a/source/blender/editors/asset/ED_asset_catalog.hh +++ b/source/blender/editors/asset/ED_asset_catalog.hh @@ -16,10 +16,17 @@ /** \file * \ingroup edasset + * + * UI/Editor level API for catalog operations, creating richer functionality than the BKE catalog + * API provides (which this uses internally). + * + * Note that `ED_asset_catalog.h` is part of this API. */ #pragma once +#include <optional> + #include "BKE_asset_catalog.hh" #include "BLI_string_ref.hh" @@ -37,6 +44,18 @@ void ED_asset_catalog_remove(AssetLibrary *library, const blender::bke::CatalogI void ED_asset_catalog_rename(AssetLibrary *library, blender::bke::CatalogID catalog_id, blender::StringRefNull new_name); -void ED_asset_catalog_move(AssetLibrary *library, - blender::bke::CatalogID src_catalog_id, - blender::bke::CatalogID dst_parent_catalog_id); +/** + * Reinsert catalog identified by \a src_catalog_id as child to catalog identified by \a + * dst_parent_catalog_id. If \a dst_parent_catalog_id is not set, the catalog is moved to the root + * level of the tree. + * The name of the reinserted catalog is made unique within the parent. Note that moving a catalog + * to the same level it was before will also change its name, since the name uniqueness check isn't + * smart enough to ignore the item to be reinserted. So the caller is expected to handle this case + * to avoid unwanted renames. + * + * Nothing is done (debug builds run into an assert) if the given catalog IDs can't be identified. + */ +void ED_asset_catalog_move( + AssetLibrary *library, + blender::bke::CatalogID src_catalog_id, + std::optional<blender::bke::CatalogID> dst_parent_catalog_id = std::nullopt); diff --git a/source/blender/editors/asset/ED_asset_filter.h b/source/blender/editors/asset/ED_asset_filter.h index 340c4c9dbe7..3b92baea117 100644 --- a/source/blender/editors/asset/ED_asset_filter.h +++ b/source/blender/editors/asset/ED_asset_filter.h @@ -16,6 +16,8 @@ /** \file * \ingroup edasset + * + * Functions for filtering assets. */ #pragma once @@ -27,6 +29,18 @@ extern "C" { struct AssetFilterSettings; struct AssetHandle; +/** + * Compare \a asset against the settings of \a filter. + * + * Individual filter parameters are ORed with the asset properties. That means: + * * The asset type must be one of the ID types filtered by, and + * * The asset must contain at least one of the tags filtered by. + * However for an asset to be matching it must have one match in each of the parameters. I.e. one + * matching type __and__ at least one matching tag. + * + * \returns True if the asset should be visible with these filter settings (parameters match). + * Otherwise returns false (mismatch). + */ bool ED_asset_filter_matches_asset(const struct AssetFilterSettings *filter, const struct AssetHandle *asset); diff --git a/source/blender/editors/asset/ED_asset_handle.h b/source/blender/editors/asset/ED_asset_handle.h index efb99410d3d..dce851164a5 100644 --- a/source/blender/editors/asset/ED_asset_handle.h +++ b/source/blender/editors/asset/ED_asset_handle.h @@ -16,6 +16,12 @@ /** \file * \ingroup edasset + * + * Asset-handle is a temporary design, not part of the core asset system design. + * + * Currently asset-list items are just file directory items (#FileDirEntry). So an asset-handle + * just wraps a pointer to this. We try to abstract away the fact that it's just a file entry, + * although that doesn't always work (see #rna_def_asset_handle()). */ #pragma once diff --git a/source/blender/editors/asset/ED_asset_indexer.h b/source/blender/editors/asset/ED_asset_indexer.h new file mode 100644 index 00000000000..33558d8cda5 --- /dev/null +++ b/source/blender/editors/asset/ED_asset_indexer.h @@ -0,0 +1,50 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup edasset + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ED_file_indexer.h" + +/** + * File Indexer Service for indexing asset files. + * + * Opening and parsing a large collection of asset files inside a library can take a lot of time. + * To reduce the time it takes the files are indexed. + * + * - Index files are created for each blend file in the asset library, even when the blend file + * doesn't contain any assets. + * - Indexes are stored in an persistent cache folder (`BKE_appdir_folder_caches` + + * `asset_library_indexes/{asset_library_dir}/{asset_index_file.json}`). + * - The content of the indexes are used when: + * - Index exists and can be opened + * - Last modification date is earlier than the file it represents. + * - The index file version is the latest. + * - Blend files without any assets can be determined by the size of the index file for some + * additional performance. + */ +extern const FileIndexerType file_indexer_asset; + +#ifdef __cplusplus +} +#endif diff --git a/source/blender/editors/asset/ED_asset_library.h b/source/blender/editors/asset/ED_asset_library.h index 905d097d223..62a7d6ffa9b 100644 --- a/source/blender/editors/asset/ED_asset_library.h +++ b/source/blender/editors/asset/ED_asset_library.h @@ -26,9 +26,27 @@ extern "C" { #endif +/** + * Return an index that can be used to uniquely identify \a library, assuming + * that all relevant indices were created with this function. + */ int ED_asset_library_reference_to_enum_value(const AssetLibraryReference *library); +/** + * Return an asset library reference matching the index returned by + * #ED_asset_library_reference_to_enum_value(). + */ AssetLibraryReference ED_asset_library_reference_from_enum_value(int value); -const struct EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf(void); +/** + * Translate all available asset libraries to an RNA enum, whereby the enum values match the result + * of #ED_asset_library_reference_to_enum_value() for any given library. + * + * Since this is meant for UI display, skips non-displayable libraries, that is, libraries with an + * empty name or path. + * + * \param include_local_library: Whether to include the "Current File" library or not. + */ +const struct EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf( + bool include_local_library); #ifdef __cplusplus } diff --git a/source/blender/editors/asset/ED_asset_list.h b/source/blender/editors/asset/ED_asset_list.h index 61d7729b651..669bb6dbe36 100644 --- a/source/blender/editors/asset/ED_asset_list.h +++ b/source/blender/editors/asset/ED_asset_list.h @@ -31,21 +31,47 @@ struct ID; struct bContext; struct wmNotifier; +/** + * Invoke asset list reading, potentially in a parallel job. Won't wait until the job is done, + * and may return earlier. + */ void ED_assetlist_storage_fetch(const struct AssetLibraryReference *library_reference, const struct bContext *C); void ED_assetlist_ensure_previews_job(const struct AssetLibraryReference *library_reference, struct bContext *C); void ED_assetlist_clear(const struct AssetLibraryReference *library_reference, struct bContext *C); bool ED_assetlist_storage_has_list_for_library(const AssetLibraryReference *library_reference); +/** + * Tag all asset lists in the storage that show main data as needing an update (re-fetch). + * + * This only tags the data. If the asset list is visible on screen, the space is still responsible + * for ensuring the necessary redraw. It can use #ED_assetlist_listen() to check if the asset-list + * needs a redraw for a given notifier. + */ void ED_assetlist_storage_tag_main_data_dirty(void); +/** + * Remapping of ID pointers within the asset lists. Typically called when an ID is deleted to clear + * all references to it (\a id_new is null then). + */ void ED_assetlist_storage_id_remap(struct ID *id_old, struct ID *id_new); +/** + * Can't wait for static deallocation to run. There's nested data allocated with our guarded + * allocator, it will complain about unfreed memory on exit. + */ void ED_assetlist_storage_exit(void); struct ImBuf *ED_assetlist_asset_image_get(const AssetHandle *asset_handle); const char *ED_assetlist_library_path(const struct AssetLibraryReference *library_reference); +/** + * \return True if the region needs a UI redraw. + */ bool ED_assetlist_listen(const struct AssetLibraryReference *library_reference, const struct wmNotifier *notifier); +/** + * \return The number of assets stored in the asset list for \a library_reference, or -1 if there + * is no list fetched for it. + */ int ED_assetlist_size(const struct AssetLibraryReference *library_reference); #ifdef __cplusplus diff --git a/source/blender/editors/asset/ED_asset_temp_id_consumer.h b/source/blender/editors/asset/ED_asset_temp_id_consumer.h index 9a47e435612..0848f4225bd 100644 --- a/source/blender/editors/asset/ED_asset_temp_id_consumer.h +++ b/source/blender/editors/asset/ED_asset_temp_id_consumer.h @@ -16,6 +16,11 @@ /** \file * \ingroup edasset + * + * API to abstract away details for temporary loading of an ID from an asset. If the ID is stored + * in the current file (or more precisely, in the #Main given when requesting an ID) no loading is + * performed and the ID is returned. Otherwise it's imported for temporary access using the + * `BLO_library_temp` API. */ #pragma once diff --git a/source/blender/editors/asset/intern/asset_catalog.cc b/source/blender/editors/asset/intern/asset_catalog.cc index 9634665be7b..f6c02f86f0a 100644 --- a/source/blender/editors/asset/intern/asset_catalog.cc +++ b/source/blender/editors/asset/intern/asset_catalog.cc @@ -122,7 +122,7 @@ void ED_asset_catalog_rename(::AssetLibrary *library, void ED_asset_catalog_move(::AssetLibrary *library, const CatalogID src_catalog_id, - const CatalogID dst_parent_catalog_id) + const std::optional<CatalogID> dst_parent_catalog_id) { bke::AssetCatalogService *catalog_service = BKE_asset_library_get_catalog_service(library); if (!catalog_service) { @@ -131,9 +131,24 @@ void ED_asset_catalog_move(::AssetLibrary *library, } AssetCatalog *src_catalog = catalog_service->find_catalog(src_catalog_id); - AssetCatalog *dst_catalog = catalog_service->find_catalog(dst_parent_catalog_id); + if (!src_catalog) { + BLI_assert_unreachable(); + return; + } + AssetCatalog *dst_catalog = dst_parent_catalog_id ? + catalog_service->find_catalog(*dst_parent_catalog_id) : + nullptr; + if (!dst_catalog && dst_parent_catalog_id) { + BLI_assert_unreachable(); + return; + } - const AssetCatalogPath new_path = dst_catalog->path / StringRef(src_catalog->path.name()); + std::string unique_name = catalog_name_ensure_unique( + *catalog_service, src_catalog->path.name(), dst_catalog ? dst_catalog->path.c_str() : ""); + /* If a destination catalog was given, construct the path using that. Otherwise, the path is just + * the name of the catalog to be moved, which means it ends up at the root level. */ + const AssetCatalogPath new_path = dst_catalog ? (dst_catalog->path / unique_name) : + AssetCatalogPath{unique_name}; const AssetCatalogPath clean_new_path = new_path.cleanup(); if (new_path == src_catalog->path || clean_new_path == src_catalog->path) { @@ -158,7 +173,7 @@ void ED_asset_catalogs_save_from_main_path(::AssetLibrary *library, const Main * /* Since writing to disk also means loading any on-disk changes, it may be a good idea to store * an undo step. */ catalog_service->undo_push(); - catalog_service->write_to_disk(bmain->name); + catalog_service->write_to_disk(bmain->filepath); } void ED_asset_catalogs_set_save_catalogs_when_file_is_saved(const bool should_save) diff --git a/source/blender/editors/asset/intern/asset_filter.cc b/source/blender/editors/asset/intern/asset_filter.cc index c22bbc923eb..70e3e2f55ea 100644 --- a/source/blender/editors/asset/intern/asset_filter.cc +++ b/source/blender/editors/asset/intern/asset_filter.cc @@ -27,18 +27,6 @@ #include "ED_asset_filter.h" #include "ED_asset_handle.h" -/** - * Compare \a asset against the settings of \a filter. - * - * Individual filter parameters are ORed with the asset properties. That means: - * * The asset type must be one of the ID types filtered by, and - * * The asset must contain at least one of the tags filtered by. - * However for an asset to be matching it must have one match in each of the parameters. I.e. one - * matching type __and__ at least one matching tag. - * - * \returns True if the asset should be visible with these filter settings (parameters match). - * Otherwise returns false (mismatch). - */ bool ED_asset_filter_matches_asset(const AssetFilterSettings *filter, const AssetHandle *asset) { ID_Type asset_type = ED_asset_handle_get_id_type(asset); diff --git a/source/blender/editors/asset/intern/asset_handle.cc b/source/blender/editors/asset/intern/asset_handle.cc index 363bd9226da..a2029d3cc50 100644 --- a/source/blender/editors/asset/intern/asset_handle.cc +++ b/source/blender/editors/asset/intern/asset_handle.cc @@ -16,12 +16,6 @@ /** \file * \ingroup edasset - * - * Asset-handle is a temporary design, not part of the core asset system design. - * - * Currently asset-list items are just file directory items (#FileDirEntry). So an asset-handle is - * just wraps a pointer to this. We try to abstract away the fact that it's just a file entry, - * although that doesn't always work (see #rna_def_asset_handle()). */ #include <string> diff --git a/source/blender/editors/asset/intern/asset_indexer.cc b/source/blender/editors/asset/intern/asset_indexer.cc new file mode 100644 index 00000000000..4107d28b5e3 --- /dev/null +++ b/source/blender/editors/asset/intern/asset_indexer.cc @@ -0,0 +1,781 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup edasset + */ + +#include <fstream> +#include <iomanip> +#include <optional> + +#include "ED_asset_indexer.h" + +#include "DNA_asset_types.h" +#include "DNA_userdef_types.h" + +#include "BLI_fileops.h" +#include "BLI_hash.hh" +#include "BLI_linklist.h" +#include "BLI_path_util.h" +#include "BLI_serialize.hh" +#include "BLI_set.hh" +#include "BLI_string_ref.hh" +#include "BLI_uuid.h" + +#include "BKE_appdir.h" +#include "BKE_asset.h" +#include "BKE_asset_catalog.hh" +#include "BKE_preferences.h" + +#include "CLG_log.h" + +static CLG_LogRef LOG = {"ed.asset"}; + +namespace blender::ed::asset::index { + +using namespace blender::io::serialize; +using namespace blender::bke; + +/** + * \file asset_indexer.cc + * \brief Indexer for asset libraries. + * + * Indexes are stored per input file. Each index can contain zero to multiple asset entries. + * The indexes are grouped together per asset library. They are stored in + * #BKE_appdir_folder_caches + + * /asset-library-indices/<asset-library-hash>/<asset-index-hash>_<asset_file>.index.json. + * + * The structure of an index file is + * \code + * { + * "version": <file version number>, + * "entries": [{ + * "name": "<asset name>", + * "catalog_id": "<catalog_id>", + * "catalog_name": "<catalog_name>", + * "description": "<description>", + * "author": "<author>", + * "tags": ["<tag>"] + * }] + * } + * \endcode + * + * NOTE: entries, author, description and tags are optional attributes. + * + * NOTE: File browser uses name and idcode separate. Inside the index they are joined together like + * #ID.name. + * NOTE: File browser group name isn't stored in the index as it is a translatable name. + */ +constexpr StringRef ATTRIBUTE_VERSION("version"); +constexpr StringRef ATTRIBUTE_ENTRIES("entries"); +constexpr StringRef ATTRIBUTE_ENTRIES_NAME("name"); +constexpr StringRef ATTRIBUTE_ENTRIES_CATALOG_ID("catalog_id"); +constexpr StringRef ATTRIBUTE_ENTRIES_CATALOG_NAME("catalog_name"); +constexpr StringRef ATTRIBUTE_ENTRIES_DESCRIPTION("description"); +constexpr StringRef ATTRIBUTE_ENTRIES_AUTHOR("author"); +constexpr StringRef ATTRIBUTE_ENTRIES_TAGS("tags"); + +/** Abstract class for #BlendFile and #AssetIndexFile. */ +class AbstractFile { + public: + virtual ~AbstractFile() = default; + + virtual const char *get_file_path() const = 0; + + bool exists() const + { + return BLI_exists(get_file_path()); + } + + size_t get_file_size() const + { + return BLI_file_size(get_file_path()); + } +}; + +/** + * \brief Reference to a blend file that can be indexed. + */ +class BlendFile : public AbstractFile { + StringRefNull file_path_; + + public: + BlendFile(StringRefNull file_path) : file_path_(file_path) + { + } + + uint64_t hash() const + { + DefaultHash<StringRefNull> hasher; + return hasher(file_path_); + } + + std::string get_filename() const + { + char filename[FILE_MAX]; + BLI_split_file_part(get_file_path(), filename, sizeof(filename)); + return std::string(filename); + } + + const char *get_file_path() const override + { + return file_path_.c_str(); + } +}; + +/** + * \brief Single entry inside a #AssetIndexFile for reading. + */ +struct AssetEntryReader { + private: + /** + * \brief Lookup table containing the elements of the entry. + */ + ObjectValue::Lookup lookup; + + StringRefNull get_name_with_idcode() const + { + return lookup.lookup(ATTRIBUTE_ENTRIES_NAME)->as_string_value()->value(); + } + + public: + AssetEntryReader(const ObjectValue &entry) : lookup(entry.create_lookup()) + { + } + + ID_Type get_idcode() const + { + const StringRefNull name_with_idcode = get_name_with_idcode(); + return GS(name_with_idcode.c_str()); + } + + StringRef get_name() const + { + const StringRefNull name_with_idcode = get_name_with_idcode(); + return name_with_idcode.substr(2); + } + + bool has_description() const + { + return lookup.contains(ATTRIBUTE_ENTRIES_DESCRIPTION); + } + + StringRefNull get_description() const + { + return lookup.lookup(ATTRIBUTE_ENTRIES_DESCRIPTION)->as_string_value()->value(); + } + + bool has_author() const + { + return lookup.contains(ATTRIBUTE_ENTRIES_AUTHOR); + } + + StringRefNull get_author() const + { + return lookup.lookup(ATTRIBUTE_ENTRIES_AUTHOR)->as_string_value()->value(); + } + + StringRefNull get_catalog_name() const + { + return lookup.lookup(ATTRIBUTE_ENTRIES_CATALOG_NAME)->as_string_value()->value(); + } + + CatalogID get_catalog_id() const + { + const std::string &catalog_id = + lookup.lookup(ATTRIBUTE_ENTRIES_CATALOG_ID)->as_string_value()->value(); + CatalogID catalog_uuid(catalog_id); + return catalog_uuid; + } + + void add_tags_to_meta_data(AssetMetaData *asset_data) const + { + const ObjectValue::LookupValue *value_ptr = lookup.lookup_ptr(ATTRIBUTE_ENTRIES_TAGS); + if (value_ptr == nullptr) { + return; + } + + const ArrayValue *array_value = (*value_ptr)->as_array_value(); + const ArrayValue::Items &elements = array_value->elements(); + for (const ArrayValue::Item &item : elements) { + const StringRefNull tag_name = item->as_string_value()->value(); + BKE_asset_metadata_tag_add(asset_data, tag_name.c_str()); + } + } +}; + +struct AssetEntryWriter { + private: + ObjectValue::Items &attributes; + + public: + AssetEntryWriter(ObjectValue &entry) : attributes(entry.elements()) + { + } + + /** + * \brief add id + name to the attributes. + * + * NOTE: id and name are encoded like #ID.name + */ + void add_id_name(const short idcode, const StringRefNull name) + { + char idcode_prefix[2]; + /* Similar to `BKE_libblock_alloc`. */ + *((short *)idcode_prefix) = idcode; + std::string name_with_idcode = std::string(idcode_prefix, sizeof(idcode_prefix)) + name; + + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES_NAME, new StringValue(name_with_idcode))); + } + + void add_catalog_id(const CatalogID &catalog_id) + { + char catalog_id_str[UUID_STRING_LEN]; + BLI_uuid_format(catalog_id_str, catalog_id); + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES_CATALOG_ID, new StringValue(catalog_id_str))); + } + + void add_catalog_name(const StringRefNull catalog_name) + { + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES_CATALOG_NAME, new StringValue(catalog_name))); + } + + void add_description(const StringRefNull description) + { + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES_DESCRIPTION, new StringValue(description))); + } + + void add_author(const StringRefNull author) + { + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES_AUTHOR, new StringValue(author))); + } + + void add_tags(const ListBase /* AssetTag */ *asset_tags) + { + ArrayValue *tags = new ArrayValue(); + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES_TAGS, tags)); + ArrayValue::Items &tag_items = tags->elements(); + + LISTBASE_FOREACH (AssetTag *, tag, asset_tags) { + tag_items.append_as(new StringValue(tag->name)); + } + } +}; + +static void init_value_from_file_indexer_entry(AssetEntryWriter &result, + const FileIndexerEntry *indexer_entry) +{ + const BLODataBlockInfo &datablock_info = indexer_entry->datablock_info; + + result.add_id_name(indexer_entry->idcode, datablock_info.name); + + const AssetMetaData &asset_data = *datablock_info.asset_data; + result.add_catalog_id(asset_data.catalog_id); + result.add_catalog_name(asset_data.catalog_simple_name); + + if (asset_data.description != nullptr) { + result.add_description(asset_data.description); + } + if (asset_data.author != nullptr) { + result.add_author(asset_data.author); + } + + if (!BLI_listbase_is_empty(&asset_data.tags)) { + result.add_tags(&asset_data.tags); + } + + /* TODO: asset_data.IDProperties */ +} + +static void init_value_from_file_indexer_entries(ObjectValue &result, + const FileIndexerEntries &indexer_entries) +{ + ArrayValue *entries = new ArrayValue(); + ArrayValue::Items &items = entries->elements(); + + for (LinkNode *ln = indexer_entries.entries; ln; ln = ln->next) { + const FileIndexerEntry *indexer_entry = static_cast<const FileIndexerEntry *>(ln->link); + /* We also get non asset types (brushes, workspaces), when browsing using the asset browser. */ + if (indexer_entry->datablock_info.asset_data == nullptr) { + continue; + } + ObjectValue *entry_value = new ObjectValue(); + AssetEntryWriter entry(*entry_value); + init_value_from_file_indexer_entry(entry, indexer_entry); + items.append_as(entry_value); + } + + /* When no entries to index, we should not store the entries attribute as this would make the + * size bigger than the #MIN_FILE_SIZE_WITH_ENTRIES. */ + if (items.is_empty()) { + delete entries; + return; + } + + ObjectValue::Items &attributes = result.elements(); + attributes.append_as(std::pair(ATTRIBUTE_ENTRIES, entries)); +} + +static void init_indexer_entry_from_value(FileIndexerEntry &indexer_entry, + const AssetEntryReader &entry) +{ + indexer_entry.idcode = entry.get_idcode(); + + const std::string &name = entry.get_name(); + BLI_strncpy( + indexer_entry.datablock_info.name, name.c_str(), sizeof(indexer_entry.datablock_info.name)); + + AssetMetaData *asset_data = BKE_asset_metadata_create(); + indexer_entry.datablock_info.asset_data = asset_data; + + if (entry.has_description()) { + const std::string &description = entry.get_description(); + char *description_c_str = static_cast<char *>(MEM_mallocN(description.length() + 1, __func__)); + BLI_strncpy(description_c_str, description.c_str(), description.length() + 1); + asset_data->description = description_c_str; + } + if (entry.has_author()) { + const std::string &author = entry.get_author(); + char *author_c_str = static_cast<char *>(MEM_mallocN(author.length() + 1, __func__)); + BLI_strncpy(author_c_str, author.c_str(), author.length() + 1); + asset_data->author = author_c_str; + } + + const std::string &catalog_name = entry.get_catalog_name(); + BLI_strncpy(asset_data->catalog_simple_name, + catalog_name.c_str(), + sizeof(asset_data->catalog_simple_name)); + + asset_data->catalog_id = entry.get_catalog_id(); + + entry.add_tags_to_meta_data(asset_data); +} + +static int init_indexer_entries_from_value(FileIndexerEntries &indexer_entries, + const ObjectValue &value) +{ + const ObjectValue::Lookup attributes = value.create_lookup(); + const ObjectValue::LookupValue *entries_value = attributes.lookup_ptr(ATTRIBUTE_ENTRIES); + BLI_assert(entries_value != nullptr); + + if (entries_value == nullptr) { + return 0; + } + + int num_entries_read = 0; + const ArrayValue::Items elements = (*entries_value)->as_array_value()->elements(); + for (ArrayValue::Item element : elements) { + const AssetEntryReader asset_entry(*element->as_object_value()); + + FileIndexerEntry *entry = static_cast<FileIndexerEntry *>( + MEM_callocN(sizeof(FileIndexerEntry), __func__)); + init_indexer_entry_from_value(*entry, asset_entry); + + BLI_linklist_prepend(&indexer_entries.entries, entry); + num_entries_read += 1; + } + + return num_entries_read; +} + +/** + * \brief References the asset library directory. + * + * The #AssetLibraryIndex instance is used to keep track of unused file indices. When reading any + * used indices are removed from the list and when reading is finished the unused + * indices are removed. + */ +struct AssetLibraryIndex { + /** + * Tracks indices that haven't been used yet. + * + * Contains absolute paths to the indices. + */ + Set<std::string> unused_file_indices; + + /** + * \brief Absolute path where the indices of `library` are stored. + * + * \NOTE: includes trailing directory separator. + */ + std::string indices_base_path; + + std::string library_path; + + public: + AssetLibraryIndex(const StringRef library_path) : library_path(library_path) + { + init_indices_base_path(); + } + + uint64_t hash() const + { + DefaultHash<StringRefNull> hasher; + return hasher(get_library_file_path()); + } + + StringRefNull get_library_file_path() const + { + return library_path; + } + + /** + * \brief Initializes #AssetLibraryIndex.indices_base_path. + * + * `BKE_appdir_folder_caches/asset-library-indices/<asset-library-name-hash>/` + */ + void init_indices_base_path() + { + char index_path[FILE_MAX]; + BKE_appdir_folder_caches(index_path, sizeof(index_path)); + + BLI_path_append(index_path, sizeof(index_path), "asset-library-indices"); + + std::stringstream ss; + ss << std::setfill('0') << std::setw(16) << std::hex << hash() << "/"; + BLI_path_append(index_path, sizeof(index_path), ss.str().c_str()); + + indices_base_path = std::string(index_path); + } + + /** + * \return absolute path to the index file of the given `asset_file`. + * + * `{indices_base_path}/{asset-file_hash}_{asset-file-filename}.index.json`. + */ + std::string index_file_path(const BlendFile &asset_file) const + { + std::stringstream ss; + ss << indices_base_path; + ss << std::setfill('0') << std::setw(16) << std::hex << asset_file.hash() << "_" + << asset_file.get_filename() << ".index.json"; + return ss.str(); + } + + /** + * Initialize to keep track of unused file indices. + */ + void init_unused_index_files() + { + const char *index_path = indices_base_path.c_str(); + if (!BLI_is_dir(index_path)) { + return; + } + struct direntry *dir_entries = nullptr; + int num_entries = BLI_filelist_dir_contents(index_path, &dir_entries); + for (int i = 0; i < num_entries; i++) { + struct direntry *entry = &dir_entries[i]; + if (BLI_str_endswith(entry->relname, ".index.json")) { + unused_file_indices.add_as(std::string(entry->path)); + } + } + + BLI_filelist_free(dir_entries, num_entries); + } + + void mark_as_used(const std::string &filename) + { + unused_file_indices.remove(filename); + } + + int remove_unused_index_files() const + { + int num_files_deleted = 0; + for (const std::string &unused_index : unused_file_indices) { + const char *file_path = unused_index.c_str(); + CLOG_INFO(&LOG, 2, "Remove unused index file [%s].", file_path); + BLI_delete(file_path, false, false); + num_files_deleted++; + } + + return num_files_deleted; + } +}; + +/** + * Instance of this class represents the contents of an asset index file. + * + * \code + * { + * "version": {version}, + * "entries": ... + * } + * \endcode + */ +struct AssetIndex { + /** + * \brief Version to store in new index files. + * + * Versions are written to each index file. When reading the version is checked against + * `CURRENT_VERSION` to make sure we can use the index. Developer should increase + * `CURRENT_VERSION` when changes are made to the structure of the stored index. + */ + static const int CURRENT_VERSION = 1; + + /** + * Version number to use when version couldn't be read from an index file. + */ + const int UNKNOWN_VERSION = -1; + + /** + * `blender::io::serialize::Value` representing the contents of an index file. + * + * Value is used over #ObjectValue as the contents of the index could be corrupted and doesn't + * represent an object. In case corrupted files are detected the `get_version` would return + * `UNKNOWN_VERSION`. + */ + std::unique_ptr<Value> contents; + + /** + * Constructor for when creating/updating an asset index file. + * #AssetIndex.contents are filled from the given \p indexer_entries. + */ + AssetIndex(const FileIndexerEntries &indexer_entries) + { + std::unique_ptr<ObjectValue> root = std::make_unique<ObjectValue>(); + ObjectValue::Items &root_attributes = root->elements(); + root_attributes.append_as(std::pair(ATTRIBUTE_VERSION, new IntValue(CURRENT_VERSION))); + init_value_from_file_indexer_entries(*root, indexer_entries); + + contents = std::move(root); + } + + /** + * Constructor when reading an asset index file. + * #AssetIndex.contents are read from the given \p value. + */ + AssetIndex(std::unique_ptr<Value> &value) : contents(std::move(value)) + { + } + + int get_version() const + { + const ObjectValue *root = contents->as_object_value(); + if (root == nullptr) { + return UNKNOWN_VERSION; + } + const ObjectValue::Lookup attributes = root->create_lookup(); + const ObjectValue::LookupValue *version_value = attributes.lookup_ptr(ATTRIBUTE_VERSION); + if (version_value == nullptr) { + return UNKNOWN_VERSION; + } + return (*version_value)->as_int_value()->value(); + } + + bool is_latest_version() const + { + return get_version() == CURRENT_VERSION; + } + + /** + * Extract the contents of this index into the given \p indexer_entries. + * + * \return The number of entries read from the given entries. + */ + int extract_into(FileIndexerEntries &indexer_entries) const + { + const ObjectValue *root = contents->as_object_value(); + const int num_entries_read = init_indexer_entries_from_value(indexer_entries, *root); + return num_entries_read; + } +}; + +class AssetIndexFile : public AbstractFile { + public: + AssetLibraryIndex &library_index; + /** + * Asset index files with a size smaller than this attribute would be considered to not contain + * any entries. + */ + const size_t MIN_FILE_SIZE_WITH_ENTRIES = 32; + std::string filename; + + AssetIndexFile(AssetLibraryIndex &library_index, BlendFile &asset_filename) + : library_index(library_index), filename(library_index.index_file_path(asset_filename)) + { + } + + void mark_as_used() + { + library_index.mark_as_used(filename); + } + + const char *get_file_path() const override + { + return filename.c_str(); + } + + /** + * Returns whether the index file is older than the given asset file. + */ + bool is_older_than(BlendFile &asset_file) const + { + return BLI_file_older(get_file_path(), asset_file.get_file_path()); + } + + /** + * Check whether the index file contains entries without opening the file. + */ + bool constains_entries() const + { + const size_t file_size = get_file_size(); + return file_size >= MIN_FILE_SIZE_WITH_ENTRIES; + } + + std::unique_ptr<AssetIndex> read_contents() const + { + JsonFormatter formatter; + std::ifstream is; + is.open(filename); + std::unique_ptr<Value> read_data = formatter.deserialize(is); + is.close(); + + return std::make_unique<AssetIndex>(read_data); + } + + bool ensure_parent_path_exists() const + { + /* `BLI_make_existing_file` only ensures parent path, otherwise than expected from the name of + * the function. */ + return BLI_make_existing_file(get_file_path()); + } + + void write_contents(AssetIndex &content) + { + JsonFormatter formatter; + if (!ensure_parent_path_exists()) { + CLOG_ERROR(&LOG, "Index not created: couldn't create folder [%s].", get_file_path()); + return; + } + + std::ofstream os; + os.open(filename, std::ios::out | std::ios::trunc); + formatter.serialize(os, *content.contents); + os.close(); + } +}; + +static eFileIndexerResult read_index(const char *filename, + FileIndexerEntries *entries, + int *r_read_entries_len, + void *user_data) +{ + AssetLibraryIndex &library_index = *static_cast<AssetLibraryIndex *>(user_data); + BlendFile asset_file(filename); + AssetIndexFile asset_index_file(library_index, asset_file); + + if (!asset_index_file.exists()) { + return FILE_INDEXER_NEEDS_UPDATE; + } + + /* Mark index as used, even when it will be recreated. When not done it would remove the index + * when the indexing has finished (see `AssetLibraryIndex.remove_unused_index_files`), thereby + * removing the newly created index. + */ + asset_index_file.mark_as_used(); + + if (asset_index_file.is_older_than(asset_file)) { + CLOG_INFO( + &LOG, + 3, + "Asset index file [%s] needs to be refreshed as it is older than the asset file [%s].", + asset_index_file.filename.c_str(), + filename); + return FILE_INDEXER_NEEDS_UPDATE; + } + + if (!asset_index_file.constains_entries()) { + CLOG_INFO(&LOG, + 3, + "Asset file index is to small to contain any entries. [%s]", + asset_index_file.filename.c_str()); + *r_read_entries_len = 0; + return FILE_INDEXER_ENTRIES_LOADED; + } + + std::unique_ptr<AssetIndex> contents = asset_index_file.read_contents(); + if (!contents->is_latest_version()) { + CLOG_INFO(&LOG, + 3, + "Asset file index is ignored; expected version %d but file is version %d [%s].", + AssetIndex::CURRENT_VERSION, + contents->get_version(), + asset_index_file.filename.c_str()); + return FILE_INDEXER_NEEDS_UPDATE; + } + + const int read_entries_len = contents->extract_into(*entries); + CLOG_INFO(&LOG, 1, "Read %d entries from asset index for [%s].", read_entries_len, filename); + *r_read_entries_len = read_entries_len; + + return FILE_INDEXER_ENTRIES_LOADED; +} + +static void update_index(const char *filename, FileIndexerEntries *entries, void *user_data) +{ + AssetLibraryIndex &library_index = *static_cast<AssetLibraryIndex *>(user_data); + BlendFile asset_file(filename); + AssetIndexFile asset_index_file(library_index, asset_file); + CLOG_INFO(&LOG, + 1, + "Update asset index for [%s] store index in [%s].", + asset_file.get_file_path(), + asset_index_file.get_file_path()); + + AssetIndex content(*entries); + asset_index_file.write_contents(content); +} + +static void *init_user_data(const char *root_directory, size_t root_directory_maxlen) +{ + AssetLibraryIndex *library_index = OBJECT_GUARDED_NEW( + AssetLibraryIndex, + StringRef(root_directory, BLI_strnlen(root_directory, root_directory_maxlen))); + library_index->init_unused_index_files(); + return library_index; +} + +static void free_user_data(void *user_data) +{ + OBJECT_GUARDED_DELETE(user_data, AssetLibraryIndex); +} + +static void filelist_finished(void *user_data) +{ + AssetLibraryIndex &library_index = *static_cast<AssetLibraryIndex *>(user_data); + const int num_indices_removed = library_index.remove_unused_index_files(); + if (num_indices_removed > 0) { + CLOG_INFO(&LOG, 1, "Removed %d unused indices.", num_indices_removed); + } +} + +constexpr FileIndexerType asset_indexer() +{ + FileIndexerType indexer = {nullptr}; + indexer.read_index = read_index; + indexer.update_index = update_index; + indexer.init_user_data = init_user_data; + indexer.free_user_data = free_user_data; + indexer.filelist_finished = filelist_finished; + return indexer; +} + +} // namespace blender::ed::asset::index + +extern "C" { +const FileIndexerType file_indexer_asset = blender::ed::asset::index::asset_indexer(); +} 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 1a2d3f5837a..05526f222a5 100644 --- a/source/blender/editors/asset/intern/asset_library_reference_enum.cc +++ b/source/blender/editors/asset/intern/asset_library_reference_enum.cc @@ -35,10 +35,6 @@ #include "ED_asset_library.h" -/** - * Return an index that can be used to uniquely identify \a library, assuming - * that all relevant indices were created with this function. - */ int ED_asset_library_reference_to_enum_value(const AssetLibraryReference *library) { /* Simple case: Predefined repository, just set the value. */ @@ -57,10 +53,6 @@ int ED_asset_library_reference_to_enum_value(const AssetLibraryReference *librar return ASSET_LIBRARY_LOCAL; } -/** - * Return an asset library reference matching the index returned by - * #ED_asset_library_reference_to_enum_value(). - */ AssetLibraryReference ED_asset_library_reference_from_enum_value(int value) { AssetLibraryReference library; @@ -92,31 +84,27 @@ AssetLibraryReference ED_asset_library_reference_from_enum_value(int value) return library; } -/** - * Translate all available asset libraries to an RNA enum, whereby the enum values match the result - * of #ED_asset_library_reference_to_enum_value() for any given library. - * - * Since this is meant for UI display, skips non-displayable libraries, that is, libraries with an - * empty name or path. - */ -const EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf() +const EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf( + const bool include_local_library) { - const EnumPropertyItem predefined_items[] = { - /* For the future. */ - // {ASSET_REPO_BUNDLED, "BUNDLED", 0, "Bundled", "Show the default user assets"}, - {ASSET_LIBRARY_LOCAL, - "LOCAL", - ICON_BLENDER, - "Current File", - "Show the assets currently available in this Blender session"}, - {0, nullptr, 0, nullptr, nullptr}, - }; - EnumPropertyItem *item = nullptr; int totitem = 0; - /* Add predefined items. */ - RNA_enum_items_add(&item, &totitem, predefined_items); + if (include_local_library) { + const EnumPropertyItem predefined_items[] = { + /* For the future. */ + // {ASSET_REPO_BUNDLED, "BUNDLED", 0, "Bundled", "Show the default user assets"}, + {ASSET_LIBRARY_LOCAL, + "LOCAL", + ICON_CURRENT_FILE, + "Current File", + "Show the assets currently available in this Blender session"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + /* Add predefined items. */ + RNA_enum_items_add(&item, &totitem, predefined_items); + } /* Add separator if needed. */ if (!BLI_listbase_is_empty(&U.asset_libraries)) { diff --git a/source/blender/editors/asset/intern/asset_list.cc b/source/blender/editors/asset/intern/asset_list.cc index c1b1e33d428..1bfdd83b8e3 100644 --- a/source/blender/editors/asset/intern/asset_list.cc +++ b/source/blender/editors/asset/intern/asset_list.cc @@ -44,10 +44,15 @@ #include "../space_file/filelist.h" #include "ED_asset_handle.h" +#include "ED_asset_indexer.h" #include "ED_asset_list.h" #include "ED_asset_list.hh" #include "asset_library_reference.hh" +/* Enable asset indexing. Currently disabled as ID properties aren't indexed yet and is needed for + * object snapping. See {D12990}. */ +//#define SPACE_FILE_ENABLE_ASSET_INDEXING + namespace blender::ed::asset { /* -------------------------------------------------------------------- */ @@ -169,6 +174,10 @@ void AssetList::setup() "", ""); +#ifdef SPACE_FILE_ENABLE_ASSET_INDEXING + filelist_setindexer(files, &file_indexer_asset); +#endif + char path[FILE_MAXDIR] = ""; if (user_library) { BLI_strncpy(path, user_library->path, sizeof(path)); @@ -309,6 +318,7 @@ StringRef AssetList::filepath() const { return filelist_dir(filelist_); } + /** \} */ /* -------------------------------------------------------------------- */ @@ -425,10 +435,6 @@ AssetListStorage::AssetListMap &AssetListStorage::global_storage() using namespace blender::ed::asset; -/** - * Invoke asset list reading, potentially in a parallel job. Won't wait until the job is done, - * and may return earlier. - */ void ED_assetlist_storage_fetch(const AssetLibraryReference *library_reference, const bContext *C) { AssetListStorage::fetch_library(*library_reference, *C); @@ -523,9 +529,6 @@ const char *ED_assetlist_library_path(const AssetLibraryReference *library_refer return nullptr; } -/** - * \return True if the region needs a UI redraw. - */ bool ED_assetlist_listen(const AssetLibraryReference *library_reference, const wmNotifier *notifier) { @@ -536,10 +539,6 @@ bool ED_assetlist_listen(const AssetLibraryReference *library_reference, return false; } -/** - * \return The number of assets stored in the asset list for \a library_reference, or -1 if there - * is no list fetched for it. - */ int ED_assetlist_size(const AssetLibraryReference *library_reference) { AssetList *list = AssetListStorage::lookup_list(*library_reference); @@ -549,31 +548,16 @@ int ED_assetlist_size(const AssetLibraryReference *library_reference) return -1; } -/** - * Tag all asset lists in the storage that show main data as needing an update (re-fetch). - * - * This only tags the data. If the asset list is visible on screen, the space is still responsible - * for ensuring the necessary redraw. It can use #ED_assetlist_listen() to check if the asset-list - * needs a redraw for a given notifier. - */ void ED_assetlist_storage_tag_main_data_dirty() { AssetListStorage::tagMainDataDirty(); } -/** - * Remapping of ID pointers within the asset lists. Typically called when an ID is deleted to clear - * all references to it (\a id_new is null then). - */ void ED_assetlist_storage_id_remap(ID *id_old, ID *id_new) { AssetListStorage::remapID(id_old, id_new); } -/** - * Can't wait for static deallocation to run. There's nested data allocated with our guarded - * allocator, it will complain about unfreed memory on exit. - */ void ED_assetlist_storage_exit() { AssetListStorage::destruct(); diff --git a/source/blender/editors/asset/intern/asset_ops.cc b/source/blender/editors/asset/intern/asset_ops.cc index f7c567c89f6..e4edff19a21 100644 --- a/source/blender/editors/asset/intern/asset_ops.cc +++ b/source/blender/editors/asset/intern/asset_ops.cc @@ -19,12 +19,23 @@ */ #include "BKE_asset_library.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" +#include "BLI_fnmatch.h" +#include "BLI_path_util.h" +#include "BLI_set.hh" + #include "ED_asset.h" +#include "ED_asset_catalog.hh" +#include "ED_screen.h" +#include "ED_util.h" /* XXX needs access to the file list, should all be done via the asset system in future. */ #include "ED_fileselect.h" @@ -33,6 +44,10 @@ #include "WM_api.h" +#include "DNA_space_types.h" + +#include "BLO_writefile.h" + using namespace blender; /* -------------------------------------------------------------------- */ @@ -377,8 +392,14 @@ static void ASSET_OT_clear(wmOperatorType *ot) /* -------------------------------------------------------------------- */ -static bool asset_list_refresh_poll(bContext *C) +static bool asset_library_refresh_poll(bContext *C) { + if (ED_operator_asset_browsing_active(C)) { + return true; + } + + /* While not inside an Asset Browser, check if there's a asset list stored for the active asset + * library (stored in the workspace, obtained via context). */ const AssetLibraryReference *library = CTX_wm_asset_library_ref(C); if (!library) { return false; @@ -387,23 +408,38 @@ static bool asset_list_refresh_poll(bContext *C) return ED_assetlist_storage_has_list_for_library(library); } -static int asset_list_refresh_exec(bContext *C, wmOperator *UNUSED(unused)) +static int asset_library_refresh_exec(bContext *C, wmOperator *UNUSED(unused)) { - const AssetLibraryReference *library = CTX_wm_asset_library_ref(C); - ED_assetlist_clear(library, C); + /* Execution mode #1: Inside the Asset Browser. */ + if (ED_operator_asset_browsing_active(C)) { + SpaceFile *sfile = CTX_wm_space_file(C); + ED_fileselect_clear(CTX_wm_manager(C), sfile); + WM_event_add_notifier(C, NC_SPACE | ND_SPACE_FILE_LIST, nullptr); + } + else { + /* Execution mode #2: Outside the Asset Browser, use the asset list. */ + const AssetLibraryReference *library = CTX_wm_asset_library_ref(C); + ED_assetlist_clear(library, C); + } + return OPERATOR_FINISHED; } -static void ASSET_OT_list_refresh(struct wmOperatorType *ot) +/** + * This operator currently covers both cases, the File/Asset Browser file list and the asset list + * used for the asset-view template. Once the asset list design is used by the Asset Browser, this + * can be simplified to just that case. + */ +static void ASSET_OT_library_refresh(struct wmOperatorType *ot) { /* identifiers */ - ot->name = "Refresh Asset List"; - ot->description = "Trigger a reread of the assets"; - ot->idname = "ASSET_OT_list_refresh"; + ot->name = "Refresh Asset Library"; + ot->description = "Reread assets and asset catalogs from the asset library on disk"; + ot->idname = "ASSET_OT_library_refresh"; /* api callbacks */ - ot->exec = asset_list_refresh_exec; - ot->poll = asset_list_refresh_poll; + ot->exec = asset_library_refresh_exec; + ot->poll = asset_library_refresh_poll; } /* -------------------------------------------------------------------- */ @@ -601,7 +637,7 @@ static bool asset_catalogs_save_poll(bContext *C) } const Main *bmain = CTX_data_main(C); - if (!bmain->name[0]) { + if (!bmain->filepath[0]) { CTX_wm_operator_poll_msg_set(C, "Cannot save asset catalogs before the Blender file is saved"); return false; } @@ -643,7 +679,274 @@ static void ASSET_OT_catalogs_save(struct wmOperatorType *ot) /* -------------------------------------------------------------------- */ -void ED_operatortypes_asset(void) +static bool could_be_asset_bundle(const Main *bmain); +static const bUserAssetLibrary *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); + +static bool asset_bundle_install_poll(bContext *C) +{ + /* This operator only works when the asset browser is set to Current File. */ + const SpaceFile *sfile = CTX_wm_space_file(C); + if (sfile == nullptr) { + return false; + } + if (!ED_fileselect_is_local_asset_library(sfile)) { + return false; + } + + const Main *bmain = CTX_data_main(C); + if (!could_be_asset_bundle(bmain)) { + return false; + } + + /* 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); + if (asset_lib) { + return false; + } + + return true; +} + +static int asset_bundle_install_invoke(struct bContext *C, + struct wmOperator *op, + const struct wmEvent * /*event*/) +{ + Main *bmain = CTX_data_main(C); + if (has_external_files(bmain, op->reports)) { + return OPERATOR_CANCELLED; + } + + WM_event_add_fileselect(C, op); + + /* Make the "Save As" dialog box default to "${ASSET_LIB_ROOT}/${CURRENT_FILE}.blend". */ + if (!set_filepath_for_asset_lib(bmain, op)) { + return OPERATOR_CANCELLED; + } + + return OPERATOR_RUNNING_MODAL; +} + +static int asset_bundle_install_exec(bContext *C, wmOperator *op) +{ + Main *bmain = CTX_data_main(C); + if (has_external_files(bmain, op->reports)) { + return OPERATOR_CANCELLED; + } + + /* Check file path, copied from #wm_file_write(). */ + char filepath[PATH_MAX]; + RNA_string_get(op->ptr, "filepath", filepath); + const size_t len = strlen(filepath); + + if (len == 0) { + BKE_report(op->reports, RPT_ERROR, "Path is empty, cannot save"); + return OPERATOR_CANCELLED; + } + + if (len >= FILE_MAX) { + BKE_report(op->reports, RPT_ERROR, "Path too long, cannot save"); + return OPERATOR_CANCELLED; + } + + /* Check that the destination is actually contained in the selected asset library. */ + if (!is_contained_in_selected_asset_library(op, filepath)) { + BKE_reportf(op->reports, RPT_ERROR, "Selected path is outside of the selected asset library"); + return OPERATOR_CANCELLED; + } + + WM_cursor_wait(true); + bke::AssetCatalogService *cat_service = get_catalog_service(C); + /* Store undo step, such that on a failed save the 'prepare_to_merge_on_write' call can be + * un-done. */ + cat_service->undo_push(); + cat_service->prepare_to_merge_on_write(); + + const int operator_result = WM_operator_name_call( + C, "WM_OT_save_mainfile", WM_OP_EXEC_DEFAULT, op->ptr); + WM_cursor_wait(false); + + if (operator_result != OPERATOR_FINISHED) { + cat_service->undo(); + return operator_result; + } + + const bUserAssetLibrary *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, + R"(Saved "%s" to asset library "%s")", + BLI_path_basename(bmain->filepath), + lib->name); + return OPERATOR_FINISHED; +} + +static const EnumPropertyItem *rna_asset_library_reference_itemf(bContext *UNUSED(C), + PointerRNA *UNUSED(ptr), + PropertyRNA *UNUSED(prop), + bool *r_free) +{ + const EnumPropertyItem *items = ED_asset_library_reference_to_rna_enum_itemf(false); + if (!items) { + *r_free = false; + } + + *r_free = true; + return items; +} + +static void ASSET_OT_bundle_install(struct wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Copy to Asset Library"; + ot->description = + "Copy the current .blend file into an Asset Library. Only works on standalone .blend files " + "(i.e. when no other files are referenced)"; + ot->idname = "ASSET_OT_bundle_install"; + + /* api callbacks */ + ot->exec = asset_bundle_install_exec; + ot->invoke = asset_bundle_install_invoke; + ot->poll = asset_bundle_install_poll; + + ot->prop = RNA_def_property(ot->srna, "asset_library_ref", PROP_ENUM, PROP_NONE); + RNA_def_property_flag(ot->prop, PROP_HIDDEN); + RNA_def_enum_funcs(ot->prop, rna_asset_library_reference_itemf); + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER | FILE_TYPE_BLENDER, + FILE_BLENDER, + FILE_SAVE, + WM_FILESEL_FILEPATH, + FILE_DEFAULTDISPLAY, + FILE_SORT_DEFAULT); +} + +/* Cheap check to see if this is an "asset bundle" just by checking main file name. + * A proper check will be done in the exec function, to ensure that no external files will be + * referenced. */ +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) +{ + 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); + return lib; +} + +static bool is_contained_in_selected_asset_library(struct wmOperator *op, const char *filepath) +{ + const bUserAssetLibrary *lib = selected_asset_library(op); + if (!lib) { + return false; + } + return BLI_path_contains(lib->path, filepath); +} + +/** + * Set the "filepath" RNA property based on selected "asset_library_ref". + * \return true if ok, false if error. + */ +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); + if (lib == nullptr) { + return false; + } + + /* Concatenate the filename of the current blend file. */ + const char *blend_filename = BLI_path_basename(bmain->filepath); + if (blend_filename == nullptr || blend_filename[0] == '\0') { + return false; + } + + char file_path[PATH_MAX]; + BLI_join_dirfile(file_path, sizeof(file_path), lib->path, blend_filename); + RNA_string_set(op->ptr, "filepath", file_path); + + return true; +} + +struct FileCheckCallbackInfo { + struct ReportList *reports; + Set<std::string> external_files; +}; + +static bool external_file_check_callback(BPathForeachPathData *bpath_data, + char * /*path_dst*/, + const char *path_src) +{ + FileCheckCallbackInfo *callback_info = static_cast<FileCheckCallbackInfo *>( + bpath_data->user_data); + callback_info->external_files.add(std::string(path_src)); + return false; +} + +/** + * Do a check on any external files (.blend, textures, etc.) being used. + * The ASSET_OT_bundle_install operator only works on standalone .blend files + * (catalog definition files are fine, though). + * + * \return true when there are external files, false otherwise. + */ +static bool has_external_files(Main *bmain, struct ReportList *reports) +{ + struct FileCheckCallbackInfo callback_info = {reports, Set<std::string>()}; + + eBPathForeachFlag flag = static_cast<eBPathForeachFlag>( + BKE_BPATH_FOREACH_PATH_SKIP_PACKED /* Packed files are fine. */ + | BKE_BPATH_FOREACH_PATH_SKIP_MULTIFILE /* Only report multi-files once, it's enough. */ + | BKE_BPATH_TRAVERSE_SKIP_WEAK_REFERENCES); /* Only care about actually used files. */ + + BPathForeachPathData bpath_data = { + /* bmain */ bmain, + /* callback_function */ &external_file_check_callback, + /* flag */ flag, + /* user_data */ &callback_info, + /* absolute_base_path */ nullptr, + }; + BKE_bpath_foreach_path_main(&bpath_data); + + if (callback_info.external_files.is_empty()) { + /* No external dependencies. */ + return false; + } + + if (callback_info.external_files.size() == 1) { + /* Only one external dependency, report it directly. */ + BKE_reportf(callback_info.reports, + RPT_ERROR, + "Unable to copy bundle due to external dependency: \"%s\"", + callback_info.external_files.begin()->c_str()); + return true; + } + + /* Multiple external dependencies, report the aggregate and put details on console. */ + BKE_reportf( + callback_info.reports, + RPT_ERROR, + "Unable to copy bundle due to %ld external dependencies; more details on the console", + callback_info.external_files.size()); + printf("Unable to copy bundle due to %ld external dependencies:\n", + callback_info.external_files.size()); + for (const std::string &path : callback_info.external_files) { + printf(" \"%s\"\n", path.c_str()); + } + return true; +} + +/* -------------------------------------------------------------------- */ + +void ED_operatortypes_asset() { WM_operatortype_append(ASSET_OT_mark); WM_operatortype_append(ASSET_OT_clear); @@ -654,6 +957,7 @@ void ED_operatortypes_asset(void) WM_operatortype_append(ASSET_OT_catalog_undo); WM_operatortype_append(ASSET_OT_catalog_redo); WM_operatortype_append(ASSET_OT_catalog_undo_push); + WM_operatortype_append(ASSET_OT_bundle_install); - WM_operatortype_append(ASSET_OT_list_refresh); + WM_operatortype_append(ASSET_OT_library_refresh); } |