Asset Browser: add operator for installing asset bundles

Add an operator "Copy to Asset Library" for installing asset bundles
into already-existing asset libraries.

The operator is shown when:
- the "Current File" library is selected,
- the blend file name matches `*_bundle.blend`, and
- the file is not already located in an asset library.

The user can select a target asset library, then gets a "Save As"
dialogue box to select where in that library the file should be saved.
This allows for renaming, saving in a subdirectory, etc.

The Asset Catalogs from the asset bundle are merged into the target
asset library.

The operator will refuse to run when external files are referenced. This
is not done in its poll function, as it's quite an extensive operator
(it loops over all ID datablocks).

Reviewed by: Severin

Differential Revision: https://developer.blender.org/D13312
This commit is contained in:
Sybren A. Stüvel 2021-11-23 12:29:44 +01:00
parent b02ac2d8be
commit cd2849c89b
Notes: blender-bot 2023-08-04 07:56:27 +02:00
Referenced by issue #93339, Crash with Menu Search and Developer Extras
Referenced by issue #109544, no "Copy Bundle to Asset Library" button
12 changed files with 353 additions and 17 deletions

View File

@ -97,6 +97,15 @@ class AssetCatalogService {
* or the save was successful. */
bool write_to_disk(const CatalogFilePath &blend_file_path);
/**
* Ensure that the next call to #on_blend_save_post() will choose a new location for the CDF
* suitable for the location of the blend file (regardless of where the current catalogs come
* from), and that catalogs will be merged with already-existing ones in that location.
*
* Use this for a "Save as..." that has to write the catalogs to the new blend file location,
* instead of updating the previously read CDF. */
void prepare_to_merge_on_write();
/**
* Merge on-disk changes into the in-memory asset catalogs.
* This should be called before writing the asset catalogs to disk.
@ -238,6 +247,11 @@ class AssetCatalogService {
*/
void create_missing_catalogs();
/**
* For every catalog, mark it as "dirty".
*/
void tag_all_catalogs_as_unsaved_changes();
/* For access by subclasses, as those will not be marked as friend by #AssetCatalogCollection. */
AssetCatalogDefinitionFile *get_catalog_definition_file();
OwningAssetCatalogMap &get_catalogs();

View File

@ -103,6 +103,14 @@ bool AssetCatalogService::has_unsaved_changes() const
return catalog_collection_->has_unsaved_changes_;
}
void AssetCatalogService::tag_all_catalogs_as_unsaved_changes()
{
for (auto &catalog : catalog_collection_->catalogs_.values()) {
catalog->flags.has_unsaved_changes = true;
}
catalog_collection_->has_unsaved_changes_ = true;
}
bool AssetCatalogService::is_empty() const
{
BLI_assert(catalog_collection_);
@ -491,6 +499,24 @@ bool AssetCatalogService::write_to_disk_ex(const CatalogFilePath &blend_file_pat
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
void AssetCatalogService::prepare_to_merge_on_write()
{
/* TODO(Sybren): expand to support multiple CDFs. */
if (!catalog_collection_->catalog_definition_file_) {
/* There is no CDF connected, so it's a no-op. */
return;
}
/* Remove any association with the CDF, so that a new location will be chosen
* when the blend file is saved. */
catalog_collection_->catalog_definition_file_.reset();
/* Mark all in-memory catalogs as "dirty", to force them to be kept around on
* the next "load-merge-write" cycle. */
tag_all_catalogs_as_unsaved_changes();
}
CatalogFilePath AssetCatalogService::find_suitable_cdf_path_for_writing(
const CatalogFilePath &blend_file_path)
{

View File

@ -24,6 +24,10 @@
* \brief external writefile function prototypes.
*/
#ifdef __cplusplus
extern "C" {
#endif
struct BlendThumbnail;
struct Main;
struct MemFile;
@ -72,3 +76,7 @@ extern bool BLO_write_file_mem(struct Main *mainvar,
int write_flags);
/** \} */
#ifdef __cplusplus
}
#endif

View File

@ -28,7 +28,8 @@ extern "C" {
int ED_asset_library_reference_to_enum_value(const AssetLibraryReference *library);
AssetLibraryReference ED_asset_library_reference_from_enum_value(int value);
const struct EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf(void);
const struct EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf(
bool include_local_library);
#ifdef __cplusplus
}

View File

@ -99,25 +99,30 @@ AssetLibraryReference ED_asset_library_reference_from_enum_value(int value)
*
* Since this is meant for UI display, skips non-displayable libraries, that is, libraries with an
* empty name or path.
*
* \param include_local_library whether to include the "Current File" library or not.
*/
const EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf()
const EnumPropertyItem *ED_asset_library_reference_to_rna_enum_itemf(
const bool include_local_library)
{
const EnumPropertyItem predefined_items[] = {
/* For the future. */
// {ASSET_REPO_BUNDLED, "BUNDLED", 0, "Bundled", "Show the default user assets"},
{ASSET_LIBRARY_LOCAL,
"LOCAL",
ICON_BLENDER,
"Current File",
"Show the assets currently available in this Blender session"},
{0, nullptr, 0, nullptr, nullptr},
};
EnumPropertyItem *item = nullptr;
int totitem = 0;
/* Add predefined items. */
RNA_enum_items_add(&item, &totitem, predefined_items);
if (include_local_library) {
const EnumPropertyItem predefined_items[] = {
/* For the future. */
// {ASSET_REPO_BUNDLED, "BUNDLED", 0, "Bundled", "Show the default user assets"},
{ASSET_LIBRARY_LOCAL,
"LOCAL",
ICON_BLENDER,
"Current File",
"Show the assets currently available in this Blender session"},
{0, nullptr, 0, nullptr, nullptr},
};
/* Add predefined items. */
RNA_enum_items_add(&item, &totitem, predefined_items);
}
/* Add separator if needed. */
if (!BLI_listbase_is_empty(&U.asset_libraries)) {

View File

@ -21,16 +21,23 @@
#include "BKE_asset.h"
#include "BKE_asset_catalog.hh"
#include "BKE_asset_library.hh"
#include "BKE_bpath.h"
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
#include "BKE_preferences.h"
#include "BKE_report.h"
#include "BLI_fileops.h"
#include "BLI_fnmatch.h"
#include "BLI_path_util.h"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "ED_asset.h"
#include "ED_asset_catalog.hh"
#include "ED_util.h"
/* XXX needs access to the file list, should all be done via the asset system in future. */
#include "ED_fileselect.h"
@ -40,6 +47,10 @@
#include "WM_api.h"
#include "WM_types.h"
#include "DNA_space_types.h"
#include "BLO_writefile.h"
using namespace blender;
/* -------------------------------------------------------------------- */
@ -650,6 +661,240 @@ static void ASSET_OT_catalogs_save(struct wmOperatorType *ot)
/* -------------------------------------------------------------------- */
static bool could_be_asset_bundle(const Main *bmain);
static const bUserAssetLibrary *selected_asset_library(struct wmOperator *op);
static bool is_contained_in_selected_asset_library(struct wmOperator *op, const char *filepath);
static bool set_filepath_for_asset_lib(const Main *bmain, struct wmOperator *op);
static bool has_external_files(Main *bmain, struct ReportList *reports);
static bool asset_bundle_install_poll(bContext *C)
{
/* This operator only works when the asset browser is set to Current File. */
const SpaceFile *sfile = CTX_wm_space_file(C);
if (!ED_fileselect_is_local_asset_library(sfile)) {
return false;
}
const Main *bmain = CTX_data_main(C);
if (!could_be_asset_bundle(bmain)) {
return false;
}
/* Check whether this file is already located inside any asset library. */
const struct bUserAssetLibrary *asset_lib = BKE_preferences_asset_library_containing_path(
&U, bmain->name);
if (asset_lib) {
return false;
}
return true;
}
static int asset_bundle_install_invoke(struct bContext *C,
struct wmOperator *op,
const struct wmEvent * /*event*/)
{
Main *bmain = CTX_data_main(C);
if (has_external_files(bmain, op->reports)) {
return OPERATOR_CANCELLED;
}
WM_event_add_fileselect(C, op);
/* Make the "Save As" dialog box default to "${ASSET_LIB_ROOT}/${CURRENT_FILE}.blend". */
if (!set_filepath_for_asset_lib(bmain, op)) {
return OPERATOR_CANCELLED;
}
return OPERATOR_RUNNING_MODAL;
}
static int asset_bundle_install_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
if (has_external_files(bmain, op->reports)) {
return OPERATOR_CANCELLED;
}
/* Check file path, copied from #wm_file_write(). */
char filepath[PATH_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
const size_t len = strlen(filepath);
if (len == 0) {
BKE_report(op->reports, RPT_ERROR, "Path is empty, cannot save");
return OPERATOR_CANCELLED;
}
if (len >= FILE_MAX) {
BKE_report(op->reports, RPT_ERROR, "Path too long, cannot save");
return OPERATOR_CANCELLED;
}
/* Check that the destination is actually contained in the selected asset library. */
if (!is_contained_in_selected_asset_library(op, filepath)) {
BKE_reportf(op->reports, RPT_ERROR, "Selected path is outside of the selected asset library");
return OPERATOR_CANCELLED;
}
WM_cursor_wait(true);
bke::AssetCatalogService *cat_service = get_catalog_service(C);
/* Store undo step, such that on a failed save the 'prepare_to_merge_on_write' call can be
* un-done. */
cat_service->undo_push();
cat_service->prepare_to_merge_on_write();
const int operator_result = WM_operator_name_call(
C, "WM_OT_save_mainfile", WM_OP_EXEC_DEFAULT, op->ptr);
WM_cursor_wait(false);
if (operator_result != OPERATOR_FINISHED) {
cat_service->undo();
return operator_result;
}
const bUserAssetLibrary *lib = selected_asset_library(op);
BLI_assert_msg(lib, "If the asset library is not known, how did we get here?");
BKE_reportf(op->reports,
RPT_INFO,
"Saved \"%s\" to asset library \"%s\"",
BLI_path_basename(bmain->name),
lib->name);
return OPERATOR_FINISHED;
}
static const EnumPropertyItem *rna_asset_library_reference_itemf(bContext *UNUSED(C),
PointerRNA *UNUSED(ptr),
PropertyRNA *UNUSED(prop),
bool *r_free)
{
const EnumPropertyItem *items = ED_asset_library_reference_to_rna_enum_itemf(false);
if (!items) {
*r_free = false;
}
*r_free = true;
return items;
}
static void ASSET_OT_bundle_install(struct wmOperatorType *ot)
{
/* identifiers */
ot->name = "Copy to Asset Library";
ot->description =
"Copy the current .blend file into an Asset Library. Only works on standalone .blend files "
"(i.e. when no other files are referenced)";
ot->idname = "ASSET_OT_bundle_install";
/* api callbacks */
ot->exec = asset_bundle_install_exec;
ot->invoke = asset_bundle_install_invoke;
ot->poll = asset_bundle_install_poll;
ot->prop = RNA_def_property(ot->srna, "asset_library_ref", PROP_ENUM, PROP_NONE);
RNA_def_property_flag(ot->prop, PROP_HIDDEN);
RNA_def_enum_funcs(ot->prop, rna_asset_library_reference_itemf);
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_BLENDER,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
}
/* Cheap check to see if this is an "asset bundle" just by checking main file name.
* A proper check will be done in the exec function, to ensure that no external files will be
* referenced. */
static bool could_be_asset_bundle(const Main *bmain)
{
return fnmatch("*_bundle.blend", bmain->name, FNM_CASEFOLD) == 0;
}
static const bUserAssetLibrary *selected_asset_library(struct wmOperator *op)
{
const int enum_value = RNA_enum_get(op->ptr, "asset_library_ref");
const AssetLibraryReference lib_ref = ED_asset_library_reference_from_enum_value(enum_value);
const bUserAssetLibrary *lib = BKE_preferences_asset_library_find_from_index(
&U, lib_ref.custom_library_index);
return lib;
}
static bool is_contained_in_selected_asset_library(struct wmOperator *op, const char *filepath)
{
const bUserAssetLibrary *lib = selected_asset_library(op);
if (!lib) {
return false;
}
return BLI_path_contains(lib->path, filepath);
}
/**
* Set the "filepath" RNA property based on selected "asset_library_ref".
* \return true if ok, false if error.
*/
static bool set_filepath_for_asset_lib(const Main *bmain, struct wmOperator *op)
{
/* Find the directory path of the selected asset library. */
const bUserAssetLibrary *lib = selected_asset_library(op);
if (lib == nullptr) {
return false;
}
/* Concatenate the filename of the current blend file. */
const char *blend_filename = BLI_path_basename(bmain->name);
if (blend_filename == NULL || blend_filename[0] == '\0') {
return false;
}
char file_path[PATH_MAX];
BLI_join_dirfile(file_path, sizeof(file_path), lib->path, blend_filename);
RNA_string_set(op->ptr, "filepath", file_path);
return true;
}
struct FileCheckCallbackInfo {
struct ReportList *reports;
bool external_file_found;
};
static bool external_file_check_callback(void *callback_info_ptr,
char * /*path_dst*/,
const char *path_src)
{
FileCheckCallbackInfo *callback_info = static_cast<FileCheckCallbackInfo *>(callback_info_ptr);
BKE_reportf(callback_info->reports,
RPT_ERROR,
"Unable to install asset bundle, has external dependency \"%s\"",
path_src);
callback_info->external_file_found = true;
return false;
}
/**
* Do a check on any external files (.blend, textures, etc.) being used.
* The "Install asset bundle" operator only works on standalone .blend files
* (catalog definition files are fine, though).
*
* \return true when there are external files, false otherwise.
*/
static bool has_external_files(Main *bmain, struct ReportList *reports)
{
struct FileCheckCallbackInfo callback_info = {reports, false};
BKE_bpath_traverse_main(
bmain,
&external_file_check_callback,
BKE_BPATH_TRAVERSE_SKIP_PACKED /* Packed files are fine. */
| BKE_BPATH_TRAVERSE_SKIP_MULTIFILE /* Only report multifiles once, it's enough. */,
&callback_info);
return callback_info.external_file_found;
}
/* -------------------------------------------------------------------- */
void ED_operatortypes_asset(void)
{
WM_operatortype_append(ASSET_OT_mark);
@ -661,6 +906,7 @@ void ED_operatortypes_asset(void)
WM_operatortype_append(ASSET_OT_catalog_undo);
WM_operatortype_append(ASSET_OT_catalog_redo);
WM_operatortype_append(ASSET_OT_catalog_undo_push);
WM_operatortype_append(ASSET_OT_bundle_install);
WM_operatortype_append(ASSET_OT_list_refresh);
}

View File

@ -109,6 +109,7 @@ struct FileSelectParams *ED_fileselect_ensure_active_params(struct SpaceFile *sf
struct FileSelectParams *ED_fileselect_get_active_params(const struct SpaceFile *sfile);
struct FileSelectParams *ED_fileselect_get_file_params(const struct SpaceFile *sfile);
struct FileAssetSelectParams *ED_fileselect_get_asset_params(const struct SpaceFile *sfile);
bool ED_fileselect_is_local_asset_library(const struct SpaceFile *sfile);
void ED_fileselect_set_params_from_userdef(struct SpaceFile *sfile);
void ED_fileselect_params_to_userdef(struct SpaceFile *sfile,

View File

@ -41,6 +41,7 @@
#include "ED_fileselect.h"
#include "UI_interface.h"
#include "UI_interface_icons.h"
#include "UI_resources.h"
#include "WM_api.h"
@ -246,7 +247,20 @@ static void file_panel_asset_catalog_buttons_draw(const bContext *C, Panel *pane
RNA_pointer_create(&screen->id, &RNA_FileAssetSelectParams, params, &params_ptr);
uiItemR(row, &params_ptr, "asset_library_ref", 0, "", ICON_NONE);
if (params->asset_library_ref.type != ASSET_LIBRARY_LOCAL) {
if (params->asset_library_ref.type == ASSET_LIBRARY_LOCAL) {
bContext *mutable_ctx = CTX_copy(C);
if (WM_operator_name_poll(mutable_ctx, "asset.bundle_install")) {
uiItemS(col);
uiItemMenuEnumO(col,
mutable_ctx,
"asset.bundle_install",
"asset_library_ref",
"Copy Bundle to Asset Library...",
ICON_IMPORT);
}
CTX_free(mutable_ctx);
}
else {
uiItemO(row, "", ICON_FILE_REFRESH, "FILE_OT_asset_library_refresh");
}

View File

@ -411,6 +411,15 @@ FileAssetSelectParams *ED_fileselect_get_asset_params(const SpaceFile *sfile)
return (sfile->browse_mode == FILE_BROWSE_MODE_ASSETS) ? sfile->asset_params : NULL;
}
bool ED_fileselect_is_local_asset_library(const SpaceFile *sfile)
{
const FileAssetSelectParams *asset_params = ED_fileselect_get_asset_params(sfile);
if (asset_params == NULL) {
return false;
}
return asset_params->asset_library_ref.type == ASSET_LIBRARY_LOCAL;
}
static void fileselect_refresh_asset_params(FileAssetSelectParams *asset_params)
{
AssetLibraryReference *library = &asset_params->asset_library_ref;

View File

@ -307,7 +307,7 @@ const EnumPropertyItem *rna_asset_library_reference_itemf(bContext *UNUSED(C),
PropertyRNA *UNUSED(prop),
bool *r_free)
{
const EnumPropertyItem *items = ED_asset_library_reference_to_rna_enum_itemf();
const EnumPropertyItem *items = ED_asset_library_reference_to_rna_enum_itemf(true);
if (!items) {
*r_free = false;
}

View File

@ -470,6 +470,8 @@ int WM_operator_repeat(struct bContext *C, struct wmOperator *op);
int WM_operator_repeat_last(struct bContext *C, struct wmOperator *op);
bool WM_operator_repeat_check(const struct bContext *C, struct wmOperator *op);
bool WM_operator_is_repeat(const struct bContext *C, const struct wmOperator *op);
bool WM_operator_name_poll(struct bContext *C, const char *opstring);
int WM_operator_name_call_ptr(struct bContext *C,
struct wmOperatorType *ot,
short context,

View File

@ -1611,6 +1611,16 @@ int WM_operator_name_call(bContext *C, const char *opstring, short context, Poin
return 0;
}
bool WM_operator_name_poll(bContext *C, const char *opstring)
{
wmOperatorType *ot = WM_operatortype_find(opstring, 0);
if (!ot) {
return false;
}
return WM_operator_poll(C, ot);
}
int WM_operator_name_call_with_properties(struct bContext *C,
const char *opstring,
short context,