Add Chromium-only Blender WebEngine parity work
This commit is contained in:
83
blender-5.2.0/source/blender/editors/asset/CMakeLists.txt
Normal file
83
blender-5.2.0/source/blender/editors/asset/CMakeLists.txt
Normal 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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
112
blender-5.2.0/source/blender/editors/asset/ED_asset_indexer.hh
Normal file
112
blender-5.2.0/source/blender/editors/asset/ED_asset_indexer.hh
Normal 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
|
||||
@@ -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
|
||||
124
blender-5.2.0/source/blender/editors/asset/ED_asset_list.hh
Normal file
124
blender-5.2.0/source/blender/editors/asset/ED_asset_list.hh
Normal 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
|
||||
@@ -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
|
||||
121
blender-5.2.0/source/blender/editors/asset/ED_asset_shelf.hh
Normal file
121
blender-5.2.0/source/blender/editors/asset/ED_asset_shelf.hh
Normal 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
|
||||
@@ -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
|
||||
39
blender-5.2.0/source/blender/editors/asset/ED_asset_type.hh
Normal file
39
blender-5.2.0/source/blender/editors/asset/ED_asset_type.hh
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
254
blender-5.2.0/source/blender/editors/asset/intern/asset_index.hh
Normal file
254
blender-5.2.0/source/blender/editors/asset/intern/asset_index.hh
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
645
blender-5.2.0/source/blender/editors/asset/intern/asset_list.cc
Normal file
645
blender-5.2.0/source/blender/editors/asset/intern/asset_list.cc
Normal 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 ¬ifier);
|
||||
|
||||
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 ¬ifier)
|
||||
{
|
||||
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(¤t_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
|
||||
@@ -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
|
||||
@@ -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
|
||||
1862
blender-5.2.0/source/blender/editors/asset/intern/asset_ops.cc
Normal file
1862
blender-5.2.0/source/blender/editors/asset/intern/asset_ops.cc
Normal file
File diff suppressed because it is too large
Load Diff
1000
blender-5.2.0/source/blender/editors/asset/intern/asset_shelf.cc
Normal file
1000
blender-5.2.0/source/blender/editors/asset/intern/asset_shelf.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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 ®ion : area->regionbase) {
|
||||
if (ui::textbutton_activate_rna(&C, ®ion, &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 ¢er_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
|
||||
@@ -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(®ion_type->paneltypes, pt);
|
||||
WM_paneltype_add(pt);
|
||||
}
|
||||
|
||||
} // namespace blender::ed::asset::shelf
|
||||
@@ -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(®ion_type->paneltypes, pt);
|
||||
WM_paneltype_add(pt);
|
||||
}
|
||||
|
||||
} // namespace blender::ed::asset::shelf
|
||||
@@ -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 ®ion)
|
||||
{
|
||||
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 ®ion)
|
||||
{
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user