1
0
mirror of https://github.com/OpenTTD/OpenTTD.git synced 2025-08-31 10:29:10 +00:00

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.
This commit is contained in:
2024-05-07 12:13:49 +01:00
parent b6077d75ff
commit 29e8394c15
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

@@ -2810,6 +2810,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 Normal file
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.