From 25f1c97beab4c430ad0e54e7fe7bd5e95070507e Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Sun, 25 May 2025 09:51:48 +0100 Subject: [PATCH] Change: Add badge dropdown filters and configuration. --- src/lang/english.txt | 4 ++ src/newgrf_badge.cpp | 17 ++++- src/newgrf_badge.h | 16 +++++ src/newgrf_badge_config.cpp | 10 +-- src/newgrf_badge_config.h | 1 + src/newgrf_badge_gui.cpp | 138 ++++++++++++++++++++++++++++++++++-- src/newgrf_badge_gui.h | 23 +++++- src/newgrf_badge_type.h | 11 +++ src/widget_type.h | 1 + 9 files changed, 206 insertions(+), 15 deletions(-) diff --git a/src/lang/english.txt b/src/lang/english.txt index b291327e2d..bf32eb9b84 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -5919,5 +5919,9 @@ STR_TOOLBAR_RAILTYPE_VELOCITY :{STRING} ({VELO STR_BADGE_NAME_LIST :{STRING}: {GOLD}{RAW_STRING} STR_BADGE_CONFIG_MENU_TOOLTIP :Open badge configuration STR_BADGE_CONFIG_RESET :Reset +STR_BADGE_CONFIG_ICONS :{WHITE}Badge Icons +STR_BADGE_CONFIG_FILTERS :{WHITE}Badge Filters STR_BADGE_CONFIG_PREVIEW :Preview Image STR_BADGE_CONFIG_NAME :Name +STR_BADGE_FILTER_ANY_LABEL :Any {STRING} +STR_BADGE_FILTER_IS_LABEL :{STRING} is {STRING} diff --git a/src/newgrf_badge.cpp b/src/newgrf_badge.cpp index da9a8604ce..59a9a4a1ca 100644 --- a/src/newgrf_badge.cpp +++ b/src/newgrf_badge.cpp @@ -10,7 +10,6 @@ #include "stdafx.h" #include "newgrf.h" #include "newgrf_badge.h" -#include "newgrf_badge_config.h" #include "newgrf_badge_type.h" #include "newgrf_spritegroup.h" #include "stringfilter_type.h" @@ -271,6 +270,7 @@ void ApplyBadgeFeaturesToClassBadges() Badge *class_badge = GetClassBadge(badge.class_index); assert(class_badge != nullptr); class_badge->features.Set(badge.features); + if (badge.name != STR_NULL) class_badge->flags.Set(BadgeFlag::HasText); } } @@ -297,7 +297,7 @@ PalSpriteID GetBadgeSprite(const Badge &badge, GrfSpecFeature feature, std::opti * Create a list of used badge classes for a feature. * @param feature GRF feature being used. */ -UsedBadgeClasses::UsedBadgeClasses(GrfSpecFeature feature) +UsedBadgeClasses::UsedBadgeClasses(GrfSpecFeature feature) : feature(feature) { for (auto index : _badges.classes) { Badge *class_badge = GetBadge(index); @@ -341,3 +341,16 @@ bool BadgeTextFilter::Filter(std::span badges) const { return std::ranges::any_of(badges, [this](const BadgeID &badge) { return std::ranges::binary_search(this->badges, badge); }); } + +/** + * Test if the given badges matches the filtered badge list. + * @param badges List of badges. + * @return true iff all required badges are present in the provided list. + */ +bool BadgeDropdownFilter::Filter(std::span badges) const +{ + if (this->badges.empty()) return true; + + /* We want all filtered badges to match. */ + return std::ranges::all_of(this->badges, [&badges](const auto &badge) { return std::ranges::find(badges, badge.second) != std::end(badges); }); +} diff --git a/src/newgrf_badge.h b/src/newgrf_badge.h index 2c7c0855ca..f08882351a 100644 --- a/src/newgrf_badge.h +++ b/src/newgrf_badge.h @@ -33,14 +33,21 @@ public: /** Utility class to create a list of badge classes used by a feature. */ class UsedBadgeClasses { public: + UsedBadgeClasses() = default; explicit UsedBadgeClasses(GrfSpecFeature feature); + inline GrfSpecFeature GetFeature() const + { + return this->feature; + } + inline std::span Classes() const { return this->classes; } private: + GrfSpecFeature feature; std::vector classes; ///< List of badge classes. }; @@ -70,4 +77,13 @@ private: FlatSet badges{}; }; +class BadgeDropdownFilter { +public: + BadgeDropdownFilter(const BadgeFilterChoices &conf) : badges(conf) {} + bool Filter(std::span badges) const; + +private: + const BadgeFilterChoices &badges; +}; + #endif /* NEWGRF_BADGE_H */ diff --git a/src/newgrf_badge_config.cpp b/src/newgrf_badge_config.cpp index 8dcbbf353c..a1da087efb 100644 --- a/src/newgrf_badge_config.cpp +++ b/src/newgrf_badge_config.cpp @@ -87,7 +87,7 @@ void AddBadgeClassesToConfiguration() if (found != std::end(config)) continue; /* Not found, insert it. */ - config.emplace_back(badge.label, 0, true); + config.emplace_back(badge.label, 0, true, false); } } } @@ -106,7 +106,7 @@ void ResetBadgeClassConfiguration(GrfSpecFeature feature) for (const BadgeID &index : GetClassBadges()) { const Badge &badge = *GetBadge(index); if (badge.name == STR_NULL) continue; - config.emplace_back(badge.label, 0, true); + config.emplace_back(badge.label, 0, true, false); } } @@ -147,14 +147,16 @@ static void BadgeClassLoadConfigFeature(const IniFile &ini, GrfSpecFeature featu for (const IniItem &item : group->items) { int column = 0; bool show_icon = true; + bool show_filter = false; if (item.value.has_value() && !item.value.value().empty()) { StringConsumer consumer(item.value.value()); + if (consumer.ReadCharIf('?')) show_filter = true; if (consumer.ReadCharIf('!')) show_icon = false; if (auto value = consumer.TryReadIntegerBase(10); value.has_value()) column = *value; } - config.emplace_back(item.name, column, show_icon); + config.emplace_back(item.name, column, show_icon, show_filter); } } @@ -183,7 +185,7 @@ static void BadgeClassSaveConfigFeature(IniFile &ini, GrfSpecFeature feature) group.Clear(); for (const auto &item : _badge_config.features[to_underlying(feature)]) { - group.CreateItem(item.label).SetValue(fmt::format("{}{}", item.show_icon ? "" : "!", item.column)); + group.CreateItem(item.label).SetValue(fmt::format("{}{}{}", item.show_filter ? "?" : "", item.show_icon ? "" : "!", item.column)); } } diff --git a/src/newgrf_badge_config.h b/src/newgrf_badge_config.h index 4b7f7845a3..1da4fa75b3 100644 --- a/src/newgrf_badge_config.h +++ b/src/newgrf_badge_config.h @@ -18,6 +18,7 @@ public: std::string label; ///< Class label. int column = -1; ///< UI column, feature-dependent. bool show_icon = false; ///< Set if the badge icons should be displayed for this class. + bool show_filter = false; ///< Set if a drop down filter should be added for this class. }; void BadgeClassLoadConfig(const struct IniFile &ini); diff --git a/src/newgrf_badge_gui.cpp b/src/newgrf_badge_gui.cpp index 6716ee152b..c36981a7bc 100644 --- a/src/newgrf_badge_gui.cpp +++ b/src/newgrf_badge_gui.cpp @@ -67,23 +67,21 @@ static bool operator<(const GUIBadgeClasses::Element &a, const GUIBadgeClasses:: * Construct of list of badge classes and column groups to display. * @param feature feature being used. */ -GUIBadgeClasses::GUIBadgeClasses(GrfSpecFeature feature) +GUIBadgeClasses::GUIBadgeClasses(GrfSpecFeature feature) : UsedBadgeClasses(feature) { /* Get list of classes used by feature. */ - UsedBadgeClasses used(feature); - uint max_column = 0; - for (BadgeClassID class_index : used.Classes()) { + for (BadgeClassID class_index : this->Classes()) { const Badge *class_badge = GetClassBadge(class_index); if (class_badge->name == STR_NULL) continue; Dimension size = GetBadgeMaximalDimension(class_index, feature); if (size.width == 0) continue; - auto [config, sort_order] = GetBadgeClassConfigItem(feature, class_badge->label); + const auto [config, sort_order] = GetBadgeClassConfigItem(feature, class_badge->label); this->gui_classes.emplace_back(class_index, config.column, config.show_icon, sort_order, size, class_badge->label); - if (config.show_icon) max_column = std::max(max_column, config.column); + if (size.width != 0 && config.show_icon) max_column = std::max(max_column, config.column); } std::sort(std::begin(this->gui_classes), std::end(this->gui_classes)); @@ -91,7 +89,7 @@ GUIBadgeClasses::GUIBadgeClasses(GrfSpecFeature feature) /* Determine total width of visible badge columns. */ this->column_widths.resize(max_column + 1); for (const auto &el : this->gui_classes) { - if (!el.visible) continue; + if (!el.visible || el.size.width == 0) continue; this->column_widths[el.column_group] += ScaleGUITrad(el.size.width) + WidgetDimensions::scaled.hsep_normal; } @@ -306,12 +304,14 @@ private: }; using DropDownListToggleMoverItem = DropDownMover>>; +using DropDownListToggleItem = DropDownToggle>; enum BadgeClick : int { BADGE_CLICK_NONE, BADGE_CLICK_MOVE_UP, BADGE_CLICK_MOVE_DOWN, BADGE_CLICK_TOGGLE_ICON, + BADGE_CLICK_TOGGLE_FILTER, }; DropDownList BuildBadgeClassConfigurationList(const GUIBadgeClasses &gui_classes, uint columns, std::span column_separators) @@ -321,6 +321,7 @@ DropDownList BuildBadgeClassConfigurationList(const GUIBadgeClasses &gui_classes list.push_back(MakeDropDownListStringItem(STR_BADGE_CONFIG_RESET, INT_MAX)); if (gui_classes.GetClasses().empty()) return list; list.push_back(MakeDropDownListDividerItem()); + list.push_back(std::make_unique>(GetString(STR_BADGE_CONFIG_ICONS), -1)); const BadgeClassID front = gui_classes.GetClasses().front().class_index; const BadgeClassID back = gui_classes.GetClasses().back().class_index; @@ -328,6 +329,7 @@ DropDownList BuildBadgeClassConfigurationList(const GUIBadgeClasses &gui_classes for (uint i = 0; i < columns; ++i) { for (const auto &gc : gui_classes.GetClasses()) { if (gc.column_group != i) continue; + if (gc.size.width == 0) continue; bool first = (i == 0 && gc.class_index == front); bool last = (i == columns - 1 && gc.class_index == back); @@ -343,6 +345,17 @@ DropDownList BuildBadgeClassConfigurationList(const GUIBadgeClasses &gui_classes } } + list.push_back(MakeDropDownListDividerItem()); + list.push_back(std::make_unique>(GetString(STR_BADGE_CONFIG_FILTERS), -1)); + + for (const BadgeClassID &badge_class_index : gui_classes.Classes()) { + const Badge *badge = GetClassBadge(badge_class_index); + if (!badge->flags.Test(BadgeFlag::HasText)) continue; + + const auto [config, _] = GetBadgeClassConfigItem(gui_classes.GetFeature(), badge->label); + list.push_back(std::make_unique(config.show_filter, BADGE_CLICK_TOGGLE_FILTER, COLOUR_YELLOW, COLOUR_GREY, GetString(badge->name), (1U << 16) | badge_class_index.base())); + } + return list; } @@ -359,6 +372,7 @@ static void BadgeClassToggleVisibility(GrfSpecFeature feature, Badge &class_badg if (it == std::end(config)) return; if (click_result == BADGE_CLICK_TOGGLE_ICON) it->show_icon = !it->show_icon; + if (click_result == BADGE_CLICK_TOGGLE_FILTER) it->show_filter = !it->show_filter; } /** @@ -446,6 +460,7 @@ bool HandleBadgeConfigurationDropDownClick(GrfSpecFeature feature, uint columns, BadgeClassMovePrevious(feature, *class_badge); break; case BADGE_CLICK_TOGGLE_ICON: + case BADGE_CLICK_TOGGLE_FILTER: BadgeClassToggleVisibility(feature, *class_badge, click_result); break; default: @@ -454,3 +469,112 @@ bool HandleBadgeConfigurationDropDownClick(GrfSpecFeature feature, uint columns, return true; } + +NWidgetBadgeFilter::NWidgetBadgeFilter(Colours colour, WidgetID index, GrfSpecFeature feature, BadgeClassID badge_class) + : NWidgetLeaf(WWT_DROPDOWN, colour, index, WidgetData{ .string = STR_JUST_STRING }, STR_NULL) + , feature(feature), badge_class(badge_class) +{ + this->SetFill(1, 0); + this->SetResize(1, 0); +} + +std::string NWidgetBadgeFilter::GetStringParameter(const BadgeFilterChoices &choices) const +{ + auto it = choices.find(this->badge_class); + if (it == std::end(choices)) { + return ::GetString(STR_BADGE_FILTER_ANY_LABEL, GetClassBadge(this->badge_class)->name); + } + + return ::GetString(STR_BADGE_FILTER_IS_LABEL, GetClassBadge(it->first)->name, GetBadge(it->second)->name); +} + +/** + * Get the drop down list of badges for this filter. + * @return Drop down list for filter. + */ +DropDownList NWidgetBadgeFilter::GetDropDownList() const +{ + DropDownList list; + + /* Add item for disabling filtering. */ + list.push_back(MakeDropDownListStringItem(::GetString(STR_BADGE_FILTER_ANY_LABEL, GetClassBadge(this->badge_class)->name), -1)); + list.push_back(MakeDropDownListDividerItem()); + + /* Add badges */ + Dimension d = GetBadgeMaximalDimension(this->badge_class, this->feature); + d.width = ScaleGUITrad(d.width); + d.height = ScaleGUITrad(d.height); + + auto start = list.size(); + + const auto *bc = GetClassBadge(this->badge_class); + + for (const Badge &badge : GetBadges()) { + if (badge.class_index != this->badge_class) continue; + if (badge.index == bc->index) continue; + if (badge.name == STR_NULL) continue; + if (!badge.features.Test(this->feature)) continue; + + PalSpriteID ps = GetBadgeSprite(badge, this->feature, std::nullopt, PAL_NONE); + if (ps.sprite == 0) { + list.push_back(MakeDropDownListStringItem(badge.name, badge.index.base())); + } else { + list.push_back(MakeDropDownListIconItem(d, ps.sprite, ps.pal, badge.name, badge.index.base())); + } + } + + std::sort(std::begin(list) + start, std::end(list), DropDownListStringItem::NatSortFunc); + + return list; +} + +/** + * Add badge drop down filter widgets. + * @param container Container widget to hold filter widgets. + * @param widget Widget index to apply to first filter. + * @param colour Background colour of widgets. + * @param feature GRF feature for filters. + * @return First and last widget indexes of filter widgets. + */ +std::pair AddBadgeDropdownFilters(NWidgetContainer &container, WidgetID widget, Colours colour, GrfSpecFeature feature) +{ + container.Clear(); + WidgetID first = ++widget; + + /* Get list of classes used by feature. */ + UsedBadgeClasses used(feature); + + for (BadgeClassID class_index : used.Classes()) { + const auto [config, _] = GetBadgeClassConfigItem(feature, GetClassBadge(class_index)->label); + if (!config.show_filter) continue; + + container.Add(std::make_unique(colour, widget, feature, class_index)); + ++widget; + } + + return {first, widget}; +} + +/** + * Reset badge filter choice for a class. + * @param choices Badge filter choices. + * @param badge_class_index Badge class to reset. + */ +void ResetBadgeFilter(BadgeFilterChoices &choices, BadgeClassID badge_class_index) +{ + choices.erase(badge_class_index); +} + +/** + * Set badge filter choice for a class. + * @param choides Badge filter choides. + * @param badge_index Badge to set. The badge class is inferred from the badge. + * @note if the badge_index is invalid, the filter will be reset instead. + */ +void SetBadgeFilter(BadgeFilterChoices &choices, BadgeID badge_index) +{ + const Badge *badge = GetBadge(badge_index); + assert(badge != nullptr); + + choices[badge->class_index] = badge_index; +} diff --git a/src/newgrf_badge_gui.h b/src/newgrf_badge_gui.h index 89b72656d6..935c48df22 100644 --- a/src/newgrf_badge_gui.h +++ b/src/newgrf_badge_gui.h @@ -12,15 +12,16 @@ #include "dropdown_type.h" #include "newgrf.h" +#include "newgrf_badge.h" #include "newgrf_badge_type.h" #include "timer/timer_game_calendar.h" -class GUIBadgeClasses { +class GUIBadgeClasses : public UsedBadgeClasses { public: struct Element { BadgeClassID class_index; ///< Badge class index. uint8_t column_group; ///< Column group in UI. 0 = left, 1 = centre, 2 = right. - bool visible; ///< Whether this element is visible. + bool visible; ///< Whether the icon is visible. uint sort_order; ///< Order of element. Dimension size; ///< Maximal size of this element. std::string_view label; ///< Class label (string owned by the class badge) @@ -55,4 +56,22 @@ std::unique_ptr MakeDropDownListBadgeIconItem(const std::share DropDownList BuildBadgeClassConfigurationList(const class GUIBadgeClasses &badge_class, uint columns, std::span column_separators); bool HandleBadgeConfigurationDropDownClick(GrfSpecFeature feature, uint columns, int result, int click_result); +std::pair AddBadgeDropdownFilters(NWidgetContainer &container, WidgetID widget, Colours colour, GrfSpecFeature feature); + +class NWidgetBadgeFilter : public NWidgetLeaf { +public: + NWidgetBadgeFilter(Colours colour, WidgetID index, GrfSpecFeature feature, BadgeClassID badge_class); + + BadgeClassID GetBadgeClassID() const { return this->badge_class; } + std::string GetStringParameter(const BadgeFilterChoices &choices) const; + DropDownList GetDropDownList() const; + +private: + GrfSpecFeature feature; ///< Feature of this dropdown. + BadgeClassID badge_class; ///< Badge class of this dropdown. +}; + +void ResetBadgeFilter(BadgeFilterChoices &choices, BadgeClassID badge_class_index); +void SetBadgeFilter(BadgeFilterChoices &choices, BadgeID badge_index); + #endif /* NEWGRF_BADGE_GUI_H */ diff --git a/src/newgrf_badge_type.h b/src/newgrf_badge_type.h index 54810aca27..f23daf2f9a 100644 --- a/src/newgrf_badge_type.h +++ b/src/newgrf_badge_type.h @@ -16,12 +16,23 @@ using BadgeID = StrongType::Typedef; using BadgeClassID = StrongType::Typedef; +template <> struct std::hash { + std::size_t operator()(const BadgeClassID &badge_class_index) const noexcept + { + return std::hash{}(badge_class_index.base()); + } +}; + enum class BadgeFlag : uint8_t { Copy = 0, ///< Copy badge to related things. NameListStop = 1, ///< Stop adding names to the name list after this badge. NameListFirstOnly = 2, ///< Don't add this name to the name list if not first. UseCompanyColour = 3, ///< Apply company colour palette to this badge. + + HasText, ///< Internal flag set if the badge has text. }; using BadgeFlags = EnumBitSet; +using BadgeFilterChoices = std::unordered_map; + #endif /* NEWGRF_BADGE_TYPE_H */ diff --git a/src/widget_type.h b/src/widget_type.h index 06494a4533..b9c7da1bbb 100644 --- a/src/widget_type.h +++ b/src/widget_type.h @@ -484,6 +484,7 @@ public: inline bool IsEmpty() { return this->children.empty(); } NWidgetBase *GetWidgetOfType(WidgetType tp) override; + void Clear() { this->children.clear(); } protected: std::vector> children{}; ///< Child widgets in container.