Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
authorSybren A. Stüvel <sybren@blender.org>2021-10-12 13:39:24 +0300
committerSybren A. Stüvel <sybren@blender.org>2021-10-12 13:42:44 +0300
commita06435e43a6516207c617c2b4ad9961f865b66d8 (patch)
treed21e378f4c33f1d203cefeeca03dbefa81691158 /source
parentb67a9373945d07700b378aaa8be06aa2fd6d2cb9 (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')
-rw-r--r--source/blender/blenkernel/BKE_asset_catalog.hh64
-rw-r--r--source/blender/blenkernel/intern/asset_catalog.cc181
-rw-r--r--source/blender/blenkernel/intern/asset_catalog_test.cc187
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