diff --git a/trview.app.tests/Filters/FilterStoreTests.cpp b/trview.app.tests/Filters/FilterStoreTests.cpp new file mode 100644 index 000000000..dccdaa275 --- /dev/null +++ b/trview.app.tests/Filters/FilterStoreTests.cpp @@ -0,0 +1,129 @@ +#include +#include +#include + +using namespace trview; +using namespace trview::tests; +using namespace trview::mocks; +using namespace testing; + +namespace +{ + TEST(FilterStore, AddAndGet) + { + auto files = mock_shared(); + UserSettings settings; + FilterStore store(files, settings); + + Filters::Filter filter{ .type_key = "Test" }; + Filters::Filter filter2{ .type_key = "Test" }; + Filters::Filter filter3{ .type_key = "Unrelated" }; + store.add("Test filter 1", filter); + store.add("Test filter 2", filter2); + store.add("Test filter 1", filter3); + + auto results = store.filters_for_key("Test"); + + ASSERT_EQ(results.size(), 2); + + auto found1 = results.find("Test filter 1"); + ASSERT_NE(found1, results.end()); + ASSERT_EQ(found1->first, "Test filter 1"); + + auto found2 = results.find("Test filter 2"); + ASSERT_NE(found2, results.end()); + ASSERT_EQ(found2->first, "Test filter 2"); + + auto found3 = results.find("Test filter 1"); + ASSERT_NE(found3, results.end()); + ASSERT_EQ(found3->first, "Test filter 1"); + } + + TEST(FilterStore, LoadTypeKeys) + { + const std::vector directories + { + {.path = "Test", .friendly_name = "Test" }, + {.path = "Test 2", .friendly_name = "Test 2" } + }; + + const std::vector test_files + { + {.path = "Test1_1" }, + {.path = "Test1_2" } + }; + + const std::vector test2_files + { + {.path = "Test2_1" } + }; + + auto files = mock_shared(); + ON_CALL(*files, get_directories("dir")).WillByDefault(Return(directories)); + ON_CALL(*files, get_files("Test", "\\*.json")).WillByDefault(Return(test_files)); + ON_CALL(*files, get_files("Test 2", "\\*.json")).WillByDefault(Return(test2_files)); + ON_CALL(*files, load_file("Test1_1")).WillByDefault(Return(std::string("{\"name\":\"Test filter 1\"}") | std::ranges::to>())); + ON_CALL(*files, load_file("Test1_2")).WillByDefault(Return(std::string("{\"name\":\"Test filter 2\"}") | std::ranges::to>())); + ON_CALL(*files, load_file("Test2_1")).WillByDefault(Return(std::string("{\"name\":\"Test filter 1\"}") | std::ranges::to>())); + + UserSettings settings{ .filter_directory = "dir" }; + FilterStore store(files, settings); + + store.load(); + + auto results = store.filters_for_key("Test"); + auto found1 = results.find("Test filter 1"); + ASSERT_NE(found1, results.end()); + ASSERT_EQ(found1->first, "Test filter 1"); + + auto found2 = results.find("Test filter 2"); + ASSERT_NE(found2, results.end()); + ASSERT_EQ(found2->first, "Test filter 2"); + + auto results2 = store.filters_for_key("Test 2"); + auto found3 = results2.find("Test filter 1"); + ASSERT_NE(found3, results2.end()); + ASSERT_EQ(found3->first, "Test filter 1"); + } + + TEST(FilterStore, Remove) + { + UserSettings settings; + FilterStore store(mock_shared(), settings); + + Filters::Filter filter{ .type_key = "Test" }; + store.add("Test filter 1", filter); + + auto results = store.filters_for_key("Test"); + auto found1 = results.find("Test filter 1"); + ASSERT_NE(found1, results.end()); + ASSERT_EQ(found1->first, "Test filter 1"); + + store.remove("Test", "Test filter 1"); + + results = store.filters_for_key("Test"); + found1 = results.find("Test filter 1"); + ASSERT_EQ(found1, results.end()); + } + + TEST(FilterStore, Save) + { + std::string filename; + std::string data; + + auto files = mock_shared(); + EXPECT_CALL(*files, save_file(An(), An())).WillOnce(SaveArg<1>(&data)); + + UserSettings settings; + FilterStore store(files, settings); + + Filters::Filter filter{ .type_key = "Test" }; + store.add("Test filter 1", filter); + + store.save(); + + std::string result = data; + + ASSERT_EQ(result, "{\"filter\":{\"children\":[],\"compare\":\"Equal\",\"invert\":false,\"key\":\"\",\"op\":\"And\",\"type_key\":\"Test\",\"value\":\"\",\"value2\":\"\"},\"name\":\"Test filter 1\"}"); + } +} \ No newline at end of file diff --git a/trview.app.tests/Filters/FiltersTests.cpp b/trview.app.tests/Filters/FiltersTests.cpp index 65c21b8f5..2f89eab0f 100644 --- a/trview.app.tests/Filters/FiltersTests.cpp +++ b/trview.app.tests/Filters/FiltersTests.cpp @@ -1,4 +1,5 @@ #include +#include using namespace trview; using namespace trview::tests; @@ -12,6 +13,7 @@ namespace std::string text; std::vector texts; std::optional option; + std::vector> filterables; Object with_number(float value) { @@ -43,6 +45,12 @@ namespace return *this; } + Object with_filterables(const std::vector>& value) + { + filterables = value; + return *this; + } + int32_t filterable_index() const { return static_cast(number); @@ -233,6 +241,36 @@ TEST(Filters, PresentFloat) ASSERT_FALSE(filters.match(Object().with_numbers({ }))); } +TEST(Filters, PresentFilterable) +{ + Filters filters; + filters.add_getters(Filters::GettersBuilder() + .with_multi_getter>("value", [](auto&& o) { return o.filterables; }) + .build()); + + Filters::Filter present_filterable = make_filter().key("value").compare_op(CompareOp::Exists); + filters.set_filters({ present_filterable }); + + auto filterable = mock_shared(); + ASSERT_TRUE(filters.match(Object().with_filterables({ filterable }))); + ASSERT_FALSE(filters.match(Object().with_filterables({ }))); +} + +TEST(Filters, NotPresentFilterable) +{ + Filters filters; + filters.add_getters(Filters::GettersBuilder() + .with_multi_getter>("value", [](auto&& o) { return o.filterables; }) + .build()); + + Filters::Filter present_filterable = make_filter().key("value").compare_op(CompareOp::NotExists); + filters.set_filters({ present_filterable }); + + auto filterable = mock_shared(); + ASSERT_FALSE(filters.match(Object().with_filterables({ filterable }))); + ASSERT_TRUE(filters.match(Object().with_filterables({ }))); +} + TEST(Filters, IsString) { Filters filters; diff --git a/trview.app.tests/RoomsWindowTests.cpp b/trview.app.tests/RoomsWindowTests.cpp index 9467bf39d..29ac06d23 100644 --- a/trview.app.tests/RoomsWindowTests.cpp +++ b/trview.app.tests/RoomsWindowTests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include using namespace trview; using namespace trview::tests; @@ -19,10 +20,11 @@ namespace IMapRenderer::Source map_renderer_source{ [](auto&&...) { return mock_unique(); } }; std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(map_renderer_source, clipboard, messaging); + return std::make_unique(map_renderer_source, clipboard, filter_store, messaging); } test_module& with_map_renderer_source(IMapRenderer::Source map_renderer_source) diff --git a/trview.app.tests/Settings/SettingsLoaderTests.cpp b/trview.app.tests/Settings/SettingsLoaderTests.cpp index 470d75ee7..13213590f 100644 --- a/trview.app.tests/Settings/SettingsLoaderTests.cpp +++ b/trview.app.tests/Settings/SettingsLoaderTests.cpp @@ -42,7 +42,7 @@ namespace { const auto contents = to_bytes(setting); auto files = mock_shared(); - EXPECT_CALL(*files, appdata_directory).Times(2).WillRepeatedly(Return("appdata")); + EXPECT_CALL(*files, appdata_directory).Times(3).WillRepeatedly(Return("appdata")); EXPECT_CALL(*files, load_file("appdata\\trview\\settings.txt")).Times(1).WillRepeatedly(Return(contents)); EXPECT_CALL(*files, load_file("appdata\\trview\\randomizer.json")).Times(1).WillRepeatedly(Return(to_bytes(randomizer_settings))); return register_test_module().with_files(files).build(); @@ -60,7 +60,7 @@ namespace TEST(SettingsLoader, FileNotFound) { auto files = mock_shared(); - EXPECT_CALL(*files, appdata_directory).Times(1).WillRepeatedly(Return("appdata")); + EXPECT_CALL(*files, appdata_directory).Times(2).WillRepeatedly(Return("appdata")); EXPECT_CALL(*files, load_file("appdata\\trview\\settings.txt")).Times(1); auto loader = register_test_module().with_files(files).build(); auto settings = loader->load_user_settings(); diff --git a/trview.app.tests/trview.app.tests.vcxproj b/trview.app.tests/trview.app.tests.vcxproj index a2e2b9a8a..b30d12e47 100644 --- a/trview.app.tests/trview.app.tests.vcxproj +++ b/trview.app.tests/trview.app.tests.vcxproj @@ -34,6 +34,7 @@ + diff --git a/trview.app.tests/trview.app.tests.vcxproj.filters b/trview.app.tests/trview.app.tests.vcxproj.filters index 62dae2e9c..ab79433a8 100644 --- a/trview.app.tests/trview.app.tests.vcxproj.filters +++ b/trview.app.tests/trview.app.tests.vcxproj.filters @@ -65,6 +65,7 @@ + diff --git a/trview.app.ui.tests/CameraSinkWindowTests.cpp b/trview.app.ui.tests/CameraSinkWindowTests.cpp index d7cb878ed..5c28e305a 100644 --- a/trview.app.ui.tests/CameraSinkWindowTests.cpp +++ b/trview.app.ui.tests/CameraSinkWindowTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include using namespace testing; using namespace trview; @@ -27,10 +28,11 @@ namespace std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr camera{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(clipboard, camera, messaging); + return std::make_unique(clipboard, camera, filter_store, messaging); } test_module& with_messaging(const std::shared_ptr& messaging) diff --git a/trview.app.ui.tests/ItemsWindowTests.cpp b/trview.app.ui.tests/ItemsWindowTests.cpp index b945490fe..8e68248a0 100644 --- a/trview.app.ui.tests/ItemsWindowTests.cpp +++ b/trview.app.ui.tests/ItemsWindowTests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -26,10 +27,11 @@ namespace { std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(clipboard, messaging); + return std::make_unique(clipboard, filter_store, messaging); } test_module& with_clipboard(const std::shared_ptr& clipboard) diff --git a/trview.app.ui.tests/LightsWindowTests.cpp b/trview.app.ui.tests/LightsWindowTests.cpp index 5a431de70..43887efd9 100644 --- a/trview.app.ui.tests/LightsWindowTests.cpp +++ b/trview.app.ui.tests/LightsWindowTests.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace testing; using namespace trview; @@ -21,10 +22,11 @@ namespace { std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(clipboard, messaging); + return std::make_unique(clipboard, filter_store, messaging); } test_module& with_messaging(const std::shared_ptr& messaging) diff --git a/trview.app.ui.tests/RoomsWindowTests.cpp b/trview.app.ui.tests/RoomsWindowTests.cpp index 4d2474b22..c1afc2da3 100644 --- a/trview.app.ui.tests/RoomsWindowTests.cpp +++ b/trview.app.ui.tests/RoomsWindowTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace trview; using namespace trview::tests; @@ -21,10 +22,11 @@ namespace IMapRenderer::Source map_renderer_source{ [](auto&&...) { return mock_unique(); } }; std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(map_renderer_source, clipboard, messaging); + return std::make_unique(map_renderer_source, clipboard, filter_store, messaging); } test_module& with_map_renderer_source(IMapRenderer::Source map_renderer_source) diff --git a/trview.app.ui.tests/StaticsWindowTests.cpp b/trview.app.ui.tests/StaticsWindowTests.cpp index ed0dc8f66..0d362ddd8 100644 --- a/trview.app.ui.tests/StaticsWindowTests.cpp +++ b/trview.app.ui.tests/StaticsWindowTests.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -24,10 +25,11 @@ namespace { std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(clipboard, messaging); + return std::make_unique(clipboard, filter_store, messaging); } test_module& with_clipboard(const std::shared_ptr& clipboard) diff --git a/trview.app.ui.tests/TriggersWindowTests.cpp b/trview.app.ui.tests/TriggersWindowTests.cpp index 5dfff9ef4..f44759c50 100644 --- a/trview.app.ui.tests/TriggersWindowTests.cpp +++ b/trview.app.ui.tests/TriggersWindowTests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include using namespace trview; using namespace trview::mocks; @@ -25,10 +26,11 @@ namespace { std::shared_ptr clipboard{ mock_shared() }; std::shared_ptr messaging{ mock_shared() }; + std::shared_ptr filter_store{ mock_shared() }; std::unique_ptr build() { - return std::make_unique(clipboard, messaging); + return std::make_unique(clipboard, filter_store, messaging); } test_module& with_messaging(const std::shared_ptr& messaging) diff --git a/trview.app/ApplicationCreate.cpp b/trview.app/ApplicationCreate.cpp index 4ea8361f2..084547c90 100644 --- a/trview.app/ApplicationCreate.cpp +++ b/trview.app/ApplicationCreate.cpp @@ -34,6 +34,7 @@ #include "Elements/Sector.h" #include "Elements/SoundSource/SoundSource.h" #include "Elements/Level.h" +#include "Filters/FilterStore.h" #include "Graphics/TextureStorage.h" #include "Geometry/Mesh.h" #include "Geometry/Picking.h" @@ -168,6 +169,9 @@ namespace trview auto files = std::make_shared(); auto settings_loader = std::make_shared(files); auto window = create_window(hInstance, command_show, settings_loader->load_user_settings()); + auto filters = std::make_shared(files, settings_loader->load_user_settings()); + messaging->add_recipient(filters); + filters->load(); auto device = std::make_shared(); auto shortcuts = std::make_shared(window); @@ -385,7 +389,12 @@ namespace trview settings_loader->load_user_settings()); messaging->add_recipient(plugins); - auto plugins_window_source = [=]() { return std::make_shared(plugins, shell, dialogs, messaging); }; + auto plugins_window_source = [=]() + { + auto plugins_window = std::make_shared(plugins, shell, dialogs, messaging); + messaging->add_recipient(plugins_window); + return plugins_window; + }; auto imgui_backend = std::make_shared(window, device, files); auto fonts = std::make_shared(files, imgui_backend); auto map_renderer_source = [=]() @@ -398,14 +407,14 @@ namespace trview auto clipboard = std::make_shared(window); auto items_window_source = [=]() { - auto new_window = std::make_shared(clipboard, messaging); + auto new_window = std::make_shared(clipboard, filters, messaging); messaging->add_recipient(new_window); new_window->initialise(); return new_window; }; auto rooms_window_source = [=]() { - auto new_window = std::make_shared(map_renderer_source, clipboard, messaging); + auto new_window = std::make_shared(map_renderer_source, clipboard, filters, messaging); messaging->add_recipient(new_window); new_window->initialise(); return new_window; @@ -416,7 +425,7 @@ namespace trview auto triggers_window_source = [=]() { - auto triggers_window = std::make_shared(clipboard, messaging); + auto triggers_window = std::make_shared(clipboard, filters, messaging); messaging->add_recipient(triggers_window); triggers_window->initialise(); return triggers_window; @@ -430,7 +439,7 @@ namespace trview }; auto lights_window_source = [=]() { - auto lights_window = std::make_shared(clipboard, messaging); + auto lights_window = std::make_shared(clipboard, filters, messaging); messaging->add_recipient(lights_window); lights_window->initialise(); return lights_window; @@ -440,7 +449,7 @@ namespace trview const auto camera = std::make_shared(window.size()); auto camera_sink_window_source = [=]() { - auto camera_sink_window = std::make_shared(clipboard, camera, messaging); + auto camera_sink_window = std::make_shared(clipboard, camera, filters, messaging); messaging->add_recipient(camera_sink_window); camera_sink_window->initialise(); return camera_sink_window; @@ -456,14 +465,14 @@ namespace trview auto console_source = [=]() { return std::make_shared(dialogs, plugins, fonts); }; auto statics_window_source = [=]() { - auto statics_window = std::make_shared(clipboard, messaging); + auto statics_window = std::make_shared(clipboard, filters, messaging); messaging->add_recipient(statics_window); statics_window->initialise(); return statics_window; }; auto sounds_window_source = [=]() { - auto sounds_window = std::make_shared(messaging); + auto sounds_window = std::make_shared(filters, messaging); messaging->add_recipient(sounds_window); sounds_window->initialise(); return sounds_window; diff --git a/trview.app/Filters/FilterStore.cpp b/trview.app/Filters/FilterStore.cpp new file mode 100644 index 000000000..02ee870a0 --- /dev/null +++ b/trview.app/Filters/FilterStore.cpp @@ -0,0 +1,299 @@ +#include "FilterStore.h" +#include "../Messages/Messages.h" + +#include +#include +#include + +#include + +namespace trview +{ + void from_json(const nlohmann::json& json, CompareOp& op) + { + const std::string value = to_lowercase(json.get()); + if (value == "equal") + { + op = CompareOp::Equal; + } + else if (value == "notequal") + { + op = CompareOp::NotEqual; + } + else if (value == "greaterthan") + { + op = CompareOp::GreaterThan; + } + else if (value == "greaterthanorqqual") + { + op = CompareOp::GreaterThanOrEqual; + } + else if (value == "lessthan") + { + op = CompareOp::LessThan; + } + else if (value == "lessthanorequal") + { + op = CompareOp::LessThanOrEqual; + } + else if (value == "between") + { + op = CompareOp::Between; + } + else if (value == "betweeninclusive") + { + op = CompareOp::BetweenInclusive; + } + else if (value == "exists") + { + op = CompareOp::Exists; + } + else if (value == "notexists") + { + op = CompareOp::NotExists; + } + else if (value == "startswith") + { + op = CompareOp::StartsWith; + } + else if (value == "endswith") + { + op = CompareOp::EndsWith; + } + else if (value == "matches") + { + op = CompareOp::Matches; + } + else + { + op = CompareOp::Equal; + } + } + + void from_json(const nlohmann::json& json, Op& op) + { + const std::string value = to_lowercase(json.get()); + if (value == "and") + { + op = Op::And; + } + else if (value == "or") + { + op = Op::Or; + } + else + { + op = Op::And; + } + } + + void from_json(const nlohmann::json& json, Filters::Filter& filter) + { + read_attribute(json, filter.key, "key"); + read_attribute(json, filter.compare, "compare"); + read_attribute(json, filter.value, "value"); + read_attribute(json, filter.value2, "value2"); + read_attribute(json, filter.op, "op"); + read_attribute(json, filter.invert, "invert"); + read_attribute(json, filter.type_key, "type_key"); + read_attribute(json, filter.children, "children"); + } + + void to_json(nlohmann::json& json, const CompareOp& op) + { + std::string value; + switch (op) + { + case CompareOp::Equal: + value = "Equal"; + break; + case CompareOp::NotEqual: + value = "NotEqual"; + break; + case CompareOp::GreaterThan: + value = "GreaterThan"; + break; + case CompareOp::GreaterThanOrEqual: + value = "GreaterThanOrEqual"; + break; + case CompareOp::LessThan: + value = "LessThan"; + break; + case CompareOp::LessThanOrEqual: + value = "LessThanOrEqual"; + break; + case CompareOp::Between: + value = "Between"; + break; + case CompareOp::BetweenInclusive: + value = "BetweenInclusive"; + break; + case CompareOp::Exists: + value = "Exists"; + break; + case CompareOp::NotExists: + value = "NotExists"; + break; + case CompareOp::StartsWith: + value = "StartsWith"; + break; + case CompareOp::EndsWith: + value = "EndsWith"; + break; + case CompareOp::Matches: + value = "Matches"; + break; + } + json = value; + } + + void to_json(nlohmann::json& json, const Op& op) + { + std::string value; + switch (op) + { + case Op::And: + value = "And"; + break; + case Op::Or: + value = "Or"; + break; + } + json = value; + } + + void to_json(nlohmann::json& json, const Filters::Filter& filter) + { + json["key"] = filter.key; + json["compare"] = filter.compare; + json["value"] = filter.value; + json["value2"] = filter.value2; + json["children"] = filter.children; + json["op"] = filter.op; + json["invert"] = filter.invert; + json["type_key"] = filter.type_key; + } + + IFilterStore::~IFilterStore() + { + } + + FilterStore::FilterStore(const std::shared_ptr& files, const UserSettings& settings) + : _files(files), _settings(settings) + { + } + + void FilterStore::add(const std::string& key, const Filters::Filter& filter) + { + _filters[filter.type_key].push_back( + { + .name = key, + .filter = filter + }); + } + + void FilterStore::load() + { + if (_settings.filter_directory.empty()) + { + return; + } + + for (const auto& directory : _files->get_directories(_settings.filter_directory)) + { + const std::string type_key = directory.friendly_name; + + const auto files = _files->get_files(directory.path, "\\*.json"); + for (const auto& file : files) + { + try + { + const auto data = _files->load_file(file.path); + if (!data) + { + continue; + } + + auto json = nlohmann::json::parse(data.value().begin(), data.value().end(), nullptr, true, true, true); + StoredFilter new_filter{ .filename = file.path }; + read_attribute(json, new_filter.name, "name"); + read_attribute(json, new_filter.filter, "filter"); + _filters[type_key].push_back(new_filter); + } + catch (...) + { + } + } + } + } + + std::map FilterStore::filters_for_key(const std::string& key) const + { + const auto found = _filters.find(key); + if (found == _filters.end()) + { + return {}; + } + + std::map results; + for (const auto& filter : found->second) + { + results[filter.name] = filter.filter; + } + return results; + } + + void FilterStore::receive_message(const Message& message) + { + if (auto settings = messages::read_settings(message)) + { + _settings = settings.value(); + } + } + + void FilterStore::remove(const std::string& type_key, const std::string& name) + { + const auto filters_for_key = _filters.find(type_key); + if (filters_for_key == _filters.end()) + { + return; + } + + const auto found = std::ranges::find_if(filters_for_key->second, [&](auto&& f) { return f.name == name; }); + if (found == filters_for_key->second.end()) + { + return; + } + + _files->delete_file(found->filename); + filters_for_key->second.erase(found); + } + + void FilterStore::save() + { + const auto dir = _settings.filter_directory.empty() ? + (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; + _files->create_directory(dir); + + for (auto& type : _filters) + { + const std::string type_dir = std::format("{}\\{}", dir, type.first); + _files->create_directory(type_dir); + for (auto& filter : type.second) + { + try + { + nlohmann::json json; + json["name"] = filter.name; + json["filter"] = filter.filter; + std::string path = type_dir + "\\" + filter.name + ".json"; + _files->save_file(path, json.dump()); + filter.filename = path; + } + catch (...) + { + } + } + } + } +} diff --git a/trview.app/Filters/FilterStore.h b/trview.app/Filters/FilterStore.h new file mode 100644 index 000000000..90bb91f75 --- /dev/null +++ b/trview.app/Filters/FilterStore.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "IFilterStore.h" +#include "Filters.h" +#include "../Settings/UserSettings.h" + +namespace trview +{ + class FilterStore final : public IFilterStore, public IRecipient + { + public: + explicit FilterStore(const std::shared_ptr& files, const UserSettings& settings); + virtual ~FilterStore() = default; + void add(const std::string& key, const Filters::Filter& filter) override; + void load() override; + std::map filters_for_key(const std::string& key) const override; + void receive_message(const Message& message) override; + void remove(const std::string& type_key, const std::string& name) override; + void save() override; + private: + struct StoredFilter + { + std::string name; + std::string filename; + Filters::Filter filter; + }; + + std::shared_ptr _files; + std::map> _filters; + UserSettings _settings; + }; +} diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index 79a3da9ac..1f98bb3fd 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -1,7 +1,22 @@ #include "Filters.h" +#include "IFilterStore.h" namespace trview { + namespace + { + int filter_path_letters(ImGuiInputTextCallbackData* data) + { + switch (data->EventChar) + { + case L'\\': + case L'/': + return 1; + } + return 0; + } + } + IFilterable::~IFilterable() { } @@ -43,6 +58,11 @@ namespace trview return *this; } + Filters::Filters(const std::weak_ptr& filter_store) + : _filter_store(filter_store) + { + } + void Filters::add_filter(const Filter& filter) { _filter.children.push_back(filter); @@ -149,6 +169,10 @@ namespace trview { return is_match(static_cast(*value_int), filter); } + else if (const std::weak_ptr* value_filterable = std::get_if>(&value)) + { + return is_match(static_cast>(*value_filterable), filter); + } return false; } @@ -224,6 +248,15 @@ namespace trview return false; } + bool Filters::is_match(std::weak_ptr, const Filter& filter) const + { + if (filter.compare == CompareOp::Exists) + { + return true; + } + return false; + } + bool Filters::match(const IFilterable& value) const { return match(_filter, value, _filter.type_key); @@ -449,11 +482,16 @@ namespace trview toggle_visible(); } - if (_show_filters && ImGui::BeginPopup(Names::Popup.c_str())) + if (_show_filters) { - ImGui::Text("Filters"); - render(_filter, 0, 0, _filter, _filter.type_key); - ImGui::EndPopup(); + ImGui::SetNextWindowSizeConstraints(ImVec2(200, 50), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(std::format("{} ({})", Names::Popup, _id).c_str(), &_show_filters, ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_AlwaysAutoResize)) + { + render_menu_bar(); + render_filter_name_modal(); + render_filters(); + } + ImGui::End(); } else { @@ -461,6 +499,53 @@ namespace trview } } + void Filters::render_filters() + { + render(_filter, 0, 0, _filter, _filter.type_key); + } + + void Filters::render_filter_name_modal() + { + _save_modal.render( + [&](auto&& state) + { + if (ImGui::IsWindowAppearing()) + { + ImGui::SetKeyboardFocusHere(); + } + + if (ImGui::InputText("##Filter Name", &state.name_value, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCharFilter, filter_path_letters)) + { + if (const auto store = _filter_store.lock()) + { + store->add(state.name_value, _filter); + store->save(); + _name = state.name_value; + } + return false; + } + + ImGui::SameLine(); + if (ImGui::Button("Save")) + { + if (const auto store = _filter_store.lock()) + { + store->add(state.name_value, _filter); + store->save(); + _name = state.name_value; + } + return false; + } + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + { + return false; + } + + return true; + }); + } + Filters::Action Filters::render(Filter& filter, int32_t depth, int32_t index, Filter& parent, const std::string& type_key) { const auto keys = this->keys(type_key); @@ -831,10 +916,6 @@ namespace trview void Filters::toggle_visible() { - if (!_show_filters) - { - ImGui::OpenPopup(Names::Popup.c_str()); - } _show_filters = !_show_filters; } @@ -847,4 +928,75 @@ namespace trview .multi_getters = _multi_getters }; } + + void Filters::render_menu_bar() + { + if (ImGui::BeginMenuBar()) + { + const auto store = _filter_store.lock(); + const auto values = store ? store->filters_for_key(_filter.type_key) : std::map { }; + if (ImGui::BeginMenu("Open", !values.empty())) + { + if (ImGui::BeginTable("##filters-open-menu", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupScrollFreeze(0, 1); + for (const auto& value : values) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + bool selected = false; + ImGui::SetNextItemAllowOverlap(); + + ImGui::AlignTextToFramePadding(); + const auto pos = ImGui::GetCursorPos(); + ImGui::Selectable(value.first.c_str(), &selected, ImGuiSelectableFlags_SpanAllColumns); + + const auto text_size = ImGui::GetItemRectSize(); + const auto style = ImGui::GetStyle(); + + ImGui::TableNextColumn(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.ItemSpacing.y * 0.25f); + + const auto button_size = ImGui::CalcTextSize("X") + ImGui::GetStyle().FramePadding * 2; + if (ImGui::Button("X", ImVec2(button_size.x, text_size.y))) + { + store->remove(_filter.type_key, value.first); + } + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Delete this filter"); + } + + if (selected) + { + _filter = { value.second }; + _name = value.first; + } + } + ImGui::EndTable(); + } + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("Save")) + { + _save_modal.show("Enter Filter Name", { .name_value = _name }); + } + + if (ImGui::MenuItem("Clear")) + { + _filter.children = {}; + _name = ""; + } + + ImGui::EndMenuBar(); + } + } + + void Filters::set_name(const std::string& id) + { + _id = id; + } } diff --git a/trview.app/Filters/Filters.h b/trview.app/Filters/Filters.h index 689a8f944..3a20aa3e5 100644 --- a/trview.app/Filters/Filters.h +++ b/trview.app/Filters/Filters.h @@ -9,9 +9,12 @@ #include "../Windows/RowCounter.h" #include "IFilterable.h" +#include "../UI/Modal.h" namespace trview { + struct IFilterStore; + enum class CompareOp { Equal, @@ -148,6 +151,8 @@ namespace trview Event<> on_columns_reset; Event<> on_columns_saved; + Filters() = default; + explicit Filters(const std::weak_ptr& filter_store); void add_filter(const Filter& filter); void add_getters(const Getters& getters); void clear_all_getters(); @@ -160,6 +165,7 @@ namespace trview bool is_match(const std::string& value, const Filter& filter) const; bool is_match(float value, const Filter& filter) const; bool is_match(bool value, const Filter& filter) const; + bool is_match(std::weak_ptr value, const Filter& filter) const; std::vector keys(const std::string& type_key) const; bool match(const IFilterable& value) const; bool match(const Filter& filter, const IFilterable& value, const std::string& type_key) const; @@ -177,6 +183,7 @@ namespace trview void scroll_to_item(); bool test_and_reset_changed(); void toggle_visible(); + void set_name(const std::string& id); private: enum class Action { @@ -191,16 +198,28 @@ namespace trview std::vector options_for_key(const std::string& type_key, const std::string& key) const; Action render(Filter& filter, int32_t depth, int32_t index, Filter& parent, const std::string& type_key); Action render_leaf(Filter& filter, int32_t depth, int32_t index, const std::string& type_key); - - bool _changed{ true }; - std::vector _columns; - std::vector _column_order; - bool _enabled{ true }; - Filter _filter; - std::vector _getters; - mutable bool _force_sort{ false }; - mutable bool _scroll_to_item{ false }; - bool _show_filters{ false }; + void render_menu_bar(); + void render_filter_name_modal(); + void render_filters(); + + bool _changed{ true }; + std::vector _columns; + std::vector _column_order; + bool _enabled{ true }; + Filter _filter; + std::vector _getters; + mutable bool _force_sort{ false }; + mutable bool _scroll_to_item{ false }; + bool _show_filters{ false }; + std::weak_ptr _filter_store; + std::string _id; + std::string _name; + + struct ModalState + { + std::string name_value; + }; + Modal _save_modal; }; constexpr std::string to_string(CompareOp op) noexcept; diff --git a/trview.app/Filters/IFilterStore.h b/trview.app/Filters/IFilterStore.h new file mode 100644 index 000000000..4ca1607fb --- /dev/null +++ b/trview.app/Filters/IFilterStore.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include "Filters.h" + +namespace trview +{ + struct IFilterStore + { + virtual ~IFilterStore() = 0; + virtual void add(const std::string& key, const Filters::Filter& filter) = 0; + virtual void load() = 0; + virtual std::map filters_for_key(const std::string& key) const = 0; + virtual void remove(const std::string& type_key, const std::string& name) = 0; + virtual void save() = 0; + }; +} diff --git a/trview.app/Mocks/Filters/IFilterStore.h b/trview.app/Mocks/Filters/IFilterStore.h new file mode 100644 index 000000000..e5639d877 --- /dev/null +++ b/trview.app/Mocks/Filters/IFilterStore.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../../Filters/IFilterStore.h" + +namespace trview +{ + namespace mocks + { + struct MockFilterStore : public IFilterStore + { + MockFilterStore(); + virtual ~MockFilterStore(); + MOCK_METHOD(void, add, (const std::string&, const Filters::Filter&), (override)); + MOCK_METHOD(void, load, (), (override)); + MOCK_METHOD((std::map), filters_for_key, (const std::string&), (const, override)); + MOCK_METHOD(void, remove, (const std::string&, const std::string&), (override)); + MOCK_METHOD(void, save, (), (override)); + }; + } +} diff --git a/trview.app/Mocks/Filters/IFilterable.h b/trview.app/Mocks/Filters/IFilterable.h new file mode 100644 index 000000000..e6d9e7d9e --- /dev/null +++ b/trview.app/Mocks/Filters/IFilterable.h @@ -0,0 +1,16 @@ +#pragma once + +#include "../../Filters/IFilterable.h" + +namespace trview +{ + namespace mocks + { + struct MockFilterable : public IFilterable + { + MockFilterable(); + virtual ~MockFilterable(); + MOCK_METHOD(int32_t, filterable_index, (), (const, override)); + }; + } +} diff --git a/trview.app/Mocks/Mocks.cpp b/trview.app/Mocks/Mocks.cpp index b6d5af99c..a250aa3bc 100644 --- a/trview.app/Mocks/Mocks.cpp +++ b/trview.app/Mocks/Mocks.cpp @@ -15,6 +15,8 @@ #include "Elements/ITrigger.h" #include "Elements/ITypeInfoLookup.h" #include "Elements/ILevelNameLookup.h" +#include "Filters/IFilterStore.h" +#include "Filters/IFilterable.h" #include "Geometry/IMesh.h" #include "Geometry/IPicking.h" #include "Geometry/ITransparencyBuffer.h" @@ -213,5 +215,11 @@ namespace trview MockFlybyNode::MockFlybyNode() {}; MockFlybyNode::~MockFlybyNode() {}; + + MockFilterStore::MockFilterStore() {}; + MockFilterStore::~MockFilterStore() {}; + + MockFilterable::MockFilterable() {}; + MockFilterable::~MockFilterable() {}; } } \ No newline at end of file diff --git a/trview.app/Settings/SettingsLoader.cpp b/trview.app/Settings/SettingsLoader.cpp index c104d4c7a..8af6ed7de 100644 --- a/trview.app/Settings/SettingsLoader.cpp +++ b/trview.app/Settings/SettingsLoader.cpp @@ -79,6 +79,7 @@ namespace trview UserSettings SettingsLoader::load_user_settings() const { UserSettings settings; + settings.filter_directory = _files->appdata_directory() + "\\trview\\filters"; try { @@ -133,6 +134,7 @@ namespace trview read_attribute(json, settings.flyby_node_columns, "flyby_node_columns"); read_attribute(json, settings.linear_filtering, "linear_filtering"); read_attribute(json, settings.version, "version"); + read_attribute(json, settings.filter_directory, "filter_directory"); settings.recent_files.resize(std::min(settings.recent_files.size(), settings.max_recent_files)); } @@ -218,6 +220,7 @@ namespace trview json["flyby_node_columns"] = settings.flyby_node_columns; json["linear_filtering"] = settings.linear_filtering; json["version"] = trview::version(); + json["filter_directory"] = settings.filter_directory; _files->save_file(file_path, json.dump()); } catch (...) diff --git a/trview.app/Settings/UserSettings.h b/trview.app/Settings/UserSettings.h index 9a39d50c9..4c62a21ff 100644 --- a/trview.app/Settings/UserSettings.h +++ b/trview.app/Settings/UserSettings.h @@ -90,6 +90,7 @@ namespace trview std::vector flyby_node_columns{ "#", "Room" }; bool linear_filtering{ false }; std::string version; + std::string filter_directory; bool operator==(const UserSettings& other) const; }; diff --git a/trview.app/UI/Modal.h b/trview.app/UI/Modal.h new file mode 100644 index 000000000..2aefa7866 --- /dev/null +++ b/trview.app/UI/Modal.h @@ -0,0 +1,19 @@ +#pragma once + +namespace trview +{ + template + class Modal + { + public: + void show(const std::string& id, const State& state); + void render(const std::function& callback); + private: + std::string _id; + State _state; + std::optional _open; + bool _is_open{ false }; + }; +} + +#include "Modal.hpp" diff --git a/trview.app/UI/Modal.hpp b/trview.app/UI/Modal.hpp new file mode 100644 index 000000000..6c2325ccd --- /dev/null +++ b/trview.app/UI/Modal.hpp @@ -0,0 +1,43 @@ +#pragma once + +namespace trview +{ + template + void Modal::show(const std::string& id, const State& state) + { + _id = id; + _state = state; + _open = true; + } + + template + void Modal::render(const std::function& callback) + { + if (!_open) + { + return; + } + + if (_open.value()) + { + ImGui::OpenPopup(_id.c_str()); + _open = false; + _is_open = true; + } + + if (ImGui::BeginPopupModal(_id.c_str(), &_is_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (!callback(_state)) + { + ImGui::CloseCurrentPopup(); + _open.reset(); + } + ImGui::EndPopup(); + } + + if (!_open) + { + _open.reset(); + } + } +} diff --git a/trview.app/UI/SettingsWindow.cpp b/trview.app/UI/SettingsWindow.cpp index ad0110017..7b2bd67c2 100644 --- a/trview.app/UI/SettingsWindow.cpp +++ b/trview.app/UI/SettingsWindow.cpp @@ -79,6 +79,11 @@ namespace trview ImGui::EndCombo(); } + if (ImGui::Button("Show Filters Directory")) + { + _shell->open(to_utf16(_settings.filter_directory)); + } + ImGui::EndTabItem(); } diff --git a/trview.app/Windows/CameraSink/CameraSinkWindow.cpp b/trview.app/Windows/CameraSink/CameraSinkWindow.cpp index 146123677..969843972 100644 --- a/trview.app/Windows/CameraSink/CameraSinkWindow.cpp +++ b/trview.app/Windows/CameraSink/CameraSinkWindow.cpp @@ -126,14 +126,14 @@ namespace trview void add_camera_sink_filters(Filters& filters) { - if (filters.has_type_key("ICameraSink")) + if (filters.has_type_key("CameraSink")) { return; } std::set available_types{ "Camera", "Sink" }; auto camera_sink_getters = Filters::GettersBuilder() - .with_type_key("ICameraSink") + .with_type_key("CameraSink") .with_getter("Type", { available_types.begin(), available_types.end() }, [](auto&& camera_sink) { return to_string(camera_sink.type()); }) .with_getter("#", [](auto&& camera_sink) { return static_cast(camera_sink.number()); }) .with_getter("X", [](auto&& camera_sink) { return static_cast(camera_sink.position().x * trlevel::Scale_X); }) @@ -147,7 +147,7 @@ namespace trview } const auto rooms = camera_sink.inferred_rooms() | std::ranges::to>>(); return !rooms.empty() ? rooms[0] : std::weak_ptr{}; - }, {}, EditMode::Read, "IRoom") + }, {}, EditMode::Read, "Room") .with_getter("Room #", [](auto&& camera_sink) { if (camera_sink.type() == ICameraSink::Type::Camera) @@ -166,7 +166,7 @@ namespace trview return { camera_sink.room() }; } return camera_sink.inferred_rooms() | std::ranges::to>>(); - }, {}, "IRoom") + }, {}, "Room") .with_multi_getter("Rooms #", [](auto&& camera_sink) -> std::vector { if (camera_sink.type() == ICameraSink::Type::Camera) @@ -181,7 +181,7 @@ namespace trview .with_multi_getter>("Trigger", {}, [](auto&& camera_sink) -> std::vector> { return camera_sink.triggers() | std::ranges::to>>(); - }, {}, "ITrigger") + }, {}, "Trigger") .with_multi_getter("Trigger References", [&](auto&& camera_sink) { std::vector results; @@ -214,13 +214,13 @@ namespace trview void add_flyby_filters(Filters& filters) { - if (filters.has_type_key("IFlyby")) + if (filters.has_type_key("Flyby")) { return; } auto flyby_getters = Filters::GettersBuilder() - .with_type_key("IFlyby") + .with_type_key("Flyby") .with_getter("#", [](auto&& flyby) { return static_cast(flyby.number()); }) .with_getter("Hide", [](auto&& flyby) { return !flyby.visible(); }, EditMode::ReadWrite) .with_multi_getter>("Room", {}, [](auto&& flyby) @@ -237,7 +237,7 @@ namespace trview } } return rooms; - }, {}, "IRoom") + }, {}, "Room") .with_multi_getter("Room #", [](auto&& flyby) { std::unordered_set rooms; @@ -273,7 +273,7 @@ namespace trview void add_flyby_node_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("IFlybyNode")) + if (filters.has_type_key("FlybyNode")) { return; } @@ -282,7 +282,7 @@ namespace trview const auto platform_and_version = level_ptr ? level_ptr->platform_and_version() : trlevel::PlatformAndVersion{ .platform = trlevel::Platform::PC, .version = trlevel::LevelVersion::Tomb4 }; auto flyby_node_getters = Filters::GettersBuilder() - .with_type_key("IFlybyNode") + .with_type_key("FlybyNode") .with_getter("#", [](auto&& node) { return static_cast(node.number()); }) .with_getter("X", [](auto&& node) { return static_cast(node.position().x * trlevel::Scale_X); }) .with_getter("Y", [](auto&& node) { return static_cast(node.position().y * trlevel::Scale_Y); }) @@ -294,7 +294,7 @@ namespace trview return node_level->room(node.room()); } return {}; - }, {}, EditMode::Read, "IRoom") + }, {}, EditMode::Read, "Room") .with_getter("Room #", [](auto&& node) { return node.room(); }) .with_getter("Roll", [](auto&& node) { return node.roll(); }) .with_getter("Speed", [](auto&& node) { return node.speed(); }) @@ -308,8 +308,8 @@ namespace trview filters.add_getters(flyby_node_getters.build()); } - CameraSinkWindow::CameraSinkWindow(const std::shared_ptr& clipboard, const std::weak_ptr& camera, const std::weak_ptr& messaging) - : _clipboard(clipboard), _camera(camera), _messaging(messaging) + CameraSinkWindow::CameraSinkWindow(const std::shared_ptr& clipboard, const std::weak_ptr& camera, const std::shared_ptr& filter_store, const std::weak_ptr& messaging) + : _clipboard(clipboard), _camera(camera), _messaging(messaging), _filters(filter_store), _flyby_filters(filter_store), _node_filters(filter_store) { setup_filters(); setup_flyby_filters(); @@ -332,6 +332,7 @@ namespace trview void CameraSinkWindow::set_number(int32_t number) { _id = std::format("Camera/Sink {}", number); + _filters.set_name(_id); } void CameraSinkWindow::set_camera_sinks(const std::vector>& camera_sinks) @@ -627,18 +628,18 @@ namespace trview messages::send_settings(_messaging, *_settings); } }; - _filters.set_type_key("ICameraSink"); + _filters.set_type_key("CameraSink"); } void CameraSinkWindow::setup_flyby_filters() { _flyby_filters.clear_all_getters(); add_all_filters(_flyby_filters, _level); - _flyby_filters.set_type_key("IFlyby"); + _flyby_filters.set_type_key("Flyby"); _node_filters.clear_all_getters(); add_all_filters(_node_filters, _level); - _node_filters.set_type_key("IFlybyNode"); + _node_filters.set_type_key("FlybyNode"); _flyby_filters.set_columns(std::vector{ "#", "Hide" }); _token_store += _flyby_filters.on_columns_reset += [this]() diff --git a/trview.app/Windows/CameraSink/CameraSinkWindow.h b/trview.app/Windows/CameraSink/CameraSinkWindow.h index 37629c7b6..cc0a61410 100644 --- a/trview.app/Windows/CameraSink/CameraSinkWindow.h +++ b/trview.app/Windows/CameraSink/CameraSinkWindow.h @@ -32,7 +32,7 @@ namespace trview static inline const std::string type = "Type"; }; - explicit CameraSinkWindow(const std::shared_ptr& clipboard, const std::weak_ptr& camera, const std::weak_ptr& messaging); + explicit CameraSinkWindow(const std::shared_ptr& clipboard, const std::weak_ptr& camera, const std::shared_ptr& filter_store, const std::weak_ptr& messaging); virtual ~CameraSinkWindow() = default; void render() override; void set_number(int32_t number) override; diff --git a/trview.app/Windows/ItemsWindow.cpp b/trview.app/Windows/ItemsWindow.cpp index b1c2877f5..c98017d31 100644 --- a/trview.app/Windows/ItemsWindow.cpp +++ b/trview.app/Windows/ItemsWindow.cpp @@ -32,7 +32,7 @@ namespace trview void add_item_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("IItem")) + if (filters.has_type_key("Item")) { return; } @@ -55,7 +55,7 @@ namespace trview } auto getters = Filters::GettersBuilder() - .with_type_key("IItem") + .with_type_key("Item") .with_getter("#", [](auto&& item) { return static_cast(item.number()); }) .with_getter("Type", { available_types.begin(), available_types.end() }, [](auto&& item) { return item.type(); }) .with_multi_getter("Category", { available_categories.begin(), available_categories.end() }, [](auto&& item) @@ -75,7 +75,7 @@ namespace trview .with_getter("Angle Degrees", [](auto&& item) { return static_cast(bound_rotation(item.angle()) / 182); }) .with_getter("Type ID", [](auto&& item) { return static_cast(item.type_id()); }, EditMode::Read) .with_getter("Room #", [](auto&& item) { return static_cast(item_room(item)); }, EditMode::Read) - .with_getter>("Room", {}, [](auto&& item) { return item.room(); }, {}, EditMode::Read, "IRoom") + .with_getter>("Room", {}, [](auto&& item) { return item.room(); }, {}, EditMode::Read, "Room") .with_getter("Clear Body", [](auto&& item) { return item.clear_body_flag(); }) .with_getter("Invisible", [](auto&& item) { return item.invisible_flag(); }) .with_getter("Flags", [](auto&& item) { return format_binary(item.activation_flags()); }) @@ -94,7 +94,7 @@ namespace trview } return results; }) - .with_multi_getter>("Trigger", {}, [](auto&& item) { return item.triggers() | std::ranges::to>>(); }, {}, "ITrigger") + .with_multi_getter>("Trigger", {}, [](auto&& item) { return item.triggers() | std::ranges::to>>(); }, {}, "Trigger") .with_multi_getter("NG+", [](auto&& item) { return item.ng_plus() == std::nullopt ? std::vector{} : std::vector{ false,true }; @@ -112,8 +112,8 @@ namespace trview filters.add_getters(getters); } - ItemsWindow::ItemsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging) - : _clipboard(clipboard), _messaging(messaging) + ItemsWindow::ItemsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& filter_store, const std::weak_ptr& messaging) + : _clipboard(clipboard), _messaging(messaging), _filters(filter_store) { _tips["OCB"] = "Changes entity behaviour"; _tips["Clear Body"] = "If true, removed when Bodybag is triggered"; @@ -418,6 +418,7 @@ namespace trview void ItemsWindow::set_number(int32_t number) { _id = "Items " + std::to_string(number); + _filters.set_name(_id); } void ItemsWindow::set_local_selected_item(std::weak_ptr item) @@ -435,7 +436,7 @@ namespace trview { _filters.clear_all_getters(); add_all_filters(_filters, _level); - _filters.set_type_key("IItem"); + _filters.set_type_key("Item"); } void ItemsWindow::set_level_version(trlevel::LevelVersion version) diff --git a/trview.app/Windows/ItemsWindow.h b/trview.app/Windows/ItemsWindow.h index a24f5319d..79c96533f 100644 --- a/trview.app/Windows/ItemsWindow.h +++ b/trview.app/Windows/ItemsWindow.h @@ -32,7 +32,7 @@ namespace trview static inline const std::string auto_hide = "Auto-Hide"; }; - explicit ItemsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging); + explicit ItemsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& filter_store, const std::weak_ptr& messaging); virtual ~ItemsWindow() = default; void set_filters(std::vector filters); virtual void render() override; diff --git a/trview.app/Windows/LightsWindow.cpp b/trview.app/Windows/LightsWindow.cpp index 7c2c2bd12..530e43c83 100644 --- a/trview.app/Windows/LightsWindow.cpp +++ b/trview.app/Windows/LightsWindow.cpp @@ -10,7 +10,7 @@ namespace trview { void add_light_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("ILight")) + if (filters.has_type_key("Light")) { return; } @@ -33,10 +33,10 @@ namespace trview } auto light_getters = Filters::GettersBuilder() - .with_type_key("ILight") + .with_type_key("Light") .with_getter("Type", { available_types.begin(), available_types.end() }, [](auto&& light) { return to_string(light.type()); }) .with_getter("#", [](auto&& light) { return static_cast(light.number()); }) - .with_getter>("Room", {}, [](auto&& light) { return light.room(); }, {}, EditMode::Read, "IRoom") + .with_getter>("Room", {}, [](auto&& light) { return light.room(); }, {}, EditMode::Read, "Room") .with_getter("Room #", [](auto&& light) { return static_cast(light_room(light)); }) .with_getter("X", [](auto&& light) { return static_cast(light.position().x * trlevel::Scale_X); }, has_position) .with_getter("Y", [](auto&& light) { return static_cast(light.position().y * trlevel::Scale_Y); }, has_position) @@ -88,8 +88,8 @@ namespace trview filters.add_getters(light_getters.build()); } - LightsWindow::LightsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging) - : _clipboard(clipboard), _messaging(messaging) + LightsWindow::LightsWindow(const std::shared_ptr& clipboard, const std::shared_ptr& filter_store, const std::weak_ptr& messaging) + : _clipboard(clipboard), _messaging(messaging), _filters(filter_store) { _tips["Direction"] = "Direction is inverted in-game. 3D view shows correct direction."; setup_filters(); @@ -336,6 +336,7 @@ namespace trview void LightsWindow::set_number(int32_t number) { _id = "Lights " + std::to_string(number); + _filters.set_name(_id); } void LightsWindow::set_local_selected_light(const std::weak_ptr& light) @@ -352,7 +353,7 @@ namespace trview { _filters.clear_all_getters(); add_light_filters(_filters, _level); - _filters.set_type_key("ILight"); + _filters.set_type_key("Light"); } void LightsWindow::receive_message(const Message& message) diff --git a/trview.app/Windows/LightsWindow.h b/trview.app/Windows/LightsWindow.h index 29083136a..9018721b7 100644 --- a/trview.app/Windows/LightsWindow.h +++ b/trview.app/Windows/LightsWindow.h @@ -27,7 +27,7 @@ namespace trview static inline const std::string details_panel = "Light Details"; }; - explicit LightsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging); + explicit LightsWindow(const std::shared_ptr& clipboard, const std::shared_ptr& filter_store, const std::weak_ptr& messaging); virtual ~LightsWindow() = default; void clear_selected_light(); void render() override; diff --git a/trview.app/Windows/RoomsWindow.cpp b/trview.app/Windows/RoomsWindow.cpp index b5c278548..d89a4ab5c 100644 --- a/trview.app/Windows/RoomsWindow.cpp +++ b/trview.app/Windows/RoomsWindow.cpp @@ -116,7 +116,7 @@ namespace trview void add_room_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("IRoom")) + if (filters.has_type_key("Room")) { return; } @@ -129,7 +129,7 @@ namespace trview } auto room_getters = Filters::GettersBuilder() - .with_type_key("IRoom") + .with_type_key("Room") .with_getter("#", [](auto&& room) { return static_cast(room.number()); }) .with_getter("Alternate", [](auto&& room) { return room.alternate_room(); }, [](auto&& room) { return room.alternate_mode() != IRoom::AlternateMode::None; }) .with_getter("X size", [](auto&& room) { return static_cast(room.num_x_sectors()); }) @@ -138,11 +138,11 @@ namespace trview .with_getter("Y", [](auto&& room) { return static_cast(room.info().yBottom); }) .with_getter("Z", [](auto&& room) { return static_cast(room.info().z); }) .with_getter("Triggers #", [](auto&& room) { return static_cast(room.triggers().size()); }) - .with_multi_getter>("Triggers", {}, [](auto&& room) { return room.triggers() | std::ranges::to>>(); }, {}, "ITrigger") + .with_multi_getter>("Triggers", {}, [](auto&& room) { return room.triggers() | std::ranges::to>>(); }, {}, "Trigger") .with_getter("Statics #", [](auto&& room) { return static_cast(room.static_meshes().size()); }) - .with_multi_getter>("Statics", {}, [](auto&& room) { return room.static_meshes() | std::ranges::to>>(); }, {}, "IStaticMesh") + .with_multi_getter>("Statics", {}, [](auto&& room) { return room.static_meshes() | std::ranges::to>>(); }, {}, "StaticMesh") .with_getter("Items #", [](auto&& room) { return static_cast(room.items().size()); }) - .with_multi_getter>("Items", {}, [](auto&& room) { return room.items() | std::ranges::to>>(); }, {}, "IItem") + .with_multi_getter>("Items", {}, [](auto&& room) { return room.items() | std::ranges::to>>(); }, {}, "Item") .with_multi_getter>("Neighbours", {}, [](auto&& room) -> std::vector> { if (const auto room_level = room.level().lock()) @@ -152,7 +152,7 @@ namespace trview std::ranges::to>>(); } return {}; - }, {}, "IRoom") + }, {}, "Room") .with_multi_getter("Neighbours #", [](auto&& room) { std::vector results; @@ -267,7 +267,7 @@ namespace trview return room_level->room(room.alternate_room()); } return {}; - }, {}, EditMode::Read, "IRoom") + }, {}, EditMode::Read, "Room") .with_getter("Alternate Group", [](auto&& room) { return room.alternate_group(); }, [](auto&& room) { return room.alternate_mode() != IRoom::AlternateMode::None; }) .with_getter("No Space", room_is_no_space); @@ -371,14 +371,14 @@ namespace trview } return false; }) - .with_multi_getter>("Sectors", {}, [&](auto&& room) { return room.sectors() | std::ranges::to>>(); }, {}, "ISector"); + .with_multi_getter>("Sectors", {}, [&](auto&& room) { return room.sectors() | std::ranges::to>>(); }, {}, "Sector"); filters.add_getters(room_getters.build()); } void add_sector_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("ISector")) + if (filters.has_type_key("Sector")) { return; } @@ -401,7 +401,7 @@ namespace trview | std::ranges::to(); auto sector_getters = Filters::GettersBuilder() - .with_type_key("ISector") + .with_type_key("Sector") .with_multi_getter("Floordata Type", { available_floordata_types.begin(), available_floordata_types.end() }, [=](auto&& sector) { return parse_floordata(floordata, sector.floordata_index(), FloordataMeanings::None, trng, platform_and_version.value_or({ .platform = trlevel::Platform::PC, .version = trlevel::LevelVersion::Tomb1 })).commands @@ -421,8 +421,8 @@ namespace trview filters.add_getters(sector_getters.build()); } - RoomsWindow::RoomsWindow(const IMapRenderer::Source& map_renderer_source, const std::shared_ptr& clipboard, const std::weak_ptr& messaging) - : _map_renderer(map_renderer_source()), _clipboard(clipboard), _messaging(messaging) + RoomsWindow::RoomsWindow(const IMapRenderer::Source& map_renderer_source, const std::shared_ptr& clipboard, const std::shared_ptr& filter_store, const std::weak_ptr& messaging) + : _map_renderer(map_renderer_source()), _clipboard(clipboard), _messaging(messaging), _filters(filter_store) { _token_store += _map_renderer->on_sector_selected += [&](auto sector) { _local_selected_sector = sector; }; @@ -772,13 +772,14 @@ namespace trview void RoomsWindow::set_number(int32_t number) { _id = "Rooms " + std::to_string(number); + _filters.set_name(_id); } void RoomsWindow::generate_filters() { _filters.clear_all_getters(); add_all_filters(_filters, _level); - _filters.set_type_key("IRoom"); + _filters.set_type_key("Room"); } void RoomsWindow::render_properties_tab(const std::shared_ptr& room) @@ -1500,7 +1501,7 @@ namespace trview if (!sector_filters.filters().empty()) { add_sector_filters(sector_filters, _level); - sector_filters.set_type_key("ISector"); + sector_filters.set_type_key("Sector"); return sector_filters; } diff --git a/trview.app/Windows/RoomsWindow.h b/trview.app/Windows/RoomsWindow.h index 550cd3c6a..3982118b1 100644 --- a/trview.app/Windows/RoomsWindow.h +++ b/trview.app/Windows/RoomsWindow.h @@ -32,7 +32,7 @@ namespace trview /// Create a rooms window as a child of the specified window. /// @param device The graphics device /// @param renderer_source The function to call to get a renderer. - explicit RoomsWindow(const IMapRenderer::Source& map_renderer_source, const std::shared_ptr& clipboard, const std::weak_ptr& messaging); + explicit RoomsWindow(const IMapRenderer::Source& map_renderer_source, const std::shared_ptr& clipboard, const std::shared_ptr& filter_store, const std::weak_ptr& messaging); virtual ~RoomsWindow() = default; void clear_selected_trigger(); void render() override; diff --git a/trview.app/Windows/Sounds/SoundsWindow.cpp b/trview.app/Windows/Sounds/SoundsWindow.cpp index 601a958c1..064a66d6f 100644 --- a/trview.app/Windows/Sounds/SoundsWindow.cpp +++ b/trview.app/Windows/Sounds/SoundsWindow.cpp @@ -15,13 +15,13 @@ namespace trview { void add_sounds_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("ISoundSource")) + if (filters.has_type_key("SoundSource")) { return; } auto sound_getters = Filters::GettersBuilder() - .with_type_key("ISoundSource") + .with_type_key("SoundSource") .with_getter("#", [](auto&& sound_source) { return static_cast(sound_source.number()); }) .with_getter("X", [](auto&& sound_source) { return static_cast(sound_source.position().x * trlevel::Scale_X); }) .with_getter("Y", [](auto&& sound_source) { return static_cast(sound_source.position().y * trlevel::Scale_Y); }) @@ -44,8 +44,8 @@ namespace trview filters.add_getters(sound_getters.build()); } - SoundsWindow::SoundsWindow(const std::weak_ptr& messaging) - : _messaging(messaging) + SoundsWindow::SoundsWindow(const std::shared_ptr& filter_store, const std::weak_ptr& messaging) + : _messaging(messaging), _filters(filter_store) { setup_filters(); @@ -98,6 +98,7 @@ namespace trview void SoundsWindow::set_number(int32_t number) { _id = std::format("Sounds {}", number); + _filters.set_name(_id); } void SoundsWindow::set_selected_sound_source(const std::weak_ptr& sound_source) @@ -348,7 +349,7 @@ namespace trview { _filters.clear_all_getters(); add_all_filters(_filters, _level); - _filters.set_type_key("ISoundSource"); + _filters.set_type_key("SoundSource"); } void SoundsWindow::receive_message(const Message& message) diff --git a/trview.app/Windows/Sounds/SoundsWindow.h b/trview.app/Windows/Sounds/SoundsWindow.h index 4cafe14bc..bcbba1ae8 100644 --- a/trview.app/Windows/Sounds/SoundsWindow.h +++ b/trview.app/Windows/Sounds/SoundsWindow.h @@ -31,7 +31,7 @@ namespace trview static inline const std::string sync_sound_source = "Sync"; }; - explicit SoundsWindow(const std::weak_ptr& messaging); + explicit SoundsWindow(const std::shared_ptr& filter_store, const std::weak_ptr& messaging); virtual ~SoundsWindow() = default; void update(float delta) override; void render() override; diff --git a/trview.app/Windows/Statics/StaticsWindow.cpp b/trview.app/Windows/Statics/StaticsWindow.cpp index 213131e75..ad77dc7f9 100644 --- a/trview.app/Windows/Statics/StaticsWindow.cpp +++ b/trview.app/Windows/Statics/StaticsWindow.cpp @@ -9,7 +9,7 @@ namespace trview { void add_static_mesh_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("IStaticMesh")) + if (filters.has_type_key("StaticMesh")) { return; } @@ -27,7 +27,7 @@ namespace trview } auto static_mesh_getters = Filters::GettersBuilder() - .with_type_key("IStaticMesh") + .with_type_key("StaticMesh") .with_getter("#", [](auto&& stat) { return static_cast(stat.number()); }) .with_getter("Type", { available_types.begin(), available_types.end() }, [](auto&& stat) { return to_string(stat.type()); }) .with_getter("X", [](auto&& stat) { return static_cast(stat.position().x * trlevel::Scale_X); }) @@ -35,7 +35,7 @@ namespace trview .with_getter("Z", [](auto&& stat) { return static_cast(stat.position().z * trlevel::Scale_Z); }) .with_getter("Rotation", [](auto&& stat) { return static_cast(DirectX::XMConvertToDegrees(stat.rotation())); }) .with_getter("ID", [](auto&& stat) { return static_cast(stat.id()); }) - .with_getter>("Room", {}, [](auto&& stat) { return stat.room(); }, {}, EditMode::Read, "IRoom") + .with_getter>("Room", {}, [](auto&& stat) { return stat.room(); }, {}, EditMode::Read, "Room") .with_getter("Room #", [](auto&& stat) { return static_cast(static_mesh_room(stat)); }) .with_getter("Breakable", [](auto&& item) { return item.breakable(); }) .with_getter("Flags", [](auto&& stat) { return format_binary(stat.flags()); }) @@ -53,8 +53,8 @@ namespace trview filters.add_getters(static_mesh_getters.build()); } - StaticsWindow::StaticsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging) - : _clipboard(clipboard), _messaging(messaging) + StaticsWindow::StaticsWindow(const std::shared_ptr& clipboard, const std::shared_ptr& filter_store, const std::weak_ptr& messaging) + : _clipboard(clipboard), _messaging(messaging), _filters(filter_store) { setup_filters(); @@ -208,6 +208,7 @@ namespace trview void StaticsWindow::set_number(int32_t number) { _id = std::format("Statics {}", number); + _filters.set_name(_id); } void StaticsWindow::update(float) @@ -225,7 +226,7 @@ namespace trview { _filters.clear_all_getters(); add_all_filters(_filters, _level); - _filters.set_type_key("IStaticMesh"); + _filters.set_type_key("StaticMesh"); } void StaticsWindow::set_local_selected_static_mesh(std::weak_ptr static_mesh) diff --git a/trview.app/Windows/Statics/StaticsWindow.h b/trview.app/Windows/Statics/StaticsWindow.h index c590ef70b..f9186f35d 100644 --- a/trview.app/Windows/Statics/StaticsWindow.h +++ b/trview.app/Windows/Statics/StaticsWindow.h @@ -28,7 +28,7 @@ namespace trview static inline const std::string static_stats = "##staticstats"; }; - explicit StaticsWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging); + explicit StaticsWindow(const std::shared_ptr& clipboard, const std::shared_ptr& filter_store, const std::weak_ptr& messaging); virtual ~StaticsWindow() = default; void initialise(); void render() override; diff --git a/trview.app/Windows/TriggersWindow.cpp b/trview.app/Windows/TriggersWindow.cpp index a328ae47a..9b01486de 100644 --- a/trview.app/Windows/TriggersWindow.cpp +++ b/trview.app/Windows/TriggersWindow.cpp @@ -39,7 +39,7 @@ namespace trview void add_trigger_filters(Filters& filters, const std::weak_ptr& level) { - if (filters.has_type_key("ITrigger")) + if (filters.has_type_key("Trigger")) { return; } @@ -63,14 +63,14 @@ namespace trview } auto getters = Filters::GettersBuilder() - .with_type_key("ITrigger") + .with_type_key("Trigger") .with_getter("Type", { available_types.begin(), available_types.end() }, [](auto&& trigger) { return to_string(trigger.type()); }) .with_getter("#", [](auto&& trigger) { return static_cast(trigger.number()); }) .with_getter("X", [](auto&& trigger) { return static_cast(trigger.position().x * trlevel::Scale_X); }) .with_getter("Y", [](auto&& trigger) { return static_cast(trigger.position().y * trlevel::Scale_Y); }) .with_getter("Z", [](auto&& trigger) { return static_cast(trigger.position().z * trlevel::Scale_Z); }) .with_getter("Room #", [](auto&& trigger) { return static_cast(trigger_room(trigger)); }) - .with_getter>("Room", {}, [](auto&& trigger) { return trigger.room(); }, {}, EditMode::Read, "IRoom") + .with_getter>("Room", {}, [](auto&& trigger) { return trigger.room(); }, {}, EditMode::Read, "Room") .with_getter("Flags", [](auto&& trigger) { return format_binary(trigger.flags()); }) .with_getter("Only once", [](auto&& trigger) { return trigger.only_once(); }) .with_getter("Timer", [](auto&& trigger) { return static_cast(trigger.timer()); }) @@ -101,7 +101,7 @@ namespace trview | std::views::transform([](const auto& i) -> std::shared_ptr { return i.lock(); }) | std::views::filter([](const auto& i) { return i != nullptr; }) | std::ranges::to>>(); - }, {}, "ITrigger") + }, {}, "Trigger") .with_multi_getter("Trigger triggerer #", [=](auto&& trigger) { const auto sector = trigger.sector().lock(); @@ -141,7 +141,7 @@ namespace trview | std::ranges::to>>(); } return {}; - }, {}, "IItem"); + }, {}, "Item"); auto all_trigger_indices = [](TriggerCommandType type, const auto& trigger) @@ -201,8 +201,8 @@ namespace trview filters.add_getters(getters.build()); } - TriggersWindow::TriggersWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging) - : _clipboard(clipboard), _messaging(messaging) + TriggersWindow::TriggersWindow(const std::shared_ptr& clipboard, const std::weak_ptr& filter_store, const std::weak_ptr& messaging) + : _clipboard(clipboard), _messaging(messaging), _filters(filter_store) { setup_filters(); @@ -267,6 +267,7 @@ namespace trview void TriggersWindow::set_number(int32_t number) { _id = "Triggers " + std::to_string(number); + _filters.set_name(_id); } void TriggersWindow::set_selected_trigger(const std::weak_ptr& trigger) @@ -654,7 +655,7 @@ namespace trview { _filters.clear_all_getters(); add_all_filters(_filters, _level); - _filters.set_type_key("ITrigger"); + _filters.set_type_key("Trigger"); } bool TriggersWindow::VirtualCommand::operator == (const VirtualCommand& other) const noexcept diff --git a/trview.app/Windows/TriggersWindow.h b/trview.app/Windows/TriggersWindow.h index 86b10a3c6..f3aadf56f 100644 --- a/trview.app/Windows/TriggersWindow.h +++ b/trview.app/Windows/TriggersWindow.h @@ -34,7 +34,7 @@ namespace trview static inline const std::string colour = "##colour"; }; - explicit TriggersWindow(const std::shared_ptr& clipboard, const std::weak_ptr& messaging); + explicit TriggersWindow(const std::shared_ptr& clipboard, const std::weak_ptr& filter_store, const std::weak_ptr& messaging); virtual ~TriggersWindow() = default; virtual void render() override; void set_triggers(const std::vector>& triggers); diff --git a/trview.app/trview.app.vcxproj b/trview.app/trview.app.vcxproj index f88d12f40..b33d1858e 100644 --- a/trview.app/trview.app.vcxproj +++ b/trview.app/trview.app.vcxproj @@ -44,6 +44,7 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + @@ -105,7 +106,9 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + @@ -122,6 +125,8 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + @@ -223,6 +228,8 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + diff --git a/trview.app/trview.app.vcxproj.filters b/trview.app/trview.app.vcxproj.filters index 9aed543a1..5d5575e16 100644 --- a/trview.app/trview.app.vcxproj.filters +++ b/trview.app/trview.app.vcxproj.filters @@ -120,9 +120,8 @@ - - Settings - + + @@ -366,9 +365,13 @@ - - Settings - + + + + + + + @@ -599,6 +602,15 @@ {a047438d-5a92-49e4-b596-108105134baa} + + {42407171-2e71-4eb4-8831-1c2f46223206} + + + {a790e61b-eddf-4acc-a368-29a916640695} + + + {29828ba3-89c1-4f39-b907-41e2b40d8341} +