From d046e1cb5c07c1447a7fd0fb4f252dd43e76b979 Mon Sep 17 00:00:00 2001 From: Pavel Stupnikov Date: Tue, 25 Mar 2014 15:50:02 +0400 Subject: [PATCH] missing files --- src/cargo_table_gui.cpp | 217 ++ src/cargo_table_gui.h | 14 + src/commands_gui.cpp | 499 +++++ src/watch_gui.cpp | 397 ++++ src/watch_gui.h | 61 + src/widgets/cargo_table_widget.h | 20 + src/window.cpp.orig | 3443 ++++++++++++++++++++++++++++++ src/zoning.h | 34 + src/zoning_cmd.cpp | 378 ++++ src/zoning_gui.cpp | 138 ++ 10 files changed, 5201 insertions(+) create mode 100644 src/cargo_table_gui.cpp create mode 100644 src/cargo_table_gui.h create mode 100644 src/commands_gui.cpp create mode 100644 src/watch_gui.cpp create mode 100644 src/watch_gui.h create mode 100644 src/widgets/cargo_table_widget.h create mode 100644 src/window.cpp.orig create mode 100644 src/zoning.h create mode 100644 src/zoning_cmd.cpp create mode 100644 src/zoning_gui.cpp diff --git a/src/cargo_table_gui.cpp b/src/cargo_table_gui.cpp new file mode 100644 index 0000000000..625e90ee4e --- /dev/null +++ b/src/cargo_table_gui.cpp @@ -0,0 +1,217 @@ +/* $Id: cargo_table_gui.cpp 21909 2011-01-26 08:14:36Z TheDude $ */ + +#include "stdafx.h" +#include "window_gui.h" +#include "window_func.h" +#include "strings_func.h" +#include "company_func.h" +#include "company_base.h" +#include "table/strings.h" +#include "textbuf_gui.h" +#include "cargotype.h" +#include "widgets/dropdown_type.h" + +#include "widgets/cargo_table_widget.h" + +static const uint EXP_TOPPADDING = 5; +static const uint EXP_LINESPACE = 2; ///< Amount of vertical space for a horizontal (sub-)total line. +static const uint EXP_BLOCKSPACE = 10; ///< Amount of vertical space between two blocks of numbers. + +enum CargoOption { + WID_CT_OPTION_CARGO_TOTAL = 0, + WID_CT_OPTION_CARGO_MONTH, +}; + +static void DrawPrice(Money amount, int left, int right, int top) +{ + SetDParam(0, amount); + DrawString(left, right, top, STR_FINANCES_POSITIVE_INCOME, TC_FROMSTRING, SA_RIGHT); +} + +void InvalidateCargosWindows(CompanyID cid) +{ + if (cid == _local_company) SetWindowDirty(WC_STATUS_BAR, 0); + SetWindowDirty(WC_CARGOS, cid); +} + +/** Cargos window handler. */ +struct CargosWindow : Window { + + CargoOption cargoPeriod; + CargosWindow(WindowDesc *desc, WindowNumber window_number) : Window(desc) + { + this->InitNested(window_number); + this->owner = (Owner)this->window_number; + this->cargoPeriod = WID_CT_OPTION_CARGO_TOTAL; + } + + virtual void SetStringParameters(int widget) const + { + if(widget != WID_CT_CAPTION) return; + SetDParam(0, (CompanyID)this->window_number); + SetDParam(1, (CompanyID)this->window_number); + } + + virtual void OnClick(Point pt, int widget, int click_count) + { + if(widget != WID_CT_HEADER_CARGO) return; + this->cargoPeriod = (this->cargoPeriod == WID_CT_OPTION_CARGO_TOTAL) ? WID_CT_OPTION_CARGO_MONTH : WID_CT_OPTION_CARGO_TOTAL; + this->SetDirty(); + } + + void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) + { + uint extra_width = 0; + switch(widget){ + case WID_CT_HEADER_AMOUNT: + case WID_CT_HEADER_INCOME: + extra_width += 16; + case WID_CT_HEADER_CARGO: + size->width = 96 + extra_width; + size->height = EXP_BLOCKSPACE + EXP_LINESPACE; + break; + + case WID_CT_AMOUNT: + case WID_CT_INCOME: + extra_width += 16; + case WID_CT_LIST: + size->width = 96 + extra_width; + size->height = (_sorted_standard_cargo_specs_size + 3) * (EXP_BLOCKSPACE + EXP_LINESPACE); + break; + } + } + + void DrawWidget(const Rect &r, int widget) const + { + int rect_x = (r.left + WD_FRAMERECT_LEFT); + int y = r.top; + const Company *c = Company::Get((CompanyID)this->window_number); + uint32 sum_cargo_amount = 0; + Money sum_cargo_income = 0; + + switch(widget){ + case WID_CT_HEADER_CARGO: + //DrawString(r.left, r.right, y, STR_TOOLBAR_CARGOS_HEADER_CARGO, TC_FROMSTRING, SA_LEFT); + y += EXP_BLOCKSPACE + EXP_LINESPACE; + break; + case WID_CT_HEADER_AMOUNT: + DrawString(r.left, r.right, y, STR_TOOLBAR_CARGOS_HEADER_AMOUNT, TC_FROMSTRING, SA_CENTER); + y += EXP_BLOCKSPACE + EXP_LINESPACE; + break; + case WID_CT_HEADER_INCOME: + DrawString(r.left, r.right, y, STR_TOOLBAR_CARGOS_HEADER_INCOME, TC_FROMSTRING, SA_RIGHT); + y += EXP_BLOCKSPACE + EXP_LINESPACE; + break; + + case WID_CT_LIST:{ + y += EXP_TOPPADDING; //top padding + for (int i = 0; i < _sorted_standard_cargo_specs_size; i++) { + const CargoSpec *cs = _sorted_cargo_specs[i]; + + GfxFillRect(rect_x, y, rect_x + 8, y + 5, 0); + GfxFillRect(rect_x + 1, y + 1, rect_x + 7, y + 4, cs->legend_colour); //coloured cargo rectangles + + SetDParam(0, cs->name); + DrawString(r.left + 14, r.right, y, STR_TOOLBAR_CARGOS_NAME); //cargo name + + y += EXP_BLOCKSPACE + EXP_LINESPACE; //padding + } + + //total + GfxFillRect(rect_x, y, rect_x + 96, y, 0); + y += EXP_BLOCKSPACE + EXP_LINESPACE; + + StringID string_to_draw = STR_TOOLBAR_CARGOS_HEADER_TOTAL; + if(this->cargoPeriod != WID_CT_OPTION_CARGO_TOTAL) string_to_draw++; + DrawString(r.left, r.right, y, string_to_draw); + break; + } + case WID_CT_AMOUNT: + y += EXP_TOPPADDING; + for (int i = 0; i < _sorted_standard_cargo_specs_size; i++) { + const CargoSpec *cs = _sorted_cargo_specs[i]; + + if(this->cargoPeriod == WID_CT_OPTION_CARGO_MONTH){ + sum_cargo_amount += c->cargo_units_period[0][cs->Index()]; + SetDParam(0, c->cargo_units_period[0][cs->Index()]); + } + else{ + sum_cargo_amount += c->cargo_units[cs->Index()]; + SetDParam(0, c->cargo_units[cs->Index()]); + } + + DrawString(r.left, r.right, y, STR_TOOLBAR_CARGOS_UNITS, TC_FROMSTRING, SA_RIGHT); //cargo amount in pcs + y += EXP_BLOCKSPACE + EXP_LINESPACE; + } + + //total + GfxFillRect(rect_x, y, rect_x + 108, y, 0); + y += EXP_BLOCKSPACE + EXP_LINESPACE; + SetDParam(0, sum_cargo_amount); + DrawString(r.left, r.right, y, STR_TOOLBAR_CARGOS_UNITS_TOTAL, TC_FROMSTRING, SA_RIGHT); + break; + + case WID_CT_INCOME: + y += EXP_TOPPADDING; + for (int i = 0; i < _sorted_standard_cargo_specs_size; i++) { + const CargoSpec *cs = _sorted_cargo_specs[i]; + + if(this->cargoPeriod == WID_CT_OPTION_CARGO_MONTH){ + sum_cargo_income += c->cargo_income_period[0][cs->Index()]; + DrawPrice(c->cargo_income_period[0][cs->Index()], r.left, r.right, y); //cargo income in money + } + else{ + sum_cargo_income += c->cargo_income[cs->Index()]; + DrawPrice(c->cargo_income[cs->Index()], r.left, r.right, y); //cargo income in money + } + + y += EXP_BLOCKSPACE + EXP_LINESPACE; + } + + //total + GfxFillRect(rect_x, y, rect_x + 108, y, 0); + y += EXP_BLOCKSPACE + EXP_LINESPACE; + DrawPrice(sum_cargo_income, r.left, r.right, y); + break; + } + } +}; + +static const NWidgetPart _nested_cargos_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_GREY), + NWidget(WWT_CAPTION, COLOUR_GREY, WID_CT_CAPTION), SetDataTip(STR_TOOLBAR_CARGOS_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_SHADEBOX, COLOUR_GREY), + NWidget(WWT_STICKYBOX, COLOUR_GREY), + EndContainer(), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(1, 1), + NWidget(NWID_HORIZONTAL), SetPadding(WD_FRAMERECT_TOP, WD_FRAMERECT_RIGHT, WD_FRAMERECT_BOTTOM, WD_FRAMERECT_LEFT), SetPIP(0, 9, 0), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_CT_HEADER_CARGO), SetMinimalSize(96, 16),SetFill(1, 0), SetPadding(2,2,2,2), SetDataTip(STR_TOOLBAR_CARGOS_HEADER_CARGO, STR_TOOLBAR_CARGOS_HEADER_CARGO), + NWidget(WWT_EMPTY, COLOUR_GREY, WID_CT_HEADER_AMOUNT), SetMinimalSize(108, 16), SetFill(1, 0), SetPadding(2,2,2,2), SetDataTip(STR_TOOLBAR_CARGOS_HEADER_AMOUNT, STR_TOOLBAR_CARGOS_HEADER_AMOUNT), + NWidget(WWT_EMPTY, COLOUR_GREY, WID_CT_HEADER_INCOME), SetMinimalSize(108, 16), SetFill(1, 0), SetPadding(2,2,2,2), SetDataTip(STR_TOOLBAR_CARGOS_HEADER_INCOME, STR_TOOLBAR_CARGOS_HEADER_INCOME), + EndContainer(), + EndContainer(), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(1, 1), + NWidget(NWID_HORIZONTAL), SetPadding(WD_FRAMERECT_TOP, WD_FRAMERECT_RIGHT, WD_FRAMERECT_BOTTOM, WD_FRAMERECT_LEFT), SetPIP(0, 9, 0), + NWidget(WWT_EMPTY, COLOUR_GREY, WID_CT_LIST),SetMinimalSize(96, 0),SetFill(1, 0), SetPadding(2,2,2,2), SetResize(1, 1), + NWidget(WWT_EMPTY, COLOUR_GREY, WID_CT_AMOUNT),SetMinimalSize(108, 0),SetFill(1, 0), SetPadding(2,2,2,2), SetResize(1, 1), + NWidget(WWT_EMPTY, COLOUR_GREY, WID_CT_INCOME),SetMinimalSize(108, 0),SetFill(1, 0), SetPadding(2,2,2,2), SetResize(1, 1), + NWidget(NWID_VERTICAL), + NWidget(NWID_SPACER), SetFill(0, 1), SetResize(0, 1), + EndContainer(), + EndContainer(), + EndContainer(), +}; + +static WindowDesc _cargos_desc( + WDP_AUTO, NULL, 0, 0, + WC_CARGOS, WC_NONE, + WDF_CONSTRUCTION, + _nested_cargos_widgets, lengthof(_nested_cargos_widgets) +); + +void ShowCompanyCargos(CompanyID company) +{ + if (!Company::IsValidID(company)) return; + AllocateWindowDescFront(&_cargos_desc, company); +} diff --git a/src/cargo_table_gui.h b/src/cargo_table_gui.h new file mode 100644 index 0000000000..a21b3483a9 --- /dev/null +++ b/src/cargo_table_gui.h @@ -0,0 +1,14 @@ +/* $Id: cargo_table_gui.h 21700 2011-01-03 11:55:08Z $ */ + +/** @file cargo_table_gui.h GUI Functions related to cargos. */ + +#ifndef CARGO_TABLE_H +#define CARGO_TABLE_H + +#include "company_type.h" +#include "gfx_type.h" + +void ShowCompanyCargos(CompanyID company); +void InvalidateCargosWindows(CompanyID cid); + +#endif /* CARGO_TABLE_H */ diff --git a/src/commands_gui.cpp b/src/commands_gui.cpp new file mode 100644 index 0000000000..4a585f1ce5 --- /dev/null +++ b/src/commands_gui.cpp @@ -0,0 +1,499 @@ +/* $Id: commands_gui.cpp 21909 2011-01-26 08:14:36Z xi $ */ + +#ifdef ENABLE_NETWORK +#include "stdafx.h" +#include "window_gui.h" +#include "window_func.h" +#include "strings_func.h" +#include "widgets/dropdown_type.h" +#include "core/geometry_func.hpp" +#include "settings_type.h" +#include "settings_func.h" //saveconfig +#include "3rdparty/md5/md5.h" +#include "table/strings.h" +#include "network/network_type.h" +#include "network/network_func.h" +#include "textbuf_gui.h" +#include "cargotype.h" +#include "console_func.h" +#include "network/network.h" +#include "network/core/tcp_http.h" +#include + +bool _novahost = false; + +/** Widget number of the commands window. */ +enum CommandsToolbarWidgets { + CTW_BACKGROUND, + CTW_GOAL, + CTW_SCORE, + CTW_TOWN, + CTW_QUEST, + CTW_TOWN_ID, + CTW_HINT, + CTW_LOGIN, + CTW_AUTOLOGIN, + CTW_LOGIN_CREDENTIALS, + CTW_NAME, + CTW_TIMELEFT, + CTW_INFO, + CTW_RESETME, + CTW_SAVEME, + CTW_HELP, + CTW_RULES, + CTW_CBHINT, + CTW_CLIENTS, + CTW_BEST, + CTW_ME, + CTW_RANK, + CTW_TOPICS, + CTW_TOPIC1, + CTW_TOPIC2, + CTW_TOPIC3, + CTW_TOPIC4, + CTW_TOPIC5, + CTW_TOPIC6, + CTW_CHOOSE_CARGO_AMOUNT, + CTW_CHOOSE_CARGO_INCOME, + CTW_NS0, + CTW_NS1, + CTW_NS2, + CTW_NS3, + CTW_NS4, + CTW_NS5, + CTW_NS6, + CTW_NS7, + CTW_NS8, + CTW_NS9, + CTW_NS10, + CTW_NSX1, + CTW_NSX2, + CTW_NSX3, + CTW_NSX4, + CTW_NSX5, + CTW_NSX6, + CTW_NSX7, + CTW_NSEND, + CTW_CARGO_FIRST, +}; + +enum CommandsToolbarQuery { + CTQ_TOWN_ID = 0, + CTQ_LOGIN_NAME, + CTQ_LOGIN_PASSWORD, + CTQ_NAME_NEWNAME, + CTQ_LOGIN_CREDENTIALS_NAME, + CTQ_LOGIN_CREDENTIALS_PW, +}; + +enum CommandsToolbarCargoOption { + CTW_OPTION_CARGO_AMOUNT = 0, + CTW_OPTION_CARGO_INCOME, +}; +void AccountLogin(); +void novaautologin(){ + char msg[128]; + if(_settings_client.network.username == NULL || _settings_client.network.userpw == NULL + || strlen(_settings_client.network.username) == 0 || strlen(_settings_client.network.userpw) == 0) return; + sprintf(msg, "!login %s %s", _settings_client.network.username, _settings_client.network.userpw); + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, msg); +} + +bool novahost(){ + _novahost = false; + if(strcmp(_settings_client.network.last_host, "37.157.196.78") == 0 + || strcmp(_settings_client.network.last_host, "2a02:2b88:2:1::1d73:1") == 0 + || strcmp(_settings_client.network.last_host, "89.111.65.225") == 0 + || strcmp(_settings_client.network.last_host, "fe80::20e:7fff:fe23:bee0") == 0 + || strstr(_settings_client.network.last_host, "novapolis") != NULL) + { + _novahost = true; + } + return _novahost; +} + +/** Commands toolbar window handler. */ +struct CommandsToolbarWindow : Window { + + CommandsToolbarQuery query_widget; + CommandsToolbarCargoOption cargo_option; + char login_name[25]; + + CommandsToolbarWindow(WindowDesc *desc, WindowNumber window_number) : Window(desc) + { + this->InitNested(window_number); + this->cargo_option = CTW_OPTION_CARGO_AMOUNT; + this->DisableWidget(CTW_CHOOSE_CARGO_INCOME); + } + + virtual void SetStringParameters(int widget) const + { + if(widget >= CTW_NS0 && widget < CTW_NSEND){ + SetDParam(0, widget - CTW_NS0); + } + } + + virtual void OnClick(Point pt, int widget, int click_count) + { + if (!_networking) return; + char msg[64]; + switch (widget) { + case CTW_GOAL: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!goal"); + break; + case CTW_SCORE: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!score"); + break; + case CTW_TOWN: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!town"); + break; + case CTW_QUEST: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!quest"); + break; + case CTW_TIMELEFT: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!timeleft"); + break; + case CTW_INFO: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!info"); + break; + case CTW_TOWN_ID: + this->query_widget = CTQ_TOWN_ID; + ShowQueryString(STR_EMPTY, STR_TOOLBAR_COMMANDS_TOWN_QUERY, 25, this, CS_NUMERAL, QSF_NONE); + break; + case CTW_HINT: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!hint"); + break; + case CTW_LOGIN: + this->query_widget = CTQ_LOGIN_NAME; + ShowQueryString(STR_EMPTY, STR_TOOLBAR_COMMANDS_LOGIN_NAME_QUERY, 32, this, CS_ALPHANUMERAL, QSF_NONE); + break; + case CTW_LOGIN_CREDENTIALS: + this->query_widget = CTQ_LOGIN_CREDENTIALS_NAME; + ShowQueryString(STR_EMPTY, STR_TOOLBAR_COMMANDS_LOGIN_NAME_QUERY, 32, this, CS_ALPHANUMERAL, QSF_NONE); + break; + case CTW_AUTOLOGIN: + novaautologin(); + break; + case CTW_NAME: + this->query_widget = CTQ_NAME_NEWNAME; + ShowQueryString(STR_EMPTY, STR_TOOLBAR_COMMANDS_NAME_NEWNAME_QUERY, 25, this, CS_ALPHANUMERAL, QSF_NONE); + break; + case CTW_RESETME: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!resetme"); + break; + case CTW_SAVEME: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!saveme"); + break; + case CTW_HELP: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!help"); + break; + case CTW_RULES: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!rules"); + break; + case CTW_CBHINT: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!hint"); + break; + case CTW_CLIENTS: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!clients"); + break; + case CTW_BEST: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!best"); + break; + case CTW_ME: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!me"); + break; + case CTW_RANK: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!rank"); + break; + case CTW_TOPICS: + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, "!topic"); + break; + case CTW_TOPIC1: + case CTW_TOPIC2: + case CTW_TOPIC3: + case CTW_TOPIC4: + case CTW_TOPIC5: + case CTW_TOPIC6: + sprintf(msg, "!topic %i", widget - CTW_TOPIC1 + 1); + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, msg); + break; + case CTW_CHOOSE_CARGO_AMOUNT: + this->cargo_option = CTW_OPTION_CARGO_AMOUNT; + this->DisableWidget(widget); + this->EnableWidget(CTW_CHOOSE_CARGO_INCOME); + this->SetDirty(); + break; + case CTW_CHOOSE_CARGO_INCOME: + this->cargo_option = CTW_OPTION_CARGO_INCOME; + this->DisableWidget(widget); + this->EnableWidget(CTW_CHOOSE_CARGO_AMOUNT); + this->SetDirty(); + break; + default: + if(widget >= CTW_NS0 && widget < CTW_NSEND){ + char ip[16]; + if(widget < CTW_NSX4) strecpy(ip, "37.157.196.78", lastof(ip)); + else strecpy(ip, "89.111.65.225", lastof(ip)); + + NetworkClientConnectGame(NetworkAddress(ip, (3980 + widget - CTW_NS0)), COMPANY_SPECTATOR); + } + else if (widget >= CTW_CARGO_FIRST) { + int i = widget - CTW_CARGO_FIRST; + char name[128]; + GetString(name, _sorted_cargo_specs[i]->name, lastof(name)); + if (this->cargo_option == CTW_OPTION_CARGO_AMOUNT) { + sprintf(msg, "!A%s", name); + } + else { + sprintf(msg, "!I%s", name); + } + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, msg); + this->ToggleWidgetLoweredState(widget); + this->SetDirty(); + } + break; + } + } + void OnQueryTextFinished(char * str) + { + if (!_networking || str == NULL) return; + char msg[128]; + switch (this->query_widget) { + case CTQ_TOWN_ID: + sprintf(msg, "!town %s", str); + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, msg); + break; + case CTQ_LOGIN_NAME: + sprintf(msg, "!login %s", str); + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, msg); + break; + case CTQ_LOGIN_CREDENTIALS_NAME:{ + char name[32]; + char pass[32]; + char buf[64]; + bool space = false; + //separate name and apss + for(int i = 0; pass[i] != '\0'; i++){ + if(str[i] == ' '){ + space = true; + continue; + } + if(!space) name[i] = str[i]; + else pass[i] = str[i]; + } + + if(!space) break; //no pass found + + uint8 digest[16]; + Md5 checksum; + checksum.Append(pass, strlen(pass)); + checksum.Finish(digest); + md5sumToString(buf, lastof(buf), digest); + for(int i = 0; buf[i] != '\0'; i++){ + buf[i] = tolower(buf[i]); + } + strecpy(_settings_client.network.username, this->login_name, lastof(_settings_client.network.username)); + strecpy(_settings_client.network.userpw, buf, lastof(_settings_client.network.userpw)); + SaveToConfig(); + novaautologin(); + break; + } + case CTQ_NAME_NEWNAME: + this->query_widget = CTQ_NAME_NEWNAME; + sprintf(msg, "!name %s", str); + NetworkClientSendChat(NETWORK_ACTION_CHAT_CLIENT, DESTTYPE_CLIENT, CLIENT_ID_SERVER, msg); + break; + default: break; + } + } + + void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) + { + if (widget < CTW_CARGO_FIRST) return; + + const CargoSpec *cs = _sorted_cargo_specs[widget - CTW_CARGO_FIRST]; + SetDParam(0, cs->name); + Dimension d = GetStringBoundingBox(STR_GRAPH_CARGO_PAYMENT_CARGO); + d.width += 14; // colour field + d.width += WD_FRAMERECT_LEFT + WD_FRAMERECT_RIGHT; + d.height += WD_FRAMERECT_TOP + WD_FRAMERECT_BOTTOM; + *size = maxdim(d, *size); + } + void DrawWidget(const Rect &r, int widget) const + { + if (widget < CTW_CARGO_FIRST) return; + + const CargoSpec *cs = _sorted_cargo_specs[widget - CTW_CARGO_FIRST]; + bool rtl = _current_text_dir == TD_RTL; + + /* Since the buttons have no text, no images, + * both the text and the coloured box have to be manually painted. + * clk_dif will move one pixel down and one pixel to the right + * when the button is clicked */ + byte clk_dif = this->IsWidgetLowered(widget) ? 1 : 0; + int x = r.left + WD_FRAMERECT_LEFT; + int y = r.top; + + int rect_x = clk_dif + (rtl ? r.right - 12 : r.left + WD_FRAMERECT_LEFT); + + GfxFillRect(rect_x, y + clk_dif, rect_x + 8, y + 5 + clk_dif, 0); + GfxFillRect(rect_x + 1, y + 1 + clk_dif, rect_x + 7, y + 4 + clk_dif, cs->legend_colour); + SetDParam(0, cs->name); + DrawString(rtl ? r.left : x + 14 + clk_dif, (rtl ? r.right - 14 + clk_dif : r.right), y + clk_dif, STR_GRAPH_CARGO_PAYMENT_CARGO); + } + void OnHundredthTick() + { + for (int i = 0; i < _sorted_standard_cargo_specs_size; i++) { + if (this->IsWidgetLowered(i + CTW_CARGO_FIRST)) { + static int x = 0; + x++; + if (x >= 2) { + this->ToggleWidgetLoweredState(i + CTW_CARGO_FIRST); + this->SetDirty(); + x = 0; + } + } + } + } + +}; + +/** Construct the row containing the digit keys. */ +static NWidgetBase *MakeCargoButtons(int *biggest_index) +{ + NWidgetVertical *ver = new NWidgetVertical; + + for (int i = 0; i < _sorted_standard_cargo_specs_size; i++) { + NWidgetBackground *leaf = new NWidgetBackground(WWT_PANEL, COLOUR_ORANGE, CTW_CARGO_FIRST + i, NULL); + leaf->tool_tip = STR_GRAPH_CARGO_PAYMENT_TOGGLE_CARGO; + leaf->SetFill(1, 0); + ver->Add(leaf); + } + *biggest_index = CTW_CARGO_FIRST + _sorted_standard_cargo_specs_size - 1; + return ver; +} + +static const NWidgetPart _nested_commands_toolbar_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_GREY), + NWidget(WWT_CAPTION, COLOUR_GREY), SetDataTip(STR_TOOLBAR_COMMANDS_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_STICKYBOX, COLOUR_GREY), + EndContainer(), + NWidget(NWID_HORIZONTAL), + NWidget(NWID_VERTICAL), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_GOAL),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_GOAL_CAPTION, STR_TOOLBAR_COMMANDS_GOAL_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_QUEST),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_QUEST_CAPTION, STR_TOOLBAR_COMMANDS_QUEST_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_SCORE),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_SCORE_CAPTION, STR_TOOLBAR_COMMANDS_SCORE_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOWN),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOWN_CAPTION, STR_TOOLBAR_COMMANDS_TOWN_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOWN_ID),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOWN_ID_CAPTION, STR_TOOLBAR_COMMANDS_TOWN_ID_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_HINT),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_HINT_CAPTION, STR_TOOLBAR_COMMANDS_HINT_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_LOGIN),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_LOGIN_CAPTION, STR_TOOLBAR_COMMANDS_LOGIN_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_NAME),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NAME_CAPTION, STR_TOOLBAR_COMMANDS_NAME_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_AUTOLOGIN),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_AUTOLOGIN_CAPTION, STR_TOOLBAR_COMMANDS_AUTOLOGIN_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_LOGIN_CREDENTIALS),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_CREDENTIALS_CAPTION, STR_TOOLBAR_COMMANDS_CREDENTIALS_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TIMELEFT),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TIMELEFT_CAPTION, STR_TOOLBAR_COMMANDS_TIMELEFT_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_INFO),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_INFO_CAPTION, STR_TOOLBAR_COMMANDS_INFO_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS1),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS2),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS3),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS4),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS5),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS6),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS7),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NS8),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(0, 1), EndContainer(), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX1),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX2),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX3),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX4),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX5),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX6),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_WHITE, CTW_NSX7),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_NS_CAPTION, STR_TOOLBAR_COMMANDS_NS_TOOLTIP), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(0, 1), EndContainer(), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_RESETME),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_RESETME_CAPTION, STR_TOOLBAR_COMMANDS_RESETME_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_SAVEME),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_SAVEME_CAPTION, STR_TOOLBAR_COMMANDS_SAVEME_TOOLTIP), + EndContainer(), + //more + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPICS),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPICS_CAPTION, STR_TOOLBAR_COMMANDS_TOPICS_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_HELP),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_HELP_CAPTION, STR_TOOLBAR_COMMANDS_HELP_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPIC1),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPIC1_CAPTION, STR_TOOLBAR_COMMANDS_TOPIC1_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_RULES),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_RULES_CAPTION, STR_TOOLBAR_COMMANDS_RULES_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPIC2),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPIC2_CAPTION, STR_TOOLBAR_COMMANDS_TOPIC2_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_CBHINT),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_CBHINT_CAPTION, STR_TOOLBAR_COMMANDS_CBHINT_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPIC3),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPIC3_CAPTION, STR_TOOLBAR_COMMANDS_TOPIC3_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_BEST),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_BEST_CAPTION, STR_TOOLBAR_COMMANDS_BEST_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPIC4),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPIC4_CAPTION, STR_TOOLBAR_COMMANDS_TOPIC4_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_RANK),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_RANK_CAPTION, STR_TOOLBAR_COMMANDS_RANK_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPIC5),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPIC5_CAPTION, STR_TOOLBAR_COMMANDS_TOPIC5_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_ME),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_ME_CAPTION, STR_TOOLBAR_COMMANDS_ME_TOOLTIP), + EndContainer(), + NWidget(NWID_HORIZONTAL, NC_EQUALSIZE), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_TOPIC6),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_TOPIC6_CAPTION, STR_TOOLBAR_COMMANDS_TOPIC6_TOOLTIP), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(0, 1), EndContainer(), + EndContainer(), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(0, 1), EndContainer(), + EndContainer(), + NWidget(NWID_VERTICAL), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_CHOOSE_CARGO_AMOUNT),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_OPTION_CARGO_A_CAPTION, STR_TOOLBAR_COMMANDS_OPTION_CARGO_A_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, CTW_CHOOSE_CARGO_INCOME),SetMinimalSize(40, 20),SetFill(1, 0), SetDataTip(STR_TOOLBAR_COMMANDS_OPTION_CARGO_I_CAPTION, STR_TOOLBAR_COMMANDS_OPTION_CARGO_I_TOOLTIP), + EndContainer(), + NWidgetFunction(MakeCargoButtons), + NWidget(WWT_PANEL, COLOUR_GREY), SetResize(0, 1), EndContainer(), + EndContainer(), + EndContainer(), +}; + +static WindowDesc _commands_toolbar_desc( + WDP_ALIGN_TOOLBAR, NULL, 0, 0, + WC_COMMAND_TOOLBAR, WC_NONE, + WDF_CONSTRUCTION, + _nested_commands_toolbar_widgets, lengthof(_nested_commands_toolbar_widgets) +); + +/** + * Open the commands toolbar window + * + * @return newly commands toolbar, or NULL if the toolbar could not be opened. + */ +Window *ShowCommandsToolbar() +{ + DeleteWindowByClass(WC_COMMAND_TOOLBAR); + return AllocateWindowDescFront(&_commands_toolbar_desc, 0); +} + +#endif ENABLE_NETWORK \ No newline at end of file diff --git a/src/watch_gui.cpp b/src/watch_gui.cpp new file mode 100644 index 0000000000..c2e380d5d5 --- /dev/null +++ b/src/watch_gui.cpp @@ -0,0 +1,397 @@ +/* $Id: watch_gui.cpp 17678 2009-10-07 20:54:05 muxy $ */ + +/** @file watch_gui.cpp GUI that follow other company building. */ + +#include "stdafx.h" +#include "watch_gui.h" +#include "widget_type.h" +#include "gfx_type.h" +#include "gfx_func.h" +#include "company_base.h" +#include "company_gui.h" +#include "viewport_func.h" +#include "window_func.h" +#include "strings_func.h" +#include "zoom_func.h" +#include "map_func.h" + +#include "network/network.h" +#include "network/network_func.h" +#include "network/network_base.h" +#include "network/network_gui.h" +#include "table/sprites.h" +#include "table/strings.h" + +/** Make the widgets columns for company button, has_client and activity Blot. + * @param biggest_index Storage for collecting the biggest index used in the returned tree. + * @return Horizontal container with butons columns. + * @post \c *biggest_index contains the largest used index in the tree. + */ +static NWidgetBase *MakeCompanyButtons(int *biggest_index) +{ + NWidgetHorizontal *widget_container_horiz = NULL; // Storage for all cols. + NWidgetVertical *widget_container_company = NULL; // Storage for company Col. + NWidgetVertical *widget_container_hasclient = NULL; // Storage for Has Client Blot. + NWidgetVertical *widget_container_activity = NULL; // Storage for Activity Blot. + + widget_container_horiz = new NWidgetHorizontal( ); + widget_container_company = new NWidgetVertical( ); + widget_container_hasclient = new NWidgetVertical( ); + widget_container_activity = new NWidgetVertical( ); + + Dimension company_sprite_size = GetSpriteSize( SPR_COMPANY_ICON ); + company_sprite_size.width += WD_MATRIX_LEFT + WD_MATRIX_RIGHT; + company_sprite_size.height += WD_MATRIX_TOP + WD_MATRIX_BOTTOM + 1; // 1 for the 'offset' of being pressed + + Dimension blot_sprite_size = GetSpriteSize( SPR_BLOT ); + blot_sprite_size.width += WD_MATRIX_LEFT + WD_MATRIX_RIGHT; + blot_sprite_size.height += WD_MATRIX_TOP + WD_MATRIX_BOTTOM + 1; // 1 for the 'offset' of being pressed + + + for (int company_num = COMPANY_FIRST; company_num < MAX_COMPANIES; company_num++ ) { + /* Manage Company Buttons */ + NWidgetBackground *company_panel = new NWidgetBackground( WWT_PANEL, COLOUR_GREY, EWW_PB_COMPANY_FIRST + company_num ); + company_panel->SetMinimalSize( company_sprite_size.width, company_sprite_size.height ); + company_panel->SetResize( 0, 0 ); + company_panel->SetFill( 1, 0 ); + company_panel->SetDataTip( 0x0, STR_WATCH_CLICK_TO_WATCH_COMPANY ); + widget_container_company->Add( company_panel ); + + /* Manage Has Client Blot */ + NWidgetBackground *hasclient_panel = new NWidgetBackground( WWT_PANEL, COLOUR_GREY, EWW_HAS_CLIENT_FIRST + company_num ); + company_panel->SetMinimalSize( blot_sprite_size.width, blot_sprite_size.height ); + company_panel->SetResize( 0, 0 ); + company_panel->SetFill( 1, 0 ); + widget_container_hasclient->Add( hasclient_panel ); + } + + /* Add the verticals widgets to the horizontal container */ + widget_container_horiz->Add( widget_container_company ); + widget_container_horiz->Add( widget_container_hasclient ); + + /* return the horizontal widget container */ + return widget_container_horiz; +} + +/** + * Watch Company Window Widgets Array + * The Company Button, Has Client Blot and Activity Blot Columns + * Are made through a function regarding MAX_COMPANIES value + */ +static const NWidgetPart _nested_watch_company_widgets[] = { + /* Title Bar with close box, title, shade and stick boxes */ + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_GREY), + NWidget(WWT_CAPTION, COLOUR_GREY, EWW_CAPTION ), SetDataTip(STR_WATCH_WINDOW_TITLE, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_SHADEBOX, COLOUR_GREY), + NWidget(WWT_STICKYBOX, COLOUR_GREY), + EndContainer( ), + NWidget( NWID_HORIZONTAL ), + NWidget( NWID_VERTICAL ), + NWidgetFunction( MakeCompanyButtons ), + /* Buton Zoom Out, In, Scrollto */ + NWidget(NWID_HORIZONTAL), + NWidget( WWT_PUSHIMGBTN, COLOUR_GREY, EWW_ZOOMOUT ), SetDataTip( SPR_IMG_ZOOMOUT, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_OUT), + NWidget( WWT_PUSHIMGBTN, COLOUR_GREY, EWW_ZOOMIN ), SetDataTip( SPR_IMG_ZOOMIN, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_IN), + NWidget( WWT_PUSHIMGBTN, COLOUR_GREY, EWW_CENTER ), SetDataTip( SPR_CENTRE_VIEW_VEHICLE, STR_EXTRA_VIEW_MOVE_MAIN_TO_VIEW_TT), + NWidget( WWT_PANEL, COLOUR_GREY, EWW_NEW_WINDOW ), SetDataTip( 0, STR_WATCH_CLICK_NEW_WINDOW ), EndContainer( ), + EndContainer( ), + /* Background panel for resize purpose */ + NWidget( WWT_PANEL, COLOUR_GREY ), SetResize( 0, 1 ), EndContainer( ), + EndContainer( ), + /* Watch Pannel */ + NWidget(WWT_PANEL, COLOUR_GREY), + NWidget(NWID_VIEWPORT, INVALID_COLOUR, EWW_WATCH), SetPadding(2, 2, 2, 2), SetResize(1, 1), SetFill(1, 1), + EndContainer( ), + EndContainer( ), + /* Status Bar with resize buton */ + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PANEL, COLOUR_GREY), SetFill(1, 1), SetResize(1, 0), EndContainer(), + NWidget(WWT_RESIZEBOX, COLOUR_GREY), + EndContainer( ), +}; + +/** + * Watch Company Window Descriptor + */ +static WindowDesc _watch_company_desc( + WDP_AUTO, NULL, 300, 257, + WC_WATCH_COMPANY, WC_NONE, + WDF_NO_FOCUS, + _nested_watch_company_widgets, lengthof( _nested_watch_company_widgets ) +); + +//static int WatchCompany::button1_state[MAX_COMPANIES]; + +/** Watch Company Class Constructor + * @param desc Window Descriptor The Window Descriptor + * @param window_number The window number for the class + * @param company_to_watch Company ID for watching a particular company + */ +WatchCompany::WatchCompany(WindowDesc *desc, int window_number, CompanyID company_to_watch = INVALID_COMPANY ) : Window(desc) +{ + this->watched_company = company_to_watch; + + this->InitNested(window_number); + this->owner = this->watched_company; + + /* Reset activity and client count for all companies */ + for (CompanyID i = COMPANY_FIRST; i < MAX_COMPANIES; i++) { + this->company_activity[i] = 0; + this->company_count_client[i] = 0; + } + + GetString( this->company_name, STR_JUST_NOTHING, lastof(this->company_name) ); + + /* Init the viewport area */ + NWidgetViewport *nvp = this->GetWidget(EWW_WATCH); + nvp->InitializeViewport(this, 0, ZOOM_LVL_NORMAL); + + Point pt; + /* the main window with the main view */ + const Window *w = FindWindowById(WC_MAIN_WINDOW, 0); + + /* center on same place as main window (zoom is maximum, no adjustment needed) */ + pt.x = w->viewport->scrollpos_x + w->viewport->virtual_width / 2; + pt.y = w->viewport->scrollpos_y + w->viewport->virtual_height / 2; + + this->viewport->scrollpos_x = pt.x - this->viewport->virtual_width / 2; + this->viewport->scrollpos_y = pt.y - this->viewport->virtual_height / 2; + this->viewport->dest_scrollpos_x = this->viewport->scrollpos_x; + this->viewport->dest_scrollpos_y = this->viewport->scrollpos_y; + + if ( this->watched_company != INVALID_COMPANY ) { + Company *c = Company::Get( this->watched_company ); + this->ScrollToTile( c->last_build_coordinate ); + } + this->InvalidateData( ); +} + +void WatchCompany::SetStringParameters(int widget) const +{ + switch (widget) { + case EWW_CAPTION: + SetDParamStr( 0, this->company_name ); + break; + } +} + +void WatchCompany::DrawWidget(const Rect &r, int widget) const +{ + /* draw the widget */ + /* Company Button */ + if (IsInsideMM(widget, EWW_PB_COMPANY_FIRST, EWW_PB_COMPANY_LAST + 1)) { + if (this->IsWidgetDisabled(widget)) return; + if ( Company::IsValidID( widget - EWW_PB_COMPANY_FIRST ) ) { + CompanyID cid = (CompanyID)(widget - ( EWW_PB_COMPANY_FIRST ) ); + int offset = (cid == this->watched_company) ? 1 : 0; + Dimension sprite_size = GetSpriteSize(SPR_COMPANY_ICON); + DrawCompanyIcon(cid, (r.left + r.right - sprite_size.width) / 2 + offset, (r.top + r.bottom - sprite_size.height) / 2 + offset); + } + return; + } + /* Has Client Blot */ + if (IsInsideMM( widget, EWW_HAS_CLIENT_FIRST, EWW_HAS_CLIENT_LAST + 1 )) { + if ( Company::IsValidID( widget-EWW_HAS_CLIENT_FIRST ) ) { + /* Draw the Blot only if Company Exists */ + Dimension sprite_size = GetSpriteSize(SPR_BLOT); +#ifdef ENABLE_NETWORK + if (!_networking) { // Local game, draw the Blot + DrawSprite(SPR_BLOT, Company::IsValidAiID(widget - EWW_HAS_CLIENT_FIRST) ? PALETTE_TO_ORANGE : PALETTE_TO_GREEN, (r.left + r.right - sprite_size.width) / 2, (r.top + r.bottom - sprite_size.height) / 2 ); + } else { // Network game, draw the blot according to company client count + DrawSprite(SPR_BLOT, this->company_count_client[widget-EWW_HAS_CLIENT_FIRST] > 0 ? (company_activity[widget-EWW_HAS_CLIENT_FIRST] > 0 ? PALETTE_TO_RED : PALETTE_TO_GREEN) : PALETTE_TO_GREY, (r.left + r.right - sprite_size.width) / 2, (r.top + r.bottom - sprite_size.height) / 2 ); + } +#else + DrawSprite(SPR_BLOT, Company::IsValidAiID(widget-EWW_HAS_CLIENT_FIRST) ? PALETTE_TO_ORANGE : PALETTE_TO_GREEN, (r.left + r.right - sprite_size.width) / 2, (r.top + r.bottom - sprite_size.height) / 2 ); +#endif + } + } +} + +void WatchCompany::OnResize() +{ + if (this->viewport != NULL) { + NWidgetViewport *nvp = this->GetWidget(EWW_WATCH); + nvp->UpdateViewportCoordinates(this); + } +} + +void WatchCompany::OnScroll(Point delta) +{ + const ViewPort *vp = IsPtInWindowViewport(this, _cursor.pos.x, _cursor.pos.y); + if (vp == NULL) return; + + this->viewport->scrollpos_x += ScaleByZoom(delta.x, vp->zoom); + this->viewport->scrollpos_y += ScaleByZoom(delta.y, vp->zoom); + this->viewport->dest_scrollpos_x = this->viewport->scrollpos_x; + this->viewport->dest_scrollpos_y = this->viewport->scrollpos_y; +} + +void WatchCompany::OnMouseWheel( int wheel ) +{ + ZoomInOrOutToCursorWindow(wheel < 0, this); +} + +void WatchCompany::OnClick(Point pt, int widget, int click_count) +{ + if (IsInsideMM(widget, EWW_PB_COMPANY_FIRST, EWW_PB_COMPANY_LAST + 1)) { + /* Click on Company Button */ + if (!this->IsWidgetDisabled(widget)) { + if (this->watched_company != INVALID_COMPANY) { + /* Raise the watched company button */ + this->RaiseWidget(this->watched_company + EWW_PB_COMPANY_FIRST); + } + if (this->watched_company == (CompanyID)(widget - EWW_PB_COMPANY_FIRST)) { + /* Stop watching watched_company */ + this->watched_company = INVALID_COMPANY; + GetString( this->company_name, STR_JUST_NOTHING, lastof(this->company_name) ); + } else { + /* Lower the new watched company button */ + this->watched_company = (CompanyID)(widget - EWW_PB_COMPANY_FIRST); + this->LowerWidget(this->watched_company + EWW_PB_COMPANY_FIRST); + Company *c = Company::Get( this->watched_company ); + SetDParam( 0, c->index ); + GetString( this->company_name, STR_COMPANY_NAME, lastof(this->company_name) ); + + this->ScrollToTile( c->last_build_coordinate ); + } + this->owner = this->watched_company; + this->SetDirty(); + } + } + else if ( IsInsideMM(widget, EWW_PB_ACTION1_FIRST, EWW_PB_ACTION1_LAST + 1)) { + if ( !this->IsWidgetDisabled(widget) ) { + this->ToggleWidgetLoweredState( widget ); + this->SetDirty(); + } + } +#ifdef ENABLE_NETWORK + else if ( IsInsideMM(widget, EWW_HAS_CLIENT_FIRST, EWW_HAS_CLIENT_LAST + 1)) { + if(_networking && Company::IsValidID(widget - EWW_HAS_CLIENT_FIRST)){ + ShowNetworkChatQueryWindow(DESTTYPE_TEAM, widget - EWW_HAS_CLIENT_FIRST); + } + } +#endif + else { + switch (widget) { + case EWW_ZOOMOUT: DoZoomInOutWindow(ZOOM_OUT, this); break; + case EWW_ZOOMIN: DoZoomInOutWindow(ZOOM_IN, this); break; + + case EWW_CENTER: { // location button (move main view to same spot as this view) 'Center Main View' + Window *w = FindWindowById(WC_MAIN_WINDOW, 0); + int x = this->viewport->scrollpos_x; // Where is the watch looking at + int y = this->viewport->scrollpos_y; + + /* set the main view to same location. Based on the center, adjusting for zoom */ + w->viewport->dest_scrollpos_x = x - (w->viewport->virtual_width - this->viewport->virtual_width) / 2; + w->viewport->dest_scrollpos_y = y - (w->viewport->virtual_height - this->viewport->virtual_height) / 2; + } break; + + case EWW_NEW_WINDOW: + ShowWatchWindow( this->watched_company ); + break; + } + } +} + +void WatchCompany::OnInvalidateData(int data, bool gui_scope) +{ + /* Disable the companies who are not active */ + for (CompanyID i = COMPANY_FIRST; i < MAX_COMPANIES; i++) { + this->SetWidgetDisabledState(EWW_PB_COMPANY_FIRST + i , !Company::IsValidID(i) ); + this->SetWidgetDisabledState(EWW_PB_ACTION1_FIRST + i , !Company::IsValidID(i) ); + } + /* Check if the currently selected company is still active. */ + if (this->watched_company != INVALID_COMPANY) { + /* Make sure the widget is lowered */ + this->LowerWidget(EWW_PB_COMPANY_FIRST + this->watched_company); + /* Check if the watched Company is still a valid one */ + if (!Company::IsValidID(this->watched_company)) { + /* Invalid Company Raise the associated widget. */ + this->RaiseWidget(this->watched_company + EWW_PB_COMPANY_FIRST ); + this->watched_company = INVALID_COMPANY; + GetString( this->company_name, STR_JUST_NOTHING, lastof(this->company_name) ); + } else { + Company *c = Company::Get( this->watched_company ); + SetDParam( 0, c->index ); + GetString( this->company_name, STR_COMPANY_NAME, lastof(this->company_name) ); + } + } else { + GetString( this->company_name, STR_JUST_NOTHING, lastof(this->company_name) ); + + } +#ifdef ENABLE_NETWORK + if (_networking) { // Local game, draw the Blot + /* Reset company count - network only */ + for (CompanyID i = COMPANY_FIRST; i < MAX_COMPANIES; i++) { + this->company_count_client[i] = 0; + } + /* Calculate client count into company - network only */ + NetworkClientInfo *ci; + FOR_ALL_CLIENT_INFOS( ci ) { + if (Company::IsValidID(ci->client_playas)) { + company_count_client[ci->client_playas]+=1; + } + } + } +#endif + HandleZoomMessage(this, this->viewport, EWW_ZOOMIN, EWW_ZOOMOUT); +} + +void WatchCompany::ScrollToTile( TileIndex tile ) +{ + /* Scroll window to the tile, only if not zero */ + if (tile != 0) { + ScrollWindowTo( TileX(tile) * TILE_SIZE + TILE_SIZE / 2, TileY(tile) * TILE_SIZE + TILE_SIZE / 2, -1, this ); + } +} + +/** OnDoCommand function - Called by the DoCommand + * @param company The company ID who's client is building + * @param tile The tile number where action took place + */ +void WatchCompany::OnDoCommand( CompanyByte company, TileIndex tile ) +{ + /* Check if its my company */ + if (this->watched_company == company) + { + this->ScrollToTile( tile ); + } + /* set the company_activity to its max in order to paint the BLOT in red + * This will result by having the activity blot set to red for all companies + * even the one watched. To avoid this behaviour and not to light the blot of + * the watched company, the code can be moved just after the ScrollToTile call. + */ + if (tile != 0) { + this->company_activity[company] = MAX_ACTIVITY; + this->SetDirty( ); + } +} + +/** Used to decrement the activity counter + * + */ +void WatchCompany::OnTick() +{ + bool set_dirty = false; + for (CompanyID i = COMPANY_FIRST; i < MAX_COMPANIES; i++) { + if ( this->company_activity[i]>0 ) { + this->company_activity[i]--; + if ( this->company_activity[i]==0 ) { + set_dirty = true; + } + } + } + /* If one company_activity reaches 0, then redraw */ + if (set_dirty) { + this->SetDirty(); + } +} + +void ShowWatchWindow( CompanyID company_to_watch = INVALID_COMPANY ) +{ + int i = 0; + /* find next free window number for watch viewport */ + while (FindWindowById(WC_WATCH_COMPANY, i) != NULL) i++; + new WatchCompany( &_watch_company_desc, i, company_to_watch ); +} diff --git a/src/watch_gui.h b/src/watch_gui.h new file mode 100644 index 0000000000..e1de429e8f --- /dev/null +++ b/src/watch_gui.h @@ -0,0 +1,61 @@ +/* $Id: watch_gui.h 17678 2009-10-07 20:54:05 muxy $ */ + +/** @file watch_gui.h GUI Functions related to watching. */ + +#ifndef WATCH_GUI_H +#define WATCH_GUI_H + +#include "window_gui.h" +#include "company_base.h" + +#define MAX_ACTIVITY 30 + +enum WatchCompanyWidgets { + EWW_CAPTION, + EWW_PB_COMPANY_FIRST, + EWW_PB_COMPANY_LAST = EWW_PB_COMPANY_FIRST + MAX_COMPANIES - 1, + EWW_HAS_CLIENT_FIRST, + EWW_HAS_CLIENT_LAST = EWW_HAS_CLIENT_FIRST + MAX_COMPANIES - 1, + EWW_ACTIVITY_FIRST, + EWW_ACTIVITY_LAST = EWW_ACTIVITY_FIRST + MAX_COMPANIES - 1, + EWW_PB_ACTION1_FIRST, + EWW_PB_ACTION1_LAST = EWW_PB_ACTION1_FIRST + MAX_COMPANIES - 1, + EWW_WATCH, + EWW_ZOOMIN, + EWW_ZOOMOUT, + EWW_CENTER, + EWW_NEW_WINDOW +}; + +class WatchCompany : public Window +{ + +protected: + CompanyID watched_company; // Company ID beeing watched. + int company_activity[MAX_COMPANIES]; // int array for activity blot. + int company_count_client[MAX_COMPANIES]; // company client count. + char company_name[MAX_LENGTH_COMPANY_NAME_CHARS]; // company name for title display + + void SetWatchWindowTitle( ); + void ScrollToTile( TileIndex tile ); + + +public: + WatchCompany(WindowDesc *desc, int window_number, CompanyID company_to_watch ); + + virtual void SetStringParameters(int widget) const; + //virtual void OnPaint( ); + virtual void DrawWidget(const Rect &r, int widget) const; + virtual void OnClick(Point pt, int widget, int click_count); + virtual void OnResize( ); + virtual void OnScroll(Point delta); + virtual void OnMouseWheel(int wheel); + virtual void OnInvalidateData(int data, bool gui_scope ); + virtual void OnTick( ); + + void OnDoCommand( CompanyByte company, TileIndex tile ); +}; + +void ShowWatchWindow( CompanyID company_to_watch ); + +#endif // COMPANY_GUI_H diff --git a/src/widgets/cargo_table_widget.h b/src/widgets/cargo_table_widget.h new file mode 100644 index 0000000000..75a177f44e --- /dev/null +++ b/src/widgets/cargo_table_widget.h @@ -0,0 +1,20 @@ +/* $Id: cargo_table_widget.h 23600 2011-12-19 20:46:17Z TheDude $ */ + +/** @file cargo_table_widget.h Types related to the cargos widgets. */ + +#ifndef WIDGETS_CARGO_TABLE_WIDGET_H +#define WIDGETS_CARGO_TABLE_WIDGET_H + +/** Widgets of the #CargosWindow class. */ +enum CargosWidgets { + WID_CT_BACKGROUND, ///< Caption of the window. + WID_CT_CAPTION, + WID_CT_HEADER_CARGO, + WID_CT_HEADER_AMOUNT, + WID_CT_HEADER_INCOME, + WID_CT_LIST, + WID_CT_AMOUNT, + WID_CT_INCOME, +}; + +#endif /* WIDGETS_CARGO_TABLE_WIDGET_H */ diff --git a/src/window.cpp.orig b/src/window.cpp.orig new file mode 100644 index 0000000000..a4fc3276cd --- /dev/null +++ b/src/window.cpp.orig @@ -0,0 +1,3443 @@ +/* $Id: window.cpp 26392 2014-03-05 21:21:55Z alberth $ */ + +/* + * 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 window.cpp Windowing system, widgets and events */ + +#include "stdafx.h" +#include +#include "company_func.h" +#include "gfx_func.h" +#include "console_func.h" +#include "console_gui.h" +#include "viewport_func.h" +#include "progress.h" +#include "blitter/factory.hpp" +#include "zoom_func.h" +#include "vehicle_base.h" +#include "window_func.h" +#include "tilehighlight_func.h" +#include "network/network.h" +#include "querystring_gui.h" +#include "widgets/dropdown_func.h" +#include "strings_func.h" +#include "settings_type.h" +#include "settings_func.h" +#include "ini_type.h" +#include "newgrf_debug.h" +#include "hotkeys.h" +#include "toolbar_gui.h" +#include "statusbar_gui.h" +#include "error.h" +#include "game/game.hpp" +#include "video/video_driver.hpp" + +/** Values for _settings_client.gui.auto_scrolling */ +enum ViewportAutoscrolling { + VA_DISABLED, //!< Do not autoscroll when mouse is at edge of viewport. + VA_MAIN_VIEWPORT_FULLSCREEN, //!< Scroll main viewport at edge when using fullscreen. + VA_MAIN_VIEWPORT, //!< Scroll main viewport at edge. + VA_EVERY_VIEWPORT, //!< Scroll all viewports at their edges. +}; + +static Point _drag_delta; ///< delta between mouse cursor and upper left corner of dragged window +static Window *_mouseover_last_w = NULL; ///< Window of the last #MOUSEOVER event. +static Window *_last_scroll_window = NULL; ///< Window of the last scroll event. + +/** List of windows opened at the screen sorted from the front. */ +Window *_z_front_window = NULL; +/** List of windows opened at the screen sorted from the back. */ +Window *_z_back_window = NULL; + +/** If false, highlight is white, otherwise the by the widget defined colour. */ +bool _window_highlight_colour = false; + +/* + * Window that currently has focus. - The main purpose is to generate + * #FocusLost events, not to give next window in z-order focus when a + * window is closed. + */ +Window *_focused_window; + +Point _cursorpos_drag_start; + +int _scrollbar_start_pos; +int _scrollbar_size; +byte _scroller_click_timeout = 0; + +bool _scrolling_viewport; ///< A viewport is being scrolled with the mouse. +bool _mouse_hovering; ///< The mouse is hovering over the same point. + +SpecialMouseMode _special_mouse_mode; ///< Mode of the mouse. + +/** + * List of all WindowDescs. + * This is a pointer to ensure initialisation order with the various static WindowDesc instances. + */ +static SmallVector *_window_descs = NULL; + +/** Config file to store WindowDesc */ +char *_windows_file; + +/** Window description constructor. */ +WindowDesc::WindowDesc(WindowPosition def_pos, const char *ini_key, int16 def_width, int16 def_height, + WindowClass window_class, WindowClass parent_class, uint32 flags, + const NWidgetPart *nwid_parts, int16 nwid_length, HotkeyList *hotkeys) : + default_pos(def_pos), + default_width(def_width), + default_height(def_height), + cls(window_class), + parent_cls(parent_class), + ini_key(ini_key), + flags(flags), + nwid_parts(nwid_parts), + nwid_length(nwid_length), + hotkeys(hotkeys), + pref_sticky(false), + pref_width(0), + pref_height(0) +{ + if (_window_descs == NULL) _window_descs = new SmallVector(); + *_window_descs->Append() = this; +} + +WindowDesc::~WindowDesc() +{ + _window_descs->Erase(_window_descs->Find(this)); +} + +/** + * Load all WindowDesc settings from _windows_file. + */ +void WindowDesc::LoadFromConfig() +{ + IniFile *ini = new IniFile(); + ini->LoadFromDisk(_windows_file, BASE_DIR); + for (WindowDesc **it = _window_descs->Begin(); it != _window_descs->End(); ++it) { + if ((*it)->ini_key == NULL) continue; + IniLoadWindowSettings(ini, (*it)->ini_key, *it); + } + delete ini; +} + +/** + * Sort WindowDesc by ini_key. + */ +static int CDECL DescSorter(WindowDesc * const *a, WindowDesc * const *b) +{ + if ((*a)->ini_key != NULL && (*b)->ini_key != NULL) return strcmp((*a)->ini_key, (*b)->ini_key); + return ((*b)->ini_key != NULL ? 1 : 0) - ((*a)->ini_key != NULL ? 1 : 0); +} + +/** + * Save all WindowDesc settings to _windows_file. + */ +void WindowDesc::SaveToConfig() +{ + /* Sort the stuff to get a nice ini file on first write */ + QSortT(_window_descs->Begin(), _window_descs->Length(), DescSorter); + + IniFile *ini = new IniFile(); + ini->LoadFromDisk(_windows_file, BASE_DIR); + for (WindowDesc **it = _window_descs->Begin(); it != _window_descs->End(); ++it) { + if ((*it)->ini_key == NULL) continue; + IniSaveWindowSettings(ini, (*it)->ini_key, *it); + } + ini->SaveToDisk(_windows_file); + delete ini; +} + +/** + * Read default values from WindowDesc configuration an apply them to the window. + */ +void Window::ApplyDefaults() +{ + if (this->nested_root != NULL && this->nested_root->GetWidgetOfType(WWT_STICKYBOX) != NULL) { + if (this->window_desc->pref_sticky) this->flags |= WF_STICKY; + } else { + /* There is no stickybox; clear the preference in case someone tried to be funny */ + this->window_desc->pref_sticky = false; + } +} + +/** + * Compute the row of a widget that a user clicked in. + * @param clickpos Vertical position of the mouse click. + * @param widget Widget number of the widget clicked in. + * @param padding Amount of empty space between the widget edge and the top of the first row. + * @param line_height Height of a single row. A negative value means using the vertical resize step of the widget. + * @return Row number clicked at. If clicked at a wrong position, #INT_MAX is returned. + * @note The widget does not know where a list printed at the widget ends, so below a list is not a wrong position. + */ +int Window::GetRowFromWidget(int clickpos, int widget, int padding, int line_height) const +{ + const NWidgetBase *wid = this->GetWidget(widget); + if (line_height < 0) line_height = wid->resize_y; + if (clickpos < (int)wid->pos_y + padding) return INT_MAX; + return (clickpos - (int)wid->pos_y - padding) / line_height; +} + +/** + * Disable the highlighted status of all widgets. + */ +void Window::DisableAllWidgetHighlight() +{ + for (uint i = 0; i < this->nested_array_size; i++) { + NWidgetBase *nwid = this->GetWidget(i); + if (nwid == NULL) continue; + + if (nwid->IsHighlighted()) { + nwid->SetHighlighted(TC_INVALID); + this->SetWidgetDirty(i); + } + } + + CLRBITS(this->flags, WF_HIGHLIGHTED); +} + +/** + * Sets the highlighted status of a widget. + * @param widget_index index of this widget in the window + * @param highlighted_colour Colour of highlight, or TC_INVALID to disable. + */ +void Window::SetWidgetHighlight(byte widget_index, TextColour highlighted_colour) +{ + assert(widget_index < this->nested_array_size); + + NWidgetBase *nwid = this->GetWidget(widget_index); + if (nwid == NULL) return; + + nwid->SetHighlighted(highlighted_colour); + this->SetWidgetDirty(widget_index); + + if (highlighted_colour != TC_INVALID) { + /* If we set a highlight, the window has a highlight */ + this->flags |= WF_HIGHLIGHTED; + } else { + /* If we disable a highlight, check all widgets if anyone still has a highlight */ + bool valid = false; + for (uint i = 0; i < this->nested_array_size; i++) { + NWidgetBase *nwid = this->GetWidget(i); + if (nwid == NULL) continue; + if (!nwid->IsHighlighted()) continue; + + valid = true; + } + /* If nobody has a highlight, disable the flag on the window */ + if (!valid) CLRBITS(this->flags, WF_HIGHLIGHTED); + } +} + +/** + * Gets the highlighted status of a widget. + * @param widget_index index of this widget in the window + * @return status of the widget ie: highlighted = true, not highlighted = false + */ +bool Window::IsWidgetHighlighted(byte widget_index) const +{ + assert(widget_index < this->nested_array_size); + + const NWidgetBase *nwid = this->GetWidget(widget_index); + if (nwid == NULL) return false; + + return nwid->IsHighlighted(); +} + +/** + * A dropdown window associated to this window has been closed. + * @param pt the point inside the window the mouse resides on after closure. + * @param widget the widget (button) that the dropdown is associated with. + * @param index the element in the dropdown that is selected. + * @param instant_close whether the dropdown was configured to close on mouse up. + */ +void Window::OnDropdownClose(Point pt, int widget, int index, bool instant_close) +{ + if (widget < 0) return; + + if (instant_close) { + /* Send event for selected option if we're still + * on the parent button of the dropdown (behaviour of the dropdowns in the main toolbar). */ + if (GetWidgetFromPos(this, pt.x, pt.y) == widget) { + this->OnDropdownSelect(widget, index); + } + } + + /* Raise the dropdown button */ + NWidgetCore *nwi2 = this->GetWidget(widget); + if ((nwi2->type & WWT_MASK) == NWID_BUTTON_DROPDOWN) { + nwi2->disp_flags &= ~ND_DROPDOWN_ACTIVE; + } else { + this->RaiseWidget(widget); + } + this->SetWidgetDirty(widget); +} + +/** + * Return the Scrollbar to a widget index. + * @param widnum Scrollbar widget index + * @return Scrollbar to the widget + */ +const Scrollbar *Window::GetScrollbar(uint widnum) const +{ + return this->GetWidget(widnum); +} + +/** + * Return the Scrollbar to a widget index. + * @param widnum Scrollbar widget index + * @return Scrollbar to the widget + */ +Scrollbar *Window::GetScrollbar(uint widnum) +{ + return this->GetWidget(widnum); +} + +/** + * Return the querystring associated to a editbox. + * @param widnum Editbox widget index + * @return QueryString or NULL. + */ +const QueryString *Window::GetQueryString(uint widnum) const +{ + const SmallMap::Pair *query = this->querystrings.Find(widnum); + return query != this->querystrings.End() ? query->second : NULL; +} + +/** + * Return the querystring associated to a editbox. + * @param widnum Editbox widget index + * @return QueryString or NULL. + */ +QueryString *Window::GetQueryString(uint widnum) +{ + SmallMap::Pair *query = this->querystrings.Find(widnum); + return query != this->querystrings.End() ? query->second : NULL; +} + +/** + * Get the current input text if an edit box has the focus. + * @return The currently focused input text or NULL if no input focused. + */ +/* virtual */ const char *Window::GetFocusedText() const +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) { + return this->GetQueryString(this->nested_focus->index)->GetText(); + } + + return NULL; +} + +/** + * Get the string at the caret if an edit box has the focus. + * @return The text at the caret or NULL if no edit box is focused. + */ +/* virtual */ const char *Window::GetCaret() const +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) { + return this->GetQueryString(this->nested_focus->index)->GetCaret(); + } + + return NULL; +} + +/** + * Get the range of the currently marked input text. + * @param[out] length Length of the marked text. + * @return Pointer to the start of the marked text or NULL if no text is marked. + */ +/* virtual */ const char *Window::GetMarkedText(size_t *length) const +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) { + return this->GetQueryString(this->nested_focus->index)->GetMarkedText(length); + } + + return NULL; +} + +/** + * Get the current caret position if an edit box has the focus. + * @return Top-left location of the caret, relative to the window. + */ +/* virtual */ Point Window::GetCaretPosition() const +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) { + return this->GetQueryString(this->nested_focus->index)->GetCaretPosition(this, this->nested_focus->index); + } + + Point pt = {0, 0}; + return pt; +} + +/** + * Get the bounding rectangle for a text range if an edit box has the focus. + * @param from Start of the string range. + * @param to End of the string range. + * @return Rectangle encompassing the string range, relative to the window. + */ +/* virtual */ Rect Window::GetTextBoundingRect(const char *from, const char *to) const +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) { + return this->GetQueryString(this->nested_focus->index)->GetBoundingRect(this, this->nested_focus->index, from, to); + } + + Rect r = {0, 0, 0, 0}; + return r; +} + +/** + * Get the character that is rendered at a position by the focused edit box. + * @param pt The position to test. + * @return Pointer to the character at the position or NULL if no character is at the position. + */ +/* virtual */ const char *Window::GetTextCharacterAtPosition(const Point &pt) const +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) { + return this->GetQueryString(this->nested_focus->index)->GetCharAtPosition(this, this->nested_focus->index, pt); + } + + return NULL; +} + +/** + * Set the window that has the focus + * @param w The window to set the focus on + */ +void SetFocusedWindow(Window *w) +{ + if (_focused_window == w) return; + + /* Invalidate focused widget */ + if (_focused_window != NULL) { + if (_focused_window->nested_focus != NULL) _focused_window->nested_focus->SetDirty(_focused_window); + } + + /* Remember which window was previously focused */ + Window *old_focused = _focused_window; + _focused_window = w; + + /* So we can inform it that it lost focus */ + if (old_focused != NULL) old_focused->OnFocusLost(); + if (_focused_window != NULL) _focused_window->OnFocus(); +} + +/** + * Check if an edit box is in global focus. That is if focused window + * has a edit box as focused widget, or if a console is focused. + * @return returns true if an edit box is in global focus or if the focused window is a console, else false + */ +bool EditBoxInGlobalFocus() +{ + if (_focused_window == NULL) return false; + + /* The console does not have an edit box so a special case is needed. */ + if (_focused_window->window_class == WC_CONSOLE) return true; + + return _focused_window->nested_focus != NULL && _focused_window->nested_focus->type == WWT_EDITBOX; +} + +/** + * Makes no widget on this window have focus. The function however doesn't change which window has focus. + */ +void Window::UnfocusFocusedWidget() +{ + if (this->nested_focus != NULL) { + if (this->nested_focus->type == WWT_EDITBOX) _video_driver->EditBoxLostFocus(); + + /* Repaint the widget that lost focus. A focused edit box may else leave the caret on the screen. */ + this->nested_focus->SetDirty(this); + this->nested_focus = NULL; + } +} + +/** + * Set focus within this window to the given widget. The function however doesn't change which window has focus. + * @param widget_index Index of the widget in the window to set the focus to. + * @return Focus has changed. + */ +bool Window::SetFocusedWidget(int widget_index) +{ + /* Do nothing if widget_index is already focused, or if it wasn't a valid widget. */ + if ((uint)widget_index >= this->nested_array_size) return false; + + assert(this->nested_array[widget_index] != NULL); // Setting focus to a non-existing widget is a bad idea. + if (this->nested_focus != NULL) { + if (this->GetWidget(widget_index) == this->nested_focus) return false; + + /* Repaint the widget that lost focus. A focused edit box may else leave the caret on the screen. */ + this->nested_focus->SetDirty(this); + if (this->nested_focus->type == WWT_EDITBOX) _video_driver->EditBoxLostFocus(); + } + this->nested_focus = this->GetWidget(widget_index); + return true; +} + +/** + * Called when window looses focus + */ +void Window::OnFocusLost() +{ + if (this->nested_focus != NULL && this->nested_focus->type == WWT_EDITBOX) _video_driver->EditBoxLostFocus(); +} + +/** + * Sets the enabled/disabled status of a list of widgets. + * By default, widgets are enabled. + * On certain conditions, they have to be disabled. + * @param disab_stat status to use ie: disabled = true, enabled = false + * @param widgets list of widgets ended by WIDGET_LIST_END + */ +void CDECL Window::SetWidgetsDisabledState(bool disab_stat, int widgets, ...) +{ + va_list wdg_list; + + va_start(wdg_list, widgets); + + while (widgets != WIDGET_LIST_END) { + SetWidgetDisabledState(widgets, disab_stat); + widgets = va_arg(wdg_list, int); + } + + va_end(wdg_list); +} + +/** + * Sets the lowered/raised status of a list of widgets. + * @param lowered_stat status to use ie: lowered = true, raised = false + * @param widgets list of widgets ended by WIDGET_LIST_END + */ +void CDECL Window::SetWidgetsLoweredState(bool lowered_stat, int widgets, ...) +{ + va_list wdg_list; + + va_start(wdg_list, widgets); + + while (widgets != WIDGET_LIST_END) { + SetWidgetLoweredState(widgets, lowered_stat); + widgets = va_arg(wdg_list, int); + } + + va_end(wdg_list); +} + +/** + * Raise the buttons of the window. + * @param autoraise Raise only the push buttons of the window. + */ +void Window::RaiseButtons(bool autoraise) +{ + for (uint i = 0; i < this->nested_array_size; i++) { + if (this->nested_array[i] == NULL) continue; + WidgetType type = this->nested_array[i]->type; + if (((type & ~WWB_PUSHBUTTON) < WWT_LAST || type == NWID_PUSHBUTTON_DROPDOWN) && + (!autoraise || (type & WWB_PUSHBUTTON) || type == WWT_EDITBOX) && this->IsWidgetLowered(i)) { + this->RaiseWidget(i); + this->SetWidgetDirty(i); + } + } + + /* Special widgets without widget index */ + NWidgetCore *wid = this->nested_root != NULL ? (NWidgetCore*)this->nested_root->GetWidgetOfType(WWT_DEFSIZEBOX) : NULL; + if (wid != NULL) { + wid->SetLowered(false); + wid->SetDirty(this); + } +} + +/** + * Invalidate a widget, i.e. mark it as being changed and in need of redraw. + * @param widget_index the widget to redraw. + */ +void Window::SetWidgetDirty(byte widget_index) const +{ + /* Sometimes this function is called before the window is even fully initialized */ + if (this->nested_array == NULL) return; + + this->nested_array[widget_index]->SetDirty(this); +} + +/** + * A hotkey has been pressed. + * @param hotkey Hotkey index, by default a widget index of a button or editbox. + * @return #ES_HANDLED if the key press has been handled, and the hotkey is not unavailable for some reason. + */ +EventState Window::OnHotkey(int hotkey) +{ + if (hotkey < 0) return ES_NOT_HANDLED; + + NWidgetCore *nw = this->GetWidget(hotkey); + if (nw == NULL || nw->IsDisabled()) return ES_NOT_HANDLED; + + if (nw->type == WWT_EDITBOX) { + if (this->IsShaded()) return ES_NOT_HANDLED; + + /* Focus editbox */ + this->SetFocusedWidget(hotkey); + SetFocusedWindow(this); + } else { + /* Click button */ + this->OnClick(Point(), hotkey, 1); + } + return ES_HANDLED; +} + +/** + * Do all things to make a button look clicked and mark it to be + * unclicked in a few ticks. + * @param widget the widget to "click" + */ +void Window::HandleButtonClick(byte widget) +{ + this->LowerWidget(widget); + this->SetTimeout(); + this->SetWidgetDirty(widget); +} + +static void StartWindowDrag(Window *w); +static void StartWindowSizing(Window *w, bool to_left); + +/** + * Dispatch left mouse-button (possibly double) click in window. + * @param w Window to dispatch event in + * @param x X coordinate of the click + * @param y Y coordinate of the click + * @param click_count Number of fast consecutive clicks at same position + */ +static void DispatchLeftClickEvent(Window *w, int x, int y, int click_count) +{ + NWidgetCore *nw = w->nested_root->GetWidgetFromPos(x, y); + WidgetType widget_type = (nw != NULL) ? nw->type : WWT_EMPTY; + + bool focused_widget_changed = false; + /* If clicked on a window that previously did dot have focus */ + if (_focused_window != w && // We already have focus, right? + (w->window_desc->flags & WDF_NO_FOCUS) == 0 && // Don't lose focus to toolbars + widget_type != WWT_CLOSEBOX) { // Don't change focused window if 'X' (close button) was clicked + focused_widget_changed = true; + SetFocusedWindow(w); + } + + if (nw == NULL) return; // exit if clicked outside of widgets + + /* don't allow any interaction if the button has been disabled */ + if (nw->IsDisabled()) return; + + int widget_index = nw->index; ///< Index of the widget + + /* Clicked on a widget that is not disabled. + * So unless the clicked widget is the caption bar, change focus to this widget. + * Exception: In the OSK we always want the editbox to stay focussed. */ + if (widget_type != WWT_CAPTION && w->window_class != WC_OSK) { + /* focused_widget_changed is 'now' only true if the window this widget + * is in gained focus. In that case it must remain true, also if the + * local widget focus did not change. As such it's the logical-or of + * both changed states. + * + * If this is not preserved, then the OSK window would be opened when + * a user has the edit box focused and then click on another window and + * then back again on the edit box (to type some text). + */ + focused_widget_changed |= w->SetFocusedWidget(widget_index); + } + + /* Close any child drop down menus. If the button pressed was the drop down + * list's own button, then we should not process the click any further. */ + if (HideDropDownMenu(w) == widget_index && widget_index >= 0) return; + + if ((widget_type & ~WWB_PUSHBUTTON) < WWT_LAST && (widget_type & WWB_PUSHBUTTON)) w->HandleButtonClick(widget_index); + + Point pt = { x, y }; + + switch (widget_type) { + case NWID_VSCROLLBAR: + case NWID_HSCROLLBAR: + ScrollbarClickHandler(w, nw, x, y); + break; + + case WWT_EDITBOX: { + QueryString *query = w->GetQueryString(widget_index); + if (query != NULL) query->ClickEditBox(w, pt, widget_index, click_count, focused_widget_changed); + break; + } + + case WWT_CLOSEBOX: // 'X' + delete w; + return; + + case WWT_CAPTION: // 'Title bar' + StartWindowDrag(w); + return; + + case WWT_RESIZEBOX: + /* When the resize widget is on the left size of the window + * we assume that that button is used to resize to the left. */ + StartWindowSizing(w, (int)nw->pos_x < (w->width / 2)); + nw->SetDirty(w); + return; + + case WWT_DEFSIZEBOX: { + if (_ctrl_pressed) { + w->window_desc->pref_width = w->width; + w->window_desc->pref_height = w->height; + } else { + int16 def_width = max(min(w->window_desc->GetDefaultWidth(), _screen.width), w->nested_root->smallest_x); + int16 def_height = max(min(w->window_desc->GetDefaultHeight(), _screen.height - 50), w->nested_root->smallest_y); + + int dx = (w->resize.step_width == 0) ? 0 : def_width - w->width; + int dy = (w->resize.step_height == 0) ? 0 : def_height - w->height; + /* dx and dy has to go by step.. calculate it. + * The cast to int is necessary else dx/dy are implicitly casted to unsigned int, which won't work. */ + if (w->resize.step_width > 1) dx -= dx % (int)w->resize.step_width; + if (w->resize.step_height > 1) dy -= dy % (int)w->resize.step_height; + ResizeWindow(w, dx, dy, false); + } + + nw->SetLowered(true); + nw->SetDirty(w); + w->SetTimeout(); + break; + } + + case WWT_DEBUGBOX: + w->ShowNewGRFInspectWindow(); + break; + + case WWT_SHADEBOX: + nw->SetDirty(w); + w->SetShaded(!w->IsShaded()); + return; + + case WWT_STICKYBOX: + w->flags ^= WF_STICKY; + nw->SetDirty(w); + if (_ctrl_pressed) w->window_desc->pref_sticky = (w->flags & WF_STICKY) != 0; + return; + + default: + break; + } + + /* Widget has no index, so the window is not interested in it. */ + if (widget_index < 0) return; + + /* Check if the widget is highlighted; if so, disable highlight and dispatch an event to the GameScript */ + if (w->IsWidgetHighlighted(widget_index)) { + w->SetWidgetHighlight(widget_index, TC_INVALID); + Game::NewEvent(new ScriptEventWindowWidgetClick((ScriptWindow::WindowClass)w->window_class, w->window_number, widget_index)); + } + + w->OnClick(pt, widget_index, click_count); +} + +/** + * Dispatch right mouse-button click in window. + * @param w Window to dispatch event in + * @param x X coordinate of the click + * @param y Y coordinate of the click + */ +static void DispatchRightClickEvent(Window *w, int x, int y) +{ + NWidgetCore *wid = w->nested_root->GetWidgetFromPos(x, y); + if (wid == NULL) return; + + /* No widget to handle, or the window is not interested in it. */ + if (wid->index >= 0) { + Point pt = { x, y }; + if (w->OnRightClick(pt, wid->index)) return; + } + + if (_settings_client.gui.hover_delay == 0 && wid->tool_tip != 0) GuiShowTooltips(w, wid->tool_tip, 0, NULL, TCC_RIGHT_CLICK); +} + +/** + * Dispatch hover of the mouse over a window. + * @param w Window to dispatch event in. + * @param x X coordinate of the click. + * @param y Y coordinate of the click. + */ +static void DispatchHoverEvent(Window *w, int x, int y) +{ + NWidgetCore *wid = w->nested_root->GetWidgetFromPos(x, y); + + /* No widget to handle */ + if (wid == NULL) return; + + /* Show the tooltip if there is any */ + if (wid->tool_tip != 0) { + GuiShowTooltips(w, wid->tool_tip); + return; + } + + /* Widget has no index, so the window is not interested in it. */ + if (wid->index < 0) return; + + Point pt = { x, y }; + w->OnHover(pt, wid->index); +} + +/** + * Dispatch the mousewheel-action to the window. + * The window will scroll any compatible scrollbars if the mouse is pointed over the bar or its contents + * @param w Window + * @param nwid the widget where the scrollwheel was used + * @param wheel scroll up or down + */ +static void DispatchMouseWheelEvent(Window *w, NWidgetCore *nwid, int wheel) +{ + if (nwid == NULL) return; + + /* Using wheel on caption/shade-box shades or unshades the window. */ + if (nwid->type == WWT_CAPTION || nwid->type == WWT_SHADEBOX) { + w->SetShaded(wheel < 0); + return; + } + + /* Wheeling a vertical scrollbar. */ + if (nwid->type == NWID_VSCROLLBAR) { + NWidgetScrollbar *sb = static_cast(nwid); + if (sb->GetCount() > sb->GetCapacity()) { + sb->UpdatePosition(wheel); + w->SetDirty(); + } + return; + } + + /* Scroll the widget attached to the scrollbar. */ + Scrollbar *sb = (nwid->scrollbar_index >= 0 ? w->GetScrollbar(nwid->scrollbar_index) : NULL); + if (sb != NULL && sb->GetCount() > sb->GetCapacity()) { + sb->UpdatePosition(wheel); + w->SetDirty(); + } +} + +/** + * Returns whether a window may be shown or not. + * @param w The window to consider. + * @return True iff it may be shown, otherwise false. + */ +static bool MayBeShown(const Window *w) +{ + /* If we're not modal, everything is okay. */ + if (!HasModalProgress()) return true; + + switch (w->window_class) { + case WC_MAIN_WINDOW: ///< The background, i.e. the game. + case WC_MODAL_PROGRESS: ///< The actual progress window. + case WC_CONFIRM_POPUP_QUERY: ///< The abort window. + return true; + + default: + return false; + } +} + +/** + * Generate repaint events for the visible part of window w within the rectangle. + * + * The function goes recursively upwards in the window stack, and splits the rectangle + * into multiple pieces at the window edges, so obscured parts are not redrawn. + * + * @param w Window that needs to be repainted + * @param left Left edge of the rectangle that should be repainted + * @param top Top edge of the rectangle that should be repainted + * @param right Right edge of the rectangle that should be repainted + * @param bottom Bottom edge of the rectangle that should be repainted + */ +static void DrawOverlappedWindow(Window *w, int left, int top, int right, int bottom) +{ + const Window *v; + FOR_ALL_WINDOWS_FROM_BACK_FROM(v, w->z_front) { + if (MayBeShown(v) && + right > v->left && + bottom > v->top && + left < v->left + v->width && + top < v->top + v->height) { + /* v and rectangle intersect with each other */ + int x; + + if (left < (x = v->left)) { + DrawOverlappedWindow(w, left, top, x, bottom); + DrawOverlappedWindow(w, x, top, right, bottom); + return; + } + + if (right > (x = v->left + v->width)) { + DrawOverlappedWindow(w, left, top, x, bottom); + DrawOverlappedWindow(w, x, top, right, bottom); + return; + } + + if (top < (x = v->top)) { + DrawOverlappedWindow(w, left, top, right, x); + DrawOverlappedWindow(w, left, x, right, bottom); + return; + } + + if (bottom > (x = v->top + v->height)) { + DrawOverlappedWindow(w, left, top, right, x); + DrawOverlappedWindow(w, left, x, right, bottom); + return; + } + + return; + } + } + + /* Setup blitter, and dispatch a repaint event to window *wz */ + DrawPixelInfo *dp = _cur_dpi; + dp->width = right - left; + dp->height = bottom - top; + dp->left = left - w->left; + dp->top = top - w->top; + dp->pitch = _screen.pitch; + dp->dst_ptr = BlitterFactory::GetCurrentBlitter()->MoveTo(_screen.dst_ptr, left, top); + dp->zoom = ZOOM_LVL_NORMAL; + w->OnPaint(); +} + +/** + * From a rectangle that needs redrawing, find the windows that intersect with the rectangle. + * These windows should be re-painted. + * @param left Left edge of the rectangle that should be repainted + * @param top Top edge of the rectangle that should be repainted + * @param right Right edge of the rectangle that should be repainted + * @param bottom Bottom edge of the rectangle that should be repainted + */ +void DrawOverlappedWindowForAll(int left, int top, int right, int bottom) +{ + Window *w; + DrawPixelInfo bk; + _cur_dpi = &bk; + + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (MayBeShown(w) && + right > w->left && + bottom > w->top && + left < w->left + w->width && + top < w->top + w->height) { + /* Window w intersects with the rectangle => needs repaint */ + DrawOverlappedWindow(w, left, top, right, bottom); + } + } +} + +/** + * Mark entire window as dirty (in need of re-paint) + * @ingroup dirty + */ +void Window::SetDirty() const +{ + SetDirtyBlocks(this->left, this->top, this->left + this->width, this->top + this->height); +} + +/** + * Re-initialize a window, and optionally change its size. + * @param rx Horizontal resize of the window. + * @param ry Vertical resize of the window. + * @note For just resizing the window, use #ResizeWindow instead. + */ +void Window::ReInit(int rx, int ry) +{ + this->SetDirty(); // Mark whole current window as dirty. + + /* Save current size. */ + int window_width = this->width; + int window_height = this->height; + + this->OnInit(); + /* Re-initialize the window from the ground up. No need to change the nested_array, as all widgets stay where they are. */ + this->nested_root->SetupSmallestSize(this, false); + this->nested_root->AssignSizePosition(ST_SMALLEST, 0, 0, this->nested_root->smallest_x, this->nested_root->smallest_y, _current_text_dir == TD_RTL); + this->width = this->nested_root->smallest_x; + this->height = this->nested_root->smallest_y; + this->resize.step_width = this->nested_root->resize_x; + this->resize.step_height = this->nested_root->resize_y; + + /* Resize as close to the original size + requested resize as possible. */ + window_width = max(window_width + rx, this->width); + window_height = max(window_height + ry, this->height); + int dx = (this->resize.step_width == 0) ? 0 : window_width - this->width; + int dy = (this->resize.step_height == 0) ? 0 : window_height - this->height; + /* dx and dy has to go by step.. calculate it. + * The cast to int is necessary else dx/dy are implicitly casted to unsigned int, which won't work. */ + if (this->resize.step_width > 1) dx -= dx % (int)this->resize.step_width; + if (this->resize.step_height > 1) dy -= dy % (int)this->resize.step_height; + + ResizeWindow(this, dx, dy); + /* ResizeWindow() does this->SetDirty() already, no need to do it again here. */ +} + +/** + * Set the shaded state of the window to \a make_shaded. + * @param make_shaded If \c true, shade the window (roll up until just the title bar is visible), else unshade/unroll the window to its original size. + * @note The method uses #Window::ReInit(), thus after the call, the whole window should be considered changed. + */ +void Window::SetShaded(bool make_shaded) +{ + if (this->shade_select == NULL) return; + + int desired = make_shaded ? SZSP_HORIZONTAL : 0; + if (this->shade_select->shown_plane != desired) { + if (make_shaded) { + if (this->nested_focus != NULL) this->UnfocusFocusedWidget(); + this->unshaded_size.width = this->width; + this->unshaded_size.height = this->height; + this->shade_select->SetDisplayedPlane(desired); + this->ReInit(0, -this->height); + } else { + this->shade_select->SetDisplayedPlane(desired); + int dx = ((int)this->unshaded_size.width > this->width) ? (int)this->unshaded_size.width - this->width : 0; + int dy = ((int)this->unshaded_size.height > this->height) ? (int)this->unshaded_size.height - this->height : 0; + this->ReInit(dx, dy); + } + } +} + +/** + * Find the Window whose parent pointer points to this window + * @param w parent Window to find child of + * @param wc Window class of the window to remove; #WC_INVALID if class does not matter + * @return a Window pointer that is the child of \a w, or \c NULL otherwise + */ +static Window *FindChildWindow(const Window *w, WindowClass wc) +{ + Window *v; + FOR_ALL_WINDOWS_FROM_BACK(v) { + if ((wc == WC_INVALID || wc == v->window_class) && v->parent == w) return v; + } + + return NULL; +} + +/** + * Delete all children a window might have in a head-recursive manner + * @param wc Window class of the window to remove; #WC_INVALID if class does not matter + */ +void Window::DeleteChildWindows(WindowClass wc) const +{ + Window *child = FindChildWindow(this, wc); + while (child != NULL) { + delete child; + child = FindChildWindow(this, wc); + } +} + +/** + * Remove window and all its child windows from the window stack. + */ +Window::~Window() +{ + if (_thd.window_class == this->window_class && + _thd.window_number == this->window_number) { + ResetObjectToPlace(); + } + + /* Prevent Mouseover() from resetting mouse-over coordinates on a non-existing window */ + if (_mouseover_last_w == this) _mouseover_last_w = NULL; + + /* We can't scroll the window when it's closed. */ + if (_last_scroll_window == this) _last_scroll_window = NULL; + + /* Make sure we don't try to access this window as the focused window when it doesn't exist anymore. */ + if (_focused_window == this) { + this->OnFocusLost(); + _focused_window = NULL; + } + + this->DeleteChildWindows(); + + if (this->viewport != NULL) DeleteWindowViewport(this); + + this->SetDirty(); + + free(this->nested_array); // Contents is released through deletion of #nested_root. + delete this->nested_root; + + this->window_class = WC_INVALID; +} + +/** + * Find a window by its class and window number + * @param cls Window class + * @param number Number of the window within the window class + * @return Pointer to the found window, or \c NULL if not available + */ +Window *FindWindowById(WindowClass cls, WindowNumber number) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls && w->window_number == number) return w; + } + + return NULL; +} + +/** + * Find any window by its class. Useful when searching for a window that uses + * the window number as a #WindowType, like #WC_SEND_NETWORK_MSG. + * @param cls Window class + * @return Pointer to the found window, or \c NULL if not available + */ +Window *FindWindowByClass(WindowClass cls) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls) return w; + } + + return NULL; +} + +/** + * Delete a window by its class and window number (if it is open). + * @param cls Window class + * @param number Number of the window within the window class + * @param force force deletion; if false don't delete when stickied + */ +void DeleteWindowById(WindowClass cls, WindowNumber number, bool force) +{ + Window *w = FindWindowById(cls, number); + if (force || w == NULL || + (w->flags & WF_STICKY) == 0) { + delete w; + } +} + +/** + * Delete all windows of a given class + * @param cls Window class of windows to delete + */ +void DeleteWindowByClass(WindowClass cls) +{ + Window *w; + +restart_search: + /* When we find the window to delete, we need to restart the search + * as deleting this window could cascade in deleting (many) others + * anywhere in the z-array */ + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls) { + delete w; + goto restart_search; + } + } +} + +/** + * Delete all windows of a company. We identify windows of a company + * by looking at the caption colour. If it is equal to the company ID + * then we say the window belongs to the company and should be deleted + * @param id company identifier + */ +void DeleteCompanyWindows(CompanyID id) +{ + Window *w; + +restart_search: + /* When we find the window to delete, we need to restart the search + * as deleting this window could cascade in deleting (many) others + * anywhere in the z-array */ + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->owner == id) { + delete w; + goto restart_search; + } + } + + /* Also delete the company specific windows that don't have a company-colour. */ + DeleteWindowById(WC_BUY_COMPANY, id); +} + +/** + * Change the owner of all the windows one company can take over from another + * company in the case of a company merger. Do not change ownership of windows + * that need to be deleted once takeover is complete + * @param old_owner original owner of the window + * @param new_owner the new owner of the window + */ +void ChangeWindowOwner(Owner old_owner, Owner new_owner) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->owner != old_owner) continue; + + switch (w->window_class) { + case WC_COMPANY_COLOUR: + case WC_FINANCES: + case WC_STATION_LIST: + case WC_TRAINS_LIST: + case WC_ROADVEH_LIST: + case WC_SHIPS_LIST: + case WC_AIRCRAFT_LIST: + case WC_BUY_COMPANY: + case WC_COMPANY: + case WC_COMPANY_INFRASTRUCTURE: + continue; + + default: + w->owner = new_owner; + break; + } + } +} + +static void BringWindowToFront(Window *w); + +/** + * Find a window and make it the relative top-window on the screen. + * The window gets unshaded if it was shaded, and a white border is drawn at its edges for a brief period of time to visualize its "activation". + * @param cls WindowClass of the window to activate + * @param number WindowNumber of the window to activate + * @return a pointer to the window thus activated + */ +Window *BringWindowToFrontById(WindowClass cls, WindowNumber number) +{ + Window *w = FindWindowById(cls, number); + + if (w != NULL) { + if (w->IsShaded()) w->SetShaded(false); // Restore original window size if it was shaded. + + w->SetWhiteBorder(); + BringWindowToFront(w); + w->SetDirty(); + } + + return w; +} + +static inline bool IsVitalWindow(const Window *w) +{ + switch (w->window_class) { + case WC_MAIN_TOOLBAR: + case WC_STATUS_BAR: + case WC_NEWS_WINDOW: + case WC_SEND_NETWORK_MSG: + return true; + + default: + return false; + } +} + +/** + * Get the z-priority for a given window. This is used in comparison with other z-priority values; + * a window with a given z-priority will appear above other windows with a lower value, and below + * those with a higher one (the ordering within z-priorities is arbitrary). + * @param w The window to get the z-priority for + * @pre w->window_class != WC_INVALID + * @return The window's z-priority + */ +static uint GetWindowZPriority(const Window *w) +{ + assert(w->window_class != WC_INVALID); + + uint z_priority = 0; + + switch (w->window_class) { + case WC_ENDSCREEN: + ++z_priority; + + case WC_HIGHSCORE: + ++z_priority; + + case WC_TOOLTIPS: + ++z_priority; + + case WC_DROPDOWN_MENU: + ++z_priority; + + case WC_MAIN_TOOLBAR: + case WC_STATUS_BAR: + ++z_priority; + + case WC_OSK: + ++z_priority; + + case WC_QUERY_STRING: + case WC_SEND_NETWORK_MSG: + ++z_priority; + + case WC_ERRMSG: + case WC_CONFIRM_POPUP_QUERY: + case WC_MODAL_PROGRESS: + case WC_NETWORK_STATUS_WINDOW: + ++z_priority; + + case WC_GENERATE_LANDSCAPE: + case WC_SAVELOAD: + case WC_GAME_OPTIONS: + case WC_CUSTOM_CURRENCY: + case WC_NETWORK_WINDOW: + case WC_GRF_PARAMETERS: + case WC_AI_LIST: + case WC_AI_SETTINGS: + case WC_TEXTFILE: + ++z_priority; + + case WC_CONSOLE: + ++z_priority; + + case WC_NEWS_WINDOW: + ++z_priority; + + default: + ++z_priority; + + case WC_MAIN_WINDOW: + return z_priority; + } +} + +/** + * Adds a window to the z-ordering, according to its z-priority. + * @param w Window to add + */ +static void AddWindowToZOrdering(Window *w) +{ + assert(w->z_front == NULL && w->z_back == NULL); + + if (_z_front_window == NULL) { + /* It's the only window. */ + _z_front_window = _z_back_window = w; + w->z_front = w->z_back = NULL; + } else { + /* Search down the z-ordering for its location. */ + Window *v = _z_front_window; + uint last_z_priority = UINT_MAX; + while (v != NULL && (v->window_class == WC_INVALID || GetWindowZPriority(v) > GetWindowZPriority(w))) { + if (v->window_class != WC_INVALID) { + /* Sanity check z-ordering, while we're at it. */ + assert(last_z_priority >= GetWindowZPriority(v)); + last_z_priority = GetWindowZPriority(v); + } + + v = v->z_back; + } + + if (v == NULL) { + /* It's the new back window. */ + w->z_front = _z_back_window; + w->z_back = NULL; + _z_back_window->z_back = w; + _z_back_window = w; + } else if (v == _z_front_window) { + /* It's the new front window. */ + w->z_front = NULL; + w->z_back = _z_front_window; + _z_front_window->z_front = w; + _z_front_window = w; + } else { + /* It's somewhere else in the z-ordering. */ + w->z_front = v->z_front; + w->z_back = v; + v->z_front->z_back = w; + v->z_front = w; + } + } +} + + +/** + * Removes a window from the z-ordering. + * @param w Window to remove + */ +static void RemoveWindowFromZOrdering(Window *w) +{ + if (w->z_front == NULL) { + assert(_z_front_window == w); + _z_front_window = w->z_back; + } else { + w->z_front->z_back = w->z_back; + } + + if (w->z_back == NULL) { + assert(_z_back_window == w); + _z_back_window = w->z_front; + } else { + w->z_back->z_front = w->z_front; + } + + w->z_front = w->z_back = NULL; +} + +/** + * On clicking on a window, make it the frontmost window of all windows with an equal + * or lower z-priority. The window is marked dirty for a repaint + * @param w window that is put into the relative foreground + */ +static void BringWindowToFront(Window *w) +{ + RemoveWindowFromZOrdering(w); + AddWindowToZOrdering(w); + + w->SetDirty(); +} + +/** + * Initializes the data (except the position and initial size) of a new Window. + * @param desc Window description. + * @param window_number Number being assigned to the new window + * @return Window pointer of the newly created window + * @pre If nested widgets are used (\a widget is \c NULL), #nested_root and #nested_array_size must be initialized. + * In addition, #nested_array is either \c NULL, or already initialized. + */ +void Window::InitializeData(WindowNumber window_number) +{ + /* Set up window properties; some of them are needed to set up smallest size below */ + this->window_class = this->window_desc->cls; + this->SetWhiteBorder(); + if (this->window_desc->default_pos == WDP_CENTER) this->flags |= WF_CENTERED; + this->owner = INVALID_OWNER; + this->nested_focus = NULL; + this->window_number = window_number; + + this->OnInit(); + /* Initialize nested widget tree. */ + if (this->nested_array == NULL) { + this->nested_array = CallocT(this->nested_array_size); + this->nested_root->SetupSmallestSize(this, true); + } else { + this->nested_root->SetupSmallestSize(this, false); + } + /* Initialize to smallest size. */ + this->nested_root->AssignSizePosition(ST_SMALLEST, 0, 0, this->nested_root->smallest_x, this->nested_root->smallest_y, _current_text_dir == TD_RTL); + + /* Further set up window properties, + * this->left, this->top, this->width, this->height, this->resize.width, and this->resize.height are initialized later. */ + this->resize.step_width = this->nested_root->resize_x; + this->resize.step_height = this->nested_root->resize_y; + + /* Give focus to the opened window unless a text box + * of focused window has focus (so we don't interrupt typing). But if the new + * window has a text box, then take focus anyway. */ + if (!EditBoxInGlobalFocus() || this->nested_root->GetWidgetOfType(WWT_EDITBOX) != NULL) SetFocusedWindow(this); + + /* Insert the window into the correct location in the z-ordering. */ + AddWindowToZOrdering(this); +} + +/** + * Set the position and smallest size of the window. + * @param x Offset in pixels from the left of the screen of the new window. + * @param y Offset in pixels from the top of the screen of the new window. + * @param sm_width Smallest width in pixels of the window. + * @param sm_height Smallest height in pixels of the window. + */ +void Window::InitializePositionSize(int x, int y, int sm_width, int sm_height) +{ + this->left = x; + this->top = y; + this->width = sm_width; + this->height = sm_height; +} + +/** + * Resize window towards the default size. + * Prior to construction, a position for the new window (for its default size) + * has been found with LocalGetWindowPlacement(). Initially, the window is + * constructed with minimal size. Resizing the window to its default size is + * done here. + * @param def_width default width in pixels of the window + * @param def_height default height in pixels of the window + * @see Window::Window(), Window::InitializeData(), Window::InitializePositionSize() + */ +void Window::FindWindowPlacementAndResize(int def_width, int def_height) +{ + def_width = max(def_width, this->width); // Don't allow default size to be smaller than smallest size + def_height = max(def_height, this->height); + /* Try to make windows smaller when our window is too small. + * w->(width|height) is normally the same as min_(width|height), + * but this way the GUIs can be made a little more dynamic; + * one can use the same spec for multiple windows and those + * can then determine the real minimum size of the window. */ + if (this->width != def_width || this->height != def_height) { + /* Think about the overlapping toolbars when determining the minimum window size */ + int free_height = _screen.height; + const Window *wt = FindWindowById(WC_STATUS_BAR, 0); + if (wt != NULL) free_height -= wt->height; + wt = FindWindowById(WC_MAIN_TOOLBAR, 0); + if (wt != NULL) free_height -= wt->height; + + int enlarge_x = max(min(def_width - this->width, _screen.width - this->width), 0); + int enlarge_y = max(min(def_height - this->height, free_height - this->height), 0); + + /* X and Y has to go by step.. calculate it. + * The cast to int is necessary else x/y are implicitly casted to + * unsigned int, which won't work. */ + if (this->resize.step_width > 1) enlarge_x -= enlarge_x % (int)this->resize.step_width; + if (this->resize.step_height > 1) enlarge_y -= enlarge_y % (int)this->resize.step_height; + + ResizeWindow(this, enlarge_x, enlarge_y); + /* ResizeWindow() calls this->OnResize(). */ + } else { + /* Always call OnResize; that way the scrollbars and matrices get initialized. */ + this->OnResize(); + } + + int nx = this->left; + int ny = this->top; + + if (nx + this->width > _screen.width) nx -= (nx + this->width - _screen.width); + + const Window *wt = FindWindowById(WC_MAIN_TOOLBAR, 0); + ny = max(ny, (wt == NULL || this == wt || this->top == 0) ? 0 : wt->height); + nx = max(nx, 0); + + if (this->viewport != NULL) { + this->viewport->left += nx - this->left; + this->viewport->top += ny - this->top; + } + this->left = nx; + this->top = ny; + + this->SetDirty(); +} + +/** + * Decide whether a given rectangle is a good place to open a completely visible new window. + * The new window should be within screen borders, and not overlap with another already + * existing window (except for the main window in the background). + * @param left Left edge of the rectangle + * @param top Top edge of the rectangle + * @param width Width of the rectangle + * @param height Height of the rectangle + * @param pos If rectangle is good, use this parameter to return the top-left corner of the new window + * @return Boolean indication that the rectangle is a good place for the new window + */ +static bool IsGoodAutoPlace1(int left, int top, int width, int height, Point &pos) +{ + int right = width + left; + int bottom = height + top; + + const Window *main_toolbar = FindWindowByClass(WC_MAIN_TOOLBAR); + if (left < 0 || (main_toolbar != NULL && top < main_toolbar->height) || right > _screen.width || bottom > _screen.height) return false; + + /* Make sure it is not obscured by any window. */ + const Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == WC_MAIN_WINDOW) continue; + + if (right > w->left && + w->left + w->width > left && + bottom > w->top && + w->top + w->height > top) { + return false; + } + } + + pos.x = left; + pos.y = top; + return true; +} + +/** + * Decide whether a given rectangle is a good place to open a mostly visible new window. + * The new window should be mostly within screen borders, and not overlap with another already + * existing window (except for the main window in the background). + * @param left Left edge of the rectangle + * @param top Top edge of the rectangle + * @param width Width of the rectangle + * @param height Height of the rectangle + * @param pos If rectangle is good, use this parameter to return the top-left corner of the new window + * @return Boolean indication that the rectangle is a good place for the new window + */ +static bool IsGoodAutoPlace2(int left, int top, int width, int height, Point &pos) +{ + /* Left part of the rectangle may be at most 1/4 off-screen, + * right part of the rectangle may be at most 1/2 off-screen + */ + if (left < -(width >> 2) || left > _screen.width - (width >> 1)) return false; + /* Bottom part of the rectangle may be at most 1/4 off-screen */ + if (top < 22 || top > _screen.height - (height >> 2)) return false; + + /* Make sure it is not obscured by any window. */ + const Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == WC_MAIN_WINDOW) continue; + + if (left + width > w->left && + w->left + w->width > left && + top + height > w->top && + w->top + w->height > top) { + return false; + } + } + + pos.x = left; + pos.y = top; + return true; +} + +/** + * Find a good place for opening a new window of a given width and height. + * @param width Width of the new window + * @param height Height of the new window + * @return Top-left coordinate of the new window + */ +static Point GetAutoPlacePosition(int width, int height) +{ + Point pt; + + /* First attempt, try top-left of the screen */ + const Window *main_toolbar = FindWindowByClass(WC_MAIN_TOOLBAR); + if (IsGoodAutoPlace1(0, main_toolbar != NULL ? main_toolbar->height + 2 : 2, width, height, pt)) return pt; + + /* Second attempt, try around all existing windows with a distance of 2 pixels. + * The new window must be entirely on-screen, and not overlap with an existing window. + * Eight starting points are tried, two at each corner. + */ + const Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == WC_MAIN_WINDOW) continue; + + if (IsGoodAutoPlace1(w->left + w->width + 2, w->top, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left - width - 2, w->top, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left, w->top + w->height + 2, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left, w->top - height - 2, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left + w->width + 2, w->top + w->height - height, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left - width - 2, w->top + w->height - height, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left + w->width - width, w->top + w->height + 2, width, height, pt)) return pt; + if (IsGoodAutoPlace1(w->left + w->width - width, w->top - height - 2, width, height, pt)) return pt; + } + + /* Third attempt, try around all existing windows with a distance of 2 pixels. + * The new window may be partly off-screen, and must not overlap with an existing window. + * Only four starting points are tried. + */ + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == WC_MAIN_WINDOW) continue; + + if (IsGoodAutoPlace2(w->left + w->width + 2, w->top, width, height, pt)) return pt; + if (IsGoodAutoPlace2(w->left - width - 2, w->top, width, height, pt)) return pt; + if (IsGoodAutoPlace2(w->left, w->top + w->height + 2, width, height, pt)) return pt; + if (IsGoodAutoPlace2(w->left, w->top - height - 2, width, height, pt)) return pt; + } + + /* Fourth and final attempt, put window at diagonal starting from (0, 24), try multiples + * of (+5, +5) + */ + int left = 0, top = 24; + +restart: + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->left == left && w->top == top) { + left += 5; + top += 5; + goto restart; + } + } + + pt.x = left; + pt.y = top; + return pt; +} + +/** + * Computer the position of the top-left corner of a window to be opened right + * under the toolbar. + * @param window_width the width of the window to get the position for + * @return Coordinate of the top-left corner of the new window. + */ +Point GetToolbarAlignedWindowPosition(int window_width) +{ + const Window *w = FindWindowById(WC_MAIN_TOOLBAR, 0); + assert(w != NULL); + Point pt = { _current_text_dir == TD_RTL ? w->left : (w->left + w->width) - window_width, w->top + w->height }; + return pt; +} + +/** + * Compute the position of the top-left corner of a new window that is opened. + * + * By default position a child window at an offset of 10/10 of its parent. + * With the exception of WC_BUILD_TOOLBAR (build railway/roads/ship docks/airports) + * and WC_SCEN_LAND_GEN (landscaping). Whose child window has an offset of 0/toolbar-height of + * its parent. So it's exactly under the parent toolbar and no buttons will be covered. + * However if it falls too extremely outside window positions, reposition + * it to an automatic place. + * + * @param *desc The pointer to the WindowDesc to be created. + * @param sm_width Smallest width of the window. + * @param sm_height Smallest height of the window. + * @param window_number The window number of the new window. + * + * @return Coordinate of the top-left corner of the new window. + */ +static Point LocalGetWindowPlacement(const WindowDesc *desc, int16 sm_width, int16 sm_height, int window_number) +{ + Point pt; + const Window *w; + + int16 default_width = max(desc->GetDefaultWidth(), sm_width); + int16 default_height = max(desc->GetDefaultHeight(), sm_height); + + if (desc->parent_cls != 0 /* WC_MAIN_WINDOW */ && + (w = FindWindowById(desc->parent_cls, window_number)) != NULL && + w->left < _screen.width - 20 && w->left > -60 && w->top < _screen.height - 20) { + + pt.x = w->left + ((desc->parent_cls == WC_BUILD_TOOLBAR || desc->parent_cls == WC_SCEN_LAND_GEN) ? 0 : 10); + if (pt.x > _screen.width + 10 - default_width) { + pt.x = (_screen.width + 10 - default_width) - 20; + } + pt.y = w->top + ((desc->parent_cls == WC_BUILD_TOOLBAR || desc->parent_cls == WC_SCEN_LAND_GEN) ? w->height : 10); + return pt; + } + + switch (desc->default_pos) { + case WDP_ALIGN_TOOLBAR: // Align to the toolbar + return GetToolbarAlignedWindowPosition(default_width); + + case WDP_AUTO: // Find a good automatic position for the window + return GetAutoPlacePosition(default_width, default_height); + + case WDP_CENTER: // Centre the window horizontally + pt.x = (_screen.width - default_width) / 2; + pt.y = (_screen.height - default_height) / 2; + break; + + case WDP_MANUAL: + pt.x = 0; + pt.y = 0; + break; + + default: + NOT_REACHED(); + } + + return pt; +} + +/* virtual */ Point Window::OnInitialPosition(int16 sm_width, int16 sm_height, int window_number) +{ + return LocalGetWindowPlacement(this->window_desc, sm_width, sm_height, window_number); +} + +/** + * Perform the first part of the initialization of a nested widget tree. + * Construct a nested widget tree in #nested_root, and optionally fill the #nested_array array to provide quick access to the uninitialized widgets. + * This is mainly useful for setting very basic properties. + * @param fill_nested Fill the #nested_array (enabling is expensive!). + * @note Filling the nested array requires an additional traversal through the nested widget tree, and is best performed by #FinishInitNested rather than here. + */ +void Window::CreateNestedTree(bool fill_nested) +{ + int biggest_index = -1; + this->nested_root = MakeWindowNWidgetTree(this->window_desc->nwid_parts, this->window_desc->nwid_length, &biggest_index, &this->shade_select); + this->nested_array_size = (uint)(biggest_index + 1); + + if (fill_nested) { + this->nested_array = CallocT(this->nested_array_size); + this->nested_root->FillNestedArray(this->nested_array, this->nested_array_size); + } +} + +/** + * Perform the second part of the initialization of a nested widget tree. + * @param window_number Number of the new window. + */ +void Window::FinishInitNested(WindowNumber window_number) +{ + this->InitializeData(window_number); + this->ApplyDefaults(); + Point pt = this->OnInitialPosition(this->nested_root->smallest_x, this->nested_root->smallest_y, window_number); + this->InitializePositionSize(pt.x, pt.y, this->nested_root->smallest_x, this->nested_root->smallest_y); + this->FindWindowPlacementAndResize(this->window_desc->GetDefaultWidth(), this->window_desc->GetDefaultHeight()); +} + +/** + * Perform complete initialization of the #Window with nested widgets, to allow use. + * @param window_number Number of the new window. + */ +void Window::InitNested(WindowNumber window_number) +{ + this->CreateNestedTree(false); + this->FinishInitNested(window_number); +} + +/** + * Empty constructor, initialization has been moved to #InitNested() called from the constructor of the derived class. + * @param desc The description of the window. + */ +Window::Window(WindowDesc *desc) : window_desc(desc), scrolling_scrollbar(-1) +{ +} + +/** + * Do a search for a window at specific coordinates. For this we start + * at the topmost window, obviously and work our way down to the bottom + * @param x position x to query + * @param y position y to query + * @return a pointer to the found window if any, NULL otherwise + */ +Window *FindWindowFromPt(int x, int y) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if (MayBeShown(w) && IsInsideBS(x, w->left, w->width) && IsInsideBS(y, w->top, w->height)) { + return w; + } + } + + return NULL; +} + +/** + * (re)initialize the windowing system + */ +void InitWindowSystem() +{ + IConsoleClose(); + + _z_back_window = NULL; + _z_front_window = NULL; + _focused_window = NULL; + _mouseover_last_w = NULL; + _last_scroll_window = NULL; + _scrolling_viewport = false; + _mouse_hovering = false; + + NWidgetLeaf::InvalidateDimensionCache(); // Reset cached sizes of several widgets. + NWidgetScrollbar::InvalidateDimensionCache(); + + ShowFirstError(); +} + +/** + * Close down the windowing system + */ +void UnInitWindowSystem() +{ + UnshowCriticalError(); + + Window *w; + FOR_ALL_WINDOWS_FROM_FRONT(w) delete w; + + for (w = _z_front_window; w != NULL; /* nothing */) { + Window *to_del = w; + w = w->z_back; + free(to_del); + } + + _z_front_window = NULL; + _z_back_window = NULL; +} + +/** + * Reset the windowing system, by means of shutting it down followed by re-initialization + */ +void ResetWindowSystem() +{ + UnInitWindowSystem(); + InitWindowSystem(); + _thd.Reset(); +} + +static void DecreaseWindowCounters() +{ + Window *w; + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if (_scroller_click_timeout == 0) { + /* Unclick scrollbar buttons if they are pressed. */ + for (uint i = 0; i < w->nested_array_size; i++) { + NWidgetBase *nwid = w->nested_array[i]; + if (nwid != NULL && (nwid->type == NWID_HSCROLLBAR || nwid->type == NWID_VSCROLLBAR)) { + NWidgetScrollbar *sb = static_cast(nwid); + if (sb->disp_flags & (ND_SCROLLBAR_UP | ND_SCROLLBAR_DOWN)) { + sb->disp_flags &= ~(ND_SCROLLBAR_UP | ND_SCROLLBAR_DOWN); + w->scrolling_scrollbar = -1; + sb->SetDirty(w); + } + } + } + } + + /* Handle editboxes */ + for (SmallMap::Pair *it = w->querystrings.Begin(); it != w->querystrings.End(); ++it) { + it->second->HandleEditBox(w, it->first); + } + + w->OnMouseLoop(); + } + + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if ((w->flags & WF_TIMEOUT) && --w->timeout_timer == 0) { + CLRBITS(w->flags, WF_TIMEOUT); + + w->OnTimeout(); + w->RaiseButtons(true); + } + } +} + +static void HandlePlacePresize() +{ + if (_special_mouse_mode != WSM_PRESIZE) return; + + Window *w = _thd.GetCallbackWnd(); + if (w == NULL) return; + + Point pt = GetTileBelowCursor(); + if (pt.x == -1) { + _thd.selend.x = -1; + return; + } + + w->OnPlacePresize(pt, TileVirtXY(pt.x, pt.y)); +} + +/** + * Handle dragging and dropping in mouse dragging mode (#WSM_DRAGDROP). + * @return State of handling the event. + */ +static EventState HandleMouseDragDrop() +{ + if (_special_mouse_mode != WSM_DRAGDROP) return ES_NOT_HANDLED; + + if (_left_button_down && _cursor.delta.x == 0 && _cursor.delta.y == 0) return ES_HANDLED; // Dragging, but the mouse did not move. + + Window *w = _thd.GetCallbackWnd(); + if (w != NULL) { + /* Send an event in client coordinates. */ + Point pt; + pt.x = _cursor.pos.x - w->left; + pt.y = _cursor.pos.y - w->top; + if (_left_button_down) { + w->OnMouseDrag(pt, GetWidgetFromPos(w, pt.x, pt.y)); + } else { + w->OnDragDrop(pt, GetWidgetFromPos(w, pt.x, pt.y)); + } + } + + if (!_left_button_down) ResetObjectToPlace(); // Button released, finished dragging. + return ES_HANDLED; +} + +/** Report position of the mouse to the underlying window. */ +static void HandleMouseOver() +{ + Window *w = FindWindowFromPt(_cursor.pos.x, _cursor.pos.y); + + /* We changed window, put a MOUSEOVER event to the last window */ + if (_mouseover_last_w != NULL && _mouseover_last_w != w) { + /* Reset mouse-over coordinates of previous window */ + Point pt = { -1, -1 }; + _mouseover_last_w->OnMouseOver(pt, 0); + } + + /* _mouseover_last_w will get reset when the window is deleted, see DeleteWindow() */ + _mouseover_last_w = w; + + if (w != NULL) { + /* send an event in client coordinates. */ + Point pt = { _cursor.pos.x - w->left, _cursor.pos.y - w->top }; + const NWidgetCore *widget = w->nested_root->GetWidgetFromPos(pt.x, pt.y); + if (widget != NULL) w->OnMouseOver(pt, widget->index); + } +} + +/** The minimum number of pixels of the title bar must be visible in both the X or Y direction */ +static const int MIN_VISIBLE_TITLE_BAR = 13; + +/** Direction for moving the window. */ +enum PreventHideDirection { + PHD_UP, ///< Above v is a safe position. + PHD_DOWN, ///< Below v is a safe position. +}; + +/** + * Do not allow hiding of the rectangle with base coordinates \a nx and \a ny behind window \a v. + * If needed, move the window base coordinates to keep it visible. + * @param nx Base horizontal coordinate of the rectangle. + * @param ny Base vertical coordinate of the rectangle. + * @param rect Rectangle that must stay visible for #MIN_VISIBLE_TITLE_BAR pixels (horizontally, vertically, or both) + * @param v Window lying in front of the rectangle. + * @param px Previous horizontal base coordinate. + * @param dir If no room horizontally, move the rectangle to the indicated position. + */ +static void PreventHiding(int *nx, int *ny, const Rect &rect, const Window *v, int px, PreventHideDirection dir) +{ + if (v == NULL) return; + + int v_bottom = v->top + v->height; + int v_right = v->left + v->width; + int safe_y = (dir == PHD_UP) ? (v->top - MIN_VISIBLE_TITLE_BAR - rect.top) : (v_bottom + MIN_VISIBLE_TITLE_BAR - rect.bottom); // Compute safe vertical position. + + if (*ny + rect.top <= v->top - MIN_VISIBLE_TITLE_BAR) return; // Above v is enough space + if (*ny + rect.bottom >= v_bottom + MIN_VISIBLE_TITLE_BAR) return; // Below v is enough space + + /* Vertically, the rectangle is hidden behind v. */ + if (*nx + rect.left + MIN_VISIBLE_TITLE_BAR < v->left) { // At left of v. + if (v->left < MIN_VISIBLE_TITLE_BAR) *ny = safe_y; // But enough room, force it to a safe position. + return; + } + if (*nx + rect.right - MIN_VISIBLE_TITLE_BAR > v_right) { // At right of v. + if (v_right > _screen.width - MIN_VISIBLE_TITLE_BAR) *ny = safe_y; // Not enough room, force it to a safe position. + return; + } + + /* Horizontally also hidden, force movement to a safe area. */ + if (px + rect.left < v->left && v->left >= MIN_VISIBLE_TITLE_BAR) { // Coming from the left, and enough room there. + *nx = v->left - MIN_VISIBLE_TITLE_BAR - rect.left; + } else if (px + rect.right > v_right && v_right <= _screen.width - MIN_VISIBLE_TITLE_BAR) { // Coming from the right, and enough room there. + *nx = v_right + MIN_VISIBLE_TITLE_BAR - rect.right; + } else { + *ny = safe_y; + } +} + +/** + * Make sure at least a part of the caption bar is still visible by moving + * the window if necessary. + * @param w The window to check. + * @param nx The proposed new x-location of the window. + * @param ny The proposed new y-location of the window. + */ +static void EnsureVisibleCaption(Window *w, int nx, int ny) +{ + /* Search for the title bar rectangle. */ + Rect caption_rect; + const NWidgetBase *caption = w->nested_root->GetWidgetOfType(WWT_CAPTION); + if (caption != NULL) { + caption_rect.left = caption->pos_x; + caption_rect.right = caption->pos_x + caption->current_x; + caption_rect.top = caption->pos_y; + caption_rect.bottom = caption->pos_y + caption->current_y; + + /* Make sure the window doesn't leave the screen */ + nx = Clamp(nx, MIN_VISIBLE_TITLE_BAR - caption_rect.right, _screen.width - MIN_VISIBLE_TITLE_BAR - caption_rect.left); + ny = Clamp(ny, 0, _screen.height - MIN_VISIBLE_TITLE_BAR); + + /* Make sure the title bar isn't hidden behind the main tool bar or the status bar. */ + PreventHiding(&nx, &ny, caption_rect, FindWindowById(WC_MAIN_TOOLBAR, 0), w->left, PHD_DOWN); + PreventHiding(&nx, &ny, caption_rect, FindWindowById(WC_STATUS_BAR, 0), w->left, PHD_UP); + } + + if (w->viewport != NULL) { + w->viewport->left += nx - w->left; + w->viewport->top += ny - w->top; + } + + w->left = nx; + w->top = ny; +} + +/** + * Resize the window. + * Update all the widgets of a window based on their resize flags + * Both the areas of the old window and the new sized window are set dirty + * ensuring proper redrawal. + * @param w Window to resize + * @param delta_x Delta x-size of changed window (positive if larger, etc.) + * @param delta_y Delta y-size of changed window + * @param clamp_to_screen Whether to make sure the whole window stays visible + */ +void ResizeWindow(Window *w, int delta_x, int delta_y, bool clamp_to_screen) +{ + if (delta_x != 0 || delta_y != 0) { + if (clamp_to_screen) { + /* Determine the new right/bottom position. If that is outside of the bounds of + * the resolution clamp it in such a manner that it stays within the bounds. */ + int new_right = w->left + w->width + delta_x; + int new_bottom = w->top + w->height + delta_y; + if (new_right >= (int)_cur_resolution.width) delta_x -= Ceil(new_right - _cur_resolution.width, max(1U, w->nested_root->resize_x)); + if (new_bottom >= (int)_cur_resolution.height) delta_y -= Ceil(new_bottom - _cur_resolution.height, max(1U, w->nested_root->resize_y)); + } + + w->SetDirty(); + + uint new_xinc = max(0, (w->nested_root->resize_x == 0) ? 0 : (int)(w->nested_root->current_x - w->nested_root->smallest_x) + delta_x); + uint new_yinc = max(0, (w->nested_root->resize_y == 0) ? 0 : (int)(w->nested_root->current_y - w->nested_root->smallest_y) + delta_y); + assert(w->nested_root->resize_x == 0 || new_xinc % w->nested_root->resize_x == 0); + assert(w->nested_root->resize_y == 0 || new_yinc % w->nested_root->resize_y == 0); + + w->nested_root->AssignSizePosition(ST_RESIZE, 0, 0, w->nested_root->smallest_x + new_xinc, w->nested_root->smallest_y + new_yinc, _current_text_dir == TD_RTL); + w->width = w->nested_root->current_x; + w->height = w->nested_root->current_y; + } + + EnsureVisibleCaption(w, w->left, w->top); + + /* Always call OnResize to make sure everything is initialised correctly if it needs to be. */ + w->OnResize(); + w->SetDirty(); +} + +/** + * Return the top of the main view available for general use. + * @return Uppermost vertical coordinate available. + * @note Above the upper y coordinate is often the main toolbar. + */ +int GetMainViewTop() +{ + Window *w = FindWindowById(WC_MAIN_TOOLBAR, 0); + return (w == NULL) ? 0 : w->top + w->height; +} + +/** + * Return the bottom of the main view available for general use. + * @return The vertical coordinate of the first unusable row, so 'top + height <= bottom' gives the correct result. + * @note At and below the bottom y coordinate is often the status bar. + */ +int GetMainViewBottom() +{ + Window *w = FindWindowById(WC_STATUS_BAR, 0); + return (w == NULL) ? _screen.height : w->top; +} + +static bool _dragging_window; ///< A window is being dragged or resized. + +/** + * Handle dragging/resizing of a window. + * @return State of handling the event. + */ +static EventState HandleWindowDragging() +{ + /* Get out immediately if no window is being dragged at all. */ + if (!_dragging_window) return ES_NOT_HANDLED; + + /* If button still down, but cursor hasn't moved, there is nothing to do. */ + if (_left_button_down && _cursor.delta.x == 0 && _cursor.delta.y == 0) return ES_HANDLED; + + /* Otherwise find the window... */ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->flags & WF_DRAGGING) { + /* Stop the dragging if the left mouse button was released */ + if (!_left_button_down) { + w->flags &= ~WF_DRAGGING; + break; + } + + w->SetDirty(); + + int x = _cursor.pos.x + _drag_delta.x; + int y = _cursor.pos.y + _drag_delta.y; + int nx = x; + int ny = y; + + if (_settings_client.gui.window_snap_radius != 0) { + const Window *v; + + int hsnap = _settings_client.gui.window_snap_radius; + int vsnap = _settings_client.gui.window_snap_radius; + int delta; + + FOR_ALL_WINDOWS_FROM_BACK(v) { + if (v == w) continue; // Don't snap at yourself + + if (y + w->height > v->top && y < v->top + v->height) { + /* Your left border <-> other right border */ + delta = abs(v->left + v->width - x); + if (delta <= hsnap) { + nx = v->left + v->width; + hsnap = delta; + } + + /* Your right border <-> other left border */ + delta = abs(v->left - x - w->width); + if (delta <= hsnap) { + nx = v->left - w->width; + hsnap = delta; + } + } + + if (w->top + w->height >= v->top && w->top <= v->top + v->height) { + /* Your left border <-> other left border */ + delta = abs(v->left - x); + if (delta <= hsnap) { + nx = v->left; + hsnap = delta; + } + + /* Your right border <-> other right border */ + delta = abs(v->left + v->width - x - w->width); + if (delta <= hsnap) { + nx = v->left + v->width - w->width; + hsnap = delta; + } + } + + if (x + w->width > v->left && x < v->left + v->width) { + /* Your top border <-> other bottom border */ + delta = abs(v->top + v->height - y); + if (delta <= vsnap) { + ny = v->top + v->height; + vsnap = delta; + } + + /* Your bottom border <-> other top border */ + delta = abs(v->top - y - w->height); + if (delta <= vsnap) { + ny = v->top - w->height; + vsnap = delta; + } + } + + if (w->left + w->width >= v->left && w->left <= v->left + v->width) { + /* Your top border <-> other top border */ + delta = abs(v->top - y); + if (delta <= vsnap) { + ny = v->top; + vsnap = delta; + } + + /* Your bottom border <-> other bottom border */ + delta = abs(v->top + v->height - y - w->height); + if (delta <= vsnap) { + ny = v->top + v->height - w->height; + vsnap = delta; + } + } + } + } + + EnsureVisibleCaption(w, nx, ny); + + w->SetDirty(); + return ES_HANDLED; + } else if (w->flags & WF_SIZING) { + /* Stop the sizing if the left mouse button was released */ + if (!_left_button_down) { + w->flags &= ~WF_SIZING; + w->SetDirty(); + break; + } + + /* Compute difference in pixels between cursor position and reference point in the window. + * If resizing the left edge of the window, moving to the left makes the window bigger not smaller. + */ + int x, y = _cursor.pos.y - _drag_delta.y; + if (w->flags & WF_SIZING_LEFT) { + x = _drag_delta.x - _cursor.pos.x; + } else { + x = _cursor.pos.x - _drag_delta.x; + } + + /* resize.step_width and/or resize.step_height may be 0, which means no resize is possible. */ + if (w->resize.step_width == 0) x = 0; + if (w->resize.step_height == 0) y = 0; + + /* Check the resize button won't go past the bottom of the screen */ + if (w->top + w->height + y > _screen.height) { + y = _screen.height - w->height - w->top; + } + + /* X and Y has to go by step.. calculate it. + * The cast to int is necessary else x/y are implicitly casted to + * unsigned int, which won't work. */ + if (w->resize.step_width > 1) x -= x % (int)w->resize.step_width; + if (w->resize.step_height > 1) y -= y % (int)w->resize.step_height; + + /* Check that we don't go below the minimum set size */ + if ((int)w->width + x < (int)w->nested_root->smallest_x) { + x = w->nested_root->smallest_x - w->width; + } + if ((int)w->height + y < (int)w->nested_root->smallest_y) { + y = w->nested_root->smallest_y - w->height; + } + + /* Window already on size */ + if (x == 0 && y == 0) return ES_HANDLED; + + /* Now find the new cursor pos.. this is NOT _cursor, because we move in steps. */ + _drag_delta.y += y; + if ((w->flags & WF_SIZING_LEFT) && x != 0) { + _drag_delta.x -= x; // x > 0 -> window gets longer -> left-edge moves to left -> subtract x to get new position. + w->SetDirty(); + w->left -= x; // If dragging left edge, move left window edge in opposite direction by the same amount. + /* ResizeWindow() below ensures marking new position as dirty. */ + } else { + _drag_delta.x += x; + } + + /* ResizeWindow sets both pre- and after-size to dirty for redrawal */ + ResizeWindow(w, x, y); + return ES_HANDLED; + } + } + + _dragging_window = false; + return ES_HANDLED; +} + +/** + * Start window dragging + * @param w Window to start dragging + */ +static void StartWindowDrag(Window *w) +{ + w->flags |= WF_DRAGGING; + w->flags &= ~WF_CENTERED; + _dragging_window = true; + + _drag_delta.x = w->left - _cursor.pos.x; + _drag_delta.y = w->top - _cursor.pos.y; + + BringWindowToFront(w); + DeleteWindowById(WC_DROPDOWN_MENU, 0); +} + +/** + * Start resizing a window. + * @param w Window to start resizing. + * @param to_left Whether to drag towards the left or not + */ +static void StartWindowSizing(Window *w, bool to_left) +{ + w->flags |= to_left ? WF_SIZING_LEFT : WF_SIZING_RIGHT; + w->flags &= ~WF_CENTERED; + _dragging_window = true; + + _drag_delta.x = _cursor.pos.x; + _drag_delta.y = _cursor.pos.y; + + BringWindowToFront(w); + DeleteWindowById(WC_DROPDOWN_MENU, 0); +} + +/** + * handle scrollbar scrolling with the mouse. + * @return State of handling the event. + */ +static EventState HandleScrollbarScrolling() +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->scrolling_scrollbar >= 0) { + /* Abort if no button is clicked any more. */ + if (!_left_button_down) { + w->scrolling_scrollbar = -1; + w->SetDirty(); + return ES_HANDLED; + } + + int i; + NWidgetScrollbar *sb = w->GetWidget(w->scrolling_scrollbar); + bool rtl = false; + + if (sb->type == NWID_HSCROLLBAR) { + i = _cursor.pos.x - _cursorpos_drag_start.x; + rtl = _current_text_dir == TD_RTL; + } else { + i = _cursor.pos.y - _cursorpos_drag_start.y; + } + + if (sb->disp_flags & ND_SCROLLBAR_BTN) { + if (_scroller_click_timeout == 1) { + _scroller_click_timeout = 3; + sb->UpdatePosition(rtl == HasBit(sb->disp_flags, NDB_SCROLLBAR_UP) ? 1 : -1); + w->SetDirty(); + } + return ES_HANDLED; + } + + /* Find the item we want to move to and make sure it's inside bounds. */ + int pos = min(max(0, i + _scrollbar_start_pos) * sb->GetCount() / _scrollbar_size, max(0, sb->GetCount() - sb->GetCapacity())); + if (rtl) pos = max(0, sb->GetCount() - sb->GetCapacity() - pos); + if (pos != sb->GetPosition()) { + sb->SetPosition(pos); + w->SetDirty(); + } + return ES_HANDLED; + } + } + + return ES_NOT_HANDLED; +} + +/** + * Handle viewport scrolling with the mouse. + * @return State of handling the event. + */ +static EventState HandleViewportScroll() +{ + bool scrollwheel_scrolling = _settings_client.gui.scrollwheel_scrolling == 1 && (_cursor.v_wheel != 0 || _cursor.h_wheel != 0); + + if (!_scrolling_viewport) return ES_NOT_HANDLED; + + /* When we don't have a last scroll window we are starting to scroll. + * When the last scroll window and this are not the same we went + * outside of the window and should not left-mouse scroll anymore. */ + if (_last_scroll_window == NULL) _last_scroll_window = FindWindowFromPt(_cursor.pos.x, _cursor.pos.y); + + if (_last_scroll_window == NULL || !(_right_button_down || scrollwheel_scrolling || (_settings_client.gui.left_mouse_btn_scrolling && _left_button_down))) { + _cursor.fix_at = false; + _scrolling_viewport = false; + _last_scroll_window = NULL; + return ES_NOT_HANDLED; + } + + if (_last_scroll_window == FindWindowById(WC_MAIN_WINDOW, 0) && _last_scroll_window->viewport->follow_vehicle != INVALID_VEHICLE) { + /* If the main window is following a vehicle, then first let go of it! */ + const Vehicle *veh = Vehicle::Get(_last_scroll_window->viewport->follow_vehicle); + ScrollMainWindowTo(veh->x_pos, veh->y_pos, veh->z_pos, true); // This also resets follow_vehicle + return ES_NOT_HANDLED; + } + + Point delta; + if (_settings_client.gui.reverse_scroll || (_settings_client.gui.left_mouse_btn_scrolling && _left_button_down)) { + delta.x = -_cursor.delta.x; + delta.y = -_cursor.delta.y; + } else { + delta.x = _cursor.delta.x; + delta.y = _cursor.delta.y; + } + + if (scrollwheel_scrolling) { + /* We are using scrollwheels for scrolling */ + delta.x = _cursor.h_wheel; + delta.y = _cursor.v_wheel; + _cursor.v_wheel = 0; + _cursor.h_wheel = 0; + } + + /* Create a scroll-event and send it to the window */ + if (delta.x != 0 || delta.y != 0) _last_scroll_window->OnScroll(delta); + + _cursor.delta.x = 0; + _cursor.delta.y = 0; + return ES_HANDLED; +} + +/** + * Check if a window can be made relative top-most window, and if so do + * it. If a window does not obscure any other windows, it will not + * be brought to the foreground. Also if the only obscuring windows + * are so-called system-windows, the window will not be moved. + * The function will return false when a child window of this window is a + * modal-popup; function returns a false and child window gets a white border + * @param w Window to bring relatively on-top + * @return false if the window has an active modal child, true otherwise + */ +static bool MaybeBringWindowToFront(Window *w) +{ + bool bring_to_front = false; + + if (w->window_class == WC_MAIN_WINDOW || + IsVitalWindow(w) || + w->window_class == WC_TOOLTIPS || + w->window_class == WC_DROPDOWN_MENU) { + return true; + } + + /* Use unshaded window size rather than current size for shaded windows. */ + int w_width = w->width; + int w_height = w->height; + if (w->IsShaded()) { + w_width = w->unshaded_size.width; + w_height = w->unshaded_size.height; + } + + Window *u; + FOR_ALL_WINDOWS_FROM_BACK_FROM(u, w->z_front) { + /* A modal child will prevent the activation of the parent window */ + if (u->parent == w && (u->window_desc->flags & WDF_MODAL)) { + u->SetWhiteBorder(); + u->SetDirty(); + return false; + } + + if (u->window_class == WC_MAIN_WINDOW || + IsVitalWindow(u) || + u->window_class == WC_TOOLTIPS || + u->window_class == WC_DROPDOWN_MENU) { + continue; + } + + /* Window sizes don't interfere, leave z-order alone */ + if (w->left + w_width <= u->left || + u->left + u->width <= w->left || + w->top + w_height <= u->top || + u->top + u->height <= w->top) { + continue; + } + + bring_to_front = true; + } + + if (bring_to_front) BringWindowToFront(w); + return true; +} + +/** + * Process keypress for editbox widget. + * @param wid Editbox widget. + * @param key the Unicode value of the key. + * @param keycode the untranslated key code including shift state. + * @return #ES_HANDLED if the key press has been handled and no other + * window should receive the event. + */ +EventState Window::HandleEditBoxKey(int wid, WChar key, uint16 keycode) +{ + QueryString *query = this->GetQueryString(wid); + if (query == NULL) return ES_NOT_HANDLED; + + int action = QueryString::ACTION_NOTHING; + + switch (query->text.HandleKeyPress(key, keycode)) { + case HKPR_EDITING: + this->SetWidgetDirty(wid); + this->OnEditboxChanged(wid); + break; + + case HKPR_CURSOR: + this->SetWidgetDirty(wid); + /* For the OSK also invalidate the parent window */ + if (this->window_class == WC_OSK) this->InvalidateData(); + break; + + case HKPR_CONFIRM: + if (this->window_class == WC_OSK) { + this->OnClick(Point(), WID_OSK_OK, 1); + } else if (query->ok_button >= 0) { + this->OnClick(Point(), query->ok_button, 1); + } else { + action = query->ok_button; + } + break; + + case HKPR_CANCEL: + if (this->window_class == WC_OSK) { + this->OnClick(Point(), WID_OSK_CANCEL, 1); + } else if (query->cancel_button >= 0) { + this->OnClick(Point(), query->cancel_button, 1); + } else { + action = query->cancel_button; + } + break; + + case HKPR_NOT_HANDLED: + return ES_NOT_HANDLED; + + default: break; + } + + switch (action) { + case QueryString::ACTION_DESELECT: + this->UnfocusFocusedWidget(); + break; + + case QueryString::ACTION_CLEAR: + if (query->text.bytes <= 1) { + /* If already empty, unfocus instead */ + this->UnfocusFocusedWidget(); + } else { + query->text.DeleteAll(); + this->SetWidgetDirty(wid); + this->OnEditboxChanged(wid); + } + break; + + default: + break; + } + + return ES_HANDLED; +} + +/** + * Handle keyboard input. + * @param keycode Virtual keycode of the key. + * @param key Unicode character of the key. + */ +void HandleKeypress(uint keycode, WChar key) +{ + /* World generation is multithreaded and messes with companies. + * But there is no company related window open anyway, so _current_company is not used. */ + assert(HasModalProgress() || IsLocalCompany()); + + /* + * The Unicode standard defines an area called the private use area. Code points in this + * area are reserved for private use and thus not portable between systems. For instance, + * Apple defines code points for the arrow keys in this area, but these are only printable + * on a system running OS X. We don't want these keys to show up in text fields and such, + * and thus we have to clear the unicode character when we encounter such a key. + */ + if (key >= 0xE000 && key <= 0xF8FF) key = 0; + + /* + * If both key and keycode is zero, we don't bother to process the event. + */ + if (key == 0 && keycode == 0) return; + + /* Check if the focused window has a focused editbox */ + if (EditBoxInGlobalFocus()) { + /* All input will in this case go to the focused editbox */ + if (_focused_window->window_class == WC_CONSOLE) { + if (_focused_window->OnKeyPress(key, keycode) == ES_HANDLED) return; + } else { + if (_focused_window->HandleEditBoxKey(_focused_window->nested_focus->index, key, keycode) == ES_HANDLED) return; + } + } + + /* Call the event, start with the uppermost window, but ignore the toolbar. */ + Window *w; + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if (w->window_class == WC_MAIN_TOOLBAR) continue; + if (w->window_desc->hotkeys != NULL) { + int hotkey = w->window_desc->hotkeys->CheckMatch(keycode); + if (hotkey >= 0 && w->OnHotkey(hotkey) == ES_HANDLED) return; + } + if (w->OnKeyPress(key, keycode) == ES_HANDLED) return; + } + + w = FindWindowById(WC_MAIN_TOOLBAR, 0); + /* When there is no toolbar w is null, check for that */ + if (w != NULL) { + if (w->window_desc->hotkeys != NULL) { + int hotkey = w->window_desc->hotkeys->CheckMatch(keycode); + if (hotkey >= 0 && w->OnHotkey(hotkey) == ES_HANDLED) return; + } + if (w->OnKeyPress(key, keycode) == ES_HANDLED) return; + } + + HandleGlobalHotkeys(key, keycode); +} + +/** + * State of CONTROL key has changed + */ +void HandleCtrlChanged() +{ + /* Call the event, start with the uppermost window. */ + Window *w; + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if (w->OnCTRLStateChange() == ES_HANDLED) return; + } +} + +/** + * Insert a text string at the cursor position into the edit box widget. + * @param wid Edit box widget. + * @param str Text string to insert. + */ +/* virtual */ void Window::InsertTextString(int wid, const char *str, bool marked, const char *caret, const char *insert_location, const char *replacement_end) +{ + QueryString *query = this->GetQueryString(wid); + if (query == NULL) return; + + if (query->text.InsertString(str, marked, caret, insert_location, replacement_end) || marked) { + this->SetWidgetDirty(wid); + this->OnEditboxChanged(wid); + } +} + +/** + * Handle text input. + * @param str Text string to input. + * @param marked Is the input a marked composition string from an IME? + * @param caret Move the caret to this point in the insertion string. + */ +void HandleTextInput(const char *str, bool marked, const char *caret, const char *insert_location, const char *replacement_end) +{ + if (!EditBoxInGlobalFocus()) return; + + _focused_window->InsertTextString(_focused_window->window_class == WC_CONSOLE ? 0 : _focused_window->nested_focus->index, str, marked, caret, insert_location, replacement_end); +} + +/** + * Local counter that is incremented each time an mouse input event is detected. + * The counter is used to stop auto-scrolling. + * @see HandleAutoscroll() + * @see HandleMouseEvents() + */ +static int _input_events_this_tick = 0; + +/** + * If needed and switched on, perform auto scrolling (automatically + * moving window contents when mouse is near edge of the window). + */ +static void HandleAutoscroll() +{ + if (_game_mode == GM_MENU || HasModalProgress()) return; + if (_settings_client.gui.auto_scrolling == VA_DISABLED) return; + if (_settings_client.gui.auto_scrolling == VA_MAIN_VIEWPORT_FULLSCREEN && !_fullscreen) return; + + int x = _cursor.pos.x; + int y = _cursor.pos.y; + Window *w = FindWindowFromPt(x, y); + if (w == NULL || w->flags & WF_DISABLE_VP_SCROLL) return; + if (_settings_client.gui.auto_scrolling != VA_EVERY_VIEWPORT && w->window_class != WC_MAIN_WINDOW) return; + + ViewPort *vp = IsPtInWindowViewport(w, x, y); + if (vp == NULL) return; + + x -= vp->left; + y -= vp->top; + + /* here allows scrolling in both x and y axis */ +#define scrollspeed 3 + if (x - 15 < 0) { + w->viewport->dest_scrollpos_x += ScaleByZoom((x - 15) * scrollspeed, vp->zoom); + } else if (15 - (vp->width - x) > 0) { + w->viewport->dest_scrollpos_x += ScaleByZoom((15 - (vp->width - x)) * scrollspeed, vp->zoom); + } + if (y - 15 < 0) { + w->viewport->dest_scrollpos_y += ScaleByZoom((y - 15) * scrollspeed, vp->zoom); + } else if (15 - (vp->height - y) > 0) { + w->viewport->dest_scrollpos_y += ScaleByZoom((15 - (vp->height - y)) * scrollspeed, vp->zoom); + } +#undef scrollspeed +} + +enum MouseClick { + MC_NONE = 0, + MC_LEFT, + MC_RIGHT, + MC_DOUBLE_LEFT, + MC_HOVER, + + MAX_OFFSET_DOUBLE_CLICK = 5, ///< How much the mouse is allowed to move to call it a double click + TIME_BETWEEN_DOUBLE_CLICK = 500, ///< Time between 2 left clicks before it becoming a double click, in ms + MAX_OFFSET_HOVER = 5, ///< Maximum mouse movement before stopping a hover event. +}; +extern EventState VpHandlePlaceSizingDrag(); + +static void ScrollMainViewport(int x, int y) +{ + if (_game_mode != GM_MENU) { + Window *w = FindWindowById(WC_MAIN_WINDOW, 0); + assert(w); + + w->viewport->dest_scrollpos_x += ScaleByZoom(x, w->viewport->zoom); + w->viewport->dest_scrollpos_y += ScaleByZoom(y, w->viewport->zoom); + } +} + +/** + * Describes all the different arrow key combinations the game allows + * when it is in scrolling mode. + * The real arrow keys are bitwise numbered as + * 1 = left + * 2 = up + * 4 = right + * 8 = down + */ +static const int8 scrollamt[16][2] = { + { 0, 0}, ///< no key specified + {-2, 0}, ///< 1 : left + { 0, -2}, ///< 2 : up + {-2, -1}, ///< 3 : left + up + { 2, 0}, ///< 4 : right + { 0, 0}, ///< 5 : left + right = nothing + { 2, -1}, ///< 6 : right + up + { 0, -2}, ///< 7 : right + left + up = up + { 0, 2}, ///< 8 : down + {-2, 1}, ///< 9 : down + left + { 0, 0}, ///< 10 : down + up = nothing + {-2, 0}, ///< 11 : left + up + down = left + { 2, 1}, ///< 12 : down + right + { 0, 2}, ///< 13 : left + right + down = down + { 2, 0}, ///< 14 : right + up + down = right + { 0, 0}, ///< 15 : left + up + right + down = nothing +}; + +static void HandleKeyScrolling() +{ + /* + * Check that any of the dirkeys is pressed and that the focused window + * doesn't have an edit-box as focused widget. + */ + if (_dirkeys && !EditBoxInGlobalFocus()) { + int factor = _shift_pressed ? 50 : 10; + ScrollMainViewport(scrollamt[_dirkeys][0] * factor, scrollamt[_dirkeys][1] * factor); + } +} + +static void MouseLoop(MouseClick click, int mousewheel) +{ + /* World generation is multithreaded and messes with companies. + * But there is no company related window open anyway, so _current_company is not used. */ + assert(HasModalProgress() || IsLocalCompany()); + + HandlePlacePresize(); + UpdateTileSelection(); + + if (VpHandlePlaceSizingDrag() == ES_HANDLED) return; + if (HandleMouseDragDrop() == ES_HANDLED) return; + if (HandleWindowDragging() == ES_HANDLED) return; + if (HandleScrollbarScrolling() == ES_HANDLED) return; + if (HandleViewportScroll() == ES_HANDLED) return; + + HandleMouseOver(); + + bool scrollwheel_scrolling = _settings_client.gui.scrollwheel_scrolling == 1 && (_cursor.v_wheel != 0 || _cursor.h_wheel != 0); + if (click == MC_NONE && mousewheel == 0 && !scrollwheel_scrolling) return; + + int x = _cursor.pos.x; + int y = _cursor.pos.y; + Window *w = FindWindowFromPt(x, y); + if (w == NULL) return; + + if (click != MC_HOVER && !MaybeBringWindowToFront(w)) return; + ViewPort *vp = IsPtInWindowViewport(w, x, y); + + /* Don't allow any action in a viewport if either in menu or when having a modal progress window */ + if (vp != NULL && (_game_mode == GM_MENU || HasModalProgress())) return; + + if (mousewheel != 0) { + /* Send mousewheel event to window */ + w->OnMouseWheel(mousewheel); + + /* Dispatch a MouseWheelEvent for widgets if it is not a viewport */ + if (vp == NULL) DispatchMouseWheelEvent(w, w->nested_root->GetWidgetFromPos(x - w->left, y - w->top), mousewheel); + } + + if (vp != NULL) { + if (scrollwheel_scrolling) click = MC_RIGHT; // we are using the scrollwheel in a viewport, so we emulate right mouse button + switch (click) { + case MC_DOUBLE_LEFT: + case MC_LEFT: + DEBUG(misc, 2, "Cursor: 0x%X (%d)", _cursor.sprite, _cursor.sprite); + if (!HandleViewportClicked(vp, x, y) && + !(w->flags & WF_DISABLE_VP_SCROLL) && + _settings_client.gui.left_mouse_btn_scrolling) { + _scrolling_viewport = true; + _cursor.fix_at = false; + } + break; + + case MC_RIGHT: + if (!(w->flags & WF_DISABLE_VP_SCROLL)) { + _scrolling_viewport = true; + _cursor.fix_at = true; + + /* clear 2D scrolling caches before we start a 2D scroll */ + _cursor.h_wheel = 0; + _cursor.v_wheel = 0; + } + break; + + default: + break; + } + } else { + switch (click) { + case MC_LEFT: + case MC_DOUBLE_LEFT: + DispatchLeftClickEvent(w, x - w->left, y - w->top, click == MC_DOUBLE_LEFT ? 2 : 1); + break; + + default: + if (!scrollwheel_scrolling || w == NULL || w->window_class != WC_SMALLMAP) break; + /* We try to use the scrollwheel to scroll since we didn't touch any of the buttons. + * Simulate a right button click so we can get started. */ + /* FALL THROUGH */ + + case MC_RIGHT: DispatchRightClickEvent(w, x - w->left, y - w->top); break; + + case MC_HOVER: DispatchHoverEvent(w, x - w->left, y - w->top); break; + } + } +} + +/** + * Handle a mouse event from the video driver + */ +void HandleMouseEvents() +{ + /* World generation is multithreaded and messes with companies. + * But there is no company related window open anyway, so _current_company is not used. */ + assert(HasModalProgress() || IsLocalCompany()); + + static int double_click_time = 0; + static Point double_click_pos = {0, 0}; + + /* Mouse event? */ + MouseClick click = MC_NONE; + if (_left_button_down && !_left_button_clicked) { + click = MC_LEFT; + if (double_click_time != 0 && _realtime_tick - double_click_time < TIME_BETWEEN_DOUBLE_CLICK && + double_click_pos.x != 0 && abs(_cursor.pos.x - double_click_pos.x) < MAX_OFFSET_DOUBLE_CLICK && + double_click_pos.y != 0 && abs(_cursor.pos.y - double_click_pos.y) < MAX_OFFSET_DOUBLE_CLICK) { + click = MC_DOUBLE_LEFT; + } + double_click_time = _realtime_tick; + double_click_pos = _cursor.pos; + _left_button_clicked = true; + _input_events_this_tick++; + } else if (_right_button_clicked) { + _right_button_clicked = false; + click = MC_RIGHT; + _input_events_this_tick++; + } + + int mousewheel = 0; + if (_cursor.wheel) { + mousewheel = _cursor.wheel; + _cursor.wheel = 0; + _input_events_this_tick++; + } + + static uint32 hover_time = 0; + static Point hover_pos = {0, 0}; + + if (_settings_client.gui.hover_delay > 0) { + if (!_cursor.in_window || click != MC_NONE || mousewheel != 0 || _left_button_down || _right_button_down || + hover_pos.x == 0 || abs(_cursor.pos.x - hover_pos.x) >= MAX_OFFSET_HOVER || + hover_pos.y == 0 || abs(_cursor.pos.y - hover_pos.y) >= MAX_OFFSET_HOVER) { + hover_pos = _cursor.pos; + hover_time = _realtime_tick; + _mouse_hovering = false; + } else { + if (hover_time != 0 && _realtime_tick > hover_time + _settings_client.gui.hover_delay * 1000) { + click = MC_HOVER; + _input_events_this_tick++; + _mouse_hovering = true; + } + } + } + + /* Handle sprite picker before any GUI interaction */ + if (_newgrf_debug_sprite_picker.mode == SPM_REDRAW && _newgrf_debug_sprite_picker.click_time != _realtime_tick) { + /* Next realtime tick? Then redraw has finished */ + _newgrf_debug_sprite_picker.mode = SPM_NONE; + InvalidateWindowData(WC_SPRITE_ALIGNER, 0, 1); + } + + if (click == MC_LEFT && _newgrf_debug_sprite_picker.mode == SPM_WAIT_CLICK) { + /* Mark whole screen dirty, and wait for the next realtime tick, when drawing is finished. */ + Blitter *blitter = BlitterFactory::GetCurrentBlitter(); + _newgrf_debug_sprite_picker.clicked_pixel = blitter->MoveTo(_screen.dst_ptr, _cursor.pos.x, _cursor.pos.y); + _newgrf_debug_sprite_picker.click_time = _realtime_tick; + _newgrf_debug_sprite_picker.sprites.Clear(); + _newgrf_debug_sprite_picker.mode = SPM_REDRAW; + MarkWholeScreenDirty(); + } else { + MouseLoop(click, mousewheel); + } + + /* We have moved the mouse the required distance, + * no need to move it at any later time. */ + _cursor.delta.x = 0; + _cursor.delta.y = 0; +} + +/** + * Check the soft limit of deletable (non vital, non sticky) windows. + */ +static void CheckSoftLimit() +{ + if (_settings_client.gui.window_soft_limit == 0) return; + + for (;;) { + uint deletable_count = 0; + Window *w, *last_deletable = NULL; + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if (w->window_class == WC_MAIN_WINDOW || IsVitalWindow(w) || (w->flags & WF_STICKY)) continue; + + last_deletable = w; + deletable_count++; + } + + /* We've not reached the soft limit yet. */ + if (deletable_count <= _settings_client.gui.window_soft_limit) break; + + assert(last_deletable != NULL); + delete last_deletable; + } +} + +/** + * Regular call from the global game loop + */ +void InputLoop() +{ + /* World generation is multithreaded and messes with companies. + * But there is no company related window open anyway, so _current_company is not used. */ + assert(HasModalProgress() || IsLocalCompany()); + + CheckSoftLimit(); + HandleKeyScrolling(); + + /* Do the actual free of the deleted windows. */ + for (Window *v = _z_front_window; v != NULL; /* nothing */) { + Window *w = v; + v = v->z_back; + + if (w->window_class != WC_INVALID) continue; + + RemoveWindowFromZOrdering(w); + free(w); + } + + if (_scroller_click_timeout != 0) _scroller_click_timeout--; + DecreaseWindowCounters(); + + if (_input_events_this_tick != 0) { + /* The input loop is called only once per GameLoop() - so we can clear the counter here */ + _input_events_this_tick = 0; + /* there were some inputs this tick, don't scroll ??? */ + return; + } + + /* HandleMouseEvents was already called for this tick */ + HandleMouseEvents(); + HandleAutoscroll(); +} + +/** + * Update the continuously changing contents of the windows, such as the viewports + */ +void UpdateWindows() +{ + Window *w; + + static int highlight_timer = 1; + if (--highlight_timer == 0) { + highlight_timer = 15; + _window_highlight_colour = !_window_highlight_colour; + } + + FOR_ALL_WINDOWS_FROM_FRONT(w) { + w->ProcessScheduledInvalidations(); + w->ProcessHighlightedInvalidations(); + } + + static int we4_timer = 0; + int t = we4_timer + 1; + + if (t >= 100) { + FOR_ALL_WINDOWS_FROM_FRONT(w) { + w->OnHundredthTick(); + } + t = 0; + } + we4_timer = t; + + FOR_ALL_WINDOWS_FROM_FRONT(w) { + if ((w->flags & WF_WHITE_BORDER) && --w->white_border_timer == 0) { + CLRBITS(w->flags, WF_WHITE_BORDER); + w->SetDirty(); + } + } + + DrawDirtyBlocks(); + + FOR_ALL_WINDOWS_FROM_BACK(w) { + /* Update viewport only if window is not shaded. */ + if (w->viewport != NULL && !w->IsShaded()) UpdateViewportPosition(w); + } + NetworkDrawChatMessage(); + /* Redraw mouse cursor in case it was hidden */ + DrawMouseCursor(); +} + +/** + * Mark window as dirty (in need of repainting) + * @param cls Window class + * @param number Window number in that class + */ +void SetWindowDirty(WindowClass cls, WindowNumber number) +{ + const Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls && w->window_number == number) w->SetDirty(); + } +} + +/** + * Mark a particular widget in a particular window as dirty (in need of repainting) + * @param cls Window class + * @param number Window number in that class + * @param widget_index Index number of the widget that needs repainting + */ +void SetWindowWidgetDirty(WindowClass cls, WindowNumber number, byte widget_index) +{ + const Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls && w->window_number == number) { + w->SetWidgetDirty(widget_index); + } + } +} + +/** + * Mark all windows of a particular class as dirty (in need of repainting) + * @param cls Window class + */ +void SetWindowClassesDirty(WindowClass cls) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls) w->SetDirty(); + } +} + +/** + * Mark this window's data as invalid (in need of re-computing) + * @param data The data to invalidate with + * @param gui_scope Whether the function is called from GUI scope. + */ +void Window::InvalidateData(int data, bool gui_scope) +{ + this->SetDirty(); + if (!gui_scope) { + /* Schedule GUI-scope invalidation for next redraw. */ + *this->scheduled_invalidation_data.Append() = data; + } + this->OnInvalidateData(data, gui_scope); +} + +/** + * Process all scheduled invalidations. + */ +void Window::ProcessScheduledInvalidations() +{ + for (int *data = this->scheduled_invalidation_data.Begin(); this->window_class != WC_INVALID && data != this->scheduled_invalidation_data.End(); data++) { + this->OnInvalidateData(*data, true); + } + this->scheduled_invalidation_data.Clear(); +} + +/** + * Process all invalidation of highlighted widgets. + */ +void Window::ProcessHighlightedInvalidations() +{ + if ((this->flags & WF_HIGHLIGHTED) == 0) return; + + for (uint i = 0; i < this->nested_array_size; i++) { + if (this->IsWidgetHighlighted(i)) this->SetWidgetDirty(i); + } +} + +/** + * Mark window data of the window of a given class and specific window number as invalid (in need of re-computing) + * + * Note that by default the invalidation is not considered to be called from GUI scope. + * That means only a part of invalidation is executed immediately. The rest is scheduled for the next redraw. + * The asynchronous execution is important to prevent GUI code being executed from command scope. + * When not in GUI-scope: + * - OnInvalidateData() may not do test-runs on commands, as they might affect the execution of + * the command which triggered the invalidation. (town rating and such) + * - OnInvalidateData() may not rely on _current_company == _local_company. + * This implies that no NewGRF callbacks may be run. + * + * However, when invalidations are scheduled, then multiple calls may be scheduled before execution starts. Earlier scheduled + * invalidations may be called with invalidation-data, which is already invalid at the point of execution. + * That means some stuff requires to be executed immediately in command scope, while not everything may be executed in command + * scope. While GUI-scope calls have no restrictions on what they may do, they cannot assume the game to still be in the state + * when the invalidation was scheduled; passed IDs may have got invalid in the mean time. + * + * Finally, note that invalidations triggered from commands or the game loop result in OnInvalidateData() being called twice. + * Once in command-scope, once in GUI-scope. So make sure to not process differential-changes twice. + * + * @param cls Window class + * @param number Window number within the class + * @param data The data to invalidate with + * @param gui_scope Whether the call is done from GUI scope + */ +void InvalidateWindowData(WindowClass cls, WindowNumber number, int data, bool gui_scope) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls && w->window_number == number) { + w->InvalidateData(data, gui_scope); + } + } +} + +/** + * Mark window data of all windows of a given class as invalid (in need of re-computing) + * Note that by default the invalidation is not considered to be called from GUI scope. + * See InvalidateWindowData() for details on GUI-scope vs. command-scope. + * @param cls Window class + * @param data The data to invalidate with + * @param gui_scope Whether the call is done from GUI scope + */ +void InvalidateWindowClassesData(WindowClass cls, int data, bool gui_scope) +{ + Window *w; + + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class == cls) { + w->InvalidateData(data, gui_scope); + } + } +} + +/** + * Dispatch WE_TICK event over all windows + */ +void CallWindowTickEvent() +{ + Window *w; + FOR_ALL_WINDOWS_FROM_FRONT(w) { + w->OnTick(); + } +} + +/** + * Try to delete a non-vital window. + * Non-vital windows are windows other than the game selection, main toolbar, + * status bar, toolbar menu, and tooltip windows. Stickied windows are also + * considered vital. + */ +void DeleteNonVitalWindows() +{ + Window *w; + +restart_search: + /* When we find the window to delete, we need to restart the search + * as deleting this window could cascade in deleting (many) others + * anywhere in the z-array */ + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_class != WC_MAIN_WINDOW && + w->window_class != WC_SELECT_GAME && + w->window_class != WC_MAIN_TOOLBAR && + w->window_class != WC_STATUS_BAR && + w->window_class != WC_TOOLTIPS && + (w->flags & WF_STICKY) == 0) { // do not delete windows which are 'pinned' + + delete w; + goto restart_search; + } + } +} + +/** + * It is possible that a stickied window gets to a position where the + * 'close' button is outside the gaming area. You cannot close it then; except + * with this function. It closes all windows calling the standard function, + * then, does a little hacked loop of closing all stickied windows. Note + * that standard windows (status bar, etc.) are not stickied, so these aren't affected + */ +void DeleteAllNonVitalWindows() +{ + Window *w; + + /* Delete every window except for stickied ones, then sticky ones as well */ + DeleteNonVitalWindows(); + +restart_search: + /* When we find the window to delete, we need to restart the search + * as deleting this window could cascade in deleting (many) others + * anywhere in the z-array */ + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->flags & WF_STICKY) { + delete w; + goto restart_search; + } + } +} + +/** + * Delete all windows that are used for construction of vehicle etc. + * Once done with that invalidate the others to ensure they get refreshed too. + */ +void DeleteConstructionWindows() +{ + Window *w; + +restart_search: + /* When we find the window to delete, we need to restart the search + * as deleting this window could cascade in deleting (many) others + * anywhere in the z-array */ + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->window_desc->flags & WDF_CONSTRUCTION) { + delete w; + goto restart_search; + } + } + + FOR_ALL_WINDOWS_FROM_BACK(w) w->SetDirty(); +} + +/** Delete all always on-top windows to get an empty screen */ +void HideVitalWindows() +{ + DeleteWindowById(WC_MAIN_TOOLBAR, 0); + DeleteWindowById(WC_STATUS_BAR, 0); +} + +/** Re-initialize all windows. */ +void ReInitAllWindows() +{ + NWidgetLeaf::InvalidateDimensionCache(); // Reset cached sizes of several widgets. + NWidgetScrollbar::InvalidateDimensionCache(); + + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + w->ReInit(); + } +#ifdef ENABLE_NETWORK + void NetworkReInitChatBoxSize(); + NetworkReInitChatBoxSize(); +#endif + + /* Make sure essential parts of all windows are visible */ + RelocateAllWindows(_cur_resolution.width, _cur_resolution.height); + MarkWholeScreenDirty(); +} + +/** + * (Re)position a window at the screen. + * @param w Window structure of the window, may also be \c NULL. + * @param clss The class of the window to position. + * @param setting The actual setting used for the window's position. + * @return X coordinate of left edge of the repositioned window. + */ +static int PositionWindow(Window *w, WindowClass clss, int setting) +{ + if (w == NULL || w->window_class != clss) { + w = FindWindowById(clss, 0); + } + if (w == NULL) return 0; + + int old_left = w->left; + switch (setting) { + case 1: w->left = (_screen.width - w->width) / 2; break; + case 2: w->left = _screen.width - w->width; break; + default: w->left = 0; break; + } + if (w->viewport != NULL) w->viewport->left += w->left - old_left; + SetDirtyBlocks(0, w->top, _screen.width, w->top + w->height); // invalidate the whole row + return w->left; +} + +/** + * (Re)position main toolbar window at the screen. + * @param w Window structure of the main toolbar window, may also be \c NULL. + * @return X coordinate of left edge of the repositioned toolbar window. + */ +int PositionMainToolbar(Window *w) +{ + DEBUG(misc, 5, "Repositioning Main Toolbar..."); + return PositionWindow(w, WC_MAIN_TOOLBAR, _settings_client.gui.toolbar_pos); +} + +/** + * (Re)position statusbar window at the screen. + * @param w Window structure of the statusbar window, may also be \c NULL. + * @return X coordinate of left edge of the repositioned statusbar. + */ +int PositionStatusbar(Window *w) +{ + DEBUG(misc, 5, "Repositioning statusbar..."); + return PositionWindow(w, WC_STATUS_BAR, _settings_client.gui.statusbar_pos); +} + +/** + * (Re)position news message window at the screen. + * @param w Window structure of the news message window, may also be \c NULL. + * @return X coordinate of left edge of the repositioned news message. + */ +int PositionNewsMessage(Window *w) +{ + DEBUG(misc, 5, "Repositioning news message..."); + return PositionWindow(w, WC_NEWS_WINDOW, _settings_client.gui.statusbar_pos); +} + +/** + * (Re)position network chat window at the screen. + * @param w Window structure of the network chat window, may also be \c NULL. + * @return X coordinate of left edge of the repositioned network chat window. + */ +int PositionNetworkChatWindow(Window *w) +{ + DEBUG(misc, 5, "Repositioning network chat window..."); + return PositionWindow(w, WC_SEND_NETWORK_MSG, _settings_client.gui.statusbar_pos); +} + + +/** + * Switches viewports following vehicles, which get autoreplaced + * @param from_index the old vehicle ID + * @param to_index the new vehicle ID + */ +void ChangeVehicleViewports(VehicleID from_index, VehicleID to_index) +{ + Window *w; + FOR_ALL_WINDOWS_FROM_BACK(w) { + if (w->viewport != NULL && w->viewport->follow_vehicle == from_index) { + w->viewport->follow_vehicle = to_index; + w->SetDirty(); + } + } +} + + +/** + * Relocate all windows to fit the new size of the game application screen + * @param neww New width of the game application screen + * @param newh New height of the game application screen. + */ +void RelocateAllWindows(int neww, int newh) +{ + Window *w; + + FOR_ALL_WINDOWS_FROM_BACK(w) { + int left, top; + /* XXX - this probably needs something more sane. For example specifying + * in a 'backup'-desc that the window should always be centered. */ + switch (w->window_class) { + case WC_MAIN_WINDOW: + case WC_BOOTSTRAP: + ResizeWindow(w, neww, newh); + continue; + + case WC_MAIN_TOOLBAR: + ResizeWindow(w, min(neww, w->window_desc->default_width) - w->width, 0, false); + + top = w->top; + left = PositionMainToolbar(w); // changes toolbar orientation + break; + + case WC_NEWS_WINDOW: + top = newh - w->height; + left = PositionNewsMessage(w); + break; + + case WC_STATUS_BAR: + ResizeWindow(w, min(neww, w->window_desc->default_width) - w->width, 0, false); + + top = newh - w->height; + left = PositionStatusbar(w); + break; + + case WC_SEND_NETWORK_MSG: + ResizeWindow(w, Clamp(neww, 320, 640) - w->width, 0, false); + top = newh - w->height - FindWindowById(WC_STATUS_BAR, 0)->height; + left = PositionNetworkChatWindow(w); + break; + + case WC_CONSOLE: + IConsoleResize(w); + continue; + + default: { + if (w->flags & WF_CENTERED) { + top = (newh - w->height) >> 1; + left = (neww - w->width) >> 1; + break; + } + + left = w->left; + if (left + (w->width >> 1) >= neww) left = neww - w->width; + if (left < 0) left = 0; + + top = w->top; + if (top + (w->height >> 1) >= newh) top = newh - w->height; + break; + } + } + + EnsureVisibleCaption(w, left, top); + } +} + +/** + * Destructor of the base class PickerWindowBase + * Main utility is to stop the base Window destructor from triggering + * a free while the child will already be free, in this case by the ResetObjectToPlace(). + */ +PickerWindowBase::~PickerWindowBase() +{ + this->window_class = WC_INVALID; // stop the ancestor from freeing the already (to be) child + ResetObjectToPlace(); +} diff --git a/src/zoning.h b/src/zoning.h new file mode 100644 index 0000000000..3314b84877 --- /dev/null +++ b/src/zoning.h @@ -0,0 +1,34 @@ +/** @file zoning.h */ + +#ifndef ZONING_H_ +#define ZONING_H_ + +#include "openttd.h" +#include "tile_cmd.h" + +enum EvaluationMode { + CHECKNOTHING = 0, + CHECKOPINION = 1, ///< Check the local authority's opinion. + CHECKBUILD = 2, ///< Check wither or not the player can build. + CHECKSTACATCH = 3, ///< Check catchment area for stations + CHECKBULUNSER = 4, ///< Check for unserved buildings + CHECKINDUNSER = 5, ///< Check for unserved industries + CHECKTOWNZONES = 6, ///< Town zones (Tz*) + CHECKCBBORDERS = 7, ///< Citybuilder cargo acceptment zone + CHECKCBTOWNBORDERS = 8, ///< Citybuilder server town borders + CHECKTOWNADZONES = 9, ///< Town advertisement zone + CHECKTOWNGROWTHTILES = 10 ///< Town growth tiles (new house, skipped/removed house) +}; + +struct Zoning { + EvaluationMode inner; + EvaluationMode outer; +}; + +extern Zoning _zoning; + +SpriteID TileZoningSpriteEvaluation(TileIndex tile, Owner owner, EvaluationMode ev_mode); +void DrawTileZoning(const TileInfo *ti); +void ShowZoningToolbar(); + +#endif /*ZONING_H_*/ diff --git a/src/zoning_cmd.cpp b/src/zoning_cmd.cpp new file mode 100644 index 0000000000..f0e21ca2cb --- /dev/null +++ b/src/zoning_cmd.cpp @@ -0,0 +1,378 @@ +/** @file zoning_cmd.cpp */ + +#include "stdafx.h" +#include "openttd.h" +#include "station_type.h" +#include "station_base.h" +#include "industry.h" +#include "gfx_func.h" +#include "viewport_func.h" +#include "map_func.h" +#include "company_func.h" +#include "town_map.h" +#include "table/sprites.h" +#include "station_func.h" +#include "station_map.h" +#include "town.h" +#include "zoning.h" +#include + +Zoning _zoning; +static const SpriteID INVALID_SPRITE_ID = UINT_MAX; + +/** + * Draw the zoning sprites. + * + * @param SpriteID image + * the image + * @param SpriteID colour + * the colour of the zoning + * @param TileInfo ti + * the tile + */ +const byte _tileh_to_sprite[32] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0, + 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 17, 0, 15, 18, 0, +}; +void DrawZoningSprites(SpriteID image, SpriteID colour, const TileInfo *ti) { + if ( colour!=INVALID_SPRITE_ID ) + AddSortableSpriteToDraw(image + _tileh_to_sprite[ti->tileh], colour, ti->x, ti->y, 0x10, 0x10, 1, ti->z + 7); +} + +/** + * Detect whether this area is within the acceptance of any station. + * + * @param TileArea area + * the area to search by + * @param Owner owner + * the owner of the stations which we need to match again + * @return true if a station is found + */ +bool IsAreaWithinAcceptanceZoneOfStation(TileArea area, Owner owner) { + + // TODO: Actually do owner check. + + int catchment = _settings_game.station.station_spread + (_settings_game.station.modified_catchment ? MAX_CATCHMENT : CA_UNMODIFIED); + + StationFinder morestations(TileArea(TileXY(TileX(area.tile)-catchment/2, TileY(area.tile)-catchment/2), + TileX(area.tile) + area.w + catchment, TileY(area.tile) + area.h + catchment)); + + for ( Station * const *st_iter = morestations.GetStations()->Begin(); st_iter != morestations.GetStations()->End(); ++st_iter ) { + Station *st = *st_iter; + Rect rect = st->GetCatchmentRect(); + return TileArea(TileXY(rect.left, rect.top), TileXY(rect.right, rect.bottom)).Intersects(area); + } + + return false; +} + +/** + * Detect whether this tile is within the acceptance of any station. + * + * @param TileIndex tile + * the tile to search by + * @param Owner owner + * the owner of the stations + * @return true if a station is found + */ +bool IsTileWithinAcceptanceZoneOfStation(TileIndex tile, Owner owner) { + + // TODO: Actually do owner check. + + int catchment = _settings_game.station.station_spread + (_settings_game.station.modified_catchment ? MAX_CATCHMENT : CA_UNMODIFIED); + + StationFinder morestations(TileArea(TileXY(TileX(tile)-catchment/2, TileY(tile)-catchment/2), + catchment, catchment)); + + for ( Station * const *st_iter = morestations.GetStations()->Begin(); st_iter != morestations.GetStations()->End(); ++st_iter ) { + Station *st = *st_iter; + Rect rect = st->GetCatchmentRect(); + if ( (uint)rect.left <= TileX(tile) && TileX(tile) <= (uint)rect.right + && (uint)rect.top <= TileY(tile) && TileY(tile) <= (uint)rect.bottom ) + return true; + } + + return false; +} + +/** + * Check whether the player can build in tile. + * + * @param TileIndex tile + * @param Owner owner + * @return red if they cannot + */ +SpriteID TileZoneCheckBuildEvaluation(TileIndex tile, Owner owner) { + /* Let's first check for the obvious things you cannot build on */ + switch ( GetTileType(tile) ) { + case MP_INDUSTRY: + case MP_OBJECT: + case MP_STATION: + case MP_HOUSE: + case MP_TUNNELBRIDGE: return SPR_PALETTE_ZONING_RED; + /* There are only two things you can own (or some else + * can own) that you can still build on. i.e. roads and + * railways. + * @todo + * Add something more intelligent, check what tool the + * user is currently using (and if none, assume some + * standards), then check it against if owned by some- + * one else (e.g. railway on someone else's road). + * While that being said, it should also check if it + * is not possible to build railway/road on someone + * else's/your own road/railway (e.g. the railway track + * is curved or a cross).*/ + case MP_ROAD: + case MP_RAILWAY: { + if ( GetTileOwner(tile) != owner ) return SPR_PALETTE_ZONING_RED; + else return INVALID_SPRITE_ID; + } + default: return INVALID_SPRITE_ID; + } +} + +/** + * Check the opinion of the local authority in the tile. + * + * @param TileIndex tile + * @param Owner owner + * @return black if no opinion, orange if bad, + * light blue if good or invalid if no town + */ +SpriteID TileZoneCheckOpinionEvaluation(TileIndex tile, Owner owner) { + Town *town = ClosestTownFromTile(tile, _settings_game.economy.dist_local_authority); + + if ( town !=NULL ) { + if ( HasBit(town->have_ratings, owner) ) { // good : bad + return ( town->ratings[owner] > 0 ) ? SPR_PALETTE_ZONING_LIGHT_BLUE : SPR_PALETTE_ZONING_ORANGE; + } else { + return SPR_PALETTE_ZONING_BLACK; // no opinion + } + } + return INVALID_SPRITE_ID; // no town +} + +/* + * Check which town zone tile belongs to. + * @param TileIndex tile + */ +SpriteID TileZoneCheckTownZones(TileIndex tile) { + HouseZonesBits next_zone = HZB_BEGIN; + Town *town; + + FOR_ALL_TOWNS(town) { + while (next_zone < HZB_END && DistanceSquare(tile, town->xy) <= town->cache.squared_town_zone_radius[next_zone]){ + next_zone++; + if (next_zone == HZB_TOWN_OUTER_SUBURB) next_zone = HZB_TOWN_INNER_SUBURB; + } + if (next_zone >= HZB_END) break; + } + + switch (next_zone) { + case HZB_TOWN_EDGE: return INVALID_SPRITE_ID; // no town + case HZB_TOWN_OUTSKIRT: return SPR_PALETTE_ZONING_LIGHT_BLUE; // Tz0 + case HZB_TOWN_INNER_SUBURB: return SPR_PALETTE_ZONING_BLACK; // Tz1 Tz2 + case HZB_TOWN_CENTRE: return SPR_PALETTE_ZONING_GREEN; // Tz3 + default: return SPR_PALETTE_ZONING_BLACK; // Tz4 - center + } + return INVALID_SPRITE_ID; //nothing +} + +/* + * Check whether the tile is within citybuilder server town border (where houses could be built). + * @param TileIndex tile + */ +SpriteID TileZoneCheckCBTownBorders(TileIndex tile) { + Town *town; + FOR_ALL_TOWNS(town) { + double x = town->cache.squared_town_zone_radius[0]; + if (DistanceMax(town->xy, tile) < (sqrt(x))) + return SPR_PALETTE_ZONING_BLACK; + } + return INVALID_SPRITE_ID; +} + +/* + * Check which advertisement zone(small, medium, large) tile belongs to + * @param TileIndex tile + */ +SpriteID TileZoneCheckTownAdvertisementZones(TileIndex tile) { + Town *town = CalcClosestTownFromTile(tile, max((uint)_settings_game.economy.dist_local_authority, 20U)); + if (!town) + return INVALID_SPRITE_ID; //nothing + + uint dist = DistanceManhattan(town->xy, tile); + + if (dist <= 10) return SPR_PALETTE_ZONING_LIGHT_BLUE; + if (dist <= 15) return SPR_PALETTE_ZONING_BLACK; + if (dist <= 20) return SPR_PALETTE_ZONING_LIGHT_BLUE; + return INVALID_SPRITE_ID; +} + + +/* + * Checks for tile in growth tiles info + * @param TileIndex tile + */ +SpriteID TileZoneCheckTownsGrowthTiles(TileIndex tile) { + switch (max(_towns_growth_tiles[tile], _towns_growth_tiles_last_month[tile])) { + case TGTS_CB_HOUSE_REMOVED_NOGROW: return SPR_PALETTE_ZONING_LIGHT_BLUE; + case TGTS_RH_REMOVED: return SPR_PALETTE_ZONING_LIGHT_BLUE; + case TGTS_RH_REBUILT: return SPR_PALETTE_ZONING_WHITE; + case TGTS_NEW_HOUSE: return SPR_PALETTE_ZONING_GREEN; + case TGTS_CYCLE_SKIPPED: return SPR_PALETTE_ZONING_ORANGE; + case TGTS_HOUSE_SKIPPED: return SPR_PALETTE_ZONING_RED; + case TGTS_CB_HOUSE_REMOVED: return SPR_PALETTE_ZONING_RED; + default: return INVALID_SPRITE_ID; + } +} + + +SpriteID TileZoneCheckCBBorders(TileIndex tile) { + Town *town = CalcClosestTownFromTile(tile); + + if ( town != NULL ) { + if (DistanceManhattan(town->xy, tile) <= _settings_client.gui.cb_distance_check) { + return SPR_PALETTE_ZONING_LIGHT_BLUE; //cb catchment + } + } + + return INVALID_SPRITE_ID; // no town +} + +/** + * Detect whether the tile is within the catchment zone of a station. + * + * @param TileIndex tile + * @param Owner owner + * @return black if within, light blue if only in acceptance zone + * and nothing if no nearby station. + */ +SpriteID TileZoneCheckStationCatchmentEvaluation(TileIndex tile, Owner owner) { + //TODO: Actually check owner. + + // Never on a station. + if ( IsTileType(tile, MP_STATION) ) + return INVALID_SPRITE_ID; + + // For provided goods + StationFinder stations(TileArea(tile, 1, 1)); + + if ( stations.GetStations()->Length() > 0 ) { + return SPR_PALETTE_ZONING_BLACK; + } + + // For accepted goods + if ( IsTileWithinAcceptanceZoneOfStation(tile, owner) ){ + return SPR_PALETTE_ZONING_LIGHT_BLUE; + } + + return INVALID_SPRITE_ID; +} + +/** + * Detect whether a building is unserved by a station of owner. + * + * @param TileIndex tile + * @param Owner owner + * @return red if unserved, orange if only accepting, nothing if served or not + * a building + */ +SpriteID TileZoneCheckUnservedBuildingsEvaluation(TileIndex tile, Owner owner) { + //TODO: Actually use owner. + CargoArray dat; + + if ( IsTileType (tile, MP_HOUSE) + && ( ( memset(&dat, 0, sizeof(dat)), AddAcceptedCargo(tile, dat, NULL), (dat[CT_MAIL] + dat[CT_PASSENGERS] > 0) ) + || ( memset(&dat, 0, sizeof(dat)), AddProducedCargo(tile, dat), (dat[CT_MAIL] + dat[CT_PASSENGERS] > 0) ) ) ) { + StationFinder stations(TileArea(tile, 1, 1)); + + if ( stations.GetStations()->Length() > 0 ) { + return INVALID_SPRITE_ID; + } + + // For accepted goods + if ( IsTileWithinAcceptanceZoneOfStation(tile, owner) ) + return SPR_PALETTE_ZONING_ORANGE; + + //TODO: Check for stations that does not accept mail/passengers, + //which is currently only truck stops. + return SPR_PALETTE_ZONING_RED; + } + + return INVALID_SPRITE_ID; +} + +/** + * Detect whether an industry is unserved by a station of owner. + * + * @param TileIndex tile + * @param Owner owner + * @return red if unserved, orange if only accepting, nothing if served or not + * a building + */ +SpriteID TileZoneCheckUnservedIndustriesEvaluation(TileIndex tile, Owner owner) { + //TODO: Actually use owner + if ( IsTileType(tile, MP_INDUSTRY) ) { + Industry *ind = Industry::GetByTile(tile); + StationFinder stations(ind->location); + + if ( stations.GetStations()->Length() > 0 ) { + return INVALID_SPRITE_ID; + } + + // For accepted goods + if ( IsAreaWithinAcceptanceZoneOfStation(ind->location, owner) ) + return SPR_PALETTE_ZONING_ORANGE; + + //TODO: Check for stations that only accepts mail/passengers, + //which is currently only bus stops. + return SPR_PALETTE_ZONING_RED; + } + + return INVALID_SPRITE_ID; +} + +/** + * General evaluation function; calls all the other functions depending on + * evaluation mode. + * + * @param TileIndex tile + * Tile to be evaluated. + * @param Owner owner + * The current player + * @param EvaluationMode ev_mode + * The current evaluation mode. + * @return The colour returned by the evaluation functions (none if no ev_mode). + */ +SpriteID TileZoningSpriteEvaluation(TileIndex tile, Owner owner, EvaluationMode ev_mode) { + switch ( ev_mode ) { + case CHECKBUILD: return TileZoneCheckBuildEvaluation(tile, owner); + case CHECKOPINION: return TileZoneCheckOpinionEvaluation(tile, owner); + case CHECKSTACATCH: return TileZoneCheckStationCatchmentEvaluation(tile, owner); + case CHECKBULUNSER: return TileZoneCheckUnservedBuildingsEvaluation(tile, owner); + case CHECKINDUNSER: return TileZoneCheckUnservedIndustriesEvaluation(tile, owner); + case CHECKTOWNZONES: return TileZoneCheckTownZones(tile); + case CHECKCBBORDERS: return TileZoneCheckCBBorders(tile); + case CHECKCBTOWNBORDERS: return TileZoneCheckCBTownBorders(tile); + case CHECKTOWNADZONES: return TileZoneCheckTownAdvertisementZones(tile); + case CHECKTOWNGROWTHTILES: return TileZoneCheckTownsGrowthTiles(tile); + default: return INVALID_SPRITE_ID; + } +} + +/** + * Draw the the zoning on the tile. + * + * @param TileInfo ti + * the tile to draw on. + */ +void DrawTileZoning(const TileInfo *ti) { + if ( IsTileType(ti->tile, MP_VOID) || _game_mode != GM_NORMAL ) return; + + if ( _zoning.outer != CHECKNOTHING ) + DrawZoningSprites(SPR_SELECT_TILE, TileZoningSpriteEvaluation(ti->tile, _local_company, _zoning.outer), ti); + + if ( _zoning.inner != CHECKNOTHING ) + DrawZoningSprites(SPR_INNER_HIGHLIGHT_BASE, TileZoningSpriteEvaluation(ti->tile, _local_company, _zoning.inner), ti); +} diff --git a/src/zoning_gui.cpp b/src/zoning_gui.cpp new file mode 100644 index 0000000000..35a8b0b156 --- /dev/null +++ b/src/zoning_gui.cpp @@ -0,0 +1,138 @@ + +/** @file zoning_gui.cpp */ + +#include "stdafx.h" +#include "openttd.h" +#include "widgets/dropdown_func.h" +#include "widget_type.h" +//#include "functions.h" +#include "window_func.h" +#include "gui.h" +#include "viewport_func.h" +#include "sound_func.h" +//#include "variables.h" +#include "table/sprites.h" +#include "table/strings.h" +#include "strings_func.h" +#include "gfx_func.h" +#include "core/geometry_func.hpp" +#include "core/random_func.hpp" +#include "zoning.h" + +enum ZoningToolbarWidgets { + ZTW_OUTER = 4, + ZTW_OUTER_DROPDOWN, + ZTW_INNER, + ZTW_INNER_DROPDOWN, + ZTW_CAPTION +}; + +const StringID _zone_types[] = { + STR_ZONING_NO_ZONING, + STR_ZONING_AUTHORITY, + STR_ZONING_CAN_BUILD, + STR_ZONING_STA_CATCH, + STR_ZONING_BUL_UNSER, + STR_ZONING_IND_UNSER, + STR_ZONING_TOWN_ZONES, + STR_ZONING_CB_BORDERS, + STR_ZONING_CB_TOWN_BORDERS, + STR_ZONING_ADVERTISEMENT_ZONES, + STR_ZONING_TOWN_GROWTH_TILES, + INVALID_STRING_ID +}; + +struct ZoningWindow : public Window { + + ZoningWindow(WindowDesc *desc, int window_number) : Window(desc) { + this->InitNested(window_number); + this->InvalidateData(); + } + + virtual void OnPaint() { + this->DrawWidgets(); + } + + virtual void OnClick(Point pt, int widget, int click_count) { + switch ( widget ) { + case ZTW_OUTER_DROPDOWN: + ShowDropDownMenu(this, _zone_types, _zoning.outer, ZTW_OUTER_DROPDOWN, 0, 0); + break; + case ZTW_INNER_DROPDOWN: + ShowDropDownMenu(this, _zone_types, _zoning.inner, ZTW_INNER_DROPDOWN, 0, 0); + break; + } + } + + virtual void OnDropdownSelect(int widget, int index) { + switch(widget) { + case ZTW_OUTER_DROPDOWN: + _zoning.outer = (EvaluationMode)index; + break; + case ZTW_INNER_DROPDOWN: + _zoning.inner = (EvaluationMode)index; + break; + } + this->InvalidateData(); + MarkWholeScreenDirty(); + } + + virtual void SetStringParameters(int widget) const { + switch ( widget ) { + case ZTW_OUTER_DROPDOWN: SetDParam(0, _zone_types[_zoning.outer]); break; + case ZTW_INNER_DROPDOWN: SetDParam(0, _zone_types[_zoning.inner]); break; + } + } + + virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) { + const StringID *strs = NULL; + switch ( widget ) { + case ZTW_OUTER_DROPDOWN: strs = _zone_types; break; + case ZTW_INNER_DROPDOWN: strs = _zone_types; break; + } + if ( strs != NULL ) { + while ( *strs != INVALID_STRING_ID ) { + *size = maxdim(*size, GetStringBoundingBox(*strs++)); + } + } + size->width += padding.width; + size->height = FONT_HEIGHT_NORMAL + WD_DROPDOWNTEXT_TOP + WD_DROPDOWNTEXT_BOTTOM; + } +}; + +static const NWidgetPart _nested_zoning_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_GREY), + NWidget(WWT_CAPTION, COLOUR_GREY, ZTW_CAPTION), SetDataTip(STR_ZONING_TOOLBAR, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_SHADEBOX, COLOUR_GREY), + NWidget(WWT_STICKYBOX, COLOUR_GREY), + EndContainer(), + + NWidget(WWT_PANEL, COLOUR_GREY), + NWidget(NWID_HORIZONTAL, COLOUR_GREY), SetPIP(10, 3, 10), + NWidget(NWID_VERTICAL, COLOUR_GREY), SetPadding(5, 0, 5, 0), + NWidget(WWT_TEXT, COLOUR_GREY), SetDataTip(STR_ZONING_OUTER, STR_NULL), SetResize(1, 0), SetPadding(1, 6, 1, 6), + NWidget(WWT_TEXT, COLOUR_GREY, ZTW_OUTER), + NWidget(WWT_TEXT, COLOUR_GREY), SetDataTip(STR_ZONING_INNER, STR_NULL), SetResize(1, 0), SetPadding(1, 6, 1, 6), + NWidget(WWT_TEXT, COLOUR_GREY, ZTW_INNER), + EndContainer(), + NWidget(NWID_VERTICAL, COLOUR_GREY), SetPadding(5, 0, 5, 0), + NWidget(WWT_DROPDOWN, COLOUR_GREY, ZTW_OUTER_DROPDOWN), SetDataTip(STR_JUST_STRING, STR_NULL), SetFill(1, 0), + NWidget(WWT_TEXT, COLOUR_GREY), + NWidget(WWT_DROPDOWN, COLOUR_GREY, ZTW_INNER_DROPDOWN), SetDataTip(STR_JUST_STRING, STR_NULL), SetFill(1, 0), + NWidget(WWT_TEXT, COLOUR_GREY), + EndContainer(), + EndContainer(), + EndContainer() +}; + +static WindowDesc _zoning_desc ( + WDP_CENTER, NULL, 0, 0, + WC_ZONING_TOOLBAR, WC_NONE, + 0, + _nested_zoning_widgets, lengthof(_nested_zoning_widgets) +); + +void ShowZoningToolbar() { + AllocateWindowDescFront(&_zoning_desc, 0); +}