From 2bcbea26943df1b44774b617b349bb2fc7fdc0c8 Mon Sep 17 00:00:00 2001 From: mmtunligit <156685720+mmtunligit@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:38:01 +0100 Subject: [PATCH] Feature: User-defined collections of saved items in the picker window (#14813) --- src/lang/english.txt | 20 +++ src/object_gui.cpp | 1 + src/picker_gui.cpp | 288 ++++++++++++++++++++++++++++++------ src/picker_gui.h | 73 +++++++-- src/rail_gui.cpp | 2 + src/road_gui.cpp | 4 + src/town_gui.cpp | 40 +++-- src/widgets/picker_widget.h | 7 +- 8 files changed, 365 insertions(+), 70 deletions(-) diff --git a/src/lang/english.txt b/src/lang/english.txt index e7dbb2ebdb..41f22bda19 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2858,18 +2858,38 @@ STR_PICKER_PREVIEW_SHRINK_TOOLTIP :Reduce the heig STR_PICKER_PREVIEW_EXPAND :+ STR_PICKER_PREVIEW_EXPAND_TOOLTIP :Increase the height of preview images. Ctrl+Click to increase to maximum +STR_PICKER_DEFAULT_COLLECTION :Default collection +STR_PICKER_SELECT_COLLECTION_TOOLTIP :Select a collection +STR_PICKER_COLLECTION_ADD :Add +STR_PICKER_COLLECTION_ADD_TOOLTIP :Create a new collection +STR_PICKER_COLLECTION_RENAME :Rename +STR_PICKER_COLLECTION_RENAME_TOOLTIP :Rename a collection +STR_PICKER_COLLECTION_DELETE :Delete +STR_PICKER_COLLECTION_DELETE_TOOLTIP :Delete a collection + +STR_PICKER_COLLECTION_RENAME_QUERY :Rename this collection +STR_PICKER_COLLECTION_DELETE_QUERY :Delete collection +STR_PICKER_COLLECTION_DELETE_QUERY_TEXT :{YELLOW}Are you sure you want to delete this collection? +STR_PICKER_COLLECTION_DELETE_QUERY_DISABLED_TEXT :{YELLOW}Are you sure you want to delete this collection? There are items from disabled NewGRFs in it! + STR_PICKER_STATION_CLASS_TOOLTIP :Select a station class to display STR_PICKER_STATION_TYPE_TOOLTIP :Select a station type to build. Ctrl+Click to add or remove in saved items +STR_PICKER_STATION_COLLECTION_TOOLTIP :Select a collection of stations to display STR_PICKER_WAYPOINT_CLASS_TOOLTIP :Select a waypoint class to display STR_PICKER_WAYPOINT_TYPE_TOOLTIP :Select a waypoint to build. Ctrl+Click to add or remove in saved items +STR_PICKER_WAYPOINT_COLLECTION_TOOLTIP :Select a collection of waypoints to display STR_PICKER_ROADSTOP_BUS_CLASS_TOOLTIP :Select a bus station class to display STR_PICKER_ROADSTOP_BUS_TYPE_TOOLTIP :Select a bus station type to build. Ctrl+Click to add or remove in saved items +STR_PICKER_ROADSTOP_BUS_COLLECTION_TOOLTIP :Select a collection of bus stations to display STR_PICKER_ROADSTOP_TRUCK_CLASS_TOOLTIP :Select a lorry station class to display STR_PICKER_ROADSTOP_TRUCK_TYPE_TOOLTIP :Select a lorry station type to build. Ctrl+Click to add or remove in saved items +STR_PICKER_ROADSTOP_TRUCK_COLLECTION_TOOLTIP :Select a collection of lorry stations to display STR_PICKER_OBJECT_CLASS_TOOLTIP :Select an object class to display STR_PICKER_OBJECT_TYPE_TOOLTIP :Select an object type to build. Ctrl+Click to add or remove in saved items. Ctrl+Click+Drag to select the area diagonally. Also press Shift to show cost estimate only +STR_PICKER_OBJECT_COLLECTION_TOOLTIP :Select a collection of objects to display STR_PICKER_HOUSE_CLASS_TOOLTIP :Select a town zone to display STR_PICKER_HOUSE_TYPE_TOOLTIP :Select a house type to build. Ctrl+Click to add or remove in saved items +STR_PICKER_HOUSE_COLLECTION_TOOLTIP :Select a collection of houses to display STR_HOUSE_PICKER_CAPTION :House Selection STR_HOUSE_PICKER_NAME :{BLACK}Name: {GOLD}{STRING} diff --git a/src/object_gui.cpp b/src/object_gui.cpp index 746c5c8f57..b40a342c88 100644 --- a/src/object_gui.cpp +++ b/src/object_gui.cpp @@ -50,6 +50,7 @@ public: StringID GetClassTooltip() const override { return STR_PICKER_OBJECT_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_OBJECT_TYPE_TOOLTIP; } + StringID GetCollectionTooltip() const override { return STR_PICKER_OBJECT_COLLECTION_TOOLTIP; } bool IsActive() const override { diff --git a/src/picker_gui.cpp b/src/picker_gui.cpp index 330e27ce0a..780f83cc3b 100644 --- a/src/picker_gui.cpp +++ b/src/picker_gui.cpp @@ -10,6 +10,7 @@ #include "stdafx.h" #include "core/backup_type.hpp" #include "company_func.h" +#include "dropdown_func.h" #include "gui.h" #include "hotkeys.h" #include "ini_type.h" @@ -64,29 +65,39 @@ PickerCallbacks::~PickerCallbacks() */ static void PickerLoadConfig(const IniFile &ini, PickerCallbacks &callbacks) { - const IniGroup *group = ini.GetGroup(callbacks.ini_group); - if (group == nullptr) return; - callbacks.saved.clear(); - for (const IniItem &item : group->items) { - std::array grfid_buf; + for (const IniGroup &group : ini.groups) { + /* Read the collection name */ + if (!group.name.starts_with(callbacks.ini_group)) continue; + auto pos = group.name.find('-'); + if (pos == std::string_view::npos && group.name != callbacks.ini_group) continue; + std::string collection = (pos == std::string_view::npos) ? "" : group.name.substr(pos + 1); - std::string_view str = item.name; + if (group.items.empty() && pos != std::string_view::npos) { + callbacks.saved[collection]; + continue; + } - /* Try reading "|" */ - auto grfid_pos = str.find('|'); - if (grfid_pos == std::string_view::npos) continue; + for (const IniItem &item : group.items) { + std::array grfid_buf; - std::string_view grfid_str = str.substr(0, grfid_pos); - if (!ConvertHexToBytes(grfid_str, grfid_buf)) continue; + std::string_view str = item.name; - str = str.substr(grfid_pos + 1); - uint32_t grfid = grfid_buf[0] | (grfid_buf[1] << 8) | (grfid_buf[2] << 16) | (grfid_buf[3] << 24); - uint16_t localid; - auto [ptr, err] = std::from_chars(str.data(), str.data() + str.size(), localid); + /* Try reading "|" */ + auto grfid_pos = str.find('|'); + if (grfid_pos == std::string_view::npos) continue; - if (err == std::errc{} && ptr == str.data() + str.size()) { - callbacks.saved.insert({grfid, localid, 0, 0}); + std::string_view grfid_str = str.substr(0, grfid_pos); + if (!ConvertHexToBytes(grfid_str, grfid_buf)) continue; + + str = str.substr(grfid_pos + 1); + uint32_t grfid = grfid_buf[0] | (grfid_buf[1] << 8) | (grfid_buf[2] << 16) | (grfid_buf[3] << 24); + uint16_t localid; + auto [ptr, err] = std::from_chars(str.data(), str.data() + str.size(), localid); + + if (err == std::errc{} && ptr == str.data() + str.size()) { + callbacks.saved[collection].emplace(grfid, localid, 0, 0); + } } } } @@ -98,12 +109,18 @@ static void PickerLoadConfig(const IniFile &ini, PickerCallbacks &callbacks) */ static void PickerSaveConfig(IniFile &ini, const PickerCallbacks &callbacks) { - IniGroup &group = ini.GetOrCreateGroup(callbacks.ini_group); - group.Clear(); + /* Clean the ini file of any obsolete collections to prevent them coming back after a restart */ + for (const std::string &rm_collection : callbacks.rm_collections) { + ini.RemoveGroup(callbacks.ini_group + "-" + rm_collection); + } - for (const PickerItem &item : callbacks.saved) { - std::string key = fmt::format("{:08X}|{}", std::byteswap(item.grfid), item.local_id); - group.CreateItem(key); + for (const auto &collection : callbacks.saved) { + IniGroup &group = ini.GetOrCreateGroup(collection.first == "" ? callbacks.ini_group : callbacks.ini_group + "-" + collection.first); + group.Clear(); + for (const PickerItem &item : collection.second) { + std::string key = fmt::format("{:08X}|{}", std::byteswap(item.grfid), item.local_id); + group.CreateItem(key); + } } } @@ -159,10 +176,28 @@ static bool TypeTagNameFilter(PickerItem const *item, PickerFilterData &filter) return filter.GetState(); } +/** Allow the collection sorter to test if the collection has inactive items */ +PickerWindow *picker_window; + +/** + * Sort collections by id. + * @param a First string for sorting. + * @param b Second string for sorting. + * @return Sort order. + */ +static bool CollectionIDSorter(std::string const &a, std::string const &b) +{ + if (a == GetString(STR_PICKER_DEFAULT_COLLECTION) || b == GetString(STR_PICKER_DEFAULT_COLLECTION)) return a == GetString(STR_PICKER_DEFAULT_COLLECTION); + if (picker_window->inactive.contains(a) == picker_window->inactive.contains(b)) return StrNaturalCompare(a, b) < 0; + return picker_window->inactive.contains(a) < picker_window->inactive.contains(b); +} + static const std::initializer_list _class_sorter_funcs = { &ClassIDSorter }; ///< Sort functions of the #PickerClassList static const std::initializer_list _class_filter_funcs = { &ClassTagNameFilter }; ///< Filter functions of the #PickerClassList. static const std::initializer_list _type_sorter_funcs = { TypeIDSorter }; ///< Sort functions of the #PickerTypeList. static const std::initializer_list _type_filter_funcs = { TypeTagNameFilter }; ///< Filter functions of the #PickerTypeList. +static const std::initializer_list _collection_sorter_funcs = { &CollectionIDSorter }; ///< Sort functions of the #PickerCollectionList. + PickerWindow::PickerWindow(WindowDesc &desc, Window *parent, int window_number, PickerCallbacks &callbacks) : PickerWindowBase(desc, parent), callbacks(callbacks), class_editbox(EDITBOX_MAX_SIZE * MAX_CHAR_LENGTH, EDITBOX_MAX_SIZE), @@ -182,10 +217,12 @@ void PickerWindow::ConstructWindow() bool is_active = this->callbacks.IsActive(); this->preview_height = std::max(this->callbacks.preview_height, PREVIEW_HEIGHT); + picker_window = this; /* Functionality depends on widgets being present, not window class. */ this->has_class_picker = is_active && this->GetWidget(WID_PW_CLASS_LIST) != nullptr && this->callbacks.HasClassChoice(); this->has_type_picker = is_active && this->GetWidget(WID_PW_TYPE_MATRIX) != nullptr; + this->has_collection_picker = is_active && this->GetWidget(WID_PW_COLEC_LIST) != nullptr; if (this->has_class_picker) { this->GetWidget(WID_PW_CLASS_LIST)->SetToolTip(this->callbacks.GetClassTooltip()); @@ -209,7 +246,10 @@ void PickerWindow::ConstructWindow() this->classes.SetFilterFuncs(_class_filter_funcs); /* Update saved type information. */ + if (this->callbacks.sel_collection == "") SetWidgetsDisabledState(true, WID_PW_COLEC_RENAME, WID_PW_COLEC_DELETE); this->callbacks.saved = this->callbacks.UpdateSavedItems(this->callbacks.saved); + this->inactive = this->callbacks.InitializeInactiveCollections(this->callbacks.saved); + this->collections.ForceRebuild(); /* Clear used type information. */ this->callbacks.used.clear(); @@ -243,6 +283,13 @@ void PickerWindow::ConstructWindow() this->types.SetSortFuncs(_type_sorter_funcs); this->types.SetFilterFuncs(_type_filter_funcs); + if (this->has_collection_picker) { + this->GetWidget(WID_PW_COLEC_LIST)->SetToolTip(this->callbacks.GetCollectionTooltip()); + } + + this->collections.SetListing(this->callbacks.collection_last_sorting); + this->collections.SetSortFuncs(_collection_sorter_funcs); + this->FinishInitNested(this->window_number); this->InvalidateData(PICKER_INVALIDATION_ALL); @@ -298,13 +345,30 @@ void PickerWindow::UpdateWidgetSize(WidgetID widget, Dimension &size, const Dime std::string PickerWindow::GetWidgetString(WidgetID widget, StringID stringid) const { - if (IsInsideMM(widget, this->badge_filters.first, this->badge_filters.second)) { - return this->GetWidget(widget)->GetStringParameter(this->badge_filter_choices); - } + switch (widget) { + case WID_PW_COLEC_LIST: + return this->callbacks.sel_collection == "" ? GetString(STR_PICKER_DEFAULT_COLLECTION) : this->callbacks.sel_collection; + default: + if (IsInsideMM(widget, this->badge_filters.first, this->badge_filters.second)) { + return this->GetWidget(widget)->GetStringParameter(this->badge_filter_choices); + } + break; + } return this->Window::GetWidgetString(widget, stringid); } +DropDownList PickerWindow::BuildCollectionDropDownList() +{ + DropDownList list; + int i = 0; + for (const auto &collection : collections) { + list.push_back(MakeDropDownListStringItem(GetString(collection == "" ? STR_PICKER_DEFAULT_COLLECTION : STR_JUST_RAW_STRING, collection), i, false, this->inactive.contains(collection))); + i++; + } + return list; +} + void PickerWindow::DrawWidget(const Rect &r, WidgetID widget) const { switch (widget) { @@ -343,8 +407,10 @@ void PickerWindow::DrawWidget(const Rect &r, WidgetID widget) const PaletteID palette = _game_mode != GM_NORMAL || feature == GSF_HOUSES ? PAL_NONE : GetCompanyPalette(_local_company); DrawBadgeColumn({0, by, ir.Width() - 1, ir.Height() - 1}, 0, this->badge_classes, this->callbacks.GetTypeBadges(item.class_index, item.index), feature, std::nullopt, palette); - if (this->callbacks.saved.contains(item)) { - DrawSprite(SPR_BLOT, PALETTE_TO_YELLOW, 0, 0); + if (this->callbacks.saved.contains(this->callbacks.sel_collection)) { + if (this->callbacks.saved.at(this->callbacks.sel_collection).contains(item)) { + DrawSprite(SPR_BLOT, PALETTE_TO_YELLOW, 0, 0); + } } if (this->callbacks.used.contains(item)) { DrawSprite(SPR_BLOT, PALETTE_TO_GREEN, ir.Width() - GetSpriteSize(SPR_BLOT).width, 0); @@ -372,6 +438,21 @@ void PickerWindow::OnResize() } } +void PickerWindow::DeletePickerCollectionCallback(Window *win, bool confirmed) +{ + if (confirmed) { + PickerWindow *w = (PickerWindow*)win; + w->callbacks.saved.erase(w->callbacks.saved.find(w->callbacks.edit_collection)); + w->inactive.erase(w->callbacks.edit_collection); + w->callbacks.rm_collections.emplace(w->callbacks.edit_collection); + w->callbacks.sel_collection = ""; + w->callbacks.edit_collection.clear(); + picker_window = w; + w->SetWidgetsDisabledState(true, WID_PW_COLEC_RENAME, WID_PW_COLEC_DELETE); + w->InvalidateData({PickerInvalidation::Collection, PickerInvalidation::Position}); + } +} + void PickerWindow::OnClick(Point pt, WidgetID widget, int) { switch (widget) { @@ -422,13 +503,20 @@ void PickerWindow::OnClick(Point pt, WidgetID widget, int) const auto &item = this->types[sel]; if (_ctrl_pressed) { - auto it = this->callbacks.saved.find(item); - if (it == std::end(this->callbacks.saved)) { - this->callbacks.saved.insert(item); - } else { - this->callbacks.saved.erase(it); + if (this->callbacks.saved.find(this->callbacks.sel_collection) == this->callbacks.saved.end()) { + this->callbacks.saved[""].emplace(item); + this->InvalidateData({PickerInvalidation::Collection, PickerInvalidation::Class}); + this->SetDirty(); + break; } - this->InvalidateData(PickerInvalidation::Type); + + auto it = this->callbacks.saved.at(this->callbacks.sel_collection).find(item); + if (it == std::end(this->callbacks.saved.at(this->callbacks.sel_collection))) { + this->callbacks.saved.at(this->callbacks.sel_collection).emplace(item); + } else { + this->callbacks.saved.at(this->callbacks.sel_collection).erase(it); + } + this->InvalidateData({PickerInvalidation::Type, PickerInvalidation::Class}); break; } @@ -442,6 +530,37 @@ void PickerWindow::OnClick(Point pt, WidgetID widget, int) break; } + case WID_PW_COLEC_LIST: { + ShowDropDownList(this, this->BuildCollectionDropDownList(), -1, widget, 0); + CloseWindowById(WC_SELECT_STATION, 0); + break; + } + + case WID_PW_COLEC_ADD: + this->callbacks.rename_collection = false; + ShowQueryString({}, STR_PICKER_COLLECTION_ADD_TOOLTIP, MAX_LENGTH_GROUP_NAME_CHARS, this, CS_ALPHANUMERAL, QueryStringFlag::LengthIsInChars); + break; + + case WID_PW_COLEC_RENAME: + if (this->callbacks.saved.contains(this->callbacks.sel_collection)) { + CloseChildWindows(WC_CONFIRM_POPUP_QUERY); + this->callbacks.edit_collection = this->callbacks.sel_collection; + this->callbacks.rename_collection = true; + ShowQueryString(this->callbacks.sel_collection, STR_PICKER_COLLECTION_RENAME_QUERY, MAX_LENGTH_GROUP_NAME_CHARS, this, CS_ALPHANUMERAL, QueryStringFlag::LengthIsInChars); + } + break; + + case WID_PW_COLEC_DELETE: + if (this->callbacks.saved.contains(this->callbacks.sel_collection)) { + CloseChildWindows(WC_QUERY_STRING); + this->callbacks.edit_collection = this->callbacks.sel_collection; + + this->inactive.contains(this->callbacks.sel_collection) ? + ShowQuery(GetEncodedString(STR_PICKER_COLLECTION_DELETE_QUERY), GetEncodedString(STR_PICKER_COLLECTION_DELETE_QUERY_DISABLED_TEXT), this, DeletePickerCollectionCallback) : + ShowQuery(GetEncodedString(STR_PICKER_COLLECTION_DELETE_QUERY), GetEncodedString(STR_PICKER_COLLECTION_DELETE_QUERY_TEXT), this, DeletePickerCollectionCallback); + } + break; + case WID_PW_CONFIGURE_BADGES: if (this->badge_classes.GetClasses().empty()) break; ShowDropDownList(this, BuildBadgeClassConfigurationList(this->badge_classes, 1, {}, COLOUR_DARK_GREEN), -1, widget, 0, DropDownOption::Persist); @@ -457,9 +576,54 @@ void PickerWindow::OnClick(Point pt, WidgetID widget, int) } } +void PickerWindow::OnQueryTextFinished(std::optional str) +{ + if (!str.has_value()) return; + + if (!this->callbacks.saved.contains(*str)) { + if (this->callbacks.saved.contains(this->callbacks.edit_collection) && this->callbacks.rename_collection) { + auto rename_collection = this->callbacks.saved.extract(this->callbacks.edit_collection); + rename_collection.key() = *str; + this->callbacks.saved.insert(std::move(rename_collection)); + + if (this->inactive.contains(this->callbacks.edit_collection)) { + this->inactive.erase(this->callbacks.edit_collection); + this->inactive.emplace(*str); + } + + this->callbacks.rm_collections.emplace(this->callbacks.edit_collection); + this->callbacks.edit_collection.clear(); + + } else { + this->callbacks.saved.insert({*str, {}}); + } + } + + this->callbacks.sel_collection = *str; + picker_window = this; + SetWidgetsDisabledState(this->callbacks.sel_collection == "" ? true : false, WID_PW_COLEC_RENAME, WID_PW_COLEC_DELETE); + if (!IsWidgetLowered(WID_PW_MODE_SAVED)) { + this->InvalidateData({PickerInvalidation::Type, PickerInvalidation::Class}); + } + this->InvalidateData({PickerInvalidation::Collection, PickerInvalidation::Position}); +} + void PickerWindow::OnDropdownSelect(WidgetID widget, int index, int click_result) { switch (widget) { + case WID_PW_COLEC_LIST: { + auto it = this->collections.begin() + index; + if (this->callbacks.sel_collection != *it) { + this->callbacks.sel_collection = *it; + if (this->IsWidgetLowered(WID_PW_MODE_SAVED)) this->InvalidateData({PickerInvalidation::Class, PickerInvalidation::Type, PickerInvalidation::Validate}); + this->InvalidateData(PickerInvalidation::Position); + } + SetWidgetsDisabledState(this->callbacks.sel_collection == "" ? true : false, WID_PW_COLEC_RENAME, WID_PW_COLEC_DELETE); + + SndClickBeep(); + break; + } + case WID_PW_CONFIGURE_BADGES: { bool reopen = HandleBadgeConfigurationDropDownClick(this->callbacks.GetFeature(), 1, index, click_result, this->badge_filter_choices); @@ -506,6 +670,7 @@ void PickerWindow::OnInvalidateData(int data, bool gui_scope) if (pi.Test(PickerInvalidation::Class)) this->classes.ForceRebuild(); if (pi.Test(PickerInvalidation::Type)) this->types.ForceRebuild(); + if (pi.Test(PickerInvalidation::Collection)) this->collections.ForceRebuild(); this->BuildPickerClassList(); if (pi.Test(PickerInvalidation::Validate)) this->EnsureSelectedClassIsValid(); @@ -515,6 +680,8 @@ void PickerWindow::OnInvalidateData(int data, bool gui_scope) if (pi.Test(PickerInvalidation::Validate)) this->EnsureSelectedTypeIsValid(); if (pi.Test(PickerInvalidation::Position)) this->EnsureSelectedTypeIsVisible(); + this->BuildPickerCollectionList(); + if (this->has_type_picker) { SetWidgetLoweredState(WID_PW_MODE_ALL, HasBit(this->callbacks.mode, PFM_ALL)); SetWidgetLoweredState(WID_PW_MODE_USED, HasBit(this->callbacks.mode, PFM_USED)); @@ -582,7 +749,8 @@ void PickerWindow::BuildPickerClassList() for (int i = 0; i < count; i++) { if (this->callbacks.GetClassName(i) == INVALID_STRING_ID) continue; if (filter_used && std::none_of(std::begin(this->callbacks.used), std::end(this->callbacks.used), [i](const PickerItem &item) { return item.class_index == i; })) continue; - if (filter_saved && std::none_of(std::begin(this->callbacks.saved), std::end(this->callbacks.saved), [i](const PickerItem &item) { return item.class_index == i; })) continue; + if (filter_saved && this->callbacks.saved.find(this->callbacks.sel_collection) == this->callbacks.saved.end()) continue; + if (filter_saved && std::none_of(std::begin(this->callbacks.saved.at(this->callbacks.sel_collection)), std::end(this->callbacks.saved.at(this->callbacks.sel_collection)), [i](const PickerItem &item) { return item.class_index == i; })) continue; this->classes.emplace_back(i); } @@ -656,10 +824,10 @@ void PickerWindow::BuildPickerTypeList() if (this->callbacks.GetTypeName(item.class_index, item.index) == INVALID_STRING_ID) continue; this->types.emplace_back(item); } - } else if (filter_saved) { + } else if (filter_saved && this->callbacks.saved.contains(this->callbacks.sel_collection)) { /* Showing only saved items. */ - this->types.reserve(this->callbacks.saved.size()); - for (const PickerItem &item : this->callbacks.saved) { + this->types.reserve(std::size(this->callbacks.saved.at(this->callbacks.sel_collection))); + for (const PickerItem &item : this->callbacks.saved.at(this->callbacks.sel_collection)) { /* The used list may contain items that aren't currently loaded, skip these. */ if (item.class_index == -1) continue; if (!show_all && item.class_index != cls_id) continue; @@ -741,6 +909,30 @@ void PickerWindow::EnsureSelectedTypeIsVisible() this->GetWidget(WID_PW_TYPE_MATRIX)->SetClicked(pos); } +/** Builds the filter list of collections. */ +void PickerWindow::BuildPickerCollectionList() +{ + if (!this->collections.NeedRebuild()) return; + + int count = std::max(static_cast(this->callbacks.saved.size()), 1); + + this->collections.clear(); + this->collections.reserve(count); + + if (this->callbacks.saved.find("") == this->callbacks.saved.end()) { + this->collections.emplace_back(""); + } + + for (auto it = this->callbacks.saved.begin(); it != this->callbacks.saved.end(); it++) { + this->collections.emplace_back(it->first); + } + + this->collections.RebuildDone(); + this->collections.Sort(); + + if (!this->has_class_picker) return; +} + /** Create nested widgets for the class picker widgets. */ std::unique_ptr MakePickerClassWidgets() { @@ -750,12 +942,24 @@ std::unique_ptr MakePickerClassWidgets() NWidget(WWT_PANEL, COLOUR_DARK_GREEN), NWidget(WWT_EDITBOX, COLOUR_DARK_GREEN, WID_PW_CLASS_FILTER), SetMinimalSize(144, 0), SetPadding(2), SetFill(1, 0), SetStringTip(STR_LIST_FILTER_OSKTITLE, STR_LIST_FILTER_TOOLTIP), EndContainer(), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PANEL, COLOUR_DARK_GREEN), - NWidget(WWT_MATRIX, COLOUR_GREY, WID_PW_CLASS_LIST), SetFill(1, 1), SetResize(1, 1), SetPadding(WidgetDimensions::unscaled.picker), - SetMatrixDataTip(1, 0), SetScrollbar(WID_PW_CLASS_SCROLL), + /* Collection view */ + NWidget(NWID_VERTICAL), + NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), + NWidget(WWT_PUSHTXTBTN, COLOUR_DARK_GREEN, WID_PW_COLEC_ADD), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_PICKER_COLLECTION_ADD, STR_PICKER_COLLECTION_ADD_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_DARK_GREEN, WID_PW_COLEC_RENAME), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_PICKER_COLLECTION_RENAME, STR_PICKER_COLLECTION_RENAME_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_DARK_GREEN, WID_PW_COLEC_DELETE), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_PICKER_COLLECTION_DELETE, STR_PICKER_COLLECTION_DELETE_TOOLTIP), + EndContainer(), + NWidget(WWT_DROPDOWN, COLOUR_DARK_GREEN, WID_PW_COLEC_LIST), SetMinimalSize(144, 12), SetFill(0, 1), SetResize(1, 0), SetToolTip(STR_PICKER_SELECT_COLLECTION_TOOLTIP), + EndContainer(), + /* Class view */ + NWidget(NWID_VERTICAL), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PANEL, COLOUR_DARK_GREEN), + NWidget(WWT_MATRIX, COLOUR_GREY, WID_PW_CLASS_LIST), SetFill(1, 1), SetResize(1, 1), SetPadding(WidgetDimensions::unscaled.picker), + SetMatrixDataTip(1, 0), SetScrollbar(WID_PW_CLASS_SCROLL), + EndContainer(), + NWidget(NWID_VSCROLLBAR, COLOUR_DARK_GREEN, WID_PW_CLASS_SCROLL), EndContainer(), - NWidget(NWID_VSCROLLBAR, COLOUR_DARK_GREEN, WID_PW_CLASS_SCROLL), EndContainer(), EndContainer(), EndContainer(), diff --git a/src/picker_gui.h b/src/picker_gui.h index 6a235e1e4f..7670ec9f3d 100644 --- a/src/picker_gui.h +++ b/src/picker_gui.h @@ -66,7 +66,6 @@ public: virtual StringID GetTypeTooltip() const = 0; /** Get the number of types in a class. @note Used only to estimate space requirements. */ virtual int GetTypeCount(int cls_id) const = 0; - /** Get the selected type. */ virtual int GetSelectedType() const = 0; /** Set the selected type. */ @@ -82,10 +81,38 @@ public: /** Draw preview image of an item. */ virtual void DrawType(int x, int y, int cls_id, int id) const = 0; + /* Collection Callbacks */ + /** Get the tooltip string for the collection list. */ + virtual StringID GetCollectionTooltip() const = 0; + /** Fill a set with all items that are used by the current player. */ virtual void FillUsedItems(std::set &items) = 0; /** Update link between grfid/localidx and class_index/index in saved items. */ - virtual std::set UpdateSavedItems(const std::set &src) = 0; + virtual std::map> UpdateSavedItems(const std::map> &src) = 0; + /** + * Initialize the list of active collections for sorting purposes. + * @param collections The map of collections to check. + * @return The set of collections with inactive items. + */ + inline std::set InitializeInactiveCollections(const std::map> collections) + { + std::set inactive; + + for (const auto &collection : collections) { + if ((collection.second.size() == 1 && collection.second.contains({})) || collection.first == "") continue; + for (const PickerItem &item : collection.second) { + if (item.class_index == -1 || item.index == -1) { + inactive.emplace(collection.first); + break; + } + if (GetTypeName(item.class_index, item.index) == INVALID_STRING_ID) { + inactive.emplace(collection.first); + break; + } + } + } + return inactive; + } Listing class_last_sorting = { false, 0 }; ///< Default sorting of #PickerClassList. Filtering class_last_filtering = { false, 0 }; ///< Default filtering of #PickerClassList. @@ -93,13 +120,19 @@ public: Listing type_last_sorting = { false, 0 }; ///< Default sorting of #PickerTypeList. Filtering type_last_filtering = { false, 0 }; ///< Default filtering of #PickerTypeList. + Listing collection_last_sorting = { false, 0 }; ///< Default sorting of #PickerCollectionList. + const std::string ini_group; ///< Ini Group for saving favourites. uint8_t mode = 0; ///< Bitmask of \c PickerFilterModes. + bool rename_collection = false; ///< Are we renaming a collection? + std::string sel_collection; ///< Currently selected collection of saved items. + std::string edit_collection; ///< Collection to rename or delete. + std::set rm_collections; ///< Set of removed or renamed collections for updating ini file. int preview_height = 0; ///< Previously adjusted height. std::set used; ///< Set of items used in the current game by the current company. - std::set saved; ///< Set of saved favourite items. + std::map> saved; ///< Set of saved collections of items. }; /** Helper for PickerCallbacks when the class system is based on NewGRFClass. */ @@ -128,17 +161,24 @@ public: return GetPickerItem(GetClass(cls_id)->GetSpec(id), cls_id, id); } - std::set UpdateSavedItems(const std::set &src) override + std::map> UpdateSavedItems(const std::map> &src) override { if (src.empty()) return {}; - std::set dst; - for (const auto &item : src) { - const auto *spec = T::GetByGrf(item.grfid, item.local_id); - if (spec == nullptr) { - dst.emplace(item.grfid, item.local_id, -1, -1); - } else { - dst.emplace(GetPickerItem(spec)); + std::map> dst; + for (auto it = src.begin(); it != src.end(); it++) { + if (it->second.empty() || (it->second.size() == 1 && it->second.contains({}))) { + dst[it->first]; + continue; + } + + for (const auto &item : it->second) { + const auto *spec = T::GetByGrf(item.grfid, item.local_id); + if (spec == nullptr) { + dst[it->first].emplace(item.grfid, item.local_id, -1, -1); + } else { + dst[it->first].emplace(GetPickerItem(spec)); + } } } return dst; @@ -153,6 +193,7 @@ struct PickerFilterData : StringFilter { using PickerClassList = GUIList; ///< GUIList holding classes to display. using PickerTypeList = GUIList; ///< GUIList holding classes/types to display. +using PickerCollectionList = GUIList; ///< GUIList holding collections to display. class PickerWindow : public PickerWindowBase { public: @@ -166,6 +207,7 @@ public: enum class PickerInvalidation : uint8_t { Class, ///< Refresh the class list. Type, ///< Refresh the type list. + Collection, ///< Refresh the collection list. Position, ///< Update scroll positions. Validate, ///< Validate selected item. Filter, ///< Update filter state. @@ -186,17 +228,22 @@ public: bool has_class_picker = false; ///< Set if this window has a class picker 'component'. bool has_type_picker = false; ///< Set if this window has a type picker 'component'. + bool has_collection_picker = false; ///< Set if this window has a collection picker 'component'. int preview_height = 0; ///< Height of preview images. + std::set inactive; ///< Set of collections with inactive items. PickerWindow(WindowDesc &desc, Window *parent, int window_number, PickerCallbacks &callbacks); void OnInit() override; void Close(int data = 0) override; void UpdateWidgetSize(WidgetID widget, Dimension &size, const Dimension &padding, Dimension &fill, Dimension &resize) override; std::string GetWidgetString(WidgetID widget, StringID stringid) const override; + DropDownList BuildCollectionDropDownList(); void DrawWidget(const Rect &r, WidgetID widget) const override; void OnDropdownSelect(WidgetID widget, int index, int click_result) override; void OnResize() override; + void static DeletePickerCollectionCallback(Window *win, bool confirmed); void OnClick(Point pt, WidgetID widget, int click_count) override; + void OnQueryTextFinished(std::optional str) override; void OnInvalidateData(int data = 0, bool gui_scope = true) override; EventState OnHotkey(int hotkey) override; void OnEditboxChanged(WidgetID wid) override; @@ -231,6 +278,10 @@ private: void EnsureSelectedTypeIsValid(); void EnsureSelectedTypeIsVisible(); + PickerCollectionList collections; ///< List of collections. + + void BuildPickerCollectionList(); + GUIBadgeClasses badge_classes; std::pair badge_filters{}; BadgeFilterChoices badge_filter_choices{}; diff --git a/src/rail_gui.cpp b/src/rail_gui.cpp index 1ce857d2ac..059bd6c2b0 100644 --- a/src/rail_gui.cpp +++ b/src/rail_gui.cpp @@ -978,6 +978,7 @@ public: StringID GetClassTooltip() const override { return STR_PICKER_STATION_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_STATION_TYPE_TOOLTIP; } + StringID GetCollectionTooltip() const override { return STR_PICKER_STATION_COLLECTION_TOOLTIP; } bool IsActive() const override { @@ -1796,6 +1797,7 @@ public: StringID GetClassTooltip() const override { return STR_PICKER_WAYPOINT_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_WAYPOINT_TYPE_TOOLTIP; } + StringID GetCollectionTooltip() const override { return STR_PICKER_WAYPOINT_COLLECTION_TOOLTIP; } bool IsActive() const override { diff --git a/src/road_gui.cpp b/src/road_gui.cpp index 11e8505661..7bc74b76f7 100644 --- a/src/road_gui.cpp +++ b/src/road_gui.cpp @@ -1200,6 +1200,7 @@ public: StringID GetClassTooltip() const override; StringID GetTypeTooltip() const override; + StringID GetCollectionTooltip() const override; bool IsActive() const override { @@ -1290,9 +1291,11 @@ public: template <> StringID RoadStopPickerCallbacks::GetClassTooltip() const { return STR_PICKER_ROADSTOP_BUS_CLASS_TOOLTIP; } template <> StringID RoadStopPickerCallbacks::GetTypeTooltip() const { return STR_PICKER_ROADSTOP_BUS_TYPE_TOOLTIP; } +template <> StringID RoadStopPickerCallbacks::GetCollectionTooltip() const { return STR_PICKER_ROADSTOP_BUS_COLLECTION_TOOLTIP; } template <> StringID RoadStopPickerCallbacks::GetClassTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_CLASS_TOOLTIP; } template <> StringID RoadStopPickerCallbacks::GetTypeTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_TYPE_TOOLTIP; } +template <> StringID RoadStopPickerCallbacks::GetCollectionTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_COLLECTION_TOOLTIP; } static RoadStopPickerCallbacks _bus_callback_instance("fav_passenger_roadstops"); static RoadStopPickerCallbacks _truck_callback_instance("fav_freight_roadstops"); @@ -1632,6 +1635,7 @@ public: StringID GetClassTooltip() const override { return STR_PICKER_WAYPOINT_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_WAYPOINT_TYPE_TOOLTIP; } + StringID GetCollectionTooltip() const override { return STR_PICKER_WAYPOINT_COLLECTION_TOOLTIP; } bool IsActive() const override { diff --git a/src/town_gui.cpp b/src/town_gui.cpp index 502e6d723a..9005e5191e 100644 --- a/src/town_gui.cpp +++ b/src/town_gui.cpp @@ -1491,6 +1491,7 @@ public: StringID GetClassTooltip() const override { return STR_PICKER_HOUSE_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_HOUSE_TYPE_TOOLTIP; } + StringID GetCollectionTooltip() const override { return STR_PICKER_HOUSE_COLLECTION_TOOLTIP; } bool IsActive() const override { return true; } bool HasClassChoice() const override { return true; } @@ -1580,27 +1581,34 @@ public: } } - std::set UpdateSavedItems(const std::set &src) override + std::map> UpdateSavedItems(const std::map> &src) override { if (src.empty()) return src; const auto &specs = HouseSpec::Specs(); - std::set dst; - for (const auto &item : src) { - if (item.grfid == 0) { - const HouseSpec *hs = HouseSpec::Get(item.local_id); - if (hs == nullptr) continue; - int class_index = GetClassIdFromHouseZone(hs->building_availability); - dst.emplace(item.grfid, item.local_id, class_index, item.local_id); - } else { - /* Search for spec by grfid and local index. */ - auto it = std::ranges::find_if(specs, [&item](const HouseSpec &spec) { return spec.grf_prop.grfid == item.grfid && spec.grf_prop.local_id == item.local_id; }); - if (it == specs.end()) { - /* Not preset, hide from UI. */ - dst.emplace(item.grfid, item.local_id, -1, -1); + std::map> dst; + for (auto group_it = src.begin(); group_it != src.end(); group_it++) { + if (group_it->second.empty() || (group_it->second.size() == 1 && group_it->second.contains({}))) { + dst[group_it->first]; + continue; + } + + for (const auto &item : group_it->second) { + if (item.grfid == 0) { + const HouseSpec *hs = HouseSpec::Get(item.local_id); + if (hs == nullptr) continue; + int class_index = GetClassIdFromHouseZone(hs->building_availability); + dst[group_it->first].emplace(item.grfid, item.local_id, class_index, item.local_id); } else { - int class_index = GetClassIdFromHouseZone(it->building_availability); - dst.emplace(item.grfid, item.local_id, class_index, it->Index()); + /* Search for spec by grfid and local index. */ + auto it = std::ranges::find_if(specs, [&item](const HouseSpec &spec) { return spec.grf_prop.grfid == item.grfid && spec.grf_prop.local_id == item.local_id; }); + if (it == specs.end()) { + /* Not preset, hide from UI. */ + dst[group_it->first].emplace(item.grfid, item.local_id, -1, -1); + } else { + int class_index = GetClassIdFromHouseZone(it->building_availability); + dst[group_it->first].emplace(item.grfid, item.local_id, class_index, it->Index()); + } } } } diff --git a/src/widgets/picker_widget.h b/src/widgets/picker_widget.h index b153bb6d11..a82e180553 100644 --- a/src/widgets/picker_widget.h +++ b/src/widgets/picker_widget.h @@ -19,6 +19,11 @@ enum PickerClassWindowWidgets : WidgetID { WID_PW_CLASS_LIST, ///< List of classes. WID_PW_CLASS_SCROLL, ///< Scrollbar for list of classes. + WID_PW_COLEC_LIST, ///< List of collections. + WID_PW_COLEC_ADD, ///< Button to create a new collections. + WID_PW_COLEC_RENAME, ///< Button to rename a collections. + WID_PW_COLEC_DELETE, ///< Button to delete a collection. + WID_PW_TYPE_SEL, ///< Stack to hide the type picker. WID_PW_TYPE_FILTER, ///< Text filter. WID_PW_MODE_ALL, ///< Toggle "Show all" filter mode. @@ -32,7 +37,7 @@ enum PickerClassWindowWidgets : WidgetID { WID_PW_TYPE_NAME, ///< Name of selected item. WID_PW_TYPE_RESIZE, ///< Type resize handle. WID_PW_CONFIGURE_BADGES, ///< Button to configure badges. - WID_PW_BADGE_FILTER, ///< Container for dropdown badge filters. + WID_PW_BADGE_FILTER, ///< Container for dropdown badge filters. Must be last in this list. }; #endif /* WIDGETS_PICKER_WIDGET_H */