Asset Catalogs: undo stack for catalog edits

Add an undo stack for catalog edits. This only implements the backend,
no operators or UI yet.

A bunch of `this->xxx` has been replaced by `catalog_collection_->xxx`.
Things are getting a bit long, and the class is turning into a god
object; refactoring the class is tracked in T92114.

Reviewed By: Severin

Maniphest Tasks: T92047

Differential Revision: https://developer.blender.org/D12825
This commit is contained in:
Sybren A. Stüvel 2021-10-12 12:39:24 +02:00
parent b67a937394
commit a06435e43a
Notes: blender-bot 2023-02-13 17:28:49 +01:00
Referenced by issue #92047, Undo system for catalog edits
3 changed files with 390 additions and 42 deletions

View File

@ -60,7 +60,7 @@ class AssetCatalogService {
static const CatalogFilePath DEFAULT_CATALOG_FILENAME;
public:
AssetCatalogService() = default;
AssetCatalogService();
explicit AssetCatalogService(const CatalogFilePath &asset_library_root);
/** Load asset catalog definitions from the files found in the asset library. */
@ -143,14 +143,30 @@ class AssetCatalogService {
/** Return true only if there are no catalogs known. */
bool is_empty() const;
/**
* Store the current catalogs in the undo stack.
* This snapshots everything in the #AssetCatalogCollection. */
void store_undo_snapshot();
/**
* Restore the last-saved undo snapshot, pushing the current state onto the redo stack.
* The caller is responsible for first checking that undoing is possible.
*/
void undo();
bool is_undo_possbile() const;
/**
* Restore the last-saved redo snapshot, pushing the current state onto the undo stack.
* The caller is responsible for first checking that undoing is possible. */
void redo();
bool is_redo_possbile() const;
protected:
/* These pointers are owned by this AssetCatalogService. */
OwningAssetCatalogMap catalogs_;
OwningAssetCatalogMap deleted_catalogs_;
std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;
std::unique_ptr<AssetCatalogCollection> catalog_collection_;
std::unique_ptr<AssetCatalogTree> catalog_tree_ = std::make_unique<AssetCatalogTree>();
CatalogFilePath asset_library_root_;
Vector<std::unique_ptr<AssetCatalogCollection>> undo_snapshots_;
Vector<std::unique_ptr<AssetCatalogCollection>> redo_snapshots_;
void load_directory_recursive(const CatalogFilePath &directory_path);
void load_single_file(const CatalogFilePath &catalog_definition_file_path);
@ -179,6 +195,41 @@ class AssetCatalogService {
* For every catalog, ensure that its parent path also has a known catalog.
*/
void create_missing_catalogs();
/* For access by subclasses, as those will not be marked as friend by #AssetCatalogCollection. */
AssetCatalogDefinitionFile *get_catalog_definition_file();
OwningAssetCatalogMap &get_catalogs();
};
/**
* All catalogs that are owned by a single asset library, and managed by a single instance of
* #AssetCatalogService. The undo system for asset catalog edits contains historical copies of this
* struct.
*/
class AssetCatalogCollection {
friend AssetCatalogService;
public:
AssetCatalogCollection() = default;
AssetCatalogCollection(const AssetCatalogCollection &other) = delete;
AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default;
std::unique_ptr<AssetCatalogCollection> deep_copy() const;
protected:
/** All catalogs known, except the known-but-deleted ones. */
OwningAssetCatalogMap catalogs_;
/** Catalogs that have been deleted. They are kept around so that the load-merge-save of catalog
* definition files can actually delete them if they already existed on disk (instead of the
* merge operation resurrecting them). */
OwningAssetCatalogMap deleted_catalogs_;
/* For now only a single catalog definition file is supported.
* The aim is to support an arbitrary number of such files per asset library in the future. */
std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;
static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig);
};
/**
@ -292,6 +343,9 @@ class AssetCatalogDefinitionFile {
void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path,
AssetCatalogParsedFn callback);
std::unique_ptr<AssetCatalogDefinitionFile> copy_and_remap(
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const;
protected:
/* Catalogs stored in this file. They are mapped by ID to make it possible to query whether a
* catalog is already known, without having to find the corresponding `AssetCatalog*`. */

View File

@ -55,19 +55,36 @@ const std::string AssetCatalogDefinitionFile::HEADER =
"# The first non-ignored line should be the version indicator.\n"
"# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n";
AssetCatalogService::AssetCatalogService()
: catalog_collection_(std::make_unique<AssetCatalogCollection>())
{
}
AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_root)
: asset_library_root_(asset_library_root)
: catalog_collection_(std::make_unique<AssetCatalogCollection>()),
asset_library_root_(asset_library_root)
{
}
bool AssetCatalogService::is_empty() const
{
return catalogs_.is_empty();
return catalog_collection_->catalogs_.is_empty();
}
OwningAssetCatalogMap &AssetCatalogService::get_catalogs()
{
return catalog_collection_->catalogs_;
}
AssetCatalogDefinitionFile *AssetCatalogService::get_catalog_definition_file()
{
return catalog_collection_->catalog_definition_file_.get();
}
AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id) const
{
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
if (catalog_uptr_ptr == nullptr) {
return nullptr;
}
@ -76,7 +93,7 @@ AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id) const
AssetCatalog *AssetCatalogService::find_catalog_by_path(const AssetCatalogPath &path) const
{
for (const auto &catalog : catalogs_.values()) {
for (const auto &catalog : catalog_collection_->catalogs_.values()) {
if (catalog->path == path) {
return catalog.get();
}
@ -103,7 +120,7 @@ AssetCatalogFilter AssetCatalogService::create_catalog_filter(
* then only do an exact match on the path (instead of the more complex `is_contained_in()`
* call). Without an extra indexed-by-path acceleration structure, this is still going to require
* a linear search, though. */
for (const auto &catalog_uptr : this->catalogs_.values()) {
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
if (catalog_uptr->path.is_contained_in(active_catalog->path)) {
matching_catalog_ids.add(catalog_uptr->catalog_id);
}
@ -114,7 +131,8 @@ AssetCatalogFilter AssetCatalogService::create_catalog_filter(
void AssetCatalogService::delete_catalog_by_id(const CatalogID catalog_id)
{
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = catalog_collection_->catalogs_.lookup_ptr(
catalog_id);
if (catalog_uptr_ptr == nullptr) {
/* Catalog cannot be found, which is fine. */
return;
@ -124,18 +142,19 @@ void AssetCatalogService::delete_catalog_by_id(const CatalogID catalog_id)
AssetCatalog *catalog = catalog_uptr_ptr->get();
catalog->flags.is_deleted = true;
/* Move ownership from this->catalogs_ to this->deleted_catalogs_. */
this->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
/* Move ownership from catalog_collection_->catalogs_ to catalog_collection_->deleted_catalogs_.
*/
catalog_collection_->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
/* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
this->catalogs_.remove(catalog_id);
catalog_collection_->catalogs_.remove(catalog_id);
}
void AssetCatalogService::prune_catalogs_by_path(const AssetCatalogPath &path)
{
/* Build a collection of catalog IDs to delete. */
Set<CatalogID> catalogs_to_delete;
for (const auto &catalog_uptr : this->catalogs_.values()) {
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
const AssetCatalog *cat = catalog_uptr.get();
if (cat->path.is_contained_in(path)) {
catalogs_to_delete.add(cat->catalog_id);
@ -166,7 +185,7 @@ void AssetCatalogService::update_catalog_path(const CatalogID catalog_id,
AssetCatalog *renamed_cat = this->find_catalog(catalog_id);
const AssetCatalogPath old_cat_path = renamed_cat->path;
for (auto &catalog_uptr : catalogs_.values()) {
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
AssetCatalog *cat = catalog_uptr.get();
const AssetCatalogPath new_path = cat->path.rebase(old_cat_path, new_catalog_path);
@ -189,13 +208,14 @@ AssetCatalog *AssetCatalogService::create_catalog(const AssetCatalogPath &catalo
/* TODO(@sybren): move the `AssetCatalog::from_path()` function to another place, that can reuse
* catalogs when a catalog with the given path is already known, and avoid duplicate catalog IDs.
*/
BLI_assert_msg(!catalogs_.contains(catalog->catalog_id), "duplicate catalog ID not supported");
catalogs_.add_new(catalog->catalog_id, std::move(catalog));
BLI_assert_msg(!catalog_collection_->catalogs_.contains(catalog->catalog_id),
"duplicate catalog ID not supported");
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
if (catalog_definition_file_) {
if (catalog_collection_->catalog_definition_file_) {
/* Ensure the new catalog gets written to disk at some point. If there is no CDF in memory yet,
* it's enough to have the catalog known to the service as it'll be saved to a new file. */
catalog_definition_file_->add_new(catalog_ptr);
catalog_collection_->catalog_definition_file_->add_new(catalog_ptr);
}
BLI_assert_msg(catalog_tree_, "An Asset Catalog tree should always exist.");
@ -263,9 +283,9 @@ void AssetCatalogService::load_single_file(const CatalogFilePath &catalog_defini
std::unique_ptr<AssetCatalogDefinitionFile> cdf = parse_catalog_file(
catalog_definition_file_path);
BLI_assert_msg(!this->catalog_definition_file_,
BLI_assert_msg(!catalog_collection_->catalog_definition_file_,
"Only loading of a single catalog definition file is supported.");
this->catalog_definition_file_ = std::move(cdf);
catalog_collection_->catalog_definition_file_ = std::move(cdf);
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_file(
@ -276,7 +296,7 @@ std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_f
auto catalog_parsed_callback = [this, catalog_definition_file_path](
std::unique_ptr<AssetCatalog> catalog) {
if (this->catalogs_.contains(catalog->catalog_id)) {
if (catalog_collection_->catalogs_.contains(catalog->catalog_id)) {
// TODO(@sybren): apparently another CDF was already loaded. This is not supported yet.
std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
<< catalog->catalog_id << " in multiple files, ignoring this one." << std::endl;
@ -285,7 +305,7 @@ std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_f
}
/* The AssetCatalog pointer is now owned by the AssetCatalogService. */
this->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
return true;
};
@ -297,9 +317,8 @@ std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_f
void AssetCatalogService::merge_from_disk_before_writing()
{
/* TODO(Sybren): expand to support multiple CDFs. */
if (!catalog_definition_file_ || catalog_definition_file_->file_path.empty() ||
!BLI_is_file(catalog_definition_file_->file_path.c_str())) {
AssetCatalogDefinitionFile *const cdf = catalog_collection_->catalog_definition_file_.get();
if (!cdf || cdf->file_path.empty() || !BLI_is_file(cdf->file_path.c_str())) {
return;
}
@ -308,22 +327,21 @@ void AssetCatalogService::merge_from_disk_before_writing()
/* The following two conditions could be or'ed together. Keeping them separated helps when
* adding debug prints, breakpoints, etc. */
if (this->catalogs_.contains(catalog_id)) {
if (catalog_collection_->catalogs_.contains(catalog_id)) {
/* This catalog was already seen, so just ignore it. */
return false;
}
if (this->deleted_catalogs_.contains(catalog_id)) {
if (catalog_collection_->deleted_catalogs_.contains(catalog_id)) {
/* This catalog was already seen and subsequently deleted, so just ignore it. */
return false;
}
/* This is a new catalog, so let's keep it around. */
this->catalogs_.add_new(catalog_id, std::move(catalog));
catalog_collection_->catalogs_.add_new(catalog_id, std::move(catalog));
return true;
};
catalog_definition_file_->parse_catalog_file(catalog_definition_file_->file_path,
catalog_parsed_callback);
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
}
bool AssetCatalogService::write_to_disk_on_blendfile_save(const CatalogFilePath &blend_file_path)
@ -331,20 +349,21 @@ bool AssetCatalogService::write_to_disk_on_blendfile_save(const CatalogFilePath
/* TODO(Sybren): expand to support multiple CDFs. */
/* - Already loaded a CDF from disk? -> Always write to that file. */
if (this->catalog_definition_file_) {
if (catalog_collection_->catalog_definition_file_) {
merge_from_disk_before_writing();
return catalog_definition_file_->write_to_disk();
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
if (catalogs_.is_empty() && deleted_catalogs_.is_empty()) {
if (catalog_collection_->catalogs_.is_empty() &&
catalog_collection_->deleted_catalogs_.is_empty()) {
/* Avoid saving anything, when there is nothing to save. */
return true; /* Writing nothing when there is nothing to write is still a success. */
}
const CatalogFilePath cdf_path_to_write = find_suitable_cdf_path_for_writing(blend_file_path);
this->catalog_definition_file_ = construct_cdf_in_memory(cdf_path_to_write);
catalog_collection_->catalog_definition_file_ = construct_cdf_in_memory(cdf_path_to_write);
merge_from_disk_before_writing();
return catalog_definition_file_->write_to_disk();
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
CatalogFilePath AssetCatalogService::find_suitable_cdf_path_for_writing(
@ -382,7 +401,7 @@ std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_i
auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
cdf->file_path = file_path;
for (auto &catalog : catalogs_.values()) {
for (auto &catalog : catalog_collection_->catalogs_.values()) {
cdf->add_new(catalog.get());
}
@ -399,7 +418,7 @@ std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree()
auto tree = std::make_unique<AssetCatalogTree>();
/* Go through the catalogs, insert each path component into the tree where needed. */
for (auto &catalog : catalogs_.values()) {
for (auto &catalog : catalog_collection_->catalogs_.values()) {
tree->insert_item(*catalog);
}
@ -416,7 +435,7 @@ void AssetCatalogService::create_missing_catalogs()
{
/* Construct an ordered set of paths to check, so that parents are ordered before children. */
std::set<AssetCatalogPath> paths_to_check;
for (auto &catalog : catalogs_.values()) {
for (auto &catalog : catalog_collection_->catalogs_.values()) {
paths_to_check.insert(catalog->path);
}
@ -450,6 +469,68 @@ void AssetCatalogService::create_missing_catalogs()
/* TODO(Sybren): bind the newly created catalogs to a CDF, if we know about it. */
}
bool AssetCatalogService::is_undo_possbile() const
{
return !undo_snapshots_.is_empty();
}
bool AssetCatalogService::is_redo_possbile() const
{
return !redo_snapshots_.is_empty();
}
void AssetCatalogService::undo()
{
BLI_assert_msg(is_undo_possbile(), "Undo stack is empty");
redo_snapshots_.append(std::move(catalog_collection_));
catalog_collection_ = std::move(undo_snapshots_.pop_last());
}
void AssetCatalogService::redo()
{
BLI_assert_msg(is_redo_possbile(), "Redo stack is empty");
undo_snapshots_.append(std::move(catalog_collection_));
catalog_collection_ = std::move(redo_snapshots_.pop_last());
}
void AssetCatalogService::store_undo_snapshot()
{
std::unique_ptr<AssetCatalogCollection> snapshot = catalog_collection_->deep_copy();
undo_snapshots_.append(std::move(snapshot));
redo_snapshots_.clear();
}
/* ---------------------------------------------------------------------- */
std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
{
auto copy = std::make_unique<AssetCatalogCollection>();
copy->catalogs_ = std::move(copy_catalog_map(this->catalogs_));
copy->deleted_catalogs_ = std::move(copy_catalog_map(this->deleted_catalogs_));
if (catalog_definition_file_) {
copy->catalog_definition_file_ = std::move(
catalog_definition_file_->copy_and_remap(copy->catalogs_, copy->deleted_catalogs_));
}
return copy;
}
OwningAssetCatalogMap AssetCatalogCollection::copy_catalog_map(const OwningAssetCatalogMap &orig)
{
OwningAssetCatalogMap copy;
for (const auto &orig_catalog_uptr : orig.values()) {
auto copy_catalog_uptr = std::make_unique<AssetCatalog>(*orig_catalog_uptr);
copy.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr));
}
return copy;
}
/* ---------------------------------------------------------------------- */
AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name,
@ -564,6 +645,8 @@ void AssetCatalogTree::foreach_root_item(const ItemIterFn callback)
/* ---------------------------------------------------------------------- */
/* ---------------------------------------------------------------------- */
bool AssetCatalogDefinitionFile::contains(const CatalogID catalog_id) const
{
return catalogs_.contains(catalog_id);
@ -782,6 +865,34 @@ bool AssetCatalogDefinitionFile::ensure_directory_exists(
return true;
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogDefinitionFile::copy_and_remap(
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const
{
auto copy = std::make_unique<AssetCatalogDefinitionFile>(*this);
copy->catalogs_.clear();
/* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */
for (CatalogID catalog_id : catalogs_.keys()) {
/* The catalog can be in the regular or the deleted map. */
const std::unique_ptr<AssetCatalog> *remapped_catalog_uptr_ptr = catalogs.lookup_ptr(
catalog_id);
if (remapped_catalog_uptr_ptr) {
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
continue;
}
remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id);
if (remapped_catalog_uptr_ptr) {
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
continue;
}
BLI_assert(!"A CDF should only reference known catalogs.");
}
return copy;
}
AssetCatalog::AssetCatalog(const CatalogID catalog_id,
const AssetCatalogPath &path,
const std::string &simple_name)

View File

@ -55,7 +55,7 @@ class TestableAssetCatalogService : public AssetCatalogService {
AssetCatalogDefinitionFile *get_catalog_definition_file()
{
return catalog_definition_file_.get();
return AssetCatalogService::get_catalog_definition_file();
}
void create_missing_catalogs()
@ -66,7 +66,7 @@ class TestableAssetCatalogService : public AssetCatalogService {
int64_t count_catalogs_with_path(const CatalogFilePath &path)
{
int64_t count = 0;
for (auto &catalog_uptr : catalogs_.values()) {
for (auto &catalog_uptr : get_catalogs().values()) {
if (catalog_uptr->path == path) {
count++;
}
@ -1054,4 +1054,187 @@ TEST_F(AssetCatalogTest, create_catalog_filter_for_unassigned_assets)
EXPECT_FALSE(filter.contains(UUID_POSES_ELLIE));
}
TEST_F(AssetCatalogTest, cat_collection_deep_copy__empty)
{
const AssetCatalogCollection empty;
auto copy = empty.deep_copy();
EXPECT_NE(&empty, copy.get());
}
class TestableAssetCatalogCollection : public AssetCatalogCollection {
public:
OwningAssetCatalogMap &get_catalogs()
{
return catalogs_;
}
OwningAssetCatalogMap &get_deleted_catalogs()
{
return deleted_catalogs_;
}
AssetCatalogDefinitionFile *get_catalog_definition_file()
{
return catalog_definition_file_.get();
}
AssetCatalogDefinitionFile *allocate_catalog_definition_file()
{
catalog_definition_file_ = std::make_unique<AssetCatalogDefinitionFile>();
return get_catalog_definition_file();
}
};
TEST_F(AssetCatalogTest, cat_collection_deep_copy__nonempty_nocdf)
{
TestableAssetCatalogCollection catcoll;
auto cat1 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA, "poses/Henrik", "");
auto cat2 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_FACE, "poses/Henrik/face", "");
auto cat3 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_HAND, "poses/Henrik/hands", "");
cat3->flags.is_deleted = true;
AssetCatalog *cat1_ptr = cat1.get();
AssetCatalog *cat3_ptr = cat3.get();
catcoll.get_catalogs().add_new(cat1->catalog_id, std::move(cat1));
catcoll.get_catalogs().add_new(cat2->catalog_id, std::move(cat2));
catcoll.get_deleted_catalogs().add_new(cat3->catalog_id, std::move(cat3));
auto copy = catcoll.deep_copy();
EXPECT_NE(&catcoll, copy.get());
TestableAssetCatalogCollection *testcopy = reinterpret_cast<TestableAssetCatalogCollection *>(
copy.get());
/* Test catalogs & deleted catalogs. */
EXPECT_EQ(2, testcopy->get_catalogs().size());
EXPECT_EQ(1, testcopy->get_deleted_catalogs().size());
ASSERT_TRUE(testcopy->get_catalogs().contains(UUID_POSES_RUZENA));
ASSERT_TRUE(testcopy->get_catalogs().contains(UUID_POSES_RUZENA_FACE));
ASSERT_TRUE(testcopy->get_deleted_catalogs().contains(UUID_POSES_RUZENA_HAND));
EXPECT_NE(nullptr, testcopy->get_catalogs().lookup(UUID_POSES_RUZENA));
EXPECT_NE(cat1_ptr, testcopy->get_catalogs().lookup(UUID_POSES_RUZENA).get())
<< "AssetCatalogs should be actual copies.";
EXPECT_NE(nullptr, testcopy->get_deleted_catalogs().lookup(UUID_POSES_RUZENA_HAND));
EXPECT_NE(cat3_ptr, testcopy->get_deleted_catalogs().lookup(UUID_POSES_RUZENA_HAND).get())
<< "AssetCatalogs should be actual copies.";
}
class TestableAssetCatalogDefinitionFile : public AssetCatalogDefinitionFile {
public:
Map<CatalogID, AssetCatalog *> get_catalogs()
{
return catalogs_;
}
};
TEST_F(AssetCatalogTest, cat_collection_deep_copy__nonempty_cdf)
{
TestableAssetCatalogCollection catcoll;
auto cat1 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA, "poses/Henrik", "");
auto cat2 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_FACE, "poses/Henrik/face", "");
auto cat3 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_HAND, "poses/Henrik/hands", "");
cat3->flags.is_deleted = true;
AssetCatalog *cat1_ptr = cat1.get();
AssetCatalog *cat2_ptr = cat2.get();
AssetCatalog *cat3_ptr = cat3.get();
catcoll.get_catalogs().add_new(cat1->catalog_id, std::move(cat1));
catcoll.get_catalogs().add_new(cat2->catalog_id, std::move(cat2));
catcoll.get_deleted_catalogs().add_new(cat3->catalog_id, std::move(cat3));
AssetCatalogDefinitionFile *cdf = catcoll.allocate_catalog_definition_file();
cdf->file_path = "path/to/somewhere.cats.txt";
cdf->add_new(cat1_ptr);
cdf->add_new(cat2_ptr);
cdf->add_new(cat3_ptr);
/* Test CDF remapping. */
auto copy = catcoll.deep_copy();
TestableAssetCatalogCollection *testable_copy = static_cast<TestableAssetCatalogCollection *>(
copy.get());
TestableAssetCatalogDefinitionFile *cdf_copy = static_cast<TestableAssetCatalogDefinitionFile *>(
testable_copy->get_catalog_definition_file());
EXPECT_EQ(testable_copy->get_catalogs().lookup(UUID_POSES_RUZENA).get(),
cdf_copy->get_catalogs().lookup(UUID_POSES_RUZENA))
<< "AssetCatalog pointers should have been remapped to the copy.";
EXPECT_EQ(testable_copy->get_deleted_catalogs().lookup(UUID_POSES_RUZENA_HAND).get(),
cdf_copy->get_catalogs().lookup(UUID_POSES_RUZENA_HAND))
<< "Deleted AssetCatalog pointers should have been remapped to the copy.";
}
TEST_F(AssetCatalogTest, undo_redo_one_step)
{
TestableAssetCatalogService service(asset_library_root_);
service.load_from_disk();
EXPECT_FALSE(service.is_undo_possbile());
EXPECT_FALSE(service.is_redo_possbile());
service.create_catalog("some/catalog/path");
EXPECT_FALSE(service.is_undo_possbile())
<< "Undo steps should be created explicitly, and not after creating any catalog.";
service.store_undo_snapshot();
const bUUID other_catalog_id = service.create_catalog("other/catalog/path")->catalog_id;
EXPECT_TRUE(service.is_undo_possbile())
<< "Undo should be possible after creating an undo snapshot.";
// Undo the creation of the catalog.
service.undo();
EXPECT_FALSE(service.is_undo_possbile())
<< "Undoing the only stored step should make it impossible to undo further.";
EXPECT_TRUE(service.is_redo_possbile()) << "Undoing a step should make redo possible.";
EXPECT_EQ(nullptr, service.find_catalog_by_path("other/catalog/path"))
<< "Undone catalog should not exist after undo.";
EXPECT_NE(nullptr, service.find_catalog_by_path("some/catalog/path"))
<< "First catalog should still exist after undo.";
EXPECT_FALSE(service.get_catalog_definition_file()->contains(other_catalog_id))
<< "The CDF should also not contain the undone catalog.";
// Redo the creation of the catalog.
service.redo();
EXPECT_TRUE(service.is_undo_possbile())
<< "Undoing and then redoing a step should make it possible to undo again.";
EXPECT_FALSE(service.is_redo_possbile())
<< "Undoing and then redoing a step should make redo impossible.";
EXPECT_NE(nullptr, service.find_catalog_by_path("other/catalog/path"))
<< "Redone catalog should exist after redo.";
EXPECT_NE(nullptr, service.find_catalog_by_path("some/catalog/path"))
<< "First catalog should still exist after redo.";
EXPECT_TRUE(service.get_catalog_definition_file()->contains(other_catalog_id))
<< "The CDF should contain the redone catalog.";
}
TEST_F(AssetCatalogTest, undo_redo_more_complex)
{
TestableAssetCatalogService service(asset_library_root_);
service.load_from_disk();
service.store_undo_snapshot();
service.find_catalog(UUID_POSES_ELLIE_WHITESPACE)->simple_name = "Edited simple name";
service.store_undo_snapshot();
service.find_catalog(UUID_POSES_ELLIE)->path = "poselib/EllieWithEditedPath";
service.undo();
service.undo();
service.store_undo_snapshot();
service.find_catalog(UUID_POSES_ELLIE)->simple_name = "Ellie Simple";
EXPECT_FALSE(service.is_redo_possbile())
<< "After storing an undo snapshot, the redo buffer should be empty.";
EXPECT_TRUE(service.is_undo_possbile())
<< "After storing an undo snapshot, undoing should be possible";
EXPECT_EQ(service.find_catalog(UUID_POSES_ELLIE)->simple_name, "Ellie Simple"); /* Not undone. */
EXPECT_EQ(service.find_catalog(UUID_POSES_ELLIE_WHITESPACE)->simple_name,
"POSES_ELLIE WHITESPACE"); /* Undone. */
EXPECT_EQ(service.find_catalog(UUID_POSES_ELLIE)->path, "character/Ellie/poselib"); /* Undone. */
}
} // namespace blender::bke::tests