diff options
author | Sybren A. Stüvel <sybren@blender.org> | 2021-10-12 13:39:24 +0300 |
---|---|---|
committer | Sybren A. Stüvel <sybren@blender.org> | 2021-10-12 13:42:44 +0300 |
commit | a06435e43a6516207c617c2b4ad9961f865b66d8 (patch) | |
tree | d21e378f4c33f1d203cefeeca03dbefa81691158 /source/blender/blenkernel | |
parent | b67a9373945d07700b378aaa8be06aa2fd6d2cb9 (diff) |
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
Diffstat (limited to 'source/blender/blenkernel')
-rw-r--r-- | source/blender/blenkernel/BKE_asset_catalog.hh | 64 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/asset_catalog.cc | 181 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/asset_catalog_test.cc | 187 |
3 files changed, 390 insertions, 42 deletions
diff --git a/source/blender/blenkernel/BKE_asset_catalog.hh b/source/blender/blenkernel/BKE_asset_catalog.hh index 4ea6abd65e0..b677adefb46 100644 --- a/source/blender/blenkernel/BKE_asset_catalog.hh +++ b/source/blender/blenkernel/BKE_asset_catalog.hh @@ -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*`. */ diff --git a/source/blender/blenkernel/intern/asset_catalog.cc b/source/blender/blenkernel/intern/asset_catalog.cc index 4531dabf0cf..1b8ab6f7196 100644 --- a/source/blender/blenkernel/intern/asset_catalog.cc +++ b/source/blender/blenkernel/intern/asset_catalog.cc @@ -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) diff --git a/source/blender/blenkernel/intern/asset_catalog_test.cc b/source/blender/blenkernel/intern/asset_catalog_test.cc index cf06638bcdd..3522ad80bdd 100644 --- a/source/blender/blenkernel/intern/asset_catalog_test.cc +++ b/source/blender/blenkernel/intern/asset_catalog_test.cc @@ -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 |