OpenTTD/src/misc_gui.cpp
Rubidium bcfe0fb076 Codechange: introduce GetMainWindow() to properly account for nullptr checks
Some nullptr checks have been removed as they were not triggered with nullptr
with the null video driver and in dedicated server mode.
2023-01-14 21:15:23 +01:00

1281 lines
45 KiB
C++

/*
* 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 misc_gui.cpp GUIs for a number of misc windows. */
#include "stdafx.h"
#include "debug.h"
#include "landscape.h"
#include "error.h"
#include "gui.h"
#include "command_func.h"
#include "company_func.h"
#include "town.h"
#include "string_func.h"
#include "company_base.h"
#include "texteff.hpp"
#include "strings_func.h"
#include "window_func.h"
#include "querystring_gui.h"
#include "core/geometry_func.hpp"
#include "newgrf_debug.h"
#include "zoom_func.h"
#include "guitimer_func.h"
#include "viewport_func.h"
#include "landscape_cmd.h"
#include "rev.h"
#include "widgets/misc_widget.h"
#include "table/strings.h"
#include <sstream>
#include <iomanip>
#include "safeguards.h"
/** Method to open the OSK. */
enum OskActivation {
OSKA_DISABLED, ///< The OSK shall not be activated at all.
OSKA_DOUBLE_CLICK, ///< Double click on the edit box opens OSK.
OSKA_SINGLE_CLICK, ///< Single click after focus click opens OSK.
OSKA_IMMEDIATELY, ///< Focusing click already opens OSK.
};
static const NWidgetPart _nested_land_info_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_GREY),
NWidget(WWT_CAPTION, COLOUR_GREY), SetDataTip(STR_LAND_AREA_INFORMATION_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
NWidget(WWT_PUSHIMGBTN, COLOUR_GREY, WID_LI_LOCATION), SetMinimalSize(12, 14), SetDataTip(SPR_GOTO_LOCATION, STR_LAND_AREA_INFORMATION_LOCATION_TOOLTIP),
NWidget(WWT_DEBUGBOX, COLOUR_GREY),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_GREY, WID_LI_BACKGROUND), EndContainer(),
};
static WindowDesc _land_info_desc(
WDP_AUTO, "land_info", 0, 0,
WC_LAND_INFO, WC_NONE,
0,
_nested_land_info_widgets, lengthof(_nested_land_info_widgets)
);
class LandInfoWindow : public Window {
StringList landinfo_data; ///< Info lines to show.
std::string cargo_acceptance; ///< Centered multi-line string for cargo acceptance.
public:
TileIndex tile;
void DrawWidget(const Rect &r, int widget) const override
{
if (widget != WID_LI_BACKGROUND) return;
Rect ir = r.Shrink(WidgetDimensions::scaled.frametext);
for (size_t i = 0; i < this->landinfo_data.size(); i++) {
DrawString(ir, this->landinfo_data[i], i == 0 ? TC_LIGHT_BLUE : TC_FROMSTRING, SA_HOR_CENTER);
ir.top += FONT_HEIGHT_NORMAL + (i == 0 ? WidgetDimensions::scaled.vsep_wide : WidgetDimensions::scaled.vsep_normal);
}
if (!this->cargo_acceptance.empty()) {
SetDParamStr(0, this->cargo_acceptance);
DrawStringMultiLine(ir, STR_JUST_RAW_STRING, TC_FROMSTRING, SA_CENTER);
}
}
void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
{
if (widget != WID_LI_BACKGROUND) return;
size->height = WidgetDimensions::scaled.frametext.Vertical();
for (size_t i = 0; i < this->landinfo_data.size(); i++) {
uint width = GetStringBoundingBox(this->landinfo_data[i]).width + WidgetDimensions::scaled.frametext.Horizontal();
size->width = std::max(size->width, width);
size->height += FONT_HEIGHT_NORMAL + (i == 0 ? WidgetDimensions::scaled.vsep_wide : WidgetDimensions::scaled.vsep_normal);
}
if (!this->cargo_acceptance.empty()) {
uint width = GetStringBoundingBox(this->cargo_acceptance).width + WidgetDimensions::scaled.frametext.Horizontal();
size->width = std::max(size->width, std::min(300u, width));
SetDParamStr(0, cargo_acceptance);
size->height += GetStringHeight(STR_JUST_RAW_STRING, size->width - WidgetDimensions::scaled.frametext.Horizontal());
}
}
LandInfoWindow(TileIndex tile) : Window(&_land_info_desc), tile(tile)
{
this->InitNested();
#if defined(_DEBUG)
# define LANDINFOD_LEVEL 0
#else
# define LANDINFOD_LEVEL 1
#endif
Debug(misc, LANDINFOD_LEVEL, "TILE: {:#x} ({},{})", tile, TileX(tile), TileY(tile));
Debug(misc, LANDINFOD_LEVEL, "type = {:#x}", _m[tile].type);
Debug(misc, LANDINFOD_LEVEL, "height = {:#x}", _m[tile].height);
Debug(misc, LANDINFOD_LEVEL, "m1 = {:#x}", _m[tile].m1);
Debug(misc, LANDINFOD_LEVEL, "m2 = {:#x}", _m[tile].m2);
Debug(misc, LANDINFOD_LEVEL, "m3 = {:#x}", _m[tile].m3);
Debug(misc, LANDINFOD_LEVEL, "m4 = {:#x}", _m[tile].m4);
Debug(misc, LANDINFOD_LEVEL, "m5 = {:#x}", _m[tile].m5);
Debug(misc, LANDINFOD_LEVEL, "m6 = {:#x}", _me[tile].m6);
Debug(misc, LANDINFOD_LEVEL, "m7 = {:#x}", _me[tile].m7);
Debug(misc, LANDINFOD_LEVEL, "m8 = {:#x}", _me[tile].m8);
#undef LANDINFOD_LEVEL
}
void OnInit() override
{
Town *t = ClosestTownFromTile(tile, _settings_game.economy.dist_local_authority);
/* Because build_date is not set yet in every TileDesc, we make sure it is empty */
TileDesc td;
td.build_date = INVALID_DATE;
/* Most tiles have only one owner, but
* - drivethrough roadstops can be build on town owned roads (up to 2 owners) and
* - roads can have up to four owners (railroad, road, tram, 3rd-roadtype "highway").
*/
td.owner_type[0] = STR_LAND_AREA_INFORMATION_OWNER; // At least one owner is displayed, though it might be "N/A".
td.owner_type[1] = STR_NULL; // STR_NULL results in skipping the owner
td.owner_type[2] = STR_NULL;
td.owner_type[3] = STR_NULL;
td.owner[0] = OWNER_NONE;
td.owner[1] = OWNER_NONE;
td.owner[2] = OWNER_NONE;
td.owner[3] = OWNER_NONE;
td.station_class = STR_NULL;
td.station_name = STR_NULL;
td.airport_class = STR_NULL;
td.airport_name = STR_NULL;
td.airport_tile_name = STR_NULL;
td.railtype = STR_NULL;
td.rail_speed = 0;
td.roadtype = STR_NULL;
td.road_speed = 0;
td.tramtype = STR_NULL;
td.tram_speed = 0;
td.grf = nullptr;
CargoArray acceptance;
AddAcceptedCargo(tile, acceptance, nullptr);
GetTileDesc(tile, &td);
this->landinfo_data.clear();
/* Tiletype */
SetDParam(0, td.dparam[0]);
this->landinfo_data.push_back(GetString(td.str));
/* Up to four owners */
for (uint i = 0; i < 4; i++) {
if (td.owner_type[i] == STR_NULL) continue;
SetDParam(0, STR_LAND_AREA_INFORMATION_OWNER_N_A);
if (td.owner[i] != OWNER_NONE && td.owner[i] != OWNER_WATER) GetNameOfOwner(td.owner[i], tile);
this->landinfo_data.push_back(GetString(td.owner_type[i]));
}
/* Cost to clear/revenue when cleared */
StringID str = STR_LAND_AREA_INFORMATION_COST_TO_CLEAR_N_A;
Company *c = Company::GetIfValid(_local_company);
if (c != nullptr) {
assert(_current_company == _local_company);
CommandCost costclear = Command<CMD_LANDSCAPE_CLEAR>::Do(DC_QUERY_COST, tile);
if (costclear.Succeeded()) {
Money cost = costclear.GetCost();
if (cost < 0) {
cost = -cost; // Negate negative cost to a positive revenue
str = STR_LAND_AREA_INFORMATION_REVENUE_WHEN_CLEARED;
} else {
str = STR_LAND_AREA_INFORMATION_COST_TO_CLEAR;
}
SetDParam(0, cost);
}
}
this->landinfo_data.push_back(GetString(str));
/* Location */
std::stringstream tile_ss;
tile_ss << "0x" << std::setfill('0') << std::setw(4) << std::hex << std::uppercase << tile; // 0x%.4X
std::string tile_str = tile_ss.str(); // Can't pass it directly to SetDParamStr as the string is only a temporary and would be destructed before the GetString call.
SetDParam(0, TileX(tile));
SetDParam(1, TileY(tile));
SetDParam(2, GetTileZ(tile));
SetDParamStr(3, tile_str);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_LANDINFO_COORDS));
/* Local authority */
SetDParam(0, STR_LAND_AREA_INFORMATION_LOCAL_AUTHORITY_NONE);
if (t != nullptr) {
SetDParam(0, STR_TOWN_NAME);
SetDParam(1, t->index);
}
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_LOCAL_AUTHORITY));
/* Build date */
if (td.build_date != INVALID_DATE) {
SetDParam(0, td.build_date);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_BUILD_DATE));
}
/* Station class */
if (td.station_class != STR_NULL) {
SetDParam(0, td.station_class);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_STATION_CLASS));
}
/* Station type name */
if (td.station_name != STR_NULL) {
SetDParam(0, td.station_name);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_STATION_TYPE));
}
/* Airport class */
if (td.airport_class != STR_NULL) {
SetDParam(0, td.airport_class);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_AIRPORT_CLASS));
}
/* Airport name */
if (td.airport_name != STR_NULL) {
SetDParam(0, td.airport_name);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_AIRPORT_NAME));
}
/* Airport tile name */
if (td.airport_tile_name != STR_NULL) {
SetDParam(0, td.airport_tile_name);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_AIRPORTTILE_NAME));
}
/* Rail type name */
if (td.railtype != STR_NULL) {
SetDParam(0, td.railtype);
this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_RAIL_TYPE));
}
/* Rail speed limit */
if (td.rail_speed != 0) {
SetDParam(0, td.rail_speed);
this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_RAIL_SPEED_LIMIT));
}
/* Road type name */
if (td.roadtype != STR_NULL) {
SetDParam(0, td.roadtype);
this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_ROAD_TYPE));
}
/* Road speed limit */
if (td.road_speed != 0) {
SetDParam(0, td.road_speed);
this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_ROAD_SPEED_LIMIT));
}
/* Tram type name */
if (td.tramtype != STR_NULL) {
SetDParam(0, td.tramtype);
this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_TRAM_TYPE));
}
/* Tram speed limit */
if (td.tram_speed != 0) {
SetDParam(0, td.tram_speed);
this->landinfo_data.push_back(GetString(STR_LANG_AREA_INFORMATION_TRAM_SPEED_LIMIT));
}
/* NewGRF name */
if (td.grf != nullptr) {
SetDParamStr(0, td.grf);
this->landinfo_data.push_back(GetString(STR_LAND_AREA_INFORMATION_NEWGRF_NAME));
}
/* Cargo acceptance is displayed in a extra multiline */
std::stringstream line;
line << GetString(STR_LAND_AREA_INFORMATION_CARGO_ACCEPTED);
bool found = false;
for (CargoID i = 0; i < NUM_CARGO; ++i) {
if (acceptance[i] > 0) {
/* Add a comma between each item. */
if (found) line << ", ";
found = true;
/* If the accepted value is less than 8, show it in 1/8:ths */
if (acceptance[i] < 8) {
SetDParam(0, acceptance[i]);
SetDParam(1, CargoSpec::Get(i)->name);
line << GetString(STR_LAND_AREA_INFORMATION_CARGO_EIGHTS);
} else {
line << GetString(CargoSpec::Get(i)->name);
}
}
}
if (found) {
this->cargo_acceptance = line.str();
} else {
this->cargo_acceptance.clear();
}
}
bool IsNewGRFInspectable() const override
{
return ::IsNewGRFInspectable(GetGrfSpecFeature(this->tile), this->tile);
}
void ShowNewGRFInspectWindow() const override
{
::ShowNewGRFInspectWindow(GetGrfSpecFeature(this->tile), this->tile);
}
void OnClick(Point pt, int widget, int click_count) override
{
switch (widget) {
case WID_LI_LOCATION:
if (_ctrl_pressed) {
ShowExtraViewportWindow(this->tile);
} else {
ScrollMainWindowToTile(this->tile);
}
break;
}
}
/**
* Some data on this window has become invalid.
* @param data Information about the changed data.
* @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
*/
void OnInvalidateData(int data = 0, bool gui_scope = true) override
{
if (!gui_scope) return;
switch (data) {
case 1:
/* ReInit, "debug" sprite might have changed */
this->ReInit();
break;
}
}
};
/**
* Show land information window.
* @param tile The tile to show information about.
*/
void ShowLandInfo(TileIndex tile)
{
CloseWindowById(WC_LAND_INFO, 0);
new LandInfoWindow(tile);
}
static const NWidgetPart _nested_about_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_GREY),
NWidget(WWT_CAPTION, COLOUR_GREY), SetDataTip(STR_ABOUT_OPENTTD, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_GREY), SetPIP(4, 2, 4),
NWidget(WWT_LABEL, COLOUR_GREY), SetDataTip(STR_ABOUT_ORIGINAL_COPYRIGHT, STR_NULL),
NWidget(WWT_LABEL, COLOUR_GREY), SetDataTip(STR_ABOUT_VERSION, STR_NULL),
NWidget(WWT_FRAME, COLOUR_GREY), SetPadding(0, 5, 1, 5),
NWidget(WWT_EMPTY, INVALID_COLOUR, WID_A_SCROLLING_TEXT),
EndContainer(),
NWidget(WWT_LABEL, COLOUR_GREY, WID_A_WEBSITE), SetDataTip(STR_BLACK_RAW_STRING, STR_NULL),
NWidget(WWT_LABEL, COLOUR_GREY, WID_A_COPYRIGHT), SetDataTip(STR_ABOUT_COPYRIGHT_OPENTTD, STR_NULL),
EndContainer(),
};
static WindowDesc _about_desc(
WDP_CENTER, nullptr, 0, 0,
WC_GAME_OPTIONS, WC_NONE,
0,
_nested_about_widgets, lengthof(_nested_about_widgets)
);
static const char * const _credits[] = {
u8"Original design by Chris Sawyer",
u8"Original graphics by Simon Foster",
u8"",
u8"The OpenTTD team (in alphabetical order):",
u8" Matthijs Kooijman (blathijs) - Pathfinder-guru, Debian port (since 0.3)",
u8" Christoph Elsenhans (frosch) - General coding (since 0.6)",
u8" Lo\u00efc Guilloux (glx) - General / Windows Expert (since 0.4.5)",
u8" Charles Pigott (LordAro) - General / Correctness police (since 1.9)",
u8" Michael Lutz (michi_cc) - Path based signals (since 0.7)",
u8" Niels Martin Hansen (nielsm) - Music system, general coding (since 1.9)",
u8" Owen Rudge (orudge) - Forum host, OS/2 port (since 0.1)",
u8" Peter Nelson (peter1138) - Spiritual descendant from NewGRF gods (since 0.4.5)",
u8" Remko Bijker (Rubidium) - Coder and way more (since 0.4.5)",
u8" Patric Stout (TrueBrain) - NoProgrammer (since 0.3), sys op",
u8" Tyler Trahan (2TallTyler) - General coding (since 13)",
u8"",
u8"Inactive Developers:",
u8" Grzegorz Duczy\u0144ski (adf88) - General coding (1.7 - 1.8)",
u8" Albert Hofkamp (Alberth) - GUI expert (0.7 - 1.9)",
u8" Jean-Fran\u00e7ois Claeys (Belugas) - GUI, NewGRF and more (0.4.5 - 1.0)",
u8" Bjarni Corfitzen (Bjarni) - MacOSX port, coder and vehicles (0.3 - 0.7)",
u8" Victor Fischer (Celestar) - Programming everywhere you need him to (0.3 - 0.6)",
u8" Ulf Hermann (fonsinchen) - Cargo Distribution (1.3 - 1.6)",
u8" Jaroslav Mazanec (KUDr) - YAPG (Yet Another Pathfinder God) ;) (0.4.5 - 0.6)",
u8" Jonathan Coome (Maedhros) - High priest of the NewGRF Temple (0.5 - 0.6)",
u8" Attila B\u00e1n (MiHaMiX) - Developer WebTranslator 1 and 2 (0.3 - 0.5)",
u8" Ingo von Borstel (planetmaker) - General coding, Support (1.1 - 1.9)",
u8" Zden\u011bk Sojka (SmatZ) - Bug finder and fixer (0.6 - 1.3)",
u8" Jos\u00e9 Soler (Terkhen) - General coding (1.0 - 1.4)",
u8" Christoph Mallon (Tron) - Programmer, code correctness police (0.3 - 0.5)",
u8" Thijs Marinussen (Yexo) - AI Framework, General (0.6 - 1.3)",
u8" Leif Linse (Zuu) - AI/Game Script (1.2 - 1.6)",
u8"",
u8"Retired Developers:",
u8" Tam\u00e1s Farag\u00f3 (Darkvater) - Ex-Lead coder (0.3 - 0.5)",
u8" Dominik Scherer (dominik81) - Lead programmer, GUI expert (0.3 - 0.3)",
u8" Emil Djupfeld (egladil) - MacOSX (0.4.5 - 0.6)",
u8" Simon Sasburg (HackyKid) - Many bugfixes (0.4 - 0.4.5)",
u8" Ludvig Strigeus (ludde) - Original author of OpenTTD, main coder (0.1 - 0.3)",
u8" Cian Duffy (MYOB) - BeOS port / manual writing (0.1 - 0.3)",
u8" Petr Baudi\u0161 (pasky) - Many patches, NewGRF support (0.3 - 0.3)",
u8" Benedikt Br\u00fcggemeier (skidd13) - Bug fixer and code reworker (0.6 - 0.7)",
u8" Serge Paquet (vurlix) - 2nd contributor after ludde (0.1 - 0.3)",
u8"",
u8"Special thanks go out to:",
u8" Josef Drexler - For his great work on TTDPatch",
u8" Marcin Grzegorczyk - Track foundations and for describing TTD internals",
u8" Stefan Mei\u00dfner (sign_de) - For his work on the console",
u8" Mike Ragsdale - OpenTTD installer",
u8" Christian Rosentreter (tokai) - MorphOS / AmigaOS port",
u8" Richard Kempton (richK) - additional airports, initial TGP implementation",
u8" Alberto Demichelis - Squirrel scripting language \u00a9 2003-2008",
u8" L. Peter Deutsch - MD5 implementation \u00a9 1999, 2000, 2002",
u8" Michael Blunck - Pre-signals and semaphores \u00a9 2003",
u8" George - Canal/Lock graphics \u00a9 2003-2004",
u8" Andrew Parkhouse (andythenorth) - River graphics",
u8" David Dallaston (Pikka) - Tram tracks",
u8" All Translators - Who made OpenTTD a truly international game",
u8" Bug Reporters - Without whom OpenTTD would still be full of bugs!",
u8"",
u8"",
u8"And last but not least:",
u8" Chris Sawyer - For an amazing game!"
};
struct AboutWindow : public Window {
int text_position; ///< The top of the scrolling text
int line_height; ///< The height of a single line
static const int num_visible_lines = 19; ///< The number of lines visible simultaneously
static const uint TIMER_INTERVAL = 2100; ///< Scrolling interval, scaled by line text line height. This value chosen to maintain parity: 2100 / FONT_HEIGHT_NORMAL = 150ms
GUITimer timer;
AboutWindow() : Window(&_about_desc)
{
this->InitNested(WN_GAME_OPTIONS_ABOUT);
this->text_position = this->GetWidget<NWidgetBase>(WID_A_SCROLLING_TEXT)->pos_y + this->GetWidget<NWidgetBase>(WID_A_SCROLLING_TEXT)->current_y;
}
void SetStringParameters(int widget) const override
{
if (widget == WID_A_WEBSITE) SetDParamStr(0, "Website: https://www.openttd.org");
if (widget == WID_A_COPYRIGHT) SetDParamStr(0, _openttd_revision_year);
}
void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
{
if (widget != WID_A_SCROLLING_TEXT) return;
this->line_height = FONT_HEIGHT_NORMAL;
Dimension d;
d.height = this->line_height * num_visible_lines;
d.width = 0;
for (uint i = 0; i < lengthof(_credits); i++) {
d.width = std::max(d.width, GetStringBoundingBox(_credits[i]).width);
}
*size = maxdim(*size, d);
/* Set scroll interval based on required speed. To keep scrolling smooth,
* the interval is adjusted rather than the distance moved. */
this->timer.SetInterval(TIMER_INTERVAL / FONT_HEIGHT_NORMAL);
}
void DrawWidget(const Rect &r, int widget) const override
{
if (widget != WID_A_SCROLLING_TEXT) return;
int y = this->text_position;
/* Show all scrolling _credits */
for (uint i = 0; i < lengthof(_credits); i++) {
if (y >= r.top + 7 && y < r.bottom - this->line_height) {
DrawString(r.left, r.right, y, _credits[i], TC_BLACK, SA_LEFT | SA_FORCE);
}
y += this->line_height;
}
}
void OnRealtimeTick(uint delta_ms) override
{
uint count = this->timer.CountElapsed(delta_ms);
if (count > 0) {
this->text_position -= count;
/* If the last text has scrolled start a new from the start */
if (this->text_position < (int)(this->GetWidget<NWidgetBase>(WID_A_SCROLLING_TEXT)->pos_y - lengthof(_credits) * this->line_height)) {
this->text_position = this->GetWidget<NWidgetBase>(WID_A_SCROLLING_TEXT)->pos_y + this->GetWidget<NWidgetBase>(WID_A_SCROLLING_TEXT)->current_y;
}
this->SetWidgetDirty(WID_A_SCROLLING_TEXT);
}
}
};
void ShowAboutWindow()
{
CloseWindowByClass(WC_GAME_OPTIONS);
new AboutWindow();
}
/**
* Display estimated costs.
* @param cost Estimated cost (or income if negative).
* @param x X position of the notification window.
* @param y Y position of the notification window.
*/
void ShowEstimatedCostOrIncome(Money cost, int x, int y)
{
StringID msg = STR_MESSAGE_ESTIMATED_COST;
if (cost < 0) {
cost = -cost;
msg = STR_MESSAGE_ESTIMATED_INCOME;
}
SetDParam(0, cost);
ShowErrorMessage(msg, INVALID_STRING_ID, WL_INFO, x, y);
}
/**
* Display animated income or costs on the map. Does nothing if cost is zero.
* @param x World X position of the animation location.
* @param y World Y position of the animation location.
* @param z World Z position of the animation location.
* @param cost Estimated cost (or income if negative).
*/
void ShowCostOrIncomeAnimation(int x, int y, int z, Money cost)
{
if (cost == 0) {
return;
}
Point pt = RemapCoords(x, y, z);
StringID msg = STR_INCOME_FLOAT_COST;
if (cost < 0) {
cost = -cost;
msg = STR_INCOME_FLOAT_INCOME;
}
SetDParam(0, cost);
AddTextEffect(msg, pt.x, pt.y, DAY_TICKS, TE_RISING);
}
/**
* Display animated feeder income.
* @param x World X position of the animation location.
* @param y World Y position of the animation location.
* @param z World Z position of the animation location.
* @param transfer Estimated feeder income.
* @param income Real income from goods being delivered to their final destination.
*/
void ShowFeederIncomeAnimation(int x, int y, int z, Money transfer, Money income)
{
Point pt = RemapCoords(x, y, z);
SetDParam(0, transfer);
if (income == 0) {
AddTextEffect(STR_FEEDER, pt.x, pt.y, DAY_TICKS, TE_RISING);
} else {
StringID msg = STR_FEEDER_COST;
if (income < 0) {
income = -income;
msg = STR_FEEDER_INCOME;
}
SetDParam(1, income);
AddTextEffect(msg, pt.x, pt.y, DAY_TICKS, TE_RISING);
}
}
/**
* Display vehicle loading indicators.
* @param x World X position of the animation location.
* @param y World Y position of the animation location.
* @param z World Z position of the animation location.
* @param percent Estimated feeder income.
* @param string String which is drawn on the map.
* @return TextEffectID to be used for future updates of the loading indicators.
*/
TextEffectID ShowFillingPercent(int x, int y, int z, uint8 percent, StringID string)
{
Point pt = RemapCoords(x, y, z);
assert(string != STR_NULL);
SetDParam(0, percent);
return AddTextEffect(string, pt.x, pt.y, 0, TE_STATIC);
}
/**
* Update vehicle loading indicators.
* @param te_id TextEffectID to be updated.
* @param string String which is printed.
*/
void UpdateFillingPercent(TextEffectID te_id, uint8 percent, StringID string)
{
assert(string != STR_NULL);
SetDParam(0, percent);
UpdateTextEffect(te_id, string);
}
/**
* Hide vehicle loading indicators.
* @param *te_id TextEffectID which is supposed to be hidden.
*/
void HideFillingPercent(TextEffectID *te_id)
{
if (*te_id == INVALID_TE_ID) return;
RemoveTextEffect(*te_id);
*te_id = INVALID_TE_ID;
}
static const NWidgetPart _nested_tooltips_widgets[] = {
NWidget(WWT_EMPTY, INVALID_COLOUR, WID_TT_BACKGROUND), EndContainer(),
};
static WindowDesc _tool_tips_desc(
WDP_MANUAL, nullptr, 0, 0, // Coordinates and sizes are not used,
WC_TOOLTIPS, WC_NONE,
WDF_NO_FOCUS,
_nested_tooltips_widgets, lengthof(_nested_tooltips_widgets)
);
/** Window for displaying a tooltip. */
struct TooltipsWindow : public Window
{
StringID string_id; ///< String to display as tooltip.
byte paramcount; ///< Number of string parameters in #string_id.
uint64 params[8]; ///< The string parameters.
TooltipCloseCondition close_cond; ///< Condition for closing the window.
TooltipsWindow(Window *parent, StringID str, uint paramcount, const uint64 params[], TooltipCloseCondition close_tooltip) : Window(&_tool_tips_desc)
{
this->parent = parent;
this->string_id = str;
static_assert(sizeof(this->params[0]) == sizeof(params[0]));
assert(paramcount <= lengthof(this->params));
if (params == nullptr) {
_global_string_params.offset = 0;
params = _global_string_params.GetDataPointer();
}
if (paramcount > 0) memcpy(this->params, params, sizeof(this->params[0]) * paramcount);
this->paramcount = paramcount;
this->close_cond = close_tooltip;
this->InitNested();
CLRBITS(this->flags, WF_WHITE_BORDER);
}
Point OnInitialPosition(int16 sm_width, int16 sm_height, int window_number) override
{
/* Find the free screen space between the main toolbar at the top, and the statusbar at the bottom.
* Add a fixed distance 2 so the tooltip floats free from both bars.
*/
int scr_top = GetMainViewTop() + 2;
int scr_bot = GetMainViewBottom() - 2;
Point pt;
/* Correctly position the tooltip position, watch out for window and cursor size
* Clamp value to below main toolbar and above statusbar. If tooltip would
* go below window, flip it so it is shown above the cursor */
pt.y = Clamp(_cursor.pos.y + _cursor.total_size.y + _cursor.total_offs.y + 5, scr_top, scr_bot);
if (pt.y + sm_height > scr_bot) pt.y = std::min(_cursor.pos.y + _cursor.total_offs.y - 5, scr_bot) - sm_height;
pt.x = sm_width >= _screen.width ? 0 : Clamp(_cursor.pos.x - (sm_width >> 1), 0, _screen.width - sm_width);
return pt;
}
void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
{
/* There is only one widget. */
for (uint i = 0; i != this->paramcount; i++) SetDParam(i, this->params[i]);
size->width = std::min<uint>(GetStringBoundingBox(this->string_id).width, ScaleGUITrad(194));
size->height = GetStringHeight(this->string_id, size->width);
/* Increase slightly to have some space around the box. */
size->width += WidgetDimensions::scaled.framerect.Horizontal() + WidgetDimensions::scaled.fullbevel.Horizontal();
size->height += WidgetDimensions::scaled.framerect.Vertical() + WidgetDimensions::scaled.fullbevel.Vertical();
}
void DrawWidget(const Rect &r, int widget) const override
{
/* There is only one widget. */
GfxFillRect(r, PC_BLACK);
GfxFillRect(r.Shrink(WidgetDimensions::scaled.bevel), PC_LIGHT_YELLOW);
for (uint arg = 0; arg < this->paramcount; arg++) {
SetDParam(arg, this->params[arg]);
}
DrawStringMultiLine(r.Shrink(WidgetDimensions::scaled.framerect).Shrink(WidgetDimensions::scaled.fullbevel), this->string_id, TC_FROMSTRING, SA_CENTER);
}
void OnMouseLoop() override
{
/* Always close tooltips when the cursor is not in our window. */
if (!_cursor.in_window) {
this->Close();
return;
}
/* We can show tooltips while dragging tools. These are shown as long as
* we are dragging the tool. Normal tooltips work with hover or rmb. */
switch (this->close_cond) {
case TCC_RIGHT_CLICK: if (!_right_button_down) this->Close(); break;
case TCC_HOVER: if (!_mouse_hovering) this->Close(); break;
case TCC_NONE: break;
case TCC_EXIT_VIEWPORT: {
Window *w = FindWindowFromPt(_cursor.pos.x, _cursor.pos.y);
if (w == nullptr || IsPtInWindowViewport(w, _cursor.pos.x, _cursor.pos.y) == nullptr) this->Close();
break;
}
}
}
};
/**
* Shows a tooltip
* @param parent The window this tooltip is related to.
* @param str String to be displayed
* @param paramcount number of params to deal with
* @param params (optional) up to 5 pieces of additional information that may be added to a tooltip
* @param close_tooltip when the left (true) or right (false) mouse button is released
*/
void GuiShowTooltips(Window *parent, StringID str, uint paramcount, const uint64 params[], TooltipCloseCondition close_tooltip)
{
CloseWindowById(WC_TOOLTIPS, 0);
if (str == STR_NULL || !_cursor.in_window) return;
new TooltipsWindow(parent, str, paramcount, params, close_tooltip);
}
void QueryString::HandleEditBox(Window *w, int wid)
{
if (w->IsWidgetGloballyFocused(wid) && this->text.HandleCaret()) {
w->SetWidgetDirty(wid);
/* For the OSK also invalidate the parent window */
if (w->window_class == WC_OSK) w->InvalidateData();
}
}
static int GetCaretWidth()
{
return GetCharacterWidth(FS_NORMAL, '_');
}
void QueryString::DrawEditBox(const Window *w, int wid) const
{
const NWidgetLeaf *wi = w->GetWidget<NWidgetLeaf>(wid);
assert((wi->type & WWT_MASK) == WWT_EDITBOX);
bool rtl = _current_text_dir == TD_RTL;
Dimension sprite_size = GetSpriteSize(rtl ? SPR_IMG_DELETE_RIGHT : SPR_IMG_DELETE_LEFT);
int clearbtn_width = sprite_size.width + WidgetDimensions::scaled.imgbtn.Horizontal();
Rect r = wi->GetCurrentRect();
Rect cr = r.WithWidth(clearbtn_width, !rtl);
Rect fr = r.Indent(clearbtn_width, !rtl);
DrawFrameRect(cr, wi->colour, wi->IsLowered() ? FR_LOWERED : FR_NONE);
DrawSprite(rtl ? SPR_IMG_DELETE_RIGHT : SPR_IMG_DELETE_LEFT, PAL_NONE, cr.left + WidgetDimensions::scaled.imgbtn.left + (wi->IsLowered() ? 1 : 0), CenterBounds(r.top, r.bottom, sprite_size.height) + (wi->IsLowered() ? 1 : 0));
if (this->text.bytes == 1) GfxFillRect(cr.Shrink(WidgetDimensions::scaled.bevel), _colour_gradient[wi->colour & 0xF][2], FILLRECT_CHECKER);
DrawFrameRect(fr, wi->colour, FR_LOWERED | FR_DARKENED);
GfxFillRect(fr.Shrink(WidgetDimensions::scaled.bevel), PC_BLACK);
fr = fr.Shrink(WidgetDimensions::scaled.framerect);
/* Limit the drawing of the string inside the widget boundaries */
DrawPixelInfo dpi;
if (!FillDrawPixelInfo(&dpi, fr.left, fr.top, fr.Width(), fr.Height())) return;
DrawPixelInfo *old_dpi = _cur_dpi;
_cur_dpi = &dpi;
/* We will take the current widget length as maximum width, with a small
* space reserved at the end for the caret to show */
const Textbuf *tb = &this->text;
int delta = std::min(0, (fr.right - fr.left) - tb->pixels - GetCaretWidth());
if (tb->caretxoffs + delta < 0) delta = -tb->caretxoffs;
/* If we have a marked area, draw a background highlight. */
if (tb->marklength != 0) GfxFillRect(delta + tb->markxoffs, 0, delta + tb->markxoffs + tb->marklength - 1, fr.bottom - fr.top, PC_GREY);
DrawString(delta, tb->pixels, 0, tb->buf, TC_YELLOW);
bool focussed = w->IsWidgetGloballyFocused(wid) || IsOSKOpenedFor(w, wid);
if (focussed && tb->caret) {
int caret_width = GetStringBoundingBox("_").width;
DrawString(tb->caretxoffs + delta, tb->caretxoffs + delta + caret_width, 0, "_", TC_WHITE);
}
_cur_dpi = old_dpi;
}
/**
* Get the current caret position.
* @param w Window the edit box is in.
* @param wid Widget index.
* @return Top-left location of the caret, relative to the window.
*/
Point QueryString::GetCaretPosition(const Window *w, int wid) const
{
const NWidgetLeaf *wi = w->GetWidget<NWidgetLeaf>(wid);
assert((wi->type & WWT_MASK) == WWT_EDITBOX);
bool rtl = _current_text_dir == TD_RTL;
Dimension sprite_size = GetSpriteSize(rtl ? SPR_IMG_DELETE_RIGHT : SPR_IMG_DELETE_LEFT);
int clearbtn_width = sprite_size.width + WidgetDimensions::scaled.imgbtn.Horizontal();
Rect r = wi->GetCurrentRect().Indent(clearbtn_width, !rtl).Shrink(WidgetDimensions::scaled.framerect);
/* Clamp caret position to be inside out current width. */
const Textbuf *tb = &this->text;
int delta = std::min(0, (r.right - r.left) - tb->pixels - GetCaretWidth());
if (tb->caretxoffs + delta < 0) delta = -tb->caretxoffs;
Point pt = {r.left + tb->caretxoffs + delta, r.top};
return pt;
}
/**
* Get the bounding rectangle for a range of the query string.
* @param w Window the edit box is in.
* @param wid Widget index.
* @param from Start of the string range.
* @param to End of the string range.
* @return Rectangle encompassing the string range, relative to the window.
*/
Rect QueryString::GetBoundingRect(const Window *w, int wid, const char *from, const char *to) const
{
const NWidgetLeaf *wi = w->GetWidget<NWidgetLeaf>(wid);
assert((wi->type & WWT_MASK) == WWT_EDITBOX);
bool rtl = _current_text_dir == TD_RTL;
Dimension sprite_size = GetSpriteSize(rtl ? SPR_IMG_DELETE_RIGHT : SPR_IMG_DELETE_LEFT);
int clearbtn_width = sprite_size.width + WidgetDimensions::scaled.imgbtn.Horizontal();
Rect r = wi->GetCurrentRect().Indent(clearbtn_width, !rtl).Shrink(WidgetDimensions::scaled.framerect);
/* Clamp caret position to be inside our current width. */
const Textbuf *tb = &this->text;
int delta = std::min(0, r.Width() - tb->pixels - GetCaretWidth());
if (tb->caretxoffs + delta < 0) delta = -tb->caretxoffs;
/* Get location of first and last character. */
Point p1 = GetCharPosInString(tb->buf, from, FS_NORMAL);
Point p2 = from != to ? GetCharPosInString(tb->buf, to, FS_NORMAL) : p1;
return { Clamp(r.left + p1.x + delta, r.left, r.right), r.top, Clamp(r.left + p2.x + delta, r.left, r.right), r.bottom };
}
/**
* Get the character that is rendered at a position.
* @param w Window the edit box is in.
* @param wid Widget index.
* @param pt Position to test.
* @return Pointer to the character at the position or nullptr if no character is at the position.
*/
const char *QueryString::GetCharAtPosition(const Window *w, int wid, const Point &pt) const
{
const NWidgetLeaf *wi = w->GetWidget<NWidgetLeaf>(wid);
assert((wi->type & WWT_MASK) == WWT_EDITBOX);
bool rtl = _current_text_dir == TD_RTL;
Dimension sprite_size = GetSpriteSize(rtl ? SPR_IMG_DELETE_RIGHT : SPR_IMG_DELETE_LEFT);
int clearbtn_width = sprite_size.width + WidgetDimensions::scaled.imgbtn.Horizontal();
Rect r = wi->GetCurrentRect().Indent(clearbtn_width, !rtl).Shrink(WidgetDimensions::scaled.framerect);
if (!IsInsideMM(pt.y, r.top, r.bottom)) return nullptr;
/* Clamp caret position to be inside our current width. */
const Textbuf *tb = &this->text;
int delta = std::min(0, r.Width() - tb->pixels - GetCaretWidth());
if (tb->caretxoffs + delta < 0) delta = -tb->caretxoffs;
return ::GetCharAtPosition(tb->buf, pt.x - delta - r.left);
}
void QueryString::ClickEditBox(Window *w, Point pt, int wid, int click_count, bool focus_changed)
{
const NWidgetLeaf *wi = w->GetWidget<NWidgetLeaf>(wid);
assert((wi->type & WWT_MASK) == WWT_EDITBOX);
bool rtl = _current_text_dir == TD_RTL;
int clearbtn_width = GetSpriteSize(rtl ? SPR_IMG_DELETE_RIGHT : SPR_IMG_DELETE_LEFT).width;
Rect cr = wi->GetCurrentRect().WithWidth(clearbtn_width, !rtl);
if (IsInsideMM(pt.x, cr.left, cr.right)) {
if (this->text.bytes > 1) {
this->text.DeleteAll();
w->HandleButtonClick(wid);
w->OnEditboxChanged(wid);
}
return;
}
if (w->window_class != WC_OSK && _settings_client.gui.osk_activation != OSKA_DISABLED &&
(!focus_changed || _settings_client.gui.osk_activation == OSKA_IMMEDIATELY) &&
(click_count == 2 || _settings_client.gui.osk_activation != OSKA_DOUBLE_CLICK)) {
/* Open the OSK window */
ShowOnScreenKeyboard(w, wid);
}
}
/** Class for the string query window. */
struct QueryStringWindow : public Window
{
QueryString editbox; ///< Editbox.
QueryStringFlags flags; ///< Flags controlling behaviour of the window.
Dimension warning_size; ///< How much space to use for the warning text
QueryStringWindow(StringID str, StringID caption, uint max_bytes, uint max_chars, WindowDesc *desc, Window *parent, CharSetFilter afilter, QueryStringFlags flags) :
Window(desc), editbox(max_bytes, max_chars)
{
char *last_of = &this->editbox.text.buf[this->editbox.text.max_bytes - 1];
GetString(this->editbox.text.buf, str, last_of);
StrMakeValidInPlace(this->editbox.text.buf, last_of, SVS_NONE);
/* Make sure the name isn't too long for the text buffer in the number of
* characters (not bytes). max_chars also counts the '\0' characters. */
while (Utf8StringLength(this->editbox.text.buf) + 1 > this->editbox.text.max_chars) {
*Utf8PrevChar(this->editbox.text.buf + strlen(this->editbox.text.buf)) = '\0';
}
this->editbox.text.UpdateSize();
if ((flags & QSF_ACCEPT_UNCHANGED) == 0) this->editbox.orig = stredup(this->editbox.text.buf);
this->querystrings[WID_QS_TEXT] = &this->editbox;
this->editbox.caption = caption;
this->editbox.cancel_button = WID_QS_CANCEL;
this->editbox.ok_button = WID_QS_OK;
this->editbox.text.afilter = afilter;
this->flags = flags;
this->InitNested(WN_QUERY_STRING);
this->UpdateWarningStringSize();
this->parent = parent;
this->SetFocusedWidget(WID_QS_TEXT);
}
void UpdateWarningStringSize()
{
if (this->flags & QSF_PASSWORD) {
assert(this->nested_root->smallest_x > 0);
this->warning_size.width = this->nested_root->current_x - WidgetDimensions::scaled.frametext.Horizontal() - WidgetDimensions::scaled.framerect.Horizontal();
this->warning_size.height = GetStringHeight(STR_WARNING_PASSWORD_SECURITY, this->warning_size.width);
this->warning_size.height += WidgetDimensions::scaled.frametext.Vertical() + WidgetDimensions::scaled.framerect.Vertical();
} else {
this->warning_size = Dimension{ 0, 0 };
}
this->ReInit();
}
void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
{
if (widget == WID_QS_DEFAULT && (this->flags & QSF_ENABLE_DEFAULT) == 0) {
/* We don't want this widget to show! */
fill->width = 0;
resize->width = 0;
size->width = 0;
}
if (widget == WID_QS_WARNING) {
*size = this->warning_size;
}
}
void DrawWidget(const Rect &r, int widget) const override
{
if (widget != WID_QS_WARNING) return;
if (this->flags & QSF_PASSWORD) {
DrawStringMultiLine(r.Shrink(WidgetDimensions::scaled.framerect).Shrink(WidgetDimensions::scaled.frametext),
STR_WARNING_PASSWORD_SECURITY, TC_FROMSTRING, SA_CENTER);
}
}
void SetStringParameters(int widget) const override
{
if (widget == WID_QS_CAPTION) SetDParam(0, this->editbox.caption);
}
void OnOk()
{
if (this->editbox.orig == nullptr || strcmp(this->editbox.text.buf, this->editbox.orig) != 0) {
assert(this->parent != nullptr);
this->parent->OnQueryTextFinished(this->editbox.text.buf);
this->editbox.handled = true;
}
}
void OnClick(Point pt, int widget, int click_count) override
{
switch (widget) {
case WID_QS_DEFAULT:
this->editbox.text.DeleteAll();
FALLTHROUGH;
case WID_QS_OK:
this->OnOk();
FALLTHROUGH;
case WID_QS_CANCEL:
this->Close();
break;
}
}
void Close() override
{
if (!this->editbox.handled && this->parent != nullptr) {
Window *parent = this->parent;
this->parent = nullptr; // so parent doesn't try to close us again
parent->OnQueryTextFinished(nullptr);
}
this->Window::Close();
}
};
static const NWidgetPart _nested_query_string_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_GREY),
NWidget(WWT_CAPTION, COLOUR_GREY, WID_QS_CAPTION), SetDataTip(STR_WHITE_STRING, STR_NULL),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_GREY),
NWidget(WWT_EDITBOX, COLOUR_GREY, WID_QS_TEXT), SetMinimalSize(256, 12), SetFill(1, 1), SetPadding(2, 2, 2, 2),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_GREY, WID_QS_WARNING), EndContainer(),
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_QS_DEFAULT), SetMinimalSize(87, 12), SetFill(1, 1), SetDataTip(STR_BUTTON_DEFAULT, STR_NULL),
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_QS_CANCEL), SetMinimalSize(86, 12), SetFill(1, 1), SetDataTip(STR_BUTTON_CANCEL, STR_NULL),
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_QS_OK), SetMinimalSize(87, 12), SetFill(1, 1), SetDataTip(STR_BUTTON_OK, STR_NULL),
EndContainer(),
};
static WindowDesc _query_string_desc(
WDP_CENTER, "query_string", 0, 0,
WC_QUERY_STRING, WC_NONE,
0,
_nested_query_string_widgets, lengthof(_nested_query_string_widgets)
);
/**
* Show a query popup window with a textbox in it.
* @param str StringID for the text shown in the textbox
* @param caption StringID of text shown in caption of querywindow
* @param maxsize maximum size in bytes or characters (including terminating '\0') depending on flags
* @param parent pointer to a Window that will handle the events (ok/cancel) of this window.
* @param afilter filters out unwanted character input
* @param flags various flags, @see QueryStringFlags
*/
void ShowQueryString(StringID str, StringID caption, uint maxsize, Window *parent, CharSetFilter afilter, QueryStringFlags flags)
{
assert(parent != nullptr);
CloseWindowByClass(WC_QUERY_STRING);
new QueryStringWindow(str, caption, ((flags & QSF_LEN_IN_CHARS) ? MAX_CHAR_LENGTH : 1) * maxsize, maxsize, &_query_string_desc, parent, afilter, flags);
}
/**
* Window used for asking the user a YES/NO question.
*/
struct QueryWindow : public Window {
QueryCallbackProc *proc; ///< callback function executed on closing of popup. Window* points to parent, bool is true if 'yes' clicked, false otherwise
uint64 params[10]; ///< local copy of #_global_string_params
StringID message; ///< message shown for query window
StringID caption; ///< title of window
QueryWindow(WindowDesc *desc, StringID caption, StringID message, Window *parent, QueryCallbackProc *callback) : Window(desc)
{
/* Create a backup of the variadic arguments to strings because it will be
* overridden pretty often. We will copy these back for drawing */
CopyOutDParam(this->params, 0, lengthof(this->params));
this->caption = caption;
this->message = message;
this->proc = callback;
this->parent = parent;
this->InitNested(WN_CONFIRM_POPUP_QUERY);
}
void Close() override
{
if (this->proc != nullptr) this->proc(this->parent, false);
this->Window::Close();
}
void FindWindowPlacementAndResize(int def_width, int def_height) override
{
/* Position query window over the calling window, ensuring it's within screen bounds. */
this->left = Clamp(parent->left + (parent->width / 2) - (this->width / 2), 0, _screen.width - this->width);
this->top = Clamp(parent->top + (parent->height / 2) - (this->height / 2), 0, _screen.height - this->height);
this->SetDirty();
}
void SetStringParameters(int widget) const override
{
switch (widget) {
case WID_Q_CAPTION:
CopyInDParam(1, this->params, lengthof(this->params));
SetDParam(0, this->caption);
break;
case WID_Q_TEXT:
CopyInDParam(0, this->params, lengthof(this->params));
break;
}
}
void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
{
if (widget != WID_Q_TEXT) return;
Dimension d = GetStringMultiLineBoundingBox(this->message, *size);
d.width += WidgetDimensions::scaled.frametext.Horizontal();
d.height += WidgetDimensions::scaled.framerect.Vertical();
*size = d;
}
void DrawWidget(const Rect &r, int widget) const override
{
if (widget != WID_Q_TEXT) return;
DrawStringMultiLine(r.Shrink(WidgetDimensions::scaled.frametext, WidgetDimensions::scaled.framerect),
this->message, TC_FROMSTRING, SA_CENTER);
}
void OnClick(Point pt, int widget, int click_count) override
{
switch (widget) {
case WID_Q_YES: {
/* in the Generate New World window, clicking 'Yes' causes
* CloseNonVitalWindows() to be called - we shouldn't be in a window then */
QueryCallbackProc *proc = this->proc;
Window *parent = this->parent;
/* Prevent the destructor calling the callback function */
this->proc = nullptr;
this->Close();
if (proc != nullptr) {
proc(parent, true);
proc = nullptr;
}
break;
}
case WID_Q_NO:
this->Close();
break;
}
}
EventState OnKeyPress(WChar key, uint16 keycode) override
{
/* ESC closes the window, Enter confirms the action */
switch (keycode) {
case WKC_RETURN:
case WKC_NUM_ENTER:
if (this->proc != nullptr) {
this->proc(this->parent, true);
this->proc = nullptr;
}
FALLTHROUGH;
case WKC_ESC:
this->Close();
return ES_HANDLED;
}
return ES_NOT_HANDLED;
}
};
static const NWidgetPart _nested_query_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_RED),
NWidget(WWT_CAPTION, COLOUR_RED, WID_Q_CAPTION), SetDataTip(STR_JUST_STRING, STR_NULL),
EndContainer(),
NWidget(WWT_PANEL, COLOUR_RED),
NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_wide, 0), SetPadding(WidgetDimensions::unscaled.modalpopup),
NWidget(WWT_TEXT, COLOUR_RED, WID_Q_TEXT), SetMinimalSize(200, 12),
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), SetPIP(WidgetDimensions::unscaled.hsep_indent, WidgetDimensions::unscaled.hsep_indent, WidgetDimensions::unscaled.hsep_indent),
NWidget(WWT_PUSHTXTBTN, COLOUR_YELLOW, WID_Q_NO), SetMinimalSize(71, 12), SetFill(1, 1), SetDataTip(STR_QUIT_NO, STR_NULL),
NWidget(WWT_PUSHTXTBTN, COLOUR_YELLOW, WID_Q_YES), SetMinimalSize(71, 12), SetFill(1, 1), SetDataTip(STR_QUIT_YES, STR_NULL),
EndContainer(),
EndContainer(),
EndContainer(),
};
static WindowDesc _query_desc(
WDP_CENTER, nullptr, 0, 0,
WC_CONFIRM_POPUP_QUERY, WC_NONE,
WDF_MODAL,
_nested_query_widgets, lengthof(_nested_query_widgets)
);
/**
* Show a modal confirmation window with standard 'yes' and 'no' buttons
* The window is aligned to the centre of its parent.
* @param caption string shown as window caption
* @param message string that will be shown for the window
* @param parent pointer to parent window, if this pointer is nullptr the parent becomes
* the main window WC_MAIN_WINDOW
* @param callback callback function pointer to set in the window descriptor
*/
void ShowQuery(StringID caption, StringID message, Window *parent, QueryCallbackProc *callback)
{
if (parent == nullptr) parent = GetMainWindow();
for (Window *w : Window::Iterate()) {
if (w->window_class != WC_CONFIRM_POPUP_QUERY) continue;
QueryWindow *qw = dynamic_cast<QueryWindow *>(w);
if (qw->parent != parent || qw->proc != callback) continue;
qw->Close();
break;
}
new QueryWindow(&_query_desc, caption, message, parent, callback);
}