From 06a7c85af6f77eb180416210a2dc0ce6b60a84ee Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:53:42 +0000 Subject: [PATCH 01/15] Add empty menu bar to filters Some options for save/open, import/export. Not sure how it will work yet. Start reading filters from directory Shell of filters window Saving and loading first steps Start on save modal Name filter windows Name filter windows so they aren't the same and don't conflict. Camera sink window might need more names because of multiple filters. Fix group present/not present Present/not present for filterables Fixes present and not present for filterables in a group match Make app vcxproj filters one liners Remove I in stored filters Update IFilterStore.h Start showing filters in filters manager window --- trview.app.tests/Filters/FiltersTests.cpp | 38 +++ .../Settings/SettingsLoaderTests.cpp | 4 +- trview.app.ui.tests/ItemsWindowTests.cpp | 4 +- trview.app/ApplicationCreate.cpp | 22 +- trview.app/Filters/FilterStore.cpp | 277 ++++++++++++++++++ trview.app/Filters/FilterStore.h | 35 +++ trview.app/Filters/Filters.cpp | 126 +++++++- trview.app/Filters/Filters.h | 33 ++- trview.app/Filters/IFilterStore.h | 19 ++ trview.app/Mocks/Filters/IFilterStore.h | 20 ++ trview.app/Mocks/Filters/IFilterable.h | 16 + trview.app/Mocks/Mocks.cpp | 8 + trview.app/Resources/resource.h | 1 + trview.app/Resources/trview.app.rc | 1 + trview.app/Settings/SettingsLoader.cpp | 3 + trview.app/Settings/UserSettings.h | 1 + .../Windows/CameraSink/CameraSinkWindow.cpp | 29 +- trview.app/Windows/Filters/FiltersWindow.cpp | 118 ++++++++ trview.app/Windows/Filters/FiltersWindow.h | 46 +++ trview.app/Windows/ItemsWindow.cpp | 15 +- trview.app/Windows/ItemsWindow.h | 2 +- trview.app/Windows/LightsWindow.cpp | 9 +- trview.app/Windows/RoomsWindow.cpp | 25 +- trview.app/Windows/Sounds/SoundsWindow.cpp | 7 +- trview.app/Windows/Statics/StaticsWindow.cpp | 9 +- trview.app/Windows/TriggersWindow.cpp | 13 +- trview.app/Windows/Windows.cpp | 5 + trview.app/trview.app.vcxproj | 7 + trview.app/trview.app.vcxproj.filters | 21 +- 29 files changed, 834 insertions(+), 80 deletions(-) create mode 100644 trview.app/Filters/FilterStore.cpp create mode 100644 trview.app/Filters/FilterStore.h create mode 100644 trview.app/Filters/IFilterStore.h create mode 100644 trview.app/Mocks/Filters/IFilterStore.h create mode 100644 trview.app/Mocks/Filters/IFilterable.h create mode 100644 trview.app/Windows/Filters/FiltersWindow.cpp create mode 100644 trview.app/Windows/Filters/FiltersWindow.h 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/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.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/ApplicationCreate.cpp b/trview.app/ApplicationCreate.cpp index 4ea8361f2..a1a4f81af 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" @@ -85,6 +86,7 @@ #include "Windows/Pack/PackWindow.h" #include "UI/LevelInfo.h" #include "Elements/Level/LevelNameLookup.h" +#include "Windows/Filters/FiltersWindow.h" #include #include @@ -168,6 +170,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 +390,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,7 +408,7 @@ 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; @@ -499,6 +509,13 @@ namespace trview pack_window->initialise(); return pack_window; }; + auto filters_window_source = [=]() + { + auto filters_window = std::make_shared(filters, shell, dialogs, messaging); + messaging->add_recipient(filters_window); + filters_window->initialise(); + return filters_window; + }; auto windows = std::make_shared(window, shortcuts); windows->register_window("About", about_window_source); @@ -516,6 +533,7 @@ namespace trview windows->register_window("Statics", statics_window_source); windows->register_window("Textures", textures_window_source); windows->register_window("Triggers", triggers_window_source); + windows->register_window("Filters", filters_window_source); auto viewer_ui = std::make_shared( window, diff --git a/trview.app/Filters/FilterStore.cpp b/trview.app/Filters/FilterStore.cpp new file mode 100644 index 000000000..b3b0e4785 --- /dev/null +++ b/trview.app/Filters/FilterStore.cpp @@ -0,0 +1,277 @@ +#include "FilterStore.h" +#include "../Messages/Messages.h" + +#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.push_back( + { + .name = key, + .filename = "C:\\dev\\trview-filters\\new.json", + .filter = filter + }); + } + + void FilterStore::load() + { + // TODO: Load filters from files in the directories. + // For now let's just use the appdata directory. + if (!_settings.filter_directory.empty()) + { + const auto files = _files->get_files(_settings.filter_directory, "\\*.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.push_back(new_filter); + } + catch (...) + { + } + } + } + } + + std::map FilterStore::filters() const + { + std::map results; + for (const auto& filter : _filters) + { + results[filter.name] = filter.filter; + } + return results; + } + + std::map FilterStore::filters_for_key(const std::string& key) const + { + std::map results; + for (const auto& filter : _filters) + { + if (filter.filter.type_key == key) + { + 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::save() + { + const auto dir = _settings.filter_directory.empty() ? + (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; + _files->create_directory(dir); + + for (const auto& filter : _filters) + { + try + { + nlohmann::json json; + json["name"] = filter.name; + json["filter"] = filter.filter; + _files->save_file(dir + "\\" + filter.name + ".json", json.dump()); + } + catch (...) + { + } + } + } +} diff --git a/trview.app/Filters/FilterStore.h b/trview.app/Filters/FilterStore.h new file mode 100644 index 000000000..b36f7b06a --- /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() const override; + std::map filters_for_key(const std::string& key) const override; + void receive_message(const Message& message) override; + void save() override; + private: + struct StoredFilter + { + std::string name; + std::string filename; + Filters::Filter filter; + }; + + std::shared_ptr _files; + std::vector _filters; + UserSettings _settings; + }; +} diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index 79a3da9ac..af2a2b95f 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -1,4 +1,5 @@ #include "Filters.h" +#include "IFilterStore.h" namespace trview { @@ -43,6 +44,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 +155,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 +234,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 +468,15 @@ 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(); + 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 +484,59 @@ namespace trview } } + void Filters::render_filters() + { + render(_filter, 0, 0, _filter, _filter.type_key); + } + + void Filters::render_filter_name_modal() + { + if (!_save_modal_open) + { + return; + } + + if (_save_modal_open.value()) + { + ImGui::OpenPopup("Choose Filter Name"); + _save_modal_open = false; + _save_modal_is_open = true; + } + + if (ImGui::BeginPopupModal("Choose Filter Name", &_save_modal_is_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (ImGui::IsWindowAppearing()) + { + ImGui::SetKeyboardFocusHere(); + } + + std::string name_value; + if (ImGui::InputText("Filter Name", &name_value, ImGuiInputTextFlags_EnterReturnsTrue)) + { + if (const auto store = _filter_store.lock()) + { + store->add(name_value, _filter); + store->save(); + } + _save_modal_open.reset(); + ImGui::CloseCurrentPopup(); + } + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + { + _save_modal_open.reset(); + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + if (!_save_modal_is_open) + { + _save_modal_open.reset(); + } + } + 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 +907,6 @@ namespace trview void Filters::toggle_visible() { - if (!_show_filters) - { - ImGui::OpenPopup(Names::Popup.c_str()); - } _show_filters = !_show_filters; } @@ -847,4 +919,42 @@ namespace trview .multi_getters = _multi_getters }; } + + void Filters::render_menu_bar() + { + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::BeginMenu("Open")) + { + if (const auto store = _filter_store.lock()) + { + for (const auto& value : store->filters_for_key(_filter.type_key)) + { + if (ImGui::MenuItem(value.first.c_str())) + { + _filter = { value.second }; + } + } + } + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("Save")) + { + _save_modal_open = true; + } + + ImGui::EndMenu(); + } + + 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..d5a158764 100644 --- a/trview.app/Filters/Filters.h +++ b/trview.app/Filters/Filters.h @@ -12,6 +12,8 @@ namespace trview { + struct IFilterStore; + enum class CompareOp { Equal, @@ -148,6 +150,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,10 +164,12 @@ 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; void render(); + void render_filters(); void render_settings(); void render_table(const std::ranges::forward_range auto& items, std::ranges::forward_range auto& all_items, @@ -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,22 @@ 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(); + + 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::optional _save_modal_open; + bool _save_modal_is_open{ false }; + std::string _id; }; 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..390b2cb68 --- /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() const = 0; + virtual std::map filters_for_key(const std::string& key) const = 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..955d35eac --- /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, (), (const, override)); + MOCK_METHOD((std::map), filters_for_key, (const std::string&), (const, 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/Resources/resource.h b/trview.app/Resources/resource.h index 09dff1b60..26e23ccc8 100644 --- a/trview.app/Resources/resource.h +++ b/trview.app/Resources/resource.h @@ -54,6 +54,7 @@ #define ID_WINDOWS_DIFF 33031 #define ID_WINDOWS_PACK 33032 #define IDR_LEVEL_HASHES 33033 +#define ID_WINDOWS_FILTERS 33034 // Next default values for new objects // diff --git a/trview.app/Resources/trview.app.rc b/trview.app/Resources/trview.app.rc index 9698b36d6..b05af03f4 100644 --- a/trview.app/Resources/trview.app.rc +++ b/trview.app/Resources/trview.app.rc @@ -79,6 +79,7 @@ BEGIN MENUITEM "Sounds" ID_WINDOWS_SOUNDS MENUITEM "Diff\tCtrl+D" ID_WINDOWS_DIFF MENUITEM "Pack" ID_WINDOWS_PACK + MENUITEM "Filters" ID_WINDOWS_FILTERS MENUITEM SEPARATOR MENUITEM "Camera Position" ID_WINDOWS_CAMERA_POSITION MENUITEM SEPARATOR 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/Windows/CameraSink/CameraSinkWindow.cpp b/trview.app/Windows/CameraSink/CameraSinkWindow.cpp index 146123677..d525f31ac 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(); }) @@ -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/Filters/FiltersWindow.cpp b/trview.app/Windows/Filters/FiltersWindow.cpp new file mode 100644 index 000000000..cc00755ed --- /dev/null +++ b/trview.app/Windows/Filters/FiltersWindow.cpp @@ -0,0 +1,118 @@ +#include "FiltersWindow.h" +#include "../../Messages/Messages.h" +#include "../../Elements/ElementFilters.h" + +namespace trview +{ + FiltersWindow::FiltersWindow(const std::weak_ptr& filter_store, + const std::shared_ptr& shell, + const std::shared_ptr& dialogs, + const std::weak_ptr& messaging) + : _filter_store(filter_store), _messaging(messaging), _shell(shell), _dialogs(dialogs) + { + add_all_filters(_filters, {}); + } + + void FiltersWindow::initialise() + { + messages::get_settings(_messaging, weak_from_this()); + } + + void FiltersWindow::render() + { + if (!_settings) + { + messages::get_settings(_messaging, weak_from_this()); + return; + } + + if (!render_filters_window()) + { + on_window_closed(); + return; + } + } + + void FiltersWindow::set_number(int32_t number) + { + _id = std::format("Filters Manager {}", number); + } + + void FiltersWindow::update(float) + { + } + + void FiltersWindow::receive_message(const Message& message) + { + if (auto settings = messages::read_settings(message)) + { + _settings = settings.value(); + } + } + + bool FiltersWindow::render_filters_window() + { + bool stay_open = true; + ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(540, 500)); + if (ImGui::Begin(_id.c_str(), &stay_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (auto filter_store = _filter_store.lock()) + { + const auto filters_map = filter_store->filters(); + const auto filters = filters_map | + std::views::transform([](auto&& f) + { + return f.second; + }) | std::ranges::to(); + const std::vector keys = filters_map | + std::views::transform([](auto&& f) + { + return std::format("{} | {}", f.second.type_key, f.first); + }) | + std::ranges::to(); + + const std::string preview = _selected_filter < keys.size() ? keys[_selected_filter] : ""; + if (ImGui::BeginCombo("Filter", preview.c_str())) + { + for (std::size_t n = 0; n < keys.size(); ++n) + { + bool is_selected = _selected_filter == n; + if (ImGui::Selectable(keys[n].c_str(), is_selected)) + { + _selected_filter = static_cast(n); + + auto found = filters[_selected_filter]; + _filters.set_filters(found.children); + _filters.set_type_key(found.type_key); + } + + if (is_selected) + { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + const auto set_filters = _filters.filters(); + if (!set_filters.empty()) + { + _filters.render_filters(); + } + } + } + ImGui::End(); + ImGui::PopStyleVar(); + return stay_open; + } + + std::string FiltersWindow::type() const + { + return "Filters Manager"; + } + + std::string FiltersWindow::title() const + { + return _id; + } +} diff --git a/trview.app/Windows/Filters/FiltersWindow.h b/trview.app/Windows/Filters/FiltersWindow.h new file mode 100644 index 000000000..a3576f203 --- /dev/null +++ b/trview.app/Windows/Filters/FiltersWindow.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "../IWindow.h" +#include "../../Filters/Filters.h" +#include "../../Filters/IFilterStore.h" +#include "../../Settings/UserSettings.h" + +namespace trview +{ + class FiltersWindow final : public IWindow, public std::enable_shared_from_this + { + public: + struct Names + { + static inline const std::string filters_list = "Filters"; + }; + + explicit FiltersWindow(const std::weak_ptr& filter_store, + const std::shared_ptr& shell, + const std::shared_ptr& dialogs, + const std::weak_ptr& messaging); + virtual ~FiltersWindow() = default; + void initialise(); + void render() override; + void set_number(int32_t number) override; + void update(float dt) override; + void receive_message(const Message& message) override; + std::string type() const override; + std::string title() const override; + private: + bool render_filters_window(); + + std::string _id{ "Plugins 0" }; + std::weak_ptr _filter_store; + std::shared_ptr _shell; + std::shared_ptr _dialogs; + std::optional _settings; + std::weak_ptr _messaging; + int32_t _selected_filter{ 0 }; + Filters _filters; + }; +} 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..6c8e0e205 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) @@ -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/RoomsWindow.cpp b/trview.app/Windows/RoomsWindow.cpp index b5c278548..e0c679a3a 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 @@ -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/Sounds/SoundsWindow.cpp b/trview.app/Windows/Sounds/SoundsWindow.cpp index 601a958c1..71e36e203 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); }) @@ -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/Statics/StaticsWindow.cpp b/trview.app/Windows/Statics/StaticsWindow.cpp index 213131e75..bd11e0674 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()); }) @@ -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/TriggersWindow.cpp b/trview.app/Windows/TriggersWindow.cpp index a328ae47a..49b224a06 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) @@ -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/Windows.cpp b/trview.app/Windows/Windows.cpp index 5113f012f..23c7e5659 100644 --- a/trview.app/Windows/Windows.cpp +++ b/trview.app/Windows/Windows.cpp @@ -57,6 +57,11 @@ namespace trview create("Diff"); break; } + case ID_WINDOWS_FILTERS: + { + create("Filters"); + break; + } case ID_WINDOWS_ITEMS: { create("Items"); diff --git a/trview.app/trview.app.vcxproj b/trview.app/trview.app.vcxproj index f88d12f40..411057c12 100644 --- a/trview.app/trview.app.vcxproj +++ b/trview.app/trview.app.vcxproj @@ -44,6 +44,7 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + @@ -91,6 +92,7 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + @@ -105,7 +107,9 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + @@ -122,6 +126,8 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + @@ -228,6 +234,7 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + diff --git a/trview.app/trview.app.vcxproj.filters b/trview.app/trview.app.vcxproj.filters index 9aed543a1..711e1386c 100644 --- a/trview.app/trview.app.vcxproj.filters +++ b/trview.app/trview.app.vcxproj.filters @@ -120,9 +120,9 @@ - - Settings - + + + @@ -366,9 +366,12 @@ - - Settings - + + + + + + @@ -599,6 +602,12 @@ {a047438d-5a92-49e4-b596-108105134baa} + + {42407171-2e71-4eb4-8831-1c2f46223206} + + + {a790e61b-eddf-4acc-a368-29a916640695} + From 7355ffb68c2ab9cda33c6c45c60c97a8da71aa1d Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:53:55 +0000 Subject: [PATCH 02/15] Move modal code to class --- trview.app/Filters/Filters.cpp | 93 ++++++++++++--------------- trview.app/Filters/Filters.h | 10 ++- trview.app/UI/Modal.h | 18 ++++++ trview.app/UI/Modal.hpp | 42 ++++++++++++ trview.app/trview.app.vcxproj | 2 + trview.app/trview.app.vcxproj.filters | 9 +++ 6 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 trview.app/UI/Modal.h create mode 100644 trview.app/UI/Modal.hpp diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index af2a2b95f..d92be410f 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -470,6 +470,7 @@ namespace trview if (_show_filters) { + 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(); @@ -491,50 +492,42 @@ namespace trview void Filters::render_filter_name_modal() { - if (!_save_modal_open) - { - return; - } - - if (_save_modal_open.value()) - { - ImGui::OpenPopup("Choose Filter Name"); - _save_modal_open = false; - _save_modal_is_open = true; - } - - if (ImGui::BeginPopupModal("Choose Filter Name", &_save_modal_is_open, ImGuiWindowFlags_AlwaysAutoResize)) - { - if (ImGui::IsWindowAppearing()) + _save_modal.render( + [&](auto&& state) { - ImGui::SetKeyboardFocusHere(); - } + if (ImGui::IsWindowAppearing()) + { + ImGui::SetKeyboardFocusHere(); + } - std::string name_value; - if (ImGui::InputText("Filter Name", &name_value, ImGuiInputTextFlags_EnterReturnsTrue)) - { - if (const auto store = _filter_store.lock()) + if (ImGui::InputText("##Filter Name", &state.name_value, ImGuiInputTextFlags_EnterReturnsTrue)) { - store->add(name_value, _filter); - store->save(); + if (const auto store = _filter_store.lock()) + { + store->add(state.name_value, _filter); + store->save(); + } + return false; } - _save_modal_open.reset(); - ImGui::CloseCurrentPopup(); - } - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) - { - _save_modal_open.reset(); - ImGui::CloseCurrentPopup(); - } + ImGui::SameLine(); + if (ImGui::Button("Save")) + { + if (const auto store = _filter_store.lock()) + { + store->add(state.name_value, _filter); + store->save(); + } + return false; + } - ImGui::EndPopup(); - } + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + { + return false; + } - if (!_save_modal_is_open) - { - _save_modal_open.reset(); - } + return true; + }); } Filters::Action Filters::render(Filter& filter, int32_t depth, int32_t index, Filter& parent, const std::string& type_key) @@ -924,31 +917,27 @@ namespace trview { if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("File")) + if (ImGui::BeginMenu("Open")) { - if (ImGui::BeginMenu("Open")) + if (const auto store = _filter_store.lock()) { - if (const auto store = _filter_store.lock()) + for (const auto& value : store->filters_for_key(_filter.type_key)) { - for (const auto& value : store->filters_for_key(_filter.type_key)) + if (ImGui::MenuItem(value.first.c_str())) { - if (ImGui::MenuItem(value.first.c_str())) - { - _filter = { value.second }; - } + _filter = { value.second }; } } - ImGui::EndMenu(); - } - - if (ImGui::MenuItem("Save")) - { - _save_modal_open = true; } - ImGui::EndMenu(); } + if (ImGui::MenuItem("Save")) + { + _save_modal.show({}); + // _save_modal_open = true; + } + ImGui::EndMenuBar(); } } diff --git a/trview.app/Filters/Filters.h b/trview.app/Filters/Filters.h index d5a158764..a74bf0629 100644 --- a/trview.app/Filters/Filters.h +++ b/trview.app/Filters/Filters.h @@ -9,6 +9,7 @@ #include "../Windows/RowCounter.h" #include "IFilterable.h" +#include "../UI/Modal.h" namespace trview { @@ -199,6 +200,7 @@ namespace trview 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); void render_menu_bar(); + void render_menu_bar2(); void render_filter_name_modal(); bool _changed{ true }; @@ -211,9 +213,13 @@ namespace trview mutable bool _scroll_to_item{ false }; bool _show_filters{ false }; std::weak_ptr _filter_store; - std::optional _save_modal_open; - bool _save_modal_is_open{ false }; std::string _id; + + struct ModalState + { + std::string name_value; + }; + Modal _save_modal; }; constexpr std::string to_string(CompareOp op) noexcept; diff --git a/trview.app/UI/Modal.h b/trview.app/UI/Modal.h new file mode 100644 index 000000000..393ed79c4 --- /dev/null +++ b/trview.app/UI/Modal.h @@ -0,0 +1,18 @@ +#pragma once + +namespace trview +{ + template + class Modal + { + public: + void show(const State& state); + void render(const std::function& callback); + private: + 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..e91da9367 --- /dev/null +++ b/trview.app/UI/Modal.hpp @@ -0,0 +1,42 @@ +#pragma once + +namespace trview +{ + template + void Modal::show(const State& state) + { + _state = state; + _open = true; + } + + template + void Modal::render(const std::function& callback) + { + if (!_open) + { + return; + } + + if (_open.value()) + { + ImGui::OpenPopup("Enter Filter Name"); + _open = false; + _is_open = true; + } + + if (ImGui::BeginPopupModal("Enter Filter Name", &_is_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (!callback(_state)) + { + ImGui::CloseCurrentPopup(); + _open.reset(); + } + ImGui::EndPopup(); + } + + if (!_open) + { + _open.reset(); + } + } +} diff --git a/trview.app/trview.app.vcxproj b/trview.app/trview.app.vcxproj index 411057c12..1589961ee 100644 --- a/trview.app/trview.app.vcxproj +++ b/trview.app/trview.app.vcxproj @@ -229,6 +229,8 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" + + diff --git a/trview.app/trview.app.vcxproj.filters b/trview.app/trview.app.vcxproj.filters index 711e1386c..e6b910509 100644 --- a/trview.app/trview.app.vcxproj.filters +++ b/trview.app/trview.app.vcxproj.filters @@ -372,6 +372,12 @@ + + UI\Modal + + + UI\Modal + @@ -608,6 +614,9 @@ {a790e61b-eddf-4acc-a368-29a916640695} + + {29828ba3-89c1-4f39-b907-41e2b40d8341} + From bd922307b6db204c5b6c4d0c4e82ae74e42a20d0 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:04:06 +0000 Subject: [PATCH 03/15] More modal tweaks --- trview.app/Filters/Filters.cpp | 3 +-- trview.app/UI/Modal.h | 3 ++- trview.app/UI/Modal.hpp | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index d92be410f..c0b16ee96 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -934,8 +934,7 @@ namespace trview if (ImGui::MenuItem("Save")) { - _save_modal.show({}); - // _save_modal_open = true; + _save_modal.show("Enter Filter Name", {}); } ImGui::EndMenuBar(); diff --git a/trview.app/UI/Modal.h b/trview.app/UI/Modal.h index 393ed79c4..2aefa7866 100644 --- a/trview.app/UI/Modal.h +++ b/trview.app/UI/Modal.h @@ -6,9 +6,10 @@ namespace trview class Modal { public: - void show(const State& state); + 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 }; diff --git a/trview.app/UI/Modal.hpp b/trview.app/UI/Modal.hpp index e91da9367..6c2325ccd 100644 --- a/trview.app/UI/Modal.hpp +++ b/trview.app/UI/Modal.hpp @@ -3,8 +3,9 @@ namespace trview { template - void Modal::show(const State& state) + void Modal::show(const std::string& id, const State& state) { + _id = id; _state = state; _open = true; } @@ -19,12 +20,12 @@ namespace trview if (_open.value()) { - ImGui::OpenPopup("Enter Filter Name"); + ImGui::OpenPopup(_id.c_str()); _open = false; _is_open = true; } - if (ImGui::BeginPopupModal("Enter Filter Name", &_is_open, ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::BeginPopupModal(_id.c_str(), &_is_open, ImGuiWindowFlags_AlwaysAutoResize)) { if (!callback(_state)) { From 479ff039b4fd660d14151a4f862f0456ce3fe52b Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:22:56 +0000 Subject: [PATCH 04/15] Name on load --- trview.app/Filters/Filters.cpp | 5 ++++- trview.app/Filters/Filters.h | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index c0b16ee96..367a39a80 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -506,6 +506,7 @@ namespace trview { store->add(state.name_value, _filter); store->save(); + _name = state.name_value; } return false; } @@ -517,6 +518,7 @@ namespace trview { store->add(state.name_value, _filter); store->save(); + _name = state.name_value; } return false; } @@ -926,6 +928,7 @@ namespace trview if (ImGui::MenuItem(value.first.c_str())) { _filter = { value.second }; + _name = value.first; } } } @@ -934,7 +937,7 @@ namespace trview if (ImGui::MenuItem("Save")) { - _save_modal.show("Enter Filter Name", {}); + _save_modal.show("Enter Filter Name", { .name_value = _name }); } ImGui::EndMenuBar(); diff --git a/trview.app/Filters/Filters.h b/trview.app/Filters/Filters.h index a74bf0629..d577adbb0 100644 --- a/trview.app/Filters/Filters.h +++ b/trview.app/Filters/Filters.h @@ -214,6 +214,7 @@ namespace trview bool _show_filters{ false }; std::weak_ptr _filter_store; std::string _id; + std::string _name; struct ModalState { From 546b15e849dbd4c6e9c1797945456c65eea52a4c Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:29:01 +0000 Subject: [PATCH 05/15] Disable open if no filters --- trview.app/Filters/Filters.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index 367a39a80..bb7c1cd5d 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -919,17 +919,16 @@ namespace trview { if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("Open")) + 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 (const auto store = _filter_store.lock()) + for (const auto& value : values) { - for (const auto& value : store->filters_for_key(_filter.type_key)) + if (ImGui::MenuItem(value.first.c_str())) { - if (ImGui::MenuItem(value.first.c_str())) - { - _filter = { value.second }; - _name = value.first; - } + _filter = { value.second }; + _name = value.first; } } ImGui::EndMenu(); From e832cdc967e9a648981802adcceb48539a0b12be Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:33:52 +0000 Subject: [PATCH 06/15] Add store to triggers window --- trview.app.ui.tests/TriggersWindowTests.cpp | 4 +++- trview.app/ApplicationCreate.cpp | 2 +- trview.app/Windows/TriggersWindow.cpp | 4 ++-- trview.app/Windows/TriggersWindow.h | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) 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 a1a4f81af..2c2c36755 100644 --- a/trview.app/ApplicationCreate.cpp +++ b/trview.app/ApplicationCreate.cpp @@ -426,7 +426,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; diff --git a/trview.app/Windows/TriggersWindow.cpp b/trview.app/Windows/TriggersWindow.cpp index 49b224a06..9b01486de 100644 --- a/trview.app/Windows/TriggersWindow.cpp +++ b/trview.app/Windows/TriggersWindow.cpp @@ -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(); 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); From f80eef1957091249e414db8b114e5a8f898d9a76 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:18:07 +0000 Subject: [PATCH 07/15] Pass store to filters Pass filter store to more filter windows More management from the filter windows. #1548 --- trview.app.tests/RoomsWindowTests.cpp | 4 +++- trview.app.ui.tests/CameraSinkWindowTests.cpp | 4 +++- trview.app.ui.tests/LightsWindowTests.cpp | 4 +++- trview.app.ui.tests/RoomsWindowTests.cpp | 4 +++- trview.app.ui.tests/StaticsWindowTests.cpp | 4 +++- trview.app/ApplicationCreate.cpp | 10 +++++----- trview.app/Filters/FilterStore.cpp | 16 ++++++++++++++++ trview.app/Filters/FilterStore.h | 1 + trview.app/Filters/Filters.cpp | 14 ++++++++++++++ trview.app/Filters/IFilterStore.h | 1 + trview.app/Mocks/Filters/IFilterStore.h | 1 + .../Windows/CameraSink/CameraSinkWindow.cpp | 4 ++-- trview.app/Windows/CameraSink/CameraSinkWindow.h | 2 +- trview.app/Windows/LightsWindow.cpp | 4 ++-- trview.app/Windows/LightsWindow.h | 2 +- trview.app/Windows/RoomsWindow.cpp | 4 ++-- trview.app/Windows/RoomsWindow.h | 2 +- trview.app/Windows/Sounds/SoundsWindow.cpp | 4 ++-- trview.app/Windows/Sounds/SoundsWindow.h | 2 +- trview.app/Windows/Statics/StaticsWindow.cpp | 4 ++-- trview.app/Windows/Statics/StaticsWindow.h | 2 +- 21 files changed, 68 insertions(+), 25 deletions(-) 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.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/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/ApplicationCreate.cpp b/trview.app/ApplicationCreate.cpp index 2c2c36755..0320d8178 100644 --- a/trview.app/ApplicationCreate.cpp +++ b/trview.app/ApplicationCreate.cpp @@ -415,7 +415,7 @@ namespace trview }; 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; @@ -440,7 +440,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; @@ -450,7 +450,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; @@ -466,14 +466,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 index b3b0e4785..8d732aec9 100644 --- a/trview.app/Filters/FilterStore.cpp +++ b/trview.app/Filters/FilterStore.cpp @@ -5,6 +5,8 @@ #include #include +#include + namespace trview { void from_json(const nlohmann::json& json, CompareOp& op) @@ -254,6 +256,20 @@ namespace trview } } + void FilterStore::remove(const std::string& name) + { + const auto found = std::ranges::find_if(_filters, [&](auto&& f) { return f.name == name; }); + if (found == _filters.end()) + { + return; + } + + const auto dir = _settings.filter_directory.empty() ? + (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; + _files->delete_file(found->filename); + _filters.erase(found); + } + void FilterStore::save() { const auto dir = _settings.filter_directory.empty() ? diff --git a/trview.app/Filters/FilterStore.h b/trview.app/Filters/FilterStore.h index b36f7b06a..1c32e57d3 100644 --- a/trview.app/Filters/FilterStore.h +++ b/trview.app/Filters/FilterStore.h @@ -19,6 +19,7 @@ namespace trview std::map filters() const override; std::map filters_for_key(const std::string& key) const override; void receive_message(const Message& message) override; + void remove(const std::string& name) override; void save() override; private: struct StoredFilter diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index bb7c1cd5d..a063b742b 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -930,6 +930,15 @@ namespace trview _filter = { value.second }; _name = value.first; } + + if (ImGui::BeginPopupContextItem()) + { + if (ImGui::MenuItem("Delete")) + { + store->remove(value.first); + } + ImGui::EndPopup(); + } } ImGui::EndMenu(); } @@ -939,6 +948,11 @@ namespace trview _save_modal.show("Enter Filter Name", { .name_value = _name }); } + if (ImGui::MenuItem("Clear")) + { + _filter.children = {}; + } + ImGui::EndMenuBar(); } } diff --git a/trview.app/Filters/IFilterStore.h b/trview.app/Filters/IFilterStore.h index 390b2cb68..20b15dcae 100644 --- a/trview.app/Filters/IFilterStore.h +++ b/trview.app/Filters/IFilterStore.h @@ -14,6 +14,7 @@ namespace trview virtual void load() = 0; virtual std::map filters() const = 0; virtual std::map filters_for_key(const std::string& key) const = 0; + virtual void remove(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 index 955d35eac..f6d060647 100644 --- a/trview.app/Mocks/Filters/IFilterStore.h +++ b/trview.app/Mocks/Filters/IFilterStore.h @@ -14,6 +14,7 @@ namespace trview MOCK_METHOD(void, load, (), (override)); MOCK_METHOD((std::map), filters, (), (const, override)); MOCK_METHOD((std::map), filters_for_key, (const std::string&), (const, override)); + MOCK_METHOD(void, remove, (const std::string&), (override)); MOCK_METHOD(void, save, (), (override)); }; } diff --git a/trview.app/Windows/CameraSink/CameraSinkWindow.cpp b/trview.app/Windows/CameraSink/CameraSinkWindow.cpp index d525f31ac..969843972 100644 --- a/trview.app/Windows/CameraSink/CameraSinkWindow.cpp +++ b/trview.app/Windows/CameraSink/CameraSinkWindow.cpp @@ -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(); 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/LightsWindow.cpp b/trview.app/Windows/LightsWindow.cpp index 6c8e0e205..530e43c83 100644 --- a/trview.app/Windows/LightsWindow.cpp +++ b/trview.app/Windows/LightsWindow.cpp @@ -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(); 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 e0c679a3a..d89a4ab5c 100644 --- a/trview.app/Windows/RoomsWindow.cpp +++ b/trview.app/Windows/RoomsWindow.cpp @@ -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; }; 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 71e36e203..064a66d6f 100644 --- a/trview.app/Windows/Sounds/SoundsWindow.cpp +++ b/trview.app/Windows/Sounds/SoundsWindow.cpp @@ -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(); 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 bd11e0674..ad77dc7f9 100644 --- a/trview.app/Windows/Statics/StaticsWindow.cpp +++ b/trview.app/Windows/Statics/StaticsWindow.cpp @@ -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(); 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; From 511b5ac90c5cd1a69fb124bbd0ca0238fc1ce868 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sat, 21 Mar 2026 20:23:38 +0000 Subject: [PATCH 08/15] Remove filters window --- trview.app/ApplicationCreate.cpp | 9 -- trview.app/Resources/resource.h | 1 - trview.app/Resources/trview.app.rc | 1 - trview.app/Windows/Filters/FiltersWindow.cpp | 118 ------------------- trview.app/Windows/Filters/FiltersWindow.h | 46 -------- trview.app/Windows/Windows.cpp | 5 - trview.app/trview.app.vcxproj | 2 - trview.app/trview.app.vcxproj.filters | 2 - 8 files changed, 184 deletions(-) delete mode 100644 trview.app/Windows/Filters/FiltersWindow.cpp delete mode 100644 trview.app/Windows/Filters/FiltersWindow.h diff --git a/trview.app/ApplicationCreate.cpp b/trview.app/ApplicationCreate.cpp index 0320d8178..084547c90 100644 --- a/trview.app/ApplicationCreate.cpp +++ b/trview.app/ApplicationCreate.cpp @@ -86,7 +86,6 @@ #include "Windows/Pack/PackWindow.h" #include "UI/LevelInfo.h" #include "Elements/Level/LevelNameLookup.h" -#include "Windows/Filters/FiltersWindow.h" #include #include @@ -509,13 +508,6 @@ namespace trview pack_window->initialise(); return pack_window; }; - auto filters_window_source = [=]() - { - auto filters_window = std::make_shared(filters, shell, dialogs, messaging); - messaging->add_recipient(filters_window); - filters_window->initialise(); - return filters_window; - }; auto windows = std::make_shared(window, shortcuts); windows->register_window("About", about_window_source); @@ -533,7 +525,6 @@ namespace trview windows->register_window("Statics", statics_window_source); windows->register_window("Textures", textures_window_source); windows->register_window("Triggers", triggers_window_source); - windows->register_window("Filters", filters_window_source); auto viewer_ui = std::make_shared( window, diff --git a/trview.app/Resources/resource.h b/trview.app/Resources/resource.h index 26e23ccc8..09dff1b60 100644 --- a/trview.app/Resources/resource.h +++ b/trview.app/Resources/resource.h @@ -54,7 +54,6 @@ #define ID_WINDOWS_DIFF 33031 #define ID_WINDOWS_PACK 33032 #define IDR_LEVEL_HASHES 33033 -#define ID_WINDOWS_FILTERS 33034 // Next default values for new objects // diff --git a/trview.app/Resources/trview.app.rc b/trview.app/Resources/trview.app.rc index b05af03f4..9698b36d6 100644 --- a/trview.app/Resources/trview.app.rc +++ b/trview.app/Resources/trview.app.rc @@ -79,7 +79,6 @@ BEGIN MENUITEM "Sounds" ID_WINDOWS_SOUNDS MENUITEM "Diff\tCtrl+D" ID_WINDOWS_DIFF MENUITEM "Pack" ID_WINDOWS_PACK - MENUITEM "Filters" ID_WINDOWS_FILTERS MENUITEM SEPARATOR MENUITEM "Camera Position" ID_WINDOWS_CAMERA_POSITION MENUITEM SEPARATOR diff --git a/trview.app/Windows/Filters/FiltersWindow.cpp b/trview.app/Windows/Filters/FiltersWindow.cpp deleted file mode 100644 index cc00755ed..000000000 --- a/trview.app/Windows/Filters/FiltersWindow.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "FiltersWindow.h" -#include "../../Messages/Messages.h" -#include "../../Elements/ElementFilters.h" - -namespace trview -{ - FiltersWindow::FiltersWindow(const std::weak_ptr& filter_store, - const std::shared_ptr& shell, - const std::shared_ptr& dialogs, - const std::weak_ptr& messaging) - : _filter_store(filter_store), _messaging(messaging), _shell(shell), _dialogs(dialogs) - { - add_all_filters(_filters, {}); - } - - void FiltersWindow::initialise() - { - messages::get_settings(_messaging, weak_from_this()); - } - - void FiltersWindow::render() - { - if (!_settings) - { - messages::get_settings(_messaging, weak_from_this()); - return; - } - - if (!render_filters_window()) - { - on_window_closed(); - return; - } - } - - void FiltersWindow::set_number(int32_t number) - { - _id = std::format("Filters Manager {}", number); - } - - void FiltersWindow::update(float) - { - } - - void FiltersWindow::receive_message(const Message& message) - { - if (auto settings = messages::read_settings(message)) - { - _settings = settings.value(); - } - } - - bool FiltersWindow::render_filters_window() - { - bool stay_open = true; - ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(540, 500)); - if (ImGui::Begin(_id.c_str(), &stay_open, ImGuiWindowFlags_AlwaysAutoResize)) - { - if (auto filter_store = _filter_store.lock()) - { - const auto filters_map = filter_store->filters(); - const auto filters = filters_map | - std::views::transform([](auto&& f) - { - return f.second; - }) | std::ranges::to(); - const std::vector keys = filters_map | - std::views::transform([](auto&& f) - { - return std::format("{} | {}", f.second.type_key, f.first); - }) | - std::ranges::to(); - - const std::string preview = _selected_filter < keys.size() ? keys[_selected_filter] : ""; - if (ImGui::BeginCombo("Filter", preview.c_str())) - { - for (std::size_t n = 0; n < keys.size(); ++n) - { - bool is_selected = _selected_filter == n; - if (ImGui::Selectable(keys[n].c_str(), is_selected)) - { - _selected_filter = static_cast(n); - - auto found = filters[_selected_filter]; - _filters.set_filters(found.children); - _filters.set_type_key(found.type_key); - } - - if (is_selected) - { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - const auto set_filters = _filters.filters(); - if (!set_filters.empty()) - { - _filters.render_filters(); - } - } - } - ImGui::End(); - ImGui::PopStyleVar(); - return stay_open; - } - - std::string FiltersWindow::type() const - { - return "Filters Manager"; - } - - std::string FiltersWindow::title() const - { - return _id; - } -} diff --git a/trview.app/Windows/Filters/FiltersWindow.h b/trview.app/Windows/Filters/FiltersWindow.h deleted file mode 100644 index a3576f203..000000000 --- a/trview.app/Windows/Filters/FiltersWindow.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "../IWindow.h" -#include "../../Filters/Filters.h" -#include "../../Filters/IFilterStore.h" -#include "../../Settings/UserSettings.h" - -namespace trview -{ - class FiltersWindow final : public IWindow, public std::enable_shared_from_this - { - public: - struct Names - { - static inline const std::string filters_list = "Filters"; - }; - - explicit FiltersWindow(const std::weak_ptr& filter_store, - const std::shared_ptr& shell, - const std::shared_ptr& dialogs, - const std::weak_ptr& messaging); - virtual ~FiltersWindow() = default; - void initialise(); - void render() override; - void set_number(int32_t number) override; - void update(float dt) override; - void receive_message(const Message& message) override; - std::string type() const override; - std::string title() const override; - private: - bool render_filters_window(); - - std::string _id{ "Plugins 0" }; - std::weak_ptr _filter_store; - std::shared_ptr _shell; - std::shared_ptr _dialogs; - std::optional _settings; - std::weak_ptr _messaging; - int32_t _selected_filter{ 0 }; - Filters _filters; - }; -} diff --git a/trview.app/Windows/Windows.cpp b/trview.app/Windows/Windows.cpp index 23c7e5659..5113f012f 100644 --- a/trview.app/Windows/Windows.cpp +++ b/trview.app/Windows/Windows.cpp @@ -57,11 +57,6 @@ namespace trview create("Diff"); break; } - case ID_WINDOWS_FILTERS: - { - create("Filters"); - break; - } case ID_WINDOWS_ITEMS: { create("Items"); diff --git a/trview.app/trview.app.vcxproj b/trview.app/trview.app.vcxproj index 1589961ee..b33d1858e 100644 --- a/trview.app/trview.app.vcxproj +++ b/trview.app/trview.app.vcxproj @@ -92,7 +92,6 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" - @@ -236,7 +235,6 @@ copy ""$(OutDir)*.cso"" ""$(ProjectDir)Resources\Generated"" - diff --git a/trview.app/trview.app.vcxproj.filters b/trview.app/trview.app.vcxproj.filters index e6b910509..882f7c327 100644 --- a/trview.app/trview.app.vcxproj.filters +++ b/trview.app/trview.app.vcxproj.filters @@ -122,7 +122,6 @@ - @@ -369,7 +368,6 @@ - From 376b13d3d0f650e735796902bd6995319c6b17a1 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 22 Mar 2026 09:26:19 +0000 Subject: [PATCH 09/15] Save/load filters to subdirectories --- trview.app/Filters/FilterStore.cpp | 76 +++++++++++++------------ trview.app/Filters/FilterStore.h | 5 +- trview.app/Filters/Filters.cpp | 3 +- trview.app/Filters/IFilterStore.h | 3 +- trview.app/Mocks/Filters/IFilterStore.h | 3 +- 5 files changed, 47 insertions(+), 43 deletions(-) diff --git a/trview.app/Filters/FilterStore.cpp b/trview.app/Filters/FilterStore.cpp index 8d732aec9..b919b9f43 100644 --- a/trview.app/Filters/FilterStore.cpp +++ b/trview.app/Filters/FilterStore.cpp @@ -185,21 +185,25 @@ namespace trview void FilterStore::add(const std::string& key, const Filters::Filter& filter) { - _filters.push_back( + _filters[filter.type_key].push_back( { .name = key, - .filename = "C:\\dev\\trview-filters\\new.json", .filter = filter }); } void FilterStore::load() { - // TODO: Load filters from files in the directories. - // For now let's just use the appdata directory. - if (!_settings.filter_directory.empty()) + if (_settings.filter_directory.empty()) { - const auto files = _files->get_files(_settings.filter_directory, "\\*.json"); + 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 @@ -211,12 +215,10 @@ namespace trview } 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.push_back(new_filter); + _filters[type_key].push_back(new_filter); } catch (...) { @@ -225,25 +227,18 @@ namespace trview } } - std::map FilterStore::filters() const + std::map FilterStore::filters_for_key(const std::string& key) const { - std::map results; - for (const auto& filter : _filters) + const auto found = _filters.find(key); + if (found == _filters.end()) { - results[filter.name] = filter.filter; + return {}; } - return results; - } - std::map FilterStore::filters_for_key(const std::string& key) const - { std::map results; - for (const auto& filter : _filters) + for (const auto& filter : found->second) { - if (filter.filter.type_key == key) - { - results[filter.name] = filter.filter; - } + results[filter.name] = filter.filter; } return results; } @@ -256,10 +251,16 @@ namespace trview } } - void FilterStore::remove(const std::string& name) + void FilterStore::remove(const std::string& type_key, const std::string& name) { - const auto found = std::ranges::find_if(_filters, [&](auto&& f) { return f.name == name; }); - if (found == _filters.end()) + 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; } @@ -267,7 +268,7 @@ namespace trview const auto dir = _settings.filter_directory.empty() ? (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; _files->delete_file(found->filename); - _filters.erase(found); + filters_for_key->second.erase(found); } void FilterStore::save() @@ -276,17 +277,22 @@ namespace trview (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; _files->create_directory(dir); - for (const auto& filter : _filters) + for (const auto& type : _filters) { - try - { - nlohmann::json json; - json["name"] = filter.name; - json["filter"] = filter.filter; - _files->save_file(dir + "\\" + filter.name + ".json", json.dump()); - } - catch (...) + const std::string type_dir = std::format("{}\\{}", dir, type.first); + _files->create_directory(type_dir); + for (const auto& filter : type.second) { + try + { + nlohmann::json json; + json["name"] = filter.name; + json["filter"] = filter.filter; + _files->save_file(type_dir + "\\" + filter.name + ".json", json.dump()); + } + catch (...) + { + } } } } diff --git a/trview.app/Filters/FilterStore.h b/trview.app/Filters/FilterStore.h index 1c32e57d3..90bb91f75 100644 --- a/trview.app/Filters/FilterStore.h +++ b/trview.app/Filters/FilterStore.h @@ -16,10 +16,9 @@ namespace trview virtual ~FilterStore() = default; void add(const std::string& key, const Filters::Filter& filter) override; void load() override; - std::map filters() const override; std::map filters_for_key(const std::string& key) const override; void receive_message(const Message& message) override; - void remove(const std::string& name) override; + void remove(const std::string& type_key, const std::string& name) override; void save() override; private: struct StoredFilter @@ -30,7 +29,7 @@ namespace trview }; std::shared_ptr _files; - std::vector _filters; + std::map> _filters; UserSettings _settings; }; } diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index a063b742b..1e2cbd4a7 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -935,7 +935,7 @@ namespace trview { if (ImGui::MenuItem("Delete")) { - store->remove(value.first); + store->remove(_filter.type_key, value.first); } ImGui::EndPopup(); } @@ -951,6 +951,7 @@ namespace trview if (ImGui::MenuItem("Clear")) { _filter.children = {}; + _name = ""; } ImGui::EndMenuBar(); diff --git a/trview.app/Filters/IFilterStore.h b/trview.app/Filters/IFilterStore.h index 20b15dcae..4ca1607fb 100644 --- a/trview.app/Filters/IFilterStore.h +++ b/trview.app/Filters/IFilterStore.h @@ -12,9 +12,8 @@ namespace trview virtual ~IFilterStore() = 0; virtual void add(const std::string& key, const Filters::Filter& filter) = 0; virtual void load() = 0; - virtual std::map filters() const = 0; virtual std::map filters_for_key(const std::string& key) const = 0; - virtual void remove(const std::string& name) = 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 index f6d060647..e5639d877 100644 --- a/trview.app/Mocks/Filters/IFilterStore.h +++ b/trview.app/Mocks/Filters/IFilterStore.h @@ -12,9 +12,8 @@ namespace trview virtual ~MockFilterStore(); MOCK_METHOD(void, add, (const std::string&, const Filters::Filter&), (override)); MOCK_METHOD(void, load, (), (override)); - MOCK_METHOD((std::map), filters, (), (const, override)); MOCK_METHOD((std::map), filters_for_key, (const std::string&), (const, override)); - MOCK_METHOD(void, remove, (const std::string&), (override)); + MOCK_METHOD(void, remove, (const std::string&, const std::string&), (override)); MOCK_METHOD(void, save, (), (override)); }; } From 46aa96cee44ac925649654460029360b06731374 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:53:28 +0000 Subject: [PATCH 10/15] Exclude forward and backslash from filter name --- trview.app/Filters/Filters.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index 1e2cbd4a7..a0c124316 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -3,6 +3,20 @@ namespace trview { + namespace + { + int filter_path_letters(ImGuiInputTextCallbackData* data) + { + switch (data->EventChar) + { + case L'\\': + case L'/': + return 1; + } + return 0; + } + } + IFilterable::~IFilterable() { } @@ -500,7 +514,7 @@ namespace trview ImGui::SetKeyboardFocusHere(); } - if (ImGui::InputText("##Filter Name", &state.name_value, ImGuiInputTextFlags_EnterReturnsTrue)) + if (ImGui::InputText("##Filter Name", &state.name_value, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCharFilter, filter_path_letters)) { if (const auto store = _filter_store.lock()) { From ef077c329243bde0755b5868edc90515724ee62e Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:40:18 +0000 Subject: [PATCH 11/15] Add a button to settings to show filters dir --- trview.app/UI/SettingsWindow.cpp | 5 +++++ 1 file changed, 5 insertions(+) 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(); } From 90d69d1ab2bc1567814485b11dd7c1aa447560b6 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:30:08 +0100 Subject: [PATCH 12/15] Some tests --- trview.app.tests/Filters/FilterStoreTests.cpp | 129 ++++++++++++++++++ trview.app.tests/trview.app.tests.vcxproj | 1 + .../trview.app.tests.vcxproj.filters | 1 + trview.app/Filters/Filters.h | 3 +- 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 trview.app.tests/Filters/FilterStoreTests.cpp 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/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/Filters/Filters.h b/trview.app/Filters/Filters.h index d577adbb0..3a20aa3e5 100644 --- a/trview.app/Filters/Filters.h +++ b/trview.app/Filters/Filters.h @@ -170,7 +170,6 @@ namespace trview bool match(const IFilterable& value) const; bool match(const Filter& filter, const IFilterable& value, const std::string& type_key) const; void render(); - void render_filters(); void render_settings(); void render_table(const std::ranges::forward_range auto& items, std::ranges::forward_range auto& all_items, @@ -200,8 +199,8 @@ namespace trview 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); void render_menu_bar(); - void render_menu_bar2(); void render_filter_name_modal(); + void render_filters(); bool _changed{ true }; std::vector _columns; From 32afafa5d44d7d7f7964817bf6a585b1b233c10b Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:33:23 +0100 Subject: [PATCH 13/15] Update trview.app.vcxproj.filters --- trview.app/trview.app.vcxproj.filters | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/trview.app/trview.app.vcxproj.filters b/trview.app/trview.app.vcxproj.filters index 882f7c327..5d5575e16 100644 --- a/trview.app/trview.app.vcxproj.filters +++ b/trview.app/trview.app.vcxproj.filters @@ -370,12 +370,8 @@ - - UI\Modal - - - UI\Modal - + + From ae1b306ba898ed724f920bac3a9f9db4bfcf1f9a Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:06:38 +0100 Subject: [PATCH 14/15] Give filters filename on save --- trview.app/Filters/FilterStore.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/trview.app/Filters/FilterStore.cpp b/trview.app/Filters/FilterStore.cpp index b919b9f43..02ee870a0 100644 --- a/trview.app/Filters/FilterStore.cpp +++ b/trview.app/Filters/FilterStore.cpp @@ -265,8 +265,6 @@ namespace trview return; } - const auto dir = _settings.filter_directory.empty() ? - (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; _files->delete_file(found->filename); filters_for_key->second.erase(found); } @@ -277,18 +275,20 @@ namespace trview (_files->appdata_directory() + "\\trview\\filters") : _settings.filter_directory; _files->create_directory(dir); - for (const auto& type : _filters) + for (auto& type : _filters) { const std::string type_dir = std::format("{}\\{}", dir, type.first); _files->create_directory(type_dir); - for (const auto& filter : type.second) + for (auto& filter : type.second) { try { nlohmann::json json; json["name"] = filter.name; json["filter"] = filter.filter; - _files->save_file(type_dir + "\\" + filter.name + ".json", json.dump()); + std::string path = type_dir + "\\" + filter.name + ".json"; + _files->save_file(path, json.dump()); + filter.filename = path; } catch (...) { From a553a44589a27fa22c38710b9ecba2d96597cf04 Mon Sep 17 00:00:00 2001 From: chreden <4263940+chreden@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:33:43 +0100 Subject: [PATCH 15/15] Delete icon on filters --- trview.app/Filters/Filters.cpp | 41 ++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/trview.app/Filters/Filters.cpp b/trview.app/Filters/Filters.cpp index a0c124316..1f98bb3fd 100644 --- a/trview.app/Filters/Filters.cpp +++ b/trview.app/Filters/Filters.cpp @@ -937,22 +937,45 @@ namespace trview const auto values = store ? store->filters_for_key(_filter.type_key) : std::map { }; if (ImGui::BeginMenu("Open", !values.empty())) { - for (const auto& value : values) + if (ImGui::BeginTable("##filters-open-menu", 2, ImGuiTableFlags_SizingStretchProp)) { - if (ImGui::MenuItem(value.first.c_str())) + ImGui::TableSetupScrollFreeze(0, 1); + for (const auto& value : values) { - _filter = { value.second }; - _name = value.first; - } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); - if (ImGui::BeginPopupContextItem()) - { - if (ImGui::MenuItem("Delete")) + 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); } - ImGui::EndPopup(); + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Delete this filter"); + } + + if (selected) + { + _filter = { value.second }; + _name = value.first; + } } + ImGui::EndTable(); } ImGui::EndMenu(); }