1
0
Fork 0

Feature: Ctrl-click to toggle favourites in build-pickers.

This allows ctrl-click on a type in a build-picker window to remember it
as a favourite. An new filter button to show only favourites makes it
simpler to use these types.

Favourite types are saved locally in favs.cfg, so are remembered between
games.
pull/12653/head
Peter Nelson 2024-05-07 12:13:49 +01:00 committed by Peter Nelson
parent fde3b35a24
commit b30fe0d7da
11 changed files with 190 additions and 6 deletions

View File

@ -338,6 +338,7 @@ add_files(
palette_func.h
pbs.cpp
pbs.h
picker_func.h
picker_gui.cpp
picker_gui.h
progress.cpp

View File

@ -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) {

View File

@ -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

View File

@ -43,6 +43,8 @@ static ObjectPickerSelection _object_gui; ///< Settings of the object picker.
class ObjectPickerCallbacks : public PickerCallbacksNewGRFClass<ObjectClass> {
public:
ObjectPickerCallbacks() : PickerCallbacksNewGRFClass<ObjectClass>("fav_objects") {}
StringID GetClassTooltip() const override { return STR_PICKER_OBJECT_CLASS_TOOLTIP; }
StringID GetTypeTooltip() const override { return STR_PICKER_OBJECT_TYPE_TOOLTIP; }

16
src/picker_func.h 100644
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/** @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 */

View File

@ -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 <charconv>
#include "safeguards.h"
static std::vector<PickerCallbacks *> &GetPickerCallbacks()
{
static std::vector<PickerCallbacks *> 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<uint8_t, 4> grfid_buf;
std::string_view str = item.name;
/* Try reading "<grfid>|<localid>" */
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<NWidgetBase>(widget)->GetParentWidget<NWidgetMatrix>()->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<NWidgetBase> 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),

View File

@ -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<PickerItem> &items) = 0;
/** Update link between grfid/localidx and class_index/index in saved items. */
virtual std::set<PickerItem> UpdateSavedItems(const std::set<PickerItem> &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<PickerItem> used; ///< Set of items used in the current game by the current company.
std::set<PickerItem> saved; ///< Set of saved favourite items.
};
/** Helper for PickerCallbacks when the class system is based on NewGRFClass. */
template <typename T>
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<typename T::index_type>(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<PickerItem> UpdateSavedItems(const std::set<PickerItem> &src) override
{
if (src.empty()) return {};
std::set<PickerItem> 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 {

View File

@ -961,6 +961,8 @@ static bool StationUsesDefaultType(const BaseStation *bst)
class StationPickerCallbacks : public PickerCallbacksNewGRFClass<StationClass> {
public:
StationPickerCallbacks() : PickerCallbacksNewGRFClass<StationClass>("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<StationClass> {
public:
WaypointPickerCallbacks() : PickerCallbacksNewGRFClass<StationClass>("fav_waypoints") {}
StringID GetClassTooltip() const override { return STR_PICKER_WAYPOINT_CLASS_TOOLTIP; }
StringID GetTypeTooltip() const override { return STR_PICKER_WAYPOINT_TYPE_TOOLTIP; }

View File

@ -1097,6 +1097,8 @@ static void ShowRoadDepotPicker(Window *parent)
template <RoadStopType roadstoptype>
class RoadStopPickerCallbacks : public PickerCallbacksNewGRFClass<RoadStopClass> {
public:
RoadStopPickerCallbacks(const std::string &ini_group) : PickerCallbacksNewGRFClass<RoadStopClass>(ini_group) {}
StringID GetClassTooltip() const override;
StringID GetTypeTooltip() const override;
@ -1185,8 +1187,8 @@ template <> StringID RoadStopPickerCallbacks<ROADSTOP_BUS>::GetTypeTooltip() con
template <> StringID RoadStopPickerCallbacks<ROADSTOP_TRUCK>::GetClassTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_CLASS_TOOLTIP; }
template <> StringID RoadStopPickerCallbacks<ROADSTOP_TRUCK>::GetTypeTooltip() const { return STR_PICKER_ROADSTOP_TRUCK_TYPE_TOOLTIP; }
static RoadStopPickerCallbacks<ROADSTOP_BUS> _bus_callback_instance;
static RoadStopPickerCallbacks<ROADSTOP_TRUCK> _truck_callback_instance;
static RoadStopPickerCallbacks<ROADSTOP_BUS> _bus_callback_instance("fav_passenger_roadstops");
static RoadStopPickerCallbacks<ROADSTOP_TRUCK> _truck_callback_instance("fav_freight_roadstops");
static PickerCallbacks &GetRoadStopPickerCallbacks(RoadStopType rs)
{

View File

@ -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);
}
/**

View File

@ -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.