Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
../include
../../gpu
../../imbuf
../../makesrna
# dna_type_offsets.h
${CMAKE_CURRENT_BINARY_DIR}/../../makesdna/intern
# RNA_prototypes.hh
${CMAKE_BINARY_DIR}/source/blender/makesrna
)
set(INC_SYS
)
set(SRC
intern/asset_catalog.cc
intern/asset_filter.cc
intern/asset_import.cc
intern/asset_indexer.cc
intern/asset_indexer_remote_file_status.cc
intern/asset_indexer_remote_listing.cc
intern/asset_indexer_remote_listing_v1.cc
intern/asset_library_reference_enum.cc
intern/asset_library_utils.cc
intern/asset_list.cc
intern/asset_mark_clear.cc
intern/asset_menu_utils.cc
intern/asset_ops.cc
intern/asset_shelf.cc
intern/asset_shelf_asset_view.cc
intern/asset_shelf_catalog_selector.cc
intern/asset_shelf_popover.cc
intern/asset_shelf_regiondata.cc
intern/asset_shelf_settings.cc
intern/asset_temp_id_consumer.cc
intern/asset_type.cc
intern/asset_ui_utils.cc
ED_asset_catalog.hh
ED_asset_filter.hh
ED_asset_import.hh
ED_asset_indexer.hh
ED_asset_library.hh
ED_asset_list.hh
ED_asset_mark_clear.hh
ED_asset_shelf.hh
ED_asset_temp_id_consumer.hh
ED_asset_type.hh
intern/asset_index.hh
intern/asset_indexer_remote_file_status.hh
intern/asset_indexer_remote_listing.hh
intern/asset_library_reference.hh
intern/asset_shelf.hh
)
set(LIB
PRIVATE bf::asset_system
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blenloader
PRIVATE bf::blentranslation
PRIVATE bf::dna
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE bf::windowmanager
)
if(WITH_PYTHON)
list(APPEND INC
../../python
)
add_definitions(-DWITH_PYTHON)
endif()
blender_add_lib(bf_editor_asset "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
# RNA_prototypes.hh
add_dependencies(bf_editor_asset bf_rna)

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* UI/Editor level API for catalog operations, creating richer functionality than the asset system
* catalog API provides (which this uses internally).
*
* Functions can be expected to not perform any change when #catalogs_read_only() returns
* true. Generally UI code should disable such functionality in this case, so these functions are
* not called at all.
*
* Note that `ED_asset_catalog.hh` is part of this API.
*/
#pragma once
#include <optional>
#include "AS_asset_catalog.hh"
#include "BLI_string_ref.hh"
namespace blender {
struct AssetWeakReference;
struct Main;
namespace asset_system {
class AssetLibrary;
}
namespace ed::asset {
void catalogs_save_from_main_path(asset_system::AssetLibrary *library, const Main *bmain);
void catalogs_save_from_asset_reference(asset_system::AssetLibrary &library,
const AssetWeakReference &reference);
/**
* Saving catalog edits when the file is saved is a global option shared for each asset library,
* and as such ignores the per asset library #catalogs_read_only().
*/
void catalogs_set_save_catalogs_when_file_is_saved(bool should_save);
bool catalogs_get_save_catalogs_when_file_is_saved();
/**
* Returns if the catalogs of \a library are allowed to be editable, or if the UI should forbid
* edits.
*/
[[nodiscard]] bool catalogs_read_only(const asset_system::AssetLibrary &library);
asset_system::AssetCatalog *catalog_add(asset_system::AssetLibrary *library,
StringRefNull name,
StringRef parent_path = nullptr);
void catalog_remove(asset_system::AssetLibrary *library,
const asset_system::CatalogID &catalog_id);
void catalog_rename(asset_system::AssetLibrary *library,
asset_system::CatalogID catalog_id,
StringRefNull new_name);
/**
* 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 catalog_move(asset_system::AssetLibrary *library,
asset_system::CatalogID src_catalog_id,
std::optional<asset_system::CatalogID> dst_parent_catalog_id = std::nullopt);
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,81 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Functions for filtering assets.
*/
#pragma once
#include <optional>
#include "DNA_listBase.h"
#include "BLI_function_ref.hh"
#include "BLI_multi_value_map.hh"
#include "BLI_vector.hh"
#include "AS_asset_catalog_path.hh"
#include "AS_asset_catalog_tree.hh"
namespace blender {
struct AssetLibraryReference;
struct AssetMetaData;
struct AssetTag;
struct bContext;
namespace asset_system {
class AssetLibrary;
class AssetRepresentation;
} // namespace asset_system
namespace ed::asset {
struct AssetFilterSettings {
/** Tags to match against. These are newly allocated, and compared against the
* #AssetMetaData.tags. */
ListBaseT<AssetTag> tags;
uint64_t id_types; /* rna_enum_id_type_filter_items */
};
/**
* 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 filter_matches_asset(const AssetFilterSettings *filter,
const asset_system::AssetRepresentation &asset);
struct AssetItemTree {
asset_system::AssetCatalogTree catalogs;
MultiValueMap<asset_system::AssetCatalogPath, asset_system::AssetRepresentation *>
assets_per_path;
/** Assets not added to a catalog, not part of #assets_per_path. */
Vector<asset_system::AssetRepresentation *> unassigned_assets;
/** True if the tree is out of date compared to asset libraries and must be rebuilt. */
bool dirty = true;
};
asset_system::AssetCatalogTree build_filtered_catalog_tree(
const asset_system::AssetLibrary &library,
const AssetLibraryReference &library_ref,
FunctionRef<bool(const asset_system::AssetRepresentation &)> is_asset_visible_fn);
AssetItemTree build_filtered_all_catalog_tree(
const AssetLibraryReference &library_ref,
const bContext &C,
const AssetFilterSettings &filter_settings,
FunctionRef<bool(const AssetMetaData &)> meta_data_filter = {},
const std::optional<StringRef> skip_prefix = std::nullopt);
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,52 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include <optional>
namespace blender {
struct ID;
struct Main;
struct ReportList;
struct Scene;
struct View3D;
struct ViewLayer;
namespace asset_system {
class AssetRepresentation;
}
namespace ed::asset {
struct ImportInstantiateContext {
Scene *scene;
ViewLayer *view_layer;
View3D *view3d;
};
/**
* If the asset already has a corresponding local #ID, return it. Otherwise, link or append the
* asset's data-block, using "Append & Reuse" if the method is unspecified.
*
* \note This can return null! Importing can fail if the asset was deleted or moved since the asset
* library was loaded.
*
* \param import_method: Overrides library's default importing method.
* If not set and the library has no default, #ASSET_IMPORT_APPEND_REUSE will be used.
*/
ID *asset_local_id_ensure_imported(
Main &bmain,
const asset_system::AssetRepresentation &asset,
int flags = 0, /* #eFileSel_Params_Flag + #eBLOLibLinkFlags */
const std::optional<eAssetImportMethod> import_method = std::nullopt,
const std::optional<ImportInstantiateContext> instantiate_context = std::nullopt,
ReportList *reports = nullptr);
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,112 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include <filesystem>
#include <optional>
#include "BLI_function_ref.hh"
#include "BLI_utility_mixins.hh"
#include "AS_asset_file_status.hh"
#include "AS_remote_library.hh"
#include "ED_file_indexer.hh"
namespace blender {
class StringRefNull;
} // namespace blender
namespace blender::ed::asset::index {
/**
* 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;
struct RemoteListingAssetEntry : NonCopyable {
BLODataBlockInfo datablock_info = {};
short idcode = 0;
/** The status of the asset's on-disk file(s). */
asset_system::RemoteAssetFileStatus remote_file_status =
asset_system::RemoteAssetFileStatus::UNSET;
asset_system::OnlineAssetInfo online_info;
RemoteListingAssetEntry() = default;
RemoteListingAssetEntry(RemoteListingAssetEntry &&);
RemoteListingAssetEntry &operator=(RemoteListingAssetEntry &&);
~RemoteListingAssetEntry();
/**
* Empty entries are used to skip assets when the Blender version doesn't match.
*/
bool is_empty() const
{
return idcode == 0;
}
};
/**
* Representation of the FileV1 type in the OpenAPI definition.
* See blender_asset_library_openapi.yaml.
*
* Not all fields are included here, just the ones that are used by Blender.
*/
struct RemoteListingFileEntry : NonCopyable {
std::string local_path;
asset_system::URLWithHash download_url;
int64_t size_in_bytes;
/**
* Status of the file on disk, compared to the information in the fields above.
*
* \see asset_system::OnlineAssetInfo::file_status for the per-asset status that may be more
* convenient to use.
*
* \see blender::ed::asset::index::FileStatusChecker. */
std::optional<asset_system::RemoteAssetFileStatus> file_status;
};
using RemoteListingEntryProcessFn = FunctionRef<bool(RemoteListingAssetEntry &)>;
using RemoteListingWaitForPagesFn = FunctionRef<bool()>;
/* Uses #std::filesystem::file_time_type because it needs to be compared against file time-stamps,
* which may have low precision (often just 1 sec). */
using Timestamp = std::filesystem::file_time_type;
/**
* \param process_fn: Called for each asset entry read from the listing. It's fine to move out the
* passed #RemoteListingAssetEntry. Returning false will cancel the whole reading process and not
* read any further entries.
* \param wait_fn: If this is set, reading will keep retrying to load unavailable pages, and call
* this wait function for each try. The wait function can block until it thinks new pages might
* be available. If this returns false the whole reading process will be cancelled.
*/
bool read_remote_listing(StringRefNull root_dirpath,
StringRefNull asset_library_name,
ReportList &reports,
RemoteListingEntryProcessFn process_fn,
RemoteListingWaitForPagesFn wait_fn = nullptr,
std::optional<Timestamp> ignore_before_timestamp = std::nullopt);
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,86 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include "BLI_function_ref.hh"
#include "BLI_string_ref.hh"
#include "DNA_asset_types.h"
namespace blender {
struct bUserAssetLibrary;
struct bContext;
struct AssetLibraryReference;
struct EnumPropertyItem;
struct StringPropertySearchVisitParams;
namespace asset_system {
class AssetCatalog;
class AssetCatalogPath;
class AssetRepresentation;
} // namespace asset_system
namespace ed::asset {
/**
* Return an index that can be used to uniquely identify \a library, assuming
* that all relevant indices were created with this function.
*/
int library_reference_to_enum_value(const AssetLibraryReference *library);
/**
* Return an asset library reference matching the index returned by
* #library_reference_to_enum_value().
*/
AssetLibraryReference library_reference_from_enum_value(int value);
/**
* Translate all available asset libraries to an RNA enum, whereby the enum values match the result
* of #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_readonly: If set, the "All" and "Essentials" asset libraries will be added, which
* cannot be written to.
* \param include_current_file: If set, "Current File" asset library will be added.
* \param include_remote_libraries: If set, all online asset libraries with a URL set will be
* added.
* \param include_separate_online_essentials: If set, the online essentials will be added as a
* separate library from the normal Essentials. Usually they are a part of the normal Essentials
* library.
*/
const EnumPropertyItem *library_reference_to_rna_enum_itemf(
bool include_readonly,
bool include_current_file,
bool include_remote_libraries,
bool include_separate_online_essentials);
/**
* Same as #library_reference_to_rna_enum_itemf(), but only includes custom on-disk asset libraries
* (libraries on disk, configured in the Preferences). Online asset libraries will be excluded,
* their on-disk location is just a cache.
*/
const EnumPropertyItem *custom_libraries_rna_enum_itemf();
/**
* Find the catalog with the given path in the library. Creates it in case it doesn't exist.
*/
asset_system::AssetCatalog &library_ensure_catalogs_in_path(
asset_system::AssetLibrary &library, const asset_system::AssetCatalogPath &path);
AssetLibraryReference user_library_to_library_ref(const bUserAssetLibrary &user_library);
/**
* Call after changes to an asset library have been made to reflect the changes in the UI.
*/
void refresh_asset_library(const bContext *C, const AssetLibraryReference &library_ref);
void refresh_asset_library(const bContext *C, const bUserAssetLibrary &user_library);
void refresh_asset_library_from_asset(const bContext *C,
const asset_system::AssetRepresentation &asset);
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,124 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include "BLI_function_ref.hh"
#include "BLI_string_ref.hh"
namespace blender {
struct AssetLibraryReference;
struct bContext;
struct ID;
struct ImBuf;
struct wmNotifier;
struct wmRegionListenerParams;
struct wmWindowManager;
namespace asset_system {
class AssetLibrary;
class AssetRepresentation;
} // namespace asset_system
namespace ed::asset::list {
void asset_reading_region_listen_fn(const wmRegionListenerParams *params);
/**
* Get the asset library being read into an asset-list and identified using \a library_reference.
*
* \note The asset library may be allocated and loaded asynchronously, so it's not available right
* after fetching, and this function will return null. The asset list code sends
* `NC_ASSET | ND_ASSET_LIST_READING` notifiers until loading is done, they can be used
* to continuously call this function to retrieve the asset library once available.
*/
asset_system::AssetLibrary *library_get_once_available(
const AssetLibraryReference &library_reference);
/** Can return false to stop iterating. */
using AssetListIterFn = FunctionRef<bool(asset_system::AssetRepresentation &)>;
void iterate(const AssetLibraryReference &library_reference, AssetListIterFn fn);
/**
* Invoke asset list reading, potentially in a parallel job. Won't wait until the job is done,
* and may return earlier.
*
* \see: #storage_fetch_blocking for a blocking version.
* \warning: Asset list reading involves an #AS_asset_library_load() call which may reload asset
* library data like catalogs (invalidating pointers). Refer to its warning for details.
* \warning: The caller is responsible for ensuring \a library_reference is valid so that it
* doesn't reference a deleted asset library. Otherwise an empty list may be loaded and
* there are assertions to catch the case. But it's unclear what library choice is being
* shown to the user, and if that matches the empty library that will be presented.
*/
void storage_fetch(const AssetLibraryReference *library_reference, const bContext *C);
/**
* Invoke asset list reading, guaranteed to execute on the same thread.
*
* \see #storage_fetch for an asynchronous version. Its warning on \a library_reference applies
* here too.
*/
void storage_fetch_blocking(const AssetLibraryReference &library_reference, const bContext &C);
bool is_loaded(const AssetLibraryReference *library_reference);
/**
* Clears this asset library and the "All" asset library for reload in both the static asset list
* storage, as well as for all open asset browsers. Call this whenever the content of the given
* asset library changed in a way that a reload is necessary.
*/
void clear(const AssetLibraryReference *library_reference, const bContext *C);
/**
* Clears the all asset library for reload in both the static asset list storage, as well as for
* all open asset browsers. Call this whenever any asset library content changed in a way that a
* reload is necessary.
*/
void clear_all_library(const bContext *C);
void on_remote_assets_downloaded(wmWindowManager &wm,
StringRef library_url,
StringRef downloaded_file_abspath);
/**
* Returns if the given asset library in global asset list storage.
*/
bool has_list_storage_for_library(const AssetLibraryReference *library_reference);
/**
* Returns if any asset browser is visible showing the given asset library. Asset browsers are not
* really handled by this API, but for convenience of managing clearing it's handled here together
* with #has_list_storage_for_library().
*/
bool has_asset_browser_storage_for_library(const AssetLibraryReference *library_reference,
const bContext *C);
/**
* 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 #listen() to check if the asset-list
* needs a redraw for a given notifier.
*/
void storage_tag_main_data_dirty();
/**
* 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 storage_id_remap(ID *id_old, 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 storage_exit();
/**
* \return True if the region needs a UI redraw.
*/
bool listen(const 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 size(const AssetLibraryReference *library_reference);
} // namespace ed::asset::list
} // namespace blender

View File

@@ -0,0 +1,63 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
namespace blender {
struct AssetMetaData;
struct bContext;
struct ID;
struct Main;
namespace ed::asset {
/**
* Mark the data-block as asset.
*
* To ensure the data-block is saved, this sets Fake User.
*
* \return whether the data-block was marked as asset; false when it is not capable of becoming an
* asset, or when it already was an asset. */
bool mark_id(ID *id);
/**
* Generate preview image for the given data-block.
*
* The preview image might be generated using a background thread.
*/
void generate_preview(const bContext *C, ID *id);
/**
* Remove the asset metadata, turning the ID into a "normal" ID.
*
* This clears the Fake User. If for some reason the data-block is meant to be saved anyway, the
* caller is responsible for explicitly setting the Fake User.
*
* \return whether the asset metadata was actually removed; false when the ID was not an asset.
*/
bool clear_id(ID *id);
/**
* Copy the asset metadata to the given destination ID.
*
* The copy is assigned to \a destination, any pre-existing asset metadata is
* freed before that. If \a destination was not yet marked as asset, it will be
* after this call.
*
* \return true when the copy succeeded, false otherwise. The only reason for
* failure is when \a destination is of a type that cannot be an asset.
*/
bool copy_to_id(const AssetMetaData *asset_data, ID *destination);
void pre_save_assets(Main *bmain);
bool can_mark_single_from_context(const bContext *C);
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,121 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include <memory>
namespace blender {
struct ARegion;
struct ARegionType;
struct AssetShelf;
struct AssetShelfSettings;
struct AssetShelfType;
struct BlendDataReader;
struct BlendWriter;
struct Main;
struct RegionPollParams;
struct ScrArea;
struct bContext;
struct bContextDataResult;
struct wmRegionListenerParams;
struct wmRegionMessageSubscribeParams;
struct wmWindowManager;
class StringRef;
class StringRefNull;
namespace asset_system {
class AssetRepresentation;
}
namespace ed::asset::shelf {
/* -------------------------------------------------------------------- */
/** \name Asset Shelf Regions
*
* Naming conventions:
* - #regions_xxx(): Applies to both regions (#RGN_TYPE_ASSET_SHELF and
* #RGN_TYPE_ASSET_SHELF_HEADER).
* - #region_xxx(): Applies to the main shelf region (#RGN_TYPE_ASSET_SHELF).
* - #header_region_xxx(): Applies to the shelf header region
* (#RGN_TYPE_ASSET_SHELF_HEADER).
*
* \{ */
bool regions_poll(const RegionPollParams *params);
/** Only needed for #RGN_TYPE_ASSET_SHELF (not #RGN_TYPE_ASSET_SHELF_HEADER). */
void *region_duplicate(void *regiondata);
void region_free(ARegion *region);
void region_init(wmWindowManager *wm, ARegion *region);
int region_snap(const ARegion *region, int size, int axis);
void region_on_user_resize(const ARegion *region);
void region_listen(const wmRegionListenerParams *params);
void region_message_subscribe(const wmRegionMessageSubscribeParams *params);
void region_layout(const bContext *C, ARegion *region);
void region_draw(const bContext *C, ARegion *region);
void region_on_poll_success(const bContext *C, ARegion *region);
void region_blend_read_data(BlendDataReader *reader, ARegion *region);
void region_blend_write(BlendWriter *writer, ARegion *region);
int region_prefsizey();
void header_region_init(wmWindowManager *wm, ARegion *region);
void header_region(const bContext *C, ARegion *region);
void header_region_listen(const wmRegionListenerParams *params);
int header_region_size();
void types_register(ARegionType *region_type, const int space_type);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Asset Shelf Type
* \{ */
void type_register(std::unique_ptr<AssetShelfType> type);
void type_unregister(const AssetShelfType &shelf_type);
/**
* Poll an asset shelf type for display as a popup. Doesn't check for space-type (the type's
* #bl_space_type) since popups should ignore this to allow displaying in any space.
*
* Permanent/non-popup asset shelf regions should use #type_poll_for_space_type() instead.
*/
bool type_poll_for_popup(const bContext &C, const AssetShelfType *shelf_type);
bool type_asset_poll(const AssetShelfType &shelf_type,
const asset_system::AssetRepresentation &asset);
AssetShelfType *type_find_from_idname(StringRef idname);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Asset Shelf Popup
* \{ */
void type_popup_unlink(const AssetShelfType &shelf_type);
void ensure_asset_library_fetched(const bContext &C, const AssetShelfType &shelf_type);
/** \} */
/* -------------------------------------------------------------------- */
void type_unlink(const Main &bmain, const AssetShelfType &shelf_type);
int tile_width(const AssetShelfSettings &settings);
int tile_height(const AssetShelfSettings &settings);
AssetShelf *active_shelf_from_area(const ScrArea *area);
/**
* Enable catalog path in all shelves visible in all windows.
*/
void show_catalog_in_visible_shelves(const bContext &C, const StringRefNull catalog_path);
int context(const bContext *C, const char *member, bContextDataResult *result);
} // namespace ed::asset::shelf
} // namespace blender

View File

@@ -0,0 +1,40 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \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
#include "DNA_ID_enums.h"
struct AssetTempIDConsumer;
namespace blender {
struct ID;
struct Main;
struct ReportList;
namespace asset_system {
class AssetRepresentation;
}
namespace ed::asset {
AssetTempIDConsumer *temp_id_consumer_create(const asset_system::AssetRepresentation *asset);
void temp_id_consumer_free(AssetTempIDConsumer **consumer);
ID *temp_id_consumer_ensure_local_id(AssetTempIDConsumer *consumer,
ID_Type id_type,
Main *bmain,
ReportList *reports);
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include "DNA_ID.h"
namespace blender {
struct ID;
namespace ed::asset {
bool id_type_is_non_experimental(const ID *id);
#define ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_FLAGS \
(FILTER_ID_BR | FILTER_ID_MA | FILTER_ID_GR | FILTER_ID_OB | FILTER_ID_AC | FILTER_ID_WO | \
FILTER_ID_NT | FILTER_ID_SCE)
/**
* Check if the asset type for \a id (which doesn't need to be an asset right now) can be an asset,
* respecting the "Extended Asset Browser" experimental feature flag.
*/
bool id_type_is_supported(const ID *id);
/**
* Get the filter flags (subset of #FILTER_ID_ALL) representing the asset ID types that may be
* turned into assets, respecting the "Extended Asset Browser" experimental feature flag.
* \note Does not check for #BKE_id_can_be_asset(), so may return filter flags for IDs that can
* never be assets.
*/
int64_t types_supported_as_filter_flags();
} // namespace ed::asset
} // namespace blender

View File

@@ -0,0 +1,198 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "AS_asset_library.hh"
#include "AS_asset_catalog.hh"
#include "BKE_main.hh"
#include "BLI_string_utils.hh"
#include "RNA_access.hh"
#include "ED_asset_catalog.hh"
#include "WM_api.hh"
namespace blender::ed::asset {
using namespace blender::asset_system;
bool catalogs_read_only(const AssetLibrary &library)
{
const asset_system::AssetCatalogService &catalog_service = library.catalog_service();
return catalog_service.is_read_only();
}
static std::string catalog_name_ensure_unique(AssetCatalogService &catalog_service,
StringRefNull name,
StringRef parent_path)
{
char unique_name[MAX_NAME] = "";
BLI_uniquename_cb(
[&](const StringRef check_name) {
AssetCatalogPath fullpath = AssetCatalogPath(parent_path) / check_name;
return catalog_service.find_catalog_by_path(fullpath);
},
name.c_str(),
'.',
unique_name,
sizeof(unique_name));
return unique_name;
}
asset_system::AssetCatalog *catalog_add(AssetLibrary *library,
StringRefNull name,
StringRef parent_path)
{
asset_system::AssetCatalogService &catalog_service = library->catalog_service();
if (catalog_service.is_read_only()) {
return nullptr;
}
std::string unique_name = catalog_name_ensure_unique(catalog_service, name, parent_path);
AssetCatalogPath fullpath = AssetCatalogPath(parent_path) / unique_name;
catalog_service.undo_push();
asset_system::AssetCatalog *new_catalog = catalog_service.create_catalog(fullpath);
if (!new_catalog) {
return nullptr;
}
catalog_service.tag_has_unsaved_changes(new_catalog);
WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, nullptr);
return new_catalog;
}
void catalog_remove(AssetLibrary *library, const CatalogID &catalog_id)
{
asset_system::AssetCatalogService &catalog_service = library->catalog_service();
if (catalog_service.is_read_only()) {
return;
}
catalog_service.undo_push();
catalog_service.tag_has_unsaved_changes(nullptr);
catalog_service.prune_catalogs_by_id(catalog_id);
WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, nullptr);
}
void catalog_rename(AssetLibrary *library,
const CatalogID catalog_id,
const StringRefNull new_name)
{
asset_system::AssetCatalogService &catalog_service = library->catalog_service();
if (catalog_service.is_read_only()) {
return;
}
AssetCatalog *catalog = catalog_service.find_catalog(catalog_id);
const AssetCatalogPath new_path = catalog->path.parent() / StringRef(new_name);
const AssetCatalogPath clean_new_path = new_path.cleanup();
if (new_path == catalog->path || clean_new_path == catalog->path) {
/* Nothing changed, so don't bother renaming for nothing. */
return;
}
catalog_service.undo_push();
catalog_service.tag_has_unsaved_changes(catalog);
catalog_service.update_catalog_path(catalog_id, clean_new_path);
WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, nullptr);
}
void catalog_move(AssetLibrary *library,
const CatalogID src_catalog_id,
const std::optional<CatalogID> dst_parent_catalog_id)
{
asset_system::AssetCatalogService &catalog_service = library->catalog_service();
if (catalog_service.is_read_only()) {
return;
}
AssetCatalog *src_catalog = catalog_service.find_catalog(src_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;
}
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) {
/* Nothing changed, so don't bother renaming for nothing. */
return;
}
catalog_service.undo_push();
catalog_service.tag_has_unsaved_changes(src_catalog);
catalog_service.update_catalog_path(src_catalog_id, clean_new_path);
WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, nullptr);
}
void catalogs_save_from_main_path(AssetLibrary *library, const Main *bmain)
{
asset_system::AssetCatalogService &catalog_service = library->catalog_service();
if (catalog_service.is_read_only()) {
return;
}
/* 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->filepath);
}
void catalogs_save_from_asset_reference(AssetLibrary &library, const AssetWeakReference &reference)
{
asset_system::AssetCatalogService &catalog_service = library.catalog_service();
if (catalog_service.is_read_only()) {
return;
}
char asset_full_path_buffer[1024 + MAX_ID_NAME /*FILE_MAX_LIBEXTRA*/];
char *file_path = nullptr;
AS_asset_full_path_explode_from_weak_ref(
&reference, asset_full_path_buffer, &file_path, nullptr, nullptr);
if (!file_path) {
BLI_assert_unreachable();
return;
}
/* 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(file_path);
}
void catalogs_set_save_catalogs_when_file_is_saved(const bool should_save)
{
asset_system::AssetLibrary::save_catalogs_when_file_is_saved = should_save;
}
bool catalogs_get_save_catalogs_when_file_is_saved()
{
return asset_system::AssetLibrary::save_catalogs_when_file_is_saved;
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,190 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "AS_asset_representation.hh"
#include "BKE_idtype.hh"
#include "BLI_listbase.h"
#include "DNA_asset_types.h"
#include "AS_asset_catalog_tree.hh"
#include "AS_asset_library.hh"
#include "ED_asset_filter.hh"
#include "ED_asset_list.hh"
namespace blender::ed::asset {
bool filter_matches_asset(const AssetFilterSettings *filter,
const asset_system::AssetRepresentation &asset)
{
ID_Type asset_type = asset.get_id_type();
uint64_t asset_id_filter = BKE_idtype_idcode_to_idfilter(asset_type);
if (filter->id_types && (filter->id_types & asset_id_filter) == 0) {
return false;
}
/* Not very efficient (O(n^2)), could be improved quite a bit. */
for (const AssetTag &filter_tag : filter->tags) {
AssetMetaData &asset_data = asset.get_metadata();
AssetTag *matched_tag = static_cast<AssetTag *>(
BLI_findstring(&asset_data.tags, filter_tag.name, offsetof(AssetTag, name)));
if (matched_tag == nullptr) {
return false;
}
}
/* Successfully passed through all filters. */
return true;
}
asset_system::AssetCatalogTree build_filtered_catalog_tree(
const asset_system::AssetLibrary &library,
const AssetLibraryReference &library_ref,
const FunctionRef<bool(const asset_system::AssetRepresentation &)> is_asset_visible_fn)
{
Set<StringRef> known_paths;
/* Collect paths containing assets. */
list::iterate(library_ref, [&](asset_system::AssetRepresentation &asset) {
if (!is_asset_visible_fn(asset)) {
return true;
}
const AssetMetaData &meta_data = asset.get_metadata();
if (BLI_uuid_is_nil(meta_data.catalog_id)) {
return true;
}
const asset_system::AssetCatalog *catalog = library.catalog_service().find_catalog(
meta_data.catalog_id);
if (catalog == nullptr) {
return true;
}
known_paths.add(catalog->path.str());
return true;
});
/* Build catalog tree. */
asset_system::AssetCatalogTree filtered_tree;
const std::shared_ptr<const asset_system::AssetCatalogTree> full_tree =
library.catalog_service().catalog_tree();
full_tree->foreach_item([&](const asset_system::AssetCatalogTreeItem &item) {
if (!known_paths.contains(item.catalog_path().str())) {
return;
}
asset_system::AssetCatalog *catalog = library.catalog_service().find_catalog(
item.get_catalog_id());
if (catalog == nullptr) {
return;
}
filtered_tree.insert_item(*catalog);
});
return filtered_tree;
}
static asset_system::AssetCatalogPath catalog_path_skipped_prefix(
const asset_system::AssetCatalogPath &full_path, const std::optional<StringRef> skip_prefix)
{
const bool has_skip_prefix = skip_prefix && full_path.str().starts_with(*skip_prefix) &&
full_path.str()[skip_prefix->size()] ==
asset_system::AssetCatalogPath::SEPARATOR;
return has_skip_prefix ? StringRef(full_path.str()).drop_prefix(skip_prefix->size() + 1) :
full_path;
}
AssetItemTree build_filtered_all_catalog_tree(
const AssetLibraryReference &library_ref,
const bContext &C,
const AssetFilterSettings &filter_settings,
const FunctionRef<bool(const AssetMetaData &)> meta_data_filter,
const std::optional<StringRef> skip_prefix)
{
MultiValueMap<asset_system::AssetCatalogPath, asset_system::AssetRepresentation *>
assets_per_path;
Vector<asset_system::AssetRepresentation *> unassigned_assets;
list::storage_fetch(&library_ref, &C);
asset_system::AssetLibrary *library = list::library_get_once_available(library_ref);
if (!library) {
return {};
}
const bool loading_finished = list::is_loaded(&library_ref);
const bool dirty = !loading_finished;
list::iterate(library_ref, [&](asset_system::AssetRepresentation &asset) {
if (!filter_matches_asset(&filter_settings, asset)) {
return true;
}
const AssetMetaData &meta_data = asset.get_metadata();
if (meta_data_filter && !meta_data_filter(meta_data)) {
return true;
}
if (BLI_uuid_is_nil(meta_data.catalog_id)) {
unassigned_assets.append(&asset);
return true;
}
const asset_system::AssetCatalog *catalog = library->catalog_service().find_catalog(
meta_data.catalog_id);
if (catalog == nullptr) {
/* Also include assets with catalogs we're unable to find (e.g. the catalog was deleted) in
* the "Unassigned" list. */
unassigned_assets.append(&asset);
return true;
}
const asset_system::AssetCatalogPath catalog_path = catalog_path_skipped_prefix(catalog->path,
skip_prefix);
if (catalog_path.str().empty() ||
catalog_path.str() == std::string{asset_system::AssetCatalogPath::SEPARATOR})
{
/* Also include assets with an empty catalog path in the "Unassigned" list. Mostly relevant
* when assets are directly placed under the skipped prefix path. */
unassigned_assets.append(&asset);
return true;
}
assets_per_path.add(catalog_path, &asset);
return true;
});
asset_system::AssetCatalogTree catalogs_with_node_assets;
const std::shared_ptr<const asset_system::AssetCatalogTree> catalog_tree =
library->catalog_service().catalog_tree();
catalog_tree->foreach_item([&](const asset_system::AssetCatalogTreeItem &item) {
const asset_system::AssetCatalogPath catalog_path = catalog_path_skipped_prefix(
item.catalog_path(), skip_prefix);
if (assets_per_path.lookup(catalog_path).is_empty()) {
return;
}
asset_system::AssetCatalog *catalog = library->catalog_service().find_catalog(
item.get_catalog_id());
if (catalog == nullptr) {
return;
}
catalogs_with_node_assets.insert_item(*catalog, skip_prefix);
});
return {std::move(catalogs_with_node_assets),
std::move(assets_per_path),
std::move(unassigned_assets),
dirty};
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,107 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "AS_asset_representation.hh"
#include "DNA_space_types.h"
#include "BLO_readfile.hh"
#include "WM_api.hh"
#include "ED_asset_import.hh"
namespace blender::ed::asset {
ID *asset_local_id_ensure_imported(
Main &bmain,
const asset_system::AssetRepresentation &asset,
const int flags, /* #eFileSel_Params_Flag + #eBLOLibLinkFlags */
const std::optional<eAssetImportMethod> import_method,
const std::optional<ImportInstantiateContext> instantiate_context,
ReportList *reports)
{
if (ID *local_id = asset.local_id()) {
return local_id;
}
std::string blend_path = asset.full_library_path();
if (blend_path.empty()) {
return nullptr;
}
const eAssetImportMethod method = [&]() {
const bool no_packing = U.experimental.no_data_block_packing;
if (import_method) {
return (no_packing && *import_method == ASSET_IMPORT_PACK) ? ASSET_IMPORT_APPEND_REUSE :
*import_method;
}
if (std::optional asset_method = asset.get_import_method()) {
return (no_packing && *asset_method == ASSET_IMPORT_PACK) ? ASSET_IMPORT_APPEND_REUSE :
*asset_method;
}
return ASSET_IMPORT_APPEND_REUSE;
}();
Scene *scene = instantiate_context ? instantiate_context->scene : nullptr;
ViewLayer *view_layer = instantiate_context ? instantiate_context->view_layer : nullptr;
View3D *view3d = instantiate_context ? instantiate_context->view3d : nullptr;
switch (method) {
case ASSET_IMPORT_LINK:
return WM_file_link_datablock(&bmain,
scene,
view_layer,
view3d,
blend_path.c_str(),
asset.get_id_type(),
asset.get_name().c_str(),
flags | (asset.get_use_relative_path() ? FILE_RELPATH : 0),
reports);
case ASSET_IMPORT_PACK:
return WM_file_link_datablock(&bmain,
scene,
view_layer,
view3d,
blend_path.c_str(),
asset.get_id_type(),
asset.get_name().c_str(),
flags | BLO_LIBLINK_PACK |
(asset.get_use_relative_path() ? FILE_RELPATH : 0),
reports);
case ASSET_IMPORT_APPEND:
return WM_file_append_datablock(&bmain,
scene,
view_layer,
view3d,
blend_path.c_str(),
asset.get_id_type(),
asset.get_name().c_str(),
flags | BLO_LIBLINK_APPEND_RECURSIVE |
BLO_LIBLINK_APPEND_ASSET_DATA_CLEAR |
(asset.get_use_relative_path() ? FILE_RELPATH : 0),
reports);
case ASSET_IMPORT_APPEND_REUSE:
return WM_file_append_datablock(&bmain,
scene,
view_layer,
view3d,
blend_path.c_str(),
asset.get_id_type(),
asset.get_name().c_str(),
flags | BLO_LIBLINK_APPEND_RECURSIVE |
BLO_LIBLINK_APPEND_ASSET_DATA_CLEAR |
BLO_LIBLINK_APPEND_LOCAL_ID_REUSE |
(asset.get_use_relative_path() ? FILE_RELPATH : 0),
reports);
}
BLI_assert_unreachable();
return nullptr;
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,254 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include "ED_asset_indexer.hh"
#include <memory>
#include <variant>
namespace blender {
struct AssetMetaData;
class StringRefNull;
namespace io::serialize {
class DictionaryValue;
class Value;
} // namespace io::serialize
} // namespace blender
namespace blender::ed::asset::index {
struct RemoteListingAssetEntry;
std::unique_ptr<io::serialize::Value> read_contents(StringRefNull filepath);
AssetMetaData *asset_metadata_from_dictionary(const io::serialize::DictionaryValue &entry);
/**
* Result of reading a remote listing.
*
* Can be in any of these three states:
*
* type=Success: has a 'success value' of type `T`, and a vector of warnings
* (strings; may be empty).
*
* type=Failure: has a 'failure message' (may be empty, but for good UX better
* to always use).
*
* type=Cancelled: has no extra info, because this was in response to a user
* cancelling an operation (and so this happening should be
* expected).
*/
template<typename T = std::monostate> class ReadingResult {
public:
enum class Type {
Success,
Failure,
Cancelled,
};
Type type;
std::string failure_reason;
std::optional<T> success_value;
/**
* Even when an operation was performed successfully, there could have been
* warnings. These are only intended to be used on success status; on failure,
* only `failure_reason` is expected to be set. On cancellation, no reason
* needs to be given (as it is in response to the user cancelling the
* operation).
*
* \see ReadingResult::append_warning()
*/
Vector<std::string> warnings;
/**
* Construct a valueless success result.
* Only enabled if T == std::monostate.
*/
template<typename U = T>
static ReadingResult Success()
requires(std::is_same_v<U, std::monostate>)
{
return ReadingResult(Type::Success);
}
/**
* Construct a valued success result.
* Only enabled if T != std::monostate.
*/
template<typename U = T>
static ReadingResult Success(T value)
requires(!std::is_same_v<U, std::monostate>)
{
ReadingResult result(Type::Success);
result.success_value = std::move(value);
return result;
}
/**
* Construct a failure result.
* The ReadingResult copies the failure reason, so the StringRef can refer to temporary data.
*
* NOTE: Don't forget to wrap the string in N_(...) for translation tagging.
*/
static ReadingResult Failure(const StringRef failure_reason)
{
ReadingResult result(Type::Failure);
result.failure_reason = failure_reason;
return result;
}
/**
* Construct a cancelled result.
*
* Callback functions passed to `index::read_remote_listing()` can return
* `false` to indicate the loading should be cancelled.
*/
static ReadingResult Cancelled()
{
return ReadingResult(Type::Cancelled);
}
/**
* Construct a ReadingResult with the given type.
*
* NOTE: Do not use this function, use one of the above functions instead. It's public only
* because it's needed in internal code of this class, but across differently-templated versions
* of this class (which C++ considers to be unrelated, and thus cannot access each other's
* private members).
*/
explicit ReadingResult(Type type) : type(type) {}
/** Return whether this result indicates a success. */
bool is_success() const
{
return this->type == Type::Success;
}
/** Return whether this result indicates a failure. */
bool is_failure() const
{
return this->type == Type::Failure;
}
/** Return whether this result indicates cancellation. */
bool is_cancelled() const
{
return this->type == Type::Cancelled;
}
bool has_warnings() const
{
return !this->warnings.is_empty();
}
/**
* Move the warnings from another result into this one.
*/
template<typename U> void move_warnings_from(ReadingResult<U> &other)
{
BLI_assert_msg(is_success(), "Attempted to move warnings into a non-success ReadingResult");
for (std::string &warning : other.warnings) {
this->warnings.append(std::move(warning));
}
}
/**
* Return this ReadingResult, but without its success value.
*
* The result type, failure message, and warnings are copied.
*/
ReadingResult<> without_success_value() const
{
ReadingResult<> without_value(static_cast<ReadingResult<>::Type>(this->type));
without_value.success_value.reset();
without_value.failure_reason = this->failure_reason;
without_value.warnings.extend(this->warnings);
return without_value;
}
/**
* Get a reference to the result's success value, similar to `std::optional<T>`.
* Only valid if this result is successful and there is an actual success value.
*/
template<typename U = T>
T &operator*()
requires(!std::is_same_v<U, std::monostate>)
{
BLI_assert_msg(is_success() || !success_value.has_value(),
"Attempted to access value of non-success ReadingResult");
return *success_value;
}
/**
* Get a reference to the result's success value, similar to `std::optional<T>`.
* Only valid if this result is successful and there is an actual success value.
*/
template<typename U = T>
const T &operator*() const
requires(!std::is_same_v<U, std::monostate>)
{
BLI_assert_msg(is_success() || !success_value.has_value(),
"Attempted to access value of non-success ReadingResult");
return *success_value;
}
/**
* Get a pointer to the result's success value, similar to `std::optional<T>`.
* Only valid if this result is successful and there is an actual success value.
*/
template<typename U = T>
T *operator->()
requires(!std::is_same_v<U, std::monostate>)
{
T &success_value = **this;
return &success_value;
}
/**
* Get a pointer to the result's success value, similar to `std::optional<T>`.
* Only valid if this result is successful and there is an actual success value.
*/
template<typename U = T>
const T *operator->() const
requires(!std::is_same_v<U, std::monostate>)
{
const T &success_value = **this;
return &success_value;
}
/**
* Conversion constructor from any other ReadingResult.
*/
template<typename U> ReadingResult(const ReadingResult<U> &other)
{
this->type = static_cast<ReadingResult<T>::Type>(other.type);
if (this->type == Type::Success) {
// Only allow if U is std::monostate.
static_assert(std::is_same<U, std::monostate>::value,
"Cannot convert a valued success to another type");
success_value.reset();
}
else {
// Failure or Cancelled can convert freely.
failure_reason = other.failure_reason;
}
}
};
std::optional<bool> file_older_than_timestamp(const char *filepath, Timestamp timestamp);
/**
* Reading of API schema version 1. See #read_remote_listing() on \a process_fn.
* \param listing_root_dirpath: Absolute path to the remote listing root directory.
*/
ReadingResult<> read_remote_listing_v1(
StringRefNull listing_root_dirpath,
RemoteListingEntryProcessFn process_fn,
RemoteListingWaitForPagesFn wait_fn = nullptr,
const std::optional<Timestamp> ignore_before_timestamp = std::nullopt);
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,818 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include <ctime>
#include <fstream>
#include <iomanip>
#include <optional>
#include "ED_asset_indexer.hh"
#include "asset_index.hh"
#include "DNA_ID.h"
#include "DNA_asset_types.h"
#include "BLI_fileops.h"
#include "BLI_hash.hh"
#include "BLI_linklist.h"
#include "BLI_listbase.h"
#include "BLI_path_utils.hh"
#include "BLI_serialize.hh"
#include "BLI_set.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "BLI_string_utf8.h"
#include "AS_asset_catalog.hh"
#include "BKE_appdir.hh"
#include "BKE_asset.hh"
#include "BKE_idprop.hh"
#include "BKE_preferences.h"
/* For converting enum values to/from identifiers. */
#include "RNA_access.hh"
#include "RNA_enum_types.hh"
#include "CLG_log.h"
#include <sstream>
namespace blender {
static CLG_LogRef LOG = {"asset.index"};
namespace ed::asset::index {
using namespace blender::asset_system;
using namespace blender::io::serialize;
using namespace blender::bke::idprop;
/**
* \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>",
* "copyright": "<copyright>",
* "license": "<license>",
* "tags": ["<tag>"],
* "preferred_import_method": eAssetImportMethod,
* "properties": [..]
* }]
* }
* \endcode
*
* NOTE: entries, author, description, copyright, license, tags and properties are optional
* attributes. If preferred_import_method is set, the #ASSETDATA_USE_OWN_IMPORT_METHOD flag will be
* set on the asset metadata. Otherwise, it will not be set.
*
* 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_COPYRIGHT("copyright");
constexpr StringRef ATTRIBUTE_ENTRIES_LICENSE("license");
constexpr StringRef ATTRIBUTE_ENTRIES_TAGS("tags");
constexpr StringRef ATTRIBUTE_ENTRIES_PROPERTIES("properties");
constexpr StringRef ATTRIBUTE_ENTRIES_PREFERRED_IMPORT_METHOD("preferred_import_method");
/** 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(this->get_file_path());
}
size_t get_file_size() const
{
return BLI_file_size(this->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_path_split_file_part(this->get_file_path(), filename, sizeof(filename));
return std::string(filename);
}
const char *get_file_path() const override
{
return file_path_.c_str();
}
};
/**
* \brief add id + name to the attributes.
*
* NOTE: id and name are encoded like #ID.name
*/
static void add_id_name(DictionaryValue &result, const short idcode, const StringRefNull name)
{
char idcode_prefix[2];
/* Similar to `BKE_libblock_alloc`. */
*(reinterpret_cast<short *>(idcode_prefix)) = idcode;
std::string name_with_idcode = std::string(idcode_prefix, sizeof(idcode_prefix)) + name;
result.append_str(ATTRIBUTE_ENTRIES_NAME, name_with_idcode);
}
static void init_value_from_file_indexer_entry(DictionaryValue &result,
const FileIndexerEntry *indexer_entry)
{
const BLODataBlockInfo &datablock_info = indexer_entry->datablock_info;
add_id_name(result, indexer_entry->idcode, datablock_info.name);
const AssetMetaData &asset_data = *datablock_info.asset_data;
result.append_str(ATTRIBUTE_ENTRIES_CATALOG_ID, CatalogID(asset_data.catalog_id).str());
result.append_str(ATTRIBUTE_ENTRIES_CATALOG_NAME, asset_data.catalog_simple_name);
if (const char *description = asset_data.description) {
result.append_str(ATTRIBUTE_ENTRIES_DESCRIPTION, description);
}
if (const char *author = asset_data.author) {
result.append_str(ATTRIBUTE_ENTRIES_AUTHOR, author);
}
if (const char *copyright = asset_data.copyright) {
result.append_str(ATTRIBUTE_ENTRIES_COPYRIGHT, copyright);
}
if (const char *license = asset_data.license) {
result.append_str(ATTRIBUTE_ENTRIES_LICENSE, license);
}
if (!asset_data.tags.is_empty()) {
ArrayValue &tags = *result.append_array(ATTRIBUTE_ENTRIES_TAGS);
for (AssetTag &tag : asset_data.tags) {
tags.append_str(tag.name);
}
}
if (asset_data.flag & ASSETDATA_USE_OWN_IMPORT_METHOD) {
const char *identifier = nullptr;
RNA_enum_identifier(
rna_enum_asset_import_method_items, asset_data.preferred_import_method, &identifier);
if (identifier) {
result.append_str(ATTRIBUTE_ENTRIES_PREFERRED_IMPORT_METHOD, identifier);
}
}
if (const IDProperty *properties = asset_data.properties) {
if (std::unique_ptr<Value> value = convert_to_serialize_values(properties)) {
result.append(ATTRIBUTE_ENTRIES_PROPERTIES, std::move(value));
}
}
}
static void init_value_from_file_indexer_entries(DictionaryValue &result,
const FileIndexerEntries &indexer_entries)
{
auto entries = std::make_shared<ArrayValue>();
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, work-spaces), when browsing using the asset browser.
*/
if (indexer_entry->datablock_info.asset_data == nullptr) {
continue;
}
init_value_from_file_indexer_entry(*entries->append_dict(), indexer_entry);
}
/* 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 (entries->elements().is_empty()) {
return;
}
result.append(ATTRIBUTE_ENTRIES, entries);
}
AssetMetaData *asset_metadata_from_dictionary(const DictionaryValue &entry)
{
AssetMetaData *asset_data = BKE_asset_metadata_create();
if (const std::optional<StringRef> value = entry.lookup_str(ATTRIBUTE_ENTRIES_DESCRIPTION)) {
asset_data->description = BLI_strdupn(value->data(), value->size());
}
if (const std::optional<StringRef> value = entry.lookup_str(ATTRIBUTE_ENTRIES_AUTHOR)) {
asset_data->author = BLI_strdupn(value->data(), value->size());
}
if (const std::optional<StringRef> value = entry.lookup_str(ATTRIBUTE_ENTRIES_COPYRIGHT)) {
asset_data->copyright = BLI_strdupn(value->data(), value->size());
}
if (const std::optional<StringRef> value = entry.lookup_str(ATTRIBUTE_ENTRIES_LICENSE)) {
asset_data->license = BLI_strdupn(value->data(), value->size());
}
if (const std::optional<StringRefNull> catalog_name = entry.lookup_str(
ATTRIBUTE_ENTRIES_CATALOG_NAME))
{
STRNCPY_UTF8(asset_data->catalog_simple_name, catalog_name->c_str());
}
if (const std::optional<StringRefNull> catalog_id = entry.lookup_str(
ATTRIBUTE_ENTRIES_CATALOG_ID))
{
asset_data->catalog_id = CatalogID(*catalog_id);
}
if (const ArrayValue *array_value = entry.lookup_array(ATTRIBUTE_ENTRIES_TAGS)) {
for (const std::shared_ptr<Value> &item : array_value->elements()) {
BKE_asset_metadata_tag_add(asset_data, item->as_string_value()->value().c_str());
}
}
if (const std::optional<StringRefNull> import_method_identifier = entry.lookup_str(
ATTRIBUTE_ENTRIES_PREFERRED_IMPORT_METHOD))
{
int preferred_import_method = 0;
if (RNA_enum_value_from_identifier(rna_enum_asset_import_method_items,
import_method_identifier->c_str(),
&preferred_import_method))
{
asset_data->preferred_import_method = eAssetImportMethod(preferred_import_method);
asset_data->flag |= ASSETDATA_USE_OWN_IMPORT_METHOD;
}
}
if (const std::shared_ptr<Value> *value = entry.lookup(ATTRIBUTE_ENTRIES_PROPERTIES)) {
IDProperty *properties = convert_from_serialize_value(**value);
/* The top level property must be a group, further asset metadata property lookups assume
* that. This is also the only way to support more than a single property. */
if (properties && (properties->next || properties->type != IDP_GROUP)) {
asset_data->properties = bke::idprop::create_group("AssetMetaData.properties").release();
for (IDProperty *property = properties; property != nullptr;) {
/* Save next before IDP_AddToGroup (via BLI_addtail) overwrites property->next. */
IDProperty *next = property->next;
IDP_AddToGroup(asset_data->properties, property);
property = next;
}
}
else {
asset_data->properties = properties;
}
}
return asset_data;
}
static void init_indexer_entry_from_value(FileIndexerEntry &indexer_entry,
const DictionaryValue &entry)
{
const StringRef idcode_name = *entry.lookup_str(ATTRIBUTE_ENTRIES_NAME);
indexer_entry.idcode = GS(idcode_name.data());
idcode_name.substr(2).copy_utf8_truncated(indexer_entry.datablock_info.name);
indexer_entry.datablock_info.asset_data = asset_metadata_from_dictionary(entry);
indexer_entry.datablock_info.free_asset_data = true;
}
static int init_indexer_entries_from_value(FileIndexerEntries &indexer_entries,
const DictionaryValue &value)
{
const ArrayValue *entries = value.lookup_array(ATTRIBUTE_ENTRIES);
BLI_assert(entries != nullptr);
if (entries == nullptr) {
return 0;
}
int num_entries_read = 0;
for (const std::shared_ptr<Value> &element : entries->elements()) {
FileIndexerEntry *entry = MEM_new<FileIndexerEntry>(__func__);
init_indexer_entry_from_value(*entry, *element->as_dictionary_value());
BLI_linklist_prepend(&indexer_entries.entries, entry);
num_entries_read += 1;
}
return num_entries_read;
}
/**
* \brief References the asset library directory.
*
* The #AssetLibraryIndex instance collects file indices that are existing before the actual
* reading/updating starts. This way, the reading/updating can tag pre-existing files as used when
* they are still needed. Remaining ones (indices that are not tagged as used) can be removed once
* reading finishes.
*/
struct AssetLibraryIndex {
struct PreexistingFileIndexInfo {
bool is_used = false;
};
/**
* File indices that are existing already before reading/updating performs changes. The key is
* the absolute path. The value can store information like if the index is known to be used.
*
* Note that when deleting a file index (#delete_index_file()), it's also removed from here,
* since it doesn't exist and isn't relevant to keep track of anymore.
*/
Map<std::string /*path*/, PreexistingFileIndexInfo> preexisting_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;
AssetLibraryIndex(const StringRef library_path) : library_path(library_path)
{
this->init_indices_base_path();
}
uint64_t hash() const
{
return get_default_hash(this->library_path);
}
StringRefNull get_library_file_path() const
{
return this->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() << SEP_STR;
BLI_path_append(index_path, sizeof(index_path), ss.str().c_str());
this->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 << this->indices_base_path;
ss << std::setfill('0') << std::setw(16) << std::hex << asset_file.hash() << "_"
<< asset_file.get_filename() << ".index.json";
return ss.str();
}
/**
* Check for pre-existing index files to be able to track what is still used and what can be
* removed. See #AssetLibraryIndex::preexisting_file_indices.
*/
void collect_preexisting_file_indices()
{
const char *index_path = this->indices_base_path.c_str();
if (!BLI_is_dir(index_path)) {
return;
}
direntry *dir_entries = nullptr;
const int dir_entries_num = BLI_filelist_dir_contents(index_path, &dir_entries);
for (int i = 0; i < dir_entries_num; i++) {
direntry *entry = &dir_entries[i];
if (BLI_str_endswith(entry->relname, ".index.json")) {
this->preexisting_file_indices.add_as(std::string(entry->path));
}
}
BLI_filelist_free(dir_entries, dir_entries_num);
}
void mark_as_used(const std::string &filename)
{
PreexistingFileIndexInfo *preexisting = this->preexisting_file_indices.lookup_ptr(filename);
if (preexisting) {
preexisting->is_used = true;
}
}
/**
* Removes the file index from disk and #preexisting_file_indices (invalidating its iterators, so
* don't call while iterating).
* \return true if deletion was successful.
*/
bool delete_file_index(const std::string &filename)
{
if (BLI_delete(filename.c_str(), false, false) == 0) {
this->preexisting_file_indices.remove(filename);
return true;
}
return false;
}
/**
* A bug was creating empty index files for a while (see D16665). Remove empty index files from
* this period, so they are regenerated.
*/
/* Implemented further below. */
int remove_broken_index_files();
int remove_unused_index_files()
{
int num_files_deleted = 0;
Set<StringRef> files_to_remove;
for (auto preexisting_index : this->preexisting_file_indices.items()) {
if (preexisting_index.value.is_used) {
continue;
}
const std::string &file_path = preexisting_index.key;
CLOG_DEBUG(&LOG, "Remove unused index file \"%s\".", file_path.c_str());
files_to_remove.add(preexisting_index.key);
}
for (StringRef file_to_remove : files_to_remove) {
if (delete_file_index(file_to_remove)) {
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;
/**
* `io::serialize::Value` representing the contents of an index file.
*
* Value is used over #DictionaryValue 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<DictionaryValue> root = std::make_unique<DictionaryValue>();
root->append_int(ATTRIBUTE_VERSION, CURRENT_VERSION);
init_value_from_file_indexer_entries(*root, indexer_entries);
this->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 DictionaryValue *root = this->contents->as_dictionary_value();
if (root == nullptr) {
return UNKNOWN_VERSION;
}
const std::optional<int64_t> version_value = root->lookup_int(ATTRIBUTE_VERSION);
return version_value.value_or(UNKNOWN_VERSION);
}
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 DictionaryValue *root = this->contents->as_dictionary_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, StringRef index_file_path)
: library_index(library_index), filename(index_file_path)
{
}
AssetIndexFile(AssetLibraryIndex &library_index, BlendFile &asset_filename)
: AssetIndexFile(library_index, library_index.index_file_path(asset_filename))
{
}
void mark_as_used()
{
this->library_index.mark_as_used(this->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(const BlendFile &asset_file) const
{
return BLI_file_older(this->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(this->filename);
BLI_SCOPED_DEFER([&]() { is.close(); });
std::unique_ptr<Value> read_data = formatter.deserialize(is);
if (!read_data) {
return nullptr;
}
return std::make_unique<AssetIndex>(read_data);
}
bool ensure_parent_path_exists() const
{
return BLI_file_ensure_parent_dir_exists(this->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\".", this->get_file_path());
return;
}
std::ofstream os;
os.open(this->filename, std::ios::out | std::ios::trunc);
formatter.serialize(os, *content.contents);
os.close();
}
};
/* TODO(Julian): remove this after a short while. Just necessary for people who've been using alpha
* builds from a certain period. */
int AssetLibraryIndex::remove_broken_index_files()
{
Set<StringRef> files_to_remove;
for (const std::string &index_path : this->preexisting_file_indices.keys()) {
AssetIndexFile index_file(*this, index_path);
/* Bug was causing empty index files, so non-empty ones can be skipped. */
if (index_file.constains_entries()) {
continue;
}
/* Use the file modification time stamp to attempt to remove empty index files from a
* certain period (when the bug was in there). Starting from a day before the bug was
* introduced until a day after the fix should be enough to mitigate possible local time
* zone issues. */
std::tm tm_from{};
tm_from.tm_year = 2022 - 1900; /* 2022 */
tm_from.tm_mon = 11 - 1; /* November */
tm_from.tm_mday = 8; /* Day before bug was introduced. */
std::tm tm_to{};
tm_from.tm_year = 2022 - 1900; /* 2022 */
tm_from.tm_mon = 12 - 1; /* December */
tm_from.tm_mday = 3; /* Day after fix. */
std::time_t timestamp_from = std::mktime(&tm_from);
std::time_t timestamp_to = std::mktime(&tm_to);
BLI_stat_t stat = {};
if (BLI_stat(index_file.get_file_path(), &stat) == -1) {
continue;
}
if (IN_RANGE(stat.st_mtime, timestamp_from, timestamp_to)) {
CLOG_DEBUG(&LOG, "Remove potentially broken index file \"%s\".", index_path.c_str());
files_to_remove.add(index_path);
}
}
int num_files_deleted = 0;
for (StringRef filepath : files_to_remove) {
if (delete_file_index(filepath)) {
num_files_deleted++;
}
}
return num_files_deleted;
}
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_DEBUG(
&LOG,
"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_DEBUG(&LOG,
"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) {
CLOG_DEBUG(&LOG, "Asset file index is ignored; failed to read contents.");
return FILE_INDEXER_NEEDS_UPDATE;
}
if (!contents->is_latest_version()) {
CLOG_DEBUG(&LOG,
"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;
}
if (entries) {
const int read_entries_len = contents->extract_into(*entries);
CLOG_INFO(&LOG, "Read %d entries 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,
"Update 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_maxncpy)
{
AssetLibraryIndex *library_index = MEM_new<AssetLibraryIndex>(
__func__, StringRef(root_directory, BLI_strnlen(root_directory, root_directory_maxncpy)));
library_index->collect_preexisting_file_indices();
library_index->remove_broken_index_files();
return library_index;
}
static void free_user_data(void *user_data)
{
MEM_delete(static_cast<AssetLibraryIndex *>(user_data));
}
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, "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;
}
const FileIndexerType file_indexer_asset = asset_indexer();
} // namespace ed::asset::index
} // namespace blender

View File

@@ -0,0 +1,85 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "AS_remote_library.hh"
#include "CLG_log.h"
#include "asset_indexer_remote_file_status.hh"
static CLG_LogRef LOG = {"asset.remote_listing"};
using namespace blender::asset_system;
namespace blender::ed::asset::index {
/**
* Filename prefix for the Disk File Hash Service used by FileStatusChecker.
*
* The Disk File Hash Service itself will complete the filename depending on the back-end used. At
* the moment of writing that's SQLite, which will append `_v{schema version}.sqlite`.
*
* NOTE: if this changes, also update the RemoteAssetListingLocator class in listing_downloader.py.
*/
constexpr const char *hash_service_filename_prefix = "_file_hashes";
FileStatusChecker::FileStatusChecker(const StringRefNull library_root_path)
: library_root_path_(library_root_path)
{
char dfhs_path[PATH_MAX];
BLI_path_join(
dfhs_path, sizeof(dfhs_path), library_root_path.c_str(), hash_service_filename_prefix);
this->dfhs_ = disk_file_hash_service_get(dfhs_path);
}
RemoteAssetFileStatus FileStatusChecker::remote_file_status(RemoteListingFileEntry &file_to_check)
{
const StringRefNull relative_file_path = file_to_check.local_path;
/* Check against our own cache to see if we checked this file before. */
if (file_to_check.file_status.has_value()) {
return *file_to_check.file_status;
}
/* Construct the absolute path, so we can check its hash on disk. */
char file_abspath[PATH_MAX];
BLI_path_join(
file_abspath, sizeof(file_abspath), library_root_path_.c_str(), relative_file_path.c_str());
if (!BLI_exists(file_abspath)) {
return this->remember(file_to_check, RemoteAssetFileStatus::NOT_ON_DISK);
}
/* Split METHOD:HASH into two StringRefs. */
const StringRefNull hash_with_method = file_to_check.download_url.hash;
const int64_t colon_index = hash_with_method.find_first_of(':');
if (colon_index == StringRef::not_found) {
CLOG_WARN(&LOG, "Asset file hash not in METHOD:HASH format: %s", hash_with_method.c_str());
return this->remember(file_to_check, RemoteAssetFileStatus::NO_MATCH);
}
std::string hash_algorithm = hash_with_method.substr(0, colon_index);
const StringRef hexhash = hash_with_method.substr(colon_index + 1);
BLI_str_tolower_ascii(hash_algorithm.data(), hash_algorithm.length());
/* Check with the Disk File Hash Service. */
const bool is_match = dfhs_->file_matches(
file_abspath, hash_algorithm, hexhash, file_to_check.size_in_bytes);
return this->remember(file_to_check,
is_match ? RemoteAssetFileStatus::MATCH : RemoteAssetFileStatus::NO_MATCH);
}
asset_system::RemoteAssetFileStatus FileStatusChecker::remember(
RemoteListingFileEntry &file_to_check, const asset_system::RemoteAssetFileStatus status)
{
file_to_check.file_status = status;
return status;
}
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include <string>
#include "BLI_string_ref.hh"
#include "AS_disk_file_hash_service.hh"
#include "AS_remote_library.hh"
#include "ED_asset_indexer.hh"
namespace blender {
struct bContext;
}
namespace blender::ed::asset::index {
/**
* Check files on disk against their expected hash / size in bytes.
*
* Instances of this class manage their own Disk File Hash Service for efficiently computing file
* hashes.
*/
class FileStatusChecker {
private:
/** Absolute path to the cache directory for the remote asset library. */
std::string library_root_path_;
std::unique_ptr<asset_system::DiskFileHashService> dfhs_;
public:
explicit FileStatusChecker(StringRefNull library_root_path);
~FileStatusChecker() = default;
/**
* Determine the status of the file on disk.
*
* Once a file has been checked, its RemoteListingFileEntry is updated to reflect its status.
* Subsequent checks just return that status, so this is efficient to call for each asset that
* uses the file.
*/
asset_system::RemoteAssetFileStatus remote_file_status(RemoteListingFileEntry &file_to_check);
private:
asset_system::RemoteAssetFileStatus remember(RemoteListingFileEntry &file_to_check,
asset_system::RemoteAssetFileStatus status);
};
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,308 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include <chrono>
#include <filesystem>
#include <fmt/format.h>
#include <fstream>
#include <optional>
#include <string>
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
#include "BLI_serialize.hh"
#include "BKE_report.hh"
#include "BLT_translation.hh"
#include "CLG_log.h"
#include "ED_asset_indexer.hh"
#include "asset_index.hh"
#include "asset_indexer_remote_listing.hh"
static CLG_LogRef LOG = {"asset.remote_listing"};
namespace blender::ed::asset::index {
using namespace blender::io::serialize;
/* -------------------------------------------------------------------- */
/** \name #RemoteListingAssetEntry type
* \{ */
RemoteListingAssetEntry::RemoteListingAssetEntry(RemoteListingAssetEntry &&other)
{
this->datablock_info = other.datablock_info;
other.datablock_info = {};
this->idcode = other.idcode;
this->online_info = std::move(other.online_info);
}
RemoteListingAssetEntry &RemoteListingAssetEntry::operator=(RemoteListingAssetEntry &&other)
{
if (this == &other) {
return *this;
}
std::destroy_at(this);
new (this) RemoteListingAssetEntry(std::move(other));
return *this;
}
RemoteListingAssetEntry::~RemoteListingAssetEntry()
{
BLO_datablock_info_free(&datablock_info);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name General functions for reading.
* \{ */
std::unique_ptr<Value> read_contents(const StringRefNull filepath)
{
JsonFormatter formatter;
std::ifstream is;
is.open(filepath.c_str());
BLI_SCOPED_DEFER([&]() { is.close(); });
return formatter.deserialize(is);
}
std::optional<asset_system::URLWithHash> parse_url_with_hash_dict(
const DictionaryValue *url_with_hash_dict)
{
if (!url_with_hash_dict) {
return {};
}
const std::optional<StringRefNull> url = url_with_hash_dict->lookup_str("url");
const std::optional<StringRefNull> hash = url_with_hash_dict->lookup_str("hash");
/* A URL without hash is not up to spec, but we can work with it. But without
* a URL it's hopeless. */
if (!url) {
return {};
}
return asset_system::URLWithHash{*url, hash.value_or("")};
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Meta file
*
* Containing info like the author and contact information (all of which is ignored here), as well
* as the API version.
* \{ */
struct AssetLibraryMeta {
/** Map of API version string ("v1", "v2", ...) to path relative to root directory. */
Map<std::string, asset_system::URLWithHash> api_versions;
static ReadingResult<AssetLibraryMeta> read(const StringRefNull root_dirpath,
std::optional<Timestamp> ignore_before_timestamp);
};
/**
* Note that this uses `std::filesystem::file_time_type` to get
* \return true if the file is older than the timestamp, or no value if the file was not found.
*/
std::optional<bool> file_older_than_timestamp(const char *filepath, Timestamp timestamp)
{
std::error_code error;
Timestamp file_timestamp = std::filesystem::last_write_time(filepath, error);
/** TODO better report error message? */
if (error) {
printf("Can't find file at path %s: %s\n", filepath, error.message().c_str());
return {};
}
return file_timestamp < timestamp;
}
/**
* \return the supported API versions read from the `_asset-library-meta.json` file.
*/
ReadingResult<AssetLibraryMeta> AssetLibraryMeta::read(
const StringRefNull root_dirpath, const std::optional<Timestamp> ignore_before_timestamp)
{
char filepath[FILE_MAX];
BLI_path_join(filepath,
sizeof(filepath),
root_dirpath.c_str(),
asset_system::REMOTE_LIBRARY_TOP_META_FILE_NAME.c_str());
if (!BLI_exists(filepath)) {
return ReadingResult<AssetLibraryMeta>::Failure(
fmt::format(N_("file does not exist: {:s}"), filepath));
}
if (ignore_before_timestamp) {
std::optional<bool> is_older = file_older_than_timestamp(filepath, *ignore_before_timestamp);
if (!is_older) {
return ReadingResult<AssetLibraryMeta>::Failure(
fmt::format(N_("file does not exist: {:s}"), filepath));
}
if (*is_older) {
return ReadingResult<AssetLibraryMeta>::Failure(
fmt::format(N_("file is too old: {:s}"), filepath));
}
}
const std::unique_ptr<Value> contents = read_contents(filepath);
if (!contents) {
return ReadingResult<AssetLibraryMeta>::Failure(
fmt::format(N_("file does not contain JSON: {:s}"), filepath));
}
const DictionaryValue *root = contents->as_dictionary_value();
if (!root) {
return ReadingResult<AssetLibraryMeta>::Failure(
fmt::format(N_("file is not a JSON dictionary: {:s}"), filepath));
}
const DictionaryValue *entries = root->lookup_dict("api_versions");
BLI_assert(entries != nullptr);
if (entries == nullptr) {
return ReadingResult<AssetLibraryMeta>::Failure(
fmt::format(N_("no API versions defined: {:s}"), filepath));
}
AssetLibraryMeta library_meta;
for (const DictionaryValue::Item &version : entries->elements()) {
/* Relative path to the listing meta-file (e.g. `_v1/asset-index.json`). */
const DictionaryValue *index_path_info = version.second->as_dictionary_value();
if (!index_path_info) {
CLOG_WARN(&LOG,
"Error reading asset listing API version '%s' in %s - ignoring",
version.first.c_str(),
filepath);
continue;
}
std::optional<asset_system::URLWithHash> url_with_hash = parse_url_with_hash_dict(
index_path_info);
if (!url_with_hash) {
CLOG_WARN(&LOG,
"Error reading asset listing API version '%s' in %s, no URL+hash found - ignoring",
version.first.c_str(),
filepath);
continue;
}
library_meta.api_versions.add(version.first, std::move(*url_with_hash));
}
return ReadingResult<AssetLibraryMeta>::Success(std::move(library_meta));
}
/** \} */
struct ApiVersionInfo {
uint version_nr;
/** Relative path to the listing meta-file (e.g. `_v1/asset-index.json`). */
std::string listing_relpath;
/** Hash of the file, like `SHA256:112233`. */
std::string listing_hash;
};
static ReadingResult<ApiVersionInfo> choose_api_version(const AssetLibraryMeta &library_meta)
{
/* API versions this version of Blender can handle, in descending order (most preferred to least
* preferred order). */
const Vector<std::pair<uint, StringRefNull>> readable_versions = {
{1, "v1"},
};
for (const auto &[version_nr, version_str] : readable_versions) {
if (const asset_system::URLWithHash *url_with_hash = library_meta.api_versions.lookup_ptr(
version_str))
{
ApiVersionInfo version_info{version_nr, url_with_hash->url, url_with_hash->hash};
return ReadingResult<ApiVersionInfo>::Success(std::move(version_info));
}
}
return ReadingResult<ApiVersionInfo>::Failure(
N_("remote does not offer an API version supported by this version of Blender"));
}
bool read_remote_listing(const StringRefNull root_dirpath,
const StringRefNull asset_library_name,
ReportList &reports,
const RemoteListingEntryProcessFn process_fn,
const RemoteListingWaitForPagesFn wait_fn,
const std::optional<Timestamp> ignore_before_timestamp)
{
/* This actually does the work, and returns a ReadingResult. It's implemented as a lambda
* function, to be able to use early returns on error. */
auto get_result = [&]() {
const ReadingResult<AssetLibraryMeta> meta = AssetLibraryMeta::read(root_dirpath,
ignore_before_timestamp);
if (!meta.is_success()) {
return meta.without_success_value();
}
const ReadingResult<ApiVersionInfo> api_version_info = choose_api_version(*meta);
if (!api_version_info.is_success()) {
return api_version_info.without_success_value();
}
/* Path to the listing meta-file is version-dependent. */
switch (api_version_info->version_nr) {
case 1: {
return read_remote_listing_v1(root_dirpath, process_fn, wait_fn, ignore_before_timestamp);
}
default:
/* choose_api_version() should not have chosen this version. */
BLI_assert_unreachable();
return ReadingResult<>::Failure(N_("internal error, please report a bug"));
}
};
const ReadingResult<> result = get_result();
/* Get these messages up-stream. The last call to BKE_report(f) will be the one shown in the
* status bar. The rest are just printed to the terminal and gathered at the Info editor. */
if (result.is_failure()) {
BKE_reportf(&reports,
RPT_ERROR,
"Asset Library '%s': %s",
asset_library_name.c_str(),
RPT_(result.failure_reason.c_str()));
BKE_reportf(&reports,
RPT_ERROR,
"Could not read asset listing '%s', see Info Editor for details",
asset_library_name.c_str());
return false;
}
if (result.is_cancelled()) {
return false;
}
if (result.has_warnings()) {
for (const std::string &warning : result.warnings) {
BKE_reportf(&reports,
RPT_WARNING,
"Asset Library '%s': %s",
asset_library_name.c_str(),
RPT_(warning.c_str()));
}
BKE_reportf(&reports,
RPT_WARNING,
"Could not read asset listing for '%s', see Info Editor for details",
asset_library_name.c_str());
}
return true;
}
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,35 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include <optional>
/* Forward references. */
namespace blender::asset_system {
struct URLWithHash;
}
namespace blender::io::serialize {
class DictionaryValue;
}
namespace blender::ed::asset::index {
/**
* Parse a dictionary `{url: "https://some.url/", hash: "sha256:abcd"}` into a
* URLWithHash object.
*
* If `url_with_hash_dict` is `nullptr`, or has no "url" field, `std::nullopt`
* is returned.
*
* If the "hash" field is missing, it will simply be set to an empty string on
* the returned URLWithHash.
*/
std::optional<asset_system::URLWithHash> parse_url_with_hash_dict(
const io::serialize::DictionaryValue *url_with_hash_dict);
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,533 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include <fmt/format.h>
#include "BLI_assert.h"
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
#include "BLI_serialize.hh"
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BKE_asset.hh"
#include "BKE_blender_version.h"
#include "BKE_idtype.hh"
#include "BLT_translation.hh"
#include "CLG_log.h"
#include "ED_asset_indexer.hh"
#include "AS_remote_library.hh"
#include "asset_index.hh"
#include "asset_indexer_remote_file_status.hh"
#include "asset_indexer_remote_listing.hh"
static CLG_LogRef LOG = {"asset.remote_listing"};
namespace blender::ed::asset::index {
using namespace blender::io::serialize;
/* -------------------------------------------------------------------- */
/** \name Remote asset listing page
* \{ */
struct AssetLibraryListingPageV1 {
static ReadingResult<> read_asset_entries(const StringRefNull listing_root_dirpath,
const StringRefNull filepath,
RemoteListingEntryProcessFn process_fn);
};
/**
* Parse the string as "major.minor" version, returning (major*100 + minor).
* This can then be compared to BLENDER_VERSION from BKE_blender_version.h.
*/
static std::optional<int> blender_version_from_string(const blender::StringRef str)
{
const int64_t dot = str.find('.');
if (dot == blender::StringRef::not_found) {
return {};
}
int major, minor;
const blender::StringRef major_str = str.substr(0, dot);
const blender::StringRef minor_str = str.substr(dot + 1);
if (std::from_chars(major_str.begin(), major_str.end(), major).ec != std::errc() ||
std::from_chars(minor_str.begin(), minor_str.end(), minor).ec != std::errc())
{
return {};
}
if (major < 0 || minor < 0 || minor >= 100) {
return {};
}
return major * 100 + minor;
}
static ReadingResult<bool> blender_version_matches(const DictionaryValue &asset_dictionary)
{
const DictionaryValue *bl_versions_dict = asset_dictionary.lookup_dict("bl_versions");
if (!bl_versions_dict) {
return ReadingResult<bool>::Failure(
N_("could not read asset Blender versions, 'bl_versions' field not set"));
}
/* Check the 'min' field. */
const std::optional<StringRef> min_opt = bl_versions_dict->lookup_str("min");
if (!min_opt) {
return ReadingResult<bool>::Failure(
N_("could not read asset Blender versions, 'bl_versions.min' field not set"));
}
const std::optional<int> bl_version_min = blender_version_from_string(*min_opt);
if (!bl_version_min) {
return ReadingResult<bool>::Failure(
N_("could not read asset Blender versions, 'bl_versions.min' field not in X.Y notation"));
}
if (BLENDER_VERSION < *bl_version_min) {
/* This Blender version is older than what the asset needs, so skip it. */
return ReadingResult<bool>::Success(false);
}
/* Check the 'until' field. */
const std::optional<StringRef> until_opt = bl_versions_dict->lookup_str("until");
if (!until_opt) {
/* Fine to be missing, this field is optional. If it is not there, the asset has no maximum
* version. */
return ReadingResult<bool>::Success(true);
}
const std::optional<int> bl_version_until = blender_version_from_string(*until_opt);
if (!bl_version_until) {
return ReadingResult<bool>::Failure(
N_("could not read asset Blender versions, 'bl_versions.min' field not in X.Y notation"));
}
return ReadingResult<bool>::Success(BLENDER_VERSION < *bl_version_until);
}
static ReadingResult<RemoteListingAssetEntry> listing_entry_from_asset_dictionary(
const DictionaryValue &dictionary,
const Map<std::string, RemoteListingFileEntry> &file_path_to_entry_map)
{
RemoteListingAssetEntry listing_entry{};
/* Check the min/until Blender versions first. If the current Blender doesn't match, the entire
* asset can be ignored. */
ReadingResult<bool> version_check = blender_version_matches(dictionary);
if (!version_check.is_success()) {
return ReadingResult<RemoteListingAssetEntry>::Failure(
std::move(version_check.failure_reason));
}
if (!*version_check) {
/* Return an empty entry, to indicate to the caller a successfully parsed entry that didn't
* yield an asset. */
return ReadingResult<RemoteListingAssetEntry>::Success(RemoteListingAssetEntry{});
}
/* 'id': name of the asset. Required string. */
const std::optional<StringRefNull> asset_name_opt = dictionary.lookup_str("name");
if (!asset_name_opt) {
return ReadingResult<RemoteListingAssetEntry>::Failure(
N_("could not read asset name, 'name' field not set"));
}
const StringRefNull asset_name = *asset_name_opt;
asset_name.copy_utf8_truncated(listing_entry.datablock_info.name);
/* 'type': data-block type, must match the #IDTypeInfo.name of the given type. required string.
*/
if (const std::optional<StringRefNull> idtype_name = dictionary.lookup_str("id_type")) {
const char *normalized_name = BKE_idtype_name_normalize(idtype_name->c_str());
if (!normalized_name) {
/* This could actually be a new asset type that's not supported by this Blender. Just
* silently ignore it and continue. */
CLOG_DEBUG(&LOG,
N_("could not read type of asset '%s': 'id_type' field is not a valid type (%s)"),
asset_name.c_str(),
idtype_name->c_str());
return ReadingResult<RemoteListingAssetEntry>::Success(RemoteListingAssetEntry{});
}
listing_entry.idcode = BKE_idtype_idcode_from_name(normalized_name);
}
else {
return ReadingResult<RemoteListingAssetEntry>::Failure(
fmt::format(N_("could not read type of asset '{:s}', 'type' field not set"), asset_name));
}
/* 'files': required list of strings. */
if (const ArrayValue *file_paths = dictionary.lookup_array("files")) {
if (file_paths->elements().is_empty()) {
return ReadingResult<RemoteListingAssetEntry>::Failure(
fmt::format(N_("asset '{:s}' has no files"), asset_name));
}
for (const std::shared_ptr<Value> &file_path_element : file_paths->elements()) {
asset_system::OnlineAssetFile file = {};
const io::serialize::StringValue *file_path_string = file_path_element->as_string_value();
if (!file_path_string) {
return ReadingResult<RemoteListingAssetEntry>::Failure(fmt::format(
N_("asset '{:s}' has a non-string entry in its 'files' list"), asset_name));
}
file.path = file_path_string->value();
if (file.path.empty()) {
/* TODO: use CLOG to have _some_ logging of this dubious empty file
* entry. But keep going, maybe there's another, non-empty entry. */
continue;
}
/* Look up the file URL and hash from the <files> section of the JSON. */
if (const RemoteListingFileEntry *file_entry = file_path_to_entry_map.lookup_ptr(file.path))
{
file.url = file_entry->download_url;
file.size_in_bytes = file_entry->size_in_bytes;
}
else {
return ReadingResult<RemoteListingAssetEntry>::Failure(
fmt::format(N_("asset '{:s}' references unknown file '{:s}'"), asset_name, file.path));
}
listing_entry.online_info.files.append(file);
}
}
else {
return ReadingResult<RemoteListingAssetEntry>::Failure(
fmt::format(N_("asset '{:s}' has no 'files' field"), asset_name));
}
/* 'thumbnail': URL and hash of the preview image. */
listing_entry.online_info.preview_url = ed::asset::index::parse_url_with_hash_dict(
dictionary.lookup_dict("thumbnail"));
/* 'metadata': optional dictionary. If all the metadata fields are empty, this can be left out of
* the listing. Default metadata will then be allocated, with all fields empty/0. */
const DictionaryValue *metadata_dict = dictionary.lookup_dict("meta");
listing_entry.datablock_info.asset_data = metadata_dict ?
asset_metadata_from_dictionary(*metadata_dict) :
BKE_asset_metadata_create();
listing_entry.datablock_info.free_asset_data = true;
return ReadingResult<RemoteListingAssetEntry>::Success(std::move(listing_entry));
}
static ReadingResult<RemoteListingFileEntry> listing_file_from_asset_dictionary(
const DictionaryValue &dictionary)
{
RemoteListingFileEntry file_entry{};
/* Path is mandatory. */
if (const std::optional<StringRefNull> path = dictionary.lookup_str("path")) {
file_entry.local_path = *path;
}
else {
return ReadingResult<RemoteListingFileEntry>::Failure(
N_("Error reading asset listing file entry, skipping. Reason: found a file without 'path' "
"field"));
}
/* Hash is mandatory. */
if (const std::optional<StringRefNull> hash = dictionary.lookup_str("hash")) {
file_entry.download_url.hash = *hash;
}
else {
return ReadingResult<RemoteListingFileEntry>::Failure(fmt::format(
N_("Error reading asset listing file entry, skipping. Reason: found a file ({:s}) without "
"'hash' field"),
file_entry.local_path.c_str()));
}
/* Size is mandatory. */
if (const std::optional<int64_t> size_in_bytes = dictionary.lookup_int("size_in_bytes")) {
file_entry.size_in_bytes = *size_in_bytes;
}
else {
return ReadingResult<RemoteListingFileEntry>::Failure(fmt::format(
N_("Error reading asset listing file entry, skipping. Reason: found a file ({:s}) without "
"'size_in_bytes' field"),
file_entry.local_path.c_str()));
}
/* URL is optional, and defaults to the local path. That's handled in Python
* (see `download_asset()` in `asset_downloader.py`) so here we can just use
* an empty string to indicate "no URL". */
file_entry.download_url.url = dictionary.lookup_str("url").value_or("");
return ReadingResult<RemoteListingFileEntry>::Success(std::move(file_entry));
}
static ReadingResult<> listing_entries_from_root(const StringRefNull listing_root_dirpath,
const DictionaryValue &value,
const RemoteListingEntryProcessFn process_fn)
{
const ArrayValue *assets = value.lookup_array("assets");
BLI_assert(assets != nullptr);
if (assets == nullptr) {
return ReadingResult<>::Failure(N_("no assets listed"));
}
/* Build a mapping from local file path to its file info. */
const ArrayValue *files = value.lookup_array("files");
BLI_assert(files != nullptr);
if (files == nullptr) {
/* The 'files' section is mandatory in the OpenAPI schema. */
return ReadingResult<>::Failure(
N_("error reading asset listing, page file has no files section"));
}
Vector<std::string> warnings;
Map<std::string, RemoteListingFileEntry> path_to_file_info;
for (const std::shared_ptr<Value> &file_element : files->elements()) {
ReadingResult<RemoteListingFileEntry> file_result = listing_file_from_asset_dictionary(
*file_element->as_dictionary_value());
if (file_result.is_failure()) {
warnings.append(std::move(file_result.failure_reason));
continue;
}
if (file_result.is_cancelled()) {
return ReadingResult<>::Cancelled();
}
BLI_assert(file_result.is_success());
RemoteListingFileEntry &file_entry = *file_result;
if (file_entry.local_path.empty()) {
continue;
}
std::string local_path = file_entry.local_path; /* Make a copy before std::moving. */
path_to_file_info.add_overwrite(local_path, std::move(file_entry));
}
/* Store whether asset files match their listing's hash or not. */
FileStatusChecker file_status_checker(listing_root_dirpath);
/* Convert the assets into RemoteListingAssetEntry objects. */
for (const std::shared_ptr<Value> &asset_element : assets->elements()) {
ReadingResult<RemoteListingAssetEntry> result = listing_entry_from_asset_dictionary(
*asset_element->as_dictionary_value(), path_to_file_info);
if (result.is_failure()) {
if (!result.failure_reason.empty()) {
warnings.append(std::move(result.failure_reason));
}
continue;
}
RemoteListingAssetEntry &entry = *result;
if (entry.is_empty()) {
continue;
}
/* Check the up-to-dateness of the asset's files. */
/* TODO: this has to change when Blender starts supporting multi-file assets. */
{
const StringRef asset_file = entry.online_info.asset_file();
RemoteListingFileEntry *file_entry = path_to_file_info.lookup_ptr(asset_file);
BLI_assert_msg(file_entry, "Assets without file info should have been filtered out by now");
entry.remote_file_status = file_status_checker.remote_file_status(*file_entry);
}
if (!process_fn(entry)) {
return ReadingResult<>::Cancelled();
}
}
ReadingResult<> overall_result = ReadingResult<>::Success();
overall_result.warnings.extend(std::move(warnings));
return overall_result;
}
ReadingResult<> AssetLibraryListingPageV1::read_asset_entries(
const StringRefNull listing_root_dirpath,
const StringRefNull filepath,
const RemoteListingEntryProcessFn process_fn)
{
if (!BLI_exists(filepath.c_str())) {
return ReadingResult<>::Failure(fmt::format(N_("file does not exist: {:s}"), filepath));
}
const std::unique_ptr<Value> contents = read_contents(filepath);
if (!contents) {
return ReadingResult<>::Failure(fmt::format(N_("file is empty: {:s}"), filepath));
}
const DictionaryValue *root = contents->as_dictionary_value();
if (!root) {
return ReadingResult<>::Failure(
fmt::format(N_("file is not a JSON dictionary: {:s}"), filepath));
}
return listing_entries_from_root(listing_root_dirpath, *root, process_fn);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Remote asset listing
*
* Sort of an index file listing the individual page files and meta information about the asset
* listing (such as the count of assets).
*
* \{ */
struct AssetLibraryListingV1 {
/** File paths to the individual asset listing files containing the assets, relative to \a
* root_dirpath. */
Vector<std::string> page_rel_paths;
static std::optional<AssetLibraryListingV1> read(const StringRefNull listing_filepath);
};
std::optional<AssetLibraryListingV1> AssetLibraryListingV1::read(
const StringRefNull listing_filepath)
{
if (!BLI_exists(listing_filepath.c_str())) {
/** TODO report error message? */
return {};
}
const std::unique_ptr<Value> contents = read_contents(listing_filepath);
if (!contents) {
/** TODO report error message? */
return {};
}
const DictionaryValue *root = contents->as_dictionary_value();
if (!root) {
/** TODO report error message? */
return {};
}
const ArrayValue *entries = root->lookup_array("pages");
BLI_assert(entries != nullptr);
if (entries == nullptr) {
return {};
}
AssetLibraryListingV1 listing;
int i = 0;
for (const std::shared_ptr<Value> &element : entries->elements()) {
const std::optional<asset_system::URLWithHash> page_info = parse_url_with_hash_dict(
element->as_dictionary_value());
if (!page_info) {
printf("Error reading asset listing page path at index %i in %s - ignoring\n",
i,
listing_filepath.c_str());
i++;
continue;
}
listing.page_rel_paths.append(std::move(page_info->url));
i++;
}
return listing;
}
/** \} */
ReadingResult<> read_remote_listing_v1(const StringRefNull listing_root_dirpath,
const RemoteListingEntryProcessFn process_fn,
const RemoteListingWaitForPagesFn wait_fn,
const std::optional<Timestamp> ignore_before_timestamp)
{
/* Version 1 asset indices are always stored in this path by RemoteAssetListingDownloader. */
constexpr const char *asset_index_relpath = "_v1/asset-index.processed.json";
char asset_index_abspath[FILE_MAX];
BLI_path_join(asset_index_abspath,
sizeof(asset_index_abspath),
listing_root_dirpath.c_str(),
asset_index_relpath);
if (ignore_before_timestamp) {
std::optional<bool> is_older = file_older_than_timestamp(asset_index_abspath,
*ignore_before_timestamp);
if (!is_older) {
return ReadingResult<>::Failure(
fmt::format(N_("Couldn't find index file {:s}"), asset_index_abspath));
}
/* TODO the .processed.json file doesn't get touched by the downloader to indicate it's up to
* date. Should this be done, or should we just note compare the timestamps for meta-files? The
* downloader notifies about them being in place already anyway. */
// if (*is_older) {
// CLOG_ERROR(&LOG, "Index file too old %s\n", asset_index_abspath);
// return {};
// }
}
const std::optional<AssetLibraryListingV1> listing = AssetLibraryListingV1::read(
asset_index_abspath);
if (!listing) {
return ReadingResult<>::Failure(
fmt::format(N_("Couldn't read V1 listing from {:s}"), asset_index_abspath));
}
Set<StringRef> done_pages;
char filepath[FILE_MAX];
// TODO should we have some timeout here too? Like timeout after 30 seconds without a new page?
Vector<std::string> warnings;
while (true) {
for (const std::string &page_path : listing->page_rel_paths) {
if (done_pages.contains(page_path)) {
continue;
}
BLI_path_join(filepath, sizeof(filepath), listing_root_dirpath.c_str(), page_path.c_str());
if (wait_fn) {
if (!BLI_exists(filepath)) {
continue;
}
if (ignore_before_timestamp &&
file_older_than_timestamp(filepath, *ignore_before_timestamp).value_or(true))
{
CLOG_DEBUG(&LOG, "Ignoring old listing file %s - waiting for a new version\n", filepath);
continue;
}
}
ReadingResult page_result = AssetLibraryListingPageV1::read_asset_entries(
listing_root_dirpath, filepath, process_fn);
done_pages.add(page_path);
if (page_result.is_cancelled()) {
return page_result;
}
if (page_result.is_failure()) {
printf("Couldn't read V1 listing from %s%c%s: %s\n",
listing_root_dirpath.c_str(),
SEP,
page_path.c_str(),
page_result.failure_reason.c_str());
return page_result;
}
BLI_assert(page_result.is_success());
/* Gather per-page warnings into the overall result. */
if (page_result.has_warnings()) {
warnings.extend(std::move(page_result.warnings));
}
}
BLI_assert(done_pages.size() <= listing->page_rel_paths.size());
if (done_pages.size() >= listing->page_rel_paths.size()) {
break;
}
if (!wait_fn) {
break;
}
if (!wait_fn()) {
return ReadingResult<>::Cancelled();
}
}
/* Return a success, with all the warnings. */
ReadingResult<> result = ReadingResult<>::Success();
result.warnings.extend(std::move(warnings));
return result;
}
} // namespace blender::ed::asset::index

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Utility to extend #AssetLibraryReference with C++ functionality (operators, hash function, etc).
*/
#pragma once
#include "BLI_hash.hh"
#include "DNA_asset_types.h"
namespace blender {
inline bool operator==(const AssetLibraryReference &a, const AssetLibraryReference &b)
{
return (a.type == b.type) &&
((a.type == ASSET_LIBRARY_CUSTOM) ? (a.custom_library_index == b.custom_library_index) :
true);
}
template<> struct DefaultHash<AssetLibraryReference> {
uint64_t operator()(const AssetLibraryReference &value) const
{
return get_default_hash(value.type, value.custom_library_index);
}
};
} // namespace blender

View File

@@ -0,0 +1,185 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Helpers to convert asset library references from and to enum values and RNA enums.
* In some cases it's simply not possible to reference an asset library with
* #AssetLibraryReferences. This API guarantees a safe translation to indices/enum values for as
* long as there is no change in the order of registered custom asset libraries.
*/
#include "BLI_listbase.h"
#include "BKE_preferences.h"
#include "DNA_userdef_types.h"
#include "UI_resources.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "ED_asset_library.hh"
namespace blender::ed::asset {
int library_reference_to_enum_value(const AssetLibraryReference *library)
{
/* Simple case: Predefined repository, just set the value. */
if (library->type < ASSET_LIBRARY_CUSTOM) {
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_index(
&U, library->custom_library_index);
if (user_library) {
return ASSET_LIBRARY_CUSTOM + library->custom_library_index;
}
return ASSET_LIBRARY_LOCAL;
}
static bool custom_library_is_valid(const bUserAssetLibrary *user_library)
{
if (user_library->flag & ASSET_LIBRARY_DISABLED) {
return false;
}
if (!user_library->name[0]) {
return false;
}
return BKE_preferences_asset_library_is_valid(
&U,
user_library,
/* Don't check if the path exists on disk. If an invalid library path is used, the Asset
* Browser can give a nice hint on what's wrong, so include such items in menus the user can
* choose from. */
/*check_directory_exists=*/false);
}
AssetLibraryReference library_reference_from_enum_value(int value)
{
AssetLibraryReference library;
/* Simple case: Predefined repository, just set the value. */
if (value < ASSET_LIBRARY_CUSTOM) {
library.type = eAssetLibraryType(value);
library.custom_library_index = -1;
BLI_assert(ELEM(value,
ASSET_LIBRARY_ALL,
ASSET_LIBRARY_LOCAL,
ASSET_LIBRARY_ESSENTIALS,
ASSET_LIBRARY_ONLINE_ESSENTIALS));
return library;
}
const bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_index(
&U, value - ASSET_LIBRARY_CUSTOM);
if (!user_library) {
library.type = ASSET_LIBRARY_ALL;
library.custom_library_index = -1;
}
else if (custom_library_is_valid(user_library)) {
library.custom_library_index = value - ASSET_LIBRARY_CUSTOM;
library.type = ASSET_LIBRARY_CUSTOM;
}
return library;
}
static void rna_enum_add_custom_libraries(EnumPropertyItem **item,
int *totitem,
const bool include_remote_libraries)
{
for (const auto [i, user_library] : U.asset_libraries.enumerate()) {
if (!include_remote_libraries && (user_library.flag & ASSET_LIBRARY_USE_REMOTE_URL)) {
continue;
}
if (!custom_library_is_valid(&user_library)) {
continue;
}
AssetLibraryReference library_reference;
library_reference.type = ASSET_LIBRARY_CUSTOM;
library_reference.custom_library_index = i;
const int enum_value = library_reference_to_enum_value(&library_reference);
EnumPropertyItem tmp = {
enum_value,
user_library.name,
ICON_NONE,
user_library.name,
/* Use library path or URL as description, it's a nice hint for users. */
(user_library.flag & ASSET_LIBRARY_USE_REMOTE_URL) ? user_library.remote_url :
user_library.dirpath};
RNA_enum_item_add(item, totitem, &tmp);
}
}
const EnumPropertyItem *library_reference_to_rna_enum_itemf(
const bool include_readonly,
const bool include_current_file,
const bool include_remote_libraries,
const bool include_separate_online_essentials)
{
EnumPropertyItem *item = nullptr;
int totitem = 0;
if (include_readonly) {
BLI_assert(rna_enum_asset_library_type_items[0].value == ASSET_LIBRARY_ALL);
RNA_enum_item_add(&item, &totitem, &rna_enum_asset_library_type_items[0]);
RNA_enum_item_add_separator(&item, &totitem);
}
if (include_current_file) {
BLI_assert(rna_enum_asset_library_type_items[1].value == ASSET_LIBRARY_LOCAL);
RNA_enum_item_add(&item, &totitem, &rna_enum_asset_library_type_items[1]);
}
if (include_readonly) {
BLI_assert(rna_enum_asset_library_type_items[2].value == ASSET_LIBRARY_ESSENTIALS);
RNA_enum_item_add(&item, &totitem, &rna_enum_asset_library_type_items[2]);
}
if (include_separate_online_essentials) {
BLI_assert(rna_enum_asset_library_type_items[3].value == ASSET_LIBRARY_ONLINE_ESSENTIALS);
RNA_enum_item_add(&item, &totitem, &rna_enum_asset_library_type_items[3]);
}
{
EnumPropertyItem *custom_item = nullptr;
int tot_custom_item = 0;
rna_enum_add_custom_libraries(&custom_item, &tot_custom_item, include_remote_libraries);
/* Add separator if needed. */
if ((tot_custom_item > 0) && (include_readonly || include_current_file)) {
RNA_enum_item_add_separator(&item, &totitem);
}
RNA_enum_item_end(&custom_item, &tot_custom_item);
RNA_enum_items_add(&item, &totitem, custom_item);
MEM_delete(custom_item);
}
RNA_enum_item_end(&item, &totitem);
return item;
}
const EnumPropertyItem *custom_libraries_rna_enum_itemf()
{
EnumPropertyItem *item = nullptr;
int totitem = 0;
rna_enum_add_custom_libraries(
&item,
&totitem,
/* This function should return local/on-disk libraries only, so skip remote ones. */
/*include_remote_libraries=*/false);
RNA_enum_item_end(&item, &totitem);
return item;
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,85 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "AS_asset_representation.hh"
#include "BKE_context.hh"
#include "ED_asset_library.hh"
#include "ED_asset_list.hh"
#include "ED_asset_shelf.hh"
#include "BLI_listbase.h"
#include "BLI_string_ref.hh"
#include "DNA_userdef_types.h"
#include "RNA_access.hh"
#include "WM_api.hh"
#include "AS_asset_catalog.hh"
#include "AS_asset_library.hh"
namespace blender::ed::asset {
static asset_system::AssetCatalog &library_ensure_catalog(
asset_system::AssetLibrary &library, const asset_system::AssetCatalogPath &path)
{
asset_system::AssetCatalogService &catalog_service = library.catalog_service();
if (asset_system::AssetCatalog *catalog = catalog_service.find_catalog_by_path(path)) {
return *catalog;
}
asset_system::AssetCatalog *new_catalog = catalog_service.create_catalog(path);
catalog_service.tag_has_unsaved_changes(new_catalog);
return *new_catalog;
}
asset_system::AssetCatalog &library_ensure_catalogs_in_path(
asset_system::AssetLibrary &library, const asset_system::AssetCatalogPath &path)
{
/* Adding multiple catalogs in a path at a time with #AssetCatalogService::create_catalog()
* doesn't work; add each potentially new catalog in the hierarchy manually here. */
asset_system::AssetCatalogPath parent = "";
path.iterate_components([&](StringRef component_name, bool /*is_last_component*/) {
library_ensure_catalog(library, parent / component_name);
parent = parent / component_name;
});
return *library.catalog_service().find_catalog_by_path(path);
}
AssetLibraryReference user_library_to_library_ref(const bUserAssetLibrary &user_library)
{
AssetLibraryReference library_ref{};
library_ref.custom_library_index = BLI_findindex(&U.asset_libraries, &user_library);
library_ref.type = ASSET_LIBRARY_CUSTOM;
return library_ref;
}
void refresh_asset_library(const bContext *C, const AssetLibraryReference &library_ref)
{
asset::list::clear(&library_ref, C);
/* TODO: Should the all library reference be automatically cleared? */
AssetLibraryReference all_lib_ref = asset_system::all_library_reference();
asset::list::clear(&all_lib_ref, C);
}
void refresh_asset_library(const bContext *C, const bUserAssetLibrary &user_library)
{
refresh_asset_library(C, user_library_to_library_ref(user_library));
}
void refresh_asset_library_from_asset(const bContext *C,
const asset_system::AssetRepresentation &asset)
{
if (std::optional<AssetLibraryReference> library_ref =
asset.owner_asset_library().library_reference())
{
refresh_asset_library(C, *library_ref);
}
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,645 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Abstractions to manage runtime asset lists with a global cache for multiple UI elements to
* access.
* Internally this uses the #FileList API and structures from `filelist.cc`.
* This is just because it contains most necessary logic already and
* there's not much time for a more long-term solution.
*/
#include <optional>
#include <string>
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "BKE_context.hh"
#include "BKE_main.hh"
#include "BKE_preferences.h"
#include "BKE_screen.hh"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_string.h"
#include "BLI_utility_mixins.hh"
#include "DNA_asset_types.h"
#include "DNA_space_enums.h"
#include "DNA_space_types.h"
#include "WM_api.hh"
/* XXX uses private header of file-space. */
#include "../space_file/file_indexer.hh"
#include "../space_file/filelist.hh"
#include "ED_asset_indexer.hh"
#include "ED_asset_list.hh"
#include "ED_fileselect.hh"
#include "ED_screen.hh"
#include "asset_library_reference.hh"
/* TODO somehow update online asset status after downloaded by subscribing to
* #WM_MSG_TYPE_REMOTE_DOWNLOADER messages. */
namespace blender::ed::asset::list {
/* -------------------------------------------------------------------- */
/** \name Asset list API
*
* Internally re-uses #FileList from the File Browser. It does all the heavy lifting already.
* \{ */
/**
* RAII wrapper for `FileList`
*/
class FileListWrapper {
static void filelist_free_fn(FileList *list)
{
filelist_free(list);
}
std::unique_ptr<FileList, decltype(&filelist_free_fn)> file_list_;
public:
explicit FileListWrapper(eFileSelectType filesel_type)
: file_list_(filelist_new(filesel_type, /*is_from_global_asset_list=*/true),
filelist_free_fn)
{
}
FileListWrapper(FileListWrapper &&other) = default;
FileListWrapper &operator=(FileListWrapper &&other) = default;
~FileListWrapper()
{
/* Destructs the owned pointer. */
file_list_ = nullptr;
}
operator FileList *() const
{
return file_list_.get();
}
};
class AssetList : NonCopyable {
public:
FileListWrapper filelist_;
AssetLibraryReference library_ref_;
AssetList() = delete;
AssetList(eFileSelectType filesel_type, const AssetLibraryReference &asset_library_ref);
AssetList(AssetList &&other) = default;
~AssetList() = default;
static bool listen(const wmNotifier &notifier);
void ensure_updated();
void setup();
void fetch(const bContext &C);
void ensure_blocking(const bContext &C);
void clear(wmWindowManager *wm);
void clear_current_file_assets(wmWindowManager *wm);
bool needs_refetch() const;
bool is_loaded() const;
asset_system::AssetLibrary *asset_library() const;
void iterate(AssetListIterFn fn) const;
int size() const;
void tag_main_data_dirty() const;
void remap_id(ID *id_old, ID *id_new) const;
};
AssetList::AssetList(eFileSelectType filesel_type, const AssetLibraryReference &asset_library_ref)
: filelist_(filesel_type), library_ref_(asset_library_ref)
{
}
void AssetList::setup()
{
FileList *files = filelist_;
std::string asset_lib_path = AS_asset_library_root_path_from_library_ref(library_ref_);
/* Relevant bits from file_refresh(). */
/* TODO pass options properly. */
filelist_setrecursion(files, FILE_SELECT_MAX_RECURSIONS);
filelist_setsorting(files, FILE_SORT_ASSET_CATALOG, false);
const bool use_asset_indexer = !USER_DEVELOPER_TOOL_TEST(&U, no_asset_indexing);
filelist_setindexer(files, use_asset_indexer ? &index::file_indexer_asset : &file_indexer_noop);
char dirpath[FILE_MAX_LIBEXTRA] = "";
if (!asset_lib_path.empty()) {
STRNCPY(dirpath, asset_lib_path.c_str());
}
filelist_setdir(files, dirpath);
}
void AssetList::ensure_updated()
{
FileList *files = filelist_;
filelist_setlibrary(files, &library_ref_);
const bool show_online = ELEM(
U.asset_access, AssetAccess::OnlineAndOffline, AssetAccess::OnlyOnline);
const bool show_offline = ELEM(
U.asset_access, AssetAccess::OnlineAndOffline, AssetAccess::OnlyOffline);
filelist_setfilter_options(
files,
true,
true,
true, /* Just always hide parent, prefer to not add an extra user option for this. */
FILE_TYPE_BLENDERLIB,
FILTER_ID_ALL,
true,
/*filter_assets_hide_online=*/!show_online,
/*filter_assets_hide_offline=*/!show_offline,
"",
"");
filelist_set_asset_include_online(files, show_online);
}
void AssetList::fetch(const bContext &C)
{
FileList *files = filelist_;
if (filelist_needs_force_reset(files)) {
filelist_readjob_stop(files, CTX_wm_manager(&C));
filelist_clear_from_reset_tag(files);
}
if (filelist_needs_reading(files)) {
if (!filelist_pending(files)) {
filelist_readjob_start(files, NC_ASSET | ND_ASSET_LIST_READING, &C);
}
}
filelist_sort(files);
filelist_filter(files);
}
void AssetList::ensure_blocking(const bContext &C)
{
FileList *files = filelist_;
if (filelist_needs_force_reset(files)) {
filelist_clear_from_reset_tag(files);
}
if (filelist_needs_reading(files)) {
filelist_readjob_blocking_run(files, NC_ASSET | ND_ASSET_LIST_READING, &C);
}
filelist_sort(files);
filelist_filter(files);
}
bool AssetList::needs_refetch() const
{
return filelist_needs_force_reset(filelist_) || filelist_needs_reading(filelist_);
}
bool AssetList::is_loaded() const
{
return filelist_is_ready(filelist_);
}
asset_system::AssetLibrary *AssetList::asset_library() const
{
return reinterpret_cast<asset_system::AssetLibrary *>(filelist_asset_library(filelist_));
}
void AssetList::iterate(AssetListIterFn fn) const
{
FileList *files = filelist_;
const int numfiles = filelist_files_ensure(files);
for (int i = 0; i < numfiles; i++) {
asset_system::AssetRepresentation *asset = filelist_entry_get_asset_representation(files, i);
if (!asset) {
continue;
}
if (!fn(*asset)) {
break;
}
}
}
void AssetList::clear(wmWindowManager *wm)
{
/* Based on #ED_fileselect_clear() */
FileList *files = filelist_;
filelist_readjob_stop(files, wm);
filelist_freelib(files);
filelist_clear(files);
filelist_tag_force_reset(files);
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST, nullptr);
}
void AssetList::clear_current_file_assets(wmWindowManager *wm)
{
/* Based on #ED_fileselect_clear_main_assets() */
FileList *files = filelist_;
filelist_readjob_stop(files, wm);
filelist_freelib(files);
filelist_tag_force_reset_mainfiles(files);
filelist_tag_reload_asset_library(files);
filelist_clear_from_reset_tag(files);
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST, nullptr);
}
/**
* \return True if the asset-list needs a UI redraw.
*/
bool AssetList::listen(const wmNotifier &notifier)
{
switch (notifier.category) {
case NC_ID: {
if (ELEM(notifier.action, NA_RENAME)) {
return true;
}
break;
}
case NC_ASSET:
if (ELEM(notifier.data, ND_ASSET_LIST, ND_ASSET_LIST_READING, ND_ASSET_LIST_PREVIEW)) {
return true;
}
if (ELEM(notifier.action, NA_ADDED, NA_REMOVED, NA_EDITED, NA_DOWNLOAD_FINISHED)) {
return true;
}
break;
}
return false;
}
/**
* \return The number of assets in the list.
*/
int AssetList::size() const
{
return filelist_files_ensure(filelist_);
}
void AssetList::tag_main_data_dirty() const
{
if (filelist_needs_reset_on_main_changes(filelist_)) {
if (!filelist_is_ready(filelist_)) {
filelist_tag_force_reset(filelist_);
}
else {
filelist_tag_force_reset_mainfiles(filelist_);
}
}
}
void AssetList::remap_id(ID * /*id_old*/, ID * /*id_new*/) const
{
/* Trigger full re-fetch of the file list if main data was changed, don't even attempt remap
* pointers. We could give file list types a id-remap callback, but it's probably not worth it.
* Refreshing local file lists is relatively cheap. */
this->tag_main_data_dirty();
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Runtime asset list cache
* \{ */
static void clear(const AssetLibraryReference *library_reference, wmWindowManager *wm);
static void on_save_post(Main *main, PointerRNA **pointers, int num_pointers, void *arg);
/**
* A global asset list map, each entry being a list for a specific asset library.
*/
using AssetListMap = Map<AssetLibraryReference, AssetList>;
struct GlobalStorage {
AssetListMap list_map;
bCallbackFuncStore on_save_callback_store{};
GlobalStorage()
{
on_save_callback_store.alloc = false;
on_save_callback_store.func = on_save_post;
BKE_callback_add(&on_save_callback_store, BKE_CB_EVT_SAVE_POST);
}
};
/**
* Wrapper for Construct on First Use idiom, to avoid the Static Initialization Fiasco.
*/
static AssetListMap &libraries_map()
{
static GlobalStorage global_storage;
return global_storage.list_map;
}
static AssetList *lookup_list(const AssetLibraryReference &library_ref)
{
return libraries_map().lookup_ptr(library_ref);
}
void storage_tag_main_data_dirty()
{
for (AssetList &list : libraries_map().values()) {
list.tag_main_data_dirty();
}
}
void storage_id_remap(ID *id_old, ID *id_new)
{
for (AssetList &list : libraries_map().values()) {
list.remap_id(id_old, id_new);
}
}
static std::optional<eFileSelectType> asset_library_reference_to_fileselect_type(
const AssetLibraryReference &library_reference)
{
switch (eAssetLibraryType(library_reference.type)) {
case ASSET_LIBRARY_ALL:
return FILE_ASSET_LIBRARY_ALL;
case ASSET_LIBRARY_ESSENTIALS:
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
return FILE_ASSET_LIBRARY_ESSENTIALS;
case ASSET_LIBRARY_CUSTOM: {
const bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_index(
&U, library_reference.custom_library_index);
if (!user_library) {
/* The caller should make sure the passed library reference is valid. */
BLI_assert_unreachable();
return std::nullopt;
}
if (user_library->flag & ASSET_LIBRARY_USE_REMOTE_URL) {
return FILE_ASSET_LIBRARY_REMOTE;
}
return FILE_ASSET_LIBRARY;
}
case ASSET_LIBRARY_LOCAL:
return FILE_MAIN_ASSET;
}
return std::nullopt;
}
using is_new_t = bool;
static std::tuple<AssetList &, is_new_t> ensure_list_storage(
const AssetLibraryReference &library_reference, eFileSelectType filesel_type)
{
AssetListMap &storage = libraries_map();
if (AssetList *list = storage.lookup_ptr(library_reference)) {
return {*list, false};
}
storage.add(library_reference, AssetList(filesel_type, library_reference));
return {storage.lookup(library_reference), true};
}
/** \} */
void asset_reading_region_listen_fn(const wmRegionListenerParams *params)
{
const wmNotifier *wmn = params->notifier;
ARegion *region = params->region;
switch (wmn->category) {
case NC_ASSET:
if (ELEM(wmn->data, ND_ASSET_LIST_READING, ND_ASSET_LIST_PREVIEW)) {
ED_region_tag_refresh_ui(region);
}
if (ELEM(wmn->action, NA_DOWNLOAD_FINISHED)) {
ED_region_tag_refresh_ui(region);
}
break;
}
}
static void on_save_post(Main *main,
PointerRNA ** /*pointers*/,
int /*num_pointers*/,
void * /*arg*/)
{
wmWindowManager *wm = static_cast<wmWindowManager *>(main->wm.first);
const AssetLibraryReference current_file_library =
asset_system::current_file_library_reference();
clear(&current_file_library, wm);
}
/* -------------------------------------------------------------------- */
/** \name C-API
* \{ */
void storage_fetch(const AssetLibraryReference *library_reference, const bContext *C)
{
std::optional filesel_type = asset_library_reference_to_fileselect_type(*library_reference);
if (!filesel_type) {
return;
}
auto [list, is_new] = ensure_list_storage(*library_reference, *filesel_type);
list.ensure_updated();
if (is_new || list.needs_refetch()) {
list.setup();
list.fetch(*C);
}
}
void storage_fetch_blocking(const AssetLibraryReference &library_reference, const bContext &C)
{
std::optional filesel_type = asset_library_reference_to_fileselect_type(library_reference);
if (!filesel_type) {
/* TODO: Warn? */
return;
}
auto [list, is_new] = ensure_list_storage(library_reference, *filesel_type);
list.ensure_updated();
if (is_new || list.needs_refetch()) {
list.setup();
list.ensure_blocking(C);
}
}
bool is_loaded(const AssetLibraryReference *library_reference)
{
AssetList *list = lookup_list(*library_reference);
if (!list) {
return false;
}
if (list->needs_refetch()) {
return false;
}
return list->is_loaded();
}
static void foreach_visible_asset_browser_showing_library(
const AssetLibraryReference &library_reference,
const wmWindowManager *wm,
const FunctionRef<void(SpaceFile &sfile)> fn)
{
for (const wmWindow &win : wm->windows) {
const bScreen *screen = WM_window_get_active_screen(&win);
for (const ScrArea &area : screen->areabase) {
/* Only needs to cover visible file/asset browsers, since others are already cleared through
* area exiting. */
if (area.spacetype == SPACE_FILE) {
SpaceFile *sfile = reinterpret_cast<SpaceFile *>(area.spacedata.first);
if (sfile->browse_mode == FILE_BROWSE_MODE_ASSETS) {
if (sfile->asset_params && sfile->asset_params->asset_library_ref == library_reference) {
fn(*sfile);
}
}
}
}
}
}
void clear(const AssetLibraryReference *library_reference, wmWindowManager *wm)
{
AssetList *list = lookup_list(*library_reference);
if (list) {
list->clear(wm);
}
/* Only needs to cover visible file/asset browsers, since others are already cleared through area
* exiting. */
foreach_visible_asset_browser_showing_library(
*library_reference, wm, [&](SpaceFile &sfile) { ED_fileselect_clear(wm, &sfile); });
/* Always clear the all library when clearing a nested one. */
if (library_reference->type != ASSET_LIBRARY_ALL) {
const AssetLibraryReference all_lib_ref = asset_system::all_library_reference();
AssetList *all_lib_list = lookup_list(all_lib_ref);
/* If the cleared nested library is the current file one, only clear current file assets. */
if (library_reference->type == ASSET_LIBRARY_LOCAL) {
if (all_lib_list) {
all_lib_list->clear_current_file_assets(wm);
}
foreach_visible_asset_browser_showing_library(
all_lib_ref, wm, [&](SpaceFile &sfile) { ED_fileselect_clear_main_assets(wm, &sfile); });
}
else {
if (all_lib_list) {
all_lib_list->clear(wm);
}
foreach_visible_asset_browser_showing_library(
all_lib_ref, wm, [&](SpaceFile &sfile) { ED_fileselect_clear(wm, &sfile); });
}
}
}
void clear(const AssetLibraryReference *library_reference, const bContext *C)
{
clear(library_reference, CTX_wm_manager(C));
}
void clear_all_library(const bContext *C)
{
const AssetLibraryReference all_lib_ref = asset_system::all_library_reference();
clear(&all_lib_ref, CTX_wm_manager(C));
}
void on_remote_assets_downloaded(wmWindowManager &wm,
const StringRef library_url,
const StringRef downloaded_file_abspath)
{
for (const wmWindow &win : wm.windows) {
const bScreen *screen = WM_window_get_active_screen(&win);
for (const ScrArea &area : screen->areabase) {
/* Only needs to cover visible file/asset browsers, since others are already cleared through
* area exiting. */
if (area.spacetype == SPACE_FILE) {
SpaceFile *sfile = reinterpret_cast<SpaceFile *>(area.spacedata.first);
if (sfile->browse_mode == FILE_BROWSE_MODE_ASSETS) {
filelist_remote_asset_library_refresh_online_assets_status(
sfile->files, library_url, downloaded_file_abspath);
}
}
}
}
for (AssetList &list : libraries_map().values()) {
filelist_remote_asset_library_refresh_online_assets_status(
list.filelist_, library_url, downloaded_file_abspath);
}
WM_event_add_notifier_ex(&wm, nullptr, NC_ASSET | NA_DOWNLOAD_FINISHED, nullptr);
}
bool has_list_storage_for_library(const AssetLibraryReference *library_reference)
{
return lookup_list(*library_reference) != nullptr;
}
bool has_asset_browser_storage_for_library(const AssetLibraryReference *library_reference,
const bContext *C)
{
bool has_asset_browser = false;
foreach_visible_asset_browser_showing_library(
*library_reference, CTX_wm_manager(C), [&](SpaceFile & /*sfile*/) {
has_asset_browser = true;
});
return has_asset_browser;
}
void iterate(const AssetLibraryReference &library_reference, AssetListIterFn fn)
{
AssetList *list = lookup_list(library_reference);
if (list) {
list->iterate(fn);
}
}
asset_system::AssetLibrary *library_get_once_available(
const AssetLibraryReference &library_reference)
{
const AssetList *list = lookup_list(library_reference);
if (!list) {
return nullptr;
}
return list->asset_library();
}
bool listen(const wmNotifier *notifier)
{
return AssetList::listen(*notifier);
}
int size(const AssetLibraryReference *library_reference)
{
AssetList *list = lookup_list(*library_reference);
if (list) {
return list->size();
}
return -1;
}
void storage_exit()
{
libraries_map().clear();
}
/** \} */
} // namespace blender::ed::asset::list

View File

@@ -0,0 +1,135 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Functions for marking and clearing assets.
*/
#include "DNA_ID.h"
#include "BKE_asset.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_preview_image.hh"
#include "UI_interface_icons.hh"
#include "RNA_prototypes.hh"
#include "ED_asset_list.hh"
#include "ED_asset_mark_clear.hh"
#include "ED_asset_type.hh"
#include "ED_render.hh"
#include "WM_types.hh"
namespace blender::ed::asset {
bool mark_id(ID *id)
{
if (id->asset_data) {
return false;
}
if (!BKE_id_can_be_asset(id)) {
return false;
}
id_fake_user_set(id);
const IDTypeInfo *id_type_info = BKE_idtype_get_info_from_id(id);
id->asset_data = BKE_asset_metadata_create();
if (AssetTypeInfo *type_info = id_type_info->asset_type_info) {
id->asset_data->local_type_info = type_info;
type_info->on_mark_asset_fn(id, id->asset_data);
}
/* Important for asset storage to update properly! */
list::storage_tag_main_data_dirty();
return true;
}
void generate_preview(const bContext *C, ID *id)
{
if (!ED_preview_id_render_is_supported(id)) {
return;
}
ED_preview_kill_jobs_for_id(CTX_wm_manager(C), id);
PreviewImage *preview = BKE_previewimg_id_get(id);
if (preview) {
BKE_previewimg_clear(preview);
}
ui::icon_render_id(C, nullptr, id, ICON_SIZE_PREVIEW, !G.background);
}
bool clear_id(ID *id)
{
if (!id->asset_data) {
return false;
}
const IDTypeInfo *id_type_info = BKE_idtype_get_info_from_id(id);
if (AssetTypeInfo *type_info = id_type_info->asset_type_info) {
if (type_info->on_clear_asset_fn) {
type_info->on_clear_asset_fn(id, id->asset_data);
}
}
BKE_asset_metadata_free(&id->asset_data);
id_fake_user_clear(id);
/* Important for asset storage to update properly! */
list::storage_tag_main_data_dirty();
return true;
}
void pre_save_assets(Main *bmain)
{
ID *id;
FOREACH_MAIN_ID_BEGIN (bmain, id) {
if (!id->asset_data || !id->asset_data->local_type_info) {
continue;
}
if (id->asset_data->local_type_info->pre_save_fn) {
id->asset_data->local_type_info->pre_save_fn(id, id->asset_data);
}
}
FOREACH_MAIN_ID_END;
}
bool can_mark_single_from_context(const bContext *C)
{
/* Context needs a "id" pointer to be set for #ASSET_OT_mark()/#ASSET_OT_mark_single() and
* #ASSET_OT_clear()/#ASSET_OT_clear_single() to use. */
const ID *id = static_cast<ID *>(CTX_data_pointer_get_type_silent(C, "id", RNA_ID).data);
if (!id) {
return false;
}
return id_type_is_supported(id);
}
bool copy_to_id(const AssetMetaData *asset_data, ID *destination)
{
if (!BKE_id_can_be_asset(destination)) {
return false;
}
if (destination->asset_data) {
BKE_asset_metadata_free(&destination->asset_data);
}
destination->asset_data = BKE_asset_metadata_copy(asset_data);
return true;
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,188 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "AS_asset_catalog_tree.hh"
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "DNA_screen_types.h"
#include "BKE_context.hh"
#include "BKE_report.hh"
#include "BLT_translation.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "RNA_prototypes.hh"
#include "ED_asset_list.hh"
#include "ED_asset_menu_utils.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
namespace blender::ed::asset {
void operator_asset_reference_props_register(StructRNA &srna)
{
PropertyRNA *prop;
prop = RNA_def_enum(&srna,
"asset_library_type",
rna_enum_asset_library_type_items,
ASSET_LIBRARY_LOCAL,
"Asset Library Type",
"");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
prop = RNA_def_string(
&srna, "asset_library_identifier", nullptr, 0, "Asset Library Identifier", "");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
prop = RNA_def_string(
&srna, "relative_asset_identifier", nullptr, 0, "Relative Asset Identifier", "");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
}
void operator_asset_reference_props_set(const asset_system::AssetRepresentation &asset,
PointerRNA &ptr)
{
const AssetWeakReference weak_ref = asset.make_weak_reference();
RNA_enum_set(&ptr, "asset_library_type", weak_ref.asset_library_type);
RNA_string_set(&ptr, "asset_library_identifier", weak_ref.asset_library_identifier);
RNA_string_set(&ptr, "relative_asset_identifier", weak_ref.relative_asset_identifier);
}
bool operator_asset_reference_props_is_set(PointerRNA &ptr)
{
return RNA_struct_property_is_set(&ptr, "asset_library_type") &&
RNA_struct_property_is_set(&ptr, "asset_library_identifier") &&
RNA_struct_property_is_set(&ptr, "relative_asset_identifier");
}
/**
* #AssetLibrary::resolve_asset_weak_reference_to_full_path() currently does not support local
* assets.
*/
static const asset_system::AssetRepresentation *get_local_asset_from_weak_ref(
const bContext &C, const AssetWeakReference &weak_ref, ReportList *reports)
{
AssetLibraryReference library_ref{};
library_ref.type = ASSET_LIBRARY_LOCAL;
list::storage_fetch(&library_ref, &C);
const asset_system::AssetRepresentation *matching_asset = nullptr;
list::iterate(library_ref, [&](asset_system::AssetRepresentation &asset) {
if (asset.make_weak_reference() == weak_ref) {
matching_asset = &asset;
return false;
}
return true;
});
if (reports && !matching_asset) {
if (list::is_loaded(&library_ref)) {
BKE_reportf(
reports, RPT_ERROR, "No asset found at path \"%s\"", weak_ref.relative_asset_identifier);
}
else {
BKE_report(reports, RPT_WARNING, "Asset loading is unfinished");
}
}
return matching_asset;
}
const asset_system::AssetRepresentation *find_asset_from_weak_ref(
const bContext &C, const AssetWeakReference &weak_ref, ReportList *reports)
{
if (weak_ref.asset_library_type == ASSET_LIBRARY_LOCAL) {
return get_local_asset_from_weak_ref(C, weak_ref, reports);
}
const AssetLibraryReference library_ref = asset_system::all_library_reference();
list::storage_fetch(&library_ref, &C);
asset_system::AssetLibrary *all_library = list::library_get_once_available(
asset_system::all_library_reference());
if (!all_library) {
BKE_report(reports, RPT_WARNING, "Asset loading is unfinished");
return nullptr;
}
const asset_system::AssetRepresentation *matching_asset = nullptr;
list::iterate(library_ref, [&](asset_system::AssetRepresentation &asset) {
if (asset.make_weak_reference() == weak_ref) {
matching_asset = &asset;
return false;
}
return true;
});
if (reports && !matching_asset) {
if (list::is_loaded(&library_ref)) {
const std::string full_path = all_library->resolve_asset_weak_reference_to_full_path(
weak_ref);
BKE_reportf(reports, RPT_ERROR, "No asset found at path \"%s\"", full_path.c_str());
}
}
return matching_asset;
}
const asset_system::AssetRepresentation *operator_asset_reference_props_get_asset_from_all_library(
const bContext &C, PointerRNA &ptr, ReportList *reports)
{
AssetWeakReference weak_ref{};
weak_ref.asset_library_type = eAssetLibraryType(RNA_enum_get(&ptr, "asset_library_type"));
weak_ref.asset_library_identifier = RNA_string_get_alloc(
&ptr, "asset_library_identifier", nullptr, 0, nullptr);
weak_ref.relative_asset_identifier = RNA_string_get_alloc(
&ptr, "relative_asset_identifier", nullptr, 0, nullptr);
return find_asset_from_weak_ref(C, weak_ref, reports);
}
void draw_menu_for_catalog(const asset_system::AssetCatalogTreeItem &item,
const StringRefNull menu_name,
ui::Layout &layout)
{
ui::Layout &col = layout.column(false);
col.context_string_set("asset_catalog_path", item.catalog_path().c_str());
col.menu(menu_name, IFACE_(item.get_name()), ICON_NONE);
}
void draw_node_menu_for_catalog(const asset_system::AssetCatalogTreeItem &item,
const StringRefNull operator_id,
const StringRefNull menu_name,
ui::Layout &layout)
{
ui::Layout &col = layout.column(false);
col.context_string_set("asset_catalog_path", item.catalog_path().c_str());
col.context_string_set("operator_id", operator_id);
col.menu(menu_name, IFACE_(item.get_name()), ICON_NONE);
}
void draw_asset_menu_item(const asset_system::AssetRepresentation *asset,
StringRefNull opname,
ui::Layout &layout)
{
ui::Layout &row = layout.row(true);
if (asset->is_online_only()) {
row.enabled_set(false);
}
PointerRNA asset_ptr = RNA_pointer_create_discrete(
nullptr, RNA_AssetRepresentation, const_cast<asset_system::AssetRepresentation *>(asset));
row.context_ptr_set("asset", &asset_ptr);
const int icon_local = asset->remote_file_status() ==
asset_system::RemoteAssetFileStatus::NO_MATCH ?
ICON_ERROR :
ICON_NONE;
const int icon = asset->is_online_only() ? ICON_INTERNET : icon_local;
PointerRNA props_ptr = row.op(
opname, IFACE_(asset->get_name()), icon, wm::OpCallContext::InvokeDefault, UI_ITEM_NONE);
asset::operator_asset_reference_props_set(*asset, props_ptr);
}
} // namespace blender::ed::asset

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#pragma once
#include "BLI_function_ref.hh"
namespace blender {
struct ARegion;
struct ARegionType;
struct AssetLibraryReference;
struct AssetShelf;
struct AssetShelfType;
struct AssetShelfSettings;
struct bContext;
struct BlendDataReader;
struct BlendWriter;
struct RegionAssetShelf;
namespace asset_system {
class AssetCatalogPath;
}
namespace ui {
struct Layout;
} // namespace ui
namespace ed::asset::shelf {
void build_asset_view(ui::Layout &layout,
const AssetLibraryReference &library_ref,
const AssetShelf &shelf,
const bContext &C);
void catalog_selector_panel_register(ARegionType *region_type);
void popover_panel_register(ARegionType *region_type);
AssetShelf *active_shelf_from_context(const bContext *C);
void send_redraw_notifier(const bContext &C);
AssetShelfType *ensure_shelf_has_type(AssetShelf &shelf);
AssetShelf *create_shelf_from_type(AssetShelfType &type);
void library_selector_draw(const bContext *C, ui::Layout &layout, AssetShelf &shelf);
/**
* Deep-copies \a shelf_regiondata into newly allocated memory. Must be freed using
* #regiondata_free().
*/
RegionAssetShelf *regiondata_duplicate(const RegionAssetShelf *shelf_regiondata);
/** Frees the contained data and \a shelf_regiondata itself. */
void regiondata_free(RegionAssetShelf *shelf_regiondata);
void regiondata_blend_write(BlendWriter *writer, const RegionAssetShelf *shelf_regiondata);
void regiondata_blend_read_data(BlendDataReader *reader, RegionAssetShelf **shelf_regiondata);
void settings_blend_write(BlendWriter *writer, const AssetShelfSettings &settings);
void settings_blend_read_data(BlendDataReader *reader, AssetShelfSettings &settings);
/**
* Important: Must be called before #AssetShelfSettings.asset_library_reference is used. It will
* make sure to fall back to the "All" library if the reference refers to a deleted library. An
* invalid reference would make loading the asset listing fail.
*
* The library reference in \a settings will be updated and returned (for convenience).
*/
AssetLibraryReference &settings_ensure_valid_library_ref(AssetShelfSettings &settings);
void settings_set_active_catalog(AssetShelfSettings &settings,
const asset_system::AssetCatalogPath &path);
void settings_set_all_catalog_active(AssetShelfSettings &settings);
bool settings_is_active_catalog(const AssetShelfSettings &settings,
const asset_system::AssetCatalogPath &path);
bool settings_is_all_catalog_active(const AssetShelfSettings &settings);
/**
* Clears the list of enabled catalogs in either the Preferences (if any) or the asset shelf
* settings (if any), depending on the #ASSET_SHELF_TYPE_FLAG_STORE_CATALOGS_IN_PREFS flag.
*/
void settings_clear_enabled_catalogs(AssetShelf &shelf);
bool settings_is_catalog_path_enabled(const AssetShelf &shelf,
const asset_system::AssetCatalogPath &path);
void settings_set_catalog_path_enabled(AssetShelf &shelf,
const asset_system::AssetCatalogPath &path);
void settings_foreach_enabled_catalog_path(
const AssetShelf &shelf,
FunctionRef<void(const asset_system::AssetCatalogPath &catalog_path)> fn);
} // namespace ed::asset::shelf
} // namespace blender

View File

@@ -0,0 +1,511 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Grid-view showing all assets according to the giving shelf-type and settings.
*/
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "BKE_screen.hh"
#include "BLI_fnmatch.h"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLT_translation.hh"
#include "DNA_asset_types.h"
#include "DNA_screen_types.h"
#include "ED_asset.hh"
#include "ED_asset_menu_utils.hh"
#include "ED_asset_shelf.hh"
#include "UI_grid_view.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "WM_api.hh"
#include "asset_shelf.hh"
namespace blender::ed::asset::shelf {
class AssetView : public ui::AbstractGridView {
const AssetLibraryReference library_ref_;
const AssetShelf &shelf_;
std::optional<AssetWeakReference> active_asset_;
std::optional<asset_system::AssetCatalogFilter> catalog_filter_ = std::nullopt;
friend class AssetViewItem;
friend class AssetDragController;
public:
AssetView(const AssetLibraryReference &library_ref, const AssetShelf &shelf);
void build_items() override;
bool begin_filtering(const bContext &C) const override;
void set_catalog_filter(const std::optional<asset_system::AssetCatalogFilter> &catalog_filter);
};
class AssetViewItem : public ui::PreviewGridItem {
asset_system::AssetRepresentation &asset_;
bool allow_asset_drag_ = true;
public:
AssetViewItem(asset_system::AssetRepresentation &asset_, StringRef identifier, StringRef label);
void disable_asset_drag();
void build_grid_tile(const bContext &C, ui::Layout &layout) const override;
void build_context_menu(bContext &C, ui::Layout &column) const override;
std::optional<bool> should_be_active() const override;
void on_activate(bContext &C) override;
bool should_be_filtered_visible(StringRefNull filter_string) const override;
std::unique_ptr<ui::AbstractViewItemDragController> create_drag_controller() const override;
};
class AssetDragController : public ui::AbstractViewItemDragController {
asset_system::AssetRepresentation &asset_;
public:
AssetDragController(ui::AbstractGridView &view, asset_system::AssetRepresentation &asset);
std::optional<eWM_DragDataType> get_drag_type() const override;
void *create_drag_data() const override;
void on_drag_start(bContext &C, ui::AbstractViewItem &item) override;
};
AssetView::AssetView(const AssetLibraryReference &library_ref, const AssetShelf &shelf)
: library_ref_(library_ref), shelf_(shelf)
{
if (shelf.type->get_active_asset) {
if (const AssetWeakReference *weak_ref = shelf.type->get_active_asset(shelf.type)) {
active_asset_ = *weak_ref;
}
else {
active_asset_.reset();
}
}
}
void AssetView::build_items()
{
const asset_system::AssetLibrary *library = list::library_get_once_available(library_ref_);
if (!library) {
return;
}
list::iterate(library_ref_, [&](asset_system::AssetRepresentation &asset) {
if (!shelf::type_asset_poll(*shelf_.type, asset)) {
/* Skip this asset. */
return true;
}
const AssetMetaData &asset_data = asset.get_metadata();
if (catalog_filter_ && !catalog_filter_->contains(asset_data.catalog_id)) {
/* Skip this asset. */
return true;
}
const bool show_names = (shelf_.settings.display_flag & ASSETSHELF_SHOW_NAMES);
const StringRef identifier = asset.library_relative_identifier();
AssetViewItem &item = this->add_item<AssetViewItem>(asset, identifier, asset.get_name());
if (!show_names) {
item.hide_label();
}
if (shelf_.type->flag & ASSET_SHELF_TYPE_FLAG_NO_ASSET_DRAG) {
item.disable_asset_drag();
}
if (!shelf_.type->drag_operator.empty()) {
/* For now always select/activate items on click instead of press when there's a drag
* operator set. Important for pose library blending. Maybe we want to make this an explicit
* option of the asset shelf instead. */
item.select_on_click_set();
}
/* Make sure every click calls the #bl_activate_operator. We might want to add a flag to
* enable/disable this. Or we only call #bl_activate_operator when an item becomes active, and
* add a #bl_click_operator for repeated execution on every click. So far it seems like every
* asset shelf use case works with activating on every click though. */
item.always_reactivate_on_click();
if (shelf_.type->flag & ASSET_SHELF_TYPE_FLAG_ACTIVATE_FOR_CONTEXT_MENU &&
!asset.is_online_only())
{
item.activate_for_context_menu_set();
}
return true;
});
}
bool AssetView::begin_filtering(const bContext &C) const
{
const ScrArea *area = CTX_wm_area(&C);
for (ARegion &region : area->regionbase) {
if (ui::textbutton_activate_rna(&C, &region, &shelf_, "search_filter")) {
return true;
}
}
return false;
}
void AssetView::set_catalog_filter(
const std::optional<asset_system::AssetCatalogFilter> &catalog_filter)
{
if (catalog_filter) {
catalog_filter_.emplace(*catalog_filter);
}
else {
catalog_filter_ = std::nullopt;
}
}
static std::optional<asset_system::AssetCatalogFilter> catalog_filter_from_shelf_settings(
const AssetShelfSettings &shelf_settings, const asset_system::AssetLibrary &library)
{
if (!shelf_settings.active_catalog_path) {
return {};
}
asset_system::AssetCatalog *active_catalog = library.catalog_service().find_catalog_by_path(
shelf_settings.active_catalog_path);
if (!active_catalog) {
return {};
}
return library.catalog_service().create_catalog_filter(active_catalog->catalog_id);
}
/* ---------------------------------------------------------------------- */
AssetViewItem::AssetViewItem(asset_system::AssetRepresentation &asset,
StringRef identifier,
StringRef label)
: ui::PreviewGridItem(identifier, label, ICON_NONE), asset_(asset)
{
}
void AssetViewItem::disable_asset_drag()
{
allow_asset_drag_ = false;
}
/**
* Needs freeing with #WM_operator_properties_free() (will be done by button if passed to that) and
* #MEM_delete().
*/
static std::optional<wmOperatorCallParams> create_asset_operator_params(
const StringRefNull op_name, const asset_system::AssetRepresentation &asset)
{
if (op_name.is_empty()) {
return {};
}
wmOperatorType *ot = WM_operatortype_find(op_name.c_str(), true);
if (!ot) {
return {};
}
PointerRNA *op_props = MEM_new<PointerRNA>(__func__, WM_operator_properties_create_ptr(ot));
asset::operator_asset_reference_props_set(asset, *op_props);
return wmOperatorCallParams{ot, op_props, wm::OpCallContext::InvokeRegionWin};
}
void AssetViewItem::build_grid_tile(const bContext &C, ui::Layout &layout) const
{
const AssetView &asset_view = reinterpret_cast<const AssetView &>(this->get_view());
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
PointerRNA asset_ptr = RNA_pointer_create_discrete(nullptr, RNA_AssetRepresentation, &asset_);
button_context_ptr_set(
layout.block(), reinterpret_cast<ui::Button *>(view_item_but_), "asset", &asset_ptr);
ui::Button *item_but = reinterpret_cast<ui::Button *>(this->view_item_button());
if (std::optional<wmOperatorCallParams> activate_op = create_asset_operator_params(
shelf_type.activate_operator, asset_))
{
/* Attach the operator, but don't call it through the button. We call it using
* #on_activate(). */
button_operator_set(item_but, activate_op->optype, activate_op->opcontext, activate_op->opptr);
button_operator_set_never_call(item_but);
MEM_delete(activate_op->opptr);
}
const ui::GridViewStyle &style = this->get_view().get_style();
/* Increase background draw size slightly, so highlights are well visible behind previews with an
* opaque background. */
button_view_item_draw_size_set(
item_but, style.tile_width + 2 * U.pixelsize, style.tile_height + 2 * U.pixelsize);
button_func_tooltip_custom_set(
item_but,
[](bContext & /*C*/, ui::TooltipData &tip, ui::Button * /*but*/, void *argN) {
const asset_system::AssetRepresentation *asset =
static_cast<const asset_system::AssetRepresentation *>(argN);
asset_tooltip(*asset, tip);
},
(&asset_),
nullptr);
/* Request preview when drawing. Grid views have an optimization to only draw items that are
* actually visible, so only previews scrolled into view will be loaded this way. This reduces
* total loading time and memory footprint. */
asset_.ensure_previewable(C);
const int preview_id = [&]() -> int {
/* Show loading icon while list is loading still. Previews might get pushed out of view again
* while the list grows, which can cause a lot of flickering. Note that this also means the
* actual loading of previews is delayed, because that only happens when a preview icon-ID is
* attached to a button. */
if (!list::is_loaded(&asset_view.library_ref_)) {
return ICON_PREVIEW_LOADING;
}
return asset_preview_or_icon(asset_);
}();
ui::GridViewStyle grid_style = asset_view.get_style();
/* Add overlap layout so indicator icons can be displayed on top of the preview. */
ui::Layout &overlap = layout.overlap();
overlap.ui_units_x_set(grid_style.tile_width / UI_UNIT_X);
overlap.ui_units_y_set(grid_style.tile_height / UI_UNIT_Y);
ui::PreviewGridItem::build_grid_tile_button(overlap.column(true), preview_id);
ui::Layout &overlay_row = overlap.row(true);
overlay_row.alignment_set(ui::LayoutAlign::Right);
if (asset_.is_online_only()) {
ui::Button *online_icon = uiItemL_ex(&overlay_row, "", ICON_INTERNET, false, false);
button_label_alpha_factor_set(online_icon, 0.6f);
button_label_draw_icon_border_set(online_icon, true);
}
else if (asset_.needs_download()) {
ui::Button *needs_download_icon = uiItemL_ex(&overlay_row, "", ICON_ERROR, false, false);
button_label_alpha_factor_set(needs_download_icon, 0.6f);
button_label_draw_icon_border_set(needs_download_icon, true);
}
/* Download overlay button for online assets. */
if (is_hovered() && asset_.needs_download()) {
ui::Block *block = overlap.block();
ui::Layout &center_row = overlap.row(true);
center_row.alignment_set(ui::LayoutAlign::Center);
center_row.ui_units_x_set(overlap.ui_units_x());
center_row.column(true);
const int overlay_width = ICON_DEFAULT_WIDTH_SCALE * 2.0f;
const int overlay_height = ICON_DEFAULT_HEIGHT_SCALE * 2.0f;
const int preview_height = tile_height(asset_view.shelf_.settings) -
((asset_view.shelf_.settings.display_flag & ASSETSHELF_SHOW_NAMES) ?
UI_UNIT_Y :
0.0f);
/* Insert padding above the overlay to center it vertically. */
ui::uiDefBut(block,
ui::ButtonType::Label,
"",
0,
0,
1,
std::max(0.0f, (preview_height - overlay_height + U.pixelsize) * 0.5f),
nullptr,
0,
0,
std::nullopt);
ui::Button *but = uiDefIconButO(block,
ui::ButtonType::But,
"ASSET_OT_asset_download",
wm::OpCallContext::ExecDefault,
ICON_DOWNLOAD,
0,
0,
overlay_width,
overlay_height,
std::nullopt);
PointerRNA *opptr = ui::button_operator_ptr_ensure(but);
ed::asset::operator_asset_reference_props_set(asset_, *opptr);
ui::button_icon_scale_set(but, 1.5f);
ui::button_pushbutton_draw_as_overlay_set(but, true);
}
}
void AssetViewItem::build_context_menu(bContext &C, ui::Layout &column) const
{
const AssetView &asset_view = dynamic_cast<const AssetView &>(this->get_view());
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
if (shelf_type.draw_context_menu) {
shelf_type.draw_context_menu(&C, &shelf_type, &asset_, column);
}
}
std::optional<bool> AssetViewItem::should_be_active() const
{
const AssetView &asset_view = dynamic_cast<const AssetView &>(this->get_view());
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
if (!shelf_type.get_active_asset) {
return {};
}
if (!asset_view.active_asset_) {
return false;
}
AssetWeakReference weak_ref = asset_.make_weak_reference();
const bool matches = *asset_view.active_asset_ == weak_ref;
return matches;
}
void AssetViewItem::on_activate(bContext &C)
{
const AssetView &asset_view = dynamic_cast<const AssetView &>(this->get_view());
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
/* Don't allow activating the asset when it requires downloading. */
if (asset_.is_online_only()) {
return;
}
if (std::optional<wmOperatorCallParams> activate_op = create_asset_operator_params(
shelf_type.activate_operator, asset_))
{
WM_operator_name_call_ptr(
&C, activate_op->optype, activate_op->opcontext, activate_op->opptr, nullptr);
WM_operator_properties_free(activate_op->opptr);
MEM_delete(activate_op->opptr);
}
}
bool AssetViewItem::should_be_filtered_visible(const StringRefNull filter_string) const
{
const StringRefNull asset_name = asset_.get_name();
return fnmatch(filter_string.c_str(), asset_name.c_str(), FNM_CASEFOLD) == 0;
}
std::unique_ptr<ui::AbstractViewItemDragController> AssetViewItem::create_drag_controller() const
{
const AssetView &asset_view = dynamic_cast<const AssetView &>(this->get_view());
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
if (!allow_asset_drag_ && shelf_type.drag_operator.empty()) {
return nullptr;
}
return std::make_unique<AssetDragController>(this->get_view(), asset_);
}
/* ---------------------------------------------------------------------- */
static std::string filter_string_get(const AssetShelf &shelf)
{
/* Copy of the filter string from #AssetShelfSettings, with extra '*' added to the beginning and
* end of the string, for `fnmatch()` to work. */
char search_string[sizeof(AssetShelfSettings::search_string) + 2];
BLI_strncpy_ensure_pad(search_string, shelf.settings.search_string, '*', sizeof(search_string));
return search_string;
}
void build_asset_view(ui::Layout &layout,
const AssetLibraryReference &library_ref,
const AssetShelf &shelf,
const bContext &C)
{
list::storage_fetch(&library_ref, &C);
const asset_system::AssetLibrary *library = list::library_get_once_available(library_ref);
if (!library) {
return;
}
const float tile_width = shelf::tile_width(shelf.settings);
const float tile_height = shelf::tile_height(shelf.settings);
BLI_assert(tile_width != 0);
BLI_assert(tile_height != 0);
std::unique_ptr asset_view = std::make_unique<AssetView>(library_ref, shelf);
asset_view->set_catalog_filter(catalog_filter_from_shelf_settings(shelf.settings, *library));
asset_view->set_tile_size(tile_width, tile_height);
ui::Block *block = layout.block();
ui::AbstractGridView *grid_view = block_add_view(
*block, "asset shelf asset view", std::move(asset_view));
grid_view->set_context_menu_title("Asset Shelf");
ui::GridViewBuilder builder(*block);
builder.build_grid_view(C, *grid_view, layout, filter_string_get(shelf));
}
/* ---------------------------------------------------------------------- */
/* Dragging. */
AssetDragController::AssetDragController(ui::AbstractGridView &view,
asset_system::AssetRepresentation &asset)
: ui::AbstractViewItemDragController(view), asset_(asset)
{
}
std::optional<eWM_DragDataType> AssetDragController::get_drag_type() const
{
const AssetView &asset_view = this->get_view<AssetView>();
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
/* Disable asset dragging, only call #AssetShelfType::drag_operator in #on_drag_start(). */
if (!shelf_type.drag_operator.empty()) {
return std::nullopt;
}
return asset_.is_local_id() ? WM_DRAG_ID : WM_DRAG_ASSET;
}
void AssetDragController::on_drag_start(bContext &C, ui::AbstractViewItem &item)
{
const AssetView &asset_view = this->get_view<AssetView>();
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
if (std::optional<wmOperatorCallParams> drag_op = create_asset_operator_params(
shelf_type.drag_operator, asset_))
{
WM_operator_name_call_ptr(&C, drag_op->optype, drag_op->opcontext, drag_op->opptr, nullptr);
WM_operator_properties_free(drag_op->opptr);
MEM_delete(drag_op->opptr);
/* Display as active so it's clear which item is being operated on. #activate() would trigger
* the activation operator. We really don't want this for poses, since dragging shouldn't fully
* apply a pose, but trigger interactive pose blending instead.
*
* Messing with the active state could cause problems, in that case a separate highlighting
* feature might make sense (so e.g. dragged from assets get an outline). */
item.set_state_active();
}
}
void *AssetDragController::create_drag_data() const
{
ID *local_id = asset_.local_id();
if (local_id) {
return static_cast<void *>(local_id);
}
eAssetImportMethod import_method = asset_.get_import_method().value_or(ASSET_IMPORT_PACK);
if (U.experimental.no_data_block_packing && import_method == ASSET_IMPORT_PACK) {
import_method = ASSET_IMPORT_APPEND_REUSE;
}
AssetImportSettings import_settings{};
import_settings.method = import_method;
import_settings.use_instance_collections = false;
return WM_drag_create_asset_data(&asset_, import_settings);
}
} // namespace blender::ed::asset::shelf

View File

@@ -0,0 +1,248 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Catalog tree-view to enable/disable catalogs in the asset shelf settings.
*/
#include "AS_asset_catalog.hh"
#include "AS_asset_catalog_tree.hh"
#include "BLI_string_utf8.h"
#include "DNA_screen_types.h"
#include "BLI_listbase.h"
#include "BKE_context.hh"
#include "BKE_screen.hh"
#include "BLT_translation.hh"
#include "ED_asset_filter.hh"
#include "ED_asset_list.hh"
#include "ED_asset_shelf.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_tree_view.hh"
#include "WM_api.hh"
#include "asset_shelf.hh"
namespace blender::ed::asset::shelf {
class AssetCatalogSelectorTree : public ui::AbstractTreeView {
AssetShelf &shelf_;
AssetShelfSettings &shelf_settings_;
asset_system::AssetCatalogTree catalog_tree_;
public:
class Item;
AssetCatalogSelectorTree(asset_system::AssetLibrary &library, AssetShelf &shelf)
: shelf_(shelf), shelf_settings_(shelf_.settings)
{
catalog_tree_ = build_filtered_catalog_tree(
library,
shelf_settings_.asset_library_reference,
[this](const asset_system::AssetRepresentation &asset) {
return type_asset_poll(*shelf_.type, asset);
});
}
void build_tree() override
{
if (catalog_tree_.is_empty()) {
auto &item = add_tree_item<ui::BasicTreeViewItem>(RPT_("No asset catalogs"), ICON_INFO);
item.disable_interaction();
this->is_flat_ = true;
return;
}
catalog_tree_.foreach_root_item(
[this](const asset_system::AssetCatalogTreeItem &catalog_item) {
Item &item = build_catalog_items_recursive(*this, catalog_item);
item.uncollapse_by_default();
});
}
Item &build_catalog_items_recursive(ui::TreeViewOrItem &parent_view_item,
const asset_system::AssetCatalogTreeItem &catalog_item) const
{
Item &view_item = parent_view_item.add_tree_item<Item>(catalog_item, shelf_);
const int parent_count = view_item.count_parents() + 1;
catalog_item.foreach_child([&, this](const asset_system::AssetCatalogTreeItem &child) {
Item &child_item = build_catalog_items_recursive(view_item, child);
/* Uncollapse to some level (gives quick access, but don't let the tree get too big). */
if (parent_count < 2) {
child_item.uncollapse_by_default();
}
});
return view_item;
}
void update_shelf_settings_from_enabled_catalogs();
class Item : public ui::BasicTreeViewItem {
const asset_system::AssetCatalogTreeItem &catalog_item_;
/* Is the catalog path enabled in this redraw? Set on construction, updated by the UI (which
* gets a pointer to it). The UI needs it as char. */
char catalog_path_enabled_ = false;
public:
Item(const asset_system::AssetCatalogTreeItem &catalog_item, AssetShelf &shelf)
: ui::BasicTreeViewItem(catalog_item.get_name()),
catalog_item_(catalog_item),
catalog_path_enabled_(
settings_is_catalog_path_enabled(shelf, catalog_item.catalog_path()))
{
disable_activatable();
}
bool is_catalog_path_enabled() const
{
return catalog_path_enabled_ != 0;
}
bool has_enabled_in_subtree()
{
bool has_enabled = false;
foreach_item_recursive(
[&has_enabled](const ui::AbstractTreeViewItem &abstract_item) {
const Item &item = dynamic_cast<const Item &>(abstract_item);
if (item.is_catalog_path_enabled()) {
has_enabled = true;
}
},
IterOptions::SkipFiltered);
return has_enabled;
}
asset_system::AssetCatalogPath catalog_path() const
{
return catalog_item_.catalog_path();
}
void build_row(ui::Layout &row) override
{
AssetCatalogSelectorTree &tree = dynamic_cast<AssetCatalogSelectorTree &>(get_tree_view());
ui::Block *block = row.block();
row.emboss_set(ui::EmbossType::Emboss);
ui::Layout &subrow = row.row(false);
subrow.active_set(catalog_path_enabled_);
subrow.label(catalog_item_.get_name(), ICON_NONE);
ui::block_layout_set_current(block, &row);
ui::Button *toggle_but = uiDefButV(block,
ui::ButtonType::Checkbox,
"",
0,
0,
UI_UNIT_X,
UI_UNIT_Y,
&catalog_path_enabled_,
0,
0,
TIP_("Toggle catalog visibility in the asset shelf"));
button_func_set(toggle_but, [&tree](bContext &C) {
tree.update_shelf_settings_from_enabled_catalogs();
send_redraw_notifier(C);
});
if (!is_catalog_path_enabled() && has_enabled_in_subtree()) {
button_drawflag_enable(toggle_but, ui::BUT_INDETERMINATE);
}
button_flag_disable(toggle_but, ui::BUT_UNDO);
}
};
};
void AssetCatalogSelectorTree::update_shelf_settings_from_enabled_catalogs()
{
settings_clear_enabled_catalogs(shelf_);
foreach_item([this](ui::AbstractTreeViewItem &view_item) {
const auto &selector_tree_item = dynamic_cast<AssetCatalogSelectorTree::Item &>(view_item);
if (selector_tree_item.is_catalog_path_enabled()) {
settings_set_catalog_path_enabled(shelf_, selector_tree_item.catalog_path());
}
});
}
void library_selector_draw(const bContext *C, ui::Layout &layout, AssetShelf &shelf)
{
layout.operator_context_set(wm::OpCallContext::InvokeDefault);
PointerRNA shelf_ptr = RNA_pointer_create_discrete(
&CTX_wm_screen(C)->id, RNA_AssetShelf, &shelf);
ui::Layout &row = layout.row(true);
row.prop(&shelf_ptr, "asset_library_reference", UI_ITEM_NONE, "", ICON_NONE);
if (shelf.settings.asset_library_reference.type != ASSET_LIBRARY_LOCAL) {
PointerRNA ptr = row.op("ASSET_OT_library_refresh", "", ICON_FILE_REFRESH);
RNA_boolean_set(&ptr, "use_shift_for_remote_listing", true);
}
}
static void catalog_selector_panel_draw(const bContext *C, Panel *panel)
{
AssetShelf *shelf = active_shelf_from_context(C);
if (!shelf) {
return;
}
settings_ensure_valid_library_ref(shelf->settings);
ui::Layout &layout = *panel->layout;
library_selector_draw(C, layout, *shelf);
asset_system::AssetLibrary *library = list::library_get_once_available(
shelf->settings.asset_library_reference);
if (!library) {
return;
}
ui::Block *block = layout.block();
ui::AbstractTreeView *tree_view = block_add_view(
*block,
"asset catalog tree view",
std::make_unique<AssetCatalogSelectorTree>(*library, *shelf));
tree_view->set_context_menu_title("Catalog");
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, layout);
}
void catalog_selector_panel_register(ARegionType *region_type)
{
/* Uses global paneltype registry to allow usage as popover. So only register this once (may be
* called from multiple spaces). */
if (WM_paneltype_find("ASSETSHELF_PT_catalog_selector", true)) {
return;
}
PanelType *pt = MEM_new_zeroed<PanelType>(__func__);
STRNCPY_UTF8(pt->idname, "ASSETSHELF_PT_catalog_selector");
STRNCPY_UTF8(pt->label, N_("Catalog Selector"));
STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
pt->description = N_(
"Select the asset library and the contained catalogs to display in the asset shelf");
pt->draw = catalog_selector_panel_draw;
pt->listener = asset::list::asset_reading_region_listen_fn;
BLI_addtail(&region_type->paneltypes, pt);
WM_paneltype_add(pt);
}
} // namespace blender::ed::asset::shelf

View File

@@ -0,0 +1,307 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "AS_asset_library.hh"
#include "asset_shelf.hh"
#include "BKE_screen.hh"
#include "BLI_listbase.h"
#include "BLI_string_utf8.h"
#include "BLT_translation.hh"
#include "UI_interface_c.hh"
#include "UI_interface_layout.hh"
#include "UI_tree_view.hh"
#include "ED_asset_filter.hh"
#include "ED_asset_list.hh"
#include "ED_asset_shelf.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "WM_api.hh"
namespace blender::ed::asset::shelf {
class StaticPopupShelves {
public:
Vector<AssetShelf *> popup_shelves;
~StaticPopupShelves()
{
for (AssetShelf *shelf : popup_shelves) {
MEM_delete(shelf);
}
}
static Vector<AssetShelf *> &shelves()
{
static StaticPopupShelves storage;
return storage.popup_shelves;
}
};
void type_popup_unlink(const AssetShelfType &shelf_type)
{
for (AssetShelf *shelf : StaticPopupShelves::shelves()) {
if (shelf->type == &shelf_type) {
shelf->type = nullptr;
}
}
}
static AssetShelf *lookup_shelf_for_popup(const bContext &C, const AssetShelfType &shelf_type)
{
Vector<AssetShelf *> &popup_shelves = StaticPopupShelves::shelves();
for (AssetShelf *shelf : popup_shelves) {
if (STREQ(shelf->idname, shelf_type.idname)) {
if (type_poll_for_popup(C, ensure_shelf_has_type(*shelf))) {
return shelf;
}
break;
}
}
return nullptr;
}
static AssetShelf *get_shelf_for_popup(const bContext &C, AssetShelfType &shelf_type)
{
Vector<AssetShelf *> &popup_shelves = StaticPopupShelves::shelves();
if (AssetShelf *shelf = lookup_shelf_for_popup(C, shelf_type)) {
return shelf;
}
if (type_poll_for_popup(C, &shelf_type)) {
AssetShelf *new_shelf = create_shelf_from_type(shelf_type);
new_shelf->settings.display_flag |= ASSETSHELF_SHOW_NAMES;
/* Increased size of previews, to leave more space for the name. */
new_shelf->settings.preview_size = ASSET_SHELF_PREVIEW_SIZE_DEFAULT;
popup_shelves.append(new_shelf);
return new_shelf;
}
return nullptr;
}
void ensure_asset_library_fetched(const bContext &C, const AssetShelfType &shelf_type)
{
if (AssetShelf *shelf = lookup_shelf_for_popup(C, shelf_type)) {
list::storage_fetch(&shelf->settings.asset_library_reference, &C);
}
else {
AssetLibraryReference library_ref = asset_system::all_library_reference();
list::storage_fetch(&library_ref, &C);
}
}
class AssetCatalogTreeView : public ui::AbstractTreeView {
AssetShelf &shelf_;
asset_system::AssetCatalogTree catalog_tree_;
public:
AssetCatalogTreeView(const asset_system::AssetLibrary &library, AssetShelf &shelf)
: shelf_(shelf)
{
catalog_tree_ = build_filtered_catalog_tree(
library,
shelf_.settings.asset_library_reference,
[this](const asset_system::AssetRepresentation &asset) {
return type_asset_poll(*shelf_.type, asset);
});
/* Keep the popup open when clicking to activate a catalog. */
this->set_popup_keep_open();
}
void build_tree() override
{
if (catalog_tree_.is_empty()) {
auto &item = this->add_tree_item<ui::BasicTreeViewItem>(RPT_("No asset catalogs"),
ICON_INFO);
item.disable_interaction();
this->is_flat_ = true;
return;
}
auto &all_item = this->add_tree_item<ui::BasicTreeViewItem>(IFACE_("All"));
all_item.set_on_activate_fn([this](bContext &C, ui::BasicTreeViewItem &) {
settings_set_all_catalog_active(shelf_.settings);
send_redraw_notifier(C);
});
all_item.set_is_active_fn(
[this]() { return settings_is_all_catalog_active(shelf_.settings); });
all_item.uncollapse_by_default();
catalog_tree_.foreach_root_item([&, this](
const asset_system::AssetCatalogTreeItem &catalog_item) {
ui::BasicTreeViewItem &item = this->build_catalog_items_recursive(all_item, catalog_item);
item.uncollapse_by_default();
});
}
ui::BasicTreeViewItem &build_catalog_items_recursive(
ui::TreeViewOrItem &parent_view_item,
const asset_system::AssetCatalogTreeItem &catalog_item) const
{
ui::BasicTreeViewItem &view_item = parent_view_item.add_tree_item<ui::BasicTreeViewItem>(
catalog_item.get_name());
std::string catalog_path = catalog_item.catalog_path().str();
view_item.set_on_activate_fn([this, catalog_path](bContext &C, ui::BasicTreeViewItem &) {
settings_set_active_catalog(shelf_.settings, catalog_path);
send_redraw_notifier(C);
});
view_item.set_is_active_fn([this, catalog_path]() {
return settings_is_active_catalog(shelf_.settings, catalog_path);
});
const int parent_count = view_item.count_parents() + 1;
catalog_item.foreach_child([&, this](const asset_system::AssetCatalogTreeItem &child) {
ui::BasicTreeViewItem &child_item = build_catalog_items_recursive(view_item, child);
/* Uncollapse to some level (gives quick access, but don't let the tree get too big). */
if (parent_count < 3) {
child_item.uncollapse_by_default();
}
});
return view_item;
}
};
static void catalog_tree_draw(const bContext &C, ui::Layout &layout, AssetShelf &shelf)
{
const asset_system::AssetLibrary *library = list::library_get_once_available(
shelf.settings.asset_library_reference);
if (!library) {
return;
}
ui::Block *block = layout.block();
ui::AbstractTreeView *tree_view = block_add_view(
*block,
"asset shelf catalog tree view",
std::make_unique<AssetCatalogTreeView>(*library, shelf));
ui::TreeViewBuilder::build_tree_view(C, *tree_view, layout);
}
static AssetShelfType *lookup_type_from_idname_in_context(const bContext *C)
{
const std::optional<StringRefNull> idname = CTX_data_string_get(C, "asset_shelf_idname");
if (!idname) {
return nullptr;
}
return type_find_from_idname(*idname);
}
constexpr int LEFT_COL_WIDTH_UNITS = 10;
constexpr int RIGHT_COL_WIDTH_UNITS_DEFAULT = 50;
/**
* Ensure the popover width fits into the window: Clamp width by the window width, minus some
* padding.
*/
static int layout_width_units_clamped(const wmWindow *win)
{
const int max_units_x = (WM_window_native_pixel_x(win) / UI_UNIT_X) - 2;
return std::min(LEFT_COL_WIDTH_UNITS + RIGHT_COL_WIDTH_UNITS_DEFAULT, max_units_x);
}
static void popover_panel_draw(const bContext *C, Panel *panel)
{
const wmWindow *win = CTX_wm_window(C);
const int layout_width_units = layout_width_units_clamped(win);
AssetShelfType *shelf_type = lookup_type_from_idname_in_context(C);
BLI_assert_msg(shelf_type != nullptr, "couldn't find asset shelf type from context");
ui::Layout &layout = *panel->layout;
layout.ui_units_x_set(layout_width_units);
AssetShelf *shelf = get_shelf_for_popup(*C, *shelf_type);
if (!shelf) {
BLI_assert_unreachable();
return;
}
settings_ensure_valid_library_ref(shelf->settings);
bScreen *screen = CTX_wm_screen(C);
PointerRNA library_ref_ptr = RNA_pointer_create_discrete(
&screen->id, RNA_AssetLibraryReference, &shelf->settings.asset_library_reference);
layout.context_ptr_set("asset_library_reference", &library_ref_ptr);
ui::Layout &row = layout.row(false);
ui::Layout &catalogs_col = row.column(false);
catalogs_col.ui_units_x_set(LEFT_COL_WIDTH_UNITS);
catalogs_col.fixed_size_set(true);
library_selector_draw(C, catalogs_col, *shelf);
catalog_tree_draw(*C, catalogs_col, *shelf);
ui::Layout &right_col = row.column(false);
ui::Layout &sub = right_col.row(false);
/* Same as file/asset browser header. */
PointerRNA shelf_ptr = RNA_pointer_create_discrete(&screen->id, RNA_AssetShelf, shelf);
sub.prop(&shelf_ptr,
"search_filter",
/* Force the button to be active in a semi-modal state. */
ui::ITEM_R_TEXT_BUT_FORCE_SEMI_MODAL_ACTIVE,
"",
ICON_VIEWZOOM);
ui::Layout &asset_view_col = right_col.column(false);
BLI_assert((layout_width_units - LEFT_COL_WIDTH_UNITS) > 0);
asset_view_col.ui_units_x_set(layout_width_units - LEFT_COL_WIDTH_UNITS);
asset_view_col.fixed_size_set(true);
build_asset_view(asset_view_col, shelf->settings.asset_library_reference, *shelf, *C);
}
static bool popover_panel_poll(const bContext *C, PanelType * /*panel_type*/)
{
const AssetShelfType *shelf_type = lookup_type_from_idname_in_context(C);
if (!shelf_type) {
return false;
}
return type_poll_for_popup(*C, shelf_type);
}
void popover_panel_register(ARegionType *region_type)
{
/* Uses global paneltype registry to allow usage as popover. So only register this once (may be
* called from multiple spaces). */
if (WM_paneltype_find("ASSETSHELF_PT_popover_panel", true)) {
return;
}
PanelType *pt = MEM_new_zeroed<PanelType>(__func__);
STRNCPY_UTF8(pt->idname, "ASSETSHELF_PT_popover_panel");
STRNCPY_UTF8(pt->label, N_("Asset Shelf Panel"));
STRNCPY_UTF8(pt->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
pt->description = N_("Display an asset shelf in a popover panel");
pt->draw = popover_panel_draw;
pt->poll = popover_panel_poll;
pt->listener = asset::list::asset_reading_region_listen_fn;
/* Move to have first asset item under cursor. */
pt->offset_units_xy.x = -(LEFT_COL_WIDTH_UNITS + 1.5f);
/* Offset so mouse is below search button, over the first row of assets. */
pt->offset_units_xy.y = 2.5f;
BLI_addtail(&region_type->paneltypes, pt);
WM_paneltype_add(pt);
}
} // namespace blender::ed::asset::shelf

View File

@@ -0,0 +1,100 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "BLI_listbase.h"
#include "BLO_read_write.hh"
#include "DNA_defs.h"
#include "DNA_screen_types.h"
#include "asset_shelf.hh"
namespace blender {
RegionAssetShelf *RegionAssetShelf::get_from_asset_shelf_region(const ARegion &region)
{
if (region.regiontype != RGN_TYPE_ASSET_SHELF) {
/* Should only be called on main asset shelf region. */
BLI_assert_unreachable();
return nullptr;
}
return static_cast<RegionAssetShelf *>(region.regiondata);
}
RegionAssetShelf *RegionAssetShelf::ensure_from_asset_shelf_region(ARegion &region)
{
if (region.regiontype != RGN_TYPE_ASSET_SHELF) {
/* Should only be called on main asset shelf region. */
BLI_assert_unreachable();
return nullptr;
}
if (!region.regiondata) {
region.regiondata = MEM_new<RegionAssetShelf>("RegionAssetShelf");
}
return static_cast<RegionAssetShelf *>(region.regiondata);
}
namespace ed::asset::shelf {
RegionAssetShelf *regiondata_duplicate(const RegionAssetShelf *shelf_regiondata)
{
static_assert(
std::is_trivially_copyable_v<RegionAssetShelf>,
"RegionAssetShelf needs to be trivially copyable to allow freeing with MEM_delete()");
RegionAssetShelf *new_shelf_regiondata = MEM_new<RegionAssetShelf>(__func__);
*new_shelf_regiondata = *shelf_regiondata;
new_shelf_regiondata->shelves.clear_no_delete();
for (const AssetShelf &shelf : shelf_regiondata->shelves) {
AssetShelf *new_shelf = MEM_new<AssetShelf>("duplicate asset shelf", dna::shallow_copy(shelf));
new_shelf->settings = shelf.settings;
BLI_addtail(&new_shelf_regiondata->shelves, new_shelf);
if (shelf_regiondata->active_shelf == &shelf) {
new_shelf_regiondata->active_shelf = new_shelf;
}
}
return new_shelf_regiondata;
}
void regiondata_free(RegionAssetShelf *shelf_regiondata)
{
for (AssetShelf &shelf : shelf_regiondata->shelves.items_mutable()) {
MEM_delete(&shelf);
}
MEM_delete(shelf_regiondata);
}
void regiondata_blend_write(BlendWriter *writer, const RegionAssetShelf *shelf_regiondata)
{
writer->write_struct(shelf_regiondata);
for (const AssetShelf &shelf : shelf_regiondata->shelves) {
writer->write_struct(&shelf);
settings_blend_write(writer, shelf.settings);
}
}
void regiondata_blend_read_data(BlendDataReader *reader, RegionAssetShelf **shelf_regiondata)
{
if (!BLO_read_struct_nonnull(reader, RegionAssetShelf, shelf_regiondata)) {
return;
}
if ((*shelf_regiondata)->active_shelf) {
BLO_read_struct_nonnull(reader, AssetShelf, &(*shelf_regiondata)->active_shelf);
}
BLO_read_struct_list(reader, AssetShelf, &(*shelf_regiondata)->shelves);
for (AssetShelf &shelf : (*shelf_regiondata)->shelves) {
shelf.type = nullptr;
settings_blend_read_data(reader, shelf.settings);
}
}
} // namespace ed::asset::shelf
} // namespace blender

View File

@@ -0,0 +1,210 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* Internal and external APIs for #AssetShelfSettings.
*/
#include "AS_asset_catalog_path.hh"
#include "AS_asset_library.hh"
#include "DNA_defs.h"
#include "DNA_screen_types.h"
#include "DNA_userdef_types.h"
#include "BLO_read_write.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BKE_asset.hh"
#include "BKE_preferences.h"
#include "BKE_screen.hh"
#include "asset_shelf.hh"
namespace blender {
using namespace blender::ed::asset;
AssetShelfSettings::AssetShelfSettings() = default;
AssetShelfSettings::AssetShelfSettings(const AssetShelfSettings &other)
{
operator=(other);
}
AssetShelfSettings &AssetShelfSettings::operator=(const AssetShelfSettings &other)
{
if (this == &other) {
return *this; /* Handle self-assignment safely. */
}
/* Free existing properties. Check if they point to the same memory first, #AssetShelfSettings
* might have been shallow copied before. */
if (this->enabled_catalog_paths != other.enabled_catalog_paths) {
BKE_asset_catalog_path_list_free(this->enabled_catalog_paths);
}
if (this->active_catalog_path != other.active_catalog_path) {
MEM_SAFE_DELETE(this->active_catalog_path);
}
/* Copy from 'other'. */
this->asset_library_reference = other.asset_library_reference;
STRNCPY_UTF8(this->search_string, other.search_string);
this->preview_size = other.preview_size;
this->display_flag = other.display_flag;
if (other.active_catalog_path) {
this->active_catalog_path = BLI_strdup(other.active_catalog_path);
}
this->enabled_catalog_paths = BKE_asset_catalog_path_list_duplicate(other.enabled_catalog_paths);
return *this;
}
AssetShelfSettings::~AssetShelfSettings()
{
BKE_asset_catalog_path_list_free(enabled_catalog_paths);
MEM_SAFE_DELETE(active_catalog_path);
}
namespace ed::asset::shelf {
void settings_blend_write(BlendWriter *writer, const AssetShelfSettings &settings)
{
writer->write_struct(&settings);
BKE_asset_catalog_path_list_blend_write(writer, settings.enabled_catalog_paths);
writer->write_string(settings.active_catalog_path);
}
void settings_blend_read_data(BlendDataReader *reader, AssetShelfSettings &settings)
{
BKE_asset_catalog_path_list_blend_read_data(reader, settings.enabled_catalog_paths);
BLO_read_string(reader, &settings.active_catalog_path);
}
AssetLibraryReference &settings_ensure_valid_library_ref(AssetShelfSettings &settings)
{
if (settings.asset_library_reference.type != ASSET_LIBRARY_CUSTOM) {
/* Nothing to validate, all good. */
return settings.asset_library_reference;
}
const bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_index(
&U, settings.asset_library_reference.custom_library_index);
/* If the library wasn't found, fall back to the "All" library. */
if (!user_library || user_library->flag & ASSET_LIBRARY_DISABLED) {
settings.asset_library_reference = asset_system::all_library_reference();
}
return settings.asset_library_reference;
}
void settings_set_active_catalog(AssetShelfSettings &settings,
const asset_system::AssetCatalogPath &path)
{
MEM_delete(settings.active_catalog_path);
settings.active_catalog_path = BLI_strdupn(path.c_str(), path.length());
}
void settings_set_all_catalog_active(AssetShelfSettings &settings)
{
MEM_delete(settings.active_catalog_path);
settings.active_catalog_path = nullptr;
}
bool settings_is_active_catalog(const AssetShelfSettings &settings,
const asset_system::AssetCatalogPath &path)
{
return settings.active_catalog_path && settings.active_catalog_path == path.str();
}
bool settings_is_all_catalog_active(const AssetShelfSettings &settings)
{
return !settings.active_catalog_path || !settings.active_catalog_path[0];
}
static bool use_enabled_catalogs_from_prefs(const AssetShelf &shelf)
{
return shelf.type && (shelf.type->flag & ASSET_SHELF_TYPE_FLAG_STORE_CATALOGS_IN_PREFS);
}
static const ListBaseT<AssetCatalogPathLink> *get_enabled_catalog_path_list(
const AssetShelf &shelf)
{
if (use_enabled_catalogs_from_prefs(shelf)) {
bUserAssetShelfSettings *pref_settings = BKE_preferences_asset_shelf_settings_get(
&U, shelf.idname);
return pref_settings ? &pref_settings->enabled_catalog_paths : nullptr;
}
return &shelf.settings.enabled_catalog_paths;
}
static ListBaseT<AssetCatalogPathLink> *get_enabled_catalog_path_list(AssetShelf &shelf)
{
return const_cast<ListBaseT<AssetCatalogPathLink> *>(
get_enabled_catalog_path_list(const_cast<const AssetShelf &>(shelf)));
}
void settings_clear_enabled_catalogs(AssetShelf &shelf)
{
ListBaseT<AssetCatalogPathLink> *enabled_catalog_paths = get_enabled_catalog_path_list(shelf);
if (enabled_catalog_paths) {
BKE_asset_catalog_path_list_free(*enabled_catalog_paths);
BLI_assert(enabled_catalog_paths->is_empty());
}
}
bool settings_is_catalog_path_enabled(const AssetShelf &shelf,
const asset_system::AssetCatalogPath &path)
{
const ListBaseT<AssetCatalogPathLink> *enabled_catalog_paths = get_enabled_catalog_path_list(
shelf);
if (!enabled_catalog_paths) {
return false;
}
return BKE_asset_catalog_path_list_has_path(*enabled_catalog_paths, path.c_str());
}
void settings_set_catalog_path_enabled(AssetShelf &shelf,
const asset_system::AssetCatalogPath &path)
{
if (use_enabled_catalogs_from_prefs(shelf)) {
if (BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
&U, shelf.idname, path.c_str()))
{
U.runtime.is_dirty = true;
}
}
else {
if (!BKE_asset_catalog_path_list_has_path(shelf.settings.enabled_catalog_paths, path.c_str()))
{
BKE_asset_catalog_path_list_add_path(shelf.settings.enabled_catalog_paths, path.c_str());
}
}
}
void settings_foreach_enabled_catalog_path(
const AssetShelf &shelf,
FunctionRef<void(const asset_system::AssetCatalogPath &catalog_path)> fn)
{
const ListBaseT<AssetCatalogPathLink> *enabled_catalog_paths = get_enabled_catalog_path_list(
shelf);
if (!enabled_catalog_paths) {
return;
}
for (const AssetCatalogPathLink &path_link : *enabled_catalog_paths) {
fn(asset_system::AssetCatalogPath(path_link.path));
}
}
} // namespace ed::asset::shelf
} // namespace blender

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*
* API for temporary loading of asset IDs.
* Uses the `BLO_library_temp_xxx()` API internally.
*/
#include <string>
#include "AS_asset_representation.hh"
#include "BKE_report.hh"
#include "BLI_utility_mixins.hh"
#include "BLO_readfile.hh"
#include "MEM_guardedalloc.h"
#include "DNA_ID.h"
#include "ED_asset_temp_id_consumer.hh"
namespace blender::ed::asset {
class AssetTemporaryIDConsumer : NonCopyable, NonMovable {
const asset_system::AssetRepresentation *asset_;
TempLibraryContext *temp_lib_context_ = nullptr;
public:
AssetTemporaryIDConsumer(const asset_system::AssetRepresentation *asset) : asset_(asset) {}
~AssetTemporaryIDConsumer()
{
if (temp_lib_context_) {
BLO_library_temp_free(temp_lib_context_);
}
}
ID *get_local_id()
{
return asset_->local_id();
}
ID *import_id(ID_Type id_type, Main &bmain, ReportList &reports)
{
const char *asset_name = asset_->get_name().c_str();
std::string blend_file_path = asset_->full_library_path();
temp_lib_context_ = BLO_library_temp_load_id(
&bmain, blend_file_path.c_str(), id_type, asset_name, &reports);
if (temp_lib_context_ == nullptr || temp_lib_context_->temp_id == nullptr) {
BKE_reportf(
&reports, RPT_ERROR, "Unable to load %s from %s", asset_name, blend_file_path.c_str());
return nullptr;
}
BLI_assert(GS(temp_lib_context_->temp_id->name) == id_type);
return temp_lib_context_->temp_id;
}
};
AssetTempIDConsumer *temp_id_consumer_create(const asset_system::AssetRepresentation *asset)
{
if (!asset) {
return nullptr;
}
return reinterpret_cast<AssetTempIDConsumer *>(
MEM_new<AssetTemporaryIDConsumer>(__func__, asset));
}
void temp_id_consumer_free(AssetTempIDConsumer **consumer)
{
MEM_delete(reinterpret_cast<AssetTemporaryIDConsumer *>(*consumer));
*consumer = nullptr;
}
ID *temp_id_consumer_ensure_local_id(AssetTempIDConsumer *consumer_,
ID_Type id_type,
Main *bmain,
ReportList *reports)
{
if (!(consumer_ && bmain && reports)) {
return nullptr;
}
AssetTemporaryIDConsumer *consumer = reinterpret_cast<AssetTemporaryIDConsumer *>(consumer_);
if (ID *local_id = consumer->get_local_id()) {
return local_id;
}
return consumer->import_id(id_type, *bmain, *reports);
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include "BLI_utildefines.h"
#include "DNA_userdef_types.h"
#include "BKE_lib_id.hh"
#include "ED_asset_type.hh"
namespace blender::ed::asset {
bool id_type_is_non_experimental(const ID *id)
{
/* Remember to update #ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_FLAGS() and the messages in
* asset_operation_unsupported_type_msg with this! */
return ELEM(GS(id->name), ID_BR, ID_MA, ID_GR, ID_OB, ID_AC, ID_WO, ID_NT, ID_SCE);
}
bool id_type_is_supported(const ID *id)
{
if (!BKE_id_can_be_asset(id)) {
return false;
}
if (USER_EXPERIMENTAL_TEST(&U, use_extended_asset_browser)) {
/* The "Extended Asset Browser" experimental feature flag enables all asset types that can
* technically be assets. */
return true;
}
return id_type_is_non_experimental(id);
}
int64_t types_supported_as_filter_flags()
{
if (USER_EXPERIMENTAL_TEST(&U, use_extended_asset_browser)) {
return FILTER_ID_ALL;
}
return ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_FLAGS;
}
} // namespace blender::ed::asset

View File

@@ -0,0 +1,198 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edasset
*/
#include <optional>
#include <string>
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "BKE_preferences.h"
#include "BKE_preview_image.hh"
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLT_translation.hh"
#include "UI_interface_c.hh"
#include "UI_interface_icons.hh"
#include "UI_resources.hh"
#include "DNA_userdef_types.h"
#include "RNA_access.hh"
#include "ED_asset.hh"
namespace blender::ed::asset {
void asset_tooltip(const asset_system::AssetRepresentation &asset,
ui::TooltipData &tip,
const bool include_name)
{
if (include_name) {
tooltip_text_field_add(tip, asset.get_name(), {}, ui::TIP_STYLE_HEADER, ui::TIP_LC_MAIN);
tooltip_text_field_add(tip, {}, {}, ui::TIP_STYLE_SPACER, ui::TIP_LC_NORMAL, false);
}
const AssetMetaData &meta_data = asset.get_metadata();
if (meta_data.description) {
tooltip_text_field_add(tip, meta_data.description, {}, ui::TIP_STYLE_HEADER, ui::TIP_LC_MAIN);
}
if (asset.remote_file_status() == asset_system::RemoteAssetFileStatus::NO_MATCH) {
tooltip_text_field_add(
tip,
TIP_("This asset was previously downloaded, but it is outdated or inconsistent.\n"
"Downloading it again is recommended."),
{},
ui::TIP_STYLE_NORMAL,
ui::TIP_LC_ALERT);
}
switch (asset.owner_asset_library().library_type()) {
case ASSET_LIBRARY_CUSTOM: {
if (asset.is_online_only()) {
/* Don't show file path or .blend name. Data on disk is just a cache. */
break;
}
tooltip_text_field_add(tip, {}, {}, ui::TIP_STYLE_SPACER, ui::TIP_LC_NORMAL, false);
const std::string full_blend_path = asset.full_library_path();
char dir[FILE_MAX], file[FILE_MAX];
BLI_path_split_dir_file(full_blend_path.c_str(), dir, sizeof(dir), file, sizeof(file));
if (file[0]) {
tooltip_text_field_add(tip, file, {}, ui::TIP_STYLE_NORMAL, ui::TIP_LC_MAIN);
}
if (dir[0]) {
tooltip_text_field_add(tip, dir, {}, ui::TIP_STYLE_NORMAL, ui::TIP_LC_MAIN);
}
break;
}
case ASSET_LIBRARY_LOCAL:
tooltip_text_field_add(tip, {}, {}, ui::TIP_STYLE_SPACER, ui::TIP_LC_NORMAL, false);
tooltip_text_field_add(
tip, TIP_("Asset Library: Current File"), {}, ui::TIP_STYLE_NORMAL, ui::TIP_LC_VALUE);
break;
case ASSET_LIBRARY_ESSENTIALS:
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
tooltip_text_field_add(tip, {}, {}, ui::TIP_STYLE_SPACER, ui::TIP_LC_NORMAL, false);
tooltip_text_field_add(
tip, TIP_("Asset Library: Essentials"), {}, ui::TIP_STYLE_NORMAL, ui::TIP_LC_VALUE);
break;
default:
/* Intentionally empty. */
break;
}
if (asset.is_online_only()) {
if (std::optional<int64_t> combined_size = asset.online_asset_files_combined_size_in_bytes()) {
tooltip_text_field_add(tip, {}, {}, ui::TIP_STYLE_SPACER, ui::TIP_LC_NORMAL, false);
char size_ui_str[BLI_STR_FORMAT_INT64_BYTE_UNIT_SIZE];
BLI_str_format_byte_unit(size_ui_str, *combined_size, true);
tooltip_text_field_add(tip,
fmt::format(fmt::runtime(TIP_("Download Size: {}")), size_ui_str),
{},
ui::TIP_STYLE_NORMAL,
ui::TIP_LC_VALUE);
}
}
}
BIFIconID asset_preview_icon_id(const asset_system::AssetRepresentation &asset)
{
if (const PreviewImage *preview = asset.get_preview()) {
if (!BKE_previewimg_is_invalid(preview, ICON_SIZE_ICON)) {
return preview->runtime->icon_id;
}
}
return ICON_NONE;
}
BIFIconID asset_preview_or_icon(const asset_system::AssetRepresentation &asset)
{
const BIFIconID preview_icon = asset_preview_icon_id(asset);
if (preview_icon != ICON_NONE) {
return preview_icon;
}
/* Preview image not found or invalid. Use type icon. */
return ui::icon_from_idcode(asset.get_id_type());
}
const bUserAssetLibrary *get_asset_library_from_opptr(PointerRNA &ptr)
{
const int enum_value = RNA_enum_get(&ptr, "asset_library_reference");
const AssetLibraryReference lib_ref = asset::library_reference_from_enum_value(enum_value);
return BKE_preferences_asset_library_find_index(&U, lib_ref.custom_library_index);
}
AssetLibraryReference get_asset_library_ref_from_opptr(PointerRNA &ptr)
{
const int enum_value = RNA_enum_get(&ptr, "asset_library_reference");
return asset::library_reference_from_enum_value(enum_value);
}
std::optional<AssetLibraryReference> get_user_library_ref_for_save(
const asset_system::AssetLibrary *preferred_library)
{
if (preferred_library && !preferred_library->is_read_only()) {
if (std::optional<AssetLibraryReference> preferred_library_ref =
preferred_library->library_reference())
{
return preferred_library_ref;
}
BLI_assert_unreachable();
}
/* Fallback to the first enabled on-disk user library. */
for (const bUserAssetLibrary &asset_library : U.asset_libraries) {
if (asset_library.flag & (ASSET_LIBRARY_DISABLED | ASSET_LIBRARY_USE_REMOTE_URL)) {
continue;
}
return asset::user_library_to_library_ref(asset_library);
}
/* No enabled user asset library found. */
return {};
}
void visit_library_catalogs_catalog_for_search(
const Main &bmain,
const AssetLibraryReference lib,
const StringRef edit_text,
const FunctionRef<void(StringPropertySearchVisitParams)> visit_fn)
{
const asset_system::AssetLibrary *library = AS_asset_library_load(&bmain, lib);
if (!library) {
return;
}
if (!edit_text.is_empty()) {
const asset_system::AssetCatalogPath edit_path = edit_text;
if (!library->catalog_service().find_catalog_by_path(edit_path)) {
visit_fn(StringPropertySearchVisitParams{edit_path.str(), std::nullopt, ICON_ADD});
}
}
const std::shared_ptr<const asset_system::AssetCatalogTree> full_tree =
library->catalog_service().catalog_tree();
full_tree->foreach_item([&](const asset_system::AssetCatalogTreeItem &item) {
visit_fn(StringPropertySearchVisitParams{item.catalog_path().str(), std::nullopt});
});
}
} // namespace blender::ed::asset