diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c913e1f190..9b946e02a1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -338,6 +338,7 @@ add_files( palette_func.h pbs.cpp pbs.h + picker_func.h picker_gui.cpp picker_gui.h progress.cpp diff --git a/src/fileio.cpp b/src/fileio.cpp index c99ec88bc1..7bf8a76198 100644 --- a/src/fileio.cpp +++ b/src/fileio.cpp @@ -959,6 +959,8 @@ void DeterminePaths(const char *exe, bool only_local_path) _private_file = config_dir + "private.cfg"; extern std::string _secrets_file; _secrets_file = config_dir + "secrets.cfg"; + extern std::string _favs_file; + _favs_file = config_dir + "favs.cfg"; #ifdef USE_XDG if (config_dir == config_home) { diff --git a/src/lang/english.txt b/src/lang/english.txt index d45870a205..12eed0068a 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2801,6 +2801,8 @@ STR_PICKER_MODE_ALL :All STR_PICKER_MODE_ALL_TOOLTIP :Toggle showing items from all classes STR_PICKER_MODE_USED :Used STR_PICKER_MODE_USED_TOOLTIP :Toggle showing only existing items +STR_PICKER_MODE_SAVED :Saved +STR_PICKER_MODE_SAVED_TOOLTIP :Toogle showing only saved items 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 diff --git a/src/object_gui.cpp b/src/object_gui.cpp index ff4d86824f..bc8a9f6b5e 100644 --- a/src/object_gui.cpp +++ b/src/object_gui.cpp @@ -43,6 +43,8 @@ static ObjectPickerSelection _object_gui; ///< Settings of the object picker. class ObjectPickerCallbacks : public PickerCallbacksNewGRFClass { public: + ObjectPickerCallbacks() : PickerCallbacksNewGRFClass("fav_objects") {} + StringID GetClassTooltip() const override { return STR_PICKER_OBJECT_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_OBJECT_TYPE_TOOLTIP; } diff --git a/src/picker_func.h b/src/picker_func.h new file mode 100644 index 0000000000..b3c1a8a16b --- /dev/null +++ b/src/picker_func.h @@ -0,0 +1,16 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file picker_func.h Functions/types etc. related to non-GUI parts of the Picker system. */ + +#ifndef PICKER_FUNC_H +#define PICKER_FUNC_H + +void PickerLoadConfig(const IniFile &ini); +void PickerSaveConfig(IniFile &ini); + +#endif /* PICKER_FUNC_H */ diff --git a/src/picker_gui.cpp b/src/picker_gui.cpp index 94841519bf..f1fd6f661b 100644 --- a/src/picker_gui.cpp +++ b/src/picker_gui.cpp @@ -11,12 +11,14 @@ #include "core/backup_type.hpp" #include "gui.h" #include "hotkeys.h" +#include "ini_type.h" #include "picker_gui.h" #include "querystring_gui.h" #include "settings_type.h" #include "sortlist_type.h" #include "sound_func.h" #include "sound_type.h" +#include "string_func.h" #include "stringfilter_type.h" #include "strings_func.h" #include "widget_type.h" @@ -29,8 +31,95 @@ #include "table/sprites.h" +#include + #include "safeguards.h" +static std::vector &GetPickerCallbacks() +{ + static std::vector callbacks; + return callbacks; +} + +PickerCallbacks::PickerCallbacks(const std::string &ini_group) : ini_group(ini_group) +{ + GetPickerCallbacks().push_back(this); +} + +PickerCallbacks::~PickerCallbacks() +{ + auto &callbacks = GetPickerCallbacks(); + callbacks.erase(std::find(callbacks.begin(), callbacks.end(), this)); +} + +/** + * Load favourites of a picker from config. + * @param ini IniFile to load to. + * @param callbacks Picker to load. + */ +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; + + std::string_view str = item.name; + + /* Try reading "|" */ + auto grfid_pos = str.find('|'); + if (grfid_pos == std::string_view::npos) continue; + + 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.insert({grfid, localid, 0, 0}); + } + } +} + +/** + * Save favourites of a picker to config. + * @param ini IniFile to save to. + * @param callbacks Picker to save. + */ +static void PickerSaveConfig(IniFile &ini, const PickerCallbacks &callbacks) +{ + IniGroup &group = ini.GetOrCreateGroup(callbacks.ini_group); + group.Clear(); + + for (const PickerItem &item : callbacks.saved) { + std::string key = fmt::format("{:08X}|{}", BSWAP32(item.grfid), item.local_id); + group.CreateItem(key); + } +} + +/** + * Load favourites of all registered Pickers from config. + * @param ini IniFile to load to. + */ +void PickerLoadConfig(const IniFile &ini) +{ + for (auto *cb : GetPickerCallbacks()) PickerLoadConfig(ini, *cb); +} + +/** + * Save favourites of all registered Pickers to config. + * @param ini IniFile to save to. + */ +void PickerSaveConfig(IniFile &ini) +{ + for (const auto *cb : GetPickerCallbacks()) PickerSaveConfig(ini, *cb); +} + /** Sort classes by id. */ static bool ClassIDSorter(int const &a, int const &b) { @@ -109,7 +198,8 @@ void PickerWindow::ConstructWindow() this->classes.SetFilterFuncs(_class_filter_funcs); if (this->has_type_picker) { - /* Update used type information. */ + /* Update used and saved type information. */ + this->callbacks.saved = this->callbacks.UpdateSavedItems(this->callbacks.saved); this->callbacks.used.clear(); this->callbacks.FillUsedItems(this->callbacks.used); @@ -206,6 +296,9 @@ void PickerWindow::DrawWidget(const Rect &r, WidgetID widget) const int y = (ir.Height() + ScaleSpriteTrad(PREVIEW_HEIGHT)) / 2 - ScaleSpriteTrad(PREVIEW_BOTTOM); this->callbacks.DrawType(x, y, item.class_index, item.index); + if (this->callbacks.saved.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); } @@ -251,6 +344,7 @@ void PickerWindow::OnClick(Point pt, WidgetID widget, int) case WID_PW_MODE_ALL: case WID_PW_MODE_USED: + case WID_PW_MODE_SAVED: ToggleBit(this->callbacks.mode, widget - WID_PW_MODE_ALL); if (!this->IsWidgetDisabled(WID_PW_MODE_ALL) && HasBit(this->callbacks.mode, widget - WID_PW_MODE_ALL)) { /* Enabling used or saved filters automatically enables all. */ @@ -264,6 +358,18 @@ void PickerWindow::OnClick(Point pt, WidgetID widget, int) int sel = this->GetWidget(widget)->GetParentWidget()->GetCurrentElement(); assert(sel < (int)this->types.size()); 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); + } + this->InvalidateData(PFI_TYPE); + break; + } + if (this->callbacks.IsTypeAvailable(item.class_index, item.index)) { this->callbacks.SetSelectedClass(item.class_index); this->callbacks.SetSelectedType(item.index); @@ -294,6 +400,7 @@ void PickerWindow::OnInvalidateData(int data, bool gui_scope) 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)); + SetWidgetLoweredState(WID_PW_MODE_SAVED, HasBit(this->callbacks.mode, PFM_SAVED)); } } @@ -346,9 +453,11 @@ void PickerWindow::BuildPickerClassList() this->classes.reserve(count); bool filter_used = HasBit(this->callbacks.mode, PFM_USED); + bool filter_saved = HasBit(this->callbacks.mode, PFM_SAVED); 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; this->classes.emplace_back(i); } @@ -396,18 +505,30 @@ void PickerWindow::BuildPickerTypeList() if (!this->types.NeedRebuild()) return; this->types.clear(); + bool show_all = HasBit(this->callbacks.mode, PFM_ALL); bool filter_used = HasBit(this->callbacks.mode, PFM_USED); + bool filter_saved = HasBit(this->callbacks.mode, PFM_SAVED); int cls_id = this->callbacks.GetSelectedClass(); if (filter_used) { - /* Showing used items. */ + /* Showing used items. May also be filtered by saved items. */ this->types.reserve(this->callbacks.used.size()); for (const PickerItem &item : this->callbacks.used) { if (!show_all && item.class_index != cls_id) continue; if (this->callbacks.GetTypeName(item.class_index, item.index) == INVALID_STRING_ID) continue; this->types.emplace_back(item); } + } else if (filter_saved) { + /* Showing only saved items. */ + this->types.reserve(this->callbacks.saved.size()); + for (const PickerItem &item : this->callbacks.saved) { + /* 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; + if (this->callbacks.GetTypeName(item.class_index, item.index) == INVALID_STRING_ID) continue; + this->types.emplace_back(item); + } } else if (show_all) { /* Reserve enough space for everything. */ int total = 0; @@ -506,6 +627,7 @@ std::unique_ptr MakePickerTypeWidgets() NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), NWidget(WWT_TEXTBTN, COLOUR_DARK_GREEN, WID_PW_MODE_ALL), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_PICKER_MODE_ALL, STR_PICKER_MODE_ALL_TOOLTIP), NWidget(WWT_TEXTBTN, COLOUR_DARK_GREEN, WID_PW_MODE_USED), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_PICKER_MODE_USED, STR_PICKER_MODE_USED_TOOLTIP), + NWidget(WWT_TEXTBTN, COLOUR_DARK_GREEN, WID_PW_MODE_SAVED), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_PICKER_MODE_SAVED, STR_PICKER_MODE_SAVED_TOOLTIP), EndContainer(), NWidget(NWID_HORIZONTAL), NWidget(WWT_PANEL, COLOUR_DARK_GREEN), SetScrollbar(WID_PW_TYPE_SCROLL), diff --git a/src/picker_gui.h b/src/picker_gui.h index 6f323372ee..78403be187 100644 --- a/src/picker_gui.h +++ b/src/picker_gui.h @@ -36,7 +36,9 @@ struct PickerItem { /** Class for PickerClassWindow to collect information and retain state. */ class PickerCallbacks { public: - virtual ~PickerCallbacks() {} + explicit PickerCallbacks(const std::string &ini_group); + virtual ~PickerCallbacks(); + virtual void Close(int) { } /** Should picker class/type selection be enabled? */ @@ -77,6 +79,8 @@ public: /** 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; Listing class_last_sorting = { false, 0 }; ///< Default sorting of #PickerClassList. Filtering class_last_filtering = { false, 0 }; ///< Default filtering of #PickerClassList. @@ -84,15 +88,19 @@ public: Listing type_last_sorting = { false, 0 }; ///< Default sorting of #PickerTypeList. Filtering type_last_filtering = { false, 0 }; ///< Default filtering of #PickerTypeList. + const std::string ini_group; ///< Ini Group for saving favourites. uint8_t mode = 0; ///< Bitmask of \c PickerFilterModes. std::set used; ///< Set of items used in the current game by the current company. + std::set saved; ///< Set of saved favourite items. }; /** Helper for PickerCallbacks when the class system is based on NewGRFClass. */ template class PickerCallbacksNewGRFClass : public PickerCallbacks { public: + explicit PickerCallbacksNewGRFClass(const std::string &ini_group) : PickerCallbacks(ini_group) {} + inline typename T::index_type GetClassIndex(int cls_id) const { return static_cast(cls_id); } inline const T *GetClass(int cls_id) const { return T::Get(this->GetClassIndex(cls_id)); } inline const typename T::spec_type *GetSpec(int cls_id, int id) const { return this->GetClass(cls_id)->GetSpec(id); } @@ -112,8 +120,23 @@ public: { return GetPickerItem(GetClass(cls_id)->GetSpec(id), cls_id, id); } -}; + std::set UpdateSavedItems(const std::set &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.insert({item.grfid, item.local_id, -1, -1}); + } else { + dst.insert(GetPickerItem(spec)); + } + } + return dst; + } +}; struct PickerFilterData : StringFilter { const PickerCallbacks *callbacks; ///< Callbacks for filter functions to access to callbacks. @@ -127,6 +150,7 @@ public: enum PickerFilterModes { PFM_ALL = 0, ///< Show all classes. PFM_USED = 1, ///< Show used types. + PFM_SAVED = 2, ///< Show saved types. }; enum PickerFilterInvalidation { diff --git a/src/rail_gui.cpp b/src/rail_gui.cpp index 6e2e87ab93..316ea47ee2 100644 --- a/src/rail_gui.cpp +++ b/src/rail_gui.cpp @@ -961,6 +961,8 @@ static bool StationUsesDefaultType(const BaseStation *bst) class StationPickerCallbacks : public PickerCallbacksNewGRFClass { public: + StationPickerCallbacks() : PickerCallbacksNewGRFClass("fav_stations") {} + StringID GetClassTooltip() const override { return STR_PICKER_STATION_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_STATION_TYPE_TOOLTIP; } @@ -1770,6 +1772,8 @@ static void ShowBuildTrainDepotPicker(Window *parent) class WaypointPickerCallbacks : public PickerCallbacksNewGRFClass { public: + WaypointPickerCallbacks() : PickerCallbacksNewGRFClass("fav_waypoints") {} + StringID GetClassTooltip() const override { return STR_PICKER_WAYPOINT_CLASS_TOOLTIP; } StringID GetTypeTooltip() const override { return STR_PICKER_WAYPOINT_TYPE_TOOLTIP; } diff --git a/src/road_gui.cpp b/src/road_gui.cpp index dc21a4fadb..6cb897e20e 100644 --- a/src/road_gui.cpp +++ b/src/road_gui.cpp @@ -1097,6 +1097,8 @@ static void ShowRoadDepotPicker(Window *parent) template class RoadStopPickerCallbacks : public PickerCallbacksNewGRFClass { public: + RoadStopPickerCallbacks(const std::string &ini_group) : PickerCallbacksNewGRFClass(ini_group) {} + StringID GetClassTooltip() const override; StringID GetTypeTooltip() const override; @@ -1185,8 +1187,8 @@ template <> StringID RoadStopPickerCallbacks::GetTypeTooltip() con template <> StringID RoadStopPickerCallbacks::GetClassTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_CLASS_TOOLTIP; } template <> StringID RoadStopPickerCallbacks::GetTypeTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_TYPE_TOOLTIP; } -static RoadStopPickerCallbacks _bus_callback_instance; -static RoadStopPickerCallbacks _truck_callback_instance; +static RoadStopPickerCallbacks _bus_callback_instance("fav_passenger_roadstops"); +static RoadStopPickerCallbacks _truck_callback_instance("fav_freight_roadstops"); static PickerCallbacks &GetRoadStopPickerCallbacks(RoadStopType rs) { diff --git a/src/settings.cpp b/src/settings.cpp index 293c20fb95..33f6b1d0bf 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -43,6 +43,7 @@ #include "ai/ai_config.hpp" #include "game/game_config.hpp" #include "newgrf_config.h" +#include "picker_func.h" #include "base_media_base.h" #include "fios.h" #include "fileio_func.h" @@ -59,6 +60,7 @@ VehicleDefaultSettings _old_vds; ///< Used for loading default vehicles settings std::string _config_file; ///< Configuration file of OpenTTD. std::string _private_file; ///< Private configuration file of OpenTTD. std::string _secrets_file; ///< Secrets configuration file of OpenTTD. +std::string _favs_file; ///< Picker favourites configuration file of OpenTTD. static ErrorList _settings_error_list; ///< Errors while loading minimal settings. @@ -1352,6 +1354,7 @@ void LoadFromConfig(bool startup) ConfigIniFile generic_ini(_config_file); ConfigIniFile private_ini(_private_file); ConfigIniFile secrets_ini(_secrets_file); + ConfigIniFile favs_ini(_favs_file); if (!startup) ResetCurrencies(false); // Initialize the array of currencies, without preserving the custom one @@ -1423,6 +1426,7 @@ void LoadFromConfig(bool startup) _grfconfig_static = GRFLoadConfig(generic_ini, "newgrf-static", true); AILoadConfig(generic_ini, "ai_players"); GameLoadConfig(generic_ini, "game_scripts"); + PickerLoadConfig(favs_ini); PrepareOldDiffCustom(); IniLoadSettings(generic_ini, _old_gameopt_settings, "gameopt", &_settings_newgame, false); @@ -1443,6 +1447,7 @@ void SaveToConfig() ConfigIniFile generic_ini(_config_file); ConfigIniFile private_ini(_private_file); ConfigIniFile secrets_ini(_secrets_file); + ConfigIniFile favs_ini(_favs_file); IniFileVersion generic_version = LoadVersionFromConfig(generic_ini); @@ -1494,14 +1499,17 @@ void SaveToConfig() GRFSaveConfig(generic_ini, "newgrf-static", _grfconfig_static); AISaveConfig(generic_ini, "ai_players"); GameSaveConfig(generic_ini, "game_scripts"); + PickerSaveConfig(favs_ini); SaveVersionInConfig(generic_ini); SaveVersionInConfig(private_ini); SaveVersionInConfig(secrets_ini); + SaveVersionInConfig(favs_ini); generic_ini.SaveToDisk(_config_file); private_ini.SaveToDisk(_private_file); secrets_ini.SaveToDisk(_secrets_file); + favs_ini.SaveToDisk(_favs_file); } /** diff --git a/src/widgets/picker_widget.h b/src/widgets/picker_widget.h index ee026db0e0..c6612c2b9d 100644 --- a/src/widgets/picker_widget.h +++ b/src/widgets/picker_widget.h @@ -23,6 +23,7 @@ enum PickerClassWindowWidgets : WidgetID { WID_PW_TYPE_FILTER, ///< Text filter. WID_PW_MODE_ALL, ///< Toggle "Show all" filter mode. WID_PW_MODE_USED, ///< Toggle showing only used types. + WID_PW_MODE_SAVED, ///< Toggle showing only saved types. WID_PW_TYPE_MATRIX, ///< Matrix with items. WID_PW_TYPE_ITEM, ///< A single item. WID_PW_TYPE_SCROLL, ///< Scrollbar for the matrix.