Asset Browser: Improved workflow for asset catalog saving

No longer save asset catalogs on blendfile save. Instead:

- extend the confirmation prompt for unsaved changes to show unsaved
  catalogs.
- In the confirmation prompt, make catalog saving explicit & optional,
  just like we do it for external images. {F10881736}
- In the Asset Browser catalog tree, show an operator icon to save the
  catalogs to disk. It's grayed out if there are no changes to save, or
  if the .blend wasn't saved yet (required to know where to save the
  catalog definitions to). {F10881743}

Much of the work was done by @Severin and reviewed by me, then we
swapped roles.

Reviewed By: Severin

Differential Revision: https://developer.blender.org/D12796
This commit is contained in:
Sybren A. Stüvel 2021-10-19 18:07:22 +02:00
parent b6c3b41d41
commit 823996b034
Notes: blender-bot 2023-02-14 08:07:50 +01:00
Referenced by issue #92310, Improved workflow for asset catalog saving
21 changed files with 402 additions and 51 deletions

View File

@ -63,13 +63,21 @@ class AssetCatalogService {
AssetCatalogService();
explicit AssetCatalogService(const CatalogFilePath &asset_library_root);
/**
* Set global tag indicating that some catalog modifications are unsaved that could get lost
* on exit. This tag is not set by internal catalog code, the catalog service user is responsible
* for it. It is cleared by #write_to_disk().
*/
void tag_has_unsaved_changes();
bool has_unsaved_changes() const;
/** Load asset catalog definitions from the files found in the asset library. */
void load_from_disk();
/** Load asset catalog definitions from the given file or directory. */
void load_from_disk(const CatalogFilePath &file_or_directory_path);
/**
* Write the catalog definitions to disk in response to the blend file being saved.
* Write the catalog definitions to disk.
*
* The location where the catalogs are saved is variable, and depends on the location of the
* blend file. The first matching rule wins:
@ -85,7 +93,7 @@ class AssetCatalogService {
*
* Return true on success, which either means there were no in-memory categories to save,
* or the save was successful. */
bool write_to_disk_on_blendfile_save(const CatalogFilePath &blend_file_path);
bool write_to_disk(const CatalogFilePath &blend_file_path);
/**
* Merge on-disk changes into the in-memory asset catalogs.
@ -166,10 +174,15 @@ class AssetCatalogService {
Vector<std::unique_ptr<AssetCatalogCollection>> undo_snapshots_;
Vector<std::unique_ptr<AssetCatalogCollection>> redo_snapshots_;
bool has_unsaved_changes_ = false;
void load_directory_recursive(const CatalogFilePath &directory_path);
void load_single_file(const CatalogFilePath &catalog_definition_file_path);
/** Implementation of #write_to_disk() that doesn't clear the "has unsaved changes" tag. */
bool write_to_disk_ex(const CatalogFilePath &blend_file_path);
void untag_has_unsaved_changes();
std::unique_ptr<AssetCatalogDefinitionFile> parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path);

View File

@ -77,6 +77,9 @@ bool BKE_asset_library_find_suitable_root_path_from_main(
void BKE_asset_library_refresh_catalog_simplename(struct AssetLibrary *asset_library,
struct AssetMetaData *asset_data);
/** Return whether any loaded AssetLibrary has unsaved changes to its catalogs. */
bool BKE_asset_library_has_any_unsaved_catalogs(void);
#ifdef __cplusplus
}
#endif

View File

@ -38,6 +38,10 @@ namespace blender::bke {
* For now this is only for catalogs, later this can be expanded to indexes/caches/more.
*/
struct AssetLibrary {
/* Controlled by #ED_asset_catalogs_set_save_catalogs_when_file_is_saved,
* for managing the "Save Catalog Changes" in the quit-confirmation dialog box. */
static bool save_catalogs_when_file_is_saved;
std::unique_ptr<AssetCatalogService> catalog_service;
AssetLibrary();
@ -53,10 +57,10 @@ struct AssetLibrary {
* meant to help recover from. */
void refresh_catalog_simplename(struct AssetMetaData *asset_data);
void on_save_handler_register();
void on_save_handler_unregister();
void on_blend_save_handler_register();
void on_blend_save_handler_unregister();
void on_save_post(struct Main *, struct PointerRNA **pointers, const int num_pointers);
void on_blend_save_post(struct Main *, struct PointerRNA **pointers, const int num_pointers);
private:
bCallbackFuncStore on_save_callback_store_{};

View File

@ -66,6 +66,21 @@ AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_ro
{
}
void AssetCatalogService::tag_has_unsaved_changes()
{
has_unsaved_changes_ = true;
}
void AssetCatalogService::untag_has_unsaved_changes()
{
has_unsaved_changes_ = false;
}
bool AssetCatalogService::has_unsaved_changes() const
{
return has_unsaved_changes_;
}
bool AssetCatalogService::is_empty() const
{
return catalog_collection_->catalogs_.is_empty();
@ -344,7 +359,17 @@ void AssetCatalogService::merge_from_disk_before_writing()
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
}
bool AssetCatalogService::write_to_disk_on_blendfile_save(const CatalogFilePath &blend_file_path)
bool AssetCatalogService::write_to_disk(const CatalogFilePath &blend_file_path)
{
if (!write_to_disk_ex(blend_file_path)) {
return false;
}
untag_has_unsaved_changes();
return true;
}
bool AssetCatalogService::write_to_disk_ex(const CatalogFilePath &blend_file_path)
{
/* TODO(Sybren): expand to support multiple CDFs. */

View File

@ -222,7 +222,7 @@ class AssetCatalogTest : public testing::Test {
const AssetCatalog *cat = service.create_catalog("some/catalog/path");
/* Mock that the blend file is written to the directory already containing a CDF. */
ASSERT_TRUE(service.write_to_disk_on_blendfile_save(blendfilename));
ASSERT_TRUE(service.write_to_disk(blendfilename));
/* Test that the CDF still exists in the expected location. */
EXPECT_TRUE(BLI_exists(cdf_toplevel.c_str()));
@ -489,7 +489,7 @@ TEST_F(AssetCatalogTest, no_writing_empty_files)
{
const CatalogFilePath temp_lib_root = create_temp_path();
AssetCatalogService service(temp_lib_root);
service.write_to_disk_on_blendfile_save(temp_lib_root + "phony.blend");
service.write_to_disk(temp_lib_root + "phony.blend");
const CatalogFilePath default_cdf_path = temp_lib_root +
AssetCatalogService::DEFAULT_CATALOG_FILENAME;
@ -515,7 +515,7 @@ TEST_F(AssetCatalogTest, on_blendfile_save__with_existing_cdf)
const AssetCatalog *cat = service.create_catalog("some/catalog/path");
const CatalogFilePath blendfilename = top_level_dir + "subdir/some_file.blend";
ASSERT_TRUE(service.write_to_disk_on_blendfile_save(blendfilename));
ASSERT_TRUE(service.write_to_disk(blendfilename));
EXPECT_EQ(cdf_filename, service.get_catalog_definition_file()->file_path);
/* Test that the CDF was created in the expected location. */
@ -542,7 +542,7 @@ TEST_F(AssetCatalogTest, on_blendfile_save__from_memory_into_empty_directory)
const AssetCatalog *cat = service.create_catalog("some/catalog/path");
const CatalogFilePath blendfilename = target_dir + "some_file.blend";
ASSERT_TRUE(service.write_to_disk_on_blendfile_save(blendfilename));
ASSERT_TRUE(service.write_to_disk(blendfilename));
/* Test that the CDF was created in the expected location. */
const CatalogFilePath expected_cdf_path = target_dir +
@ -575,7 +575,7 @@ TEST_F(AssetCatalogTest, on_blendfile_save__from_memory_into_existing_cdf_and_me
/* Mock that the blend file is written to a subdirectory of the asset library. */
const CatalogFilePath blendfilename = target_dir + "some_file.blend";
ASSERT_TRUE(service.write_to_disk_on_blendfile_save(blendfilename));
ASSERT_TRUE(service.write_to_disk(blendfilename));
/* Test that the CDF still exists in the expected location. */
const CatalogFilePath backup_filename = writable_cdf_file + "~";
@ -630,7 +630,7 @@ TEST_F(AssetCatalogTest, create_first_catalog_from_scratch)
EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
/* Writing to disk should create the directory + the default file. */
service.write_to_disk_on_blendfile_save(temp_lib_root + "phony.blend");
service.write_to_disk(temp_lib_root + "phony.blend");
EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
const CatalogFilePath definition_file_path = temp_lib_root + "/" +
@ -681,7 +681,7 @@ TEST_F(AssetCatalogTest, create_catalog_after_loading_file)
<< "expecting newly added catalog to not yet be saved to " << temp_lib_root;
/* Write and reload the catalog file. */
service.write_to_disk_on_blendfile_save(temp_lib_root + "phony.blend");
service.write_to_disk(temp_lib_root + "phony.blend");
AssetCatalogService reloaded_service(temp_lib_root);
reloaded_service.load_from_disk();
EXPECT_NE(nullptr, reloaded_service.find_catalog(UUID_POSES_ELLIE))
@ -867,7 +867,7 @@ TEST_F(AssetCatalogTest, merge_catalog_files)
ASSERT_EQ(0, BLI_copy(modified_cdf_file.c_str(), temp_cdf_file.c_str()));
/* Overwrite the modified file. This should merge the on-disk file with our catalogs. */
service.write_to_disk_on_blendfile_save(cdf_dir + "phony.blend");
service.write_to_disk(cdf_dir + "phony.blend");
AssetCatalogService loaded_service(cdf_dir);
loaded_service.load_from_disk();
@ -897,7 +897,7 @@ TEST_F(AssetCatalogTest, backups)
AssetCatalogService service(cdf_dir);
service.load_from_disk();
service.delete_catalog_by_id(UUID_POSES_ELLIE);
service.write_to_disk_on_blendfile_save(cdf_dir + "phony.blend");
service.write_to_disk(cdf_dir + "phony.blend");
const CatalogFilePath backup_path = writable_cdf_file + "~";
ASSERT_TRUE(BLI_is_file(backup_path.c_str()));

View File

@ -35,6 +35,8 @@
#include <memory>
bool blender::bke::AssetLibrary::save_catalogs_when_file_is_saved = true;
/**
* Loading an asset library at this point only means loading the catalogs. Later on this should
* invoke reading of asset representations too.
@ -52,6 +54,12 @@ struct AssetLibrary *BKE_asset_library_load(const char *library_path)
return reinterpret_cast<struct AssetLibrary *>(lib);
}
bool BKE_asset_library_has_any_unsaved_catalogs()
{
blender::bke::AssetLibraryService *service = blender::bke::AssetLibraryService::get();
return service->has_any_unsaved_catalogs();
}
bool BKE_asset_library_find_suitable_root_path_from_path(const char *input_path,
char *r_library_path)
{
@ -109,7 +117,7 @@ AssetLibrary::AssetLibrary() : catalog_service(std::make_unique<AssetCatalogServ
AssetLibrary::~AssetLibrary()
{
if (on_save_callback_store_.func) {
on_save_handler_unregister();
on_blend_save_handler_unregister();
}
}
@ -127,11 +135,12 @@ void asset_library_on_save_post(struct Main *main,
void *arg)
{
AssetLibrary *asset_lib = static_cast<AssetLibrary *>(arg);
asset_lib->on_save_post(main, pointers, num_pointers);
asset_lib->on_blend_save_post(main, pointers, num_pointers);
}
} // namespace
void AssetLibrary::on_save_handler_register()
void AssetLibrary::on_blend_save_handler_register()
{
/* The callback system doesn't own `on_save_callback_store_`. */
on_save_callback_store_.alloc = false;
@ -142,22 +151,24 @@ void AssetLibrary::on_save_handler_register()
BKE_callback_add(&on_save_callback_store_, BKE_CB_EVT_SAVE_POST);
}
void AssetLibrary::on_save_handler_unregister()
void AssetLibrary::on_blend_save_handler_unregister()
{
BKE_callback_remove(&on_save_callback_store_, BKE_CB_EVT_SAVE_POST);
on_save_callback_store_.func = nullptr;
on_save_callback_store_.arg = nullptr;
}
void AssetLibrary::on_save_post(struct Main *main,
struct PointerRNA ** /*pointers*/,
const int /*num_pointers*/)
void AssetLibrary::on_blend_save_post(struct Main *main,
struct PointerRNA ** /*pointers*/,
const int /*num_pointers*/)
{
if (this->catalog_service == nullptr) {
return;
}
this->catalog_service->write_to_disk_on_blendfile_save(main->name);
if (save_catalogs_when_file_is_saved) {
this->catalog_service->write_to_disk(main->name);
}
}
void AssetLibrary::refresh_catalog_simplename(struct AssetMetaData *asset_data)
@ -166,15 +177,12 @@ void AssetLibrary::refresh_catalog_simplename(struct AssetMetaData *asset_data)
asset_data->catalog_simple_name[0] = '\0';
return;
}
const AssetCatalog *catalog = this->catalog_service->find_catalog(asset_data->catalog_id);
if (catalog == nullptr) {
/* No-op if the catalog cannot be found. This could be the kind of "the catalog definition file
* is corrupt/lost" scenario that the simple name is meant to help recover from. */
return;
}
STRNCPY(asset_data->catalog_simple_name, catalog->simple_name.c_str());
}
} // namespace blender::bke

View File

@ -83,7 +83,7 @@ AssetLibrary *AssetLibraryService::get_asset_library_on_disk(StringRefNull top_l
AssetLibraryPtr lib_uptr = std::make_unique<AssetLibrary>();
AssetLibrary *lib = lib_uptr.get();
lib->on_save_handler_register();
lib->on_blend_save_handler_register();
lib->load(top_dir_trailing_slash);
on_disk_libraries_.add_new(top_dir_trailing_slash, std::move(lib_uptr));
@ -99,7 +99,7 @@ AssetLibrary *AssetLibraryService::get_asset_library_current_file()
else {
CLOG_INFO(&LOG, 2, "get current file lib (loaded)");
current_file_library_ = std::make_unique<AssetLibrary>();
current_file_library_->on_save_handler_register();
current_file_library_->on_blend_save_handler_register();
}
AssetLibrary *lib = current_file_library_.get();
@ -148,4 +148,19 @@ void AssetLibraryService::app_handler_unregister()
on_load_callback_store_.arg = nullptr;
}
bool AssetLibraryService::has_any_unsaved_catalogs() const
{
if (current_file_library_ && current_file_library_->catalog_service->has_unsaved_changes()) {
return true;
}
for (const auto &asset_lib_uptr : on_disk_libraries_.values()) {
if (asset_lib_uptr->catalog_service->has_unsaved_changes()) {
return true;
}
}
return false;
}
} // namespace blender::bke

View File

@ -66,6 +66,9 @@ class AssetLibraryService {
/** Get the "Current File" asset library. */
AssetLibrary *get_asset_library_current_file();
/** Returns whether there are any known asset libraries with unsaved catalog edits. */
bool has_any_unsaved_catalogs() const;
protected:
static std::unique_ptr<AssetLibraryService> instance_;

View File

@ -22,6 +22,8 @@
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "BKE_appdir.h"
#include "CLG_log.h"
#include "testing/testing.h"
@ -31,6 +33,7 @@ namespace blender::bke::tests {
class AssetLibraryServiceTest : public testing::Test {
public:
CatalogFilePath asset_library_root_;
CatalogFilePath temp_library_path_;
static void SetUpTestSuite()
{
@ -48,11 +51,34 @@ class AssetLibraryServiceTest : public testing::Test {
FAIL();
}
asset_library_root_ = test_files_dir + "/" + "asset_library";
temp_library_path_ = "";
}
void TearDown() override
{
AssetLibraryService::destroy();
if (!temp_library_path_.empty()) {
BLI_delete(temp_library_path_.c_str(), true, true);
temp_library_path_ = "";
}
}
/* Register a temporary path, which will be removed at the end of the test.
* The returned path ends in a slash. */
CatalogFilePath use_temp_path()
{
BKE_tempdir_init("");
const CatalogFilePath tempdir = BKE_tempdir_session();
temp_library_path_ = tempdir + "test-temporary-path/";
return temp_library_path_;
}
CatalogFilePath create_temp_path()
{
CatalogFilePath path = use_temp_path();
BLI_dir_create_recursive(path.c_str());
return path;
}
};
@ -122,4 +148,52 @@ TEST_F(AssetLibraryServiceTest, catalogs_loaded)
<< "Catalogs should be loaded after getting an asset library from disk.";
}
TEST_F(AssetLibraryServiceTest, has_any_unsaved_catalogs)
{
AssetLibraryService *const service = AssetLibraryService::get();
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Empty AssetLibraryService should have no unsaved catalogs";
AssetLibrary *const lib = service->get_asset_library_on_disk(asset_library_root_);
AssetCatalogService *const cat_service = lib->catalog_service.get();
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Unchanged AssetLibrary should have no unsaved catalogs";
const bUUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
cat_service->prune_catalogs_by_id(UUID_POSES_ELLIE);
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Deletion of catalogs via AssetCatalogService should not tag as 'unsaved changes'.";
cat_service->tag_has_unsaved_changes();
EXPECT_TRUE(service->has_any_unsaved_catalogs())
<< "Tagging as having unsaved changes of a single catalog service should result in unsaved "
"changes being reported.";
}
TEST_F(AssetLibraryServiceTest, has_any_unsaved_catalogs_after_write)
{
const CatalogFilePath writable_dir = create_temp_path(); /* Has trailing slash. */
const CatalogFilePath original_cdf_file = asset_library_root_ + "/blender_assets.cats.txt";
CatalogFilePath writable_cdf_file = writable_dir + AssetCatalogService::DEFAULT_CATALOG_FILENAME;
BLI_path_slash_native(writable_cdf_file.data());
ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str()));
AssetLibraryService *const service = AssetLibraryService::get();
AssetLibrary *const lib = service->get_asset_library_on_disk(writable_dir);
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Unchanged AssetLibrary should have no unsaved catalogs";
AssetCatalogService *const cat_service = lib->catalog_service.get();
cat_service->tag_has_unsaved_changes();
EXPECT_TRUE(service->has_any_unsaved_catalogs())
<< "Tagging as having unsaved changes of a single catalog service should result in unsaved "
"changes being reported.";
cat_service->write_to_disk(writable_dir + "dummy_path.blend");
EXPECT_FALSE(service->has_any_unsaved_catalogs())
<< "Written AssetCatalogService should have no unsaved catalogs";
}
} // namespace blender::bke::tests

View File

@ -29,7 +29,7 @@
namespace blender::bke::tests {
class AssetLibraryServiceTest : public testing::Test {
class AssetLibraryTest : public testing::Test {
public:
static void SetUpTestSuite()
{
@ -46,7 +46,7 @@ class AssetLibraryServiceTest : public testing::Test {
}
};
TEST_F(AssetLibraryServiceTest, bke_asset_library_load)
TEST_F(AssetLibraryTest, bke_asset_library_load)
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {
@ -73,7 +73,7 @@ TEST_F(AssetLibraryServiceTest, bke_asset_library_load)
EXPECT_EQ("character/Ellie/poselib", poses_ellie->path.str());
}
TEST_F(AssetLibraryServiceTest, load_nonexistent_directory)
TEST_F(AssetLibraryTest, load_nonexistent_directory)
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {

View File

@ -41,6 +41,7 @@ set(SRC
intern/asset_ops.cc
intern/asset_temp_id_consumer.cc
ED_asset_catalog.h
ED_asset_catalog.hh
ED_asset_filter.h
ED_asset_handle.h

View File

@ -0,0 +1,39 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup edasset
*/
#pragma once
#include "BLI_utildefines.h"
#ifdef __cplusplus
extern "C" {
#endif
struct Main;
struct AssetLibrary;
void ED_asset_catalogs_save_from_main_path(struct AssetLibrary *library, const struct Main *bmain);
void ED_asset_catalogs_set_save_catalogs_when_file_is_saved(bool should_save);
bool ED_asset_catalogs_get_save_catalogs_when_file_is_saved(void);
#ifdef __cplusplus
}
#endif

View File

@ -33,3 +33,7 @@ blender::bke::AssetCatalog *ED_asset_catalog_add(AssetLibrary *library,
blender::StringRefNull name,
blender::StringRef parent_path = nullptr);
void ED_asset_catalog_remove(AssetLibrary *library, const blender::bke::CatalogID &catalog_id);
void ED_asset_catalog_rename(AssetLibrary *library,
blender::bke::CatalogID catalog_id,
blender::StringRefNull new_name);

View File

@ -21,11 +21,15 @@
#include "BKE_asset_catalog.hh"
#include "BKE_asset_catalog_path.hh"
#include "BKE_asset_library.hh"
#include "BKE_main.h"
#include "BLI_string_utils.h"
#include "ED_asset_catalog.h"
#include "ED_asset_catalog.hh"
#include "WM_api.h"
using namespace blender;
using namespace blender::bke;
@ -67,7 +71,13 @@ AssetCatalog *ED_asset_catalog_add(::AssetLibrary *library,
AssetCatalogPath fullpath = AssetCatalogPath(parent_path) / unique_name;
catalog_service->undo_push();
return catalog_service->create_catalog(fullpath);
catalog_service->tag_has_unsaved_changes();
bke::AssetCatalog *new_catalog = catalog_service->create_catalog(fullpath);
if (!new_catalog) {
return nullptr;
}
return new_catalog;
}
void ED_asset_catalog_remove(::AssetLibrary *library, const CatalogID &catalog_id)
@ -79,5 +89,47 @@ void ED_asset_catalog_remove(::AssetLibrary *library, const CatalogID &catalog_i
}
catalog_service->undo_push();
catalog_service->tag_has_unsaved_changes();
catalog_service->prune_catalogs_by_id(catalog_id);
}
void ED_asset_catalog_rename(::AssetLibrary *library,
const CatalogID catalog_id,
const StringRefNull new_name)
{
bke::AssetCatalogService *catalog_service = BKE_asset_library_get_catalog_service(library);
if (!catalog_service) {
BLI_assert_unreachable();
return;
}
const AssetCatalog *catalog = catalog_service->find_catalog(catalog_id);
AssetCatalogPath new_path = catalog->path.parent();
new_path = new_path / StringRef(new_name);
catalog_service->undo_push();
catalog_service->tag_has_unsaved_changes();
catalog_service->update_catalog_path(catalog_id, new_path);
}
void ED_asset_catalogs_save_from_main_path(::AssetLibrary *library, const Main *bmain)
{
bke::AssetCatalogService *catalog_service = BKE_asset_library_get_catalog_service(library);
if (!catalog_service) {
BLI_assert_unreachable();
return;
}
catalog_service->write_to_disk(bmain->name);
}
void ED_asset_catalogs_set_save_catalogs_when_file_is_saved(const bool should_save)
{
bke::AssetLibrary::save_catalogs_when_file_is_saved = should_save;
}
bool ED_asset_catalogs_get_save_catalogs_when_file_is_saved()
{
return bke::AssetLibrary::save_catalogs_when_file_is_saved;
}

View File

@ -22,6 +22,7 @@
#include "BKE_asset_library.hh"
#include "BKE_context.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
#include "BKE_report.h"
#include "BLI_string_ref.hh"
@ -398,7 +399,8 @@ static int asset_catalog_new_exec(bContext *C, wmOperator *op)
MEM_freeN(parent_path);
WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, nullptr);
WM_event_add_notifier_ex(
CTX_wm_manager(C), CTX_wm_window(C), NC_ASSET | ND_ASSET_CATALOGS, nullptr);
return OPERATOR_FINISHED;
}
@ -436,7 +438,8 @@ static int asset_catalog_delete_exec(bContext *C, wmOperator *op)
MEM_freeN(catalog_id_str);
WM_main_add_notifier(NC_SPACE | ND_SPACE_ASSET_PARAMS, nullptr);
WM_event_add_notifier_ex(
CTX_wm_manager(C), CTX_wm_window(C), NC_ASSET | ND_ASSET_CATALOGS, nullptr);
return OPERATOR_FINISHED;
}
@ -561,6 +564,55 @@ static void ASSET_OT_catalog_undo_push(struct wmOperatorType *ot)
/* -------------------------------------------------------------------- */
static bool asset_catalogs_save_poll(bContext *C)
{
if (!asset_catalog_operator_poll(C)) {
return false;
}
const Main *bmain = CTX_data_main(C);
if (!bmain->name[0]) {
CTX_wm_operator_poll_msg_set(C, "Cannot save asset catalogs before the Blender file is saved");
return false;
}
if (!BKE_asset_library_has_any_unsaved_catalogs()) {
CTX_wm_operator_poll_msg_set(C, "No changes to be saved");
return false;
}
return true;
}
static int asset_catalogs_save_exec(bContext *C, wmOperator * /*op*/)
{
const SpaceFile *sfile = CTX_wm_space_file(C);
::AssetLibrary *asset_library = ED_fileselect_active_asset_library_get(sfile);
ED_asset_catalogs_save_from_main_path(asset_library, CTX_data_main(C));
WM_event_add_notifier_ex(
CTX_wm_manager(C), CTX_wm_window(C), NC_ASSET | ND_ASSET_CATALOGS, nullptr);
return OPERATOR_FINISHED;
}
static void ASSET_OT_catalogs_save(struct wmOperatorType *ot)
{
/* identifiers */
ot->name = "Save Asset Catalogs";
ot->description =
"Make any edits to any catalogs permanent by writing the current set up to the asset "
"library";
ot->idname = "ASSET_OT_catalogs_save";
/* api callbacks */
ot->exec = asset_catalogs_save_exec;
ot->poll = asset_catalogs_save_poll;
}
/* -------------------------------------------------------------------- */
void ED_operatortypes_asset(void)
{
WM_operatortype_append(ASSET_OT_mark);
@ -568,6 +620,7 @@ void ED_operatortypes_asset(void)
WM_operatortype_append(ASSET_OT_catalog_new);
WM_operatortype_append(ASSET_OT_catalog_delete);
WM_operatortype_append(ASSET_OT_catalogs_save);
WM_operatortype_append(ASSET_OT_catalog_undo);
WM_operatortype_append(ASSET_OT_catalog_redo);
WM_operatortype_append(ASSET_OT_catalog_undo_push);

View File

@ -36,6 +36,7 @@ void ED_operatortypes_asset(void);
}
#endif
#include "../asset/ED_asset_catalog.h"
#include "../asset/ED_asset_filter.h"
#include "../asset/ED_asset_handle.h"
#include "../asset/ED_asset_library.h"
@ -45,5 +46,6 @@ void ED_operatortypes_asset(void);
/* C++ only headers. */
#ifdef __cplusplus
# include "../asset/ED_asset_catalog.hh"
# include "../asset/ED_asset_list.hh"
#endif

View File

@ -21,8 +21,6 @@
* \ingroup spfile
*/
#include "ED_fileselect.h"
#include "DNA_space_types.h"
#include "BKE_asset.h"
@ -33,6 +31,9 @@
#include "BLT_translation.h"
#include "ED_asset.h"
#include "ED_fileselect.h"
#include "RNA_access.h"
#include "UI_interface.h"
@ -52,7 +53,7 @@ using namespace blender::bke;
namespace blender::ed::asset_browser {
class AssetCatalogTreeView : public ui::AbstractTreeView {
bke::AssetCatalogService *catalog_service_;
::AssetLibrary *asset_library_;
/** The asset catalog tree this tree-view represents. */
bke::AssetCatalogTree *catalog_tree_;
FileAssetSelectParams *params_;
@ -129,7 +130,7 @@ class AssetCatalogTreeViewUnassignedItem : public ui::BasicTreeViewItem {
AssetCatalogTreeView::AssetCatalogTreeView(::AssetLibrary *library,
FileAssetSelectParams *params,
SpaceFile &space_file)
: catalog_service_(BKE_asset_library_get_catalog_service(library)),
: asset_library_(library),
catalog_tree_(BKE_asset_library_get_catalog_tree(library)),
params_(params),
space_file_(space_file)
@ -353,12 +354,7 @@ bool AssetCatalogTreeViewItem::rename(StringRefNull new_name)
const AssetCatalogTreeView &tree_view = static_cast<const AssetCatalogTreeView &>(
get_tree_view());
AssetCatalogPath new_path = catalog_item_.catalog_path().parent();
new_path = new_path / StringRef(new_name);
tree_view.catalog_service_->undo_push();
tree_view.catalog_service_->update_catalog_path(catalog_item_.get_catalog_id(), new_path);
ED_asset_catalog_rename(tree_view.asset_library_, catalog_item_.get_catalog_id(), new_name);
return true;
}
@ -369,6 +365,10 @@ void AssetCatalogTreeViewAllItem::build_row(uiLayout &row)
ui::BasicTreeViewItem::build_row(row);
PointerRNA *props;
UI_but_extra_operator_icon_add(
(uiBut *)tree_row_button(), "ASSET_OT_catalogs_save", WM_OP_INVOKE_DEFAULT, ICON_FILE_TICK);
props = UI_but_extra_operator_icon_add(
(uiBut *)tree_row_button(), "ASSET_OT_catalog_new", WM_OP_INVOKE_DEFAULT, ICON_ADD);
/* No parent path to use the root level. */

View File

@ -462,6 +462,10 @@ typedef struct wmNotifier {
#define ND_ASSET_LIST (1 << 16)
#define ND_ASSET_LIST_PREVIEW (2 << 16)
#define ND_ASSET_LIST_READING (3 << 16)
/* Catalog data changed, requiring a redraw of catalog UIs. Note that this doesn't denote a
* reloading of asset libraries & their catalogs should happen. That only happens on explicit user
* action. */
#define ND_ASSET_CATALOGS (4 << 16)
/* subtype, 256 entries too */
#define NOTE_SUBTYPE 0x0000FF00

View File

@ -75,6 +75,7 @@
#include "BKE_addon.h"
#include "BKE_appdir.h"
#include "BKE_asset_library.h"
#include "BKE_autoexec.h"
#include "BKE_blender.h"
#include "BKE_blendfile.h"
@ -105,6 +106,7 @@
#include "IMB_imbuf_types.h"
#include "IMB_thumbs.h"
#include "ED_asset.h"
#include "ED_datafiles.h"
#include "ED_fileselect.h"
#include "ED_image.h"
@ -168,9 +170,13 @@ void WM_file_tag_modified(void)
}
}
bool wm_file_or_image_is_modified(const Main *bmain, const wmWindowManager *wm)
/**
* Check if there is data that would be lost when closing the current file without saving.
*/
bool wm_file_or_session_data_has_unsaved_changes(const Main *bmain, const wmWindowManager *wm)
{
return !wm->file_saved || ED_image_should_save_modified(bmain);
return !wm->file_saved || ED_image_should_save_modified(bmain) ||
BKE_asset_library_has_any_unsaved_catalogs();
}
/** \} */
@ -3598,6 +3604,14 @@ static void wm_block_file_close_save_button(uiBlock *block, wmGenericCallback *p
static const char *close_file_dialog_name = "file_close_popup";
static void save_catalogs_when_file_is_closed_set_fn(bContext *UNUSED(C),
void *arg1,
void *UNUSED(arg2))
{
char *save_catalogs_when_file_is_closed = arg1;
ED_asset_catalogs_set_save_catalogs_when_file_is_saved(*save_catalogs_when_file_is_closed != 0);
}
static uiBlock *block_create__close_file_dialog(struct bContext *C,
struct ARegion *region,
void *arg1)
@ -3654,11 +3668,17 @@ static uiBlock *block_create__close_file_dialog(struct bContext *C,
MEM_freeN(message);
}
/* Used to determine if extra separators are needed. */
bool has_extra_checkboxes = false;
/* Modified Images Checkbox. */
if (modified_images_count > 0) {
char message[64];
BLI_snprintf(message, sizeof(message), "Save %u modified image(s)", modified_images_count);
uiItemS(layout);
/* Only the first checkbox should get extra separation. */
if (!has_extra_checkboxes) {
uiItemS(layout);
}
uiDefButBitC(block,
UI_BTYPE_CHECKBOX,
1,
@ -3674,11 +3694,41 @@ static uiBlock *block_create__close_file_dialog(struct bContext *C,
0,
0,
"");
has_extra_checkboxes = true;
}
if (BKE_asset_library_has_any_unsaved_catalogs()) {
static char save_catalogs_when_file_is_closed;
save_catalogs_when_file_is_closed = ED_asset_catalogs_get_save_catalogs_when_file_is_saved();
/* Only the first checkbox should get extra separation. */
if (!has_extra_checkboxes) {
uiItemS(layout);
}
uiBut *but = uiDefButBitC(block,
UI_BTYPE_CHECKBOX,
1,
0,
"Save modified asset catalogs",
0,
0,
0,
UI_UNIT_Y,
&save_catalogs_when_file_is_closed,
0,
0,
0,
0,
"");
UI_but_func_set(
but, save_catalogs_when_file_is_closed_set_fn, &save_catalogs_when_file_is_closed, NULL);
has_extra_checkboxes = true;
}
BKE_reports_clear(&reports);
uiItemS_ex(layout, modified_images_count > 0 ? 2.0f : 4.0f);
uiItemS_ex(layout, has_extra_checkboxes ? 2.0f : 4.0f);
/* Buttons. */
#ifdef _WIN32
@ -3759,7 +3809,7 @@ bool wm_operator_close_file_dialog_if_needed(bContext *C,
wmGenericCallbackFn post_action_fn)
{
if (U.uiflag & USER_SAVE_PROMPT &&
wm_file_or_image_is_modified(CTX_data_main(C), CTX_wm_manager(C))) {
wm_file_or_session_data_has_unsaved_changes(CTX_data_main(C), CTX_wm_manager(C))) {
wmGenericCallback *callback = MEM_callocN(sizeof(*callback), __func__);
callback->exec = post_action_fn;
callback->user_data = IDP_CopyProperty(op->properties);

View File

@ -368,7 +368,8 @@ void wm_quit_with_optional_confirmation_prompt(bContext *C, wmWindow *win)
CTX_wm_window_set(C, win);
if (U.uiflag & USER_SAVE_PROMPT) {
if (wm_file_or_image_is_modified(CTX_data_main(C), CTX_wm_manager(C)) && !G.background) {
if (wm_file_or_session_data_has_unsaved_changes(CTX_data_main(C), CTX_wm_manager(C)) &&
!G.background) {
wm_window_raise(win);
wm_confirm_quit(C);
}

View File

@ -79,7 +79,7 @@ void wm_close_file_dialog(bContext *C, struct wmGenericCallback *post_action);
bool wm_operator_close_file_dialog_if_needed(bContext *C,
wmOperator *op,
wmGenericCallbackFn exec_fn);
bool wm_file_or_image_is_modified(const Main *bmain, const wmWindowManager *wm);
bool wm_file_or_session_data_has_unsaved_changes(const Main *bmain, const wmWindowManager *wm);
void WM_OT_save_homefile(struct wmOperatorType *ot);
void WM_OT_save_userpref(struct wmOperatorType *ot);